diff --git a/network.json b/network.json index a69d446cb..76d5e7520 100644 --- a/network.json +++ b/network.json @@ -31,7 +31,9 @@ "LCD": ["https://lcd.serenity.aura.network"], "databaseName": "horoscope_dev_serenitytestnet001", "redisDBNumber": 3, - "moleculerNamespace": "namespace-serenity" + "moleculerNamespace": "namespace-serenity", + "EVMJSONRPC": ["https://jsonrpc.serenity.aura.network/"], + "EVMchainId": 1236 }, { "chainId": "auradev_1235-3", diff --git a/src/models/erc721_contract.ts b/src/models/erc721_contract.ts index 66c20217c..2a3b9fb13 100644 --- a/src/models/erc721_contract.ts +++ b/src/models/erc721_contract.ts @@ -230,5 +230,24 @@ export class Erc721Contract extends BaseModel { stateMutability: 'view', type: 'function', }, + { + inputs: [ + { + internalType: 'uint256', + name: '_index', + type: 'uint256', + }, + ], + name: 'tokenByIndex', + outputs: [ + { + internalType: 'uint256', + name: 'tokenId', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, ]; } diff --git a/src/services/api-gateways/api_gateway.service.ts b/src/services/api-gateways/api_gateway.service.ts index 90fa8939d..9c166f538 100644 --- a/src/services/api-gateways/api_gateway.service.ts +++ b/src/services/api-gateways/api_gateway.service.ts @@ -44,6 +44,7 @@ import config from '../../../config.json' assert { type: 'json' }; 'v1.job.update-delegator-validator', 'v1.job.signature-mapping', 'v1.job.insert-verify-by-codehash', + 'v1.erc721-admin.*', ], }, { diff --git a/src/services/api-gateways/erc721_admin.service.ts b/src/services/api-gateways/erc721_admin.service.ts new file mode 100644 index 000000000..8a817fcb6 --- /dev/null +++ b/src/services/api-gateways/erc721_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: 'erc721-admin', + version: 1, +}) +export default class Erc721AdminService extends BaseService { + public constructor(public broker: ServiceBroker) { + super(broker); + } + + @Post('/erc721-reindexing', { + name: 'erc721Reindexing', + params: { + chainid: { + type: 'string', + optional: false, + enum: networks.map((network) => network.chainId), + }, + addresses: { + type: 'array', + optional: false, + items: 'string', + }, + }, + }) + async erc721Reindexing( + ctx: Context< + { + chainid: string; + addresses: string[]; + }, + Record + > + ) { + const selectedChain = networks.find( + (network) => network.chainId === ctx.params.chainid + ); + return this.broker.call( + `v1.Erc721.reindexing@${selectedChain?.moleculerNamespace}`, + { + addresses: ctx.params.addresses, + } + ); + } +} diff --git a/src/services/evm/constant.ts b/src/services/evm/constant.ts index 7fc1fd612..62d21f7d5 100644 --- a/src/services/evm/constant.ts +++ b/src/services/evm/constant.ts @@ -121,6 +121,10 @@ export const SERVICE = { key: 'insertNewErc721Contracts', path: 'v1.Erc721.insertNewErc721Contracts', }, + reindexing: { + key: 'reindexing', + path: 'v1.Erc721.reindexing', + }, }, SignatureMappingEVM: { key: 'SignatureMappingEVM', diff --git a/src/services/evm/erc721.service.ts b/src/services/evm/erc721.service.ts index 8b1bebd5d..1b786e92f 100644 --- a/src/services/evm/erc721.service.ts +++ b/src/services/evm/erc721.service.ts @@ -3,7 +3,7 @@ import { Service, } from '@ourparentcenter/moleculer-decorators-extended'; import { Knex } from 'knex'; -import _, { Dictionary } from 'lodash'; +import _ from 'lodash'; import { Context, ServiceBroker } from 'moleculer'; import { PublicClient, getContract } from 'viem'; import config from '../../../config.json' assert { type: 'json' }; @@ -12,22 +12,17 @@ import { Config } from '../../common'; import knex from '../../common/utils/db_connection'; import { getViemClient } from '../../common/utils/etherjs_client'; import { - Block, BlockCheckpoint, EVMSmartContract, Erc721Activity, Erc721Stats, Erc721Token, - EvmEvent, } from '../../models'; import { Erc721Contract } from '../../models/erc721_contract'; import { BULL_JOB_NAME, SERVICE } from './constant'; -import { - ERC721_ACTION, - ERC721_EVENT_TOPIC0, - Erc721Handler, -} from './erc721_handler'; +import { Erc721Handler } from './erc721_handler'; import * as Erc721MediaHandler from './erc721_media_handler'; +import { Erc721Reindexer } from './erc721_reindex'; const { NODE_ENV } = Config; @Service({ @@ -90,55 +85,14 @@ export default class Erc721Service extends BullableService { ], config.erc721.key ); - const erc721Events = await EvmEvent.query() - .transacting(trx) - .joinRelated('[evm_smart_contract,evm_transaction]') - .innerJoin( - 'erc721_contract', - 'evm_event.address', - 'erc721_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', - 'erc721_contract.track as track' + const erc721Activities: Erc721Activity[] = + await Erc721Handler.getErc721Activities( + startBlock, + endBlock, + trx, + this.logger ); - await this.handleMissingErc721Contract(erc721Events, trx); - const erc721Activities: Erc721Activity[] = []; - erc721Events - .filter((e) => e.track) - .forEach((e) => { - if (e.topic0 === ERC721_EVENT_TOPIC0.TRANSFER) { - const activity = Erc721Handler.buildTransferActivity( - e, - this.logger - ); - if (activity) { - erc721Activities.push(activity); - } - } else if (e.topic0 === ERC721_EVENT_TOPIC0.APPROVAL) { - const activity = Erc721Handler.buildApprovalActivity( - e, - this.logger - ); - if (activity) { - erc721Activities.push(activity); - } - } else if (e.topic0 === ERC721_EVENT_TOPIC0.APPROVAL_FOR_ALL) { - const activity = Erc721Handler.buildApprovalForAllActivity( - e, - this.logger - ); - if (activity) { - erc721Activities.push(activity); - } - } - }); + await this.handleMissingErc721Contract(erc721Activities, trx); if (erc721Activities.length > 0) { const erc721Tokens = _.keyBy( await Erc721Token.query() @@ -155,7 +109,7 @@ export default class Erc721Service extends BullableService { ); const erc721Handler = new Erc721Handler(erc721Tokens, erc721Activities); erc721Handler.process(); - await this.updateErc721( + await Erc721Handler.updateErc721( erc721Activities, Object.values(erc721Handler.erc721Tokens), trx @@ -263,7 +217,7 @@ export default class Erc721Service extends BullableService { jobName: BULL_JOB_NAME.REFRESH_ERC721_STATS, }) async jobHandlerRefresh(): Promise { - const erc721Stats = await this.calErc721Stats(); + const erc721Stats = await Erc721Handler.calErc721Stats(); // Upsert erc721 stats await Erc721Stats.query() .insert( @@ -280,6 +234,17 @@ export default class Erc721Service extends BullableService { .returning('id'); } + @QueueHandler({ + queueName: BULL_JOB_NAME.REINDEX_ERC721, + jobName: BULL_JOB_NAME.REINDEX_ERC721, + }) + async reindexErc721(_payload: { address: `0x${string}` }): Promise { + const { address } = _payload; + const erc721Reindexer = new Erc721Reindexer(this.viemClient, this.logger); + await erc721Reindexer.reindex(address); + this.logger.info(`Reindex erc721 contract ${address} done.`); + } + @Action({ name: SERVICE.V1.Erc721.insertNewErc721Contracts.key, params: { @@ -315,49 +280,40 @@ export default class Erc721Service extends BullableService { } } - async updateErc721( - erc721Activities: Erc721Activity[], - erc721Tokens: Erc721Token[], - trx: Knex.Transaction + @Action({ + name: SERVICE.V1.Erc721.reindexing.key, + params: { + addresses: { + type: 'array', + items: 'string', + optional: false, + }, + }, + }) + public async reindexing( + ctx: Context<{ + addresses: `0x${string}`[]; + }> ) { - let updatedTokens: Dictionary = {}; - if (erc721Tokens.length > 0) { - updatedTokens = _.keyBy( - await Erc721Token.query() - .insert( - erc721Tokens.map((token) => - Erc721Token.fromJson({ - token_id: token.token_id, - owner: token.owner, - erc721_contract_address: token.erc721_contract_address, - last_updated_height: token.last_updated_height, - }) - ) + let { addresses } = ctx.params; + const erc721Reindexer = new Erc721Reindexer(this.viemClient, this.logger); + addresses = await erc721Reindexer.filterReindex(addresses); + if (addresses.length > 0) { + await Promise.all( + addresses.map((address) => + this.createJob( + BULL_JOB_NAME.REINDEX_ERC721, + BULL_JOB_NAME.REINDEX_ERC721, + { + address, + }, + { + jobId: address, + removeOnComplete: true, + } ) - .onConflict(['token_id', 'erc721_contract_address']) - .merge() - .transacting(trx), - (o) => `${o.erc721_contract_address}_${o.token_id}` - ); - } - if (erc721Activities.length > 0) { - erc721Activities.forEach((activity) => { - const token = - updatedTokens[ - `${activity.erc721_contract_address}_${activity.token_id}` - ]; - if (token) { - // eslint-disable-next-line no-param-reassign - activity.erc721_token_id = token.id; - } - }); - await knex - .batchInsert( - 'erc721_activity', - erc721Activities.map((e) => _.omit(e, 'token_id')), - config.erc721.chunkSizeInsert ) - .transacting(trx); + ); } } @@ -403,32 +359,15 @@ export default class Erc721Service extends BullableService { })); } - async calErc721Stats(): Promise { - // Get once block height 24h ago. - const blockSince24hAgo = await Block.query() - .select('height') - .where('time', '<=', knex.raw("now() - '24 hours'::interval")) - .orderBy('height', 'desc') - .limit(1); - - // Calculate total activity and transfer_24h of erc721 - return Erc721Contract.query() - .count('erc721_activity.id AS total_activity') - .select( - knex.raw( - `SUM( CASE WHEN erc721_activity.height >= ? AND erc721_activity.action = '${ERC721_ACTION.TRANSFER}' THEN 1 ELSE 0 END ) AS transfer_24h`, - blockSince24hAgo[0]?.height - ) - ) - .select('erc721_contract.id as erc721_contract_id') - .where('erc721_contract.track', '=', true) - .joinRelated('erc721_activity') - .groupBy('erc721_contract.id'); - } - - async handleMissingErc721Contract(events: EvmEvent[], trx: Knex.Transaction) { + async handleMissingErc721Contract( + erc721Activities: Erc721Activity[], + trx: Knex.Transaction + ) { try { - const eventsUniqByAddress = _.keyBy(events, (e) => e.address); + const eventsUniqByAddress = _.keyBy( + erc721Activities, + (e) => e.erc721_contract_address + ); const addresses = Object.keys(eventsUniqByAddress); const erc721ContractsByAddress = _.keyBy( await Erc721Contract.query() diff --git a/src/services/evm/erc721_handler.ts b/src/services/evm/erc721_handler.ts index 90adb4ae7..969757f50 100644 --- a/src/services/evm/erc721_handler.ts +++ b/src/services/evm/erc721_handler.ts @@ -1,7 +1,16 @@ -import { Dictionary } from 'lodash'; +import { Knex } from 'knex'; +import _, { Dictionary } from 'lodash'; import Moleculer from 'moleculer'; import { decodeAbiParameters, keccak256, toHex } from 'viem'; -import { Erc721Activity, Erc721Token, EvmEvent } from '../../models'; +import config from '../../../config.json' assert { type: 'json' }; +import knex from '../../common/utils/db_connection'; +import { + Block, + Erc721Activity, + Erc721Contract, + Erc721Token, + EvmEvent, +} from '../../models'; import { ZERO_ADDRESS } from './constant'; export const ERC721_EVENT_TOPIC0 = { @@ -179,4 +188,132 @@ export class Erc721Handler { return undefined; } } + + static async getErc721Activities( + startBlock: number, + endBlock: number, + trx: Knex.Transaction, + logger: Moleculer.LoggerInstance, + addresses?: string[] + ) { + const erc721Events = await EvmEvent.query() + .transacting(trx) + .joinRelated('[evm_smart_contract,evm_transaction]') + .innerJoin( + 'erc721_contract', + 'evm_event.address', + 'erc721_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', + 'erc721_contract.track as track' + ); + const erc721Activities: Erc721Activity[] = []; + erc721Events + .filter((e) => e.track) + .forEach((e) => { + if (e.topic0 === ERC721_EVENT_TOPIC0.TRANSFER) { + const activity = Erc721Handler.buildTransferActivity(e, logger); + if (activity) { + erc721Activities.push(activity); + } + } else if (e.topic0 === ERC721_EVENT_TOPIC0.APPROVAL) { + const activity = Erc721Handler.buildApprovalActivity(e, logger); + if (activity) { + erc721Activities.push(activity); + } + } else if (e.topic0 === ERC721_EVENT_TOPIC0.APPROVAL_FOR_ALL) { + const activity = Erc721Handler.buildApprovalForAllActivity(e, logger); + if (activity) { + erc721Activities.push(activity); + } + } + }); + return erc721Activities; + } + + static async updateErc721( + erc721Activities: Erc721Activity[], + erc721Tokens: Erc721Token[], + trx: Knex.Transaction + ) { + let updatedTokens: Dictionary = {}; + if (erc721Tokens.length > 0) { + updatedTokens = _.keyBy( + await Erc721Token.query() + .insert( + erc721Tokens.map((token) => + Erc721Token.fromJson({ + token_id: token.token_id, + owner: token.owner, + erc721_contract_address: token.erc721_contract_address, + last_updated_height: token.last_updated_height, + }) + ) + ) + .onConflict(['token_id', 'erc721_contract_address']) + .merge() + .transacting(trx), + (o) => `${o.erc721_contract_address}_${o.token_id}` + ); + } + if (erc721Activities.length > 0) { + erc721Activities.forEach((activity) => { + const token = + updatedTokens[ + `${activity.erc721_contract_address}_${activity.token_id}` + ]; + if (token) { + // eslint-disable-next-line no-param-reassign + activity.erc721_token_id = token.id; + } + }); + await knex + .batchInsert( + 'erc721_activity', + erc721Activities.map((e) => _.omit(e, 'token_id')), + config.erc721.chunkSizeInsert + ) + .transacting(trx); + } + } + + static async calErc721Stats(addresses?: string[]): Promise { + // Get once block height 24h ago. + const blockSince24hAgo = await Block.query() + .select('height') + .where('time', '<=', knex.raw("now() - '24 hours'::interval")) + .orderBy('height', 'desc') + .limit(1); + + // Calculate total activity and transfer_24h of erc721 + return Erc721Contract.query() + .count('erc721_activity.id AS total_activity') + .select( + knex.raw( + `SUM( CASE WHEN erc721_activity.height >= ? AND erc721_activity.action = '${ERC721_ACTION.TRANSFER}' THEN 1 ELSE 0 END ) AS transfer_24h`, + blockSince24hAgo[0]?.height + ) + ) + .select('erc721_contract.id as erc721_contract_id') + .where('erc721_contract.track', '=', true) + .modify((builder) => { + if (addresses) { + builder.whereIn('erc721_contract.address', addresses); + } + }) + .joinRelated('erc721_activity') + .groupBy('erc721_contract.id'); + } } diff --git a/src/services/evm/erc721_reindex.ts b/src/services/evm/erc721_reindex.ts new file mode 100644 index 000000000..23ddb2dff --- /dev/null +++ b/src/services/evm/erc721_reindex.ts @@ -0,0 +1,179 @@ +import Moleculer from 'moleculer'; +import { PublicClient, getContract } from 'viem'; +import knex from '../../common/utils/db_connection'; +import { + Erc721Activity, + Erc721Contract, + Erc721Stats, + Erc721Token, +} from '../../models'; +import { Erc721Handler } from './erc721_handler'; + +export class Erc721Reindexer { + viemClient: PublicClient; + + logger!: Moleculer.LoggerInstance; + + constructor(viemClient: PublicClient, logger: Moleculer.LoggerInstance) { + this.viemClient = viemClient; + this.logger = logger; + } + + /** + * @description filter reindexable erc721 contracts + * @param addresses Contracts address that you want to filter + * @steps + * - check type + * - check ERC721Enumerable implementation + */ + async filterReindex(addresses: `0x${string}`[]) { + const erc721ContractAddrs = ( + await Erc721Contract.query().whereIn('address', addresses) + ).map((e) => e.address) as `0x${string}`[]; + const erc721Contracts = erc721ContractAddrs.map((address) => + getContract({ + address, + abi: Erc721Contract.ABI, + client: this.viemClient, + }) + ); + const isErc721Enumerable = await Promise.all( + erc721Contracts.map((e) => + e.read + // Erc721 Enumerable InterfaceId + .supportsInterface(['0x780e9d63']) + .catch(() => Promise.resolve(false)) + ) + ); + return erc721ContractAddrs.filter((_, index) => isErc721Enumerable[index]); + } + + /** + * @description reindex erc721 contract + * @requires filterReindex + * @param addresses Contracts address that you want to reindex + * @steps + * - clean database: ERC721 Contract, ERC721 Activity, ERC721 Holder + * - get current status of those erc721 contracts: contracts info, tokens and holders + * - reinsert to database + */ + async reindex(address: `0x${string}`) { + // stop tracking => if start reindexing, track will be false (although error when reindex) + await Erc721Contract.query() + .patch({ track: false }) + .where('address', address); + // reindex + await knex.transaction(async (trx) => { + const erc721Contract = await Erc721Contract.query() + .transacting(trx) + .joinRelated('evm_smart_contract') + .where('erc721_contract.address', address) + .select( + 'evm_smart_contract.id as evm_smart_contract_id', + 'erc721_contract.id' + ) + .first() + .throwIfNotFound(); + await Erc721Stats.query() + .delete() + .where('erc721_contract_id', erc721Contract.id) + .transacting(trx); + await Erc721Activity.query() + .delete() + .where('erc721_contract_address', address) + .transacting(trx); + await Erc721Token.query() + .delete() + .where('erc721_contract_address', address) + .transacting(trx); + await Erc721Contract.query() + .delete() + .where('address', address) + .transacting(trx); + const contract = getContract({ + address, + abi: Erc721Contract.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)), + ]); + await Erc721Contract.query() + .insert( + Erc721Contract.fromJson({ + evm_smart_contract_id: erc721Contract.evm_smart_contract_id, + address, + symbol: contractInfo[1], + name: contractInfo[0], + track: true, + last_updated_height: Number(blockHeight), + }) + ) + .transacting(trx); + const [tokens, height] = await this.getCurrentTokens(address); + const activities = await Erc721Handler.getErc721Activities( + 0, + height, + trx, + this.logger, + [address] + ); + await Erc721Handler.updateErc721(activities, tokens, trx); + }); + const erc721Stats = await Erc721Handler.calErc721Stats([address]); + // Upsert erc721 stats + await Erc721Stats.query() + .insert( + erc721Stats.map((e) => + Erc721Stats.fromJson({ + total_activity: e.total_activity, + transfer_24h: e.transfer_24h, + erc721_contract_id: e.erc721_contract_id, + }) + ) + ) + .onConflict('erc721_contract_id') + .merge() + .returning('id'); + } + + async getCurrentTokens( + address: `0x${string}` + ): Promise<[Erc721Token[], number]> { + const contract = getContract({ + address, + abi: Erc721Contract.ABI, + client: this.viemClient, + }); + const totalSupply = (await contract.read + .totalSupply() + .catch(() => Promise.resolve(BigInt(0)))) as bigint; + const tokensId = (await Promise.all( + Array.from(Array(Number(totalSupply)).keys()).map((i) => + contract.read.tokenByIndex([i]) + ) + )) as bigint[]; + const [height, ...owners]: [bigint, ...string[]] = await Promise.all([ + this.viemClient.getBlockNumber(), + ...tokensId.map( + (tokenId) => + contract.read + .ownerOf([tokenId]) + .catch(() => Promise.resolve('')) as Promise + ), + ]); + return [ + tokensId.map((tokenId, index) => + Erc721Token.fromJson({ + token_id: tokenId.toString(), + owner: owners[index].toLowerCase(), + erc721_contract_address: address, + last_updated_height: Number(height), + }) + ), + Number(height), + ]; + } +} diff --git a/test/unit/services/erc721/erc721.spec.ts b/test/unit/services/erc721/erc721.spec.ts index 65562de05..9d352233e 100644 --- a/test/unit/services/erc721/erc721.spec.ts +++ b/test/unit/services/erc721/erc721.spec.ts @@ -8,7 +8,6 @@ import { import { ServiceBroker } from 'moleculer'; import knex from '../../../../src/common/utils/db_connection'; import { - Block, BlockCheckpoint, EVMSmartContract, EVMTransaction, @@ -193,6 +192,7 @@ export default class Erc721Test { andWhere: () => mockQueryEvents, orderBy: () => mockQueryEvents, innerJoin: () => mockQueryEvents, + modify: () => mockQueryEvents, }; jest.spyOn(EvmEvent, 'query').mockImplementation(() => mockQueryEvents); await this.erc721Service.handleErc721Activity(); @@ -225,61 +225,4 @@ export default class Erc721Test { erc721_token_id: erc721Token.id, }); } - - @Test('test calErc721Stats') - async testCalErc721Stats() { - const mockQueryBlocks: any = { - limit: () => [{ height: 10001 }], - orderBy: () => mockQueryBlocks, - select: () => mockQueryBlocks, - where: () => mockQueryBlocks, - }; - jest.spyOn(Block, 'query').mockImplementation(() => mockQueryBlocks); - const erc721Activities = [ - { - action: ERC721_ACTION.TRANSFER, - erc721_contract_address: this.evmSmartContract.address, - height: 10000, - evm_event_id: this.evmEvent.id, - }, - { - action: ERC721_ACTION.APPROVAL, - erc721_contract_address: this.evmSmartContract.address, - height: 10000, - evm_event_id: this.evmEvent.id, - }, - { - action: ERC721_ACTION.TRANSFER, - erc721_contract_address: this.evmSmartContract.address, - height: 10001, - evm_event_id: this.evmEvent.id, - }, - { - action: ERC721_ACTION.TRANSFER, - erc721_contract_address: this.evmSmartContract.address, - height: 10001, - evm_event_id: this.evmEvent.id, - }, - { - action: ERC721_ACTION.TRANSFER, - erc721_contract_address: this.evmSmartContract.address, - height: 10001, - evm_event_id: this.evmEvent.id, - }, - { - erc721_contract_address: this.evmSmartContract.address, - height: 10001, - evm_event_id: this.evmEvent.id, - }, - ]; - await Erc721Activity.query().insert( - erc721Activities.map((e) => Erc721Activity.fromJson(e)) - ); - const result = await this.erc721Service.calErc721Stats(); - expect(result[0]).toMatchObject({ - total_activity: '6', - transfer_24h: '3', - erc721_contract_id: this.erc721Contract1.id, - }); - } } diff --git a/test/unit/services/erc721/erc721_handler.spec.ts b/test/unit/services/erc721/erc721_handler.spec.ts new file mode 100644 index 000000000..0782ee91a --- /dev/null +++ b/test/unit/services/erc721/erc721_handler.spec.ts @@ -0,0 +1,278 @@ +import { BeforeAll, Describe, Test } from '@jest-decorated/core'; +import { ServiceBroker } from 'moleculer'; +import knex from '../../../../src/common/utils/db_connection'; +import { + Block, + EVMSmartContract, + EVMTransaction, + Erc721Activity, + Erc721Contract, + EvmEvent, +} from '../../../../src/models'; +import { + ERC721_ACTION, + Erc721Handler, +} from '../../../../src/services/evm/erc721_handler'; + +@Describe('Test erc721 handler') +export default class Erc721HandlerTest { + broker = new ServiceBroker({ logger: false }); + + evmSmartContract = EVMSmartContract.fromJson({ + id: 555, + address: 'ghghdfgdsgre', + creator: 'dfgdfbvxcvxgfds', + created_height: 100, + created_hash: 'cvxcvcxv', + type: EVMSmartContract.TYPES.ERC721, + code_hash: 'dfgdfghf', + }); + + evmSmartContract2 = EVMSmartContract.fromJson({ + id: 666, + address: 'bcvbcvbcv', + creator: 'dfgdfbvxcvxgfds', + created_height: 100, + created_hash: 'xdasfsf', + type: EVMSmartContract.TYPES.ERC721, + code_hash: 'xcsadf', + }); + + evmTx = EVMTransaction.fromJson({ + id: 11111, + hash: '', + height: 111, + tx_msg_id: 222, + tx_id: 223, + contract_address: '', + index: 1, + }); + + erc721Contract1 = Erc721Contract.fromJson({ + evm_smart_contract_id: this.evmSmartContract.id, + id: 123, + track: true, + address: this.evmSmartContract.address, + }); + + erc721Contract2 = Erc721Contract.fromJson({ + evm_smart_contract_id: this.evmSmartContract2.id, + id: 1234, + track: true, + address: this.evmSmartContract2.address, + }); + + @BeforeAll() + async initSuite() { + await knex.raw( + 'TRUNCATE TABLE erc721_contract, account, evm_smart_contract, evm_event, evm_transaction RESTART IDENTITY CASCADE' + ); + await EVMSmartContract.query().insert([ + this.evmSmartContract, + this.evmSmartContract2, + ]); + await Erc721Contract.query().insert([ + this.erc721Contract1, + this.erc721Contract2, + ]); + await EVMTransaction.query().insert(this.evmTx); + } + + @Test('test getErc721Activities') + async testGetErc721Activities() { + await knex.transaction(async (trx) => { + const erc721Events = [ + EvmEvent.fromJson({ + block_hash: + '0x6d70a03cda3fb815b54742fbd47c6141a7e754ff4d7426f10a73644ac44411d2', + block_height: 21937980, + data: null, + topic0: + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + topic1: + '0x0000000000000000000000000000000000000000000000000000000000000000', + topic2: + '0x0000000000000000000000001317df02a4e712265f5376a9d34156f73ebad640', + topic3: + '0x0000000000000000000000000000000000000000000000000000000000000000', + address: this.evmSmartContract.address, + evm_tx_id: this.evmTx.id, + tx_id: 1234, + tx_hash: this.evmTx.hash, + tx_index: 1, + }), + EvmEvent.fromJson({ + block_hash: + '0xd39b1e6c35a7985db6ca367b1e061162b7a8610097e99cadaf98bea6b81a6096', + block_height: 21937981, + data: null, + topic0: + '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925', + topic1: + '0x0000000000000000000000001317df02a4e712265f5376a9d34156f73ebad640', + topic2: + '0x000000000000000000000000a3b6d252c1df2ce88f01fdb75b5479bcdc8f5007', + topic3: + '0x0000000000000000000000000000000000000000000000000000000000000000', + address: this.evmSmartContract.address, + evm_tx_id: this.evmTx.id, + tx_id: 1234, + tx_hash: this.evmTx.hash, + tx_index: 1, + }), + EvmEvent.fromJson({ + block_hash: + '0xd39b1e6c35a7985db6ca367b1e061162b7a8610097e99cadaf98bea6b81a6096', + block_height: 21937981, + data: null, + topic0: + '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925', + topic1: + '0x0000000000000000000000001317df02a4e712265f5376a9d34156f73ebad640', + topic2: + '0x000000000000000000000000a3b6d252c1df2ce88f01fdb75b5479bcdc8f5007', + topic3: + '0x0000000000000000000000000000000000000000000000000000000000000000', + address: this.evmSmartContract2.address, + evm_tx_id: this.evmTx.id, + tx_id: 1234, + tx_hash: this.evmTx.hash, + tx_index: 1, + }), + EvmEvent.fromJson({ + block_hash: + '0x6d70a03cda3fb815b54742fbd47c6141a7e754ff4d7426f10a73644ac44411d2', + block_height: 21937982, + data: null, + topic0: + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + topic1: + '0x0000000000000000000000001317df02a4e712265f5376a9d34156f73ebad640', + topic2: + '0x000000000000000000000000e39633931ec4a1841e438b15005a6f141d30789e', + topic3: + '0x0000000000000000000000000000000000000000000000000000000000000000', + address: this.evmSmartContract.address, + evm_tx_id: this.evmTx.id, + tx_id: 1234, + tx_hash: this.evmTx.hash, + tx_index: 1, + }), + ]; + await EvmEvent.query().insert(erc721Events).transacting(trx); + const erc721Activities = await Erc721Handler.getErc721Activities( + 21937979, + 21937985, + trx, + this.broker.logger + ); + expect(erc721Activities[0]).toMatchObject({ + action: ERC721_ACTION.TRANSFER, + erc721_contract_address: this.evmSmartContract.address, + from: '0x0000000000000000000000000000000000000000', + to: '0x1317df02a4e712265f5376a9d34156f73ebad640', + }); + expect(erc721Activities[1]).toMatchObject({ + action: ERC721_ACTION.APPROVAL, + erc721_contract_address: this.evmSmartContract.address, + from: '0x1317df02a4e712265f5376a9d34156f73ebad640', + to: '0xa3b6d252c1df2ce88f01fdb75b5479bcdc8f5007', + }); + expect(erc721Activities[2]).toMatchObject({ + action: ERC721_ACTION.APPROVAL, + erc721_contract_address: this.evmSmartContract2.address, + from: '0x1317df02a4e712265f5376a9d34156f73ebad640', + to: '0xa3b6d252c1df2ce88f01fdb75b5479bcdc8f5007', + }); + expect(erc721Activities[3]).toMatchObject({ + action: ERC721_ACTION.TRANSFER, + erc721_contract_address: this.evmSmartContract.address, + from: '0x1317df02a4e712265f5376a9d34156f73ebad640', + to: '0xe39633931ec4a1841e438b15005a6f141d30789e', + }); + const erc721ActivitiesByContract = + await Erc721Handler.getErc721Activities( + 21937979, + 21937985, + trx, + this.broker.logger, + [this.evmSmartContract2.address] + ); + expect(erc721ActivitiesByContract[0]).toMatchObject({ + action: ERC721_ACTION.APPROVAL, + erc721_contract_address: this.evmSmartContract2.address, + from: '0x1317df02a4e712265f5376a9d34156f73ebad640', + to: '0xa3b6d252c1df2ce88f01fdb75b5479bcdc8f5007', + }); + await trx.rollback(); + }); + } + + @Test('test calErc721Stats') + async testCalErc721Stats() { + const 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, + }); + await EvmEvent.query().insert(evmEvent); + const mockQueryBlocks: any = { + limit: () => [{ height: 10001 }], + orderBy: () => mockQueryBlocks, + select: () => mockQueryBlocks, + where: () => mockQueryBlocks, + }; + jest.spyOn(Block, 'query').mockImplementation(() => mockQueryBlocks); + const erc721Activities = [ + { + action: ERC721_ACTION.TRANSFER, + erc721_contract_address: this.evmSmartContract.address, + height: 10000, + evm_event_id: evmEvent.id, + }, + { + action: ERC721_ACTION.APPROVAL, + erc721_contract_address: this.evmSmartContract.address, + height: 10000, + evm_event_id: evmEvent.id, + }, + { + action: ERC721_ACTION.TRANSFER, + erc721_contract_address: this.evmSmartContract.address, + height: 10001, + evm_event_id: evmEvent.id, + }, + { + action: ERC721_ACTION.TRANSFER, + erc721_contract_address: this.evmSmartContract.address, + height: 10001, + evm_event_id: evmEvent.id, + }, + { + action: ERC721_ACTION.TRANSFER, + erc721_contract_address: this.evmSmartContract.address, + height: 10001, + evm_event_id: evmEvent.id, + }, + { + erc721_contract_address: this.evmSmartContract.address, + height: 10001, + evm_event_id: evmEvent.id, + }, + ]; + await Erc721Activity.query().insert( + erc721Activities.map((e) => Erc721Activity.fromJson(e)) + ); + const result = await Erc721Handler.calErc721Stats(); + expect(result[0]).toMatchObject({ + total_activity: '6', + transfer_24h: '3', + erc721_contract_id: this.erc721Contract1.id, + }); + } +}