diff --git a/package-lock.json b/package-lock.json index 64ef2906..01917704 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23504,6 +23504,11 @@ "node": ">= 0.10" } }, + "node_modules/reconnecting-websocket": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz", + "integrity": "sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==" + }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -30484,6 +30489,7 @@ "@cosmjs/cosmwasm-stargate": "^0.31.1", "@cosmjs/stargate": "^0.31.1", "@types/sinon": "^10.0.20", + "reconnecting-websocket": "^4.4.0", "sinon": "^17.0.0" }, "devDependencies": { diff --git a/packages/axelar-local-dev-cosmos/.gitignore b/packages/axelar-local-dev-cosmos/.gitignore index aa554534..053920fe 100644 --- a/packages/axelar-local-dev-cosmos/.gitignore +++ b/packages/axelar-local-dev-cosmos/.gitignore @@ -3,3 +3,6 @@ # Docker docker/**/.* config.json +channel.json +connection.json +info/* diff --git a/packages/axelar-local-dev-cosmos/docker/axelar/bin/04-register-broadcaster.sh b/packages/axelar-local-dev-cosmos/docker/axelar/bin/04-register-broadcaster.sh deleted file mode 100755 index 7d20cebf..00000000 --- a/packages/axelar-local-dev-cosmos/docker/axelar/bin/04-register-broadcaster.sh +++ /dev/null @@ -1,18 +0,0 @@ - -#!/bin/sh - -CHAIN_ID=axelar -HOME=/root/private/.axelar -DEFAULT_KEYS_FLAGS="--keyring-backend test --home ${HOME}" - -echo "Registering broadcaster" -docker exec -it axelar /bin/sh -c "axelard tx snapshot register-proxy \$(axelard keys show owner -a ${DEFAULT_KEYS_FLAGS}) --generate-only \ ---chain-id ${CHAIN_ID} --from \$(axelard keys show governance -a ${DEFAULT_KEYS_FLAGS}) ${DEFAULT_KEYS_FLAGS} \ ---output json --gas 1000000 &> ${HOME}/unsigned_msg.json" -docker exec -t axelar /bin/sh -c "cat ${HOME}/unsigned_msg.json" -echo "Registered broadcaster" - -sh broadcast-unsigned-tx.sh - - - diff --git a/packages/axelar-local-dev-cosmos/docker/axelar/bin/init_axelar.sh b/packages/axelar-local-dev-cosmos/docker/axelar/bin/init_axelar.sh index e65665b8..8ac74d9b 100755 --- a/packages/axelar-local-dev-cosmos/docker/axelar/bin/init_axelar.sh +++ b/packages/axelar-local-dev-cosmos/docker/axelar/bin/init_axelar.sh @@ -30,6 +30,7 @@ sed -i '/\[api\]/,/\[/ s/swagger = false/swagger = true/' "$HOME"/config/app.tom # staking/governance token is hardcoded in config, change this sed -i "s/\"stake\"/\"$DENOM\"/" "$HOME"/config/genesis.json && echo "Updated staking token to $DENOM" + # Adding a new key named 'owner' with a test keyring-backend in the specified home directory # and storing the mnemonic in the mnemonic.txt file mnemonic=$(axelard keys add owner ${DEFAULT_KEYS_FLAGS} 2>&1 | tail -n 1) diff --git a/packages/axelar-local-dev-cosmos/docker/axelar/bin/activate-chain.sh b/packages/axelar-local-dev-cosmos/docker/axelar/bin/libs/activate-chain.sh similarity index 90% rename from packages/axelar-local-dev-cosmos/docker/axelar/bin/activate-chain.sh rename to packages/axelar-local-dev-cosmos/docker/axelar/bin/libs/activate-chain.sh index 6996c94f..6722f458 100755 --- a/packages/axelar-local-dev-cosmos/docker/axelar/bin/activate-chain.sh +++ b/packages/axelar-local-dev-cosmos/docker/axelar/bin/libs/activate-chain.sh @@ -4,6 +4,7 @@ CHAIN_ID=axelar HOME=/root/private/.axelar DEFAULT_KEYS_FLAGS="--keyring-backend test --home ${HOME}" CHAIN=$1 +DIR="$(dirname "$0")" if [ -z "$CHAIN" ] then @@ -18,4 +19,4 @@ docker exec -it axelar /bin/sh -c "axelard tx nexus activate-chain ${CHAIN} --ge echo "Activated chain ${CHAIN}" docker exec -t axelar /bin/sh -c "cat ${HOME}/unsigned_msg.json" -sh broadcast-unsigned-tx.sh +sh "$DIR/broadcast-unsigned-multi-tx.sh" diff --git a/packages/axelar-local-dev-cosmos/docker/axelar/bin/broadcast-unsigned-tx.sh b/packages/axelar-local-dev-cosmos/docker/axelar/bin/libs/broadcast-unsigned-multi-tx.sh similarity index 100% rename from packages/axelar-local-dev-cosmos/docker/axelar/bin/broadcast-unsigned-tx.sh rename to packages/axelar-local-dev-cosmos/docker/axelar/bin/libs/broadcast-unsigned-multi-tx.sh diff --git a/packages/axelar-local-dev-cosmos/docker/axelar/bin/libs/evm-rpc.toml b/packages/axelar-local-dev-cosmos/docker/axelar/bin/libs/evm-rpc.toml new file mode 100644 index 00000000..0ed30c4c --- /dev/null +++ b/packages/axelar-local-dev-cosmos/docker/axelar/bin/libs/evm-rpc.toml @@ -0,0 +1,24 @@ +[[axelar_bridge_evm]] +name = "Ethereum" +rpc_addr = "localhost:8500/0" +start-with-bridge = false + +[[axelar_bridge_evm]] +name = "Avalanche" +rpc_addr = "localhost:8500/1" +start-with-bridge = false + +[[axelar_bridge_evm]] +name = "Fantom" +rpc_addr = "localhost:8500/2" +start-with-bridge = false + +[[axelar_bridge_evm]] +name = "Moonbeam" +rpc_addr = "localhost:8500/3" +start-with-bridge = false + +[[axelar_bridge_evm]] +name = "Polygon" +rpc_addr = "localhost:8500/4" +start-with-bridge = false diff --git a/packages/axelar-local-dev-cosmos/docker/axelar/bin/params.json b/packages/axelar-local-dev-cosmos/docker/axelar/bin/libs/params.json similarity index 100% rename from packages/axelar-local-dev-cosmos/docker/axelar/bin/params.json rename to packages/axelar-local-dev-cosmos/docker/axelar/bin/libs/params.json diff --git a/packages/axelar-local-dev-cosmos/docker/axelar/bin/setup.sh b/packages/axelar-local-dev-cosmos/docker/axelar/bin/setup.sh new file mode 100755 index 00000000..44863d68 --- /dev/null +++ b/packages/axelar-local-dev-cosmos/docker/axelar/bin/setup.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +EVM_CHAIN=ethereum +COSMOS_CHAIN=wasm + +# 1. Add EVM chain +sh ./steps/01-add-chain.sh ${EVM_CHAIN} + +# # 2. Add cosmos-based chain +sh ./steps/02-add-cosmos-chain.sh ${COSMOS_CHAIN} + +# # 3. Register Broadcaster Account and Maintainer +# sh ./steps/04-register-broadcaster.sh diff --git a/packages/axelar-local-dev-cosmos/docker/axelar/bin/01-add-chain.sh b/packages/axelar-local-dev-cosmos/docker/axelar/bin/steps/01-add-chain.sh similarity index 74% rename from packages/axelar-local-dev-cosmos/docker/axelar/bin/01-add-chain.sh rename to packages/axelar-local-dev-cosmos/docker/axelar/bin/steps/01-add-chain.sh index 1cf913e0..51115fbb 100755 --- a/packages/axelar-local-dev-cosmos/docker/axelar/bin/01-add-chain.sh +++ b/packages/axelar-local-dev-cosmos/docker/axelar/bin/steps/01-add-chain.sh @@ -4,6 +4,7 @@ CHAIN_ID=axelar HOME=/root/private/.axelar DEFAULT_KEYS_FLAGS="--keyring-backend test --home ${HOME}" CHAIN=$1 +DIR="$(dirname "$0")" if [ -z "$CHAIN" ] then @@ -11,10 +12,12 @@ then exit 1 fi -docker exec -it axelar /bin/sh -c "axelard tx evm add-chain ${CHAIN} /root/private/bin/params.json --generate-only \ +docker exec -it axelar /bin/sh -c "axelard tx evm add-chain ${CHAIN} /root/private/bin/libs/params.json --generate-only \ --chain-id ${CHAIN_ID} --from \$(axelard keys show governance -a ${DEFAULT_KEYS_FLAGS}) --home ${HOME} \ --output json --gas 500000 &> ${HOME}/unsigned_msg.json" echo "Added evm chain" docker exec -t axelar /bin/sh -c "cat ${HOME}/unsigned_msg.json" -sh broadcast-unsigned-tx.sh +sh "$DIR/../libs/broadcast-unsigned-multi-tx.sh" + +sh "$DIR/../libs/activate-chain.sh" ${CHAIN} diff --git a/packages/axelar-local-dev-cosmos/docker/axelar/bin/02-add-cosmos-chain.sh b/packages/axelar-local-dev-cosmos/docker/axelar/bin/steps/02-add-cosmos-chain.sh similarity index 85% rename from packages/axelar-local-dev-cosmos/docker/axelar/bin/02-add-cosmos-chain.sh rename to packages/axelar-local-dev-cosmos/docker/axelar/bin/steps/02-add-cosmos-chain.sh index 3c58e097..62d6957e 100755 --- a/packages/axelar-local-dev-cosmos/docker/axelar/bin/02-add-cosmos-chain.sh +++ b/packages/axelar-local-dev-cosmos/docker/axelar/bin/steps/02-add-cosmos-chain.sh @@ -5,6 +5,7 @@ HOME=/root/private/.axelar DEFAULT_KEYS_FLAGS="--keyring-backend test --home ${HOME}" CHAIN=$1 CHANNEL_ID=${2:-channel-0} +DIR="$(dirname "$0")" if [ -z "$CHAIN" ] then @@ -21,4 +22,6 @@ docker exec -it axelar /bin/sh -c "axelard tx axelarnet add-cosmos-based-chain $ echo "Added cosmos-based chain" docker exec -t axelar /bin/sh -c "cat ${HOME}/unsigned_msg.json" -sh broadcast-unsigned-tx.sh +sh "$DIR/../libs/broadcast-unsigned-multi-tx.sh" + +sh "$DIR/../libs/activate-chain.sh" ${CHAIN} diff --git a/packages/axelar-local-dev-cosmos/docker/axelar/bin/03-register-asset.sh b/packages/axelar-local-dev-cosmos/docker/axelar/bin/steps/03-register-asset.sh similarity index 90% rename from packages/axelar-local-dev-cosmos/docker/axelar/bin/03-register-asset.sh rename to packages/axelar-local-dev-cosmos/docker/axelar/bin/steps/03-register-asset.sh index fef713a7..70e6a4f4 100755 --- a/packages/axelar-local-dev-cosmos/docker/axelar/bin/03-register-asset.sh +++ b/packages/axelar-local-dev-cosmos/docker/axelar/bin/steps/03-register-asset.sh @@ -5,6 +5,7 @@ HOME=/root/private/.axelar DEFAULT_KEYS_FLAGS="--keyring-backend test --home ${HOME}" CHAIN=$1 DENOM=${2:-uwasm} +DIR="$(dirname "$0")" if [ -z "$CHAIN" ] then @@ -19,4 +20,4 @@ docker exec -it axelar /bin/sh -c "axelard tx axelarnet register-asset ${CHAIN} docker exec -t axelar /bin/sh -c "cat ${HOME}/unsigned_msg.json" echo "Registered asset ${CHAIN} ${DENOM}" -sh broadcast-unsigned-tx.sh +sh "$DIR/../libs/broadcast-unsigned-multi-tx.sh" diff --git a/packages/axelar-local-dev-cosmos/docker/axelar/bin/steps/04-register-broadcaster.sh b/packages/axelar-local-dev-cosmos/docker/axelar/bin/steps/04-register-broadcaster.sh new file mode 100755 index 00000000..510ac900 --- /dev/null +++ b/packages/axelar-local-dev-cosmos/docker/axelar/bin/steps/04-register-broadcaster.sh @@ -0,0 +1,30 @@ + +#!/bin/sh + +CHAIN_ID=axelar +HOME=/root/private/.axelar +DEFAULT_KEYS_FLAGS="--keyring-backend test --home ${HOME}" +DIR="$(dirname "$0")" + +# 1. Register broadcaster +echo "Registering broadcaster" +docker exec -it axelar /bin/sh -c "axelard tx snapshot register-proxy \$(axelard keys show gov1 -a ${DEFAULT_KEYS_FLAGS}) \ +--chain-id ${CHAIN_ID} --from owner ${DEFAULT_KEYS_FLAGS} \ +--output json --gas 1000000" +echo "Registered broadcaster" + +# Read the content of the local file and append it to the file inside the Docker container +docker exec -t axelar /bin/sh -c "cat /root/private/bin/libs/evm-rpc.toml >> "$HOME"/config/config.toml" +echo "Added evm-rpc.toml to config.toml" + +# 2. Register broadcaster as a maintainerf +echo "Registering maintainer" +docker exec -it axelar /bin/sh -c "axelard tx nexus register-chain-maintainer avalanche ethereum fantom moonbeam polygon \ +--chain-id ${CHAIN_ID} --from gov1 ${DEFAULT_KEYS_FLAGS} \ +--output json --gas 1000000" + +echo "Registered maintainer" + + + + diff --git a/packages/axelar-local-dev-cosmos/package.json b/packages/axelar-local-dev-cosmos/package.json index ce96d9b2..9803e750 100644 --- a/packages/axelar-local-dev-cosmos/package.json +++ b/packages/axelar-local-dev-cosmos/package.json @@ -14,7 +14,7 @@ "prettier": "prettier --write 'src/**/*.ts'", "build": "npm run clean && npm run build-ts", "build-ts": "tsc", - "start": "ts-node scripts/start.ts", + "start": "./scripts/clean.sh && ts-node scripts/start.ts", "stop": "ts-node scripts/stop.ts" }, "dependencies": { @@ -23,6 +23,7 @@ "@cosmjs/cosmwasm-stargate": "^0.31.1", "@cosmjs/stargate": "^0.31.1", "@types/sinon": "^10.0.20", + "reconnecting-websocket": "^4.4.0", "sinon": "^17.0.0" }, "author": "euro@axelar.network", diff --git a/packages/axelar-local-dev-cosmos/scripts/clean.sh b/packages/axelar-local-dev-cosmos/scripts/clean.sh new file mode 100755 index 00000000..be95ff08 --- /dev/null +++ b/packages/axelar-local-dev-cosmos/scripts/clean.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Get the directory of the current script +DIR="$(dirname "$0")" + +# Remove the directory relative to the script's location +rm -rf "$DIR/../info" diff --git a/packages/axelar-local-dev-cosmos/scripts/start.ts b/packages/axelar-local-dev-cosmos/scripts/start.ts index bceac859..f3c2225f 100644 --- a/packages/axelar-local-dev-cosmos/scripts/start.ts +++ b/packages/axelar-local-dev-cosmos/scripts/start.ts @@ -1,3 +1,6 @@ import { startAll } from "../src/docker"; +const path = require("path"); +import childProcess from "child_process"; +// clean up info json startAll(); diff --git a/packages/axelar-local-dev-cosmos/src/__tests__/ibc-relayer-client.spec.ts b/packages/axelar-local-dev-cosmos/src/__tests__/ibc-relayer-client.spec.ts index 5de64167..d6313dbc 100644 --- a/packages/axelar-local-dev-cosmos/src/__tests__/ibc-relayer-client.spec.ts +++ b/packages/axelar-local-dev-cosmos/src/__tests__/ibc-relayer-client.spec.ts @@ -1,4 +1,5 @@ import { IBCRelayerClient } from "../clients/IBCRelayerClient"; +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; describe("IBCRelayerClient", () => { it.skip("should be able to create a connection and channel", async () => { @@ -15,4 +16,20 @@ describe("IBCRelayerClient", () => { expect(response2!.src).toBeDefined(); expect(response2!.dest).toBeDefined(); }); + + it("should create a wallet from a mnemonic if provided", async () => { + const mockMnemonic = await DirectSecp256k1HdWallet.generate(12).then( + (w) => w.mnemonic + ); + const result = await IBCRelayerClient.create(mockMnemonic); + + expect(result.relayerAccount.mnemonic).toEqual(mockMnemonic); + }); + + it("should generate a new wallet if no mnemonic is provided", async () => { + const result = await IBCRelayerClient.create(); + + expect(result).toBeDefined(); + expect(result.relayerAccount.mnemonic).toBeDefined(); + }); }); diff --git a/packages/axelar-local-dev-cosmos/src/__tests__/listener.e2e.ts b/packages/axelar-local-dev-cosmos/src/__tests__/listener.e2e.ts index b8f30639..b09d64df 100644 --- a/packages/axelar-local-dev-cosmos/src/__tests__/listener.e2e.ts +++ b/packages/axelar-local-dev-cosmos/src/__tests__/listener.e2e.ts @@ -19,6 +19,53 @@ describe("E2E - Listener", () => { "illness step primary sibling donkey body sphere pigeon inject antique head educate"; async function executeContractCall() { + // Upload the wasm contract + const _path = path.resolve(__dirname, "../..", "wasm/send_receive.wasm"); + const response = await wasmClient.uploadWasm(_path).catch((err) => { + console.log(err); + + throw err; + }); + console.log(response); + console.log("Uploaded wasm:", response.codeId); + + // Instantiate the contract + const { client } = wasmClient; + const ownerAddress = await wasmClient.getOwnerAccount(); + const { contractAddress } = await client.instantiate( + ownerAddress, + response.codeId, + { + channel: srcChannelId, + }, + "amazing random contract", + "auto" + ); + console.log("Deployed contract:", contractAddress); + + const denom = wasmClient.chainInfo.denom; + + const execution = await client.execute( + ownerAddress, + contractAddress, + { + send_message_evm: { + destination_chain: "ethereum", + destination_address: "0x49324C7f83568861AB1b66E547BB1B66431f1070", + message: "Hello", + }, + }, + "auto", + "test", + [{ amount: "1000000", denom }] + ); + + // console.log(JSON.stringify(execution, null, 2)); + + await relayerClient.relayPackets(); + } + + async function executeContractCallWithToken() { // Upload the wasm contract const _path = path.resolve(__dirname, "../..", "wasm/multi_send.wasm"); const response = await wasmClient.uploadWasm(_path); @@ -82,19 +129,25 @@ describe("E2E - Listener", () => { it("should receive ibc events from call contract", (done) => { (async () => { - axelarListener.listen(AxelarIBCEvent, (args) => { - console.log("Any event", args); - }); + // axelarListener.listen(AxelarIBCEvent, (args) => { + // console.log("Any event", args); + // }); axelarListener.listen(AxelarCosmosContractCallEvent, (args) => { console.log("Received ContractCall", args); done(); }); - axelarListener.listen(AxelarCosmosContractCallWithTokenEvent, (args) => { - console.log("Received ContractCallWithToken:", args); - done(); - }); + // axelarListener.listen(AxelarCosmosContractCallWithTokenEvent, (args) => { + // console.log("Received ContractCallWithToken:", args); + // done(); + // }); - await executeContractCall(); + try { + await executeContractCall(); + } catch (e) { + console.log(e); + done(); + } + await executeContractCallWithToken(); })(); }); }); diff --git a/packages/axelar-local-dev-cosmos/src/clients/CosmosClient.ts b/packages/axelar-local-dev-cosmos/src/clients/CosmosClient.ts index 6e309178..76ce26b7 100644 --- a/packages/axelar-local-dev-cosmos/src/clients/CosmosClient.ts +++ b/packages/axelar-local-dev-cosmos/src/clients/CosmosClient.ts @@ -38,6 +38,7 @@ export class CosmosClient { denom: config.denom || defaultDenom, lcdUrl: config.lcdUrl || `http://localhost/${chain}-lcd`, rpcUrl: config.rpcUrl || `http://localhost/${chain}-rpc`, + wsUrl: config.wsUrl || `ws://localhost/${chain}-rpc/websocket`, }; const walletOptions = { @@ -96,6 +97,7 @@ export class CosmosClient { denom: this.chainInfo.denom, lcdUrl: this.chainInfo.lcdUrl, rpcUrl: this.chainInfo.rpcUrl, + wsUrl: this.chainInfo.wsUrl, }; } diff --git a/packages/axelar-local-dev-cosmos/src/clients/CosmosRelayerClient.ts b/packages/axelar-local-dev-cosmos/src/clients/CosmosRelayerClient.ts new file mode 100644 index 00000000..3334c85e --- /dev/null +++ b/packages/axelar-local-dev-cosmos/src/clients/CosmosRelayerClient.ts @@ -0,0 +1,12 @@ +import ReconnectingWebSocket from "reconnecting-websocket"; +import { CosmosChainInfo } from "../types"; + +export class CosmosRelayerClient { + private wsMap: Map; + + private constructor(config: Omit) { + this.wsMap = new Map(); + } + + async listenIncomingIBCEvents() {} +} diff --git a/packages/axelar-local-dev-cosmos/src/clients/IBCRelayerClient.ts b/packages/axelar-local-dev-cosmos/src/clients/IBCRelayerClient.ts index 21717503..dc17f1bf 100644 --- a/packages/axelar-local-dev-cosmos/src/clients/IBCRelayerClient.ts +++ b/packages/axelar-local-dev-cosmos/src/clients/IBCRelayerClient.ts @@ -3,8 +3,11 @@ import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; import { IbcClient, Link, RelayedHeights } from "@confio/relayer"; import { ChannelPair } from "@confio/relayer/build/lib/link"; import { CosmosClient } from "../clients/CosmosClient"; -import { relay } from "@axelar-network/axelar-local-dev"; +import { ethers } from "ethers"; +import fs from "fs"; +import path from "path"; import { convertCosmosAddress } from "../docker"; +import { CosmosChain } from "../types"; export class IBCRelayerClient { axelarClient: CosmosClient; @@ -24,37 +27,99 @@ export class IBCRelayerClient { this.relayerAccount = relayer; } - static async create() { + static async create(mnemonic?: string) { const axelarClient = await CosmosClient.create("axelar"); const wasmClient = await CosmosClient.create("wasm"); - const relayer = await DirectSecp256k1HdWallet.generate(12, { - prefix: "wasm", + + const relayer = await IBCRelayerClient.createRelayerAccount( + "wasm", + mnemonic + ); + + return new IBCRelayerClient(axelarClient, wasmClient, relayer); + } + + private static async createRelayerAccount( + prefix: CosmosChain, + mnemonic?: string + ) { + if (mnemonic) { + return await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { + prefix, + }); + } + + return await DirectSecp256k1HdWallet.generate(12, { + prefix, }); + } - // Fund the relayer address - const relayerAddress = await relayer - .getAccounts() - .then((accounts) => accounts[0].address); - const relayerAxelarAddress = convertCosmosAddress(relayerAddress, "axelar"); + async fundRelayerAccountsIfNeeded(minAmount = "10000000") { + const fund = await this.getRelayerFund(); + + if ( + ethers.BigNumber.from(fund.wasm.balance).lt(minAmount) || + ethers.BigNumber.from(fund.axelar.balance).lt(minAmount) + ) { + await this.fundRelayer(minAmount); + } + } + + async fundRelayer(amount = "1000000000") { + const relayerAddress = await this.getRelayerAddress("wasm"); + const relayerAxelarAddress = await this.getRelayerAddress("axelar"); - await wasmClient.fundWallet(relayerAddress, "100000000"); - console.log("Funded relayer address on wasm:", relayerAddress); - await axelarClient.fundWallet(relayerAxelarAddress, "100000000"); - console.log("Funded relayer address on axelar:", relayerAxelarAddress); + // Fund the relayer address + await this.wasmClient.fundWallet(relayerAddress, amount); + console.log( + `Funded ${amount}${ + this.wasmClient.getChainInfo().denom + } to relayer address on wasm:`, + relayerAddress + ); + await this.axelarClient.fundWallet(relayerAxelarAddress, amount); + console.log( + `Funded ${amount}${ + this.axelarClient.getChainInfo().denom + } to relayer address on axelar:`, + relayerAxelarAddress + ); + } + async getRelayerFund() { // check the fund - const balance = await wasmClient.getBalance(relayerAddress); + const relayerAddress = await this.getRelayerAddress("wasm"); + const relayerAxelarAddress = await this.getRelayerAddress("axelar"); + + const balance = await this.wasmClient.getBalance(relayerAddress); console.log("Relayer wasm balance", balance); - const axelarBalance = await axelarClient.getBalance(relayerAxelarAddress); - console.log("Relayer axelar balance", axelarBalance); + const axelarBalance = await this.axelarClient.getBalance( + relayerAxelarAddress + ); + console.log("Relayer axelar balance", relayerAxelarAddress, axelarBalance); - return new IBCRelayerClient(axelarClient, wasmClient, relayer); + return { + wasm: { + address: relayerAddress, + balance, + }, + axelar: { + address: relayerAxelarAddress, + balance: axelarBalance, + }, + }; } - getRelayerAddress() { - return this.relayerAccount + async getRelayerAddress(prefix: CosmosChain = "wasm") { + const relayerAddress = await this.relayerAccount .getAccounts() .then((accounts) => accounts[0].address); + + if (prefix === "wasm") { + return relayerAddress; + } + + return convertCosmosAddress(relayerAddress, prefix); } async getIBCClient(client: CosmosClient) { @@ -79,7 +144,31 @@ export class IBCRelayerClient { ); } - async initConnection() { + getCurrentConnection() { + try { + const json = fs.readFileSync( + path.join(__dirname, "../../info/connection.json"), + "utf8" + ); + return JSON.parse(json); + } catch (e) { + return undefined; + } + } + + getCurrentChannel(): ChannelPair | undefined { + try { + const json = fs.readFileSync( + path.join(__dirname, "../../info/channel.json"), + "utf8" + ); + return JSON.parse(json); + } catch (e) { + return undefined; + } + } + + async initConnection(saveToFile = false) { const axelarIBCClient = await this.getIBCClient(this.axelarClient); const wasmIBCClient = await this.getIBCClient(this.wasmClient); @@ -93,10 +182,42 @@ export class IBCRelayerClient { }, }; - this.link = await Link.createWithNewConnections( - axelarIBCClient, - wasmIBCClient - ); + const connection = await this.getCurrentConnection(); + + if (connection) { + console.log("Using existing connection", connection); + this.link = await Link.createWithExistingConnections( + axelarIBCClient, + wasmIBCClient, + connection.axelar.connectionId, + connection.wasm.connectionId + ); + } else { + console.log("Creating new connection"); + this.link = await Link.createWithNewConnections( + axelarIBCClient, + wasmIBCClient + ); + + if (saveToFile) { + const infoPath = path.join(__dirname, "../../info"); + await fs.promises + .mkdir(infoPath, { recursive: true }) + .catch(console.error); + const channelPath = path.join(infoPath, "connection.json"); + fs.writeFileSync( + channelPath, + JSON.stringify({ + axelar: { + connectionId: this.link.endA.connectionID, + }, + wasm: { + connectionId: this.link.endB.connectionID, + }, + }) + ); + } + } return { axelar: { @@ -108,13 +229,19 @@ export class IBCRelayerClient { }; } - async createChannel(sender: "A" | "B") { + async createChannel(sender: "A" | "B", saveToFile = false) { if (!this.link) { throw new Error("Link not initialized"); } if (this.channel) return this.channel; + const channel = await this.getCurrentChannel(); + if (channel) { + console.log("Using existing channel", channel); + return channel; + } + this.channel = await this.link?.createChannel( sender, "transfer", @@ -123,6 +250,15 @@ export class IBCRelayerClient { "ics20-1" ); + if (saveToFile) { + const infoPath = path.join(__dirname, "../../info"); + await fs.promises + .mkdir(infoPath, { recursive: true }) + .catch(console.error); + const channelPath = path.join(infoPath, "channel.json"); + fs.writeFileSync(channelPath, JSON.stringify(this.channel)); + } + return this.channel; } @@ -131,6 +267,10 @@ export class IBCRelayerClient { throw new Error("Link not initialized"); } + // Update the clients to get the latest height. Otherwise, the relayer will not relay packets + await this.link!.updateClient("A"); + await this.link!.updateClient("B"); + this.lastRelayedHeight = await this.link!.checkAndRelayPacketsAndAcks( this.lastRelayedHeight, 2, diff --git a/packages/axelar-local-dev-cosmos/src/docker/start.ts b/packages/axelar-local-dev-cosmos/src/docker/start.ts index 10a73b67..85fb3580 100644 --- a/packages/axelar-local-dev-cosmos/src/docker/start.ts +++ b/packages/axelar-local-dev-cosmos/src/docker/start.ts @@ -68,9 +68,11 @@ export async function start( const rpcUrl = `http://localhost/${chain}-rpc`; const lcdUrl = `http://localhost/${chain}-lcd`; + const wsUrl = `ws://localhost/${chain}-rpc/websocket`; console.log(`RPC server for ${chain} is started at ${rpcUrl}`); console.log(`LCD server for ${chain} is started at ${lcdUrl}`); + console.log(`WS server for ${chain} is started at ${wsUrl}`); return { prefix: chain, @@ -78,6 +80,7 @@ export async function start( denom: getChainDenom(chain), lcdUrl, rpcUrl, + wsUrl, }; } diff --git a/packages/axelar-local-dev-cosmos/src/index.ts b/packages/axelar-local-dev-cosmos/src/index.ts index b8fd0ad6..ebaffdd9 100644 --- a/packages/axelar-local-dev-cosmos/src/index.ts +++ b/packages/axelar-local-dev-cosmos/src/index.ts @@ -1 +1,2 @@ export * from "./clients"; +export * from "./listeners"; diff --git a/packages/axelar-local-dev-cosmos/src/listeners/AxelarListener.ts b/packages/axelar-local-dev-cosmos/src/listeners/AxelarListener.ts new file mode 100644 index 00000000..a3aebc50 --- /dev/null +++ b/packages/axelar-local-dev-cosmos/src/listeners/AxelarListener.ts @@ -0,0 +1,73 @@ +import ReconnectingWebSocket from "reconnecting-websocket"; +import WebSocket from "isomorphic-ws"; +import { AxelarListenerEvent, CosmosChainInfo } from "../types"; + +export class AxelarListener { + private wsMap: Map; + private wsOptions = { + WebSocket, // custom WebSocket constructor + maxRetries: Infinity, + }; + + private wsUrl: string; + + constructor(config: Pick) { + this.wsMap = new Map(); + this.wsUrl = config.wsUrl || `ws://localhost/axelar-rpc/websocket`; + } + + private initWs(topicId: string) { + const _ws = this.wsMap.get(topicId); + if (_ws) { + return _ws; + } + const ws = new ReconnectingWebSocket(this.wsUrl, [], this.wsOptions); + this.wsMap.set(topicId, ws); + + return ws; + } + + public listen(event: AxelarListenerEvent, callback: (args: T) => void) { + const ws = this.initWs(event.topicId); + ws.addEventListener("open", () => { + ws.send( + JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "subscribe", + params: [event.topicId], + }) + ); + console.info(`[AxelarListener] Listening to "${event.type}" event`); + }); + + ws.addEventListener("close", () => { + console.debug( + `[AxelarListener] ws connection for ${event.type} is closed. Reconnect Ws...` + ); + ws.reconnect(); + }); + + ws.addEventListener("message", (ev: MessageEvent) => { + // convert buffer to json + const _event = JSON.parse(ev.data.toString()); + + // check if the event topic is matched + if (!_event.result || _event.result.query !== event.topicId) return; + + console.debug(`[AxelarListener] Received ${event.type} event`); + + // parse the event data + event + .parseEvent(_event.result.events) + .then((ev) => { + callback(ev); + }) + .catch((e) => { + console.debug( + `[AxelarListener] Failed to parse topic ${event.topicId} GMP event: ${e}` + ); + }); + }); + } +} diff --git a/packages/axelar-local-dev-cosmos/src/listeners/eventTypes.ts b/packages/axelar-local-dev-cosmos/src/listeners/eventTypes.ts new file mode 100644 index 00000000..a9bf9e27 --- /dev/null +++ b/packages/axelar-local-dev-cosmos/src/listeners/eventTypes.ts @@ -0,0 +1,38 @@ +import { + ContractCallSubmitted, + ContractCallWithTokenSubmitted, + IBCEvent, +} from "../types"; +import { + parseContractCallSubmittedEvent, + parseContractCallWithTokenSubmittedEvent, + parseIBCEvent, +} from "./parser"; + +export interface AxelarListenerEvent { + type: string; + topicId: string; + parseEvent: (event: any) => Promise; +} + +export const AxelarIBCEvent: AxelarListenerEvent> = { + type: "IBCEvent", + topicId: `tm.event='Tx' AND message.action='/ibc.core.channel.v1.MsgRecvPacket'`, + parseEvent: parseIBCEvent, +}; + +export const AxelarCosmosContractCallEvent: AxelarListenerEvent< + IBCEvent +> = { + type: "axelar.axelarnet.v1beta1.ContractCallSubmitted", + topicId: `tm.event='Tx' AND axelar.axelarnet.v1beta1.ContractCallSubmitted.message_id EXISTS`, + parseEvent: parseContractCallSubmittedEvent, +}; + +export const AxelarCosmosContractCallWithTokenEvent: AxelarListenerEvent< + IBCEvent +> = { + type: "axelar.axelarnet.v1beta1.ContractCallWithTokenSubmitted", + topicId: `tm.event='Tx' AND axelar.axelarnet.v1beta1.ContractCallWithTokenSubmitted.message_id EXISTS`, + parseEvent: parseContractCallWithTokenSubmittedEvent, +}; diff --git a/packages/axelar-local-dev-cosmos/src/listeners/index.ts b/packages/axelar-local-dev-cosmos/src/listeners/index.ts new file mode 100644 index 00000000..c7404a70 --- /dev/null +++ b/packages/axelar-local-dev-cosmos/src/listeners/index.ts @@ -0,0 +1,2 @@ +export * from "./AxelarListener"; +export * from "./eventTypes"; diff --git a/packages/axelar-local-dev-cosmos/src/listeners/parser.ts b/packages/axelar-local-dev-cosmos/src/listeners/parser.ts new file mode 100644 index 00000000..ffe7ca49 --- /dev/null +++ b/packages/axelar-local-dev-cosmos/src/listeners/parser.ts @@ -0,0 +1,73 @@ +import { + ContractCallSubmitted, + ContractCallWithTokenSubmitted, + IBCEvent, +} from "../types"; + +const decodeBase64 = (str: string) => { + return Buffer.from(str, "base64").toString("hex"); +}; + +const removeQuote = (str: string) => { + return str.replace(/['"]+/g, ""); +}; + +export function parseIBCEvent(event: any) { + console.log("parseIBCEvent", event); + // const data = { + // event: removeQuote(event["write_acknowledgement.packet_data"][0]), + // } as any; + return Promise.resolve(event); +} + +export function parseContractCallSubmittedEvent( + event: any +): Promise> { + const key = "axelar.axelarnet.v1beta1.ContractCallSubmitted"; + const data = { + messageId: removeQuote(event[`${key}.message_id`][0]), + sender: removeQuote(event[`${key}.sender`][0]), + sourceChain: removeQuote(event[`${key}.source_chain`][0]), + destinationChain: removeQuote(event[`${key}.destination_chain`][0]), + contractAddress: removeQuote(event[`${key}.contract_address`][0]), + payload: `0x${decodeBase64(removeQuote(event[`${key}.payload`][0]))}`, + payloadHash: `0x${decodeBase64( + removeQuote(event[`${key}.payload_hash`][0]) + )}`, + }; + + return Promise.resolve({ + hash: event["tx.hash"][0], + srcChannel: event?.["write_acknowledgement.packet_src_channel"]?.[0], + destChannel: event?.["write_acknowledgement.packet_dst_channel"]?.[0], + args: data, + }); +} + +export function parseContractCallWithTokenSubmittedEvent( + event: any +): Promise> { + console.log("dsad", event); + const key = "axelar.axelarnet.v1beta1.ContractCallWithTokenSubmitted"; + const asset = JSON.parse(event[`${key}.asset`][0]); + const data = { + messageId: removeQuote(event[`${key}.message_id`][0]), + sender: removeQuote(event[`${key}.sender`][0]), + sourceChain: removeQuote(event[`${key}.source_chain`][0]), + destinationChain: removeQuote(event[`${key}.destination_chain`][0]), + contractAddress: removeQuote(event[`${key}.contract_address`][0]), + amount: asset.amount.toString(), + symbol: asset.denom, + payload: `0x${decodeBase64(removeQuote(event[`${key}.payload`][0]))}`, + payloadHash: `0x${decodeBase64( + removeQuote(event[`${key}.payload_hash`][0]) + )}`, + }; + + return Promise.resolve({ + hash: event["tx.hash"][0], + srcChannel: event?.["write_acknowledgement.packet_src_channel"]?.[0], + destChannel: event?.["write_acknowledgement.packet_dst_channel"]?.[0], + args: data, + }); +} diff --git a/packages/axelar-local-dev-cosmos/src/types.ts b/packages/axelar-local-dev-cosmos/src/types.ts index e0a63bea..89409b39 100644 --- a/packages/axelar-local-dev-cosmos/src/types.ts +++ b/packages/axelar-local-dev-cosmos/src/types.ts @@ -1,3 +1,6 @@ +import { Height } from "cosmjs-types/ibc/core/client/v1/client"; +import { Coin } from "@cosmjs/stargate"; + export interface CosmosChainInfo { owner: { mnemonic: string; @@ -7,6 +10,7 @@ export interface CosmosChainInfo { denom?: string; rpcUrl?: string; lcdUrl?: string; + wsUrl?: string; } export type ChainConfig = { @@ -20,3 +24,69 @@ export type ChainDenom = T extends "axelar" : CosmosChain extends "wasm" ? "uwasm" : never; + +export interface AxelarListenerEvent { + type: string; + topicId: string; + parseEvent: (event: any) => Promise; +} + +export interface ContractCallSubmitted { + messageId: string; + sender: string; + sourceChain: string; + destinationChain: string; + contractAddress: string; + payload: string; + payloadHash: string; +} + +export interface ContractCallWithTokenSubmitted { + messageId: string; + sender: string; + sourceChain: string; + destinationChain: string; + contractAddress: string; + payload: string; + payloadHash: string; + symbol: string; + amount: string; +} + +export interface MsgTransfer { + /** the port on which the packet will be sent */ + sourcePort: string; + /** the channel by which the packet will be sent */ + + sourceChannel: string; + /** the tokens to be transferred */ + + token?: Coin; + /** the sender address */ + + sender: string; + /** the recipient address on the destination chain */ + + receiver: string; + /** + * Timeout height relative to the current block height. + * The timeout is disabled when set to 0. + */ + + timeoutHeight?: Height; + /** + * Timeout timestamp in absolute nanoseconds since unix epoch. + * The timeout is disabled when set to 0. + */ + + timeoutTimestamp: Long; + + memo: string; +} + +export interface IBCEvent { + hash: string; + srcChannel?: string; + destChannel?: string; + args: T; +} diff --git a/packages/axelar-local-dev-cosmos/wasm/send_receive.wasm b/packages/axelar-local-dev-cosmos/wasm/send_receive.wasm new file mode 100644 index 00000000..bce5e336 Binary files /dev/null and b/packages/axelar-local-dev-cosmos/wasm/send_receive.wasm differ