diff --git a/ci/config.json.ci b/ci/config.json.ci index b418ca70e..7740c5bc7 100644 --- a/ci/config.json.ci +++ b/ci/config.json.ci @@ -5,6 +5,7 @@ "consensusPrefixAddress": "valcons", "validatorPrefixAddress": "valoper", "networkDenom": "utaura", + "evmOnly": false, "jobRetryAttempt": 5, "jobRetryBackoff": 1000, "crawlValidator": { @@ -371,7 +372,8 @@ "key": "erc20", "blocksPerCall": 100, "millisecondRepeatJob": 5000, - "chunkSizeInsert": 1000 + "chunkSizeInsert": 1000, + "wrapExtensionContract": [] }, "erc721": { "key": "erc721", diff --git a/config.json b/config.json index 07682658e..6f58d8603 100644 --- a/config.json +++ b/config.json @@ -4,6 +4,7 @@ "networkPrefixAddress": "aura", "consensusPrefixAddress": "valcons", "validatorPrefixAddress": "valoper", + "evmOnly": false, "networkDenom": "utaura", "jobRetryAttempt": 5, "jobRetryBackoff": 1000, @@ -375,7 +376,8 @@ "key": "erc20", "blocksPerCall": 100, "millisecondRepeatJob": 2000, - "chunkSizeInsert": 1000 + "chunkSizeInsert": 1000, + "wrapExtensionContract": [""] }, "erc721": { "key": "erc721", 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 0f0717e22..1e4c7a6e1 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/evm/erc20.service.ts b/src/services/evm/erc20.service.ts index 2e2dfa549..d4e2091a7 100644 --- a/src/services/evm/erc20.service.ts +++ b/src/services/evm/erc20.service.ts @@ -1,269 +1,267 @@ -import { - Action, - Service, -} from '@ourparentcenter/moleculer-decorators-extended'; import { Knex } from 'knex'; -import _ from 'lodash'; -import { Context, ServiceBroker } from 'moleculer'; -import { PublicClient, getContract } from 'viem'; +import _, { Dictionary } from 'lodash'; +import Moleculer from 'moleculer'; +import { decodeAbiParameters, keccak256, toHex } 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 knex from '../../common/utils/db_connection'; -import { getViemClient } from '../../common/utils/etherjs_client'; -import { BlockCheckpoint, EVMSmartContract, EvmEvent } from '../../models'; +import { + Erc20Activity, + Erc20Contract, + Event, + EventAttribute, + EvmEvent, +} from '../../models'; 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 { ERC20_EVENT_TOPIC0, Erc20Handler } from './erc20_handler'; -import { convertEthAddressToBech32Address } from './utils'; +import { ZERO_ADDRESS } from './constant'; +import { convertBech32AddressToEthAddress } from './utils'; + +export const ERC20_ACTION = { + TRANSFER: 'transfer', + APPROVAL: 'approval', + DEPOSIT: 'deposit', + WITHDRAWAL: 'withdrawal', +}; +export const ABI_TRANSFER_PARAMS = { + FROM: { + name: 'from', + type: 'address', + }, + TO: { + name: 'to', + type: 'address', + }, + VALUE: { + name: 'value', + type: 'uint256', + }, +}; +export const ABI_APPROVAL_PARAMS = { + OWNER: { + name: 'owner', + type: 'address', + }, + SPENDER: { + name: 'spender', + type: 'address', + }, + VALUE: { + name: 'value', + type: 'uint256', + }, +}; +export const ERC20_EVENT_TOPIC0 = { + TRANSFER: keccak256(toHex('Transfer(address,address,uint256)')), + APPROVAL: keccak256(toHex('Approval(address,address,uint256)')), + DEPOSIT: keccak256(toHex('Deposit(address,uint256)')), + WITHDRAWAL: keccak256(toHex('Withdrawal(address,uint256)')), +}; +export class Erc20Handler { + // key: {accountId}_{erc20ContractAddress} + // value: accountBalance with account_id -> accountId, denom -> erc20ContractAddress + accountBalances: Dictionary; -@Service({ - name: EVM_SERVICE.V1.Erc20.key, - version: 1, -}) -export default class Erc20Service extends BullableService { - viemClient!: PublicClient; + erc20Activities: Erc20Activity[]; - public constructor(public broker: ServiceBroker) { - super(broker); + erc20Contracts: Dictionary; + + constructor( + accountBalances: Dictionary, + erc20Activities: Erc20Activity[], + erc20Contracts: Dictionary + ) { + this.accountBalances = accountBalances; + this.erc20Activities = erc20Activities; + this.erc20Contracts = erc20Contracts; } - @QueueHandler({ - queueName: BULL_JOB_NAME.HANDLE_ERC20_CONTRACT, - jobName: BULL_JOB_NAME.HANDLE_ERC20_CONTRACT, - }) - async handleErc20Contract(): Promise { - await knex.transaction(async (trx) => { - // get id evm smart contract checkpoint - // get range blocks for proccessing - const [startBlock, endBlock, updateBlockCheckpoint] = - await BlockCheckpoint.getCheckpoint( - BULL_JOB_NAME.HANDLE_ERC20_CONTRACT, - [BULL_JOB_NAME.CRAWL_SMART_CONTRACT_EVM], - config.erc20.key - ); - const erc20SmartContracts = await EVMSmartContract.query() - .transacting(trx) - .where('created_height', '>', startBlock) - .andWhere('created_height', '<=', endBlock) - .andWhere('type', EVMSmartContract.TYPES.ERC20) - .orderBy('id', 'asc'); - if (erc20SmartContracts.length > 0) { - const erc20Instances = await this.getErc20Instances( - erc20SmartContracts - ); - this.logger.info( - `Crawl Erc20 contract from block ${startBlock} to block ${endBlock}:\n ${JSON.stringify( - erc20Instances - )}` - ); - await Erc20Contract.query() - .transacting(trx) - .insert(erc20Instances) - .onConflict(['address']) - .merge(); + process() { + this.erc20Activities.forEach((erc20Activity) => { + if ( + [ + ERC20_ACTION.TRANSFER, + ERC20_ACTION.DEPOSIT, + ERC20_ACTION.WITHDRAWAL, + ].includes(erc20Activity.action) + ) { + this.handlerErc20Transfer(erc20Activity); } - updateBlockCheckpoint.height = endBlock; - await BlockCheckpoint.query() - .insert(updateBlockCheckpoint) - .onConflict('job_name') - .merge() - .transacting(trx); }); } - @QueueHandler({ - queueName: BULL_JOB_NAME.HANDLE_ERC20_ACTIVITY, - jobName: BULL_JOB_NAME.HANDLE_ERC20_ACTIVITY, - }) - async handleErc20Activity(): Promise { - await knex.transaction(async (trx) => { - const [startBlock, endBlock, updateBlockCheckpoint] = - await BlockCheckpoint.getCheckpoint( - BULL_JOB_NAME.HANDLE_ERC20_ACTIVITY, - [ - BULL_JOB_NAME.HANDLE_ERC20_CONTRACT, - BULL_JOB_NAME.HANDLE_EVM_PROXY_HISTORY, - ], - 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' + 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 && + erc20Activity.height < fromAccountBalance.last_updated_height + ) { + throw new Error( + `Process erc20 balance: fromAccountBalance ${erc20Activity.from} was updated` ); - await this.handleMissingErc20Contract(erc20Events, trx); - const erc20Activities: Erc20Activity[] = []; - erc20Events.forEach((e) => { - if (e.topic0 === ERC20_EVENT_TOPIC0.TRANSFER) { - const activity = Erc20Handler.buildTransferActivity(e); - if (activity) { - erc20Activities.push(activity); - } - } else if (e.topic0 === ERC20_EVENT_TOPIC0.APPROVAL) { - const activity = Erc20Handler.buildApprovalActivity(e); - if (activity) { - erc20Activities.push(activity); - } - } + } + // 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, }); - if (erc20Activities.length > 0) { - this.logger.info( - `Crawl Erc20 activity from block ${startBlock} to block ${endBlock}:\n ${JSON.stringify( - erc20Activities - )}` + } 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 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` ); - await knex - .batchInsert( - 'erc20_activity', - erc20Activities, - config.erc20.chunkSizeInsert - ) - .transacting(trx); } - updateBlockCheckpoint.height = endBlock; - await BlockCheckpoint.query() - .insert(updateBlockCheckpoint) - .onConflict('job_name') - .merge() - .transacting(trx); - }); + // 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 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; + } } - @QueueHandler({ - queueName: BULL_JOB_NAME.HANDLE_ERC20_BALANCE, - jobName: BULL_JOB_NAME.HANDLE_ERC20_BALANCE, - }) - async handleErc20Balance(): Promise { - const [startBlock, endBlock, updateBlockCheckpoint] = - await BlockCheckpoint.getCheckpoint( - BULL_JOB_NAME.HANDLE_ERC20_BALANCE, - [BULL_JOB_NAME.HANDLE_ERC20_ACTIVITY], - config.erc20.key - ); - // get Erc20 activities - let erc20Activities = await this.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) - ) + 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' ) - ); - if (missingAccountsAddress.length > 0) { - // crawl missing Account and requery erc20Activities - await this.broker.call( - COSMOS_SERVICE.V1.HandleAddressService.CrawlNewAccountApi.path, - { - addresses: missingAccountsAddress, + .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' ); - erc20Activities = await this.getErc20Activities(startBlock, endBlock); + 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'); } - 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(); + 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); } } - updateBlockCheckpoint.height = endBlock; - await BlockCheckpoint.query() - .insert(updateBlockCheckpoint) - .onConflict('job_name') - .merge() - .transacting(trx); }); - } - - @Action({ - name: SERVICE.V1.Erc20.insertNewErc20Contracts.key, - params: { - evmSmartContracts: 'any[]', - }, - }) - async insertNewErc20Contracts( - ctx: Context<{ - evmSmartContracts: { - id: number; - address: string; - }[]; - }> - ) { - const { evmSmartContracts } = ctx.params; - if (evmSmartContracts.length > 0) { - const currentHeight = await this.viemClient.getBlockNumber(); - const erc20Instances = await this.getErc20Instances( - evmSmartContracts.map((e) => - EVMSmartContract.fromJson({ - ...e, - created_height: currentHeight.toString(), - }) - ) - ); - this.logger.info( - `New Erc20 Instances:\n ${JSON.stringify(erc20Instances)}` + erc20CosmosEvents.forEach((event) => { + const activity = Erc20Handler.buildTransferActivityByCosmos( + event, + logger ); - await Erc20Contract.query() - .insert(erc20Instances) - .onConflict(['address']) - .merge(); - } + if (activity) { + erc20Activities.push(activity); + } + }); + return _.sortBy(erc20Activities, 'cosmos_tx_id'); } - async getErc20Activities( + static async getErc20Activities( startBlock: number, - endBlock: 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', @@ -290,132 +288,255 @@ export default class Erc20Service extends BullableService { .orderBy('erc20_activity.id'); } - async handleMissingErc20Contract(events: EvmEvent[], trx: Knex.Transaction) { - const eventsUniqByAddress = _.keyBy(events, (e) => e.address); - const addresses = Object.keys(eventsUniqByAddress); - const erc20ContractsByAddress = _.keyBy( - await Erc20Contract.query() - .whereIn('address', addresses) - .transacting(trx), - (e) => e.address - ); - const missingErc20ContractsAddress: string[] = addresses.filter( - (addr) => !erc20ContractsByAddress[addr] - ); - if (missingErc20ContractsAddress.length > 0) { - const erc20ContractsInfo = await this.getBatchErc20Info( - missingErc20ContractsAddress as `0x${string}`[] + 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 ); - await Erc20Contract.query() - .insert( - missingErc20ContractsAddress.map((addr, index) => - Erc20Contract.fromJson({ - evm_smart_contract_id: - eventsUniqByAddress[addr].evm_smart_contract_id, - address: addr, - total_supply: erc20ContractsInfo[index].totalSupply, - symbol: erc20ContractsInfo[index].symbol, - decimal: erc20ContractsInfo[index].decimals, - name: erc20ContractsInfo[index].name, - track: false, - last_updated_height: -1, - }) - ) - ) - .transacting(trx); + 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(); + } } } - async getErc20Instances(evmSmartContracts: EVMSmartContract[]) { - const addresses = evmSmartContracts.map((e) => e.address); - const erc20ContractsInfo = await this.getBatchErc20Info( - addresses as `0x${string}`[] - ); - return evmSmartContracts.map((e, index) => - Erc20Contract.fromJson({ - evm_smart_contract_id: evmSmartContracts[index].id, - address: e.address, - total_supply: erc20ContractsInfo[index].totalSupply, - symbol: erc20ContractsInfo[index].symbol, - decimal: erc20ContractsInfo[index].decimals, - name: erc20ContractsInfo[index].name, - track: true, - last_updated_height: e.created_height, - }) - ); + static buildTransferActivity( + e: EvmEvent, + logger: Moleculer.LoggerInstance + ): Erc20Activity | undefined { + try { + 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}` + ) as [string, string, bigint]; + return Erc20Activity.fromJson({ + evm_event_id: e.id, + sender: e.sender, + action: ERC20_ACTION.TRANSFER, + erc20_contract_address: e.address, + amount: amount.toString(), + from: from.toLowerCase(), + to: to.toLowerCase(), + 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; + } } - async getBatchErc20Info(addresses: `0x${string}`[]) { - const contracts = addresses.map((address) => - getContract({ - address, - abi: Erc20Contract.ABI, - client: this.viemClient, - }) - ); - const batchReqs: any[] = []; - contracts.forEach((e) => { - batchReqs.push( - e.read.name().catch(() => Promise.resolve(undefined)), - e.read.symbol().catch(() => Promise.resolve(undefined)), - e.read.decimals().catch(() => Promise.resolve(undefined)), - e.read.totalSupply().catch(() => Promise.resolve(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 ); - }); - const results = await Promise.all(batchReqs); - return addresses.map((address, index) => ({ - address, - name: results[4 * index], - symbol: results[4 * index + 1], - decimals: results[4 * index + 2], - totalSupply: results[4 * index + 3]?.toString(), - })); + 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); + return undefined; + } } - 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, - }, - } - ); - return super._start(); + static buildApprovalActivity( + e: EvmEvent, + logger: Moleculer.LoggerInstance + ): Erc20Activity | undefined { + try { + const [from, to, amount] = decodeAbiParameters( + [ + ABI_APPROVAL_PARAMS.OWNER, + ABI_APPROVAL_PARAMS.SPENDER, + ABI_APPROVAL_PARAMS.VALUE, + ], + (e.topic1 + e.topic2.slice(2) + toHex(e.data).slice(2)) as `0x${string}` + ) as [string, string, bigint]; + return Erc20Activity.fromJson({ + evm_event_id: e.id, + sender: e.sender, + action: ERC20_ACTION.APPROVAL, + erc20_contract_address: e.address, + amount: amount.toString(), + from: from.toLowerCase(), + to: to.toLowerCase(), + 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 buildWrapExtensionActivity( + e: EvmEvent, + logger: Moleculer.LoggerInstance + ): Erc20Activity | undefined { + if (e.topic0 === ERC20_EVENT_TOPIC0.DEPOSIT) { + const activity = Erc20Handler.buildWrapDepositActivity(e, logger); + return activity; + } + if (e.topic0 === ERC20_EVENT_TOPIC0.WITHDRAWAL) { + const activity = Erc20Handler.buildWrapWithdrawalActivity(e, logger); + return activity; + } + return undefined; + } + + private static buildWrapDepositActivity( + e: EvmEvent, + logger: Moleculer.LoggerInstance + ): Erc20Activity | undefined { + try { + const [to, amount] = decodeAbiParameters( + [ABI_TRANSFER_PARAMS.TO, ABI_APPROVAL_PARAMS.VALUE], + (e.topic1 + toHex(e.data).slice(2)) as `0x${string}` + ) as [string, bigint]; + return Erc20Activity.fromJson({ + evm_event_id: e.id, + sender: e.sender, + action: ERC20_ACTION.DEPOSIT, + erc20_contract_address: e.address, + amount: amount.toString(), + from: ZERO_ADDRESS, + to: to.toLowerCase(), + 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; + } + } + + private static buildWrapWithdrawalActivity( + e: EvmEvent, + logger: Moleculer.LoggerInstance + ): Erc20Activity | undefined { + try { + const [from, amount] = decodeAbiParameters( + [ABI_TRANSFER_PARAMS.FROM, ABI_APPROVAL_PARAMS.VALUE], + (e.topic1 + toHex(e.data).slice(2)) as `0x${string}` + ) as [string, bigint]; + return Erc20Activity.fromJson({ + evm_event_id: e.id, + sender: e.sender, + action: ERC20_ACTION.WITHDRAWAL, + erc20_contract_address: e.address, + amount: amount.toString(), + from: from.toLowerCase(), + to: ZERO_ADDRESS, + 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; + } } } diff --git a/src/services/evm/erc20_handler.ts b/src/services/evm/erc20_handler.ts index 187ab773b..d4e2091a7 100644 --- a/src/services/evm/erc20_handler.ts +++ b/src/services/evm/erc20_handler.ts @@ -1,12 +1,25 @@ +import { Knex } from 'knex'; +import _, { Dictionary } from 'lodash'; +import Moleculer from 'moleculer'; import { decodeAbiParameters, keccak256, toHex } from 'viem'; -import { Dictionary } from 'lodash'; -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', APPROVAL: 'approval', + DEPOSIT: 'deposit', + WITHDRAWAL: 'withdrawal', }; export const ABI_TRANSFER_PARAMS = { FROM: { @@ -39,6 +52,8 @@ export const ABI_APPROVAL_PARAMS = { export const ERC20_EVENT_TOPIC0 = { TRANSFER: keccak256(toHex('Transfer(address,address,uint256)')), APPROVAL: keccak256(toHex('Approval(address,address,uint256)')), + DEPOSIT: keccak256(toHex('Deposit(address,uint256)')), + WITHDRAWAL: keccak256(toHex('Withdrawal(address,uint256)')), }; export class Erc20Handler { // key: {accountId}_{erc20ContractAddress} @@ -47,54 +62,87 @@ 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() { this.erc20Activities.forEach((erc20Activity) => { - if (erc20Activity.action === ERC20_ACTION.TRANSFER) { + if ( + [ + ERC20_ACTION.TRANSFER, + ERC20_ACTION.DEPOSIT, + ERC20_ACTION.WITHDRAWAL, + ].includes(erc20Activity.action) + ) { this.handlerErc20Transfer(erc20Activity); } }); } 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) @@ -107,10 +155,198 @@ 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(); + } } } - static buildTransferActivity(e: EvmEvent) { + static buildTransferActivity( + e: EvmEvent, + logger: Moleculer.LoggerInstance + ): Erc20Activity | undefined { try { const [from, to, amount] = decodeAbiParameters( [ @@ -131,13 +367,80 @@ 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 { + } catch (e) { + logger.error(e); return undefined; } } - static buildApprovalActivity(e: EvmEvent) { + static buildApprovalActivity( + e: EvmEvent, + logger: Moleculer.LoggerInstance + ): Erc20Activity | undefined { try { const [from, to, amount] = decodeAbiParameters( [ @@ -158,8 +461,81 @@ 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 buildWrapExtensionActivity( + e: EvmEvent, + logger: Moleculer.LoggerInstance + ): Erc20Activity | undefined { + if (e.topic0 === ERC20_EVENT_TOPIC0.DEPOSIT) { + const activity = Erc20Handler.buildWrapDepositActivity(e, logger); + return activity; + } + if (e.topic0 === ERC20_EVENT_TOPIC0.WITHDRAWAL) { + const activity = Erc20Handler.buildWrapWithdrawalActivity(e, logger); + return activity; + } + return undefined; + } + + private static buildWrapDepositActivity( + e: EvmEvent, + logger: Moleculer.LoggerInstance + ): Erc20Activity | undefined { + try { + const [to, amount] = decodeAbiParameters( + [ABI_TRANSFER_PARAMS.TO, ABI_APPROVAL_PARAMS.VALUE], + (e.topic1 + toHex(e.data).slice(2)) as `0x${string}` + ) as [string, bigint]; + return Erc20Activity.fromJson({ + evm_event_id: e.id, + sender: e.sender, + action: ERC20_ACTION.DEPOSIT, + erc20_contract_address: e.address, + amount: amount.toString(), + from: ZERO_ADDRESS, + to: to.toLowerCase(), + 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; + } + } + + private static buildWrapWithdrawalActivity( + e: EvmEvent, + logger: Moleculer.LoggerInstance + ): Erc20Activity | undefined { + try { + const [from, amount] = decodeAbiParameters( + [ABI_TRANSFER_PARAMS.FROM, ABI_APPROVAL_PARAMS.VALUE], + (e.topic1 + toHex(e.data).slice(2)) as `0x${string}` + ) as [string, bigint]; + return Erc20Activity.fromJson({ + evm_event_id: e.id, + sender: e.sender, + action: ERC20_ACTION.WITHDRAWAL, + erc20_contract_address: e.address, + amount: amount.toString(), + from: from.toLowerCase(), + to: ZERO_ADDRESS, + height: e.block_height, + tx_hash: e.tx_hash, + evm_tx_id: e.evm_tx_id, + cosmos_tx_id: e.tx_id, }); - } catch { + } catch (e) { + logger.error(e); return undefined; } } 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); + }); + } +}