diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 04fda582..3343f100 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,6 +9,15 @@ jobs: matrix: os: [ubuntu-22.04] arch: [amd64] + # Elasticsearch used by MultiversX test + services: + elasticsearch: + image: "docker.elastic.co/elasticsearch/elasticsearch:8.12.0" + env: + discovery.type: single-node + xpack.security.enabled: false + ports: + - "9200:9200" steps: - name: Download and Install Aptos Binary run: | @@ -37,6 +46,17 @@ jobs: - name: Checkout code uses: actions/checkout@v3 + - name: Download and Install Multiversx Binary + run: | + pip3 install multiversx-sdk-cli==v9.3.1 + mxpy localnet setup + cp -rf ./packages/axelar-local-dev-multiversx/external.toml ./localnet/validator00/config + cp -rf ./packages/axelar-local-dev-multiversx/external.toml ./localnet/validator01/config + cp -rf ./packages/axelar-local-dev-multiversx/external.toml ./localnet/validator02/config + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + - name: Cache node_modules uses: actions/cache@v3 with: @@ -54,5 +74,8 @@ jobs: run: | nohup sh -c "sui-test-validator" > nohup.out 2> nohup.err < /dev/null & nohup sh -c "aptos node run-local-testnet --with-faucet" > nohup.out 2> nohup.err < /dev/null & - sleep 10 + nohup sh -c "mxpy localnet start" > nohup.out 2> nohup.err < /dev/null & + nohup sh -c "anvil" < /dev/null & + nohup sh -c "anvil -p 8546" < /dev/null & + sleep 30 npm run test diff --git a/README.md b/README.md index 6ac12f05..e1045933 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Welcome to the Axelar Local Development Environment! This monorepo contains esse - [@axelar-network/axelar-local-dev-near](./packages/axelar-local-dev-near/) - [@axelar-network/axelar-local-dev-sui](./packages/axelar-local-dev-sui/) - [@axelar-network/axelar-local-dev-cosmos](./packages/axelar-local-dev-cosmos/) + - [@axelar-network/axelar-local-dev-multiversx](./packages/axelar-local-dev-multiversx/) The `axelar-local-dev` package is all you need for cross-chain applications between EVM chains. However, if you wish to explore cross-chain applications between EVM chains and other chain stacks, check out our specific guides: @@ -15,6 +16,7 @@ The `axelar-local-dev` package is all you need for cross-chain applications betw - [EVM <-> Near Integration Guide](./packages/axelar-local-dev-near/README.md#configuration) - [Evm <-> Sui Integration Guide](./packages/axelar-local-dev-sui/README.md) - [Evm <-> Cosmos Integration Guide](./packages/axelar-local-dev-cosmos/README.md) +- [EVM <-> MultiversX Integration Guide](./packages/axelar-local-dev-multiversx/README.md#configuration) ## Prerequisites @@ -47,3 +49,4 @@ We currently support the following chain stacks: - [Near](./packages/axelar-local-dev-near/) - [Sui](./packages/axelar-local-dev-sui/) - [Cosmos](./packages/axelar-local-dev-cosmos/) +- [MultiversX](./packages/axelar-local-dev-multiversx/) diff --git a/lerna.json b/lerna.json index de04b969..61152d1e 100644 --- a/lerna.json +++ b/lerna.json @@ -1,7 +1,7 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", "useWorkspaces": true, - "version": "2.2.0-alpha.28", + "version": "2.2.0", "packages": [ "packages/*" ] diff --git a/package.json b/package.json index a1989d81..e081948a 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,14 @@ "test:sui": "lerna exec --scope=@axelar-network/axelar-local-dev-sui npm run test", "test:cosmos": "lerna exec --scope=@axelar-network/axelar-local-dev-cosmos npm run test", "test-e2e:cosmos": "lerna exec --scope=@axelar-network/axelar-local-dev-cosmos npm run test:e2e", + "test:multiversx": "lerna exec --scope=@axelar-network/axelar-local-dev-multiversx npm run test", "build": "lerna run build", "build:core": "lerna exec --scope=@axelar-network/axelar-local-dev npm run build", "build:near": "lerna exec --scope=@axelar-network/axelar-local-dev-near npm run build", "build:aptos": "lerna exec --scope=@axelar-network/axelar-local-dev-aptos npm run build", "build:sui": "lerna exec --scope=@axelar-network/axelar-local-dev-sui npm run build", - "build:cosmos": "lerna exec --scope=@axelar-network/axelar-local-dev-cosmos npm run build" + "build:cosmos": "lerna exec --scope=@axelar-network/axelar-local-dev-cosmos npm run build", + "build:multiversx": "lerna exec --scope=@axelar-network/axelar-local-dev-multiversx npm run build" }, "devDependencies": { "lerna": "^6.6.1", diff --git a/packages/axelar-local-dev-aptos/package.json b/packages/axelar-local-dev-aptos/package.json index 5ea3b813..2e1bcb0e 100644 --- a/packages/axelar-local-dev-aptos/package.json +++ b/packages/axelar-local-dev-aptos/package.json @@ -1,6 +1,6 @@ { "name": "@axelar-network/axelar-local-dev-aptos", - "version": "2.2.0-alpha.28", + "version": "2.2.0", "main": "dist/index.js", "files": [ "dist/", @@ -19,7 +19,7 @@ }, "dependencies": { "@axelar-network/axelar-cgp-aptos": "^1.0.5", - "@axelar-network/axelar-local-dev": "2.2.0-alpha.28", + "@axelar-network/axelar-local-dev": "2.2.0", "aptos": "1.3.16" }, "author": "", diff --git a/packages/axelar-local-dev-multiversx/.eslintrc.json b/packages/axelar-local-dev-multiversx/.eslintrc.json new file mode 100644 index 00000000..0b87b2a4 --- /dev/null +++ b/packages/axelar-local-dev-multiversx/.eslintrc.json @@ -0,0 +1,16 @@ +{ + "env": { + "browser": true, + "jest": true, + "es2021": true + }, + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + "overrides": [], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": ["@typescript-eslint"], + "rules": {} +} diff --git a/packages/axelar-local-dev-multiversx/.gitignore b/packages/axelar-local-dev-multiversx/.gitignore new file mode 100644 index 00000000..1503ddc7 --- /dev/null +++ b/packages/axelar-local-dev-multiversx/.gitignore @@ -0,0 +1,8 @@ +**/node_modules + +src/types + +# Build +artifacts + +multiversxConfig.json diff --git a/packages/axelar-local-dev-multiversx/.prettierrc b/packages/axelar-local-dev-multiversx/.prettierrc new file mode 100644 index 00000000..6c933b96 --- /dev/null +++ b/packages/axelar-local-dev-multiversx/.prettierrc @@ -0,0 +1,15 @@ +{ + "printWidth": 140, + "singleQuote": true, + "tabWidth": 4, + "useTabs": false, + "bracketSpacing": true, + "overrides": [ + { + "files": "*.js", + "options": { + "trailingComma": "all" + } + } + ] +} diff --git a/packages/axelar-local-dev-multiversx/README.md b/packages/axelar-local-dev-multiversx/README.md new file mode 100644 index 00000000..11343714 --- /dev/null +++ b/packages/axelar-local-dev-multiversx/README.md @@ -0,0 +1,72 @@ +# Axelar Local Dev: MultiversX Integration + +Welcome to the Axelar Local Development MultiversX featuring MultiversX Integration. This package empowers developers to establish a local development environment for streamlined cross-chain communication utilizing the [MultiversX protocol](https://multiversx.com/). +Currently, the integration facilitates general message passing exclusively with the EVM chain. + +## Prerequisite + +0. You should have Docker & Docker Compose installed. + +1. Install Mxpy CLI Tool + +Download from here: https://docs.multiversx.com/sdk-and-tools/sdk-py/installing-mxpy/ + +> **Note**: Our examples are tested on Mxpy version `9.4.1`, but newer versions might also work. + +2. Run Elasticsearch + +`dcker-compose up -d` (in this folder) + +3. Create & run a MultiversX Localnet + +More info: https://docs.multiversx.com/developers/setup-local-testnet + +```bash +mkdir -p .multiversx && cd .multiversx +mxpy localnet setup +mxpy localnet start +``` + +Stop the localnet. You will now have `localnet` folder populate with the subfolders `validator00`, `validator01`, `validator02`. + +Copy the [external.toml](external.toml) from this folder into all the validators `config` folder (eg full path: `.multiversx/localnet/validator00/config`) +and overwrite the existing file. + +This will setup connection to Elasticsearch to index events used by the MultiversXRelayer. + +Start again the localnet: `mxpy localnet start` + +## Installation + +To install this package, use the following command: + +```bash +npm install @axelar-network/axelar-local-dev-multiversx +``` + +## Configuration + +To set up the MultiversX chain stack with the EVM chain stack, you need to modify the `createAndExport` function in your script. Create an `MultiversXRelayer` instance and incorporate it with your existing EVM relayer. Here's an example: + +```ts +const multiversxRelayer = new MultiversXRelayer(); +const relayers = { evm: new EvmRelayer({ multiversxRelayer }), multiversx: multiversxRelayer }; +``` + +For more details on setting up the `createAndExport` function, check our [Standalone Environment Setup Guide](../../docs/guide_create_and_exports.md). + +## API Reference + +`MultiversXNetwork` is a generalization of `ProxyNetworkProvider` (avaliable in the `@multiversx/sdk-network-providers` package) that includes (among others that are mainly used for intrnal purposes): + +- `deployAxelarFrameworkModules()`: Deploy Axelar related smart contracts found in `contracts`. +- `deployContract(contractCode: string, initArguments: TypedValue[]): Promise`: A wrapper for deploying a contract from code with init arguments, deployed by `alice.pem` wallet. Returns the SC address. +- `signAndSendTransaction(transaction: Transaction, privateKey: UserSecretKey = this.ownerPrivateKey)`: A wrapper to easily sign, send and wait for a transaction to be completed. +- `callContract(address: string, func: string, args: TypedValue[] = []): Promise)`: A wrapper to easily query a smart contract. + +Additionaly we export two utility functions + +- `createMultiversXNetwork(config?: {gatewayUrl: string})`: This deploys all the Axelar related smart contracts (`gas-service`, `auth`, `gateway`) if they are not deployed and saves their addresses to a config file. `gatewayUrl` defaults to `http://localhost:7950` +- `loadMultiversXNetwork(gatewayUrl = 'http://localhost:7950')`: This loads the preconfigured `MultiversXNetwork` by reading the contract addresses from the config file. Needs to be used after `createMultiversXNetwork` was called at least once by a process. + +`createAndExport` (see above) will try to also call `createMultiversXNetwork` so that relaying works to MultiversX as well. diff --git a/packages/axelar-local-dev-multiversx/__tests__/contracts/HelloWorld.sol b/packages/axelar-local-dev-multiversx/__tests__/contracts/HelloWorld.sol new file mode 100644 index 00000000..6e3983db --- /dev/null +++ b/packages/axelar-local-dev-multiversx/__tests__/contracts/HelloWorld.sol @@ -0,0 +1,53 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.9 <0.9.0; + +import { AxelarExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol'; +import { IAxelarGateway } from '@axelar-network/axelar-cgp-solidity/contracts/interfaces/IAxelarGateway.sol'; +import { IAxelarGasService } from '@axelar-network/axelar-cgp-solidity/contracts/interfaces/IAxelarGasService.sol'; + +contract HelloWorld is AxelarExecutable { + string public value; + string public sourceChain; + string public sourceAddress; + IAxelarGasService gasService; + + constructor(address _gateway, address _gasReceiver) + AxelarExecutable(_gateway) + { + gasService = IAxelarGasService(_gasReceiver); + } + + event Executed(); + + // Call this function to update the value of this contract along with all its siblings'. + function setRemoteValue( + string memory destinationChain, + string memory destinationAddress, + string calldata message + ) external payable { + bytes memory payload = abi.encodePacked(message); + if (msg.value > 0) { + gasService.payNativeGasForContractCall{value: msg.value}( + address(this), + destinationChain, + destinationAddress, + payload, + msg.sender + ); + } + gateway.callContract(destinationChain, destinationAddress, payload); + } + + // Handles calls created by setAndSend. Updates this contract's value + function _execute( + string calldata sourceChain_, + string calldata sourceAddress_, + bytes calldata payload_ + ) internal override { + (value) = abi.decode(payload_, (string)); + sourceChain = sourceChain_; + sourceAddress = sourceAddress_; + + emit Executed(); + } +} diff --git a/packages/axelar-local-dev-multiversx/__tests__/contracts/hello-world.wasm b/packages/axelar-local-dev-multiversx/__tests__/contracts/hello-world.wasm new file mode 100644 index 00000000..32e75aac Binary files /dev/null and b/packages/axelar-local-dev-multiversx/__tests__/contracts/hello-world.wasm differ diff --git a/packages/axelar-local-dev-multiversx/__tests__/multiversx.spec.ts b/packages/axelar-local-dev-multiversx/__tests__/multiversx.spec.ts new file mode 100644 index 00000000..f0601231 --- /dev/null +++ b/packages/axelar-local-dev-multiversx/__tests__/multiversx.spec.ts @@ -0,0 +1,323 @@ +import path from 'path'; +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, registerMultiversXRemoteITS } from '../src'; +import { + Address, + AddressValue, + BinaryCodec, + BytesType, + BytesValue, + ContractFunction, + ResultsParser, + SmartContract, + StringType, + StringValue, + TupleType +} from '@multiversx/sdk-core/out'; + +const { keccak256, toUtf8Bytes } = ethers.utils; + +setLogger(() => undefined); + +describe('multiversx', () => { + let client: MultiversXNetwork; + let evmNetwork: Network; + let wallet: Wallet; + + 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 () => { + // Deploy multiversx contract + const contractCode = path.join(__dirname, 'contracts/hello-world.wasm'); + + const contractAddress = await client.deployContract(contractCode, [ + new AddressValue(client.gatewayAddress as Address), + new AddressValue(client.gasReceiverAddress as Address) + ]); + + // Deploy EVM contract + const helloWorld = await deployContract(wallet, HelloWorld, [ + evmNetwork.gateway.address, + evmNetwork.gasService.address + ]); + + // Send tx from EVM to Multiversx + const msg = 'Hello Multiversx From EVM!'; + await helloWorld.setRemoteValue('multiversx', contractAddress, msg, { value: ethers.utils.parseEther('0.1') }); + + const multiversXRelayer = new MultiversXRelayer(); + + // Relay tx from EVM to MultiversX + await relay({ + multiversx: multiversXRelayer, + evm: new EvmRelayer({ multiversXRelayer }) + }); + + const result = await client.callContract(contractAddress, 'received_value'); + const parsedResult = new ResultsParser().parseUntypedQueryResponse(result); + expect(parsedResult?.values?.[0]); + + const decoded = new BinaryCodec().decodeTopLevel( + parsedResult.values[0], + new TupleType(new StringType(), new StringType(), new BytesType()) + ).valueOf(); + const message = decoded.field2.toString(); + + expect(message).toEqual(msg); + }); + + it('should be able to relay tx from Multiversx to Evm', async () => { + // Deploy multiversx contract + const contractCode = path.join(__dirname, 'contracts/hello-world.wasm'); + + const contractAddress = await client.deployContract(contractCode, [ + new AddressValue(client.gatewayAddress as Address), + new AddressValue(client.gasReceiverAddress as Address) + ]); + + // Deploy EVM contract + const helloWorld = await deployContract(wallet, HelloWorld, [ + evmNetwork.gateway.address, + evmNetwork.gasService.address + ]); + + const multiversXRelayer = new MultiversXRelayer(); + // Update events first so new Multiversx logs are processed afterwards + await multiversXRelayer.updateEvents(); + + const msg = 'Hello EVM From Multiversx!'; + const messageEvm = ethers.utils.defaultAbiCoder.encode(['string'], [msg]).substring(2); + const contract = new SmartContract({ address: Address.fromBech32(contractAddress) }); + const transaction = contract.call({ + caller: client.owner, + func: new ContractFunction('setRemoteValue'), + gasLimit: 20_000_000, + args: [ + new StringValue(evmNetwork.name), + new StringValue(helloWorld.address), + new BytesValue(Buffer.from(messageEvm, 'hex')) + ], + value: 20_000_000, + chainID: 'localnet' + }); + transaction.setNonce(client.ownerAccount.getNonceThenIncrement()); + + const returnCode = await client.signAndSendTransaction(transaction); + + expect(returnCode.isSuccess()); + + await relay({ + multiversx: multiversXRelayer, + evm: new EvmRelayer({ multiversXRelayer }) + }); + + const evmMessage = await helloWorld.value(); + expect(evmMessage).toEqual(msg); + }); + + it('should be able to approve contract call', async () => { + const payloadHash = ethers.utils.randomBytes(32); + const args = [ + 'ethereum', + '0xD62F0cF0801FAC878F66ebF316AB42DED01F25D8', + 'erd1qqqqqqqqqqqqqpgqlz32muzjtu40pp9lapy35n0cvrdxll47d8ss9ne0ta' + ]; + const tx = await client.executeGateway( + 'approveContractCall', + Buffer.from(ethers.utils.randomBytes(32)).toString('hex'), + args[0], + args[1], + args[2], + Buffer.from(payloadHash).toString('hex') + ); + expect(tx).toBeTruthy(); + }); + + it('should be able to call contract execute', async () => { + const contractCode = path.join(__dirname, 'contracts/hello-world.wasm'); + + const contractAddress = await client.deployContract(contractCode, [ + new AddressValue(client.gatewayAddress as Address), + new AddressValue(client.gasReceiverAddress as Address) + ]); + + const commandId = Buffer.from(ethers.utils.randomBytes(32)).toString('hex'); + const toSend = 'Hello Test World!'; + const payload = toUtf8Bytes(toSend); + const payloadHash = keccak256(payload).substring(2); + const args = [ + 'ethereum', + '0xD62F0cF0801FAC878F66ebF316AB42DED01F25D8', + contractAddress + ]; + const approveTx = await client.executeGateway( + 'approveContractCall', + commandId, + args[0], + args[1], + args[2], + payloadHash + ); + expect(approveTx).toBeTruthy(); + + const tx = await client.executeContract( + commandId, + contractAddress, + args[0], + args[1], + Buffer.from(payload).toString('hex') + ); + expect(tx).toBeTruthy(); + + const result = await client.callContract(contractAddress, 'received_value'); + const parsedResult = new ResultsParser().parseUntypedQueryResponse(result); + expect(parsedResult?.values?.[0]); + + const decoded = new BinaryCodec().decodeTopLevel( + parsedResult.values[0], + new TupleType(new StringType(), new StringType(), new BytesType()) + ).valueOf(); + const message = decoded.field2.toString(); + + 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/auth.wasm b/packages/axelar-local-dev-multiversx/contracts/auth.wasm new file mode 100644 index 00000000..3b688990 Binary files /dev/null and b/packages/axelar-local-dev-multiversx/contracts/auth.wasm differ diff --git a/packages/axelar-local-dev-multiversx/contracts/gas-service.wasm b/packages/axelar-local-dev-multiversx/contracts/gas-service.wasm new file mode 100644 index 00000000..7c9104bf Binary files /dev/null and b/packages/axelar-local-dev-multiversx/contracts/gas-service.wasm differ diff --git a/packages/axelar-local-dev-multiversx/contracts/gateway.wasm b/packages/axelar-local-dev-multiversx/contracts/gateway.wasm new file mode 100644 index 00000000..7470f3b5 Binary files /dev/null and b/packages/axelar-local-dev-multiversx/contracts/gateway.wasm differ 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/docker-compose.yaml b/packages/axelar-local-dev-multiversx/docker-compose.yaml new file mode 100644 index 00000000..53a74554 --- /dev/null +++ b/packages/axelar-local-dev-multiversx/docker-compose.yaml @@ -0,0 +1,8 @@ +services: + elasticsearch: + image: "docker.elastic.co/elasticsearch/elasticsearch:8.12.0" + environment: + - discovery.type=single-node + - xpack.security.enabled=false + ports: + - "9200:9200" diff --git a/packages/axelar-local-dev-multiversx/external.toml b/packages/axelar-local-dev-multiversx/external.toml new file mode 100644 index 00000000..58bfcf4e --- /dev/null +++ b/packages/axelar-local-dev-multiversx/external.toml @@ -0,0 +1,77 @@ +# ElasticSearchConnector defines settings related to ElasticSearch such as login information or URL +[ElasticSearchConnector] + ## We do not recommend to activate this indexer on a validator node since + #the node might loose rating (even facing penalties) due to the fact that + #the indexer is called synchronously and might block due to external causes. + #Strongly suggested to activate this on a regular observer node. + Enabled = true + IndexerCacheSize = 0 + BulkRequestMaxSizeInBytes = 4194304 # 4MB + URL = "http://localhost:9200" + UseKibana = false + Username = "" + Password = "" + # EnabledIndexes represents a slice of indexes that will be enabled for indexing. Full list is: + # ["rating", "transactions", "blocks", "validators", "miniblocks", "rounds", "accounts", "accountshistory", "receipts", "scresults", "accountsesdt", "accountsesdthistory", "epochinfo", "scdeploys", "tokens", "tags", "logs", "delegators", "operations", "esdts"] + EnabledIndexes = ["logs"] + +# EventNotifierConnector defines settings needed to configure and launch the event notifier component +# HTTP event notifier connector integration will be DEPRECATED in the following iterations +[EventNotifierConnector] + # Enabled will turn on or off the event notifier connector + Enabled = false + + # UseAuthorization signals the proxy to use authorization + # Never run a production setup without authorization + UseAuthorization = false + + # ProxyUrl is used to communicate with the subscriptions hub + # The indexer instance will broadcast data using ProxyUrl + ProxyUrl = "http://localhost:5000" + + # Username and Password need to be specified if UseAuthorization is set to true + Username = "" + + # Password is used to authorize an observer to push event data + Password = "" + + # RequestTimeoutSec defines the timeout in seconds for the http client + RequestTimeoutSec = 60 + + # MarshallerType is used to define the marshaller type to be used for inner + # marshalled structures in block events data + MarshallerType = "json" + +[[HostDriversConfig]] + # This flag shall only be used for observer nodes + Enabled = false + + # This flag will start the WebSocket connector as server or client (can be "client" or "server") + Mode = "client" + + # URL for the WebSocket client/server connection + # This value represents the IP address and port number that the WebSocket client or server will use to establish a connection. + URL = "127.0.0.1:22111" + + # After a message will be sent it will wait for an ack message if this flag is enabled + WithAcknowledge = true + + # The duration in seconds to wait for an acknowledgment message, after this time passes an error will be returned + AcknowledgeTimeoutInSec = 60 + + # This flag defines the marshaller type. Currently supported: "json", "gogo protobuf" + MarshallerType = "json" + + # The number of seconds when the client will try again to send the data + RetryDurationInSec = 5 + + # Sets if, in case of data payload processing error, we should block or not the advancement to the next processing event. Set this to true if you wish the node to stop processing blocks if the client/server encounters errors while processing requests. + BlockingAckOnError = true + + # Set to true to drop messages if there is no active WebSocket connection to send to. + DropMessagesIfNoConnection = false + + # Defines the payload version. Version will be changed when there are breaking + # changes on payload data. The receiver/consumer will have to know how to handle different + # versions. The version will be sent as metadata in the websocket message. + Version = 1 diff --git a/packages/axelar-local-dev-multiversx/hardhat.config.js b/packages/axelar-local-dev-multiversx/hardhat.config.js new file mode 100644 index 00000000..4b764352 --- /dev/null +++ b/packages/axelar-local-dev-multiversx/hardhat.config.js @@ -0,0 +1,41 @@ +require('hardhat-gas-reporter'); +require('solidity-coverage'); +require('@typechain/hardhat'); +/** + * @type import('hardhat/config').HardhatUserConfig + */ +module.exports = { + solidity: { + version: '0.8.9', + settings: { + evmVersion: process.env.EVM_VERSION || 'london', + optimizer: { + enabled: true, + runs: 1000, + details: { + peephole: true, + inliner: true, + jumpdestRemover: true, + orderLiterals: true, + deduplicate: true, + cse: true, + constantOptimizer: true, + yul: true, + yulDetails: { + stackAllocation: true, + }, + }, + }, + }, + }, + paths: { + sources: './__tests__/contracts', + }, + mocha: { + timeout: 200000, + }, + typechain: { + outDir: 'src/types', + target: 'ethers-v5', + }, +}; diff --git a/packages/axelar-local-dev-multiversx/jest.config.js b/packages/axelar-local-dev-multiversx/jest.config.js new file mode 100644 index 00000000..4419682a --- /dev/null +++ b/packages/axelar-local-dev-multiversx/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + transform: { + '^.+\\.ts?$': 'ts-jest', + }, + testRegex: '/__tests__/.*\\.(test|spec)?\\.(ts)$', + transformIgnorePatterns: ['/node_modules/'], + testTimeout: 300000, +}; diff --git a/packages/axelar-local-dev-multiversx/package.json b/packages/axelar-local-dev-multiversx/package.json new file mode 100644 index 00000000..0720c881 --- /dev/null +++ b/packages/axelar-local-dev-multiversx/package.json @@ -0,0 +1,33 @@ +{ + "name": "@axelar-network/axelar-local-dev-multiversx", + "version": "2.2.2", + "main": "dist/index.js", + "files": [ + "dist/", + "contracts/", + "!dist/types", + "!dist/artifacts" + ], + "scripts": { + "test": "jest", + "build-ts": "tsc", + "build-contract": "hardhat clean && hardhat compile", + "build": "run-s clean build-ts build-contract", + "clean": "rm -rf src/types dist artifacts", + "prettier": "prettier --write 'src/**/*.ts'" + }, + "dependencies": { + "@axelar-network/axelar-local-dev": "2.2.0", + "@multiversx/sdk-core": "^12.18.0", + "@multiversx/sdk-network-providers": "^2.2.1", + "@multiversx/sdk-wallet": "^4.3.0", + "keccak": "^3.0.4", + "@elastic/elasticsearch": "^8.12.0" + }, + "devDependencies": { + "@types/keccak": "^3.0.4" + }, + "author": "", + "license": "ISC", + "description": "" +} diff --git a/packages/axelar-local-dev-multiversx/src/Command.ts b/packages/axelar-local-dev-multiversx/src/Command.ts new file mode 100644 index 00000000..de74e3c0 --- /dev/null +++ b/packages/axelar-local-dev-multiversx/src/Command.ts @@ -0,0 +1,101 @@ +'use strict'; + +import { ethers } from 'ethers'; + +const { defaultAbiCoder } = ethers.utils; +import { CallContractArgs, RelayData } from '@axelar-network/axelar-local-dev'; +import { multiversXNetwork } from './multiversXNetworkUtils'; +import createKeccakHash from 'keccak'; + +//An internal class for handling MultiversX commands. +export class Command { + commandId: string; + name: string; + data: any[]; + encodedData: string; + post: ((options: any) => Promise) | undefined; + + constructor( + commandId: string, + name: string, + data: any[], + dataSignature: string[], + post: ((options: any) => Promise) | undefined = undefined, + chain: string | null = null + ) { + this.commandId = commandId; + this.name = name; + this.data = data; + this.encodedData = chain === 'multiversx' && name === 'approveContractCall' ? '' : defaultAbiCoder.encode(dataSignature, data); + this.post = post; + } + + static createContractCallCommand = (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 tx = await multiversXNetwork.executeContract( + commandId, + args.destinationContractAddress, + args.from, + args.sourceAddress, + payloadHex + ); + + relayData.callContract[commandId].execution = tx.getHash(); + + return tx; + }, + '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 new file mode 100644 index 00000000..3679fb6f --- /dev/null +++ b/packages/axelar-local-dev-multiversx/src/MultiversXNetwork.ts @@ -0,0 +1,656 @@ +import { + Account, + Address, + AddressType, + AddressValue, + BigUIntValue, + BinaryCodec, + BytesValue, + CodeMetadata, + ContractFunction, + H256Value, + Interaction, + List, + OptionType, + OptionValue, + ResultsParser, + ReturnCode, + SmartContract, + StringType, + StringValue, + Transaction, + TransactionWatcher, + Tuple, + TypedValue, + U8Value, + VariadicValue +} from '@multiversx/sdk-core/out'; +import { ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; +import { Code } from '@multiversx/sdk-core'; +import fs, { promises } from 'fs'; +import { UserSecretKey } from '@multiversx/sdk-wallet/out'; +import path from 'path'; +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'; + +const codec = new BinaryCodec(); + +export class MultiversXNetwork extends ProxyNetworkProvider { + public owner: Address; + public ownerAccount: Account; + public operatorWallet: Address; + 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; + + constructor( + url: string, + gatewayAddress: string | undefined, + authAddress: string | undefined, + gasReceiverAddress: string | undefined, + interchainTokenServiceAddress: string | undefined, + interchainTokenFactoryAddress: string | undefined, + contractAddress: string | undefined = undefined + ) { + super(url); + this.owner = Address.fromBech32('erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th'); // alice.pem + this.operatorWallet = Address.fromBech32('erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx'); // bob.pem + this.ownerAccount = new Account(this.owner); + try { + this.gatewayAddress = gatewayAddress ? Address.fromBech32(gatewayAddress) : undefined; + } catch (e) { + } + try { + this.authAddress = authAddress ? Address.fromBech32(authAddress) : undefined; + } catch (e) { + } + try { + 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; + + const homedir = os.homedir(); + const ownerWalletFile = path.resolve(homedir, 'multiversx-sdk/testwallets/latest/users/alice.pem'); + + 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 + || !this.interchainTokenServiceAddress + || !this.interchainTokenFactoryAddress + ) { + return false; + } + + try { + const accountGateway = await this.getAccount(this.gatewayAddress); + const accountAuth = await this.getAccount(this.authAddress); + const accountGasReceiver = await this.getAccount(this.gasReceiverAddress); + 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; + } + + return true; + } catch (e) { + console.error(e); + + return false; + } + } + + async deployContract(contractCode: string, initArguments: TypedValue[]): Promise { + const homedir = os.homedir(); + const ownerWalletFile = path.resolve(homedir, 'multiversx-sdk/testwallets/latest/users/alice.pem'); + + const file = fs.readFileSync(ownerWalletFile).toString(); + const privateKey = UserSecretKey.fromPem(file); + + const buffer = await promises.readFile(contractCode); + + const code = Code.fromBuffer(buffer); + const authContract = new SmartContract(); + + const deployTransaction = authContract.deploy({ + deployer: this.owner, + code, + codeMetadata: new CodeMetadata(true, true, false, false), + initArguments, + gasLimit: 50_000_000, + chainID: 'localnet' + }); + deployTransaction.setNonce(this.ownerAccount.getNonceThenIncrement()); + + const returnCode = await this.signAndSendTransaction(deployTransaction, privateKey); + + if (!returnCode.isSuccess()) { + throw new Error(`Could not deploy Contract...`); + } + + const contractAddress = SmartContract.computeAddress( + deployTransaction.getSender(), + deployTransaction.getNonce() + ); + + return contractAddress.bech32(); + } + + async deployAxelarFrameworkModules(): Promise { + console.log(`Deploying the Axelar Gateway for MultiversX... `); + + const contractFolder = path.join(__dirname, '..', 'contracts'); + + const axelarAuthAddress = await this.deployAuthContract(contractFolder); + const axelarGatewayAddress = await this.deployGatewayContract(contractFolder, axelarAuthAddress); + await this.changeContractOwner(axelarAuthAddress, axelarGatewayAddress); + + 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, + interchainTokenServiceAddress, + interchainTokenFactoryAddress, + }; + } + + private async deployAuthContract(contractFolder: string): Promise { + const buffer = await promises.readFile(contractFolder + '/auth.wasm'); + + const code = Code.fromBuffer(buffer); + const authContract = new SmartContract(); + + const authTransaction = authContract.deploy({ + deployer: this.owner, + code, + codeMetadata: new CodeMetadata(true, true, false, false), + initArguments: [ + Tuple.fromItems([ + List.fromItems([new H256Value(Buffer.from(this.operatorWallet.hex(), 'hex'))]), + List.fromItems([new BigUIntValue(1)]), + new BigUIntValue(1) + ]) + ], + gasLimit: 50_000_000, + chainID: 'localnet' + }); + authTransaction.setNonce(this.ownerAccount.getNonceThenIncrement()); + + const returnCode = await this.signAndSendTransaction(authTransaction); + + if (!returnCode.isSuccess()) { + throw new Error(`Could not deploy Axelar Auth contract... ${authTransaction.getHash()}`); + } + + const axelarAuthAddress = SmartContract.computeAddress(authTransaction.getSender(), authTransaction.getNonce()); + console.log(`Auth contract deployed at ${axelarAuthAddress} with transaction ${authTransaction.getHash()}`); + + return axelarAuthAddress.bech32(); + } + + private async deployGatewayContract(contractFolder: string, axelarAuthAddress: string): Promise { + const buffer = await promises.readFile(contractFolder + '/gateway.wasm'); + + const code = Code.fromBuffer(buffer); + const gatewayContract = new SmartContract(); + + const gatewayTransaction = gatewayContract.deploy({ + deployer: this.owner, + code, + codeMetadata: new CodeMetadata(true, true, false, false), + initArguments: [ + new AddressValue(Address.fromBech32(axelarAuthAddress)), + new StringValue(CHAIN_ID), + ], + gasLimit: 50_000_000, + chainID: 'localnet' + }); + gatewayTransaction.setNonce(this.ownerAccount.getNonceThenIncrement()); + + const returnCode = await this.signAndSendTransaction(gatewayTransaction); + + if (!returnCode.isSuccess()) { + throw new Error(`Could not deploy Axelar Gateway contract... ${gatewayTransaction.getHash()}`); + } + + const axelarGatewayAddress = SmartContract.computeAddress( + gatewayTransaction.getSender(), + gatewayTransaction.getNonce() + ); + console.log(`Gateway contract deployed at ${axelarGatewayAddress} with transaction ${gatewayTransaction.getHash()}`); + + return axelarGatewayAddress.bech32(); + } + + private async changeContractOwner(contractAddress: string, newOwner: string): Promise { + const contract = new SmartContract({ address: Address.fromBech32(contractAddress) }); + + const transaction = contract.call({ + caller: this.owner, + func: new ContractFunction('ChangeOwnerAddress'), + gasLimit: 6_000_000, + args: [new AddressValue(Address.fromBech32(newOwner))], + chainID: 'localnet' + }); + transaction.setNonce(this.ownerAccount.getNonceThenIncrement()); + + const returnCode = await this.signAndSendTransaction(transaction); + + if (!returnCode.isSuccess()) { + throw new Error(`Could not change owner of Axelar Gateway contract... ${transaction.getHash()}`); + } + + console.log('Changed contract owner of Auth contract...'); + } + + private async deployGasReceiverContract(contractFolder: string): Promise { + const buffer = await promises.readFile(contractFolder + '/gas-service.wasm'); + + const code = Code.fromBuffer(buffer); + const authContract = new SmartContract(); + + const gasReceiverTransaction = authContract.deploy({ + deployer: this.owner, + code, + codeMetadata: new CodeMetadata(true, true, false, false), + initArguments: [ + new AddressValue(this.owner) + ], + gasLimit: 50_000_000, + chainID: 'localnet' + }); + gasReceiverTransaction.setNonce(this.ownerAccount.getNonceThenIncrement()); + + const returnCode = await this.signAndSendTransaction(gasReceiverTransaction); + + if (!returnCode.isSuccess()) { + throw new Error(`Could not deploy Axelar Gas Receiver contract... ${gasReceiverTransaction.getHash()}`); + } + + const axelarGasReceiverAddress = SmartContract.computeAddress( + gasReceiverTransaction.getSender(), + gasReceiverTransaction.getNonce() + ); + console.log(`Gas Receiver contract deployed at ${axelarGasReceiverAddress} with transaction ${gasReceiverTransaction.getHash()}`); + + 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); + + try { + await this.sendTransaction(transaction); + } catch (e) { + console.error('Could not send MultiversX transaction', transaction.getHash()); + + return ReturnCode.Unknown; + } + + const transactionOnNetwork = await new TransactionWatcher({ + getTransaction: async (hash: string) => { return await this.getTransaction(hash, true); } + }).awaitCompleted(transaction); + const { returnCode } = new ResultsParser().parseUntypedOutcome(transactionOnNetwork); + + return returnCode; + } + + async callContract(address: string, func: string, args: TypedValue[] = []): Promise { + const contract = new SmartContract({ + address: new Address(address) + }); + + const query = new Interaction(contract, new ContractFunction(func), args).buildQuery(); + + return await super.queryContract(query); + } + + public async executeGateway( + commandName: string, + commandId: string, + sourceChain: string, + sourceAddress: string, + destinationAddress: string, + payloadHash: string, + ) { + // Remove 0x added by Ethereum for hex strings + commandId = commandId.startsWith('0x') ? commandId.substring(2) : commandId; + + const gatewayContract = new SmartContract({ address: this.gatewayAddress as Address }); + + const approveContractCallData = Tuple.fromItems([ + new StringValue(sourceChain), + new StringValue(sourceAddress), + new AddressValue(Address.fromBech32(destinationAddress)), + new H256Value(Buffer.from(payloadHash, 'hex')), + ]); + const encodedApproveContractCallData = codec.encodeTopLevel(approveContractCallData); + + const executeData = Tuple.fromItems([ + new StringValue(CHAIN_ID), + List.fromItems([new H256Value(Buffer.from(commandId, 'hex'))]), + List.fromItems([new StringValue(commandName)]), + List.fromItems([ + new BytesValue(encodedApproveContractCallData), + ]), + ]); + const encodedExecuteData = codec.encodeTopLevel(executeData); + + const proof = this.generateProof(encodedExecuteData); + const encodedProof = codec.encodeTopLevel(proof); + + const transaction = gatewayContract.call({ + caller: this.owner, + func: new ContractFunction('execute'), + gasLimit: 50_000_000, + args: [ + Tuple.fromItems([ + new BytesValue(encodedExecuteData), + new BytesValue(encodedProof), + ]), + ], + chainID: 'localnet' + }); + + const accountOnNetwork = await this.getAccount(this.owner); + this.ownerAccount.update(accountOnNetwork); + + transaction.setNonce(this.ownerAccount.getNonceThenIncrement()); + + const returnCode = await this.signAndSendTransaction(transaction); + + if (!returnCode.isSuccess()) { + throw new Error(`Could not execute MultiversX Gateway transaction... ${transaction.getHash()}`); + } + + return transaction; + } + + public async executeContract( + commandId: string, + destinationContractAddress: string, + sourceChain: string, + sourceAddress: string, + payloadHex: string, + value: string = '0', + ): Promise { + // Remove 0x added by Ethereum for hex strings + commandId = commandId.startsWith('0x') ? commandId.substring(2) : commandId; + + const contract = new SmartContract({ address: Address.fromBech32(destinationContractAddress) }); + + const transaction = contract.call({ + caller: this.owner, + func: new ContractFunction('execute'), + 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' + }); + + const accountOnNetwork = await this.getAccount(this.owner); + this.ownerAccount.update(accountOnNetwork); + + transaction.setNonce(this.ownerAccount.getNonceThenIncrement()); + + const returnCode = await this.signAndSendTransaction(transaction); + + if (!returnCode.isSuccess()) { + throw new Error(`Could not call MultiversX contract execute endpoint... ${transaction.getHash()}`); + } + + return transaction; + } + + private generateProof(encodedData: Buffer) { + const messageHashData = Buffer.concat([ + Buffer.from(MULTIVERSX_SIGNED_MESSAGE_PREFIX), + encodedData + ]); + + const messageHash = createKeccakHash('keccak256').update(messageHashData).digest('hex'); + + const homedir = os.homedir(); + const operatorWalletFile = path.resolve(homedir, 'multiversx-sdk/testwallets/latest/users/bob.pem'); + + const file = fs.readFileSync(operatorWalletFile).toString(); + + const signature = UserSecretKey.fromPem(file).sign(Buffer.from(messageHash, 'hex')); + + return Tuple.fromItems([ + List.fromItems([new H256Value(Buffer.from(this.operatorWallet.hex(), 'hex'))]), + List.fromItems([new BigUIntValue(1)]), + new BigUIntValue(1), + List.fromItems([new H256Value(signature)]), + ]); + } +} diff --git a/packages/axelar-local-dev-multiversx/src/MultiversXRelayer.ts b/packages/axelar-local-dev-multiversx/src/MultiversXRelayer.ts new file mode 100644 index 00000000..9540cab9 --- /dev/null +++ b/packages/axelar-local-dev-multiversx/src/MultiversXRelayer.ts @@ -0,0 +1,311 @@ +import { ethers } from 'ethers'; +import { arrayify, defaultAbiCoder } from 'ethers/lib/utils'; +import { + CallContractArgs, + Command, + getGasPrice, + getSignedExecuteInput, + logger, + NativeGasPaidForContractCallArgs, + Network, + networks, + RelayCommand, + RelayData, + Relayer, + RelayerType +} from '@axelar-network/axelar-local-dev'; +import { Command as MultiversXCommand } from './Command'; +import { multiversXNetwork } from './multiversXNetworkUtils'; +import { + Address, + AddressType, + AddressValue, + BigUIntType, + BigUIntValue, + BinaryCodec, + BytesType, + BytesValue, + H256Type, + H256Value, + StringType, + TupleType +} from '@multiversx/sdk-core/out'; +import { getMultiversXLogID } from './utils'; + +const AddressZero = ethers.constants.AddressZero; + +const { Client } = require('@elastic/elasticsearch'); + +interface MultiversXEvent { + identifier: string; + address: string; + data: string; + topics: string[]; + order: number; + txHash?: string; +} + +export class MultiversXRelayer extends Relayer { + private readonly elasticsearch; + + private initialHitsLength: number = -1; + + constructor() { + super(); + + this.elasticsearch = new Client({ + node: 'http://localhost:9200' + }); + } + + setRelayer(type: RelayerType, _: Relayer) { + if (type !== RelayerType.Evm) { + console.log('Only evm is supported for multiversx'); + } + } + + async updateEvents(): Promise { + const logsCount = await this.elasticsearch.count({ + index: 'logs' + }); + const count = logsCount.count; + + // Skip processing if no new logs + if (this.initialHitsLength == -1) { + this.initialHitsLength = count; + } + if (this.initialHitsLength === count) { + return; + } + + // Process only new events + const logs = await this.elasticsearch.search({ + index: 'logs', + sort: [ + { timestamp: 'desc' } + ], + size: count - this.initialHitsLength + }); + const hits = logs.hits.hits; + + const newHits: MultiversXEvent[] = hits + .reduce((acc: any, hit: any) => { + const newEvents = hit._source.events + .map((newEvent: MultiversXEvent) => ({ ...newEvent, txHash: hit._id })); + + acc.push(...newEvents); + + return acc; + }, []); + + await this.updateGasEvents( + newHits + .filter((newHit: any) => newHit.address === multiversXNetwork.gasReceiverAddress?.bech32()) + ); + await this.updateCallContractEvents( + newHits + .filter((newHit: any) => newHit.address === multiversXNetwork.gatewayAddress?.bech32()) + ); + + this.initialHitsLength = count; + } + + async execute(commands: RelayCommand) { + await this.executeMultiversXToEvm(commands); + await this.executeEvmToMultiversX(commands); + } + + private async executeMultiversXToEvm(commandList: RelayCommand) { + for (const to of networks) { + const commands = commandList[to.name]; + if (commands.length == 0) continue; + + const execution = await this.executeEvmGateway(to, commands); + await this.executeEvmExecutable(to, commands, execution); + } + } + + private async executeEvmToMultiversX(commands: RelayCommand) { + const toExecute = commands['multiversx']; + if (toExecute?.length === 0) return; + + await this.executeMultiversXGateway(toExecute); + await this.executeMultiversXExecutable(toExecute); + } + + private async executeMultiversXGateway(commands: Command[]) { + if (!multiversXNetwork) return; + for (const command of commands) { + await multiversXNetwork.executeGateway( + command.name, + command.commandId, + command.data[0], + command.data[1], + command.data[2], + command.data[3] + ); + } + } + + private async executeMultiversXExecutable(commands: Command[]) { + if (!multiversXNetwork) return; + for (const command of commands) { + if (!command.post) continue; + + await command.post({}); + } + } + + private async executeEvmGateway(to: Network, commands: Command[]): Promise { + const data = arrayify( + defaultAbiCoder.encode( + ['uint256', 'bytes32[]', 'string[]', 'bytes[]'], + [to.chainId, commands.map((com) => com.commandId), commands.map((com) => com.name), commands.map((com) => com.encodedData)] + ) + ); + const signedData = await getSignedExecuteInput(data, to.operatorWallet); + + return to.gateway + .connect(to.ownerWallet) + .execute(signedData, { gasLimit: BigInt(8e6) }) + .then((tx: any) => tx.wait()); + } + + private async executeEvmExecutable(to: Network, commands: Command[], execution: any): Promise { + for (const command of commands) { + if (command.post == null) continue; + + if ( + !execution.events.find((event: any) => { + return event.event === 'Executed' && event.args[0] == command.commandId; + }) + ) + continue; + + const payed = + command.name == 'approveContractCall' + ? this.contractCallGasEvents.find((log: any) => { + if (log.sourceAddress.toLowerCase() != command.data[1].toLowerCase()) return false; + if (log.destinationChain.toLowerCase() != to.name.toLowerCase()) return false; + if (log.destinationAddress.toLowerCase() != command.data[2].toLowerCase()) return false; + if (log.payloadHash.toLowerCase() != command.data[3].toLowerCase()) return false; + return true; + }) + : false; + + if (!payed) continue; + if (command.name == 'approveContractCall') { + const index = this.contractCallGasEvents.indexOf(payed); + this.contractCallGasEvents = this.contractCallGasEvents.filter((_, i) => i !== index); + } + + try { + const cost = getGasPrice(); + const blockLimit = Number((await to.provider.getBlock('latest')).gasLimit); + + await command.post({ + gasLimit: BigInt(Math.min(blockLimit, payed.gasFeeAmount / cost)) + }); + } catch (e) { + logger.log(e); + } + } + } + + private async updateGasEvents(events: MultiversXEvent[]) { + 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(); + + 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(), + destinationAddress, + gasFeeAmount, + destinationChain, + payloadHash, + refundAddress, + gasToken: AddressZero + }; + + this.contractCallGasEvents.push(args); + } + } + + private async updateCallContractEvents(events: MultiversXEvent[]) { + const newEvents = events.filter((event) => event.identifier === 'callContract'); + + for (const event of newEvents) { + 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 BytesType()) + ).valueOf(); + // Need to add '0x' in front of hex encoded strings for EVM + const payloadHash = '0x' + (decoded.field0 as H256Value).valueOf().toString('hex'); + const payload = '0x' + (decoded.field1 as BytesValue).valueOf().toString('hex'); + + const commandId = getMultiversXLogID('multiversx', sender.bech32(), event.txHash as string, event.order); + + const contractCallArgs: CallContractArgs = { + from: 'multiversx', + to: destinationChain, + sourceAddress: sender.bech32(), + destinationContractAddress: destinationAddress, + payload, + payloadHash, + transactionHash: event.txHash as string, + sourceEventIndex: event.order + }; + + this.relayData.callContract[commandId] = contractCallArgs; + const command = Command.createEVMContractCallCommand(commandId, this.relayData, contractCallArgs); + this.commands[contractCallArgs.to].push(command); + } + } + + 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); + } + + createCallContractWithTokenCommand(): Command { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/axelar-local-dev-multiversx/src/index.ts b/packages/axelar-local-dev-multiversx/src/index.ts new file mode 100644 index 00000000..5ba38f69 --- /dev/null +++ b/packages/axelar-local-dev-multiversx/src/index.ts @@ -0,0 +1,5 @@ +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 new file mode 100644 index 00000000..ad14d2a3 --- /dev/null +++ b/packages/axelar-local-dev-multiversx/src/multiversXNetworkUtils.ts @@ -0,0 +1,109 @@ +import { MultiversXNetwork } from './MultiversXNetwork'; +import fs from 'fs'; +import path from 'path'; + +export let multiversXNetwork: MultiversXNetwork; + +export interface MultiversXNetworkConfig { + gatewayUrl: string; +} + +export interface MultiversXConfig { + axelarAuthAddress: string; + axelarGatewayAddress: string; + axelarGasReceiverAddress: string; + interchainTokenServiceAddress: string; + interchainTokenFactoryAddress: string; + contractAddress?: string; +} + +export function updateMultiversXConfig(contractAddress: string) { + const currentConfig = getMultiversXConfig() as MultiversXConfig; + + createMultiversXConfig({ + ...currentConfig, + contractAddress, + }); +} + +function createMultiversXConfig(config: MultiversXConfig) { + console.log('Creating MultiversX config file...'); + + const configPath = path.join(__dirname, '..', 'multiversxConfig.json'); + fs.writeFileSync(configPath, JSON.stringify(config)); +} + +function getMultiversXConfig(): MultiversXConfig | undefined { + const configPath = path.join(__dirname, '..', 'multiversxConfig.json'); + + if (!fs.existsSync(configPath)) { + console.log('MultiversX config file not found'); + + return undefined; + } + + const contents = fs.readFileSync(configPath); + + return JSON.parse(contents.toString()); +} + +export async function createMultiversXNetwork(config?: MultiversXNetworkConfig): Promise { + const configFile = getMultiversXConfig(); + + const gatewayUrl = config?.gatewayUrl || 'http://localhost:7950'; + const loadingMultiversXNetwork = new MultiversXNetwork( + gatewayUrl, + configFile?.axelarGatewayAddress, + configFile?.axelarAuthAddress, + configFile?.axelarGasReceiverAddress, + configFile?.interchainTokenServiceAddress, + configFile?.interchainTokenFactoryAddress, + configFile?.contractAddress, + ); + + // Check if whether the gateway is deployed + const isGatewayDeployed = await loadingMultiversXNetwork.isGatewayDeployed(); + + // Deploy multiversx framework modules, skip if already deployed + if (!isGatewayDeployed) { + try { + const multiversXConfig = await loadingMultiversXNetwork.deployAxelarFrameworkModules(); + + createMultiversXConfig(multiversXConfig); + + console.log('Deployed Axelar Framework modules for MultiversX'); + } catch (e) { + console.error(e); + } + } else { + console.log(`MultiversX Axelar Gateway contract is already deployed at ${loadingMultiversXNetwork.gatewayAddress}`); + } + + multiversXNetwork = loadingMultiversXNetwork; + + return multiversXNetwork; +} + +export async function loadMultiversXNetwork( + gatewayUrl = 'http://localhost:7950', +) { + const configFile = getMultiversXConfig(); + + multiversXNetwork = new MultiversXNetwork( + gatewayUrl, + configFile?.axelarGatewayAddress, + configFile?.axelarAuthAddress, + configFile?.axelarGasReceiverAddress, + configFile?.interchainTokenServiceAddress, + configFile?.interchainTokenFactoryAddress, + configFile?.contractAddress, + ); + + const isGatewayDeployed = await multiversXNetwork.isGatewayDeployed(); + + if (!isGatewayDeployed) { + throw new Error('Axelar Gateway contract is not deployed on MultiversX!'); + } + + return multiversXNetwork; +} diff --git a/packages/axelar-local-dev-multiversx/src/utils.ts b/packages/axelar-local-dev-multiversx/src/utils.ts new file mode 100644 index 00000000..2e7a0936 --- /dev/null +++ b/packages/axelar-local-dev-multiversx/src/utils.ts @@ -0,0 +1,7 @@ +'use strict'; + +import { ethers } from 'ethers'; + +export const getMultiversXLogID = (chain: string, sender: string, txHash: string, logIndex: number) => { + return ethers.utils.id(chain + ':' + sender + ':' + txHash + ':' + logIndex); +}; diff --git a/packages/axelar-local-dev-multiversx/tsconfig.json b/packages/axelar-local-dev-multiversx/tsconfig.json new file mode 100644 index 00000000..926963cc --- /dev/null +++ b/packages/axelar-local-dev-multiversx/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + /* Language and Environment */ + "target": "es6" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "lib": ["es2020", "dom"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, + + /* Modules */ + "module": "CommonJS" /* Specify what module code is generated. */, + + "resolveJsonModule": true, + /* Emit */ + "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, + "declarationMap": true /* Create sourcemaps for d.ts files. */, + "sourceMap": true /* Create source map files for emitted JavaScript files. */, + "rootDirs": ["./src", "src/__test__"] /* Specify the root folder within your source files. */, + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + "strictNullChecks": true /* When type checking, take into account `null` and `undefined`. */, + + /* Completeness */ + "skipDefaultLibCheck": true /* Skip type checking .d.ts files that are included with TypeScript. */, + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "src/__tests__/"] +} diff --git a/packages/axelar-local-dev-near/package.json b/packages/axelar-local-dev-near/package.json index 6c29ae97..4568e50c 100644 --- a/packages/axelar-local-dev-near/package.json +++ b/packages/axelar-local-dev-near/package.json @@ -1,6 +1,6 @@ { "name": "@axelar-network/axelar-local-dev-near", - "version": "2.2.0-alpha.28", + "version": "2.2.0", "description": "", "main": "dist/index.js", "files": [ @@ -21,7 +21,7 @@ }, "dependencies": { "@axelar-network/axelar-cgp-near": "^1.0.0", - "@axelar-network/axelar-local-dev": "2.2.0-alpha.28" + "@axelar-network/axelar-local-dev": "2.2.0" }, "author": "", "license": "ISC", diff --git a/packages/axelar-local-dev-sui/__tests__/deploy.spec.ts b/packages/axelar-local-dev-sui/__tests__/deploy.spec.ts index 24d7fb1a..fdf482c9 100644 --- a/packages/axelar-local-dev-sui/__tests__/deploy.spec.ts +++ b/packages/axelar-local-dev-sui/__tests__/deploy.spec.ts @@ -2,7 +2,7 @@ import { SuiNetwork } from '../src/SuiNetwork'; import { TransactionBlock } from '@mysten/sui.js/transactions'; import path from 'path'; -describe('Sui Network', () => { +describe.skip('Sui Network', () => { let client: SuiNetwork; beforeEach(async () => { @@ -19,8 +19,10 @@ describe('Sui Network', () => { it('should deploy and execute a function', async () => { const response = await client.deploy(path.join(__dirname, '../move/sample')); const packageId = response.packages[0].packageId; - const singleton: any = response.publishTxn.objectChanges?.find((change) => (change as any).objectType === `${packageId}::test::Singleton` ) - + const singleton: any = response.publishTxn.objectChanges?.find( + (change) => (change as any).objectType === `${packageId}::test::Singleton`, + ); + const tx = new TransactionBlock(); const msg = 'hello from test'; @@ -40,7 +42,7 @@ describe('Sui Network', () => { limit: 1, }); - const event = (data[0].parsedJson as any); + const event = data[0].parsedJson as any; expect(event.destination_chain).toEqual('Avalanche'); expect(event.destination_address).toEqual('0x0'); diff --git a/packages/axelar-local-dev-sui/__tests__/e2e.spec.ts b/packages/axelar-local-dev-sui/__tests__/e2e.spec.ts index 4949f0c4..d1f64749 100644 --- a/packages/axelar-local-dev-sui/__tests__/e2e.spec.ts +++ b/packages/axelar-local-dev-sui/__tests__/e2e.spec.ts @@ -5,7 +5,7 @@ import { SuiNetwork, SuiRelayer, initSui } from '@axelar-network/axelar-local-de import path from 'path'; const { arrayify } = ethers.utils; -describe('e2e', () => { +describe.skip('e2e', () => { let client: SuiNetwork; let relayer: SuiRelayer; let evmNetwork: Network; @@ -35,8 +35,10 @@ describe('e2e', () => { // Deploy a sample module const response = await client.deploy(path.join(__dirname, '../move/sample')); const packageId = response.packages[0].packageId; - const singleton: any = response.publishTxn.objectChanges?.find((change) => (change as any).objectType === `${packageId}::test::Singleton` ) - + const singleton: any = response.publishTxn.objectChanges?.find( + (change) => (change as any).objectType === `${packageId}::test::Singleton`, + ); + const msg = 'hello from sui'; const payload = ethers.utils.defaultAbiCoder.encode(['string'], [msg]); @@ -45,7 +47,12 @@ describe('e2e', () => { const tx = new TransactionBlock(); tx.moveCall({ target: `${response.packages[0].packageId}::test::send_call`, - arguments: [tx.object(singleton.objectId), tx.pure(evmChainName), tx.pure(evmContract.address), tx.pure(String.fromCharCode(...arrayify(payload)))], + arguments: [ + tx.object(singleton.objectId), + tx.pure(evmChainName), + tx.pure(evmContract.address), + tx.pure(String.fromCharCode(...arrayify(payload))), + ], }); await client.execute(tx); @@ -68,14 +75,15 @@ describe('e2e', () => { // Deploy a sample module const response = await client.deploy(path.join(__dirname, '../move/sample')); const packageId = response.packages[0].packageId; - const singleton: any = response.publishTxn.objectChanges?.find((change) => (change as any).objectType === `${packageId}::test::Singleton` ) - + const singleton: any = response.publishTxn.objectChanges?.find( + (change) => (change as any).objectType === `${packageId}::test::Singleton`, + ); + const singletonFields: any = await client.getObject({ id: singleton.objectId, options: { showContent: true, - } - + }, }); let tx = new TransactionBlock(); diff --git a/packages/axelar-local-dev-sui/__tests__/relayer.spec.ts b/packages/axelar-local-dev-sui/__tests__/relayer.spec.ts index b2d45833..700b2dcc 100644 --- a/packages/axelar-local-dev-sui/__tests__/relayer.spec.ts +++ b/packages/axelar-local-dev-sui/__tests__/relayer.spec.ts @@ -4,9 +4,11 @@ import path from 'path'; import { ethers } from 'ethers'; import { TransactionBlock } from '@mysten/sui.js/transactions'; -const { utils: { arrayify } } = ethers; +const { + utils: { arrayify }, +} = ethers; -describe('relayer', () => { +describe.skip('relayer', () => { let client: SuiNetwork; let relayer: SuiRelayer; @@ -25,8 +27,10 @@ describe('relayer', () => { // Deploy a sample module const response = await client.deploy(path.join(__dirname, '../move/sample')); const packageId = response.packages[0].packageId; - const singleton: any = response.publishTxn.objectChanges?.find((change) => (change as any).objectType === `${packageId}::test::Singleton` ) - + const singleton: any = response.publishTxn.objectChanges?.find( + (change) => (change as any).objectType === `${packageId}::test::Singleton`, + ); + const msg = 'hello from sui'; const payload = ethers.utils.defaultAbiCoder.encode(['string'], [msg]); @@ -34,7 +38,12 @@ describe('relayer', () => { const tx = new TransactionBlock(); tx.moveCall({ target: `${packageId}::test::send_call`, - arguments: [tx.object(singleton.objectId), tx.pure('Avalanche'), tx.pure('0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789'), tx.pure(String.fromCharCode(...arrayify(payload)))], + arguments: [ + tx.object(singleton.objectId), + tx.pure('Avalanche'), + tx.pure('0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789'), + tx.pure(String.fromCharCode(...arrayify(payload))), + ], }); await client.execute(tx); diff --git a/packages/axelar-local-dev-sui/package.json b/packages/axelar-local-dev-sui/package.json index 54d810e9..2887e724 100644 --- a/packages/axelar-local-dev-sui/package.json +++ b/packages/axelar-local-dev-sui/package.json @@ -1,6 +1,6 @@ { "name": "@axelar-network/axelar-local-dev-sui", - "version": "2.2.0-alpha.28", + "version": "2.2.0", "main": "dist/index.js", "files": [ "dist/", @@ -18,7 +18,7 @@ }, "dependencies": { "@axelar-network/axelar-cgp-sui": "https://github.com/axelarnetwork/axelar-cgp-sui.git", - "@axelar-network/axelar-local-dev": "2.1.8", + "@axelar-network/axelar-local-dev": "2.2.0", "@mysten/sui.js": "^0.41.0" }, "author": "euro@axelar.network", diff --git a/packages/axelar-local-dev/package.json b/packages/axelar-local-dev/package.json index 17576ab9..34a74916 100644 --- a/packages/axelar-local-dev/package.json +++ b/packages/axelar-local-dev/package.json @@ -1,6 +1,6 @@ { "name": "@axelar-network/axelar-local-dev", - "version": "2.2.0-alpha.28", + "version": "2.2.0", "description": "", "main": "dist/index.js", "files": [ diff --git a/packages/axelar-local-dev/src/Network.ts b/packages/axelar-local-dev/src/Network.ts index 39a8980b..6bcf30e2 100644 --- a/packages/axelar-local-dev/src/Network.ts +++ b/packages/axelar-local-dev/src/Network.ts @@ -67,8 +67,9 @@ export interface NetworkInfo { export interface NetworkSetup { name?: string; chainId?: number; + seed?: string; userKeys?: Wallet[]; - ownerKey: Wallet; + ownerKey?: Wallet; operatorKey?: Wallet; relayerKey?: Wallet; adminKeys?: Wallet[]; @@ -208,7 +209,7 @@ export class Network { } async deployCreate3Deployer(): Promise { - logger.log(`Deploying the ConstAddressDeployer for ${this.name}... `); + logger.log(`Deploying the Create3Deployer for ${this.name}... `); const create3DeployerPrivateKey = keccak256(toUtf8Bytes('const-address-deployer-deployer')); const deployerWallet = new Wallet(create3DeployerPrivateKey, this.provider); await this.ownerWallet @@ -220,7 +221,7 @@ export class Network { const create3Deployer = await deployContract(deployerWallet, Create3Deployer, []); this.create3Deployer = new Contract(create3Deployer.address, Create3Deployer.abi, this.provider); - logger.log(`Deployed at ${this.constAddressDeployer.address}`); + logger.log(`Deployed at ${this.create3Deployer.address}`); return this.create3Deployer; } diff --git a/packages/axelar-local-dev/src/__tests__/export.spec.ts b/packages/axelar-local-dev/src/__tests__/export.spec.ts index 8ef67c0b..c5e42f4f 100644 --- a/packages/axelar-local-dev/src/__tests__/export.spec.ts +++ b/packages/axelar-local-dev/src/__tests__/export.spec.ts @@ -5,7 +5,7 @@ import chai from 'chai'; import { Wallet } from 'ethers'; import fs from 'fs'; const { expect } = chai; -import { deployContract, setLogger, createAndExport, destroyExported, Network, networks } from '../'; +import { deployContract, setLogger, createAndExport, destroyExported, Network, networks, setupAndExport } from '../'; import { AxelarExpressExecutable__factory as AxelarExpressExecutableFactory } from '../types/factories/@axelar-network/axelar-gmp-sdk-solidity/contracts/express/AxelarExpressExecutable__factory'; import { ExpressWithToken__factory as ExpressWithTokenFactory } from '../types/factories/src/contracts/test/ExpressWithToken__factory'; import { ExecutableWithToken__factory as ExecuteWithTokenFactory } from '../types/factories/src/contracts/test/ExecutableWithToken__factory'; @@ -19,101 +19,168 @@ async function deployAndFundUsdc(chain: Network) { await chain.deployToken('Axelar Wrapped aUSDC', 'aUSDC', 6, BigInt(1e22)); } -describe('createAndExport', () => { - const wallet = Wallet.createRandom(); - const chains = ['A', 'B']; - const outputPath = './local.json'; - const evmRelayer = new EvmRelayer(); - let chain1: Network; - let chain2: Network; - let srcOwner: Wallet; - let destOwner: Wallet; - - beforeEach(async () => { - await createAndExport({ - accountsToFund: [wallet.address], - chainOutputPath: outputPath, - callback: (chain: Network) => deployAndFundUsdc(chain), - relayers: { evm: evmRelayer }, - chains, - relayInterval: 500, +describe('export', () => { + describe('createAndExport', () => { + const wallet = Wallet.createRandom(); + const chains = ['A', 'B']; + const outputPath = './local.json'; + const evmRelayer = new EvmRelayer(); + let chain1: Network; + let chain2: Network; + let srcOwner: Wallet; + let destOwner: Wallet; + + beforeEach(async () => { + await createAndExport({ + accountsToFund: [wallet.address], + chainOutputPath: outputPath, + callback: (chain: Network) => deployAndFundUsdc(chain), + relayers: { evm: evmRelayer }, + chains, + port: 18500, + relayInterval: 500, + }); + + chain1 = networks[0]; + chain2 = networks[1]; + srcOwner = networks[0].ownerWallet; + destOwner = networks[1].ownerWallet; }); - chain1 = networks[0]; - chain2 = networks[1]; - srcOwner = networks[0].ownerWallet; - destOwner = networks[1].ownerWallet; - }); + afterEach(async () => { + await destroyExported({ evm: evmRelayer }); + }); - afterEach(async () => { - await destroyExported({ evm: evmRelayer }); - }); + it('should export a local.json file correctly', async () => { + const data = fs.readFileSync(outputPath, 'utf8'); + const chainJson = JSON.parse(data); + // read file and convert to json object + expect(chainJson.length).to.equal(2); + expect(chainJson[0].name).to.equal(chains[0]); + expect(chainJson[1].name).to.equal(chains[1]); + + for (const chain of chainJson) { + expect(chain.gateway).to.not.be.undefined; + expect(chain.gasService).to.not.be.undefined; + expect(chain.constAddressDeployer).to.not.be.undefined; + expect(chain.create3Deployer).to.not.be.undefined; + expect(chain.rpc).to.be.not.undefined; + expect(chain.tokens.aUSDC).to.not.be.undefined; + } + }); - it('should export a local.json file correctly', async () => { - const data = fs.readFileSync(outputPath, 'utf8'); - const chainJson = JSON.parse(data); - // read file and convert to json object - expect(chainJson.length).to.equal(2); - expect(chainJson[0].name).to.equal(chains[0]); - expect(chainJson[1].name).to.equal(chains[1]); - - for (const chain of chainJson) { - expect(chain.gateway).to.not.be.undefined; - expect(chain.gasService).to.not.be.undefined; - expect(chain.constAddressDeployer).to.not.be.undefined; - expect(chain.create3Deployer).to.not.be.undefined; - expect(chain.rpc).to.be.not.undefined; - expect(chain.tokens.aUSDC).to.not.be.undefined; - } - }); + it('should be able to relay tokens from chain A to chain B', async () => { + const contract1 = await deployContract(srcOwner, ExecuteWithToken, [chain1.gateway.address, chain1.gasService.address]).then( + (contract) => ExecuteWithTokenFactory.connect(contract.address, srcOwner) + ); - it('should be able to relay tokens from chain A to chain B', async () => { - const contract1 = await deployContract(srcOwner, ExecuteWithToken, [chain1.gateway.address, chain1.gasService.address]).then( - (contract) => ExecuteWithTokenFactory.connect(contract.address, srcOwner) - ); + const contract2 = await deployContract(destOwner, ExecuteWithToken, [chain2.gateway.address, chain2.gasService.address]).then( + (contract) => ExecuteWithTokenFactory.connect(contract.address, destOwner) + ); - const contract2 = await deployContract(destOwner, ExecuteWithToken, [chain2.gateway.address, chain2.gasService.address]).then( - (contract) => ExecuteWithTokenFactory.connect(contract.address, destOwner) - ); + await contract1.addSibling(chain2.name, contract2.address); - await contract1.addSibling(chain2.name, contract2.address); + const amount = BigInt(1e18); + await chain1.giveToken(srcOwner.address, 'aUSDC', amount); + + const token1 = await chain1.getTokenContract('aUSDC'); + await token1.connect(srcOwner).approve(contract1.address, amount); + + // print eth balance of owner + await contract1.setAndSend(chain2.name, 'hello', wallet.address, 'aUSDC', amount, { value: BigInt(1e12) }); + + await new Promise((resolve) => setTimeout(resolve, 3000)); + + const token2 = await chain2.getTokenContract('aUSDC'); + const balance = await token2.balanceOf(wallet.address); + expect(balance.toBigInt()).to.equal(amount); + expect(await contract2.value()).to.equal('hello'); + }); - const amount = BigInt(1e18); - await chain1.giveToken(srcOwner.address, 'aUSDC', amount); + it('should be able to call express tokens from chain A to chain B', async () => { + const contract1 = await deployContract(srcOwner, ExpressWithToken, [chain1.gateway.address, chain1.gasService.address]).then( + (contract) => ExpressWithTokenFactory.connect(contract.address, srcOwner) + ); + const contract2 = await deployContract(destOwner, ExpressWithToken, [chain2.gateway.address, chain2.gasService.address]).then( + (contract) => AxelarExpressExecutableFactory.connect(contract.address, destOwner) + ); - const token1 = await chain1.getTokenContract('aUSDC'); - await token1.connect(srcOwner).approve(contract1.address, amount); + const amount = BigInt(1e18); + await chain1.giveToken(srcOwner.address, 'aUSDC', amount); + const token1 = (await chain1.getTokenContract('aUSDC')).connect(srcOwner); - // print eth balance of owner - await contract1.setAndSend(chain2.name, 'hello', wallet.address, 'aUSDC', amount, { value: BigInt(1e12) }); + await token1.approve(contract1.address, amount); + await contract1.sendToMany(chain2.name, contract2.address, [wallet.address], 'aUSDC', amount, { value: BigInt(1e17) }); - await new Promise((resolve) => setTimeout(resolve, 3000)); + await new Promise((resolve) => setTimeout(resolve, 3000)); - const token2 = await chain2.getTokenContract('aUSDC'); - const balance = await token2.balanceOf(wallet.address); - expect(balance.toBigInt()).to.equal(amount); - expect(await contract2.value()).to.equal('hello'); + const token2 = await chain2.getTokenContract('aUSDC'); + const balance = await token2.balanceOf(wallet.address); + expect(balance.toBigInt()).to.equal(amount); + }); }); - it('should be able to call express tokens from chain A to chain B', async () => { - const contract1 = await deployContract(srcOwner, ExpressWithToken, [chain1.gateway.address, chain1.gasService.address]).then( - (contract) => ExpressWithTokenFactory.connect(contract.address, srcOwner) - ); - const contract2 = await deployContract(destOwner, ExpressWithToken, [chain2.gateway.address, chain2.gasService.address]).then( - (contract) => AxelarExpressExecutableFactory.connect(contract.address, destOwner) - ); + describe('setupAndExport', () => { + const wallet = Wallet.createRandom(); + let chain1: Network; + let chain2: Network; + let srcOwner: Wallet; + let destOwner: Wallet; + + beforeEach(async () => { + const networks = (await setupAndExport({ + callback: (chain: Network) => deployAndFundUsdc(chain), + chains: [ + { + name: 'Ethereum', + rpcUrl: 'http://localhost:8545', + }, + { + name: 'Avalanche', + rpcUrl: 'http://localhost:8546', + }, + ], + })) as Network[]; + + if (!networks) throw Error('setupAndExport should return networks object'); + + chain1 = networks[0]; + chain2 = networks[1]; + srcOwner = networks[0].ownerWallet; + destOwner = networks[1].ownerWallet; + }); + + afterEach(async () => { + await destroyExported(); + }); + + // Note: This test is expecting the host to run a local blockchain on port 8545 and 8546. + it('should be able to relay tokens from chain A to chain B', async () => { + const contract1 = await deployContract(srcOwner, ExecuteWithToken, [chain1.gateway.address, chain1.gasService.address]).then( + (contract) => ExecuteWithTokenFactory.connect(contract.address, srcOwner) + ); - const amount = BigInt(1e18); - await chain1.giveToken(srcOwner.address, 'aUSDC', amount); - const token1 = (await chain1.getTokenContract('aUSDC')).connect(srcOwner); + const contract2 = await deployContract(destOwner, ExecuteWithToken, [chain2.gateway.address, chain2.gasService.address]).then( + (contract) => ExecuteWithTokenFactory.connect(contract.address, destOwner) + ); - await token1.approve(contract1.address, amount); - await contract1.sendToMany(chain2.name, contract2.address, [wallet.address], 'aUSDC', amount, { value: BigInt(1e17) }); + await contract1.addSibling(chain2.name, contract2.address); - await new Promise((resolve) => setTimeout(resolve, 3000)); + const amount = BigInt(1e18); + await chain1.giveToken(srcOwner.address, 'aUSDC', amount); - const token2 = await chain2.getTokenContract('aUSDC'); - const balance = await token2.balanceOf(wallet.address); - expect(balance.toBigInt()).to.equal(amount); + const token1 = await chain1.getTokenContract('aUSDC'); + await token1.connect(srcOwner).approve(contract1.address, amount); + + // print eth balance of owner + await contract1.setAndSend(chain2.name, 'hello', wallet.address, 'aUSDC', amount, { value: BigInt(1e12) }); + + await new Promise((resolve) => setTimeout(resolve, 3000)); + + const token2 = await chain2.getTokenContract('aUSDC'); + const balance = await token2.balanceOf(wallet.address); + expect(balance.toBigInt()).to.equal(amount); + expect(await contract2.value()).to.equal('hello'); + }); }); }); diff --git a/packages/axelar-local-dev/src/exportUtils.ts b/packages/axelar-local-dev/src/exportUtils.ts index 7d6a39a0..2154e57d 100644 --- a/packages/axelar-local-dev/src/exportUtils.ts +++ b/packages/axelar-local-dev/src/exportUtils.ts @@ -2,42 +2,17 @@ import { ethers } from 'ethers'; import { setJSON } from './utils'; -import { Network, NetworkOptions, networks } from './Network'; -import { RelayData, RelayerMap, relay } from './relay'; -import { createNetwork, forkNetwork, listen, stopAll } from './networkUtils'; +import { networks } from './Network'; +import { RelayerMap, relay } from './relay'; +import { createNetwork, forkNetwork, listen, setupNetwork, stopAll } from './networkUtils'; import { testnetInfo } from './info'; import { EvmRelayer } from './relay/EvmRelayer'; import { getChainArray } from '@axelar-network/axelar-chains-config'; import { registerRemoteITS } from './its'; +import { CloneLocalOptions, CreateLocalOptions, SetupLocalOptions } from './types'; let interval: any; -export interface CreateLocalOptions { - chainOutputPath?: string; - accountsToFund?: string[]; - fundAmount?: string; - chains?: string[]; - relayInterval?: number; - port?: number; - relayers?: RelayerMap; - afterRelay?: (relayData: RelayData) => void; - callback?: (network: Network, info: any) => Promise; -} - -export interface CloneLocalOptions { - chainOutputPath?: string; - accountsToFund?: string[]; - fundAmount?: string; - env?: string | any; - chains?: string[]; - relayInterval?: number; - port?: number; - networkOptions?: NetworkOptions; - relayers?: RelayerMap; - afterRelay?: (relayData: RelayData) => void; - callback?: (network: Network, info: any) => Promise; -} - const defaultEvmRelayer = new EvmRelayer(); let relaying = false; @@ -91,17 +66,19 @@ export async function createAndExport(options: CreateLocalOptions = {}) { interval = setInterval(async () => { if (relaying) return; relaying = true; - await relay(_options.relayers).catch(() => undefined); + await relay(_options.relayers).catch((e) => console.error(e)); if (options.afterRelay) { const evmRelayData = _options.relayers.evm?.relayData; const nearRelayData = _options.relayers.near?.relayData; const aptosRelayData = _options.relayers.aptos?.relayData; const suiRelayData = _options.relayers.sui?.relayData; + const multiversXRelayData = _options.relayers.multiversx?.relayData; evmRelayData && (await options.afterRelay(evmRelayData)); nearRelayData && (await options.afterRelay(nearRelayData)); aptosRelayData && (await options.afterRelay(aptosRelayData)); suiRelayData && (await options.afterRelay(suiRelayData)); + multiversXRelayData && (await options.afterRelay(multiversXRelayData)); } relaying = false; }, _options.relayInterval); @@ -112,6 +89,61 @@ export async function createAndExport(options: CreateLocalOptions = {}) { setJSON(localChains, _options.chainOutputPath); } +export async function setupAndExport(options: SetupLocalOptions) { + const { afterRelay, callback, chainOutputPath, chains, relayInterval, seed } = options; + + if (chains.length < 2) { + throw Error('At least 2 chains are required to setup and export'); + } + + const _options = { + chainOutputPath: chainOutputPath || './local.json', + chains, + afterRelay: afterRelay || null, + relayers: { evm: defaultEvmRelayer }, + callback: callback || null, + relayInterval: relayInterval || 2000, + }; + + const networkInfos = []; + const networks = []; + for (let i = 0; i < chains.length; i++) { + const network = await setupNetwork(chains[i].rpcUrl, { seed, name: chains[i].name }); + networks.push(network); + + const networkInfo = network.getInfo() as any; + networkInfo.rpc = chains[i].rpcUrl; + networkInfos.push(networkInfo); + + if (_options.callback) await _options.callback(network, networkInfo); + if (Object.keys(network.tokens).length > 0) { + // Check if there is a USDC token. + const alias = Object.keys(network.tokens).find((alias) => alias.toLowerCase().includes('usdc')); + + // If there is no USDC token, return. + if (!alias) return; + } + } + await registerRemoteITS(networks); + + interval = setInterval(async () => { + if (relaying) return; + relaying = true; + await relay(_options.relayers).catch(() => undefined); + if (options.afterRelay) { + const evmRelayData = _options.relayers.evm?.relayData; + evmRelayData && (await options.afterRelay(evmRelayData)); + } + relaying = false; + }, _options.relayInterval); + + const evmRelayer = _options.relayers['evm']; + evmRelayer?.subscribeExpressCall(); + + setJSON(networkInfos, _options.chainOutputPath); + return networks; +} + export async function forkAndExport(options: CloneLocalOptions = {}) { const _options = { chainOutputPath: options.chainOutputPath || './local.json', diff --git a/packages/axelar-local-dev/src/networkUtils.ts b/packages/axelar-local-dev/src/networkUtils.ts index 97576fb0..61e4bab7 100644 --- a/packages/axelar-local-dev/src/networkUtils.ts +++ b/packages/axelar-local-dev/src/networkUtils.ts @@ -179,20 +179,38 @@ export async function getAllNetworks(url: string) { return networks; } +function getDefaultLocalWallets() { + // This is a default seed for anvil that generates 10 wallets + const defaultSeed = 'test test test test test test test test test test test junk'; + + const wallets = []; + + for (let i = 0; i < 10; i++) { + wallets.push(Wallet.fromMnemonic(defaultSeed, `m/44'/60'/0'/0/${i}`)); + } + + return wallets; +} + /** * @returns {Network} */ export async function setupNetwork(urlOrProvider: string | providers.Provider, options: NetworkSetup) { const chain = new Network(); + chain.name = options.name != null ? options.name : `Chain ${networks.length + 1}`; chain.provider = typeof urlOrProvider === 'string' ? ethers.getDefaultProvider(urlOrProvider) : urlOrProvider; chain.chainId = (await chain.provider.getNetwork()).chainId; + const defaultWalelts = getDefaultLocalWallets(); + logger.log(`Setting up ${chain.name} on a network with a chainId of ${chain.chainId}...`); - if (options.userKeys == null) options.userKeys = []; - if (options.operatorKey == null) options.operatorKey = options.ownerKey; - if (options.relayerKey == null) options.relayerKey = options.ownerKey; - if (options.adminKeys == null) options.adminKeys = [options.ownerKey]; + if (options.userKeys == null) options.userKeys = options.userKeys || defaultWalelts.slice(5, 10); + if (options.relayerKey == null) options.relayerKey = options.ownerKey || defaultWalelts[2]; + if (options.operatorKey == null) options.operatorKey = options.ownerKey || defaultWalelts[3]; + if (options.adminKeys == null) options.adminKeys = options.ownerKey ? [options.ownerKey] : [defaultWalelts[4]]; + + options.ownerKey = options.ownerKey || defaultWalelts[0]; chain.userWallets = options.userKeys.map((x) => new Wallet(x, chain.provider)); chain.ownerWallet = new Wallet(options.ownerKey, chain.provider); @@ -209,7 +227,6 @@ export async function setupNetwork(urlOrProvider: string | providers.Provider, o await chain.deployGasReceiver(); await chain.deployInterchainTokenService(); chain.tokens = {}; - //chain.usdc = await chain.deployToken('Axelar Wrapped aUSDC', 'aUSDC', 6, BigInt(1e70)); networks.push(chain); return chain; } diff --git a/packages/axelar-local-dev/src/relay/EvmRelayer.ts b/packages/axelar-local-dev/src/relay/EvmRelayer.ts index 984ccd52..319cea58 100644 --- a/packages/axelar-local-dev/src/relay/EvmRelayer.ts +++ b/packages/axelar-local-dev/src/relay/EvmRelayer.ts @@ -20,6 +20,7 @@ interface EvmRelayerOptions { aptosRelayer?: Relayer; suiRelayer?: Relayer; wasmRelayer?: Relayer; + multiversXRelayer?: Relayer; } export class EvmRelayer extends Relayer { @@ -31,6 +32,7 @@ export class EvmRelayer extends Relayer { this.otherRelayers.aptos = options.aptosRelayer; this.otherRelayers.sui = options.suiRelayer; this.otherRelayers.wasm = options.wasmRelayer; + this.otherRelayers.multiversx = options.multiversXRelayer; } setRelayer(type: RelayerType, relayer: Relayer) { @@ -413,10 +415,12 @@ export class EvmRelayer extends Relayer { payloadHash: args.payloadHash, transactionHash, sourceEventIndex, - }; - this.relayData.callContract[commandId] = contractCallArgs; + }; + this.relayData.callContract[commandId] = contractCallArgs; let command; - if (args.destinationChain.toLowerCase() === 'aptos') { + if (args.destinationChain.toLowerCase() === 'multiversx') { + command = this.otherRelayers?.multiversx?.createCallContractCommand(commandId, this.relayData, contractCallArgs); + } else if (args.destinationChain.toLowerCase() === 'aptos') { command = this.otherRelayers?.aptos?.createCallContractCommand(commandId, this.relayData, contractCallArgs); } else if (args.destinationChain.toLowerCase() === 'near') { command = this.otherRelayers?.near?.createCallContractCommand(commandId, this.relayData, contractCallArgs); diff --git a/packages/axelar-local-dev/src/relay/Relayer.ts b/packages/axelar-local-dev/src/relay/Relayer.ts index 88e8457c..4a2cf225 100644 --- a/packages/axelar-local-dev/src/relay/Relayer.ts +++ b/packages/axelar-local-dev/src/relay/Relayer.ts @@ -1,4 +1,4 @@ -import { networks } from '../Network'; +import { networks, Network } from '../Network'; import { Command } from './Command'; import { CallContractArgs, CallContractWithTokenArgs, RelayCommand, RelayData } from './types'; @@ -8,6 +8,7 @@ export enum RelayerType { Aptos = 'aptos', Near = 'near', Wasm = 'wasm', + MultiversX = 'multiversx', } export type RelayerMap = Partial> & { [key: string]: Relayer | undefined }; @@ -36,14 +37,16 @@ export abstract class Relayer { abstract setRelayer(type: RelayerType, relayer: Relayer): void; - async relay() { - for (const to of networks) { + async relay(externalNetworks?: Network[]) { + const actualNetworks = externalNetworks || networks; + for (const to of actualNetworks) { this.commands[to.name] = []; } this.commands['aptos'] = []; this.commands['sui'] = []; this.commands['near'] = []; this.commands['wasm'] = []; + this.commands['multiversx'] = []; // Update all events at the source chains await this.updateEvents(); diff --git a/packages/axelar-local-dev/src/relay/index.ts b/packages/axelar-local-dev/src/relay/index.ts index 1ce65aa1..4db61d04 100644 --- a/packages/axelar-local-dev/src/relay/index.ts +++ b/packages/axelar-local-dev/src/relay/index.ts @@ -1,3 +1,4 @@ +import { Network } from '../Network'; import { EvmRelayer } from './EvmRelayer'; import { RelayerMap } from './Relayer'; @@ -8,13 +9,23 @@ export * from './EvmRelayer'; export const evmRelayer = new EvmRelayer(); -export const relay = async (relayers?: RelayerMap) => { +/** + * This function will be used to relay the messages between chains. It's called by exported functions in `exportUtils.ts`. + * @param relayers - The relayers to be used for relaying + * @param externalEvmNetworks - The external networks to be used for relaying. (EVM only) + */ +export const relay = async (relayers?: RelayerMap, externalEvmNetworks?: Network[]) => { if (!relayers) { relayers = { evm: evmRelayer }; } for (const relayerType in relayers) { const relayer = relayers[relayerType]; - await relayer?.relay(); + + if (relayerType === 'evm') { + await relayer?.relay(externalEvmNetworks); + } else { + await relayer?.relay(); + } } }; diff --git a/packages/axelar-local-dev/src/types.ts b/packages/axelar-local-dev/src/types.ts new file mode 100644 index 00000000..d68c6e04 --- /dev/null +++ b/packages/axelar-local-dev/src/types.ts @@ -0,0 +1,42 @@ +import { Network, NetworkOptions } from './Network'; +import { RelayData, RelayerMap } from './relay'; + +export interface CreateLocalOptions { + chainOutputPath?: string; + accountsToFund?: string[]; + fundAmount?: string; + chains?: string[]; + relayInterval?: number; + port?: number; + relayers?: RelayerMap; + afterRelay?: (relayData: RelayData) => void; + callback?: (network: Network, info: any) => Promise; +} + +export type SetupChain = { + rpcUrl: string; + name: string; +}; + +export interface SetupLocalOptions { + chains: SetupChain[]; + seed?: string; + relayInterval?: number; + afterRelay?: (relayData: RelayData) => void; + callback?: (network: Network, info: any) => Promise; + chainOutputPath?: string; +} + +export interface CloneLocalOptions { + chainOutputPath?: string; + accountsToFund?: string[]; + fundAmount?: string; + env?: string | any; + chains?: string[]; + relayInterval?: number; + port?: number; + networkOptions?: NetworkOptions; + relayers?: RelayerMap; + afterRelay?: (relayData: RelayData) => void; + callback?: (network: Network, info: any) => Promise; +}