From ace881d8ea1bef0ca32e504617bf70cbf2c29458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=E1=BA=A1m=20Th=C3=A0nh=20Phong?= <49814372+phamphong9981@users.noreply.github.com> Date: Fri, 2 Aug 2024 11:16:04 +0700 Subject: [PATCH] Feat/merge erc20 from develop to evm (#878) * feat: merge erc20 from develop to evm * feat: merge erc20 from develop to evm --- ...0702013020_add_col_to_save_cosmos_erc20.ts | 18 + src/models/account_balance.ts | 2 + src/models/erc20_activity.ts | 8 +- src/models/event.ts | 2 + src/models/event_attribute.ts | 1 + .../api-gateways/api_gateway.service.ts | 1 + .../api-gateways/erc20_admin.service.ts | 49 +++ src/services/evm/constant.ts | 5 + src/services/evm/erc20.service.ts | 347 ++++++++---------- src/services/evm/erc20_handler.ts | 341 +++++++++++++++-- src/services/evm/erc20_reindex.ts | 122 ++++++ test/unit/services/erc20/erc20.spec.ts | 192 ---------- .../unit/services/erc20/erc20_handler.spec.ts | 174 --------- test/unit/services/evm/erc20.spec.ts | 339 +++++++++++++++++ test/unit/services/evm/erc20_handler.spec.ts | 224 +++++++++++ test/unit/services/evm/erc20_reindex.spec.ts | 224 +++++++++++ 16 files changed, 1459 insertions(+), 590 deletions(-) create mode 100644 migrations/evm/20240702013020_add_col_to_save_cosmos_erc20.ts create mode 100644 src/services/api-gateways/erc20_admin.service.ts create mode 100644 src/services/evm/erc20_reindex.ts delete mode 100644 test/unit/services/erc20/erc20.spec.ts delete mode 100644 test/unit/services/erc20/erc20_handler.spec.ts create mode 100644 test/unit/services/evm/erc20.spec.ts create mode 100644 test/unit/services/evm/erc20_handler.spec.ts create mode 100644 test/unit/services/evm/erc20_reindex.spec.ts diff --git a/migrations/evm/20240702013020_add_col_to_save_cosmos_erc20.ts b/migrations/evm/20240702013020_add_col_to_save_cosmos_erc20.ts new file mode 100644 index 000000000..c7bb941a1 --- /dev/null +++ b/migrations/evm/20240702013020_add_col_to_save_cosmos_erc20.ts @@ -0,0 +1,18 @@ +import { Knex } from 'knex'; +import { Erc20Activity } from '../../src/models'; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable(Erc20Activity.tableName, (table) => { + table.bigint('cosmos_event_id').index(); + table.integer('cosmos_tx_id').index(); + table.setNullable('evm_event_id'); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable(Erc20Activity.tableName, (table) => { + table.dropColumn('cosmos_event_id'); + table.dropColumn('cosmos_tx_id'); + table.dropNullable('evm_event_id'); + }); +} diff --git a/src/models/account_balance.ts b/src/models/account_balance.ts index 6100451e3..4f78806cd 100644 --- a/src/models/account_balance.ts +++ b/src/models/account_balance.ts @@ -4,6 +4,8 @@ import BaseModel from './base'; import { Account } from './account'; export class AccountBalance extends BaseModel { + static softDelete = false; + account!: Account; id!: number; diff --git a/src/models/erc20_activity.ts b/src/models/erc20_activity.ts index c0fb67096..268571e87 100644 --- a/src/models/erc20_activity.ts +++ b/src/models/erc20_activity.ts @@ -32,6 +32,10 @@ export class Erc20Activity extends BaseModel { evm_tx_id!: number; + cosmos_tx_id!: number; + + cosmos_event_id!: string; + static get tableName() { return 'erc20_activity'; } @@ -39,7 +43,7 @@ export class Erc20Activity extends BaseModel { static get jsonSchema() { return { type: 'object', - required: ['evm_event_id', 'erc20_contract_address', 'height'], + required: ['erc20_contract_address', 'height'], properties: { evm_event_id: { type: 'number' }, erc20_contract_address: { type: 'string' }, @@ -48,6 +52,8 @@ export class Erc20Activity extends BaseModel { action: { type: 'string' }, from: { type: 'string' }, sender: { type: 'string' }, + cosmos_tx_id: { type: 'number' }, + cosmos_event_id: { type: 'string' }, }, }; } diff --git a/src/models/event.ts b/src/models/event.ts index cafe70326..9655acff2 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -111,5 +111,7 @@ export class Event extends BaseModel { CHANNEL_CLOSE_CONFIRM: 'channel_close_confirm', CHANNEL_CLOSE: 'channel_close', BLOCK_BLOOM: 'block_bloom', + CONVERT_COIN: 'convert_coin', + CONVERT_ERC20: 'convert_erc20', }; } diff --git a/src/models/event_attribute.ts b/src/models/event_attribute.ts index b0e42f33d..8d90a9a0d 100644 --- a/src/models/event_attribute.ts +++ b/src/models/event_attribute.ts @@ -121,6 +121,7 @@ export class EventAttribute extends BaseModel { REFUND_AMOUNT: 'refund_amount', MEMO: 'memo', BLOOM: 'bloom', + ERC20_TOKEN: 'erc20_token', }; static ATTRIBUTE_COMPOSITE_KEY = { diff --git a/src/services/api-gateways/api_gateway.service.ts b/src/services/api-gateways/api_gateway.service.ts index 4f9ece22d..8b87e7b53 100644 --- a/src/services/api-gateways/api_gateway.service.ts +++ b/src/services/api-gateways/api_gateway.service.ts @@ -48,6 +48,7 @@ import config from '../../../config.json' assert { type: 'json' }; 'v1.job.insert-verify-by-codehash', 'v1.erc721-admin.*', 'v2.evm-statistics.syncPrevDateStatsByChainId', + 'v1.erc20-admin.*', ], }, { diff --git a/src/services/api-gateways/erc20_admin.service.ts b/src/services/api-gateways/erc20_admin.service.ts new file mode 100644 index 000000000..be9e6dbe9 --- /dev/null +++ b/src/services/api-gateways/erc20_admin.service.ts @@ -0,0 +1,49 @@ +import { Post, Service } from '@ourparentcenter/moleculer-decorators-extended'; +import { Context, ServiceBroker } from 'moleculer'; +import networks from '../../../network.json' assert { type: 'json' }; +import BaseService from '../../base/base.service'; + +@Service({ + name: 'erc20-admin', + version: 1, +}) +export default class Erc20AdminService extends BaseService { + public constructor(public broker: ServiceBroker) { + super(broker); + } + + @Post('/erc20-reindexing', { + name: 'erc20Reindexing', + params: { + chainid: { + type: 'string', + optional: false, + enum: networks.map((network) => network.chainId), + }, + addresses: { + type: 'array', + optional: false, + items: 'string', + }, + }, + }) + async erc20Reindexing( + ctx: Context< + { + chainid: string; + addresses: string[]; + }, + Record + > + ) { + const selectedChain = networks.find( + (network) => network.chainId === ctx.params.chainid + ); + return this.broker.call( + `v1.Erc20.reindexing@${selectedChain?.moleculerNamespace}`, + { + addresses: ctx.params.addresses, + } + ); + } +} diff --git a/src/services/evm/constant.ts b/src/services/evm/constant.ts index 81bbcfac7..9588e70fe 100644 --- a/src/services/evm/constant.ts +++ b/src/services/evm/constant.ts @@ -117,6 +117,10 @@ export const SERVICE = { key: 'insertNewErc20Contracts', path: 'v1.Erc20.insertNewErc20Contracts', }, + reindexing: { + key: 'reindexing', + path: 'v1.Erc20.reindexing', + }, }, Erc721: { key: 'Erc721', @@ -288,6 +292,7 @@ export const BULL_JOB_NAME = { UPDATE_OPTIMISM_WITHDRAWAL_STATUS: 'handle:update-optimism-withdrawal-status', INSERT_VERIFY_BY_CODEHASH: 'job:insert-verify-by-codehash', HANDLE_SELF_DESTRUCT: 'handle:self-destruct', + REINDEX_ERC20: 'reindex:erc20', }; export const MSG_TYPE = { diff --git a/src/services/evm/erc20.service.ts b/src/services/evm/erc20.service.ts index b9684bff2..a50edbd8e 100644 --- a/src/services/evm/erc20.service.ts +++ b/src/services/evm/erc20.service.ts @@ -8,17 +8,18 @@ import { Context, ServiceBroker } from 'moleculer'; import { PublicClient, getContract } from 'viem'; import config from '../../../config.json' assert { type: 'json' }; import BullableService, { QueueHandler } from '../../base/bullable.service'; -import { SERVICE as COSMOS_SERVICE } from '../../common'; +import { SERVICE as COSMOS_SERVICE, Config } from '../../common'; import knex from '../../common/utils/db_connection'; import { getViemClient } from '../../common/utils/etherjs_client'; -import { BlockCheckpoint, EVMSmartContract, EvmEvent } from '../../models'; -import { AccountBalance } from '../../models/account_balance'; +import { BlockCheckpoint, EVMSmartContract } from '../../models'; import { Erc20Activity } from '../../models/erc20_activity'; import { Erc20Contract } from '../../models/erc20_contract'; import { BULL_JOB_NAME, SERVICE as EVM_SERVICE, SERVICE } from './constant'; -import { ERC20_EVENT_TOPIC0, Erc20Handler } from './erc20_handler'; +import { Erc20Handler } from './erc20_handler'; +import { Erc20Reindexer } from './erc20_reindex'; import { convertEthAddressToBech32Address } from './utils'; +const { NODE_ENV } = Config; @Service({ name: EVM_SERVICE.V1.Erc20.key, version: 1, @@ -89,47 +90,14 @@ export default class Erc20Service extends BullableService { ], config.erc20.key ); - // TODO: handle track erc20 contract only - const erc20Events = await EvmEvent.query() - .transacting(trx) - .joinRelated('[evm_smart_contract,evm_transaction]') - .innerJoin( - 'erc20_contract', - 'evm_event.address', - 'erc20_contract.address' - ) - .where('evm_event.block_height', '>', startBlock) - .andWhere('evm_event.block_height', '<=', endBlock) - .orderBy('evm_event.id', 'asc') - .select( - 'evm_event.*', - 'evm_transaction.from as sender', - 'evm_smart_contract.id as evm_smart_contract_id', - 'evm_transaction.id as evm_tx_id' + const erc20Activities: Erc20Activity[] = + await Erc20Handler.buildErc20Activities( + startBlock, + endBlock, + trx, + this.logger ); - await this.handleMissingErc20Contract(erc20Events, trx); - const erc20Activities: Erc20Activity[] = []; - erc20Events.forEach((e) => { - if (e.topic0 === ERC20_EVENT_TOPIC0.TRANSFER) { - const activity = Erc20Handler.buildTransferActivity(e, this.logger); - if (activity) { - erc20Activities.push(activity); - } - } else if (e.topic0 === ERC20_EVENT_TOPIC0.APPROVAL) { - const activity = Erc20Handler.buildApprovalActivity(e, this.logger); - if (activity) { - erc20Activities.push(activity); - } - } else if (config.erc20.wrapExtensionContract.includes(e.address)) { - const wrapActivity = Erc20Handler.buildWrapExtensionActivity( - e, - this.logger - ); - if (wrapActivity) { - erc20Activities.push(wrapActivity); - } - } - }); + await this.handleMissingErc20Contract(erc20Activities, trx); if (erc20Activities.length > 0) { this.logger.info( `Crawl Erc20 activity from block ${startBlock} to block ${endBlock}:\n ${JSON.stringify( @@ -165,41 +133,40 @@ export default class Erc20Service extends BullableService { config.erc20.key ); // get Erc20 activities - let erc20Activities = await this.getErc20Activities(startBlock, endBlock); - await this.handleMissingAccounts(erc20Activities); - erc20Activities = await this.getErc20Activities(startBlock, endBlock); - await knex.transaction(async (trx) => { - if (erc20Activities.length > 0) { - const accountBalances = _.keyBy( - await AccountBalance.query() - .transacting(trx) - .joinRelated('account') - .whereIn( - ['account.evm_address', 'denom'], - [ - ...erc20Activities.map((e) => [ - e.from, - e.erc20_contract_address, - ]), - ...erc20Activities.map((e) => [e.to, e.erc20_contract_address]), - ] - ), - (o) => `${o.account_id}_${o.denom}` - ); - // construct cw721 handler object - const erc20Handler = new Erc20Handler(accountBalances, erc20Activities); - erc20Handler.process(); - const updatedAccountBalances = Object.values( - erc20Handler.accountBalances - ); - if (updatedAccountBalances.length > 0) { - await AccountBalance.query() - .transacting(trx) - .insert(updatedAccountBalances) - .onConflict(['account_id', 'denom']) - .merge(); + let erc20Activities = await Erc20Handler.getErc20Activities( + startBlock, + endBlock + ); + // get missing Account + const missingAccountsAddress = Array.from( + new Set( + ( + [ + ...erc20Activities + .filter((e) => !e.from_account_id) + .map((e) => e.from), + ...erc20Activities.filter((e) => !e.to_account_id).map((e) => e.to), + ] as string[] + ).map((e) => + convertEthAddressToBech32Address(config.networkPrefixAddress, e) + ) + ) + ); + if (missingAccountsAddress.length > 0) { + // crawl missing Account and requery erc20Activities + await this.broker.call( + COSMOS_SERVICE.V1.HandleAddressService.CrawlNewAccountApi.path, + { + addresses: missingAccountsAddress, } - } + ); + erc20Activities = await Erc20Handler.getErc20Activities( + startBlock, + endBlock + ); + } + await knex.transaction(async (trx) => { + await Erc20Handler.updateErc20AccountsBalance(erc20Activities, trx); updateBlockCheckpoint.height = endBlock; await BlockCheckpoint.query() .insert(updateBlockCheckpoint) @@ -209,6 +176,52 @@ export default class Erc20Service extends BullableService { }); } + @QueueHandler({ + queueName: BULL_JOB_NAME.REINDEX_ERC20, + jobName: BULL_JOB_NAME.REINDEX_ERC20, + }) + async reindexErc20(_payload: { address: `0x${string}` }): Promise { + const { address } = _payload; + const erc20Reindexer = new Erc20Reindexer(this.viemClient, this.logger); + await erc20Reindexer.reindex(address.toLowerCase() as `0x${string}`); + this.logger.info(`Reindex erc20 contract ${address} done.`); + } + + @Action({ + name: SERVICE.V1.Erc20.reindexing.key, + params: { + addresses: { + type: 'array', + items: 'string', + optional: false, + }, + }, + }) + public async reindexing( + ctx: Context<{ + addresses: `0x${string}`[]; + }> + ) { + const { addresses } = ctx.params; + if (addresses.length > 0) { + await Promise.all( + addresses.map((address) => + this.createJob( + BULL_JOB_NAME.REINDEX_ERC20, + BULL_JOB_NAME.REINDEX_ERC20, + { + address, + }, + { + jobId: address, + removeOnComplete: true, + } + ) + ) + ); + } + } + @Action({ name: SERVICE.V1.Erc20.insertNewErc20Contracts.key, params: { @@ -244,46 +257,23 @@ export default class Erc20Service extends BullableService { } } - async getErc20Activities( - startBlock: number, - endBlock: number - ): Promise { - return Erc20Activity.query() - .leftJoin( - 'account as from_account', - 'erc20_activity.from', - 'from_account.evm_address' - ) - .leftJoin( - 'account as to_account', - 'erc20_activity.to', - 'to_account.evm_address' - ) - .leftJoin( - 'erc20_contract as erc20_contract', - 'erc20_activity.erc20_contract_address', - 'erc20_contract.address' - ) - .where('erc20_activity.height', '>', startBlock) - .andWhere('erc20_activity.height', '<=', endBlock) - .andWhere('erc20_contract.track', true) - .select( - 'erc20_activity.*', - 'from_account.id as from_account_id', - 'to_account.id as to_account_id' - ) - .orderBy('erc20_activity.id'); - } - - async handleMissingErc20Contract(events: EvmEvent[], trx: Knex.Transaction) { - const eventsUniqByAddress = _.keyBy(events, (e) => e.address); - const addresses = Object.keys(eventsUniqByAddress); + async handleMissingErc20Contract( + erc20Activities: Erc20Activity[], + trx: Knex.Transaction + ) { + const addresses = _.uniq( + erc20Activities.map((activity) => activity.erc20_contract_address) + ); const erc20ContractsByAddress = _.keyBy( - await Erc20Contract.query() - .whereIn('address', addresses) - .transacting(trx), + await Erc20Contract.query().whereIn('address', addresses), + (e) => e.address + ); + + const evmSmartContracts = _.keyBy( + await EVMSmartContract.query().whereIn('address', addresses), (e) => e.address ); + const missingErc20ContractsAddress: string[] = addresses.filter( (addr) => !erc20ContractsByAddress[addr] ); @@ -295,8 +285,7 @@ export default class Erc20Service extends BullableService { .insert( missingErc20ContractsAddress.map((addr, index) => Erc20Contract.fromJson({ - evm_smart_contract_id: - eventsUniqByAddress[addr].evm_smart_contract_id, + evm_smart_contract_id: evmSmartContracts[addr].id, address: addr, total_supply: erc20ContractsInfo[index].totalSupply, symbol: erc20ContractsInfo[index].symbol, @@ -311,44 +300,6 @@ export default class Erc20Service extends BullableService { } } - async handleMissingAccounts(erc20Activities: Erc20Activity[]) { - // get missing Account - const missingAccountsAddress = Array.from( - new Set([ - ...erc20Activities.filter((e) => !e.from_account_id).map((e) => e.from), - ...erc20Activities.filter((e) => !e.to_account_id).map((e) => e.to), - ] as string[]) - ); - if (missingAccountsAddress.length > 0) { - try { - if (!config.evmOnly) { - // crawl missing Cosmos Account - await this.broker.call( - COSMOS_SERVICE.V1.HandleAddressService.CrawlNewAccountApi.path, - { - addresses: missingAccountsAddress.map((e) => - convertEthAddressToBech32Address(config.networkPrefixAddress, e) - ), - } - ); - } else { - // crawl missing Evm Account - await this.broker.call( - SERVICE.V1.CrawlEvmAccount.CrawlNewAccountApi.path, - { - addresses: missingAccountsAddress, - } - ); - } - } catch (error) { - this.logger.error(error); - throw new Error( - `Unable crawl missing account: ${missingAccountsAddress}` - ); - } - } - } - async getErc20Instances(evmSmartContracts: EVMSmartContract[]) { const addresses = evmSmartContracts.map((e) => e.address); const erc20ContractsInfo = await this.getBatchErc20Info( @@ -397,48 +348,50 @@ export default class Erc20Service extends BullableService { public async _start(): Promise { this.viemClient = getViemClient(); - await this.createJob( - BULL_JOB_NAME.HANDLE_ERC20_CONTRACT, - BULL_JOB_NAME.HANDLE_ERC20_CONTRACT, - {}, - { - removeOnComplete: true, - removeOnFail: { - count: 3, - }, - repeat: { - every: config.erc20.millisecondRepeatJob, - }, - } - ); - await this.createJob( - BULL_JOB_NAME.HANDLE_ERC20_ACTIVITY, - BULL_JOB_NAME.HANDLE_ERC20_ACTIVITY, - {}, - { - removeOnComplete: true, - removeOnFail: { - count: 3, - }, - repeat: { - every: config.erc20.millisecondRepeatJob, - }, - } - ); - await this.createJob( - BULL_JOB_NAME.HANDLE_ERC20_BALANCE, - BULL_JOB_NAME.HANDLE_ERC20_BALANCE, - {}, - { - removeOnComplete: true, - removeOnFail: { - count: 3, - }, - repeat: { - every: config.erc20.millisecondRepeatJob, - }, - } - ); + if (NODE_ENV !== 'test') { + await this.createJob( + BULL_JOB_NAME.HANDLE_ERC20_CONTRACT, + BULL_JOB_NAME.HANDLE_ERC20_CONTRACT, + {}, + { + removeOnComplete: true, + removeOnFail: { + count: 3, + }, + repeat: { + every: config.erc20.millisecondRepeatJob, + }, + } + ); + await this.createJob( + BULL_JOB_NAME.HANDLE_ERC20_ACTIVITY, + BULL_JOB_NAME.HANDLE_ERC20_ACTIVITY, + {}, + { + removeOnComplete: true, + removeOnFail: { + count: 3, + }, + repeat: { + every: config.erc20.millisecondRepeatJob, + }, + } + ); + await this.createJob( + BULL_JOB_NAME.HANDLE_ERC20_BALANCE, + BULL_JOB_NAME.HANDLE_ERC20_BALANCE, + {}, + { + removeOnComplete: true, + removeOnFail: { + count: 3, + }, + repeat: { + every: config.erc20.millisecondRepeatJob, + }, + } + ); + } return super._start(); } } diff --git a/src/services/evm/erc20_handler.ts b/src/services/evm/erc20_handler.ts index a4d8ef959..0ac755698 100644 --- a/src/services/evm/erc20_handler.ts +++ b/src/services/evm/erc20_handler.ts @@ -1,9 +1,19 @@ -import { Dictionary } from 'lodash'; +import { Knex } from 'knex'; +import _, { Dictionary } from 'lodash'; import Moleculer from 'moleculer'; import { decodeAbiParameters, keccak256, toHex } from 'viem'; -import { Erc20Activity, EvmEvent } from '../../models'; +import config from '../../../config.json' assert { type: 'json' }; +import knex from '../../common/utils/db_connection'; +import { + Erc20Activity, + Erc20Contract, + Event, + EventAttribute, + EvmEvent, +} from '../../models'; import { AccountBalance } from '../../models/account_balance'; import { ZERO_ADDRESS } from './constant'; +import { convertBech32AddressToEthAddress } from './utils'; export const ERC20_ACTION = { TRANSFER: 'transfer', @@ -52,12 +62,16 @@ export class Erc20Handler { erc20Activities: Erc20Activity[]; + erc20Contracts: Dictionary; + constructor( accountBalances: Dictionary, - erc20Activities: Erc20Activity[] + erc20Activities: Erc20Activity[], + erc20Contracts: Dictionary ) { this.accountBalances = accountBalances; this.erc20Activities = erc20Activities; + this.erc20Contracts = erc20Contracts; } process() { @@ -75,37 +89,60 @@ export class Erc20Handler { } handlerErc20Transfer(erc20Activity: Erc20Activity) { + const erc20Contract: Erc20Contract = + this.erc20Contracts[erc20Activity.erc20_contract_address]; + if (!erc20Contract) { + throw new Error( + `Erc20 contract not found:${erc20Activity.erc20_contract_address}` + ); + } // update from account balance if from != ZERO_ADDRESS if (erc20Activity.from !== ZERO_ADDRESS) { const fromAccountId = erc20Activity.from_account_id; const key = `${fromAccountId}_${erc20Activity.erc20_contract_address}`; const fromAccountBalance = this.accountBalances[key]; if ( - !fromAccountBalance || - fromAccountBalance.last_updated_height <= erc20Activity.height + fromAccountBalance && + erc20Activity.height < fromAccountBalance.last_updated_height ) { - // calculate new balance: decrease balance of from account - const amount = ( - BigInt(fromAccountBalance?.amount || 0) - BigInt(erc20Activity.amount) - ).toString(); - // update object accountBalance - this.accountBalances[key] = AccountBalance.fromJson({ - denom: erc20Activity.erc20_contract_address, - amount, - last_updated_height: erc20Activity.height, - account_id: fromAccountId, - type: AccountBalance.TYPE.ERC20_TOKEN, - }); + throw new Error( + `Process erc20 balance: fromAccountBalance ${erc20Activity.from} was updated` + ); } + // calculate new balance: decrease balance of from account + const amount = ( + BigInt(fromAccountBalance?.amount || 0) - BigInt(erc20Activity.amount) + ).toString(); + // update object accountBalance + this.accountBalances[key] = AccountBalance.fromJson({ + denom: erc20Activity.erc20_contract_address, + amount, + last_updated_height: erc20Activity.height, + account_id: fromAccountId, + type: AccountBalance.TYPE.ERC20_TOKEN, + }); + } else if (erc20Contract.total_supply !== null) { + // update total supply + erc20Contract.total_supply = ( + BigInt(erc20Contract.total_supply) + BigInt(erc20Activity.amount) + ).toString(); + // update last updated height + erc20Contract.last_updated_height = erc20Activity.height; } - // update to account balance - const toAccountId = erc20Activity.to_account_id; - const key = `${toAccountId}_${erc20Activity.erc20_contract_address}`; - const toAccountBalance = this.accountBalances[key]; - if ( - !toAccountBalance || - toAccountBalance.last_updated_height <= erc20Activity.height - ) { + // update from account balance if to != ZERO_ADDRESS + if (erc20Activity.to !== ZERO_ADDRESS) { + // update to account balance + const toAccountId = erc20Activity.to_account_id; + const key = `${toAccountId}_${erc20Activity.erc20_contract_address}`; + const toAccountBalance = this.accountBalances[key]; + if ( + toAccountBalance && + erc20Activity.height < toAccountBalance.last_updated_height + ) { + throw new Error( + `Process erc20 balance: toAccountBalance ${erc20Activity.to} was updated` + ); + } // calculate new balance: increase balance of to account const amount = ( BigInt(toAccountBalance?.amount || 0) + BigInt(erc20Activity.amount) @@ -118,6 +155,191 @@ export class Erc20Handler { account_id: toAccountId, type: AccountBalance.TYPE.ERC20_TOKEN, }); + } else if (erc20Contract.total_supply !== null) { + // update total supply + erc20Contract.total_supply = ( + BigInt(erc20Contract.total_supply) - BigInt(erc20Activity.amount) + ).toString(); + // update last updated height + erc20Contract.last_updated_height = erc20Activity.height; + } + } + + static async buildErc20Activities( + startBlock: number, + endBlock: number, + trx: Knex.Transaction, + logger: Moleculer.LoggerInstance, + addresses?: string[] + ) { + const erc20Activities: Erc20Activity[] = []; + const erc20Events = await EvmEvent.query() + .transacting(trx) + .joinRelated('[evm_smart_contract,evm_transaction]') + .innerJoin( + 'erc20_contract', + 'evm_event.address', + 'erc20_contract.address' + ) + .modify((builder) => { + if (addresses) { + builder.whereIn('evm_event.address', addresses); + } + }) + .where('evm_event.block_height', '>', startBlock) + .andWhere('evm_event.block_height', '<=', endBlock) + .orderBy('evm_event.id', 'asc') + .select( + 'evm_event.*', + 'evm_transaction.from as sender', + 'evm_smart_contract.id as evm_smart_contract_id', + 'evm_transaction.id as evm_tx_id' + ); + let erc20CosmosEvents: Event[] = []; + if (config.evmOnly === false) { + erc20CosmosEvents = await Event.query() + .transacting(trx) + .where('event.block_height', '>', startBlock) + .andWhere('event.block_height', '<=', endBlock) + .andWhere((query) => { + query + .where('event.type', Event.EVENT_TYPE.CONVERT_COIN) + .orWhere('event.type', Event.EVENT_TYPE.CONVERT_ERC20); + }) + .modify((builder) => { + if (addresses) { + builder + .joinRelated('attributes') + .where('attributes.key', EventAttribute.ATTRIBUTE_KEY.ERC20_TOKEN) + .whereIn(knex.raw('lower("value")'), addresses); + } + }) + .withGraphFetched('[transaction, attributes]') + .orderBy('event.id', 'asc'); + } + erc20Events.forEach((e) => { + if (e.topic0 === ERC20_EVENT_TOPIC0.TRANSFER) { + const activity = Erc20Handler.buildTransferActivity(e, logger); + if (activity) { + erc20Activities.push(activity); + } + } else if (e.topic0 === ERC20_EVENT_TOPIC0.APPROVAL) { + const activity = Erc20Handler.buildApprovalActivity(e, logger); + if (activity) { + erc20Activities.push(activity); + } + } else if (config.erc20.wrapExtensionContract.includes(e.address)) { + const wrapActivity = Erc20Handler.buildWrapExtensionActivity(e, logger); + if (wrapActivity) { + erc20Activities.push(wrapActivity); + } + } + }); + erc20CosmosEvents.forEach((event) => { + const activity = Erc20Handler.buildTransferActivityByCosmos( + event, + logger + ); + if (activity) { + erc20Activities.push(activity); + } + }); + return _.sortBy(erc20Activities, 'cosmos_tx_id'); + } + + static async getErc20Activities( + startBlock: number, + endBlock: number, + trx?: Knex.Transaction, + addresses?: string[] + ): Promise { + return Erc20Activity.query() + .modify((builder) => { + if (addresses) { + builder.whereIn('erc20_contract.address', addresses); + } + if (trx) { + builder.transacting(trx); + } + }) + .leftJoin( + 'account as from_account', + 'erc20_activity.from', + 'from_account.evm_address' + ) + .leftJoin( + 'account as to_account', + 'erc20_activity.to', + 'to_account.evm_address' + ) + .leftJoin( + 'erc20_contract as erc20_contract', + 'erc20_activity.erc20_contract_address', + 'erc20_contract.address' + ) + .where('erc20_activity.height', '>', startBlock) + .andWhere('erc20_activity.height', '<=', endBlock) + .andWhere('erc20_contract.track', true) + .select( + 'erc20_activity.*', + 'from_account.id as from_account_id', + 'to_account.id as to_account_id' + ) + .orderBy('erc20_activity.id'); + } + + static async updateErc20AccountsBalance( + erc20Activities: Erc20Activity[], + trx: Knex.Transaction + ) { + if (erc20Activities.length > 0) { + const accountBalances = _.keyBy( + await AccountBalance.query() + .transacting(trx) + .joinRelated('account') + .whereIn( + ['account.evm_address', 'denom'], + [ + ...erc20Activities.map((e) => [e.from, e.erc20_contract_address]), + ...erc20Activities.map((e) => [e.to, e.erc20_contract_address]), + ] + ), + (o) => `${o.account_id}_${o.denom}` + ); + const erc20Contracts = _.keyBy( + await Erc20Contract.query() + .transacting(trx) + .whereIn( + 'address', + erc20Activities.map((e) => e.erc20_contract_address) + ), + 'address' + ); + // construct cw721 handler object + const erc20Handler = new Erc20Handler( + accountBalances, + erc20Activities, + erc20Contracts + ); + erc20Handler.process(); + const updatedErc20Contracts = Object.values(erc20Handler.erc20Contracts); + if (updatedErc20Contracts.length > 0) { + await Erc20Contract.query() + .transacting(trx) + .insert(updatedErc20Contracts) + .onConflict(['id']) + .merge(); + } + const updatedAccountBalances = Object.values( + erc20Handler.accountBalances + ); + if (updatedAccountBalances.length > 0) { + await AccountBalance.query() + .transacting(trx) + .insert(updatedAccountBalances) + .onConflict(['account_id', 'denom']) + .merge(); + } } } @@ -126,13 +348,14 @@ export class Erc20Handler { logger: Moleculer.LoggerInstance ): Erc20Activity | undefined { try { + const amountEncoded = e.data ? toHex(e.data) : e.topic3; const [from, to, amount] = decodeAbiParameters( [ ABI_TRANSFER_PARAMS.FROM, ABI_TRANSFER_PARAMS.TO, ABI_TRANSFER_PARAMS.VALUE, ], - (e.topic1 + e.topic2.slice(2) + toHex(e.data).slice(2)) as `0x${string}` + (e.topic1 + e.topic2.slice(2) + amountEncoded.slice(2)) as `0x${string}` ) as [string, string, bigint]; return Erc20Activity.fromJson({ evm_event_id: e.id, @@ -145,6 +368,69 @@ export class Erc20Handler { height: e.block_height, tx_hash: e.tx_hash, evm_tx_id: e.evm_tx_id, + cosmos_tx_id: e.tx_id, + }); + } catch (e) { + logger.error(e); + return undefined; + } + } + + static buildTransferActivityByCosmos( + e: Event, + logger: Moleculer.LoggerInstance + ): Erc20Activity | undefined { + try { + const getAddressFromAttrAndConvert0x = ( + attrs: EventAttribute[], + key: string + ) => { + const attr = attrs.find((attr) => attr.key === key); + if (attr) { + let { value } = attr; + if (!value.startsWith('0x')) { + value = convertBech32AddressToEthAddress( + config.networkPrefixAddress, + value + ); + } + return value.toLowerCase(); + } + return undefined; + }; + + let from = getAddressFromAttrAndConvert0x( + e.attributes, + EventAttribute.ATTRIBUTE_KEY.SENDER + ); + let to = getAddressFromAttrAndConvert0x( + e.attributes, + EventAttribute.ATTRIBUTE_KEY.RECEIVER + ); + const sender = from; + if (e.type === Event.EVENT_TYPE.CONVERT_COIN) { + from = ZERO_ADDRESS; + } else if (e.type === Event.EVENT_TYPE.CONVERT_ERC20) { + to = ZERO_ADDRESS; + } + const amount = e.attributes.find( + (attr) => attr.key === EventAttribute.ATTRIBUTE_KEY.AMOUNT + ); + const address = getAddressFromAttrAndConvert0x( + e.attributes, + EventAttribute.ATTRIBUTE_KEY.ERC20_TOKEN + ); + return Erc20Activity.fromJson({ + sender, + action: ERC20_ACTION.TRANSFER, + erc20_contract_address: address, + amount: amount?.value, + from, + to, + height: e.block_height, + tx_hash: e.transaction.hash, + cosmos_event_id: e.id, + cosmos_tx_id: e.tx_id, }); } catch (e) { logger.error(e); @@ -176,6 +462,7 @@ export class Erc20Handler { height: e.block_height, tx_hash: e.tx_hash, evm_tx_id: e.evm_tx_id, + cosmos_tx_id: e.tx_id, }); } catch (e) { logger.error(e); @@ -218,6 +505,7 @@ export class Erc20Handler { height: e.block_height, tx_hash: e.tx_hash, evm_tx_id: e.evm_tx_id, + cosmos_tx_id: e.tx_id, }); } catch (e) { logger.error(e); @@ -245,6 +533,7 @@ export class Erc20Handler { height: e.block_height, tx_hash: e.tx_hash, evm_tx_id: e.evm_tx_id, + cosmos_tx_id: e.tx_id, }); } catch (e) { logger.error(e); diff --git a/src/services/evm/erc20_reindex.ts b/src/services/evm/erc20_reindex.ts new file mode 100644 index 000000000..4838aeca8 --- /dev/null +++ b/src/services/evm/erc20_reindex.ts @@ -0,0 +1,122 @@ +import Moleculer from 'moleculer'; +import { getContract, PublicClient } from 'viem'; +import config from '../../../config.json' assert { type: 'json' }; +import knex from '../../common/utils/db_connection'; +import { AccountBalance, Erc20Activity, Erc20Contract } from '../../models'; +import { Erc20Handler } from './erc20_handler'; +import { convertEthAddressToBech32Address } from './utils'; + +export class Erc20Reindexer { + viemClient: PublicClient; + + logger!: Moleculer.LoggerInstance; + + constructor(viemClient: PublicClient, logger: Moleculer.LoggerInstance) { + this.viemClient = viemClient; + this.logger = logger; + } + + /** + * @description reindex erc20 contract + * @param addresses Contracts address that you want to reindex + * @steps + * - clean database: erc20 AccountBalance + * - re-compute erc20 AccountBalance + */ + async reindex(address: `0x${string}`) { + // stop tracking => if start reindexing, track will be false (although error when reindex) + await Erc20Contract.query() + .patch({ track: false }) + .where('address', address); + // reindex + await knex.transaction(async (trx) => { + const erc20Contract = await Erc20Contract.query() + .transacting(trx) + .joinRelated('evm_smart_contract') + .where('erc20_contract.address', address) + .select('evm_smart_contract.id as evm_smart_contract_id') + .first() + .throwIfNotFound(); + await Erc20Activity.query() + .delete() + .where('erc20_contract_address', address) + .transacting(trx); + await AccountBalance.query() + .delete() + .where('denom', address) + .transacting(trx); + await Erc20Contract.query() + .delete() + .where('address', address) + .transacting(trx); + const contract = getContract({ + address, + abi: Erc20Contract.ABI, + client: this.viemClient, + }); + const [blockHeight, ...contractInfo] = await Promise.all([ + this.viemClient.getBlockNumber(), + contract.read.name().catch(() => Promise.resolve(undefined)), + contract.read.symbol().catch(() => Promise.resolve(undefined)), + contract.read.decimals().catch(() => Promise.resolve(undefined)), + ]); + await Erc20Contract.query() + .insert( + Erc20Contract.fromJson({ + evm_smart_contract_id: erc20Contract.evm_smart_contract_id, + address, + symbol: contractInfo[1], + name: contractInfo[0], + total_supply: '0', + decimal: contractInfo[2], + track: true, + last_updated_height: Number(blockHeight), + }) + ) + .transacting(trx); + const erc20Activities = await Erc20Handler.buildErc20Activities( + 0, + Number(blockHeight), + trx, + this.logger, + [address] + ); + if (erc20Activities.length > 0) { + await knex + .batchInsert( + 'erc20_activity', + erc20Activities, + config.erc20.chunkSizeInsert + ) + .transacting(trx); + } + const erc20ActivitiesInDb = await Erc20Handler.getErc20Activities( + 0, + Number(blockHeight), + trx, + [address] + ); + // get missing Account + const missingAccountsAddress = Array.from( + new Set( + [ + ...erc20ActivitiesInDb + .filter((e) => !e.from_account_id) + .map((e) => e.from), + ...erc20ActivitiesInDb + .filter((e) => !e.to_account_id) + .map((e) => e.to), + ].map((e) => + convertEthAddressToBech32Address(config.networkPrefixAddress, e) + ) as string[] + ) + ); + if (missingAccountsAddress.length > 0) { + throw new Error( + `Missing accounts ${missingAccountsAddress}. You should reindex them` + ); + } + await Erc20Handler.updateErc20AccountsBalance(erc20ActivitiesInDb, trx); + }); + } +} diff --git a/test/unit/services/erc20/erc20.spec.ts b/test/unit/services/erc20/erc20.spec.ts deleted file mode 100644 index 9cb0eef9f..000000000 --- a/test/unit/services/erc20/erc20.spec.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { AfterAll, BeforeAll, Describe, Test } from '@jest-decorated/core'; -import { ServiceBroker } from 'moleculer'; -import knex from '../../../../src/common/utils/db_connection'; -import { - Account, - EVMSmartContract, - EVMTransaction, - Erc20Activity, - Erc20Contract, - EvmEvent, -} from '../../../../src/models'; -import Erc20Service from '../../../../src/services/evm/erc20.service'; - -@Describe('Test erc20 handler') -export default class Erc20Test { - broker = new ServiceBroker({ logger: false }); - - erc20Service = this.broker.createService(Erc20Service) as Erc20Service; - - evmSmartContract = EVMSmartContract.fromJson({ - id: 555, - address: 'ghghdfgdsgre', - creator: 'dfgdfbvxcvxgfds', - created_height: 100, - created_hash: 'cvxcvcxv', - type: EVMSmartContract.TYPES.ERC20, - code_hash: 'dfgdfghf', - }); - - evmSmartContract2 = EVMSmartContract.fromJson({ - id: 666, - address: 'bcvbcvbcv', - creator: 'dfgdfbvxcvxgfds', - created_height: 100, - created_hash: 'xdasfsf', - type: EVMSmartContract.TYPES.PROXY_EIP_1967, - code_hash: 'xcsadf', - }); - - evmTx = EVMTransaction.fromJson({ - id: 11111, - hash: '', - height: 111, - tx_msg_id: 222, - tx_id: 223, - contract_address: '', - index: 1, - }); - - evmEvent = EvmEvent.fromJson({ - id: 888, - tx_id: 1234, - evm_tx_id: this.evmTx.id, - tx_hash: '', - address: '', - block_height: 1, - block_hash: '', - tx_index: 1, - }); - - @BeforeAll() - async initSuite() { - await this.broker.start(); - await knex.raw( - 'TRUNCATE TABLE erc20_contract, account, erc20_activity, evm_smart_contract, evm_event, evm_transaction RESTART IDENTITY CASCADE' - ); - await EVMSmartContract.query().insert([ - this.evmSmartContract, - this.evmSmartContract2, - ]); - await EVMTransaction.query().insert(this.evmTx); - await EvmEvent.query().insert(this.evmEvent); - } - - @AfterAll() - async tearDown() { - await this.broker.stop(); - } - - @Test('test getErc20Activities') - async testGetErc20Activities() { - const fromAccount = Account.fromJson({ - id: 123, - address: 'fgsdgfdfgdfgfsdg', - balances: [], - spendable_balances: [], - type: '', - pubkey: '', - account_number: 1, - sequence: 234, - evm_address: '0x124537cfdxvfsdfgv', - }); - const toAccount = Account.fromJson({ - id: 345, - address: 'xczfsdfsfsdg', - balances: [], - spendable_balances: [], - type: '', - pubkey: '', - account_number: 2, - sequence: 432, - evm_address: '0xghgfhfghfg', - }); - await Account.query().insert([fromAccount, toAccount]); - const erc20Contracts = [ - { - ...Erc20Contract.fromJson({ - id: 444, - evm_smart_contract_id: this.evmSmartContract.id, - total_supply: 123456, - symbol: 'PHO', - address: this.evmSmartContract.address, - decimal: 'fggdfgdgdg', - name: 'Phong', - track: true, - last_updated_height: 200, - }), - activities: [ - { - ...Erc20Activity.fromJson({ - id: 44444, - evm_event_id: this.evmEvent.id, - sender: 'fdgdfgdf', - action: 'transfer', - erc20_contract_address: this.evmSmartContract.address, - amount: '4543', - from: fromAccount.evm_address, - to: toAccount.evm_address, - height: 400, - tx_hash: 'dfghdfhdfhgdf', - evm_tx_id: this.evmTx.id, - }), - }, - { - ...Erc20Activity.fromJson({ - id: 1234, - evm_event_id: this.evmEvent.id, - sender: 'vgcxbvb', - action: 'transfer', - erc20_contract_address: this.evmSmartContract.address, - amount: '666666', - from: fromAccount.evm_address, - to: toAccount.evm_address, - height: 401, - tx_hash: 'dfghdfhdfhgdf', - evm_tx_id: this.evmTx.id, - }), - }, - ], - }, - { - ...Erc20Contract.fromJson({ - id: 445, - evm_smart_contract_id: this.evmSmartContract2.id, - total_supply: 15555, - symbol: 'ABC', - address: this.evmSmartContract2.address, - decimal: 'vbvbgdfg', - name: 'Abc', - track: false, - last_updated_height: 200, - }), - activities: [ - { - ...Erc20Activity.fromJson({ - id: 4444211, - evm_event_id: this.evmEvent.id, - sender: 'fdgdfgdf', - action: 'transfer', - erc20_contract_address: this.evmSmartContract2.address, - amount: '4543', - from: fromAccount.evm_address, - to: toAccount.evm_address, - height: 400, - tx_hash: 'dfghdfhdfhgdf', - evm_tx_id: this.evmTx.id, - }), - }, - ], - }, - ]; - await Erc20Contract.query().insertGraph(erc20Contracts); - const result = await this.erc20Service.getErc20Activities(0, 10000000); - expect(result.length).toEqual(2); - expect(result[0]).toMatchObject(erc20Contracts[0].activities[1]); - expect(result[0].from_account_id).toEqual(fromAccount.id); - expect(result[0].to_account_id).toEqual(toAccount.id); - expect(result[1]).toMatchObject(erc20Contracts[0].activities[0]); - expect(result[1].from_account_id).toEqual(fromAccount.id); - expect(result[1].to_account_id).toEqual(toAccount.id); - } -} diff --git a/test/unit/services/erc20/erc20_handler.spec.ts b/test/unit/services/erc20/erc20_handler.spec.ts deleted file mode 100644 index f1119a634..000000000 --- a/test/unit/services/erc20/erc20_handler.spec.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { fromBase64 } from '@cosmjs/encoding'; -import { AfterAll, BeforeAll, Describe, Test } from '@jest-decorated/core'; -import { ServiceBroker } from 'moleculer'; -import { decodeAbiParameters, toHex } from 'viem'; -import { Dictionary } from 'lodash'; -import { Erc20Activity, EvmEvent } from '../../../../src/models'; -import { - ABI_APPROVAL_PARAMS, - ABI_TRANSFER_PARAMS, - ERC20_ACTION, - Erc20Handler, -} from '../../../../src/services/evm/erc20_handler'; -import { AccountBalance } from '../../../../src/models/account_balance'; - -@Describe('Test erc20 handler') -export default class Erc20HandlerTest { - broker = new ServiceBroker({ logger: false }); - - @BeforeAll() - async initSuite() { - await this.broker.start(); - } - - @AfterAll() - async tearDown() { - await this.broker.stop(); - } - - @Test('test build erc20 transfer activity') - async testBuildErc20TransferActivity() { - const evmEvent = { - id: 872436, - tx_id: 9377483, - evm_tx_id: 6789103, - address: '0xf4dcd1ba7a2d862077a12918b9cf1889568b1fc5', - topic0: - '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', - topic1: - '0x00000000000000000000000089413d5a8601622a03fd63f8aab595a12e65b9c0', - topic2: - '0x0000000000000000000000004b919d8175dba25dbf733e7dcf9241ea7e51943b', - topic3: null, - block_height: 22024821, - tx_hash: - '0x1d646b55ef69dc9cf5e6b025b783c947f36d51c9b4e164895bbfe9e2af8b6e22', - tx_index: 0, - block_hash: - '0x6daa455dda31eb9e09000087bee9540bee9622842d5a423baf82da5b7b534a38', - data: fromBase64('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADVdLhb6wpEI='), - sender: 'evmos1fwgemqt4mw39m0mn8e7ulyjpafl9r9pmzyv3hv', - }; - const [from, to, amount] = decodeAbiParameters( - [ - ABI_TRANSFER_PARAMS.FROM, - ABI_TRANSFER_PARAMS.TO, - ABI_TRANSFER_PARAMS.VALUE, - ], - (evmEvent.topic1 + - evmEvent.topic2.slice(2) + - toHex(evmEvent.data).slice(2)) as `0x${string}` - ) as [string, string, bigint]; - const result = Erc20Handler.buildTransferActivity( - EvmEvent.fromJson(evmEvent), - this.broker.logger - ); - expect(result).toMatchObject({ - evm_event_id: evmEvent.id, - sender: evmEvent.sender, - action: ERC20_ACTION.TRANSFER, - erc20_contract_address: evmEvent.address, - amount: amount.toString(), - from: from.toLowerCase(), - to: to.toLowerCase(), - height: evmEvent.block_height, - tx_hash: evmEvent.tx_hash, - evm_tx_id: evmEvent.evm_tx_id, - }); - } - - @Test('test build erc20 approval activity') - async testBuildErc20ApprovalActivity() { - const evmEvent = { - id: 881548, - tx_id: 9381778, - evm_tx_id: 6793335, - address: '0xf4dcd1ba7a2d862077a12918b9cf1889568b1fc5', - topic0: - '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925', - topic1: - '0x000000000000000000000000e57c921f5f3f3a2aade9a462dab70b0cb97ded4d', - topic2: - '0x000000000000000000000000cbd61600b891a738150e68d5a58646321189cf6f', - topic3: null, - block_height: 22033598, - tx_hash: - '0x89dd0093c3c7633276c20be92fd5838f1eca99314a0c6375e9050e5cc82b51c3', - tx_index: 1, - block_hash: - '0x692c859d6254ef6c27fc7accf1131d55351c62a1357fe261d8517e3144cfbebe', - data: fromBase64('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='), - sender: 'evmos1u47fy86l8uaz4t0f533d4dctpjuhmm2dh3ezg0', - }; - const result = Erc20Handler.buildApprovalActivity( - EvmEvent.fromJson(evmEvent), - this.broker.logger - ); - const [from, to, amount] = decodeAbiParameters( - [ - ABI_APPROVAL_PARAMS.OWNER, - ABI_APPROVAL_PARAMS.SPENDER, - ABI_APPROVAL_PARAMS.VALUE, - ], - (evmEvent.topic1 + - evmEvent.topic2.slice(2) + - toHex(evmEvent.data).slice(2)) as `0x${string}` - ) as [string, string, bigint]; - expect(result).toMatchObject({ - evm_event_id: evmEvent.id, - sender: evmEvent.sender, - action: ERC20_ACTION.APPROVAL, - erc20_contract_address: evmEvent.address, - amount: amount.toString(), - from: from.toLowerCase(), - to: to.toLowerCase(), - height: evmEvent.block_height, - tx_hash: evmEvent.tx_hash, - evm_tx_id: evmEvent.evm_tx_id, - }); - } - - @Test('test handlerErc20Transfer') - async testHandlerErc20Transfer() { - const erc20Activity = Erc20Activity.fromJson({ - evm_event_id: 1, - sender: 'dafjfjj', - action: ERC20_ACTION.TRANSFER, - erc20_contract_address: 'hsdbjbfbdsfc', - amount: '12345222', - from: 'phamphong1', - to: 'phamphong2', - height: 10000, - tx_hash: 'fghkjghfdkjgbvkdfngkjdf', - evm_tx_id: 1, - from_account_id: 123, - to_account_id: 234, - }); - const [fromKey, toKey] = [ - `${erc20Activity.from_account_id}_${erc20Activity.erc20_contract_address}`, - `${erc20Activity.to_account_id}_${erc20Activity.erc20_contract_address}`, - ]; - const accountBalances: Dictionary = { - [fromKey]: AccountBalance.fromJson({ - denom: erc20Activity.erc20_contract_address, - amount: '998222', - last_updated_height: 1, - }), - [toKey]: AccountBalance.fromJson({ - denom: erc20Activity.erc20_contract_address, - amount: '1111111', - last_updated_height: 1, - }), - }; - const erc20Handler = new Erc20Handler(accountBalances, []); - erc20Handler.handlerErc20Transfer(erc20Activity); - expect(erc20Handler.accountBalances[fromKey]).toMatchObject({ - denom: erc20Activity.erc20_contract_address, - amount: '-11347000', - }); - expect(erc20Handler.accountBalances[toKey]).toMatchObject({ - denom: erc20Activity.erc20_contract_address, - amount: '13456333', - }); - } -} diff --git a/test/unit/services/evm/erc20.spec.ts b/test/unit/services/evm/erc20.spec.ts new file mode 100644 index 000000000..71ff28b6f --- /dev/null +++ b/test/unit/services/evm/erc20.spec.ts @@ -0,0 +1,339 @@ +import { + AfterAll, + BeforeAll, + BeforeEach, + Describe, + Test, +} from '@jest-decorated/core'; +import { ServiceBroker } from 'moleculer'; +import _ from 'lodash'; +import knex from '../../../../src/common/utils/db_connection'; +import { + Account, + AccountBalance, + BlockCheckpoint, + EVMSmartContract, + EVMTransaction, + Erc20Activity, + Erc20Contract, + EvmEvent, +} from '../../../../src/models'; +import Erc20Service from '../../../../src/services/evm/erc20.service'; +import { BULL_JOB_NAME } from '../../../../src/services/evm/constant'; +import { SERVICE } from '../../../../src/common/constant'; +import config from '../../../../config.json' assert { type: 'json' }; +import { convertEthAddressToBech32Address } from '../../../../src/services/evm/utils'; + +async function cleanQueue(erc20Service: Erc20Service) { + await erc20Service + .getQueueManager() + .getQueue(BULL_JOB_NAME.HANDLE_ERC20_CONTRACT) + .drain(true); + await erc20Service + .getQueueManager() + .getQueue(BULL_JOB_NAME.HANDLE_ERC20_ACTIVITY) + .drain(true); + await erc20Service + .getQueueManager() + .getQueue(BULL_JOB_NAME.HANDLE_ERC20_BALANCE) + .drain(true); +} +@Describe('Test erc20 handle balance job') +export class Erc20Test { + broker = new ServiceBroker({ logger: false }); + + erc20Service = this.broker.createService(Erc20Service) as Erc20Service; + + evmSmartContract = EVMSmartContract.fromJson({ + id: 1, + address: '0xE974cC14c93FC6077B0d65F98832B846C5454A0B', + creator: '0x5606b4eA93F696Dd82Ca983BAF5723d00729f127', + created_height: 100, + created_hash: + '0x8c46cf6373f2f6e528b56becf0ce6b460b5d90cf9b0325a136b9b3a820e1e489', + type: EVMSmartContract.TYPES.ERC20, + code_hash: '0xdsf3335453454tsgfsdrtserf43645y4h4tAAvfgfgds', + }); + + evmSmartContract2 = EVMSmartContract.fromJson({ + id: 2, + address: '0x3CB367e7C920Ff15879Bd4CBd708b8c60eB0f537', + creator: '0xa9497CC4F95773A744D408b54dAC724626ee31d2', + created_height: 100, + created_hash: + '0x5bca9ee42c4c32941c58f2a510dae5ff5c6ed848d9a396a8e9e146a166b3a3fc', + type: EVMSmartContract.TYPES.PROXY_EIP_1967, + code_hash: '0xdfskjgdsgfgweruwie4535t3tu34tjkewtgjwe', + }); + + evmTx = EVMTransaction.fromJson({ + id: 11111, + hash: '', + height: 111, + tx_msg_id: 222, + tx_id: 223, + contract_address: '', + index: 1, + }); + + evmEvent = EvmEvent.fromJson({ + id: 888, + tx_id: 1234, + evm_tx_id: this.evmTx.id, + tx_hash: '', + address: '', + block_height: 1, + block_hash: '', + tx_index: 1, + }); + + erc20Contracts = [ + Erc20Contract.fromJson({ + id: 444, + evm_smart_contract_id: this.evmSmartContract.id, + total_supply: 123456, + symbol: 'PHO', + address: this.evmSmartContract.address, + decimal: 'fggdfgdgdg', + name: 'Phong', + track: true, + last_updated_height: 200, + }), + Erc20Contract.fromJson({ + id: 445, + evm_smart_contract_id: this.evmSmartContract2.id, + total_supply: 15555, + symbol: 'ABC', + address: this.evmSmartContract2.address, + decimal: 'vbvbgdfg', + name: 'Abc', + track: false, + last_updated_height: 200, + }), + ]; + + account1 = Account.fromJson({ + id: 1, + address: 'aura1w9vxuke5dz6hyza2j932qgmxltnfxwl7gdfgdg', + balances: [], + spendable_balances: [], + type: '', + pubkey: '', + account_number: 1, + sequence: 234, + evm_address: '0xDF587daaC47ae7B5586E34bCdb23d0b900b18a6C', + }); + + account2 = Account.fromJson({ + id: 2, + address: 'aura1w9vxuke5dz6hyza2j932qgmxltnfxwl7qlx4mg', + balances: [], + spendable_balances: [], + type: '', + pubkey: '', + account_number: 2, + sequence: 432, + evm_address: '0x71586E5B3468B5720BAa9162A02366Fae6933BfE', + }); + + account3 = Account.fromJson({ + id: 3, + address: 'aura888vxuke5dz6hyza2j932qgmxltnfxwl7sfsfs', + balances: [], + spendable_balances: [], + type: '', + pubkey: '', + account_number: 3, + sequence: 432, + evm_address: '0x7c756Cba10Ff2C65016494E8BA37C12a108572b5', + }); + + blockCheckpoints = [ + BlockCheckpoint.fromJson({ + job_name: BULL_JOB_NAME.HANDLE_ERC20_BALANCE, + height: 100, + }), + BlockCheckpoint.fromJson({ + job_name: BULL_JOB_NAME.HANDLE_ERC20_ACTIVITY, + height: 1000, + }), + ]; + + @BeforeAll() + async initSuite() { + await cleanQueue(this.erc20Service); + await this.erc20Service.getQueueManager().stopAll(); + await this.broker.start(); + await knex.raw( + 'TRUNCATE TABLE erc20_contract, account, erc20_activity, evm_smart_contract, evm_event, evm_transaction, account_balance, block_checkpoint RESTART IDENTITY CASCADE' + ); + await EVMSmartContract.query().insert([ + this.evmSmartContract, + this.evmSmartContract2, + ]); + await EVMTransaction.query().insert(this.evmTx); + await EvmEvent.query().insert(this.evmEvent); + await Erc20Contract.query().insert(this.erc20Contracts); + } + + @BeforeEach() + async beforeEach() { + await knex.raw( + 'TRUNCATE TABLE erc20_activity, block_checkpoint, account RESTART IDENTITY CASCADE' + ); + await BlockCheckpoint.query().insert(this.blockCheckpoints); + await Account.query().insert([this.account1, this.account2, this.account3]); + } + + @AfterAll() + async tearDown() { + await cleanQueue(this.erc20Service); + await this.broker.stop(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + } + + @Test('handle erc20 balance success') + async testHandleErc20Balance() { + const erc20Activities = [ + { + id: 1, + evm_event_id: this.evmEvent.id, + sender: '0xDF587daaC47ae7B5586E34bCdb23d0b900b18a6C', + action: 'transfer', + erc20_contract_address: this.evmSmartContract.address, + amount: '4543', + from: this.account1.evm_address, + to: this.account2.evm_address, + height: this.blockCheckpoints[0].height + 1, + tx_hash: + '0x21f905f14a26c5b35e43e1bfbcf8ff395e453d6497a0ce0a0c3cd7814ec0ba03', + evm_tx_id: this.evmTx.id, + }, + { + id: 2, + evm_event_id: this.evmEvent.id, + sender: '0xDF587daaC47ae7B5586E34bCdb23d0b900b18a6C', + action: 'transfer', + erc20_contract_address: this.evmSmartContract.address, + amount: '2211', + from: this.account2.evm_address, + to: this.account3.evm_address, + height: this.blockCheckpoints[0].height + 1, + tx_hash: + '0x21f905f14a26c5b35e43e1bfbcf8ff395e453d6497a0ce0a0c3cd7814ec0ba03', + evm_tx_id: this.evmTx.id, + }, + ]; + await Erc20Activity.query().insert(erc20Activities); + await this.erc20Service.handleErc20Balance(); + const accountBalances = _.keyBy( + await AccountBalance.query(), + (o) => `${o.account_id}_${o.denom}` + ); + expect( + accountBalances[`${this.account1.id}_${this.evmSmartContract.address}`] + .amount + ).toEqual(`-${BigInt(erc20Activities[0].amount).toString()}`); + expect( + accountBalances[`${this.account2.id}_${this.evmSmartContract.address}`] + .amount + ).toEqual( + `${( + BigInt(erc20Activities[0].amount) - BigInt(erc20Activities[1].amount) + ).toString()}` + ); + expect( + accountBalances[`${this.account3.id}_${this.evmSmartContract.address}`] + .amount + ).toEqual(`${BigInt(erc20Activities[1].amount).toString()}`); + } + + @Test('handle erc20 balance with missing account') + async testHandleErc20BalanceWithMissingAccount() { + const missingAccountId = 5; + const missingAccountAddr = '0x5606b4eA93F696Dd82Ca983BAF5723d00729f127'; + const erc20Activities = [ + { + id: 1, + evm_event_id: this.evmEvent.id, + sender: '0xDF587daaC47ae7B5586E34bCdb23d0b900b18a6C', + action: 'transfer', + erc20_contract_address: this.evmSmartContract.address, + amount: '4543', + from: this.account1.evm_address, + to: this.account2.evm_address, + height: this.blockCheckpoints[0].height + 1, + tx_hash: + '0x21f905f14a26c5b35e43e1bfbcf8ff395e453d6497a0ce0a0c3cd7814ec0ba03', + evm_tx_id: this.evmTx.id, + }, + { + id: 2, + evm_event_id: this.evmEvent.id, + sender: '0xDF587daaC47ae7B5586E34bCdb23d0b900b18a6C', + action: 'transfer', + erc20_contract_address: this.evmSmartContract.address, + amount: '2211', + from: missingAccountAddr, + to: this.account3.evm_address, + height: this.blockCheckpoints[0].height + 1, + tx_hash: + '0x21f905f14a26c5b35e43e1bfbcf8ff395e453d6497a0ce0a0c3cd7814ec0ba03', + evm_tx_id: this.evmTx.id, + }, + ]; + await Erc20Activity.query().insert(erc20Activities); + const mockCall = jest + .spyOn(this.erc20Service.broker, 'call') + .mockImplementation(async () => + Promise.resolve( + Account.query().insert( + Account.fromJson({ + id: missingAccountId, + address: 'aura12crtf65n76tdmqk2nqa674er6qrjnuf8h89p22', + balances: [], + spendable_balances: [], + type: '', + pubkey: '', + account_number: 4, + sequence: 2, + evm_address: missingAccountAddr, + }) + ) + ) + ); + await this.erc20Service.handleErc20Balance(); + const accountBalances = _.keyBy( + await AccountBalance.query(), + (o) => `${o.account_id}_${o.denom}` + ); + expect(mockCall).toHaveBeenCalledWith( + SERVICE.V1.HandleAddressService.CrawlNewAccountApi.path, + { + addresses: [ + convertEthAddressToBech32Address( + config.networkPrefixAddress, + missingAccountAddr + ), + ], + } + ); + expect( + accountBalances[`${this.account1.id}_${this.evmSmartContract.address}`] + .amount + ).toEqual(`-${BigInt(erc20Activities[0].amount).toString()}`); + expect( + accountBalances[`${this.account2.id}_${this.evmSmartContract.address}`] + .amount + ).toEqual(`${BigInt(erc20Activities[0].amount).toString()}`); + expect( + accountBalances[`${missingAccountId}_${this.evmSmartContract.address}`] + .amount + ).toEqual(`-${BigInt(erc20Activities[1].amount).toString()}`); + expect( + accountBalances[`${this.account3.id}_${this.evmSmartContract.address}`] + .amount + ).toEqual(`${BigInt(erc20Activities[1].amount).toString()}`); + } +} diff --git a/test/unit/services/evm/erc20_handler.spec.ts b/test/unit/services/evm/erc20_handler.spec.ts new file mode 100644 index 000000000..ef92dbf97 --- /dev/null +++ b/test/unit/services/evm/erc20_handler.spec.ts @@ -0,0 +1,224 @@ +import { AfterAll, BeforeAll, Describe, Test } from '@jest-decorated/core'; +import _ from 'lodash'; +import { ServiceBroker } from 'moleculer'; +import { decodeAbiParameters, toHex } from 'viem'; +import knex from '../../../../src/common/utils/db_connection'; +import { getViemClient } from '../../../../src/common/utils/etherjs_client'; +import { + Account, + AccountBalance, + Erc20Activity, + Erc20Contract, + EvmEvent, + EVMSmartContract, + EVMTransaction, +} from '../../../../src/models'; +import { ABI_TRANSFER_PARAMS } from '../../../../src/services/evm/erc20_handler'; +import { Erc20Reindexer } from '../../../../src/services/evm/erc20_reindex'; + +const accounts = [ + Account.fromJson({ + id: 116058, + address: 'aura1az8cmnr4ppfggj0vt4llzdun0ptlkc8jxha27w', + balances: [ + { + denom: 'uaura', + amount: '42981159', + }, + ], + code_hash: null, + evm_address: '0xe88f8dcc7508528449ec5d7ff137937857fb60f2', + pubkey: {}, + sequence: 11, + account_number: 5, + spendable_balances: [ + { + denom: 'uaura', + amount: '42981159', + }, + ], + type: '/cosmos.auth.v1beta1.BaseAccount', + account_balances: [ + { + denom: '0x80b5a32e4f032b2a058b4f29ec95eefeeb87adcd', + amount: '422142', + type: 'ERC20_TOKEN', + }, + { + denom: '0xde47a655a5d9904bd3f7e1a536d8323fbd99993a', + amount: '-10000000000000000000', + type: 'ERC20_TOKEN', + }, + ], + }), + Account.fromJson({ + id: 116059, + address: 'aura1xnjpzgtqztcy9l8vhafmr5zulfn7a8l8sjlhp3', + balances: [ + { + denom: 'uaura', + amount: '32218705', + }, + ], + code_hash: null, + evm_address: '0x34e411216012f042fcecbf53b1d05cfa67ee9fe7', + pubkey: {}, + sequence: 11, + account_number: 5, + spendable_balances: [ + { + denom: 'uaura', + amount: '42981159', + }, + ], + type: '/cosmos.auth.v1beta1.BaseAccount', + account_balances: [], + }), +]; +const evmSmartContract = EVMSmartContract.fromJson({ + id: 1, + address: '0xde47a655a5d9904bd3f7e1a536d8323fbd99993a', + creator: '0x5606b4eA93F696Dd82Ca983BAF5723d00729f127', + created_height: 100, + created_hash: + '0x8c46cf6373f2f6e528b56becf0ce6b460b5d90cf9b0325a136b9b3a820e1e489', + type: EVMSmartContract.TYPES.ERC20, + code_hash: '0xdsf3335453454tsgfsdrtserf43645y4h4tAAvfgfgds', +}); + +const erc20Contract = Erc20Contract.fromJson({ + id: 10, + address: evmSmartContract.address, + decimal: '18', + name: 'Wrapped Aura', + symbol: 'WAURA', + total_supply: '0', + track: true, + activities: [ + { + from: '0x93f8e7ec7e054b476d7de8e6bb096e56cd575beb', + erc20_contract_address: '0xde47a655a5d9904bd3f7e1a536d8323fbd99993a', + height: 7116660, + sender: '0x34e411216012f042fcecbf53b1d05cfa67ee9fe7', + to: '0x34e411216012f042fcecbf53b1d05cfa67ee9fe7', + tx_hash: + '0xf15467ec2a25eeef95798d93c2fe9ed8e7c891578b8e1bcc3284105849656c9d', + action: 'transfer', + amount: '258773093659445577552', + }, + ], + evm_smart_contract_id: evmSmartContract.id, +}); +const evmTransaction = EVMTransaction.fromJson({ + id: 2931, + hash: '0xf15467ec2a25eeef95798d93c2fe9ed8e7c891578b8e1bcc3284105849656c9d', + height: 1, + tx_id: 1612438, + tx_msg_id: 4752908, + contract_address: null, + index: 0, +}); + +@Describe('Test erc20 reindex') +export default class Erc20ReindexTest { + broker = new ServiceBroker({ logger: false }); + + @BeforeAll() + async initSuite() { + await this.broker.start(); + await knex.raw( + 'TRUNCATE TABLE evm_transaction, evm_smart_contract, erc20_contract, account, erc20_activity, account_balance, evm_event RESTART IDENTITY CASCADE' + ); + await EVMTransaction.query().insert(evmTransaction); + await EVMSmartContract.query().insert(evmSmartContract); + await Erc20Contract.query().insertGraph(erc20Contract); + await Account.query().insertGraph(accounts); + } + + @AfterAll() + async tearDown() { + await this.broker.stop(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + } + + @Test('test reindex') + async testReindex() { + const viemClient = getViemClient(); + jest.spyOn(viemClient, 'getBlockNumber').mockResolvedValue(BigInt(123456)); + // Instantiate Erc20Reindexer with the mock + const reindexer = new Erc20Reindexer(viemClient, this.broker.logger); + const event = EvmEvent.fromJson({ + id: 4227, + topic0: + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + topic1: + '0x000000000000000000000000e88f8dcc7508528449ec5d7ff137937857fb60f2', + topic2: + '0x00000000000000000000000034e411216012f042fcecbf53b1d05cfa67ee9fe7', + topic3: null, + tx_hash: + '0xf15467ec2a25eeef95798d93c2fe9ed8e7c891578b8e1bcc3284105849656c9d', + tx_index: 0, + tx_id: 1612438, + data: Buffer.from( + '\\x00000000000000000000000000000000000000000000000e0732b5a508244750' + ), + block_height: 1, + block_hash: + '0x16f84b38d58b8aedbba1e108990f858b09fe8373610659d4c37a67f5c87e09e1', + address: evmSmartContract.address, + evm_tx_id: evmTransaction.id, + }); + await EvmEvent.query().insert(event); + // Call the reindex method + await reindexer.reindex(erc20Contract.address as `0x${string}`); + // Test phase + const erc20Activity = await Erc20Activity.query().first().throwIfNotFound(); + const [from, to, amount] = decodeAbiParameters( + [ + ABI_TRANSFER_PARAMS.FROM, + ABI_TRANSFER_PARAMS.TO, + ABI_TRANSFER_PARAMS.VALUE, + ], + (event.topic1 + + event.topic2.slice(2) + + toHex(event.data).slice(2)) as `0x${string}` + ) as [string, string, bigint]; + // Test new activity had been inserted + expect(erc20Activity).toMatchObject({ + action: 'transfer', + erc20_contract_address: erc20Contract.address, + amount: amount.toString(), + from: from.toLowerCase(), + to: to.toLowerCase(), + }); + const accountBalances = _.keyBy( + await AccountBalance.query(), + (o) => `${o.account_id}_${o.denom}` + ); + // from account balance had been reindexed + expect( + accountBalances[`${accounts[0].id}_${erc20Contract.address}`] + ).toMatchObject({ + denom: erc20Contract.address, + amount: `-${amount.toString()}`, + }); + // to account balance had been reindexed + expect( + accountBalances[`${accounts[1].id}_${erc20Contract.address}`] + ).toMatchObject({ + denom: erc20Contract.address, + amount: amount.toString(), + }); + // from account balance without erc20 had been orinal + expect( + accountBalances[ + `${accounts[0].id}_${accounts[0].account_balances[0].denom}` + ] + ).toMatchObject({ + denom: accounts[0].account_balances[0].denom, + amount: accounts[0].account_balances[0].amount, + }); + } +} diff --git a/test/unit/services/evm/erc20_reindex.spec.ts b/test/unit/services/evm/erc20_reindex.spec.ts new file mode 100644 index 000000000..ef92dbf97 --- /dev/null +++ b/test/unit/services/evm/erc20_reindex.spec.ts @@ -0,0 +1,224 @@ +import { AfterAll, BeforeAll, Describe, Test } from '@jest-decorated/core'; +import _ from 'lodash'; +import { ServiceBroker } from 'moleculer'; +import { decodeAbiParameters, toHex } from 'viem'; +import knex from '../../../../src/common/utils/db_connection'; +import { getViemClient } from '../../../../src/common/utils/etherjs_client'; +import { + Account, + AccountBalance, + Erc20Activity, + Erc20Contract, + EvmEvent, + EVMSmartContract, + EVMTransaction, +} from '../../../../src/models'; +import { ABI_TRANSFER_PARAMS } from '../../../../src/services/evm/erc20_handler'; +import { Erc20Reindexer } from '../../../../src/services/evm/erc20_reindex'; + +const accounts = [ + Account.fromJson({ + id: 116058, + address: 'aura1az8cmnr4ppfggj0vt4llzdun0ptlkc8jxha27w', + balances: [ + { + denom: 'uaura', + amount: '42981159', + }, + ], + code_hash: null, + evm_address: '0xe88f8dcc7508528449ec5d7ff137937857fb60f2', + pubkey: {}, + sequence: 11, + account_number: 5, + spendable_balances: [ + { + denom: 'uaura', + amount: '42981159', + }, + ], + type: '/cosmos.auth.v1beta1.BaseAccount', + account_balances: [ + { + denom: '0x80b5a32e4f032b2a058b4f29ec95eefeeb87adcd', + amount: '422142', + type: 'ERC20_TOKEN', + }, + { + denom: '0xde47a655a5d9904bd3f7e1a536d8323fbd99993a', + amount: '-10000000000000000000', + type: 'ERC20_TOKEN', + }, + ], + }), + Account.fromJson({ + id: 116059, + address: 'aura1xnjpzgtqztcy9l8vhafmr5zulfn7a8l8sjlhp3', + balances: [ + { + denom: 'uaura', + amount: '32218705', + }, + ], + code_hash: null, + evm_address: '0x34e411216012f042fcecbf53b1d05cfa67ee9fe7', + pubkey: {}, + sequence: 11, + account_number: 5, + spendable_balances: [ + { + denom: 'uaura', + amount: '42981159', + }, + ], + type: '/cosmos.auth.v1beta1.BaseAccount', + account_balances: [], + }), +]; +const evmSmartContract = EVMSmartContract.fromJson({ + id: 1, + address: '0xde47a655a5d9904bd3f7e1a536d8323fbd99993a', + creator: '0x5606b4eA93F696Dd82Ca983BAF5723d00729f127', + created_height: 100, + created_hash: + '0x8c46cf6373f2f6e528b56becf0ce6b460b5d90cf9b0325a136b9b3a820e1e489', + type: EVMSmartContract.TYPES.ERC20, + code_hash: '0xdsf3335453454tsgfsdrtserf43645y4h4tAAvfgfgds', +}); + +const erc20Contract = Erc20Contract.fromJson({ + id: 10, + address: evmSmartContract.address, + decimal: '18', + name: 'Wrapped Aura', + symbol: 'WAURA', + total_supply: '0', + track: true, + activities: [ + { + from: '0x93f8e7ec7e054b476d7de8e6bb096e56cd575beb', + erc20_contract_address: '0xde47a655a5d9904bd3f7e1a536d8323fbd99993a', + height: 7116660, + sender: '0x34e411216012f042fcecbf53b1d05cfa67ee9fe7', + to: '0x34e411216012f042fcecbf53b1d05cfa67ee9fe7', + tx_hash: + '0xf15467ec2a25eeef95798d93c2fe9ed8e7c891578b8e1bcc3284105849656c9d', + action: 'transfer', + amount: '258773093659445577552', + }, + ], + evm_smart_contract_id: evmSmartContract.id, +}); +const evmTransaction = EVMTransaction.fromJson({ + id: 2931, + hash: '0xf15467ec2a25eeef95798d93c2fe9ed8e7c891578b8e1bcc3284105849656c9d', + height: 1, + tx_id: 1612438, + tx_msg_id: 4752908, + contract_address: null, + index: 0, +}); + +@Describe('Test erc20 reindex') +export default class Erc20ReindexTest { + broker = new ServiceBroker({ logger: false }); + + @BeforeAll() + async initSuite() { + await this.broker.start(); + await knex.raw( + 'TRUNCATE TABLE evm_transaction, evm_smart_contract, erc20_contract, account, erc20_activity, account_balance, evm_event RESTART IDENTITY CASCADE' + ); + await EVMTransaction.query().insert(evmTransaction); + await EVMSmartContract.query().insert(evmSmartContract); + await Erc20Contract.query().insertGraph(erc20Contract); + await Account.query().insertGraph(accounts); + } + + @AfterAll() + async tearDown() { + await this.broker.stop(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + } + + @Test('test reindex') + async testReindex() { + const viemClient = getViemClient(); + jest.spyOn(viemClient, 'getBlockNumber').mockResolvedValue(BigInt(123456)); + // Instantiate Erc20Reindexer with the mock + const reindexer = new Erc20Reindexer(viemClient, this.broker.logger); + const event = EvmEvent.fromJson({ + id: 4227, + topic0: + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + topic1: + '0x000000000000000000000000e88f8dcc7508528449ec5d7ff137937857fb60f2', + topic2: + '0x00000000000000000000000034e411216012f042fcecbf53b1d05cfa67ee9fe7', + topic3: null, + tx_hash: + '0xf15467ec2a25eeef95798d93c2fe9ed8e7c891578b8e1bcc3284105849656c9d', + tx_index: 0, + tx_id: 1612438, + data: Buffer.from( + '\\x00000000000000000000000000000000000000000000000e0732b5a508244750' + ), + block_height: 1, + block_hash: + '0x16f84b38d58b8aedbba1e108990f858b09fe8373610659d4c37a67f5c87e09e1', + address: evmSmartContract.address, + evm_tx_id: evmTransaction.id, + }); + await EvmEvent.query().insert(event); + // Call the reindex method + await reindexer.reindex(erc20Contract.address as `0x${string}`); + // Test phase + const erc20Activity = await Erc20Activity.query().first().throwIfNotFound(); + const [from, to, amount] = decodeAbiParameters( + [ + ABI_TRANSFER_PARAMS.FROM, + ABI_TRANSFER_PARAMS.TO, + ABI_TRANSFER_PARAMS.VALUE, + ], + (event.topic1 + + event.topic2.slice(2) + + toHex(event.data).slice(2)) as `0x${string}` + ) as [string, string, bigint]; + // Test new activity had been inserted + expect(erc20Activity).toMatchObject({ + action: 'transfer', + erc20_contract_address: erc20Contract.address, + amount: amount.toString(), + from: from.toLowerCase(), + to: to.toLowerCase(), + }); + const accountBalances = _.keyBy( + await AccountBalance.query(), + (o) => `${o.account_id}_${o.denom}` + ); + // from account balance had been reindexed + expect( + accountBalances[`${accounts[0].id}_${erc20Contract.address}`] + ).toMatchObject({ + denom: erc20Contract.address, + amount: `-${amount.toString()}`, + }); + // to account balance had been reindexed + expect( + accountBalances[`${accounts[1].id}_${erc20Contract.address}`] + ).toMatchObject({ + denom: erc20Contract.address, + amount: amount.toString(), + }); + // from account balance without erc20 had been orinal + expect( + accountBalances[ + `${accounts[0].id}_${accounts[0].account_balances[0].denom}` + ] + ).toMatchObject({ + denom: accounts[0].account_balances[0].denom, + amount: accounts[0].account_balances[0].amount, + }); + } +}