|
| 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 | +<!--  --> |
| 27 | +<!--  --> |
| 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 | +<!--  --> |
| 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 | +<!--  --> |
| 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 | +<!--  --> |
| 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