diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 95316f05..ab8cb5b5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,13 +23,16 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - name: Cache node_modules - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: node_modules key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - name: Install dependencies run: npm ci + - name: Build + run: npm run build - name: Test + timeout-minutes: 15 run: | nohup sh -c "aptos node run-local-testnet --with-faucet" > nohup.out 2> nohup.err < /dev/null & sleep 10 diff --git a/package-lock.json b/package-lock.json index 0d705be8..df4041a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,20 +41,20 @@ } }, "node_modules/@axelar-network/axelar-cgp-solidity": { - "version": "4.5.0", - "resolved": "git+ssh://git@github.com/axelarnetwork/axelar-cgp-solidity.git#f6686932370517f012b7ed7dfe461fb04e7de96c", - "license": "ISC", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@axelar-network/axelar-cgp-solidity/-/axelar-cgp-solidity-5.0.0.tgz", + "integrity": "sha512-j/HshdS6MNZgEzqVgJq7gz9b1TgIwEr0QjmAUQMJWcGXk6EPp520+EWIFP+SomX3Zpu6p+Hg6+wf9giaLmrIQw==", "dependencies": { - "@axelar-network/axelar-gmp-sdk-solidity": "^3.0.0" + "@axelar-network/axelar-gmp-sdk-solidity": "^4.0.0" }, "engines": { "node": "^16.0.0 || ^18.0.0" } }, "node_modules/@axelar-network/axelar-gmp-sdk-solidity": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@axelar-network/axelar-gmp-sdk-solidity/-/axelar-gmp-sdk-solidity-3.5.0.tgz", - "integrity": "sha512-xKqcdLG1F2pxnSf37A2qOemfVy1vYts0rgI684WuUJz8GM70wWpeIUZYW1VlP8X1wB8okS5dSPsDWiuFPcYCFA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@axelar-network/axelar-gmp-sdk-solidity/-/axelar-gmp-sdk-solidity-4.0.2.tgz", + "integrity": "sha512-AiO6ez3A42Kh8PTtoo3R8gqoZzBKGFZRogTQBy0GXm7ZMAUdAbHcDPbQwBwjOw8+fxVNSpLTvXxsAR2FUq85GQ==", "engines": { "node": ">=16" } @@ -29406,8 +29406,8 @@ "version": "2.0.3", "license": "ISC", "dependencies": { - "@axelar-network/axelar-cgp-solidity": "github:axelarnetwork/axelar-cgp-solidity#f668693", - "@axelar-network/axelar-gmp-sdk-solidity": "3.5.0", + "@axelar-network/axelar-cgp-solidity": "^5.0.0", + "@axelar-network/axelar-gmp-sdk-solidity": "^4.0.2", "ethers": "^5.6.5", "fs-extra": "^10.1.0", "ganache": "^7.1.0", diff --git a/package.json b/package.json index 4076e87a..62eb0401 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,10 @@ "publish": "lerna publish from-package --yes", "version": "lerna version --yes --exact", "bootstrap": "lerna bootstrap", - "postinstall": "npm run bootstrap && lerna run build", + "postinstall": "lerna bootstrap", "link": "lerna link", "test": "lerna run test", - "update": "lernaupdate", + "update": "lerna update", "test:core": "lerna exec --scope=@axelar-network/axelar-local-dev npm run test", "test:near": "lerna exec --scope=@axelar-network/axelar-local-dev-near npm run test", "test:aptos": "lerna exec --scope=@axelar-network/axelar-local-dev-aptos npm run test", diff --git a/packages/axelar-local-dev-aptos/__tests__/aptos.spec.ts b/packages/axelar-local-dev-aptos/__tests__/aptos.spec.ts index 10c646b7..255bd985 100644 --- a/packages/axelar-local-dev-aptos/__tests__/aptos.spec.ts +++ b/packages/axelar-local-dev-aptos/__tests__/aptos.spec.ts @@ -11,7 +11,6 @@ const { keccak256, toUtf8Bytes } = ethers.utils; setLogger(() => undefined); describe('aptos', () => { - jest.setTimeout(60000); let client: AptosNetwork; let evmNetwork: Network; diff --git a/packages/axelar-local-dev-aptos/jest.config.js b/packages/axelar-local-dev-aptos/jest.config.js index 565effdb..4419682a 100644 --- a/packages/axelar-local-dev-aptos/jest.config.js +++ b/packages/axelar-local-dev-aptos/jest.config.js @@ -6,4 +6,5 @@ module.exports = { }, testRegex: '/__tests__/.*\\.(test|spec)?\\.(ts)$', transformIgnorePatterns: ['/node_modules/'], + testTimeout: 300000, }; diff --git a/packages/axelar-local-dev-near/jest.config.js b/packages/axelar-local-dev-near/jest.config.js index c17ba278..220b0abd 100644 --- a/packages/axelar-local-dev-near/jest.config.js +++ b/packages/axelar-local-dev-near/jest.config.js @@ -5,4 +5,5 @@ module.exports = { '^.+\\.ts?$': 'ts-jest', }, transformIgnorePatterns: ['/node_modules/'], + testTimeout: 300000, }; diff --git a/packages/axelar-local-dev-near/src/__tests__/near.spec.ts b/packages/axelar-local-dev-near/src/__tests__/near.spec.ts index 65cae19f..a4fc004c 100644 --- a/packages/axelar-local-dev-near/src/__tests__/near.spec.ts +++ b/packages/axelar-local-dev-near/src/__tests__/near.spec.ts @@ -6,8 +6,6 @@ import { createNearNetwork, NearNetwork } from '..'; import { Network, createNetwork, stopAll, deployContract, relay } from '@axelar-network/axelar-local-dev'; import { EvmRelayer } from '@axelar-network/axelar-local-dev/dist/relay/EvmRelayer'; -jest.setTimeout(120000); - describe('near', () => { let client: NearNetwork; diff --git a/packages/axelar-local-dev/jest.config.js b/packages/axelar-local-dev/jest.config.js index c17ba278..220b0abd 100644 --- a/packages/axelar-local-dev/jest.config.js +++ b/packages/axelar-local-dev/jest.config.js @@ -5,4 +5,5 @@ module.exports = { '^.+\\.ts?$': 'ts-jest', }, transformIgnorePatterns: ['/node_modules/'], + testTimeout: 300000, }; diff --git a/packages/axelar-local-dev/package.json b/packages/axelar-local-dev/package.json index f35982d0..f3059456 100644 --- a/packages/axelar-local-dev/package.json +++ b/packages/axelar-local-dev/package.json @@ -33,8 +33,8 @@ }, "homepage": "https://github.com/axelarnetwork/axelar-local-dev#readme", "dependencies": { - "@axelar-network/axelar-cgp-solidity": "github:axelarnetwork/axelar-cgp-solidity#f668693", - "@axelar-network/axelar-gmp-sdk-solidity": "3.5.0", + "@axelar-network/axelar-cgp-solidity": "^5.0.0", + "@axelar-network/axelar-gmp-sdk-solidity": "^4.0.2", "ethers": "^5.6.5", "fs-extra": "^10.1.0", "ganache": "^7.1.0", diff --git a/packages/axelar-local-dev/src/Network.ts b/packages/axelar-local-dev/src/Network.ts index 7b666319..ce56b0af 100644 --- a/packages/axelar-local-dev/src/Network.ts +++ b/packages/axelar-local-dev/src/Network.ts @@ -11,14 +11,14 @@ import { AxelarGasReceiverProxy, ConstAddressDeployer, Create3Deployer, - GMPExpressService, - GMPExpressProxyDeployer, } from './contracts'; import { AxelarGateway__factory as AxelarGatewayFactory } from './types/factories/@axelar-network/axelar-cgp-solidity/contracts/AxelarGateway__factory'; import { AxelarGateway } from './types/@axelar-network/axelar-cgp-solidity/contracts/AxelarGateway'; import { AxelarGasService__factory as AxelarGasServiceFactory } from './types/factories/@axelar-network/axelar-cgp-solidity/contracts/gas-service/AxelarGasService__factory'; import { AxelarGasService } from './types/@axelar-network/axelar-cgp-solidity/contracts/gas-service/AxelarGasService'; import http from 'http'; +import { EvmRelayer } from './relay/EvmRelayer'; +import { evmRelayer } from './relay'; const ADDRESS_ZERO = '0x0000000000000000000000000000000000000000'; const { defaultAbiCoder, arrayify, keccak256, toUtf8Bytes } = ethers.utils; @@ -43,10 +43,9 @@ export interface NetworkInfo { adminKeys: string[]; threshold: number; lastRelayedBlock: number; + lastExpressedBlock: number; gatewayAddress: string; gasReceiverAddress: string; - expressServiceAddress: string; - expressProxyDeployerAddress: string; constAddressDeployerAddress: string; create3DeployerAddress: string; tokens: { [key: string]: string }; @@ -61,6 +60,7 @@ export interface NetworkSetup { adminKeys?: Wallet[]; threshold?: number; lastRelayedBlock?: number; + lastExpressedBlock?: number; } /* @@ -77,12 +77,11 @@ export class Network { adminWallets: Wallet[]; threshold: number; lastRelayedBlock: number; + lastExpressedBlock: number; gateway: AxelarGateway; gasService: AxelarGasService; constAddressDeployer: Contract; create3Deployer: Contract; - expressService: Contract; - expressProxyDeployer: Contract; isRemote: boolean | undefined; url: string | undefined; ganacheProvider: any; @@ -100,12 +99,11 @@ export class Network { this.adminWallets = networkish.adminWallets; this.threshold = networkish.threshold; this.lastRelayedBlock = networkish.lastRelayedBlock; + this.lastExpressedBlock = networkish.lastExpressedBlock; this.gateway = networkish.gateway; this.gasService = networkish.gasService; this.constAddressDeployer = networkish.constAddressDeployer; this.create3Deployer = networkish.create3Deployer; - this.expressService = networkish.expressService; - this.expressProxyDeployer = networkish.expressProxyDeployer; this.isRemote = networkish.isRemote; this.url = networkish.url; this.tokens = networkish.tokens; @@ -203,20 +201,6 @@ export class Network { return this.create3Deployer; } - async deployExpressServiceContract(): Promise { - logger.log(`Deploying the Express Service Contract for ${this.name}... `); - const expressProxyDeployer = await deployContract(this.ownerWallet, GMPExpressProxyDeployer, [this.gateway.address]); - const expressService = await deployContract(this.ownerWallet, GMPExpressService, [ - this.gateway.address, - expressProxyDeployer.address, - this.ownerWallet.address, - ]); - this.expressService = new Contract(expressService.address, GMPExpressService.abi, this.provider); - this.expressProxyDeployer = new Contract(expressProxyDeployer.address, GMPExpressProxyDeployer.abi, this.provider); - logger.log(`Deployed ExpressService at ${expressService.address}`); - return expressService; - } - async deployToken(name: string, symbol: string, decimals: number, cap: bigint, address: string = ADDRESS_ZERO, alias: string = symbol) { logger.log(`Deploying ${name} for ${this.name}... `); const data = arrayify( @@ -252,6 +236,7 @@ export class Network { async giveToken(address: string, alias: string, amount: bigint) { const symbol = this.tokens[alias] || alias; + const data = arrayify( defaultAbiCoder.encode( ['uint256', 'bytes32[]', 'string[]', 'bytes[]'], @@ -279,10 +264,9 @@ export class Network { adminKeys: this.adminWallets.map((wallet) => wallet.privateKey), threshold: this.threshold, lastRelayedBlock: this.lastRelayedBlock, + lastExpressedBlock: this.lastExpressedBlock, gatewayAddress: this.gateway.address, gasReceiverAddress: this.gasService.address, - expressProxyDeployerAddress: this.expressProxyDeployer.address, - expressServiceAddress: this.expressService.address, constAddressDeployerAddress: this.constAddressDeployer.address, create3DeployerAddress: this.create3Deployer.address, tokens: this.tokens, @@ -299,14 +283,6 @@ export class Network { constAddressDeployer: this.constAddressDeployer.address, create3Deployer: this.create3Deployer.address, tokens: this.tokens, - GMPExpressService: { - expressOperator: this.ownerWallet.address, - salt: 'GMPExpressService', - address: this.expressService.address, - implementation: this.expressService.address, - deployer: this.ownerWallet.address, - proxyDeployer: this.expressProxyDeployer.address, - }, }; } } diff --git a/packages/axelar-local-dev/src/__tests__/export.spec.ts b/packages/axelar-local-dev/src/__tests__/export.spec.ts new file mode 100644 index 00000000..8ef67c0b --- /dev/null +++ b/packages/axelar-local-dev/src/__tests__/export.spec.ts @@ -0,0 +1,119 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +'use strict'; + +import chai from 'chai'; +import { Wallet } from 'ethers'; +import fs from 'fs'; +const { expect } = chai; +import { deployContract, setLogger, createAndExport, destroyExported, Network, networks } 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'; +import ExpressWithToken from '../artifacts/src/contracts/test/ExpressWithToken.sol/ExpressWithToken.json'; +import ExecuteWithToken from '../artifacts/src/contracts/test/ExecutableWithToken.sol/ExecutableWithToken.json'; +import { EvmRelayer } from '../relay/EvmRelayer'; + +setLogger(() => null); + +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, + }); + + chain1 = networks[0]; + chain2 = networks[1]; + srcOwner = networks[0].ownerWallet; + destOwner = networks[1].ownerWallet; + }); + + 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 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) + ); + + 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'); + }); + + 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 amount = BigInt(1e18); + await chain1.giveToken(srcOwner.address, 'aUSDC', amount); + const token1 = (await chain1.getTokenContract('aUSDC')).connect(srcOwner); + + 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)); + + const token2 = await chain2.getTokenContract('aUSDC'); + const balance = await token2.balanceOf(wallet.address); + expect(balance.toBigInt()).to.equal(amount); + }); +}); diff --git a/packages/axelar-local-dev/src/__tests__/forking.spec.ts b/packages/axelar-local-dev/src/__tests__/forking.spec.ts index ed79fb29..a50ef925 100644 --- a/packages/axelar-local-dev/src/__tests__/forking.spec.ts +++ b/packages/axelar-local-dev/src/__tests__/forking.spec.ts @@ -15,7 +15,6 @@ interface NetworkUsdc extends Network { } describe.skip("forking", () => { - jest.setTimeout(2000000); afterEach(async () => { await stopAll(); diff --git a/packages/axelar-local-dev/src/__tests__/network.spec.ts b/packages/axelar-local-dev/src/__tests__/network.spec.ts index c33ae491..cccef5c9 100644 --- a/packages/axelar-local-dev/src/__tests__/network.spec.ts +++ b/packages/axelar-local-dev/src/__tests__/network.spec.ts @@ -11,7 +11,6 @@ import chai from 'chai'; const { expect } = chai; setLogger(() => null); -jest.setTimeout(300000); function validateNetwork(network: Network) { // wallets @@ -26,8 +25,6 @@ function validateNetwork(network: Network) { expect(network.constAddressDeployer).to.not.be.undefined; expect(network.create3Deployer).to.not.be.undefined; expect(network.gateway).to.not.be.undefined; - expect(network.expressService).to.not.be.undefined; - expect(network.expressProxyDeployer).to.not.be.undefined; } describe('Network', () => { @@ -63,6 +60,7 @@ describe('Network', () => { network = await getNetwork(`http://localhost:${port}`); validateNetwork(network); }); + it('should deploy a network on a preexisting chain', async () => { const port = 8600; const accounts = defaultAccounts(20); diff --git a/packages/axelar-local-dev/src/__tests__/relay.spec.ts b/packages/axelar-local-dev/src/__tests__/relay.spec.ts index 7e2b5508..295661a3 100644 --- a/packages/axelar-local-dev/src/__tests__/relay.spec.ts +++ b/packages/axelar-local-dev/src/__tests__/relay.spec.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-var-requires */ 'use strict'; -import { createNetwork, relay, stopAll, listen, getFee, getDepositAddress, deployContract, setLogger } from '../'; +import { createNetwork, relay, stopAll, listen, getFee, getDepositAddress, deployContract, setLogger, evmRelayer } from '../'; import { defaultAbiCoder } from 'ethers/lib/utils'; -import { BigNumber, Contract, ContractFactory, ContractTransaction, Wallet, ethers } from 'ethers'; +import { BigNumber, Contract, ContractTransaction, Wallet, ethers } from 'ethers'; import { Network } from '../Network'; import ExpressWithToken from '../artifacts/src/contracts/test/ExpressWithToken.sol/ExpressWithToken.json'; import chai from 'chai'; @@ -14,7 +14,6 @@ setLogger(() => null); interface NetworkUsdc extends Network { usdc?: Contract; } -jest.setTimeout(200000); describe('relay', () => { let chain1: NetworkUsdc; @@ -29,9 +28,11 @@ describe('relay', () => { [user2] = chain2.userWallets; chain2.usdc = await chain2.deployToken('Axelar Wrapped USDC', 'aUSDC', 6, BigInt(1e18)).then((c: Contract) => c.connect(user2)); }); + afterEach(async () => { await stopAll(); }); + describe('deposit address', () => { it('should generate a deposit address', async () => { const depositAddress = await getDepositAddress(chain1, chain2, user2.address, 'aUSDC'); @@ -251,58 +252,47 @@ describe('relay', () => { }); describe('gmp express', () => { - const amount = 1e10; + const amount = 1e6; it('should be able to relay a valid gmp-express call', async () => { // source chain contract doesn't need to be proxy contract. it can be a normal contract - const contract1 = await deployContract(chain1.ownerWallet, ExpressWithToken, [ - chain1.gateway.address, - chain1.gasService.address, - ]); + const contract1 = await deployContract(user1, ExpressWithToken, [chain1.gateway.address, chain1.gasService.address]); // get bytecode with constructor data - const expressWithToken = await deployContract(chain2.ownerWallet, ExpressWithToken, [ - chain2.gateway.address, - chain2.gasService.address, - ]); - const salt = ethers.utils.id(Date.now().toString()); - - // deploy the express proxy + implementation contracts - await chain2.expressService - .connect(chain2.ownerWallet) - .deployExpressProxy(salt, expressWithToken.address, chain2.ownerWallet.address, '0x') - .then((tx: ContractTransaction) => tx.wait()); - - // get the proxy address - const proxyAddress = await chain2.expressService.deployedProxyAddress(salt, chain2.ownerWallet.address); + const contract2 = await deployContract(user2, ExpressWithToken, [chain2.gateway.address, chain2.gasService.address]); - // initialize the proxy contract with the implementation abi - const contract2 = new Contract(proxyAddress, ExpressWithToken.abi, chain2.ownerWallet); + // subscribe to the express call + await evmRelayer.subscribeExpressCall(); // mint usdc tokens to user1 await chain1.giveToken(user1.address, 'aUSDC', BigInt(amount)); - // mint usdc tokens to the destination express service contract - await chain2.giveToken(chain2.expressService.address, 'aUSDC', BigInt(amount)); - // approve the ExpressWithToken contract to spend our tokens await chain1.usdc?.approve(contract1.address, amount).then((tx: ContractTransaction) => tx.wait()); - // contract2 should be an express proxy contract - const isExpress = await chain2.expressService.isExpressProxy(contract2.address); - expect(isExpress).to.be.true; - // call the express contract await contract1 - .connect(user1) - .sendToMany(chain2.name, contract2.address, [user2.address], 'aUSDC', amount, { value: 1e15 }) + .sendToMany(chain2.name, contract2.address, [user2.address], 'aUSDC', amount, { value: ethers.utils.parseEther('1') }) .then((tx: ContractTransaction) => tx.wait()); - // relay the call - await relay(); + await new Promise((resolve) => setTimeout(resolve, 5000)); + await evmRelayer.unsubscribe(); // check that the call was successful - expect((await chain2.usdc?.balanceOf(user2.address))?.toNumber()).to.equal(amount); + const user2Balance = (await chain2.usdc?.balanceOf(user2.address))?.toNumber(); + expect(user2Balance).to.equal(amount); + + // check that relayer wallet balance + const balanceRelayerAfterExpressed = await chain2.usdc?.balanceOf(chain2.relayerWallet.address); + + // relay the usual gmp call + await relay(); + + // check that relayer should receive fund back. + const balanceRelayerAfterRelayed = await chain2.usdc?.balanceOf(chain2.relayerWallet.address); + + // check that relayer wallet balance should be increased by amount from the usual gmp call + expect(balanceRelayerAfterRelayed.sub(balanceRelayerAfterExpressed).toNumber()).to.be.eq(amount); }); }); }); diff --git a/packages/axelar-local-dev/src/__tests__/token.spec.ts b/packages/axelar-local-dev/src/__tests__/token.spec.ts index 5565d09d..5c879f34 100644 --- a/packages/axelar-local-dev/src/__tests__/token.spec.ts +++ b/packages/axelar-local-dev/src/__tests__/token.spec.ts @@ -9,7 +9,6 @@ const { expect } = chai; setLogger(() => undefined); -jest.setTimeout(300000); describe('token', () => { let chain: Network; diff --git a/packages/axelar-local-dev/src/contracts/GMP.sol b/packages/axelar-local-dev/src/contracts/GMP.sol index 3fec4fb8..72ffe7b8 100644 --- a/packages/axelar-local-dev/src/contracts/GMP.sol +++ b/packages/axelar-local-dev/src/contracts/GMP.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +pragma solidity ^0.8.0; // Axelar CGP SDK import { TokenDeployer } from '@axelar-network/axelar-cgp-solidity/contracts/TokenDeployer.sol'; @@ -12,10 +12,4 @@ import { AxelarGasServiceProxy } from '@axelar-network/axelar-cgp-solidity/contr // Axelar GMP SDK import { IAxelarExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarExecutable.sol'; -import { ExpressExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/express/ExpressExecutable.sol'; -import { ExpressRegistry } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/express/ExpressRegistry.sol'; -import { ExpressProxyDeployer } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/express/ExpressProxyDeployer.sol'; -import { ExpressService } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/express/ExpressService.sol'; -import { ExpressServiceProxy } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/express/ExpressServiceProxy.sol'; -// import { ExpressProxy } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/express/ExpressProxy.sol'; -// import { ExpressProxyDeployer } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/express/ExpressProxyDeployer.sol'; +import { AxelarExpressExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/express/AxelarExpressExecutable.sol'; diff --git a/packages/axelar-local-dev/src/contracts/index.ts b/packages/axelar-local-dev/src/contracts/index.ts index f0c153d1..96682334 100644 --- a/packages/axelar-local-dev/src/contracts/index.ts +++ b/packages/axelar-local-dev/src/contracts/index.ts @@ -6,9 +6,6 @@ import BurnableMintableCappedERC20 from '../artifacts/@axelar-network/axelar-cgp import Auth from '../artifacts/@axelar-network/axelar-cgp-solidity/contracts/auth/AxelarAuthWeighted.sol/AxelarAuthWeighted.json'; import AxelarGasReceiver from '../artifacts/@axelar-network/axelar-cgp-solidity/contracts/gas-service/AxelarGasService.sol/AxelarGasService.json'; import AxelarGasReceiverProxy from '../artifacts/@axelar-network/axelar-cgp-solidity/contracts/gas-service/AxelarGasServiceProxy.sol/AxelarGasServiceProxy.json'; -import GMPExpressService from '../artifacts/@axelar-network/axelar-gmp-sdk-solidity/contracts/express/ExpressService.sol/ExpressService.json'; -import GMPExpressServiceProxy from '../artifacts/@axelar-network/axelar-gmp-sdk-solidity/contracts/express/ExpressServiceProxy.sol/ExpressServiceProxy.json'; -import GMPExpressProxyDeployer from '../artifacts/@axelar-network/axelar-gmp-sdk-solidity/contracts/express/ExpressProxyDeployer.sol/ExpressProxyDeployer.json'; import IAxelarGasService from '../artifacts/@axelar-network/axelar-cgp-solidity/contracts/interfaces/IAxelarGasService.sol/IAxelarGasService.json'; import ConstAddressDeployer from '@axelar-network/axelar-gmp-sdk-solidity/dist/ConstAddressDeployer.json'; import Create3Deployer from '@axelar-network/axelar-gmp-sdk-solidity/dist/Create3Deployer.json'; @@ -23,11 +20,8 @@ export { Auth, AxelarGasReceiver, AxelarGasReceiverProxy, - GMPExpressService, - GMPExpressServiceProxy, ConstAddressDeployer, Create3Deployer, IAxelarGasService, IAxelarExecutable, - GMPExpressProxyDeployer, }; diff --git a/packages/axelar-local-dev/src/contracts/test/Executable.sol b/packages/axelar-local-dev/src/contracts/test/Executable.sol index e96c7140..829dd8b8 100644 --- a/packages/axelar-local-dev/src/contracts/test/Executable.sol +++ b/packages/axelar-local-dev/src/contracts/test/Executable.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +pragma solidity ^0.8.0; import { IAxelarGasService } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol'; import { AxelarExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol'; diff --git a/packages/axelar-local-dev/src/contracts/test/ExecutableWithToken.sol b/packages/axelar-local-dev/src/contracts/test/ExecutableWithToken.sol index 77c3a035..4959d866 100644 --- a/packages/axelar-local-dev/src/contracts/test/ExecutableWithToken.sol +++ b/packages/axelar-local-dev/src/contracts/test/ExecutableWithToken.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +pragma solidity ^0.8.0; import { IERC20 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol'; import { IAxelarGasService } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol'; diff --git a/packages/axelar-local-dev/src/contracts/test/ExpressWithToken.sol b/packages/axelar-local-dev/src/contracts/test/ExpressWithToken.sol index 6611811c..0e56f88b 100644 --- a/packages/axelar-local-dev/src/contracts/test/ExpressWithToken.sol +++ b/packages/axelar-local-dev/src/contracts/test/ExpressWithToken.sol @@ -1,17 +1,17 @@ //SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +pragma solidity ^0.8.0; import { AxelarExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol'; -import { ExpressExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/express/ExpressExecutable.sol'; +import { AxelarExpressExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/express/AxelarExpressExecutable.sol'; import { IAxelarGateway } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol'; import { IERC20 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol'; import { IAxelarGasService } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol'; import { Upgradable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/upgradable/Upgradable.sol'; -contract ExpressWithToken is ExpressExecutable { +contract ExpressWithToken is AxelarExpressExecutable { IAxelarGasService public immutable gasService; - constructor(address gateway_, address gasReceiver_) ExpressExecutable(gateway_) { + constructor(address gateway_, address gasReceiver_) AxelarExpressExecutable(gateway_) { gasService = IAxelarGasService(gasReceiver_); } @@ -56,10 +56,5 @@ contract ExpressWithToken is ExpressExecutable { } } - function _execute(string calldata, string calldata, bytes calldata payload) internal override{ - } - - function contractId() external pure returns (bytes32) { - return keccak256('distribution-proxy'); - } + function _execute(string calldata, string calldata, bytes calldata payload) internal override {} } diff --git a/packages/axelar-local-dev/src/exportUtils.ts b/packages/axelar-local-dev/src/exportUtils.ts index e1118d7a..beeba654 100644 --- a/packages/axelar-local-dev/src/exportUtils.ts +++ b/packages/axelar-local-dev/src/exportUtils.ts @@ -82,13 +82,8 @@ export async function createAndExport(options: CreateLocalOptions = {}) { // If there is no USDC token, return. if (!alias) return; - - // Get the symbol of the USDC token. - const symbol = chain.tokens[alias]; - - // Mint 1e12 USDC tokens to the GMPExpressService contract. - await chain.giveToken(info.GMPExpressService.address, symbol, BigInt(1e18)); } + i++; } listen(_options.port); @@ -107,6 +102,10 @@ export async function createAndExport(options: CreateLocalOptions = {}) { } relaying = false; }, _options.relayInterval); + + const evmRelayer = _options.relayers['evm']; + evmRelayer?.subscribeExpressCall(); + setJSON(localChains, _options.chainOutputPath); } @@ -165,6 +164,9 @@ export async function forkAndExport(options: CloneLocalOptions = {}) { if (_options.afterRelay) _options.afterRelay(evmRelayer.relayData); }, _options.relayInterval); + const evmRelayer = _options.relayers['evm']; + evmRelayer?.subscribeExpressCall(); + setJSON(chains_local, _options.chainOutputPath); } @@ -174,8 +176,12 @@ export async function destroyExported(relayers?: RelayerMap) { clearInterval(interval); } + await defaultEvmRelayer?.unsubscribe(); + if (!relayers) return; + await relayers['evm']?.unsubscribe(); + for (const relayerType in relayers) { const relayer = relayers[relayerType]; if (relayer) { diff --git a/packages/axelar-local-dev/src/networkUtils.ts b/packages/axelar-local-dev/src/networkUtils.ts index ee8085ff..cae195db 100644 --- a/packages/axelar-local-dev/src/networkUtils.ts +++ b/packages/axelar-local-dev/src/networkUtils.ts @@ -9,11 +9,12 @@ import { defaultAccounts, setJSON, httpGet, logger } from './utils'; import { Network, networks, NetworkOptions, NetworkInfo, NetworkSetup } from './Network'; import { AxelarGateway__factory as AxelarGatewayFactory } from './types/factories/@axelar-network/axelar-cgp-solidity/contracts/AxelarGateway__factory'; import { AxelarGasService__factory as AxelarGasServiceFactory } from './types/factories/@axelar-network/axelar-cgp-solidity/contracts/gas-service/AxelarGasService__factory'; -import { GMPExpressService, ConstAddressDeployer, GMPExpressProxyDeployer, Create3Deployer } from './contracts'; +import { ConstAddressDeployer, Create3Deployer } from './contracts'; +import { Server } from 'http'; const { keccak256, id, solidityPack, toUtf8Bytes } = ethers.utils; -let serverInstance: any; +let serverInstance: Server | undefined; export interface ChainCloneData { name: string; @@ -102,11 +103,11 @@ export async function createNetwork(options: NetworkOptions = {}) { chain.adminWallets = wallets.splice(4, 10); chain.threshold = 3; chain.lastRelayedBlock = await chain.provider.getBlockNumber(); + chain.lastExpressedBlock = chain.lastRelayedBlock; await chain.deployConstAddressDeployer(); await chain.deployCreate3Deployer(); await chain.deployGateway(); await chain.deployGasReceiver(); - await chain.deployExpressServiceContract(); chain.tokens = {}; //chain.usdc = await chain.deployToken('Axelar Wrapped aUSDC', 'aUSDC', 6, BigInt(1e70)); @@ -144,14 +145,13 @@ export async function getNetwork(urlOrProvider: string | providers.Provider, inf chain.adminWallets = info.adminKeys.map((x) => new Wallet(x, chain.provider)); chain.threshold = info.threshold; chain.lastRelayedBlock = info.lastRelayedBlock; + chain.lastExpressedBlock = info.lastExpressedBlock; chain.tokens = info.tokens; chain.constAddressDeployer = new Contract(info.constAddressDeployerAddress, ConstAddressDeployer.abi, chain.provider); chain.create3Deployer = new Contract(info.create3DeployerAddress, Create3Deployer.abi, chain.provider); chain.gateway = AxelarGatewayFactory.connect(info.gatewayAddress, chain.provider); chain.gasService = AxelarGasServiceFactory.connect(info.gasReceiverAddress, chain.provider); - chain.expressService = new Contract(info.expressServiceAddress, GMPExpressService.abi, chain.provider); - chain.expressProxyDeployer = new Contract(info.expressProxyDeployerAddress, GMPExpressProxyDeployer.abi, chain.provider); //chain.usdc = await chain.getTokenContract('aUSDC'); logger.log(`Its gateway is deployed at ${chain.gateway.address}.`); @@ -194,11 +194,11 @@ export async function setupNetwork(urlOrProvider: string | providers.Provider, o chain.adminWallets = options.adminKeys.map((x) => new Wallet(x, chain.provider)); chain.threshold = options.threshold != null ? options.threshold : 1; chain.lastRelayedBlock = await chain.provider.getBlockNumber(); + chain.lastExpressedBlock = chain.lastRelayedBlock; await chain.deployConstAddressDeployer(); await chain.deployCreate3Deployer(); await chain.deployGateway(); await chain.deployGasReceiver(); - await chain.deployExpressServiceContract(); chain.tokens = {}; //chain.usdc = await chain.deployToken('Axelar Wrapped aUSDC', 'aUSDC', 6, BigInt(1e70)); networks.push(chain); @@ -256,6 +256,7 @@ export async function forkNetwork(chainInfo: ChainCloneData, options: NetworkOpt chain.adminWallets = wallets.splice(4, 10); chain.threshold = 3; chain.lastRelayedBlock = await chain.provider.getBlockNumber(); + chain.lastExpressedBlock = chain.lastRelayedBlock; chain.constAddressDeployer = new Contract(chainInfo.constAddressDeployer, ConstAddressDeployer.abi, chain.provider); // Delete the line below and uncomment the line after when we deploy create3Deployer await chain.deployCreate3Deployer(); @@ -263,8 +264,7 @@ export async function forkNetwork(chainInfo: ChainCloneData, options: NetworkOpt chain.gateway = AxelarGatewayFactory.connect(chainInfo.gateway, chain.provider); await chain._upgradeGateway(oldAdminAddresses, oldThreshold); chain.gasService = AxelarGasServiceFactory.connect(chainInfo.AxelarGasService.address, chain.provider); - await chain.deployExpressServiceContract(); - + chain.tokens = { uusdc: chain.name === 'Ethereum' ? 'USDC' : 'axlUSDC', uausdc: 'aUSDC', @@ -284,8 +284,8 @@ export async function forkNetwork(chainInfo: ChainCloneData, options: NetworkOpt } export async function stop(network: string | Network) { - if (typeof network === 'string') network = networks.find((chain) => chain.name == network)!; - if (network.server != null) await network.server.close(); + if (typeof network === 'string') network = networks.find((chain) => chain.name === network)!; + if (network.server) await network.server.close(); networks.splice(networks.indexOf(network), 1); } @@ -295,7 +295,7 @@ export async function stopAll() { } if (serverInstance) { await serverInstance.close(); - serverInstance = null; + serverInstance = undefined; } } diff --git a/packages/axelar-local-dev/src/relay/EvmRelayer.ts b/packages/axelar-local-dev/src/relay/EvmRelayer.ts index abe14148..e819d1a6 100644 --- a/packages/axelar-local-dev/src/relay/EvmRelayer.ts +++ b/packages/axelar-local-dev/src/relay/EvmRelayer.ts @@ -1,6 +1,6 @@ import { Relayer, RelayerType } from './Relayer'; import { CallContractArgs, CallContractWithTokenArgs, RelayCommand, RelayData } from './types'; -import { ContractReceipt, ContractTransaction, ethers, Wallet } from 'ethers'; +import { ContractReceipt, ethers, Wallet } from 'ethers'; import { getEVMLogID, getRandomID, getSignedExecuteInput, logger } from '../utils'; import { Command } from './Command'; import { arrayify, defaultAbiCoder } from 'ethers/lib/utils'; @@ -11,6 +11,7 @@ import { ContractCallEventObject, ContractCallWithTokenEventObject, } from '../types/@axelar-network/axelar-cgp-solidity/contracts/AxelarGateway'; +import { AxelarExpressExecutable__factory as AxelarExpressExecutableFactory } from '../types/factories/@axelar-network/axelar-gmp-sdk-solidity/contracts/express/AxelarExpressExecutable__factory'; const AddressZero = ethers.constants.AddressZero; @@ -20,6 +21,8 @@ interface EvmRelayerOptions { } export class EvmRelayer extends Relayer { + eventSubscribers: ethers.Contract[] = []; + constructor(options: EvmRelayerOptions = {}) { super(); this.otherRelayers.near = options.nearRelayer; @@ -54,14 +57,48 @@ export class EvmRelayer extends Relayer { } } + override async subscribeExpressCall() { + for (const chain of networks) { + if (!this.commands[chain.name]) { + this.commands[chain.name] = []; + } + } + for (const chain of networks) { + const subscriber = chain.gasService.on( + 'NativeGasPaidForExpressCallWithToken', + async (_sourceAddress: string, destinationChain: string) => { + const blockNumber = await chain.provider.getBlockNumber(); + await this.updateExpressGasEvents(chain, blockNumber); + await this.updateCallContractWithTokensEvents(chain, blockNumber, chain.lastExpressedBlock + 1); + const destChain = networks.find((network) => network.name === destinationChain); + if (!destChain) return; + const commands = this.commands[destinationChain]; + if (!commands || commands?.length === 0) return; + await this.executeEvmExpress(destChain, commands).catch((e) => { + logger.log(e); + }); + chain.lastExpressedBlock = blockNumber; + } + ); + + this.eventSubscribers.push(subscriber); + } + } + + override unsubscribe() { + this.eventSubscribers.forEach((subscriber) => { + subscriber.removeAllListeners(); + }); + } + private async executeEvm(commandList: RelayCommand) { for (const to of networks) { const commands = commandList[to.name]; if (!commands || commands?.length === 0) continue; - await this.executeEvmExpress(to, commands); - const execution = await this.executeEvmGateway(to, commands); - await this.completeEvmExpress(to, commands, execution); - await this.executeEvmExecutable(to, commands, execution); + + await this.executeEvmGateway(to, commands); + + await this.executeEvmExecutable(to, commands); } } @@ -84,6 +121,10 @@ export class EvmRelayer extends Relayer { for (const command of commands) { if (command.post === null) continue; + const executed = await this.isExecuted(to, command); + // If the command has already been approved, skip it. + if (executed) continue; + const fromName = command.data[0]; const from = networks.find((network) => network.name === fromName); if (!from) continue; @@ -101,112 +142,59 @@ export class EvmRelayer extends Relayer { if (!payed) continue; - try { - const cost = getGasPrice(); - const blockLimit = Number((await to.provider.getBlock('latest')).gasLimit); - const gasLimit = BigInt(Math.min(blockLimit, payed.gasFeeAmount / cost)); - const { payload } = this.relayData.callContractWithToken[command.commandId]; - - await to.expressService - .connect(to.ownerWallet) - .callWithToken( - command.commandId || ethers.constants.HashZero, - fromName, - payed.sourceAddress, - payed.destinationAddress, - payload, - payed.symbol, - payed.amount, - { gasLimit } - ) - .then((tx: ContractTransaction) => tx.wait()); - } catch (e) { - logger.log(e); - } - } - } - - private async completeEvmExpress(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 fromName = command.data[0]; - const from = networks.find((network) => network.name === fromName); - if (!from) continue; - - const payed = this.expressContractCallWithTokenGasEvents.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; - const alias = this.getAliasFromSymbol(from.tokens, log.symbol); - if (to.tokens[alias] !== command.data[4]) return false; - if (!command.data[5].eq(log.amount)) return false; - return true; - }); + const cost = getGasPrice(); + const blockLimit = Number((await to.provider.getBlock('latest')).gasLimit); + const gasLimit = BigInt(Math.min(blockLimit, payed.gasFeeAmount / cost)); + const { payload } = this.relayData.callContractWithToken[command.commandId]; + + const expressExecutorContract = AxelarExpressExecutableFactory.connect(payed.destinationAddress, to.relayerWallet); + + const tokenAddress = await to.gateway.tokenAddresses(to.tokens[payed.symbol]); + const tokenContract = new ethers.Contract( + tokenAddress, + [ + 'function allowance(address,address) view returns (uint256)', + 'function approve(address,uint256)', + 'function balanceOf(address) view returns (uint256)', + ], + to.relayerWallet + ); - if (!payed) continue; - if (command.name === 'approveContractCallWithMint') { - const index = this.expressContractCallWithTokenGasEvents.indexOf(payed); - this.expressContractCallWithTokenGasEvents = this.expressContractCallWithTokenGasEvents.filter((_, i) => i !== index); + // fund relayer wallet with token + const balance = await tokenContract.balanceOf(to.relayerWallet.address); + if (balance.lt(payed.amount)) { + const fundAmount = ethers.BigNumber.from(1e10); + await to.giveToken( + to.relayerWallet.address, + payed.symbol, + fundAmount.gt(payed.amount) ? fundAmount.toBigInt() : payed.amount + ); } - const { sourceAddress, destinationContractAddress, payload, alias, amountIn } = - this.relayData.callContractWithToken[command.commandId]; + const allowance = await tokenContract.allowance(to.relayerWallet.address, expressExecutorContract.address); - try { - await to.expressService - .connect(to.ownerWallet) - .callWithToken(command.commandId, from.name, sourceAddress, destinationContractAddress, payload, alias, amountIn) - .then((tx: ContractTransaction) => tx.wait()); - } catch (e) { - logger.log(e); + // If the allowance is insufficient, approve the contract + if (allowance.lt(payed.amount)) { + await tokenContract.approve(expressExecutorContract.address, ethers.constants.MaxUint256).then((tx: any) => tx.wait()); } - } - } - - private isExecuted(execution: any, command: Command) { - return !execution.events.find((event: any) => { - return event.event === 'Executed' && event.args[0] === command.commandId; - }); - } - private findMatchedGasEvent(command: Command, from: Network, to: Network): any { - if (command.name === 'approveContractCall') { - return this.contractCallGasEvents.find((event) => { - if (event.sourceAddress.toLowerCase() !== command.data[1].toLowerCase()) return false; - if (event.destinationChain.toLowerCase() !== to.name.toLowerCase()) return false; - if (event.destinationAddress.toLowerCase() !== command.data[2].toLowerCase()) return false; - if (event.payloadHash.toLowerCase() !== command.data[3].toLowerCase()) return false; - return true; - }); - } else { - return this.contractCallWithTokenGasEvents.find((event) => { - if (event.sourceAddress.toLowerCase() !== command.data[1].toLowerCase()) return false; - if (event.destinationChain.toLowerCase() !== to.name.toLowerCase()) return false; - if (event.destinationAddress.toLowerCase() !== command.data[2].toLowerCase()) return false; - if (event.payloadHash.toLowerCase() !== command.data[3].toLowerCase()) return false; - const alias = this.getAliasFromSymbol(from.tokens, event.symbol); - if (to.tokens[alias] !== command.data[4]) return false; - if (!event.amount.eq(command.data[5])) return false; - return true; - }); + await expressExecutorContract + .expressExecuteWithToken(command.commandId, fromName, payed.sourceAddress, payload, payed.symbol, payed.amount, { + gasLimit, + }) + .then((tx) => tx.wait()) + .catch(() => undefined); } } - private async executeEvmExecutable(to: Network, commands: Command[], execution: any): Promise { + private async executeEvmExecutable(to: Network, commands: Command[]): Promise { for (const command of commands) { // If the command doesn't have post execution, skip it if (command.post === null) continue; - // If the command is already executed, skip it - if (this.isExecuted(execution, command)) continue; + // If the command has not approved yet, skip it + const executed = await this.isExecuted(to, command); + if (!executed) continue; // Find the network that the command is executed on const fromName = command.data[0]; @@ -216,16 +204,10 @@ export class EvmRelayer extends Relayer { if (!from) continue; // Find the gas event that matches the command - const gasPaidEvent = this.findMatchedGasEvent(command, from, to); + const { event: gasPaidEvent, gasEventIndex } = this.findMatchedGasEvent(command, from, to); // If the gas event is not found, skip it - if (!gasPaidEvent) continue; - - // Find the index of the gas event - const gasEventIndex = - command.name === 'approveContractCall' - ? this.contractCallGasEvents.indexOf(gasPaidEvent) - : this.contractCallWithTokenGasEvents.indexOf(gasPaidEvent); + if (!gasPaidEvent || gasEventIndex === -1) continue; try { const cost = getGasPrice(); @@ -233,18 +215,20 @@ export class EvmRelayer extends Relayer { // Get the block gas limit const blockGasLimit = await from.provider.getBlock('latest').then((block) => block.gasLimit); + const filterUnmatchedGasEvents = (_: any, index: number) => { + return index !== gasEventIndex; + }; if (command.name === 'approveContractCall') { - this.contractCallGasEvents = this.contractCallGasEvents.filter((_, index) => { - return index !== gasEventIndex; - }); + this.contractCallGasEvents = this.contractCallGasEvents.filter(filterUnmatchedGasEvents); } else { - this.contractCallWithTokenGasEvents = this.contractCallWithTokenGasEvents.filter((_, index) => { - return index !== gasEventIndex; - }); + this.contractCallWithTokenGasEvents = this.contractCallWithTokenGasEvents.filter(filterUnmatchedGasEvents); + this.expressContractCallWithTokenGasEvents = + this.expressContractCallWithTokenGasEvents.filter(filterUnmatchedGasEvents); } // Execute the command const paidGasLimit = gasPaidEvent.gasFeeAmount.div(cost); + const receipt: ContractReceipt = await command.post?.({ gasLimit: blockGasLimit.lt(paidGasLimit) ? blockGasLimit : paidGasLimit, }); @@ -298,12 +282,61 @@ export class EvmRelayer extends Relayer { } } } - } catch (e) { + } catch (e: any) { logger.log(e); } } } + private isExecuted(to: Network, command: Command) { + return to.gateway.isCommandExecuted(command.commandId); + } + + private findMatchedGasEvent(command: Command, from: Network, to: Network): any { + if (command.name === 'approveContractCall') { + const event = this.contractCallGasEvents.find((event) => { + if (event.sourceAddress.toLowerCase() !== command.data[1].toLowerCase()) return false; + if (event.destinationChain.toLowerCase() !== to.name.toLowerCase()) return false; + if (event.destinationAddress.toLowerCase() !== command.data[2].toLowerCase()) return false; + if (event.payloadHash.toLowerCase() !== command.data[3].toLowerCase()) return false; + return true; + }); + return { event, eventIndex: this.contractCallGasEvents.indexOf(event) }; + } else { + const gmpGasEvent = this.contractCallWithTokenGasEvents.find((event) => { + if (event.sourceAddress.toLowerCase() !== command.data[1].toLowerCase()) return false; + if (event.destinationChain.toLowerCase() !== to.name.toLowerCase()) return false; + if (event.destinationAddress.toLowerCase() !== command.data[2].toLowerCase()) return false; + if (event.payloadHash.toLowerCase() !== command.data[3].toLowerCase()) return false; + const alias = this.getAliasFromSymbol(from.tokens, event.symbol); + if (to.tokens[alias] !== command.data[4]) return false; + if (!event.amount.eq(command.data[5])) return false; + return true; + }); + + const expressGmpGasEvent = this.expressContractCallWithTokenGasEvents.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; + const alias = this.getAliasFromSymbol(from.tokens, log.symbol); + if (to.tokens[alias] !== command.data[4]) return false; + if (!command.data[5].eq(log.amount)) return false; + return true; + }); + + return gmpGasEvent + ? { + event: gmpGasEvent, + eventIndex: this.contractCallWithTokenGasEvents.indexOf(gmpGasEvent), + } + : { + event: expressGmpGasEvent, + eventIndex: this.expressContractCallWithTokenGasEvents.indexOf(expressGmpGasEvent), + }; + } + } + private async updateGasEvents(from: Network, blockNumber: number) { const gasPaidForContractCallFilter = from.gasService.filters.GasPaidForContractCall(); const gasPaidForContractCallLogs = ( @@ -343,12 +376,12 @@ export class EvmRelayer extends Relayer { private async updateExpressGasEvents(from: Network, blockNumber: number) { let filter = from.gasService.filters.GasPaidForExpressCallWithToken(); - let newGasLogs: any = (await from.gasService.queryFilter(filter, from.lastRelayedBlock + 1, blockNumber)).map((log) => log.args); + let newGasLogs: any = (await from.gasService.queryFilter(filter, from.lastExpressedBlock + 1, blockNumber)).map((log) => log.args); for (const gasLog of newGasLogs) { this.expressContractCallWithTokenGasEvents.push(gasLog); } filter = from.gasService.filters.NativeGasPaidForExpressCallWithToken(); - newGasLogs = (await from.gasService.queryFilter(filter, from.lastRelayedBlock + 1, blockNumber)).map((log) => { + newGasLogs = (await from.gasService.queryFilter(filter, from.lastExpressedBlock + 1, blockNumber)).map((log) => { return { ...log.args, gasToken: AddressZero }; }); for (const gasLog of newGasLogs) { @@ -439,11 +472,12 @@ export class EvmRelayer extends Relayer { } } - private async updateCallContractWithTokensEvents(from: Network, blockNumber: number) { + private async updateCallContractWithTokensEvents(from: Network, toBlock: number, fromBlock = from.lastRelayedBlock + 1) { const filter = from.gateway.filters.ContractCallWithToken(); - const logsFrom = await from.gateway.queryFilter(filter, from.lastRelayedBlock + 1, blockNumber); + const logsFrom = await from.gateway.queryFilter(filter, fromBlock, toBlock); for (const log of logsFrom) { const args: any = log.args; + if (!this.commands[args.destinationChain]) continue; const alias = this.getAliasFromSymbol(from.tokens, args.symbol); const amountOut = args.amount; const commandId = getEVMLogID(from.name, log); @@ -466,7 +500,7 @@ export class EvmRelayer extends Relayer { }; this.relayData.callContractWithToken[commandId] = callContractWithTokenArgs; - const command = Command.createEVMContractCallWithTokenCommand(commandId, this.relayData, callContractWithTokenArgs); + const command = this.createCallContractWithTokenCommand(commandId, this.relayData, callContractWithTokenArgs); this.commands[args.destinationChain].push(command); } } diff --git a/packages/axelar-local-dev/src/relay/Relayer.ts b/packages/axelar-local-dev/src/relay/Relayer.ts index 6ba89b8c..1e02f7e9 100644 --- a/packages/axelar-local-dev/src/relay/Relayer.ts +++ b/packages/axelar-local-dev/src/relay/Relayer.ts @@ -40,7 +40,13 @@ export abstract class Relayer { await this.updateEvents(); await this.execute(this.commands); + } + + async subscribeExpressCall() { + // this is a no-op by default + } - this.commands = {}; + unsubscribe() { + // this is a no-op by default } } diff --git a/packages/axelar-local-dev/src/utils.ts b/packages/axelar-local-dev/src/utils.ts index 01555e12..296e5c6d 100644 --- a/packages/axelar-local-dev/src/utils.ts +++ b/packages/axelar-local-dev/src/utils.ts @@ -23,7 +23,8 @@ export async function getSignedExecuteInput(data: any, wallet: Wallet) { export const getRandomID = () => id(getRandomInt(1e10).toString()); export const getEVMLogID = (chain: string, log: any) => { - return id(chain + ':' + log.blockNumber + ':' + log.transactionIndex + ':' + log.logIndex + ':' + new Date().getMilliseconds()); + const txData = [chain, log.transactionHash, log.logIndex].join(':'); + return id(txData); }; export const getNearLogID = (chain: string, event: any) => {