From eebe7006c532b4d03bd6ee559b1e01b9c81dd83c Mon Sep 17 00:00:00 2001 From: phamphong9981 Date: Fri, 13 Sep 2024 17:03:47 +0700 Subject: [PATCH 1/8] feat: erc721 holder statistic --- .../20240912082032_erc721_holder_statistic.ts | 18 ++++++ src/models/erc721_holder_statistic.ts | 27 +++++++++ src/models/index.ts | 1 + src/services/evm/erc721.service.ts | 60 +++++++++---------- src/services/evm/erc721_handler.ts | 23 ++++++- 5 files changed, 97 insertions(+), 32 deletions(-) create mode 100644 migrations/evm/20240912082032_erc721_holder_statistic.ts create mode 100644 src/models/erc721_holder_statistic.ts diff --git a/migrations/evm/20240912082032_erc721_holder_statistic.ts b/migrations/evm/20240912082032_erc721_holder_statistic.ts new file mode 100644 index 000000000..a4cb132f1 --- /dev/null +++ b/migrations/evm/20240912082032_erc721_holder_statistic.ts @@ -0,0 +1,18 @@ +import { Knex } from 'knex'; +export async function up(knex: Knex): Promise { + await knex.raw(`set statement_timeout to 0`); + await knex.raw(` + CREATE TABLE erc721_holder_statistic AS + select count(*), erc721_token.owner, erc721_token.erc721_contract_address + from erc721_token + group by erc721_token.owner, erc721_token.erc721_contract_address; + CREATE INDEX erc721_holder_statistic_owner_index + ON erc721_holder_statistic (owner); + CREATE INDEX erc721_holder_statistic_erc721_contract_address_count_index + ON erc721_holder_statistic (erc721_contract_address, count); + `); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists('erc721_holder_statistic'); +} diff --git a/src/models/erc721_holder_statistic.ts b/src/models/erc721_holder_statistic.ts new file mode 100644 index 000000000..dd55fdd1a --- /dev/null +++ b/src/models/erc721_holder_statistic.ts @@ -0,0 +1,27 @@ +import BaseModel from './base'; + +export class Erc721HolderStatistic extends BaseModel { + static softDelete = false; + + erc721_contract_address!: string; + + owner!: string; + + count!: number; + + static get tableName() { + return 'erc721_holder_statistic'; + } + + static get jsonSchema() { + return { + type: 'object', + required: ['erc721_contract_address', 'owner', 'count'], + properties: { + erc721_contract_address: { type: 'string' }, + owner: { type: 'string' }, + count: { type: 'number' }, + }, + }; + } +} diff --git a/src/models/index.ts b/src/models/index.ts index 1d4e16dda..92cbbd82a 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -48,3 +48,4 @@ export * from './erc721_stats'; export * from './evm_block'; export * from './optimism_deposit'; export * from './optimism_withdrawal'; +export * from './erc721_holder_statistic'; diff --git a/src/services/evm/erc721.service.ts b/src/services/evm/erc721.service.ts index b561549a5..cf03fbbcc 100644 --- a/src/services/evm/erc721.service.ts +++ b/src/services/evm/erc721.service.ts @@ -15,6 +15,7 @@ import { BlockCheckpoint, EVMSmartContract, Erc721Activity, + Erc721HolderStatistic, Erc721Stats, Erc721Token, } from '../../models'; @@ -103,6 +104,7 @@ export default class Erc721Service extends BullableService { if (erc721Activities.length > 0) { // create chunk array const listChunkErc721Activities = []; + const erc721HolderStatsOnDB: Erc721HolderStatistic[] = []; const erc721TokensOnDB: Erc721Token[] = []; for ( let i = 0; @@ -130,12 +132,37 @@ export default class Erc721Service extends BullableService { erc721TokensOnDB.push(...erc721TokensInChunk); }) ); - + // process chunk array + await Promise.all( + // eslint-disable-next-line array-callback-return + listChunkErc721Activities.map(async (chunk) => { + const erc721HolderStatsInChunk = + await Erc721HolderStatistic.query().whereIn( + ['erc721_contract_address', 'owner'], + _.uniqWith( + [ + ...chunk.map((e) => [e.erc721_contract_address, e.from]), + ...chunk.map((e) => [e.erc721_contract_address, e.to]), + ], + _.isEqual + ) + ); + erc721HolderStatsOnDB.push(...erc721HolderStatsInChunk); + }) + ); const erc721Tokens = _.keyBy( erc721TokensOnDB, (o) => `${o.erc721_contract_address}_${o.token_id}` ); - const erc721Handler = new Erc721Handler(erc721Tokens, erc721Activities); + const erc721HolderStats = _.keyBy( + erc721HolderStatsOnDB, + (o) => `${o.erc721_contract_address}_${o.owner}` + ); + const erc721Handler = new Erc721Handler( + erc721Tokens, + erc721Activities, + erc721HolderStats + ); erc721Handler.process(); await Erc721Handler.updateErc721( erc721Activities, @@ -278,21 +305,6 @@ export default class Erc721Service extends BullableService { this.logger.info(`Reindex erc721 contract ${address} done.`); } - @QueueHandler({ - queueName: BULL_JOB_NAME.REFRESH_ERC721_HOLDER_STATISTIC, - jobName: BULL_JOB_NAME.REFRESH_ERC721_HOLDER_STATISTIC, - }) - public async refreshErc721HolderStatistic(): Promise { - await knex.transaction(async (trx) => { - await knex - .raw(`set statement_timeout to ${config.erc721.statementTimeout}`) - .transacting(trx); - await knex.schema - .refreshMaterializedView('m_view_erc721_holder_statistic') - .transacting(trx); - }); - } - @Action({ name: SERVICE.V1.Erc721.insertNewErc721Contracts.key, params: { @@ -550,20 +562,6 @@ export default class Erc721Service extends BullableService { }, } ); - await this.createJob( - BULL_JOB_NAME.REFRESH_ERC721_HOLDER_STATISTIC, - BULL_JOB_NAME.REFRESH_ERC721_HOLDER_STATISTIC, - {}, - { - removeOnComplete: true, - removeOnFail: { - count: 3, - }, - repeat: { - pattern: config.erc721.timeRefreshMViewErc721HolderStats, - }, - } - ); } return super._start(); } diff --git a/src/services/evm/erc721_handler.ts b/src/services/evm/erc721_handler.ts index 5c9274b51..9bf5c254c 100644 --- a/src/services/evm/erc721_handler.ts +++ b/src/services/evm/erc721_handler.ts @@ -10,6 +10,7 @@ import { EVMTransaction, Erc721Activity, Erc721Contract, + Erc721HolderStatistic, Erc721Token, EvmEvent, } from '../../models'; @@ -70,12 +71,16 @@ export class Erc721Handler { erc721Activities: Erc721Activity[]; + erc721HolderStats: Dictionary; + constructor( erc721Tokens: Dictionary, - erc721Activities: Erc721Activity[] + erc721Activities: Erc721Activity[], + erc721HolderStats: Dictionary ) { this.erc721Tokens = erc721Tokens; this.erc721Activities = erc721Activities; + this.erc721HolderStats = erc721HolderStats; } process() { @@ -87,6 +92,7 @@ export class Erc721Handler { } handlerErc721Transfer(erc721Activity: Erc721Activity) { + // update erc721 token const token = this.erc721Tokens[ `${erc721Activity.erc721_contract_address}_${erc721Activity.token_id}` @@ -109,6 +115,21 @@ export class Erc721Handler { } else { throw new Error('Handle erc721 tranfer error'); } + // update erc721 holder statistics + if (erc721Activity.from !== ZERO_ADDRESS) { + const erc721HolderStatFrom = + this.erc721HolderStats[ + `${erc721Activity.erc721_contract_address}_${erc721Activity.from}` + ]; + erc721HolderStatFrom.count -= 1; + } + if (erc721Activity.to !== ZERO_ADDRESS) { + const erc721HolderStatTo = + this.erc721HolderStats[ + `${erc721Activity.erc721_contract_address}_${erc721Activity.to}` + ]; + erc721HolderStatTo.count += 1; + } } static buildTransferActivity( From 1a394f4f8b576b6021120252815c7898dc195ccc Mon Sep 17 00:00:00 2001 From: phamphong9981 Date: Mon, 16 Sep 2024 13:57:51 +0700 Subject: [PATCH 2/8] feat: erc721 contract total supply --- ...40916032201_erc721_contract_totalSupply.ts | 30 ++++++++++++ src/models/erc721_contract.ts | 2 + src/services/evm/erc721.service.ts | 12 +++++ src/services/evm/erc721_handler.ts | 46 ++++++++++++++++++- src/services/evm/erc721_reindex.ts | 31 +++++++++++-- 5 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 migrations/evm/20240916032201_erc721_contract_totalSupply.ts diff --git a/migrations/evm/20240916032201_erc721_contract_totalSupply.ts b/migrations/evm/20240916032201_erc721_contract_totalSupply.ts new file mode 100644 index 000000000..7d6570d64 --- /dev/null +++ b/migrations/evm/20240916032201_erc721_contract_totalSupply.ts @@ -0,0 +1,30 @@ +import { Knex } from 'knex'; +import { Erc721Token } from '../../src/models'; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable('erc721_contract', (table) => { + table.integer('total_supply').defaultTo(0).index(); + }); + await knex.raw(`set statement_timeout to 0`); + const totalSupplies = await Erc721Token.query(knex) + .select('erc721_token.erc721_contract_address') + .count() + .groupBy('erc721_token.erc721_contract_address'); + if (totalSupplies.length > 0) { + const stringListUpdates = totalSupplies + .map( + (totalSuply) => + `('${totalSuply.erc721_contract_address}', ${totalSuply.count})` + ) + .join(','); + await knex.raw( + `UPDATE erc721_contract SET total_supply = temp.total_supply from (VALUES ${stringListUpdates}) as temp(address, total_supply) where temp.address = erc721_contract.address` + ); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable('erc721_contract', (table) => { + table.dropColumn('total_supply'); + }); +} diff --git a/src/models/erc721_contract.ts b/src/models/erc721_contract.ts index 2a3b9fb13..e1d78dce5 100644 --- a/src/models/erc721_contract.ts +++ b/src/models/erc721_contract.ts @@ -25,6 +25,8 @@ export class Erc721Contract extends BaseModel { last_updated_height!: number; + total_supply!: number; + static get tableName() { return 'erc721_contract'; } diff --git a/src/services/evm/erc721.service.ts b/src/services/evm/erc721.service.ts index cf03fbbcc..dd7e70d87 100644 --- a/src/services/evm/erc721.service.ts +++ b/src/services/evm/erc721.service.ts @@ -104,6 +104,15 @@ export default class Erc721Service extends BullableService { if (erc721Activities.length > 0) { // create chunk array const listChunkErc721Activities = []; + const erc721Contracts = _.keyBy( + await Erc721Contract.query() + .whereIn( + 'address', + erc721Activities.map((e) => e.erc721_contract_address) + ) + .transacting(trx), + 'address' + ); const erc721HolderStatsOnDB: Erc721HolderStatistic[] = []; const erc721TokensOnDB: Erc721Token[] = []; for ( @@ -159,14 +168,17 @@ export default class Erc721Service extends BullableService { (o) => `${o.erc721_contract_address}_${o.owner}` ); const erc721Handler = new Erc721Handler( + erc721Contracts, erc721Tokens, erc721Activities, erc721HolderStats ); erc721Handler.process(); await Erc721Handler.updateErc721( + Object.values(erc721Handler.erc721Contracts), erc721Activities, Object.values(erc721Handler.erc721Tokens), + Object.values(erc721Handler.erc721HolderStats), trx ); } diff --git a/src/services/evm/erc721_handler.ts b/src/services/evm/erc721_handler.ts index 9bf5c254c..0ab06978c 100644 --- a/src/services/evm/erc721_handler.ts +++ b/src/services/evm/erc721_handler.ts @@ -65,6 +65,8 @@ export const ERC721_ACTION = { APPROVAL_FOR_ALL: 'approval_for_all', }; export class Erc721Handler { + erc721Contracts: Dictionary; + // key: {contract_address}_{token_id} // value: erc721 token erc721Tokens: Dictionary; @@ -74,10 +76,12 @@ export class Erc721Handler { erc721HolderStats: Dictionary; constructor( + erc721Contracts: Dictionary, erc721Tokens: Dictionary, erc721Activities: Erc721Activity[], erc721HolderStats: Dictionary ) { + this.erc721Contracts = erc721Contracts; this.erc721Tokens = erc721Tokens; this.erc721Activities = erc721Activities; this.erc721HolderStats = erc721HolderStats; @@ -92,7 +96,8 @@ export class Erc721Handler { } handlerErc721Transfer(erc721Activity: Erc721Activity) { - // update erc721 token + const erc721Contract = + this.erc721Contracts[`${erc721Activity.erc721_contract_address}`]; const token = this.erc721Tokens[ `${erc721Activity.erc721_contract_address}_${erc721Activity.token_id}` @@ -101,6 +106,9 @@ export class Erc721Handler { // update new owner and last updated height token.owner = erc721Activity.to; token.last_updated_height = erc721Activity.height; + if (erc721Activity.to === ZERO_ADDRESS) { + erc721Contract.total_supply -= 1; + } } else if (erc721Activity.from === ZERO_ADDRESS) { // handle mint this.erc721Tokens[ @@ -112,6 +120,7 @@ export class Erc721Handler { last_updated_height: erc721Activity.height, burned: false, }); + erc721Contract.total_supply += 1; } else { throw new Error('Handle erc721 tranfer error'); } @@ -121,6 +130,11 @@ export class Erc721Handler { this.erc721HolderStats[ `${erc721Activity.erc721_contract_address}_${erc721Activity.from}` ]; + if (!erc721HolderStatFrom) { + throw new Error( + `Erc721 holder ${ erc721Activity.from } havent been audited` + ); + } erc721HolderStatFrom.count -= 1; } if (erc721Activity.to !== ZERO_ADDRESS) { @@ -128,7 +142,9 @@ export class Erc721Handler { this.erc721HolderStats[ `${erc721Activity.erc721_contract_address}_${erc721Activity.to}` ]; - erc721HolderStatTo.count += 1; + erc721HolderStatTo.count = erc721HolderStatTo + ? erc721HolderStatTo.count + 1 + : 1; } } @@ -287,11 +303,28 @@ export class Erc721Handler { } static async updateErc721( + erc721Contracts: Erc721Contract[], erc721Activities: Erc721Activity[], erc721Tokens: Erc721Token[], + erc721HolderStats: Erc721HolderStatistic[], trx: Knex.Transaction ) { + // update erc721 contract: total supply + if (erc721Contracts.length > 0) { + const stringListUpdates = erc721Contracts + .map( + (erc721Contract) => + `('${erc721Contract.id}', ${erc721Contract.total_supply})` + ) + .join(','); + await knex + .raw( + `UPDATE erc721_contract SET total_supply = temp.total_supply from (VALUES ${stringListUpdates}) as temp(id, total_supply) where temp.id = erc721_contract.id` + ) + .transacting(trx); + } let updatedTokens: Dictionary = {}; + // update erc721 token: new token & new holder if (erc721Tokens.length > 0) { updatedTokens = _.keyBy( await Erc721Token.query() @@ -311,6 +344,7 @@ export class Erc721Handler { (o) => `${o.erc721_contract_address}_${o.token_id}` ); } + // insert new erc721 activities if (erc721Activities.length > 0) { erc721Activities.forEach((activity) => { const token = @@ -330,6 +364,14 @@ export class Erc721Handler { ) .transacting(trx); } + // update erc721 holder statistic + if (erc721HolderStats.length > 0) { + await Erc721HolderStatistic.query() + .transacting(trx) + .insert(erc721HolderStats) + .onConflict(['erc721_contract_address', 'owner']) + .merge(); + } } static async calErc721Stats(addresses?: string[]): Promise { diff --git a/src/services/evm/erc721_reindex.ts b/src/services/evm/erc721_reindex.ts index 99d56be04..ba0490ec3 100644 --- a/src/services/evm/erc721_reindex.ts +++ b/src/services/evm/erc721_reindex.ts @@ -1,9 +1,11 @@ import Moleculer from 'moleculer'; import { PublicClient, getContract } from 'viem'; +import { Dictionary } from 'lodash'; import knex from '../../common/utils/db_connection'; import { Erc721Activity, Erc721Contract, + Erc721HolderStatistic, Erc721Stats, Erc721Token, } from '../../models'; @@ -74,6 +76,10 @@ export class Erc721Reindexer { ) .first() .throwIfNotFound(); + await Erc721HolderStatistic.query() + .delete() + .where('erc721_contract_address', address) + .transacting(trx); await Erc721Stats.query() .delete() .where('erc721_contract_id', erc721Contract.id) @@ -100,7 +106,10 @@ export class Erc721Reindexer { contract.read.name().catch(() => Promise.resolve(undefined)), contract.read.symbol().catch(() => Promise.resolve(undefined)), ]); - await Erc721Contract.query() + const [tokens, height] = await this.getCurrentTokens(address); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const newErc721Contract: Erc721Contract[] = await Erc721Contract.query() .insert( Erc721Contract.fromJson({ evm_smart_contract_id: erc721Contract.evm_smart_contract_id, @@ -109,10 +118,10 @@ export class Erc721Reindexer { name: contractInfo[0], track: true, last_updated_height: Number(blockHeight), + total_supply: tokens.length, }) ) .transacting(trx); - const [tokens, height] = await this.getCurrentTokens(address); const activities = await Erc721Handler.getErc721Activities( 0, height, @@ -120,7 +129,23 @@ export class Erc721Reindexer { [address], trx ); - await Erc721Handler.updateErc721(activities, tokens, trx); + const erc721HolderStats: Dictionary = + tokens.reduce((acc: Dictionary, curr) => { + const count = acc[curr.owner] ? acc[curr.owner].count + 1 : 1; + acc[curr.owner] = Erc721HolderStatistic.fromJson({ + erc721_contract_address: address, + owner: curr.owner, + count, + }); + return acc; + }, {}); + await Erc721Handler.updateErc721( + newErc721Contract, + activities, + tokens, + Object.values(erc721HolderStats), + trx + ); }); const erc721Stats = await Erc721Handler.calErc721Stats([address]); if (erc721Stats.length > 0) { From d8354680898ebe90604928733c38b099ac6051bc Mon Sep 17 00:00:00 2001 From: phamphong9981 Date: Mon, 16 Sep 2024 16:50:13 +0700 Subject: [PATCH 3/8] test: test --- .../20240912082032_erc721_holder_statistic.ts | 5 +- ...40916032201_erc721_contract_totalSupply.ts | 4 +- src/models/erc721_contract.ts | 2 +- src/models/erc721_holder_statistic.ts | 4 +- src/services/evm/erc721_handler.ts | 38 +++- .../{erc721 => evm}/erc721-media.spec.ts | 0 .../services/{erc721 => evm}/erc721.spec.ts | 119 ++++++----- .../{erc721 => evm}/erc721_handler.spec.ts | 184 +++++++++++++++--- 8 files changed, 254 insertions(+), 102 deletions(-) rename test/unit/services/{erc721 => evm}/erc721-media.spec.ts (100%) rename test/unit/services/{erc721 => evm}/erc721.spec.ts (66%) rename test/unit/services/{erc721 => evm}/erc721_handler.spec.ts (62%) diff --git a/migrations/evm/20240912082032_erc721_holder_statistic.ts b/migrations/evm/20240912082032_erc721_holder_statistic.ts index a4cb132f1..6767b859f 100644 --- a/migrations/evm/20240912082032_erc721_holder_statistic.ts +++ b/migrations/evm/20240912082032_erc721_holder_statistic.ts @@ -6,10 +6,11 @@ export async function up(knex: Knex): Promise { select count(*), erc721_token.owner, erc721_token.erc721_contract_address from erc721_token group by erc721_token.owner, erc721_token.erc721_contract_address; + ALTER TABLE erc721_holder_statistic ADD COLUMN id SERIAL PRIMARY KEY; CREATE INDEX erc721_holder_statistic_owner_index ON erc721_holder_statistic (owner); - CREATE INDEX erc721_holder_statistic_erc721_contract_address_count_index - ON erc721_holder_statistic (erc721_contract_address, count); + CREATE UNIQUE INDEX erc721_holder_statistic_erc721_contract_address_owner_index + ON erc721_holder_statistic (erc721_contract_address, owner); `); } diff --git a/migrations/evm/20240916032201_erc721_contract_totalSupply.ts b/migrations/evm/20240916032201_erc721_contract_totalSupply.ts index 7d6570d64..0f05f918d 100644 --- a/migrations/evm/20240916032201_erc721_contract_totalSupply.ts +++ b/migrations/evm/20240916032201_erc721_contract_totalSupply.ts @@ -3,7 +3,7 @@ import { Erc721Token } from '../../src/models'; export async function up(knex: Knex): Promise { await knex.schema.alterTable('erc721_contract', (table) => { - table.integer('total_supply').defaultTo(0).index(); + table.bigInteger('total_supply').defaultTo(0).index(); }); await knex.raw(`set statement_timeout to 0`); const totalSupplies = await Erc721Token.query(knex) @@ -14,7 +14,7 @@ export async function up(knex: Knex): Promise { const stringListUpdates = totalSupplies .map( (totalSuply) => - `('${totalSuply.erc721_contract_address}', ${totalSuply.count})` + `('${totalSuply.erc721_contract_address}', '${totalSuply.count}')` ) .join(','); await knex.raw( diff --git a/src/models/erc721_contract.ts b/src/models/erc721_contract.ts index e1d78dce5..4aaba11cd 100644 --- a/src/models/erc721_contract.ts +++ b/src/models/erc721_contract.ts @@ -25,7 +25,7 @@ export class Erc721Contract extends BaseModel { last_updated_height!: number; - total_supply!: number; + total_supply!: string; static get tableName() { return 'erc721_contract'; diff --git a/src/models/erc721_holder_statistic.ts b/src/models/erc721_holder_statistic.ts index dd55fdd1a..3f8d16f72 100644 --- a/src/models/erc721_holder_statistic.ts +++ b/src/models/erc721_holder_statistic.ts @@ -7,7 +7,7 @@ export class Erc721HolderStatistic extends BaseModel { owner!: string; - count!: number; + count!: string; static get tableName() { return 'erc721_holder_statistic'; @@ -20,7 +20,7 @@ export class Erc721HolderStatistic extends BaseModel { properties: { erc721_contract_address: { type: 'string' }, owner: { type: 'string' }, - count: { type: 'number' }, + count: { type: 'string' }, }, }; } diff --git a/src/services/evm/erc721_handler.ts b/src/services/evm/erc721_handler.ts index 0ab06978c..4515aea52 100644 --- a/src/services/evm/erc721_handler.ts +++ b/src/services/evm/erc721_handler.ts @@ -107,7 +107,9 @@ export class Erc721Handler { token.owner = erc721Activity.to; token.last_updated_height = erc721Activity.height; if (erc721Activity.to === ZERO_ADDRESS) { - erc721Contract.total_supply -= 1; + erc721Contract.total_supply = ( + BigInt(erc721Contract.total_supply) - BigInt(1) + ).toString(); } } else if (erc721Activity.from === ZERO_ADDRESS) { // handle mint @@ -120,7 +122,9 @@ export class Erc721Handler { last_updated_height: erc721Activity.height, burned: false, }); - erc721Contract.total_supply += 1; + erc721Contract.total_supply = ( + BigInt(erc721Contract.total_supply) + BigInt(1) + ).toString(); } else { throw new Error('Handle erc721 tranfer error'); } @@ -132,19 +136,27 @@ export class Erc721Handler { ]; if (!erc721HolderStatFrom) { throw new Error( - `Erc721 holder ${ erc721Activity.from } havent been audited` + `Erc721 holder ${erc721Activity.from} havent been audited` ); } - erc721HolderStatFrom.count -= 1; + erc721HolderStatFrom.count = ( + BigInt(erc721HolderStatFrom.count) - BigInt(1) + ).toString(); } if (erc721Activity.to !== ZERO_ADDRESS) { const erc721HolderStatTo = this.erc721HolderStats[ `${erc721Activity.erc721_contract_address}_${erc721Activity.to}` ]; - erc721HolderStatTo.count = erc721HolderStatTo - ? erc721HolderStatTo.count + 1 - : 1; + this.erc721HolderStats[ + `${erc721Activity.erc721_contract_address}_${erc721Activity.to}` + ] = Erc721HolderStatistic.fromJson({ + erc721_contract_address: erc721Activity.erc721_contract_address, + owner: erc721Activity.to, + count: erc721HolderStatTo + ? (BigInt(erc721HolderStatTo.count) + BigInt(1)).toString() + : BigInt(1).toString(), + }); } } @@ -314,7 +326,7 @@ export class Erc721Handler { const stringListUpdates = erc721Contracts .map( (erc721Contract) => - `('${erc721Contract.id}', ${erc721Contract.total_supply})` + `(${erc721Contract.id}, ${erc721Contract.total_supply})` ) .join(','); await knex @@ -368,7 +380,15 @@ export class Erc721Handler { if (erc721HolderStats.length > 0) { await Erc721HolderStatistic.query() .transacting(trx) - .insert(erc721HolderStats) + .insert( + erc721HolderStats.map((e) => + Erc721HolderStatistic.fromJson({ + erc721_contract_address: e.erc721_contract_address, + owner: e.owner, + count: e.count, + }) + ) + ) .onConflict(['erc721_contract_address', 'owner']) .merge(); } diff --git a/test/unit/services/erc721/erc721-media.spec.ts b/test/unit/services/evm/erc721-media.spec.ts similarity index 100% rename from test/unit/services/erc721/erc721-media.spec.ts rename to test/unit/services/evm/erc721-media.spec.ts diff --git a/test/unit/services/erc721/erc721.spec.ts b/test/unit/services/evm/erc721.spec.ts similarity index 66% rename from test/unit/services/erc721/erc721.spec.ts rename to test/unit/services/evm/erc721.spec.ts index d2b8c30b0..b1735712c 100644 --- a/test/unit/services/erc721/erc721.spec.ts +++ b/test/unit/services/evm/erc721.spec.ts @@ -6,6 +6,7 @@ import { Test, } from '@jest-decorated/core'; import { ServiceBroker } from 'moleculer'; +import _ from 'lodash'; import knex from '../../../../src/common/utils/db_connection'; import { BlockCheckpoint, @@ -13,11 +14,13 @@ import { EVMTransaction, Erc721Activity, Erc721Contract, + Erc721HolderStatistic, Erc721Token, EvmEvent, } from '../../../../src/models'; import Erc721Service from '../../../../src/services/evm/erc721.service'; import { ERC721_ACTION } from '../../../../src/services/evm/erc721_handler'; +import { BULL_JOB_NAME } from '../../../../src/services/evm/constant'; @Describe('Test erc721') export default class Erc721Test { @@ -27,22 +30,24 @@ export default class Erc721Test { evmSmartContract = EVMSmartContract.fromJson({ id: 555, - address: 'ghghdfgdsgre', - creator: 'dfgdfbvxcvxgfds', + address: '0x98605ae21dd3be686337a6d7a8f156d0d8baee92', + creator: '0xc7663c6a454fc9971C93235A170c8997e8c5E661', created_height: 100, - created_hash: 'cvxcvcxv', + created_hash: + '0x9ed3b06713baf17b7d7266294a82d51c36c84514a7d29f28585d85e586249525', type: EVMSmartContract.TYPES.ERC721, - code_hash: 'dfgdfghf', + code_hash: '0x623d4ffdfa5555aa1234366sdaff1a1sdfafa4dasdfas4r', }); evmSmartContract2 = EVMSmartContract.fromJson({ id: 666, - address: 'bcvbcvbcv', - creator: 'dfgdfbvxcvxgfds', + address: '0xc211C2CF383A38933f8352CBDd326B51b94574e6', + creator: '0xc7663c6a454fc9971C93235A170c8997e8c5E661', created_height: 100, - created_hash: 'xdasfsf', + created_hash: + '0x888d3b06713baf17b7d7266294a82d51c36c84514a7d29f28585d85e586252200', type: EVMSmartContract.TYPES.ERC721, - code_hash: 'xcsadf', + code_hash: '0x5fdfa5555aa1234366sdaff1a1sdfafa4dasdfas4r', }); evmTx = EVMTransaction.fromJson({ @@ -53,10 +58,11 @@ export default class Erc721Test { tx_id: 223, contract_address: '', index: 1, + from: '0x38828FA9766dE6eb49011fCC970ed1beFE15974a', }); evmEvent = EvmEvent.fromJson({ - id: 888, + id: 1, tx_id: 1234, evm_tx_id: this.evmTx.id, tx_hash: '', @@ -73,12 +79,23 @@ export default class Erc721Test { address: this.evmSmartContract.address, }); + blockCheckpoints = [ + BlockCheckpoint.fromJson({ + job_name: BULL_JOB_NAME.HANDLE_ERC721_ACTIVITY, + height: this.evmTx.height - 1, + }), + BlockCheckpoint.fromJson({ + job_name: BULL_JOB_NAME.HANDLE_ERC721_CONTRACT, + height: 400, + }), + ]; + @BeforeAll() async initSuite() { - this.erc721Service.getQueueManager().stopAll(); + await this.erc721Service.getQueueManager().stopAll(); await this.broker.start(); await knex.raw( - 'TRUNCATE TABLE erc721_contract, account, erc721_activity, evm_smart_contract, evm_event, evm_transaction, block_checkpoint RESTART IDENTITY CASCADE' + 'TRUNCATE TABLE erc721_contract, account, erc721_activity, evm_smart_contract, evm_event, evm_transaction, block_checkpoint, erc721_holder_statistic RESTART IDENTITY CASCADE' ); await EVMSmartContract.query().insert([ this.evmSmartContract, @@ -92,8 +109,9 @@ export default class Erc721Test { @BeforeEach() async beforeEach() { await knex.raw( - 'TRUNCATE TABLE erc721_activity, erc721_token RESTART IDENTITY CASCADE' + 'TRUNCATE TABLE erc721_activity, erc721_token, block_checkpoint, evm_event RESTART IDENTITY CASCADE' ); + await BlockCheckpoint.query().insert(this.blockCheckpoints); } @AfterAll() @@ -105,27 +123,19 @@ export default class Erc721Test { @Test('test handleErc721Activity') async testHandleErc721Activity() { - jest.spyOn(BlockCheckpoint, 'getCheckpoint').mockResolvedValue([ - this.evmSmartContract.created_height - 1, - this.evmSmartContract.created_height, - BlockCheckpoint.fromJson({ - job_name: 'dfdsfgsg', - height: this.evmSmartContract.created_height - 1, - }), - ]); + const holder1 = '0x1317df02a4e712265f5376a9d34156f73ebad640'; + const holder2 = '0xa3b6d252c1df2ce88f01fdb75b5479bcdc8f5007'; const erc721Events = [ EvmEvent.fromJson({ - id: this.evmEvent.id, block_hash: '0x6d70a03cda3fb815b54742fbd47c6141a7e754ff4d7426f10a73644ac44411d2', - block_height: 21937980, + block_height: this.blockCheckpoints[0].height + 1, data: null, topic0: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', topic1: '0x0000000000000000000000000000000000000000000000000000000000000000', - topic2: - '0x0000000000000000000000001317df02a4e712265f5376a9d34156f73ebad640', + topic2: `0x000000000000000000000000${ holder1.slice(2)}`, topic3: '0x0000000000000000000000000000000000000000000000000000000000000000', address: this.evmSmartContract.address, @@ -133,22 +143,16 @@ export default class Erc721Test { tx_id: 1234, tx_hash: this.evmTx.hash, tx_index: 1, - sender: 'fgfdg', - evm_smart_contract_id: this.evmSmartContract.id, - track: true, }), EvmEvent.fromJson({ - id: this.evmEvent.id, block_hash: '0xd39b1e6c35a7985db6ca367b1e061162b7a8610097e99cadaf98bea6b81a6096', - block_height: 21937981, + block_height: this.blockCheckpoints[0].height + 2, data: null, topic0: '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925', - topic1: - '0x0000000000000000000000001317df02a4e712265f5376a9d34156f73ebad640', - topic2: - '0x000000000000000000000000a3b6d252c1df2ce88f01fdb75b5479bcdc8f5007', + topic1: `0x000000000000000000000000${ holder1.slice(2)}`, + topic2: `0x000000000000000000000000${ holder2.slice(2)}`, topic3: '0x0000000000000000000000000000000000000000000000000000000000000000', address: this.evmSmartContract.address, @@ -156,22 +160,16 @@ export default class Erc721Test { tx_id: 1234, tx_hash: this.evmTx.hash, tx_index: 1, - sender: 'fgfdg', - evm_smart_contract_id: this.evmSmartContract.id, - track: true, }), EvmEvent.fromJson({ - id: this.evmEvent.id, block_hash: '0x6d70a03cda3fb815b54742fbd47c6141a7e754ff4d7426f10a73644ac44411d2', - block_height: 21937982, + block_height: this.blockCheckpoints[0].height + 3, data: null, topic0: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', - topic1: - '0x0000000000000000000000001317df02a4e712265f5376a9d34156f73ebad640', - topic2: - '0x000000000000000000000000e39633931ec4a1841e438b15005a6f141d30789e', + topic1: `0x000000000000000000000000${ holder1.slice(2)}`, + topic2: `0x000000000000000000000000${ holder2.slice(2)}`, topic3: '0x0000000000000000000000000000000000000000000000000000000000000000', address: this.evmSmartContract.address, @@ -179,27 +177,14 @@ export default class Erc721Test { tx_id: 1234, tx_hash: this.evmTx.hash, tx_index: 1, - sender: 'fgfdg', - evm_smart_contract_id: this.evmSmartContract.id, - track: true, }), ]; - const mockQueryEvents: any = { - select: () => erc721Events, - transacting: () => mockQueryEvents, - joinRelated: () => mockQueryEvents, - where: () => mockQueryEvents, - andWhere: () => mockQueryEvents, - orderBy: () => mockQueryEvents, - innerJoin: () => mockQueryEvents, - modify: () => mockQueryEvents, - }; - jest.spyOn(EvmEvent, 'query').mockImplementation(() => mockQueryEvents); + await EvmEvent.query().insert(erc721Events); await this.erc721Service.handleErc721Activity(); const erc721Token = await Erc721Token.query().first().throwIfNotFound(); expect(erc721Token).toMatchObject({ token_id: '0', - owner: '0xe39633931ec4a1841e438b15005a6f141d30789e', + owner: holder2, erc721_contract_address: this.evmSmartContract.address, }); const erc721Activities = await Erc721Activity.query().orderBy('height'); @@ -207,22 +192,32 @@ export default class Erc721Test { action: ERC721_ACTION.TRANSFER, erc721_contract_address: erc721Events[0].address, from: '0x0000000000000000000000000000000000000000', - to: '0x1317df02a4e712265f5376a9d34156f73ebad640', + to: holder1, erc721_token_id: erc721Token.id, }); expect(erc721Activities[1]).toMatchObject({ action: ERC721_ACTION.APPROVAL, erc721_contract_address: erc721Events[0].address, - from: '0x1317df02a4e712265f5376a9d34156f73ebad640', - to: '0xa3b6d252c1df2ce88f01fdb75b5479bcdc8f5007', + from: holder1, + to: holder2, erc721_token_id: erc721Token.id, }); expect(erc721Activities[2]).toMatchObject({ action: ERC721_ACTION.TRANSFER, erc721_contract_address: erc721Events[0].address, - from: '0x1317df02a4e712265f5376a9d34156f73ebad640', - to: '0xe39633931ec4a1841e438b15005a6f141d30789e', + from: holder1, + to: holder2, erc721_token_id: erc721Token.id, }); + const erc721Contract = await Erc721Contract.query() + .first() + .throwIfNotFound(); + expect(erc721Contract.total_supply).toEqual('1'); + const erc721HolderStats = _.keyBy( + await Erc721HolderStatistic.query(), + 'owner' + ); + expect(erc721HolderStats[holder1].count).toEqual('0'); + expect(erc721HolderStats[holder2].count).toEqual('1'); } } diff --git a/test/unit/services/erc721/erc721_handler.spec.ts b/test/unit/services/evm/erc721_handler.spec.ts similarity index 62% rename from test/unit/services/erc721/erc721_handler.spec.ts rename to test/unit/services/evm/erc721_handler.spec.ts index 6c5a752a6..873188d04 100644 --- a/test/unit/services/erc721/erc721_handler.spec.ts +++ b/test/unit/services/evm/erc721_handler.spec.ts @@ -1,6 +1,5 @@ -import { BeforeAll, Describe, Test } from '@jest-decorated/core'; +import { AfterAll, BeforeAll, Describe, Test } from '@jest-decorated/core'; import { ServiceBroker } from 'moleculer'; -import { bytesToHex, hexToBytes } from 'viem'; import config from '../../../../config.json' assert { type: 'json' }; import knex from '../../../../src/common/utils/db_connection'; import { @@ -22,35 +21,36 @@ export default class Erc721HandlerTest { broker = new ServiceBroker({ logger: false }); evmSmartContract = EVMSmartContract.fromJson({ - id: 555, - address: 'ghghdfgdsgre', - creator: 'dfgdfbvxcvxgfds', + id: 1, + address: '0xE974cC14c93FC6077B0d65F98832B846C5454A0B', + creator: '0x5606b4eA93F696Dd82Ca983BAF5723d00729f127', created_height: 100, - created_hash: 'cvxcvcxv', + created_hash: + '0x8c46cf6373f2f6e528b56becf0ce6b460b5d90cf9b0325a136b9b3a820e1e489', + code_hash: '0xdsf3335453454tsgfsdrtserf43645y4h4tAAvfgfgds', type: EVMSmartContract.TYPES.ERC721, - code_hash: 'dfgdfghf', }); evmSmartContract2 = EVMSmartContract.fromJson({ - id: 666, - address: 'bcvbcvbcv', - creator: 'dfgdfbvxcvxgfds', + id: 2, + address: '0x3CB367e7C920Ff15879Bd4CBd708b8c60eB0f537', + creator: '0xa9497CC4F95773A744D408b54dAC724626ee31d2', created_height: 100, - created_hash: 'xdasfsf', + created_hash: + '0x5bca9ee42c4c32941c58f2a510dae5ff5c6ed848d9a396a8e9e146a166b3a3fc', + code_hash: '0xdfskjgdsgfgweruwie4535t3tu34tjkewtgjwe', type: EVMSmartContract.TYPES.ERC721, - code_hash: 'xcsadf', }); evmTx = EVMTransaction.fromJson({ id: 11111, - hash: hexToBytes( - '0x3faac2ed3ca031892c04598177f7c36e9fdcdf2fb3b6c4a13c520590facb82ef' - ), + hash: '', height: 111, tx_msg_id: 222, tx_id: 223, + contract_address: '', index: 1, - from: hexToBytes('0x51aeade652867f342ddc012e15c27d0cd6220398'), + from: '0x38828FA9766dE6eb49011fCC970ed1beFE15974a', }); erc721Contract1 = Erc721Contract.fromJson({ @@ -83,6 +83,13 @@ export default class Erc721HandlerTest { await EVMTransaction.query().insert(this.evmTx); } + @AfterAll() + async tearDown() { + await this.broker.stop(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + } + @Test('test getErc721Activities') async testGetErc721Activities() { await knex.transaction(async (trx) => { @@ -103,7 +110,7 @@ export default class Erc721HandlerTest { address: this.evmSmartContract.address, evm_tx_id: this.evmTx.id, tx_id: 1234, - tx_hash: bytesToHex(this.evmTx.hash), + tx_hash: this.evmTx.hash, tx_index: 1, }), EvmEvent.fromJson({ @@ -122,7 +129,7 @@ export default class Erc721HandlerTest { address: this.evmSmartContract.address, evm_tx_id: this.evmTx.id, tx_id: 1234, - tx_hash: bytesToHex(this.evmTx.hash), + tx_hash: this.evmTx.hash, tx_index: 1, }), EvmEvent.fromJson({ @@ -141,7 +148,7 @@ export default class Erc721HandlerTest { address: this.evmSmartContract2.address, evm_tx_id: this.evmTx.id, tx_id: 1234, - tx_hash: bytesToHex(this.evmTx.hash), + tx_hash: this.evmTx.hash, tx_index: 1, }), EvmEvent.fromJson({ @@ -150,17 +157,16 @@ export default class Erc721HandlerTest { block_height: 21937982, data: null, topic0: - '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31', topic1: '0x0000000000000000000000001317df02a4e712265f5376a9d34156f73ebad640', topic2: '0x000000000000000000000000e39633931ec4a1841e438b15005a6f141d30789e', - topic3: - '0x0000000000000000000000000000000000000000000000000000000000000000', + topic3: null, address: this.evmSmartContract.address, evm_tx_id: this.evmTx.id, tx_id: 1234, - tx_hash: bytesToHex(this.evmTx.hash), + tx_hash: this.evmTx.hash, tx_index: 1, }), ]; @@ -191,11 +197,12 @@ export default class Erc721HandlerTest { to: '0xa3b6d252c1df2ce88f01fdb75b5479bcdc8f5007', }); expect(erc721Activities[3]).toMatchObject({ - action: ERC721_ACTION.TRANSFER, + action: ERC721_ACTION.APPROVAL_FOR_ALL, erc721_contract_address: this.evmSmartContract.address, from: '0x1317df02a4e712265f5376a9d34156f73ebad640', to: '0xe39633931ec4a1841e438b15005a6f141d30789e', }); + // get a contract's activities const erc721ActivitiesByContract = await Erc721Handler.getErc721Activities( this.evmTx.height - 1, @@ -214,6 +221,135 @@ export default class Erc721HandlerTest { }); } + @Test('test process') + async testProcess() { + const holders = [ + '0x38828FA9766dE6eb49011fCC970ed1beFE15974a', + '0x27CF763feEE58f14C2c69663EA8De784FCF2Dbbb', + '0xE1dbb634e2aefbe63085CB0b60f4155f9ABCa48f', + ]; + const initErc721Contract = Erc721Contract.fromJson({ + evm_smart_contract_id: 1, + address: '0x4c5f56a2FF75a8617337E33f75EB459Db422916F', + symbol: 'ITCIPA', + name: 'Iliad Testnet Commemorative IP Asset', + track: true, + last_updated_height: 0, + total_supply: '3', + }); + const initErc721Tokens = {}; + const initErc721HolderStats = {}; + const erc721Activities = [ + // mint token 0 to holder0 + Erc721Activity.fromJson({ + action: ERC721_ACTION.TRANSFER, + erc721_contract_address: initErc721Contract.address, + from: '0x0000000000000000000000000000000000000000', + to: holders[0], + token_id: '0', + height: 1, + evm_event_id: 1, + }), + // mint token 1 to holder0 + Erc721Activity.fromJson({ + action: ERC721_ACTION.TRANSFER, + erc721_contract_address: initErc721Contract.address, + from: '0x0000000000000000000000000000000000000000', + to: holders[0], + token_id: '1', + height: 1, + evm_event_id: 1, + }), + // mint token 2 to holder1 + Erc721Activity.fromJson({ + action: ERC721_ACTION.TRANSFER, + erc721_contract_address: initErc721Contract.address, + from: '0x0000000000000000000000000000000000000000', + to: holders[1], + token_id: '2', + height: 1, + evm_event_id: 1, + }), + // mint token 3 to holder0 + Erc721Activity.fromJson({ + action: ERC721_ACTION.TRANSFER, + erc721_contract_address: initErc721Contract.address, + from: '0x0000000000000000000000000000000000000000', + to: holders[0], + token_id: '3', + height: 1, + evm_event_id: 1, + }), + // transfer token 0 from holder0 to holder2 + Erc721Activity.fromJson({ + action: ERC721_ACTION.TRANSFER, + erc721_contract_address: initErc721Contract.address, + from: holders[0], + to: holders[2], + token_id: '0', + height: 1, + evm_event_id: 1, + }), + // transfer token 1 from holder0 to holder1 + Erc721Activity.fromJson({ + action: ERC721_ACTION.TRANSFER, + erc721_contract_address: initErc721Contract.address, + from: holders[0], + to: holders[1], + token_id: '1', + height: 1, + evm_event_id: 1, + }), + // burn token 3 + // mint token 0 to holder0 + Erc721Activity.fromJson({ + action: ERC721_ACTION.TRANSFER, + erc721_contract_address: initErc721Contract.address, + from: holders[0], + to: '0x0000000000000000000000000000000000000000', + token_id: '3', + height: 1, + evm_event_id: 1, + }), + ]; + const erc721Handler = new Erc721Handler( + { [initErc721Contract.address]: initErc721Contract }, + initErc721Tokens, + erc721Activities, + initErc721HolderStats + ); + erc721Handler.process(); + const updatedErc721Contract = + erc721Handler.erc721Contracts[initErc721Contract.address]; + + // check total supply + expect(updatedErc721Contract.total_supply).toEqual('6'); + // check new owner for each token + const updatedTokens = erc721Handler.erc721Tokens; + expect(updatedTokens[`${initErc721Contract.address}_0`].owner).toEqual( + holders[2] + ); + expect(updatedTokens[`${initErc721Contract.address}_1`].owner).toEqual( + holders[1] + ); + expect(updatedTokens[`${initErc721Contract.address}_2`].owner).toEqual( + holders[1] + ); + const updatedErc721HolderStats = erc721Handler.erc721HolderStats; + expect( + updatedErc721HolderStats[`${initErc721Contract.address}_${holders[0]}`] + .count + ).toEqual('0'); + expect( + updatedErc721HolderStats[`${initErc721Contract.address}_${holders[1]}`] + .count + ).toEqual('2'); + expect( + updatedErc721HolderStats[`${initErc721Contract.address}_${holders[2]}`] + .count + ).toEqual('1'); + } + @Test('test calErc721Stats') async testCalErc721Stats() { const evmEvent = EvmEvent.fromJson({ From cfdd5fcbf2ae93709b1777e8933828496783f40d Mon Sep 17 00:00:00 2001 From: phamphong9981 Date: Tue, 17 Sep 2024 10:50:18 +0700 Subject: [PATCH 4/8] fix: migration --- migrations/evm/20240916032201_erc721_contract_totalSupply.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/migrations/evm/20240916032201_erc721_contract_totalSupply.ts b/migrations/evm/20240916032201_erc721_contract_totalSupply.ts index 0f05f918d..fc4b0aaa3 100644 --- a/migrations/evm/20240916032201_erc721_contract_totalSupply.ts +++ b/migrations/evm/20240916032201_erc721_contract_totalSupply.ts @@ -1,5 +1,6 @@ import { Knex } from 'knex'; import { Erc721Token } from '../../src/models'; +import { ZERO_ADDRESS } from '../../src/services/evm/constant'; export async function up(knex: Knex): Promise { await knex.schema.alterTable('erc721_contract', (table) => { @@ -8,13 +9,14 @@ export async function up(knex: Knex): Promise { await knex.raw(`set statement_timeout to 0`); const totalSupplies = await Erc721Token.query(knex) .select('erc721_token.erc721_contract_address') + .where('erc721_token.owner', '!=', ZERO_ADDRESS) .count() .groupBy('erc721_token.erc721_contract_address'); if (totalSupplies.length > 0) { const stringListUpdates = totalSupplies .map( (totalSuply) => - `('${totalSuply.erc721_contract_address}', '${totalSuply.count}')` + `('${totalSuply.erc721_contract_address}', ${totalSuply.count})` ) .join(','); await knex.raw( From b723c034c6db2e33996001bc5df8368433283aec Mon Sep 17 00:00:00 2001 From: phamphong9981 Date: Tue, 17 Sep 2024 10:52:50 +0700 Subject: [PATCH 5/8] fix: lint --- network.json | 10 ++++++++++ test/unit/services/evm/erc721.spec.ts | 10 +++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/network.json b/network.json index f022ad8df..dfc1ac9aa 100644 --- a/network.json +++ b/network.json @@ -116,5 +116,15 @@ ], "EVMchainId": 1513, "moleculerNamespace": "erascope-dev-story-testnet" + }, + { + "chainId": "story-protocol", + "RPC": ["https://testnet.storyrpc.io"], + "LCD": ["https://lcd.dev.aura.network"], + "databaseName": "horoscope_dev_story_testnet", + "redisDBNumber": 4, + "moleculerNamespace": "namespace-auratestnet", + "EVMJSONRPC": ["https://testnet.storyrpc.io"], + "EVMchainId": 1513 } ] diff --git a/test/unit/services/evm/erc721.spec.ts b/test/unit/services/evm/erc721.spec.ts index b1735712c..ad01f9554 100644 --- a/test/unit/services/evm/erc721.spec.ts +++ b/test/unit/services/evm/erc721.spec.ts @@ -135,7 +135,7 @@ export default class Erc721Test { '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', topic1: '0x0000000000000000000000000000000000000000000000000000000000000000', - topic2: `0x000000000000000000000000${ holder1.slice(2)}`, + topic2: `0x000000000000000000000000${holder1.slice(2)}`, topic3: '0x0000000000000000000000000000000000000000000000000000000000000000', address: this.evmSmartContract.address, @@ -151,8 +151,8 @@ export default class Erc721Test { data: null, topic0: '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925', - topic1: `0x000000000000000000000000${ holder1.slice(2)}`, - topic2: `0x000000000000000000000000${ holder2.slice(2)}`, + topic1: `0x000000000000000000000000${holder1.slice(2)}`, + topic2: `0x000000000000000000000000${holder2.slice(2)}`, topic3: '0x0000000000000000000000000000000000000000000000000000000000000000', address: this.evmSmartContract.address, @@ -168,8 +168,8 @@ export default class Erc721Test { data: null, topic0: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', - topic1: `0x000000000000000000000000${ holder1.slice(2)}`, - topic2: `0x000000000000000000000000${ holder2.slice(2)}`, + topic1: `0x000000000000000000000000${holder1.slice(2)}`, + topic2: `0x000000000000000000000000${holder2.slice(2)}`, topic3: '0x0000000000000000000000000000000000000000000000000000000000000000', address: this.evmSmartContract.address, From 8bff6e595d5a4d1f2daafebfd7d145c0e6d0567a Mon Sep 17 00:00:00 2001 From: phamphong9981 Date: Tue, 17 Sep 2024 10:54:47 +0700 Subject: [PATCH 6/8] fix: lint --- network.json | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/network.json b/network.json index dfc1ac9aa..f022ad8df 100644 --- a/network.json +++ b/network.json @@ -116,15 +116,5 @@ ], "EVMchainId": 1513, "moleculerNamespace": "erascope-dev-story-testnet" - }, - { - "chainId": "story-protocol", - "RPC": ["https://testnet.storyrpc.io"], - "LCD": ["https://lcd.dev.aura.network"], - "databaseName": "horoscope_dev_story_testnet", - "redisDBNumber": 4, - "moleculerNamespace": "namespace-auratestnet", - "EVMJSONRPC": ["https://testnet.storyrpc.io"], - "EVMchainId": 1513 } ] From 145f59147607e97a0d3653c5618cebf2e51ad4a4 Mon Sep 17 00:00:00 2001 From: phamphong9981 Date: Thu, 19 Sep 2024 10:41:40 +0700 Subject: [PATCH 7/8] refactor: review --- .../20240912082032_erc721_holder_statistic.ts | 2 + src/models/erc721_holder_statistic.ts | 2 + src/services/evm/erc721.service.ts | 47 ++++++++--------- src/services/evm/erc721_handler.ts | 50 +++++++++---------- src/services/evm/erc721_reindex.ts | 21 ++++---- test/unit/services/evm/erc721_handler.spec.ts | 37 +++++++++----- 6 files changed, 85 insertions(+), 74 deletions(-) diff --git a/migrations/evm/20240912082032_erc721_holder_statistic.ts b/migrations/evm/20240912082032_erc721_holder_statistic.ts index 6767b859f..bbecb92b3 100644 --- a/migrations/evm/20240912082032_erc721_holder_statistic.ts +++ b/migrations/evm/20240912082032_erc721_holder_statistic.ts @@ -7,10 +7,12 @@ export async function up(knex: Knex): Promise { from erc721_token group by erc721_token.owner, erc721_token.erc721_contract_address; ALTER TABLE erc721_holder_statistic ADD COLUMN id SERIAL PRIMARY KEY; + ALTER TABLE erc721_holder_statistic ADD COLUMN last_updated_height INTEGER; CREATE INDEX erc721_holder_statistic_owner_index ON erc721_holder_statistic (owner); CREATE UNIQUE INDEX erc721_holder_statistic_erc721_contract_address_owner_index ON erc721_holder_statistic (erc721_contract_address, owner); + CREATE INDEX erc721_holder_statistic_last_updated_height_index ON erc721_holder_statistic (last_updated_height) `); } diff --git a/src/models/erc721_holder_statistic.ts b/src/models/erc721_holder_statistic.ts index 3f8d16f72..1fd62b660 100644 --- a/src/models/erc721_holder_statistic.ts +++ b/src/models/erc721_holder_statistic.ts @@ -9,6 +9,8 @@ export class Erc721HolderStatistic extends BaseModel { count!: string; + last_updated_height!: number; + static get tableName() { return 'erc721_holder_statistic'; } diff --git a/src/services/evm/erc721.service.ts b/src/services/evm/erc721.service.ts index dd7e70d87..49a130b32 100644 --- a/src/services/evm/erc721.service.ts +++ b/src/services/evm/erc721.service.ts @@ -113,8 +113,6 @@ export default class Erc721Service extends BullableService { .transacting(trx), 'address' ); - const erc721HolderStatsOnDB: Erc721HolderStatistic[] = []; - const erc721TokensOnDB: Erc721Token[] = []; for ( let i = 0; i < erc721Activities.length; @@ -127,26 +125,25 @@ export default class Erc721Service extends BullableService { listChunkErc721Activities.push(chunk); } // process chunk array - await Promise.all( - // eslint-disable-next-line array-callback-return - listChunkErc721Activities.map(async (chunk) => { - const erc721TokensInChunk = await Erc721Token.query().whereIn( - ['erc721_contract_address', 'token_id'], - chunk.map((e) => [ - e.erc721_contract_address, - // if token_id undefined (case approval_all), replace by null => not get any token (because token must have token_id) - e.token_id || null, - ]) - ); - erc721TokensOnDB.push(...erc721TokensInChunk); - }) - ); + const erc721TokensOnDB: Erc721Token[] = ( + await Promise.all( + listChunkErc721Activities.map(async (chunk) => + Erc721Token.query().whereIn( + ['erc721_contract_address', 'token_id'], + chunk.map((e) => [ + e.erc721_contract_address, + // if token_id undefined (case approval_all), replace by null => not get any token (because token must have token_id) + e.token_id || null, + ]) + ) + ) + ) + ).flat(); // process chunk array - await Promise.all( - // eslint-disable-next-line array-callback-return - listChunkErc721Activities.map(async (chunk) => { - const erc721HolderStatsInChunk = - await Erc721HolderStatistic.query().whereIn( + const erc721HolderStatsOnDB: Erc721HolderStatistic[] = ( + await Promise.all( + listChunkErc721Activities.map(async (chunk) => + Erc721HolderStatistic.query().whereIn( ['erc721_contract_address', 'owner'], _.uniqWith( [ @@ -155,10 +152,10 @@ export default class Erc721Service extends BullableService { ], _.isEqual ) - ); - erc721HolderStatsOnDB.push(...erc721HolderStatsInChunk); - }) - ); + ) + ) + ) + ).flat(); const erc721Tokens = _.keyBy( erc721TokensOnDB, (o) => `${o.erc721_contract_address}_${o.token_id}` diff --git a/src/services/evm/erc721_handler.ts b/src/services/evm/erc721_handler.ts index 4515aea52..e6de23a0e 100644 --- a/src/services/evm/erc721_handler.ts +++ b/src/services/evm/erc721_handler.ts @@ -129,35 +129,30 @@ export class Erc721Handler { throw new Error('Handle erc721 tranfer error'); } // update erc721 holder statistics - if (erc721Activity.from !== ZERO_ADDRESS) { - const erc721HolderStatFrom = - this.erc721HolderStats[ - `${erc721Activity.erc721_contract_address}_${erc721Activity.from}` - ]; - if (!erc721HolderStatFrom) { - throw new Error( - `Erc721 holder ${erc721Activity.from} havent been audited` - ); - } - erc721HolderStatFrom.count = ( - BigInt(erc721HolderStatFrom.count) - BigInt(1) - ).toString(); - } - if (erc721Activity.to !== ZERO_ADDRESS) { - const erc721HolderStatTo = - this.erc721HolderStats[ - `${erc721Activity.erc721_contract_address}_${erc721Activity.to}` - ]; + const erc721HolderStatFrom = + this.erc721HolderStats[ + `${erc721Activity.erc721_contract_address}_${erc721Activity.from}` + ]; + this.erc721HolderStats[ + `${erc721Activity.erc721_contract_address}_${erc721Activity.from}` + ] = Erc721HolderStatistic.fromJson({ + erc721_contract_address: erc721Activity.erc721_contract_address, + owner: erc721Activity.from, + count: (BigInt(erc721HolderStatFrom?.count || 0) - BigInt(1)).toString(), + last_updated_height: erc721Activity.height, + }); + const erc721HolderStatTo = this.erc721HolderStats[ `${erc721Activity.erc721_contract_address}_${erc721Activity.to}` - ] = Erc721HolderStatistic.fromJson({ - erc721_contract_address: erc721Activity.erc721_contract_address, - owner: erc721Activity.to, - count: erc721HolderStatTo - ? (BigInt(erc721HolderStatTo.count) + BigInt(1)).toString() - : BigInt(1).toString(), - }); - } + ]; + this.erc721HolderStats[ + `${erc721Activity.erc721_contract_address}_${erc721Activity.to}` + ] = Erc721HolderStatistic.fromJson({ + erc721_contract_address: erc721Activity.erc721_contract_address, + owner: erc721Activity.to, + count: (BigInt(erc721HolderStatTo?.count || 0) + BigInt(1)).toString(), + last_updated_height: erc721Activity.height, + }); } static buildTransferActivity( @@ -386,6 +381,7 @@ export class Erc721Handler { erc721_contract_address: e.erc721_contract_address, owner: e.owner, count: e.count, + last_updated_height: e.last_updated_height, }) ) ) diff --git a/src/services/evm/erc721_reindex.ts b/src/services/evm/erc721_reindex.ts index ba0490ec3..dc80f2ede 100644 --- a/src/services/evm/erc721_reindex.ts +++ b/src/services/evm/erc721_reindex.ts @@ -129,16 +129,17 @@ export class Erc721Reindexer { [address], trx ); - const erc721HolderStats: Dictionary = - tokens.reduce((acc: Dictionary, curr) => { - const count = acc[curr.owner] ? acc[curr.owner].count + 1 : 1; - acc[curr.owner] = Erc721HolderStatistic.fromJson({ - erc721_contract_address: address, - owner: curr.owner, - count, - }); - return acc; - }, {}); + const erc721HolderStats: Dictionary = {}; + tokens.forEach((token) => { + const {owner} = token; + const currentCount = erc721HolderStats[owner]?.count || '0'; + const newCount = (BigInt(currentCount) + BigInt(1)).toString(); + erc721HolderStats[owner] = Erc721HolderStatistic.fromJson({ + erc721_contract_address: address, + owner, + count: newCount, + }); + }); await Erc721Handler.updateErc721( newErc721Contract, activities, diff --git a/test/unit/services/evm/erc721_handler.spec.ts b/test/unit/services/evm/erc721_handler.spec.ts index 873188d04..802a05481 100644 --- a/test/unit/services/evm/erc721_handler.spec.ts +++ b/test/unit/services/evm/erc721_handler.spec.ts @@ -267,7 +267,7 @@ export default class Erc721HandlerTest { from: '0x0000000000000000000000000000000000000000', to: holders[1], token_id: '2', - height: 1, + height: 2, evm_event_id: 1, }), // mint token 3 to holder0 @@ -277,7 +277,7 @@ export default class Erc721HandlerTest { from: '0x0000000000000000000000000000000000000000', to: holders[0], token_id: '3', - height: 1, + height: 3, evm_event_id: 1, }), // transfer token 0 from holder0 to holder2 @@ -287,7 +287,7 @@ export default class Erc721HandlerTest { from: holders[0], to: holders[2], token_id: '0', - height: 1, + height: 4, evm_event_id: 1, }), // transfer token 1 from holder0 to holder1 @@ -297,18 +297,17 @@ export default class Erc721HandlerTest { from: holders[0], to: holders[1], token_id: '1', - height: 1, + height: 5, evm_event_id: 1, }), // burn token 3 - // mint token 0 to holder0 Erc721Activity.fromJson({ action: ERC721_ACTION.TRANSFER, erc721_contract_address: initErc721Contract.address, from: holders[0], to: '0x0000000000000000000000000000000000000000', token_id: '3', - height: 1, + height: 6, evm_event_id: 1, }), ]; @@ -338,16 +337,30 @@ export default class Erc721HandlerTest { const updatedErc721HolderStats = erc721Handler.erc721HolderStats; expect( updatedErc721HolderStats[`${initErc721Contract.address}_${holders[0]}`] - .count - ).toEqual('0'); + ).toMatchObject({ + count: '0', + last_updated_height: erc721Activities[6].height, + }); expect( updatedErc721HolderStats[`${initErc721Contract.address}_${holders[1]}`] - .count - ).toEqual('2'); + ).toMatchObject({ + count: '2', + last_updated_height: erc721Activities[5].height, + }); expect( updatedErc721HolderStats[`${initErc721Contract.address}_${holders[2]}`] - .count - ).toEqual('1'); + ).toMatchObject({ + count: '1', + last_updated_height: erc721Activities[4].height, + }); + expect( + updatedErc721HolderStats[ + `${initErc721Contract.address}_0x0000000000000000000000000000000000000000` + ] + ).toMatchObject({ + count: '-3', + last_updated_height: erc721Activities[6].height, + }); } @Test('test calErc721Stats') From d434a5ce2f4728ba0261355fd7de40895d0d9520 Mon Sep 17 00:00:00 2001 From: phamphong9981 Date: Thu, 19 Sep 2024 14:26:47 +0700 Subject: [PATCH 8/8] refactor: review --- src/services/evm/erc721_reindex.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/services/evm/erc721_reindex.ts b/src/services/evm/erc721_reindex.ts index dc80f2ede..0301f256c 100644 --- a/src/services/evm/erc721_reindex.ts +++ b/src/services/evm/erc721_reindex.ts @@ -107,9 +107,7 @@ export class Erc721Reindexer { contract.read.symbol().catch(() => Promise.resolve(undefined)), ]); const [tokens, height] = await this.getCurrentTokens(address); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const newErc721Contract: Erc721Contract[] = await Erc721Contract.query() + const newErc721Contract: Erc721Contract = await Erc721Contract.query() .insert( Erc721Contract.fromJson({ evm_smart_contract_id: erc721Contract.evm_smart_contract_id, @@ -131,7 +129,7 @@ export class Erc721Reindexer { ); const erc721HolderStats: Dictionary = {}; tokens.forEach((token) => { - const {owner} = token; + const { owner } = token; const currentCount = erc721HolderStats[owner]?.count || '0'; const newCount = (BigInt(currentCount) + BigInt(1)).toString(); erc721HolderStats[owner] = Erc721HolderStatistic.fromJson({ @@ -141,7 +139,7 @@ export class Erc721Reindexer { }); }); await Erc721Handler.updateErc721( - newErc721Contract, + [newErc721Contract], activities, tokens, Object.values(erc721HolderStats),