From f276b3c515fea8a024342f9c41ecc0efecb6109f Mon Sep 17 00:00:00 2001 From: phamphong9981 Date: Fri, 12 Jul 2024 15:22:05 +0700 Subject: [PATCH 1/3] feat: update erc20 total supply --- src/services/evm/erc20.service.ts | 25 ++++++++++++++++++- src/services/evm/erc20_handler.ts | 40 ++++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/services/evm/erc20.service.ts b/src/services/evm/erc20.service.ts index b02d28d29..cb23863d0 100644 --- a/src/services/evm/erc20.service.ts +++ b/src/services/evm/erc20.service.ts @@ -252,8 +252,21 @@ export default class Erc20Service extends BullableService { ), (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); + const erc20Handler = new Erc20Handler( + accountBalances, + erc20Contracts, + erc20Activities + ); erc20Handler.process(); const updatedAccountBalances = Object.values( erc20Handler.accountBalances @@ -265,6 +278,16 @@ export default class Erc20Service extends BullableService { .onConflict(['account_id', 'denom']) .merge(); } + const updatedErc20Contracts = Object.values( + erc20Handler.erc20Contracts + ); + if (updatedErc20Contracts.length > 0) { + await Erc20Contract.query() + .transacting(trx) + .insert(updatedErc20Contracts) + .onConflict(['id']) + .merge(); + } } updateBlockCheckpoint.height = endBlock; await BlockCheckpoint.query() diff --git a/src/services/evm/erc20_handler.ts b/src/services/evm/erc20_handler.ts index c17251997..06dfe8a0d 100644 --- a/src/services/evm/erc20_handler.ts +++ b/src/services/evm/erc20_handler.ts @@ -1,7 +1,13 @@ import { Dictionary } from 'lodash'; import Moleculer from 'moleculer'; import { decodeAbiParameters, keccak256, toHex } from 'viem'; -import { Erc20Activity, Event, EventAttribute, EvmEvent } from '../../models'; +import { + Erc20Activity, + Erc20Contract, + Event, + EventAttribute, + EvmEvent, +} from '../../models'; import { AccountBalance } from '../../models/account_balance'; import { ZERO_ADDRESS } from './constant'; import { convertBech32AddressToEthAddress } from './utils'; @@ -54,12 +60,16 @@ export class Erc20Handler { erc20Activities: Erc20Activity[]; + erc20Contracts: Dictionary; + constructor( accountBalances: Dictionary, + erc20Contracts: Dictionary, erc20Activities: Erc20Activity[] ) { this.accountBalances = accountBalances; this.erc20Activities = erc20Activities; + this.erc20Contracts = erc20Contracts; } process() { @@ -99,6 +109,20 @@ export class Erc20Handler { type: AccountBalance.TYPE.ERC20_TOKEN, }); } + } else { + const erc20Contract: Erc20Contract = + this.erc20Contracts[erc20Activity.erc20_contract_address]; + if ( + erc20Contract && + erc20Contract.last_updated_height <= erc20Activity.height + ) { + // update total supply + erc20Contract.total_supply = ( + BigInt(erc20Contract.total_supply || 0) + BigInt(erc20Activity.amount) + ).toString(); + // update last updated height + erc20Contract.last_updated_height = erc20Activity.height; + } } // update to account balance const toAccountId = erc20Activity.to_account_id; @@ -120,6 +144,20 @@ export class Erc20Handler { account_id: toAccountId, type: AccountBalance.TYPE.ERC20_TOKEN, }); + } else { + const erc20Contract: Erc20Contract = + this.erc20Contracts[erc20Activity.erc20_contract_address]; + if ( + erc20Contract && + erc20Contract.last_updated_height <= erc20Activity.height + ) { + // update total supply + erc20Contract.total_supply = ( + BigInt(erc20Contract.total_supply || 0) - BigInt(erc20Activity.amount) + ).toString(); + // update last updated height + erc20Contract.last_updated_height = erc20Activity.height; + } } } From a97b3352f94b3028cd5a8434b76b5f9dc22e02d6 Mon Sep 17 00:00:00 2001 From: phamphong9981 Date: Fri, 19 Jul 2024 14:18:58 +0700 Subject: [PATCH 2/3] test: test --- src/services/evm/erc20_handler.ts | 43 ++++---- test/unit/services/evm/erc20_handler.spec.ts | 110 ++++++++++++++++++- 2 files changed, 129 insertions(+), 24 deletions(-) diff --git a/src/services/evm/erc20_handler.ts b/src/services/evm/erc20_handler.ts index 06dfe8a0d..a97dabdb8 100644 --- a/src/services/evm/erc20_handler.ts +++ b/src/services/evm/erc20_handler.ts @@ -124,26 +124,29 @@ export class Erc20Handler { 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 - ) { - // calculate new balance: increase balance of to account - const amount = ( - BigInt(toAccountBalance?.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: toAccountId, - type: AccountBalance.TYPE.ERC20_TOKEN, - }); + // update from account balance if from != 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 || + toAccountBalance.last_updated_height <= erc20Activity.height + ) { + // calculate new balance: increase balance of to account + const amount = ( + BigInt(toAccountBalance?.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: toAccountId, + type: AccountBalance.TYPE.ERC20_TOKEN, + }); + } } else { const erc20Contract: Erc20Contract = this.erc20Contracts[erc20Activity.erc20_contract_address]; diff --git a/test/unit/services/evm/erc20_handler.spec.ts b/test/unit/services/evm/erc20_handler.spec.ts index 5eae8e8b6..bb34d7672 100644 --- a/test/unit/services/evm/erc20_handler.spec.ts +++ b/test/unit/services/evm/erc20_handler.spec.ts @@ -3,7 +3,7 @@ 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 { Erc20Activity, Erc20Contract, EvmEvent } from '../../../../src/models'; import { ABI_APPROVAL_PARAMS, ABI_TRANSFER_PARAMS, @@ -176,7 +176,20 @@ export default class Erc20HandlerTest { last_updated_height: 1, }), }; - const erc20Handler = new Erc20Handler(accountBalances, []); + const totalSupply = '123654'; + const erc20Contracts = { + [erc20Activity.erc20_contract_address]: Erc20Contract.fromJson({ + evm_smart_contract_id: 1, + total_supply: totalSupply, + symbol: 'ALPHA', + address: erc20Activity.erc20_contract_address, + decimal: '20', + name: 'Alpha Grand Wolf', + track: true, + last_updated_height: 1, + }), + }; + const erc20Handler = new Erc20Handler(accountBalances, erc20Contracts, []); erc20Handler.handlerErc20Transfer(erc20Activity); expect(erc20Handler.accountBalances[fromKey]).toMatchObject({ denom: erc20Activity.erc20_contract_address, @@ -186,6 +199,9 @@ export default class Erc20HandlerTest { denom: erc20Activity.erc20_contract_address, amount: (BigInt(erc20Activity.amount) + BigInt(toAmount)).toString(), }); + expect( + erc20Contracts[erc20Activity.erc20_contract_address].total_supply + ).toEqual(totalSupply); } @Test('test handlerErc20Transfer when from is zero') @@ -217,13 +233,83 @@ export default class Erc20HandlerTest { last_updated_height: 1, }), }; - const erc20Handler = new Erc20Handler(accountBalances, []); + const totalSupply = '123654'; + const erc20Contracts = { + [erc20Activity.erc20_contract_address]: Erc20Contract.fromJson({ + evm_smart_contract_id: 1, + total_supply: totalSupply, + symbol: 'ALPHA', + address: erc20Activity.erc20_contract_address, + decimal: '20', + name: 'Alpha Grand Wolf', + track: true, + last_updated_height: 1, + }), + }; + const erc20Handler = new Erc20Handler(accountBalances, erc20Contracts, []); erc20Handler.handlerErc20Transfer(erc20Activity); expect(erc20Handler.accountBalances[fromKey]).toBeUndefined(); expect(erc20Handler.accountBalances[toKey]).toMatchObject({ denom: erc20Activity.erc20_contract_address, amount: (BigInt(erc20Activity.amount) + BigInt(toAmount)).toString(), }); + expect( + erc20Contracts[erc20Activity.erc20_contract_address].total_supply + ).toEqual((BigInt(totalSupply) + BigInt(erc20Activity.amount)).toString()); + } + + @Test('test handlerErc20Transfer when to is zero') + async testHandlerErc20TransferWhenToIsZero() { + const balance = '242423234'; + const erc20Activity = Erc20Activity.fromJson({ + evm_event_id: 1, + sender: '0x7c756Cba10Ff2C65016494E8BA37C12a108572b5', + action: ERC20_ACTION.TRANSFER, + erc20_contract_address: '0x98605ae21dd3be686337a6d7a8f156d0d8baee92', + amount: '12345222', + from: '0xD83E708D7FE0E769Af80d990f9241458734808Ac', + to: ZERO_ADDRESS, + height: 10000, + tx_hash: + '0xb97228e533e3af1323d873c9c3e4c0a9b85d95ecd8e98110c8890c9453d2f077', + 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: balance, + last_updated_height: 1, + }), + }; + const totalSupply = '123654'; + const erc20Contracts = { + [erc20Activity.erc20_contract_address]: Erc20Contract.fromJson({ + evm_smart_contract_id: 1, + total_supply: totalSupply, + symbol: 'ALPHA', + address: erc20Activity.erc20_contract_address, + decimal: '20', + name: 'Alpha Grand Wolf', + track: true, + last_updated_height: 1, + }), + }; + const erc20Handler = new Erc20Handler(accountBalances, erc20Contracts, []); + erc20Handler.handlerErc20Transfer(erc20Activity); + expect(erc20Handler.accountBalances[fromKey]).toMatchObject({ + denom: erc20Activity.erc20_contract_address, + amount: (BigInt(balance) - BigInt(erc20Activity.amount)).toString(), + }); + expect(erc20Handler.accountBalances[toKey]).toBeUndefined(); + expect( + erc20Contracts[erc20Activity.erc20_contract_address].total_supply + ).toEqual((BigInt(totalSupply) - BigInt(erc20Activity.amount)).toString()); } @Test('test handlerErc20Transfer when last_updated_height not suitable') @@ -261,7 +347,20 @@ export default class Erc20HandlerTest { last_updated_height: 1, }), }; - const erc20Handler = new Erc20Handler(accountBalances, []); + const totalSupply = '123654'; + const erc20Contracts = { + [erc20Activity.erc20_contract_address]: Erc20Contract.fromJson({ + evm_smart_contract_id: 1, + total_supply: totalSupply, + symbol: 'ALPHA', + address: erc20Activity.erc20_contract_address, + decimal: '20', + name: 'Alpha Grand Wolf', + track: true, + last_updated_height: 1, + }), + }; + const erc20Handler = new Erc20Handler(accountBalances, erc20Contracts, []); erc20Handler.handlerErc20Transfer(erc20Activity); expect(erc20Handler.accountBalances[fromKey]).toMatchObject({ denom: erc20Activity.erc20_contract_address, @@ -271,5 +370,8 @@ export default class Erc20HandlerTest { denom: erc20Activity.erc20_contract_address, amount: (BigInt(erc20Activity.amount) + BigInt(toAmount)).toString(), }); + expect( + erc20Contracts[erc20Activity.erc20_contract_address].total_supply + ).toEqual(totalSupply); } } From 05f296d47072183267896810bbcc04edc10e6715 Mon Sep 17 00:00:00 2001 From: phamphong9981 Date: Fri, 19 Jul 2024 15:05:19 +0700 Subject: [PATCH 3/3] feat: factory accounts --- src/services/evm/erc20.service.ts | 36 +++++++++++++------- src/services/evm/erc20_handler.ts | 14 +++++--- test/unit/services/evm/erc20_handler.spec.ts | 28 ++++++++++++--- 3 files changed, 57 insertions(+), 21 deletions(-) diff --git a/src/services/evm/erc20.service.ts b/src/services/evm/erc20.service.ts index 1560818e7..0917ccb3f 100644 --- a/src/services/evm/erc20.service.ts +++ b/src/services/evm/erc20.service.ts @@ -21,7 +21,12 @@ import { import { AccountBalance } from '../../models/account_balance'; import { Erc20Activity } from '../../models/erc20_activity'; import { Erc20Contract } from '../../models/erc20_contract'; -import { BULL_JOB_NAME, SERVICE as EVM_SERVICE, SERVICE } from './constant'; +import { + BULL_JOB_NAME, + SERVICE as EVM_SERVICE, + SERVICE, + ZERO_ADDRESS, +} from './constant'; import { ERC20_EVENT_TOPIC0, Erc20Handler } from './erc20_handler'; import { convertEthAddressToBech32Address } from './utils'; @@ -117,16 +122,6 @@ export default class Erc20Service extends BullableService { ); let erc20CosmosEvents: Event[] = []; if (config.evmOnly === false) { - if (!this.erc20ModuleAccount) { - const lcdClient = await getLcdClient(); - const erc20Account: QueryModuleAccountByNameResponseSDKType = - await lcdClient.provider.cosmos.auth.v1beta1.moduleAccountByName({ - name: 'erc20', - }); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - this.erc20ModuleAccount = erc20Account.account.base_account.address; - } erc20CosmosEvents = await Event.query() .where('block_height', '>', startBlock) .andWhere('block_height', '<=', endBlock) @@ -262,11 +257,16 @@ export default class Erc20Service extends BullableService { ), 'address' ); + const factoryAccounts = [ZERO_ADDRESS]; + if (this.erc20ModuleAccount) { + factoryAccounts.push(this.erc20ModuleAccount); + } // construct cw721 handler object const erc20Handler = new Erc20Handler( accountBalances, erc20Contracts, - erc20Activities + erc20Activities, + factoryAccounts ); erc20Handler.process(); const updatedAccountBalances = Object.values( @@ -465,6 +465,18 @@ export default class Erc20Service extends BullableService { public async _start(): Promise { this.viemClient = getViemClient(); if (NODE_ENV !== 'test') { + if (config.evmOnly === false) { + if (!this.erc20ModuleAccount) { + const lcdClient = await getLcdClient(); + const erc20Account: QueryModuleAccountByNameResponseSDKType = + await lcdClient.provider.cosmos.auth.v1beta1.moduleAccountByName({ + name: 'erc20', + }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.erc20ModuleAccount = erc20Account.account.base_account.address; + } + } await this.createJob( BULL_JOB_NAME.HANDLE_ERC20_CONTRACT, BULL_JOB_NAME.HANDLE_ERC20_CONTRACT, diff --git a/src/services/evm/erc20_handler.ts b/src/services/evm/erc20_handler.ts index a97dabdb8..91169e9b3 100644 --- a/src/services/evm/erc20_handler.ts +++ b/src/services/evm/erc20_handler.ts @@ -62,14 +62,18 @@ export class Erc20Handler { erc20Contracts: Dictionary; + factoryAccounts: string[]; + constructor( accountBalances: Dictionary, erc20Contracts: Dictionary, - erc20Activities: Erc20Activity[] + erc20Activities: Erc20Activity[], + factoryAccounts: string[] ) { this.accountBalances = accountBalances; this.erc20Activities = erc20Activities; this.erc20Contracts = erc20Contracts; + this.factoryAccounts = factoryAccounts; } process() { @@ -87,8 +91,8 @@ export class Erc20Handler { } handlerErc20Transfer(erc20Activity: Erc20Activity) { - // update from account balance if from != ZERO_ADDRESS - if (erc20Activity.from !== ZERO_ADDRESS) { + // update from account balance if from != factory account + if (!this.factoryAccounts.includes(erc20Activity.from)) { const fromAccountId = erc20Activity.from_account_id; const key = `${fromAccountId}_${erc20Activity.erc20_contract_address}`; const fromAccountBalance = this.accountBalances[key]; @@ -124,8 +128,8 @@ export class Erc20Handler { erc20Contract.last_updated_height = erc20Activity.height; } } - // update from account balance if from != ZERO_ADDRESS - if (erc20Activity.to !== ZERO_ADDRESS) { + // update from account balance if to != factory account + if (!this.factoryAccounts.includes(erc20Activity.to)) { // update to account balance const toAccountId = erc20Activity.to_account_id; const key = `${toAccountId}_${erc20Activity.erc20_contract_address}`; diff --git a/test/unit/services/evm/erc20_handler.spec.ts b/test/unit/services/evm/erc20_handler.spec.ts index bb34d7672..f5a42471d 100644 --- a/test/unit/services/evm/erc20_handler.spec.ts +++ b/test/unit/services/evm/erc20_handler.spec.ts @@ -189,7 +189,12 @@ export default class Erc20HandlerTest { last_updated_height: 1, }), }; - const erc20Handler = new Erc20Handler(accountBalances, erc20Contracts, []); + const erc20Handler = new Erc20Handler( + accountBalances, + erc20Contracts, + [], + [ZERO_ADDRESS] + ); erc20Handler.handlerErc20Transfer(erc20Activity); expect(erc20Handler.accountBalances[fromKey]).toMatchObject({ denom: erc20Activity.erc20_contract_address, @@ -246,7 +251,12 @@ export default class Erc20HandlerTest { last_updated_height: 1, }), }; - const erc20Handler = new Erc20Handler(accountBalances, erc20Contracts, []); + const erc20Handler = new Erc20Handler( + accountBalances, + erc20Contracts, + [], + [ZERO_ADDRESS] + ); erc20Handler.handlerErc20Transfer(erc20Activity); expect(erc20Handler.accountBalances[fromKey]).toBeUndefined(); expect(erc20Handler.accountBalances[toKey]).toMatchObject({ @@ -300,7 +310,12 @@ export default class Erc20HandlerTest { last_updated_height: 1, }), }; - const erc20Handler = new Erc20Handler(accountBalances, erc20Contracts, []); + const erc20Handler = new Erc20Handler( + accountBalances, + erc20Contracts, + [], + [ZERO_ADDRESS] + ); erc20Handler.handlerErc20Transfer(erc20Activity); expect(erc20Handler.accountBalances[fromKey]).toMatchObject({ denom: erc20Activity.erc20_contract_address, @@ -360,7 +375,12 @@ export default class Erc20HandlerTest { last_updated_height: 1, }), }; - const erc20Handler = new Erc20Handler(accountBalances, erc20Contracts, []); + const erc20Handler = new Erc20Handler( + accountBalances, + erc20Contracts, + [], + [ZERO_ADDRESS] + ); erc20Handler.handlerErc20Transfer(erc20Activity); expect(erc20Handler.accountBalances[fromKey]).toMatchObject({ denom: erc20Activity.erc20_contract_address,