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