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 00000000..9a938e4b Binary files /dev/null and b/packages/axelar-local-dev-multiversx/contracts/interchain-token-factory.wasm differ 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 00000000..e7f09195 Binary files /dev/null and b/packages/axelar-local-dev-multiversx/contracts/interchain-token-service.wasm differ diff --git a/packages/axelar-local-dev-multiversx/contracts/token-manager.wasm b/packages/axelar-local-dev-multiversx/contracts/token-manager.wasm new file mode 100644 index 00000000..4cbeff04 Binary files /dev/null and b/packages/axelar-local-dev-multiversx/contracts/token-manager.wasm differ diff --git a/packages/axelar-local-dev-multiversx/package.json b/packages/axelar-local-dev-multiversx/package.json index 5b955462..0720c881 100644 --- a/packages/axelar-local-dev-multiversx/package.json +++ b/packages/axelar-local-dev-multiversx/package.json @@ -1,6 +1,6 @@ { "name": "@axelar-network/axelar-local-dev-multiversx", - "version": "2.2.1", + "version": "2.2.2", "main": "dist/index.js", "files": [ "dist/", diff --git a/packages/axelar-local-dev-multiversx/src/Command.ts b/packages/axelar-local-dev-multiversx/src/Command.ts index 1668255b..de74e3c0 100644 --- a/packages/axelar-local-dev-multiversx/src/Command.ts +++ b/packages/axelar-local-dev-multiversx/src/Command.ts @@ -56,4 +56,46 @@ export class Command { 'multiversx' ); }; + + static createContractCallCommandIts = (commandId: string, relayData: RelayData, args: CallContractArgs) => { + // 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, );