From ae9a8cb3a3e6ef454dd649f6c8a8515dcf1b11ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rare=C8=99?= <6453351+raress96@users.noreply.github.com> Date: Tue, 12 Mar 2024 11:11:27 +0200 Subject: [PATCH] feat: axelar local dev multiversx add support for ITS (#106) * Start setup of multiversx package. * Start developing multiversx package integration. * Deploy multiversx axelar contracts to localnet. * Start working on call contract example for evm to multiversx. * Working temp example with avalanche to ethereum message. * Add hello world contract for multiversx and deploy it. * Working cross chain call from Ethereum to MultiversX. * Cleanup for multiversx package and fully working multiversx relayer with elasticsearch events. * Add wasm contracts to repo. * MultiversX transaction watcher fixes and updates for new contracts. * Fix multiversx contract execute. * Deploy interchain token service for MultiversX. * Add its transactions processing for multiversx. * Create multiversx its class for querying contract. * Process other multiversx gas paid events. * Add integration tests for multiversx. * Add test for multiversx its. * Fix contracts directory not being included in multiversx package. * Update multiversx package. * Update multiversx tests. --------- Co-authored-by: Rares <6453351+raresserban@users.noreply.github.com> --- package-lock.json | 2 +- .../__tests__/multiversx.spec.ts | 147 ++++++++++- .../contracts/interchain-token-factory.wasm | Bin 0 -> 11231 bytes .../contracts/interchain-token-service.wasm | Bin 0 -> 37471 bytes .../contracts/token-manager.wasm | Bin 0 -> 12611 bytes .../axelar-local-dev-multiversx/package.json | 2 +- .../src/Command.ts | 42 ++++ .../src/MultiversXNetwork.ts | 234 +++++++++++++++++- .../src/MultiversXRelayer.ts | 47 +++- .../axelar-local-dev-multiversx/src/index.ts | 1 + .../axelar-local-dev-multiversx/src/its.ts | 200 +++++++++++++++ .../src/multiversXNetworkUtils.ts | 6 + 12 files changed, 655 insertions(+), 26 deletions(-) create mode 100644 packages/axelar-local-dev-multiversx/contracts/interchain-token-factory.wasm create mode 100644 packages/axelar-local-dev-multiversx/contracts/interchain-token-service.wasm create mode 100644 packages/axelar-local-dev-multiversx/contracts/token-manager.wasm create mode 100644 packages/axelar-local-dev-multiversx/src/its.ts diff --git a/package-lock.json b/package-lock.json index 907bb771..36256875 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30081,7 +30081,7 @@ }, "packages/axelar-local-dev-multiversx": { "name": "@axelar-network/axelar-local-dev-multiversx", - "version": "2.2.0", + "version": "2.2.2", "license": "ISC", "dependencies": { "@axelar-network/axelar-local-dev": "2.2.0", diff --git a/packages/axelar-local-dev-multiversx/__tests__/multiversx.spec.ts b/packages/axelar-local-dev-multiversx/__tests__/multiversx.spec.ts index b9e367a5..f0601231 100644 --- a/packages/axelar-local-dev-multiversx/__tests__/multiversx.spec.ts +++ b/packages/axelar-local-dev-multiversx/__tests__/multiversx.spec.ts @@ -1,8 +1,8 @@ import path from 'path'; -import { ethers } from 'ethers'; -import { createNetwork, deployContract, EvmRelayer, Network, relay, setLogger } from '@axelar-network/axelar-local-dev'; +import { Contract, ethers, Wallet } from 'ethers'; +import { contracts, createNetwork, deployContract, EvmRelayer, Network, relay, setLogger } from '@axelar-network/axelar-local-dev'; import HelloWorld from '../artifacts/__tests__/contracts/HelloWorld.sol/HelloWorld.json'; -import { createMultiversXNetwork, MultiversXNetwork, MultiversXRelayer } from '../src'; +import { createMultiversXNetwork, MultiversXNetwork, MultiversXRelayer, registerMultiversXRemoteITS } from '../src'; import { Address, AddressValue, @@ -24,10 +24,15 @@ setLogger(() => undefined); describe('multiversx', () => { let client: MultiversXNetwork; let evmNetwork: Network; + let wallet: Wallet; - beforeEach(async () => { + beforeAll(async () => { client = await createMultiversXNetwork(); + }); + + beforeEach(async () => { evmNetwork = await createNetwork(); + wallet = evmNetwork.userWallets[0]; }); it('should be able to relay tx from EVM to MultiversX', async () => { @@ -40,7 +45,7 @@ describe('multiversx', () => { ]); // Deploy EVM contract - const helloWorld = await deployContract(evmNetwork.userWallets[0], HelloWorld, [ + const helloWorld = await deployContract(wallet, HelloWorld, [ evmNetwork.gateway.address, evmNetwork.gasService.address ]); @@ -80,7 +85,7 @@ describe('multiversx', () => { ]); // Deploy EVM contract - const helloWorld = await deployContract(evmNetwork.userWallets[0], HelloWorld, [ + const helloWorld = await deployContract(wallet, HelloWorld, [ evmNetwork.gateway.address, evmNetwork.gasService.address ]); @@ -185,4 +190,134 @@ describe('multiversx', () => { expect(message).toEqual(toSend); }); + + it('should be able to send token from EVM to MultiversX', async () => { + const evmIts = new Contract(evmNetwork.interchainTokenService.address, contracts.IInterchainTokenService.abi, wallet.connect(evmNetwork.provider)); + const evmItsFactory = new Contract(evmNetwork.interchainTokenFactory.address, contracts.IInterchainTokenFactory.abi, wallet.connect(evmNetwork.provider)); + + await registerMultiversXRemoteITS(client, [evmNetwork]); + + const name = 'InterchainToken'; + const symbol = 'ITE'; + const decimals = 18; + const amount = 1000; + const salt = keccak256(ethers.utils.defaultAbiCoder.encode(['uint256', 'uint256'], [process.pid, process.ppid])); + const fee = 100000000; + + const tokenId = await evmItsFactory.interchainTokenId(wallet.address, salt); + + await (await evmItsFactory.deployInterchainToken( + salt, + name, + symbol, + decimals, + amount, + wallet.address, + )).wait(); + + await (await evmItsFactory.deployRemoteInterchainToken( + '', + salt, + wallet.address, + 'multiversx', + fee, + { value: fee }, + )).wait(); + + const multiversXRelayer = new MultiversXRelayer(); + + // Relay tx from EVM to MultiversX + await relay({ + multiversx: multiversXRelayer, + evm: new EvmRelayer({ multiversXRelayer }) + }); + + let tokenIdentifier = await client.its.getValidTokenIdentifier(tokenId); + expect(tokenIdentifier); + tokenIdentifier = tokenIdentifier as string; + + let balance = (await client.getFungibleTokenOfAccount(client.owner, tokenIdentifier)).balance?.toString(); + expect(!balance); + + const tx = await evmIts.interchainTransfer(tokenId, 'multiversx', client.owner.pubkey(), amount, '0x', fee, { + value: fee, + }); + await tx.wait(); + + // Relay tx from EVM to MultiversX + await relay({ + multiversx: multiversXRelayer, + evm: new EvmRelayer({ multiversXRelayer }) + }); + + balance = (await client.getFungibleTokenOfAccount(client.owner, tokenIdentifier)).balance?.toString(); + expect(balance === '1000'); + }); + + // it('should be able to send token from MultiversX to EVM', async () => { + // const evmIts = new Contract(evmNetwork.interchainTokenService.address, contracts.IInterchainTokenService.abi, wallet.connect(evmNetwork.provider)); + // + // await registerMultiversXRemoteITS(client, [evmNetwork]); + // + // const name = 'InterchainToken'; + // const symbol = 'ITE'; + // const decimals = 18; + // const amount = 1000; + // const salt = keccak256(ethers.utils.defaultAbiCoder.encode(['uint256', 'uint256'], [process.pid, process.ppid])); + // const fee = 100000000; + // + // const tokenId = await client.its.interchainTokenId(client.owner, salt); + // + // await client.its.deployInterchainToken( + // salt, + // name, + // symbol, + // decimals, + // amount, + // client.owner, + // ); + // + // let tokenIdentifier = await client.its.getValidTokenIdentifier(tokenId); + // expect(tokenIdentifier); + // tokenIdentifier = tokenIdentifier as string; + // + // const multiversXRelayer = new MultiversXRelayer(); + // // Update events first so new Multiversx logs are processed afterwards + // await multiversXRelayer.updateEvents(); + // + // await client.its.deployRemoteInterchainToken( + // '', + // salt, + // client.owner, + // evmNetwork.name, + // fee, + // ); + // + // // Relay tx from MultiversX to EVM + // await relay({ + // multiversx: multiversXRelayer, + // evm: new EvmRelayer({ multiversXRelayer }) + // }); + // + // // TODO: The evm execute transaction is not actually executed successfully for some reason... + // // const evmTokenAddress = await evmIts.interchainTokenAddress('0x' + tokenId); + // // const code = await evmNetwork.provider.getCode(evmTokenAddress); + // // expect (code !== '0x'); + // // + // // const destinationToken = new Contract(evmTokenAddress, IERC20.abi, evmNetwork.provider); + // // let balance = await destinationToken.balanceOf(wallet.address); + // // expect(!balance); + // // + // // const result = await client.its.interchainTransfer(tokenId, evmNetwork.name, wallet.address, tokenIdentifier, amount.toString(), '5'); + // // expect(result); + // // + // // // Relay tx from MultiversX to EVM + // // await relay({ + // // multiversx: multiversXRelayer, + // // evm: new EvmRelayer({ multiversXRelayer }) + // // }); + // // + // // balance = await destinationToken.balanceOf(wallet.address); + // // expect(balance === '995'); + // }); }); diff --git a/packages/axelar-local-dev-multiversx/contracts/interchain-token-factory.wasm b/packages/axelar-local-dev-multiversx/contracts/interchain-token-factory.wasm new file mode 100644 index 0000000000000000000000000000000000000000..9a938e4b0598ef8d71005ac36ad6b25c27e8ac10 GIT binary patch literal 11231 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 KY4Sx5F#RvNK79HB literal 0 HcmV?d00001 diff --git a/packages/axelar-local-dev-multiversx/contracts/interchain-token-service.wasm b/packages/axelar-local-dev-multiversx/contracts/interchain-token-service.wasm new file mode 100644 index 0000000000000000000000000000000000000000..e7f091957ce973513613a99bad7b6c9551e5cb14 GIT binary patch literal 37471 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 { + // Remove 0x added by Ethereum for hex strings + const payloadHex = args.payload.startsWith('0x') ? args.payload.substring(2) : args.payload; + const payloadHash = createKeccakHash('keccak256').update(Buffer.from(payloadHex, 'hex')).digest('hex'); + + return new Command( + commandId, + 'approveContractCall', + [args.from, args.sourceAddress, args.destinationContractAddress, payloadHash, args.transactionHash, args.sourceEventIndex], + [], + async () => { + const result = defaultAbiCoder.decode(['uint256'], Buffer.from(payloadHex, 'hex')); + const messageType = Number(result[0]); + + let tx = await multiversXNetwork.executeContract( + commandId, + args.destinationContractAddress, + args.from, + args.sourceAddress, + payloadHex + ); + + // In case of deploy interchain token, call 2nd time with EGLD value + if (messageType === 1) { + tx = await multiversXNetwork.executeContract( + commandId, + args.destinationContractAddress, + args.from, + args.sourceAddress, + payloadHex, + '5000000000000000000' // 5 EGLD for ESDT issue cost on localnet + ); + } + + relayData.callContract[commandId].execution = tx.getHash(); + + return tx; + }, + 'multiversx' + ); + }; } diff --git a/packages/axelar-local-dev-multiversx/src/MultiversXNetwork.ts b/packages/axelar-local-dev-multiversx/src/MultiversXNetwork.ts index 342ce66a..3679fb6f 100644 --- a/packages/axelar-local-dev-multiversx/src/MultiversXNetwork.ts +++ b/packages/axelar-local-dev-multiversx/src/MultiversXNetwork.ts @@ -1,6 +1,7 @@ import { Account, Address, + AddressType, AddressValue, BigUIntValue, BinaryCodec, @@ -10,14 +11,19 @@ import { H256Value, Interaction, List, + OptionType, + OptionValue, ResultsParser, ReturnCode, SmartContract, + StringType, StringValue, Transaction, TransactionWatcher, Tuple, - TypedValue + TypedValue, + U8Value, + VariadicValue } from '@multiversx/sdk-core/out'; import { ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; import { Code } from '@multiversx/sdk-core'; @@ -28,6 +34,7 @@ import * as os from 'os'; import { MultiversXConfig } from './multiversXNetworkUtils'; import { ContractQueryResponse } from '@multiversx/sdk-network-providers/out/contractQueryResponse'; import createKeccakHash from 'keccak'; +import { MultiversXITS } from './its'; const MULTIVERSX_SIGNED_MESSAGE_PREFIX = '\x19MultiversX Signed Message:\n'; const CHAIN_ID = 'multiversx-localnet'; @@ -41,6 +48,9 @@ export class MultiversXNetwork extends ProxyNetworkProvider { public gatewayAddress?: Address; public authAddress?: Address; public gasReceiverAddress?: Address; + public interchainTokenServiceAddress?: Address; + public interchainTokenFactoryAddress?: Address; + public its: MultiversXITS; public contractAddress?: string; private readonly ownerPrivateKey: UserSecretKey; @@ -50,6 +60,8 @@ export class MultiversXNetwork extends ProxyNetworkProvider { gatewayAddress: string | undefined, authAddress: string | undefined, gasReceiverAddress: string | undefined, + interchainTokenServiceAddress: string | undefined, + interchainTokenFactoryAddress: string | undefined, contractAddress: string | undefined = undefined ) { super(url); @@ -68,6 +80,16 @@ export class MultiversXNetwork extends ProxyNetworkProvider { this.gasReceiverAddress = gasReceiverAddress ? Address.fromBech32(gasReceiverAddress) : undefined; } catch (e) { } + try { + this.interchainTokenServiceAddress = interchainTokenServiceAddress ? Address.fromBech32( + interchainTokenServiceAddress) : undefined; + } catch (e) { + } + try { + this.interchainTokenFactoryAddress = interchainTokenFactoryAddress ? Address.fromBech32( + interchainTokenFactoryAddress) : undefined; + } catch (e) { + } this.contractAddress = contractAddress; @@ -76,13 +98,20 @@ export class MultiversXNetwork extends ProxyNetworkProvider { const file = fs.readFileSync(ownerWalletFile).toString(); this.ownerPrivateKey = UserSecretKey.fromPem(file); + this.its = new MultiversXITS(this, interchainTokenServiceAddress as string, interchainTokenFactoryAddress as string); } async isGatewayDeployed(): Promise { const accountOnNetwork = await this.getAccount(this.owner); this.ownerAccount.update(accountOnNetwork); - if (!this.gatewayAddress || !this.authAddress || !this.gasReceiverAddress) { + if ( + !this.gatewayAddress + || !this.authAddress + || !this.gasReceiverAddress + || !this.interchainTokenServiceAddress + || !this.interchainTokenFactoryAddress + ) { return false; } @@ -90,8 +119,16 @@ export class MultiversXNetwork extends ProxyNetworkProvider { const accountGateway = await this.getAccount(this.gatewayAddress); const accountAuth = await this.getAccount(this.authAddress); const accountGasReceiver = await this.getAccount(this.gasReceiverAddress); - - if (!accountGateway.code || !accountAuth.code || !accountGasReceiver.code) { + const interchainTokenServiceAddress = await this.getAccount(this.interchainTokenServiceAddress); + const interchainTokenFactoryAddress = await this.getAccount(this.interchainTokenFactoryAddress); + + if ( + !accountGateway.code + || !accountAuth.code + || !accountGasReceiver.code + || !interchainTokenServiceAddress.code + || !interchainTokenFactoryAddress.code + ) { return false; } @@ -150,14 +187,31 @@ export class MultiversXNetwork extends ProxyNetworkProvider { const axelarGasReceiverAddress = await this.deployGasReceiverContract(contractFolder); + const baseTokenManager = await this.deployBaseTokenManager(contractFolder); + const interchainTokenServiceAddress = await this.deployInterchainTokenService( + contractFolder, + axelarGatewayAddress, + axelarGasReceiverAddress, + baseTokenManager + ); + const interchainTokenFactoryAddress = await this.deployInterchainTokenFactory( + contractFolder, + interchainTokenServiceAddress + ); + this.gatewayAddress = Address.fromBech32(axelarGatewayAddress); this.authAddress = Address.fromBech32(axelarAuthAddress); this.gasReceiverAddress = Address.fromBech32(axelarGasReceiverAddress); + this.interchainTokenServiceAddress = Address.fromBech32(interchainTokenServiceAddress); + this.interchainTokenFactoryAddress = Address.fromBech32(interchainTokenFactoryAddress); + this.its = new MultiversXITS(this, interchainTokenServiceAddress, interchainTokenFactoryAddress); return { axelarAuthAddress, axelarGatewayAddress, - axelarGasReceiverAddress + axelarGasReceiverAddress, + interchainTokenServiceAddress, + interchainTokenFactoryAddress, }; } @@ -271,7 +325,7 @@ export class MultiversXNetwork extends ProxyNetworkProvider { const returnCode = await this.signAndSendTransaction(gasReceiverTransaction); if (!returnCode.isSuccess()) { - throw new Error(`Could not deploy Axelar Auth contract... ${gasReceiverTransaction.getHash()}`); + throw new Error(`Could not deploy Axelar Gas Receiver contract... ${gasReceiverTransaction.getHash()}`); } const axelarGasReceiverAddress = SmartContract.computeAddress( @@ -283,6 +337,168 @@ export class MultiversXNetwork extends ProxyNetworkProvider { return axelarGasReceiverAddress.bech32(); } + // This is a custom version of the token manager with ESDT issue cost set for localnet (5000000000000000000 / 5 EGLD) + private async deployBaseTokenManager(contractFolder: string): Promise { + const buffer = await promises.readFile(contractFolder + '/token-manager.wasm'); + + const code = Code.fromBuffer(buffer); + const contract = new SmartContract(); + + // Deploy parameters don't matter since they will be overwritten + const tokenManagerTransaction = contract.deploy({ + deployer: this.owner, + code, + codeMetadata: new CodeMetadata(true, true, false, false), + initArguments: [ + new AddressValue(this.owner), + new U8Value(2), + new H256Value(Buffer.from('01b3d64c8c6530a3aad5909ae7e0985d4438ce8eafd90e51ce48fbc809bced39', 'hex')), + Tuple.fromItems([ + new OptionValue(new OptionType(new AddressType()), new AddressValue(this.owner)), + new OptionValue(new OptionType(new StringType()), new StringValue('EGLD')) + ]) + ], + gasLimit: 50_000_000, + chainID: 'localnet' + }); + tokenManagerTransaction.setNonce(this.ownerAccount.getNonceThenIncrement()); + + const returnCode = await this.signAndSendTransaction(tokenManagerTransaction); + + if (!returnCode.isSuccess()) { + throw new Error(`Could not deploy Axelar Token Manager contract... ${tokenManagerTransaction.getHash()}`); + } + + const address = SmartContract.computeAddress( + tokenManagerTransaction.getSender(), + tokenManagerTransaction.getNonce() + ); + console.log(`Base Token Manager contract deployed at ${address} with transaction ${tokenManagerTransaction.getHash()}`); + + return address.bech32(); + } + + private async deployInterchainTokenService( + contractFolder: string, + gateway: string, + gasService: string, + baseTokenManager: string + ): Promise { + const buffer = await promises.readFile(contractFolder + '/interchain-token-service.wasm'); + + const code = Code.fromBuffer(buffer); + const contract = new SmartContract(); + + const itsTransaction = contract.deploy({ + deployer: this.owner, + code, + codeMetadata: new CodeMetadata(true, true, false, false), + initArguments: [ + new AddressValue(Address.fromBech32(gateway)), + new AddressValue(Address.fromBech32(gasService)), + new AddressValue(Address.fromBech32(baseTokenManager)), + new AddressValue(this.owner), + new StringValue('multiversx'), + VariadicValue.fromItemsCounted(), // empty trusted chains + VariadicValue.fromItemsCounted() + ], + gasLimit: 200_000_000, + chainID: 'localnet' + }); + itsTransaction.setNonce(this.ownerAccount.getNonceThenIncrement()); + + const returnCode = await this.signAndSendTransaction(itsTransaction); + + if (!returnCode.isSuccess()) { + throw new Error(`Could not deploy Axelar Interchain Token Service contract... ${itsTransaction.getHash()}`); + } + + const address = SmartContract.computeAddress( + itsTransaction.getSender(), + itsTransaction.getNonce() + ); + console.log(`Interchain Token Service contract deployed at ${address} with transaction ${itsTransaction.getHash()}`); + + return address.bech32(); + } + + private async deployInterchainTokenFactory(contractFolder: string, interchainTokenService: string): Promise { + const buffer = await promises.readFile(contractFolder + '/interchain-token-factory.wasm'); + + const code = Code.fromBuffer(buffer); + const contract = new SmartContract(); + const itsAddress = Address.fromBech32(interchainTokenService); + + const factoryTransaction = contract.deploy({ + deployer: this.owner, + code, + codeMetadata: new CodeMetadata(true, true, false, false), + initArguments: [ + new AddressValue(itsAddress) + ], + gasLimit: 200_000_000, + chainID: 'localnet' + }); + factoryTransaction.setNonce(this.ownerAccount.getNonceThenIncrement()); + + let returnCode = await this.signAndSendTransaction(factoryTransaction); + + if (!returnCode.isSuccess()) { + throw new Error(`Could not deploy Axelar Interchain Token Factory contract... ${factoryTransaction.getHash()}`); + } + + const address = SmartContract.computeAddress( + factoryTransaction.getSender(), + factoryTransaction.getNonce() + ); + console.log(`Interchain Token Factory contract deployed at ${address} with transaction ${factoryTransaction.getHash()}`); + + const itsContract = new SmartContract({ address: itsAddress }); + // Set interchain token factory contract on its + const transaction = itsContract.call({ + caller: this.owner, + func: new ContractFunction('setInterchainTokenFactory'), + gasLimit: 50_000_000, + args: [ + new AddressValue(address) + ], + chainID: 'localnet' + }); + + transaction.setNonce(this.ownerAccount.getNonceThenIncrement()); + + returnCode = await this.signAndSendTransaction(transaction); + + if (!returnCode.isSuccess()) { + throw new Error(`Could not set Axelar ITS address on Axelar Interchain Token Factory... ${transaction.getHash()}`); + } + + return address.bech32(); + } + + async setInterchainTokenServiceTrustedAddress(chainName: string, address: string) { + console.log(`Registerring ITS for ${chainName} for MultiversX`); + const itsContract = new SmartContract({ address: this.interchainTokenServiceAddress }); + const transaction = itsContract.call({ + caller: this.owner, + func: new ContractFunction('setTrustedAddress'), + gasLimit: 50_000_000, + args: [ + new StringValue(chainName), + new StringValue(address) + ], + chainID: 'localnet' + }); + + transaction.setNonce(this.ownerAccount.getNonceThenIncrement()); + + const returnCode = await this.signAndSendTransaction(transaction); + + if (!returnCode.isSuccess()) { + throw new Error(`Could not call setTrustedAddress on MultiversX ITS contract form ${chainName}... ${transaction.getHash()}`); + } + } + async signAndSendTransaction(transaction: Transaction, privateKey: UserSecretKey = this.ownerPrivateKey): Promise { const signature = privateKey.sign(transaction.serializeForSigning()); transaction.applySignature(signature); @@ -379,7 +595,8 @@ export class MultiversXNetwork extends ProxyNetworkProvider { destinationContractAddress: string, sourceChain: string, sourceAddress: string, - payloadHex: string + payloadHex: string, + value: string = '0', ): Promise { // Remove 0x added by Ethereum for hex strings commandId = commandId.startsWith('0x') ? commandId.substring(2) : commandId; @@ -389,13 +606,14 @@ export class MultiversXNetwork extends ProxyNetworkProvider { const transaction = contract.call({ caller: this.owner, func: new ContractFunction('execute'), - gasLimit: 50_000_000, + gasLimit: 200_000_000, args: [ new H256Value(Buffer.from(commandId, 'hex')), new StringValue(sourceChain), new StringValue(sourceAddress), new BytesValue(Buffer.from(payloadHex, 'hex')) ], + value, chainID: 'localnet' }); diff --git a/packages/axelar-local-dev-multiversx/src/MultiversXRelayer.ts b/packages/axelar-local-dev-multiversx/src/MultiversXRelayer.ts index f3e9440f..9540cab9 100644 --- a/packages/axelar-local-dev-multiversx/src/MultiversXRelayer.ts +++ b/packages/axelar-local-dev-multiversx/src/MultiversXRelayer.ts @@ -22,9 +22,12 @@ import { AddressValue, BigUIntType, BigUIntValue, - BinaryCodec, BytesType, BytesValue, + BinaryCodec, + BytesType, + BytesValue, H256Type, H256Value, + StringType, TupleType } from '@multiversx/sdk-core/out'; import { getMultiversXLogID } from './utils'; @@ -199,6 +202,7 @@ export class MultiversXRelayer extends Relayer { try { const cost = getGasPrice(); const blockLimit = Number((await to.provider.getBlock('latest')).gasLimit); + await command.post({ gasLimit: BigInt(Math.min(blockLimit, payed.gasFeeAmount / cost)) }); @@ -209,21 +213,40 @@ export class MultiversXRelayer extends Relayer { } private async updateGasEvents(events: MultiversXEvent[]) { - const newEvents = events.filter((event) => event.identifier === 'payNativeGasForContractCall'); + const newEvents = events.filter( + (event) => event.identifier === 'payNativeGasForContractCall' || event.identifier === 'payGasForContractCall' + ); for (const event of newEvents) { + const eventName = Buffer.from(event.topics[0], 'base64').toString(); const sender = new Address(Buffer.from(event.topics[1], 'base64')); const destinationChain = Buffer.from(event.topics[2], 'base64').toString(); const destinationAddress = Buffer.from(event.topics[3], 'base64').toString(); - const decoded = new BinaryCodec().decodeTopLevel( - Buffer.from(event.data, 'base64'), - new TupleType(new H256Type(), new BigUIntType(), new AddressType()) - ).valueOf(); - // Need to add '0x' in front of hex encoded strings for EVM - const payloadHash = '0x' + (decoded.field0 as H256Value).valueOf().toString('hex'); - const gasFeeAmount = (decoded.field1 as BigUIntValue).toString(); - const refundAddress = (decoded.field2 as AddressValue).valueOf().bech32(); + let payloadHash = '0x', gasFeeAmount = '', refundAddress = ''; + if (eventName === 'native_gas_paid_for_contract_call_event') { + const decoded = new BinaryCodec().decodeTopLevel( + Buffer.from(event.data, 'base64'), + new TupleType(new H256Type(), new BigUIntType(), new AddressType()) + ).valueOf(); + + // Need to add '0x' in front of hex encoded strings for EVM + payloadHash = '0x' + (decoded.field0 as H256Value).valueOf().toString('hex'); + gasFeeAmount = (decoded.field1 as BigUIntValue).toString(); + refundAddress = (decoded.field2 as AddressValue).valueOf().bech32(); + } else if (eventName === 'gas_paid_for_contract_call_event') { + const decoded = new BinaryCodec().decodeTopLevel( + Buffer.from(event.data, 'base64'), + new TupleType(new H256Type(), new StringType(), new BigUIntType(), new AddressType()) + ).valueOf(); + + // Need to add '0x' in front of hex encoded strings for EVM + payloadHash = '0x' + (decoded.field0 as H256Value).valueOf().toString('hex'); + // Gas token not currently used for MultiversX. Gas value is multiplied by 100_000_000 to be enough for EVM + // const gasToken = (decoded.field1 as StringValue).valueOf().toString(); + gasFeeAmount = (BigInt((decoded.field2 as BigUIntValue).toString()) * BigInt('100000000')).toString(); + refundAddress = (decoded.field3 as AddressValue).valueOf().bech32(); + } const args: NativeGasPaidForContractCallArgs = { sourceAddress: sender.bech32(), @@ -275,6 +298,10 @@ export class MultiversXRelayer extends Relayer { } createCallContractCommand(commandId: string, relayData: RelayData, contractCallArgs: CallContractArgs): Command { + if (contractCallArgs.destinationContractAddress === multiversXNetwork.interchainTokenServiceAddress?.bech32()) { + return MultiversXCommand.createContractCallCommandIts(commandId, relayData, contractCallArgs); + } + return MultiversXCommand.createContractCallCommand(commandId, relayData, contractCallArgs); } diff --git a/packages/axelar-local-dev-multiversx/src/index.ts b/packages/axelar-local-dev-multiversx/src/index.ts index be8b0abe..5ba38f69 100644 --- a/packages/axelar-local-dev-multiversx/src/index.ts +++ b/packages/axelar-local-dev-multiversx/src/index.ts @@ -2,3 +2,4 @@ export * from './MultiversXNetwork'; export * from './multiversXNetworkUtils'; export * from './MultiversXRelayer'; export * from './utils'; +export * from './its'; diff --git a/packages/axelar-local-dev-multiversx/src/its.ts b/packages/axelar-local-dev-multiversx/src/its.ts new file mode 100644 index 00000000..e9d9a8a6 --- /dev/null +++ b/packages/axelar-local-dev-multiversx/src/its.ts @@ -0,0 +1,200 @@ +import { logger, Network } from '@axelar-network/axelar-local-dev'; +import { MultiversXNetwork } from './MultiversXNetwork'; +import { + Address, AddressValue, BigUIntValue, BytesValue, + ContractFunction, + H256Value, Interaction, + ResultsParser, + SmartContract, + StringValue, TokenTransfer, U8Value +} from '@multiversx/sdk-core/out'; + +export class MultiversXITS { + private readonly client; + private readonly itsContract; + private readonly itsFactoryContract; + + constructor(client: MultiversXNetwork, itsContract: string, itsFactoryContract: string) { + this.client = client; + this.itsContract = itsContract; + this.itsFactoryContract = itsFactoryContract; + } + + async getValidTokenIdentifier(tokenId: string): Promise { + // Remove 0x added by Ethereum for hex strings + tokenId = tokenId.startsWith('0x') ? tokenId.substring(2) : tokenId; + + try { + const result = await this.client.callContract(this.itsContract, "validTokenIdentifier", [new H256Value(Buffer.from(tokenId, 'hex'))]); + + const parsedResult = new ResultsParser().parseUntypedQueryResponse(result); + + return parsedResult.values[0].toString(); + } catch (e) { + return null; + } + } + + async interchainTokenId(address: Address, salt: string): Promise { + // Remove 0x added by Ethereum for hex strings + salt = salt.startsWith('0x') ? salt.substring(2) : salt; + + const result = await this.client.callContract(this.itsFactoryContract, "interchainTokenId", [ + new AddressValue(address), + new H256Value(Buffer.from(salt, 'hex')) + ]); + + const parsedResult = new ResultsParser().parseUntypedQueryResponse(result); + + return parsedResult.values[0].toString('hex'); + } + + async deployInterchainToken( + salt: string, + name: string, + symbol: string, + decimals: number, + amount: number, + minter: Address, + ) { + // Remove 0x added by Ethereum for hex strings + salt = salt.startsWith('0x') ? salt.substring(2) : salt; + + const contract = new SmartContract({ address: Address.fromBech32(this.itsFactoryContract) }); + const args = [ + new H256Value(Buffer.from(salt, 'hex')), + new StringValue(name), + new StringValue(symbol), + new U8Value(decimals), + new BigUIntValue(amount), + new AddressValue(minter), + ]; + const transaction = new Interaction(contract, new ContractFunction("deployInterchainToken"), args) + .withSender(this.client.owner) + .withChainID('localnet') + .withGasLimit(300_000_000) + .buildTransaction(); + + const accountOnNetwork = await this.client.getAccount(this.client.owner); + this.client.ownerAccount.update(accountOnNetwork); + + transaction.setNonce(this.client.ownerAccount.getNonceThenIncrement()); + + // First transaction deploys token manager + let returnCode = await this.client.signAndSendTransaction(transaction); + if (!returnCode.isSuccess()) { + return false; + } + + // Second transaction deploys token + transaction.setValue('5000000000000000000'); // 5 EGLD for ESDT issue cost on localnet + transaction.setNonce(this.client.ownerAccount.getNonceThenIncrement()); + + returnCode = await this.client.signAndSendTransaction(transaction); + if (!returnCode.isSuccess()) { + return false; + } + + // Third transaction mints tokens + transaction.setValue('0'); + transaction.setNonce(this.client.ownerAccount.getNonceThenIncrement()); + + returnCode = await this.client.signAndSendTransaction(transaction); + + return returnCode.isSuccess(); + } + + async deployRemoteInterchainToken( + chainName: string, + salt: string, + minter: Address, + destinationChain: string, + fee: number + ) { + // Remove 0x added by Ethereum for hex strings + salt = salt.startsWith('0x') ? salt.substring(2) : salt; + + const contract = new SmartContract({ address: Address.fromBech32(this.itsFactoryContract) }); + const args = [ + new StringValue(chainName), + new H256Value(Buffer.from(salt, 'hex')), + new AddressValue(minter), + new StringValue(destinationChain), + ]; + const transaction = new Interaction(contract, new ContractFunction("deployRemoteInterchainToken"), args) + .withSender(this.client.owner) + .withChainID('localnet') + .withGasLimit(300_000_000) + .withValue(fee) + .buildTransaction(); + + const accountOnNetwork = await this.client.getAccount(this.client.owner); + this.client.ownerAccount.update(accountOnNetwork); + + transaction.setNonce(this.client.ownerAccount.getNonceThenIncrement()); + + const returnCode = await this.client.signAndSendTransaction(transaction); + + return !returnCode.isSuccess(); + } + + async interchainTransfer( + tokenId: string, + destinationChain: string, + destinationAddress: string, + tokenIdentifier: string, + amount: string, + gasValue: string + ) { + // Remove 0x added by Ethereum for hex strings + tokenId = tokenId.startsWith('0x') ? tokenId.substring(2) : tokenId; + + const contract = new SmartContract({ address: Address.fromBech32(this.itsContract) }); + const args = [ + new H256Value(Buffer.from(tokenId, 'hex')), + new StringValue(destinationChain), + new StringValue(destinationAddress), + new BytesValue(Buffer.from('')), + new BigUIntValue(gasValue), + ]; + const transaction = new Interaction(contract, new ContractFunction("interchainTransfer"), args) + .withSingleESDTTransfer(TokenTransfer.fungibleFromBigInteger(tokenIdentifier, amount)) + .withSender(this.client.owner) + .withChainID('localnet') + .withGasLimit(100_000_000) + .buildTransaction(); + + const accountOnNetwork = await this.client.getAccount(this.client.owner); + this.client.ownerAccount.update(accountOnNetwork); + + transaction.setNonce(this.client.ownerAccount.getNonceThenIncrement()); + + const returnCode = await this.client.signAndSendTransaction(transaction); + + return returnCode.isSuccess(); + } +} + +export async function registerMultiversXRemoteITS(multiversxNetwork: MultiversXNetwork, networks: Network[]) { + logger.log(`Registerring ITS for ${networks.length} other chain for MultiversX...`); + + const accountOnNetwork = await multiversxNetwork.getAccount(multiversxNetwork.owner); + multiversxNetwork.ownerAccount.update(accountOnNetwork); + + for (const network of networks) { + const data = [] as string[]; + data.push( + ( + await network.interchainTokenService.populateTransaction.setTrustedAddress( + 'multiversx', + (multiversxNetwork.interchainTokenServiceAddress as Address).bech32(), + ) + ).data as string + ); + + await (await network.interchainTokenService.multicall(data)).wait(); + + await multiversxNetwork.setInterchainTokenServiceTrustedAddress(network.name, network.interchainTokenService.address); + } + logger.log(`Done`); +} diff --git a/packages/axelar-local-dev-multiversx/src/multiversXNetworkUtils.ts b/packages/axelar-local-dev-multiversx/src/multiversXNetworkUtils.ts index c71bdb33..ad14d2a3 100644 --- a/packages/axelar-local-dev-multiversx/src/multiversXNetworkUtils.ts +++ b/packages/axelar-local-dev-multiversx/src/multiversXNetworkUtils.ts @@ -12,6 +12,8 @@ export interface MultiversXConfig { axelarAuthAddress: string; axelarGatewayAddress: string; axelarGasReceiverAddress: string; + interchainTokenServiceAddress: string; + interchainTokenFactoryAddress: string; contractAddress?: string; } @@ -54,6 +56,8 @@ export async function createMultiversXNetwork(config?: MultiversXNetworkConfig): configFile?.axelarGatewayAddress, configFile?.axelarAuthAddress, configFile?.axelarGasReceiverAddress, + configFile?.interchainTokenServiceAddress, + configFile?.interchainTokenFactoryAddress, configFile?.contractAddress, ); @@ -90,6 +94,8 @@ export async function loadMultiversXNetwork( configFile?.axelarGatewayAddress, configFile?.axelarAuthAddress, configFile?.axelarGasReceiverAddress, + configFile?.interchainTokenServiceAddress, + configFile?.interchainTokenFactoryAddress, configFile?.contractAddress, );