From 75d6ba0bc1e59175e34552f1d3d575964318ff32 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raress96@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:39:48 +0200 Subject: [PATCH] Handle ITS events and send them to GMP API. --- ...oss-chain-transaction.processor.service.ts | 12 + .../cross-chain-transaction.processor.spec.ts | 26 +- .../processors/its.processor.spec.ts | 158 +++++++++++ .../processors/its.processor.ts | 112 ++++++++ .../processors/processors.module.ts | 5 +- .../event.processor.service.spec.ts | 34 +++ .../event.processor.service.ts | 17 ++ .../src/api/entities/axelar.gmp.api.d.ts | 203 ++++++++++++-- .../src/assets/axelar-gmp-api.schema.yaml | 256 +++++++++++++++++- libs/common/src/contracts/contracts.module.ts | 3 +- .../src/contracts/entities/its-events.ts | 20 ++ .../common/src/contracts/its.contract.spec.ts | 118 ++++++++ libs/common/src/contracts/its.contract.ts | 48 +++- libs/common/src/utils/event.enum.ts | 3 + 14 files changed, 982 insertions(+), 33 deletions(-) create mode 100644 apps/mvx-event-processor/src/cross-chain-transaction-processor/processors/its.processor.spec.ts create mode 100644 apps/mvx-event-processor/src/cross-chain-transaction-processor/processors/its.processor.ts create mode 100644 libs/common/src/contracts/entities/its-events.ts create mode 100644 libs/common/src/contracts/its.contract.spec.ts diff --git a/apps/mvx-event-processor/src/cross-chain-transaction-processor/cross-chain-transaction.processor.service.ts b/apps/mvx-event-processor/src/cross-chain-transaction-processor/cross-chain-transaction.processor.service.ts index f9063a4..6983d3f 100644 --- a/apps/mvx-event-processor/src/cross-chain-transaction-processor/cross-chain-transaction.processor.service.ts +++ b/apps/mvx-event-processor/src/cross-chain-transaction-processor/cross-chain-transaction.processor.service.ts @@ -7,16 +7,19 @@ import { ApiNetworkProvider, TransactionOnNetwork } from '@multiversx/sdk-networ import { GasServiceProcessor, GatewayProcessor } from './processors'; import { AxiosError } from 'axios'; import { MessageApprovedEvent } from '@mvx-monorepo/common/api/entities/axelar.gmp.api'; +import { ItsProcessor } from './processors/its.processor'; @Injectable() export class CrossChainTransactionProcessorService { private readonly contractGateway: string; private readonly contractGasService: string; + private readonly contractIts: string; private readonly logger: Logger; constructor( private readonly gatewayProcessor: GatewayProcessor, private readonly gasServiceProcessor: GasServiceProcessor, + private readonly itsProcessor: ItsProcessor, private readonly axelarGmpApi: AxelarGmpApi, private readonly redisHelper: RedisHelper, private readonly api: ApiNetworkProvider, @@ -24,6 +27,7 @@ export class CrossChainTransactionProcessorService { ) { this.contractGateway = apiConfigService.getContractGateway(); this.contractGasService = apiConfigService.getContractGasService(); + this.contractIts = apiConfigService.getContractIts(); this.logger = new Logger(CrossChainTransactionProcessorService.name); } @@ -93,6 +97,14 @@ export class CrossChainTransactionProcessorService { eventsToSend.push(event); } } + + if (address === this.contractIts) { + const event = this.itsProcessor.handleItsEvent(rawEvent, transaction, index); + + if (event) { + eventsToSend.push(event); + } + } } if (!eventsToSend.length) { diff --git a/apps/mvx-event-processor/src/cross-chain-transaction-processor/cross-chain-transaction.processor.spec.ts b/apps/mvx-event-processor/src/cross-chain-transaction-processor/cross-chain-transaction.processor.spec.ts index 534c910..3340bf8 100644 --- a/apps/mvx-event-processor/src/cross-chain-transaction-processor/cross-chain-transaction.processor.spec.ts +++ b/apps/mvx-event-processor/src/cross-chain-transaction-processor/cross-chain-transaction.processor.spec.ts @@ -8,6 +8,7 @@ import { CrossChainTransactionProcessorService } from './cross-chain-transaction import { EventIdentifiers, Events } from '@mvx-monorepo/common/utils/event.enum'; import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; import { GasServiceProcessor, GatewayProcessor } from './processors'; +import { ItsProcessor } from './processors/its.processor'; const mockTransactionResponse = { txHash: '5cc3bf9866b77b6d05b3756a0faff67d7685058579550989f39cb4319bec0fc1', @@ -60,10 +61,12 @@ const mockTransactionResponse = { const mockGatewayContract = 'erd1qqqqqqqqqqqqqpgqvc7gdl0p4s97guh498wgz75k8sav6sjfjlwqh679jy'; const mockGasServiceContract = 'erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l'; +const mockItsContract = 'erd1qqqqqqqqqqqqqpgqc5ypvy2d6z52fwscsfnrwcdkdh2fnthfkkls7kcn9j'; describe('CrossChainTransactionProcessor', () => { let gatewayProcessor: DeepMocked; let gasServiceProcessor: DeepMocked; + let itsProcessor: DeepMocked; let axelarGmpApi: DeepMocked; let redisHelper: DeepMocked; let api: DeepMocked; @@ -74,6 +77,7 @@ describe('CrossChainTransactionProcessor', () => { beforeEach(async () => { gatewayProcessor = createMock(); gasServiceProcessor = createMock(); + itsProcessor = createMock(); axelarGmpApi = createMock(); redisHelper = createMock(); api = createMock(); @@ -81,6 +85,7 @@ describe('CrossChainTransactionProcessor', () => { apiConfigService.getContractGateway.mockReturnValue(mockGatewayContract); apiConfigService.getContractGasService.mockReturnValue(mockGasServiceContract); + apiConfigService.getContractIts.mockReturnValue(mockItsContract); const moduleRef = await Test.createTestingModule({ providers: [CrossChainTransactionProcessorService], @@ -94,6 +99,10 @@ describe('CrossChainTransactionProcessor', () => { return gasServiceProcessor; } + if (token === ItsProcessor) { + return itsProcessor; + } + if (token === AxelarGmpApi) { return axelarGmpApi; } @@ -160,6 +169,12 @@ describe('CrossChainTransactionProcessor', () => { data: '', topics: [BinaryUtils.base64Encode(Events.CONTRACT_CALL_EVENT)], }; + const rawItsEvent = { + address: mockItsContract, + identifier: 'any', + data: '', + topics: [BinaryUtils.base64Encode(Events.INTERCHAIN_TRANSFER_EVENT)], + }; const rawApprovedEvent = { address: mockGatewayContract, identifier: EventIdentifiers.APPROVE_MESSAGES, @@ -175,7 +190,7 @@ describe('CrossChainTransactionProcessor', () => { it('Should handle multiple events', async () => { // @ts-ignore - transaction.logs.events = [rawGasEvent, rawGatewayEvent]; + transaction.logs.events = [rawGasEvent, rawGatewayEvent, rawItsEvent]; redisHelper.smembers.mockReturnValueOnce(Promise.resolve(['txHash'])); api.doGetGeneric.mockReturnValueOnce(Promise.resolve(transaction)); @@ -199,9 +214,16 @@ describe('CrossChainTransactionProcessor', () => { '0', ); + expect(itsProcessor.handleItsEvent).toHaveBeenCalledTimes(1); + expect(itsProcessor.handleItsEvent).toHaveBeenCalledWith( + TransactionEvent.fromHttpResponse(rawItsEvent), + expect.any(TransactionOnNetwork), + 2, + ); + expect(axelarGmpApi.postEvents).toHaveBeenCalledTimes(1); expect(axelarGmpApi.postEvents).toHaveBeenCalledWith(expect.anything(), 'txHash'); - expect(axelarGmpApi.postEvents.mock.lastCall?.[0]).toHaveLength(2); + expect(axelarGmpApi.postEvents.mock.lastCall?.[0]).toHaveLength(3); expect(redisHelper.srem).toHaveBeenCalledTimes(1); expect(redisHelper.srem).toHaveBeenCalledWith('crossChainTransactions', 'txHash'); diff --git a/apps/mvx-event-processor/src/cross-chain-transaction-processor/processors/its.processor.spec.ts b/apps/mvx-event-processor/src/cross-chain-transaction-processor/processors/its.processor.spec.ts new file mode 100644 index 0000000..5b02f36 --- /dev/null +++ b/apps/mvx-event-processor/src/cross-chain-transaction-processor/processors/its.processor.spec.ts @@ -0,0 +1,158 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; +import { Events } from '@mvx-monorepo/common/utils/event.enum'; +import { Address, ITransactionEvent } from '@multiversx/sdk-core/out'; +import { TransactionEvent, TransactionOnNetwork } from '@multiversx/sdk-network-providers/out'; +import BigNumber from 'bignumber.js'; +import { ItsProcessor } from './its.processor'; +import { ItsContract } from '@mvx-monorepo/common/contracts/its.contract'; +import { + InterchainTokenDeploymentStartedEvent, + InterchainTransferEvent, +} from '@mvx-monorepo/common/contracts/entities/its-events'; +import { Components } from '@mvx-monorepo/common/api/entities/axelar.gmp.api'; +import ITSInterchainTokenDeploymentStartedEvent = Components.Schemas.ITSInterchainTokenDeploymentStartedEvent; +import ITSInterchainTransferEvent = Components.Schemas.ITSInterchainTransferEvent; + +const mockItsContract = 'erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l'; + +describe('ItsProcessor', () => { + let itsContract: DeepMocked; + + let service: ItsProcessor; + + beforeEach(async () => { + itsContract = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ItsProcessor], + }) + .useMocker((token) => { + if (token === ItsContract) { + return itsContract; + } + + return null; + }) + .compile(); + + service = module.get(ItsProcessor); + }); + + it('Should not handle event', () => { + const rawEvent: ITransactionEvent = TransactionEvent.fromHttpResponse({ + address: 'erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l', + identifier: 'callContract', + data: '', + topics: [BinaryUtils.base64Encode(Events.CONTRACT_CALL_EVENT)], + }); + + const result = service.handleItsEvent(rawEvent, createMock(), 0); + + expect(result).toBeUndefined(); + expect(itsContract.decodeInterchainTokenDeploymentStartedEvent).not.toHaveBeenCalled(); + expect(itsContract.decodeInterchainTransferEvent).not.toHaveBeenCalled(); + }); + + describe('Handle interchain token deployment started event', () => { + const rawEvent: TransactionEvent = TransactionEvent.fromHttpResponse({ + address: mockItsContract, + identifier: 'any', + data: '', + topics: [BinaryUtils.base64Encode(Events.INTERCHAIN_TOKEN_DEPLOYMENT_STARTED_EVENT)], + }); + + const interchainTokenDeploymentStartedEvent: InterchainTokenDeploymentStartedEvent = { + tokenId: '0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15da', + name: 'name', + symbol: 'symbol', + decimals: 6, + minter: Buffer.from('F12372616f9c986355414BA06b3Ca954c0a7b0dC', 'hex'), + destinationChain: 'ethereum', + }; + + it('Should handle', () => { + itsContract.decodeInterchainTokenDeploymentStartedEvent.mockReturnValueOnce( + interchainTokenDeploymentStartedEvent, + ); + + const transaction = createMock(); + transaction.hash = 'txHash'; + transaction.sender = Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'); + + const result = service.handleItsEvent(rawEvent, transaction, 1); + + expect(result).not.toBeUndefined(); + expect(result?.type).toBe('ITS/INTERCHAIN_TOKEN_DEPLOYMENT_STARTED'); + + const event = result as ITSInterchainTokenDeploymentStartedEvent; + + expect(event.eventID).toBe('0xtxHash-1'); + expect(event.messageID).toBe('0xtxHash-0'); + expect(event.destinationChain).toBe(interchainTokenDeploymentStartedEvent.destinationChain); + expect(event.token).toEqual({ + id: `0x${interchainTokenDeploymentStartedEvent.tokenId}`, + name: interchainTokenDeploymentStartedEvent.name, + symbol: interchainTokenDeploymentStartedEvent.symbol, + decimals: interchainTokenDeploymentStartedEvent.decimals, + }); + expect(event.meta).toEqual({ + txID: 'txHash', + fromAddress: transaction.sender.bech32(), + finalized: true, + }); + }); + }); + + describe('Handle interchain transfer event', () => { + const rawEvent: TransactionEvent = TransactionEvent.fromHttpResponse({ + address: mockItsContract, + identifier: 'any', + data: '', + topics: [BinaryUtils.base64Encode(Events.INTERCHAIN_TRANSFER_EVENT)], + }); + + const interchainTransferEvent: InterchainTransferEvent = { + tokenId: '0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15da', + sourceAddress: Address.newFromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'), + dataHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + destinationChain: 'ethereum', + destinationAddress: Buffer.from('destinationAddress'), + amount: new BigNumber('1000000'), + }; + + it('Should handle', () => { + itsContract.decodeInterchainTransferEvent.mockReturnValueOnce( + interchainTransferEvent, + ); + + const transaction = createMock(); + transaction.hash = 'txHash'; + transaction.sender = Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'); + + const result = service.handleItsEvent(rawEvent, transaction, 1); + + expect(result).not.toBeUndefined(); + expect(result?.type).toBe('ITS/INTERCHAIN_TRANSFER'); + + const event = result as ITSInterchainTransferEvent; + + expect(event.eventID).toBe('0xtxHash-1'); + expect(event.messageID).toBe('0xtxHash-0'); + expect(event.destinationChain).toBe(interchainTransferEvent.destinationChain); + expect(event.tokenSpent).toEqual({ + tokenID: `0x${interchainTransferEvent.tokenId}`, + amount: '1000000', + }); + expect(event.sourceAddress).toBe('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'); + expect(event.destinationAddress).toBe(BinaryUtils.hexToBase64(interchainTransferEvent.destinationAddress.toString('hex'))); + expect(event.dataHash).toBe(BinaryUtils.hexToBase64(interchainTransferEvent.dataHash)); + expect(event.meta).toEqual({ + txID: 'txHash', + fromAddress: 'erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7', + finalized: true, + }); + }); + }); +}); diff --git a/apps/mvx-event-processor/src/cross-chain-transaction-processor/processors/its.processor.ts b/apps/mvx-event-processor/src/cross-chain-transaction-processor/processors/its.processor.ts new file mode 100644 index 0000000..2437eb0 --- /dev/null +++ b/apps/mvx-event-processor/src/cross-chain-transaction-processor/processors/its.processor.ts @@ -0,0 +1,112 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Events } from '@mvx-monorepo/common/utils/event.enum'; +import { ITransactionEvent } from '@multiversx/sdk-core/out'; +import { DecodingUtils } from '@mvx-monorepo/common/utils/decoding.utils'; +import { Components } from '@mvx-monorepo/common/api/entities/axelar.gmp.api'; +import { TransactionOnNetwork } from '@multiversx/sdk-network-providers/out'; +import { ItsContract } from '@mvx-monorepo/common/contracts/its.contract'; +import Event = Components.Schemas.Event; +import ITSInterchainTransferEvent = Components.Schemas.ITSInterchainTransferEvent; +import ITSInterchainTokenDeploymentStartedEvent = Components.Schemas.ITSInterchainTokenDeploymentStartedEvent; + +@Injectable() +export class ItsProcessor { + private logger: Logger; + + constructor(private readonly itsContract: ItsContract) { + this.logger = new Logger(ItsProcessor.name); + } + + handleItsEvent(rawEvent: ITransactionEvent, transaction: TransactionOnNetwork, index: number): Event | undefined { + const eventName = rawEvent.topics?.[0]?.toString(); + + if (eventName === Events.INTERCHAIN_TOKEN_DEPLOYMENT_STARTED_EVENT) { + return this.handleInterchainTokenDeploymentStartedEvent( + rawEvent, + transaction.sender.bech32(), + transaction.hash, + index, + ); + } + + if (eventName === Events.INTERCHAIN_TRANSFER_EVENT) { + return this.handleInterchainTransferEvent(rawEvent, transaction.sender.bech32(), transaction.hash, index); + } + + return undefined; + } + + private handleInterchainTokenDeploymentStartedEvent( + rawEvent: ITransactionEvent, + sender: string, + txHash: string, + index: number, + ): Event { + const interchainTokenDeploymentStartedEvent = + this.itsContract.decodeInterchainTokenDeploymentStartedEvent(rawEvent); + + const event: ITSInterchainTokenDeploymentStartedEvent = { + eventID: DecodingUtils.getEventId(txHash, index), + messageID: DecodingUtils.getEventId(txHash, index - 1), // Contract Call event happens before this event + destinationChain: interchainTokenDeploymentStartedEvent.destinationChain, + token: { + id: `0x${interchainTokenDeploymentStartedEvent.tokenId}`, + name: interchainTokenDeploymentStartedEvent.name, + symbol: interchainTokenDeploymentStartedEvent.symbol, + decimals: interchainTokenDeploymentStartedEvent.decimals, + }, + meta: { + txID: txHash, + fromAddress: sender, + finalized: true, + }, + }; + + this.logger.debug( + `Successfully handled interchain token deployment started event from transaction ${txHash}, log index ${index}`, + event, + ); + + return { + type: 'ITS/INTERCHAIN_TOKEN_DEPLOYMENT_STARTED', + ...event, + }; + } + + private handleInterchainTransferEvent( + rawEvent: ITransactionEvent, + sender: string, + txHash: string, + index: number, + ): Event { + const interchainTransferEvent = this.itsContract.decodeInterchainTransferEvent(rawEvent); + + const event: ITSInterchainTransferEvent = { + eventID: DecodingUtils.getEventId(txHash, index), + messageID: DecodingUtils.getEventId(txHash, index - 1), // Contract Call event happens before this event + destinationChain: interchainTransferEvent.destinationChain, + tokenSpent: { + tokenID: `0x${interchainTransferEvent.tokenId}`, + amount: interchainTransferEvent.amount.toFixed(), + }, + sourceAddress: interchainTransferEvent.sourceAddress.bech32(), + destinationAddress: interchainTransferEvent.destinationAddress.toString('base64'), + dataHash: Buffer.from(interchainTransferEvent.dataHash, 'hex').toString('base64'), + meta: { + txID: txHash, + fromAddress: sender, + finalized: true, + }, + }; + + this.logger.debug( + `Successfully handled interchain transfer event from transaction ${txHash}, log index ${index}`, + event, + ); + + return { + type: 'ITS/INTERCHAIN_TRANSFER', + ...event, + }; + } +} diff --git a/apps/mvx-event-processor/src/cross-chain-transaction-processor/processors/processors.module.ts b/apps/mvx-event-processor/src/cross-chain-transaction-processor/processors/processors.module.ts index 906e8c9..2de7ae2 100644 --- a/apps/mvx-event-processor/src/cross-chain-transaction-processor/processors/processors.module.ts +++ b/apps/mvx-event-processor/src/cross-chain-transaction-processor/processors/processors.module.ts @@ -4,10 +4,11 @@ import { ContractsModule } from '@mvx-monorepo/common/contracts/contracts.module import { DatabaseModule } from '@mvx-monorepo/common'; import { ApiModule } from '@mvx-monorepo/common/api/api.module'; import { GasServiceProcessor } from './gas-service.processor'; +import { ItsProcessor } from './its.processor'; @Module({ imports: [ContractsModule, DatabaseModule, ApiModule], - providers: [GatewayProcessor, GasServiceProcessor], - exports: [GatewayProcessor, GasServiceProcessor], + providers: [GatewayProcessor, GasServiceProcessor, ItsProcessor], + exports: [GatewayProcessor, GasServiceProcessor, ItsProcessor], }) export class ProcessorsModule {} diff --git a/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts b/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts index 65ed942..6903038 100644 --- a/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts +++ b/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts @@ -18,6 +18,7 @@ describe('EventProcessorService', () => { apiConfigService.getContractGateway.mockReturnValue('mockGatewayAddress'); apiConfigService.getContractGasService.mockReturnValue('mockGasServiceAddress'); + apiConfigService.getContractIts.mockReturnValue('mockItsAddress'); const moduleRef = await Test.createTestingModule({ providers: [EventProcessorService], @@ -70,6 +71,15 @@ describe('EventProcessorService', () => { BinaryUtils.base64Encode('any'), ], }, + { + txHash: 'test', + address: 'mockItsAddress', + identifier: 'any', + data: '', + topics: [ + BinaryUtils.base64Encode('any'), + ], + }, ], }; @@ -136,5 +146,29 @@ describe('EventProcessorService', () => { expect(redisHelper.sadd).toHaveBeenCalledTimes(1); expect(redisHelper.sadd).toHaveBeenCalledWith('crossChainTransactions', 'test'); }); + + it('Should consume ITS event', async () => { + const blockEvent: NotifierBlockEvent = { + hash: 'test', + shardId: 1, + timestamp: 123456, + events: [ + { + txHash: 'test', + address: 'mockItsAddress', + identifier: 'any', + data: '', + topics: [ + BinaryUtils.base64Encode('interchain_transfer_event'), + ], + }, + ], + }; + + await service.consumeEvents(blockEvent); + + expect(redisHelper.sadd).toHaveBeenCalledTimes(1); + expect(redisHelper.sadd).toHaveBeenCalledWith('crossChainTransactions', 'test'); + }); }); }); diff --git a/apps/mvx-event-processor/src/event-processor/event.processor.service.ts b/apps/mvx-event-processor/src/event-processor/event.processor.service.ts index 5de7661..581597a 100644 --- a/apps/mvx-event-processor/src/event-processor/event.processor.service.ts +++ b/apps/mvx-event-processor/src/event-processor/event.processor.service.ts @@ -11,6 +11,7 @@ import { EventIdentifiers, Events } from '@mvx-monorepo/common/utils/event.enum' export class EventProcessorService { private readonly contractGateway: string; private readonly contractGasService: string; + private readonly contractIts: string; private readonly logger: Logger; constructor( @@ -19,6 +20,7 @@ export class EventProcessorService { ) { this.contractGateway = apiConfigService.getContractGateway(); this.contractGasService = apiConfigService.getContractGasService(); + this.contractIts = apiConfigService.getContractIts(); this.logger = new Logger(EventProcessorService.name); } @@ -89,6 +91,21 @@ export class EventProcessorService { return validEvent; } + if (event.address === this.contractIts) { + const eventName = BinaryUtils.base64Decode(event.topics[0]); + + const validEvent = + eventName === Events.INTERCHAIN_TOKEN_DEPLOYMENT_STARTED_EVENT || + eventName === Events.INTERCHAIN_TRANSFER_EVENT; + + if (validEvent) { + this.logger.debug('Received ITS event from MultiversX:'); + this.logger.debug(JSON.stringify(event)); + } + + return validEvent; + } + return false; } } diff --git a/libs/common/src/api/entities/axelar.gmp.api.d.ts b/libs/common/src/api/entities/axelar.gmp.api.d.ts index 0ff6676..7d89bdc 100644 --- a/libs/common/src/api/entities/axelar.gmp.api.d.ts +++ b/libs/common/src/api/entities/axelar.gmp.api.d.ts @@ -58,6 +58,7 @@ declare namespace Components { fromAddress?: string | null; finalized?: boolean | null; parentMessageID?: string | null; + parentSourceChain?: string | null; } | null; message: GatewayV2Message; destinationChain: string; @@ -70,6 +71,7 @@ declare namespace Components { fromAddress?: string | null; finalized?: boolean | null; parentMessageID?: string | null; + parentSourceChain?: string | null; } export interface CannotExecuteMessageEvent { eventID: string; @@ -117,7 +119,7 @@ declare namespace Components { } export type Event = { type: EventType; - } & (GasCreditEvent | GasRefundedEvent | CallEvent | MessageApprovedEvent | MessageExecutedEvent | CannotExecuteMessageEvent | CannotExecuteMessageEventV2 | SignersRotatedEvent); + } & (GasCreditEvent | GasRefundedEvent | CallEvent | MessageApprovedEvent | MessageExecutedEvent | CannotExecuteMessageEvent | CannotExecuteMessageEventV2 | SignersRotatedEvent | ITSInterchainTokenDeploymentStartedEvent | ITSInterchainTransferEvent | ITSAppInterchainTransferSentEvent | ITSAppInterchainTransferReceivedEvent); export interface EventBase { eventID: string; meta?: { @@ -133,7 +135,7 @@ declare namespace Components { fromAddress?: string | null; finalized?: boolean | null; } - export type EventType = "GAS_CREDIT" | "GAS_REFUNDED" | "CALL" | "MESSAGE_APPROVED" | "MESSAGE_EXECUTED" | "CANNOT_EXECUTE_MESSAGE" | "CANNOT_EXECUTE_MESSAGE/V2" | "SIGNERS_ROTATED"; + export type EventType = "GAS_CREDIT" | "GAS_REFUNDED" | "CALL" | "MESSAGE_APPROVED" | "MESSAGE_EXECUTED" | "CANNOT_EXECUTE_MESSAGE" | "CANNOT_EXECUTE_MESSAGE/V2" | "SIGNERS_ROTATED" | "ITS/INTERCHAIN_TOKEN_DEPLOYMENT_STARTED" | "ITS/INTERCHAIN_TRANSFER" | "ITS_APP/INTERCHAIN_TRANSFER_SENT" | "ITS_APP/INTERCHAIN_TRANSFER_RECEIVED"; export interface ExecuteTask { message: GatewayV2Message; payload: string; // byte @@ -177,6 +179,83 @@ declare namespace Components { export interface GetTasksResult { tasks: TaskItem[]; } + export interface ITSAppEventMetadata { + txID?: string | null; + timestamp?: string; // date-time + fromAddress?: string | null; + finalized?: boolean | null; + emittedByAddress?: string | null; + } + export interface ITSAppInterchainTransferReceivedEvent { + eventID: string; + meta?: { + txID?: string | null; + timestamp?: string; // date-time + fromAddress?: string | null; + finalized?: boolean | null; + emittedByAddress?: string | null; + } | null; + messageID: string; + sourceChain: string; + sourceAddress: Address; + sender: string; // byte + recipient: Address; + tokenReceived: InterchainTransferToken; + } + export interface ITSAppInterchainTransferSentEvent { + eventID: string; + meta?: { + txID?: string | null; + timestamp?: string; // date-time + fromAddress?: string | null; + finalized?: boolean | null; + emittedByAddress?: string | null; + } | null; + messageID: string; + destinationChain: string; + destinationContractAddress: Address; + sender: Address; + recipient: string; // byte + tokenSpent: InterchainTransferToken; + } + export interface ITSInterchainTokenDeploymentStartedEvent { + eventID: string; + meta?: { + txID?: string | null; + timestamp?: string; // date-time + fromAddress?: string | null; + finalized?: boolean | null; + } | null; + messageID: string; + destinationChain: string; + token: InterchainTokenDefinition; + } + export interface ITSInterchainTransferEvent { + eventID: string; + meta?: { + txID?: string | null; + timestamp?: string; // date-time + fromAddress?: string | null; + finalized?: boolean | null; + } | null; + messageID: string; + destinationChain: string; + tokenSpent: Token; + sourceAddress: Address; + destinationAddress: string; // byte + dataHash: string; // byte + } + export interface InterchainTokenDefinition { + id: string; + name: string; + symbol: string; + decimals: number; // uint8 + } + export interface InterchainTransferToken { + tokenAddress: Address; + amount: BigInt /* ^(0|[1-9]\d*)$ */; + } + export type Keccak256Hash = string; // ^0x[0-9a-f]{64}$ export interface MessageApprovedEvent { eventID: string; meta?: { @@ -205,6 +284,7 @@ declare namespace Components { finalized?: boolean | null; commandID?: string | null; childMessageIDs?: string[] | null; + revertReason?: string | null; } | null; messageID: string; sourceChain: string; @@ -218,6 +298,7 @@ declare namespace Components { finalized?: boolean | null; commandID?: string | null; childMessageIDs?: string[] | null; + revertReason?: string | null; } export type MessageExecutionStatus = "SUCCESSFUL" | "REVERTED"; export interface PublishEventAcceptedResult { @@ -368,6 +449,9 @@ declare namespace Components { signersHash?: string; // byte epoch?: number; // int64 } + export interface StorePayloadResult { + keccak256: Keccak256Hash /* ^0x[0-9a-f]{64}$ */; + } export type Task = ConstructProofTask | GatewayTransactionTask | ExecuteTask | RefundTask | VerifyTask; export interface TaskItem { id: string; @@ -384,6 +468,7 @@ declare namespace Components { } export interface VerifyTask { message: GatewayV2Message; + destinationChain: string; payload: string; // byte } } @@ -403,23 +488,6 @@ declare namespace Paths { export type $500 = Components.Schemas.ErrorResponse; } } - namespace Chains$ChainEvents { - namespace Post { - namespace Parameters { - export type Chain = string; - } - export interface PathParameters { - chain: Parameters.Chain; - } - export type RequestBody = Components.Schemas.PublishEventsRequest; - namespace Responses { - export type $200 = Components.Schemas.PublishEventsResult; - export type $400 = Components.Schemas.ErrorResponse; - export type $404 = Components.Schemas.ErrorResponse; - export type $500 = Components.Schemas.ErrorResponse; - } - } - } namespace GetMsgExecuteContractBroadcastStatus { namespace Parameters { export type BroadcastID = Components.Schemas.BroadcastID; @@ -435,6 +503,19 @@ declare namespace Paths { export type $500 = Components.Schemas.ErrorResponse; } } + namespace GetPayload { + namespace Parameters { + export type Hash = Components.Schemas.Keccak256Hash /* ^0x[0-9a-f]{64}$ */; + } + export interface PathParameters { + hash: Parameters.Hash; + } + namespace Responses { + export type $200 = string; // binary + export type $404 = Components.Schemas.ErrorResponse; + export type $500 = Components.Schemas.ErrorResponse; + } + } namespace GetTasks { namespace Parameters { export type After = string; @@ -460,6 +541,29 @@ declare namespace Paths { } } } + namespace PublishEvents { + namespace Parameters { + export type Chain = string; + } + export interface PathParameters { + chain: Parameters.Chain; + } + export type RequestBody = Components.Schemas.PublishEventsRequest; + namespace Responses { + export type $200 = Components.Schemas.PublishEventsResult; + export type $400 = Components.Schemas.ErrorResponse; + export type $404 = Components.Schemas.ErrorResponse; + export type $500 = Components.Schemas.ErrorResponse; + } + } + namespace StorePayload { + export type RequestBody = string; // binary + namespace Responses { + export type $200 = Components.Schemas.StorePayloadResult; + export type $400 = Components.Schemas.ErrorResponse; + export type $500 = Components.Schemas.ErrorResponse; + } + } } export interface OperationMethods { @@ -487,6 +591,14 @@ export interface OperationMethods { data?: any, config?: AxiosRequestConfig ): OperationResponse + /** + * publishEvents - Publish on-chain events + */ + 'publishEvents'( + parameters?: Parameters | null, + data?: Paths.PublishEvents.RequestBody, + config?: AxiosRequestConfig + ): OperationResponse /** * getTasks - Poll transaction to be executed on chain */ @@ -495,6 +607,22 @@ export interface OperationMethods { data?: any, config?: AxiosRequestConfig ): OperationResponse + /** + * storePayload - Temporarily store a large payload against its hash to bypass size restrictions on some chains. + */ + 'storePayload'( + parameters?: Parameters | null, + data?: Paths.StorePayload.RequestBody, + config?: AxiosRequestConfig + ): OperationResponse + /** + * getPayload - Retrieve a stored payload by its hash + */ + 'getPayload'( + parameters?: Parameters | null, + data?: any, + config?: AxiosRequestConfig + ): OperationResponse } export interface PathsDictionary { @@ -529,6 +657,14 @@ export interface PathsDictionary { ): OperationResponse } ['/chains/{chain}/events']: { + /** + * publishEvents - Publish on-chain events + */ + 'post'( + parameters?: Parameters | null, + data?: Paths.PublishEvents.RequestBody, + config?: AxiosRequestConfig + ): OperationResponse } ['/chains/{chain}/tasks']: { /** @@ -540,6 +676,26 @@ export interface PathsDictionary { config?: AxiosRequestConfig ): OperationResponse } + ['/payloads']: { + /** + * storePayload - Temporarily store a large payload against its hash to bypass size restrictions on some chains. + */ + 'post'( + parameters?: Parameters | null, + data?: Paths.StorePayload.RequestBody, + config?: AxiosRequestConfig + ): OperationResponse + } + ['/payloads/{hash}']: { + /** + * getPayload - Retrieve a stored payload by its hash + */ + 'get'( + parameters?: Parameters | null, + data?: any, + config?: AxiosRequestConfig + ): OperationResponse + } } export type Client = OpenAPIClient @@ -572,6 +728,14 @@ export type GasRefundedEvent = Components.Schemas.GasRefundedEvent; export type GatewayTransactionTask = Components.Schemas.GatewayTransactionTask; export type GatewayV2Message = Components.Schemas.GatewayV2Message; export type GetTasksResult = Components.Schemas.GetTasksResult; +export type ITSAppEventMetadata = Components.Schemas.ITSAppEventMetadata; +export type ITSAppInterchainTransferReceivedEvent = Components.Schemas.ITSAppInterchainTransferReceivedEvent; +export type ITSAppInterchainTransferSentEvent = Components.Schemas.ITSAppInterchainTransferSentEvent; +export type ITSInterchainTokenDeploymentStartedEvent = Components.Schemas.ITSInterchainTokenDeploymentStartedEvent; +export type ITSInterchainTransferEvent = Components.Schemas.ITSInterchainTransferEvent; +export type InterchainTokenDefinition = Components.Schemas.InterchainTokenDefinition; +export type InterchainTransferToken = Components.Schemas.InterchainTransferToken; +export type Keccak256Hash = Components.Schemas.Keccak256Hash; export type MessageApprovedEvent = Components.Schemas.MessageApprovedEvent; export type MessageApprovedEventMetadata = Components.Schemas.MessageApprovedEventMetadata; export type MessageExecutedEvent = Components.Schemas.MessageExecutedEvent; @@ -587,6 +751,7 @@ export type PublishEventsResult = Components.Schemas.PublishEventsResult; export type RefundTask = Components.Schemas.RefundTask; export type SignersRotatedEvent = Components.Schemas.SignersRotatedEvent; export type SignersRotatedEventMetadata = Components.Schemas.SignersRotatedEventMetadata; +export type StorePayloadResult = Components.Schemas.StorePayloadResult; export type Task = Components.Schemas.Task; export type TaskItem = Components.Schemas.TaskItem; export type TaskItemID = Components.Schemas.TaskItemID; diff --git a/libs/common/src/assets/axelar-gmp-api.schema.yaml b/libs/common/src/assets/axelar-gmp-api.schema.yaml index 021530c..8f6ef71 100644 --- a/libs/common/src/assets/axelar-gmp-api.schema.yaml +++ b/libs/common/src/assets/axelar-gmp-api.schema.yaml @@ -71,7 +71,7 @@ paths: /chains/{chain}/events: post: summary: Publish on-chain events - operationID: publishEvents + operationId: publishEvents parameters: - $ref: '#/components/parameters/chain' requestBody: @@ -132,6 +132,66 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + /payloads: + post: + summary: Temporarily store a large payload against its hash to bypass size restrictions on some chains. + operationId: storePayload + requestBody: + required: true + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/StorePayloadResult' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /payloads/{hash}: + get: + summary: Retrieve a stored payload by its hash + operationId: getPayload + parameters: + - name: hash + in: path + required: true + schema: + $ref: '#/components/schemas/Keccak256Hash' + responses: + '200': + description: OK + content: + application/octet-stream: + schema: + type: string + format: binary + '404': + description: Payload Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' components: parameters: chain: @@ -199,6 +259,10 @@ components: - CANNOT_EXECUTE_MESSAGE - CANNOT_EXECUTE_MESSAGE/V2 - SIGNERS_ROTATED + - ITS/INTERCHAIN_TOKEN_DEPLOYMENT_STARTED + - ITS/INTERCHAIN_TRANSFER + - ITS_APP/INTERCHAIN_TRANSFER_SENT + - ITS_APP/INTERCHAIN_TRANSFER_RECEIVED x-enum-varnames: - EventTypeGasCredit - EventTypeGasRefunded @@ -208,6 +272,10 @@ components: - EventTypeCannotExecuteMessage - EventTypeCannotExecuteMessageV2 - EventTypeSignersRotated + - EventTypeITSInterchainTokenDeploymentStarted + - EventTypeITSInterchainTransfer + - EventTypeITSAppInterchainTransferSent + - EventTypeITSAppInterchainTransferReceived EventMetadata: type: object properties: @@ -238,6 +306,11 @@ components: nullable: true x-omitempty: true minLength: 1 + parentSourceChain: + type: string + nullable: true + x-omitempty: true + minLength: 1 MessageApprovedEventMetadata: allOf: - $ref: '#/components/schemas/EventMetadata' @@ -265,6 +338,10 @@ components: items: type: string minLength: 1 + revertReason: + type: string + nullable: true + x-omitempty: true CannotExecuteMessageEventMetadata: type: object properties: @@ -301,6 +378,15 @@ components: type: integer format: int64 minimum: 0 + ITSAppEventMetadata: + allOf: + - $ref: '#/components/schemas/EventMetadata' + - properties: + emittedByAddress: + allOf: + - $ref: '#/components/schemas/Address' + nullable: true + x-omitempty: true Event: oneOf: - $ref: '#/components/schemas/GasCreditEvent' @@ -311,6 +397,10 @@ components: - $ref: '#/components/schemas/CannotExecuteMessageEvent' - $ref: '#/components/schemas/CannotExecuteMessageEventV2' - $ref: '#/components/schemas/SignersRotatedEvent' + - $ref: '#/components/schemas/ITSInterchainTokenDeploymentStartedEvent' + - $ref: '#/components/schemas/ITSInterchainTransferEvent' + - $ref: '#/components/schemas/ITSAppInterchainTransferSentEvent' + - $ref: '#/components/schemas/ITSAppInterchainTransferReceivedEvent' discriminator: propertyName: type mapping: @@ -322,6 +412,10 @@ components: CANNOT_EXECUTE_MESSAGE: '#/components/schemas/CannotExecuteMessageEvent' CANNOT_EXECUTE_MESSAGE/V2: '#/components/schemas/CannotExecuteMessageEventV2' SIGNERS_ROTATED: '#/components/schemas/SignersRotatedEvent' + ITS/INTERCHAIN_TOKEN_DEPLOYMENT_STARTED: '#/components/schemas/ITSInterchainTokenDeploymentStartedEvent' + ITS/INTERCHAIN_TRANSFER: '#/components/schemas/ITSInterchainTransferEvent' + ITS_APP/INTERCHAIN_TRANSFER_SENT: '#/components/schemas/ITSAppInterchainTransferSentEvent' + ITS_APP/INTERCHAIN_TRANSFER_RECEIVED: '#/components/schemas/ITSAppInterchainTransferReceivedEvent' properties: type: $ref: '#/components/schemas/EventType' @@ -525,6 +619,120 @@ components: minLength: 1 required: - messageID + ITSInterchainTokenDeploymentStartedEvent: + type: object + allOf: + - $ref: '#/components/schemas/EventBase' + - properties: + messageID: + type: string + minLength: 1 + destinationChain: + type: string + minLength: 1 + token: + $ref: '#/components/schemas/InterchainTokenDefinition' + required: + - destinationChain + - messageID + - token + ITSInterchainTransferEvent: + type: object + allOf: + - $ref: '#/components/schemas/EventBase' + - properties: + messageID: + type: string + minLength: 1 + destinationChain: + type: string + minLength: 1 + tokenSpent: + $ref: '#/components/schemas/Token' + sourceAddress: + $ref: '#/components/schemas/Address' + destinationAddress: + type: string + format: byte + minLength: 1 + dataHash: + type: string + format: byte + minLength: 1 + required: + - messageID + - destinationChain + - tokenSpent + - sourceAddress + - destinationAddress + - dataHash + ITSAppInterchainTransferSentEvent: + type: object + allOf: + - $ref: '#/components/schemas/EventBase' + - properties: + meta: + allOf: + - $ref: '#/components/schemas/ITSAppEventMetadata' + nullable: true + x-omitempty: true + - properties: + messageID: + type: string + minLength: 1 + destinationChain: + type: string + minLength: 1 + destinationContractAddress: + $ref: '#/components/schemas/Address' + sender: + $ref: '#/components/schemas/Address' + recipient: + type: string + format: byte + minLength: 1 + tokenSpent: + $ref: '#/components/schemas/InterchainTransferToken' + required: + - messageID + - destinationChain + - destinationContractAddress + - sender + - recipient + - tokenSpent + ITSAppInterchainTransferReceivedEvent: + type: object + allOf: + - $ref: '#/components/schemas/EventBase' + - properties: + meta: + allOf: + - $ref: '#/components/schemas/ITSAppEventMetadata' + nullable: true + x-omitempty: true + - properties: + messageID: + type: string + minLength: 1 + sourceChain: + type: string + minLength: 1 + sourceAddress: + $ref: '#/components/schemas/Address' + sender: + type: string + format: byte + recipient: + $ref: '#/components/schemas/Address' + tokenReceived: + $ref: '#/components/schemas/InterchainTransferToken' + required: + - messageID + - sourceChain + - sourceAddress + - sender + - recipient + - tokenReceived GatewayV2Message: type: object properties: @@ -560,6 +768,38 @@ components: $ref: '#/components/schemas/BigInt' required: - amount + InterchainTransferToken: + type: object + properties: + tokenAddress: + $ref: '#/components/schemas/Address' + amount: + $ref: '#/components/schemas/BigInt' + required: + - tokenAddress + - amount + InterchainTokenDefinition: + type: object + properties: + id: + type: string + minLength: 1 + x-go-name: ID + name: + type: string + minLength: 1 + symbol: + type: string + minLength: 1 + decimals: + type: integer + format: uint8 + minimum: 0 + required: + - id + - name + - symbol + - decimals PublishEventsResult: type: object properties: @@ -720,12 +960,16 @@ components: properties: message: $ref: "#/components/schemas/GatewayV2Message" + destinationChain: + type: string + minLength: 1 payload: type: string format: byte minLength: 1 required: - message + - destinationChain - payload BroadcastRequest: type: object @@ -801,6 +1045,13 @@ components: - BroadcastStatusReceived - BroadcastStatusSuccess - BroadcastStatusError + StorePayloadResult: + type: object + properties: + keccak256: + $ref: '#/components/schemas/Keccak256Hash' + required: + - keccak256 ErrorResponse: type: object properties: @@ -825,3 +1076,6 @@ components: BigInt: type: string pattern: '^(0|[1-9]\d*)$' + Keccak256Hash: + type: string + pattern: '^0x[0-9a-f]{64}$' diff --git a/libs/common/src/contracts/contracts.module.ts b/libs/common/src/contracts/contracts.module.ts index a7003bd..56c5095 100644 --- a/libs/common/src/contracts/contracts.module.ts +++ b/libs/common/src/contracts/contracts.module.ts @@ -78,8 +78,9 @@ import { FeeHelper } from '@mvx-monorepo/common/contracts/fee.helper'; const contractLoader = new ContractLoader(join(__dirname, '../assets/interchain-token-service.abi.json')); const smartContract = await contractLoader.getContract(apiConfigService.getContractIts()); + const abi = await contractLoader.getAbiRegistry(); - return new ItsContract(smartContract); + return new ItsContract(smartContract, abi); }, inject: [ApiConfigService], }, diff --git a/libs/common/src/contracts/entities/its-events.ts b/libs/common/src/contracts/entities/its-events.ts new file mode 100644 index 0000000..69e253c --- /dev/null +++ b/libs/common/src/contracts/entities/its-events.ts @@ -0,0 +1,20 @@ +import BigNumber from 'bignumber.js'; +import { IAddress } from '@multiversx/sdk-core/out'; + +export interface InterchainTokenDeploymentStartedEvent { + tokenId: string; + name: string; + symbol: string; + decimals: number; + minter: Buffer; + destinationChain: string; +} + +export interface InterchainTransferEvent { + tokenId: string; + sourceAddress: IAddress; + dataHash: string; + destinationChain: string; + destinationAddress: Buffer; + amount: BigNumber; +} diff --git a/libs/common/src/contracts/its.contract.spec.ts b/libs/common/src/contracts/its.contract.spec.ts new file mode 100644 index 0000000..34dd376 --- /dev/null +++ b/libs/common/src/contracts/its.contract.spec.ts @@ -0,0 +1,118 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test } from '@nestjs/testing'; +import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; +import { AbiRegistry, Address, ResultsParser, SmartContract } from '@multiversx/sdk-core/out'; +import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; + +import itsAbi from '../assets/interchain-token-service.abi.json'; +import BigNumber from 'bignumber.js'; +import { ItsContract } from '@mvx-monorepo/common/contracts/its.contract'; + +describe('ItsContract', () => { + let smartContract: DeepMocked; + let abi: AbiRegistry; + let resultsParser: ResultsParser; + + let contract: ItsContract; + + beforeEach(async () => { + smartContract = createMock(); + abi = AbiRegistry.create(itsAbi); + resultsParser = new ResultsParser(); + + const moduleRef = await Test.createTestingModule({ + providers: [ItsContract], + }) + .useMocker((token) => { + if (token === SmartContract) { + return smartContract; + } + + if (token === AbiRegistry) { + return abi; + } + + if (token === ResultsParser) { + return resultsParser; + } + + return null; + }) + .compile(); + + contract = moduleRef.get(ItsContract); + }); + + describe('decodeInterchainTokenDeploymentStartedEvent', () => { + const event = TransactionEvent.fromHttpResponse({ + address: 'mockItsAddress', + identifier: 'any', + data: Buffer.from( + '0000000c49545354657374546f6b656e00000005495453545412000000000000000e6176616c616e6368652d66756a69', + 'hex', + ).toString('base64'), + topics: [ + BinaryUtils.base64Encode('interchain_token_deployment_started_event'), + Buffer.from('81748eb162a0c2c245b3fb7f29e125edf1a95cf01712d21e20a7594add9d82cd', 'hex').toString('base64'), + ], + }); + + it('Should decode event', () => { + const result = contract.decodeInterchainTokenDeploymentStartedEvent(event); + + expect(result).toEqual({ + tokenId: '81748eb162a0c2c245b3fb7f29e125edf1a95cf01712d21e20a7594add9d82cd', + name: 'ITSTestToken', + symbol: 'ITSTT', + decimals: 18, + minter: Buffer.from(''), + destinationChain: 'avalanche-fuji', + }); + }); + + it('Should throw error while decoding', () => { + event.topics = []; + + expect(() => contract.decodeInterchainTokenDeploymentStartedEvent(event)).toThrow(); + }); + }); + + describe('decodeInterchainTransferEvent', () => { + const event = TransactionEvent.fromHttpResponse({ + address: 'mockItsAddress', + identifier: 'any', + data: Buffer.from( + '0000000e6176616c616e6368652d66756a6900000014f12372616f9c986355414ba06b3ca954c0a7b0dc0000000a152d02c7e14af6800000', + 'hex', + ).toString('base64'), + topics: [ + BinaryUtils.base64Encode('interchain_transfer_event'), + Buffer.from('81748eb162a0c2c245b3fb7f29e125edf1a95cf01712d21e20a7594add9d82cd', 'hex').toString('base64'), + Buffer.from( + Address.fromBech32('erd1wavgcxq9tfyrw49k3s3h34085mayu82wqvpd4h6akyh8559pkklsknwhwh').hex(), + 'hex', + ).toString('base64'), + Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex').toString('base64'), + ], + }); + + it('Should decode event', () => { + const result = contract.decodeInterchainTransferEvent(event); + + expect(result).toEqual({ + tokenId: '81748eb162a0c2c245b3fb7f29e125edf1a95cf01712d21e20a7594add9d82cd', + sourceAddress: Address.fromBech32('erd1wavgcxq9tfyrw49k3s3h34085mayu82wqvpd4h6akyh8559pkklsknwhwh'), + dataHash: '0000000000000000000000000000000000000000000000000000000000000000', + destinationChain: 'avalanche-fuji', + destinationAddress: Buffer.from('F12372616f9c986355414BA06b3Ca954c0a7b0dC', 'hex'), + amount: new BigNumber('100000000000000000000000'), + }); + }); + + it('Should throw error while decoding', () => { + event.topics = []; + + expect(() => contract.decodeInterchainTokenDeploymentStartedEvent(event)).toThrow(); + }); + }); +}); diff --git a/libs/common/src/contracts/its.contract.ts b/libs/common/src/contracts/its.contract.ts index 0e3c434..46f52b1 100644 --- a/libs/common/src/contracts/its.contract.ts +++ b/libs/common/src/contracts/its.contract.ts @@ -1,6 +1,12 @@ -import { Interaction, SmartContract, TokenTransfer } from '@multiversx/sdk-core/out'; +import { AbiRegistry, Interaction, ITransactionEvent, SmartContract, TokenTransfer } from '@multiversx/sdk-core/out'; import { Injectable } from '@nestjs/common'; import { AbiCoder } from 'ethers'; +import { Events } from '@mvx-monorepo/common/utils/event.enum'; +import { DecodingUtils } from '@mvx-monorepo/common/utils/decoding.utils'; +import { + InterchainTokenDeploymentStartedEvent, + InterchainTransferEvent, +} from '@mvx-monorepo/common/contracts/entities/its-events'; const MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN = 1; @@ -8,7 +14,10 @@ const DEFAULT_ESDT_ISSUE_COST = '50000000000000000'; // 0.05 EGLD @Injectable() export class ItsContract { - constructor(private readonly smartContract: SmartContract) {} + constructor( + private readonly smartContract: SmartContract, + private readonly abi: AbiRegistry, + ) {} execute( sourceChain: string, @@ -19,12 +28,7 @@ export class ItsContract { ): Interaction { const messageType = this.decodeExecutePayloadMessageType(payload); - const interaction = this.smartContract.methods.execute([ - sourceChain, - messageId, - sourceAddress, - payload, - ]); + const interaction = this.smartContract.methods.execute([sourceChain, messageId, sourceAddress, payload]); // The second time this transaction is executed it needs to contain and EGLD transfer for issuing ESDT if (messageType === MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN && executedTimes === 1) { @@ -39,4 +43,32 @@ export class ItsContract { return Number(result[0]); } + + decodeInterchainTokenDeploymentStartedEvent(event: ITransactionEvent): InterchainTokenDeploymentStartedEvent { + const eventDefinition = this.abi.getEvent(Events.INTERCHAIN_TOKEN_DEPLOYMENT_STARTED_EVENT); + const outcome = DecodingUtils.parseTransactionEvent(event, eventDefinition); + + return { + tokenId: DecodingUtils.decodeByteArrayToHex(outcome.token_id), + name: outcome.data.name.toString(), + symbol: outcome.data.symbol.toString(), + decimals: outcome.data.decimals.toNumber(), + minter: outcome.data.minter, + destinationChain: outcome.data.destination_chain.toString(), + }; + } + + decodeInterchainTransferEvent(event: ITransactionEvent): InterchainTransferEvent { + const eventDefinition = this.abi.getEvent(Events.INTERCHAIN_TRANSFER_EVENT); + const outcome = DecodingUtils.parseTransactionEvent(event, eventDefinition); + + return { + tokenId: DecodingUtils.decodeByteArrayToHex(outcome.token_id), + sourceAddress: outcome.source_address, + dataHash: DecodingUtils.decodeByteArrayToHex(outcome.data_hash), + destinationChain: outcome.data.destination_chain.toString(), + destinationAddress: outcome.data.destination_address, + amount: outcome.data.amount, + }; + } } diff --git a/libs/common/src/utils/event.enum.ts b/libs/common/src/utils/event.enum.ts index a3426aa..1ee6533 100644 --- a/libs/common/src/utils/event.enum.ts +++ b/libs/common/src/utils/event.enum.ts @@ -16,4 +16,7 @@ export enum Events { GAS_ADDED_EVENT = 'gas_added_event', NATIVE_GAS_ADDED_EVENT = 'native_gas_added_event', REFUNDED_EVENT = 'refunded_event', + + INTERCHAIN_TOKEN_DEPLOYMENT_STARTED_EVENT = 'interchain_token_deployment_started_event', + INTERCHAIN_TRANSFER_EVENT = 'interchain_transfer_event', }