diff --git a/src/services/evm/erc20.service.ts b/src/services/evm/erc20.service.ts index 2ca5dcb7b..a50edbd8e 100644 --- a/src/services/evm/erc20.service.ts +++ b/src/services/evm/erc20.service.ts @@ -1,4 +1,3 @@ -import { QueryModuleAccountByNameResponseSDKType } from '@aura-nw/aurajs/types/codegen/cosmos/auth/v1beta1/query'; import { Action, Service, @@ -9,7 +8,7 @@ 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, Config, getLcdClient } 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 } from '../../models'; @@ -28,8 +27,6 @@ const { NODE_ENV } = Config; export default class Erc20Service extends BullableService { viemClient!: PublicClient; - erc20ModuleAccount!: string; - public constructor(public broker: ServiceBroker) { super(broker); } @@ -352,17 +349,6 @@ export default class Erc20Service extends BullableService { public async _start(): Promise { this.viemClient = getViemClient(); if (NODE_ENV !== 'test') { - if (config.evmOnly === false) { - const lcdClient = await getLcdClient(); - const erc20Account: QueryModuleAccountByNameResponseSDKType = - await lcdClient.provider.cosmos.auth.v1beta1.moduleAccountByName({ - name: 'erc20', - }); - Erc20Handler.erc20ModuleAccount = - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - 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 09a81c5da..50b03a494 100644 --- a/src/services/evm/erc20_handler.ts +++ b/src/services/evm/erc20_handler.ts @@ -4,7 +4,13 @@ import Moleculer from 'moleculer'; import { decodeAbiParameters, keccak256, toHex } from 'viem'; import config from '../../../config.json' assert { type: 'json' }; import knex from '../../common/utils/db_connection'; -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'; @@ -56,14 +62,16 @@ export class Erc20Handler { erc20Activities: Erc20Activity[]; - static erc20ModuleAccount: any; + erc20Contracts: Dictionary; constructor( accountBalances: Dictionary, - erc20Activities: Erc20Activity[] + erc20Activities: Erc20Activity[], + erc20Contracts: Dictionary ) { this.accountBalances = accountBalances; this.erc20Activities = erc20Activities; + this.erc20Contracts = erc20Contracts; } process() { @@ -81,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) + 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 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, - }); + // update last updated height + erc20Contract.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` + ); } - } - // 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) @@ -124,7 +155,14 @@ 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( @@ -159,9 +197,6 @@ export class Erc20Handler { ); let erc20CosmosEvents: Event[] = []; if (config.evmOnly === false) { - if (!this.erc20ModuleAccount) { - throw new Error('erc20 module account undefined'); - } erc20CosmosEvents = await Event.query() .transacting(trx) .where('event.block_height', '>', startBlock) @@ -202,7 +237,6 @@ export class Erc20Handler { erc20CosmosEvents.forEach((event) => { const activity = Erc20Handler.buildTransferActivityByCosmos( event, - this.erc20ModuleAccount, logger ); if (activity) { @@ -271,9 +305,30 @@ export class Erc20Handler { ), (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, + 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 ); @@ -320,7 +375,6 @@ export class Erc20Handler { static buildTransferActivityByCosmos( e: Event, - erc20ModuleAccount: string, logger: Moleculer.LoggerInstance ): Erc20Activity | undefined { try { @@ -352,15 +406,9 @@ export class Erc20Handler { ); const sender = from; if (e.type === Event.EVENT_TYPE.CONVERT_COIN) { - from = convertBech32AddressToEthAddress( - config.networkPrefixAddress, - erc20ModuleAccount - ).toLowerCase(); + from = ZERO_ADDRESS; } else if (e.type === Event.EVENT_TYPE.CONVERT_ERC20) { - to = convertBech32AddressToEthAddress( - config.networkPrefixAddress, - erc20ModuleAccount - ).toLowerCase(); + to = ZERO_ADDRESS; } const amount = e.attributes.find( (attr) => attr.key === EventAttribute.ATTRIBUTE_KEY.AMOUNT diff --git a/test/unit/services/evm/erc20_handler.spec.ts b/test/unit/services/evm/erc20_handler.spec.ts index 2c816015a..53d711c31 100644 --- a/test/unit/services/evm/erc20_handler.spec.ts +++ b/test/unit/services/evm/erc20_handler.spec.ts @@ -29,7 +29,6 @@ import { ERC20_ACTION, Erc20Handler, } from '../../../../src/services/evm/erc20_handler'; -import { convertBech32AddressToEthAddress } from '../../../../src/services/evm/utils'; const evmTransaction = EVMTransaction.fromJson({ id: 2931, @@ -97,7 +96,6 @@ export default class Erc20HandlerTest { wrapSmartContract, ]); await Erc20Contract.query().insert([erc20Contract, erc20WrapContract]); - Erc20Handler.erc20ModuleAccount = erc20ModuleAccount; } @AfterAll() @@ -422,10 +420,7 @@ export default class Erc20HandlerTest { // test convert coin activity const convertCoinActivity = erc20Activitites[0]; expect(convertCoinActivity).toMatchObject({ - from: convertBech32AddressToEthAddress( - config.networkPrefixAddress, - erc20ModuleAccount - ).toLowerCase(), + from: ZERO_ADDRESS, to, amount, action: ERC20_ACTION.TRANSFER, @@ -436,10 +431,7 @@ export default class Erc20HandlerTest { const convertErc20Activity = erc20Activitites[1]; expect(convertErc20Activity).toMatchObject({ from, - to: convertBech32AddressToEthAddress( - config.networkPrefixAddress, - erc20ModuleAccount - ).toLowerCase(), + to: ZERO_ADDRESS, amount, action: ERC20_ACTION.TRANSFER, erc20_contract_address: erc20Contract.address, @@ -562,6 +554,7 @@ export default class Erc20HandlerTest { async testHandlerErc20Transfer() { const fromAmount = '4424242424'; const toAmount = '1123342'; + const totalSupply = '123654'; const erc20Activity = Erc20Activity.fromJson({ evm_event_id: 1, sender: '0x7c756Cba10Ff2C65016494E8BA37C12a108572b5', @@ -593,7 +586,19 @@ export default class Erc20HandlerTest { last_updated_height: 1, }), }; - const erc20Handler = new Erc20Handler(accountBalances, []); + 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, @@ -603,11 +608,15 @@ 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') async testHandlerErc20TransferWhenFromIsZero() { const toAmount = '242423234'; + const totalSupply = '123654'; const erc20Activity = Erc20Activity.fromJson({ evm_event_id: 1, sender: '0x7c756Cba10Ff2C65016494E8BA37C12a108572b5', @@ -623,6 +632,18 @@ export default class Erc20HandlerTest { from_account_id: 123, to_account_id: 234, }); + 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 [fromKey, toKey] = [ `${erc20Activity.from_account_id}_${erc20Activity.erc20_contract_address}`, `${erc20Activity.to_account_id}_${erc20Activity.erc20_contract_address}`, @@ -634,19 +655,77 @@ export default class Erc20HandlerTest { last_updated_height: 1, }), }; - const erc20Handler = new Erc20Handler(accountBalances, []); + 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') async testHandlerErc20TransferWhenNotHeight() { const fromAmount = '23442423'; const toAmount = '32323232'; + const totalSupply = '123456'; const erc20Activity = Erc20Activity.fromJson({ evm_event_id: 1, sender: '0x7c756Cba10Ff2C65016494E8BA37C12a108572b5', @@ -662,6 +741,18 @@ export default class Erc20HandlerTest { from_account_id: 123, to_account_id: 234, }); + 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 [fromKey, toKey] = [ `${erc20Activity.from_account_id}_${erc20Activity.erc20_contract_address}`, `${erc20Activity.to_account_id}_${erc20Activity.erc20_contract_address}`, @@ -678,15 +769,46 @@ export default class Erc20HandlerTest { last_updated_height: 1, }), }; - const erc20Handler = new Erc20Handler(accountBalances, []); - erc20Handler.handlerErc20Transfer(erc20Activity); - expect(erc20Handler.accountBalances[fromKey]).toMatchObject({ - denom: erc20Activity.erc20_contract_address, - amount: fromAmount, - }); - expect(erc20Handler.accountBalances[toKey]).toMatchObject({ - denom: erc20Activity.erc20_contract_address, - amount: (BigInt(erc20Activity.amount) + BigInt(toAmount)).toString(), + const erc20Handler = new Erc20Handler(accountBalances, [], erc20Contracts); + expect(() => erc20Handler.handlerErc20Transfer(erc20Activity)).toThrow( + `Process erc20 balance: fromAccountBalance ${erc20Activity.from} was updated` + ); + } + + @Test('test handlerErc20Transfer when from/to is erc20 module account') + async testHandlerErc20TransferWhenToIsErc20ModuleAccount() { + 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 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({}, [], erc20Contracts); + erc20Handler.handlerErc20Transfer(erc20Activity); + expect( + erc20Contracts[erc20Activity.erc20_contract_address].total_supply + ).toEqual((BigInt(totalSupply) - BigInt(erc20Activity.amount)).toString()); } } diff --git a/test/unit/services/evm/erc20_reindex.spec.ts b/test/unit/services/evm/erc20_reindex.spec.ts index 1f6c0fc21..ef92dbf97 100644 --- a/test/unit/services/evm/erc20_reindex.spec.ts +++ b/test/unit/services/evm/erc20_reindex.spec.ts @@ -13,10 +13,7 @@ import { EVMSmartContract, EVMTransaction, } from '../../../../src/models'; -import { - ABI_TRANSFER_PARAMS, - Erc20Handler, -} from '../../../../src/services/evm/erc20_handler'; +import { ABI_TRANSFER_PARAMS } from '../../../../src/services/evm/erc20_handler'; import { Erc20Reindexer } from '../../../../src/services/evm/erc20_reindex'; const accounts = [ @@ -148,7 +145,6 @@ export default class Erc20ReindexTest { @Test('test reindex') async testReindex() { const viemClient = getViemClient(); - Erc20Handler.erc20ModuleAccount = '0x0000000000000dfd'; jest.spyOn(viemClient, 'getBlockNumber').mockResolvedValue(BigInt(123456)); // Instantiate Erc20Reindexer with the mock const reindexer = new Erc20Reindexer(viemClient, this.broker.logger);