diff --git a/migrations/evm/20240502080101_erc20_statistic.ts b/migrations/evm/20240502080101_erc20_statistic.ts new file mode 100644 index 000000000..8ec81e57e --- /dev/null +++ b/migrations/evm/20240502080101_erc20_statistic.ts @@ -0,0 +1,16 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + await knex.schema.createTable('erc20_statistic', (table) => { + table.increments(); + table.integer('erc20_contract_id').notNullable(); + table.foreign('erc20_contract_id').references('erc20_contract.id'); + table.integer('total_holder'); + table.date('date').index().notNullable(); + table.unique(['erc20_contract_id', 'date']); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists('erc20_statistic'); +} diff --git a/src/models/erc20_contract.ts b/src/models/erc20_contract.ts index 7694b430c..51a58a3c1 100644 --- a/src/models/erc20_contract.ts +++ b/src/models/erc20_contract.ts @@ -3,8 +3,11 @@ import BaseModel from './base'; import { EVMSmartContract } from './evm_smart_contract'; // eslint-disable-next-line import/no-cycle import { Erc20Activity } from './erc20_activity'; +import { AccountBalance } from './account_balance'; export class Erc20Contract extends BaseModel { + [relation: string]: any; + static softDelete = false; id!: number; @@ -61,6 +64,14 @@ export class Erc20Contract extends BaseModel { from: 'erc20_contract.address', }, }, + holders: { + relation: Model.HasManyRelation, + modelClass: AccountBalance, + join: { + to: 'account_balance.denom', + from: 'erc20_contract.address', + }, + }, }; } diff --git a/src/models/erc20_statistic.ts b/src/models/erc20_statistic.ts new file mode 100644 index 000000000..2dfadfa7a --- /dev/null +++ b/src/models/erc20_statistic.ts @@ -0,0 +1,44 @@ +import { Model } from 'objection'; +import BaseModel from './base'; +import { Erc20Contract } from './erc20_contract'; + +export class Erc20Statistic extends BaseModel { + static softDelete = false; + + [relation: string]: any; + + date!: Date; + + erc20_contract_id!: number; + + total_holder!: number; + + static get tableName() { + return 'erc20_statistic'; + } + + static get jsonSchema() { + return { + type: 'object', + required: ['erc20_contract_id', 'total_holder', 'date'], + properties: { + erc20_contract_id: { type: 'number' }, + total_holder: { type: 'number' }, + date: { type: 'object' }, + }, + }; + } + + static get relationMappings() { + return { + erc20_contract: { + relation: Model.BelongsToOneRelation, + modelClass: Erc20Contract, + join: { + from: 'erc20_statistic.erc20_contract_id', + to: 'erc20_contract.id', + }, + }, + }; + } +} diff --git a/src/models/index.ts b/src/models/index.ts index 1d4e16dda..3ff790365 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 './erc20_statistic'; diff --git a/src/services/evm/erc20.service.ts b/src/services/evm/erc20.service.ts index c3fc06503..fe25efb1d 100644 --- a/src/services/evm/erc20.service.ts +++ b/src/services/evm/erc20.service.ts @@ -11,7 +11,12 @@ import BullableService, { QueueHandler } from '../../base/bullable.service'; import { SERVICE as COSMOS_SERVICE, Config } from '../../common'; import knex from '../../common/utils/db_connection'; import { getViemClient } from '../../common/utils/etherjs_client'; -import { BlockCheckpoint, EVMSmartContract } from '../../models'; +import { + Block, + BlockCheckpoint, + EVMSmartContract, + Erc20Statistic, +} from '../../models'; import { Erc20Activity } from '../../models/erc20_activity'; import { Erc20Contract } from '../../models/erc20_contract'; import { BULL_JOB_NAME, SERVICE as EVM_SERVICE, SERVICE } from './constant'; @@ -129,6 +134,7 @@ export default class Erc20Service extends BullableService { [BULL_JOB_NAME.HANDLE_ERC20_ACTIVITY], config.erc20.key ); + await this.handleStatistic(startBlock); // get Erc20 activities let erc20Activities = await Erc20Handler.getErc20Activities( startBlock, @@ -358,6 +364,50 @@ export default class Erc20Service extends BullableService { } } + async handleStatistic(startBlock: number) { + const systemDate = ( + await Block.query() + .where('height', startBlock + 1) + .first() + .throwIfNotFound() + ).time; + const lastUpdatedRecord = await Erc20Statistic.query().max('date').first(); + const lastUpdatedDate = lastUpdatedRecord?.max; + if (lastUpdatedDate) { + systemDate.setHours(0, 0, 0, 0); + lastUpdatedDate.setHours(0, 0, 0, 0); + if (systemDate > lastUpdatedDate) { + await this.handleTotalHolderStatistic(systemDate); + } + } else { + await this.handleTotalHolderStatistic(systemDate); + } + } + + async handleTotalHolderStatistic(systemDate: Date) { + const totalHolders = await Erc20Contract.query() + .joinRelated('holders') + .where('erc20_contract.track', true) + .groupBy('erc20_contract.id') + .select( + 'erc20_contract.id as erc20_contract_id', + knex.raw( + 'count(CASE when holders.amount > 0 THEN 1 ELSE null END) as count' + ) + ); + if (totalHolders.length > 0) { + await Erc20Statistic.query().insert( + totalHolders.map((e) => + Erc20Statistic.fromJson({ + erc20_contract_id: e.erc20_contract_id, + total_holder: e.count, + date: systemDate, + }) + ) + ); + } + } + public async _start(): Promise { this.viemClient = getViemClient(); if (NODE_ENV !== 'test') {