Skip to content

Commit 6e36ab6

Browse files
committed
added documentation for nft indexer
1 parent c0ca732 commit 6e36ab6

File tree

1 file changed

+397
-0
lines changed
  • docs/quick-start/advance-tutorials/nft-indexer

1 file changed

+397
-0
lines changed
Lines changed: 397 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,397 @@
1+
---
2+
sidebar_position: 5
3+
---
4+
# NFT AeIndexer
5+
6+
**Description**: This application demonstrates how to maintain account balances and transfer records by indexing aelf's NFT issued data.
7+
8+
**Purpose**: Shows you how to create, develop, and deploy your own AeIndexer on AeFinder.
9+
10+
**Difficulty Level**: Easy
11+
12+
<div class="video-container">
13+
<iframe src="https://www.youtube.com/embed/9amvWGMUBs0" title="AeFinder Demo: Use AeFinder to Index, Retrieve, and Manage data on aelf AI Blockchain" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
14+
</div>
15+
16+
## Step 1 - Setting up your development environment
17+
- Install [dotnet 8.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
18+
- Install IDE: Install your favorite IDE, such as: [VS Code](https://code.visualstudio.com/), [Visual Studio](https://visualstudio.microsoft.com/), [Rider](https://www.jetbrains.com/rider/), etc.
19+
20+
## Step 2 - Create AeIndexer in AeFinder
21+
- Log in to the AeFinder website.
22+
TestNet: [https://test.aefinder.io/login](https://test.aefinder.io/login)
23+
24+
- Enter the AeIndexer Name and other information to create a NFT AeIndexer.
25+
<!-- Commenting out missing image references -->
26+
<!-- ![dashboard](./-nftindexer/img/dashboard.png) -->
27+
<!-- ![create-app](./nft-aeindexer/img/create-app.png) -->
28+
29+
## Step 3 - Develop NFT AeIndexer
30+
31+
### Project Structure
32+
The NFT AeIndexer project consists of the following key components:
33+
34+
```
35+
nftIndexer/
36+
├── Contracts/ # Contract interfaces
37+
├── Entities/ # Data models
38+
│ ├── Account.cs # NFT account information
39+
│ └── TransferRecord.cs # NFT transfer records
40+
├── Processors/ # Event processors
41+
│ └── NFTTransferredProcessor.cs # Handles NFT transfer events
42+
└── GraphQL/ # GraphQL query definitions
43+
```
44+
45+
### Core Components
46+
47+
#### 1. Entity Models
48+
49+
**Account.cs**
50+
```csharp title="Account.cs"
51+
public class Account: AeFinderEntity, IAeFinderEntity
52+
{
53+
[Keyword] public string Address { get; set; }
54+
[Keyword] public string Symbol { get; set; }
55+
public long Amount { get; set; }
56+
public string TokenName { get; set; }
57+
public ExternalInfo ExternalInfo { get; set; }
58+
}
59+
```
60+
61+
**TransferRecord.cs**
62+
```csharp title="TransferRecord.cs"
63+
public class TransferRecord: AeFinderEntity, IAeFinderEntity
64+
{
65+
[Keyword] public string Symbol { get; set; }
66+
[Keyword] public string ToAddress { get; set; }
67+
public long Amount { get; set; }
68+
[Text] public string Memo { get; set; }
69+
}
70+
```
71+
72+
#### 2. NFT Transfer Processor
73+
74+
The NFTTransferredProcessor handles NFT transfer events and updates account balances:
75+
76+
```csharp title="NFTTransferredProcessor.cs"
77+
using AElf.Contracts.MultiToken;
78+
using AeFinder.Sdk.Logging;
79+
using AeFinder.Sdk.Processor;
80+
using AeFinder.Sdk;
81+
using nftIndexer.Entities;
82+
using Volo.Abp.DependencyInjection;
83+
84+
namespace nftIndexer.Processors;
85+
86+
public class NFTTransferredProcessor : LogEventProcessorBase<Issued>, ITransientDependency
87+
{
88+
private readonly IBlockChainService _blockChainService;
89+
90+
public NFTTransferredProcessor(IBlockChainService blockChainService)
91+
{
92+
_blockChainService = blockChainService;
93+
}
94+
95+
public override string GetContractAddress(string chainId)
96+
{
97+
return chainId switch
98+
{
99+
"AELF" => "JRmBduh4nXWi1aXgdUsj5gJrzeZb2LxmrAbf7W99faZSvoAaE",
100+
"tDVW" => "ASh2Wt7nSEmYqnGxPPzp4pnVDU4uhj1XW9Se5VeZcX2UDdyjx",
101+
_ => string.Empty
102+
};
103+
}
104+
105+
public override async Task ProcessAsync(Issued logEvent, LogEventContext context)
106+
{
107+
if (!IsNftTransfer(logEvent))
108+
{
109+
return;
110+
}
111+
112+
var tokenInfoParam = new GetTokenInfoInput
113+
{
114+
Symbol = logEvent.Symbol
115+
};
116+
Logger.LogDebug("Fetching TokenInfo: ChainId={0}, Address={1}, Symbol={2}", context.ChainId, GetContractAddress(context.ChainId), logEvent.Symbol);
117+
var contractAddress = GetContractAddress(context.ChainId);
118+
Logger.LogDebug("Contract Address resolved to: {0}", contractAddress);
119+
var tokenInfo = await _blockChainService.ViewContractAsync<TokenInfo>(
120+
context.ChainId, contractAddress,
121+
"GetTokenInfo", tokenInfoParam);
122+
123+
Logger.LogDebug("TokenInfo response: {@TokenInfo}", tokenInfo);
124+
125+
var nftTransfer = new TransferRecord
126+
{
127+
Id = $"{context.ChainId}-{context.Transaction.TransactionId}-{context.LogEvent.Index}",
128+
ToAddress = logEvent.To.ToBase58(),
129+
Symbol = logEvent.Symbol,
130+
Amount = logEvent.Amount,
131+
Memo = logEvent.Memo
132+
};
133+
await SaveEntityAsync(nftTransfer);
134+
135+
await ChangeNFTBalanceAsync(context.ChainId, logEvent.To.ToBase58(), logEvent.Symbol, logEvent.Amount);
136+
}
137+
138+
private async Task ChangeNFTBalanceAsync(string chainId, string address, string symbol, long amount)
139+
{
140+
var accountId = $"{chainId}-{address}-{symbol}";
141+
var account = await GetEntityAsync<Account>(accountId);
142+
var tokenInfoParam = new GetTokenInfoInput { Symbol = symbol };
143+
var contractAddress = GetContractAddress(chainId);
144+
var tokenInfo = await _blockChainService.ViewContractAsync<TokenInfo>(
145+
chainId, contractAddress,
146+
"GetTokenInfo", tokenInfoParam);
147+
148+
if (account == null)
149+
{
150+
account = new Account
151+
{
152+
Id = accountId,
153+
Symbol = symbol,
154+
Amount = amount,
155+
Address = address,
156+
TokenName = tokenInfo.TokenName,
157+
ExternalInfo = tokenInfo.ExternalInfo
158+
};
159+
}
160+
else
161+
{
162+
account.Amount += amount;
163+
}
164+
165+
Logger.LogDebug("NFT Balance changed: {0} {1} {2} {3}", account.Address, account.Symbol, account.Amount, account.TokenName);
166+
167+
await SaveEntityAsync(account);
168+
}
169+
170+
private bool IsNftTransfer(Issued logEvent)
171+
{
172+
return !string.IsNullOrEmpty(logEvent.Symbol) && logEvent.Symbol.Contains("-") &&
173+
logEvent.Amount > 0 &&
174+
logEvent.To != null && !string.IsNullOrEmpty(logEvent.To.ToBase58());
175+
}
176+
}
177+
```
178+
179+
- Add files AccountDto.cs, TransferRecordDto.cs, GetAccountInput.cs, GetTransferRecordInput.cs to the directory src/nftIndexer/GraphQL.
180+
181+
```csharp title="AccountDto.cs"
182+
using AeFinder.Sdk.Dtos;
183+
184+
namespace nftIndexer.GraphQL;
185+
186+
public class AccountDto : AeFinderEntityDto
187+
{
188+
public string Address { get; set; }
189+
public string Symbol { get; set; }
190+
public long Amount { get; set; }
191+
}
192+
```
193+
194+
```csharp title="TransferRecordDto.cs"
195+
using AeFinder.Sdk.Dtos;
196+
197+
namespace nftIndexer.GraphQL;
198+
199+
public class TransferRecordDto : AeFinderEntityDto
200+
{
201+
public string Symbol { get; set; }
202+
public string FromAddress { get; set; }
203+
public string ToAddress { get; set; }
204+
public long Amount { get; set; }
205+
}
206+
```
207+
208+
```csharp title="GetAccountInput.cs"
209+
namespace nftIndexer.GraphQL;
210+
211+
public class GetAccountInput
212+
{
213+
public string ChainId { get; set; }
214+
public string Address { get; set; }
215+
public string Symbol { get; set; }
216+
}
217+
```
218+
219+
```csharp title="GetTransferRecordInput.cs"
220+
namespace nftIndexer.GraphQL;
221+
222+
public class GetTransferRecordInput
223+
{
224+
public string ChainId { get; set; }
225+
public string Address { get; set; }
226+
public string Symbol { get; set; }
227+
}
228+
```
229+
230+
- Modify src/nftIndexer/GraphQL/Query.cs to add query logic.
231+
232+
```csharp title="Query.cs"
233+
using AeFinder.Sdk;
234+
using GraphQL;
235+
using nftIndexer.Entities;
236+
using Volo.Abp.ObjectMapping;
237+
238+
namespace nftIndexer.GraphQL;
239+
240+
public class Query
241+
{
242+
public static async Task<List<AccountDto>> Account(
243+
[FromServices] IReadOnlyRepository<Account> repository,
244+
[FromServices] IObjectMapper objectMapper,
245+
GetAccountInput input)
246+
{
247+
var queryable = await repository.GetQueryableAsync();
248+
249+
queryable = queryable.Where(a => a.Metadata.ChainId == input.ChainId);
250+
251+
if (!input.Address.IsNullOrWhiteSpace())
252+
{
253+
queryable = queryable.Where(a => a.Address == input.Address);
254+
}
255+
256+
if(!input.Symbol.IsNullOrWhiteSpace())
257+
{
258+
queryable = queryable.Where(a => a.Symbol == input.Symbol);
259+
}
260+
261+
var accounts= queryable.OrderBy(o=>o.Metadata.Block.BlockHeight).ToList();
262+
263+
return objectMapper.Map<List<Account>, List<AccountDto>>(accounts);
264+
}
265+
266+
public static async Task<List<TransferRecordDto>> TransferRecord(
267+
[FromServices] IReadOnlyRepository<TransferRecord> repository,
268+
[FromServices] IObjectMapper objectMapper,
269+
GetTransferRecordInput input)
270+
{
271+
var queryable = await repository.GetQueryableAsync();
272+
273+
queryable = queryable.Where(a => a.Metadata.ChainId == input.ChainId);
274+
275+
if (!input.Address.IsNullOrWhiteSpace())
276+
{
277+
queryable = queryable.Where(a => a.ToAddress == input.Address);
278+
}
279+
280+
if(!input.Symbol.IsNullOrWhiteSpace())
281+
{
282+
queryable = queryable.Where(a => a.Symbol == input.Symbol);
283+
}
284+
285+
var accounts= queryable.OrderBy(o=>o.Metadata.Block.BlockHeight).ToList();
286+
287+
return objectMapper.Map<List<TransferRecord>, List<TransferRecordDto>>(accounts);
288+
}
289+
}
290+
```
291+
292+
- Register log event processor
293+
294+
Modify src/nftIndexer/nftIndexerModule.cs to register NFTTransferredProcessor.
295+
296+
```csharp title="nftIndexerModule.cs"
297+
using AeFinder.Sdk.Processor;
298+
using nftIndexer.GraphQL;
299+
using nftIndexer.Processors;
300+
using GraphQL.Types;
301+
using Microsoft.Extensions.DependencyInjection;
302+
using Volo.Abp.AutoMapper;
303+
using Volo.Abp.Modularity;
304+
305+
namespace nftIndexer;
306+
307+
public class nftIndexerModule: AbpModule
308+
{
309+
public override void ConfigureServices(ServiceConfigurationContext context)
310+
{
311+
Configure<AbpAutoMapperOptions>(options => { options.AddMaps<nftIndexerModule>(); });
312+
context.Services.AddSingleton<ISchema, AeIndexerSchema>();
313+
314+
// Add your LogEventProcessor implementation.
315+
context.Services.AddSingleton<ILogEventProcessor, NFTTransferredProcessor>();
316+
}
317+
}
318+
```
319+
320+
- Add entity mapping
321+
322+
Modify src/nftIndexer/nftIndexerAutoMapperProfile.cs and add entity mapping code.
323+
324+
```csharp title="nftIndexerAutoMapperProfile.cs"
325+
using nftIndexer.Entities;
326+
using nftIndexer.GraphQL;
327+
using AutoMapper;
328+
329+
namespace nftIndexer;
330+
331+
public class nftIndexerAutoMapperProfile : Profile
332+
{
333+
public nftIndexerAutoMapperProfile()
334+
{
335+
CreateMap<Account, AccountDto>();
336+
CreateMap<TransferRecord, TransferRecordDto>();
337+
}
338+
}
339+
```
340+
341+
### Building code
342+
Use the following command in the code directory to compile the code.
343+
```bash
344+
dotnet build -c Release
345+
```
346+
347+
## Step 4 - Deploy AeIndexer
348+
- Open the AeIndexer details page and click Deploy.
349+
<!-- ![deploy](./token-aeindexer/img/deploy.png) -->
350+
- Fill out the subscription manifest and upload the DLL file.
351+
1. Subscription manifest:
352+
```json
353+
{
354+
"subscriptionItems": [
355+
{
356+
"chainId": "tDVW",
357+
"startBlockNumber": 151018963,
358+
"onlyConfirmed": false,
359+
"transactions": [],
360+
"logEvents": [
361+
{
362+
"contractAddress": "ASh2Wt7nSEmYqnGxPPzp4pnVDU4uhj1XW9Se5VeZcX2UDdyjx",
363+
"eventNames": [
364+
"Issued",
365+
"Issue"
366+
]
367+
}
368+
]
369+
}
370+
]
371+
}
372+
```
373+
2. DLL file location: src/nftIndexer/bin/Release/net8.0/nftIndexer.dll
374+
<!-- ![deploy-2](./token-aeindexer/img/deploy-2.png) -->
375+
- Click the deploy button to submit deployment information. When the normal processing block information appears on the Logs page at the bottom of the details page, it means that the deployment has been successful and data indexing has started.
376+
<!-- ![log](./token-aeindexer/img/log.png) -->
377+
378+
## Step 5 - Query indexed data
379+
Through the Playground page below the details page, you can use GraphQL syntax to query the indexed data information. Enter the query statement on the left, and the query results will be displayed on the right.
380+
381+
```GraphQL
382+
query {
383+
account(input: {
384+
chainId: "tDVW",
385+
address: "DStUjYn3fH1pbCtz614gQhTurrt4w2WgUY7w8ymzXCggedDHb"
386+
}) {
387+
symbol,
388+
amount,
389+
address,
390+
metadata {
391+
chainId,
392+
block {
393+
blockHeight
394+
}
395+
}
396+
}
397+
}

0 commit comments

Comments
 (0)