diff --git a/clients/js/README.md b/clients/js/README.md index 6ca8d0f4a..1511dfe80 100644 --- a/clients/js/README.md +++ b/clients/js/README.md @@ -255,7 +255,8 @@ Options: "avalanche", "oasis", "algorand", "aurora", "fantom", "karura", "acala", "klaytn", "celo", "near", "moonbeam", "neon", "terra2", "injective", "osmosis", "sui", "aptos", "arbitrum", "optimism", "gnosis", "pythnet", - "xpla", "btc", "base", "sei", "wormchain", "sepolia"] + "xpla", "btc", "base", "sei", "rootstock", "wormchain", "cosmoshub", "evmos", + "kujira", "sepolia"] -n, --network Network [required] [choices: "mainnet", "testnet", "devnet"] -a, --contract-address Contract to submit VAA to (override config) [string] @@ -310,13 +311,15 @@ Options: "avalanche", "oasis", "algorand", "aurora", "fantom", "karura", "acala", "klaytn", "celo", "near", "moonbeam", "neon", "terra2", "injective", "osmosis", "sui", "aptos", "arbitrum", "optimism", "gnosis", "pythnet", - "xpla", "btc", "base", "sei", "wormchain", "sepolia"] + "xpla", "btc", "base", "sei", "rootstock", "wormchain", "cosmoshub", "evmos", + "kujira", "sepolia"] --dst-chain destination chain [required] [choices: "solana", "ethereum", "terra", "bsc", "polygon", "avalanche", "oasis", "algorand", "aurora", "fantom", "karura", "acala", "klaytn", "celo", "near", "moonbeam", "neon", "terra2", "injective", "osmosis", "sui", "aptos", "arbitrum", "optimism", "gnosis", "pythnet", - "xpla", "btc", "base", "sei", "wormchain", "sepolia"] + "xpla", "btc", "base", "sei", "rootstock", "wormchain", "cosmoshub", "evmos", + "kujira", "sepolia"] --dst-addr destination address [string] [required] --token-addr token address [string] [default: native token] --amount token amount [string] [required] @@ -342,16 +345,15 @@ Options: ```sh Positionals: - network Network [choices: "mainnet", "testnet", "devnet"] - chain Source chain + network Network [choices: "mainnet", "testnet", "devnet"] + chain Source chain [choices: "unset", "solana", "ethereum", "terra", "bsc", "polygon", "avalanche", "oasis", "algorand", "aurora", "fantom", "karura", "acala", "klaytn", "celo", "near", "moonbeam", "neon", "terra2", "injective", "osmosis", "sui", "aptos", "arbitrum", "optimism", "gnosis", "pythnet", - "xpla", "btc", "base", "sei", "wormchain", "sepolia"] - tx Source transaction hash [string] - block-start Starting Block Range, i.e. -2048 [string] - block-end Ending Block Range, i.e. latest [string] + "xpla", "btc", "base", "sei", "rootstock", "wormchain", "cosmoshub", "evmos", + "kujira", "sepolia"] + tx Source transaction hash [string] Options: --help Show help [boolean] diff --git a/clients/js/package-lock.json b/clients/js/package-lock.json index bf6edd85d..016c30145 100644 --- a/clients/js/package-lock.json +++ b/clients/js/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@celo-tools/celo-ethers-wrapper": "^0.1.0", - "@certusone/wormhole-sdk": "^0.9.24-beta.0", + "@certusone/wormhole-sdk": "^0.10.5-beta.3", "@cosmjs/encoding": "^0.26.2", "@improbable-eng/grpc-web-node-http-transport": "^0.15.0", "@injectivelabs/networks": "^1.10.7", @@ -494,9 +494,9 @@ } }, "node_modules/@certusone/wormhole-sdk": { - "version": "0.9.24-beta.0", - "resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.9.24-beta.0.tgz", - "integrity": "sha512-SiUd9EAgPM5RfwPJ56I49K4Rgzkc5pVMqXDk2udRX+Q9dbvlaTW4d/Cu1dk+x1n6uSjAjact0pNz19C1yDebLA==", + "version": "0.10.5-beta.3", + "resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.10.5-beta.3.tgz", + "integrity": "sha512-Q1IrWYQ/NPpLcuPs3J8MbUeRwBxBOTZFwLs3jahohgUFCQhGuBwhgpv02CdEYBR/iZpgaDrj2Eo3HOe3uNlYVA==", "dependencies": { "@certusone/wormhole-sdk-proto-web": "0.0.6", "@certusone/wormhole-sdk-wasm": "^0.0.1", @@ -550,9 +550,9 @@ "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" }, "node_modules/@certusone/wormhole-sdk-proto-web/node_modules/protobufjs": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.3.tgz", - "integrity": "sha512-TtpvOqwB5Gdz/PQmOjgsrGH1nHjAQVCN7JG4A6r1sXRWESL5rNMAiRcBQlCAdKxZcAbstExQePYG8xof/JVRgg==", + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", + "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", @@ -2995,11 +2995,11 @@ } }, "node_modules/@project-serum/anchor/node_modules/cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", + "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", "dependencies": { - "node-fetch": "2.6.7" + "node-fetch": "^2.6.12" } }, "node_modules/@project-serum/anchor/node_modules/superstruct": { @@ -3710,25 +3710,6 @@ "node-fetch": "^2.6.12" } }, - "node_modules/algosdk/node_modules/node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -6687,9 +6668,9 @@ "license": "MIT" }, "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -8386,9 +8367,9 @@ "requires": {} }, "@certusone/wormhole-sdk": { - "version": "0.9.24-beta.0", - "resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.9.24-beta.0.tgz", - "integrity": "sha512-SiUd9EAgPM5RfwPJ56I49K4Rgzkc5pVMqXDk2udRX+Q9dbvlaTW4d/Cu1dk+x1n6uSjAjact0pNz19C1yDebLA==", + "version": "0.10.5-beta.3", + "resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.10.5-beta.3.tgz", + "integrity": "sha512-Q1IrWYQ/NPpLcuPs3J8MbUeRwBxBOTZFwLs3jahohgUFCQhGuBwhgpv02CdEYBR/iZpgaDrj2Eo3HOe3uNlYVA==", "requires": { "@certusone/wormhole-sdk-proto-web": "0.0.6", "@certusone/wormhole-sdk-wasm": "^0.0.1", @@ -8444,9 +8425,9 @@ "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" }, "protobufjs": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.3.tgz", - "integrity": "sha512-TtpvOqwB5Gdz/PQmOjgsrGH1nHjAQVCN7JG4A6r1sXRWESL5rNMAiRcBQlCAdKxZcAbstExQePYG8xof/JVRgg==", + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", + "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -10288,11 +10269,11 @@ }, "dependencies": { "cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", + "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", "requires": { - "node-fetch": "2.6.7" + "node-fetch": "^2.6.12" } }, "superstruct": { @@ -10891,14 +10872,6 @@ "requires": { "node-fetch": "^2.6.12" } - }, - "node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", - "requires": { - "whatwg-url": "^5.0.0" - } } } }, @@ -13396,9 +13369,9 @@ "version": "2.0.2" }, "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "requires": { "whatwg-url": "^5.0.0" } diff --git a/clients/js/package.json b/clients/js/package.json index 9868d29f6..910afc7f5 100644 --- a/clients/js/package.json +++ b/clients/js/package.json @@ -30,7 +30,7 @@ ], "dependencies": { "@celo-tools/celo-ethers-wrapper": "^0.1.0", - "@certusone/wormhole-sdk": "^0.9.24-beta.0", + "@certusone/wormhole-sdk": "^0.10.5-beta.3", "@cosmjs/encoding": "^0.26.2", "@improbable-eng/grpc-web-node-http-transport": "^0.15.0", "@injectivelabs/networks": "^1.10.7", diff --git a/clients/js/src/chains/generic/getOriginalAsset.ts b/clients/js/src/chains/generic/getOriginalAsset.ts index 515bd4c4a..36b98db7c 100644 --- a/clients/js/src/chains/generic/getOriginalAsset.ts +++ b/clients/js/src/chains/generic/getOriginalAsset.ts @@ -110,6 +110,10 @@ export const getOriginalAsset = async ( case "osmosis": case "pythnet": case "wormchain": + case "cosmoshub": + case "evmos": + case "kujira": + case "rootstock": throw new Error(`${chainName} not supported`); default: impossible(chainName); diff --git a/clients/js/src/chains/generic/getWrappedAssetAddress.ts b/clients/js/src/chains/generic/getWrappedAssetAddress.ts index 894042897..04046b069 100644 --- a/clients/js/src/chains/generic/getWrappedAssetAddress.ts +++ b/clients/js/src/chains/generic/getWrappedAssetAddress.ts @@ -156,6 +156,10 @@ export const getWrappedAssetAddress = async ( case "osmosis": case "pythnet": case "wormchain": + case "cosmoshub": + case "evmos": + case "kujira": + case "rootstock": throw new Error(`${chainName} not supported`); default: impossible(chainName); diff --git a/clients/js/src/chains/generic/provider.ts b/clients/js/src/chains/generic/provider.ts index cf4b7d347..a1390c949 100644 --- a/clients/js/src/chains/generic/provider.ts +++ b/clients/js/src/chains/generic/provider.ts @@ -154,6 +154,10 @@ export const getProviderForChain = ( case "osmosis": case "pythnet": case "wormchain": + case "cosmoshub": + case "evmos": + case "kujira": + case "rootstock": throw new Error(`${chainName} not supported`); default: impossible(chainName); diff --git a/clients/js/src/cmds/generate.ts b/clients/js/src/cmds/generate.ts index 5d6b6e0aa..e59530660 100644 --- a/clients/js/src/cmds/generate.ts +++ b/clients/js/src/cmds/generate.ts @@ -311,8 +311,6 @@ function parseAddress(chain: ChainName, address: string): string { return "0x" + evm_address(address); } else if (chain === "near") { return "0x" + evm_address(address); - } else if (chain === "osmosis") { - throw Error("OSMOSIS is not supported yet"); } else if (chain === "sui") { return "0x" + evm_address(address); } else if (chain === "aptos") { @@ -321,10 +319,16 @@ function parseAddress(chain: ChainName, address: string): string { } return sha3_256(Buffer.from(address)); // address is hash of fully qualified type - } else if (chain === "wormchain") { - return "0x" + tryNativeToHexString(address, chain); } else if (chain === "btc") { throw Error("btc is not supported yet"); + } else if (chain === "cosmoshub") { + throw Error("cosmoshub is not supported yet"); + } else if (chain === "evmos") { + throw Error("evmos is not supported yet"); + } else if (chain === "kujira") { + throw Error("kujira is not supported yet"); + } else if (chain === "rootstock") { + throw Error("rootstock is not supported yet"); } else { impossible(chain); } diff --git a/clients/js/src/cmds/status.ts b/clients/js/src/cmds/status.ts index b4f1c1826..b9a4d45e8 100644 --- a/clients/js/src/cmds/status.ts +++ b/clients/js/src/cmds/status.ts @@ -29,14 +29,6 @@ export const builder = (y: typeof yargs) => describe: "Source transaction hash", type: "string", demandOption: true, - } as const) - .positional("block-start", { - describe: "Starting Block Range, i.e. -2048", - type: "string", - } as const) - .positional("block-end", { - describe: "Ending Block Range, i.e. latest", - type: "string", } as const); export const handler = async ( argv: Awaited["argv"]> @@ -59,39 +51,16 @@ export const handler = async ( targetChainProviders.set( key as ChainName, new ethers.providers.JsonRpcProvider( - NETWORKS[network][key as ChainName].rpc + NETWORKS[network as Network][key as ChainName].rpc ) ); } - const targetChainBlockRanges = new Map< - ChainName, - [ethers.providers.BlockTag, ethers.providers.BlockTag] - >(); - const getBlockTag = (tagString: string): ethers.providers.BlockTag => { - if (+tagString) return parseInt(tagString); - return tagString; - }; - for (const key in NETWORKS[network]) { - targetChainBlockRanges.set(key as ChainName, [ - getBlockTag(argv["block-start"] || "-2048"), - getBlockTag(argv["block-end"] || "latest"), - ]); - } const info = await relayer.getWormholeRelayerInfo(chain, argv.tx, { environment: network, sourceChainProvider, targetChainProviders, - targetChainBlockRanges, }); console.log(relayer.stringifyWormholeRelayerInfo(info)); - if ( - info.targetChainStatus.events[0].status === - relayer.DeliveryStatus.DeliveryDidntHappenWithinRange - ) { - console.log( - "Try using the '--block-start' and '--block-end' flags to specify a different block range" - ); - } }; diff --git a/clients/js/src/cmds/submit.ts b/clients/js/src/cmds/submit.ts index bf1a12bc7..8d9e4eef0 100644 --- a/clients/js/src/cmds/submit.ts +++ b/clients/js/src/cmds/submit.ts @@ -184,6 +184,14 @@ async function executeSubmit( throw Error("Wormchain is not supported yet"); } else if (chain === "btc") { throw Error("btc is not supported yet"); + } else if (chain === "cosmoshub") { + throw Error("Cosmoshub is not supported yet"); + } else if (chain === "evmos") { + throw Error("Evmos is not supported yet"); + } else if (chain === "kujira") { + throw Error("kujira is not supported yet"); + } else if (chain === "rootstock") { + throw Error("rootstock is not supported yet"); } else { // If you get a type error here, hover over `chain`'s type and it tells you // which cases are not handled diff --git a/clients/js/src/cmds/transfer.ts b/clients/js/src/cmds/transfer.ts index f12b30d24..788be3a97 100644 --- a/clients/js/src/cmds/transfer.ts +++ b/clients/js/src/cmds/transfer.ts @@ -138,6 +138,14 @@ export const handler = async ( throw Error("Wormchain is not supported yet"); } else if (srcChain === "btc") { throw Error("btc is not supported yet"); + } else if (srcChain === "cosmoshub") { + throw Error("cosmoshub is not supported yet"); + } else if (srcChain === "evmos") { + throw Error("evmos is not supported yet"); + } else if (srcChain === "kujira") { + throw Error("kujira is not supported yet"); + } else if (srcChain === "rootstock") { + throw Error("rootstock is not supported yet"); } else { // If you get a type error here, hover over `chain`'s type and it tells you // which cases are not handled diff --git a/clients/js/src/consts/networks.ts b/clients/js/src/consts/networks.ts index e10ae6247..fec5e16ed 100644 --- a/clients/js/src/consts/networks.ts +++ b/clients/js/src/consts/networks.ts @@ -178,6 +178,21 @@ const MAINNET = { key: undefined, chain_id: undefined, }, + cosmoshub: { + rpc: undefined, + key: undefined, + chain_id: undefined, + }, + evmos: { + rpc: undefined, + key: undefined, + chain_id: undefined, + }, + kujira: { + rpc: undefined, + key: undefined, + chain_id: undefined, + }, }; const TESTNET = { @@ -343,6 +358,21 @@ const TESTNET = { key: getEnvVar("ETH_KEY_TESTNET"), chain_id: 31, }, + cosmoshub: { + rpc: undefined, + key: undefined, + chain_id: undefined, + }, + evmos: { + rpc: undefined, + key: undefined, + chain_id: undefined, + }, + kujira: { + rpc: undefined, + key: undefined, + chain_id: undefined, + }, }; const DEVNET = { @@ -488,6 +518,18 @@ const DEVNET = { rpc: undefined, key: undefined, }, + cosmoshub: { + rpc: undefined, + key: undefined, + }, + evmos: { + rpc: undefined, + key: undefined, + }, + kujira: { + rpc: undefined, + key: undefined, + }, }; /** diff --git a/sdk/js/package.json b/sdk/js/package.json index 422577be1..d6842ca36 100644 --- a/sdk/js/package.json +++ b/sdk/js/package.json @@ -28,7 +28,9 @@ "prepublishOnly": "echo \"disabled: npm test && npm run lint\"", "preversion": "npm run lint", "version": "npm run format && git add -A src", - "postversion": "git push && git push --tags" + "postversion": "git push && git push --tags", + "test-relayer-status": "npm run test-relayer-testnet -- ./src/relayer/__tests__/wormhole_relayer.ts -t 'Checks the status of a message'", + "test-relayer-manual-delivery": "npm run test-relayer-testnet -- ./src/relayer/__tests__/wormhole_relayer.ts -t 'custom manual delivery'" }, "keywords": [ "wormhole", @@ -37,7 +39,7 @@ "sdk", "solana", "ethereum", - "terra", + "terra", "bsc" ], "author": "certusone", diff --git a/sdk/js/src/relayer/__tests__/wormhole_relayer.ts b/sdk/js/src/relayer/__tests__/wormhole_relayer.ts index e780f523d..bbb94fa3a 100644 --- a/sdk/js/src/relayer/__tests__/wormhole_relayer.ts +++ b/sdk/js/src/relayer/__tests__/wormhole_relayer.ts @@ -25,9 +25,8 @@ import { } from "../../../"; import { GovernanceEmitter, MockGuardians } from "../../../src/mock"; import { Implementation__factory } from "../../ethers-contracts"; -import { deliver } from "../relayer"; +import { manualDelivery } from "../relayer"; import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport"; -import { getSignedVAAWithRetry } from "../../rpc"; import { packEVMExecutionInfoV1 } from "../structs"; const network: Network = getNetwork(); @@ -37,6 +36,7 @@ const sourceChain = network == "DEVNET" ? "ethereum" : "celo"; const targetChain = network == "DEVNET" ? "bsc" : "avalanche"; const testIfDevnet = () => (network == "DEVNET" ? test : test.skip); +const testIfNotDevnet = () => (network != "DEVNET" ? test : test.skip); type TestChain = { chainId: ChainId; @@ -178,10 +178,6 @@ describe("Wormhole Relayer Tests", () => { await waitForRelay(); - console.log("Checking status using SDK"); - const status = await getStatus(rx.transactionHash); - expect(status).toBe("Delivery Success"); - console.log("Checking if message was relayed"); const message = await target.mockIntegration.getMessage(); expect(message).toBe(arbitraryPayload); @@ -228,10 +224,6 @@ describe("Wormhole Relayer Tests", () => { await waitForRelay(); - console.log("Checking status using SDK"); - const status = await getStatus(tx.hash); - expect(status).toBe("Delivery Success"); - console.log("Checking if message was relayed"); const message = (await target.mockIntegration.getDeliveryData()) .additionalVaas[0]; @@ -239,87 +231,86 @@ describe("Wormhole Relayer Tests", () => { expect(parsedMessage.payload).toBe(arbitraryPayload); }); - test("Executes a Delivery Success with manual delivery", async () => { - const arbitraryPayload = getArbitraryBytes32(); - console.log(`Sent message: ${arbitraryPayload}`); + testIfNotDevnet()( + "Executes a Delivery Success with manual delivery", + async () => { + const arbitraryPayload = getArbitraryBytes32(); + console.log(`Sent message: ${arbitraryPayload}`); - const deliverySeq = await Implementation__factory.connect( - CONTRACTS[network][sourceChain].core || "", - source.provider - ).nextSequence(source.wormholeRelayerAddress); + const deliverySeq = await Implementation__factory.connect( + CONTRACTS[network][sourceChain].core || "", + source.provider + ).nextSequence(source.wormholeRelayerAddress); - const rx = await testSend(arbitraryPayload, false, true); + const rx = await testSend(arbitraryPayload, false, true); - await waitForRelay(); + await waitForRelay(); - // confirm that the message was not relayed successfully - { - const message = await target.mockIntegration.getMessage(); - expect(message).not.toBe(arbitraryPayload); - - console.log("Checking status using SDK"); - const status = await getStatus(rx.transactionHash); - expect(status).toBe("Receiver Failure"); - } - const [value, refundPerGasUnused] = await relayer.getPriceAndRefundInfo( - sourceChain, - targetChain, - REASONABLE_GAS_LIMIT, - optionalParams - ); - - const info = (await relayer.getWormholeRelayerInfo( - sourceChain, - rx.transactionHash, - { wormholeRelayerAddresses, ...optionalParams } - )) as relayer.DeliveryInfo; - - const rpc = getGuardianRPC(network, ci); - const emitterAddress = Buffer.from( - tryNativeToUint8Array(source.wormholeRelayerAddress, "ethereum") - ); - const deliveryVaa = await getSignedVAAWithRetry( - [rpc], - source.chainId, - emitterAddress.toString("hex"), - deliverySeq.toBigInt().toString(), - { transport: NodeHttpTransport() } - ); - - console.log(`Got delivery VAA: ${deliveryVaa}`); - const deliveryRx = await deliver( - deliveryVaa.vaaBytes, - target.wallet, - getGuardianRPC(network, ci), - network, + // confirm that the message was not relayed successfully { - newExecutionInfo: Buffer.from( - packEVMExecutionInfoV1({ - gasLimit: ethers.BigNumber.from(REASONABLE_GAS_LIMIT), - targetChainRefundPerGasUnused: - ethers.BigNumber.from(refundPerGasUnused), - }).substring(2), - "hex" - ), - newReceiverValue: ethers.BigNumber.from(0), - redeliveryHash: Buffer.from( - ethers.utils.keccak256("0x1234").substring(2), - "hex" - ), // fake a redelivery + const message = await target.mockIntegration.getMessage(); + expect(message).not.toBe(arbitraryPayload); } - ); - console.log("Manual delivery tx hash", deliveryRx.transactionHash); - console.log("Manual delivery tx status", deliveryRx.status); + const [value, refundPerGasUnused] = await relayer.getPriceAndRefundInfo( + sourceChain, + targetChain, + REASONABLE_GAS_LIMIT, + optionalParams + ); - console.log("Checking status using SDK"); - // Get the status of the second delivery (index 1) - const status = await getStatus(rx.transactionHash, undefined, 1); - expect(status).toBe("Delivery Success"); + const priceInfo = await manualDelivery( + sourceChain, + rx.transactionHash, + { wormholeRelayerAddresses, ...optionalParams }, + true, + { + newExecutionInfo: Buffer.from( + packEVMExecutionInfoV1({ + gasLimit: ethers.BigNumber.from(REASONABLE_GAS_LIMIT), + targetChainRefundPerGasUnused: + ethers.BigNumber.from(refundPerGasUnused), + }).substring(2), + "hex" + ), + newReceiverValue: ethers.BigNumber.from(0), + redeliveryHash: Buffer.from( + ethers.utils.keccak256("0x1234").substring(2), + "hex" + ), // fake a redelivery + } + ); - console.log("Checking if message was relayed"); - const message = await target.mockIntegration.getMessage(); - expect(message).toBe(arbitraryPayload); - }); + console.log(`Price: ${priceInfo.quote} of ${priceInfo.targetChain} wei`); + + const deliveryRx = await manualDelivery( + sourceChain, + rx.transactionHash, + { wormholeRelayerAddresses, ...optionalParams }, + false, + { + newExecutionInfo: Buffer.from( + packEVMExecutionInfoV1({ + gasLimit: ethers.BigNumber.from(REASONABLE_GAS_LIMIT), + targetChainRefundPerGasUnused: + ethers.BigNumber.from(refundPerGasUnused), + }).substring(2), + "hex" + ), + newReceiverValue: ethers.BigNumber.from(0), + redeliveryHash: Buffer.from( + ethers.utils.keccak256("0x1234").substring(2), + "hex" + ), // fake a redelivery + }, + target.wallet + ); + console.log("Manual delivery tx hash", deliveryRx.txHash); + + console.log("Checking if message was relayed"); + const message = await target.mockIntegration.getMessage(); + expect(message).toBe(arbitraryPayload); + } + ); testIfDevnet()("Test getPrice in Typescript SDK", async () => { const price = await relayer.getPrice( @@ -360,10 +351,6 @@ describe("Wormhole Relayer Tests", () => { await waitForRelay(); - console.log("Checking status using SDK"); - const status = await getStatus(tx.hash); - expect(status).toBe("Receiver Failure"); - const info = (await relayer.getWormholeRelayerInfo(sourceChain, tx.hash, { wormholeRelayerAddresses, ...optionalParams, @@ -373,14 +360,6 @@ describe("Wormhole Relayer Tests", () => { const newEndingBalance = await source.wallet.getBalance(); - console.log("Checking status of refund using SDK"); - console.log(relayer.stringifyWormholeRelayerInfo(info)); - const statusOfRefund = await getStatus( - info.targetChainStatus.events[0].transactionHash || "", - targetChain - ); - expect(statusOfRefund).toBe("Delivery Success"); - console.log(`Quoted gas delivery fee: ${value}`); console.log( `Cost (including gas) ${startingBalance.sub(endingBalance).toString()}` @@ -408,9 +387,6 @@ describe("Wormhole Relayer Tests", () => { const message = await target.mockIntegration.getMessage(); expect(message).not.toBe(arbitraryPayload); - - const status = await getStatus(rx.transactionHash); - expect(status).toBe("Receiver Failure"); }); test("Executes a receiver failure and then redelivery through SDK", async () => { @@ -424,10 +400,6 @@ describe("Wormhole Relayer Tests", () => { const message = await target.mockIntegration.getMessage(); expect(message).not.toBe(arbitraryPayload); - console.log("Checking status using SDK"); - const status = await getStatus(rx.transactionHash); - expect(status).toBe("Receiver Failure"); - const value = await relayer.getPrice( sourceChain, targetChain, @@ -625,6 +597,68 @@ describe("Wormhole Relayer Tests", () => { ethers.utils.getAddress((await getImplementationAddress()).substring(26)) ).toBe(ethers.utils.getAddress(newWormholeRelayerImplementationAddress)); }); + + testIfNotDevnet()("Checks the status of a message", async () => { + const txHash = + "0xa75e4100240e9b498a48fa29de32c9e62ec241bf4071a3c93fde0df5de53c507"; + const mySourceChain: ChainName = "celo"; + const environment: Network = "TESTNET"; + + const info = await relayer.getWormholeRelayerInfo(mySourceChain, txHash, { + environment, + }); + console.log(info.stringified); + }); + + testIfNotDevnet()("Tests custom manual delivery", async () => { + const txHash = + "0xc57d12cc789e4e9fa50d496cea62c2a0f11a7557c8adf42b3420e0585ba1f911"; + const mySourceChain: ChainName = "arbitrum"; + const targetProvider = undefined; + const environment: Network = "TESTNET"; + + const info = await relayer.getWormholeRelayerInfo(mySourceChain, txHash, { + environment, + }); + console.log(info.stringified); + + const priceInfo = await manualDelivery( + mySourceChain, + txHash, + { environment }, + true + ); + console.log(`Price info: ${JSON.stringify(priceInfo)}`); + + const signer = new ethers.Wallet( + PRIVATE_KEY, + targetProvider + ? new ethers.providers.JsonRpcProvider(targetProvider) + : getDefaultProvider(environment, priceInfo.targetChain) + ); + + console.log( + `Price: ${ethers.utils.formatEther(priceInfo.quote)} of ${ + priceInfo.targetChain + } currency` + ); + const balance = await signer.getBalance(); + console.log( + `My balance: ${ethers.utils.formatEther(balance)} of ${ + priceInfo.targetChain + } currency` + ); + + const deliveryRx = await manualDelivery( + mySourceChain, + txHash, + { environment }, + false, + undefined, + signer + ); + console.log("Manual delivery tx hash", deliveryRx.txHash); + }); }); function sleep(ms: number): Promise { diff --git a/sdk/js/src/relayer/consts.ts b/sdk/js/src/relayer/consts.ts index 48c00a4a0..40f8cfa9f 100644 --- a/sdk/js/src/relayer/consts.ts +++ b/sdk/js/src/relayer/consts.ts @@ -12,6 +12,11 @@ type AddressInfo = { }; const TESTNET: { [K in ChainName]?: AddressInfo } = { + ethereum: { + wormholeRelayerAddress: "0x28D8F1Be96f97C1387e94A53e00eCcFb4E75175a", + mockDeliveryProviderAddress: "0xD1463B4fe86166768d2ff51B1A928beBB5c9f375", + mockIntegrationAddress: "0xb81bc199b73AB34c393a4192C163252116a03370", + }, bsc: { wormholeRelayerAddress: "0x80aC94316391752A193C1c47E27D382b507c93F3", mockDeliveryProviderAddress: "0x60a86b97a7596eBFd25fb769053894ed0D9A8366", @@ -37,6 +42,16 @@ const TESTNET: { [K in ChainName]?: AddressInfo } = { mockDeliveryProviderAddress: "0x60a86b97a7596eBFd25fb769053894ed0D9A8366", mockIntegrationAddress: "0x3bF0c43d88541BBCF92bE508ec41e540FbF28C56", }, + arbitrum: { + wormholeRelayerAddress: "0xAd753479354283eEE1b86c9470c84D42f229FF43", + mockDeliveryProviderAddress: "0x90995DBd1aae85872451b50A569dE947D34ac4ee", + mockIntegrationAddress: "0x0de48f34E14d08934DA1eA2286Be1b2BED5c062a", + }, + optimism: { + wormholeRelayerAddress: "0x01A957A525a5b7A72808bA9D10c389674E459891", + mockDeliveryProviderAddress: "0xfCe1Df3EF22fe5Cb7e2f5988b7d58fF633a313a7", + mockIntegrationAddress: "0x421e0bb71dDeeC727Af79766423d33D8FD7dB963", + }, base: { wormholeRelayerAddress: "0xea8029CD7FCAEFFcD1F53686430Db0Fc8ed384E1", mockDeliveryProviderAddress: "0x60a86b97a7596eBFd25fb769053894ed0D9A8366", @@ -163,6 +178,7 @@ export const RPCS_BY_CHAIN: { terra: "https://columbus-fcd.terra.dev", injective: "https://k8s.mainnet.lcd.injective.network", solana: "https://api.mainnet-beta.solana.com", + base: "https://mainnet.base.org", }, TESTNET: { solana: "https://api.devnet.solana.com", @@ -191,6 +207,7 @@ export const RPCS_BY_CHAIN: { optimism: "https://goerli.optimism.io", gnosis: "https://sokol.poa.network/", rootstock: "https://public-node.rsk.co", + base: "https://goerli.base.org", }, DEVNET: { ethereum: "http://localhost:8545", @@ -205,3 +222,30 @@ export const GUARDIAN_RPC_HOSTS = [ "https://wormhole-v2-mainnet-api.chainlayer.network", "https://wormhole-v2-mainnet-api.staking.fund", ]; + +export const getCircleAPI = (environment: Network) => { + return (environment === "TESTNET" + ? "https://iris-api-sandbox.circle.com/v1/attestations/" + : "https://iris-api.circle.com/v1/attestations/"); +} + +export const getWormscanAPI = (_network: Network) => { + switch (_network) { + case "MAINNET": + return "https://api.wormscan.io/"; + case "TESTNET": + return "https://api.testnet.wormscan.io/"; + default: + // possible extension for tilt/ci - search through the guardian api + // at localhost:7071 (tilt) or guardian:7071 (ci) + throw new Error("Not testnet or mainnet - so no wormscan api access"); + } +} + +export const CCTP_DOMAIN_TO_NAME = [ + "ethereum", + "avalanche", + "optimism", + "arbitrum", + "base" +]; \ No newline at end of file diff --git a/sdk/js/src/relayer/relayer/deliver.ts b/sdk/js/src/relayer/relayer/deliver.ts index 400a1558b..e4ba441c0 100644 --- a/sdk/js/src/relayer/relayer/deliver.ts +++ b/sdk/js/src/relayer/relayer/deliver.ts @@ -1,6 +1,12 @@ import { BigNumber, ethers, ContractReceipt } from "ethers"; import { IWormholeRelayer__factory } from "../../ethers-contracts"; -import { ChainName, toChainName, ChainId, Network } from "../../utils"; +import { + ChainName, + toChainName, + ChainId, + Network, + CHAIN_ID_TO_NAME, +} from "../../utils"; import { SignedVaa, parseVaa } from "../../vaa"; import { getWormholeRelayerAddress } from "../consts"; import { @@ -11,23 +17,56 @@ import { parseEVMExecutionInfoV1, parseWormholeRelayerPayloadType, parseWormholeRelayerSend, - VaaKey, - KeyType, parseVaaKey, + MessageKey, + parseCCTPKey, } from "../structs"; -import { DeliveryTargetInfo } from "./helpers"; -import { getSignedVAAWithRetry } from "../../rpc"; +import { + DeliveryTargetInfo, + getCCTPMessageLogURL, + getDefaultProvider, + getWormscanInfo, +} from "./helpers"; +import { InfoRequestParams, getWormholeRelayerInfo } from "./info"; + +export type CCTPTransferParsed = { + amount: bigint; // decimals is 6 + mintRecipient: string; + destinationDomain: number; + estimatedAttestationSeconds: number; + attested: boolean; +}; +export type TokenTransferParsed = { + amount: bigint; + originAddress: string; + originChain: number; + targetAddress: string; + targetChain: number; + fromAddress: string | undefined; + name?: string; + symbol?: string; + decimals?: number; + signedVaaTimestamp?: number; +}; +export type AdditionalMessageParsed = + | CCTPTransferParsed + | TokenTransferParsed + | undefined; export type DeliveryInfo = { type: RelayerPayloadId.Delivery; sourceChain: ChainName; sourceTransactionHash: string; sourceDeliverySequenceNumber: number; + sourceTimestamp: number; + signingOfVaaTimestamp: number | undefined; deliveryInstruction: DeliveryInstruction; + additionalMessageInformation: AdditionalMessageParsed[]; targetChainStatus: { chain: ChainName; events: DeliveryTargetInfo[]; }; + stringified?: string; }; export type DeliveryArguments = { @@ -36,25 +75,81 @@ export type DeliveryArguments = { deliveryHash: string; }; +export async function manualDelivery( + sourceChain: ChainName, + sourceTransaction: string, + infoRequest?: InfoRequestParams, + getQuoteOnly?: boolean, + overrides?: DeliveryOverrideArgs, + signer?: ethers.Signer +): Promise<{ quote: BigNumber; targetChain: ChainName; txHash?: string }> { + const info = await getWormholeRelayerInfo( + sourceChain, + sourceTransaction, + infoRequest + ); + const environment = infoRequest?.environment || "MAINNET"; + const sourceProvider = + infoRequest?.sourceChainProvider || + getDefaultProvider(environment, sourceChain); + const receipt = await sourceProvider.getTransactionReceipt(sourceTransaction); + const wormholeRelayerAddress = + infoRequest?.wormholeRelayerAddresses?.get(sourceChain) || + getWormholeRelayerAddress(sourceChain, environment); + const response = await ( + await getWormscanInfo( + environment, + info.sourceChain, + info.sourceDeliverySequenceNumber, + wormholeRelayerAddress + ) + ).json(); + + const signedVaa = response.data.vaa; + const signedVaaBuffer = Buffer.from(signedVaa, "base64"); + const result: { quote: BigNumber; targetChain: ChainName; txHash?: string } = + { + quote: deliveryBudget(info.deliveryInstruction, overrides), + targetChain: + CHAIN_ID_TO_NAME[info.deliveryInstruction.targetChainId as ChainId], + txHash: undefined, + }; + if (getQuoteOnly) { + return result; + } else { + if (!signer) { + throw new Error("no signer provided"); + } + const deliveryReceipt = await deliver( + signedVaaBuffer, + signer, + environment, + overrides, + sourceChain, + receipt + ); + result.txHash = deliveryReceipt.transactionHash; + return result; + } +} + export async function deliver( deliveryVaa: SignedVaa, signer: ethers.Signer, - wormholeRPCs: string | string[], environment: Network = "MAINNET", - overrides?: DeliveryOverrideArgs + overrides?: DeliveryOverrideArgs, + sourceChain?: ChainName, + sourceReceipt?: ethers.providers.TransactionReceipt ): Promise { const { budget, deliveryInstruction, deliveryHash } = extractDeliveryArguments(deliveryVaa, overrides); - const vaaKeys = deliveryInstruction.messageKeys.map((key) => { - if (key.keyType !== KeyType.VAA) { - throw new Error( - "Only VAA keys are supported by manual delivery. Found: " + key.keyType - ); - } - return parseVaaKey(key.key); - }); - const additionalVaas = await fetchAdditionalVaas(wormholeRPCs, vaaKeys); + const additionalMessages = await fetchAdditionalMessages( + deliveryInstruction.messageKeys, + environment, + sourceChain, + sourceReceipt + ); const wormholeRelayerAddress = getWormholeRelayerAddress( toChainName(deliveryInstruction.targetChainId as ChainId), @@ -65,21 +160,20 @@ export async function deliver( signer ); const gasEstimate = await wormholeRelayer.estimateGas.deliver( - additionalVaas, + additionalMessages, deliveryVaa, signer.getAddress(), overrides ? packOverrides(overrides) : new Uint8Array(), { value: budget } ); const tx = await wormholeRelayer.deliver( - additionalVaas, + additionalMessages, deliveryVaa, signer.getAddress(), overrides ? packOverrides(overrides) : new Uint8Array(), { value: budget, gasLimit: gasEstimate.mul(2) } ); const rx = await tx.wait(); - console.log(`Delivered ${deliveryHash} on ${rx.blockNumber}`); return rx; } @@ -125,20 +219,79 @@ export function extractDeliveryArguments( }; } -export async function fetchAdditionalVaas( - wormholeRPCs: string | string[], - additionalVaaKeys: VaaKey[] -): Promise { - const rpcs = typeof wormholeRPCs === "string" ? [wormholeRPCs] : wormholeRPCs; - const vaas = await Promise.all( - additionalVaaKeys.map(async (vaaKey) => - getSignedVAAWithRetry( - rpcs, - vaaKey.chainId as ChainId, - vaaKey.emitterAddress.toString("hex"), - vaaKey.sequence.toBigInt().toString() - ) - ) +export async function fetchAdditionalMessages( + additionalMessageKeys: MessageKey[], + environment: Network, + sourceChain?: ChainName, + sourceReceipt?: ethers.providers.TransactionReceipt +): Promise<(Uint8Array | Buffer)[]> { + const messages = await Promise.all( + additionalMessageKeys.map(async (messageKey) => { + if (messageKey.keyType === 1) { + const vaaKey = parseVaaKey(messageKey.key); + const signedVaa = ( + await await ( + await getWormscanInfo( + environment, + CHAIN_ID_TO_NAME[vaaKey.chainId as ChainId], + vaaKey.sequence.toNumber(), + "0x" + vaaKey.emitterAddress.toString("hex") + ) + ).json() + ).data?.vaa; + if (!signedVaa) { + throw new Error( + `No signed VAA available on WormScan for vaaKey ${JSON.stringify( + vaaKey + )}` + ); + } + return Buffer.from(signedVaa, "base64"); + } else if (messageKey.keyType === 2) { + const cctpKey = parseCCTPKey(messageKey.key); + if (!sourceReceipt) + throw new Error( + "No source receipt provided - needed to obtain CCTP message" + ); + if (!environment) + throw new Error( + "No environment provided - needed to obtain CCTP message" + ); + if (!sourceChain) + throw new Error( + "No source chain provided - needed to obtain CCTP message" + ); + + const response = await getCCTPMessageLogURL( + cctpKey, + sourceChain, + sourceReceipt, + environment + ); + + // Try to get attestation + const attestationResponse = await fetch(response?.url || ""); + const attestationResponseJson = await attestationResponse.json(); + const attestation = attestationResponseJson.attestation; + if (!attestation) { + throw new Error( + `Unable to get attestation from Circle, for cctp key ${JSON.stringify( + cctpKey + )}, message ${response?.message}` + ); + } + return Buffer.from( + new ethers.utils.AbiCoder() + .encode(["bytes", "bytes"], [response?.message || [], attestation]) + .substring(2), + "hex" + ); + } else { + throw new Error( + `Message key type unknown: ${messageKey.keyType} (messageKey ${messageKey.key})` + ); + } + }) ); - return vaas.map((vaa) => vaa.vaaBytes); + return messages; } diff --git a/sdk/js/src/relayer/relayer/helpers.ts b/sdk/js/src/relayer/relayer/helpers.ts index 318682b77..b166a10e9 100644 --- a/sdk/js/src/relayer/relayer/helpers.ts +++ b/sdk/js/src/relayer/relayer/helpers.ts @@ -13,6 +13,10 @@ import { getWormholeRelayer, RPCS_BY_CHAIN, RELAYER_CONTRACTS, + getWormholeRelayerAddress, + getCircleAPI, + getWormscanAPI, + CCTP_DOMAIN_TO_NAME } from "../consts"; import { parseWormholeRelayerPayloadType, @@ -25,12 +29,15 @@ import { VaaKey, DeliveryOverrideArgs, parseRefundStatus, + RedeliveryInstruction, + parseWormholeRelayerResend, + CCTPKey, } from "../structs"; +import { InfoRequestParams } from "./info"; import { DeliveryProvider, DeliveryProvider__factory, Implementation__factory, - IWormholeRelayerDelivery__factory, } from "../../ethers-contracts/"; import { DeliveryEvent } from "../../ethers-contracts/WormholeRelayer"; import { VaaKeyStruct } from "../../ethers-contracts/IWormholeRelayer.sol/IWormholeRelayer"; @@ -39,17 +46,18 @@ export type DeliveryTargetInfo = { status: DeliveryStatus | string; transactionHash: string | null; vaaHash: string | null; - sourceChain: ChainName; + sourceChain: ChainName | null; sourceVaaSequence: BigNumber | null; gasUsed: BigNumber; refundStatus: RefundStatus; + timestamp?: number; revertString?: string; // Only defined if status is RECEIVER_FAILURE overrides?: DeliveryOverrideArgs; }; export function parseWormholeLog(log: ethers.providers.Log): { type: RelayerPayloadId; - parsed: DeliveryInstruction | string; + parsed: DeliveryInstruction | RedeliveryInstruction | string; } { const abi = [ "event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel)", @@ -60,6 +68,8 @@ export function parseWormholeLog(log: ethers.providers.Log): { const type = parseWormholeRelayerPayloadType(payload); if (type == RelayerPayloadId.Delivery) { return { type, parsed: parseWormholeRelayerSend(payload) }; + } else if (type == RelayerPayloadId.Redelivery) { + return { type, parsed: parseWormholeRelayerResend(payload) }; } else { throw Error("Invalid wormhole log"); } @@ -71,6 +81,21 @@ export function printChain(chainId: number) { return `${CHAIN_ID_TO_NAME[chainId as ChainId]} (Chain ${chainId})`; } +export function printCCTPDomain(domain: number) { + if (domain >= CCTP_DOMAIN_TO_NAME.length) + throw Error(`Invalid cctp domain: ${domain}`); + return `${CCTP_DOMAIN_TO_NAME[domain]} (Domain ${domain})`; +} + +export const estimatedAttestationTimeInSeconds = ( + sourceChain: string, + environment: Network +): number => { + const testnetTime = sourceChain === "avalanche" ? 20 : 60; + const mainnetTime = sourceChain === "avalanche" ? 20 : 60 * 13; + return environment === "TESTNET" ? testnetTime : mainnetTime; +}; + export function getDefaultProvider( network: Network, chain: ChainName, @@ -98,74 +123,47 @@ export function getDeliveryProvider( return contract; } -export function getBlockRange( - provider: ethers.providers.Provider, - timestamp?: number -): [ethers.providers.BlockTag, ethers.providers.BlockTag] { - return [-2040, "latest"]; -} - export async function getWormholeRelayerInfoBySourceSequence( environment: Network, targetChain: ChainName, targetChainProvider: ethers.providers.Provider, - sourceChain: ChainName, - sourceVaaSequence: BigNumber, - blockStartNumber: ethers.providers.BlockTag, - blockEndNumber: ethers.providers.BlockTag, + sourceChain: ChainName | undefined, + sourceVaaSequence: BigNumber | undefined, + blockRange: + | [ethers.providers.BlockTag, ethers.providers.BlockTag] + | undefined, targetWormholeRelayerAddress: string -): Promise<{ chain: ChainName; events: DeliveryTargetInfo[] }> { +): Promise { const deliveryEvents = await getWormholeRelayerDeliveryEventsBySourceSequence( environment, targetChain, targetChainProvider, sourceChain, sourceVaaSequence, - blockStartNumber, - blockEndNumber, + blockRange, targetWormholeRelayerAddress ); - if (deliveryEvents.length == 0) { - let status = `Delivery didn't happen on ${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 ${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, - transactionHash: null, - vaaHash: null, - sourceChain: sourceChain, - sourceVaaSequence, - gasUsed: BigNumber.from(0), - refundStatus: RefundStatus.RefundFail, - }); - } - const targetChainStatus = { - chain: targetChain, - events: deliveryEvents, - }; - return targetChainStatus; + return deliveryEvents; } export async function getWormholeRelayerDeliveryEventsBySourceSequence( environment: Network, targetChain: ChainName, targetChainProvider: ethers.providers.Provider, - sourceChain: ChainName, - sourceVaaSequence: BigNumber, - blockStartNumber: ethers.providers.BlockTag, - blockEndNumber: ethers.providers.BlockTag, + sourceChain: ChainName | undefined, + sourceVaaSequence: BigNumber | undefined, + blockRange: + | [ethers.providers.BlockTag, ethers.providers.BlockTag] + | undefined, targetWormholeRelayerAddress: string ): Promise { - const sourceChainId = CHAINS[sourceChain]; - if (!sourceChainId) throw Error(`Invalid source chain: ${sourceChain}`); + let sourceChainId = undefined; + if (sourceChain) { + sourceChainId = CHAINS[sourceChain]; + if (!sourceChainId) throw Error(`Invalid source chain: ${sourceChain}`); + } + const wormholeRelayer = getWormholeRelayer( targetChain, environment, @@ -173,71 +171,27 @@ export async function getWormholeRelayerDeliveryEventsBySourceSequence( targetWormholeRelayerAddress ); - const deliveryEvents = wormholeRelayer.filters.Delivery( + const deliveryEventsFilter = wormholeRelayer.filters.Delivery( null, sourceChainId, sourceVaaSequence ); - const deliveryEventsPreFilter: DeliveryEvent[] = - await wormholeRelayer.queryFilter( - deliveryEvents, - blockStartNumber, - blockEndNumber - ); + const deliveryEvents: DeliveryEvent[] = await wormholeRelayer.queryFilter( + deliveryEventsFilter, + blockRange ? blockRange[0] : -2000, + blockRange ? blockRange[1] : "latest" + ); - const isValid: boolean[] = await Promise.all( - deliveryEventsPreFilter.map((deliveryEvent) => - areSignaturesValid( - deliveryEvent.getTransaction(), - targetChain, - targetChainProvider, - environment - ) + const timestamps = await Promise.all( + deliveryEvents.map( + async (e) => + (await targetChainProvider.getBlock(e.blockNumber)).timestamp * 1000 ) ); // There is a max limit on RPCs sometimes for how many blocks to query - return await transformDeliveryEvents( - deliveryEventsPreFilter.filter((deliveryEvent, i) => isValid[i]) - ); -} - -async function areSignaturesValid( - transaction: Promise, - targetChain: ChainName, - targetChainProvider: ethers.providers.Provider, - environment: Network -) { - const coreAddress = CONTRACTS[environment][targetChain].core; - if (!coreAddress) - throw Error( - `No Wormhole Address for chain ${targetChain}, network ${environment}` - ); - - const wormhole = Implementation__factory.connect( - coreAddress, - targetChainProvider - ); - const decodedData = - IWormholeRelayerDelivery__factory.createInterface().parseTransaction( - await transaction - ); - - const vaaIsValid = async (vaa: ethers.utils.BytesLike): Promise => { - const [, result, reason] = await wormhole.parseAndVerifyVM(vaa); - if (!result) console.log(`Invalid vaa! Reason: ${reason}`); - return result; - }; - - const vaas = decodedData.args[0]; - for (let i = 0; i < vaas.length; i++) { - if (!(await vaaIsValid(vaas[i]))) { - return false; - } - } - - return true; + return await transformDeliveryEvents(deliveryEvents, timestamps); } export function deliveryStatus(status: number) { @@ -251,20 +205,23 @@ export function deliveryStatus(status: number) { } } -export function transformDeliveryLog(log: { - args: [ - string, - number, - BigNumber, - string, - number, - BigNumber, - number, - string, - string - ]; - transactionHash: string; -}): DeliveryTargetInfo { +export function transformDeliveryLog( + log: { + args: [ + string, + number, + BigNumber, + string, + number, + BigNumber, + number, + string, + string + ]; + transactionHash: string; + }, + timestamp: number +): DeliveryTargetInfo { const status = deliveryStatus(log.args[4]); if (!isChain(log.args[1])) throw Error(`Invalid source chain id: ${log.args[1]}`); @@ -279,6 +236,7 @@ export function transformDeliveryLog(log: { refundStatus: parseRefundStatus(log.args[6]), revertString: status == DeliveryStatus.ReceiverFailure ? log.args[7] : undefined, + timestamp, overrides: Buffer.from(log.args[8].substring(2), "hex").length > 0 ? parseOverrideInfoFromDeliveryEvent( @@ -289,17 +247,19 @@ export function transformDeliveryLog(log: { } async function transformDeliveryEvents( - events: DeliveryEvent[] + events: DeliveryEvent[], + timestamps: number[] ): Promise { - return events.map((x) => transformDeliveryLog(x)); + return events.map((x, i) => transformDeliveryLog(x, timestamps[i])); } -export function getWormholeRelayerLog( +export function getWormholeLog( receipt: ContractReceipt, bridgeAddress: string, emitterAddress: string, - index: number -): { log: ethers.providers.Log; sequence: string } { + index: number, + sequence?: number +): { log: ethers.providers.Log; sequence: string; payload: string } { const bridgeLogs = receipt.logs.filter((l) => { return l.address === bridgeAddress; }); @@ -314,17 +274,23 @@ export function getWormholeRelayerLog( sequence: log.args[1].toString(), nonce: log.args[2].toString(), emitterAddress: tryNativeToHexString(log.args[0].toString(), "ethereum"), + payload: log.args[3], log: bridgeLog, }; }); - const filtered = parsed.filter( - (x) => x.emitterAddress == emitterAddress.toLowerCase() - ); + const filtered = parsed.filter((x) => { + return ( + x.emitterAddress == emitterAddress.toLowerCase() && + (sequence === undefined ? true : x.sequence + "" === sequence + "") + ); + }); if (filtered.length == 0) { throw Error( - "No WormholeRelayer contract interactions found for this transaction." + `No wormhole contract interactions found for this transaction, with emitter address ${emitterAddress} ${ + sequence === undefined ? "" : `and sequence ${sequence}` + }` ); } @@ -334,6 +300,7 @@ export function getWormholeRelayerLog( return { log: filtered[index].log, sequence: filtered[index].sequence, + payload: filtered[index].payload, }; } } @@ -348,6 +315,145 @@ export function vaaKeyToVaaKeyStruct(vaaKey: VaaKey): VaaKeyStruct { }; } +export async function getWormholeRelayerInfoByHash( + deliveryHash: string, + targetChain: ChainName, + sourceChain: ChainName | undefined, + sourceVaaSequence: number | undefined, + infoRequest?: InfoRequestParams +): Promise { + const environment = infoRequest?.environment || "MAINNET"; + const targetChainProvider = + infoRequest?.targetChainProviders?.get(targetChain) || + getDefaultProvider(environment, targetChain); + + if (!targetChainProvider) { + throw Error( + "No default RPC for this chain; pass in your own provider (as targetChainProvider)" + ); + } + const targetWormholeRelayerAddress = + infoRequest?.wormholeRelayerAddresses?.get(targetChain) || + getWormholeRelayerAddress(targetChain, environment); + const wormholeRelayer = getWormholeRelayer( + targetChain, + environment, + targetChainProvider, + targetWormholeRelayerAddress + ); + + const blockNumberSuccess = await wormholeRelayer.deliverySuccessBlock( + deliveryHash + ); + const blockNumberFailure = await wormholeRelayer.deliveryFailureBlock( + deliveryHash + ); + const blockNumber = blockNumberSuccess.gt(0) + ? blockNumberSuccess + : blockNumberFailure; + + if (blockNumber.toNumber() === 0) return []; + + // There is weirdness with arbitrum where if you call 'block.number', it gives you the L1 block number (the ethereum one) - and this is what is stored in the 'replay protection mapping' - so basically that value isn't useful in finding the delivery here + const blockRange = + infoRequest?.targetBlockRange || + (targetChain === "arbitrum" + ? undefined + : [blockNumber.toNumber(), blockNumber.toNumber()]); + + return await getWormholeRelayerInfoBySourceSequence( + environment, + targetChain, + targetChainProvider, + sourceChain, + BigNumber.from(sourceVaaSequence), + blockRange, + targetWormholeRelayerAddress + ); +} + +export function getDeliveryHashFromVaaFields( + sourceChain: number, + emitterAddress: string, + sequence: number, + timestamp: number, + nonce: number, + consistencyLevel: number, + deliveryVaaPayload: string +): string { + const body = ethers.utils.solidityPack( + ["uint32", "uint32", "uint16", "bytes32", "uint64", "uint8", "bytes"], + + [ + timestamp, + nonce, + sourceChain, + emitterAddress, + sequence, + consistencyLevel, + deliveryVaaPayload, + ] + ); + const deliveryHash = ethers.utils.keccak256(ethers.utils.keccak256(body)); + return deliveryHash; +} + +export async function getWormscanInfo( + network: Network, + sourceChain: ChainName, + sequence: number, + emitterAddress: string +) { + const wormscanAPI = getWormscanAPI(network); + const emitterAddressBytes32 = tryNativeToHexString( + emitterAddress, + sourceChain + ); + const sourceChainId = CHAINS[sourceChain]; + const result = await fetch( + `${wormscanAPI}api/v1/vaas/${sourceChainId}/${emitterAddressBytes32}/${sequence}` + ); + return result; +} + +export async function getWormscanRelayerInfo( + sourceChain: ChainName, + sequence: number, + optionalParams?: { + network?: Network; + provider?: ethers.providers.Provider; + wormholeRelayerAddress?: string; + } +): Promise { + const network = optionalParams?.network || "MAINNET"; + const wormholeRelayerAddress = + optionalParams?.wormholeRelayerAddress || + getWormholeRelayerAddress(sourceChain, network); + return getWormscanInfo( + network, + sourceChain, + sequence, + wormholeRelayerAddress + ); +} + +export async function getRelayerTransactionHashFromWormscan( + sourceChain: ChainName, + sequence: number, + optionalParams?: { + network?: Network; + provider?: ethers.providers.Provider; + wormholeRelayerAddress?: string; + } +): Promise { + const wormscanData = ( + await ( + await getWormscanRelayerInfo(sourceChain, sequence, optionalParams) + ).json() + ).data; + return "0x" + wormscanData.txHash; +} + export async function getDeliveryHash( rx: ethers.ContractReceipt, sourceChain: ChainName, @@ -355,6 +461,7 @@ export async function getDeliveryHash( network?: Network; provider?: ethers.providers.Provider; index?: number; + wormholeRelayerAddress?: string; } ): Promise { const network: Network = optionalParams?.network || "MAINNET"; @@ -365,6 +472,7 @@ export async function getDeliveryHash( throw Error(`No wormhole contract on ${sourceChain} for ${network}`); } const wormholeRelayerAddress = + optionalParams?.wormholeRelayerAddress || RELAYER_CONTRACTS[network][sourceChain]?.wormholeRelayerAddress; if (!wormholeRelayerAddress) { throw Error( @@ -385,23 +493,71 @@ export async function getDeliveryHash( index > 0 ? ` (the ${index}-th wormhole relayer log was requested)` : "" }` ); - const log = logs[index]; - const wormholePublishedMessage = - Implementation__factory.createInterface().parseLog(log); - const block = await provider.getBlock(rx.blockHash); - const body = ethers.utils.solidityPack( - ["uint32", "uint32", "uint16", "bytes32", "uint64", "uint8", "bytes"], - - [ - block.timestamp, - wormholePublishedMessage.args["nonce"], - CHAINS[sourceChain], - log.topics[1], - wormholePublishedMessage.args["sequence"], - wormholePublishedMessage.args["consistencyLevel"], - wormholePublishedMessage.args["payload"], - ] + return getDeliveryHashFromLog( + logs[index], + CHAINS[sourceChain], + provider, + rx.blockHash ); - const deliveryHash = ethers.utils.keccak256(ethers.utils.keccak256(body)); - return deliveryHash; +} + +export async function getDeliveryHashFromLog( + wormholeLog: ethers.providers.Log, + sourceChain: ChainId, + provider: ethers.providers.Provider, + blockHash: string +): Promise { + const wormholePublishedMessage = + Implementation__factory.createInterface().parseLog(wormholeLog); + + const block = await provider.getBlock(blockHash); + + return getDeliveryHashFromVaaFields( + sourceChain, + wormholeLog.topics[1], + wormholePublishedMessage.args["sequence"], + block.timestamp, + wormholePublishedMessage.args["nonce"], + wormholePublishedMessage.args["consistencyLevel"], + wormholePublishedMessage.args["payload"] + ); +} + +export async function getCCTPMessageLogURL( + cctpKey: CCTPKey, + sourceChain: ChainName, + receipt: ethers.providers.TransactionReceipt, + environment: Network +) { + let cctpLog; + let messageSentLog; + const DepositForBurnTopic = + ethers.utils.keccak256("DepositForBurn(uint64,address,uint256,address,bytes32,uint32,bytes32,bytes32)"); + const MessageSentTopic = ethers.utils.keccak256("MessageSent(bytes)") + try { + if (CCTP_DOMAIN_TO_NAME[cctpKey.domain] === sourceChain) { + const cctpLogFilter = (log: ethers.providers.Log) => { + return ( + log.topics[0] === DepositForBurnTopic && + parseInt(log.topics[1]) === cctpKey.nonce.toNumber() + ); + }; + cctpLog = receipt.logs.find(cctpLogFilter); + const index = receipt.logs.findIndex(cctpLogFilter); + const messageSentLogs = receipt.logs.filter((log, i) => { + return log.topics[0] === MessageSentTopic && i <= index; + }); + messageSentLog = messageSentLogs[messageSentLogs.length - 1]; + } + } catch (e) { + console.log(e); + } + if (!cctpLog || !messageSentLog) return undefined; + + const message = new ethers.utils.Interface([ + "event MessageSent(bytes message)", + ]).parseLog(messageSentLog).args.message; + const msgHash = ethers.utils.keccak256(message); + const url = getCircleAPI(environment) + msgHash; + return { message, cctpLog, url }; } diff --git a/sdk/js/src/relayer/relayer/info.ts b/sdk/js/src/relayer/relayer/info.ts index 451bd369e..8310f6846 100644 --- a/sdk/js/src/relayer/relayer/info.ts +++ b/sdk/js/src/relayer/relayer/info.ts @@ -8,6 +8,7 @@ import { tryNativeToHexString, Network, ethers_contracts, + parseTransferPayload, } from "../.."; import { BigNumber, ethers } from "ethers"; import { getWormholeRelayerAddress } from "../consts"; @@ -20,27 +21,37 @@ import { KeyType, parseVaaKey, parseCCTPKey, + RedeliveryInstruction, } from "../structs"; import { getDefaultProvider, printChain, - getWormholeRelayerLog, + printCCTPDomain, + getWormholeLog, parseWormholeLog, - getBlockRange, - getWormholeRelayerInfoBySourceSequence, + getDeliveryHashFromLog, + getRelayerTransactionHashFromWormscan, + getWormholeRelayerInfoByHash, + getWormscanRelayerInfo, + getWormscanInfo, + estimatedAttestationTimeInSeconds, + getCCTPMessageLogURL, } from "./helpers"; -import { DeliveryInfo } from "./deliver"; +import { + AdditionalMessageParsed, + CCTPTransferParsed, + DeliveryInfo, + TokenTransferParsed, +} from "./deliver"; +import { ERC20__factory } from "../../ethers-contracts"; export type InfoRequestParams = { environment?: Network; sourceChainProvider?: ethers.providers.Provider; targetChainProviders?: Map; - targetChainBlockRanges?: Map< - ChainName, - [ethers.providers.BlockTag, ethers.providers.BlockTag] - >; wormholeRelayerWhMessageIndex?: number; wormholeRelayerAddresses?: Map; + targetBlockRange?: [ethers.providers.BlockTag, ethers.providers.BlockTag]; }; export type GetPriceOptParams = { @@ -119,7 +130,12 @@ export async function getWormholeRelayerInfo( const receipt = await sourceChainProvider.getTransactionReceipt( sourceTransaction ); - if (!receipt) throw Error("Transaction has not been mined"); + if (!receipt) + throw Error( + `Transaction has not been mined: ${sourceTransaction} on ${sourceChain} (${environment})` + ); + const sourceTimestamp = + (await sourceChainProvider.getBlock(receipt.blockNumber)).timestamp * 1000; const bridgeAddress = CONTRACTS[environment][sourceChain].core; const wormholeRelayerAddress = infoRequest?.wormholeRelayerAddresses?.get(sourceChain) || @@ -129,7 +145,7 @@ export async function getWormholeRelayerInfo( `Invalid chain ID or network: Chain ${sourceChain}, ${environment}` ); } - const deliveryLog = getWormholeRelayerLog( + const deliveryLog = getWormholeLog( receipt, bridgeAddress, tryNativeToHexString(wormholeRelayerAddress, "ethereum"), @@ -140,9 +156,54 @@ export async function getWormholeRelayerInfo( const { type, parsed } = parseWormholeLog(deliveryLog.log); + if (type === RelayerPayloadId.Redelivery) { + const redeliveryInstruction = parsed as RedeliveryInstruction; + + if (!isChain(redeliveryInstruction.deliveryVaaKey.chainId)) { + throw new Error( + `The chain ID specified by this redelivery is invalid: ${redeliveryInstruction.deliveryVaaKey.chainId}` + ); + } + if (!isChain(redeliveryInstruction.targetChainId)) { + throw new Error( + `The target chain ID specified by this redelivery is invalid: ${redeliveryInstruction.targetChainId}` + ); + } + + const originalSourceChainName = + CHAIN_ID_TO_NAME[redeliveryInstruction.deliveryVaaKey.chainId as ChainId]; + + const modifiedInfoRequest = infoRequest; + if (modifiedInfoRequest?.sourceChainProvider) { + modifiedInfoRequest.sourceChainProvider = + modifiedInfoRequest?.targetChainProviders?.get(originalSourceChainName); + } + + const transactionHash = await getRelayerTransactionHashFromWormscan( + originalSourceChainName, + redeliveryInstruction.deliveryVaaKey.sequence.toNumber(), + { + network: infoRequest?.environment, + provider: infoRequest?.targetChainProviders?.get( + originalSourceChainName + ), + wormholeRelayerAddress: infoRequest?.wormholeRelayerAddresses?.get( + originalSourceChainName + ), + } + ); + + return getWormholeRelayerInfo( + originalSourceChainName, + transactionHash, + modifiedInfoRequest + ); + } + const instruction = parsed as DeliveryInstruction; - const targetChainId = instruction.targetChainId as ChainId; + const targetChainId = instruction.targetChainId; + if (!isChain(targetChainId)) throw Error(`Invalid Chain: ${targetChainId}`); const targetChain = CHAIN_ID_TO_NAME[targetChainId]; const targetChainProvider = @@ -154,32 +215,195 @@ export async function getWormholeRelayerInfo( "No default RPC for this chain; pass in your own provider (as targetChainProvider)" ); } - const [blockStartNumber, blockEndNumber] = - infoRequest?.targetChainBlockRanges?.get(targetChain) || - getBlockRange(targetChainProvider); - const targetChainStatus = await getWormholeRelayerInfoBySourceSequence( - environment, - targetChain, - targetChainProvider, - sourceChain, - BigNumber.from(deliveryLog.sequence), - blockStartNumber, - blockEndNumber, - infoRequest?.wormholeRelayerAddresses?.get(targetChain) || - getWormholeRelayerAddress(targetChain, environment) + const sourceSequence = BigNumber.from(deliveryLog.sequence); + + const deliveryHash = await getDeliveryHashFromLog( + deliveryLog.log, + CHAINS[sourceChain], + sourceChainProvider, + receipt.blockHash ); - return { + let signingOfVaaTimestamp; + try { + const vaa = await getWormscanRelayerInfo( + sourceChain, + sourceSequence.toNumber(), + { + network: infoRequest?.environment, + provider: infoRequest?.sourceChainProvider, + wormholeRelayerAddress: + infoRequest?.wormholeRelayerAddresses?.get(sourceChain), + } + ); + signingOfVaaTimestamp = new Date( + (await vaa.json()).data?.indexedAt + ).getTime(); + } catch { + // wormscan won't work for devnet - so let's hardcode this + if (environment === "DEVNET") { + signingOfVaaTimestamp = sourceTimestamp; + } + } + + // obtain additional message info + const additionalMessageInformation: AdditionalMessageParsed[] = + await Promise.all( + instruction.messageKeys.map(async (messageKey) => { + if (messageKey.keyType === 1) { + // check receipt + const vaaKey = parseVaaKey(messageKey.key); + + // if token bridge transfer in logs, parse it + let tokenBridgeLog; + const tokenBridgeEmitterAddress = tryNativeToHexString( + CONTRACTS[environment][sourceChain].token_bridge || "", + sourceChain + ); + try { + if ( + vaaKey.chainId === CHAINS[sourceChain] && + vaaKey.emitterAddress.toString("hex") === + tokenBridgeEmitterAddress + ) { + tokenBridgeLog = getWormholeLog( + receipt, + CONTRACTS[environment][sourceChain].core || "", + tokenBridgeEmitterAddress, + 0, + vaaKey.sequence.toNumber() + ); + } + } catch (e) { + console.log(e); + } + if (!tokenBridgeLog) return undefined; + const parsedTokenInfo = parseTransferPayload( + Buffer.from(tokenBridgeLog.payload.substring(2), "hex") + ); + const originChainName = + CHAIN_ID_TO_NAME[parsedTokenInfo.originChain as ChainId]; + let signedVaaTimestamp = undefined; + let tokenName = undefined; + let tokenSymbol = undefined; + let tokenDecimals = undefined; + + // Try to get additional token information, assuming it is an ERC20 + try { + const tokenProvider = + (parsedTokenInfo.originChain === CHAINS[sourceChain] + ? infoRequest?.sourceChainProvider + : infoRequest?.targetChainProviders?.get(originChainName)) || + getDefaultProvider(environment, originChainName); + const tokenContract = ERC20__factory.connect( + "0x" + parsedTokenInfo.originAddress.substring(24), + tokenProvider + ); + tokenName = await tokenContract.name(); + tokenSymbol = await tokenContract.symbol(); + tokenDecimals = await tokenContract.decimals(); + } catch (e) { + console.log(e); + } + // Try to get wormscan information on if the tokens have been signed + try { + const tokenVaa = await getWormscanInfo( + environment, + sourceChain, + parseInt(tokenBridgeLog.sequence), + CONTRACTS[environment][sourceChain].token_bridge || "" + ); + signedVaaTimestamp = new Date( + (await tokenVaa.json()).data?.indexedAt + ).getTime(); + } catch {} + + const parsed: TokenTransferParsed = { + amount: BigNumber.from(parsedTokenInfo.amount) + .mul( + BigNumber.from(10).pow( + tokenDecimals && tokenDecimals > 8 ? tokenDecimals - 8 : 1 + ) + ) + .toBigInt(), + originAddress: parsedTokenInfo.originAddress, + originChain: parsedTokenInfo.originChain, + targetAddress: parsedTokenInfo.targetAddress, + targetChain: parsedTokenInfo.targetChain, + fromAddress: parsedTokenInfo.fromAddress, + name: tokenName, + symbol: tokenSymbol, + decimals: tokenDecimals, + signedVaaTimestamp, + }; + return parsed; + } else if (messageKey.keyType === 2) { + // check receipt + const cctpKey = parseCCTPKey(messageKey.key); + const cctpInfo = await getCCTPMessageLogURL( + cctpKey, + sourceChain, + receipt, + environment + ); + const url = cctpInfo?.url || ""; + + // Try to get attestation information on if the tokens have been signed + let attested = false; + try { + const attestation = await fetch(url); + attested = (await attestation.json()).status === "complete"; + } catch (e) { + console.log(e); + } + const cctpLog = cctpInfo?.cctpLog!; + const parsed: CCTPTransferParsed = { + amount: BigNumber.from( + Buffer.from(cctpLog.data.substring(2, 2 + 64), "hex") + ).toBigInt(), + mintRecipient: "0x" + cctpLog.data.substring(2 + 64 + 24, 2 + 128), + destinationDomain: BigNumber.from( + Buffer.from(cctpLog.data.substring(2 + 128, 2 + 192), "hex") + ).toNumber(), + attested, + estimatedAttestationSeconds: estimatedAttestationTimeInSeconds( + sourceChain, + environment + ), + }; + return parsed; + } else { + return undefined; + } + }) + ); + + const targetChainDeliveries = await getWormholeRelayerInfoByHash( + deliveryHash, + targetChain, + sourceChain, + sourceSequence.toNumber(), + infoRequest + ); + + const result: DeliveryInfo = { type: RelayerPayloadId.Delivery, sourceChain: sourceChain, sourceTransactionHash: sourceTransaction, - sourceDeliverySequenceNumber: BigNumber.from( - deliveryLog.sequence - ).toNumber(), + sourceDeliverySequenceNumber: sourceSequence.toNumber(), deliveryInstruction: instruction, - targetChainStatus, + sourceTimestamp, + signingOfVaaTimestamp, + additionalMessageInformation, + targetChainStatus: { + chain: targetChain, + events: targetChainDeliveries, + }, }; + const stringified = stringifyWormholeRelayerInfo(result); + result.stringified = stringified; + return result; } export function printWormholeRelayerInfo(info: DeliveryInfo) { @@ -198,17 +422,16 @@ export function stringifyWormholeRelayerInfo( "0000000000000000000000000000000000000000000000000000000000000000" ) { if (!excludeSourceInformation) { - stringifiedInfo += `Found delivery request in transaction ${ - info.sourceTransactionHash - } on ${ - info.sourceChain - }\nfrom sender ${info.deliveryInstruction.senderAddress.toString( - "hex" - )} from ${info.sourceChain} with delivery sequence number ${ - info.sourceDeliverySequenceNumber + stringifiedInfo += `Source chain: ${info.sourceChain}\n`; + + stringifiedInfo += `Source Transaction Hash: ${info.sourceTransactionHash}\n`; + stringifiedInfo += `Sender: ${ + "0x" + + info.deliveryInstruction.senderAddress.toString("hex").substring(24) }\n`; + stringifiedInfo += `Delivery sequence number: ${info.sourceDeliverySequenceNumber}\n`; } else { - stringifiedInfo += `Found delivery request from sender ${info.deliveryInstruction.senderAddress.toString( + stringifiedInfo += `Sender: ${info.deliveryInstruction.senderAddress.toString( "hex" )}\n`; } @@ -216,30 +439,81 @@ export function stringifyWormholeRelayerInfo( const payload = info.deliveryInstruction.payload.toString("hex"); if (payload.length > 0) { - stringifiedInfo += `\nPayload to be relayed (as hex string): 0x${payload}`; + stringifiedInfo += `\nPayload to be relayed: 0x${payload}\n`; } if (numMsgs > 0) { - stringifiedInfo += `\nThe following ${numMsgs} wormhole messages (VAAs) were ${ + stringifiedInfo += `\nThe following ${ + numMsgs === 1 ? "" : `${numMsgs} ` + }message${numMsgs === 1 ? " was" : "s were"} ${ payload.length > 0 ? "also " : "" - }requested to be relayed:\n`; + }requested to be relayed with this delivery:\n`; stringifiedInfo += info.deliveryInstruction.messageKeys .map((msgKey, i) => { let result = ""; if (msgKey.keyType == KeyType.VAA) { const vaaKey = parseVaaKey(msgKey.key); - result += `(VAA ${i}): `; - result += `Message from ${ + result += `(Message ${i + 1}): `; + result += `Wormhole VAA from ${ vaaKey.chainId ? printChain(vaaKey.chainId) : "" }, with emitter address ${vaaKey.emitterAddress?.toString( "hex" )} and sequence number ${vaaKey.sequence}`; + if (info.additionalMessageInformation[i]) { + const tokenTransferInfo = info.additionalMessageInformation[ + i + ] as TokenTransferParsed; + result += `\nThis is a token bridge transfer of ${ + tokenTransferInfo.decimals + ? `${ethers.utils.formatUnits( + tokenTransferInfo.amount, + tokenTransferInfo.decimals + )} ` + : `${tokenTransferInfo.amount} normalized units of ` + }${ + tokenTransferInfo.name + ? `${tokenTransferInfo.name} (${tokenTransferInfo.symbol})` + : `token ${tokenTransferInfo.originAddress.substring( + 24 + )} (which is native to ${printChain( + tokenTransferInfo.originChain + )})` + }`; + if (tokenTransferInfo.signedVaaTimestamp) { + result += `\ntransfer signed by guardians: ${new Date( + tokenTransferInfo.signedVaaTimestamp + ).toString()}`; + } else { + result += `\ntransfer not yet signed by guardians`; + } + } } else if (msgKey.keyType == KeyType.CCTP) { const cctpKey = parseCCTPKey(msgKey.key); - result += `(CCTP ${i}): `; - result += `Transfer from cctp domain ${printChain(cctpKey.domain)}`; + result += `(Message ${i + 1}): `; + result += `CCTP Transfer from domain ${printCCTPDomain( + cctpKey.domain + )}`; result += `, with nonce ${cctpKey.nonce}`; + if (info.additionalMessageInformation[i]) { + const cctpTransferInfo = info.additionalMessageInformation[ + i + ] as CCTPTransferParsed; + result += `\nThis is a CCTP transfer of ${`${ethers.utils.formatUnits( + cctpTransferInfo.amount, + 6 + )}`} USDC ${ + cctpTransferInfo.attested + ? "(Attestation is complete" + : "(Attestation currently pending" + }, typically takes ${ + cctpTransferInfo.estimatedAttestationSeconds < 60 + ? `${cctpTransferInfo.estimatedAttestationSeconds} seconds` + : `${ + cctpTransferInfo.estimatedAttestationSeconds / 60 + } minutes` + })`; + } } else { - result += `(Unknown key type${i}): ${msgKey.keyType}`; + result += `(Unknown key type ${i}): ${msgKey.keyType}`; } return result; }) @@ -254,18 +528,13 @@ export function stringifyWormholeRelayerInfo( instruction.requestedReceiverValue = overrides.newReceiverValue; instruction.encodedExecutionInfo = overrides.newExecutionInfo; } - const targetChainName = CHAIN_ID_TO_NAME[instruction.targetChainId as ChainId]; - stringifiedInfo += `${ - numMsgs == 0 - ? payload.length == 0 - ? "" - : "\n\nPayload was requested to be relayed" - : "\n\nThese were requested to be sent" - } to 0x${instruction.targetAddress.toString("hex")} on ${printChain( + stringifiedInfo += `\n\nDestination chain: ${printChain( instruction.targetChainId - )}\n`; + )}\nDestination address: 0x${instruction.targetAddress + .toString("hex") + .substring(24)}\n\n`; const totalReceiverValue = instruction.requestedReceiverValue.add( instruction.extraReceiverValue ); @@ -289,42 +558,95 @@ export function stringifyWormholeRelayerInfo( stringifiedInfo += `Gas limit: ${executionInfo.gasLimit} ${targetChainName} gas\n`; const refundAddressChosen = - instruction.refundAddress !== instruction.refundDeliveryProvider; + instruction.refundAddress.toString("hex") !== + "0000000000000000000000000000000000000000000000000000000000000000"; if (refundAddressChosen) { stringifiedInfo += `Refund rate: ${ethers.utils.formatEther( executionInfo.targetChainRefundPerGasUnused )} of ${targetChainName} currency per unit of gas unused\n`; stringifiedInfo += `Refund address: ${instruction.refundAddress.toString( "hex" - )}\n`; + )} on ${printChain(instruction.refundChainId)}\n`; } stringifiedInfo += `\n`; + if (info.sourceTimestamp) { + stringifiedInfo += `Sent: ${new Date(info.sourceTimestamp).toString()}\n`; + } + if (info.signingOfVaaTimestamp) { + stringifiedInfo += `Delivery vaa signed by guardians: ${new Date( + info.signingOfVaaTimestamp + ).toString()}\n`; + } else { + stringifiedInfo += `Delivery not yet signed by guardians - check https://wormhole-foundation.github.io/wormhole-dashboard/#/ for status\n`; + } + stringifiedInfo += `\n`; + if (info.targetChainStatus.events.length === 0) { + stringifiedInfo += "Delivery has not occured yet\n"; + } stringifiedInfo += info.targetChainStatus.events - .map( - (e, i) => - `Delivery attempt ${i + 1}: ${ - e.transactionHash - ? ` ${targetChainName} transaction hash: ${e.transactionHash}` - : "" - }\nStatus: ${e.status}\n${ - e.revertString - ? `Failure reason: ${ - e.gasUsed.eq(executionInfo.gasLimit) - ? "Gas limit hit" - : e.revertString - }\n` - : "" - }Gas used: ${e.gasUsed.toString()}\nTransaction fee used: ${ethers.utils.formatEther( - executionInfo.targetChainRefundPerGasUnused.mul(e.gasUsed) - )} of ${targetChainName} currency\n${`Refund amount: ${ethers.utils.formatEther( - executionInfo.targetChainRefundPerGasUnused.mul( - executionInfo.gasLimit.sub(e.gasUsed) - ) - )} of ${targetChainName} currency \nRefund status: ${ - e.refundStatus - }\n`}` - ) + .map((e, i) => { + let override = e.overrides || false; + let overriddenExecutionInfo = e.overrides + ? parseEVMExecutionInfoV1(e.overrides.newExecutionInfo, 0)[0] + : executionInfo; + let overriddenReceiverValue = e.overrides + ? e.overrides.newReceiverValue + : totalReceiverValue; + const overriddenGasLimit = override + ? overriddenExecutionInfo.gasLimit + : executionInfo.gasLimit; + + // Add information about any override applied to the delivery + let overrideStringifiedInfo = ""; + if (override) { + overrideStringifiedInfo += !overriddenReceiverValue.eq( + totalReceiverValue + ) + ? `Overridden amount to pass into target address: ${ethers.utils.formatEther( + overriddenReceiverValue + )} of ${targetChainName} currency\n` + : ``; + overrideStringifiedInfo += !( + overriddenGasLimit === executionInfo.gasLimit + ) + ? `Overridden gas limit: ${overriddenExecutionInfo.gasLimit} ${targetChainName} gas\n` + : ""; + if ( + refundAddressChosen && + executionInfo.targetChainRefundPerGasUnused !== + overriddenExecutionInfo.targetChainRefundPerGasUnused + ) { + overrideStringifiedInfo += `Overridden refund rate: ${ethers.utils.formatEther( + overriddenExecutionInfo.targetChainRefundPerGasUnused + )} of ${targetChainName} currency per unit of gas unused\n`; + } + } + + return `Delivery attempt: ${ + e.transactionHash + ? ` ${targetChainName} transaction hash: ${e.transactionHash}` + : "" + }\nDelivery Time: ${new Date( + e.timestamp as number + ).toString()}\n${overrideStringifiedInfo}Status: ${e.status}\n${ + e.revertString + ? `Failure reason: ${ + e.gasUsed.eq(overriddenExecutionInfo.gasLimit) + ? "Gas limit hit" + : e.revertString + }\n` + : "" + }Gas used: ${e.gasUsed.toString()}\nTransaction fee used: ${ethers.utils.formatEther( + overriddenExecutionInfo.targetChainRefundPerGasUnused.mul(e.gasUsed) + )} of ${targetChainName} currency\n${`Refund amount: ${ethers.utils.formatEther( + overriddenExecutionInfo.targetChainRefundPerGasUnused.mul( + overriddenExecutionInfo.gasLimit.sub(e.gasUsed) + ) + )} of ${targetChainName} currency \nRefund status: ${ + e.refundStatus + }\n`}`; + }) .join("\n"); } else if ( info.type == RelayerPayloadId.Delivery && @@ -347,7 +669,7 @@ export function stringifyWormholeRelayerInfo( .map( (e, i) => - `Delivery attempt ${i + 1}: ${ + `Delivery attempt: ${ e.transactionHash ? ` ${targetChainName} transaction hash: ${e.transactionHash}` : "" diff --git a/sdk/js/src/relayer/structs.ts b/sdk/js/src/relayer/structs.ts index 42ba9fc03..51ebd02d8 100644 --- a/sdk/js/src/relayer/structs.ts +++ b/sdk/js/src/relayer/structs.ts @@ -1,5 +1,4 @@ import { BigNumber, ethers } from "ethers"; -import { arrayify } from "ethers/lib/utils"; export enum RelayerPayloadId { Delivery = 1, @@ -16,7 +15,6 @@ export enum DeliveryStatus { DeliverySuccess = "Delivery Success", ReceiverFailure = "Receiver Failure", ThisShouldNeverHappen = "This should never happen. Contact Support.", - DeliveryDidntHappenWithinRange = "Delivery didn't happen within given block range", } export enum RefundStatus { @@ -25,6 +23,8 @@ export enum RefundStatus { CrossChainRefundSent = "Cross Chain Refund Sent", CrossChainRefundFailProviderNotSupported = "Cross Chain Refund Fail - Provider does not support the refund chain", CrossChainRefundFailNotEnough = "Cross Chain Refund Fail - Refund too low for cross chain refund", + RefundAddressNotProvided = "No refund address provided", + InvalidRefundStatus = "Invalid refund status", } export function parseRefundStatus(index: number) { @@ -38,7 +38,9 @@ export function parseRefundStatus(index: number) { ? RefundStatus.CrossChainRefundFailProviderNotSupported : index === 4 ? RefundStatus.CrossChainRefundFailNotEnough - : RefundStatus.CrossChainRefundFailProviderNotSupported; + : index === 5 + ? RefundStatus.RefundAddressNotProvided + : RefundStatus.InvalidRefundStatus; } export enum KeyType { @@ -121,7 +123,9 @@ export function parseWormholeRelayerPayloadType( stringPayload: string | Buffer | Uint8Array ): RelayerPayloadId { const payload = - typeof stringPayload === "string" ? arrayify(stringPayload) : stringPayload; + typeof stringPayload === "string" + ? ethers.utils.arrayify(stringPayload) + : stringPayload; if ( payload[0] != RelayerPayloadId.Delivery && payload[0] != RelayerPayloadId.Redelivery @@ -152,28 +156,22 @@ export function parseWormholeRelayerSend(bytes: Buffer): DeliveryInstruction { ); } idx += 1; - const targetChainId = bytes.readUInt16BE(idx); idx += 2; const targetAddress = bytes.slice(idx, idx + 32); idx += 32; - let payload: Buffer; [payload, idx] = parsePayload(bytes, idx); - const requestedReceiverValue = ethers.BigNumber.from( Uint8Array.prototype.subarray.call(bytes, idx, idx + 32) ); idx += 32; - const extraReceiverValue = ethers.BigNumber.from( Uint8Array.prototype.subarray.call(bytes, idx, idx + 32) ); idx += 32; - let encodedExecutionInfo; [encodedExecutionInfo, idx] = parsePayload(bytes, idx); - const refundChainId = bytes.readUInt16BE(idx); idx += 2; const refundAddress = bytes.slice(idx, idx + 32); @@ -186,7 +184,6 @@ export function parseWormholeRelayerSend(bytes: Buffer): DeliveryInstruction { idx += 32; const numMessages = bytes.readUInt8(idx); idx += 1; - let messageKeys = [] as MessageKey[]; for (let i = 0; i < numMessages; ++i) { const res = parseMessageKey(bytes, idx);