diff --git a/relayer-engine-v2/pkgs/sdk/src/consts.ts b/relayer-engine-v2/pkgs/sdk/src/consts.ts index bf9b59c..398cb5f 100644 --- a/relayer-engine-v2/pkgs/sdk/src/consts.ts +++ b/relayer-engine-v2/pkgs/sdk/src/consts.ts @@ -18,9 +18,7 @@ const DEVNET = [ const MAINNET: any[] = [] -type ENV = "mainnet" | "testnet" - -export function getCoreRelayerAddressNative(chainId: ChainId, env: Network): string { +export function getWormholeRelayerAddress(chainId: ChainId, env: Network): string { if (env == "TESTNET") { const address = TESTNET.find((x) => x.chainId == chainId)?.coreRelayerAddress if (!address) { @@ -44,12 +42,12 @@ export function getCoreRelayerAddressNative(chainId: ChainId, env: Network): str } } -export function getCoreRelayer( +export function getWormholeRelayer( chainId: ChainId, env: Network, provider: ethers.providers.Provider ): CoreRelayer { - const thisChainsRelayer = getCoreRelayerAddressNative(chainId, env) + const thisChainsRelayer = getWormholeRelayerAddress(chainId, env) const contract = CoreRelayer__factory.connect(thisChainsRelayer, provider) return contract } diff --git a/relayer-engine-v2/pkgs/sdk/src/main/helpers.ts b/relayer-engine-v2/pkgs/sdk/src/main/helpers.ts new file mode 100644 index 0000000..9ea863b --- /dev/null +++ b/relayer-engine-v2/pkgs/sdk/src/main/helpers.ts @@ -0,0 +1,157 @@ +import { + ChainId, + CHAIN_ID_TO_NAME, + Network, + tryNativeToHexString, +} from "@certusone/wormhole-sdk" +import { Implementation__factory } from "@certusone/wormhole-sdk/lib/cjs/ethers-contracts" +import { BigNumber, ContractReceipt, ethers } from "ethers" +import { getWormholeRelayer, RPCS_BY_CHAIN } from "../consts" +import { + parseWormholeRelayerPayloadType, + RelayerPayloadId, + parseWormholeRelayerSend, + DeliveryInstructionsContainer, + DeliveryStatus, +} from "../structs" +import { DeliveryEvent } from "../ethers-contracts/CoreRelayer" +type DeliveryTargetInfo = { + status: DeliveryStatus | string + deliveryTxHash: string | null + vaaHash: string | null + sourceChain: number | null + sourceVaaSequence: BigNumber | null +} + +export function parseWormholeLog(log: ethers.providers.Log): { + type: RelayerPayloadId + parsed: DeliveryInstructionsContainer | string +} { + const abi = [ + "event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel);", + ] + const iface = new ethers.utils.Interface(abi) + const parsed = iface.parseLog(log) + const payload = Buffer.from(parsed.args.payload.substring(2), "hex") + const type = parseWormholeRelayerPayloadType(payload) + if (type == RelayerPayloadId.Delivery) { + return { type, parsed: parseWormholeRelayerSend(payload) } + } else { + throw Error("Invalid wormhole log") + } +} + +export function printChain(chainId: number) { + return `${CHAIN_ID_TO_NAME[chainId as ChainId]} (Chain ${chainId})` +} + +export function getDefaultProvider(network: Network, chainId: ChainId) { + return new ethers.providers.StaticJsonRpcProvider( + RPCS_BY_CHAIN[network][CHAIN_ID_TO_NAME[chainId]] + ) +} + +export function getBlockRange( + provider: ethers.providers.Provider, + timestamp?: number +): [ethers.providers.BlockTag, ethers.providers.BlockTag] { + return [-2040, "latest"] +} + +export async function getWormholeRelayerDeliveryEventsBySourceSequence( + environment: Network, + targetChain: ChainId, + targetChainProvider: ethers.providers.Provider, + sourceChain: number, + sourceVaaSequence: BigNumber, + blockStartNumber: ethers.providers.BlockTag, + blockEndNumber: ethers.providers.BlockTag +): Promise { + const coreRelayer = getWormholeRelayer(targetChain, environment, targetChainProvider) + + const deliveryEvents = coreRelayer.filters.Delivery( + null, + sourceChain, + sourceVaaSequence + ) + + // There is a max limit on RPCs sometimes for how many blocks to query + return await transformDeliveryEvents( + await coreRelayer.queryFilter(deliveryEvents, blockStartNumber, blockEndNumber), + targetChainProvider + ) +} + +export function deliveryStatus(status: number) { + switch (status) { + case 0: + return DeliveryStatus.DeliverySuccess + case 1: + return DeliveryStatus.ReceiverFailure + case 2: + return DeliveryStatus.ForwardRequestFailure + case 3: + return DeliveryStatus.ForwardRequestSuccess + case 4: + return DeliveryStatus.InvalidRedelivery + default: + return DeliveryStatus.ThisShouldNeverHappen + } +} + +async function transformDeliveryEvents( + events: DeliveryEvent[], + targetProvider: ethers.providers.Provider +): Promise { + return Promise.all( + events.map(async (x) => { + return { + status: deliveryStatus(x.args[4]), + deliveryTxHash: x.transactionHash, + vaaHash: x.args[3], + sourceVaaSequence: x.args[2], + sourceChain: x.args[1], + } + }) + ) +} + +export function getWormholeRelayerLog( + receipt: ContractReceipt, + bridgeAddress: string, + emitterAddress: string, + index: number +): { log: ethers.providers.Log; sequence: string } { + const bridgeLogs = receipt.logs.filter((l) => { + return l.address === bridgeAddress + }) + + if (bridgeLogs.length == 0) { + throw Error("No core contract interactions found for this transaction.") + } + + const parsed = bridgeLogs.map((bridgeLog) => { + const log = Implementation__factory.createInterface().parseLog(bridgeLog) + return { + sequence: log.args[1].toString(), + nonce: log.args[2].toString(), + emitterAddress: tryNativeToHexString(log.args[0].toString(), "ethereum"), + log: bridgeLog, + } + }) + + const filtered = parsed.filter((x) => x.emitterAddress == emitterAddress.toLowerCase()) + + if (filtered.length == 0) { + throw Error("No CoreRelayer contract interactions found for this transaction.") + } + + if (index >= filtered.length) { + throw Error("Specified delivery index is out of range.") + } else { + return { + log: filtered[index].log, + sequence: filtered[index].sequence, + } + } +} diff --git a/relayer-engine-v2/pkgs/sdk/src/main/index.ts b/relayer-engine-v2/pkgs/sdk/src/main/index.ts index acdaf2e..6d84522 100644 --- a/relayer-engine-v2/pkgs/sdk/src/main/index.ts +++ b/relayer-engine-v2/pkgs/sdk/src/main/index.ts @@ -1 +1 @@ -export * from "./status" +export * from "./main" diff --git a/relayer-engine-v2/pkgs/sdk/src/main/main.ts b/relayer-engine-v2/pkgs/sdk/src/main/main.ts new file mode 100644 index 0000000..3855216 --- /dev/null +++ b/relayer-engine-v2/pkgs/sdk/src/main/main.ts @@ -0,0 +1,224 @@ +import { + ChainId, + CHAIN_ID_TO_NAME, + isChain, + CONTRACTS, + tryNativeToHexString, + Network, +} from "@certusone/wormhole-sdk" +import { BigNumber, ethers } from "ethers" +import { getWormholeRelayerAddress } from "../consts" +import { + RelayerPayloadId, + DeliveryInstruction, + DeliveryInstructionsContainer, + MessageInfoType, + DeliveryStatus, +} from "../structs" +import { + getDefaultProvider, + printChain, + getWormholeRelayerLog, + parseWormholeLog, + getBlockRange, + getWormholeRelayerDeliveryEventsBySourceSequence, +} from "./helpers" + +type InfoRequest = { + environment: Network + sourceChain: ChainId + sourceTransaction: string + sourceChainProvider?: ethers.providers.Provider + targetChainProviders?: Map + targetChainBlockRanges?: Map< + number, + [ethers.providers.BlockTag, ethers.providers.BlockTag] + > + coreRelayerWhMessageIndex?: number +} + +export type DeliveryInfo = { + type: RelayerPayloadId.Delivery + sourceChainId: ChainId + sourceTransactionHash: string + sourceDeliverySequenceNumber: number + deliveryInstructionsContainer: DeliveryInstructionsContainer + targetChainStatuses: { + chainId: ChainId + events: { status: DeliveryStatus | string; transactionHash: string | null }[] + }[] +} + +export function printWormholeRelayerInfo(info: DeliveryInfo) { + console.log(stringifyWormholeRelayerInfo(info)) +} +export function stringifyWormholeRelayerInfo(info: DeliveryInfo): string { + let stringifiedInfo = "" + if (info.type == RelayerPayloadId.Delivery) { + stringifiedInfo += `Found delivery request in transaction ${ + info.sourceTransactionHash + } on ${printChain(info.sourceChainId)}\n` + + const numMsgs = info.deliveryInstructionsContainer.messages.length + stringifiedInfo += `\nThe following ${numMsgs} messages were requested to be relayed:\n` + stringifiedInfo += info.deliveryInstructionsContainer.messages.map((msgInfo, i) => { + let result = "" + result += `\n(Message ${i}): ` + if (msgInfo.payloadType == MessageInfoType.EMITTER_SEQUENCE) { + result += `Message with emitter address ${msgInfo.emitterAddress?.toString( + "hex" + )} and sequence number ${msgInfo.sequence}\n` + } else if (msgInfo.payloadType == MessageInfoType.VAAHASH) { + result += `Message with VAA Hash ${msgInfo.vaaHash?.toString("hex")}\n` + } else { + result += `Message not specified correctly\n` + } + }) + + const length = info.deliveryInstructionsContainer.instructions.length + stringifiedInfo += `\nMessages were requested to be sent to ${length} destination${ + length == 1 ? "" : "s" + }:\n` + stringifiedInfo += + info.deliveryInstructionsContainer.instructions + .map((instruction: DeliveryInstruction, i) => { + let result = "" + const targetChainName = CHAIN_ID_TO_NAME[instruction.targetChain as ChainId] + result += `\n(Destination ${i}): Target address is 0x${instruction.targetAddress.toString( + "hex" + )} on ${printChain(instruction.targetChain)}\n` + result += `Max amount to use for gas: ${instruction.maximumRefundTarget} of ${targetChainName} currency\n` + result += instruction.receiverValueTarget.gt(0) + ? `Amount to pass into target address: ${ + instruction.receiverValueTarget + } of ${CHAIN_ID_TO_NAME[instruction.targetChain as ChainId]} currency\n` + : `` + result += `Gas limit: ${instruction.executionParameters.gasLimit} ${targetChainName} gas\n` + result += `Relay Provider Delivery Address: 0x${instruction.executionParameters.providerDeliveryAddress.toString( + "hex" + )}\n` + result += info.targetChainStatuses[i].events + .map( + (e, i) => + `Delivery attempt ${i + 1}: ${e.status}${ + e.transactionHash + ? ` (${targetChainName} transaction hash: ${e.transactionHash})` + : "" + }` + ) + .join("\n") + return result + }) + .join("\n") + "\n" + } + return stringifiedInfo +} + +export async function getWormholeRelayerInfo( + infoRequest: InfoRequest +): Promise { + const sourceChainProvider = + infoRequest.sourceChainProvider || + getDefaultProvider(infoRequest.environment, infoRequest.sourceChain) + if (!sourceChainProvider) + throw Error( + "No default RPC for this chain; pass in your own provider (as sourceChainProvider)" + ) + const receipt = await sourceChainProvider.getTransactionReceipt( + infoRequest.sourceTransaction + ) + if (!receipt) throw Error("Transaction has not been mined") + const bridgeAddress = + CONTRACTS[infoRequest.environment][CHAIN_ID_TO_NAME[infoRequest.sourceChain]].core + const coreRelayerAddress = getWormholeRelayerAddress( + infoRequest.sourceChain, + infoRequest.environment + ) + if (!bridgeAddress || !coreRelayerAddress) { + throw Error( + `Invalid chain ID or network: Chain ID ${infoRequest.sourceChain}, ${infoRequest.environment}` + ) + } + + const deliveryLog = getWormholeRelayerLog( + receipt, + bridgeAddress, + tryNativeToHexString(coreRelayerAddress, "ethereum"), + infoRequest.coreRelayerWhMessageIndex ? infoRequest.coreRelayerWhMessageIndex : 0 + ) + + const { type, parsed } = parseWormholeLog(deliveryLog.log) + + const deliveryInstructionsContainer = parsed as DeliveryInstructionsContainer + + const targetChainStatuses = await Promise.all( + deliveryInstructionsContainer.instructions.map( + async (instruction: DeliveryInstruction) => { + const targetChain = instruction.targetChain as ChainId + if (!isChain(targetChain)) throw Error(`Invalid Chain: ${targetChain}`) + const targetChainProvider = + infoRequest.targetChainProviders?.get(targetChain) || + getDefaultProvider(infoRequest.environment, targetChain) + + if (!targetChainProvider) + throw Error( + "No default RPC for this chain; pass in your own provider (as targetChainProvider)" + ) + + const sourceChainBlock = await sourceChainProvider.getBlock(receipt.blockNumber) + const [blockStartNumber, blockEndNumber] = + infoRequest.targetChainBlockRanges?.get(targetChain) || + getBlockRange(targetChainProvider, sourceChainBlock.timestamp) + + const deliveryEvents = await getWormholeRelayerDeliveryEventsBySourceSequence( + infoRequest.environment, + targetChain, + targetChainProvider, + infoRequest.sourceChain, + BigNumber.from(deliveryLog.sequence), + blockStartNumber, + blockEndNumber + ) + if (deliveryEvents.length == 0) { + let status = `Delivery didn't happen on ${printChain( + targetChain + )} within blocks ${blockStartNumber} to ${blockEndNumber}.` + try { + const blockStart = await targetChainProvider.getBlock(blockStartNumber) + const blockEnd = await targetChainProvider.getBlock(blockEndNumber) + status = `Delivery didn't happen on ${printChain( + targetChain + )} within blocks ${blockStart.number} to ${ + blockEnd.number + } (within times ${new Date( + blockStart.timestamp * 1000 + ).toString()} to ${new Date(blockEnd.timestamp * 1000).toString()})` + } catch (e) {} + deliveryEvents.push({ + status, + deliveryTxHash: null, + vaaHash: null, + sourceChain: infoRequest.sourceChain, + sourceVaaSequence: BigNumber.from(deliveryLog.sequence), + }) + } + return { + chainId: targetChain, + events: deliveryEvents.map((e) => ({ + status: e.status, + transactionHash: e.deliveryTxHash, + })), + } + } + ) + ) + + return { + type, + sourceChainId: infoRequest.sourceChain, + sourceTransactionHash: infoRequest.sourceTransaction, + sourceDeliverySequenceNumber: BigNumber.from(deliveryLog.sequence).toNumber(), + deliveryInstructionsContainer, + targetChainStatuses, + } +} diff --git a/relayer-engine-v2/pkgs/sdk/src/main/status.ts b/relayer-engine-v2/pkgs/sdk/src/main/status.ts deleted file mode 100644 index f6ee534..0000000 --- a/relayer-engine-v2/pkgs/sdk/src/main/status.ts +++ /dev/null @@ -1,355 +0,0 @@ -import { - ChainId, - CHAIN_ID_TO_NAME, - CHAINS, - isChain, - CONTRACTS, - getSignedVAAWithRetry, - Network, - parseVaa, - ParsedVaa, - tryNativeToHexString, -} from "@certusone/wormhole-sdk" -import { GetSignedVAAResponse } from "@certusone/wormhole-sdk-proto-web/lib/cjs/publicrpc/v1/publicrpc" -import { Implementation__factory } from "@certusone/wormhole-sdk/lib/cjs/ethers-contracts" -import { BigNumber, ContractReceipt, ethers, providers } from "ethers" -import { - getCoreRelayer, - getCoreRelayerAddressNative, - RPCS_BY_CHAIN, - GUARDIAN_RPC_HOSTS, -} from "../consts" -import { - parsePayloadType, - RelayerPayloadId, - parseDeliveryInstructionsContainer, - parseRedeliveryByTxHashInstruction, - DeliveryInstruction, - DeliveryInstructionsContainer, - RedeliveryByTxHashInstruction, - ExecutionParameters, -} from "../structs" -import { DeliveryEvent } from "../ethers-contracts/CoreRelayer" - -enum DeliveryStatus { - WaitingForVAA = "Waiting for VAA", - PendingDelivery = "Pending Delivery", - DeliverySuccess = "Delivery Success", - ReceiverFailure = "Receiver Failure", - InvalidRedelivery = "Invalid Redelivery", - ForwardRequestSuccess = "Forward Request Success", - ForwardRequestFailure = "Forward Request Failure", - ThisShouldNeverHappen = "This should never happen. Contact Support.", - DeliveryDidntHappenWithinRange = "Delivery didn't happen within given block range", -} - -type DeliveryTargetInfo = { - status: DeliveryStatus | string - deliveryTxHash: string | null - vaaHash: string | null - sourceChain: number | null - sourceVaaSequence: BigNumber | null -} - -type InfoRequest = { - environment: Network - sourceChain: ChainId - sourceTransaction: string - sourceChainProvider?: ethers.providers.Provider - targetChainProviders?: Map - targetChainBlockRanges?: Map - coreRelayerWhMessageIndex?: number -} - -export function parseWormholeLog(log: ethers.providers.Log): { - type: RelayerPayloadId - parsed: DeliveryInstructionsContainer | RedeliveryByTxHashInstruction | string -} { - const abi = [ - "event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel);", - ] - const iface = new ethers.utils.Interface(abi) - const parsed = iface.parseLog(log) - const payload = Buffer.from(parsed.args.payload.substring(2), "hex") - const type = parsePayloadType(payload) - if (type == RelayerPayloadId.Delivery) { - return { type, parsed: parseDeliveryInstructionsContainer(payload) } - } else if (type == RelayerPayloadId.Redelivery) { - return { type, parsed: parseRedeliveryByTxHashInstruction(payload) } - } else { - throw Error("Invalid wormhole log"); - } -} - -export type DeliveryInfo = { - type: RelayerPayloadId.Delivery - sourceChainId: ChainId, - sourceTransactionHash: string - deliveryInstructionsContainer: DeliveryInstructionsContainer - targetChainStatuses: { - chainId: ChainId - events: { status: DeliveryStatus | string; transactionHash: string | null }[] - }[] -} - -export type RedeliveryInfo = { - type: RelayerPayloadId.Redelivery - redeliverySourceChainId: ChainId, - redeliverySourceTransactionHash: string - redeliveryInstruction: RedeliveryByTxHashInstruction -} - -export function printChain(chainId: number) { - return `${CHAIN_ID_TO_NAME[chainId as ChainId]} (Chain ${chainId})` -} - -export function printInfo(info: DeliveryInfo | RedeliveryInfo) { - console.log(stringifyInfo(info)); -} -export function stringifyInfo(info: DeliveryInfo | RedeliveryInfo): string { - let stringifiedInfo = ""; - if(info.type==RelayerPayloadId.Redelivery) { - stringifiedInfo += (`Found Redelivery request in transaction ${info.redeliverySourceTransactionHash} on ${printChain(info.redeliverySourceChainId)}\n`) - stringifiedInfo += (`Original Delivery Source Chain: ${printChain(info.redeliveryInstruction.sourceChain)}\n`) - stringifiedInfo += (`Original Delivery Source Transaction Hash: 0x${info.redeliveryInstruction.sourceTxHash.toString("hex")}\n`) - //stringifiedInfo += (`Original Delivery Source Nonce: ${info.redeliveryInstruction.sourceNonce}\n`) - stringifiedInfo += (`Target Chain: ${printChain(info.redeliveryInstruction.targetChain)}\n`) - stringifiedInfo += (`multisendIndex: ${info.redeliveryInstruction.multisendIndex}\n`) - //stringifiedInfo += (`deliveryIndex: ${info.redeliveryInstruction.deliveryIndex}\n`) - stringifiedInfo += (`New max amount (in target chain currency) to use for gas: ${info.redeliveryInstruction.newMaximumRefundTarget}\n`) - stringifiedInfo += (`New amount (in target chain currency) to pass into target address: ${info.redeliveryInstruction.newMaximumRefundTarget}\n`) - stringifiedInfo += (`New target chain gas limit: ${info.redeliveryInstruction.executionParameters.gasLimit}\n`) - stringifiedInfo += (`Relay Provider Delivery Address: 0x${info.redeliveryInstruction.executionParameters.providerDeliveryAddress.toString("hex")}\n`) - } else if(info.type==RelayerPayloadId.Delivery) { - stringifiedInfo += (`Found delivery request in transaction ${info.sourceTransactionHash} on ${printChain(info.sourceChainId)}\n`) - stringifiedInfo += ((info.deliveryInstructionsContainer.sufficientlyFunded ? "The delivery was funded\n" : "** NOTE: The delivery was NOT sufficiently funded. You did not have enough leftover funds to perform the forward **\n")) - const length = info.deliveryInstructionsContainer.instructions.length; - stringifiedInfo += (`\nMessages were requested to be sent to ${length} destination${length == 1 ? "" : "s"}:\n`) - stringifiedInfo += (info.deliveryInstructionsContainer.instructions.map((instruction: DeliveryInstruction, i) => { - let result = ""; - const targetChainName = CHAIN_ID_TO_NAME[instruction.targetChain as ChainId]; - result += `\n(Destination ${i}): Target address is 0x${instruction.targetAddress.toString("hex")} on ${printChain(instruction.targetChain)}\n` - result += `Max amount to use for gas: ${instruction.maximumRefundTarget} of ${targetChainName} currency\n` - result += instruction.receiverValueTarget.gt(0) ? `Amount to pass into target address: ${instruction.receiverValueTarget} of ${CHAIN_ID_TO_NAME[instruction.targetChain as ChainId]} currency\n` : `` - result += `Gas limit: ${instruction.executionParameters.gasLimit} ${targetChainName} gas\n` - result += `Relay Provider Delivery Address: 0x${instruction.executionParameters.providerDeliveryAddress.toString("hex")}\n` - result += info.targetChainStatuses[i].events.map((e, i) => (`Delivery attempt ${i+1}: ${e.status}${e.transactionHash ? ` (${targetChainName} transaction hash: ${e.transactionHash})` : ""}`)).join("\n") - return result; - }).join("\n")) + "\n" - } - return stringifiedInfo -} - -function getDefaultProvider(network: Network, chainId: ChainId) { - return new ethers.providers.StaticJsonRpcProvider( - RPCS_BY_CHAIN[network][CHAIN_ID_TO_NAME[chainId]] - ) -} - -export async function getDeliveryInfoBySourceTx( - infoRequest: InfoRequest -): Promise { - const sourceChainProvider = - infoRequest.sourceChainProvider || getDefaultProvider(infoRequest.environment, infoRequest.sourceChain); - if (!sourceChainProvider) - throw Error( - "No default RPC for this chain; pass in your own provider (as sourceChainProvider)" - ) - const receipt = await sourceChainProvider.getTransactionReceipt( - infoRequest.sourceTransaction - ) - if (!receipt) throw Error("Transaction has not been mined") - const bridgeAddress = - CONTRACTS[infoRequest.environment][CHAIN_ID_TO_NAME[infoRequest.sourceChain]].core - const coreRelayerAddress = getCoreRelayerAddressNative( - infoRequest.sourceChain, - infoRequest.environment - ) - if (!bridgeAddress || !coreRelayerAddress) { - throw Error(`Invalid chain ID or network: Chain ID ${infoRequest.sourceChain}, ${infoRequest.environment}`) - } - - const deliveryLog = findLog( - receipt, - bridgeAddress, - tryNativeToHexString(coreRelayerAddress, "ethereum"), - infoRequest.coreRelayerWhMessageIndex ? infoRequest.coreRelayerWhMessageIndex : 0, - ) - - const { type, parsed } = parseWormholeLog(deliveryLog.log) - - if (type == RelayerPayloadId.Redelivery) { - const redeliveryInstruction = parsed as RedeliveryByTxHashInstruction - return { - type, - redeliverySourceChainId: infoRequest.sourceChain, - redeliverySourceTransactionHash: infoRequest.sourceTransaction, - redeliveryInstruction, - } - } - - /* Potentially use 'guardianRPCHosts' to get status of VAA; code in comments at end [1] */ - - const deliveryInstructionsContainer = parsed as DeliveryInstructionsContainer - - const targetChainStatuses = await Promise.all(deliveryInstructionsContainer.instructions.map(async (instruction: DeliveryInstruction) => { - const targetChain = instruction.targetChain as ChainId; - if(!isChain(targetChain)) throw Error(`Invalid Chain: ${targetChain}`) - const targetChainProvider = - infoRequest.targetChainProviders?.get(targetChain) || - getDefaultProvider(infoRequest.environment, targetChain) - - if (!targetChainProvider) - throw Error( - "No default RPC for this chain; pass in your own provider (as targetChainProvider)" - ) - - const sourceChainBlock = await sourceChainProvider.getBlock(receipt.blockNumber); - const [blockStartNumber, blockEndNumber] = infoRequest.targetChainBlockRanges?.get(targetChain) || getBlockRange(targetChainProvider, sourceChainBlock.timestamp); - - const deliveryEvents = await pullEventsBySourceSequence( - infoRequest.environment, - targetChain, - targetChainProvider, - infoRequest.sourceChain, - BigNumber.from(deliveryLog.sequence), - blockStartNumber, - blockEndNumber - ) - if (deliveryEvents.length == 0) { - let status = `Delivery didn't happen on ${printChain(targetChain)} within blocks ${blockStartNumber} to ${blockEndNumber}.`; - try { - const blockStart = await targetChainProvider.getBlock(blockStartNumber); - const blockEnd = await targetChainProvider.getBlock(blockEndNumber); - status = `Delivery didn't happen on ${printChain(targetChain)} within blocks ${blockStart.number} to ${blockEnd.number} (within times ${new Date(blockStart.timestamp * 1000).toString()} to ${new Date(blockEnd.timestamp * 1000).toString()})` - } catch(e) { - - } - deliveryEvents.push({ - status, - deliveryTxHash: null, - vaaHash: null, - sourceChain: infoRequest.sourceChain, - sourceVaaSequence: BigNumber.from(deliveryLog.sequence), - }) - } - return { - chainId: targetChain, - events: deliveryEvents.map((e)=>({status: e.status, transactionHash: e.deliveryTxHash})) - } - })) - - return { - type, - sourceChainId: infoRequest.sourceChain, - sourceTransactionHash: infoRequest.sourceTransaction, - deliveryInstructionsContainer, - targetChainStatuses - } -} - -function getBlockRange(provider: ethers.providers.Provider, timestamp?: number): [ethers.providers.BlockTag, ethers.providers.BlockTag] { - return [-2040, "latest"] -} - -async function pullEventsBySourceSequence( - environment: Network, - targetChain: ChainId, - targetChainProvider: ethers.providers.Provider, - sourceChain: number, - sourceVaaSequence: BigNumber, - blockStartNumber: ethers.providers.BlockTag, - blockEndNumber: ethers.providers.BlockTag -): Promise { - const coreRelayer = getCoreRelayer(targetChain, environment, targetChainProvider) - - //TODO These compile errors on sourceChain look like an ethers bug - const deliveryEvents = coreRelayer.filters.Delivery( - null, - sourceChain, - sourceVaaSequence - ) - - // There is a max limit on RPCs sometimes for how many blocks to query - return await transformDeliveryEvents( - await coreRelayer.queryFilter(deliveryEvents, blockStartNumber, blockEndNumber), - targetChainProvider - ) -} - -function deliveryStatus(status: number) { - switch (status) { - case 0: - return DeliveryStatus.DeliverySuccess - case 1: - return DeliveryStatus.ReceiverFailure - case 2: - return DeliveryStatus.ForwardRequestFailure - case 3: - return DeliveryStatus.ForwardRequestSuccess - case 4: - return DeliveryStatus.InvalidRedelivery - default: - return DeliveryStatus.ThisShouldNeverHappen - } -} - -async function transformDeliveryEvents( - events: DeliveryEvent[], - targetProvider: ethers.providers.Provider -): Promise { - return Promise.all( - events.map(async (x) => { - return { - status: deliveryStatus(x.args[4]), - deliveryTxHash: x.transactionHash, - vaaHash: x.args[3], - sourceVaaSequence: x.args[2], - sourceChain: x.args[1], - } - }) - ) -} - -export function findLog( - receipt: ContractReceipt, - bridgeAddress: string, - emitterAddress: string, - index: number, -): { log: ethers.providers.Log; sequence: string } { - const bridgeLogs = receipt.logs.filter((l) => { - return l.address === bridgeAddress - }) - - if (bridgeLogs.length == 0) { - throw Error("No core contract interactions found for this transaction.") - } - - const parsed = bridgeLogs.map((bridgeLog) => { - const log = Implementation__factory.createInterface().parseLog(bridgeLog) - return { - sequence: log.args[1].toString(), - nonce: log.args[2].toString(), - emitterAddress: tryNativeToHexString(log.args[0].toString(), "ethereum"), - log: bridgeLog, - } - }) - - const filtered = parsed.filter( - (x) => - x.emitterAddress == emitterAddress.toLowerCase() - ) - - if (filtered.length == 0) { - throw Error("No CoreRelayer contract interactions found for this transaction.") - } - - if (index >= filtered.length) { - throw Error("Specified delivery index is out of range.") - } else { - return { - log: filtered[index].log, - sequence: filtered[index].sequence, - } - } -} diff --git a/relayer-engine-v2/pkgs/sdk/src/structs.ts b/relayer-engine-v2/pkgs/sdk/src/structs.ts index c1c197a..b72fff6 100644 --- a/relayer-engine-v2/pkgs/sdk/src/structs.ts +++ b/relayer-engine-v2/pkgs/sdk/src/structs.ts @@ -3,20 +3,25 @@ import { arrayify } from "ethers/lib/utils" export enum RelayerPayloadId { Delivery = 1, - Redelivery = 2, // DeliveryStatus = 3, } +export enum DeliveryStatus { + WaitingForVAA = "Waiting for VAA", + PendingDelivery = "Pending Delivery", + DeliverySuccess = "Delivery Success", + ReceiverFailure = "Receiver Failure", + InvalidRedelivery = "Invalid Redelivery", + ForwardRequestSuccess = "Forward Request Success", + ForwardRequestFailure = "Forward Request Failure", + ThisShouldNeverHappen = "This should never happen. Contact Support.", + DeliveryDidntHappenWithinRange = "Delivery didn't happen within given block range", +} + export interface DeliveryInstructionsContainer { payloadId: number // 1 - sufficientlyFunded: boolean - instructions: DeliveryInstruction[] messages: MessageInfo[] -} - -export enum MessageInfoType { - EmitterSequence = 0, - VaaHash = 1, + instructions: DeliveryInstruction[] } export interface MessageInfo { @@ -41,19 +46,12 @@ export interface ExecutionParameters { providerDeliveryAddress: Buffer } -export interface RedeliveryByTxHashInstruction { - payloadId: number //2 - sourceChain: number - sourceTxHash: Buffer - deliveryVaaSequence: BigNumber - targetChain: number - multisendIndex: number - newMaximumRefundTarget: BigNumber - newReceiverValueTarget: BigNumber - executionParameters: ExecutionParameters +export enum MessageInfoType { + EMITTER_SEQUENCE = 0, + VAAHASH = 1, } -export function parsePayloadType( +export function parseWormholeRelayerPayloadType( stringPayload: string | Buffer | Uint8Array ): RelayerPayloadId { const payload = @@ -64,9 +62,7 @@ export function parsePayloadType( return payload[0] } -export function parseDeliveryInstructionsContainer( - bytes: Buffer -): DeliveryInstructionsContainer { +export function parseWormholeRelayerSend(bytes: Buffer): DeliveryInstructionsContainer { let idx = 0 const payloadId = bytes.readUInt8(idx) if (payloadId !== RelayerPayloadId.Delivery) { @@ -76,11 +72,19 @@ export function parseDeliveryInstructionsContainer( } idx += 1 - const sufficientlyFunded = Boolean(bytes.readUInt8(idx)) + const numMessages = bytes.readUInt8(idx) idx += 1 + let messages = [] as MessageInfo[] + for (let i = 0; i < numMessages; ++i) { + const res = parseMessageInfo(bytes, idx) + idx = res[1] + messages.push(res[0]) + } + const numInstructions = bytes.readUInt8(idx) idx += 1 + let instructions = [] as DeliveryInstruction[] for (let i = 0; i < numInstructions; ++i) { const targetChain = bytes.readUInt16BE(idx) @@ -97,8 +101,9 @@ export function parseDeliveryInstructionsContainer( Uint8Array.prototype.subarray.call(bytes, idx, idx + 32) ) idx += 32 - const executionParameters = parseExecutionParameters(bytes, [idx]) - idx += 37 + let res = parseWormholeRelayerExecutionParameters(bytes, idx) + const executionParameters = res[0] + idx = res[1] instructions.push( // dumb typechain format { @@ -112,106 +117,59 @@ export function parseDeliveryInstructionsContainer( ) } - // parse the manifest - const numMessages = bytes.readUInt8(idx) - idx += 1 - - const idxPtr: [number] = [idx] - const messages = [] as MessageInfo[] - for (let i = 0; i < numMessages; ++i) { - messages.push(parseMessageInfo(bytes, idxPtr)) - } return { payloadId, - sufficientlyFunded, - instructions, messages, + instructions, } } -function parseMessageInfo(bytes: Buffer, idxPtr: [number]): MessageInfo { - let idx = idxPtr[0] - const payloadType = bytes.readUInt8(idx) as MessageInfoType - switch (payloadType) { - case MessageInfoType.EmitterSequence: - const emitterAddress = bytes.slice(idx, idx + 32) - idx += 32 - const sequence = ethers.BigNumber.from( - Uint8Array.prototype.subarray.call(bytes, idx, idx + 32) - ) - idx += 8 - idxPtr[0] = idx - return { emitterAddress, sequence, payloadType } - case MessageInfoType.VaaHash: - const vaaHash = bytes.slice(idx, idx + 32) - idx += 32 - idxPtr[0] = idx - return { vaaHash, payloadType } - } -} - -export function parseRedeliveryByTxHashInstruction( - bytes: Buffer -): RedeliveryByTxHashInstruction { - let idx = 0 - const payloadId = bytes.readUInt8(idx) - if (payloadId !== RelayerPayloadId.Redelivery) { - throw new Error( - `Expected Delivery payload type (${RelayerPayloadId.Redelivery}), found: ${payloadId}` +function parseMessageInfo(bytes: Buffer, idx: number): [MessageInfo, number] { + const payloadId = bytes.readUint8(idx) + idx += 1 + const payloadType = bytes.readUint8(idx) as MessageInfoType + idx += 1 + if (payloadType == MessageInfoType.EMITTER_SEQUENCE) { + const emitterAddress = bytes.slice(idx, idx + 32) + idx += 32 + const sequence = ethers.BigNumber.from( + Uint8Array.prototype.subarray.call(bytes, idx, idx + 8) ) - } - idx += 1 - - const sourceChain = bytes.readUInt16BE(idx) - idx += 2 - - const sourceTxHash = bytes.slice(idx, idx + 32) - idx += 32 - - const deliveryVaaSequence = BigNumber.from(bytes.slice(idx, idx + 8)) - idx += 8 - - const targetChain = bytes.readUInt16BE(idx) - idx += 2 - - const multisendIndex = bytes.readUint8(idx) - idx += 1 - - // note: confirmed that BigNumber.from() assumes big endian - const newMaximumRefundTarget = BigNumber.from(bytes.slice(idx, idx + 32)) - idx += 32 - - const newReceiverValueTarget = BigNumber.from(bytes.slice(idx, idx + 32)) - idx += 32 - - const executionParameters = parseExecutionParameters(bytes, [idx]) - idx += 37 - return { - payloadId, - sourceChain, - sourceTxHash, - deliveryVaaSequence, - targetChain, - multisendIndex, - newMaximumRefundTarget, - newReceiverValueTarget, - executionParameters, + idx += 8 + return [ + { + payloadType, + emitterAddress, + sequence, + }, + idx, + ] + } else if (payloadType == MessageInfoType.VAAHASH) { + const vaaHash = bytes.slice(idx, idx + 32) + idx += 32 + return [ + { + payloadType, + vaaHash, + }, + idx, + ] + } else { + throw new Error("Unexpected MessageInfo payload type") } } -function parseExecutionParameters( +function parseWormholeRelayerExecutionParameters( bytes: Buffer, - idxPtr: [number] = [0] -): ExecutionParameters { - let idx = idxPtr[0] + idx: number +): [ExecutionParameters, number] { const version = bytes.readUInt8(idx) idx += 1 const gasLimit = bytes.readUint32BE(idx) idx += 4 const providerDeliveryAddress = bytes.slice(idx, idx + 32) idx += 32 - idxPtr[0] = idx - return { version, gasLimit, providerDeliveryAddress } + return [{ version, gasLimit, providerDeliveryAddress }, idx] } /* diff --git a/relayer-engine-v2/src/processor.ts b/relayer-engine-v2/src/processor.ts index 3006f2b..578c53b 100644 --- a/relayer-engine-v2/src/processor.ts +++ b/relayer-engine-v2/src/processor.ts @@ -3,39 +3,31 @@ import { Next } from "wormhole-relayer" import { IDelivery, MessageInfoType, - parseDeliveryInstructionsContainer, - parsePayloadType, - parseRedeliveryByTxHashInstruction, RelayerPayloadId, RelayProvider__factory, + parseWormholeRelayerPayloadType, + parseWormholeRelayerSend, } from "../pkgs/sdk/src" import { EVMChainId } from "@certusone/wormhole-sdk" import { GRContext } from "./app" export async function processGenericRelayerVaa(ctx: GRContext, next: Next) { - const payloadId = parsePayloadType(ctx.vaa!.payload) + const payloadId = parseWormholeRelayerPayloadType(ctx.vaa!.payload) // route payload types - switch (payloadId) { - case RelayerPayloadId.Delivery: - await processDelivery(ctx) - break - case RelayerPayloadId.Redelivery: - await processRedelivery(ctx) - break + if (payloadId != RelayerPayloadId.Delivery) { + ctx.logger.error(`Expected GR Delivery payload type, found ${payloadId}`) + throw new Error("Expected GR Delivery payload type") } + await processDelivery(ctx) await next() } async function processDelivery(ctx: GRContext) { const chainId = ctx.vaa!.emitterChain as wh.EVMChainId - const payload = parseDeliveryInstructionsContainer(ctx.vaa!.payload) - if (payload.sufficientlyFunded) { - ctx.logger.info("Insufficiently funded delivery request, skipping") - return - } + const payload = parseWormholeRelayerSend(ctx.vaa!.payload) if ( - payload.messages.findIndex((m) => m.payloadType !== MessageInfoType.EmitterSequence) + payload.messages.findIndex((m) => m.payloadType !== MessageInfoType.EMITTER_SEQUENCE) ) { throw new Error(`Only supports EmitterSequence MessageInfoType`) } @@ -66,7 +58,8 @@ async function processDelivery(ctx: GRContext) { relayerRefundAddress: wallet.address, } - if (!(await relayProvider.approvedSender(wallet.address))) { + const isApprovedSender = await relayProvider.approvedSender(wallet.address) + if (!isApprovedSender) { ctx.logger.warn( `Approved sender not set correctly for chain ${chainId}, should be ${wallet.address}` ) @@ -86,63 +79,3 @@ async function processDelivery(ctx: GRContext) { }) } } - -async function processRedelivery(ctx: GRContext) { - const chainId = ctx.vaa!.emitterChain as wh.EVMChainId - const redelivery = parseRedeliveryByTxHashInstruction(ctx.vaa!.payload) - - const deliveryVAA = await ctx.fetchVaa( - chainId, - ctx.wormholeRelayer[chainId], - // @ts-ignore - redelivery.sequence - ) - const deliveryInstructionsContainer = parseDeliveryInstructionsContainer( - deliveryVAA.payload - ) - - if ( - deliveryInstructionsContainer.messages.findIndex( - (m) => m.payloadType !== MessageInfoType.EmitterSequence - ) - ) { - throw new Error(`Only supports EmitterSequence MessageInfoType`) - } - - const fetchedVaas = await ctx.fetchVaas({ - ids: deliveryInstructionsContainer.messages.map((m) => ({ - emitterAddress: m.emitterAddress!, - emitterChain: chainId, - sequence: m.sequence!.toBigInt(), - })), - txHash: redelivery.sourceTxHash.toString("hex"), // todo: confirm this works - }) - await ctx.wallets.onEVM(chainId, async ({ wallet }) => { - const relayProvider = RelayProvider__factory.connect( - ctx.relayProviders[chainId], - wallet - ) - - if (!(await relayProvider.approvedSender(wallet.address))) { - ctx.logger.warn( - `Approved sender not set correctly for chain ${chainId}, should be ${wallet.address}` - ) - return - } - - const { newReceiverValueTarget, newMaximumRefundTarget } = redelivery - const budget = newReceiverValueTarget.add(newMaximumRefundTarget).add(100) - const input: IDelivery.TargetRedeliveryByTxHashParamsSingleStruct = { - sourceEncodedVMs: [...fetchedVaas.map((v) => v.bytes), deliveryVAA.bytes], - originalEncodedDeliveryVAA: deliveryVAA.bytes, - redeliveryVM: ctx.vaaBytes!, - relayerRefundAddress: wallet.address, - } - - await relayProvider - .redeliverSingle(input, { value: budget, gasLimit: 3000000 }) - .then((x) => x.wait()) - - ctx.logger.info(`Redelivered instruction to chain ${chainId}`) - }) -} diff --git a/relayer_engine/pkgs/sdk/src/consts.ts b/relayer_engine/pkgs/sdk/src/consts.ts index bf9b59c..398cb5f 100644 --- a/relayer_engine/pkgs/sdk/src/consts.ts +++ b/relayer_engine/pkgs/sdk/src/consts.ts @@ -18,9 +18,7 @@ const DEVNET = [ const MAINNET: any[] = [] -type ENV = "mainnet" | "testnet" - -export function getCoreRelayerAddressNative(chainId: ChainId, env: Network): string { +export function getWormholeRelayerAddress(chainId: ChainId, env: Network): string { if (env == "TESTNET") { const address = TESTNET.find((x) => x.chainId == chainId)?.coreRelayerAddress if (!address) { @@ -44,12 +42,12 @@ export function getCoreRelayerAddressNative(chainId: ChainId, env: Network): str } } -export function getCoreRelayer( +export function getWormholeRelayer( chainId: ChainId, env: Network, provider: ethers.providers.Provider ): CoreRelayer { - const thisChainsRelayer = getCoreRelayerAddressNative(chainId, env) + const thisChainsRelayer = getWormholeRelayerAddress(chainId, env) const contract = CoreRelayer__factory.connect(thisChainsRelayer, provider) return contract } diff --git a/relayer_engine/pkgs/sdk/src/main/helpers.ts b/relayer_engine/pkgs/sdk/src/main/helpers.ts new file mode 100644 index 0000000..9ea863b --- /dev/null +++ b/relayer_engine/pkgs/sdk/src/main/helpers.ts @@ -0,0 +1,157 @@ +import { + ChainId, + CHAIN_ID_TO_NAME, + Network, + tryNativeToHexString, +} from "@certusone/wormhole-sdk" +import { Implementation__factory } from "@certusone/wormhole-sdk/lib/cjs/ethers-contracts" +import { BigNumber, ContractReceipt, ethers } from "ethers" +import { getWormholeRelayer, RPCS_BY_CHAIN } from "../consts" +import { + parseWormholeRelayerPayloadType, + RelayerPayloadId, + parseWormholeRelayerSend, + DeliveryInstructionsContainer, + DeliveryStatus, +} from "../structs" +import { DeliveryEvent } from "../ethers-contracts/CoreRelayer" +type DeliveryTargetInfo = { + status: DeliveryStatus | string + deliveryTxHash: string | null + vaaHash: string | null + sourceChain: number | null + sourceVaaSequence: BigNumber | null +} + +export function parseWormholeLog(log: ethers.providers.Log): { + type: RelayerPayloadId + parsed: DeliveryInstructionsContainer | string +} { + const abi = [ + "event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel);", + ] + const iface = new ethers.utils.Interface(abi) + const parsed = iface.parseLog(log) + const payload = Buffer.from(parsed.args.payload.substring(2), "hex") + const type = parseWormholeRelayerPayloadType(payload) + if (type == RelayerPayloadId.Delivery) { + return { type, parsed: parseWormholeRelayerSend(payload) } + } else { + throw Error("Invalid wormhole log") + } +} + +export function printChain(chainId: number) { + return `${CHAIN_ID_TO_NAME[chainId as ChainId]} (Chain ${chainId})` +} + +export function getDefaultProvider(network: Network, chainId: ChainId) { + return new ethers.providers.StaticJsonRpcProvider( + RPCS_BY_CHAIN[network][CHAIN_ID_TO_NAME[chainId]] + ) +} + +export function getBlockRange( + provider: ethers.providers.Provider, + timestamp?: number +): [ethers.providers.BlockTag, ethers.providers.BlockTag] { + return [-2040, "latest"] +} + +export async function getWormholeRelayerDeliveryEventsBySourceSequence( + environment: Network, + targetChain: ChainId, + targetChainProvider: ethers.providers.Provider, + sourceChain: number, + sourceVaaSequence: BigNumber, + blockStartNumber: ethers.providers.BlockTag, + blockEndNumber: ethers.providers.BlockTag +): Promise { + const coreRelayer = getWormholeRelayer(targetChain, environment, targetChainProvider) + + const deliveryEvents = coreRelayer.filters.Delivery( + null, + sourceChain, + sourceVaaSequence + ) + + // There is a max limit on RPCs sometimes for how many blocks to query + return await transformDeliveryEvents( + await coreRelayer.queryFilter(deliveryEvents, blockStartNumber, blockEndNumber), + targetChainProvider + ) +} + +export function deliveryStatus(status: number) { + switch (status) { + case 0: + return DeliveryStatus.DeliverySuccess + case 1: + return DeliveryStatus.ReceiverFailure + case 2: + return DeliveryStatus.ForwardRequestFailure + case 3: + return DeliveryStatus.ForwardRequestSuccess + case 4: + return DeliveryStatus.InvalidRedelivery + default: + return DeliveryStatus.ThisShouldNeverHappen + } +} + +async function transformDeliveryEvents( + events: DeliveryEvent[], + targetProvider: ethers.providers.Provider +): Promise { + return Promise.all( + events.map(async (x) => { + return { + status: deliveryStatus(x.args[4]), + deliveryTxHash: x.transactionHash, + vaaHash: x.args[3], + sourceVaaSequence: x.args[2], + sourceChain: x.args[1], + } + }) + ) +} + +export function getWormholeRelayerLog( + receipt: ContractReceipt, + bridgeAddress: string, + emitterAddress: string, + index: number +): { log: ethers.providers.Log; sequence: string } { + const bridgeLogs = receipt.logs.filter((l) => { + return l.address === bridgeAddress + }) + + if (bridgeLogs.length == 0) { + throw Error("No core contract interactions found for this transaction.") + } + + const parsed = bridgeLogs.map((bridgeLog) => { + const log = Implementation__factory.createInterface().parseLog(bridgeLog) + return { + sequence: log.args[1].toString(), + nonce: log.args[2].toString(), + emitterAddress: tryNativeToHexString(log.args[0].toString(), "ethereum"), + log: bridgeLog, + } + }) + + const filtered = parsed.filter((x) => x.emitterAddress == emitterAddress.toLowerCase()) + + if (filtered.length == 0) { + throw Error("No CoreRelayer contract interactions found for this transaction.") + } + + if (index >= filtered.length) { + throw Error("Specified delivery index is out of range.") + } else { + return { + log: filtered[index].log, + sequence: filtered[index].sequence, + } + } +} diff --git a/relayer_engine/pkgs/sdk/src/main/index.ts b/relayer_engine/pkgs/sdk/src/main/index.ts index acdaf2e..6d84522 100644 --- a/relayer_engine/pkgs/sdk/src/main/index.ts +++ b/relayer_engine/pkgs/sdk/src/main/index.ts @@ -1 +1 @@ -export * from "./status" +export * from "./main" diff --git a/relayer_engine/pkgs/sdk/src/main/main.ts b/relayer_engine/pkgs/sdk/src/main/main.ts new file mode 100644 index 0000000..3855216 --- /dev/null +++ b/relayer_engine/pkgs/sdk/src/main/main.ts @@ -0,0 +1,224 @@ +import { + ChainId, + CHAIN_ID_TO_NAME, + isChain, + CONTRACTS, + tryNativeToHexString, + Network, +} from "@certusone/wormhole-sdk" +import { BigNumber, ethers } from "ethers" +import { getWormholeRelayerAddress } from "../consts" +import { + RelayerPayloadId, + DeliveryInstruction, + DeliveryInstructionsContainer, + MessageInfoType, + DeliveryStatus, +} from "../structs" +import { + getDefaultProvider, + printChain, + getWormholeRelayerLog, + parseWormholeLog, + getBlockRange, + getWormholeRelayerDeliveryEventsBySourceSequence, +} from "./helpers" + +type InfoRequest = { + environment: Network + sourceChain: ChainId + sourceTransaction: string + sourceChainProvider?: ethers.providers.Provider + targetChainProviders?: Map + targetChainBlockRanges?: Map< + number, + [ethers.providers.BlockTag, ethers.providers.BlockTag] + > + coreRelayerWhMessageIndex?: number +} + +export type DeliveryInfo = { + type: RelayerPayloadId.Delivery + sourceChainId: ChainId + sourceTransactionHash: string + sourceDeliverySequenceNumber: number + deliveryInstructionsContainer: DeliveryInstructionsContainer + targetChainStatuses: { + chainId: ChainId + events: { status: DeliveryStatus | string; transactionHash: string | null }[] + }[] +} + +export function printWormholeRelayerInfo(info: DeliveryInfo) { + console.log(stringifyWormholeRelayerInfo(info)) +} +export function stringifyWormholeRelayerInfo(info: DeliveryInfo): string { + let stringifiedInfo = "" + if (info.type == RelayerPayloadId.Delivery) { + stringifiedInfo += `Found delivery request in transaction ${ + info.sourceTransactionHash + } on ${printChain(info.sourceChainId)}\n` + + const numMsgs = info.deliveryInstructionsContainer.messages.length + stringifiedInfo += `\nThe following ${numMsgs} messages were requested to be relayed:\n` + stringifiedInfo += info.deliveryInstructionsContainer.messages.map((msgInfo, i) => { + let result = "" + result += `\n(Message ${i}): ` + if (msgInfo.payloadType == MessageInfoType.EMITTER_SEQUENCE) { + result += `Message with emitter address ${msgInfo.emitterAddress?.toString( + "hex" + )} and sequence number ${msgInfo.sequence}\n` + } else if (msgInfo.payloadType == MessageInfoType.VAAHASH) { + result += `Message with VAA Hash ${msgInfo.vaaHash?.toString("hex")}\n` + } else { + result += `Message not specified correctly\n` + } + }) + + const length = info.deliveryInstructionsContainer.instructions.length + stringifiedInfo += `\nMessages were requested to be sent to ${length} destination${ + length == 1 ? "" : "s" + }:\n` + stringifiedInfo += + info.deliveryInstructionsContainer.instructions + .map((instruction: DeliveryInstruction, i) => { + let result = "" + const targetChainName = CHAIN_ID_TO_NAME[instruction.targetChain as ChainId] + result += `\n(Destination ${i}): Target address is 0x${instruction.targetAddress.toString( + "hex" + )} on ${printChain(instruction.targetChain)}\n` + result += `Max amount to use for gas: ${instruction.maximumRefundTarget} of ${targetChainName} currency\n` + result += instruction.receiverValueTarget.gt(0) + ? `Amount to pass into target address: ${ + instruction.receiverValueTarget + } of ${CHAIN_ID_TO_NAME[instruction.targetChain as ChainId]} currency\n` + : `` + result += `Gas limit: ${instruction.executionParameters.gasLimit} ${targetChainName} gas\n` + result += `Relay Provider Delivery Address: 0x${instruction.executionParameters.providerDeliveryAddress.toString( + "hex" + )}\n` + result += info.targetChainStatuses[i].events + .map( + (e, i) => + `Delivery attempt ${i + 1}: ${e.status}${ + e.transactionHash + ? ` (${targetChainName} transaction hash: ${e.transactionHash})` + : "" + }` + ) + .join("\n") + return result + }) + .join("\n") + "\n" + } + return stringifiedInfo +} + +export async function getWormholeRelayerInfo( + infoRequest: InfoRequest +): Promise { + const sourceChainProvider = + infoRequest.sourceChainProvider || + getDefaultProvider(infoRequest.environment, infoRequest.sourceChain) + if (!sourceChainProvider) + throw Error( + "No default RPC for this chain; pass in your own provider (as sourceChainProvider)" + ) + const receipt = await sourceChainProvider.getTransactionReceipt( + infoRequest.sourceTransaction + ) + if (!receipt) throw Error("Transaction has not been mined") + const bridgeAddress = + CONTRACTS[infoRequest.environment][CHAIN_ID_TO_NAME[infoRequest.sourceChain]].core + const coreRelayerAddress = getWormholeRelayerAddress( + infoRequest.sourceChain, + infoRequest.environment + ) + if (!bridgeAddress || !coreRelayerAddress) { + throw Error( + `Invalid chain ID or network: Chain ID ${infoRequest.sourceChain}, ${infoRequest.environment}` + ) + } + + const deliveryLog = getWormholeRelayerLog( + receipt, + bridgeAddress, + tryNativeToHexString(coreRelayerAddress, "ethereum"), + infoRequest.coreRelayerWhMessageIndex ? infoRequest.coreRelayerWhMessageIndex : 0 + ) + + const { type, parsed } = parseWormholeLog(deliveryLog.log) + + const deliveryInstructionsContainer = parsed as DeliveryInstructionsContainer + + const targetChainStatuses = await Promise.all( + deliveryInstructionsContainer.instructions.map( + async (instruction: DeliveryInstruction) => { + const targetChain = instruction.targetChain as ChainId + if (!isChain(targetChain)) throw Error(`Invalid Chain: ${targetChain}`) + const targetChainProvider = + infoRequest.targetChainProviders?.get(targetChain) || + getDefaultProvider(infoRequest.environment, targetChain) + + if (!targetChainProvider) + throw Error( + "No default RPC for this chain; pass in your own provider (as targetChainProvider)" + ) + + const sourceChainBlock = await sourceChainProvider.getBlock(receipt.blockNumber) + const [blockStartNumber, blockEndNumber] = + infoRequest.targetChainBlockRanges?.get(targetChain) || + getBlockRange(targetChainProvider, sourceChainBlock.timestamp) + + const deliveryEvents = await getWormholeRelayerDeliveryEventsBySourceSequence( + infoRequest.environment, + targetChain, + targetChainProvider, + infoRequest.sourceChain, + BigNumber.from(deliveryLog.sequence), + blockStartNumber, + blockEndNumber + ) + if (deliveryEvents.length == 0) { + let status = `Delivery didn't happen on ${printChain( + targetChain + )} within blocks ${blockStartNumber} to ${blockEndNumber}.` + try { + const blockStart = await targetChainProvider.getBlock(blockStartNumber) + const blockEnd = await targetChainProvider.getBlock(blockEndNumber) + status = `Delivery didn't happen on ${printChain( + targetChain + )} within blocks ${blockStart.number} to ${ + blockEnd.number + } (within times ${new Date( + blockStart.timestamp * 1000 + ).toString()} to ${new Date(blockEnd.timestamp * 1000).toString()})` + } catch (e) {} + deliveryEvents.push({ + status, + deliveryTxHash: null, + vaaHash: null, + sourceChain: infoRequest.sourceChain, + sourceVaaSequence: BigNumber.from(deliveryLog.sequence), + }) + } + return { + chainId: targetChain, + events: deliveryEvents.map((e) => ({ + status: e.status, + transactionHash: e.deliveryTxHash, + })), + } + } + ) + ) + + return { + type, + sourceChainId: infoRequest.sourceChain, + sourceTransactionHash: infoRequest.sourceTransaction, + sourceDeliverySequenceNumber: BigNumber.from(deliveryLog.sequence).toNumber(), + deliveryInstructionsContainer, + targetChainStatuses, + } +} diff --git a/relayer_engine/pkgs/sdk/src/main/status.ts b/relayer_engine/pkgs/sdk/src/main/status.ts deleted file mode 100644 index 887c23c..0000000 --- a/relayer_engine/pkgs/sdk/src/main/status.ts +++ /dev/null @@ -1,373 +0,0 @@ -import { - ChainId, - CHAIN_ID_TO_NAME, - CHAINS, - isChain, - CONTRACTS, - getSignedVAAWithRetry, - Network, - parseVaa, - ParsedVaa, - tryNativeToHexString, -} from "@certusone/wormhole-sdk" -import { GetSignedVAAResponse } from "@certusone/wormhole-sdk-proto-web/lib/cjs/publicrpc/v1/publicrpc" -import { Implementation__factory } from "@certusone/wormhole-sdk/lib/cjs/ethers-contracts" -import { BigNumber, ContractReceipt, ethers, providers } from "ethers" -import { - getCoreRelayer, - getCoreRelayerAddressNative, - RPCS_BY_CHAIN, - GUARDIAN_RPC_HOSTS, -} from "../consts" -import { - parsePayloadType, - RelayerPayloadId, - parseDeliveryInstructionsContainer, - parseRedeliveryByTxHashInstruction, - DeliveryInstruction, - DeliveryInstructionsContainer, - RedeliveryByTxHashInstruction, - ExecutionParameters, - MessageInfoType, -} from "../structs" -import { DeliveryEvent } from "../ethers-contracts/CoreRelayer" - -enum DeliveryStatus { - WaitingForVAA = "Waiting for VAA", - PendingDelivery = "Pending Delivery", - DeliverySuccess = "Delivery Success", - ReceiverFailure = "Receiver Failure", - InvalidRedelivery = "Invalid Redelivery", - ForwardRequestSuccess = "Forward Request Success", - ForwardRequestFailure = "Forward Request Failure", - ThisShouldNeverHappen = "This should never happen. Contact Support.", - DeliveryDidntHappenWithinRange = "Delivery didn't happen within given block range", -} - -type DeliveryTargetInfo = { - status: DeliveryStatus | string - deliveryTxHash: string | null - vaaHash: string | null - sourceChain: number | null - sourceVaaSequence: BigNumber | null -} - -type InfoRequest = { - environment: Network - sourceChain: ChainId - sourceTransaction: string - sourceChainProvider?: ethers.providers.Provider - targetChainProviders?: Map - targetChainBlockRanges?: Map - coreRelayerWhMessageIndex?: number -} - -export function parseWormholeLog(log: ethers.providers.Log): { - type: RelayerPayloadId - parsed: DeliveryInstructionsContainer | RedeliveryByTxHashInstruction | string -} { - const abi = [ - "event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel);", - ] - const iface = new ethers.utils.Interface(abi) - const parsed = iface.parseLog(log) - const payload = Buffer.from(parsed.args.payload.substring(2), "hex") - const type = parsePayloadType(payload) - if (type == RelayerPayloadId.Delivery) { - return { type, parsed: parseDeliveryInstructionsContainer(payload) } - } else if (type == RelayerPayloadId.Redelivery) { - return { type, parsed: parseRedeliveryByTxHashInstruction(payload) } - } else { - throw Error("Invalid wormhole log"); - } -} - -export type DeliveryInfo = { - type: RelayerPayloadId.Delivery - sourceChainId: ChainId, - sourceTransactionHash: string, - sourceDeliverySequenceNumber: number, - deliveryInstructionsContainer: DeliveryInstructionsContainer - targetChainStatuses: { - chainId: ChainId - events: { status: DeliveryStatus | string; transactionHash: string | null }[] - }[] -} - -export type RedeliveryInfo = { - type: RelayerPayloadId.Redelivery - redeliverySourceChainId: ChainId, - redeliverySourceTransactionHash: string - redeliveryInstruction: RedeliveryByTxHashInstruction -} - -export function printChain(chainId: number) { - return `${CHAIN_ID_TO_NAME[chainId as ChainId]} (Chain ${chainId})` -} - -export function printInfo(info: DeliveryInfo | RedeliveryInfo) { - console.log(stringifyInfo(info)); -} -export function stringifyInfo(info: DeliveryInfo | RedeliveryInfo): string { - let stringifiedInfo = ""; - if(info.type==RelayerPayloadId.Redelivery) { - stringifiedInfo += (`Found Redelivery request in transaction ${info.redeliverySourceTransactionHash} on ${printChain(info.redeliverySourceChainId)}\n`) - stringifiedInfo += (`Original Delivery Source Chain: ${printChain(info.redeliveryInstruction.sourceChain)}\n`) - stringifiedInfo += (`Original Delivery Source Transaction Hash: 0x${info.redeliveryInstruction.sourceTxHash.toString("hex")}\n`) - stringifiedInfo += (`Original Delivery sequence number: ${info.redeliveryInstruction.deliveryVAASequence}\n`) - stringifiedInfo += (`Target Chain: ${printChain(info.redeliveryInstruction.targetChain)}\n`) - stringifiedInfo += (`multisendIndex: ${info.redeliveryInstruction.multisendIndex}\n`) - stringifiedInfo += (`New max amount (in target chain currency) to use for gas: ${info.redeliveryInstruction.newMaximumRefundTarget}\n`) - stringifiedInfo += (`New amount (in target chain currency) to pass into target address: ${info.redeliveryInstruction.newMaximumRefundTarget}\n`) - stringifiedInfo += (`New target chain gas limit: ${info.redeliveryInstruction.executionParameters.gasLimit}\n`) - stringifiedInfo += (`Relay Provider Delivery Address: 0x${info.redeliveryInstruction.executionParameters.providerDeliveryAddress.toString("hex")}\n`) - } else if(info.type==RelayerPayloadId.Delivery) { - stringifiedInfo += (`Found delivery request in transaction ${info.sourceTransactionHash} on ${printChain(info.sourceChainId)}\n`) - stringifiedInfo += ((info.deliveryInstructionsContainer.sufficientlyFunded ? "The delivery was funded\n" : "** NOTE: The delivery was NOT sufficiently funded. You did not have enough leftover funds to perform the forward **\n")) - - const numMsgs = info.deliveryInstructionsContainer.messages.length; - stringifiedInfo += `\nThe following ${numMsgs} messages were requested to be relayed:\n` - stringifiedInfo += info.deliveryInstructionsContainer.messages.map((msgInfo, i) => { - let result = ""; - result += `\n(Message ${i}): ` - if(msgInfo.infoType == MessageInfoType.EMITTER_SEQUENCE) { - result += `Message with emitter address ${msgInfo.emitterAddress.toString("hex")} and sequence number ${msgInfo.sequence}\n` - } else if(msgInfo.infoType == MessageInfoType.VAAHASH) { - result += `Message with VAA Hash ${msgInfo.vaaHash.toString("hex")}\n` - } else { - result += `Message not specified correctly\n` - } - - }) - - const length = info.deliveryInstructionsContainer.instructions.length; - stringifiedInfo += (`\nMessages were requested to be sent to ${length} destination${length == 1 ? "" : "s"}:\n`) - stringifiedInfo += (info.deliveryInstructionsContainer.instructions.map((instruction: DeliveryInstruction, i) => { - let result = ""; - const targetChainName = CHAIN_ID_TO_NAME[instruction.targetChain as ChainId]; - result += `\n(Destination ${i}): Target address is 0x${instruction.targetAddress.toString("hex")} on ${printChain(instruction.targetChain)}\n` - result += `Max amount to use for gas: ${instruction.maximumRefundTarget} of ${targetChainName} currency\n` - result += instruction.receiverValueTarget.gt(0) ? `Amount to pass into target address: ${instruction.receiverValueTarget} of ${CHAIN_ID_TO_NAME[instruction.targetChain as ChainId]} currency\n` : `` - result += `Gas limit: ${instruction.executionParameters.gasLimit} ${targetChainName} gas\n` - result += `Relay Provider Delivery Address: 0x${instruction.executionParameters.providerDeliveryAddress.toString("hex")}\n` - result += info.targetChainStatuses[i].events.map((e, i) => (`Delivery attempt ${i+1}: ${e.status}${e.transactionHash ? ` (${targetChainName} transaction hash: ${e.transactionHash})` : ""}`)).join("\n") - return result; - }).join("\n")) + "\n" - } - return stringifiedInfo -} - -function getDefaultProvider(network: Network, chainId: ChainId) { - return new ethers.providers.StaticJsonRpcProvider( - RPCS_BY_CHAIN[network][CHAIN_ID_TO_NAME[chainId]] - ) -} - -export async function getDeliveryInfoBySourceTx( - infoRequest: InfoRequest -): Promise { - const sourceChainProvider = - infoRequest.sourceChainProvider || getDefaultProvider(infoRequest.environment, infoRequest.sourceChain); - if (!sourceChainProvider) - throw Error( - "No default RPC for this chain; pass in your own provider (as sourceChainProvider)" - ) - const receipt = await sourceChainProvider.getTransactionReceipt( - infoRequest.sourceTransaction - ) - if (!receipt) throw Error("Transaction has not been mined") - const bridgeAddress = - CONTRACTS[infoRequest.environment][CHAIN_ID_TO_NAME[infoRequest.sourceChain]].core - const coreRelayerAddress = getCoreRelayerAddressNative( - infoRequest.sourceChain, - infoRequest.environment - ) - if (!bridgeAddress || !coreRelayerAddress) { - throw Error(`Invalid chain ID or network: Chain ID ${infoRequest.sourceChain}, ${infoRequest.environment}`) - } - - const deliveryLog = findLog( - receipt, - bridgeAddress, - tryNativeToHexString(coreRelayerAddress, "ethereum"), - infoRequest.coreRelayerWhMessageIndex ? infoRequest.coreRelayerWhMessageIndex : 0, - ) - - const { type, parsed } = parseWormholeLog(deliveryLog.log) - - if (type == RelayerPayloadId.Redelivery) { - const redeliveryInstruction = parsed as RedeliveryByTxHashInstruction - return { - type, - redeliverySourceChainId: infoRequest.sourceChain, - redeliverySourceTransactionHash: infoRequest.sourceTransaction, - redeliveryInstruction, - } - } - - /* Potentially use 'guardianRPCHosts' to get status of VAA; code in comments at end [1] */ - - const deliveryInstructionsContainer = parsed as DeliveryInstructionsContainer - - const targetChainStatuses = await Promise.all(deliveryInstructionsContainer.instructions.map(async (instruction: DeliveryInstruction) => { - const targetChain = instruction.targetChain as ChainId; - if(!isChain(targetChain)) throw Error(`Invalid Chain: ${targetChain}`) - const targetChainProvider = - infoRequest.targetChainProviders?.get(targetChain) || - getDefaultProvider(infoRequest.environment, targetChain) - - if (!targetChainProvider) - throw Error( - "No default RPC for this chain; pass in your own provider (as targetChainProvider)" - ) - - const sourceChainBlock = await sourceChainProvider.getBlock(receipt.blockNumber); - const [blockStartNumber, blockEndNumber] = infoRequest.targetChainBlockRanges?.get(targetChain) || getBlockRange(targetChainProvider, sourceChainBlock.timestamp); - - const deliveryEvents = await pullEventsBySourceSequence( - infoRequest.environment, - targetChain, - targetChainProvider, - infoRequest.sourceChain, - BigNumber.from(deliveryLog.sequence), - blockStartNumber, - blockEndNumber - ) - if (deliveryEvents.length == 0) { - let status = `Delivery didn't happen on ${printChain(targetChain)} within blocks ${blockStartNumber} to ${blockEndNumber}.`; - try { - const blockStart = await targetChainProvider.getBlock(blockStartNumber); - const blockEnd = await targetChainProvider.getBlock(blockEndNumber); - status = `Delivery didn't happen on ${printChain(targetChain)} within blocks ${blockStart.number} to ${blockEnd.number} (within times ${new Date(blockStart.timestamp * 1000).toString()} to ${new Date(blockEnd.timestamp * 1000).toString()})` - } catch(e) { - - } - deliveryEvents.push({ - status, - deliveryTxHash: null, - vaaHash: null, - sourceChain: infoRequest.sourceChain, - sourceVaaSequence: BigNumber.from(deliveryLog.sequence), - }) - } - return { - chainId: targetChain, - events: deliveryEvents.map((e)=>({status: e.status, transactionHash: e.deliveryTxHash})) - } - })) - - return { - type, - sourceChainId: infoRequest.sourceChain, - sourceTransactionHash: infoRequest.sourceTransaction, - sourceDeliverySequenceNumber: BigNumber.from(deliveryLog.sequence).toNumber(), - deliveryInstructionsContainer, - targetChainStatuses - } -} - -function getBlockRange(provider: ethers.providers.Provider, timestamp?: number): [ethers.providers.BlockTag, ethers.providers.BlockTag] { - return [-2040, "latest"] -} - -async function pullEventsBySourceSequence( - environment: Network, - targetChain: ChainId, - targetChainProvider: ethers.providers.Provider, - sourceChain: number, - sourceVaaSequence: BigNumber, - blockStartNumber: ethers.providers.BlockTag, - blockEndNumber: ethers.providers.BlockTag -): Promise { - const coreRelayer = getCoreRelayer(targetChain, environment, targetChainProvider) - - //TODO These compile errors on sourceChain look like an ethers bug - const deliveryEvents = coreRelayer.filters.Delivery( - null, - sourceChain, - sourceVaaSequence - ) - - // There is a max limit on RPCs sometimes for how many blocks to query - return await transformDeliveryEvents( - await coreRelayer.queryFilter(deliveryEvents, blockStartNumber, blockEndNumber), - targetChainProvider - ) -} - -function deliveryStatus(status: number) { - switch (status) { - case 0: - return DeliveryStatus.DeliverySuccess - case 1: - return DeliveryStatus.ReceiverFailure - case 2: - return DeliveryStatus.ForwardRequestFailure - case 3: - return DeliveryStatus.ForwardRequestSuccess - case 4: - return DeliveryStatus.InvalidRedelivery - default: - return DeliveryStatus.ThisShouldNeverHappen - } -} - -async function transformDeliveryEvents( - events: DeliveryEvent[], - targetProvider: ethers.providers.Provider -): Promise { - return Promise.all( - events.map(async (x) => { - return { - status: deliveryStatus(x.args[4]), - deliveryTxHash: x.transactionHash, - vaaHash: x.args[3], - sourceVaaSequence: x.args[2], - sourceChain: x.args[1], - } - }) - ) -} - -export function findLog( - receipt: ContractReceipt, - bridgeAddress: string, - emitterAddress: string, - index: number, -): { log: ethers.providers.Log; sequence: string } { - const bridgeLogs = receipt.logs.filter((l) => { - return l.address === bridgeAddress - }) - - if (bridgeLogs.length == 0) { - throw Error("No core contract interactions found for this transaction.") - } - - const parsed = bridgeLogs.map((bridgeLog) => { - const log = Implementation__factory.createInterface().parseLog(bridgeLog) - return { - sequence: log.args[1].toString(), - nonce: log.args[2].toString(), - emitterAddress: tryNativeToHexString(log.args[0].toString(), "ethereum"), - log: bridgeLog, - } - }) - - const filtered = parsed.filter( - (x) => - x.emitterAddress == emitterAddress.toLowerCase() - ) - - if (filtered.length == 0) { - throw Error("No CoreRelayer contract interactions found for this transaction.") - } - - if (index >= filtered.length) { - throw Error("Specified delivery index is out of range.") - } else { - return { - log: filtered[index].log, - sequence: filtered[index].sequence, - } - } -} diff --git a/relayer_engine/pkgs/sdk/src/structs.ts b/relayer_engine/pkgs/sdk/src/structs.ts index 1b819ed..b72fff6 100644 --- a/relayer_engine/pkgs/sdk/src/structs.ts +++ b/relayer_engine/pkgs/sdk/src/structs.ts @@ -3,21 +3,25 @@ import { arrayify } from "ethers/lib/utils" export enum RelayerPayloadId { Delivery = 1, - Redelivery = 2, // DeliveryStatus = 3, } +export enum DeliveryStatus { + WaitingForVAA = "Waiting for VAA", + PendingDelivery = "Pending Delivery", + DeliverySuccess = "Delivery Success", + ReceiverFailure = "Receiver Failure", + InvalidRedelivery = "Invalid Redelivery", + ForwardRequestSuccess = "Forward Request Success", + ForwardRequestFailure = "Forward Request Failure", + ThisShouldNeverHappen = "This should never happen. Contact Support.", + DeliveryDidntHappenWithinRange = "Delivery didn't happen within given block range", +} + export interface DeliveryInstructionsContainer { payloadId: number // 1 - sufficientlyFunded: boolean messages: MessageInfo[] instructions: DeliveryInstruction[] - messages: MessageInfo[] -} - -export enum MessageInfoType { - EmitterSequence = 0, - VaaHash = 1, } export interface MessageInfo { @@ -42,28 +46,12 @@ export interface ExecutionParameters { providerDeliveryAddress: Buffer } -export interface RedeliveryByTxHashInstruction { - payloadId: number //2 - sourceChain: number - sourceTxHash: Buffer - deliveryVaaSequence: BigNumber - targetChain: number - multisendIndex: number - newMaximumRefundTarget: BigNumber - newReceiverValueTarget: BigNumber - executionParameters: ExecutionParameters +export enum MessageInfoType { + EMITTER_SEQUENCE = 0, + VAAHASH = 1, } -export enum MessageInfoType {EMITTER_SEQUENCE, VAAHASH} - -export interface MessageInfo { - infoType: MessageInfoType, - emitterAddress: Buffer, - sequence: number, - vaaHash: Buffer -} - -export function parsePayloadType( +export function parseWormholeRelayerPayloadType( stringPayload: string | Buffer | Uint8Array ): RelayerPayloadId { const payload = @@ -74,9 +62,7 @@ export function parsePayloadType( return payload[0] } -export function parseDeliveryInstructionsContainer( - bytes: Buffer -): DeliveryInstructionsContainer { +export function parseWormholeRelayerSend(bytes: Buffer): DeliveryInstructionsContainer { let idx = 0 const payloadId = bytes.readUInt8(idx) if (payloadId !== RelayerPayloadId.Delivery) { @@ -86,41 +72,19 @@ export function parseDeliveryInstructionsContainer( } idx += 1 - const sufficientlyFunded = Boolean(bytes.readUInt8(idx)) - idx += 1 - const numMessages = bytes.readUInt8(idx) idx += 1 + let messages = [] as MessageInfo[] + for (let i = 0; i < numMessages; ++i) { + const res = parseMessageInfo(bytes, idx) + idx = res[1] + messages.push(res[0]) + } + const numInstructions = bytes.readUInt8(idx) idx += 1 - let messages = [] as MessageInfo[] - for(let i=0; i < numMessages; ++i) { - const payloadId = bytes.readUint8(idx); - idx += 1; - const infoType = bytes.readUint8(idx) as MessageInfoType; - idx += 1; - let emitterAddress = Buffer.from([]); - let sequence = 0; - let vaaHash = Buffer.from([]); - if(infoType == MessageInfoType.EMITTER_SEQUENCE) { - emitterAddress = bytes.slice(idx, idx+32); - idx += 32; - sequence = ethers.BigNumber.from(Uint8Array.prototype.subarray.call(bytes, idx, idx+8)).toNumber(); - idx += 8; - } else if(infoType == MessageInfoType.VAAHASH) { - vaaHash = bytes.slice(idx, idx + 32); - idx += 32; - } - messages.push({ - infoType, - emitterAddress, - sequence, - vaaHash - }) - } - let instructions = [] as DeliveryInstruction[] for (let i = 0; i < numInstructions; ++i) { const targetChain = bytes.readUInt16BE(idx) @@ -137,8 +101,9 @@ export function parseDeliveryInstructionsContainer( Uint8Array.prototype.subarray.call(bytes, idx, idx + 32) ) idx += 32 - const executionParameters = parseExecutionParameters(bytes, [idx]) - idx += 37 + let res = parseWormholeRelayerExecutionParameters(bytes, idx) + const executionParameters = res[0] + idx = res[1] instructions.push( // dumb typechain format { @@ -152,107 +117,59 @@ export function parseDeliveryInstructionsContainer( ) } - // parse the manifest - const numMessages = bytes.readUInt8(idx) - idx += 1 - - const idxPtr: [number] = [idx] - const messages = [] as MessageInfo[] - for (let i = 0; i < numMessages; ++i) { - messages.push(parseMessageInfo(bytes, idxPtr)) - } return { payloadId, - sufficientlyFunded, messages, instructions, - messages, } } -function parseMessageInfo(bytes: Buffer, idxPtr: [number]): MessageInfo { - let idx = idxPtr[0] - const payloadType = bytes.readUInt8(idx) as MessageInfoType - switch (payloadType) { - case MessageInfoType.EmitterSequence: - const emitterAddress = bytes.slice(idx, idx + 32) - idx += 32 - const sequence = ethers.BigNumber.from( - Uint8Array.prototype.subarray.call(bytes, idx, idx + 32) - ) - idx += 8 - idxPtr[0] = idx - return { emitterAddress, sequence, payloadType } - case MessageInfoType.VaaHash: - const vaaHash = bytes.slice(idx, idx + 32) - idx += 32 - idxPtr[0] = idx - return { vaaHash, payloadType } - } -} - -export function parseRedeliveryByTxHashInstruction( - bytes: Buffer -): RedeliveryByTxHashInstruction { - let idx = 0 - const payloadId = bytes.readUInt8(idx) - if (payloadId !== RelayerPayloadId.Redelivery) { - throw new Error( - `Expected Delivery payload type (${RelayerPayloadId.Redelivery}), found: ${payloadId}` +function parseMessageInfo(bytes: Buffer, idx: number): [MessageInfo, number] { + const payloadId = bytes.readUint8(idx) + idx += 1 + const payloadType = bytes.readUint8(idx) as MessageInfoType + idx += 1 + if (payloadType == MessageInfoType.EMITTER_SEQUENCE) { + const emitterAddress = bytes.slice(idx, idx + 32) + idx += 32 + const sequence = ethers.BigNumber.from( + Uint8Array.prototype.subarray.call(bytes, idx, idx + 8) ) - } - idx += 1 - - const sourceChain = bytes.readUInt16BE(idx) - idx += 2 - - const sourceTxHash = bytes.slice(idx, idx + 32) - idx += 32 - - const deliveryVaaSequence = BigNumber.from(bytes.slice(idx, idx + 8)) - idx += 8 - - const targetChain = bytes.readUInt16BE(idx) - idx += 2 - - const multisendIndex = bytes.readUint8(idx) - idx += 1 - - // note: confirmed that BigNumber.from() assumes big endian - const newMaximumRefundTarget = BigNumber.from(bytes.slice(idx, idx + 32)) - idx += 32 - - const newReceiverValueTarget = BigNumber.from(bytes.slice(idx, idx + 32)) - idx += 32 - - const executionParameters = parseExecutionParameters(bytes, [idx]) - idx += 37 - return { - payloadId, - sourceChain, - sourceTxHash, - deliveryVaaSequence, - targetChain, - multisendIndex, - newMaximumRefundTarget, - newReceiverValueTarget, - executionParameters, + idx += 8 + return [ + { + payloadType, + emitterAddress, + sequence, + }, + idx, + ] + } else if (payloadType == MessageInfoType.VAAHASH) { + const vaaHash = bytes.slice(idx, idx + 32) + idx += 32 + return [ + { + payloadType, + vaaHash, + }, + idx, + ] + } else { + throw new Error("Unexpected MessageInfo payload type") } } -function parseExecutionParameters( +function parseWormholeRelayerExecutionParameters( bytes: Buffer, - idxPtr: [number] = [0] -): ExecutionParameters { - let idx = idxPtr[0] + idx: number +): [ExecutionParameters, number] { const version = bytes.readUInt8(idx) idx += 1 const gasLimit = bytes.readUint32BE(idx) idx += 4 const providerDeliveryAddress = bytes.slice(idx, idx + 32) idx += 32 - idxPtr[0] = idx - return { version, gasLimit, providerDeliveryAddress } + return [{ version, gasLimit, providerDeliveryAddress }, idx] } /* diff --git a/sdk/src/main/helpers.ts b/sdk/src/main/helpers.ts index ef08ff8..9ea863b 100644 --- a/sdk/src/main/helpers.ts +++ b/sdk/src/main/helpers.ts @@ -1,35 +1,18 @@ import { ChainId, CHAIN_ID_TO_NAME, - CHAINS, - isChain, - CONTRACTS, - getSignedVAAWithRetry, Network, - parseVaa, - ParsedVaa, tryNativeToHexString, } from "@certusone/wormhole-sdk" -import { GetSignedVAAResponse } from "@certusone/wormhole-sdk-proto-web/lib/cjs/publicrpc/v1/publicrpc" import { Implementation__factory } from "@certusone/wormhole-sdk/lib/cjs/ethers-contracts" -import { BigNumber, ContractReceipt, ethers, providers } from "ethers" -import { - getWormholeRelayer, - getWormholeRelayerAddress, - RPCS_BY_CHAIN, - GUARDIAN_RPC_HOSTS, -} from "../consts" +import { BigNumber, ContractReceipt, ethers } from "ethers" +import { getWormholeRelayer, RPCS_BY_CHAIN } from "../consts" import { parseWormholeRelayerPayloadType, RelayerPayloadId, parseWormholeRelayerSend, - parseWormholeRelayerResend, - DeliveryInstruction, DeliveryInstructionsContainer, - RedeliveryByTxHashInstruction, - ExecutionParameters, - MessageInfoType, - DeliveryStatus + DeliveryStatus, } from "../structs" import { DeliveryEvent } from "../ethers-contracts/CoreRelayer" type DeliveryTargetInfo = { @@ -40,10 +23,9 @@ type DeliveryTargetInfo = { sourceVaaSequence: BigNumber | null } - export function parseWormholeLog(log: ethers.providers.Log): { type: RelayerPayloadId - parsed: DeliveryInstructionsContainer | RedeliveryByTxHashInstruction | string + parsed: DeliveryInstructionsContainer | string } { const abi = [ "event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel);", @@ -54,10 +36,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"); + throw Error("Invalid wormhole log") } } @@ -71,8 +51,10 @@ export function getDefaultProvider(network: Network, chainId: ChainId) { ) } - -export function getBlockRange(provider: ethers.providers.Provider, timestamp?: number): [ethers.providers.BlockTag, ethers.providers.BlockTag] { +export function getBlockRange( + provider: ethers.providers.Provider, + timestamp?: number +): [ethers.providers.BlockTag, ethers.providers.BlockTag] { return [-2040, "latest"] } @@ -138,7 +120,7 @@ export function getWormholeRelayerLog( receipt: ContractReceipt, bridgeAddress: string, emitterAddress: string, - index: number, + index: number ): { log: ethers.providers.Log; sequence: string } { const bridgeLogs = receipt.logs.filter((l) => { return l.address === bridgeAddress @@ -158,10 +140,7 @@ export function getWormholeRelayerLog( } }) - const filtered = parsed.filter( - (x) => - x.emitterAddress == emitterAddress.toLowerCase() - ) + const filtered = parsed.filter((x) => x.emitterAddress == emitterAddress.toLowerCase()) if (filtered.length == 0) { throw Error("No CoreRelayer contract interactions found for this transaction.") diff --git a/sdk/src/main/index.ts b/sdk/src/main/index.ts index acdaf2e..6d84522 100644 --- a/sdk/src/main/index.ts +++ b/sdk/src/main/index.ts @@ -1 +1 @@ -export * from "./status" +export * from "./main" diff --git a/sdk/src/main/main.ts b/sdk/src/main/main.ts index 8c65ea5..3855216 100644 --- a/sdk/src/main/main.ts +++ b/sdk/src/main/main.ts @@ -1,213 +1,224 @@ import { - ChainId, - CHAIN_ID_TO_NAME, - isChain, - CONTRACTS, - Network, - tryNativeToHexString, - } from "@certusone/wormhole-sdk" - import { BigNumber, ethers, } from "ethers" - import { - getWormholeRelayerAddress, - } from "../consts" - import { - RelayerPayloadId, - DeliveryInstruction, - DeliveryInstructionsContainer, - RedeliveryByTxHashInstruction, - MessageInfoType, - DeliveryStatus - } from "../structs" - import { - getDefaultProvider, - printChain, - getWormholeRelayerLog, - parseWormholeLog, - getBlockRange, - getWormholeRelayerDeliveryEventsBySourceSequence, - } from "./helpers" - - type InfoRequest = { - environment: Network - sourceChain: ChainId - sourceTransaction: string - sourceChainProvider?: ethers.providers.Provider - targetChainProviders?: Map - targetChainBlockRanges?: Map - coreRelayerWhMessageIndex?: number - } - - export type DeliveryInfo = { - type: RelayerPayloadId.Delivery - sourceChainId: ChainId, - sourceTransactionHash: string, - sourceDeliverySequenceNumber: number, - deliveryInstructionsContainer: DeliveryInstructionsContainer - targetChainStatuses: { - chainId: ChainId - events: { status: DeliveryStatus | string; transactionHash: string | null }[] - }[] - } - - export type RedeliveryInfo = { - type: RelayerPayloadId.Redelivery - redeliverySourceChainId: ChainId, - redeliverySourceTransactionHash: string - redeliveryInstruction: RedeliveryByTxHashInstruction - } - - export function printWormholeRelayerInfo(info: DeliveryInfo | RedeliveryInfo) { - console.log(stringifyWormholeRelayerInfo(info)); - } - export function stringifyWormholeRelayerInfo(info: DeliveryInfo | RedeliveryInfo): string { - let stringifiedInfo = ""; - if(info.type==RelayerPayloadId.Redelivery) { - stringifiedInfo += (`Found Redelivery request in transaction ${info.redeliverySourceTransactionHash} on ${printChain(info.redeliverySourceChainId)}\n`) - stringifiedInfo += (`Original Delivery Source Chain: ${printChain(info.redeliveryInstruction.sourceChain)}\n`) - stringifiedInfo += (`Original Delivery Source Transaction Hash: 0x${info.redeliveryInstruction.sourceTxHash.toString("hex")}\n`) - stringifiedInfo += (`Original Delivery sequence number: ${info.redeliveryInstruction.deliveryVAASequence}\n`) - stringifiedInfo += (`Target Chain: ${printChain(info.redeliveryInstruction.targetChain)}\n`) - stringifiedInfo += (`multisendIndex: ${info.redeliveryInstruction.multisendIndex}\n`) - stringifiedInfo += (`New max amount (in target chain currency) to use for gas: ${info.redeliveryInstruction.newMaximumRefundTarget}\n`) - stringifiedInfo += (`New amount (in target chain currency) to pass into target address: ${info.redeliveryInstruction.newMaximumRefundTarget}\n`) - stringifiedInfo += (`New target chain gas limit: ${info.redeliveryInstruction.executionParameters.gasLimit}\n`) - stringifiedInfo += (`Relay Provider Delivery Address: 0x${info.redeliveryInstruction.executionParameters.providerDeliveryAddress.toString("hex")}\n`) - } else if(info.type==RelayerPayloadId.Delivery) { - stringifiedInfo += (`Found delivery request in transaction ${info.sourceTransactionHash} on ${printChain(info.sourceChainId)}\n`) - stringifiedInfo += ((info.deliveryInstructionsContainer.sufficientlyFunded ? "The delivery was funded\n" : "** NOTE: The delivery was NOT sufficiently funded. You did not have enough leftover funds to perform the forward **\n")) - - const numMsgs = info.deliveryInstructionsContainer.messages.length; - stringifiedInfo += `\nThe following ${numMsgs} messages were requested to be relayed:\n` - stringifiedInfo += info.deliveryInstructionsContainer.messages.map((msgInfo, i) => { - let result = ""; - result += `\n(Message ${i}): ` - if(msgInfo.infoType == MessageInfoType.EMITTER_SEQUENCE) { - result += `Message with emitter address ${msgInfo.emitterAddress.toString("hex")} and sequence number ${msgInfo.sequence}\n` - } else if(msgInfo.infoType == MessageInfoType.VAAHASH) { - result += `Message with VAA Hash ${msgInfo.vaaHash.toString("hex")}\n` - } else { - result += `Message not specified correctly\n` - } - - }) - - const length = info.deliveryInstructionsContainer.instructions.length; - stringifiedInfo += (`\nMessages were requested to be sent to ${length} destination${length == 1 ? "" : "s"}:\n`) - stringifiedInfo += (info.deliveryInstructionsContainer.instructions.map((instruction: DeliveryInstruction, i) => { - let result = ""; - const targetChainName = CHAIN_ID_TO_NAME[instruction.targetChain as ChainId]; - result += `\n(Destination ${i}): Target address is 0x${instruction.targetAddress.toString("hex")} on ${printChain(instruction.targetChain)}\n` - result += `Max amount to use for gas: ${instruction.maximumRefundTarget} of ${targetChainName} currency\n` - result += instruction.receiverValueTarget.gt(0) ? `Amount to pass into target address: ${instruction.receiverValueTarget} of ${CHAIN_ID_TO_NAME[instruction.targetChain as ChainId]} currency\n` : `` - result += `Gas limit: ${instruction.executionParameters.gasLimit} ${targetChainName} gas\n` - result += `Relay Provider Delivery Address: 0x${instruction.executionParameters.providerDeliveryAddress.toString("hex")}\n` - result += info.targetChainStatuses[i].events.map((e, i) => (`Delivery attempt ${i+1}: ${e.status}${e.transactionHash ? ` (${targetChainName} transaction hash: ${e.transactionHash})` : ""}`)).join("\n") - return result; - }).join("\n")) + "\n" - } - return stringifiedInfo - } - + ChainId, + CHAIN_ID_TO_NAME, + isChain, + CONTRACTS, + tryNativeToHexString, + Network, +} from "@certusone/wormhole-sdk" +import { BigNumber, ethers } from "ethers" +import { getWormholeRelayerAddress } from "../consts" +import { + RelayerPayloadId, + DeliveryInstruction, + DeliveryInstructionsContainer, + MessageInfoType, + DeliveryStatus, +} from "../structs" +import { + getDefaultProvider, + printChain, + getWormholeRelayerLog, + parseWormholeLog, + getBlockRange, + getWormholeRelayerDeliveryEventsBySourceSequence, +} from "./helpers" - export async function getWormholeRelayerInfo( - infoRequest: InfoRequest - ): Promise { - const sourceChainProvider = - infoRequest.sourceChainProvider || getDefaultProvider(infoRequest.environment, infoRequest.sourceChain); - if (!sourceChainProvider) - throw Error( - "No default RPC for this chain; pass in your own provider (as sourceChainProvider)" - ) - const receipt = await sourceChainProvider.getTransactionReceipt( - infoRequest.sourceTransaction - ) - if (!receipt) throw Error("Transaction has not been mined") - const bridgeAddress = - CONTRACTS[infoRequest.environment][CHAIN_ID_TO_NAME[infoRequest.sourceChain]].core - const coreRelayerAddress = getWormholeRelayerAddress( - infoRequest.sourceChain, - infoRequest.environment - ) - if (!bridgeAddress || !coreRelayerAddress) { - throw Error(`Invalid chain ID or network: Chain ID ${infoRequest.sourceChain}, ${infoRequest.environment}`) - } - - const deliveryLog = getWormholeRelayerLog( - receipt, - bridgeAddress, - tryNativeToHexString(coreRelayerAddress, "ethereum"), - infoRequest.coreRelayerWhMessageIndex ? infoRequest.coreRelayerWhMessageIndex : 0, - ) - - const { type, parsed } = parseWormholeLog(deliveryLog.log) - - if (type == RelayerPayloadId.Redelivery) { - const redeliveryInstruction = parsed as RedeliveryByTxHashInstruction - return { - type, - redeliverySourceChainId: infoRequest.sourceChain, - redeliverySourceTransactionHash: infoRequest.sourceTransaction, - redeliveryInstruction, +type InfoRequest = { + environment: Network + sourceChain: ChainId + sourceTransaction: string + sourceChainProvider?: ethers.providers.Provider + targetChainProviders?: Map + targetChainBlockRanges?: Map< + number, + [ethers.providers.BlockTag, ethers.providers.BlockTag] + > + coreRelayerWhMessageIndex?: number +} + +export type DeliveryInfo = { + type: RelayerPayloadId.Delivery + sourceChainId: ChainId + sourceTransactionHash: string + sourceDeliverySequenceNumber: number + deliveryInstructionsContainer: DeliveryInstructionsContainer + targetChainStatuses: { + chainId: ChainId + events: { status: DeliveryStatus | string; transactionHash: string | null }[] + }[] +} + +export function printWormholeRelayerInfo(info: DeliveryInfo) { + console.log(stringifyWormholeRelayerInfo(info)) +} +export function stringifyWormholeRelayerInfo(info: DeliveryInfo): string { + let stringifiedInfo = "" + if (info.type == RelayerPayloadId.Delivery) { + stringifiedInfo += `Found delivery request in transaction ${ + info.sourceTransactionHash + } on ${printChain(info.sourceChainId)}\n` + + const numMsgs = info.deliveryInstructionsContainer.messages.length + stringifiedInfo += `\nThe following ${numMsgs} messages were requested to be relayed:\n` + stringifiedInfo += info.deliveryInstructionsContainer.messages.map((msgInfo, i) => { + let result = "" + result += `\n(Message ${i}): ` + if (msgInfo.payloadType == MessageInfoType.EMITTER_SEQUENCE) { + result += `Message with emitter address ${msgInfo.emitterAddress?.toString( + "hex" + )} and sequence number ${msgInfo.sequence}\n` + } else if (msgInfo.payloadType == MessageInfoType.VAAHASH) { + result += `Message with VAA Hash ${msgInfo.vaaHash?.toString("hex")}\n` + } else { + result += `Message not specified correctly\n` } - } - - const deliveryInstructionsContainer = parsed as DeliveryInstructionsContainer - - const targetChainStatuses = await Promise.all(deliveryInstructionsContainer.instructions.map(async (instruction: DeliveryInstruction) => { - const targetChain = instruction.targetChain as ChainId; - if(!isChain(targetChain)) throw Error(`Invalid Chain: ${targetChain}`) - const targetChainProvider = - infoRequest.targetChainProviders?.get(targetChain) || - getDefaultProvider(infoRequest.environment, targetChain) - - if (!targetChainProvider) - throw Error( - "No default RPC for this chain; pass in your own provider (as targetChainProvider)" - ) - - const sourceChainBlock = await sourceChainProvider.getBlock(receipt.blockNumber); - const [blockStartNumber, blockEndNumber] = infoRequest.targetChainBlockRanges?.get(targetChain) || getBlockRange(targetChainProvider, sourceChainBlock.timestamp); - - const deliveryEvents = await getWormholeRelayerDeliveryEventsBySourceSequence( - infoRequest.environment, - targetChain, - targetChainProvider, - infoRequest.sourceChain, - BigNumber.from(deliveryLog.sequence), - blockStartNumber, - blockEndNumber - ) - if (deliveryEvents.length == 0) { - let status = `Delivery didn't happen on ${printChain(targetChain)} within blocks ${blockStartNumber} to ${blockEndNumber}.`; - try { - const blockStart = await targetChainProvider.getBlock(blockStartNumber); - const blockEnd = await targetChainProvider.getBlock(blockEndNumber); - status = `Delivery didn't happen on ${printChain(targetChain)} within blocks ${blockStart.number} to ${blockEnd.number} (within times ${new Date(blockStart.timestamp * 1000).toString()} to ${new Date(blockEnd.timestamp * 1000).toString()})` - } catch(e) { - - } - deliveryEvents.push({ - status, - deliveryTxHash: null, - vaaHash: null, - sourceChain: infoRequest.sourceChain, - sourceVaaSequence: BigNumber.from(deliveryLog.sequence), + }) + + const length = info.deliveryInstructionsContainer.instructions.length + stringifiedInfo += `\nMessages were requested to be sent to ${length} destination${ + length == 1 ? "" : "s" + }:\n` + stringifiedInfo += + info.deliveryInstructionsContainer.instructions + .map((instruction: DeliveryInstruction, i) => { + let result = "" + const targetChainName = CHAIN_ID_TO_NAME[instruction.targetChain as ChainId] + result += `\n(Destination ${i}): Target address is 0x${instruction.targetAddress.toString( + "hex" + )} on ${printChain(instruction.targetChain)}\n` + result += `Max amount to use for gas: ${instruction.maximumRefundTarget} of ${targetChainName} currency\n` + result += instruction.receiverValueTarget.gt(0) + ? `Amount to pass into target address: ${ + instruction.receiverValueTarget + } of ${CHAIN_ID_TO_NAME[instruction.targetChain as ChainId]} currency\n` + : `` + result += `Gas limit: ${instruction.executionParameters.gasLimit} ${targetChainName} gas\n` + result += `Relay Provider Delivery Address: 0x${instruction.executionParameters.providerDeliveryAddress.toString( + "hex" + )}\n` + result += info.targetChainStatuses[i].events + .map( + (e, i) => + `Delivery attempt ${i + 1}: ${e.status}${ + e.transactionHash + ? ` (${targetChainName} transaction hash: ${e.transactionHash})` + : "" + }` + ) + .join("\n") + return result }) - } - return { - chainId: targetChain, - events: deliveryEvents.map((e)=>({status: e.status, transactionHash: e.deliveryTxHash})) - } - })) - - return { - type, - sourceChainId: infoRequest.sourceChain, - sourceTransactionHash: infoRequest.sourceTransaction, - sourceDeliverySequenceNumber: BigNumber.from(deliveryLog.sequence).toNumber(), - deliveryInstructionsContainer, - targetChainStatuses - } + .join("\n") + "\n" } - - \ No newline at end of file + return stringifiedInfo +} + +export async function getWormholeRelayerInfo( + infoRequest: InfoRequest +): Promise { + const sourceChainProvider = + infoRequest.sourceChainProvider || + getDefaultProvider(infoRequest.environment, infoRequest.sourceChain) + if (!sourceChainProvider) + throw Error( + "No default RPC for this chain; pass in your own provider (as sourceChainProvider)" + ) + const receipt = await sourceChainProvider.getTransactionReceipt( + infoRequest.sourceTransaction + ) + if (!receipt) throw Error("Transaction has not been mined") + const bridgeAddress = + CONTRACTS[infoRequest.environment][CHAIN_ID_TO_NAME[infoRequest.sourceChain]].core + const coreRelayerAddress = getWormholeRelayerAddress( + infoRequest.sourceChain, + infoRequest.environment + ) + if (!bridgeAddress || !coreRelayerAddress) { + throw Error( + `Invalid chain ID or network: Chain ID ${infoRequest.sourceChain}, ${infoRequest.environment}` + ) + } + + const deliveryLog = getWormholeRelayerLog( + receipt, + bridgeAddress, + tryNativeToHexString(coreRelayerAddress, "ethereum"), + infoRequest.coreRelayerWhMessageIndex ? infoRequest.coreRelayerWhMessageIndex : 0 + ) + + const { type, parsed } = parseWormholeLog(deliveryLog.log) + + const deliveryInstructionsContainer = parsed as DeliveryInstructionsContainer + + const targetChainStatuses = await Promise.all( + deliveryInstructionsContainer.instructions.map( + async (instruction: DeliveryInstruction) => { + const targetChain = instruction.targetChain as ChainId + if (!isChain(targetChain)) throw Error(`Invalid Chain: ${targetChain}`) + const targetChainProvider = + infoRequest.targetChainProviders?.get(targetChain) || + getDefaultProvider(infoRequest.environment, targetChain) + + if (!targetChainProvider) + throw Error( + "No default RPC for this chain; pass in your own provider (as targetChainProvider)" + ) + + const sourceChainBlock = await sourceChainProvider.getBlock(receipt.blockNumber) + const [blockStartNumber, blockEndNumber] = + infoRequest.targetChainBlockRanges?.get(targetChain) || + getBlockRange(targetChainProvider, sourceChainBlock.timestamp) + + const deliveryEvents = await getWormholeRelayerDeliveryEventsBySourceSequence( + infoRequest.environment, + targetChain, + targetChainProvider, + infoRequest.sourceChain, + BigNumber.from(deliveryLog.sequence), + blockStartNumber, + blockEndNumber + ) + if (deliveryEvents.length == 0) { + let status = `Delivery didn't happen on ${printChain( + targetChain + )} within blocks ${blockStartNumber} to ${blockEndNumber}.` + try { + const blockStart = await targetChainProvider.getBlock(blockStartNumber) + const blockEnd = await targetChainProvider.getBlock(blockEndNumber) + status = `Delivery didn't happen on ${printChain( + targetChain + )} within blocks ${blockStart.number} to ${ + blockEnd.number + } (within times ${new Date( + blockStart.timestamp * 1000 + ).toString()} to ${new Date(blockEnd.timestamp * 1000).toString()})` + } catch (e) {} + deliveryEvents.push({ + status, + deliveryTxHash: null, + vaaHash: null, + sourceChain: infoRequest.sourceChain, + sourceVaaSequence: BigNumber.from(deliveryLog.sequence), + }) + } + return { + chainId: targetChain, + events: deliveryEvents.map((e) => ({ + status: e.status, + transactionHash: e.deliveryTxHash, + })), + } + } + ) + ) + + return { + type, + sourceChainId: infoRequest.sourceChain, + sourceTransactionHash: infoRequest.sourceTransaction, + sourceDeliverySequenceNumber: BigNumber.from(deliveryLog.sequence).toNumber(), + deliveryInstructionsContainer, + targetChainStatuses, + } +} diff --git a/sdk/src/structs.ts b/sdk/src/structs.ts index e4c6c40..b72fff6 100644 --- a/sdk/src/structs.ts +++ b/sdk/src/structs.ts @@ -3,7 +3,6 @@ import { arrayify } from "ethers/lib/utils" export enum RelayerPayloadId { Delivery = 1, - Redelivery = 2, // DeliveryStatus = 3, } @@ -19,18 +18,10 @@ export enum DeliveryStatus { DeliveryDidntHappenWithinRange = "Delivery didn't happen within given block range", } - export interface DeliveryInstructionsContainer { payloadId: number // 1 - sufficientlyFunded: boolean messages: MessageInfo[] instructions: DeliveryInstruction[] - messages: MessageInfo[] -} - -export enum MessageInfoType { - EmitterSequence = 0, - VaaHash = 1, } export interface MessageInfo { @@ -55,25 +46,9 @@ export interface ExecutionParameters { providerDeliveryAddress: Buffer } -export interface RedeliveryByTxHashInstruction { - payloadId: number //2 - sourceChain: number - sourceTxHash: Buffer - deliveryVaaSequence: BigNumber - targetChain: number - multisendIndex: number - newMaximumRefundTarget: BigNumber - newReceiverValueTarget: BigNumber - executionParameters: ExecutionParameters -} - -export enum MessageInfoType {EMITTER_SEQUENCE, VAAHASH} - -export interface MessageInfo { - infoType: MessageInfoType, - emitterAddress: Buffer, - sequence: number, - vaaHash: Buffer +export enum MessageInfoType { + EMITTER_SEQUENCE = 0, + VAAHASH = 1, } export function parseWormholeRelayerPayloadType( @@ -87,9 +62,7 @@ export function parseWormholeRelayerPayloadType( return payload[0] } -export function parseWormholeRelayerSend( - bytes: Buffer -): DeliveryInstructionsContainer { +export function parseWormholeRelayerSend(bytes: Buffer): DeliveryInstructionsContainer { let idx = 0 const payloadId = bytes.readUInt8(idx) if (payloadId !== RelayerPayloadId.Delivery) { @@ -99,41 +72,19 @@ export function parseWormholeRelayerSend( } idx += 1 - const sufficientlyFunded = Boolean(bytes.readUInt8(idx)) - idx += 1 - const numMessages = bytes.readUInt8(idx) idx += 1 + let messages = [] as MessageInfo[] + for (let i = 0; i < numMessages; ++i) { + const res = parseMessageInfo(bytes, idx) + idx = res[1] + messages.push(res[0]) + } + const numInstructions = bytes.readUInt8(idx) idx += 1 - let messages = [] as MessageInfo[] - for(let i=0; i < numMessages; ++i) { - const payloadId = bytes.readUint8(idx); - idx += 1; - const infoType = bytes.readUint8(idx) as MessageInfoType; - idx += 1; - let emitterAddress = Buffer.from([]); - let sequence = 0; - let vaaHash = Buffer.from([]); - if(infoType == MessageInfoType.EMITTER_SEQUENCE) { - emitterAddress = bytes.slice(idx, idx+32); - idx += 32; - sequence = ethers.BigNumber.from(Uint8Array.prototype.subarray.call(bytes, idx, idx+8)).toNumber(); - idx += 8; - } else if(infoType == MessageInfoType.VAAHASH) { - vaaHash = bytes.slice(idx, idx + 32); - idx += 32; - } - messages.push({ - infoType, - emitterAddress, - sequence, - vaaHash - }) - } - let instructions = [] as DeliveryInstruction[] for (let i = 0; i < numInstructions; ++i) { const targetChain = bytes.readUInt16BE(idx) @@ -150,8 +101,9 @@ export function parseWormholeRelayerSend( Uint8Array.prototype.subarray.call(bytes, idx, idx + 32) ) idx += 32 - const executionParameters = parseWormholeRelayerExecutionParameters(bytes, idx) - idx += 37 + let res = parseWormholeRelayerExecutionParameters(bytes, idx) + const executionParameters = res[0] + idx = res[1] instructions.push( // dumb typechain format { @@ -165,81 +117,59 @@ export function parseWormholeRelayerSend( ) } - // parse the manifest - const numMessages = bytes.readUInt8(idx) - idx += 1 - - const idxPtr: [number] = [idx] - const messages = [] as MessageInfo[] - for (let i = 0; i < numMessages; ++i) { - messages.push(parseMessageInfo(bytes, idxPtr)) - } return { payloadId, - sufficientlyFunded, messages, instructions, - messages, } } -export function parseWormholeRelayerResend( - bytes: Buffer -): RedeliveryByTxHashInstruction { - let idx = 0 - const payloadId = bytes.readUInt8(idx) - if (payloadId !== RelayerPayloadId.Redelivery) { - throw new Error( - `Expected Delivery payload type (${RelayerPayloadId.Redelivery}), found: ${payloadId}` +function parseMessageInfo(bytes: Buffer, idx: number): [MessageInfo, number] { + const payloadId = bytes.readUint8(idx) + idx += 1 + const payloadType = bytes.readUint8(idx) as MessageInfoType + idx += 1 + if (payloadType == MessageInfoType.EMITTER_SEQUENCE) { + const emitterAddress = bytes.slice(idx, idx + 32) + idx += 32 + const sequence = ethers.BigNumber.from( + Uint8Array.prototype.subarray.call(bytes, idx, idx + 8) ) - } - idx += 1 - - const sourceChain = bytes.readUInt16BE(idx) - idx += 2 - - const sourceTxHash = bytes.slice(idx, idx + 32) - idx += 32 - - const deliveryVAASequence = BigNumber.from(bytes.slice(idx, idx + 8)).toNumber() - idx += 8 - - const targetChain = bytes.readUInt16BE(idx) - idx += 2 - - const multisendIndex = bytes.readUint8(idx) - idx += 1 - - const newMaximumRefundTarget = BigNumber.from(bytes.slice(idx, idx + 32)) - idx += 32 - - const newReceiverValueTarget = BigNumber.from(bytes.slice(idx, idx + 32)) - idx += 32 - - const executionParameters = parseWormholeRelayerExecutionParameters(bytes, idx) - idx += 37 - return { - payloadId, - sourceChain, - sourceTxHash, - deliveryVAASequence, - targetChain, - multisendIndex, - newMaximumRefundTarget, - newReceiverValueTarget, - executionParameters, + idx += 8 + return [ + { + payloadType, + emitterAddress, + sequence, + }, + idx, + ] + } else if (payloadType == MessageInfoType.VAAHASH) { + const vaaHash = bytes.slice(idx, idx + 32) + idx += 32 + return [ + { + payloadType, + vaaHash, + }, + idx, + ] + } else { + throw new Error("Unexpected MessageInfo payload type") } } -function parseWormholeRelayerExecutionParameters(bytes: Buffer, idx: number = 0): ExecutionParameters { +function parseWormholeRelayerExecutionParameters( + bytes: Buffer, + idx: number +): [ExecutionParameters, number] { const version = bytes.readUInt8(idx) idx += 1 const gasLimit = bytes.readUint32BE(idx) idx += 4 const providerDeliveryAddress = bytes.slice(idx, idx + 32) idx += 32 - idxPtr[0] = idx - return { version, gasLimit, providerDeliveryAddress } + return [{ version, gasLimit, providerDeliveryAddress }, idx] } /*