Compiles w/o redelivery
This commit is contained in:
parent
d1d0abc226
commit
246bf7c387
|
@ -18,9 +18,7 @@ const DEVNET = [
|
||||||
|
|
||||||
const MAINNET: any[] = []
|
const MAINNET: any[] = []
|
||||||
|
|
||||||
type ENV = "mainnet" | "testnet"
|
export function getWormholeRelayerAddress(chainId: ChainId, env: Network): string {
|
||||||
|
|
||||||
export function getCoreRelayerAddressNative(chainId: ChainId, env: Network): string {
|
|
||||||
if (env == "TESTNET") {
|
if (env == "TESTNET") {
|
||||||
const address = TESTNET.find((x) => x.chainId == chainId)?.coreRelayerAddress
|
const address = TESTNET.find((x) => x.chainId == chainId)?.coreRelayerAddress
|
||||||
if (!address) {
|
if (!address) {
|
||||||
|
@ -44,12 +42,12 @@ export function getCoreRelayerAddressNative(chainId: ChainId, env: Network): str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCoreRelayer(
|
export function getWormholeRelayer(
|
||||||
chainId: ChainId,
|
chainId: ChainId,
|
||||||
env: Network,
|
env: Network,
|
||||||
provider: ethers.providers.Provider
|
provider: ethers.providers.Provider
|
||||||
): CoreRelayer {
|
): CoreRelayer {
|
||||||
const thisChainsRelayer = getCoreRelayerAddressNative(chainId, env)
|
const thisChainsRelayer = getWormholeRelayerAddress(chainId, env)
|
||||||
const contract = CoreRelayer__factory.connect(thisChainsRelayer, provider)
|
const contract = CoreRelayer__factory.connect(thisChainsRelayer, provider)
|
||||||
return contract
|
return contract
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<DeliveryTargetInfo[]> {
|
||||||
|
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<DeliveryTargetInfo[]> {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +1 @@
|
||||||
export * from "./status"
|
export * from "./main"
|
||||||
|
|
|
@ -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<number, ethers.providers.Provider>
|
||||||
|
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<DeliveryInfo > {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<number, ethers.providers.Provider>
|
|
||||||
targetChainBlockRanges?: Map<number, [ethers.providers.BlockTag, ethers.providers.BlockTag]>
|
|
||||||
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<DeliveryInfo | RedeliveryInfo> {
|
|
||||||
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<DeliveryTargetInfo[]> {
|
|
||||||
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<DeliveryTargetInfo[]> {
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,20 +3,25 @@ import { arrayify } from "ethers/lib/utils"
|
||||||
|
|
||||||
export enum RelayerPayloadId {
|
export enum RelayerPayloadId {
|
||||||
Delivery = 1,
|
Delivery = 1,
|
||||||
Redelivery = 2,
|
|
||||||
// DeliveryStatus = 3,
|
// 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 {
|
export interface DeliveryInstructionsContainer {
|
||||||
payloadId: number // 1
|
payloadId: number // 1
|
||||||
sufficientlyFunded: boolean
|
|
||||||
instructions: DeliveryInstruction[]
|
|
||||||
messages: MessageInfo[]
|
messages: MessageInfo[]
|
||||||
}
|
instructions: DeliveryInstruction[]
|
||||||
|
|
||||||
export enum MessageInfoType {
|
|
||||||
EmitterSequence = 0,
|
|
||||||
VaaHash = 1,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageInfo {
|
export interface MessageInfo {
|
||||||
|
@ -41,19 +46,12 @@ export interface ExecutionParameters {
|
||||||
providerDeliveryAddress: Buffer
|
providerDeliveryAddress: Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RedeliveryByTxHashInstruction {
|
export enum MessageInfoType {
|
||||||
payloadId: number //2
|
EMITTER_SEQUENCE = 0,
|
||||||
sourceChain: number
|
VAAHASH = 1,
|
||||||
sourceTxHash: Buffer
|
|
||||||
deliveryVaaSequence: BigNumber
|
|
||||||
targetChain: number
|
|
||||||
multisendIndex: number
|
|
||||||
newMaximumRefundTarget: BigNumber
|
|
||||||
newReceiverValueTarget: BigNumber
|
|
||||||
executionParameters: ExecutionParameters
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parsePayloadType(
|
export function parseWormholeRelayerPayloadType(
|
||||||
stringPayload: string | Buffer | Uint8Array
|
stringPayload: string | Buffer | Uint8Array
|
||||||
): RelayerPayloadId {
|
): RelayerPayloadId {
|
||||||
const payload =
|
const payload =
|
||||||
|
@ -64,9 +62,7 @@ export function parsePayloadType(
|
||||||
return payload[0]
|
return payload[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseDeliveryInstructionsContainer(
|
export function parseWormholeRelayerSend(bytes: Buffer): DeliveryInstructionsContainer {
|
||||||
bytes: Buffer
|
|
||||||
): DeliveryInstructionsContainer {
|
|
||||||
let idx = 0
|
let idx = 0
|
||||||
const payloadId = bytes.readUInt8(idx)
|
const payloadId = bytes.readUInt8(idx)
|
||||||
if (payloadId !== RelayerPayloadId.Delivery) {
|
if (payloadId !== RelayerPayloadId.Delivery) {
|
||||||
|
@ -76,11 +72,19 @@ export function parseDeliveryInstructionsContainer(
|
||||||
}
|
}
|
||||||
idx += 1
|
idx += 1
|
||||||
|
|
||||||
const sufficientlyFunded = Boolean(bytes.readUInt8(idx))
|
const numMessages = bytes.readUInt8(idx)
|
||||||
idx += 1
|
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)
|
const numInstructions = bytes.readUInt8(idx)
|
||||||
idx += 1
|
idx += 1
|
||||||
|
|
||||||
let instructions = [] as DeliveryInstruction[]
|
let instructions = [] as DeliveryInstruction[]
|
||||||
for (let i = 0; i < numInstructions; ++i) {
|
for (let i = 0; i < numInstructions; ++i) {
|
||||||
const targetChain = bytes.readUInt16BE(idx)
|
const targetChain = bytes.readUInt16BE(idx)
|
||||||
|
@ -97,8 +101,9 @@ export function parseDeliveryInstructionsContainer(
|
||||||
Uint8Array.prototype.subarray.call(bytes, idx, idx + 32)
|
Uint8Array.prototype.subarray.call(bytes, idx, idx + 32)
|
||||||
)
|
)
|
||||||
idx += 32
|
idx += 32
|
||||||
const executionParameters = parseExecutionParameters(bytes, [idx])
|
let res = parseWormholeRelayerExecutionParameters(bytes, idx)
|
||||||
idx += 37
|
const executionParameters = res[0]
|
||||||
|
idx = res[1]
|
||||||
instructions.push(
|
instructions.push(
|
||||||
// dumb typechain format
|
// 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 {
|
return {
|
||||||
payloadId,
|
payloadId,
|
||||||
sufficientlyFunded,
|
|
||||||
instructions,
|
|
||||||
messages,
|
messages,
|
||||||
|
instructions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseMessageInfo(bytes: Buffer, idxPtr: [number]): MessageInfo {
|
function parseMessageInfo(bytes: Buffer, idx: number): [MessageInfo, number] {
|
||||||
let idx = idxPtr[0]
|
const payloadId = bytes.readUint8(idx)
|
||||||
const payloadType = bytes.readUInt8(idx) as MessageInfoType
|
idx += 1
|
||||||
switch (payloadType) {
|
const payloadType = bytes.readUint8(idx) as MessageInfoType
|
||||||
case MessageInfoType.EmitterSequence:
|
idx += 1
|
||||||
const emitterAddress = bytes.slice(idx, idx + 32)
|
if (payloadType == MessageInfoType.EMITTER_SEQUENCE) {
|
||||||
idx += 32
|
const emitterAddress = bytes.slice(idx, idx + 32)
|
||||||
const sequence = ethers.BigNumber.from(
|
idx += 32
|
||||||
Uint8Array.prototype.subarray.call(bytes, idx, idx + 32)
|
const sequence = ethers.BigNumber.from(
|
||||||
)
|
Uint8Array.prototype.subarray.call(bytes, idx, idx + 8)
|
||||||
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}`
|
|
||||||
)
|
)
|
||||||
}
|
idx += 8
|
||||||
idx += 1
|
return [
|
||||||
|
{
|
||||||
const sourceChain = bytes.readUInt16BE(idx)
|
payloadType,
|
||||||
idx += 2
|
emitterAddress,
|
||||||
|
sequence,
|
||||||
const sourceTxHash = bytes.slice(idx, idx + 32)
|
},
|
||||||
idx += 32
|
idx,
|
||||||
|
]
|
||||||
const deliveryVaaSequence = BigNumber.from(bytes.slice(idx, idx + 8))
|
} else if (payloadType == MessageInfoType.VAAHASH) {
|
||||||
idx += 8
|
const vaaHash = bytes.slice(idx, idx + 32)
|
||||||
|
idx += 32
|
||||||
const targetChain = bytes.readUInt16BE(idx)
|
return [
|
||||||
idx += 2
|
{
|
||||||
|
payloadType,
|
||||||
const multisendIndex = bytes.readUint8(idx)
|
vaaHash,
|
||||||
idx += 1
|
},
|
||||||
|
idx,
|
||||||
// note: confirmed that BigNumber.from(<Buffer>) assumes big endian
|
]
|
||||||
const newMaximumRefundTarget = BigNumber.from(bytes.slice(idx, idx + 32))
|
} else {
|
||||||
idx += 32
|
throw new Error("Unexpected MessageInfo payload type")
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseExecutionParameters(
|
function parseWormholeRelayerExecutionParameters(
|
||||||
bytes: Buffer,
|
bytes: Buffer,
|
||||||
idxPtr: [number] = [0]
|
idx: number
|
||||||
): ExecutionParameters {
|
): [ExecutionParameters, number] {
|
||||||
let idx = idxPtr[0]
|
|
||||||
const version = bytes.readUInt8(idx)
|
const version = bytes.readUInt8(idx)
|
||||||
idx += 1
|
idx += 1
|
||||||
const gasLimit = bytes.readUint32BE(idx)
|
const gasLimit = bytes.readUint32BE(idx)
|
||||||
idx += 4
|
idx += 4
|
||||||
const providerDeliveryAddress = bytes.slice(idx, idx + 32)
|
const providerDeliveryAddress = bytes.slice(idx, idx + 32)
|
||||||
idx += 32
|
idx += 32
|
||||||
idxPtr[0] = idx
|
return [{ version, gasLimit, providerDeliveryAddress }, idx]
|
||||||
return { version, gasLimit, providerDeliveryAddress }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -3,39 +3,31 @@ import { Next } from "wormhole-relayer"
|
||||||
import {
|
import {
|
||||||
IDelivery,
|
IDelivery,
|
||||||
MessageInfoType,
|
MessageInfoType,
|
||||||
parseDeliveryInstructionsContainer,
|
|
||||||
parsePayloadType,
|
|
||||||
parseRedeliveryByTxHashInstruction,
|
|
||||||
RelayerPayloadId,
|
RelayerPayloadId,
|
||||||
RelayProvider__factory,
|
RelayProvider__factory,
|
||||||
|
parseWormholeRelayerPayloadType,
|
||||||
|
parseWormholeRelayerSend,
|
||||||
} from "../pkgs/sdk/src"
|
} from "../pkgs/sdk/src"
|
||||||
import { EVMChainId } from "@certusone/wormhole-sdk"
|
import { EVMChainId } from "@certusone/wormhole-sdk"
|
||||||
import { GRContext } from "./app"
|
import { GRContext } from "./app"
|
||||||
|
|
||||||
export async function processGenericRelayerVaa(ctx: GRContext, next: Next) {
|
export async function processGenericRelayerVaa(ctx: GRContext, next: Next) {
|
||||||
const payloadId = parsePayloadType(ctx.vaa!.payload)
|
const payloadId = parseWormholeRelayerPayloadType(ctx.vaa!.payload)
|
||||||
// route payload types
|
// route payload types
|
||||||
switch (payloadId) {
|
if (payloadId != RelayerPayloadId.Delivery) {
|
||||||
case RelayerPayloadId.Delivery:
|
ctx.logger.error(`Expected GR Delivery payload type, found ${payloadId}`)
|
||||||
await processDelivery(ctx)
|
throw new Error("Expected GR Delivery payload type")
|
||||||
break
|
|
||||||
case RelayerPayloadId.Redelivery:
|
|
||||||
await processRedelivery(ctx)
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
await processDelivery(ctx)
|
||||||
await next()
|
await next()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processDelivery(ctx: GRContext) {
|
async function processDelivery(ctx: GRContext) {
|
||||||
const chainId = ctx.vaa!.emitterChain as wh.EVMChainId
|
const chainId = ctx.vaa!.emitterChain as wh.EVMChainId
|
||||||
const payload = parseDeliveryInstructionsContainer(ctx.vaa!.payload)
|
const payload = parseWormholeRelayerSend(ctx.vaa!.payload)
|
||||||
if (payload.sufficientlyFunded) {
|
|
||||||
ctx.logger.info("Insufficiently funded delivery request, skipping")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
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`)
|
throw new Error(`Only supports EmitterSequence MessageInfoType`)
|
||||||
}
|
}
|
||||||
|
@ -66,7 +58,8 @@ async function processDelivery(ctx: GRContext) {
|
||||||
relayerRefundAddress: wallet.address,
|
relayerRefundAddress: wallet.address,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await relayProvider.approvedSender(wallet.address))) {
|
const isApprovedSender = await relayProvider.approvedSender(wallet.address)
|
||||||
|
if (!isApprovedSender) {
|
||||||
ctx.logger.warn(
|
ctx.logger.warn(
|
||||||
`Approved sender not set correctly for chain ${chainId}, should be ${wallet.address}`
|
`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}`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -18,9 +18,7 @@ const DEVNET = [
|
||||||
|
|
||||||
const MAINNET: any[] = []
|
const MAINNET: any[] = []
|
||||||
|
|
||||||
type ENV = "mainnet" | "testnet"
|
export function getWormholeRelayerAddress(chainId: ChainId, env: Network): string {
|
||||||
|
|
||||||
export function getCoreRelayerAddressNative(chainId: ChainId, env: Network): string {
|
|
||||||
if (env == "TESTNET") {
|
if (env == "TESTNET") {
|
||||||
const address = TESTNET.find((x) => x.chainId == chainId)?.coreRelayerAddress
|
const address = TESTNET.find((x) => x.chainId == chainId)?.coreRelayerAddress
|
||||||
if (!address) {
|
if (!address) {
|
||||||
|
@ -44,12 +42,12 @@ export function getCoreRelayerAddressNative(chainId: ChainId, env: Network): str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCoreRelayer(
|
export function getWormholeRelayer(
|
||||||
chainId: ChainId,
|
chainId: ChainId,
|
||||||
env: Network,
|
env: Network,
|
||||||
provider: ethers.providers.Provider
|
provider: ethers.providers.Provider
|
||||||
): CoreRelayer {
|
): CoreRelayer {
|
||||||
const thisChainsRelayer = getCoreRelayerAddressNative(chainId, env)
|
const thisChainsRelayer = getWormholeRelayerAddress(chainId, env)
|
||||||
const contract = CoreRelayer__factory.connect(thisChainsRelayer, provider)
|
const contract = CoreRelayer__factory.connect(thisChainsRelayer, provider)
|
||||||
return contract
|
return contract
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<DeliveryTargetInfo[]> {
|
||||||
|
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<DeliveryTargetInfo[]> {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +1 @@
|
||||||
export * from "./status"
|
export * from "./main"
|
||||||
|
|
|
@ -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<number, ethers.providers.Provider>
|
||||||
|
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<DeliveryInfo > {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<number, ethers.providers.Provider>
|
|
||||||
targetChainBlockRanges?: Map<number, [ethers.providers.BlockTag, ethers.providers.BlockTag]>
|
|
||||||
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<DeliveryInfo | RedeliveryInfo> {
|
|
||||||
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<DeliveryTargetInfo[]> {
|
|
||||||
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<DeliveryTargetInfo[]> {
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,21 +3,25 @@ import { arrayify } from "ethers/lib/utils"
|
||||||
|
|
||||||
export enum RelayerPayloadId {
|
export enum RelayerPayloadId {
|
||||||
Delivery = 1,
|
Delivery = 1,
|
||||||
Redelivery = 2,
|
|
||||||
// DeliveryStatus = 3,
|
// 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 {
|
export interface DeliveryInstructionsContainer {
|
||||||
payloadId: number // 1
|
payloadId: number // 1
|
||||||
sufficientlyFunded: boolean
|
|
||||||
messages: MessageInfo[]
|
messages: MessageInfo[]
|
||||||
instructions: DeliveryInstruction[]
|
instructions: DeliveryInstruction[]
|
||||||
messages: MessageInfo[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum MessageInfoType {
|
|
||||||
EmitterSequence = 0,
|
|
||||||
VaaHash = 1,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageInfo {
|
export interface MessageInfo {
|
||||||
|
@ -42,28 +46,12 @@ export interface ExecutionParameters {
|
||||||
providerDeliveryAddress: Buffer
|
providerDeliveryAddress: Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RedeliveryByTxHashInstruction {
|
export enum MessageInfoType {
|
||||||
payloadId: number //2
|
EMITTER_SEQUENCE = 0,
|
||||||
sourceChain: number
|
VAAHASH = 1,
|
||||||
sourceTxHash: Buffer
|
|
||||||
deliveryVaaSequence: BigNumber
|
|
||||||
targetChain: number
|
|
||||||
multisendIndex: number
|
|
||||||
newMaximumRefundTarget: BigNumber
|
|
||||||
newReceiverValueTarget: BigNumber
|
|
||||||
executionParameters: ExecutionParameters
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MessageInfoType {EMITTER_SEQUENCE, VAAHASH}
|
export function parseWormholeRelayerPayloadType(
|
||||||
|
|
||||||
export interface MessageInfo {
|
|
||||||
infoType: MessageInfoType,
|
|
||||||
emitterAddress: Buffer,
|
|
||||||
sequence: number,
|
|
||||||
vaaHash: Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parsePayloadType(
|
|
||||||
stringPayload: string | Buffer | Uint8Array
|
stringPayload: string | Buffer | Uint8Array
|
||||||
): RelayerPayloadId {
|
): RelayerPayloadId {
|
||||||
const payload =
|
const payload =
|
||||||
|
@ -74,9 +62,7 @@ export function parsePayloadType(
|
||||||
return payload[0]
|
return payload[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseDeliveryInstructionsContainer(
|
export function parseWormholeRelayerSend(bytes: Buffer): DeliveryInstructionsContainer {
|
||||||
bytes: Buffer
|
|
||||||
): DeliveryInstructionsContainer {
|
|
||||||
let idx = 0
|
let idx = 0
|
||||||
const payloadId = bytes.readUInt8(idx)
|
const payloadId = bytes.readUInt8(idx)
|
||||||
if (payloadId !== RelayerPayloadId.Delivery) {
|
if (payloadId !== RelayerPayloadId.Delivery) {
|
||||||
|
@ -86,41 +72,19 @@ export function parseDeliveryInstructionsContainer(
|
||||||
}
|
}
|
||||||
idx += 1
|
idx += 1
|
||||||
|
|
||||||
const sufficientlyFunded = Boolean(bytes.readUInt8(idx))
|
|
||||||
idx += 1
|
|
||||||
|
|
||||||
const numMessages = bytes.readUInt8(idx)
|
const numMessages = bytes.readUInt8(idx)
|
||||||
idx += 1
|
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)
|
const numInstructions = bytes.readUInt8(idx)
|
||||||
idx += 1
|
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[]
|
let instructions = [] as DeliveryInstruction[]
|
||||||
for (let i = 0; i < numInstructions; ++i) {
|
for (let i = 0; i < numInstructions; ++i) {
|
||||||
const targetChain = bytes.readUInt16BE(idx)
|
const targetChain = bytes.readUInt16BE(idx)
|
||||||
|
@ -137,8 +101,9 @@ export function parseDeliveryInstructionsContainer(
|
||||||
Uint8Array.prototype.subarray.call(bytes, idx, idx + 32)
|
Uint8Array.prototype.subarray.call(bytes, idx, idx + 32)
|
||||||
)
|
)
|
||||||
idx += 32
|
idx += 32
|
||||||
const executionParameters = parseExecutionParameters(bytes, [idx])
|
let res = parseWormholeRelayerExecutionParameters(bytes, idx)
|
||||||
idx += 37
|
const executionParameters = res[0]
|
||||||
|
idx = res[1]
|
||||||
instructions.push(
|
instructions.push(
|
||||||
// dumb typechain format
|
// 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 {
|
return {
|
||||||
payloadId,
|
payloadId,
|
||||||
sufficientlyFunded,
|
|
||||||
messages,
|
messages,
|
||||||
instructions,
|
instructions,
|
||||||
messages,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseMessageInfo(bytes: Buffer, idxPtr: [number]): MessageInfo {
|
function parseMessageInfo(bytes: Buffer, idx: number): [MessageInfo, number] {
|
||||||
let idx = idxPtr[0]
|
const payloadId = bytes.readUint8(idx)
|
||||||
const payloadType = bytes.readUInt8(idx) as MessageInfoType
|
idx += 1
|
||||||
switch (payloadType) {
|
const payloadType = bytes.readUint8(idx) as MessageInfoType
|
||||||
case MessageInfoType.EmitterSequence:
|
idx += 1
|
||||||
const emitterAddress = bytes.slice(idx, idx + 32)
|
if (payloadType == MessageInfoType.EMITTER_SEQUENCE) {
|
||||||
idx += 32
|
const emitterAddress = bytes.slice(idx, idx + 32)
|
||||||
const sequence = ethers.BigNumber.from(
|
idx += 32
|
||||||
Uint8Array.prototype.subarray.call(bytes, idx, idx + 32)
|
const sequence = ethers.BigNumber.from(
|
||||||
)
|
Uint8Array.prototype.subarray.call(bytes, idx, idx + 8)
|
||||||
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}`
|
|
||||||
)
|
)
|
||||||
}
|
idx += 8
|
||||||
idx += 1
|
return [
|
||||||
|
{
|
||||||
const sourceChain = bytes.readUInt16BE(idx)
|
payloadType,
|
||||||
idx += 2
|
emitterAddress,
|
||||||
|
sequence,
|
||||||
const sourceTxHash = bytes.slice(idx, idx + 32)
|
},
|
||||||
idx += 32
|
idx,
|
||||||
|
]
|
||||||
const deliveryVaaSequence = BigNumber.from(bytes.slice(idx, idx + 8))
|
} else if (payloadType == MessageInfoType.VAAHASH) {
|
||||||
idx += 8
|
const vaaHash = bytes.slice(idx, idx + 32)
|
||||||
|
idx += 32
|
||||||
const targetChain = bytes.readUInt16BE(idx)
|
return [
|
||||||
idx += 2
|
{
|
||||||
|
payloadType,
|
||||||
const multisendIndex = bytes.readUint8(idx)
|
vaaHash,
|
||||||
idx += 1
|
},
|
||||||
|
idx,
|
||||||
// note: confirmed that BigNumber.from(<Buffer>) assumes big endian
|
]
|
||||||
const newMaximumRefundTarget = BigNumber.from(bytes.slice(idx, idx + 32))
|
} else {
|
||||||
idx += 32
|
throw new Error("Unexpected MessageInfo payload type")
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseExecutionParameters(
|
function parseWormholeRelayerExecutionParameters(
|
||||||
bytes: Buffer,
|
bytes: Buffer,
|
||||||
idxPtr: [number] = [0]
|
idx: number
|
||||||
): ExecutionParameters {
|
): [ExecutionParameters, number] {
|
||||||
let idx = idxPtr[0]
|
|
||||||
const version = bytes.readUInt8(idx)
|
const version = bytes.readUInt8(idx)
|
||||||
idx += 1
|
idx += 1
|
||||||
const gasLimit = bytes.readUint32BE(idx)
|
const gasLimit = bytes.readUint32BE(idx)
|
||||||
idx += 4
|
idx += 4
|
||||||
const providerDeliveryAddress = bytes.slice(idx, idx + 32)
|
const providerDeliveryAddress = bytes.slice(idx, idx + 32)
|
||||||
idx += 32
|
idx += 32
|
||||||
idxPtr[0] = idx
|
return [{ version, gasLimit, providerDeliveryAddress }, idx]
|
||||||
return { version, gasLimit, providerDeliveryAddress }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -1,35 +1,18 @@
|
||||||
import {
|
import {
|
||||||
ChainId,
|
ChainId,
|
||||||
CHAIN_ID_TO_NAME,
|
CHAIN_ID_TO_NAME,
|
||||||
CHAINS,
|
|
||||||
isChain,
|
|
||||||
CONTRACTS,
|
|
||||||
getSignedVAAWithRetry,
|
|
||||||
Network,
|
Network,
|
||||||
parseVaa,
|
|
||||||
ParsedVaa,
|
|
||||||
tryNativeToHexString,
|
tryNativeToHexString,
|
||||||
} from "@certusone/wormhole-sdk"
|
} 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 { Implementation__factory } from "@certusone/wormhole-sdk/lib/cjs/ethers-contracts"
|
||||||
import { BigNumber, ContractReceipt, ethers, providers } from "ethers"
|
import { BigNumber, ContractReceipt, ethers } from "ethers"
|
||||||
import {
|
import { getWormholeRelayer, RPCS_BY_CHAIN } from "../consts"
|
||||||
getWormholeRelayer,
|
|
||||||
getWormholeRelayerAddress,
|
|
||||||
RPCS_BY_CHAIN,
|
|
||||||
GUARDIAN_RPC_HOSTS,
|
|
||||||
} from "../consts"
|
|
||||||
import {
|
import {
|
||||||
parseWormholeRelayerPayloadType,
|
parseWormholeRelayerPayloadType,
|
||||||
RelayerPayloadId,
|
RelayerPayloadId,
|
||||||
parseWormholeRelayerSend,
|
parseWormholeRelayerSend,
|
||||||
parseWormholeRelayerResend,
|
|
||||||
DeliveryInstruction,
|
|
||||||
DeliveryInstructionsContainer,
|
DeliveryInstructionsContainer,
|
||||||
RedeliveryByTxHashInstruction,
|
DeliveryStatus,
|
||||||
ExecutionParameters,
|
|
||||||
MessageInfoType,
|
|
||||||
DeliveryStatus
|
|
||||||
} from "../structs"
|
} from "../structs"
|
||||||
import { DeliveryEvent } from "../ethers-contracts/CoreRelayer"
|
import { DeliveryEvent } from "../ethers-contracts/CoreRelayer"
|
||||||
type DeliveryTargetInfo = {
|
type DeliveryTargetInfo = {
|
||||||
|
@ -40,10 +23,9 @@ type DeliveryTargetInfo = {
|
||||||
sourceVaaSequence: BigNumber | null
|
sourceVaaSequence: BigNumber | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function parseWormholeLog(log: ethers.providers.Log): {
|
export function parseWormholeLog(log: ethers.providers.Log): {
|
||||||
type: RelayerPayloadId
|
type: RelayerPayloadId
|
||||||
parsed: DeliveryInstructionsContainer | RedeliveryByTxHashInstruction | string
|
parsed: DeliveryInstructionsContainer | string
|
||||||
} {
|
} {
|
||||||
const abi = [
|
const abi = [
|
||||||
"event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel);",
|
"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)
|
const type = parseWormholeRelayerPayloadType(payload)
|
||||||
if (type == RelayerPayloadId.Delivery) {
|
if (type == RelayerPayloadId.Delivery) {
|
||||||
return { type, parsed: parseWormholeRelayerSend(payload) }
|
return { type, parsed: parseWormholeRelayerSend(payload) }
|
||||||
} else if (type == RelayerPayloadId.Redelivery) {
|
|
||||||
return { type, parsed: parseWormholeRelayerResend(payload) }
|
|
||||||
} else {
|
} 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(
|
||||||
export function getBlockRange(provider: ethers.providers.Provider, timestamp?: number): [ethers.providers.BlockTag, ethers.providers.BlockTag] {
|
provider: ethers.providers.Provider,
|
||||||
|
timestamp?: number
|
||||||
|
): [ethers.providers.BlockTag, ethers.providers.BlockTag] {
|
||||||
return [-2040, "latest"]
|
return [-2040, "latest"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,7 +120,7 @@ export function getWormholeRelayerLog(
|
||||||
receipt: ContractReceipt,
|
receipt: ContractReceipt,
|
||||||
bridgeAddress: string,
|
bridgeAddress: string,
|
||||||
emitterAddress: string,
|
emitterAddress: string,
|
||||||
index: number,
|
index: number
|
||||||
): { log: ethers.providers.Log; sequence: string } {
|
): { log: ethers.providers.Log; sequence: string } {
|
||||||
const bridgeLogs = receipt.logs.filter((l) => {
|
const bridgeLogs = receipt.logs.filter((l) => {
|
||||||
return l.address === bridgeAddress
|
return l.address === bridgeAddress
|
||||||
|
@ -158,10 +140,7 @@ export function getWormholeRelayerLog(
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const filtered = parsed.filter(
|
const filtered = parsed.filter((x) => x.emitterAddress == emitterAddress.toLowerCase())
|
||||||
(x) =>
|
|
||||||
x.emitterAddress == emitterAddress.toLowerCase()
|
|
||||||
)
|
|
||||||
|
|
||||||
if (filtered.length == 0) {
|
if (filtered.length == 0) {
|
||||||
throw Error("No CoreRelayer contract interactions found for this transaction.")
|
throw Error("No CoreRelayer contract interactions found for this transaction.")
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export * from "./status"
|
export * from "./main"
|
||||||
|
|
|
@ -1,213 +1,224 @@
|
||||||
import {
|
import {
|
||||||
ChainId,
|
ChainId,
|
||||||
CHAIN_ID_TO_NAME,
|
CHAIN_ID_TO_NAME,
|
||||||
isChain,
|
isChain,
|
||||||
CONTRACTS,
|
CONTRACTS,
|
||||||
Network,
|
tryNativeToHexString,
|
||||||
tryNativeToHexString,
|
Network,
|
||||||
} from "@certusone/wormhole-sdk"
|
} from "@certusone/wormhole-sdk"
|
||||||
import { BigNumber, ethers, } from "ethers"
|
import { BigNumber, ethers } from "ethers"
|
||||||
import {
|
import { getWormholeRelayerAddress } from "../consts"
|
||||||
getWormholeRelayerAddress,
|
import {
|
||||||
} from "../consts"
|
RelayerPayloadId,
|
||||||
import {
|
DeliveryInstruction,
|
||||||
RelayerPayloadId,
|
DeliveryInstructionsContainer,
|
||||||
DeliveryInstruction,
|
MessageInfoType,
|
||||||
DeliveryInstructionsContainer,
|
DeliveryStatus,
|
||||||
RedeliveryByTxHashInstruction,
|
} from "../structs"
|
||||||
MessageInfoType,
|
import {
|
||||||
DeliveryStatus
|
getDefaultProvider,
|
||||||
} from "../structs"
|
printChain,
|
||||||
import {
|
getWormholeRelayerLog,
|
||||||
getDefaultProvider,
|
parseWormholeLog,
|
||||||
printChain,
|
getBlockRange,
|
||||||
getWormholeRelayerLog,
|
getWormholeRelayerDeliveryEventsBySourceSequence,
|
||||||
parseWormholeLog,
|
} from "./helpers"
|
||||||
getBlockRange,
|
|
||||||
getWormholeRelayerDeliveryEventsBySourceSequence,
|
|
||||||
} from "./helpers"
|
|
||||||
|
|
||||||
type InfoRequest = {
|
|
||||||
environment: Network
|
|
||||||
sourceChain: ChainId
|
|
||||||
sourceTransaction: string
|
|
||||||
sourceChainProvider?: ethers.providers.Provider
|
|
||||||
targetChainProviders?: Map<number, ethers.providers.Provider>
|
|
||||||
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 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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export async function getWormholeRelayerInfo(
|
type InfoRequest = {
|
||||||
infoRequest: InfoRequest
|
environment: Network
|
||||||
): Promise<DeliveryInfo | RedeliveryInfo> {
|
sourceChain: ChainId
|
||||||
const sourceChainProvider =
|
sourceTransaction: string
|
||||||
infoRequest.sourceChainProvider || getDefaultProvider(infoRequest.environment, infoRequest.sourceChain);
|
sourceChainProvider?: ethers.providers.Provider
|
||||||
if (!sourceChainProvider)
|
targetChainProviders?: Map<number, ethers.providers.Provider>
|
||||||
throw Error(
|
targetChainBlockRanges?: Map<
|
||||||
"No default RPC for this chain; pass in your own provider (as sourceChainProvider)"
|
number,
|
||||||
)
|
[ethers.providers.BlockTag, ethers.providers.BlockTag]
|
||||||
const receipt = await sourceChainProvider.getTransactionReceipt(
|
>
|
||||||
infoRequest.sourceTransaction
|
coreRelayerWhMessageIndex?: number
|
||||||
)
|
}
|
||||||
if (!receipt) throw Error("Transaction has not been mined")
|
|
||||||
const bridgeAddress =
|
export type DeliveryInfo = {
|
||||||
CONTRACTS[infoRequest.environment][CHAIN_ID_TO_NAME[infoRequest.sourceChain]].core
|
type: RelayerPayloadId.Delivery
|
||||||
const coreRelayerAddress = getWormholeRelayerAddress(
|
sourceChainId: ChainId
|
||||||
infoRequest.sourceChain,
|
sourceTransactionHash: string
|
||||||
infoRequest.environment
|
sourceDeliverySequenceNumber: number
|
||||||
)
|
deliveryInstructionsContainer: DeliveryInstructionsContainer
|
||||||
if (!bridgeAddress || !coreRelayerAddress) {
|
targetChainStatuses: {
|
||||||
throw Error(`Invalid chain ID or network: Chain ID ${infoRequest.sourceChain}, ${infoRequest.environment}`)
|
chainId: ChainId
|
||||||
}
|
events: { status: DeliveryStatus | string; transactionHash: string | null }[]
|
||||||
|
}[]
|
||||||
const deliveryLog = getWormholeRelayerLog(
|
}
|
||||||
receipt,
|
|
||||||
bridgeAddress,
|
export function printWormholeRelayerInfo(info: DeliveryInfo) {
|
||||||
tryNativeToHexString(coreRelayerAddress, "ethereum"),
|
console.log(stringifyWormholeRelayerInfo(info))
|
||||||
infoRequest.coreRelayerWhMessageIndex ? infoRequest.coreRelayerWhMessageIndex : 0,
|
}
|
||||||
)
|
export function stringifyWormholeRelayerInfo(info: DeliveryInfo): string {
|
||||||
|
let stringifiedInfo = ""
|
||||||
const { type, parsed } = parseWormholeLog(deliveryLog.log)
|
if (info.type == RelayerPayloadId.Delivery) {
|
||||||
|
stringifiedInfo += `Found delivery request in transaction ${
|
||||||
if (type == RelayerPayloadId.Redelivery) {
|
info.sourceTransactionHash
|
||||||
const redeliveryInstruction = parsed as RedeliveryByTxHashInstruction
|
} on ${printChain(info.sourceChainId)}\n`
|
||||||
return {
|
|
||||||
type,
|
const numMsgs = info.deliveryInstructionsContainer.messages.length
|
||||||
redeliverySourceChainId: infoRequest.sourceChain,
|
stringifiedInfo += `\nThe following ${numMsgs} messages were requested to be relayed:\n`
|
||||||
redeliverySourceTransactionHash: infoRequest.sourceTransaction,
|
stringifiedInfo += info.deliveryInstructionsContainer.messages.map((msgInfo, i) => {
|
||||||
redeliveryInstruction,
|
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 length = info.deliveryInstructionsContainer.instructions.length
|
||||||
|
stringifiedInfo += `\nMessages were requested to be sent to ${length} destination${
|
||||||
const targetChainStatuses = await Promise.all(deliveryInstructionsContainer.instructions.map(async (instruction: DeliveryInstruction) => {
|
length == 1 ? "" : "s"
|
||||||
const targetChain = instruction.targetChain as ChainId;
|
}:\n`
|
||||||
if(!isChain(targetChain)) throw Error(`Invalid Chain: ${targetChain}`)
|
stringifiedInfo +=
|
||||||
const targetChainProvider =
|
info.deliveryInstructionsContainer.instructions
|
||||||
infoRequest.targetChainProviders?.get(targetChain) ||
|
.map((instruction: DeliveryInstruction, i) => {
|
||||||
getDefaultProvider(infoRequest.environment, targetChain)
|
let result = ""
|
||||||
|
const targetChainName = CHAIN_ID_TO_NAME[instruction.targetChain as ChainId]
|
||||||
if (!targetChainProvider)
|
result += `\n(Destination ${i}): Target address is 0x${instruction.targetAddress.toString(
|
||||||
throw Error(
|
"hex"
|
||||||
"No default RPC for this chain; pass in your own provider (as targetChainProvider)"
|
)} on ${printChain(instruction.targetChain)}\n`
|
||||||
)
|
result += `Max amount to use for gas: ${instruction.maximumRefundTarget} of ${targetChainName} currency\n`
|
||||||
|
result += instruction.receiverValueTarget.gt(0)
|
||||||
const sourceChainBlock = await sourceChainProvider.getBlock(receipt.blockNumber);
|
? `Amount to pass into target address: ${
|
||||||
const [blockStartNumber, blockEndNumber] = infoRequest.targetChainBlockRanges?.get(targetChain) || getBlockRange(targetChainProvider, sourceChainBlock.timestamp);
|
instruction.receiverValueTarget
|
||||||
|
} of ${CHAIN_ID_TO_NAME[instruction.targetChain as ChainId]} currency\n`
|
||||||
const deliveryEvents = await getWormholeRelayerDeliveryEventsBySourceSequence(
|
: ``
|
||||||
infoRequest.environment,
|
result += `Gas limit: ${instruction.executionParameters.gasLimit} ${targetChainName} gas\n`
|
||||||
targetChain,
|
result += `Relay Provider Delivery Address: 0x${instruction.executionParameters.providerDeliveryAddress.toString(
|
||||||
targetChainProvider,
|
"hex"
|
||||||
infoRequest.sourceChain,
|
)}\n`
|
||||||
BigNumber.from(deliveryLog.sequence),
|
result += info.targetChainStatuses[i].events
|
||||||
blockStartNumber,
|
.map(
|
||||||
blockEndNumber
|
(e, i) =>
|
||||||
)
|
`Delivery attempt ${i + 1}: ${e.status}${
|
||||||
if (deliveryEvents.length == 0) {
|
e.transactionHash
|
||||||
let status = `Delivery didn't happen on ${printChain(targetChain)} within blocks ${blockStartNumber} to ${blockEndNumber}.`;
|
? ` (${targetChainName} transaction hash: ${e.transactionHash})`
|
||||||
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()})`
|
.join("\n")
|
||||||
} catch(e) {
|
return result
|
||||||
|
|
||||||
}
|
|
||||||
deliveryEvents.push({
|
|
||||||
status,
|
|
||||||
deliveryTxHash: null,
|
|
||||||
vaaHash: null,
|
|
||||||
sourceChain: infoRequest.sourceChain,
|
|
||||||
sourceVaaSequence: BigNumber.from(deliveryLog.sequence),
|
|
||||||
})
|
})
|
||||||
}
|
.join("\n") + "\n"
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return stringifiedInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWormholeRelayerInfo(
|
||||||
|
infoRequest: InfoRequest
|
||||||
|
): Promise<DeliveryInfo > {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { arrayify } from "ethers/lib/utils"
|
||||||
|
|
||||||
export enum RelayerPayloadId {
|
export enum RelayerPayloadId {
|
||||||
Delivery = 1,
|
Delivery = 1,
|
||||||
Redelivery = 2,
|
|
||||||
// DeliveryStatus = 3,
|
// DeliveryStatus = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,18 +18,10 @@ export enum DeliveryStatus {
|
||||||
DeliveryDidntHappenWithinRange = "Delivery didn't happen within given block range",
|
DeliveryDidntHappenWithinRange = "Delivery didn't happen within given block range",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface DeliveryInstructionsContainer {
|
export interface DeliveryInstructionsContainer {
|
||||||
payloadId: number // 1
|
payloadId: number // 1
|
||||||
sufficientlyFunded: boolean
|
|
||||||
messages: MessageInfo[]
|
messages: MessageInfo[]
|
||||||
instructions: DeliveryInstruction[]
|
instructions: DeliveryInstruction[]
|
||||||
messages: MessageInfo[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum MessageInfoType {
|
|
||||||
EmitterSequence = 0,
|
|
||||||
VaaHash = 1,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageInfo {
|
export interface MessageInfo {
|
||||||
|
@ -55,25 +46,9 @@ export interface ExecutionParameters {
|
||||||
providerDeliveryAddress: Buffer
|
providerDeliveryAddress: Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RedeliveryByTxHashInstruction {
|
export enum MessageInfoType {
|
||||||
payloadId: number //2
|
EMITTER_SEQUENCE = 0,
|
||||||
sourceChain: number
|
VAAHASH = 1,
|
||||||
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 function parseWormholeRelayerPayloadType(
|
export function parseWormholeRelayerPayloadType(
|
||||||
|
@ -87,9 +62,7 @@ export function parseWormholeRelayerPayloadType(
|
||||||
return payload[0]
|
return payload[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseWormholeRelayerSend(
|
export function parseWormholeRelayerSend(bytes: Buffer): DeliveryInstructionsContainer {
|
||||||
bytes: Buffer
|
|
||||||
): DeliveryInstructionsContainer {
|
|
||||||
let idx = 0
|
let idx = 0
|
||||||
const payloadId = bytes.readUInt8(idx)
|
const payloadId = bytes.readUInt8(idx)
|
||||||
if (payloadId !== RelayerPayloadId.Delivery) {
|
if (payloadId !== RelayerPayloadId.Delivery) {
|
||||||
|
@ -99,41 +72,19 @@ export function parseWormholeRelayerSend(
|
||||||
}
|
}
|
||||||
idx += 1
|
idx += 1
|
||||||
|
|
||||||
const sufficientlyFunded = Boolean(bytes.readUInt8(idx))
|
|
||||||
idx += 1
|
|
||||||
|
|
||||||
const numMessages = bytes.readUInt8(idx)
|
const numMessages = bytes.readUInt8(idx)
|
||||||
idx += 1
|
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)
|
const numInstructions = bytes.readUInt8(idx)
|
||||||
idx += 1
|
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[]
|
let instructions = [] as DeliveryInstruction[]
|
||||||
for (let i = 0; i < numInstructions; ++i) {
|
for (let i = 0; i < numInstructions; ++i) {
|
||||||
const targetChain = bytes.readUInt16BE(idx)
|
const targetChain = bytes.readUInt16BE(idx)
|
||||||
|
@ -150,8 +101,9 @@ export function parseWormholeRelayerSend(
|
||||||
Uint8Array.prototype.subarray.call(bytes, idx, idx + 32)
|
Uint8Array.prototype.subarray.call(bytes, idx, idx + 32)
|
||||||
)
|
)
|
||||||
idx += 32
|
idx += 32
|
||||||
const executionParameters = parseWormholeRelayerExecutionParameters(bytes, idx)
|
let res = parseWormholeRelayerExecutionParameters(bytes, idx)
|
||||||
idx += 37
|
const executionParameters = res[0]
|
||||||
|
idx = res[1]
|
||||||
instructions.push(
|
instructions.push(
|
||||||
// dumb typechain format
|
// 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 {
|
return {
|
||||||
payloadId,
|
payloadId,
|
||||||
sufficientlyFunded,
|
|
||||||
messages,
|
messages,
|
||||||
instructions,
|
instructions,
|
||||||
messages,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseWormholeRelayerResend(
|
function parseMessageInfo(bytes: Buffer, idx: number): [MessageInfo, number] {
|
||||||
bytes: Buffer
|
const payloadId = bytes.readUint8(idx)
|
||||||
): RedeliveryByTxHashInstruction {
|
idx += 1
|
||||||
let idx = 0
|
const payloadType = bytes.readUint8(idx) as MessageInfoType
|
||||||
const payloadId = bytes.readUInt8(idx)
|
idx += 1
|
||||||
if (payloadId !== RelayerPayloadId.Redelivery) {
|
if (payloadType == MessageInfoType.EMITTER_SEQUENCE) {
|
||||||
throw new Error(
|
const emitterAddress = bytes.slice(idx, idx + 32)
|
||||||
`Expected Delivery payload type (${RelayerPayloadId.Redelivery}), found: ${payloadId}`
|
idx += 32
|
||||||
|
const sequence = ethers.BigNumber.from(
|
||||||
|
Uint8Array.prototype.subarray.call(bytes, idx, idx + 8)
|
||||||
)
|
)
|
||||||
}
|
idx += 8
|
||||||
idx += 1
|
return [
|
||||||
|
{
|
||||||
const sourceChain = bytes.readUInt16BE(idx)
|
payloadType,
|
||||||
idx += 2
|
emitterAddress,
|
||||||
|
sequence,
|
||||||
const sourceTxHash = bytes.slice(idx, idx + 32)
|
},
|
||||||
idx += 32
|
idx,
|
||||||
|
]
|
||||||
const deliveryVAASequence = BigNumber.from(bytes.slice(idx, idx + 8)).toNumber()
|
} else if (payloadType == MessageInfoType.VAAHASH) {
|
||||||
idx += 8
|
const vaaHash = bytes.slice(idx, idx + 32)
|
||||||
|
idx += 32
|
||||||
const targetChain = bytes.readUInt16BE(idx)
|
return [
|
||||||
idx += 2
|
{
|
||||||
|
payloadType,
|
||||||
const multisendIndex = bytes.readUint8(idx)
|
vaaHash,
|
||||||
idx += 1
|
},
|
||||||
|
idx,
|
||||||
const newMaximumRefundTarget = BigNumber.from(bytes.slice(idx, idx + 32))
|
]
|
||||||
idx += 32
|
} else {
|
||||||
|
throw new Error("Unexpected MessageInfo payload type")
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseWormholeRelayerExecutionParameters(bytes: Buffer, idx: number = 0): ExecutionParameters {
|
function parseWormholeRelayerExecutionParameters(
|
||||||
|
bytes: Buffer,
|
||||||
|
idx: number
|
||||||
|
): [ExecutionParameters, number] {
|
||||||
const version = bytes.readUInt8(idx)
|
const version = bytes.readUInt8(idx)
|
||||||
idx += 1
|
idx += 1
|
||||||
const gasLimit = bytes.readUint32BE(idx)
|
const gasLimit = bytes.readUint32BE(idx)
|
||||||
idx += 4
|
idx += 4
|
||||||
const providerDeliveryAddress = bytes.slice(idx, idx + 32)
|
const providerDeliveryAddress = bytes.slice(idx, idx + 32)
|
||||||
idx += 32
|
idx += 32
|
||||||
idxPtr[0] = idx
|
return [{ version, gasLimit, providerDeliveryAddress }, idx]
|
||||||
return { version, gasLimit, providerDeliveryAddress }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
Loading…
Reference in New Issue