Skip to main content

Token AeIndexer

Description: This application demonstrates how to maintain account balances and transfer records by indexing aelf's Token contract data.

Purpose: Shows you how to create, develop, and deploy your own AeIndexer on AeFinder.

Difficulty Level: Easy

Step 1 - Setting up your development environment

Step 2 - Create AeIndexer in AeFinder

Step 3 - Develop Token AeIndexer

Download development template

  • In the created AeIndexer details page, download the development template by entering the project name. create-app
  • After unzipping, you can start developing.

Develop AeIndexer

tip

ℹ️ Note: If other contract files are needed, they need to be generated by compiling the corresponding contract project!

TokenContract.c.cs
TokenContract.g.cs
  • Add entities

    Add Account.cs and TransferRecord.cs files to the directory src/TokenAeIndexer/Entities.

Account.cs
using AeFinder.Sdk.Entities;
using Nest;

namespace TokenAeIndexer.Entities;

public class Account: AeFinderEntity, IAeFinderEntity
{
[Keyword] public string Address { get; set; }
[Keyword] public string Symbol { get; set; }
public long Amount { get; set; }
}
TransferRecord.cs
using AeFinder.Sdk.Entities;
using Nest;

namespace TokenAeIndexer.Entities;

public class TransferRecord: AeFinderEntity, IAeFinderEntity
{
[Keyword] public string Symbol { get; set; }
[Keyword] public string FromAddress { get; set; }
[Keyword] public string ToAddress { get; set; }
public long Amount { get; set; }
}
  • Add log event processor

    Add the file TokenTransferredProcessor.cs to the directory src/TokenAeIndexer/Processors.

TokenTransferredProcessor.cs
using AElf.Contracts.MultiToken;
using AeFinder.Sdk.Logging;
using AeFinder.Sdk.Processor;
using TokenAeIndexer.Entities;
using Volo.Abp.DependencyInjection;

namespace TokenAeIndexer.Processors;

public class TokenTransferredProcessor : LogEventProcessorBase<Transferred>, ITransientDependency
{
public override string GetContractAddress(string chainId)
{
return chainId switch
{
"AELF" => "JRmBduh4nXWi1aXgdUsj5gJrzeZb2LxmrAbf7W99faZSvoAaE",
"tDVW" => "ASh2Wt7nSEmYqnGxPPzp4pnVDU4uhj1XW9Se5VeZcX2UDdyjx",
_ => string.Empty
};
}

public override async Task ProcessAsync(Transferred logEvent, LogEventContext context)
{
var transfer = new TransferRecord
{
Id = $"{context.ChainId}-{context.Transaction.TransactionId}-{context.LogEvent.Index}",
FromAddress = logEvent.From.ToBase58(),
ToAddress = logEvent.To.ToBase58(),
Symbol = logEvent.Symbol,
Amount = logEvent.Amount
};
await SaveEntityAsync(transfer);

await ChangeBalanceAsync(context.ChainId, logEvent.From.ToBase58(), logEvent.Symbol, -logEvent.Amount);
await ChangeBalanceAsync(context.ChainId, logEvent.To.ToBase58(), logEvent.Symbol, logEvent.Amount);
}

private async Task ChangeBalanceAsync(string chainId, string address, string symbol, long amount)
{
var accountId = $"{chainId}-{address}-{symbol}";
var account = await GetEntityAsync<Account>(accountId);
if (account == null)
{
account = new Account
{
Id = accountId,
Symbol = symbol,
Amount = amount,
Address = address
};
}
else
{
account.Amount += amount;
}

Logger.LogDebug("Balance changed: {0} {1} {2}", account.Address, account.Symbol, account.Amount);

await SaveEntityAsync(account);
}
}
  • Add files AccountDto.cs, TransferRecordDto.cs, GetAccountInput.cs, GetTransferRecordInput.cs to the directory src/TokenAeIndexer/GraphQL.
AccountDto.cs
using AeFinder.Sdk.Dtos;

namespace TokenAeIndexer.GraphQL;

public class AccountDto : AeFinderEntityDto
{
public string Address { get; set; }
public string Symbol { get; set; }
public long Amount { get; set; }
}
TransferRecordDto.cs
using AeFinder.Sdk.Dtos;

namespace TokenAeIndexer.GraphQL;

public class TransferRecordDto : AeFinderEntityDto
{
public string Symbol { get; set; }
public string FromAddress { get; set; }
public string ToAddress { get; set; }
public long Amount { get; set; }
}
GetAccountInput.cs
namespace TokenAeIndexer.GraphQL;

public class GetAccountInput
{
public string ChainId { get; set; }
public string Address { get; set; }
public string Symbol { get; set; }
}
GetTransferRecordInput.cs
namespace TokenAeIndexer.GraphQL;

public class GetTransferRecordInput
{
public string ChainId { get; set; }
public string Address { get; set; }
public string Symbol { get; set; }
}
  • Modify src/TokenAeIndexer/GraphQL/Query.cs to add query logic.
Query.cs
using AeFinder.Sdk;
using GraphQL;
using TokenAeIndexer.Entities;
using Volo.Abp.ObjectMapping;

namespace TokenAeIndexer.GraphQL;

public class Query
{
public static async Task<List<AccountDto>> Account(
[FromServices] IReadOnlyRepository<Account> repository,
[FromServices] IObjectMapper objectMapper,
GetAccountInput input)
{
var queryable = await repository.GetQueryableAsync();

queryable = queryable.Where(a => a.Metadata.ChainId == input.ChainId);

if (!input.Address.IsNullOrWhiteSpace())
{
queryable = queryable.Where(a => a.Address == input.Address);
}

if(!input.Symbol.IsNullOrWhiteSpace())
{
queryable = queryable.Where(a => a.Symbol == input.Symbol);
}

var accounts= queryable.OrderBy(o=>o.Metadata.Block.BlockHeight).ToList();

return objectMapper.Map<List<Account>, List<AccountDto>>(accounts);
}

public static async Task<List<TransferRecordDto>> TransferRecord(
[FromServices] IReadOnlyRepository<TransferRecord> repository,
[FromServices] IObjectMapper objectMapper,
GetTransferRecordInput input)
{
var queryable = await repository.GetQueryableAsync();

queryable = queryable.Where(a => a.Metadata.ChainId == input.ChainId);

if (!input.Address.IsNullOrWhiteSpace())
{
queryable = queryable.Where(a => a.FromAddress == input.Address || a.ToAddress == input.Address);
}

if(!input.Symbol.IsNullOrWhiteSpace())
{
queryable = queryable.Where(a => a.Symbol == input.Symbol);
}

var accounts= queryable.OrderBy(o=>o.Metadata.Block.BlockHeight).ToList();

return objectMapper.Map<List<TransferRecord>, List<TransferRecordDto>>(accounts);
}
}
  • Register log event processor

    Modify src/TokenAeIndexer/TokenAeIndexerModule.cs to register TokenTransferredProcessor.

TokenAeIndexerModule.cs
using AeFinder.Sdk.Processor;
using GraphQL.Types;
using Microsoft.Extensions.DependencyInjection;
using TokenAeIndexer.GraphQL;
using TokenAeIndexer.Processors;
using Volo.Abp.AutoMapper;
using Volo.Abp.Modularity;

namespace TokenAeIndexer;

public class TokenAeIndexerModule: AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpAutoMapperOptions>(options => { options.AddMaps<TokenAeIndexerModule>(); });
context.Services.AddSingleton<ISchema, AeIndexerSchema>();

context.Services.AddSingleton<ILogEventProcessor, TokenTransferredProcessor>();
}
}
  • Add entity mapping

    Modify src/TokenAeIndexer/TokenAeIndexerAutoMapperProfile.cs and add entity mapping code.

TokenAeIndexerAutoMapperProfile.cs
using AutoMapper;
using TokenAeIndexer.Entities;
using TokenAeIndexer.GraphQL;

namespace TokenAeIndexer;

public class TokenAeIndexerAutoMapperProfile : Profile
{
public TokenAeIndexerAutoMapperProfile()
{
CreateMap<Account, AccountDto>();
CreateMap<TransferRecord, TransferRecordDto>();
}
}

Building code

Use the following command in the code directory to compile the code.

dotnet build -c Release

Step 4 - Deploy AeIndexer

  • Open the AeIndexer details page and click Deploy. deploy
  • Fill out the subscription manifest and upload the DLL file.
  1. Subscription manifest:
{
"subscriptionItems": [
{
"chainId": "AELF",
"startBlockNumber": 225432000,
"onlyConfirmed": false,
"logEvents": [
{
"contractAddress": "JRmBduh4nXWi1aXgdUsj5gJrzeZb2LxmrAbf7W99faZSvoAaE",
"eventNames": [
"Transferred"
]
}
]
},
{
"chainId": "tDVW",
"startBlockNumber": 218592000,
"onlyConfirmed": false,
"logEvents": [
{
"contractAddress": "ASh2Wt7nSEmYqnGxPPzp4pnVDU4uhj1XW9Se5VeZcX2UDdyjx",
"eventNames": [
"Transferred"
]
}
]
}
]
}
  1. DLL file location: src/TokenAeIndexer/bin/Release/net8.0/TokenAeIndexer.dll deploy-2
  • 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. log

Step 5 - Query indexed data

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.

query{
transferRecord(input:{chainId:"AELF"}){
fromAddress,
toAddress,
symbol,
amount,
metadata{
chainId,
block{
blockHeight
}
}
}
}

query

tip

ℹ️ Note: For the complete demo code, please visit AeFinder github to download. https://github.com/AeFinderProject/aefinder/tree/master/samples/TokenAeIndexer