SDK: Auto relayer better status command (#3406)

* update clients.js version

* updates to clients/js

* update clients/js

* Generate modification

* prettier

* status change

* docs change

* add back the 'impossible's

* update sdk version

* better status check WIP

* WIP

* Improved status check and stringify function with times

* improvements to status printing

* prettier format

* Remove last console log

* prettier

* readme modify

* readme fix

* Readme fix

* readme changes

* don't rely on wormscan for status in devnet

* prettier

* Remove status check from integration testing - this is a helper that shouldn't interfere with contract testing

* prettier

* update clients.js version

* readme revert changes

* base default rpcs

* script improvements

* Add manual delivery helper

* remove console logs

* arbitrum needs a custom block range

* fix bug in testing if blocknumber is 0

* deliver fixes for manual delivery

* prettier

* fix default block tag

* pre-pend scripts with test

* review comments
This commit is contained in:
derpy-duck 2023-10-13 15:25:31 -04:00 committed by GitHub
parent 5b9b10ada4
commit 9aa4d0329d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1202 additions and 476 deletions

View File

@ -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]

View File

@ -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"
}

View File

@ -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",

View File

@ -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);

View File

@ -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);

View File

@ -154,6 +154,10 @@ export const getProviderForChain = <T extends ChainId | ChainName>(
case "osmosis":
case "pythnet":
case "wormchain":
case "cosmoshub":
case "evmos":
case "kujira":
case "rootstock":
throw new Error(`${chainName} not supported`);
default:
impossible(chainName);

View File

@ -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);
}

View File

@ -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<ReturnType<typeof builder>["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"
);
}
};

View File

@ -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

View File

@ -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

View File

@ -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,
},
};
/**

View File

@ -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",

View File

@ -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<void> {

View File

@ -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"
];

View File

@ -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<ContractReceipt> {
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<SignedVaa[]> {
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;
}

View File

@ -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<DeliveryTargetInfo[]> {
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<DeliveryTargetInfo[]> {
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<ethers.Transaction>,
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<boolean> => {
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<DeliveryTargetInfo[]> {
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<DeliveryTargetInfo[]> {
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<Response> {
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<string> {
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<string> {
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<string> {
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 };
}

View File

@ -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<ChainName, ethers.providers.Provider>;
targetChainBlockRanges?: Map<
ChainName,
[ethers.providers.BlockTag, ethers.providers.BlockTag]
>;
wormholeRelayerWhMessageIndex?: number;
wormholeRelayerAddresses?: Map<ChainName, string>;
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}`
: ""

View File

@ -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);