diff --git a/ci/config.json.ci b/ci/config.json.ci index 245de74f2..31bcf5173 100644 --- a/ci/config.json.ci +++ b/ci/config.json.ci @@ -108,7 +108,10 @@ "cw20": { "blocksPerCall": 100, "millisecondRepeatJob": 2000, - "key": "cw20" + "key": "cw20", + "reindexHistory": { + "limitRecordGet": 500 + } }, "dashboardStatistics": { "millisecondCrawl": 10000, diff --git a/config.json b/config.json index cd07b5711..e2b66e521 100644 --- a/config.json +++ b/config.json @@ -103,7 +103,10 @@ "cw20": { "blocksPerCall": 100, "millisecondRepeatJob": 2000, - "key": "cw20" + "key": "cw20", + "reindexHistory": { + "limitRecordGet": 500 + } }, "crawlContractEvent": { "key": "crawlContractEvent", diff --git a/src/common/constant.ts b/src/common/constant.ts index fcbed0f5f..624cb63ec 100644 --- a/src/common/constant.ts +++ b/src/common/constant.ts @@ -66,6 +66,8 @@ export const BULL_JOB_NAME = { REINDEX_CW721_HISTORY: 'reindex:cw721-history', HANDLE_MIGRATE_CONTRACT: 'handle:migrate-contract', JOB_REDECODE_TX: 'job:redecode-tx', + REINDEX_CW20_CONTRACT: 'reindex:cw20-contract', + REINDEX_CW20_HISTORY: 'reindex:cw20-history', }; export const SERVICE = { @@ -223,6 +225,14 @@ export const SERVICE = { path: 'v1.ReDecodeTx', }, }, + Cw20ReindexingService: { + key: 'Cw20ReindexingService', + name: 'v1.Cw20ReindexingService', + Reindexing: { + key: 'reindexing', + path: 'v1.Cw20ReindexingService.reindexing', + }, + }, }, }; diff --git a/src/models/cw20_activity.ts b/src/models/cw20_activity.ts index ac0646b53..19d8a6eb0 100644 --- a/src/models/cw20_activity.ts +++ b/src/models/cw20_activity.ts @@ -5,6 +5,8 @@ import { Cw20Contract } from './cw20_contract'; import { SmartContract } from './smart_contract'; export class Cw20Event extends BaseModel { + static softDelete = false; + [relation: string]: any; id!: number; diff --git a/src/models/cw20_contract.ts b/src/models/cw20_contract.ts index 7fd748023..b337cf097 100644 --- a/src/models/cw20_contract.ts +++ b/src/models/cw20_contract.ts @@ -26,6 +26,8 @@ export interface IContractInfo { name?: string; } export class Cw20Contract extends BaseModel { + static softDelete = false; + [relation: string]: any; id!: number; diff --git a/src/models/cw20_holder.ts b/src/models/cw20_holder.ts index 4b6b01ef4..ab4b00469 100644 --- a/src/models/cw20_holder.ts +++ b/src/models/cw20_holder.ts @@ -5,6 +5,8 @@ import { Cw20Contract } from './cw20_contract'; import { SmartContract } from './smart_contract'; export class CW20Holder extends BaseModel { + static softDelete = false; + [relation: string]: any; id?: number; diff --git a/src/models/cw20_total_holder_stats.ts b/src/models/cw20_total_holder_stats.ts index 7d5d8a846..b5bc50b2d 100644 --- a/src/models/cw20_total_holder_stats.ts +++ b/src/models/cw20_total_holder_stats.ts @@ -3,6 +3,8 @@ import BaseModel from './base'; import { Cw20Contract } from './cw20_contract'; export class CW20TotalHolderStats extends BaseModel { + static softDelete = false; + [relation: string]: any; date!: Date; diff --git a/src/services/api-gateways/api_gateway.service.ts b/src/services/api-gateways/api_gateway.service.ts index 9e02973e1..0503fd3a3 100644 --- a/src/services/api-gateways/api_gateway.service.ts +++ b/src/services/api-gateways/api_gateway.service.ts @@ -23,6 +23,12 @@ import { bullBoardMixin } from '../../mixins/bullBoard/bullBoard.mixin'; mappingPolicy: 'restrict', // allow action called with exact method whitelist: ['v2.dashboard-statistics.*', 'v2.graphql.*'], }, + { + path: '/admin', + autoAliases: true, // allow generate rest info (GET/PUT/POST...) in the services + mappingPolicy: 'restrict', // allow action called with exact method + whitelist: ['v1.cw20-admin.*'], + }, ], // empty cors object will have moleculer to generate handler for preflight request and CORS header which allow all origin cors: {}, diff --git a/src/services/api-gateways/cw20_admin.service.ts b/src/services/api-gateways/cw20_admin.service.ts new file mode 100644 index 000000000..f94fccd0e --- /dev/null +++ b/src/services/api-gateways/cw20_admin.service.ts @@ -0,0 +1,45 @@ +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: 'cw20-admin', + version: 1, +}) +export default class Cw20AdminService extends BaseService { + public constructor(public broker: ServiceBroker) { + super(broker); + } + + @Post('/cw20-reindexing', { + name: 'cw20Reindexing', + params: { + chainid: { + type: 'string', + optional: false, + enum: networks.map((network) => network.chainId), + }, + contractAddress: { + type: 'string', + optional: false, + }, + }, + }) + async cw20ReindexingByChainId( + ctx: Context< + { chainid: string; contractAddress: string }, + Record + > + ) { + const selectedChain = networks.find( + (network) => network.chainId === ctx.params.chainid + ); + return this.broker.call( + `v1.Cw20ReindexingService.reindexing@${selectedChain?.moleculerNamespace}`, + { + contractAddress: ctx.params.contractAddress, + } + ); + } +} diff --git a/src/services/cw20/cw20.service.ts b/src/services/cw20/cw20.service.ts index 49cc86251..db63a1e93 100644 --- a/src/services/cw20/cw20.service.ts +++ b/src/services/cw20/cw20.service.ts @@ -1,4 +1,5 @@ import { Service } from '@ourparentcenter/moleculer-decorators-extended'; +import { Queue } from 'bullmq'; import { Knex } from 'knex'; import _ from 'lodash'; import { ServiceBroker } from 'moleculer'; @@ -34,6 +35,13 @@ export const CW20_ACTION = { BURN_FROM: 'burn_from', SEND_FROM: 'send_from', }; +export interface ICw20ReindexingHistoryParams { + smartContractId: number; + startBlock: number; + endBlock: number; + prevId: number; + contractAddress: string; +} @Service({ name: SERVICE.V1.Cw20.key, version: 1, @@ -205,41 +213,38 @@ export default class Cw20Service extends BullableService { } } - async getCw20ContractEvents(startBlock: number, endBlock: number) { + async getCw20ContractEvents( + startBlock: number, + endBlock: number, + smartContractId?: number, + page?: { prevId: number; limit: number } + ) { return SmartContractEvent.query() - .alias('smart_contract_event') - .withGraphJoined( - '[message(selectMessage), tx(selectTransaction), attributes(selectAttribute), smart_contract(selectSmartContract).code(selectCode)]' - ) - .modifiers({ - selectCode(builder) { - builder.select('type'); - }, - selectTransaction(builder) { - builder.select('hash', 'height'); - }, - selectMessage(builder) { - builder.select('sender', 'content'); - }, - selectAttribute(builder) { - builder.select('key', 'value'); - }, - selectSmartContract(builder) { - builder.select('address', 'id'); - }, - }) + .withGraphFetched('attributes(selectAttribute)') + .joinRelated('[message, tx, smart_contract.code]') .where('smart_contract:code.type', 'CW20') .where('tx.height', '>', startBlock) .andWhere('tx.height', '<=', endBlock) + .modify((builder) => { + if (smartContractId) { + builder.andWhere('smart_contract.id', smartContractId); + } + if (page) { + builder + .andWhere('smart_contract_event.id', '>', page.prevId) + .orderBy('smart_contract_event.id', 'asc') + .limit(page.limit); + } + }) .select( 'message.sender as sender', 'smart_contract.address as contract_address', 'smart_contract_event.action', - 'smart_contract_event.event_id as event_id', - 'smart_contract_event.index', + 'smart_contract_event.event_id', 'smart_contract.id as smart_contract_id', - 'tx.height as height', - 'smart_contract_event.id as smart_contract_event_id' + 'smart_contract_event.id as smart_contract_event_id', + 'tx.hash', + 'tx.height' ) .orderBy('smart_contract_event.id', 'asc'); } @@ -304,4 +309,45 @@ export default class Cw20Service extends BullableService { } return super._start(); } + + @QueueHandler({ + queueName: BULL_JOB_NAME.REINDEX_CW20_HISTORY, + jobName: BULL_JOB_NAME.REINDEX_CW20_HISTORY, + }) + public async reindexHistory(_payload: ICw20ReindexingHistoryParams) { + const { smartContractId, startBlock, endBlock, prevId, contractAddress } = + _payload; + // insert data from event_attribute_backup to event_attribute + const { limitRecordGet } = config.cw20.reindexHistory; + const events = await this.getCw20ContractEvents( + startBlock, + endBlock, + smartContractId, + { limit: limitRecordGet, prevId } + ); + if (events.length > 0) { + await knex.transaction(async (trx) => { + await this.handleCw20Histories(events, trx); + }); + await this.createJob( + BULL_JOB_NAME.REINDEX_CW20_HISTORY, + BULL_JOB_NAME.REINDEX_CW20_HISTORY, + { + smartContractId, + startBlock, + endBlock, + prevId: events[events.length - 1].smart_contract_event_id, + contractAddress, + } satisfies ICw20ReindexingHistoryParams, + { + removeOnComplete: true, + } + ); + } else { + const queue: Queue = this.getQueueManager().getQueue( + BULL_JOB_NAME.REINDEX_CW20_CONTRACT + ); + (await queue.getJob(contractAddress))?.remove(); + } + } } diff --git a/src/services/cw20/cw20_reindexing.service.ts b/src/services/cw20/cw20_reindexing.service.ts new file mode 100644 index 000000000..b2caf4cb8 --- /dev/null +++ b/src/services/cw20/cw20_reindexing.service.ts @@ -0,0 +1,181 @@ +import { + Action, + Service, +} from '@ourparentcenter/moleculer-decorators-extended'; +import _ from 'lodash'; +import { Context, ServiceBroker } from 'moleculer'; +import config from '../../../config.json' assert { type: 'json' }; +import BullableService, { QueueHandler } from '../../base/bullable.service'; +import { BULL_JOB_NAME, IContextUpdateCw20, SERVICE } from '../../common'; +import { + CW20Holder, + CW20TotalHolderStats, + Cw20Contract, + Cw20Event, + IHolderEvent, + SmartContract, +} from '../../models'; +import { ICw20ReindexingHistoryParams } from './cw20.service'; +import knex from '../../common/utils/db_connection'; + +export interface IAddressParam { + contractAddress: string; +} +interface ICw20ReindexingParams { + contractAddress: string; + smartContractId: number; +} +@Service({ + name: SERVICE.V1.Cw20ReindexingService.key, + version: 1, +}) +export default class Cw20ReindexingContract extends BullableService { + public constructor(public broker: ServiceBroker) { + super(broker); + } + + @Action({ + name: SERVICE.V1.Cw20ReindexingService.Reindexing.key, + params: { + contractAddress: 'string', + }, + }) + public async reindexing(ctx: Context) { + const { contractAddress } = ctx.params; + const smartContract = await SmartContract.query() + .withGraphJoined('code') + .where('address', contractAddress) + .first() + .throwIfNotFound(); + + // check whether contract is Cw20 type -> throw error to user + if (smartContract.code.type === 'CW20') { + await this.createJob( + BULL_JOB_NAME.REINDEX_CW20_CONTRACT, + BULL_JOB_NAME.REINDEX_CW20_CONTRACT, + { + contractAddress, + smartContractId: smartContract.id, + } satisfies ICw20ReindexingParams, + { + jobId: contractAddress, + } + ); + } else { + throw new Error( + `Smart contract ${ctx.params.contractAddress} is not CW20 type` + ); + } + } + + @QueueHandler({ + queueName: BULL_JOB_NAME.REINDEX_CW20_CONTRACT, + jobName: BULL_JOB_NAME.REINDEX_CW20_CONTRACT, + }) + async jobHandler(_payload: ICw20ReindexingParams): Promise { + const { smartContractId, contractAddress } = _payload; + const cw20Contract = await Cw20Contract.query() + .withGraphJoined('smart_contract') + .where('smart_contract.address', contractAddress) + .select(['cw20_contract.id']) + .first(); + // query + const contractInfo = ( + await Cw20Contract.getContractsInfo([contractAddress]) + )[0]; + let track = true; + let initBalances: IHolderEvent[] = []; + // get init address holder, init amount + try { + initBalances = await Cw20Contract.getInstantiateBalances(contractAddress); + } catch (error) { + track = false; + } + const minUpdatedHeightOwner = + _.min(initBalances.map((holder) => holder.event_height)) || 0; + const maxUpdatedHeightOwner = + _.max(initBalances.map((holder) => holder.event_height)) || 0; + let id = -1; + let lastUpdatedHeight = -1; + await knex.transaction(async (trx) => { + if (cw20Contract) { + await Cw20Event.query() + .delete() + .where('cw20_contract_id', cw20Contract.id) + .transacting(trx); + await CW20TotalHolderStats.query() + .delete() + .where('cw20_contract_id', cw20Contract.id) + .transacting(trx); + await CW20Holder.query() + .delete() + .where('cw20_contract_id', cw20Contract.id) + .transacting(trx); + await Cw20Contract.query().deleteById(cw20Contract.id).transacting(trx); + } + const newCw20Contract = await Cw20Contract.query() + .insertGraph({ + ...Cw20Contract.fromJson({ + smart_contract_id: smartContractId, + symbol: contractInfo?.symbol, + minter: contractInfo?.minter, + marketing_info: contractInfo?.marketing_info, + name: contractInfo?.name, + total_supply: initBalances.reduce( + (acc: string, curr: { address: string; amount: string }) => + (BigInt(acc) + BigInt(curr.amount)).toString(), + '0' + ), + track, + decimal: contractInfo?.decimal, + last_updated_height: minUpdatedHeightOwner, + }), + holders: initBalances.map((e) => ({ + address: e.address, + amount: e.amount, + last_updated_height: e.event_height, + })), + }) + .transacting(trx); + id = newCw20Contract.id; + lastUpdatedHeight = newCw20Contract.last_updated_height; + }); + // handle from minUpdatedHeightOwner to blockHeight + await this.broker.call( + SERVICE.V1.Cw20UpdateByContract.UpdateByContract.path, + { + cw20Contracts: [ + { + id, + last_updated_height: lastUpdatedHeight, + }, + ], + startBlock: minUpdatedHeightOwner, + endBlock: maxUpdatedHeightOwner, + } satisfies IContextUpdateCw20 + ); + // insert histories + await this.createJob( + BULL_JOB_NAME.REINDEX_CW20_HISTORY, + BULL_JOB_NAME.REINDEX_CW20_HISTORY, + { + smartContractId, + startBlock: config.crawlBlock.startBlock, + endBlock: maxUpdatedHeightOwner, + prevId: 0, + contractAddress, + } satisfies ICw20ReindexingHistoryParams, + { + removeOnComplete: true, + } + ); + } + + async _start(): Promise { + await this.broker.waitForServices([ + SERVICE.V1.Cw20.name, + SERVICE.V1.Cw20UpdateByContract.name, + ]); + return super._start(); + } +} diff --git a/src/services/cw20/cw20_update_by_contract.service.ts b/src/services/cw20/cw20_update_by_contract.service.ts index f4741a785..09c865863 100644 --- a/src/services/cw20/cw20_update_by_contract.service.ts +++ b/src/services/cw20/cw20_update_by_contract.service.ts @@ -78,7 +78,7 @@ export default class Cw20UpdateByContractService extends BullableService { const { startBlock, endBlock } = ctx.params; // eslint-disable-next-line no-restricted-syntax for (const cw20Contract of ctx.params.cw20Contracts) { - const startUpdateBlock = Math.min( + const startUpdateBlock = Math.max( startBlock, cw20Contract.last_updated_height ); diff --git a/test/unit/services/cw20/cw20.spec.ts b/test/unit/services/cw20/cw20.spec.ts index f14426e2d..d87ab76d6 100644 --- a/test/unit/services/cw20/cw20.spec.ts +++ b/test/unit/services/cw20/cw20.spec.ts @@ -4,6 +4,7 @@ import { BULL_JOB_NAME } from '../../../../src/common'; import knex from '../../../../src/common/utils/db_connection'; import { Block, + BlockCheckpoint, Code, Cw20Contract, Cw20Event, @@ -12,6 +13,7 @@ import { import { SmartContractEvent } from '../../../../src/models/smart_contract_event'; import Cw20Service from '../../../../src/services/cw20/cw20.service'; import Cw20UpdateByContractService from '../../../../src/services/cw20/cw20_update_by_contract.service'; +import CrawlContractEventService from '../../../../src/services/crawl-cosmwasm/crawl_contract_event.service'; @Describe('Test cw20 service') export default class Cw20 { @@ -19,6 +21,10 @@ export default class Cw20 { cw20Service = this.broker.createService(Cw20Service) as Cw20Service; + crawlContractEventService = this.broker.createService( + CrawlContractEventService + ) as CrawlContractEventService; + cw20UpdateByContractService = this.broker.createService( Cw20UpdateByContractService ) as Cw20UpdateByContractService; @@ -31,6 +37,36 @@ export default class Cw20 { data: {}, }); + codeId = { + ...Code.fromJson({ + creator: 'code_id_creator', + code_id: 100, + data_hash: 'code_id_data_hash', + instantiate_permission: { permission: '', address: '', addresses: [] }, + store_hash: 'code_id_store_hash', + store_height: 1000, + type: 'CW20', + }), + contracts: [ + { + name: 'Base Contract 2', + address: 'mock_contract_address', + creator: 'phamphong_creator', + code_id: 100, + instantiate_hash: 'abc', + instantiate_height: 300000, + }, + { + code_id: 100, + address: 'mock_contract_address_2', + name: 'name', + creator: 'phamphong_creator 2', + instantiate_hash: 'abc', + instantiate_height: 300000, + }, + ], + }; + txInsert = { ...Transaction.fromJson({ height: this.block.height, @@ -57,24 +93,114 @@ export default class Cw20 { content: {}, events: [ { - type: 'execute', block_height: this.block.height, source: 'TX_EVENT', + type: 'instantiate', attributes: [ { - block_height: this.block.height, index: 0, + block_height: this.block.height, composite_key: 'execute._contract_address', key: '_contract_address', - value: 'this.mockInitContract_2.smart_contract.address', + value: this.codeId.contracts[0].address, }, { + index: 1, + block_height: this.block.height, + composite_key: 'execute._contract_address', + key: 'code_id', + value: '6', + }, + ], + }, + { + block_height: this.block.height, + source: 'TX_EVENT', + type: 'wasm', + attributes: [ + { + index: 0, block_height: this.block.height, + composite_key: 'execute._contract_address', + key: '_contract_address', + value: this.codeId.contracts[0].address, + }, + { index: 1, - // tx_id: 1, + block_height: this.block.height, + composite_key: 'execute._contract_address', + key: 'action', + value: 'add_whitelist', + }, + { + index: 2, + block_height: this.block.height, + composite_key: 'execute._contract_address', + key: 'token_id', + value: 'test2', + }, + ], + }, + { + block_height: this.block.height, + source: 'TX_EVENT', + type: 'wasm', + attributes: [ + { + index: 2, + block_height: this.block.height, + composite_key: 'execute._contract_address', + key: '_contract_address', + value: this.codeId.contracts[1].address, + }, + { + index: 3, + block_height: this.block.height, + composite_key: 'execute._contract_address', + key: 'action', + value: 'add_mint_phase', + }, + { + index: 4, + block_height: this.block.height, + composite_key: 'execute._contract_address', + key: 'token_id', + value: 'test1', + }, + ], + }, + { + block_height: this.block.height, + source: 'TX_EVENT', + type: 'wasm', + attributes: [ + { + index: 0, + block_height: this.block.height, composite_key: 'execute._contract_address', key: '_contract_address', - value: 'fdgdgdfgdfg', + value: this.codeId.contracts[0].address, + }, + { + index: 1, + block_height: this.block.height, + composite_key: 'execute._contract_address', + key: 'action', + value: 'dfgdfgdfgdfg', + }, + { + index: 2, + block_height: this.block.height, + composite_key: 'execute._contract_address', + key: 'bcvbcb', + value: 'fdsdfsdf', + }, + { + index: 3, + block_height: this.block.height, + composite_key: 'execute._contract_address', + key: 'vbvbv', + value: 'sesesese', }, ], }, @@ -83,42 +209,16 @@ export default class Cw20 { ], }; - codeId = { - ...Code.fromJson({ - creator: 'code_id_creator', - code_id: 100, - data_hash: 'code_id_data_hash', - instantiate_permission: { permission: '', address: '', addresses: [] }, - store_hash: 'code_id_store_hash', - store_height: 1000, - type: 'CW721', + mockBlockCheckpoint = [ + BlockCheckpoint.fromJson({ + job_name: BULL_JOB_NAME.CRAWL_CONTRACT_EVENT, + height: this.block.height - 1, }), - contracts: [ - { - name: 'Base Contract 2', - address: 'mock_contract_address', - creator: 'phamphong_creator', - code_id: 100, - instantiate_hash: 'abc', - instantiate_height: 300000, - }, - { - code_id: 100, - address: 'mock_contract_address_2', - name: 'name', - creator: 'phamphong_creator 2', - instantiate_hash: 'abc', - instantiate_height: 300000, - }, - ], - }; - - smartContractEvent = SmartContractEvent.fromJson({ - smart_contract_id: 1, - action: 'huh', - event_id: '1', - index: 1, - }); + BlockCheckpoint.fromJson({ + job_name: BULL_JOB_NAME.CRAWL_SMART_CONTRACT, + height: this.block.height, + }), + ]; @BeforeAll() async initSuite() { @@ -126,12 +226,13 @@ export default class Cw20 { this.cw20UpdateByContractService.getQueueManager().stopAll(); await this.broker.start(); await knex.raw( - 'TRUNCATE TABLE code, cw20_contract, block, transaction RESTART IDENTITY CASCADE' + 'TRUNCATE TABLE code, cw20_contract, block, transaction, smart_contract_event, block_checkpoint RESTART IDENTITY CASCADE' ); await Block.query().insert(this.block); await Transaction.query().insertGraph(this.txInsert); await Code.query().insertGraph(this.codeId); - await SmartContractEvent.query().insert(this.smartContractEvent); + await BlockCheckpoint.query().insert(this.mockBlockCheckpoint); + await this.crawlContractEventService.jobHandler(); } @AfterAll() @@ -337,4 +438,176 @@ export default class Cw20 { await trx.rollback(); }); } + + @Test('test getCw20ContractEvent function') + public async testGetCw20ContractEvent() { + const extractData = await this.cw20Service.getCw20ContractEvents( + this.block.height - 1, + this.block.height + ); + expect( + extractData.map((data) => ({ + action: data.action, + sender: data.sender, + contractAddress: data.contract_address, + attributes: data.attributes, + hash: data.hash, + height: data.height, + })) + ).toEqual([ + { + action: 'instantiate', + sender: this.txInsert.messages[0].sender, + contractAddress: + this.txInsert.messages[0].events[0].attributes[0].value, + attributes: [ + this.txInsert.messages[0].events[0].attributes[0], + this.txInsert.messages[0].events[0].attributes[1], + ].map((attribute) => ({ key: attribute.key, value: attribute.value })), + hash: this.txInsert.hash, + height: this.txInsert.height, + }, + { + action: this.txInsert.messages[0].events[1].attributes[1].value, + sender: this.txInsert.messages[0].sender, + contractAddress: + this.txInsert.messages[0].events[1].attributes[0].value, + attributes: [ + this.txInsert.messages[0].events[1].attributes[0], + this.txInsert.messages[0].events[1].attributes[1], + this.txInsert.messages[0].events[1].attributes[2], + ].map((attribute) => ({ key: attribute.key, value: attribute.value })), + hash: this.txInsert.hash, + height: this.txInsert.height, + }, + { + action: this.txInsert.messages[0].events[2].attributes[1].value, + sender: this.txInsert.messages[0].sender, + contractAddress: + this.txInsert.messages[0].events[2].attributes[0].value, + attributes: [ + this.txInsert.messages[0].events[2].attributes[0], + this.txInsert.messages[0].events[2].attributes[1], + this.txInsert.messages[0].events[2].attributes[2], + ].map((attribute) => ({ key: attribute.key, value: attribute.value })), + hash: this.txInsert.hash, + height: this.txInsert.height, + }, + { + action: this.txInsert.messages[0].events[3].attributes[1].value, + sender: this.txInsert.messages[0].sender, + contractAddress: + this.txInsert.messages[0].events[3].attributes[0].value, + attributes: [ + this.txInsert.messages[0].events[3].attributes[0], + this.txInsert.messages[0].events[3].attributes[1], + this.txInsert.messages[0].events[3].attributes[2], + this.txInsert.messages[0].events[3].attributes[3], + ].map((attribute) => ({ key: attribute.key, value: attribute.value })), + hash: this.txInsert.hash, + height: this.txInsert.height, + }, + ]); + } + + @Test('test getCw20ContractEvent function by contract') + public async testGetCw20ContractEventByContract() { + const extractData = await this.cw20Service.getCw20ContractEvents( + this.block.height - 1, + this.block.height, + 1 + ); + expect( + extractData.map((data) => ({ + action: data.action, + sender: data.sender, + contractAddress: data.contract_address, + attributes: data.attributes, + hash: data.hash, + height: data.height, + })) + ).toEqual([ + { + action: 'instantiate', + sender: this.txInsert.messages[0].sender, + contractAddress: + this.txInsert.messages[0].events[0].attributes[0].value, + attributes: [ + this.txInsert.messages[0].events[0].attributes[0], + this.txInsert.messages[0].events[0].attributes[1], + ].map((attribute) => ({ key: attribute.key, value: attribute.value })), + hash: this.txInsert.hash, + height: this.txInsert.height, + }, + { + action: this.txInsert.messages[0].events[1].attributes[1].value, + sender: this.txInsert.messages[0].sender, + contractAddress: + this.txInsert.messages[0].events[1].attributes[0].value, + attributes: [ + this.txInsert.messages[0].events[1].attributes[0], + this.txInsert.messages[0].events[1].attributes[1], + this.txInsert.messages[0].events[1].attributes[2], + ].map((attribute) => ({ key: attribute.key, value: attribute.value })), + hash: this.txInsert.hash, + height: this.txInsert.height, + }, + { + action: this.txInsert.messages[0].events[3].attributes[1].value, + sender: this.txInsert.messages[0].sender, + contractAddress: + this.txInsert.messages[0].events[3].attributes[0].value, + attributes: [ + this.txInsert.messages[0].events[3].attributes[0], + this.txInsert.messages[0].events[3].attributes[1], + this.txInsert.messages[0].events[3].attributes[2], + this.txInsert.messages[0].events[3].attributes[3], + ].map((attribute) => ({ key: attribute.key, value: attribute.value })), + hash: this.txInsert.hash, + height: this.txInsert.height, + }, + ]); + const extractData2 = await this.cw20Service.getCw20ContractEvents( + this.block.height - 1, + this.block.height, + 1, + { prevId: 0, limit: 2 } + ); + expect( + extractData2.map((data) => ({ + action: data.action, + sender: data.sender, + contractAddress: data.contract_address, + attributes: data.attributes, + hash: data.hash, + height: data.height, + })) + ).toEqual([ + { + action: 'instantiate', + sender: this.txInsert.messages[0].sender, + contractAddress: + this.txInsert.messages[0].events[0].attributes[0].value, + attributes: [ + this.txInsert.messages[0].events[0].attributes[0], + this.txInsert.messages[0].events[0].attributes[1], + ].map((attribute) => ({ key: attribute.key, value: attribute.value })), + hash: this.txInsert.hash, + height: this.txInsert.height, + }, + { + action: this.txInsert.messages[0].events[1].attributes[1].value, + sender: this.txInsert.messages[0].sender, + contractAddress: + this.txInsert.messages[0].events[1].attributes[0].value, + attributes: [ + this.txInsert.messages[0].events[1].attributes[0], + this.txInsert.messages[0].events[1].attributes[1], + this.txInsert.messages[0].events[1].attributes[2], + ].map((attribute) => ({ key: attribute.key, value: attribute.value })), + hash: this.txInsert.hash, + height: this.txInsert.height, + }, + ]); + } } diff --git a/test/unit/services/cw20/cw20_reindexing.spec.ts b/test/unit/services/cw20/cw20_reindexing.spec.ts new file mode 100644 index 000000000..8d6eba414 --- /dev/null +++ b/test/unit/services/cw20/cw20_reindexing.spec.ts @@ -0,0 +1,161 @@ +import { AfterAll, BeforeAll, Describe, Test } from '@jest-decorated/core'; +import { ServiceBroker } from 'moleculer'; +import knex from '../../../../src/common/utils/db_connection'; +import Cw20ReindexingContract from '../../../../src/services/cw20/cw20_reindexing.service'; +import { Code, Cw20Contract, CW20Holder } from '../../../../src/models'; +import Cw20UpdateByContractService from '../../../../src/services/cw20/cw20_update_by_contract.service'; +import Cw20Service from '../../../../src/services/cw20/cw20.service'; + +@Describe('Test cw20 reindexing service') +export default class TestCw20ReindexingService { + codeId = { + ...Code.fromJson({ + creator: 'code_id_creator', + code_id: 100, + data_hash: 'code_id_data_hash', + instantiate_permission: { permission: '', address: '', addresses: [] }, + store_hash: 'code_id_store_hash', + store_height: 1000, + type: 'CW721', + }), + contracts: [ + { + name: 'Base Contract 2', + address: 'mock_contract_address', + creator: 'phamphong_creator', + code_id: 100, + instantiate_hash: 'abc', + instantiate_height: 300000, + }, + { + code_id: 100, + address: 'mock_contract_address_2', + name: 'name', + creator: 'phamphong_creator 2', + instantiate_hash: 'abc', + instantiate_height: 300000, + }, + ], + }; + + cw20Contract = { + ...Cw20Contract.fromJson({ + smart_contract_id: 1, + marketing_info: {}, + total_supply: '1121112133', + symbol: 'TEST SyMbol', + minter: 'jfglkdfjgklfdgklklfdkl', + name: 'dgbdfmnlkgsdfklgjksdfl', + track: true, + last_updated_height: 10000, + }), + holders: [ + { + address: 'holder_1', + amount: '123134134434', + last_updated_height: 8000, + }, + { + address: 'holder_2', + amount: '20032204', + last_updated_height: 8500, + }, + ], + }; + + broker = new ServiceBroker({ logger: false }); + + cw20reindexingService = this.broker.createService( + Cw20ReindexingContract + ) as Cw20ReindexingContract; + + cw20UpdateByContractService = this.broker.createService( + Cw20UpdateByContractService + ) as Cw20UpdateByContractService; + + cw20Service = this.broker.createService(Cw20Service) as Cw20Service; + + @BeforeAll() + async initSuite() { + await this.broker.start(); + await knex.raw( + 'TRUNCATE TABLE code, cw20_contract, block_checkpoint RESTART IDENTITY CASCADE' + ); + await Code.query().insertGraph(this.codeId); + } + + @AfterAll() + async tearDown() { + await this.broker.stop(); + } + + @Test('Test ReindexingService function') + public async testReindexingService() { + const mockContractInfo = { + address: this.codeId.contracts[0].address, + name: 'dgjkfjgdkg', + symbol: 'NNNJNJ', + minter: 'hfgjksghkjsf', + }; + const mockHolders = [ + { + address: 'holder_1', + amount: '123134134434', + event_height: 8000, + contract_address: this.codeId.contracts[0].address, + }, + { + address: 'holder_2', + amount: '20032204', + event_height: 8500, + contract_address: this.codeId.contracts[0].address, + }, + { + address: 'holder_3', + amount: '5467987', + event_height: 9600, + contract_address: this.codeId.contracts[0].address, + }, + { + address: 'holder_4', + amount: '11111111', + event_height: 23655422, + contract_address: this.codeId.contracts[0].address, + }, + ]; + this.cw20Service.reindexHistory = jest.fn(() => Promise.resolve()); + this.cw20UpdateByContractService.UpdateByContract = jest.fn(() => + Promise.resolve() + ); + Cw20Contract.getContractsInfo = jest.fn(() => + Promise.resolve([mockContractInfo]) + ); + Cw20Contract.getInstantiateBalances = jest.fn(() => + Promise.resolve(mockHolders) + ); + await this.cw20reindexingService.jobHandler({ + contractAddress: this.codeId.contracts[0].address, + smartContractId: 1, + }); + const cw20Contract = await Cw20Contract.query() + .withGraphJoined('smart_contract') + .where('smart_contract.address', this.codeId.contracts[0].address) + .first() + .throwIfNotFound(); + expect(cw20Contract.name).toEqual(mockContractInfo.name); + expect(cw20Contract.minter).toEqual(mockContractInfo.minter); + expect(cw20Contract.symbol).toEqual(mockContractInfo.symbol); + const cw20Holders = await CW20Holder.query() + .withGraphJoined('token') + .where('token.id', cw20Contract.id) + .orderBy('address', 'asc'); + expect( + cw20Holders.map((cw20Holder) => ({ + address: cw20Holder.address, + amount: cw20Holder.amount, + event_height: cw20Holder.last_updated_height, + contract_address: this.codeId.contracts[0].address, + })) + ).toEqual(mockHolders); + } +}