From 40125b936bf3da377fd255f250a23acded6f6d1e Mon Sep 17 00:00:00 2001 From: Canh Trinh Date: Thu, 26 Sep 2024 18:15:57 -0400 Subject: [PATCH 1/9] feat: update amplifier example with gmp call --- .env.example | 1 + examples/amplifier/README.md | 369 ++++++++---------- examples/amplifier/chains.json | 27 +- examples/amplifier/config.js | 17 +- .../amplifier/gmp-api/contract-call-event.js | 8 +- examples/amplifier/index.js | 25 ++ examples/amplifier/interface/Event.ts | 23 ++ .../utils/extractEventFromReceipt.js | 76 ++++ examples/amplifier/utils/gmp.js | 33 ++ examples/amplifier/utils/sleep.js | 8 + 10 files changed, 364 insertions(+), 223 deletions(-) create mode 100644 examples/amplifier/index.js create mode 100644 examples/amplifier/interface/Event.ts create mode 100644 examples/amplifier/utils/extractEventFromReceipt.js create mode 100644 examples/amplifier/utils/gmp.js create mode 100644 examples/amplifier/utils/sleep.js diff --git a/.env.example b/.env.example index 47bf1962..8324d1ed 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,2 @@ EVM_PRIVATE_KEY=YOUR_PRIVATE_KEY_HERE +GMP_API_URL= \ No newline at end of file diff --git a/examples/amplifier/README.md b/examples/amplifier/README.md index a05142aa..65a48e9c 100644 --- a/examples/amplifier/README.md +++ b/examples/amplifier/README.md @@ -1,251 +1,210 @@ ## 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: https://bright-ambert-2bd.notion.site/Amplifier-GMP-API-EXTERNAL-911e740b570b4017826c854338b906c8 ## Setup 1. Clone this repo. 2. Install dependencies: - ```bash - npm install - ``` + ```bash + npm install + ``` 3. Go to amplifier examples directory - ``` - cd examples/amplifier - ``` + ``` + cd examples/amplifier + ``` 4. Copy `.env.example` into `.env` and set up the following environment variables: - ```bash - HOST=... - PORT=... - ``` - -## Generic endpoints - -There are three endpoints that can be used for generic commands and events: - -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 - -### `broadcast` - -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: - -```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' } -``` - -### `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. - -```bash -node amplifier get-receipt --receipt-id -``` - -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 - -For example: + ```bash + GMP_API_URL=... + ``` + +## GMP example + +### Make a Contract Call to the source chain + +```typescript +import { providers, Wallet, ethers } from 'ethers'; +import { config } from './config'; +require('dotenv').config(); + +const gmpCall = async ({ srcGatewayAddress, destinationChain, message, destinationContractAddress }) => { + const wallet = new Wallet(process.env.PRIVATE_KEY as string, new providers.JsonRpcProvider(config.avalanche.rpcUrl)); + const contractABI = ['function callContract(string destinationChain, string destinationContractAddress, bytes payload)']; + const contract = new ethers.Contract(srcGatewayAddress, contractABI, wallet); + // const destinationChain = "ethereum-sepolia"; + const payload = ethers.utils.hexlify(ethers.utils.toUtf8Bytes('hi')); + const payloadHash = ethers.utils.keccak256(payload); + + try { + const tx = await contract.callContract(destinationChain, destinationContractAddress || (await wallet.getAddress()), payload); + console.log('Transaction hash:', tx.hash); + + const transactionReceipt = await tx.wait(); + + return { + transactionReceipt, + payloadHash: payloadHash.slice(2), + payload, + }; + } catch (error) { + console.error('Error calling contract:', error); + } +}; -```bash -# succesful broadcast -$ node amplifier get-receipt -r 53992509aa3267cc7b2bb8a1bfb21d03 -Getting receipt with id: 53992509aa3267cc7b2bb8a1bfb21d03 -Connecting to server at localhost:50051 -Receipt: -87AECB2151F80616DA2CD237E9EE38DC9558FFBBC93A51DF3B3BE8BB89F0A5EF +// parameters below use `avalanche` from `devnet-amplifier` +gmpCall(`0xF128c84c3326727c3e155168daAa4C0156B87AD1`); ``` -```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', +Here’s how a Contract Call looks on Ethereum Sepolia ([0x9b447614be654eeea0c5de0319b3f2c243ab45bebd914a1f7319f4bb599d8968](https://sepolia.etherscan.io/tx/0x9b447614be654eeea0c5de0319b3f2c243ab45bebd914a1f7319f4bb599d8968)). Notice that in the logs there’s a `ContractCall` event emitted. + + + +### Make an Event API Call + +Now, we are going to use the content of the Contract Call transaction, and make a `Call Event` request to the GMP API (see how Call Event is structured [here](https://www.notion.so/Amplifier-GMP-API-EXTERNAL-911e740b570b4017826c854338b906c8?pvs=21)). + +```json +{ + "events": [ + { + "destinationChain": "test-avalanche", + "eventID": "0x9b447614be654eeea0c5de0319b3f2c243ab45bebd914a1f7319f4bb599d8968-1", + "message": { + "destinationAddress": "0xE8E348fA7b311d6E308b1A162C3ec0172B37D1C1", + "messageID": "0x9b447614be654eeea0c5de0319b3f2c243ab45bebd914a1f7319f4bb599d8968-1", + "payloadHash": "Y2YO3UuCRRackxbPYX9dWmNTYcnAMOommp9g4ydb3i4=", + "sourceAddress": "0x9e3e785dD9EA3826C9cBaFb1114868bc0e79539a", + "sourceChain": "test-sepolia" + }, + "meta": { + "finalized": true, + "fromAddress": "0xba76c6980428A0b10CFC5d8ccb61949677A61233", + "timestamp": "2024-09-11T13:32:48Z", + "txID": "0x9b447614be654eeea0c5de0319b3f2c243ab45bebd914a1f7319f4bb599d8968" + }, + "payload": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASaGVsbG8gdGVzdC1zZXBvbGlhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "type": "CALL" + } + ] } ``` -```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", -} -``` +Here’s how to retrieve these values from given the Contract Call transaction on the source chain: -### `subscribe-to-wasm-events` +- `destinationChain`: the destination chain you used to make the contract call +- `eventID`: the transaction hash of the submitted contract call + the index of the event in the transaction events (in the example above, the index is 1) -To get all wasm events emitted on the Axelar network, run: + ```bash + TOPIC="0x30ae6cc78c27e651745bf2ad08a11de83910ac1e347a52f7ac898c0fbef94dae" + TX_ID="0x9b447614be654eeea0c5de0319b3f2c243ab45bebd914a1f7319f4bb599d8968" -```bash -node amplifier subscribe-to-wasm-events -``` + receipt=$(curl -s $RPC -X POST -H "Content-Type: application/json" \ + --data '{"method":"eth_getTransactionReceipt","params":["0x9b447614be654eeea0c5de0319b3f2c243ab45bebd914a1f7319f4bb599d8968"],"id":1,"jsonrpc":"2.0"}') -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: + EVENT_INDEX=$(cat receipt | jq --arg TOPIC "$TOPIC" '.result.logs | map(.topics[0] == $TOPIC) | index(true)') -``` -$ 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 } -} -``` + echo "eventID: $TX_ID-$EVENT_INDEX" + ``` -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. +- `destinationAddress`: the destination address you used to make the contract call +- `messageID`: in one-way Contract Calls, the message identifier is equivalent to the `eventID`. In two-way calls, the `messageID` is the `eventID` of the initial Contract Call at the first segment of the multi-hop chain. +- `payloadHash`: This is available as the third topic of the event with the specific topic ID. You can extract it and encode it in base64: -## General Message Passing + ```bash + TOPIC="0x30ae6cc78c27e651745bf2ad08a11de83910ac1e347a52f7ac898c0fbef94dae" -The following endpoints are available to facilitate GMP calls: + # get 3rd argument of topic, and stript starting "0x" + payloadHashHex=$(echo $receipt | jq -r ".result.logs[] | select(.topics[0] == \"$TOPIC\") | .topics[2]" | cut -c 3-) -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 + # encode to base64 + payloadHash=$(echo -n "$payloadHashHex" | xxd -r -p | base64) -### `verify` + echo "payloadHash: $payloadHash" + ``` -Given a transaction relayed on the source chain, the `verify` command is called as follows: +- `sourceAddress`: This is the address that initiated the contract call. It can be extracted from the second topic of the event: -``` 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: + ```bash + TOPIC="0x30ae6cc78c27e651745bf2ad08a11de83910ac1e347a52f7ac898c0fbef94dae" - ![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 -``` + # get the 2nd argument of the topic and keep the address (0x | 24 zeros | address ) + sourceAddress=$(cat receipt | jq -r --arg TOPIC "$TOPIC" '.result.logs[] | select(.topics[0] == $TOPIC) | .topics[1]' | cut -c 27-) -### `subscribe-to-approvals` + echo "sourceAddress: 0x$sourceAddress" + ``` -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`: +- `sourceChain`: This is the alias of the source chain as registered in amplifier. +- `finalized`: Whether or not the transaction is finalized on the source chain. It’s not going to be processes until it’s flagged with `finalized: true` by the caller. +- `fromAddress`(optional): This is the address that signed the transaction. It can be extracted directly from the receipt: -```bash -node amplifier subscribe-to-approvals \ ---chain fantom \ ---start-height # optional -``` + ```bash + fromAddress=$(echo $receipt | jq -r '.result.from') -- `chain` -- the destination chain -- `start-height` (optional) -- start height [0 = latest] similar to `subscribe-to-wasm-events` + echo "fromAddress: $fromAddress" + ``` -For example: +- `timestamp`(optional): This is not directly available in the receipt. It typically comes from the block timestamp. You'd need to fetch the block details to get this: -```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 ---- -``` + ```bash + # Extract block number (in hex) + blockNumber=$(echo $receipt | jq -r '.result.blockNumber') -### `save-payload` + # Fetch block data + blockData=$(curl -s $RPC -X POST -H "Content-Type: application/json" \ + --data "{\"method\":\"eth_getBlockByNumber\",\"params\":[\"$blockNumber\", false],\"id\":1,\"jsonrpc\":\"2.0\"}") -To save a payload that was submitted by the transaction on the source chain, use `save-payload`: + # Extract timestamp (in hex) and convert to decimal + timestamp=$(echo $blockData | jq -r '.result.timestamp') + timestampDec=$((16#${timestamp:2})) # Remove '0x' prefix and convert hex to decimal -```bash -$ node amplifier save-payload --payload 00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000f68656c6c6f206176616c616e63686500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -Saving payload 00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000f68656c6c6f206176616c616e63686500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -Connecting to server at localhost:50051 -Payload hash: -0xa9b070ad799e19f1166fdbf4524b684f8026df510fe6a7770f949ad54047098c -``` + formattedTimestamp=$(date -r $timestampDec -u +'%Y-%m-%dT%H:%M:%SZ') -- `payload` -- the payload + echo "timestamp: $formattedTimestamp" + ``` -### `get-payload` +- `txID`(optional): This is the transaction hash +- `payload`: This is the “payload” field of the contract call, encoded in base64. To extract it, you need to decode the data field of the topic. +- `type`: This must be `CALL` for contract calls. -To get the payload that was submitted by the transaction on the source chain, use `get-payload`: +After the event payload is constructed, submit it to the API as such: ```bash -$ node amplifier get-payload --hash 0xa9b070ad799e19f1166fdbf4524b684f8026df510fe6a7770f949ad54047098c -Getting payload for payload hash a9b070ad799e19f1166fdbf4524b684f8026df510fe6a7770f949ad54047098c -Connecting to server at localhost:50051 -Payload: -00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000f68656c6c6f206176616c616e63686500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +curl -X POST $GMP_API_URL/test-avalanche/events \ + -H "Content-Type: application/json" \ + -d '{ + "events": [ + { + "destinationChain": "test-avalanche", + "eventID": "0x9b447614be654eeea0c5de0319b3f2c243ab45bebd914a1f7319f4bb599d8968-1", + "message": { + "destinationAddress": "0xE8E348fA7b311d6E308b1A162C3ec0172B37D1C1", + "messageID": "0x9b447614be654eeea0c5de0319b3f2c243ab45bebd914a1f7319f4bb599d8968-1", + "payloadHash": "Y2YO3UuCRRackxbPYX9dWmNTYcnAMOommp9g4ydb3i4=", + "sourceAddress": "0x9e3e785dD9EA3826C9cBaFb1114868bc0e79539a", + "sourceChain": "test-sepolia" + }, + "meta": { + "finalized": true, + "fromAddress": "0xba76c6980428A0b10CFC5d8ccb61949677A61233", + "timestamp": "2024-09-11T13:32:48Z", + "txID": "0x9b447614be654eeea0c5de0319b3f2c243ab45bebd914a1f7319f4bb599d8968" + }, + "payload": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASaGVsbG8gdGVzdC1zZXBvbGlhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "type": "CALL" + } + ] +}' ``` -- `hash` -- the payload hash +### Wait for the task to get published -![Payload hash](images/payload-hash.png) \ No newline at end of file +Once the event is submitted an processed, a new `GATEWAY_TX` task will be published. diff --git a/examples/amplifier/chains.json b/examples/amplifier/chains.json index 18a802c4..80047f66 100644 --- a/examples/amplifier/chains.json +++ b/examples/amplifier/chains.json @@ -1,12 +1,17 @@ [ - { - "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 + { + "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" + }, + { + "name": "avalanche-fuji", + "rpc": "https://rpc.ankr.com/avalanche_fuji", + "gateway": "0xF128c84c3326727c3e155168daAa4C0156B87AD1" + } +] diff --git a/examples/amplifier/config.js b/examples/amplifier/config.js index 04c45d10..21ea862e 100644 --- a/examples/amplifier/config.js +++ b/examples/amplifier/config.js @@ -8,11 +8,19 @@ dotenv.config(); // 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 chains = { + 'avalanche-fuji': { + rpcUrl: 'https://rpc.ankr.com/avalanche_fuji', + externalGateway: '0xF128c84c3326727c3e155168daAa4C0156B87AD1', + id: 'avalanche-fuji', + }, }; function getConfig() { @@ -25,6 +33,7 @@ function getConfig() { serverHOST, serverPort, gmpAPIURL, + chains, }; } @@ -33,7 +42,7 @@ 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/gmp-api/contract-call-event.js b/examples/amplifier/gmp-api/contract-call-event.js index f0bbbc83..c262d33b 100644 --- a/examples/amplifier/gmp-api/contract-call-event.js +++ b/examples/amplifier/gmp-api/contract-call-event.js @@ -6,7 +6,7 @@ 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); @@ -38,7 +38,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'); @@ -47,6 +47,8 @@ async function constructAPIEvent(sourceChain, txHash) { // Decode the event data const decodedLog = iface.parseLog(relevantLog); + console.log({ decodedLog }); + // Extract data from the decoded log const eventIndex = receipt.logs.indexOf(relevantLog); const eventID = `${txHash}-${eventIndex}`; @@ -98,4 +100,4 @@ async function submitContractCallEvent(apiEvent) { module.exports = { processContractCallEvent, -}; \ No newline at end of file +}; diff --git a/examples/amplifier/index.js b/examples/amplifier/index.js new file mode 100644 index 00000000..6af7ed64 --- /dev/null +++ b/examples/amplifier/index.js @@ -0,0 +1,25 @@ +const { sleep } = require('./utils/sleep'); +const { getConfig } = require('./config'); +const { gmp } = require('./utils/gmp'); +const { processContractCallEvent } = require('./gmp-api/contract-call-event'); +require('dotenv').config(); + +const config = getConfig().chains; + +const params = { + srcGatewayAddress: config['avalanche-fuji'].externalGateway, + srcChain: config['avalanche-fuji'].id, + destinationChain: 'ethereum-sepolia', + message: 'hi', + destinationContractAddress: null, +}; + +const main = async () => { + const { transactionReceipt } = await gmp(params, config); + + await sleep(2000); + + processContractCallEvent(params.srcChain, transactionReceipt.transactionHash, true); +}; + +main(null); diff --git a/examples/amplifier/interface/Event.ts b/examples/amplifier/interface/Event.ts new file mode 100644 index 00000000..760ce3e4 --- /dev/null +++ b/examples/amplifier/interface/Event.ts @@ -0,0 +1,23 @@ +export interface Message { + destinationAddress: string; + messageID: string; + payloadHash: string; + sourceAddress: string; + sourceChain: string; +} + +export interface Meta { + finalized: boolean; + fromAddress: string; + timestamp: string; + txID: string; +} + +export interface Event { + destinationChain: string; + eventID: string; + message: Message; + meta: Meta; + payload: string; + type: string; +} diff --git a/examples/amplifier/utils/extractEventFromReceipt.js b/examples/amplifier/utils/extractEventFromReceipt.js new file mode 100644 index 00000000..76f7f95b --- /dev/null +++ b/examples/amplifier/utils/extractEventFromReceipt.js @@ -0,0 +1,76 @@ +const { providers, ethers } = require('ethers'); +const { getConfig } = require('../config'); + +const config = getConfig().chains; + +function getCallContractEventIndex(transactionReceipt) { + const callContractEventSignature = 'ContractCall(address,string,string,bytes32,bytes)'; + const callContractTopic = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(callContractEventSignature)); + + for (let i = 0; i < transactionReceipt.logs.length; i++) { + const log = transactionReceipt.logs[i]; + + if (log.topics[0] === callContractTopic) { + return log.logIndex; + } + } + + return -1; +} + +async function extractEventFromReceipt(receipt, params) { + const contractABI = [ + 'event ContractCall(address indexed sender, string destinationChain, string destinationContractAddress, bytes32 indexed payloadHash, bytes payload)', + ]; + + const iface = new ethers.utils.Interface(contractABI); + + const eventIndex = getCallContractEventIndex(receipt); + const log = receipt.logs[eventIndex]; + + const decodedData = iface.decodeEventLog('ContractCall', log.data, log.topics); + + const provider = new providers.JsonRpcProvider(config['avalanche-fuji'].rpcUrl); + + const block = await provider.getBlock(receipt.blockNumber); + + const destinationChain = decodedData.destinationChain; + const eventID = `${receipt.transactionHash}-${eventIndex}`; + const destinationAddress = decodedData.destinationContractAddress; + const messageID = eventID; + const payloadHashBase64 = Buffer.from(log.topics[2].startsWith('0x') ? log.topics[2].slice(2) : log.topics[2], 'hex').toString( + 'base64', + ); + const payloadBase64 = Buffer.from(log.data.startsWith('0x') ? log.data.slice(2) : log.data, 'hex').toString('base64'); + const sourceAddress = receipt.from; + const sourceChain = params.srcChain; + + const meta = { + finalized: true, + fromAddress: receipt.from, + timestamp: block.timestamp, + txID: receipt.transactionHash, + }; + + // Create the final event object + const event = { + destinationChain, + eventID, + message: { + destinationAddress, + messageID, + payloadHash: payloadHashBase64, + sourceAddress, + sourceChain, + }, + meta, + payload: payloadBase64, + type: 'CALL', + }; + + return event; +} + +module.exports = { + extractEventFromReceipt, +}; diff --git a/examples/amplifier/utils/gmp.js b/examples/amplifier/utils/gmp.js new file mode 100644 index 00000000..a1b5d2d4 --- /dev/null +++ b/examples/amplifier/utils/gmp.js @@ -0,0 +1,33 @@ +const { providers, Wallet, ethers } = require('ethers'); + +const contractABI = [ + 'event ContractCall(address sender, string destinationChain, string destinationContractAddress, bytes32 payloadHash, bytes payload)', + 'function callContract(string destinationChain, string destinationContractAddress, bytes payload)', +]; + +const gmp = async ({ srcGatewayAddress, destinationChain, message, destinationContractAddress }, config) => { + const wallet = new Wallet(process.env.EVM_PRIVATE_KEY, new providers.JsonRpcProvider(config['avalanche-fuji'].rpcUrl)); + + const contract = new ethers.Contract(srcGatewayAddress, contractABI, wallet); + const payload = ethers.utils.hexlify(ethers.utils.toUtf8Bytes(message)); + const payloadHash = ethers.utils.keccak256(payload); + + try { + const tx = await contract.callContract(destinationChain, destinationContractAddress || (await wallet.getAddress()), payload); + const transactionReceipt = await tx.wait(); + + console.log({ transactionReceipt }); + + return { + transactionReceipt, + payloadHash: payloadHash.slice(2), + payload, + }; + } catch (error) { + throw new Error(`Error calling contract: ${error}`); + } +}; + +module.exports = { + gmp, +}; diff --git a/examples/amplifier/utils/sleep.js b/examples/amplifier/utils/sleep.js new file mode 100644 index 00000000..cc0c12cb --- /dev/null +++ b/examples/amplifier/utils/sleep.js @@ -0,0 +1,8 @@ +const sleep = (ms) => { + console.log(`Sleeping for ${ms}`); + return new Promise((resolve) => setTimeout(resolve, ms)); +}; + +module.exports = { + sleep, +}; From f1e72ef42d161e05d07075343984b29a48857940 Mon Sep 17 00:00:00 2001 From: Canh Trinh Date: Thu, 26 Sep 2024 22:37:40 -0400 Subject: [PATCH 2/9] chore: code cleanup --- examples/amplifier/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/amplifier/index.js b/examples/amplifier/index.js index 6af7ed64..09e1aede 100644 --- a/examples/amplifier/index.js +++ b/examples/amplifier/index.js @@ -2,6 +2,7 @@ const { sleep } = require('./utils/sleep'); const { getConfig } = require('./config'); const { gmp } = require('./utils/gmp'); const { processContractCallEvent } = require('./gmp-api/contract-call-event'); +const { pollTasks } = require('./gmp-api/tasks'); require('dotenv').config(); const config = getConfig().chains; @@ -22,4 +23,5 @@ const main = async () => { processContractCallEvent(params.srcChain, transactionReceipt.transactionHash, true); }; -main(null); +pollTasks(params.srcChain); +// main(null); From 2e10e31b38275acff7afede05fd166847093dcbe Mon Sep 17 00:00:00 2001 From: Canh Trinh Date: Thu, 26 Sep 2024 22:40:59 -0400 Subject: [PATCH 3/9] chore: deleting unused v1 code --- examples/amplifier/amplifier.js | 92 +++++--------- examples/amplifier/amplifier.proto | 117 ------------------ 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/grpc/client.js | 17 --- 7 files changed, 30 insertions(+), 329 deletions(-) delete mode 100644 examples/amplifier/amplifier.proto 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 diff --git a/examples/amplifier/amplifier.js b/examples/amplifier/amplifier.js index 92f89968..630dc17a 100644 --- a/examples/amplifier/amplifier.js +++ b/examples/amplifier/amplifier.js @@ -1,11 +1,7 @@ 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'); @@ -13,17 +9,9 @@ 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") + .requiredOption('-r, --receipt-id ', 'The id of the receipt') .action((options) => { getReceipt(options.receiptId); }); @@ -42,77 +30,57 @@ program 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") + .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)); + .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") + .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)); + .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") + .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) + 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)); + .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") + .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 +program.parse(); 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/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/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 From f9358726fa575e2870b895b93c65e62f64ecfaff Mon Sep 17 00:00:00 2001 From: Canh Trinh Date: Thu, 26 Sep 2024 23:39:22 -0400 Subject: [PATCH 4/9] feat: working polling --- .gitignore | 3 +++ examples/amplifier/chains.json | 5 ++++ examples/amplifier/config.js | 11 +++++++++ examples/amplifier/gmp-api/tasks.js | 38 ++++++++++++++++++++--------- examples/amplifier/index.js | 2 +- 5 files changed, 46 insertions(+), 13 deletions(-) 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/chains.json b/examples/amplifier/chains.json index 80047f66..ba1a6596 100644 --- a/examples/amplifier/chains.json +++ b/examples/amplifier/chains.json @@ -13,5 +13,10 @@ "name": "avalanche-fuji", "rpc": "https://rpc.ankr.com/avalanche_fuji", "gateway": "0xF128c84c3326727c3e155168daAa4C0156B87AD1" + }, + { + "name": "xrpl-evm-sidechain", + "rpc": "", + "gateway": "" } ] diff --git a/examples/amplifier/config.js b/examples/amplifier/config.js index 21ea862e..71f048c6 100644 --- a/examples/amplifier/config.js +++ b/examples/amplifier/config.js @@ -5,6 +5,10 @@ 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 @@ -21,6 +25,11 @@ const chains = { externalGateway: '0xF128c84c3326727c3e155168daAa4C0156B87AD1', id: 'avalanche-fuji', }, + 'xrpl-evm-sidechain': { + rpcUrl: '', + externalGateway: '', + id: 'xrpl-evm-sidechain', + }, }; function getConfig() { @@ -34,6 +43,8 @@ function getConfig() { serverPort, gmpAPIURL, chains, + cert, + key, }; } diff --git a/examples/amplifier/gmp-api/tasks.js b/examples/amplifier/gmp-api/tasks.js index b07d6bee..1ffa8d8c 100644 --- a/examples/amplifier/gmp-api/tasks.js +++ b/examples/amplifier/gmp-api/tasks.js @@ -1,13 +1,14 @@ const fs = require('fs'); const axios = require('axios'); +const https = require('https'); const ethers = require('ethers'); const { getConfig, getChainConfig } = require('../config.js'); require('dotenv').config(); -const { gmpAPIURL } = getConfig(); +const { gmpAPIURL, cert, key } = getConfig(); var dryRun = true; -async function pollTasks(chainName, pollInterval = 10000, dryRunOpt = true) { +async function pollTasks({ chainName, pollInterval, dryRunOpt, approveCb, executeCb }) { if (dryRunOpt) { console.log('Dry run enabled'); dryRun = true; @@ -16,17 +17,17 @@ async function pollTasks(chainName, pollInterval = 10000, dryRunOpt = true) { const chainConfig = getChainConfig(chainName); setInterval(() => { - getNewTasks(chainConfig); + getNewTasks(chainConfig, approveCb, executeCb); }, pollInterval); } -async function getNewTasks(chainConfig) { +async function getNewTasks(chainConfig, approveCb, executeCb) { latestTask = loadLatestTask(chainConfig.name); var urlSuffix = ''; - if (latestTask !== "") { - urlSuffix = `?after=${latestTask}` + if (latestTask !== '') { + urlSuffix = `?after=${latestTask}`; } const url = `${gmpAPIURL}/chains/${chainConfig.name}/tasks${urlSuffix}`; @@ -34,7 +35,16 @@ async function getNewTasks(chainConfig) { console.log('Polling tasks:', url); try { - const response = await axios.get(url); + const response = await axios({ + method: 'get', + url, + httpsAgent: new https.Agent({ + cert, + key, + rejectUnauthorized: false, + }), + }); + const tasks = response.data.tasks; if (tasks.length === 0) { @@ -49,21 +59,24 @@ async function getNewTasks(chainConfig) { var payload; var destinationAddress; + var cb; if (task.type === 'GATEWAY_TX') { console.log('found approve task'); payload = decodePayload(task.task.executeData); destinationAddress = chainConfig.gateway; + cb = approveCb; } else if (task.type === 'EXECUTE') { console.log('found execute task'); payload = decodePayload(task.task.payload); destinationAddress = task.task.message.destinationAddress; + cb = executeCb; } else { console.warn('Unknown task type:', task.type); continue; } - await relayToChain(chainConfig.rpc, payload, destinationAddress); + await relayToChain(chainConfig.rpc, payload, destinationAddress, cb); console.log('Task processed:', task.id); saveLatestTask(chainConfig.name, task.id); @@ -81,18 +94,18 @@ function loadLatestTask(chainName) { try { return fs.readFileSync(`./latestTask-${chainName}.json`, 'utf8'); } catch (error) { - return ""; + return ''; } } -async function relayToChain(rpc, payload, destinationAddress) { +async function relayToChain(rpc, payload, destinationAddress, cb) { if (dryRun) { console.log('Destination:', destinationAddress, 'Payload:', payload); 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); const tx = await wallet.sendTransaction({ @@ -104,6 +117,7 @@ async function relayToChain(rpc, payload, destinationAddress) { await tx.wait(); console.log('Transaction confirmed'); + cb(); } function decodePayload(executeData) { @@ -113,4 +127,4 @@ function decodePayload(executeData) { module.exports = { pollTasks, -}; \ No newline at end of file +}; diff --git a/examples/amplifier/index.js b/examples/amplifier/index.js index 09e1aede..6d3364e1 100644 --- a/examples/amplifier/index.js +++ b/examples/amplifier/index.js @@ -23,5 +23,5 @@ const main = async () => { processContractCallEvent(params.srcChain, transactionReceipt.transactionHash, true); }; -pollTasks(params.srcChain); +pollTasks({ chainName: 'xrpl-evm-sidechain', pollInterval: 10000, dryRunOpt: true }); // main(null); From d026d86199bcdaeefac383506058d4493a3f84d8 Mon Sep 17 00:00:00 2001 From: Canh Trinh Date: Thu, 26 Sep 2024 23:56:14 -0400 Subject: [PATCH 5/9] chore: cleanup --- examples/amplifier/gmp-api/contract-call-event.js | 2 -- examples/amplifier/index.js | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/amplifier/gmp-api/contract-call-event.js b/examples/amplifier/gmp-api/contract-call-event.js index c262d33b..2eb57904 100644 --- a/examples/amplifier/gmp-api/contract-call-event.js +++ b/examples/amplifier/gmp-api/contract-call-event.js @@ -47,8 +47,6 @@ async function constructAPIEvent(sourceChain, txHash) { // Decode the event data const decodedLog = iface.parseLog(relevantLog); - console.log({ decodedLog }); - // Extract data from the decoded log const eventIndex = receipt.logs.indexOf(relevantLog); const eventID = `${txHash}-${eventIndex}`; diff --git a/examples/amplifier/index.js b/examples/amplifier/index.js index 6d3364e1..069cd569 100644 --- a/examples/amplifier/index.js +++ b/examples/amplifier/index.js @@ -10,7 +10,7 @@ const config = getConfig().chains; const params = { srcGatewayAddress: config['avalanche-fuji'].externalGateway, srcChain: config['avalanche-fuji'].id, - destinationChain: 'ethereum-sepolia', + destinationChain: 'xrpl-evm-sidechain', message: 'hi', destinationContractAddress: null, }; @@ -24,4 +24,4 @@ const main = async () => { }; pollTasks({ chainName: 'xrpl-evm-sidechain', pollInterval: 10000, dryRunOpt: true }); -// main(null); +main(null); From 3052e7773949b77ac4889c314430d483a1143fbc Mon Sep 17 00:00:00 2001 From: Canh Trinh Date: Wed, 2 Oct 2024 02:11:08 -0400 Subject: [PATCH 6/9] feat: 'working' e2e integration of avalanche > xrpl example chore: cleanup config file chore: folder restructure chore: more code cleanup --- examples/amplifier/{ => config}/chains.json | 4 +- examples/amplifier/{ => config}/config.js | 27 ++---- .../amplifier/contracts/AmplifierGMPTest.sol | 42 +++++++++ .../amplifier.js | 8 +- .../get-payload.js | 0 .../get-receipt.js | 0 .../save-payload.js | 0 examples/amplifier/gmp-api/approve-event.js | 48 ++++++---- .../amplifier/gmp-api/contract-call-event.js | 4 +- examples/amplifier/gmp-api/execute-event.js | 48 +++++----- examples/amplifier/gmp-api/tasks.js | 92 ++++++++++++------- examples/amplifier/index.js | 42 ++++++--- examples/amplifier/utils/deployContract.js | 16 ++++ .../utils/extractEventFromReceipt.js | 76 --------------- examples/amplifier/utils/gmp.js | 29 +++--- examples/amplifier/utils/index.js | 9 ++ hardhat.config.js | 2 +- latestTask-xrpl-evm-sidechain.json | 1 + 18 files changed, 239 insertions(+), 209 deletions(-) rename examples/amplifier/{ => config}/chains.json (81%) rename examples/amplifier/{ => config}/config.js (68%) create mode 100644 examples/amplifier/contracts/AmplifierGMPTest.sol rename examples/amplifier/{ => deprecated_legacy_endpoints}/amplifier.js (91%) 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%) create mode 100644 examples/amplifier/utils/deployContract.js delete mode 100644 examples/amplifier/utils/extractEventFromReceipt.js create mode 100644 examples/amplifier/utils/index.js create mode 100644 latestTask-xrpl-evm-sidechain.json diff --git a/examples/amplifier/chains.json b/examples/amplifier/config/chains.json similarity index 81% rename from examples/amplifier/chains.json rename to examples/amplifier/config/chains.json index ba1a6596..48c89226 100644 --- a/examples/amplifier/chains.json +++ b/examples/amplifier/config/chains.json @@ -16,7 +16,7 @@ }, { "name": "xrpl-evm-sidechain", - "rpc": "", - "gateway": "" + "rpc": "https://rpc-evm-sidechain.xrpl.org", + "gateway": "0x48CF6E93C4C1b014F719Db2aeF049AA86A255fE2" } ] diff --git a/examples/amplifier/config.js b/examples/amplifier/config/config.js similarity index 68% rename from examples/amplifier/config.js rename to examples/amplifier/config/config.js index 71f048c6..9732a343 100644 --- a/examples/amplifier/config.js +++ b/examples/amplifier/config/config.js @@ -1,5 +1,5 @@ const fs = require('fs'); - +const https = require('https'); const dotenv = require('dotenv'); // Load environment variables from .env file @@ -19,18 +19,8 @@ const defaults = { GMP_API_URL: 'http://localhost:8080', }; -const chains = { - 'avalanche-fuji': { - rpcUrl: 'https://rpc.ankr.com/avalanche_fuji', - externalGateway: '0xF128c84c3326727c3e155168daAa4C0156B87AD1', - id: 'avalanche-fuji', - }, - 'xrpl-evm-sidechain': { - rpcUrl: '', - externalGateway: '', - id: 'xrpl-evm-sidechain', - }, -}; +const chainsConfigFile = './examples/amplifier/config/chains.json'; +const chainsConfig = JSON.parse(fs.readFileSync(chainsConfigFile, 'utf8')); function getConfig() { const serverHOST = process.env.HOST || defaults.HOST; @@ -42,17 +32,18 @@ function getConfig() { serverHOST, serverPort, gmpAPIURL, - chains, + chains: chainsConfig, + httpsAgent: new https.Agent({ + cert, + key, + rejectUnauthorized: false, + }), cert, key, }; } -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); if (!chainConfig) { 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/amplifier.js b/examples/amplifier/deprecated_legacy_endpoints/amplifier.js similarity index 91% rename from examples/amplifier/amplifier.js rename to examples/amplifier/deprecated_legacy_endpoints/amplifier.js index 630dc17a..747ebbde 100644 --- a/examples/amplifier/amplifier.js +++ b/examples/amplifier/deprecated_legacy_endpoints/amplifier.js @@ -2,10 +2,10 @@ 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 { 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(); 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/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 2eb57904..7d9d87fe 100644 --- a/examples/amplifier/gmp-api/contract-call-event.js +++ b/examples/amplifier/gmp-api/contract-call-event.js @@ -1,6 +1,6 @@ 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(); @@ -13,14 +13,12 @@ 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) { 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 1ffa8d8c..9f5da21a 100644 --- a/examples/amplifier/gmp-api/tasks.js +++ b/examples/amplifier/gmp-api/tasks.js @@ -1,14 +1,18 @@ const fs = require('fs'); const axios = require('axios'); -const https = require('https'); const ethers = require('ethers'); -const { getConfig, getChainConfig } = require('../config.js'); +const { getConfig, getChainConfig } = require('../config/config.js'); +const { processMessageApprovedEvent } = require('./approve-event.js'); +const { processMessageExecutedEvent } = require('./execute-event.js'); +const AmplifierGMPTest = require('../../../artifacts/examples/amplifier/contracts/AmplifierGMPTest.sol/AmplifierGMPTest.json'); require('dotenv').config(); -const { gmpAPIURL, cert, key } = getConfig(); -var dryRun = true; +const { gmpAPIURL, httpsAgent } = getConfig(); +var dryRun = false; -async function pollTasks({ chainName, pollInterval, dryRunOpt, approveCb, executeCb }) { +const messageIdToCommandId = {}; + +async function pollTasks({ chainName, pollInterval, dryRunOpt }) { if (dryRunOpt) { console.log('Dry run enabled'); dryRun = true; @@ -16,12 +20,12 @@ async function pollTasks({ chainName, pollInterval, dryRunOpt, approveCb, execut const chainConfig = getChainConfig(chainName); - setInterval(() => { - getNewTasks(chainConfig, approveCb, executeCb); + const intervalId = setInterval(async () => { + await getNewTasks(chainConfig, intervalId); // Pass the interval ID to the function }, pollInterval); } -async function getNewTasks(chainConfig, approveCb, executeCb) { +async function getNewTasks(chainConfig, intervalId) { latestTask = loadLatestTask(chainConfig.name); var urlSuffix = ''; @@ -32,17 +36,13 @@ async function getNewTasks(chainConfig, approveCb, executeCb) { const url = `${gmpAPIURL}/chains/${chainConfig.name}/tasks${urlSuffix}`; - console.log('Polling tasks:', url); + console.log('Polling tasks on:', url); try { const response = await axios({ method: 'get', url, - httpsAgent: new https.Agent({ - cert, - key, - rejectUnauthorized: false, - }), + httpsAgent, }); const tasks = response.data.tasks; @@ -52,32 +52,38 @@ async function getNewTasks(chainConfig, approveCb, executeCb) { return; } - console.log('Tasks:', tasks); - for (const task of tasks) { - console.log('Processing task:', task.id); - var payload; var destinationAddress; - var cb; if (task.type === 'GATEWAY_TX') { - console.log('found approve task'); + console.log('Processing approve task', task.id); payload = decodePayload(task.task.executeData); destinationAddress = chainConfig.gateway; - cb = approveCb; + const destTxRecept = await relayApproval(chainConfig.rpc, payload, destinationAddress); + const { apiEvent } = await processMessageApprovedEvent(chainConfig.name, destTxRecept.transactionHash, '0'); + messageIdToCommandId[apiEvent.message.messageID] = apiEvent.meta.commandID; } else if (task.type === 'EXECUTE') { - console.log('found execute task'); + console.log('Processing execute task', task.id); payload = decodePayload(task.task.payload); + destinationAddress = task.task.message.destinationAddress; - cb = executeCb; + const { messageID, sourceAddress, sourceChain } = task.task.message; + + const destTxRecept = await relayExecution(chainConfig.rpc, payload, destinationAddress, { + messageID, + sourceAddress, + sourceChain, + }); + await processMessageExecutedEvent(chainConfig.name, destTxRecept.transactionHash, sourceChain, messageID, '0'); + + clearInterval(intervalId); + console.log('Polling interval cleared after EXECUTE task completed'); } else { console.warn('Unknown task type:', task.type); continue; } - await relayToChain(chainConfig.rpc, payload, destinationAddress, cb); - console.log('Task processed:', task.id); saveLatestTask(chainConfig.name, task.id); } @@ -98,26 +104,48 @@ function loadLatestTask(chainName) { } } -async function relayToChain(rpc, payload, destinationAddress, cb) { +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.EVM_PRIVATE_KEY, provider); - console.log('Relaying payload:', payload); + console.log('Relaying approval tx to chain:', { rpc, payload, destinationAddress }); 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', { txHash: 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:', { rpc, payload, destinationAddress }); + + 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'); - cb(); + console.log('Transaction confirmed', { txHash: destTxRecept.transactionHash }); + return destTxRecept; } function decodePayload(executeData) { diff --git a/examples/amplifier/index.js b/examples/amplifier/index.js index 069cd569..71ff91e1 100644 --- a/examples/amplifier/index.js +++ b/examples/amplifier/index.js @@ -1,27 +1,39 @@ -const { sleep } = require('./utils/sleep'); -const { getConfig } = require('./config'); -const { gmp } = require('./utils/gmp'); -const { processContractCallEvent } = require('./gmp-api/contract-call-event'); +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; -const params = { - srcGatewayAddress: config['avalanche-fuji'].externalGateway, - srcChain: config['avalanche-fuji'].id, - destinationChain: 'xrpl-evm-sidechain', - message: 'hi', - destinationContractAddress: null, -}; +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 { transactionReceipt } = await gmp(params, config); + const { sourceChain, destinationChain, message } = options; + + const srcContractDeployment = await deploy(sourceChain); + const destContract = await deploy(destinationChain); - await sleep(2000); + await sleep(10000); // wait for contracts above to deploy - processContractCallEvent(params.srcChain, transactionReceipt.transactionHash, true); + gmp( + { + destinationChain, + sourceChain, + message, + destinationContractAddress: destContract.address, + srcContractAddress: srcContractDeployment.address, + }, + config, + ); }; -pollTasks({ chainName: 'xrpl-evm-sidechain', pollInterval: 10000, dryRunOpt: true }); +pollTasks({ chainName: options.destinationChain, pollInterval: 10000, dryRunOpt: false }); main(null); diff --git a/examples/amplifier/utils/deployContract.js b/examples/amplifier/utils/deployContract.js new file mode 100644 index 00000000..fd776257 --- /dev/null +++ b/examples/amplifier/utils/deployContract.js @@ -0,0 +1,16 @@ +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); + return factory.deploy(chain.gateway); +}; + +module.exports = { + deploy, +}; diff --git a/examples/amplifier/utils/extractEventFromReceipt.js b/examples/amplifier/utils/extractEventFromReceipt.js deleted file mode 100644 index 76f7f95b..00000000 --- a/examples/amplifier/utils/extractEventFromReceipt.js +++ /dev/null @@ -1,76 +0,0 @@ -const { providers, ethers } = require('ethers'); -const { getConfig } = require('../config'); - -const config = getConfig().chains; - -function getCallContractEventIndex(transactionReceipt) { - const callContractEventSignature = 'ContractCall(address,string,string,bytes32,bytes)'; - const callContractTopic = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(callContractEventSignature)); - - for (let i = 0; i < transactionReceipt.logs.length; i++) { - const log = transactionReceipt.logs[i]; - - if (log.topics[0] === callContractTopic) { - return log.logIndex; - } - } - - return -1; -} - -async function extractEventFromReceipt(receipt, params) { - const contractABI = [ - 'event ContractCall(address indexed sender, string destinationChain, string destinationContractAddress, bytes32 indexed payloadHash, bytes payload)', - ]; - - const iface = new ethers.utils.Interface(contractABI); - - const eventIndex = getCallContractEventIndex(receipt); - const log = receipt.logs[eventIndex]; - - const decodedData = iface.decodeEventLog('ContractCall', log.data, log.topics); - - const provider = new providers.JsonRpcProvider(config['avalanche-fuji'].rpcUrl); - - const block = await provider.getBlock(receipt.blockNumber); - - const destinationChain = decodedData.destinationChain; - const eventID = `${receipt.transactionHash}-${eventIndex}`; - const destinationAddress = decodedData.destinationContractAddress; - const messageID = eventID; - const payloadHashBase64 = Buffer.from(log.topics[2].startsWith('0x') ? log.topics[2].slice(2) : log.topics[2], 'hex').toString( - 'base64', - ); - const payloadBase64 = Buffer.from(log.data.startsWith('0x') ? log.data.slice(2) : log.data, 'hex').toString('base64'); - const sourceAddress = receipt.from; - const sourceChain = params.srcChain; - - const meta = { - finalized: true, - fromAddress: receipt.from, - timestamp: block.timestamp, - txID: receipt.transactionHash, - }; - - // Create the final event object - const event = { - destinationChain, - eventID, - message: { - destinationAddress, - messageID, - payloadHash: payloadHashBase64, - sourceAddress, - sourceChain, - }, - meta, - payload: payloadBase64, - type: 'CALL', - }; - - return event; -} - -module.exports = { - extractEventFromReceipt, -}; diff --git a/examples/amplifier/utils/gmp.js b/examples/amplifier/utils/gmp.js index a1b5d2d4..a31fa363 100644 --- a/examples/amplifier/utils/gmp.js +++ b/examples/amplifier/utils/gmp.js @@ -1,28 +1,21 @@ 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 contractABI = [ - 'event ContractCall(address sender, string destinationChain, string destinationContractAddress, bytes32 payloadHash, bytes payload)', - 'function callContract(string destinationChain, string destinationContractAddress, bytes payload)', -]; - -const gmp = async ({ srcGatewayAddress, destinationChain, message, destinationContractAddress }, config) => { - const wallet = new Wallet(process.env.EVM_PRIVATE_KEY, new providers.JsonRpcProvider(config['avalanche-fuji'].rpcUrl)); - - const contract = new ethers.Contract(srcGatewayAddress, contractABI, wallet); - const payload = ethers.utils.hexlify(ethers.utils.toUtf8Bytes(message)); - const payloadHash = ethers.utils.keccak256(payload); +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 contract.callContract(destinationChain, destinationContractAddress || (await wallet.getAddress()), payload); + const tx = await srcContract.setRemoteValue(destinationChain, destinationContractAddress, message); const transactionReceipt = await tx.wait(); - console.log({ transactionReceipt }); + await sleep(10000); // allow for gmp event to propagate before triggering indexing - return { - transactionReceipt, - payloadHash: payloadHash.slice(2), - payload, - }; + processContractCallEvent(sourceChain, transactionReceipt.transactionHash, true); } catch (error) { throw new Error(`Error calling contract: ${error}`); } 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/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 diff --git a/latestTask-xrpl-evm-sidechain.json b/latestTask-xrpl-evm-sidechain.json new file mode 100644 index 00000000..7d8dae91 --- /dev/null +++ b/latestTask-xrpl-evm-sidechain.json @@ -0,0 +1 @@ +"01924d89-1adc-7465-a3e5-2ce687413f5b" \ No newline at end of file From 34117dbdcbaa46f02da23bb39728bf3b14270e1f Mon Sep 17 00:00:00 2001 From: Canh Trinh Date: Wed, 2 Oct 2024 11:07:21 -0400 Subject: [PATCH 7/9] chore: adding environment to chains config file chore: minor cleanup --- .env.example | 3 +- examples/amplifier/config/chains.json | 36 +++--- examples/amplifier/config/config.js | 7 +- .../latestTask-xrpl-evm-sidechain.json | 1 + examples/amplifier/gmp-api/tasks.js | 103 ++++++++++-------- examples/amplifier/index.js | 8 +- examples/amplifier/utils/deployContract.js | 1 + examples/amplifier/utils/gmp.js | 1 + examples/amplifier/utils/sleep.js | 2 +- latestTask-xrpl-evm-sidechain.json | 1 - 10 files changed, 85 insertions(+), 78 deletions(-) create mode 100644 examples/amplifier/config/latestTasks/latestTask-xrpl-evm-sidechain.json delete mode 100644 latestTask-xrpl-evm-sidechain.json diff --git a/.env.example b/.env.example index 8324d1ed..02393d16 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ EVM_PRIVATE_KEY=YOUR_PRIVATE_KEY_HERE -GMP_API_URL= \ No newline at end of file +GMP_API_URL= +ENVIRONMENT= #devnet-amplifier, testnet, or mainnet \ No newline at end of file diff --git a/examples/amplifier/config/chains.json b/examples/amplifier/config/chains.json index 48c89226..650e70cb 100644 --- a/examples/amplifier/config/chains.json +++ b/examples/amplifier/config/chains.json @@ -1,22 +1,14 @@ -[ - { - "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" - }, - { - "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" - } -] +{ + "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/config.js b/examples/amplifier/config/config.js index 9732a343..bc46dced 100644 --- a/examples/amplifier/config/config.js +++ b/examples/amplifier/config/config.js @@ -19,8 +19,9 @@ const defaults = { GMP_API_URL: 'http://localhost:8080', }; -const chainsConfigFile = './examples/amplifier/config/chains.json'; -const chainsConfig = JSON.parse(fs.readFileSync(chainsConfigFile, 'utf8')); +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; @@ -38,8 +39,6 @@ function getConfig() { key, rejectUnauthorized: false, }), - cert, - key, }; } 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/gmp-api/tasks.js b/examples/amplifier/gmp-api/tasks.js index 9f5da21a..c2f9c184 100644 --- a/examples/amplifier/gmp-api/tasks.js +++ b/examples/amplifier/gmp-api/tasks.js @@ -2,14 +2,16 @@ const fs = require('fs'); const axios = require('axios'); const ethers = require('ethers'); const { getConfig, getChainConfig } = require('../config/config.js'); -const { processMessageApprovedEvent } = require('./approve-event.js'); -const { processMessageExecutedEvent } = require('./execute-event.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, httpsAgent } = getConfig(); -var dryRun = false; +let dryRun = false; +// 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 }) { @@ -21,14 +23,14 @@ async function pollTasks({ chainName, pollInterval, dryRunOpt }) { const chainConfig = getChainConfig(chainName); const intervalId = setInterval(async () => { - await getNewTasks(chainConfig, intervalId); // Pass the interval ID to the function + await getNewTasks(chainConfig, intervalId); }, pollInterval); } async function getNewTasks(chainConfig, intervalId) { latestTask = loadLatestTask(chainConfig.name); - var urlSuffix = ''; + let urlSuffix = ''; if (latestTask !== '') { urlSuffix = `?after=${latestTask}`; @@ -48,57 +50,68 @@ async function getNewTasks(chainConfig, intervalId) { const tasks = response.data.tasks; if (tasks.length === 0) { - console.log('No new tasks'); + console.log('No new tasks\n'); return; } for (const task of tasks) { - var payload; - var destinationAddress; - - if (task.type === 'GATEWAY_TX') { - console.log('Processing approve task', task.id); - payload = decodePayload(task.task.executeData); - destinationAddress = chainConfig.gateway; - const destTxRecept = await relayApproval(chainConfig.rpc, payload, destinationAddress); - const { apiEvent } = await processMessageApprovedEvent(chainConfig.name, destTxRecept.transactionHash, '0'); - messageIdToCommandId[apiEvent.message.messageID] = apiEvent.meta.commandID; - } else if (task.type === 'EXECUTE') { - console.log('Processing execute task', task.id); - payload = decodePayload(task.task.payload); - - destinationAddress = task.task.message.destinationAddress; - const { messageID, sourceAddress, sourceChain } = task.task.message; - - const destTxRecept = await relayExecution(chainConfig.rpc, payload, destinationAddress, { - messageID, - sourceAddress, - sourceChain, - }); - await processMessageExecutedEvent(chainConfig.name, destTxRecept.transactionHash, sourceChain, messageID, '0'); - - clearInterval(intervalId); - console.log('Polling interval cleared after EXECUTE task completed'); - } else { - console.warn('Unknown task type:', task.type); - continue; - } - - 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 ''; } @@ -113,7 +126,7 @@ async function relayApproval(rpc, payload, destinationAddress) { const provider = new ethers.providers.JsonRpcProvider(rpc); const wallet = new ethers.Wallet(process.env.EVM_PRIVATE_KEY, provider); - console.log('Relaying approval tx to chain:', { rpc, payload, destinationAddress }); + console.log('Relaying approval tx to chain'); const tx = await wallet.sendTransaction({ to: destinationAddress, data: payload, @@ -122,7 +135,7 @@ async function relayApproval(rpc, payload, destinationAddress) { const destTxRecept = await tx.wait(); - console.log('Transaction confirmed', { txHash: destTxRecept.transactionHash }); + console.log('Transaction confirmed: ', destTxRecept.transactionHash); return destTxRecept; } @@ -136,7 +149,7 @@ async function relayExecution(rpc, payload, destinationAddress, { messageID, sou const wallet = new ethers.Wallet(process.env.EVM_PRIVATE_KEY, provider); const commandID = messageIdToCommandId[messageID]; - console.log('Relaying execution tx to chain:', { rpc, payload, destinationAddress }); + console.log('Relaying execution tx to chain'); const executable = new ethers.Contract(destinationAddress, AmplifierGMPTest.abi, wallet); @@ -144,7 +157,7 @@ async function relayExecution(rpc, payload, destinationAddress, { messageID, sou const destTxRecept = await tx.wait(); - console.log('Transaction confirmed', { txHash: destTxRecept.transactionHash }); + console.log('Transaction confirmed: ', destTxRecept.transactionHash); return destTxRecept; } diff --git a/examples/amplifier/index.js b/examples/amplifier/index.js index 71ff91e1..5e6d4d71 100644 --- a/examples/amplifier/index.js +++ b/examples/amplifier/index.js @@ -21,9 +21,10 @@ const main = async () => { const srcContractDeployment = await deploy(sourceChain); const destContract = await deploy(destinationChain); - await sleep(10000); // wait for contracts above to deploy + console.log('Contracts deployed, waiting 10 seconds for txs to propagate'); + await sleep(10000); - gmp( + await gmp( { destinationChain, sourceChain, @@ -35,5 +36,4 @@ const main = async () => { ); }; -pollTasks({ chainName: options.destinationChain, pollInterval: 10000, dryRunOpt: false }); -main(null); +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 index fd776257..905ace7b 100644 --- a/examples/amplifier/utils/deployContract.js +++ b/examples/amplifier/utils/deployContract.js @@ -8,6 +8,7 @@ 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); }; diff --git a/examples/amplifier/utils/gmp.js b/examples/amplifier/utils/gmp.js index a31fa363..5c98b0fb 100644 --- a/examples/amplifier/utils/gmp.js +++ b/examples/amplifier/utils/gmp.js @@ -12,6 +12,7 @@ const gmp = async ({ sourceChain, destinationChain, message, destinationContract 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 diff --git a/examples/amplifier/utils/sleep.js b/examples/amplifier/utils/sleep.js index cc0c12cb..375ec017 100644 --- a/examples/amplifier/utils/sleep.js +++ b/examples/amplifier/utils/sleep.js @@ -1,5 +1,5 @@ const sleep = (ms) => { - console.log(`Sleeping for ${ms}`); + console.log(`Sleeping for ${ms}\n`); return new Promise((resolve) => setTimeout(resolve, ms)); }; diff --git a/latestTask-xrpl-evm-sidechain.json b/latestTask-xrpl-evm-sidechain.json deleted file mode 100644 index 7d8dae91..00000000 --- a/latestTask-xrpl-evm-sidechain.json +++ /dev/null @@ -1 +0,0 @@ -"01924d89-1adc-7465-a3e5-2ce687413f5b" \ No newline at end of file From b6c505f628969e50acaa3a734f0096605a39fd2b Mon Sep 17 00:00:00 2001 From: Canh Trinh Date: Wed, 2 Oct 2024 13:09:48 -0400 Subject: [PATCH 8/9] feat: update readme --- .env.example | 6 +- examples/amplifier/README.md | 263 ++++++++++---------------- examples/amplifier/interface/Event.ts | 23 --- 3 files changed, 106 insertions(+), 186 deletions(-) delete mode 100644 examples/amplifier/interface/Event.ts diff --git a/.env.example b/.env.example index 02393d16..c376d08c 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,7 @@ EVM_PRIVATE_KEY=YOUR_PRIVATE_KEY_HERE + +# For amplifier examples GMP_API_URL= -ENVIRONMENT= #devnet-amplifier, testnet, or mainnet \ No newline at end of file +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/examples/amplifier/README.md b/examples/amplifier/README.md index 65a48e9c..6a6f7e8c 100644 --- a/examples/amplifier/README.md +++ b/examples/amplifier/README.md @@ -2,14 +2,29 @@ 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. -Please see the accompanying docs here: https://bright-ambert-2bd.notion.site/Amplifier-GMP-API-EXTERNAL-911e740b570b4017826c854338b906c8 +Please see the accompanying docs here: -## Setup +### Relayer architecture overview + +https://docs.axelar.dev/dev/amplifier/chain-integration/relay-messages/automatic/ + +### GMP API endpoint and schema definitions + +https://bright-ambert-2bd.notion.site/Amplifier-GMP-API-EXTERNAL-911e740b570b4017826c854338b906c8 + +## Onboarding + +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 + +## Repository Setup 1. Clone this repo. -2. Install dependencies: +2. Install dependencies and build contracts: ```bash npm install + npm run build ``` 3. Go to amplifier examples directory ``` @@ -17,194 +32,118 @@ Please see the accompanying docs here: https://bright-ambert-2bd.notion.site/Amp ``` 4. Copy `.env.example` into `.env` and set up the following environment variables: ```bash - GMP_API_URL=... + GMP_API_URL= + ENVIRONMENT= # e.g. devnet-amplifier, testnet, or mainnet + CRT_PATH= # e.g. './client.crt' + KEY_PATH= # e.g. './client.key' ``` -## GMP example - -### Make a Contract Call to the source chain - -```typescript -import { providers, Wallet, ethers } from 'ethers'; -import { config } from './config'; -require('dotenv').config(); - -const gmpCall = async ({ srcGatewayAddress, destinationChain, message, destinationContractAddress }) => { - const wallet = new Wallet(process.env.PRIVATE_KEY as string, new providers.JsonRpcProvider(config.avalanche.rpcUrl)); - const contractABI = ['function callContract(string destinationChain, string destinationContractAddress, bytes payload)']; - const contract = new ethers.Contract(srcGatewayAddress, contractABI, wallet); - // const destinationChain = "ethereum-sepolia"; - const payload = ethers.utils.hexlify(ethers.utils.toUtf8Bytes('hi')); - const payloadHash = ethers.utils.keccak256(payload); - - try { - const tx = await contract.callContract(destinationChain, destinationContractAddress || (await wallet.getAddress()), payload); - console.log('Transaction hash:', tx.hash); +## Run the example - const transactionReceipt = await tx.wait(); - - return { - transactionReceipt, - payloadHash: payloadHash.slice(2), - payload, - }; - } catch (error) { - console.error('Error calling contract:', error); - } -}; - -// parameters below use `avalanche` from `devnet-amplifier` -gmpCall(`0xF128c84c3326727c3e155168daAa4C0156B87AD1`); -``` - -Here’s how a Contract Call looks on Ethereum Sepolia ([0x9b447614be654eeea0c5de0319b3f2c243ab45bebd914a1f7319f4bb599d8968](https://sepolia.etherscan.io/tx/0x9b447614be654eeea0c5de0319b3f2c243ab45bebd914a1f7319f4bb599d8968)). Notice that in the logs there’s a `ContractCall` event emitted. - - - -### Make an Event API Call - -Now, we are going to use the content of the Contract Call transaction, and make a `Call Event` request to the GMP API (see how Call Event is structured [here](https://www.notion.so/Amplifier-GMP-API-EXTERNAL-911e740b570b4017826c854338b906c8?pvs=21)). - -```json -{ - "events": [ - { - "destinationChain": "test-avalanche", - "eventID": "0x9b447614be654eeea0c5de0319b3f2c243ab45bebd914a1f7319f4bb599d8968-1", - "message": { - "destinationAddress": "0xE8E348fA7b311d6E308b1A162C3ec0172B37D1C1", - "messageID": "0x9b447614be654eeea0c5de0319b3f2c243ab45bebd914a1f7319f4bb599d8968-1", - "payloadHash": "Y2YO3UuCRRackxbPYX9dWmNTYcnAMOommp9g4ydb3i4=", - "sourceAddress": "0x9e3e785dD9EA3826C9cBaFb1114868bc0e79539a", - "sourceChain": "test-sepolia" - }, - "meta": { - "finalized": true, - "fromAddress": "0xba76c6980428A0b10CFC5d8ccb61949677A61233", - "timestamp": "2024-09-11T13:32:48Z", - "txID": "0x9b447614be654eeea0c5de0319b3f2c243ab45bebd914a1f7319f4bb599d8968" - }, - "payload": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASaGVsbG8gdGVzdC1zZXBvbGlhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", - "type": "CALL" - } - ] -} +```bash + node examples/amplifier/index.js -s avalanche-fuji -d xrpl-evm-sidechain -m 'hi there' ``` -Here’s how to retrieve these values from given the Contract Call transaction on the source chain: +The script above is an end-to-end example of invoking a GMP call on the `devnet-amplifier` environment. It: -- `destinationChain`: the destination chain you used to make the contract call -- `eventID`: the transaction hash of the submitted contract call + the index of the event in the transaction events (in the example above, the index is 1) +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 - TOPIC="0x30ae6cc78c27e651745bf2ad08a11de83910ac1e347a52f7ac898c0fbef94dae" - TX_ID="0x9b447614be654eeea0c5de0319b3f2c243ab45bebd914a1f7319f4bb599d8968" + ```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); - receipt=$(curl -s $RPC -X POST -H "Content-Type: application/json" \ - --data '{"method":"eth_getTransactionReceipt","params":["0x9b447614be654eeea0c5de0319b3f2c243ab45bebd914a1f7319f4bb599d8968"],"id":1,"jsonrpc":"2.0"}') + 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}`); - EVENT_INDEX=$(cat receipt | jq --arg TOPIC "$TOPIC" '.result.logs | map(.topics[0] == $TOPIC) | index(true)') + await sleep(10000); // allow for gmp event to propagate before triggering indexing - echo "eventID: $TX_ID-$EVENT_INDEX" + processContractCallEvent(sourceChain, transactionReceipt.transactionHash, true); + } catch (error) { + throw new Error(`Error calling contract: ${error}`); + } + }; ``` -- `destinationAddress`: the destination address you used to make the contract call -- `messageID`: in one-way Contract Calls, the message identifier is equivalent to the `eventID`. In two-way calls, the `messageID` is the `eventID` of the initial Contract Call at the first segment of the multi-hop chain. -- `payloadHash`: This is available as the third topic of the event with the specific topic ID. You can extract it and encode it in base64: - - ```bash - TOPIC="0x30ae6cc78c27e651745bf2ad08a11de83910ac1e347a52f7ac898c0fbef94dae" +3. initiates a poll on the `/tasks` endpoint to listen for messages that are processed and ready to be relayed to your destination chain. - # get 3rd argument of topic, and stript starting "0x" - payloadHashHex=$(echo $receipt | jq -r ".result.logs[] | select(.topics[0] == \"$TOPIC\") | .topics[2]" | cut -c 3-) + ```javascript + // examples/amplifier/index.js - # encode to base64 - payloadHash=$(echo -n "$payloadHashHex" | xxd -r -p | base64) - - echo "payloadHash: $payloadHash" + main(null).then(() => pollTasks({ chainName: options.destinationChain, pollInterval: 10000, dryRunOpt: false })); ``` -- `sourceAddress`: This is the address that initiated the contract call. It can be extracted from the second topic of the event: + ```javascript + // examples/amplifier/gmp-api/tasks.js - ```bash - TOPIC="0x30ae6cc78c27e651745bf2ad08a11de83910ac1e347a52f7ac898c0fbef94dae" + async function pollTasks({ chainName, pollInterval, dryRunOpt }) { + if (dryRunOpt) { + console.log('Dry run enabled'); + dryRun = true; + } - # get the 2nd argument of the topic and keep the address (0x | 24 zeros | address ) - sourceAddress=$(cat receipt | jq -r --arg TOPIC "$TOPIC" '.result.logs[] | select(.topics[0] == $TOPIC) | .topics[1]' | cut -c 27-) + const chainConfig = getChainConfig(chainName); - echo "sourceAddress: 0x$sourceAddress" + const intervalId = setInterval(async () => { + await getNewTasks(chainConfig, intervalId); + }, pollInterval); + } ``` -- `sourceChain`: This is the alias of the source chain as registered in amplifier. -- `finalized`: Whether or not the transaction is finalized on the source chain. It’s not going to be processes until it’s flagged with `finalized: true` by the caller. -- `fromAddress`(optional): This is the address that signed the transaction. It can be extracted directly from the receipt: - - ```bash - fromAddress=$(echo $receipt | jq -r '.result.from') - - echo "fromAddress: $fromAddress" - ``` + - In this particular example, there are two events we expect to arise from the `/tasks` endpoint: -- `timestamp`(optional): This is not directly available in the receipt. It typically comes from the block timestamp. You'd need to fetch the block details to get this: + - `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. - ```bash - # Extract block number (in hex) - blockNumber=$(echo $receipt | jq -r '.result.blockNumber') + 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. - # Fetch block data - blockData=$(curl -s $RPC -X POST -H "Content-Type: application/json" \ - --data "{\"method\":\"eth_getBlockByNumber\",\"params\":[\"$blockNumber\", false],\"id\":1,\"jsonrpc\":\"2.0\"}") + ```javascript + // examples/amplifier/gmp-api/tasks.js - # Extract timestamp (in hex) and convert to decimal - timestamp=$(echo $blockData | jq -r '.result.timestamp') - timestampDec=$((16#${timestamp:2})) # Remove '0x' prefix and convert hex to decimal + // 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 = {}; - formattedTimestamp=$(date -r $timestampDec -u +'%Y-%m-%dT%H:%M:%SZ') + async function processApproval(task, chainConfig) { + console.log('Processing approve task', task.id); + const payload = decodePayload(task.task.executeData); + const destinationAddress = chainConfig.gateway; - echo "timestamp: $formattedTimestamp" - ``` + const destTxRecept = await relayApproval(chainConfig.rpc, payload, destinationAddress); + const { apiEvent } = await recordMessageApprovedEvent(chainConfig.name, destTxRecept.transactionHash, '0'); + messageIdToCommandId[apiEvent.message.messageID] = apiEvent.meta.commandID; + } + ``` -- `txID`(optional): This is the transaction hash -- `payload`: This is the “payload” field of the contract call, encoded in base64. To extract it, you need to decode the data field of the topic. -- `type`: This must be `CALL` for contract calls. + - `EXECUTE` for the payload of the GMP message to be executed on the executable on the destination chain. -After the event payload is constructed, submit it to the API as such: + 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. -```bash -curl -X POST $GMP_API_URL/test-avalanche/events \ - -H "Content-Type: application/json" \ - -d '{ - "events": [ - { - "destinationChain": "test-avalanche", - "eventID": "0x9b447614be654eeea0c5de0319b3f2c243ab45bebd914a1f7319f4bb599d8968-1", - "message": { - "destinationAddress": "0xE8E348fA7b311d6E308b1A162C3ec0172B37D1C1", - "messageID": "0x9b447614be654eeea0c5de0319b3f2c243ab45bebd914a1f7319f4bb599d8968-1", - "payloadHash": "Y2YO3UuCRRackxbPYX9dWmNTYcnAMOommp9g4ydb3i4=", - "sourceAddress": "0x9e3e785dD9EA3826C9cBaFb1114868bc0e79539a", - "sourceChain": "test-sepolia" - }, - "meta": { - "finalized": true, - "fromAddress": "0xba76c6980428A0b10CFC5d8ccb61949677A61233", - "timestamp": "2024-09-11T13:32:48Z", - "txID": "0x9b447614be654eeea0c5de0319b3f2c243ab45bebd914a1f7319f4bb599d8968" - }, - "payload": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASaGVsbG8gdGVzdC1zZXBvbGlhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", - "type": "CALL" - } - ] -}' -``` + ```javascript + // examples/amplifier/gmp-api/tasks.js -### Wait for the task to get published + 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; -Once the event is submitted an processed, a new `GATEWAY_TX` task will be published. + 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'); + } + ``` diff --git a/examples/amplifier/interface/Event.ts b/examples/amplifier/interface/Event.ts deleted file mode 100644 index 760ce3e4..00000000 --- a/examples/amplifier/interface/Event.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface Message { - destinationAddress: string; - messageID: string; - payloadHash: string; - sourceAddress: string; - sourceChain: string; -} - -export interface Meta { - finalized: boolean; - fromAddress: string; - timestamp: string; - txID: string; -} - -export interface Event { - destinationChain: string; - eventID: string; - message: Message; - meta: Meta; - payload: string; - type: string; -} From 20585debb1be7df49ac4d847c89ed3dcc730cf9b Mon Sep 17 00:00:00 2001 From: Canh Trinh Date: Wed, 2 Oct 2024 15:04:57 -0400 Subject: [PATCH 9/9] chore: adding caveats to readme --- examples/amplifier/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/amplifier/README.md b/examples/amplifier/README.md index 6a6f7e8c..5a20c663 100644 --- a/examples/amplifier/README.md +++ b/examples/amplifier/README.md @@ -147,3 +147,8 @@ The script above is an end-to-end example of invoking a GMP call on the `devnet- console.log('Polling interval cleared after EXECUTE task completed'); } ``` + +## Considerations + +- 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.