From 16e15df5acc4bf2f26836388a5b491d67a5c6521 Mon Sep 17 00:00:00 2001 From: Canh Trinh Date: Thu, 3 Oct 2024 10:13:02 -0400 Subject: [PATCH] Feat/gmp api examples canh (#202) --- .env.example | 6 + .gitignore | 3 + examples/amplifier/README.md | 321 ++++++------------ examples/amplifier/amplifier.js | 118 ------- examples/amplifier/amplifier.proto | 117 ------- examples/amplifier/chains.json | 12 - examples/amplifier/config/chains.json | 14 + examples/amplifier/{ => config}/config.js | 28 +- .../latestTask-xrpl-evm-sidechain.json | 1 + .../amplifier/contracts/AmplifierGMPTest.sol | 42 +++ .../deprecated_legacy_endpoints/amplifier.js | 86 +++++ .../get-payload.js | 0 .../get-receipt.js | 0 .../save-payload.js | 0 examples/amplifier/endpoints/broadcast.js | 27 -- .../endpoints/subscribe-to-approvals.js | 28 -- .../endpoints/subscribe-to-wasm-events.js | 24 -- examples/amplifier/endpoints/verify.js | 54 --- examples/amplifier/gmp-api/approve-event.js | 48 +-- .../amplifier/gmp-api/contract-call-event.js | 10 +- examples/amplifier/gmp-api/execute-event.js | 48 +-- examples/amplifier/gmp-api/tasks.js | 151 +++++--- examples/amplifier/grpc/client.js | 17 - examples/amplifier/index.js | 39 +++ examples/amplifier/utils/deployContract.js | 17 + examples/amplifier/utils/gmp.js | 27 ++ examples/amplifier/utils/index.js | 9 + examples/amplifier/utils/sleep.js | 8 + hardhat.config.js | 2 +- 29 files changed, 547 insertions(+), 710 deletions(-) delete mode 100644 examples/amplifier/amplifier.js delete mode 100644 examples/amplifier/amplifier.proto delete mode 100644 examples/amplifier/chains.json create mode 100644 examples/amplifier/config/chains.json rename examples/amplifier/{ => config}/config.js (51%) create mode 100644 examples/amplifier/config/latestTasks/latestTask-xrpl-evm-sidechain.json create mode 100644 examples/amplifier/contracts/AmplifierGMPTest.sol create mode 100644 examples/amplifier/deprecated_legacy_endpoints/amplifier.js rename examples/amplifier/{endpoints => deprecated_legacy_endpoints}/get-payload.js (100%) rename examples/amplifier/{endpoints => deprecated_legacy_endpoints}/get-receipt.js (100%) rename examples/amplifier/{endpoints => deprecated_legacy_endpoints}/save-payload.js (100%) delete mode 100644 examples/amplifier/endpoints/broadcast.js delete mode 100644 examples/amplifier/endpoints/subscribe-to-approvals.js delete mode 100644 examples/amplifier/endpoints/subscribe-to-wasm-events.js delete mode 100644 examples/amplifier/endpoints/verify.js delete mode 100644 examples/amplifier/grpc/client.js create mode 100644 examples/amplifier/index.js create mode 100644 examples/amplifier/utils/deployContract.js create mode 100644 examples/amplifier/utils/gmp.js create mode 100644 examples/amplifier/utils/index.js create mode 100644 examples/amplifier/utils/sleep.js diff --git a/.env.example b/.env.example index 47bf1962..c376d08c 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,7 @@ EVM_PRIVATE_KEY=YOUR_PRIVATE_KEY_HERE + +# For amplifier examples +GMP_API_URL= +ENVIRONMENT= # e.g. devnet-amplifier, testnet, or mainnet +CRT_PATH= # e.g. './client.crt' +KEY_PATH= # e.g. './client.key' \ No newline at end of file diff --git a/.gitignore b/.gitignore index f6ddc608..b9ae77f6 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ chain-config/*.json !./**/artifacts/send_receive.wasm .multiversx + +*.crt +*.key \ No newline at end of file diff --git a/examples/amplifier/README.md b/examples/amplifier/README.md index a05142aa..5a20c663 100644 --- a/examples/amplifier/README.md +++ b/examples/amplifier/README.md @@ -1,251 +1,154 @@ ## Introduction -This repo provides the code for interacting with the Amplifier Relayer API to relay transactions to the Axelar network and listen to Axelar events. +This repo provides a code example for interacting with the Amplifier GMP API to relay transactions to the Axelar network and listen to Axelar events. -For a visual of the flow of an outgoing message see [outgoing msg](images/Outgoing-Relayer.png) -For a visual of the flow of an inbound message see [inbound msg](images/Inbound-Relayer.png) +Please see the accompanying docs here: -## Setup +### Relayer architecture overview -1. Clone this repo. -2. Install dependencies: - ```bash - npm install - ``` -3. Go to amplifier examples directory - ``` - cd examples/amplifier - ``` -4. Copy `.env.example` into `.env` and set up the following environment variables: - ```bash - HOST=... - PORT=... - ``` +https://docs.axelar.dev/dev/amplifier/chain-integration/relay-messages/automatic/ -## Generic endpoints +### GMP API endpoint and schema definitions -There are three endpoints that can be used for generic commands and events: +https://bright-ambert-2bd.notion.site/Amplifier-GMP-API-EXTERNAL-911e740b570b4017826c854338b906c8 -1. `broadcast` -- Sends a command to get executed as a wasm message on the network -2. `get-receipt` -- Returns the receipt given a receipt-id for a sent message -3. `subscribe-to-wasm-events` -- Subscribes to all wasm events emitted on the network +## Onboarding -### `broadcast` +1. In order to onboard your relayer to be able to leverage the GMP API, please follow these initial steps + - https://www.notion.so/bright-ambert-2bd/Amplifier-GMP-API-Authentication-EXTERNAL-113c53fccb77807caeeff9882b883a4c?pvs=4 + - Please reach out to the Interop Labs team for access -To broadcast a command, use the following: - -```bash -$ node amplifier broadcast \ ---address \ ---payload -``` -For example, call `distribute_rewards()` on the `Rewards` contract to distribute rewards: +## Repository Setup -```bash -$ node amplifier broadcast \ ---address axelar1wkwy0xh89ksdgj9hr347dyd2dw7zesmtrue6kfzyml4vdtz6e5ws2pvc5e \ ---payload '{"distribute_rewards":{"pool_id":{"chain_name":"fantom","contract":"axelar1ufs3tlq4umljk0qfe8k5ya0x6hpavn897u2cnf9k0en9jr7qarqqa9263g"},"epoch_count":1000}}' -Broadcasting message: -axelar1wkwy0xh89ksdgj9hr347dyd2dw7zesmtrue6kfzyml4vdtz6e5ws2pvc5e {"distribute_rewards":{"pool_id":{"chain_name":"fantom","contract":"axelar1ufs3tlq4umljk0qfe8k5ya0x6hpavn897u2cnf9k0en9jr7qarqqa9263g"},"epoch_count":1000}} -Connecting to server at localhost:50051 -Message sent for broadcast { published: true, receiptId: '862592eaadbcdb08ccd2edffd647153e' } -``` +1. Clone this repo. +2. Install dependencies and build contracts: + ```bash + npm install + npm run build + ``` +3. Go to amplifier examples directory + ``` + cd examples/amplifier + ``` +4. Copy `.env.example` into `.env` and set up the following environment variables: + ```bash + GMP_API_URL= + ENVIRONMENT= # e.g. devnet-amplifier, testnet, or mainnet + CRT_PATH= # e.g. './client.crt' + KEY_PATH= # e.g. './client.key' + ``` -### `get-receipt` -Each broadcast returns a `receiptId`, which is a unique key generated by the API to identify every broadcast request attempt. This id can be used to poll for the broadcast response. After a receipt id is returned, you can query the corresponding receipt for 24 hours. +## Run the example ```bash -node amplifier get-receipt --receipt-id + node examples/amplifier/index.js -s avalanche-fuji -d xrpl-evm-sidechain -m 'hi there' ``` -This is going to return either -* the transaction hash, if the transaction is included in a block -* a message indicating that the transaction has not been published yet (this usually takes up to 5-10 seconds) -* an error indicating the transaction failed to publish, or execute +The script above is an end-to-end example of invoking a GMP call on the `devnet-amplifier` environment. It: -For example: +1. deploys an example executable contract on the specified source (`avalanche-fuji`) and destination (`xrpl-evm-sidechain`) chains + ```javascript + // examples/amplifier/index.js + const srcContractDeployment = await deploy(sourceChain); + const destContract = await deploy(destinationChain); + ``` +2. invokes a GMP call on `avalanche-fuji` and invokes `processContractCallEvent` to index the event. The `processContractCallEvent` generates an `CallEvent` (please see [GMP API endpoint and schema definitions](README.md#gmp-api-endpoint-and-schema-definitions)) and sends to to the `/events` endpoint. -```bash -# succesful broadcast -$ node amplifier get-receipt -r 53992509aa3267cc7b2bb8a1bfb21d03 -Getting receipt with id: 53992509aa3267cc7b2bb8a1bfb21d03 -Connecting to server at localhost:50051 -Receipt: -87AECB2151F80616DA2CD237E9EE38DC9558FFBBC93A51DF3B3BE8BB89F0A5EF -``` + ```javascript + // examples/amplifier/utils/gmp.js + const gmp = async ({ sourceChain, destinationChain, message, destinationContractAddress, srcContractAddress }) => { + const provider = new providers.JsonRpcProvider(getChainConfig(sourceChain).rpc); + const wallet = new Wallet(process.env.EVM_PRIVATE_KEY, provider); + const srcContract = new ethers.Contract(srcContractAddress, AmplifierGMPTest.abi, wallet); -```bash -# unknown receipt id -Getting receipt with id: random-id -Connecting to server at localhost:50051 -Error Error: ... - code: 2, - details: 'receipt id not found', -} -``` + try { + const tx = await srcContract.setRemoteValue(destinationChain, destinationContractAddress, message); + const transactionReceipt = await tx.wait(); + console.log(`Initiated GMP event on ${sourceChain}, tx hash: ${transactionReceipt.transactionHash}`); -```bash -# transaction failed to execute -$ node amplifier get-receipt -r 5b0726f7cc6504626023328f62a7454d -Getting receipt with id: 5b0726f7cc6504626023328f62a7454d -Connecting to server at localhost:50051 -Error Error: ... - code: 2, - details: "transaction failed: broadcast tx failed: rpc error: code = Unknown desc = rpc error: code = Unknown desc = failed to execute message; message index: 0: rewards pool balance insufficient: execute wasm contract failed [CosmWasm/wasmd@v0.33.0/x/wasm/keeper/keeper.go:371] With gas wanted: '0' and gas used: '1506569' : unknown request", -} -``` + await sleep(10000); // allow for gmp event to propagate before triggering indexing -### `subscribe-to-wasm-events` + processContractCallEvent(sourceChain, transactionReceipt.transactionHash, true); + } catch (error) { + throw new Error(`Error calling contract: ${error}`); + } + }; + ``` -To get all wasm events emitted on the Axelar network, run: +3. initiates a poll on the `/tasks` endpoint to listen for messages that are processed and ready to be relayed to your destination chain. -```bash -node amplifier subscribe-to-wasm-events -``` + ```javascript + // examples/amplifier/index.js -You can optionally specify a `start-height` to catch events that were emitted at a previous time with the `--start-height` flag. It is set to `0` by default, which means that subscription starts from the current tip of the chain: + main(null).then(() => pollTasks({ chainName: options.destinationChain, pollInterval: 10000, dryRunOpt: false })); + ``` -``` -$ node amplifier subscribe-to-wasm-events --start-height 221645 -Subscribing to events starting from block: 221645 -Connecting to server at localhost:50051 -Event: { - type: 'wasm-voted', - attributes: [ - { - key: '_contract_address', - value: 'axelar1466nf3zuxpya8q9emxukd7vftaf6h4psr0a07srl5zw74zh84yjq4687qd' - }, - { key: 'poll_id', value: '"1"' }, - { - key: 'voter', - value: 'axelar1hzy33ue3a6kztvfhrv9mge45g2x33uct4ndzcy' - } - ], - height: Long { low: 221645, high: 0, unsigned: true } -} -``` - -Every event includes a `type` field that specifies the type of the event, an `attributes` field with all relevant information, and a `height` event that specifies the height emitted. - -## General Message Passing - -The following endpoints are available to facilitate GMP calls: - -1. `verify` -- triggers a verification on the source chain (routing is handled automatically) -2. `subscribe-to-approvals` -- creates a channel to return all calls that are approved on the destination chain -3. `get-payload` -- queries the payload of the initial source-chain transaction by its hash -4. `save-payload` -- stores a payload of the initial source-chain transaction and returns its hash + ```javascript + // examples/amplifier/gmp-api/tasks.js -### `verify` + async function pollTasks({ chainName, pollInterval, dryRunOpt }) { + if (dryRunOpt) { + console.log('Dry run enabled'); + dryRun = true; + } -Given a transaction relayed on the source chain, the `verify` command is called as follows: - -``` bash -node amplifier verify \ ---id 0x02293467b9d6e1ce51d8ac0fa24e9a30fb95b5e1e1e18c26c8fd737f904b564c:4 \ ---source-chain avalanche \ ---source-address 0x90AD61b0FaC683b23543Ed39B8E3Bd418D6CcBfe \ ---destination-chain fantom \ ---destination-address 0x9B35d37a8ebCb1d744ADdEC47CA2a939e811B638 \ ---payload 00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000f68656c6c6f206176616c616e63686500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -``` - -where - -- `id` -- the `:`. Note that you need the `blockLogIndex`, not the `txLogIndex`. For example, the [earlier transaction](https://testnet.snowtrace.io/tx/0x02293467b9d6e1ce51d8ac0fa24e9a30fb95b5e1e1e18c26c8fd737f904b564c) is included in [`block 31050074`](https://testnet.snowtrace.io/block/31050074?chainId=43113), and the `ContractCall` topic is `0x30ae6cc78c27e651745bf2ad08a11de83910ac1e347a52f7ac898c0fbef94dae`. Searching for this topic in the block’s logs, we see that the `logIndex` is `4` : - - ```bash - $ curl -s --location $RPC \ - --header 'Content-Type: application/json' \ - --data '{"jsonrpc":"2.0","method":"eth_getLogs","params":[{ - "fromBlock": "0x1d9c95a" - }],"id":1}' | jq | grep 0x30ae6cc78c27e651745bf2ad08a11de83910ac1e347a52f7ac898c0fbef94dae -A 11 -B 3 - { - "address": "0xca85f85c72df5f8428a440887ca7c449d94e0d0c", - "topics": ["0x30ae6cc78c27e651745bf2ad08a11de83910ac1e347a52f7ac898c0fbef94dae", "0x00000000000000000000000090ad61b0fac683b23543ed39b8e3bd418d6ccbfe", "0xa9b070ad799e19f1166fdbf4524b684f8026df510fe6a7770f949ad54047098c"], - ... - "logIndex": "0x4", # <- our logIndex - ... - }, - ``` -- `source-chain` -- the source chain -- `source-address` -- the address of the sender -- `destination-chain` -- the destination chain -- `destination-address` -- the address of the recipient -- `payload` -- the transaction payload of `ContractCall` event, in bytes. The `0x` can be omitted: - - ![Payload](images/payload.png) - -After a few seconds, the `verify` command will exit displaying the `id`, and or an error if any: - -```bash -node amplifier verify --id 0x02293467b9d6e1ce51d8ac0fa24e9a30fb95b5e1e1e18c26c8fd737f904b564c:4 --source-chain avalanche --source-address 0x90AD61b0FaC683b23543Ed39B8E3Bd418D6CcBfe --destination-chain fantom --destination-address 0x9B35d37a8ebCb1d744ADdEC47CA2a939e811B638 --payload 00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000f68656c6c6f206176616c616e63686500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -Connecting to server at localhost:50051 -Verifying message: { - message: { - id: '0x02293467b9d6e1ce51d8ac0fa24e9a30fb95b5e1e1e18c26c8fd737f904b564c:4', - sourceChain: 'avalanche', - sourceAddress: '0x90AD61b0FaC683b23543Ed39B8E3Bd418D6CcBfe', - destinationChain: 'fantom', - destinationAddress: '0x9B35d37a8ebCb1d744ADdEC47CA2a939e811B638', - payload: - } -} -Success verification for 0x02293467b9d6e1ce51d8ac0fa24e9a30fb95b5e1e1e18c26c8fd737f904b564c:4 -``` + const chainConfig = getChainConfig(chainName); -### `subscribe-to-approvals` + const intervalId = setInterval(async () => { + await getNewTasks(chainConfig, intervalId); + }, pollInterval); + } + ``` -After a verification is initiated and once all internal processes (verifying, routing messages to the destination gateway, and constructing proof) are done on the Axelar network, a `signing-completed` event is emitted which contains a `session-id`. This `session-id` can be used to query the proof from the Axelar chain and return the execute data that need to be relayed on the destination chain. Do this by running `subscribe-to-approvals`: + - In this particular example, there are two events we expect to arise from the `/tasks` endpoint: -```bash -node amplifier subscribe-to-approvals \ ---chain fantom \ ---start-height # optional -``` + - `GATEWAY_TX` for approved messages on the Amplifier network ready to be relayed to the destination chain gateway, and they are processed in the following snippet by relaying the transaction to the destination chain and recording the event on the GMP API. -- `chain` -- the destination chain -- `start-height` (optional) -- start height [0 = latest] similar to `subscribe-to-wasm-events` + The `recordMessageApprovedEvent` function generates an `MessageApprovedEvent` (please see [GMP API endpoint and schema definitions](README.md#gmp-api-endpoint-and-schema-definitions)) extracted from the destination chain tx receipt and sends to to the `/events` endpoint. -For example: + ```javascript + // examples/amplifier/gmp-api/tasks.js -```bash -$ node amplifier subscribe-to-approvals -c fantom -s 221645 -Subscribing to approvals starting from block: 221645 on chain: fantom -Connecting to server at localhost:50051 -chain: fantom -block height: 221855 -execute data: 09c5eabe000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000006e00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000034000000000000000000000000000000000000000000000000000000000000002e00000000000000000000000000000000000000000000000000000000000000fa2000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000010749a63dd8ad2d24037397e5adff4027176863d46a05e007749e3d9b2e1eadb3000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000013617070726f7665436f6e747261637443616c6c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000009b35d37a8ebcb1d744addec47ca2a939e811b638a9b070ad799e19f1166fdbf4524b684f8026df510fe6a7770f949ad54047098c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000096176616c616e6368650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a307839304144363162304661433638336232333534334564333942384533426434313844364363426665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000380000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000030000000000000000000000008054f16ad10c3bf57e178f7f9bc45ea89f84301a00000000000000000000000089a73afebb411c865074251e036d4c12eb99b7ba000000000000000000000000f330a7f2a738eefd5cf1a33211cd131a7e92fdd400000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000417d7349a27e2a6e291f54a5954ec32eb7dcb6f5ec33fe71830dac34181d8af97b6d1d5f2d1309a0c56820cf95f6d4890444e35c8cdb749bf3f2d1c69393c1a2661b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041bd88d12035d8b2aededcf1444ecba29dd933087761e8ea1fd6c0d7efb0262542240539234dcf72c2ba748b9ece101c365fd498dfd565f646249771f56ade281f1c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041af4a7296bcb0b21282a7938c7a97ec1f82c2fe440e260e0dbe8aeecfaa5c77a902bc39a6dea0235860944a3085ff6e9a1c340865e9f93d82a8a4922d0c5253fb1b00000000000000000000000000000000000000000000000000000000000000 ---- -``` + // Note: The `messageIdToCommandID` mapping is only relevant for EVM relaying. For EVM chains, commandID is still required in the 'execute' function on the destination chain + // Because the unifying identifier between APPROVE and EXECUTE events is the messageID, this mapping helps to record the relation between those events for a single GMP tx + const messageIdToCommandId = {}; -### `save-payload` + async function processApproval(task, chainConfig) { + console.log('Processing approve task', task.id); + const payload = decodePayload(task.task.executeData); + const destinationAddress = chainConfig.gateway; -To save a payload that was submitted by the transaction on the source chain, use `save-payload`: + const destTxRecept = await relayApproval(chainConfig.rpc, payload, destinationAddress); + const { apiEvent } = await recordMessageApprovedEvent(chainConfig.name, destTxRecept.transactionHash, '0'); + messageIdToCommandId[apiEvent.message.messageID] = apiEvent.meta.commandID; + } + ``` -```bash -$ node amplifier save-payload --payload 00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000f68656c6c6f206176616c616e63686500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -Saving payload 00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000f68656c6c6f206176616c616e63686500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -Connecting to server at localhost:50051 -Payload hash: -0xa9b070ad799e19f1166fdbf4524b684f8026df510fe6a7770f949ad54047098c -``` + - `EXECUTE` for the payload of the GMP message to be executed on the executable on the destination chain. -- `payload` -- the payload + The `recordMessageExecutedEvent` function generates an `MessageExecutedEvent` (please see [GMP API endpoint and schema definitions](README.md#gmp-api-endpoint-and-schema-definitions)) extracted from the destination chain tx receipt and sends to to the `/events` endpoint. -### `get-payload` + ```javascript + // examples/amplifier/gmp-api/tasks.js -To get the payload that was submitted by the transaction on the source chain, use `get-payload`: + async function processExecute(task, chainConfig, intervalId) { + console.log('Processing execute task', task.id); + const payload = decodePayload(task.task.payload); + const destinationAddress = task.task.message.destinationAddress; + const { messageID, sourceAddress, sourceChain } = task.task.message; -```bash -$ node amplifier get-payload --hash 0xa9b070ad799e19f1166fdbf4524b684f8026df510fe6a7770f949ad54047098c -Getting payload for payload hash a9b070ad799e19f1166fdbf4524b684f8026df510fe6a7770f949ad54047098c -Connecting to server at localhost:50051 -Payload: -00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000f68656c6c6f206176616c616e63686500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -``` + const destTxRecept = await relayExecution(chainConfig.rpc, payload, destinationAddress, { + messageID, + sourceAddress, + sourceChain, + }); + await recordMessageExecutedEvent(chainConfig.name, destTxRecept.transactionHash, sourceChain, messageID, '0'); + clearInterval(intervalId); + console.log('Polling interval cleared after EXECUTE task completed'); + } + ``` -- `hash` -- the payload hash +## Considerations -![Payload hash](images/payload-hash.png) \ No newline at end of file +- This example is for illustrative purposes only to demonstrate the functionality to interact with the GMP. It is not a complete example for a Relayer that is expected to have robust `Sentinel` and `Includer` components, both of which are high throughput listeners/broadcasters that interact with the GMP API. +- This indicative example only works if a single relayer is operating the intended `Includer` comoponent (i.e. the polling that listens to `tasks` that are emitted from the `/tasks` endpoint for `GATEWAY_TX` and `EXECUTE` events). If there are multiple relayers running simultaneously, it is possible that another relayer picks up the tasks to broadcast events to a chain, and by the time this relayer attempts to do the same, the transaction will result in a no-op and the accompanying data capture (i.e. `recordMessageApprovedEvent` and `recordMessageExecutedEvent`) will fail. diff --git a/examples/amplifier/amplifier.js b/examples/amplifier/amplifier.js deleted file mode 100644 index 92f89968..00000000 --- a/examples/amplifier/amplifier.js +++ /dev/null @@ -1,118 +0,0 @@ -const commander = require('commander'); -const { broadcast } = require('./endpoints/broadcast.js'); -const { getReceipt } = require('./endpoints/get-receipt.js'); -const { getPayload } = require('./endpoints/get-payload.js'); -const { savePayload } = require('./endpoints/save-payload.js'); -const { subscribe_to_approvals } = require('./endpoints/subscribe-to-approvals.js'); -const { subscribe_to_wasm_events } = require('./endpoints/subscribe-to-wasm-events.js'); -const { verify } = require('./endpoints/verify.js'); -const { processContractCallEvent } = require('./gmp-api/contract-call-event.js'); -const { processMessageApprovedEvent } = require('./gmp-api/approve-event.js'); -const { processMessageExecutedEvent } = require('./gmp-api/execute-event.js'); -const { pollTasks } = require('./gmp-api/tasks.js'); - -const program = new commander.Command(); - -program - .command('broadcast') - .requiredOption('-a, --address ', 'The address of the destination contract') - .requiredOption("-p, --payload ", "The payload of the wasm message") - .action((options) => { - broadcast(options.address, options.payload); - }); - -program - .command('get-receipt') - .requiredOption("-r, --receipt-id ", "The id of the receipt") - .action((options) => { - getReceipt(options.receiptId); - }); - -program - .command('get-payload') - .requiredOption('--hash, ', 'payload hash') - .action((options) => { - getPayload(options.hash); - }); - -program - .command('save-payload') - .requiredOption('--payload, ', 'payload') - .action((options) => { - savePayload(options.payload); - }); - -program - .command('subscribe-to-approvals') - .requiredOption("-c, --chain ", "The chain to subscribe to") - .option("-s, --start-height ", "The block height to start from (0 = latest)", parseInt, 0) - .action((options) => { - subscribe_to_approvals(options.chain, options.startHeight); - }); - -program - .command('subscribe-to-wasm-events') - .option("-s, --start-height ", "The block height to start from (0 = latest)", parseInt, 0) - .action((startHeight) => { - subscribe_to_wasm_events(startHeight) - }); - -program - .command('verify') - .requiredOption("-i, --id ", "The id of the transaction (txHash-logIndex)") - .requiredOption("--source-chain ", "The source chain") - .requiredOption("--source-address ", "The source address") - .requiredOption("--destination-chain ", "The destination chain") - .requiredOption("--destination-address ", "The destination address") - .requiredOption("--payload ", "The GMP payload in hex") - .action((options) => { - verify(options.id, options.sourceChain, options.sourceAddress, options.destinationChain, options.destinationAddress, options.payload); - }); - -program - .command('process-contract-call-event') - .requiredOption("--source-chain ", "The source chain") - .requiredOption("--tx-hash ", "The transaction hash") - .option("--dry-run", "Dry run the process") - .action((options) => { - processContractCallEvent(options.sourceChain, options.txHash, options.dryRun) - .then(() => console.log('Process completed successfully')) - .catch(error => console.error('Process failed:', error)); - }); - -program - .command('process-approve-event') - .requiredOption("--destination-chain ", "The destination chain") - .requiredOption("--tx-hash ", "The transaction hash") - .option("--amount ", "Remaining gas amount") - .option("--dry-run", "Dry run the process") - .action((options) => { - processMessageApprovedEvent(options.destinationChain, options.txHash, options.amount, options.dryRun) - .then(() => console.log('Process completed successfully')) - .catch(error => console.error('Process failed:', error)); - }); - -program - .command('process-execute-event') - .requiredOption("--destination-chain ", "The destination chain") - .requiredOption("--tx-hash ", "The transaction hash") - .requiredOption("--source-chain ", "The source chain") - .requiredOption("--message-id ", "The message id") - .option("--amount ", "Remaining gas amount") - .option("--dry-run", "Dry run the process") - .action((options) => { - processMessageExecutedEvent(options.destinationChain, options.txHash, options.sourceChain, options.messageId, options.amount, options.dryRun) - .then(() => console.log('Process completed successfully')) - .catch(error => console.error('Process failed:', error)); - }); - -program - .command("poll-tasks") - .requiredOption("--chain ", "The chain to poll task for") - .option("--poll-interval ", "The interval to poll for new tasks", parseInt, 5000) - .option("--dry-run", "Dry run the process") - .action((options) => { - pollTasks(options.chain, options.pollInterval, options.dryRun); - }); - -program.parse(); \ No newline at end of file diff --git a/examples/amplifier/amplifier.proto b/examples/amplifier/amplifier.proto deleted file mode 100644 index c0419d76..00000000 --- a/examples/amplifier/amplifier.proto +++ /dev/null @@ -1,117 +0,0 @@ -syntax = "proto3"; -package axelar.amplifier.v1beta1; - -option go_package = "github.com/axelarnetwork/axelar-eds/pkg/amplifier/server/api"; - -service Amplifier { - rpc Verify(stream VerifyRequest) returns (stream VerifyResponse); - rpc GetPayload(GetPayloadRequest) returns (GetPayloadResponse) { - option (google.api.http) = { - get : "/v1beta1/payload/{hash}" - }; - } - rpc SubscribeToApprovals(SubscribeToApprovalsRequest) - returns (stream SubscribeToApprovalsResponse); - rpc SubscribeToWasmEvents(SubscribeToWasmEventsRequest) - returns (stream SubscribeToWasmEventsResponse); - rpc Broadcast(BroadcastRequest) returns (BroadcastResponse) { - option (google.api.http) = { - post : "/v1beta1/broadcast" - body : "*" - }; - } - rpc GetReceipt(GetReceiptRequest) returns (GetReceiptResponse) { - option (google.api.http) = { - get : "/v1beta1/receipt/{receipt_id}" - }; - } - rpc SavePayload(SavePayloadRequest) returns (SavePayloadResponse) { - option (google.api.http) = { - post : "/v1beta1/payload" - body : "*" - }; - } -} - -message Message { - string id = 1; // the unique identifier with which the message can be looked - // up on the source chain - string source_chain = 2; - string source_address = 3; - string destination_chain = 4; - string destination_address = 5; - bytes payload = 6; - // when we have a better idea of the requirement, we can add an additional - // optional field here to facilitate verification proofs -} - -message GetPayloadRequest { bytes hash = 1; } - -message GetPayloadResponse { bytes payload = 1; } - -message SavePayloadRequest { bytes payload = 1; } - -message SavePayloadResponse { bytes hash = 1; } - -message SubscribeToApprovalsRequest { - repeated string chains = 1; - optional uint64 start_height = 2; // can be used to replay events -} - -message SubscribeToApprovalsResponse { - string chain = 1; - bytes execute_data = 2; - uint64 block_height = 3; -} - -message VerifyRequest { Message message = 1; } - -message VerifyResponse { - Message message = 1; - optional Error error = 2; -} - -enum ErrorCode { - VERIFICATION_FAILED = 0; - INTERNAL_ERROR = 1; - AXELAR_NETWORK_ERROR = 2; - INSUFFICIENT_GAS = 3; - FAILED_ON_CHAIN = 4; - MESSAGE_NOT_FOUND = 5; -} - -message Error { - string error = 1; - ErrorCode error_code = 2; -} - -message SubscribeToWasmEventsRequest { optional uint64 start_height = 1; } - -message SubscribeToWasmEventsResponse { - string type = 1; - repeated Attribute attributes = 2; - uint64 height = 3; -} - -message Attribute { - string key = 1; - string value = 2; -} - -message BroadcastRequest { - string address = 1; - bytes payload = 2; -} - -message BroadcastResponse { - bool published = 1; - string receipt_id = 2; -} - -message GetReceiptRequest { - string receipt_id = 1; -} - -message GetReceiptResponse { - string tx_hash = 1; -} \ No newline at end of file diff --git a/examples/amplifier/chains.json b/examples/amplifier/chains.json deleted file mode 100644 index 18a802c4..00000000 --- a/examples/amplifier/chains.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "name": "test-ethereum", - "rpc": "https://ethereum-sepolia.rpc.subquery.network/public", - "gateway": "0x" - }, - { - "name": "test-avalanche", - "rpc": "https://ava-testnet.public.blastapi.io/ext/bc/C/rpc", - "gateway": "0x" - } -] \ No newline at end of file diff --git a/examples/amplifier/config/chains.json b/examples/amplifier/config/chains.json new file mode 100644 index 00000000..650e70cb --- /dev/null +++ b/examples/amplifier/config/chains.json @@ -0,0 +1,14 @@ +{ + "devnet-amplifier": [ + { + "name": "avalanche-fuji", + "rpc": "https://rpc.ankr.com/avalanche_fuji", + "gateway": "0xF128c84c3326727c3e155168daAa4C0156B87AD1" + }, + { + "name": "xrpl-evm-sidechain", + "rpc": "https://rpc-evm-sidechain.xrpl.org", + "gateway": "0x48CF6E93C4C1b014F719Db2aeF049AA86A255fE2" + } + ] +} diff --git a/examples/amplifier/config.js b/examples/amplifier/config/config.js similarity index 51% rename from examples/amplifier/config.js rename to examples/amplifier/config/config.js index 04c45d10..bc46dced 100644 --- a/examples/amplifier/config.js +++ b/examples/amplifier/config/config.js @@ -1,20 +1,28 @@ const fs = require('fs'); - +const https = require('https'); const dotenv = require('dotenv'); // Load environment variables from .env file dotenv.config(); +// Load the certificate and key +const cert = fs.readFileSync(process.env.CRT_PATH); +const key = fs.readFileSync(process.env.KEY_PATH); + // Default configuration values const defaults = { // gRPC - HOST: "localhost", - PORT: "50051", + HOST: 'localhost', + PORT: '50051', // GMP API - GMP_API_URL: "http://localhost:8080", + GMP_API_URL: 'http://localhost:8080', }; +const chainsConfigFile = JSON.parse(fs.readFileSync('./examples/amplifier/config/chains.json', 'utf8')); +const environment = process.env.ENVIRONMENT; +const chainsConfig = chainsConfigFile[environment]; + function getConfig() { const serverHOST = process.env.HOST || defaults.HOST; const serverPort = process.env.PORT || defaults.PORT; @@ -25,15 +33,17 @@ function getConfig() { serverHOST, serverPort, gmpAPIURL, + chains: chainsConfig, + httpsAgent: new https.Agent({ + cert, + key, + rejectUnauthorized: false, + }), }; } -const chainsConfigFile = './examples/amplifier/chains.json'; - function getChainConfig(chainName) { - const chainsConfig = JSON.parse(fs.readFileSync(chainsConfigFile, 'utf8')); - - const chainConfig = chainsConfig.find(c => c.name === chainName); + const chainConfig = chainsConfig.find((c) => c.name === chainName); if (!chainConfig) { throw new Error(`RPC URL not found for chain: ${chainName}`); diff --git a/examples/amplifier/config/latestTasks/latestTask-xrpl-evm-sidechain.json b/examples/amplifier/config/latestTasks/latestTask-xrpl-evm-sidechain.json new file mode 100644 index 00000000..3c120bf2 --- /dev/null +++ b/examples/amplifier/config/latestTasks/latestTask-xrpl-evm-sidechain.json @@ -0,0 +1 @@ +"01924dc4-698e-7ad2-9f9b-0bad962771ef" \ No newline at end of file diff --git a/examples/amplifier/contracts/AmplifierGMPTest.sol b/examples/amplifier/contracts/AmplifierGMPTest.sol new file mode 100644 index 00000000..47145c50 --- /dev/null +++ b/examples/amplifier/contracts/AmplifierGMPTest.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { AxelarExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol'; +import { IAxelarGateway } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol'; + +/** + * @title AmplifierGMPTest + * @notice Send a message from chain A to chain B and stores gmp message + */ +contract AmplifierGMPTest is AxelarExecutable { + string public message; + string public sourceChain; + string public sourceAddress; + + constructor(address _gateway) AxelarExecutable(_gateway) {} + + /** + * @notice Send message from chain A to chain B + * @dev message param is passed in as gmp message + * @param destinationChain name of the dest chain (ex. "Fantom") + * @param destinationAddress address on dest chain this tx is going to + * @param _message message to be sent + */ + function setRemoteValue(string calldata destinationChain, string calldata destinationAddress, string calldata _message) external { + bytes memory payload = abi.encode(_message); + gateway.callContract(destinationChain, destinationAddress, payload); + } + + /** + * @notice logic to be executed on dest chain + * @dev this is triggered automatically by relayer + * @param _sourceChain blockchain where tx is originating from + * @param _sourceAddress address on src chain where tx is originating from + * @param _payload encoded gmp message sent from src chain + */ + function _execute(string calldata _sourceChain, string calldata _sourceAddress, bytes calldata _payload) internal override { + (message) = abi.decode(_payload, (string)); + sourceChain = _sourceChain; + sourceAddress = _sourceAddress; + } +} diff --git a/examples/amplifier/deprecated_legacy_endpoints/amplifier.js b/examples/amplifier/deprecated_legacy_endpoints/amplifier.js new file mode 100644 index 00000000..747ebbde --- /dev/null +++ b/examples/amplifier/deprecated_legacy_endpoints/amplifier.js @@ -0,0 +1,86 @@ +const commander = require('commander'); +const { getReceipt } = require('./endpoints/get-receipt.js'); +const { getPayload } = require('./endpoints/get-payload.js'); +const { savePayload } = require('./endpoints/save-payload.js'); +const { processContractCallEvent } = require('../gmp-api/contract-call-event.js'); +const { processMessageApprovedEvent } = require('../gmp-api/approve-event.js'); +const { processMessageExecutedEvent } = require('../gmp-api/execute-event.js'); +const { pollTasks } = require('../gmp-api/tasks.js'); + +const program = new commander.Command(); + +program + .command('get-receipt') + .requiredOption('-r, --receipt-id ', 'The id of the receipt') + .action((options) => { + getReceipt(options.receiptId); + }); + +program + .command('get-payload') + .requiredOption('--hash, ', 'payload hash') + .action((options) => { + getPayload(options.hash); + }); + +program + .command('save-payload') + .requiredOption('--payload, ', 'payload') + .action((options) => { + savePayload(options.payload); + }); + +program + .command('process-contract-call-event') + .requiredOption('--source-chain ', 'The source chain') + .requiredOption('--tx-hash ', 'The transaction hash') + .option('--dry-run', 'Dry run the process') + .action((options) => { + processContractCallEvent(options.sourceChain, options.txHash, options.dryRun) + .then(() => console.log('Process completed successfully')) + .catch((error) => console.error('Process failed:', error)); + }); + +program + .command('process-approve-event') + .requiredOption('--destination-chain ', 'The destination chain') + .requiredOption('--tx-hash ', 'The transaction hash') + .option('--amount ', 'Remaining gas amount') + .option('--dry-run', 'Dry run the process') + .action((options) => { + processMessageApprovedEvent(options.destinationChain, options.txHash, options.amount, options.dryRun) + .then(() => console.log('Process completed successfully')) + .catch((error) => console.error('Process failed:', error)); + }); + +program + .command('process-execute-event') + .requiredOption('--destination-chain ', 'The destination chain') + .requiredOption('--tx-hash ', 'The transaction hash') + .requiredOption('--source-chain ', 'The source chain') + .requiredOption('--message-id ', 'The message id') + .option('--amount ', 'Remaining gas amount') + .option('--dry-run', 'Dry run the process') + .action((options) => { + processMessageExecutedEvent( + options.destinationChain, + options.txHash, + options.sourceChain, + options.messageId, + options.amount, + options.dryRun, + ) + .then(() => console.log('Process completed successfully')) + .catch((error) => console.error('Process failed:', error)); + }); + +program + .command('poll-tasks') + .requiredOption('--chain ', 'The chain to poll task for') + .option('--poll-interval ', 'The interval to poll for new tasks', parseInt, 5000) + .option('--dry-run', 'Dry run the process') + .action((options) => { + pollTasks(options.chain, options.pollInterval, options.dryRun); + }); + +program.parse(); diff --git a/examples/amplifier/endpoints/get-payload.js b/examples/amplifier/deprecated_legacy_endpoints/get-payload.js similarity index 100% rename from examples/amplifier/endpoints/get-payload.js rename to examples/amplifier/deprecated_legacy_endpoints/get-payload.js diff --git a/examples/amplifier/endpoints/get-receipt.js b/examples/amplifier/deprecated_legacy_endpoints/get-receipt.js similarity index 100% rename from examples/amplifier/endpoints/get-receipt.js rename to examples/amplifier/deprecated_legacy_endpoints/get-receipt.js diff --git a/examples/amplifier/endpoints/save-payload.js b/examples/amplifier/deprecated_legacy_endpoints/save-payload.js similarity index 100% rename from examples/amplifier/endpoints/save-payload.js rename to examples/amplifier/deprecated_legacy_endpoints/save-payload.js diff --git a/examples/amplifier/endpoints/broadcast.js b/examples/amplifier/endpoints/broadcast.js deleted file mode 100644 index e7295c31..00000000 --- a/examples/amplifier/endpoints/broadcast.js +++ /dev/null @@ -1,27 +0,0 @@ -const newClient = require('../grpc/client'); - -function broadcast(address, payload) { - console.log("Broadcasting message:\n", address, payload); - - try { - JSON.parse(payload); - } catch (e) { - console.error("Payload is not valid JSON"); - process.exit(1); - } - - const client = newClient(); - const broadcastRequest = { address, payload: Buffer.from(payload) }; - response = client.Broadcast(broadcastRequest, (err, response) => { - if (err) { - console.error("Error", err); - } else { - console.log("Message sent for broadcast", response); - process.exit(0); - } - }); -} - -module.exports = { - broadcast, -}; \ No newline at end of file diff --git a/examples/amplifier/endpoints/subscribe-to-approvals.js b/examples/amplifier/endpoints/subscribe-to-approvals.js deleted file mode 100644 index b99702b2..00000000 --- a/examples/amplifier/endpoints/subscribe-to-approvals.js +++ /dev/null @@ -1,28 +0,0 @@ -const newClient = require('../grpc/client'); - -function subscribe_to_approvals(chain, startHeight) { - console.log("Subscribing to approvals starting from block:", startHeight == 0 ? "latest" : startHeight, "on chain:", chain); - - const client = newClient(); - - const call = client.SubscribeToApprovals({ startHeight: startHeight, chains: [chain] }); - call.on('data', (response) => { - console.log("chain:", response.chain); - console.log("block height:", response.blockHeight.toString()); - console.log("execute data:", response.executeData.toString('hex')); - console.log("---"); - }); - call.on('end', () => { - console.log("End"); - }); - call.on('error', (e) => { - console.log("Error", e); - }); - call.on('status', (status) => { - console.log("Status", status); - }); -} - -module.exports = { - subscribe_to_approvals, -}; \ No newline at end of file diff --git a/examples/amplifier/endpoints/subscribe-to-wasm-events.js b/examples/amplifier/endpoints/subscribe-to-wasm-events.js deleted file mode 100644 index 0d1b8088..00000000 --- a/examples/amplifier/endpoints/subscribe-to-wasm-events.js +++ /dev/null @@ -1,24 +0,0 @@ -const newClient = require('../grpc/client'); - -function subscribe_to_wasm_events(startHeight) { - console.log("Subscribing to events starting from block:", startHeight == 0 ? "latest" : startHeight); - - const client = newClient(); - const call = client.SubscribeToWasmEvents(startHeight); - call.on('data', (response) => { - console.log("Event:", response); - }); - call.on('end', () => { - console.log("End"); - }); - call.on('error', (e) => { - console.log("Error", e); - }); - call.on('status', (status) => { - console.log("Status", status); - }); -} - -module.exports = { - subscribe_to_wasm_events, -}; \ No newline at end of file diff --git a/examples/amplifier/endpoints/verify.js b/examples/amplifier/endpoints/verify.js deleted file mode 100644 index 664bdf2b..00000000 --- a/examples/amplifier/endpoints/verify.js +++ /dev/null @@ -1,54 +0,0 @@ -const newClient = require('../grpc/client'); - -function verify(id, sourceChain, sourceAddress, destinationChain, destinationAddress, payload) { - console.log("Verifying message with id, sourceChain, sourceAddress, destinationChain, destinationAddress, and payload:", id, sourceChain, sourceAddress, destinationChain, destinationAddress, payload); - - if (id.split('-').length != 2) { - console.error("Invalid transaction id. Expected format: txHash-logIndex"); - process.exit(1); - } - payload = payload.replace('0x', ''); - - request = { - message: { - id: id, - sourceChain: sourceChain, - sourceAddress: sourceAddress, - destinationChain: destinationChain, - destinationAddress: destinationAddress, - payload: Buffer.from(payload, 'hex'), - }, - }; - - const client = newClient(); - const verifyStream = client.Verify(); - - console.log("Verifying message:", request); - - verifyStream.on('data', function (response) { - if (response.error) { - console.error('Error:', response.error); - } else { - console.log('Success verification for', response.message.id); - process.exit(0); - } - }); - - verifyStream.on('end', function () { - console.log('Server has completed sending responses.'); - }); - - verifyStream.on('error', function (e) { - console.error('Error: ', e); - }); - - verifyStream.on('status', function (status) { - console.log('Status: ', status); - }); - - verifyStream.write(request); -} - -module.exports = { - verify, -}; \ No newline at end of file diff --git a/examples/amplifier/gmp-api/approve-event.js b/examples/amplifier/gmp-api/approve-event.js index a4b4de78..fa8004eb 100644 --- a/examples/amplifier/gmp-api/approve-event.js +++ b/examples/amplifier/gmp-api/approve-event.js @@ -1,26 +1,25 @@ const axios = require('axios'); const { ethers } = require('ethers'); -const { getConfig, getChainConfig } = require('../config.js'); +const { getConfig, getChainConfig } = require('../config/config.js'); -const { GMP_API_URL } = getConfig(); +const { gmpAPIURL, httpsAgent } = getConfig(); // ABI for the ContractCall and MessageApproved events const eventABI = [ - "event MessageApproved(bytes32 indexed commandId, string sourceChain, string messageId, string sourceAddress, address indexed contractAddress, bytes32 indexed payloadHash)", + 'event MessageApproved(bytes32 indexed commandId, string sourceChain, string messageId, string sourceAddress, address indexed contractAddress, bytes32 indexed payloadHash)', ]; const iface = new ethers.utils.Interface(eventABI); -async function processMessageApprovedEvent(sourceChain, txHash, costAmount = "0", dryRun = false) { - apiEvent = await constructAPIEvent(sourceChain, txHash, costAmount); - console.log(apiEvent); +async function processMessageApprovedEvent(destinationChain, txHash, costAmount = '0', dryRun = false) { + apiEvent = await constructAPIEvent(destinationChain, txHash, costAmount); if (dryRun === true) { return; } - response = submitApproveEvent(apiEvent); - console.log(response); + response = await submitApproveEvent(apiEvent); + return { apiEvent, response }; } async function constructAPIEvent(destinationChain, txHash, costAmount) { @@ -33,15 +32,17 @@ async function constructAPIEvent(destinationChain, txHash, costAmount) { const receipt = await provider.getTransactionReceipt(txHash); if (!receipt) { - throw new Error('Transaction receipt not found'); + console.error('Transaction receipt not found'); + return; } // Find the relevant log const TOPIC = iface.getEventTopic('MessageApproved'); - const relevantLog = receipt.logs.find(log => log.topics[0] === TOPIC); + const relevantLog = receipt.logs.find((log) => log.topics[0] === TOPIC); if (!relevantLog) { - throw new Error('Relevant log not found'); + console.error('Relevant log not found'); + return; } // Decode the event data @@ -66,6 +67,7 @@ async function constructAPIEvent(destinationChain, txHash, costAmount) { cost: { amount: costAmount, }, + destinationChain, eventID, message: { destinationAddress: contractAddress, @@ -90,15 +92,23 @@ async function constructAPIEvent(destinationChain, txHash, costAmount) { } async function submitApproveEvent(apiEvent) { - destinationChain = apiEvent.destinationChain; - const response = await axios.post(`${GMP_API_URL}/chains/${destinationChain}/events`, { - events: [apiEvent], - }); - - console.log('API Response:', response.data); - return response.data; + try { + const response = await axios.post( + `${gmpAPIURL}/chains/${apiEvent.destinationChain}/events`, + { + events: [apiEvent], + }, + { + httpsAgent, + }, + ); + return response.data; + } catch (e) { + console.log('something went wrong', e); + return []; + } } module.exports = { processMessageApprovedEvent, -}; \ No newline at end of file +}; diff --git a/examples/amplifier/gmp-api/contract-call-event.js b/examples/amplifier/gmp-api/contract-call-event.js index f0bbbc83..7d9d87fe 100644 --- a/examples/amplifier/gmp-api/contract-call-event.js +++ b/examples/amplifier/gmp-api/contract-call-event.js @@ -1,26 +1,24 @@ const axios = require('axios'); const { ethers } = require('ethers'); -const { getConfig, getChainConfig } = require('../config.js'); +const { getConfig, getChainConfig } = require('../config/config.js'); const { GMP_API_URL } = getConfig(); // ABI for the ContractCall event const eventABI = [ - "event ContractCall(address indexed sender, string destinationChain, string destinationContractAddress, bytes32 indexed payloadHash, bytes payload)", + 'event ContractCall(address indexed sender, string destinationChain, string destinationContractAddress, bytes32 indexed payloadHash, bytes payload)', ]; const iface = new ethers.utils.Interface(eventABI); async function processContractCallEvent(sourceChain, txHash, dryRun = false) { apiEvent = await constructAPIEvent(sourceChain, txHash); - console.log(apiEvent); if (dryRun === true) { return; } response = submitContractCallEvent(apiEvent); - console.log(response); } async function constructAPIEvent(sourceChain, txHash) { @@ -38,7 +36,7 @@ async function constructAPIEvent(sourceChain, txHash) { // Find the relevant log const TOPIC = iface.getEventTopic('ContractCall'); - const relevantLog = receipt.logs.find(log => log.topics[0] === TOPIC); + const relevantLog = receipt.logs.find((log) => log.topics[0] === TOPIC); if (!relevantLog) { throw new Error('Relevant log not found'); @@ -98,4 +96,4 @@ async function submitContractCallEvent(apiEvent) { module.exports = { processContractCallEvent, -}; \ No newline at end of file +}; diff --git a/examples/amplifier/gmp-api/execute-event.js b/examples/amplifier/gmp-api/execute-event.js index 1a66d2af..78106455 100644 --- a/examples/amplifier/gmp-api/execute-event.js +++ b/examples/amplifier/gmp-api/execute-event.js @@ -1,28 +1,22 @@ const axios = require('axios'); const { ethers } = require('ethers'); -const { getConfig, getChainConfig } = require('../config.js'); +const { getConfig, getChainConfig } = require('../config/config.js'); -const { GMP_API_URL } = getConfig(); +const { gmpAPIURL, httpsAgent } = getConfig(); // ABI for the ContractCall and MessageExecuted events -const eventABI = [ - "event MessageExecuted(bytes32 indexed commandId)", -]; +const eventABI = ['event MessageExecuted(bytes32 indexed commandId)']; const iface = new ethers.utils.Interface(eventABI); -async function processMessageExecutedEvent(destinationChain, txHash, sourceChain, messageID, costAmount = "0", dryRun = false) { - console.log('message id', messageID); - +async function processMessageExecutedEvent(destinationChain, txHash, sourceChain, messageID, costAmount = '0', dryRun = false) { apiEvent = await constructMessageExecutedAPIEvent(destinationChain, txHash, sourceChain, messageID, costAmount); - console.log(apiEvent); if (dryRun === true) { return; } - response = submitMessageExecutedEvent(apiEvent); - console.log(response); + return submitMessageExecutedEvent(apiEvent); } async function constructMessageExecutedAPIEvent(destinationChain, txHash, sourceChain, messageID, costAmount) { @@ -34,15 +28,17 @@ async function constructMessageExecutedAPIEvent(destinationChain, txHash, source const receipt = await provider.getTransactionReceipt(txHash); if (!receipt) { - throw new Error('Transaction receipt not found'); + console.error('Transaction receipt not found'); + return; } // Find the relevant log const TOPIC = iface.getEventTopic('MessageExecuted'); - const relevantLog = receipt.logs.find(log => log.topics[0] === TOPIC); + const relevantLog = receipt.logs.find((log) => log.topics[0] === TOPIC); if (!relevantLog) { - throw new Error('Relevant log not found'); + console.error('Relevant log not found'); + return; } // Decode the event data @@ -62,6 +58,7 @@ async function constructMessageExecutedAPIEvent(destinationChain, txHash, source cost: { amount: costAmount, }, + destinationChain, eventID, messageID, meta: { @@ -82,15 +79,24 @@ async function constructMessageExecutedAPIEvent(destinationChain, txHash, source } async function submitMessageExecutedEvent(apiEvent) { - sourceChain = apiEvent.sourceChain; - const response = await axios.post(`${GMP_API_URL}/chains/${sourceChain}/events`, { - events: [apiEvent], - }); + try { + const response = await axios.post( + `${gmpAPIURL}/chains/${apiEvent.destinationChain}/events`, + { + events: [apiEvent], + }, + { + httpsAgent, + }, + ); - console.log('API Response:', response.data); - return response.data; + return response.data; + } catch (e) { + console.log('something went wrong', e); + return []; + } } module.exports = { processMessageExecutedEvent, -}; \ No newline at end of file +}; diff --git a/examples/amplifier/gmp-api/tasks.js b/examples/amplifier/gmp-api/tasks.js index b07d6bee..c2f9c184 100644 --- a/examples/amplifier/gmp-api/tasks.js +++ b/examples/amplifier/gmp-api/tasks.js @@ -1,13 +1,20 @@ const fs = require('fs'); const axios = require('axios'); const ethers = require('ethers'); -const { getConfig, getChainConfig } = require('../config.js'); +const { getConfig, getChainConfig } = require('../config/config.js'); +const { processMessageApprovedEvent: recordMessageApprovedEvent } = require('./approve-event.js'); +const { processMessageExecutedEvent: recordMessageExecutedEvent } = require('./execute-event.js'); +const AmplifierGMPTest = require('../../../artifacts/examples/amplifier/contracts/AmplifierGMPTest.sol/AmplifierGMPTest.json'); require('dotenv').config(); -const { gmpAPIURL } = getConfig(); -var dryRun = true; +const { gmpAPIURL, httpsAgent } = getConfig(); +let dryRun = false; -async function pollTasks(chainName, pollInterval = 10000, dryRunOpt = true) { +// This field is only relevant for EVM relaying. For EVM chains, commandID is still required in the 'execute' function on the destination chain +// Because the unifying identifier between APPROVE and EXECUTE events is the messageID, this mapping helps to record the relation between those events for a single GMP tx +const messageIdToCommandId = {}; + +async function pollTasks({ chainName, pollInterval, dryRunOpt }) { if (dryRunOpt) { console.log('Dry run enabled'); dryRun = true; @@ -15,95 +22,143 @@ async function pollTasks(chainName, pollInterval = 10000, dryRunOpt = true) { const chainConfig = getChainConfig(chainName); - setInterval(() => { - getNewTasks(chainConfig); + const intervalId = setInterval(async () => { + await getNewTasks(chainConfig, intervalId); }, pollInterval); } -async function getNewTasks(chainConfig) { +async function getNewTasks(chainConfig, intervalId) { latestTask = loadLatestTask(chainConfig.name); - var urlSuffix = ''; + let urlSuffix = ''; - if (latestTask !== "") { - urlSuffix = `?after=${latestTask}` + if (latestTask !== '') { + urlSuffix = `?after=${latestTask}`; } const url = `${gmpAPIURL}/chains/${chainConfig.name}/tasks${urlSuffix}`; - console.log('Polling tasks:', url); + console.log('Polling tasks on:', url); try { - const response = await axios.get(url); + const response = await axios({ + method: 'get', + url, + httpsAgent, + }); + const tasks = response.data.tasks; if (tasks.length === 0) { - console.log('No new tasks'); + console.log('No new tasks\n'); return; } - console.log('Tasks:', tasks); - for (const task of tasks) { - console.log('Processing task:', task.id); - - var payload; - var destinationAddress; - - if (task.type === 'GATEWAY_TX') { - console.log('found approve task'); - payload = decodePayload(task.task.executeData); - destinationAddress = chainConfig.gateway; - } else if (task.type === 'EXECUTE') { - console.log('found execute task'); - payload = decodePayload(task.task.payload); - destinationAddress = task.task.message.destinationAddress; - } else { - console.warn('Unknown task type:', task.type); - continue; - } - - await relayToChain(chainConfig.rpc, payload, destinationAddress); - - console.log('Task processed:', task.id); - saveLatestTask(chainConfig.name, task.id); + await processTask(task, chainConfig, intervalId); } } catch (error) { console.error('Error:', error.message); } } +async function processTask(task, chainConfig, intervalId) { + switch (task.type) { + case 'GATEWAY_TX': + await processApproval(task, chainConfig); + break; + case 'EXECUTE': + await processExecute(task, chainConfig, intervalId); + break; + default: + console.warn('Unknown task type:', task.type); + break; + } + + console.log('Task processed:', task.id, '\n'); + saveLatestTask(chainConfig.name, task.id); +} + +async function processApproval(task, chainConfig) { + console.log('Processing approve task', task.id); + const payload = decodePayload(task.task.executeData); + const destinationAddress = chainConfig.gateway; + + const destTxRecept = await relayApproval(chainConfig.rpc, payload, destinationAddress); + const { apiEvent } = await recordMessageApprovedEvent(chainConfig.name, destTxRecept.transactionHash, '0'); + messageIdToCommandId[apiEvent.message.messageID] = apiEvent.meta.commandID; +} + +async function processExecute(task, chainConfig, intervalId) { + console.log('Processing execute task', task.id); + const payload = decodePayload(task.task.payload); + const destinationAddress = task.task.message.destinationAddress; + const { messageID, sourceAddress, sourceChain } = task.task.message; + + const destTxRecept = await relayExecution(chainConfig.rpc, payload, destinationAddress, { + messageID, + sourceAddress, + sourceChain, + }); + await recordMessageExecutedEvent(chainConfig.name, destTxRecept.transactionHash, sourceChain, messageID, '0'); + clearInterval(intervalId); + console.log('Polling interval cleared after EXECUTE task completed'); +} + function saveLatestTask(chainName, latestTask) { - fs.writeFileSync(`./latestTask-${chainName}.json`, JSON.stringify(latestTask)); + fs.writeFileSync(`./examples/amplifier/config/latestTasks/latestTask-${chainName}.json`, JSON.stringify(latestTask)); } function loadLatestTask(chainName) { try { - return fs.readFileSync(`./latestTask-${chainName}.json`, 'utf8'); + return fs.readFileSync(`./examples/amplifier/config/latestTasks/latestTask-${chainName}.json`, 'utf8'); } catch (error) { - return ""; + return ''; } } -async function relayToChain(rpc, payload, destinationAddress) { +async function relayApproval(rpc, payload, destinationAddress) { if (dryRun) { - console.log('Destination:', destinationAddress, 'Payload:', payload); + console.log('dryrun mode'); return; } const provider = new ethers.providers.JsonRpcProvider(rpc); - const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider); + const wallet = new ethers.Wallet(process.env.EVM_PRIVATE_KEY, provider); - console.log('Relaying payload:', payload); + console.log('Relaying approval tx to chain'); const tx = await wallet.sendTransaction({ to: destinationAddress, data: payload, + gasLimit: ethers.utils.hexlify(500000), }); - console.log(`Transaction sent: ${tx.hash}`); - await tx.wait(); + const destTxRecept = await tx.wait(); + + console.log('Transaction confirmed: ', destTxRecept.transactionHash); + return destTxRecept; +} + +async function relayExecution(rpc, payload, destinationAddress, { messageID, sourceAddress, sourceChain }) { + if (dryRun) { + console.log('dryrun mode'); + return; + } + + const provider = new ethers.providers.JsonRpcProvider(rpc); + const wallet = new ethers.Wallet(process.env.EVM_PRIVATE_KEY, provider); + const commandID = messageIdToCommandId[messageID]; + + console.log('Relaying execution tx to chain'); + + const executable = new ethers.Contract(destinationAddress, AmplifierGMPTest.abi, wallet); + + const tx = await executable.execute(commandID, sourceChain, sourceAddress, payload); + + const destTxRecept = await tx.wait(); - console.log('Transaction confirmed'); + console.log('Transaction confirmed: ', destTxRecept.transactionHash); + return destTxRecept; } function decodePayload(executeData) { @@ -113,4 +168,4 @@ function decodePayload(executeData) { module.exports = { pollTasks, -}; \ No newline at end of file +}; diff --git a/examples/amplifier/grpc/client.js b/examples/amplifier/grpc/client.js deleted file mode 100644 index bc330767..00000000 --- a/examples/amplifier/grpc/client.js +++ /dev/null @@ -1,17 +0,0 @@ -const grpc = require('@grpc/grpc-js'); -const protoLoader = require('@grpc/proto-loader'); -const getConfig = require('../config'); - -function newClient() { - const packageDefinition = protoLoader.loadSync("amplifier.proto"); - const protoDescriptor = grpc.loadPackageDefinition(packageDefinition); - const amplifierService = protoDescriptor.axelar.amplifier.v1beta1.Amplifier; - - const { serverHOST, serverPort } = getConfig(); - - console.log(`Connecting to server at ${serverHOST}:${serverPort}`); - - return new amplifierService(`${serverHOST}:${serverPort}`, grpc.credentials.createInsecure()); -} - -module.exports = newClient; \ No newline at end of file diff --git a/examples/amplifier/index.js b/examples/amplifier/index.js new file mode 100644 index 00000000..5e6d4d71 --- /dev/null +++ b/examples/amplifier/index.js @@ -0,0 +1,39 @@ +const { program } = require('commander'); +const { sleep, gmp, deploy } = require('./utils'); + +const { getConfig } = require('./config/config.js'); +const { pollTasks } = require('./gmp-api/tasks'); +require('dotenv').config(); + +const config = getConfig().chains; + +program.option('-s, --sourceChain ', 'source chain', 'avalanche-fuji'); +program.option('-d, --destinationChain ', 'destination chain', 'xrpl-evm-sidechain'); +program.option('-m, --message ', 'message string to send', 'hello'); + +program.parse(); + +const options = program.opts(); + +const main = async () => { + const { sourceChain, destinationChain, message } = options; + + const srcContractDeployment = await deploy(sourceChain); + const destContract = await deploy(destinationChain); + + console.log('Contracts deployed, waiting 10 seconds for txs to propagate'); + await sleep(10000); + + await gmp( + { + destinationChain, + sourceChain, + message, + destinationContractAddress: destContract.address, + srcContractAddress: srcContractDeployment.address, + }, + config, + ); +}; + +main(null).then(() => pollTasks({ chainName: options.destinationChain, pollInterval: 10000, dryRunOpt: false })); diff --git a/examples/amplifier/utils/deployContract.js b/examples/amplifier/utils/deployContract.js new file mode 100644 index 00000000..905ace7b --- /dev/null +++ b/examples/amplifier/utils/deployContract.js @@ -0,0 +1,17 @@ +const { ContractFactory, Wallet, providers } = require('ethers'); +const AmplifierGMPTest = require('../../../artifacts/examples/amplifier/contracts/AmplifierGMPTest.sol/AmplifierGMPTest.json'); +const { getConfig } = require('../config/config'); + +const config = getConfig().chains; + +const deploy = async (chainName) => { + const chain = config.find((chain) => chain.name === chainName); + const signer = new Wallet(process.env.EVM_PRIVATE_KEY, new providers.JsonRpcProvider(chain.rpc)); + const factory = new ContractFactory(AmplifierGMPTest.abi, AmplifierGMPTest.bytecode, signer); + console.log(`Deploying ${AmplifierGMPTest.contractName} on ${chain.name}`); + return factory.deploy(chain.gateway); +}; + +module.exports = { + deploy, +}; diff --git a/examples/amplifier/utils/gmp.js b/examples/amplifier/utils/gmp.js new file mode 100644 index 00000000..5c98b0fb --- /dev/null +++ b/examples/amplifier/utils/gmp.js @@ -0,0 +1,27 @@ +const { providers, Wallet, ethers } = require('ethers'); +const AmplifierGMPTest = require('../../../artifacts/examples/amplifier/contracts/AmplifierGMPTest.sol/AmplifierGMPTest.json'); +const { getChainConfig } = require('../config/config.js'); +const { processContractCallEvent } = require('../gmp-api/contract-call-event.js'); +const { sleep } = require('./sleep.js'); + +const gmp = async ({ sourceChain, destinationChain, message, destinationContractAddress, srcContractAddress }) => { + const provider = new providers.JsonRpcProvider(getChainConfig(sourceChain).rpc); + const wallet = new Wallet(process.env.EVM_PRIVATE_KEY, provider); + const srcContract = new ethers.Contract(srcContractAddress, AmplifierGMPTest.abi, wallet); + + try { + const tx = await srcContract.setRemoteValue(destinationChain, destinationContractAddress, message); + const transactionReceipt = await tx.wait(); + console.log(`Initiated GMP event on ${sourceChain}, tx hash: ${transactionReceipt.transactionHash}`); + + await sleep(10000); // allow for gmp event to propagate before triggering indexing + + processContractCallEvent(sourceChain, transactionReceipt.transactionHash, true); + } catch (error) { + throw new Error(`Error calling contract: ${error}`); + } +}; + +module.exports = { + gmp, +}; diff --git a/examples/amplifier/utils/index.js b/examples/amplifier/utils/index.js new file mode 100644 index 00000000..195b2760 --- /dev/null +++ b/examples/amplifier/utils/index.js @@ -0,0 +1,9 @@ +const { deploy } = require('./deployContract'); +const { gmp } = require('./gmp'); +const { sleep } = require('./sleep'); + +module.exports = { + deploy, + sleep, + gmp, +}; diff --git a/examples/amplifier/utils/sleep.js b/examples/amplifier/utils/sleep.js new file mode 100644 index 00000000..375ec017 --- /dev/null +++ b/examples/amplifier/utils/sleep.js @@ -0,0 +1,8 @@ +const sleep = (ms) => { + console.log(`Sleeping for ${ms}\n`); + return new Promise((resolve) => setTimeout(resolve, ms)); +}; + +module.exports = { + sleep, +}; diff --git a/hardhat.config.js b/hardhat.config.js index 7e5be423..27bec87d 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -1,6 +1,6 @@ require('hardhat-gas-reporter'); require('solidity-coverage'); -require("@nomicfoundation/hardhat-chai-matchers") +require('@nomicfoundation/hardhat-chai-matchers'); /** * @type import('hardhat/config').HardhatUserConfig