Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: erc20 statistic (total holder) #777

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions migrations/evm/20240502080101_erc20_statistic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
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<void> {
await knex.schema.dropTableIfExists('erc20_statistic');
}
11 changes: 11 additions & 0 deletions src/models/erc20_contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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',
},
},
};
}

Expand Down
44 changes: 44 additions & 0 deletions src/models/erc20_statistic.ts
Original file line number Diff line number Diff line change
@@ -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',
},
},
};
}
}
1 change: 1 addition & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@ export * from './evm_internal_transaction';
export * from './erc721_activity';
export * from './erc721_token';
export * from './account_balance';
export * from './erc20_statistic';
export * from './erc721_contract';
export * from './erc721_stats';
142 changes: 98 additions & 44 deletions src/services/evm/erc20.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,24 @@ import { Context, ServiceBroker } from 'moleculer';
import { PublicClient, getContract } from 'viem';
import config from '../../../config.json' assert { type: 'json' };
import BullableService, { QueueHandler } from '../../base/bullable.service';
import { SERVICE as COSMOS_SERVICE } from '../../common';
import { SERVICE as COSMOS_SERVICE, Config } from '../../common';
import knex from '../../common/utils/db_connection';
import { getViemClient } from '../../common/utils/etherjs_client';
import { BlockCheckpoint, EVMSmartContract, EvmEvent } from '../../models';
import {
Block,
BlockCheckpoint,
EVMSmartContract,
Erc20Statistic,
EvmEvent,
} from '../../models';
import { AccountBalance } from '../../models/account_balance';
import { Erc20Activity } from '../../models/erc20_activity';
import { Erc20Contract } from '../../models/erc20_contract';
import { BULL_JOB_NAME, SERVICE as EVM_SERVICE, SERVICE } from './constant';
import { ERC20_EVENT_TOPIC0, Erc20Handler } from './erc20_handler';
import { convertEthAddressToBech32Address } from './utils';

const { NODE_ENV } = Config;
@Service({
name: EVM_SERVICE.V1.Erc20.key,
version: 1,
Expand Down Expand Up @@ -156,6 +163,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 this.getErc20Activities(startBlock, endBlock);
// get missing Account
Expand Down Expand Up @@ -372,50 +380,96 @@ 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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gộp 2 câu if điều kiện chạy lại
if (!lastUpdatedDate || systemDate > lastUpdatedDate)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Đoạn này em đang check xem là ngày dd/mm/yy (tại thời điểm 0h0p0s) đã thống kê trong DB chưa. Vì mỗi 6s/block nên phải chuyển timestamp về ngày để so sánh đảm bảo 1 ngày 1 thống kê

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'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'(holders.amount > 0)::int as count' đc ko?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ko đc anh ạ

)
);
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,
peara marked this conversation as resolved.
Show resolved Hide resolved
})
)
);
}
}

public async _start(): Promise<void> {
this.viemClient = getViemClient();
await this.createJob(
BULL_JOB_NAME.HANDLE_ERC20_CONTRACT,
BULL_JOB_NAME.HANDLE_ERC20_CONTRACT,
{},
{
removeOnComplete: true,
removeOnFail: {
count: 3,
},
repeat: {
every: config.erc20.millisecondRepeatJob,
},
}
);
await this.createJob(
BULL_JOB_NAME.HANDLE_ERC20_ACTIVITY,
BULL_JOB_NAME.HANDLE_ERC20_ACTIVITY,
{},
{
removeOnComplete: true,
removeOnFail: {
count: 3,
},
repeat: {
every: config.erc20.millisecondRepeatJob,
},
}
);
await this.createJob(
BULL_JOB_NAME.HANDLE_ERC20_BALANCE,
BULL_JOB_NAME.HANDLE_ERC20_BALANCE,
{},
{
removeOnComplete: true,
removeOnFail: {
count: 3,
},
repeat: {
every: config.erc20.millisecondRepeatJob,
},
}
);
if (NODE_ENV !== 'test') {
await this.createJob(
BULL_JOB_NAME.HANDLE_ERC20_CONTRACT,
BULL_JOB_NAME.HANDLE_ERC20_CONTRACT,
{},
{
removeOnComplete: true,
removeOnFail: {
count: 3,
},
repeat: {
every: config.erc20.millisecondRepeatJob,
},
}
);
await this.createJob(
BULL_JOB_NAME.HANDLE_ERC20_ACTIVITY,
BULL_JOB_NAME.HANDLE_ERC20_ACTIVITY,
{},
{
removeOnComplete: true,
removeOnFail: {
count: 3,
},
repeat: {
every: config.erc20.millisecondRepeatJob,
},
}
);
await this.createJob(
BULL_JOB_NAME.HANDLE_ERC20_BALANCE,
BULL_JOB_NAME.HANDLE_ERC20_BALANCE,
{},
{
removeOnComplete: true,
removeOnFail: {
count: 3,
},
repeat: {
every: config.erc20.millisecondRepeatJob,
},
}
);
}
return super._start();
}
}
95 changes: 94 additions & 1 deletion test/unit/services/erc20/erc20.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { AfterAll, BeforeAll, Describe, Test } from '@jest-decorated/core';
import {
AfterAll,
BeforeAll,
BeforeEach,
Describe,
Test,
} from '@jest-decorated/core';
import { ServiceBroker } from 'moleculer';
import _ from 'lodash';
import knex from '../../../../src/common/utils/db_connection';
import {
Account,
EVMSmartContract,
EVMTransaction,
Erc20Activity,
Erc20Contract,
Erc20Statistic,
EvmEvent,
} from '../../../../src/models';
import Erc20Service from '../../../../src/services/evm/erc20.service';
Expand Down Expand Up @@ -60,6 +68,7 @@ export default class Erc20Test {

@BeforeAll()
async initSuite() {
this.erc20Service.getQueueManager().stopAll();
await this.broker.start();
await knex.raw(
'TRUNCATE TABLE erc20_contract, account, erc20_activity, evm_smart_contract, evm_event, evm_transaction RESTART IDENTITY CASCADE'
Expand All @@ -77,6 +86,13 @@ export default class Erc20Test {
await this.broker.stop();
}

@BeforeEach()
async initSuiteBeforeEach() {
await knex.raw(
'TRUNCATE TABLE erc20_activity, account, erc20_statistic RESTART IDENTITY CASCADE'
);
}

@Test('test getErc20Activities')
async testGetErc20Activities() {
const fromAccount = Account.fromJson({
Expand Down Expand Up @@ -189,4 +205,81 @@ export default class Erc20Test {
expect(result[1].from_account_id).toEqual(fromAccount.id);
expect(result[1].to_account_id).toEqual(toAccount.id);
}

@Test('test handleTotalHolderStatistic')
public async testHandleTotalHolderStatistic() {
const accounts = [
Account.fromJson({
id: 345,
address: 'xczfsdfsfsdg',
balances: [],
spendable_balances: [],
type: '',
pubkey: '',
account_number: 2,
sequence: 432,
evm_address: '0xghgfhfghfg',
account_balances: [
{
denom: this.evmSmartContract.address,
amount: 123,
},
{
denom: this.evmSmartContract2.address,
amount: 1234,
},
],
}),
Account.fromJson({
id: 456,
address: 'cbbvb',
balances: [],
spendable_balances: [],
type: '',
pubkey: '',
account_number: 2,
sequence: 432,
evm_address: '0xhgfhfghgfg',
account_balances: [
{
denom: this.evmSmartContract.address,
amount: 0,
},
{
denom: this.evmSmartContract2.address,
amount: -1234,
},
],
}),
Account.fromJson({
id: 567,
address: 'xzxzcvv ',
balances: [],
spendable_balances: [],
type: '',
pubkey: '',
account_number: 2,
sequence: 432,
evm_address: '0xdgfsdgs4',
account_balances: [
{
denom: this.evmSmartContract.address,
amount: 1,
},
],
}),
];
await Account.query().insertGraph(accounts);
const date = new Date('2023-01-12T00:53:57.000Z');
await this.erc20Service.handleTotalHolderStatistic(date);
const erc20Statistics = _.keyBy(
await Erc20Statistic.query(),
'erc20_contract_id'
);
expect(erc20Statistics[444]).toMatchObject({
total_holder: 2,
});
// because of track false
expect(erc20Statistics[445]).toBeUndefined();
}
}
Loading