From 2ea98eaa8dc93d0f51cbd77dfc791f2b9f5379da Mon Sep 17 00:00:00 2001 From: chase-45 <88348425+chase-45@users.noreply.github.com> Date: Wed, 15 Mar 2023 18:32:33 -0400 Subject: [PATCH] Typescript SDK (#79) * sdk additions, query delivery status functions * import fixes * imports * Fixes for getDeliveryStatusBySourceTx * Fix typo in invalidRedeliveryTopics * Use negative number feature of queryFilter * 2047 -> 2040 * WIP * Typescript test for statusByTx * small changes * revert reason WIP * continued WIP for getting revert reason * Remove reason parsing * WIP adding default RPCs * compiles * SDK nicely prints delivery information! * SDK nicely prints delivery information! * Change error msg * Tests pass, including test for resending a failed forward! * Enum * update SDK in relayer engine * remove testgovernance file * Nice error logging around not finding a delivery * Update relayer engine sdk * Respond to PR comments * Fix test * fixed new lines * helper --------- Co-authored-by: derpy-duck <115193320+derpy-duck@users.noreply.github.com> --- ethereum/ts-scripts/helpers/env.ts | 14 +- ethereum/ts-test/2_core_relayer.ts | 230 ++++++++++--- ethereum/ts-test/helpers/consts.ts | 3 + relayer_engine/pkgs/sdk/package-lock.json | 1 + relayer_engine/pkgs/sdk/package.json | 1 + relayer_engine/pkgs/sdk/src/consts.ts | 122 +++++++ relayer_engine/pkgs/sdk/src/index.ts | 4 +- relayer_engine/pkgs/sdk/src/main/index.ts | 1 + relayer_engine/pkgs/sdk/src/main/status.ts | 359 +++++++++++++++++++++ sdk/package-lock.json | 1 + sdk/package.json | 1 + sdk/src/consts.ts | 122 +++++++ sdk/src/index.ts | 4 +- sdk/src/main/index.ts | 1 + sdk/src/main/status.ts | 359 +++++++++++++++++++++ 15 files changed, 1174 insertions(+), 49 deletions(-) create mode 100644 relayer_engine/pkgs/sdk/src/consts.ts create mode 100644 relayer_engine/pkgs/sdk/src/main/index.ts create mode 100644 relayer_engine/pkgs/sdk/src/main/status.ts create mode 100644 sdk/src/consts.ts create mode 100644 sdk/src/main/index.ts create mode 100644 sdk/src/main/status.ts diff --git a/ethereum/ts-scripts/helpers/env.ts b/ethereum/ts-scripts/helpers/env.ts index 23368dc..7ba4897 100644 --- a/ethereum/ts-scripts/helpers/env.ts +++ b/ethereum/ts-scripts/helpers/env.ts @@ -1,4 +1,4 @@ -import { ChainId } from "@certusone/wormhole-sdk" +import { ChainId, Network } from "@certusone/wormhole-sdk" import { ethers, Signer } from "ethers" import fs from "fs" import { @@ -41,6 +41,18 @@ export function init(overrides: { lastRunOverride?: boolean } = {}): string { return env } +export function getWhNetwork(): Network { + if (env == "testnet") { + return "TESTNET" + } else if (env == "mainnet") { + return "MAINNET" + } else if (env == "tilt") { + return "DEVNET" + } + + throw Error("Unsupported wormhole network") +} + function get_env_var(env: string): string { const v = process.env[env] return v || "" diff --git a/ethereum/ts-test/2_core_relayer.ts b/ethereum/ts-test/2_core_relayer.ts index 22726b1..78f76b1 100644 --- a/ethereum/ts-test/2_core_relayer.ts +++ b/ethereum/ts-test/2_core_relayer.ts @@ -1,5 +1,5 @@ import { expect } from "chai" -import { ethers } from "ethers" +import { ethers, providers } from "ethers" import { ChainId, tryNativeToHexString } from "@certusone/wormhole-sdk" import { ChainInfo, RELAYER_DEPLOYER_PRIVATE_KEY } from "./helpers/consts" import { generateRandomString } from "./helpers/utils" @@ -15,6 +15,7 @@ import { loadMockIntegrations, } from "../ts-scripts/helpers/env" import { MockRelayerIntegration, IWormholeRelayer } from "../../sdk/src" +import { getDeliveryInfoBySourceTx, DeliveryInfo, RedeliveryInfo } from "../../sdk/src" const ETHEREUM_ROOT = `${__dirname}/..` init() @@ -88,7 +89,7 @@ describe("Core Relayer Integration Test - Two Chains", () => { await new Promise((resolve) => { setTimeout(() => { resolve(0) - }, 2000) + }, 4000) }) console.log("Checking if message was relayed") @@ -220,19 +221,19 @@ describe("Core Relayer Integration Test - Two Chains", () => { console.log(`Sent message: ${arbitraryPayload1}`) const value1 = await sourceCoreRelayer.quoteGas( sourceChain.chainId, - 500000, + 1000000, await sourceCoreRelayer.getDefaultRelayProvider() ) - const value2 = await targetCoreRelayer.quoteGas( + const value2 = (await targetCoreRelayer.quoteGas( sourceChain.chainId, 500000, await targetCoreRelayer.getDefaultRelayProvider() - ) - const value3 = await targetCoreRelayer.quoteGas( + )) + const value3 = (await targetCoreRelayer.quoteGas( targetChain.chainId, 500000, await targetCoreRelayer.getDefaultRelayProvider() - ) + )) console.log(`Quoted gas delivery fee: ${value1.add(value2).add(value3)}`) const furtherInstructions: MockRelayerIntegration.FurtherInstructionsStruct = { @@ -255,7 +256,7 @@ describe("Core Relayer Integration Test - Two Chains", () => { await new Promise((resolve) => { setTimeout(() => { resolve(0) - }, 4000) + }, 8000) }) console.log("Checking if first forward was relayed") @@ -309,7 +310,7 @@ describe("Core Relayer Integration Test - Two Chains", () => { console.log("Checking if message was relayed (it shouldn't have been!)") const message = await targetMockIntegration.getMessage() - console.log(`Sent message: ${arbitraryPayload}`) + console.log(`Sent message: ${arbitraryPayload}`) console.log(`Received message: ${message}`) expect(message).to.not.equal(arbitraryPayload) @@ -325,7 +326,7 @@ describe("Core Relayer Integration Test - Two Chains", () => { newReceiverValue: 0, newRelayParameters: sourceCoreRelayer.getDefaultRelayParams() }; - await sourceCoreRelayer.resend(request, 1, sourceCoreRelayer.getDefaultRelayProvider(), {value: value, gasLimit: 500000}).then((t)=>t.wait); + await sourceCoreRelayer.resend(request, sourceCoreRelayer.getDefaultRelayProvider(), {value: value, gasLimit: 500000}).then((t)=>t.wait); console.log("Message resent"); await new Promise((resolve) => { @@ -355,12 +356,17 @@ describe("Core Relayer Integration Test - Two Chains", () => { 500000, await sourceCoreRelayer.getDefaultRelayProvider() ) - const extraForwardingValue = await targetCoreRelayer.quoteGas( + const notEnoughExtraForwardingValue = await targetCoreRelayer.quoteGas( sourceChain.chainId, 10000, await targetCoreRelayer.getDefaultRelayProvider() ) - console.log(`Quoted gas delivery fee: ${value.add(extraForwardingValue)}`) + const enoughExtraForwardingValue = await targetCoreRelayer.quoteGas( + sourceChain.chainId, + 500000, + await targetCoreRelayer.getDefaultRelayProvider() + ) + console.log(`Quoted gas delivery fee: ${value.add(notEnoughExtraForwardingValue)}`) const furtherInstructions: MockRelayerIntegration.FurtherInstructionsStruct = { keepSending: true, @@ -372,36 +378,171 @@ describe("Core Relayer Integration Test - Two Chains", () => { [arbitraryPayload1], furtherInstructions, [targetChain.chainId], - [value.add(extraForwardingValue)], - { value: value.add(extraForwardingValue), gasLimit: 500000 } + [value.add(notEnoughExtraForwardingValue)], + { value: value.add(notEnoughExtraForwardingValue), gasLimit: 500000 }) + + console.log("Sent delivery request!") + const rx = await tx.wait() + console.log("Message confirmed!") + + await new Promise((resolve) => { + setTimeout(() => { + resolve(0) + }, 4000) + }) + + console.log("Checking if message was relayed") + const message1 = await targetMockIntegration.getMessage() + console.log( + `Sent message: ${arbitraryPayload1} (expecting ${arbitraryPayload2} from forward)` + ) + console.log(`Received message on target: ${message1}`) + expect(message1).to.equal(arbitraryPayload1) + + console.log("Checking if forward message was relayed back (it shouldn't have been!)") + const message2 = await sourceMockIntegration.getMessage() + console.log(`Sent message: ${arbitraryPayload2}`) + console.log(`Received message on source: ${message2}`) + expect(message2).to.not.equal(arbitraryPayload2) + + let info: DeliveryInfo = (await getDeliveryInfoBySourceTx({environment: "DEVNET", sourceChain: sourceChain.chainId, sourceTransaction: tx.hash})) as DeliveryInfo + let status = info.targetChainStatuses[0].events[0].status + console.log(`Status: ${status}`) + + // RESEND THE MESSAGE SOMEHOW! + + console.log("Resending the message"); + const request: IWormholeRelayer.ResendByTxStruct = { + sourceChain: targetChain.chainId, + sourceTxHash: info.targetChainStatuses[0].events[0].transactionHash as string, + sourceNonce: 1, + targetChain: sourceChain.chainId, + deliveryIndex: 2, + multisendIndex: 0, + newMaxTransactionFee: value, + newReceiverValue: 0, + newRelayParameters: sourceCoreRelayer.getDefaultRelayParams() + }; + await sourceCoreRelayer.resend(request, sourceCoreRelayer.getDefaultRelayProvider(), {value: value.add(enoughExtraForwardingValue), gasLimit: 500000}).then((t)=>t.wait); + console.log("Message resent"); + + + await new Promise((resolve) => { + setTimeout(() => { + resolve(0) + }, 4000) + }) + console.log("Checking if message was relayed") + const message3 = await targetMockIntegration.getMessage() + console.log( + `Sent message: ${arbitraryPayload1} (expecting ${arbitraryPayload2} from forward)` + ) + console.log(`Received message on target: ${message3}`) + expect(message3).to.equal(arbitraryPayload1) + console.log("Checking if forward message was relayed back (it should now have been!)") + const message4 = await sourceMockIntegration.getMessage() + console.log(`Sent message: ${arbitraryPayload2}`) + console.log(`Received message on source: ${message4}`) + expect(message4).to.equal(arbitraryPayload2) + + + }) + + + it("Tests the Typescript SDK during a delivery", async () => { + const arbitraryPayload = ethers.utils.hexlify( + ethers.utils.toUtf8Bytes(generateRandomString(32)) + ) + console.log(`Sent message: ${arbitraryPayload}`) + const value = await sourceCoreRelayer.quoteGas( + targetChain.chainId, + 500000, + await sourceCoreRelayer.getDefaultRelayProvider() + ) + console.log(`Quoted gas delivery fee: ${value}`) + const tx = await sourceMockIntegration.sendMessage( + arbitraryPayload, + targetChain.chainId, + targetMockIntegrationAddress, + { value, gasLimit: 500000 } + ) + + console.log("Sent delivery request!") + const rx = await tx.wait() + console.log("Message confirmed!") + + console.log("Checking status using SDK"); + let info: DeliveryInfo = (await getDeliveryInfoBySourceTx({environment: "DEVNET", sourceChain: sourceChain.chainId, sourceTransaction: tx.hash})) as DeliveryInfo + let status = info.targetChainStatuses[0].events[0].status + + expect(status.substring(0, 22)).to.equal("Delivery didn't happen") + + await new Promise((resolve) => { + setTimeout(() => { + resolve(0) + }, 6000) + }) + + const message = await targetMockIntegration.getMessage() + console.log(`Sent message: ${arbitraryPayload}`) + console.log(`Received message: ${message}`) + expect(message).to.equal(arbitraryPayload) + + console.log("Checking status using SDK"); + info = await getDeliveryInfoBySourceTx({environment: "DEVNET", sourceChain: sourceChain.chainId, sourceTransaction: tx.hash}) as DeliveryInfo; + status = info.targetChainStatuses[0].events[0].status + expect(status).to.equal("Delivery Success") + }) + + + it("Tests the Typescript SDK during a redelivery", async () => { + const arbitraryPayload = ethers.utils.hexlify( + ethers.utils.toUtf8Bytes(generateRandomString(32)) + ) + console.log(`Sent message: ${arbitraryPayload}`) + const valueNotEnough = await sourceCoreRelayer.quoteGas( + targetChain.chainId, + 10000, + await sourceCoreRelayer.getDefaultRelayProvider() + ) + const value = await sourceCoreRelayer.quoteGas( + targetChain.chainId, + 500000, + await sourceCoreRelayer.getDefaultRelayProvider() + ) + console.log(`Quoted gas delivery fee: ${value}`) + const tx = await sourceMockIntegration.sendMessage( + arbitraryPayload, + targetChain.chainId, + targetMockIntegrationAddress, + { value: valueNotEnough, gasLimit: 500000 } ) console.log("Sent delivery request!") const rx = await tx.wait() console.log("Message confirmed!") + console.log("Checking status using SDK"); + let info: DeliveryInfo = (await getDeliveryInfoBySourceTx({environment: "DEVNET", sourceChain: sourceChain.chainId, sourceTransaction: tx.hash })) as DeliveryInfo + let status = info.targetChainStatuses[0].events[0].status + expect(status.substring(0, 22)).to.equal("Delivery didn't happen") + await new Promise((resolve) => { setTimeout(() => { resolve(0) - }, 4000) + }, 6000) }) - console.log("Checking if message was relayed") - const message1 = await targetMockIntegration.getMessage() - console.log( - `Sent message: ${arbitraryPayload1} (expecting ${arbitraryPayload2} from forward)` - ) - console.log(`Received message on target: ${message1}`) - expect(message1).to.equal(arbitraryPayload1) + const message = await targetMockIntegration.getMessage() + console.log(`Sent message: ${arbitraryPayload}`) + console.log(`Received message: ${message}`) + expect(message).to.not.equal(arbitraryPayload) - console.log("Checking if forward message was relayed back (it shouldn't have been!)") - const message2 = await sourceMockIntegration.getMessage() - console.log(`Sent message: ${arbitraryPayload2}`) - console.log(`Received message on source: ${message2}`) - expect(message2).to.not.equal(arbitraryPayload2) + console.log("Checking status using SDK"); + info = await getDeliveryInfoBySourceTx({environment: "DEVNET", sourceChain: sourceChain.chainId, sourceTransaction: tx.hash}) as DeliveryInfo; + status = info.targetChainStatuses[0].events[0].status + expect(status).to.equal("Receiver Failure") - // RESEND THE MESSAGE SOMEHOW! - - /*console.log("Resending the message"); + console.log("Resending the message"); const request: IWormholeRelayer.ResendByTxStruct = { sourceChain: sourceChain.chainId, sourceTxHash: tx.hash, @@ -413,30 +554,27 @@ describe("Core Relayer Integration Test - Two Chains", () => { newReceiverValue: 0, newRelayParameters: sourceCoreRelayer.getDefaultRelayParams() }; - await sourceCoreRelayer.resend(request, 1, sourceCoreRelayer.getDefaultRelayProvider(), {value: value, gasLimit: 500000}).then((t)=>t.wait); - console.log("Message resent");*/ + const newTx = await sourceCoreRelayer.resend(request, sourceCoreRelayer.getDefaultRelayProvider(), {value: value, gasLimit: 500000}); + await newTx.wait(); + console.log("Message resent"); - /* await new Promise((resolve) => { setTimeout(() => { resolve(0) - }, 4000) + }, 6000) }) console.log("Checking if message was relayed") - const message3 = await targetMockIntegration.getMessage() - console.log( - `Sent message: ${arbitraryPayload1} (expecting ${arbitraryPayload2} from forward)` - ) - console.log(`Received message on target: ${message3}`) - expect(message3).to.equal(arbitraryPayload1) + const messageNew = await targetMockIntegration.getMessage() + console.log(`Sent message: ${arbitraryPayload}`) + console.log(`Received message: ${messageNew}`) + expect(messageNew).to.equal(arbitraryPayload) - console.log("Checking if forward message was relayed back (it shouldn't have been!)") - const message4 = await sourceMockIntegration.getMessage() - console.log(`Sent message: ${arbitraryPayload2}`) - console.log(`Received message on source: ${message4}`) - expect(message4).to.equal(arbitraryPayload2) - */ + console.log("Checking status using SDK"); + info = await getDeliveryInfoBySourceTx({environment: "DEVNET", sourceChain: sourceChain.chainId, sourceTransaction: tx.hash}) as DeliveryInfo; + status = info.targetChainStatuses[0].events[1].status + expect(status).to.equal("Delivery Success") }) }) + diff --git a/ethereum/ts-test/helpers/consts.ts b/ethereum/ts-test/helpers/consts.ts index e80f5b5..65f75f1 100644 --- a/ethereum/ts-test/helpers/consts.ts +++ b/ethereum/ts-test/helpers/consts.ts @@ -1,5 +1,8 @@ import { ChainId } from "@certusone/wormhole-sdk"; + + + // signer export const ORACLE_DEPLOYER_PRIVATE_KEY = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"; export const RELAYER_DEPLOYER_PRIVATE_KEY = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"; diff --git a/relayer_engine/pkgs/sdk/package-lock.json b/relayer_engine/pkgs/sdk/package-lock.json index 2fa376e..2b6dabd 100644 --- a/relayer_engine/pkgs/sdk/package-lock.json +++ b/relayer_engine/pkgs/sdk/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@certusone/wormhole-sdk": "^0.9.6", + "@certusone/wormhole-sdk-proto-web": "^0.0.6", "@improbable-eng/grpc-web-node-http-transport": "^0.15.0", "@typechain/ethers-v5": "^10.1.0", "dotenv": "^16.0.2", diff --git a/relayer_engine/pkgs/sdk/package.json b/relayer_engine/pkgs/sdk/package.json index cae00f5..630bf4c 100644 --- a/relayer_engine/pkgs/sdk/package.json +++ b/relayer_engine/pkgs/sdk/package.json @@ -25,6 +25,7 @@ "license": "ISC", "dependencies": { "@certusone/wormhole-sdk": "^0.9.6", + "@certusone/wormhole-sdk-proto-web": "^0.0.6", "@improbable-eng/grpc-web-node-http-transport": "^0.15.0", "@typechain/ethers-v5": "^10.1.0", "dotenv": "^16.0.2", diff --git a/relayer_engine/pkgs/sdk/src/consts.ts b/relayer_engine/pkgs/sdk/src/consts.ts new file mode 100644 index 0000000..bf9b59c --- /dev/null +++ b/relayer_engine/pkgs/sdk/src/consts.ts @@ -0,0 +1,122 @@ +import { ChainId, Network, ChainName} from "@certusone/wormhole-sdk" +import { ethers } from "ethers" +import { CoreRelayer__factory } from "../src/ethers-contracts/factories/CoreRelayer__factory" +import { CoreRelayer } from "../src" + +const TESTNET = [ + { chainId: 4, coreRelayerAddress: "0xda2592C43f2e10cBBA101464326fb132eFD8cB09" }, + { chainId: 5, coreRelayerAddress: "0xFAd28FcD3B05B73bBf52A3c4d8b638dFf1c5605c" }, + { chainId: 6, coreRelayerAddress: "0xDDe6b89B7d0AD383FafDe6477f0d300eC4d4033e" }, + { chainId: 14, coreRelayerAddress: "0xA92aa4f8CBE1c2d7321F1575ad85bE396e2bbE0D" }, + { chainId: 16, coreRelayerAddress: "0x57523648FB5345CF510c1F12D346A18e55Aec5f5" }, +] + +const DEVNET = [ + { chainId: 2, coreRelayerAddress: "0x42D4BA5e542d9FeD87EA657f0295F1968A61c00A" }, + { chainId: 4, coreRelayerAddress: "0xFF5181e2210AB92a5c9db93729Bc47332555B9E9" }, +] + +const MAINNET: any[] = [] + +type ENV = "mainnet" | "testnet" + +export function getCoreRelayerAddressNative(chainId: ChainId, env: Network): string { + if (env == "TESTNET") { + const address = TESTNET.find((x) => x.chainId == chainId)?.coreRelayerAddress + if (!address) { + throw Error("Invalid chain ID") + } + return address + } else if (env == "MAINNET") { + const address = MAINNET.find((x) => x.chainId == chainId)?.coreRelayerAddress + if (!address) { + throw Error("Invalid chain ID") + } + return address + } else if (env == "DEVNET") { + const address = DEVNET.find((x) => x.chainId == chainId)?.coreRelayerAddress + if (!address) { + throw Error("Invalid chain ID") + } + return address + } else { + throw Error("Invalid environment") + } +} + +export function getCoreRelayer( + chainId: ChainId, + env: Network, + provider: ethers.providers.Provider +): CoreRelayer { + const thisChainsRelayer = getCoreRelayerAddressNative(chainId, env) + const contract = CoreRelayer__factory.connect(thisChainsRelayer, provider) + return contract +} + +export const RPCS_BY_CHAIN: { [key in Network]: {[key in ChainName]?: string} } = { + MAINNET: { + ethereum: process.env.ETH_RPC, + bsc: process.env.BSC_RPC || 'https://bsc-dataseed2.defibit.io', + polygon: 'https://rpc.ankr.com/polygon', + avalanche: 'https://rpc.ankr.com/avalanche', + oasis: 'https://emerald.oasis.dev', + algorand: 'https://mainnet-api.algonode.cloud', + fantom: 'https://rpc.ankr.com/fantom', + karura: 'https://eth-rpc-karura.aca-api.network', + acala: 'https://eth-rpc-acala.aca-api.network', + klaytn: 'https://klaytn-mainnet-rpc.allthatnode.com:8551', + celo: 'https://forno.celo.org', + moonbeam: 'https://rpc.ankr.com/moonbeam', + arbitrum: 'https://rpc.ankr.com/arbitrum', + optimism: 'https://rpc.ankr.com/optimism', + aptos: 'https://fullnode.mainnet.aptoslabs.com/', + near: 'https://rpc.mainnet.near.org', + xpla: 'https://dimension-lcd.xpla.dev', + terra2: 'https://phoenix-lcd.terra.dev', + terra: 'https://columbus-fcd.terra.dev', + injective: 'https://k8s.mainnet.lcd.injective.network', + solana: process.env.SOLANA_RPC ?? 'https://api.mainnet-beta.solana.com', + }, + TESTNET: { + solana: "https://api.devnet.solana.com", + terra: "https://bombay-lcd.terra.dev", + ethereum: "https://rpc.ankr.com/eth_goerli", + bsc: "https://data-seed-prebsc-1-s1.binance.org:8545", + polygon: "https://rpc.ankr.com/polygon_mumbai", + avalanche: "https://rpc.ankr.com/avalanche_fuji", + oasis: "https://testnet.emerald.oasis.dev", + algorand: "https://testnet-api.algonode.cloud", + fantom: "https://rpc.testnet.fantom.network", + aurora: "https://testnet.aurora.dev", + karura: "https://karura-dev.aca-dev.network/eth/http", + acala: "https://acala-dev.aca-dev.network/eth/http", + klaytn: "https://api.baobab.klaytn.net:8651", + celo: "https://alfajores-forno.celo-testnet.org", + near: "https://rpc.testnet.near.org", + injective: "https://k8s.testnet.tm.injective.network:443", + aptos: "https://fullnode.testnet.aptoslabs.com/v1", + pythnet: "https://api.pythtest.pyth.network/", + xpla: "https://cube-lcd.xpla.dev:443", + moonbeam: "https://rpc.api.moonbase.moonbeam.network", + neon: "https://proxy.devnet.neonlabs.org/solana", + terra2: "https://pisco-lcd.terra.dev", + arbitrum: "https://goerli-rollup.arbitrum.io/rpc", + optimism: "https://goerli.optimism.io", + gnosis: "https://sokol.poa.network/" + }, + DEVNET: { + ethereum: "http://localhost:8545", + bsc: "http://localhost:8546" + } +}; + + + +export const GUARDIAN_RPC_HOSTS = [ + 'https://wormhole-v2-mainnet-api.certus.one', + 'https://wormhole.inotel.ro', + 'https://wormhole-v2-mainnet-api.mcf.rocks', + 'https://wormhole-v2-mainnet-api.chainlayer.network', + 'https://wormhole-v2-mainnet-api.staking.fund', +]; diff --git a/relayer_engine/pkgs/sdk/src/index.ts b/relayer_engine/pkgs/sdk/src/index.ts index ada48c7..b98b50a 100644 --- a/relayer_engine/pkgs/sdk/src/index.ts +++ b/relayer_engine/pkgs/sdk/src/index.ts @@ -8,4 +8,6 @@ export type { RelayProvider } from "./ethers-contracts/RelayProvider" export { RelayProvider__factory } from "./ethers-contracts/factories/RelayProvider__factory" export type {IDelivery} from "./ethers-contracts/IDelivery" export type {IWormholeRelayer} from "./ethers-contracts/IWormholeRelayer" -export * from './structs' \ No newline at end of file +export * from './structs' +export * from "./consts" +export * from "./main/index" diff --git a/relayer_engine/pkgs/sdk/src/main/index.ts b/relayer_engine/pkgs/sdk/src/main/index.ts new file mode 100644 index 0000000..acdaf2e --- /dev/null +++ b/relayer_engine/pkgs/sdk/src/main/index.ts @@ -0,0 +1 @@ +export * from "./status" diff --git a/relayer_engine/pkgs/sdk/src/main/status.ts b/relayer_engine/pkgs/sdk/src/main/status.ts new file mode 100644 index 0000000..5e658da --- /dev/null +++ b/relayer_engine/pkgs/sdk/src/main/status.ts @@ -0,0 +1,359 @@ +import { + ChainId, + CHAIN_ID_TO_NAME, + CHAINS, + isChain, + CONTRACTS, + getSignedVAAWithRetry, + Network, + parseVaa, + ParsedVaa, + tryNativeToHexString, +} from "@certusone/wormhole-sdk" +import { GetSignedVAAResponse } from "@certusone/wormhole-sdk-proto-web/lib/cjs/publicrpc/v1/publicrpc" +import { Implementation__factory } from "@certusone/wormhole-sdk/lib/cjs/ethers-contracts" +import { BigNumber, ContractReceipt, ethers, providers } from "ethers" +import { + getCoreRelayer, + getCoreRelayerAddressNative, + RPCS_BY_CHAIN, + GUARDIAN_RPC_HOSTS, +} from "../consts" +import { + parsePayloadType, + RelayerPayloadId, + parseDeliveryInstructionsContainer, + parseRedeliveryByTxHashInstruction, + DeliveryInstruction, + DeliveryInstructionsContainer, + RedeliveryByTxHashInstruction, + ExecutionParameters, +} from "../structs" +import { DeliveryEvent } from "../ethers-contracts/CoreRelayer" + +enum DeliveryStatus { + WaitingForVAA = "Waiting for VAA", + PendingDelivery = "Pending Delivery", + DeliverySuccess = "Delivery Success", + ReceiverFailure = "Receiver Failure", + InvalidRedelivery = "Invalid Redelivery", + ForwardRequestSuccess = "Forward Request Success", + ForwardRequestFailure = "Forward Request Failure", + ThisShouldNeverHappen = "This should never happen. Contact Support.", + DeliveryDidntHappenWithinRange = "Delivery didn't happen within given block range", +} + +type DeliveryTargetInfo = { + status: DeliveryStatus | string + deliveryTxHash: string | null + vaaHash: string | null + sourceChain: number | null + sourceVaaSequence: BigNumber | null +} + +type InfoRequest = { + environment: Network + sourceChain: ChainId + sourceTransaction: string + sourceChainProvider?: ethers.providers.Provider + targetChainProviders?: Map + targetChainBlockRanges?: Map + sourceNonce?: number + coreRelayerWhMessageIndex?: number +} + +export function parseWormholeLog(log: ethers.providers.Log): { + type: RelayerPayloadId + parsed: DeliveryInstructionsContainer | RedeliveryByTxHashInstruction | string +} { + const abi = [ + "event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel);", + ] + const iface = new ethers.utils.Interface(abi) + const parsed = iface.parseLog(log) + const payload = Buffer.from(parsed.args.payload.substring(2), "hex") + const type = parsePayloadType(payload) + if (type == RelayerPayloadId.Delivery) { + return { type, parsed: parseDeliveryInstructionsContainer(payload) } + } else if (type == RelayerPayloadId.Redelivery) { + return { type, parsed: parseRedeliveryByTxHashInstruction(payload) } + } else { + throw Error("Invalid wormhole log"); + } +} + +export type DeliveryInfo = { + type: RelayerPayloadId.Delivery + sourceChainId: ChainId, + sourceTransactionHash: string + deliveryInstructionsContainer: DeliveryInstructionsContainer + targetChainStatuses: { + chainId: ChainId + events: { status: DeliveryStatus | string; transactionHash: string | null }[] + }[] +} + +export type RedeliveryInfo = { + type: RelayerPayloadId.Redelivery + redeliverySourceChainId: ChainId, + redeliverySourceTransactionHash: string + redeliveryInstruction: RedeliveryByTxHashInstruction +} + +export function printChain(chainId: number) { + return `${CHAIN_ID_TO_NAME[chainId as ChainId]} (Chain ${chainId})` +} + +export function printInfo(info: DeliveryInfo | RedeliveryInfo) { + console.log(stringifyInfo(info)); +} +export function stringifyInfo(info: DeliveryInfo | RedeliveryInfo): string { + let stringifiedInfo = ""; + if(info.type==RelayerPayloadId.Redelivery) { + stringifiedInfo += (`Found Redelivery request in transaction ${info.redeliverySourceTransactionHash} on ${printChain(info.redeliverySourceChainId)}\n`) + stringifiedInfo += (`Original Delivery Source Chain: ${printChain(info.redeliveryInstruction.sourceChain)}\n`) + stringifiedInfo += (`Original Delivery Source Transaction Hash: 0x${info.redeliveryInstruction.sourceTxHash.toString("hex")}\n`) + stringifiedInfo += (`Original Delivery Source Nonce: ${info.redeliveryInstruction.sourceNonce}\n`) + stringifiedInfo += (`Target Chain: ${printChain(info.redeliveryInstruction.targetChain)}\n`) + stringifiedInfo += (`multisendIndex: ${info.redeliveryInstruction.multisendIndex}\n`) + stringifiedInfo += (`deliveryIndex: ${info.redeliveryInstruction.deliveryIndex}\n`) + stringifiedInfo += (`New max amount (in target chain currency) to use for gas: ${info.redeliveryInstruction.newMaximumRefundTarget}\n`) + stringifiedInfo += (`New amount (in target chain currency) to pass into target address: ${info.redeliveryInstruction.newMaximumRefundTarget}\n`) + stringifiedInfo += (`New target chain gas limit: ${info.redeliveryInstruction.executionParameters.gasLimit}\n`) + stringifiedInfo += (`Relay Provider Delivery Address: 0x${info.redeliveryInstruction.executionParameters.providerDeliveryAddress.toString("hex")}\n`) + } else if(info.type==RelayerPayloadId.Delivery) { + stringifiedInfo += (`Found delivery request in transaction ${info.sourceTransactionHash} on ${printChain(info.sourceChainId)}\n`) + stringifiedInfo += ((info.deliveryInstructionsContainer.sufficientlyFunded ? "The delivery was funded\n" : "** NOTE: The delivery was NOT sufficiently funded. You did not have enough leftover funds to perform the forward **\n")) + const length = info.deliveryInstructionsContainer.instructions.length; + stringifiedInfo += (`\nMessages were requested to be sent to ${length} destination${length == 1 ? "" : "s"}:\n`) + stringifiedInfo += (info.deliveryInstructionsContainer.instructions.map((instruction: DeliveryInstruction, i) => { + let result = ""; + const targetChainName = CHAIN_ID_TO_NAME[instruction.targetChain as ChainId]; + result += `\n(Destination ${i}): Target address is 0x${instruction.targetAddress.toString("hex")} on ${printChain(instruction.targetChain)}\n` + result += `Max amount to use for gas: ${instruction.maximumRefundTarget} of ${targetChainName} currency\n` + result += instruction.receiverValueTarget.gt(0) ? `Amount to pass into target address: ${instruction.receiverValueTarget} of ${CHAIN_ID_TO_NAME[instruction.targetChain as ChainId]} currency\n` : `` + result += `Gas limit: ${instruction.executionParameters.gasLimit} ${targetChainName} gas\n` + result += `Relay Provider Delivery Address: 0x${instruction.executionParameters.providerDeliveryAddress.toString("hex")}\n` + result += info.targetChainStatuses[i].events.map((e, i) => (`Delivery attempt ${i+1}: ${e.status}${e.transactionHash ? ` (${targetChainName} transaction hash: ${e.transactionHash})` : ""}`)).join("\n") + return result; + }).join("\n")) + "\n" + } + return stringifiedInfo +} + +function getDefaultProvider(network: Network, chainId: ChainId) { + return new ethers.providers.StaticJsonRpcProvider( + RPCS_BY_CHAIN[network][CHAIN_ID_TO_NAME[chainId]] + ) +} + +export async function getDeliveryInfoBySourceTx( + infoRequest: InfoRequest +): Promise { + const sourceChainProvider = + infoRequest.sourceChainProvider || getDefaultProvider(infoRequest.environment, infoRequest.sourceChain); + if (!sourceChainProvider) + throw Error( + "No default RPC for this chain; pass in your own provider (as sourceChainProvider)" + ) + const receipt = await sourceChainProvider.getTransactionReceipt( + infoRequest.sourceTransaction + ) + if (!receipt) throw Error("Transaction has not been mined") + const bridgeAddress = + CONTRACTS[infoRequest.environment][CHAIN_ID_TO_NAME[infoRequest.sourceChain]].core + const coreRelayerAddress = getCoreRelayerAddressNative( + infoRequest.sourceChain, + infoRequest.environment + ) + if (!bridgeAddress || !coreRelayerAddress) { + throw Error(`Invalid chain ID or network: Chain ID ${infoRequest.sourceChain}, ${infoRequest.environment}`) + } + + const deliveryLog = findLog( + receipt, + bridgeAddress, + tryNativeToHexString(coreRelayerAddress, "ethereum"), + infoRequest.coreRelayerWhMessageIndex ? infoRequest.coreRelayerWhMessageIndex : 0, + infoRequest.sourceNonce?.toString() + ) + + const { type, parsed } = parseWormholeLog(deliveryLog.log) + + if (type == RelayerPayloadId.Redelivery) { + const redeliveryInstruction = parsed as RedeliveryByTxHashInstruction + return { + type, + redeliverySourceChainId: infoRequest.sourceChain, + redeliverySourceTransactionHash: infoRequest.sourceTransaction, + redeliveryInstruction, + } + } + + /* Potentially use 'guardianRPCHosts' to get status of VAA; code in comments at end [1] */ + + const deliveryInstructionsContainer = parsed as DeliveryInstructionsContainer + + const targetChainStatuses = await Promise.all(deliveryInstructionsContainer.instructions.map(async (instruction: DeliveryInstruction) => { + const targetChain = instruction.targetChain as ChainId; + if(!isChain(targetChain)) throw Error(`Invalid Chain: ${targetChain}`) + const targetChainProvider = + infoRequest.targetChainProviders?.get(targetChain) || + getDefaultProvider(infoRequest.environment, targetChain) + + if (!targetChainProvider) + throw Error( + "No default RPC for this chain; pass in your own provider (as targetChainProvider)" + ) + + const sourceChainBlock = await sourceChainProvider.getBlock(receipt.blockNumber); + const [blockStartNumber, blockEndNumber] = infoRequest.targetChainBlockRanges?.get(targetChain) || getBlockRange(targetChainProvider, sourceChainBlock.timestamp); + + const deliveryEvents = await pullEventsBySourceSequence( + infoRequest.environment, + targetChain, + targetChainProvider, + infoRequest.sourceChain, + BigNumber.from(deliveryLog.sequence), + blockStartNumber, + blockEndNumber + ) + if (deliveryEvents.length == 0) { + let status = `Delivery didn't happen on ${printChain(targetChain)} within blocks ${blockStartNumber} to ${blockEndNumber}.`; + try { + const blockStart = await targetChainProvider.getBlock(blockStartNumber); + const blockEnd = await targetChainProvider.getBlock(blockEndNumber); + status = `Delivery didn't happen on ${printChain(targetChain)} within blocks ${blockStart.number} to ${blockEnd.number} (within times ${new Date(blockStart.timestamp * 1000).toString()} to ${new Date(blockEnd.timestamp * 1000).toString()})` + } catch(e) { + + } + deliveryEvents.push({ + status, + deliveryTxHash: null, + vaaHash: null, + sourceChain: infoRequest.sourceChain, + sourceVaaSequence: BigNumber.from(deliveryLog.sequence), + }) + } + return { + chainId: targetChain, + events: deliveryEvents.map((e)=>({status: e.status, transactionHash: e.deliveryTxHash})) + } + })) + + return { + type, + sourceChainId: infoRequest.sourceChain, + sourceTransactionHash: infoRequest.sourceTransaction, + deliveryInstructionsContainer, + targetChainStatuses + } +} + +function getBlockRange(provider: ethers.providers.Provider, timestamp?: number): [ethers.providers.BlockTag, ethers.providers.BlockTag] { + return [-2040, "latest"] +} + +async function pullEventsBySourceSequence( + environment: Network, + targetChain: ChainId, + targetChainProvider: ethers.providers.Provider, + sourceChain: number, + sourceVaaSequence: BigNumber, + blockStartNumber: ethers.providers.BlockTag, + blockEndNumber: ethers.providers.BlockTag +): Promise { + const coreRelayer = getCoreRelayer(targetChain, environment, targetChainProvider) + + //TODO These compile errors on sourceChain look like an ethers bug + const deliveryEvents = coreRelayer.filters.Delivery( + null, + sourceChain, + sourceVaaSequence + ) + + // There is a max limit on RPCs sometimes for how many blocks to query + return await transformDeliveryEvents( + await coreRelayer.queryFilter(deliveryEvents, blockStartNumber, blockEndNumber), + targetChainProvider + ) +} + +function deliveryStatus(status: number) { + switch (status) { + case 0: + return DeliveryStatus.DeliverySuccess + case 1: + return DeliveryStatus.ReceiverFailure + case 2: + return DeliveryStatus.ForwardRequestFailure + case 3: + return DeliveryStatus.ForwardRequestSuccess + case 4: + return DeliveryStatus.InvalidRedelivery + default: + return DeliveryStatus.ThisShouldNeverHappen + } +} + +async function transformDeliveryEvents( + events: DeliveryEvent[], + targetProvider: ethers.providers.Provider +): Promise { + return Promise.all( + events.map(async (x) => { + return { + status: deliveryStatus(x.args[4]), + deliveryTxHash: x.transactionHash, + vaaHash: x.args[3], + sourceVaaSequence: x.args[2], + sourceChain: x.args[1], + } + }) + ) +} + +export function findLog( + receipt: ContractReceipt, + bridgeAddress: string, + emitterAddress: string, + index: number, + nonce?: string +): { log: ethers.providers.Log; sequence: string } { + const bridgeLogs = receipt.logs.filter((l) => { + return l.address === bridgeAddress + }) + + if (bridgeLogs.length == 0) { + throw Error("No core contract interactions found for this transaction.") + } + + const parsed = bridgeLogs.map((bridgeLog) => { + const log = Implementation__factory.createInterface().parseLog(bridgeLog) + return { + sequence: log.args[1].toString(), + nonce: log.args[2].toString(), + emitterAddress: tryNativeToHexString(log.args[0].toString(), "ethereum"), + log: bridgeLog, + } + }) + + const filtered = parsed.filter( + (x) => + x.emitterAddress == emitterAddress.toLowerCase() && + (!nonce || x.nonce == nonce.toLowerCase()) + ) + + if (filtered.length == 0) { + throw Error("No CoreRelayer contract interactions found for this transaction.") + } + + if (index >= filtered.length) { + throw Error("Specified delivery index is out of range.") + } else { + return { + log: filtered[index].log, + sequence: filtered[index].sequence, + } + } +} diff --git a/sdk/package-lock.json b/sdk/package-lock.json index 2fa376e..2b6dabd 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@certusone/wormhole-sdk": "^0.9.6", + "@certusone/wormhole-sdk-proto-web": "^0.0.6", "@improbable-eng/grpc-web-node-http-transport": "^0.15.0", "@typechain/ethers-v5": "^10.1.0", "dotenv": "^16.0.2", diff --git a/sdk/package.json b/sdk/package.json index cae00f5..630bf4c 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -25,6 +25,7 @@ "license": "ISC", "dependencies": { "@certusone/wormhole-sdk": "^0.9.6", + "@certusone/wormhole-sdk-proto-web": "^0.0.6", "@improbable-eng/grpc-web-node-http-transport": "^0.15.0", "@typechain/ethers-v5": "^10.1.0", "dotenv": "^16.0.2", diff --git a/sdk/src/consts.ts b/sdk/src/consts.ts new file mode 100644 index 0000000..bf9b59c --- /dev/null +++ b/sdk/src/consts.ts @@ -0,0 +1,122 @@ +import { ChainId, Network, ChainName} from "@certusone/wormhole-sdk" +import { ethers } from "ethers" +import { CoreRelayer__factory } from "../src/ethers-contracts/factories/CoreRelayer__factory" +import { CoreRelayer } from "../src" + +const TESTNET = [ + { chainId: 4, coreRelayerAddress: "0xda2592C43f2e10cBBA101464326fb132eFD8cB09" }, + { chainId: 5, coreRelayerAddress: "0xFAd28FcD3B05B73bBf52A3c4d8b638dFf1c5605c" }, + { chainId: 6, coreRelayerAddress: "0xDDe6b89B7d0AD383FafDe6477f0d300eC4d4033e" }, + { chainId: 14, coreRelayerAddress: "0xA92aa4f8CBE1c2d7321F1575ad85bE396e2bbE0D" }, + { chainId: 16, coreRelayerAddress: "0x57523648FB5345CF510c1F12D346A18e55Aec5f5" }, +] + +const DEVNET = [ + { chainId: 2, coreRelayerAddress: "0x42D4BA5e542d9FeD87EA657f0295F1968A61c00A" }, + { chainId: 4, coreRelayerAddress: "0xFF5181e2210AB92a5c9db93729Bc47332555B9E9" }, +] + +const MAINNET: any[] = [] + +type ENV = "mainnet" | "testnet" + +export function getCoreRelayerAddressNative(chainId: ChainId, env: Network): string { + if (env == "TESTNET") { + const address = TESTNET.find((x) => x.chainId == chainId)?.coreRelayerAddress + if (!address) { + throw Error("Invalid chain ID") + } + return address + } else if (env == "MAINNET") { + const address = MAINNET.find((x) => x.chainId == chainId)?.coreRelayerAddress + if (!address) { + throw Error("Invalid chain ID") + } + return address + } else if (env == "DEVNET") { + const address = DEVNET.find((x) => x.chainId == chainId)?.coreRelayerAddress + if (!address) { + throw Error("Invalid chain ID") + } + return address + } else { + throw Error("Invalid environment") + } +} + +export function getCoreRelayer( + chainId: ChainId, + env: Network, + provider: ethers.providers.Provider +): CoreRelayer { + const thisChainsRelayer = getCoreRelayerAddressNative(chainId, env) + const contract = CoreRelayer__factory.connect(thisChainsRelayer, provider) + return contract +} + +export const RPCS_BY_CHAIN: { [key in Network]: {[key in ChainName]?: string} } = { + MAINNET: { + ethereum: process.env.ETH_RPC, + bsc: process.env.BSC_RPC || 'https://bsc-dataseed2.defibit.io', + polygon: 'https://rpc.ankr.com/polygon', + avalanche: 'https://rpc.ankr.com/avalanche', + oasis: 'https://emerald.oasis.dev', + algorand: 'https://mainnet-api.algonode.cloud', + fantom: 'https://rpc.ankr.com/fantom', + karura: 'https://eth-rpc-karura.aca-api.network', + acala: 'https://eth-rpc-acala.aca-api.network', + klaytn: 'https://klaytn-mainnet-rpc.allthatnode.com:8551', + celo: 'https://forno.celo.org', + moonbeam: 'https://rpc.ankr.com/moonbeam', + arbitrum: 'https://rpc.ankr.com/arbitrum', + optimism: 'https://rpc.ankr.com/optimism', + aptos: 'https://fullnode.mainnet.aptoslabs.com/', + near: 'https://rpc.mainnet.near.org', + xpla: 'https://dimension-lcd.xpla.dev', + terra2: 'https://phoenix-lcd.terra.dev', + terra: 'https://columbus-fcd.terra.dev', + injective: 'https://k8s.mainnet.lcd.injective.network', + solana: process.env.SOLANA_RPC ?? 'https://api.mainnet-beta.solana.com', + }, + TESTNET: { + solana: "https://api.devnet.solana.com", + terra: "https://bombay-lcd.terra.dev", + ethereum: "https://rpc.ankr.com/eth_goerli", + bsc: "https://data-seed-prebsc-1-s1.binance.org:8545", + polygon: "https://rpc.ankr.com/polygon_mumbai", + avalanche: "https://rpc.ankr.com/avalanche_fuji", + oasis: "https://testnet.emerald.oasis.dev", + algorand: "https://testnet-api.algonode.cloud", + fantom: "https://rpc.testnet.fantom.network", + aurora: "https://testnet.aurora.dev", + karura: "https://karura-dev.aca-dev.network/eth/http", + acala: "https://acala-dev.aca-dev.network/eth/http", + klaytn: "https://api.baobab.klaytn.net:8651", + celo: "https://alfajores-forno.celo-testnet.org", + near: "https://rpc.testnet.near.org", + injective: "https://k8s.testnet.tm.injective.network:443", + aptos: "https://fullnode.testnet.aptoslabs.com/v1", + pythnet: "https://api.pythtest.pyth.network/", + xpla: "https://cube-lcd.xpla.dev:443", + moonbeam: "https://rpc.api.moonbase.moonbeam.network", + neon: "https://proxy.devnet.neonlabs.org/solana", + terra2: "https://pisco-lcd.terra.dev", + arbitrum: "https://goerli-rollup.arbitrum.io/rpc", + optimism: "https://goerli.optimism.io", + gnosis: "https://sokol.poa.network/" + }, + DEVNET: { + ethereum: "http://localhost:8545", + bsc: "http://localhost:8546" + } +}; + + + +export const GUARDIAN_RPC_HOSTS = [ + 'https://wormhole-v2-mainnet-api.certus.one', + 'https://wormhole.inotel.ro', + 'https://wormhole-v2-mainnet-api.mcf.rocks', + 'https://wormhole-v2-mainnet-api.chainlayer.network', + 'https://wormhole-v2-mainnet-api.staking.fund', +]; diff --git a/sdk/src/index.ts b/sdk/src/index.ts index ada48c7..b98b50a 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -8,4 +8,6 @@ export type { RelayProvider } from "./ethers-contracts/RelayProvider" export { RelayProvider__factory } from "./ethers-contracts/factories/RelayProvider__factory" export type {IDelivery} from "./ethers-contracts/IDelivery" export type {IWormholeRelayer} from "./ethers-contracts/IWormholeRelayer" -export * from './structs' \ No newline at end of file +export * from './structs' +export * from "./consts" +export * from "./main/index" diff --git a/sdk/src/main/index.ts b/sdk/src/main/index.ts new file mode 100644 index 0000000..acdaf2e --- /dev/null +++ b/sdk/src/main/index.ts @@ -0,0 +1 @@ +export * from "./status" diff --git a/sdk/src/main/status.ts b/sdk/src/main/status.ts new file mode 100644 index 0000000..5e658da --- /dev/null +++ b/sdk/src/main/status.ts @@ -0,0 +1,359 @@ +import { + ChainId, + CHAIN_ID_TO_NAME, + CHAINS, + isChain, + CONTRACTS, + getSignedVAAWithRetry, + Network, + parseVaa, + ParsedVaa, + tryNativeToHexString, +} from "@certusone/wormhole-sdk" +import { GetSignedVAAResponse } from "@certusone/wormhole-sdk-proto-web/lib/cjs/publicrpc/v1/publicrpc" +import { Implementation__factory } from "@certusone/wormhole-sdk/lib/cjs/ethers-contracts" +import { BigNumber, ContractReceipt, ethers, providers } from "ethers" +import { + getCoreRelayer, + getCoreRelayerAddressNative, + RPCS_BY_CHAIN, + GUARDIAN_RPC_HOSTS, +} from "../consts" +import { + parsePayloadType, + RelayerPayloadId, + parseDeliveryInstructionsContainer, + parseRedeliveryByTxHashInstruction, + DeliveryInstruction, + DeliveryInstructionsContainer, + RedeliveryByTxHashInstruction, + ExecutionParameters, +} from "../structs" +import { DeliveryEvent } from "../ethers-contracts/CoreRelayer" + +enum DeliveryStatus { + WaitingForVAA = "Waiting for VAA", + PendingDelivery = "Pending Delivery", + DeliverySuccess = "Delivery Success", + ReceiverFailure = "Receiver Failure", + InvalidRedelivery = "Invalid Redelivery", + ForwardRequestSuccess = "Forward Request Success", + ForwardRequestFailure = "Forward Request Failure", + ThisShouldNeverHappen = "This should never happen. Contact Support.", + DeliveryDidntHappenWithinRange = "Delivery didn't happen within given block range", +} + +type DeliveryTargetInfo = { + status: DeliveryStatus | string + deliveryTxHash: string | null + vaaHash: string | null + sourceChain: number | null + sourceVaaSequence: BigNumber | null +} + +type InfoRequest = { + environment: Network + sourceChain: ChainId + sourceTransaction: string + sourceChainProvider?: ethers.providers.Provider + targetChainProviders?: Map + targetChainBlockRanges?: Map + sourceNonce?: number + coreRelayerWhMessageIndex?: number +} + +export function parseWormholeLog(log: ethers.providers.Log): { + type: RelayerPayloadId + parsed: DeliveryInstructionsContainer | RedeliveryByTxHashInstruction | string +} { + const abi = [ + "event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel);", + ] + const iface = new ethers.utils.Interface(abi) + const parsed = iface.parseLog(log) + const payload = Buffer.from(parsed.args.payload.substring(2), "hex") + const type = parsePayloadType(payload) + if (type == RelayerPayloadId.Delivery) { + return { type, parsed: parseDeliveryInstructionsContainer(payload) } + } else if (type == RelayerPayloadId.Redelivery) { + return { type, parsed: parseRedeliveryByTxHashInstruction(payload) } + } else { + throw Error("Invalid wormhole log"); + } +} + +export type DeliveryInfo = { + type: RelayerPayloadId.Delivery + sourceChainId: ChainId, + sourceTransactionHash: string + deliveryInstructionsContainer: DeliveryInstructionsContainer + targetChainStatuses: { + chainId: ChainId + events: { status: DeliveryStatus | string; transactionHash: string | null }[] + }[] +} + +export type RedeliveryInfo = { + type: RelayerPayloadId.Redelivery + redeliverySourceChainId: ChainId, + redeliverySourceTransactionHash: string + redeliveryInstruction: RedeliveryByTxHashInstruction +} + +export function printChain(chainId: number) { + return `${CHAIN_ID_TO_NAME[chainId as ChainId]} (Chain ${chainId})` +} + +export function printInfo(info: DeliveryInfo | RedeliveryInfo) { + console.log(stringifyInfo(info)); +} +export function stringifyInfo(info: DeliveryInfo | RedeliveryInfo): string { + let stringifiedInfo = ""; + if(info.type==RelayerPayloadId.Redelivery) { + stringifiedInfo += (`Found Redelivery request in transaction ${info.redeliverySourceTransactionHash} on ${printChain(info.redeliverySourceChainId)}\n`) + stringifiedInfo += (`Original Delivery Source Chain: ${printChain(info.redeliveryInstruction.sourceChain)}\n`) + stringifiedInfo += (`Original Delivery Source Transaction Hash: 0x${info.redeliveryInstruction.sourceTxHash.toString("hex")}\n`) + stringifiedInfo += (`Original Delivery Source Nonce: ${info.redeliveryInstruction.sourceNonce}\n`) + stringifiedInfo += (`Target Chain: ${printChain(info.redeliveryInstruction.targetChain)}\n`) + stringifiedInfo += (`multisendIndex: ${info.redeliveryInstruction.multisendIndex}\n`) + stringifiedInfo += (`deliveryIndex: ${info.redeliveryInstruction.deliveryIndex}\n`) + stringifiedInfo += (`New max amount (in target chain currency) to use for gas: ${info.redeliveryInstruction.newMaximumRefundTarget}\n`) + stringifiedInfo += (`New amount (in target chain currency) to pass into target address: ${info.redeliveryInstruction.newMaximumRefundTarget}\n`) + stringifiedInfo += (`New target chain gas limit: ${info.redeliveryInstruction.executionParameters.gasLimit}\n`) + stringifiedInfo += (`Relay Provider Delivery Address: 0x${info.redeliveryInstruction.executionParameters.providerDeliveryAddress.toString("hex")}\n`) + } else if(info.type==RelayerPayloadId.Delivery) { + stringifiedInfo += (`Found delivery request in transaction ${info.sourceTransactionHash} on ${printChain(info.sourceChainId)}\n`) + stringifiedInfo += ((info.deliveryInstructionsContainer.sufficientlyFunded ? "The delivery was funded\n" : "** NOTE: The delivery was NOT sufficiently funded. You did not have enough leftover funds to perform the forward **\n")) + const length = info.deliveryInstructionsContainer.instructions.length; + stringifiedInfo += (`\nMessages were requested to be sent to ${length} destination${length == 1 ? "" : "s"}:\n`) + stringifiedInfo += (info.deliveryInstructionsContainer.instructions.map((instruction: DeliveryInstruction, i) => { + let result = ""; + const targetChainName = CHAIN_ID_TO_NAME[instruction.targetChain as ChainId]; + result += `\n(Destination ${i}): Target address is 0x${instruction.targetAddress.toString("hex")} on ${printChain(instruction.targetChain)}\n` + result += `Max amount to use for gas: ${instruction.maximumRefundTarget} of ${targetChainName} currency\n` + result += instruction.receiverValueTarget.gt(0) ? `Amount to pass into target address: ${instruction.receiverValueTarget} of ${CHAIN_ID_TO_NAME[instruction.targetChain as ChainId]} currency\n` : `` + result += `Gas limit: ${instruction.executionParameters.gasLimit} ${targetChainName} gas\n` + result += `Relay Provider Delivery Address: 0x${instruction.executionParameters.providerDeliveryAddress.toString("hex")}\n` + result += info.targetChainStatuses[i].events.map((e, i) => (`Delivery attempt ${i+1}: ${e.status}${e.transactionHash ? ` (${targetChainName} transaction hash: ${e.transactionHash})` : ""}`)).join("\n") + return result; + }).join("\n")) + "\n" + } + return stringifiedInfo +} + +function getDefaultProvider(network: Network, chainId: ChainId) { + return new ethers.providers.StaticJsonRpcProvider( + RPCS_BY_CHAIN[network][CHAIN_ID_TO_NAME[chainId]] + ) +} + +export async function getDeliveryInfoBySourceTx( + infoRequest: InfoRequest +): Promise { + const sourceChainProvider = + infoRequest.sourceChainProvider || getDefaultProvider(infoRequest.environment, infoRequest.sourceChain); + if (!sourceChainProvider) + throw Error( + "No default RPC for this chain; pass in your own provider (as sourceChainProvider)" + ) + const receipt = await sourceChainProvider.getTransactionReceipt( + infoRequest.sourceTransaction + ) + if (!receipt) throw Error("Transaction has not been mined") + const bridgeAddress = + CONTRACTS[infoRequest.environment][CHAIN_ID_TO_NAME[infoRequest.sourceChain]].core + const coreRelayerAddress = getCoreRelayerAddressNative( + infoRequest.sourceChain, + infoRequest.environment + ) + if (!bridgeAddress || !coreRelayerAddress) { + throw Error(`Invalid chain ID or network: Chain ID ${infoRequest.sourceChain}, ${infoRequest.environment}`) + } + + const deliveryLog = findLog( + receipt, + bridgeAddress, + tryNativeToHexString(coreRelayerAddress, "ethereum"), + infoRequest.coreRelayerWhMessageIndex ? infoRequest.coreRelayerWhMessageIndex : 0, + infoRequest.sourceNonce?.toString() + ) + + const { type, parsed } = parseWormholeLog(deliveryLog.log) + + if (type == RelayerPayloadId.Redelivery) { + const redeliveryInstruction = parsed as RedeliveryByTxHashInstruction + return { + type, + redeliverySourceChainId: infoRequest.sourceChain, + redeliverySourceTransactionHash: infoRequest.sourceTransaction, + redeliveryInstruction, + } + } + + /* Potentially use 'guardianRPCHosts' to get status of VAA; code in comments at end [1] */ + + const deliveryInstructionsContainer = parsed as DeliveryInstructionsContainer + + const targetChainStatuses = await Promise.all(deliveryInstructionsContainer.instructions.map(async (instruction: DeliveryInstruction) => { + const targetChain = instruction.targetChain as ChainId; + if(!isChain(targetChain)) throw Error(`Invalid Chain: ${targetChain}`) + const targetChainProvider = + infoRequest.targetChainProviders?.get(targetChain) || + getDefaultProvider(infoRequest.environment, targetChain) + + if (!targetChainProvider) + throw Error( + "No default RPC for this chain; pass in your own provider (as targetChainProvider)" + ) + + const sourceChainBlock = await sourceChainProvider.getBlock(receipt.blockNumber); + const [blockStartNumber, blockEndNumber] = infoRequest.targetChainBlockRanges?.get(targetChain) || getBlockRange(targetChainProvider, sourceChainBlock.timestamp); + + const deliveryEvents = await pullEventsBySourceSequence( + infoRequest.environment, + targetChain, + targetChainProvider, + infoRequest.sourceChain, + BigNumber.from(deliveryLog.sequence), + blockStartNumber, + blockEndNumber + ) + if (deliveryEvents.length == 0) { + let status = `Delivery didn't happen on ${printChain(targetChain)} within blocks ${blockStartNumber} to ${blockEndNumber}.`; + try { + const blockStart = await targetChainProvider.getBlock(blockStartNumber); + const blockEnd = await targetChainProvider.getBlock(blockEndNumber); + status = `Delivery didn't happen on ${printChain(targetChain)} within blocks ${blockStart.number} to ${blockEnd.number} (within times ${new Date(blockStart.timestamp * 1000).toString()} to ${new Date(blockEnd.timestamp * 1000).toString()})` + } catch(e) { + + } + deliveryEvents.push({ + status, + deliveryTxHash: null, + vaaHash: null, + sourceChain: infoRequest.sourceChain, + sourceVaaSequence: BigNumber.from(deliveryLog.sequence), + }) + } + return { + chainId: targetChain, + events: deliveryEvents.map((e)=>({status: e.status, transactionHash: e.deliveryTxHash})) + } + })) + + return { + type, + sourceChainId: infoRequest.sourceChain, + sourceTransactionHash: infoRequest.sourceTransaction, + deliveryInstructionsContainer, + targetChainStatuses + } +} + +function getBlockRange(provider: ethers.providers.Provider, timestamp?: number): [ethers.providers.BlockTag, ethers.providers.BlockTag] { + return [-2040, "latest"] +} + +async function pullEventsBySourceSequence( + environment: Network, + targetChain: ChainId, + targetChainProvider: ethers.providers.Provider, + sourceChain: number, + sourceVaaSequence: BigNumber, + blockStartNumber: ethers.providers.BlockTag, + blockEndNumber: ethers.providers.BlockTag +): Promise { + const coreRelayer = getCoreRelayer(targetChain, environment, targetChainProvider) + + //TODO These compile errors on sourceChain look like an ethers bug + const deliveryEvents = coreRelayer.filters.Delivery( + null, + sourceChain, + sourceVaaSequence + ) + + // There is a max limit on RPCs sometimes for how many blocks to query + return await transformDeliveryEvents( + await coreRelayer.queryFilter(deliveryEvents, blockStartNumber, blockEndNumber), + targetChainProvider + ) +} + +function deliveryStatus(status: number) { + switch (status) { + case 0: + return DeliveryStatus.DeliverySuccess + case 1: + return DeliveryStatus.ReceiverFailure + case 2: + return DeliveryStatus.ForwardRequestFailure + case 3: + return DeliveryStatus.ForwardRequestSuccess + case 4: + return DeliveryStatus.InvalidRedelivery + default: + return DeliveryStatus.ThisShouldNeverHappen + } +} + +async function transformDeliveryEvents( + events: DeliveryEvent[], + targetProvider: ethers.providers.Provider +): Promise { + return Promise.all( + events.map(async (x) => { + return { + status: deliveryStatus(x.args[4]), + deliveryTxHash: x.transactionHash, + vaaHash: x.args[3], + sourceVaaSequence: x.args[2], + sourceChain: x.args[1], + } + }) + ) +} + +export function findLog( + receipt: ContractReceipt, + bridgeAddress: string, + emitterAddress: string, + index: number, + nonce?: string +): { log: ethers.providers.Log; sequence: string } { + const bridgeLogs = receipt.logs.filter((l) => { + return l.address === bridgeAddress + }) + + if (bridgeLogs.length == 0) { + throw Error("No core contract interactions found for this transaction.") + } + + const parsed = bridgeLogs.map((bridgeLog) => { + const log = Implementation__factory.createInterface().parseLog(bridgeLog) + return { + sequence: log.args[1].toString(), + nonce: log.args[2].toString(), + emitterAddress: tryNativeToHexString(log.args[0].toString(), "ethereum"), + log: bridgeLog, + } + }) + + const filtered = parsed.filter( + (x) => + x.emitterAddress == emitterAddress.toLowerCase() && + (!nonce || x.nonce == nonce.toLowerCase()) + ) + + if (filtered.length == 0) { + throw Error("No CoreRelayer contract interactions found for this transaction.") + } + + if (index >= filtered.length) { + throw Error("Specified delivery index is out of range.") + } else { + return { + log: filtered[index].log, + sequence: filtered[index].sequence, + } + } +}