From 02a0f48d0bbf075239c2f6859f3ebf50fdcd363d Mon Sep 17 00:00:00 2001 From: phamphong9981 Date: Tue, 1 Oct 2024 10:30:21 +0700 Subject: [PATCH] feat: evm smart contract total transfer --- ...30084943_evm_smart_contract_total_tx_to.ts | 13 +++ src/models/evm_smart_contract.ts | 2 + src/services/evm/constant.ts | 1 + .../evm/crawl_contract_evm.service.ts | 78 +++++++++++++++ .../services/evm/crawl_contract_evm.spec.ts | 94 ++++++++++++++++--- 5 files changed, 173 insertions(+), 15 deletions(-) create mode 100644 migrations/evm/20240930084943_evm_smart_contract_total_tx_to.ts diff --git a/migrations/evm/20240930084943_evm_smart_contract_total_tx_to.ts b/migrations/evm/20240930084943_evm_smart_contract_total_tx_to.ts new file mode 100644 index 000000000..9782cb28b --- /dev/null +++ b/migrations/evm/20240930084943_evm_smart_contract_total_tx_to.ts @@ -0,0 +1,13 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable('evm_smart_contract', (table) => { + table.integer('total_tx_to').defaultTo(0).index(); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable('evm_smart_contract', (table) => { + table.dropColumn('total_tx_to'); + }); +} diff --git a/src/models/evm_smart_contract.ts b/src/models/evm_smart_contract.ts index 6dbc616b6..0efd95fce 100644 --- a/src/models/evm_smart_contract.ts +++ b/src/models/evm_smart_contract.ts @@ -29,6 +29,8 @@ export class EVMSmartContract extends BaseModel { last_updated_tx_id!: number; + total_tx_to!: number; + static get tableName() { return 'evm_smart_contract'; } diff --git a/src/services/evm/constant.ts b/src/services/evm/constant.ts index 5bf3b6203..ac7c0cf0b 100644 --- a/src/services/evm/constant.ts +++ b/src/services/evm/constant.ts @@ -295,6 +295,7 @@ export const BULL_JOB_NAME = { REINDEX_ERC20: 'reindex:erc20', REFRESH_ERC721_HOLDER_STATISTIC: 'refresh:erc721-holder-statistic', CRAWL_EVM_ACCOUNT_PUBKEY: 'crawl:evm-account-pubkey', + HANDLE_EVM_SMART_CONTRACT_TX: 'handle:evm-smart-contract-tx', }; export const MSG_TYPE = { diff --git a/src/services/evm/crawl_contract_evm.service.ts b/src/services/evm/crawl_contract_evm.service.ts index 3b2cc5171..46c1fa554 100644 --- a/src/services/evm/crawl_contract_evm.service.ts +++ b/src/services/evm/crawl_contract_evm.service.ts @@ -328,6 +328,70 @@ export default class CrawlSmartContractEVMService extends BullableService { }); } + @QueueHandler({ + queueName: BULL_JOB_NAME.HANDLE_EVM_SMART_CONTRACT_TX, + jobName: BULL_JOB_NAME.HANDLE_EVM_SMART_CONTRACT_TX, + }) + async handleEvmSmartContractTx() { + const [startBlock, endBlock, blockCheckpoint] = + await BlockCheckpoint.getCheckpoint( + BULL_JOB_NAME.HANDLE_EVM_SMART_CONTRACT_TX, + [BULL_JOB_NAME.CRAWL_SMART_CONTRACT_EVM], + config.crawlSmartContractEVM.key + ); + this.logger.info( + `Handle evm_smart_contract tx from block ${startBlock} to block ${endBlock}` + ); + if (startBlock >= endBlock) { + return; + } + const evmSmartContractTxs = await EVMTransaction.query() + .where('height', '>', startBlock) + .andWhere('height', '<=', endBlock) + .orderBy('height', 'asc') + .orderBy('index', 'asc'); + const toAddrs = _.uniq( + evmSmartContractTxs.filter((e) => e.to).map((e) => bytesToHex(e.to)) + ); + const evmSmartContractsState = _.keyBy( + await EVMSmartContract.query() + .whereIn('address', toAddrs) + .select('address', 'total_tx_to'), + 'address' + ); + evmSmartContractTxs.forEach((tx) => { + if (!tx.to) return; + const toAddr = bytesToHex(tx.to); + const evmSmartContractState = evmSmartContractsState[toAddr]; + if (evmSmartContractsState[toAddr]) { + evmSmartContractState.total_tx_to += 1; + } + }); + await knex.transaction(async (trx) => { + if (Object.keys(evmSmartContractsState).length > 0) { + const stringListUpdates = Object.keys(evmSmartContractsState) + .map( + (addr) => `('${addr}', ${evmSmartContractsState[addr].total_tx_to})` + ) + .join(','); + await knex + .raw( + `UPDATE evm_smart_contract SET total_tx_to = temp.total_tx_to from (VALUES ${stringListUpdates}) as temp(address, total_tx_to) where temp.address = evm_smart_contract.address` + ) + .transacting(trx); + } + if (blockCheckpoint) { + blockCheckpoint.height = endBlock; + await BlockCheckpoint.query() + .insert(blockCheckpoint) + .onConflict('job_name') + .merge() + .returning('id') + .transacting(trx); + } + }); + } + async getContractsProxyInfo( addrs: string[], bytecodes: _.Dictionary, @@ -452,6 +516,20 @@ export default class CrawlSmartContractEVMService extends BullableService { }, } ); + await this.createJob( + BULL_JOB_NAME.HANDLE_EVM_SMART_CONTRACT_TX, + BULL_JOB_NAME.HANDLE_EVM_SMART_CONTRACT_TX, + {}, + { + removeOnComplete: true, + removeOnFail: { + count: 3, + }, + repeat: { + every: config.crawlSmartContractEVM.millisecondCrawl, + }, + } + ); return super._start(); } } diff --git a/test/unit/services/evm/crawl_contract_evm.spec.ts b/test/unit/services/evm/crawl_contract_evm.spec.ts index e6ca58c84..008aac630 100644 --- a/test/unit/services/evm/crawl_contract_evm.spec.ts +++ b/test/unit/services/evm/crawl_contract_evm.spec.ts @@ -1,6 +1,12 @@ -import { AfterAll, BeforeAll, Describe, Test } from '@jest-decorated/core'; +import { + AfterAll, + BeforeAll, + BeforeEach, + Describe, + Test, +} from '@jest-decorated/core'; import { ServiceBroker } from 'moleculer'; -import CrawlSmartContractEVMService from '../../../../src/services/evm/crawl_contract_evm.service'; +import { hexToBytes } from 'viem'; import knex from '../../../../src/common/utils/db_connection'; import { BlockCheckpoint, @@ -8,7 +14,22 @@ import { EVMTransaction, EvmInternalTransaction, } from '../../../../src/models'; +import { BULL_JOB_NAME } from '../../../../src/services/evm/constant'; +import CrawlSmartContractEVMService from '../../../../src/services/evm/crawl_contract_evm.service'; +const evmSmartContract = EVMSmartContract.fromJson({ + id: 555, + address: '0xc57dc0ffa86aefbdd1b3f30e825fcae878a155f6', + creator: '0xDF587daaC47ae7B5586E34bCdb23d0b900b18a6C', + created_height: 100, + created_hash: + '0xae4b0793937b440f566ba5bdec4d3728a5c26cfc3233ca3a104ff0963841ac92', + type: EVMSmartContract.TYPES.ERC721, + code_hash: + '0xaf7378ea38b2c744796688746c4234e253647da6d8e7325a36f69c1ac0e53d2c', + total_tx_to: 25, + last_updated_tx_id: 5968, +}); @Describe('Test crawl contract evm') export default class CrawlContractEvmTest { broker = new ServiceBroker({ logger: false }); @@ -21,8 +42,12 @@ export default class CrawlContractEvmTest { async initSuite() { this.crawlContractEvmService.getQueueManager().stopAll(); await this.broker.start(); + } + + @BeforeEach() + async beforeEach() { await knex.raw( - 'TRUNCATE TABLE evm_smart_contract RESTART IDENTITY CASCADE' + 'TRUNCATE TABLE evm_smart_contract, block_checkpoint, evm_transaction RESTART IDENTITY CASCADE' ); } @@ -33,20 +58,59 @@ export default class CrawlContractEvmTest { jest.restoreAllMocks(); } + @Test.only('test handleEvmSmartContractTx') + async testHandleEvmSmartContractTx() { + const blockHeight = 1233; + const blockCheckpoints = [ + BlockCheckpoint.fromJson({ + job_name: BULL_JOB_NAME.HANDLE_EVM_SMART_CONTRACT_TX, + height: blockHeight - 1, + }), + BlockCheckpoint.fromJson({ + job_name: BULL_JOB_NAME.CRAWL_SMART_CONTRACT_EVM, + height: blockHeight, + }), + ]; + const evmTxs = [ + EVMTransaction.fromJson({ + height: blockHeight, + to: hexToBytes(evmSmartContract.address as `0x${string}`), + index: 0, + hash: '0xeb6835058be18f6a13be74af2d06abbca46cdbd305aa4d378144d520310e8411', + }), + EVMTransaction.fromJson({ + height: blockHeight, + to: hexToBytes(evmSmartContract.address as `0x${string}`), + index: 1, + hash: '0xeb6835058be18f6a13be74af2d06abbca46cdbd305aa4d378144d520310e8411', + }), + EVMTransaction.fromJson({ + height: blockHeight, + from: hexToBytes(evmSmartContract.address as `0x${string}`), + index: 2, + hash: '0xeb6835058be18f6a13be74af2d06abbca46cdbd305aa4d378144d520310e8411', + }), + EVMTransaction.fromJson({ + height: blockHeight, + to: hexToBytes(evmSmartContract.address as `0x${string}`), + index: 3, + hash: '0xeb6835058be18f6a13be74af2d06abbca46cdbd305aa4d378144d520310e8411', + }), + ]; + await BlockCheckpoint.query().insert(blockCheckpoints); + await EVMSmartContract.query().insert(evmSmartContract); + await EVMTransaction.query().insert(evmTxs); + await this.crawlContractEvmService.handleEvmSmartContractTx(); + const updatedEvmSmartContract = await EVMSmartContract.query() + .first() + .throwIfNotFound(); + expect(updatedEvmSmartContract.total_tx_to).toEqual( + evmSmartContract.total_tx_to + evmTxs.length - 1 + ); + } + @Test('test handleSelfDestruct') async testHandleSelfDestruct() { - const evmSmartContract = EVMSmartContract.fromJson({ - id: 555, - address: '0xC57dC0FFa86AEFbdD1b3F30E825fcAE878A155F6', - creator: '0xDF587daaC47ae7B5586E34bCdb23d0b900b18a6C', - created_height: 100, - created_hash: - '0xae4b0793937b440f566ba5bdec4d3728a5c26cfc3233ca3a104ff0963841ac92', - type: EVMSmartContract.TYPES.ERC721, - code_hash: - '0xaf7378ea38b2c744796688746c4234e253647da6d8e7325a36f69c1ac0e53d2c', - last_updated_tx_id: 5968, - }); await EVMSmartContract.query().insert(evmSmartContract); jest.spyOn(BlockCheckpoint, 'getCheckpoint').mockResolvedValue([ 1,