diff --git a/ci/config.json.ci b/ci/config.json.ci index c2a560936..62afc8d56 100644 --- a/ci/config.json.ci +++ b/ci/config.json.ci @@ -186,6 +186,12 @@ "millisecondRepeatJob": 2000, "blocksPerCall": 100 }, + "crawlIbcIcs20": { + "key": "crawlIbcIcs20", + "millisecondRepeatJob": 2000, + "blocksPerCall": 100, + "port": "transfer" + }, "jobReassignMsgIndexToEvent": { "millisecondCrawl": 1000, "blocksPerCall": 100 diff --git a/config.json b/config.json index 3e7bf5817..76c085b54 100644 --- a/config.json +++ b/config.json @@ -186,6 +186,12 @@ "millisecondRepeatJob": 2000, "blocksPerCall": 100 }, + "crawlIbcIcs20": { + "key": "crawlIbcIcs20", + "millisecondRepeatJob": 2000, + "blocksPerCall": 100, + "port": "transfer" + }, "jobReassignMsgIndexToEvent": { "millisecondCrawl": 1000, "blocksPerCall": 100 diff --git a/migrations/20230823070516_ics20_model.ts b/migrations/20230823070516_ics20_model.ts new file mode 100644 index 000000000..4ad2657f6 --- /dev/null +++ b/migrations/20230823070516_ics20_model.ts @@ -0,0 +1,21 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + await knex.schema.createTable('ibc_ics20', (table) => { + table.increments(); + table.integer('ibc_message_id').notNullable().unique(); + table.string('sender').index(); + table.string('receiver').index().notNullable(); + table.decimal('amount', 80, 0).notNullable(); + table.string('denom').notNullable().index(); + table.string('status').notNullable(); + table.string('channel_id').notNullable().index(); + table.foreign('ibc_message_id').references('ibc_message.id'); + table.string('sequence_key').notNullable().index(); + table.string('type').notNullable().index(); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists('ibc_ics20'); +} diff --git a/src/common/constant.ts b/src/common/constant.ts index 171c0d41e..bf428de76 100644 --- a/src/common/constant.ts +++ b/src/common/constant.ts @@ -76,6 +76,7 @@ export const BULL_JOB_NAME = { REINDEX_CW20_CONTRACT: 'reindex:cw20-contract', REINDEX_CW20_HISTORY: 'reindex:cw20-history', CRAWL_IBC_APP: 'crawl:ibc-app', + CRAWL_IBC_ICS20: 'crawl:ibc-ics20', JOB_REASSIGN_MSG_INDEX_TO_EVENT: 'job:reassign-msg-index-to-event', JOB_CREATE_COMPOSITE_INDEX_ATTR_PARTITION: 'job:create-index-composite-attr-partition', @@ -284,6 +285,10 @@ export const SERVICE = { path: 'v1.Cw20ReindexingService.reindexing', }, }, + CrawlIBCIcs20Service: { + key: 'CrawlIBCIcs20Service', + name: 'v1.CrawlIBCIcs20Service', + }, ServicesManager: { key: 'ServicesManager', name: 'v1.ServicesManager', diff --git a/src/models/event.ts b/src/models/event.ts index 0ac936d41..1dcf66977 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -76,6 +76,13 @@ export class Event extends BaseModel { }; } + getAttributesFrom(attributesType: string[]) { + return attributesType.map( + (attributeType) => + this.attributes?.find((attr: any) => attr.key === attributeType)?.value + ); + } + static EVENT_TYPE = { STORE_CODE: 'store_code', SUBMIT_PROPOSAL: 'submit_proposal', diff --git a/src/models/event_attribute.ts b/src/models/event_attribute.ts index 2f450dbe8..d0385ed35 100644 --- a/src/models/event_attribute.ts +++ b/src/models/event_attribute.ts @@ -114,6 +114,11 @@ export class EventAttribute extends BaseModel { SRC_CHANNEL: 'packet_src_channel', DST_PORT: 'packet_dst_port', DST_CHANNEL: 'packet_dst_channel', + DENOM: 'denom', + SUCCESS: 'success', + REFUND_RECEIVER: 'refund_receiver', + REFUND_DENOM: 'refund_denom', + REFUND_AMOUNT: 'refund_amount', }; static ATTRIBUTE_COMPOSITE_KEY = { diff --git a/src/models/ibc_ics20.ts b/src/models/ibc_ics20.ts new file mode 100644 index 000000000..5fd618cee --- /dev/null +++ b/src/models/ibc_ics20.ts @@ -0,0 +1,93 @@ +/* eslint-disable import/no-cycle */ +import { Model } from 'objection'; +import BaseModel from './base'; +import { IbcMessage } from './ibc_message'; +import { IbcChannel } from './ibc_channel'; + +export class IbcIcs20 extends BaseModel { + id!: number; + + ibc_message_id!: number; + + sender!: string; + + receiver!: string; + + amount!: string; + + denom!: string; + + status!: string; + + channel_id!: string; + + ibc_message!: IbcMessage; + + sequence_key!: string; + + type!: string; + + static get tableName() { + return 'ibc_ics20'; + } + + static get jsonSchema() { + return { + type: 'object', + required: [ + 'receiver', + 'amount', + 'denom', + 'ibc_message_id', + 'channel_id', + 'sequence_key', + 'status', + 'type', + ], + properties: { + receiver: { type: 'string' }, + denom: { type: 'string' }, + ibc_message_id: { type: 'number' }, + amount: { type: 'string' }, + channel_id: { type: 'string' }, + sequence_key: { type: 'string' }, + status: { type: 'string' }, + type: { type: 'string' }, + }, + }; + } + + static get relationMappings() { + return { + ibc_message: { + relation: Model.BelongsToOneRelation, + modelClass: IbcMessage, + join: { + from: 'ibc_ics20.ibc_message_id', + to: 'ibc_message.id', + }, + }, + channel: { + relation: Model.BelongsToOneRelation, + modelClass: IbcChannel, + join: { + from: 'ibc_ics20.channel_id', + to: 'ibc_channel.channel_id', + }, + }, + }; + } + + static EVENT_TYPE = { + TIMEOUT: 'timeout', + FUNGIBLE_TOKEN_PACKET: 'fungible_token_packet', + DENOM_TRACE: 'denomination_trace', + }; + + static STATUS_TYPE = { + TIMEOUT: 'timeout', + ACK_ERROR: 'ack_error', + ACK_SUCCESS: 'ack_success', + ONGOING: 'ongoing', + }; +} diff --git a/src/models/ibc_message.ts b/src/models/ibc_message.ts index c393c3a2b..79b73b232 100644 --- a/src/models/ibc_message.ts +++ b/src/models/ibc_message.ts @@ -3,6 +3,7 @@ import { Model } from 'objection'; import BaseModel from './base'; import { IbcChannel } from './ibc_channel'; import { TransactionMessage } from './transaction_message'; +import config from '../../config.json' assert { type: 'json' }; export class IbcMessage extends BaseModel { id!: number; @@ -25,6 +26,8 @@ export class IbcMessage extends BaseModel { data!: any; + message!: TransactionMessage; + static get tableName() { return 'ibc_message'; } @@ -90,4 +93,8 @@ export class IbcMessage extends BaseModel { ACKNOWLEDGE_PACKET: 'acknowledge_packet', TIMEOUT_PACKET: 'timeout_packet', }; + + static PORTS = { + ICS20: config.crawlIbcIcs20.port, + }; } diff --git a/src/models/index.ts b/src/models/index.ts index bddfaad36..e0d8bd489 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -29,3 +29,4 @@ export * from './ibc_client'; export * from './ibc_connection'; export * from './ibc_channel'; export * from './ibc_message'; +export * from './ibc_ics20'; diff --git a/src/models/transaction_message.ts b/src/models/transaction_message.ts index 4f4bbf680..327f4a0f0 100644 --- a/src/models/transaction_message.ts +++ b/src/models/transaction_message.ts @@ -22,6 +22,8 @@ export class TransactionMessage extends BaseModel { parent_id!: number; + events!: Event[]; + static get tableName() { return 'transaction_message'; } diff --git a/src/services/ibc/crawl_ibc_ics20.service.ts b/src/services/ibc/crawl_ibc_ics20.service.ts new file mode 100644 index 000000000..588096622 --- /dev/null +++ b/src/services/ibc/crawl_ibc_ics20.service.ts @@ -0,0 +1,280 @@ +import { Service } from '@ourparentcenter/moleculer-decorators-extended'; +import { Knex } from 'knex'; +import { ServiceBroker } from 'moleculer'; +import config from '../../../config.json' assert { type: 'json' }; +import BullableService, { QueueHandler } from '../../base/bullable.service'; +import { BULL_JOB_NAME, SERVICE } from '../../common'; +import knex from '../../common/utils/db_connection'; +import { + BlockCheckpoint, + EventAttribute, + IbcIcs20, + IbcMessage, +} from '../../models'; + +@Service({ + name: SERVICE.V1.CrawlIBCIcs20Service.key, + version: 1, +}) +export default class CrawlIBCIcs20Service extends BullableService { + public constructor(public broker: ServiceBroker) { + super(broker); + } + + @QueueHandler({ + queueName: BULL_JOB_NAME.CRAWL_IBC_ICS20, + jobName: BULL_JOB_NAME.CRAWL_IBC_ICS20, + }) + public async crawlIbcIcs20(): Promise { + const [startHeight, endHeight, updateBlockCheckpoint] = + await BlockCheckpoint.getCheckpoint( + BULL_JOB_NAME.CRAWL_IBC_ICS20, + [BULL_JOB_NAME.CRAWL_IBC_APP], + config.crawlIbcIcs20.key + ); + this.logger.info( + `Handle IBC/ICS20, startHeight: ${startHeight}, endHeight: ${endHeight}` + ); + if (startHeight > endHeight) return; + await knex.transaction(async (trx) => { + await this.handleIcs20Send(startHeight, endHeight, trx); + await this.handleIcs20Recv(startHeight, endHeight, trx); + await this.handleIcs20Ack(startHeight, endHeight, trx); + await this.handleIcs20Timeout(startHeight, endHeight, trx); + updateBlockCheckpoint.height = endHeight; + await BlockCheckpoint.query() + .transacting(trx) + .insert(updateBlockCheckpoint) + .onConflict('job_name') + .merge(); + }); + } + + async handleIcs20Send( + startHeight: number, + endHeight: number, + trx: Knex.Transaction + ) { + const ics20Sends = await IbcMessage.query() + .joinRelated('message.transaction') + .where('src_port_id', IbcMessage.PORTS.ICS20) + .andWhere('ibc_message.type', IbcMessage.EVENT_TYPE.SEND_PACKET) + .andWhere('message:transaction.height', '>', startHeight) + .andWhere('message:transaction.height', '<=', endHeight) + .orderBy('message.id') + .transacting(trx); + if (ics20Sends.length > 0) { + const ibcIcs20s = ics20Sends.map((msg) => + IbcIcs20.fromJson({ + ibc_message_id: msg.id, + ...msg.data, + channel_id: msg.src_channel_id, + status: IbcIcs20.STATUS_TYPE.ONGOING, + sequence_key: msg.sequence_key, + type: msg.type, + }) + ); + await IbcIcs20.query().insert(ibcIcs20s).transacting(trx); + } + } + + async handleIcs20Recv( + startHeight: number, + endHeight: number, + trx: Knex.Transaction + ) { + const ics20Recvs = await IbcMessage.query() + .withGraphFetched('message.events(selectIcs20Event).attributes') + .joinRelated('message.transaction') + .modifiers({ + selectIcs20Event(builder) { + builder + .where('type', IbcIcs20.EVENT_TYPE.FUNGIBLE_TOKEN_PACKET) + .orWhere('type', IbcIcs20.EVENT_TYPE.DENOM_TRACE); + }, + }) + .where('dst_port_id', IbcMessage.PORTS.ICS20) + .andWhere('ibc_message.type', IbcMessage.EVENT_TYPE.RECV_PACKET) + .andWhere('message:transaction.height', '>', startHeight) + .andWhere('message:transaction.height', '<=', endHeight) + .orderBy('message.id') + .transacting(trx); + if (ics20Recvs.length > 0) { + const ibcIcs20s = ics20Recvs.map((msg) => { + const recvEvent = msg.message.events.find( + (e) => e.type === IbcIcs20.EVENT_TYPE.FUNGIBLE_TOKEN_PACKET + ); + if (recvEvent === undefined) { + throw Error(`Recv ibc hasn't emmitted events: ${msg.id}`); + } + const [sender, receiver, amount, originalDenom, ackStatus] = + recvEvent.getAttributesFrom([ + EventAttribute.ATTRIBUTE_KEY.SENDER, + EventAttribute.ATTRIBUTE_KEY.RECEIVER, + EventAttribute.ATTRIBUTE_KEY.AMOUNT, + EventAttribute.ATTRIBUTE_KEY.DENOM, + EventAttribute.ATTRIBUTE_KEY.SUCCESS, + ]); + if (originalDenom === undefined) { + throw Error(`Recv ibc hasn't emit denom: ${msg.id}`); + } + const isSource = + msg.message.events.find( + (e) => e.type === IbcIcs20.EVENT_TYPE.DENOM_TRACE + ) === undefined; + const denom = this.parseDenom( + originalDenom, + isSource, + msg.dst_port_id, + msg.dst_channel_id + ); + return IbcIcs20.fromJson({ + ibc_message_id: msg.id, + sender, + receiver, + amount, + denom, + status: + ackStatus === 'true' + ? IbcIcs20.STATUS_TYPE.ACK_SUCCESS + : IbcIcs20.STATUS_TYPE.ACK_ERROR, + channel_id: msg.dst_channel_id, + sequence_key: msg.sequence_key, + type: msg.type, + }); + }); + await IbcIcs20.query().insert(ibcIcs20s).transacting(trx); + } + } + + async handleIcs20Ack( + startHeight: number, + endHeight: number, + trx: Knex.Transaction + ) { + const ics20Acks = await IbcMessage.query() + .withGraphFetched('message.events(selectIcs20Event).attributes') + .joinRelated('message.transaction') + .modifiers({ + selectIcs20Event(builder) { + builder.where('type', IbcIcs20.EVENT_TYPE.FUNGIBLE_TOKEN_PACKET); + }, + }) + .where('src_port_id', IbcMessage.PORTS.ICS20) + .andWhere('ibc_message.type', IbcMessage.EVENT_TYPE.ACKNOWLEDGE_PACKET) + .andWhere('message:transaction.height', '>', startHeight) + .andWhere('message:transaction.height', '<=', endHeight) + .orderBy('message.id') + .transacting(trx); + if (ics20Acks.length > 0) { + // update success ack status for origin send ics20 + const acksSuccess = ics20Acks.filter((ack) => { + const ackEvents = ack.message.events; + if (ackEvents.length !== 2) { + throw Error(`Ack ibc hasn't emmitted enough events: ${ack.id}`); + } + const [success] = ackEvents[1].getAttributesFrom([ + EventAttribute.ATTRIBUTE_KEY.SUCCESS, + ]); + + return success !== undefined; + }); + await this.updateOriginSendStatus( + acksSuccess, + IbcIcs20.STATUS_TYPE.ACK_SUCCESS, + trx + ); + // update error ack status for origin send ics20 + const acksError = ics20Acks.filter((ack) => { + const ackEvents = ack.message.events; + if (ackEvents.length !== 2) { + throw Error(`Ack ibc hasn't emmitted enough events: ${ack.id}`); + } + const [success] = ackEvents[1].getAttributesFrom([ + EventAttribute.ATTRIBUTE_KEY.SUCCESS, + ]); + return success === undefined; + }); + await this.updateOriginSendStatus( + acksError, + IbcIcs20.STATUS_TYPE.ACK_ERROR, + trx + ); + } + } + + async handleIcs20Timeout( + startHeight: number, + endHeight: number, + trx: Knex.Transaction + ) { + const ics20Timeouts = await IbcMessage.query() + .joinRelated('message.transaction') + .modifiers({ + selectIcs20Event(builder) { + builder.where('type', IbcIcs20.EVENT_TYPE.TIMEOUT); + }, + }) + .where('src_port_id', IbcMessage.PORTS.ICS20) + .andWhere('ibc_message.type', IbcMessage.EVENT_TYPE.TIMEOUT_PACKET) + .andWhere('message:transaction.height', '>', startHeight) + .andWhere('message:transaction.height', '<=', endHeight) + .orderBy('message.id') + .transacting(trx); + if (ics20Timeouts.length > 0) { + await this.updateOriginSendStatus( + ics20Timeouts, + IbcIcs20.STATUS_TYPE.TIMEOUT, + trx + ); + } + } + + parseDenom( + denom: string, + isSource: boolean, + dstPort: string, + dstChannel: string + ) { + if (isSource) { + const tokens2 = denom.split('/').slice(2); + return tokens2.join('/'); + } + return `${dstPort}/${dstChannel}/${denom}`; + } + + async updateOriginSendStatus( + msgs: IbcMessage[], + type: string, + trx: Knex.Transaction + ) { + await IbcIcs20.query() + .transacting(trx) + .patch({ + status: type, + }) + .whereIn( + 'sequence_key', + msgs.map((msg) => msg.sequence_key) + ) + .andWhere('type', IbcMessage.EVENT_TYPE.SEND_PACKET); + } + + async _start(): Promise { + await this.createJob( + BULL_JOB_NAME.CRAWL_IBC_ICS20, + BULL_JOB_NAME.CRAWL_IBC_ICS20, + {}, + { + removeOnComplete: true, + removeOnFail: { + count: 3, + }, + repeat: { + every: config.crawlIbcIcs20.millisecondRepeatJob, + }, + } + ); + return super._start(); + } +} diff --git a/src/services/job/create_index_composite_in_attr_partition.service.ts b/src/services/job/create_index_composite_in_attr_partition.service.ts index 33d79a961..b9df42252 100644 --- a/src/services/job/create_index_composite_in_attr_partition.service.ts +++ b/src/services/job/create_index_composite_in_attr_partition.service.ts @@ -104,4 +104,4 @@ export default class CreateIndexCompositeAttrPartitionJob extends BullableServic } ); } -} \ No newline at end of file +} diff --git a/test/unit/services/ibc/crawl_ibc_ics20.spec.ts b/test/unit/services/ibc/crawl_ibc_ics20.spec.ts new file mode 100644 index 000000000..574891955 --- /dev/null +++ b/test/unit/services/ibc/crawl_ibc_ics20.spec.ts @@ -0,0 +1,638 @@ +import { AfterAll, BeforeAll, Describe, Test } from '@jest-decorated/core'; +import { ServiceBroker } from 'moleculer'; +import knex from '../../../../src/common/utils/db_connection'; +import { + Block, + Event, + EventAttribute, + IbcIcs20, + IbcMessage, + Transaction, +} from '../../../../src/models'; +import CrawlIbcIcs20 from '../../../../src/services/ibc/crawl_ibc_ics20.service'; +import config from '../../../../config.json' assert { type: 'json' }; +import { getAttributeFrom } from '../../../../src/common/utils/smart_contract'; + +const PORT = config.crawlIbcIcs20.port; +@Describe('Test crawl ibc-ics20 service') +export default class CrawlIbcIcs20Test { + broker = new ServiceBroker({ logger: false }); + + crawlIbcIcs20Serivce = this.broker.createService( + CrawlIbcIcs20 + ) as CrawlIbcIcs20; + + block: Block = Block.fromJson({ + height: 30000, + hash: '4801997745BDD354C8F11CE4A4137237194099E664CD8F83A5FBA9041C43FE9F', + time: '2023-01-12T01:53:57.216Z', + proposer_address: 'auraomd;cvpio3j4eg', + data: {}, + }); + + transaction = { + ...Transaction.fromJson({ + height: this.block.height, + hash: '4A8B0DE950F563553A81360D4782F6EC451F6BEF7AC50E2459D1997FA168997D', + codespace: '', + code: 0, + gas_used: '123035', + gas_wanted: '141106', + gas_limit: '141106', + fee: 353, + timestamp: '2023-01-12T01:53:57.000Z', + index: 0, + data: { + tx_response: { + logs: [], + }, + }, + }), + messages: [ + { + index: 1, + type: '/cosmwasm.wasm.v1.MsgExecuteContract', + sender: 'aura1uh24g2lc8hvvkaaf7awz25lrh5fptthu2dhq0n', + content: { + msg: '{"add_mint_phase":{"phase_data":{"start_time":"1679976124941000000","end_time":"1679982024941000000","max_supply":2000,"max_nfts_per_address":20,"price":{"amount":"10","denom":"ueaura"},"is_public":false},"token_id": "test"}}', + '@type': '/cosmwasm.wasm.v1.MsgExecuteContract', + funds: [], + sender: 'aura1uh24g2lc8hvvkaaf7awz25lrh5fptthu2dhq0n', + }, + }, + ], + }; + + @BeforeAll() + async initSuite() { + this.crawlIbcIcs20Serivce.getQueueManager().stopAll(); + await knex.raw( + 'TRUNCATE TABLE block, transaction, ibc_message RESTART IDENTITY CASCADE' + ); + await Block.query().insert(this.block); + await Transaction.query().insertGraph(this.transaction); + } + + @AfterAll() + async tearDown() { + await this.broker.stop(); + } + + @Test('Test handleIcs20Send') + async testHandleIcs20Send() { + await knex.transaction(async (trx) => { + const ibcMessage = IbcMessage.fromJson({ + transaction_message_id: 1, + src_channel_id: 'aaa', + src_port_id: PORT, + dst_channel_id: 'cccc', + dst_port_id: 'dddd', + type: IbcMessage.EVENT_TYPE.SEND_PACKET, + sequence: 256, + sequence_key: 'hcc', + data: { + amount: '10000', + denom: 'uatom', + receiver: + '{"autopilot":{"stakeibc":{"stride_address":"stride1e8288j8swfy7rwkyx0h3lz82fe58vz2medxndl","action":"LiquidStake"},"receiver":"stride1e8288j8swfy7rwkyx0h3lz82fe58vz2medxndl"}}', + sender: 'cosmos1e8288j8swfy7rwkyx0h3lz82fe58vz2m6xx0en', + }, + }); + const message = await IbcMessage.query() + .insert(ibcMessage) + .transacting(trx); + await this.crawlIbcIcs20Serivce.handleIcs20Send( + this.block.height - 1, + this.block.height, + trx + ); + const result = await IbcIcs20.query().first().transacting(trx); + expect(result?.ibc_message_id).toEqual(message.id); + expect(result?.sender).toEqual(ibcMessage.data.sender); + expect(result?.receiver).toEqual(ibcMessage.data.receiver); + expect(result?.amount).toEqual(ibcMessage.data.amount); + expect(result?.denom).toEqual(ibcMessage.data.denom); + expect(result?.status).toEqual(IbcIcs20.STATUS_TYPE.ONGOING); + expect(result?.sequence_key).toEqual(ibcMessage.sequence_key); + expect(result?.type).toEqual(ibcMessage.type); + expect(result?.channel_id).toEqual(ibcMessage.src_channel_id); + }); + } + + @Test('Test handleIcs20Recv from source chain') + async testHandleIcs20RecvFromSource() { + await knex.transaction(async (trx) => { + const ibcMessage = IbcMessage.fromJson({ + transaction_message_id: 1, + src_channel_id: 'aaa', + src_port_id: 'bbbb', + dst_channel_id: 'cccc', + dst_port_id: PORT, + type: IbcMessage.EVENT_TYPE.RECV_PACKET, + sequence: 256, + sequence_key: 'hcc', + data: { + amount: '10000', + denom: 'uatom', + receiver: + '{"autopilot":{"stakeibc":{"stride_address":"stride1e8288j8swfy7rwkyx0h3lz82fe58vz2medxndl","action":"LiquidStake"},"receiver":"stride1e8288j8swfy7rwkyx0h3lz82fe58vz2medxndl"}}', + sender: 'cosmos1e8288j8swfy7rwkyx0h3lz82fe58vz2m6xx0en', + }, + }); + const ibcMsg = await IbcMessage.query() + .insert(ibcMessage) + .transacting(trx); + const event1Attrs = [ + { + key: 'module', + value: 'transfer', + }, + { + key: 'sender', + value: 'cosmos1e8288j8swfy7rwkyx0h3lz82fe58vz2m6xx0en', + }, + { + key: 'receiver', + value: 'stride1e8288j8swfy7rwkyx0h3lz82fe58vz2medxndl', + }, + { + key: 'denom', + value: 'uatom', + }, + { + key: 'amount', + value: '10000', + }, + { + key: 'memo', + value: '', + }, + { + key: 'success', + value: 'true', + }, + ]; + const event2Attrs = [ + { + key: 'trace_hash', + value: + '40CA5EF447F368B7F2276A689383BE3C427B15395D4BF6639B605D36C0846A20', + }, + { + key: 'denom', + value: + 'ibc/40CA5EF447F368B7F2276A689383BE3C427B15395D4BF6639B605D36C0846A20', + }, + ]; + const events = [ + Event.fromJson({ + tx_id: 1, + tx_msg_index: 1, + type: IbcIcs20.EVENT_TYPE.FUNGIBLE_TOKEN_PACKET, + block_height: this.block.height, + source: 'TX_EVENT', + attributes: event1Attrs.map((e, index) => { + Object.assign(e, { + block_height: this.block.height, + event_id: 1, + index, + }); + return e; + }), + }), + Event.fromJson({ + tx_id: 1, + tx_msg_index: 1, + type: IbcIcs20.EVENT_TYPE.DENOM_TRACE, + block_height: this.block.height, + source: 'TX_EVENT', + attributes: event2Attrs.map((e, index) => { + Object.assign(e, { + block_height: this.block.height, + event_id: 1, + index, + }); + return e; + }), + }), + ]; + await Event.query().insertGraph(events).transacting(trx); + await this.crawlIbcIcs20Serivce.handleIcs20Recv( + this.block.height - 1, + this.block.height, + trx + ); + const result = await IbcIcs20.query() + .where('type', IbcMessage.EVENT_TYPE.RECV_PACKET) + .first() + .transacting(trx); + expect(result?.ibc_message_id).toEqual(ibcMsg.id); + expect(result?.receiver).toEqual( + getAttributeFrom(event1Attrs, EventAttribute.ATTRIBUTE_KEY.RECEIVER) + ); + expect(result?.sender).toEqual( + getAttributeFrom(event1Attrs, EventAttribute.ATTRIBUTE_KEY.SENDER) + ); + expect(result?.amount).toEqual( + getAttributeFrom(event1Attrs, EventAttribute.ATTRIBUTE_KEY.AMOUNT) + ); + expect(result?.denom).toEqual( + `${ibcMessage.dst_port_id}/${ + ibcMessage.dst_channel_id + }/${getAttributeFrom(event1Attrs, EventAttribute.ATTRIBUTE_KEY.DENOM)}` + ); + expect(result?.status).toEqual(IbcIcs20.STATUS_TYPE.ACK_SUCCESS); + expect(result?.sequence_key).toEqual(ibcMessage.sequence_key); + expect(result?.type).toEqual(ibcMessage.type); + expect(result?.channel_id).toEqual(ibcMessage.dst_channel_id); + await trx.rollback(); + }); + } + + @Test('Test handleIcs20Recv from sink chain') + async testHandleIcs20RecvFromSink() { + await knex.transaction(async (trx) => { + const ibcMessage = IbcMessage.fromJson({ + transaction_message_id: 1, + src_channel_id: 'aaa', + src_port_id: 'bbbb', + dst_channel_id: 'cccc', + dst_port_id: PORT, + type: IbcMessage.EVENT_TYPE.RECV_PACKET, + sequence: 256, + sequence_key: 'hcc', + data: { + amount: '10000', + denom: 'uatom', + receiver: + '{"autopilot":{"stakeibc":{"stride_address":"stride1e8288j8swfy7rwkyx0h3lz82fe58vz2medxndl","action":"LiquidStake"},"receiver":"stride1e8288j8swfy7rwkyx0h3lz82fe58vz2medxndl"}}', + sender: 'cosmos1e8288j8swfy7rwkyx0h3lz82fe58vz2m6xx0en', + }, + }); + const ibcMsg = await IbcMessage.query() + .insert(ibcMessage) + .transacting(trx); + const event1Attrs = [ + { + key: 'module', + value: 'transfer', + }, + { + key: 'sender', + value: 'cosmos1e8288j8swfy7rwkyx0h3lz82fe58vz2m6xx0en', + }, + { + key: 'receiver', + value: 'stride1e8288j8swfy7rwkyx0h3lz82fe58vz2medxndl', + }, + { + key: 'denom', + value: 'hhhh/jjjjj/uatom', + }, + { + key: 'amount', + value: '10000', + }, + { + key: 'memo', + value: '', + }, + { + key: 'success', + value: 'true', + }, + ]; + const events = [ + Event.fromJson({ + tx_id: 1, + tx_msg_index: 1, + type: IbcIcs20.EVENT_TYPE.FUNGIBLE_TOKEN_PACKET, + block_height: this.block.height, + source: 'TX_EVENT', + attributes: event1Attrs.map((e, index) => { + Object.assign(e, { + block_height: this.block.height, + event_id: 1, + index, + }); + return e; + }), + }), + ]; + await Event.query().insertGraph(events).transacting(trx); + await this.crawlIbcIcs20Serivce.handleIcs20Recv( + this.block.height - 1, + this.block.height, + trx + ); + const result = await IbcIcs20.query() + .where('type', IbcMessage.EVENT_TYPE.RECV_PACKET) + .first() + .transacting(trx); + expect(result?.ibc_message_id).toEqual(ibcMsg.id); + expect(result?.receiver).toEqual( + getAttributeFrom(event1Attrs, EventAttribute.ATTRIBUTE_KEY.RECEIVER) + ); + expect(result?.sender).toEqual( + getAttributeFrom(event1Attrs, EventAttribute.ATTRIBUTE_KEY.SENDER) + ); + expect(result?.amount).toEqual( + getAttributeFrom(event1Attrs, EventAttribute.ATTRIBUTE_KEY.AMOUNT) + ); + expect(result?.denom).toEqual('uatom'); + expect(result?.status).toEqual(IbcIcs20.STATUS_TYPE.ACK_SUCCESS); + expect(result?.sequence_key).toEqual(ibcMessage.sequence_key); + expect(result?.type).toEqual(ibcMessage.type); + expect(result?.channel_id).toEqual(ibcMessage.dst_channel_id); + await trx.rollback(); + }); + } + + @Test('Test handleIcs20AckError') + async testHandleIcs20AckError() { + await knex.transaction(async (trx) => { + const ibcMessage = IbcMessage.fromJson({ + transaction_message_id: 1, + src_channel_id: 'aaa', + src_port_id: PORT, + dst_channel_id: 'cccc', + dst_port_id: 'dddd', + type: IbcMessage.EVENT_TYPE.ACKNOWLEDGE_PACKET, + sequence: 256, + sequence_key: 'hcc', + data: { + amount: '10000', + denom: 'uatom', + receiver: + '{"autopilot":{"stakeibc":{"stride_address":"stride1e8288j8swfy7rwkyx0h3lz82fe58vz2medxndl","action":"LiquidStake"},"receiver":"stride1e8288j8swfy7rwkyx0h3lz82fe58vz2medxndl"}}', + sender: 'cosmos1e8288j8swfy7rwkyx0h3lz82fe58vz2m6xx0en', + }, + }); + await IbcMessage.query().insert(ibcMessage).transacting(trx); + const event1Attrs = [ + { + key: 'module', + value: 'transfer', + }, + { + key: 'sender', + value: 'cosmos1e8288j8swfy7rwkyx0h3lz82fe58vz2m6xx0en', + }, + { + key: 'receiver', + value: + '{"autopilot":{"stakeibc":{"stride_address":"stride1e8288j8swfy7rwkyx0h3lz82fe58vz2medxndl","action":"LiquidStake"},"receiver":"stride1e8288j8swfy7rwkyx0h3lz82fe58vz2medxndl"}}', + }, + { + key: 'denom', + value: 'uatom', + }, + { + key: 'amount', + value: '10000', + }, + { + key: 'memo', + value: '', + }, + { + key: 'acknowledgement', + value: 'result:"\\001" ', + }, + ]; + const event2Attrs = [ + { + key: 'error', + value: '\u0001', + }, + ]; + const events = [ + Event.fromJson({ + tx_id: 1, + tx_msg_index: 1, + type: IbcIcs20.EVENT_TYPE.FUNGIBLE_TOKEN_PACKET, + block_height: this.block.height, + source: 'TX_EVENT', + attributes: event1Attrs.map((e, index) => { + Object.assign(e, { + block_height: this.block.height, + event_id: 1, + index, + }); + return e; + }), + }), + Event.fromJson({ + tx_id: 1, + tx_msg_index: 1, + type: IbcIcs20.EVENT_TYPE.FUNGIBLE_TOKEN_PACKET, + block_height: this.block.height, + source: 'TX_EVENT', + attributes: event2Attrs.map((e, index) => { + Object.assign(e, { + block_height: this.block.height, + event_id: 1, + index, + }); + return e; + }), + }), + ]; + await Event.query().insertGraph(events).transacting(trx); + await this.crawlIbcIcs20Serivce.handleIcs20Ack( + this.block.height - 1, + this.block.height, + trx + ); + const originSend = await IbcIcs20.query() + .where('type', IbcMessage.EVENT_TYPE.SEND_PACKET) + .first() + .transacting(trx); + expect(originSend?.status).toEqual(IbcIcs20.STATUS_TYPE.ACK_ERROR); + await trx.rollback(); + }); + } + + @Test('Test handleIcs20AckSuccess') + async testHandleIcs20AckSuccess() { + await knex.transaction(async (trx) => { + const ibcMessage = IbcMessage.fromJson({ + transaction_message_id: 1, + src_channel_id: 'aaa', + src_port_id: PORT, + dst_channel_id: 'cccc', + dst_port_id: 'dddd', + type: IbcMessage.EVENT_TYPE.ACKNOWLEDGE_PACKET, + sequence: 256, + sequence_key: 'hcc', + data: { + amount: '10000', + denom: 'uatom', + receiver: + '{"autopilot":{"stakeibc":{"stride_address":"stride1e8288j8swfy7rwkyx0h3lz82fe58vz2medxndl","action":"LiquidStake"},"receiver":"stride1e8288j8swfy7rwkyx0h3lz82fe58vz2medxndl"}}', + sender: 'cosmos1e8288j8swfy7rwkyx0h3lz82fe58vz2m6xx0en', + }, + }); + await IbcMessage.query().insert(ibcMessage).transacting(trx); + const event1Attrs = [ + { + key: 'module', + value: 'transfer', + }, + { + key: 'sender', + value: 'cosmos1e8288j8swfy7rwkyx0h3lz82fe58vz2m6xx0en', + }, + { + key: 'receiver', + value: + '{"autopilot":{"stakeibc":{"stride_address":"stride1e8288j8swfy7rwkyx0h3lz82fe58vz2medxndl","action":"LiquidStake"},"receiver":"stride1e8288j8swfy7rwkyx0h3lz82fe58vz2medxndl"}}', + }, + { + key: 'denom', + value: 'uatom', + }, + { + key: 'amount', + value: '10000', + }, + { + key: 'memo', + value: '', + }, + { + key: 'acknowledgement', + value: 'result:"\\001" ', + }, + ]; + const event2Attrs = [ + { + key: 'success', + value: '\u0001', + }, + ]; + const events = [ + Event.fromJson({ + tx_id: 1, + tx_msg_index: 1, + type: IbcIcs20.EVENT_TYPE.FUNGIBLE_TOKEN_PACKET, + block_height: this.block.height, + source: 'TX_EVENT', + attributes: event1Attrs.map((e, index) => { + Object.assign(e, { + block_height: this.block.height, + event_id: 1, + index, + }); + return e; + }), + }), + Event.fromJson({ + tx_id: 1, + tx_msg_index: 1, + type: IbcIcs20.EVENT_TYPE.FUNGIBLE_TOKEN_PACKET, + block_height: this.block.height, + source: 'TX_EVENT', + attributes: event2Attrs.map((e, index) => { + Object.assign(e, { + block_height: this.block.height, + event_id: 1, + index, + }); + return e; + }), + }), + ]; + await Event.query().insertGraph(events).transacting(trx); + await this.crawlIbcIcs20Serivce.handleIcs20Ack( + this.block.height - 1, + this.block.height, + trx + ); + const originSend = await IbcIcs20.query() + .where('type', IbcMessage.EVENT_TYPE.SEND_PACKET) + .first() + .transacting(trx); + expect(originSend?.status).toEqual(IbcIcs20.STATUS_TYPE.ACK_SUCCESS); + await trx.rollback(); + }); + } + + @Test('Test handleIcs20Timeout') + async testHandleIcs20Timeout() { + await knex.transaction(async (trx) => { + const ibcMessage = IbcMessage.fromJson({ + transaction_message_id: 1, + src_channel_id: 'aaa', + src_port_id: PORT, + dst_channel_id: 'cccc', + dst_port_id: 'dddd', + type: IbcMessage.EVENT_TYPE.TIMEOUT_PACKET, + sequence: 256, + sequence_key: 'hcc', + data: { + amount: '10000', + denom: 'uatom', + receiver: + '{"autopilot":{"stakeibc":{"stride_address":"stride1e8288j8swfy7rwkyx0h3lz82fe58vz2medxndl","action":"LiquidStake"},"receiver":"stride1e8288j8swfy7rwkyx0h3lz82fe58vz2medxndl"}}', + sender: 'cosmos1e8288j8swfy7rwkyx0h3lz82fe58vz2m6xx0en', + }, + }); + await IbcMessage.query().insert(ibcMessage).transacting(trx); + const event1Attrs = [ + { + key: 'module', + value: 'transfer', + }, + { + key: 'refund_receiver', + value: 'aura1uh24g2lc8hvvkaaf7awz25lrh5fptthu2dhq0n', + }, + { + key: 'refund_denom', + value: 'utaura', + }, + { + key: 'refund_amount', + value: '1000000', + }, + { + key: 'memo', + value: '', + }, + ]; + const events = [ + Event.fromJson({ + tx_id: 1, + tx_msg_index: 1, + type: IbcIcs20.EVENT_TYPE.TIMEOUT, + block_height: this.block.height, + source: 'TX_EVENT', + attributes: event1Attrs.map((e, index) => { + Object.assign(e, { + block_height: this.block.height, + event_id: 1, + index, + }); + return e; + }), + }), + ]; + await Event.query() + .insertGraph(events) + .where('type', IbcMessage.EVENT_TYPE.TIMEOUT_PACKET) + .transacting(trx); + await this.crawlIbcIcs20Serivce.handleIcs20Timeout( + this.block.height - 1, + this.block.height, + trx + ); + const originSend = await IbcIcs20.query() + .where('type', IbcMessage.EVENT_TYPE.SEND_PACKET) + .first() + .transacting(trx); + expect(originSend?.status).toEqual(IbcIcs20.STATUS_TYPE.TIMEOUT); + await trx.rollback(); + }); + } +}