From a1e9d15187434b3fc1062db0d221bd2000a29b7d Mon Sep 17 00:00:00 2001 From: Joe Howarth Date: Thu, 22 Jun 2023 09:51:22 -0500 Subject: [PATCH] Relayer: delivery helpers in ts sdk (#3107) * basic delivery helpers done * reorganize * sdk: include manual delivery in relayer tests * fix test * fix * SDK fixes --------- Co-authored-by: derpy-duck <115193320+derpy-duck@users.noreply.github.com> --- .../src/relayer/__tests__/wormhole_relayer.ts | 631 ++++++++++++------ sdk/js/src/relayer/consts.ts | 20 +- sdk/js/src/relayer/index.ts | 2 +- sdk/js/src/relayer/relayer/deliver.ts | 127 ++++ sdk/js/src/relayer/relayer/helpers.ts | 152 +++-- sdk/js/src/relayer/relayer/index.ts | 5 + sdk/js/src/relayer/relayer/info.ts | 306 +++++++++ sdk/js/src/relayer/relayer/relayer.ts | 508 -------------- sdk/js/src/relayer/relayer/resend.ts | 128 ++++ sdk/js/src/relayer/relayer/send.ts | 114 ++++ 10 files changed, 1226 insertions(+), 767 deletions(-) create mode 100644 sdk/js/src/relayer/relayer/deliver.ts create mode 100644 sdk/js/src/relayer/relayer/index.ts create mode 100644 sdk/js/src/relayer/relayer/info.ts delete mode 100644 sdk/js/src/relayer/relayer/relayer.ts create mode 100644 sdk/js/src/relayer/relayer/resend.ts create mode 100644 sdk/js/src/relayer/relayer/send.ts diff --git a/sdk/js/src/relayer/__tests__/wormhole_relayer.ts b/sdk/js/src/relayer/__tests__/wormhole_relayer.ts index 7e9f36886..67589e8d7 100644 --- a/sdk/js/src/relayer/__tests__/wormhole_relayer.ts +++ b/sdk/js/src/relayer/__tests__/wormhole_relayer.ts @@ -1,172 +1,275 @@ -import { afterAll, beforeEach, describe, expect, jest, test} from "@jest/globals"; -import { ethers } from "ethers"; -import { getNetwork, isCI, generateRandomString, waitForRelay, PRIVATE_KEY, getGuardianRPC, GUARDIAN_KEYS, GUARDIAN_SET_INDEX, GOVERNANCE_EMITTER_ADDRESS, getArbitraryBytes32} from "./utils/utils"; -import {getAddressInfo} from "../consts" -import {getDefaultProvider} from "../relayer/helpers" import { - relayer, - ethers_contracts, - tryNativeToUint8Array, - ChainId, - CHAINS, - CONTRACTS, - CHAIN_ID_TO_NAME, - ChainName, - Network, - } from "../../../"; - import {GovernanceEmitter, MockGuardians} from "../../../src/mock"; + afterAll, + beforeEach, + describe, + expect, + jest, + test, +} from "@jest/globals"; +import { ContractReceipt, ethers } from "ethers"; +import { + getNetwork, + isCI, + generateRandomString, + waitForRelay, + PRIVATE_KEY, + getGuardianRPC, + GUARDIAN_KEYS, + GUARDIAN_SET_INDEX, + GOVERNANCE_EMITTER_ADDRESS, + getArbitraryBytes32, +} from "./utils/utils"; +import { getAddressInfo } from "../consts"; +import { getDefaultProvider, getVAA } from "../relayer/helpers"; +import { + relayer, + ethers_contracts, + tryNativeToUint8Array, + ChainId, + CHAINS, + CONTRACTS, + CHAIN_ID_TO_NAME, + ChainName, + Network, + parseSequencesFromLogEth, +} from "../../../"; +import { GovernanceEmitter, MockGuardians } from "../../../src/mock"; import { AddressInfo } from "net"; +import { + Bridge__factory, + Implementation__factory, +} from "../../ethers-contracts"; +import { Wormhole__factory } from "../../../lib/cjs/ethers-contracts"; +import { getEmitterAddressEth } from "../../bridge"; +import { deliver } from "../relayer"; +import { env } from "process"; - const network: Network = getNetwork(); - const ci: boolean = isCI(); - - const sourceChain = network == 'DEVNET' ? "ethereum" : "avalanche"; - const targetChain = network == 'DEVNET' ? "bsc" : "celo"; +const network: Network = getNetwork(); +const ci: boolean = isCI(); - type TestChain = { - chainId: ChainId, - name: ChainName, - provider: ethers.providers.Provider, - wallet: ethers.Wallet, - wormholeRelayerAddress: string, - mockIntegrationAddress: string, - wormholeRelayer: ethers_contracts.WormholeRelayer, - mockIntegration: ethers_contracts.MockRelayerIntegration +const sourceChain = network == "DEVNET" ? "ethereum" : "avalanche"; +const targetChain = network == "DEVNET" ? "bsc" : "celo"; + +type TestChain = { + chainId: ChainId; + name: ChainName; + provider: ethers.providers.Provider; + wallet: ethers.Wallet; + wormholeRelayerAddress: string; + mockIntegrationAddress: string; + wormholeRelayer: ethers_contracts.WormholeRelayer; + mockIntegration: ethers_contracts.MockRelayerIntegration; +}; + +const createTestChain = (name: ChainName) => { + const provider = getDefaultProvider(network, name, ci); + const addressInfo = getAddressInfo(name, network); + if (process.env.DEV) { + // Via ir is off -> different wormhole relayer address + addressInfo.wormholeRelayerAddress = + "0x53855d4b64E9A3CF59A84bc768adA716B5536BC5"; } - - const createTestChain = (name: ChainName) => { - const provider = getDefaultProvider(network, name, ci); - const addressInfo = getAddressInfo(name, network); - if(process.env.DEV) { - // Via ir is off -> different wormhole relayer address - addressInfo.wormholeRelayerAddress = "0x53855d4b64E9A3CF59A84bc768adA716B5536BC5" - } - if(!addressInfo.wormholeRelayerAddress) throw Error(`No core relayer address for ${name}`); - if(!addressInfo.mockIntegrationAddress) throw Error(`No mock relayer integration address for ${name}`); - const wallet = new ethers.Wallet(PRIVATE_KEY, provider); - const wormholeRelayer = ethers_contracts.WormholeRelayer__factory.connect( - addressInfo.wormholeRelayerAddress, - wallet - ); - const mockIntegration = ethers_contracts.MockRelayerIntegration__factory.connect( + if (!addressInfo.wormholeRelayerAddress) + throw Error(`No core relayer address for ${name}`); + if (!addressInfo.mockIntegrationAddress) + throw Error(`No mock relayer integration address for ${name}`); + const wallet = new ethers.Wallet(PRIVATE_KEY, provider); + const wormholeRelayer = ethers_contracts.WormholeRelayer__factory.connect( + addressInfo.wormholeRelayerAddress, + wallet + ); + const mockIntegration = + ethers_contracts.MockRelayerIntegration__factory.connect( addressInfo.mockIntegrationAddress, wallet ); - const result: TestChain = { - chainId: CHAINS[name], - name, - provider, - wallet, - wormholeRelayerAddress: addressInfo.wormholeRelayerAddress, - mockIntegrationAddress: addressInfo.mockIntegrationAddress, - wormholeRelayer, - mockIntegration - } - return result; - } + const result: TestChain = { + chainId: CHAINS[name], + name, + provider, + wallet, + wormholeRelayerAddress: addressInfo.wormholeRelayerAddress, + mockIntegrationAddress: addressInfo.mockIntegrationAddress, + wormholeRelayer, + mockIntegration, + }; + return result; +}; - const source = createTestChain(sourceChain); - const target = createTestChain(targetChain); +const source = createTestChain(sourceChain); +const target = createTestChain(targetChain); const myMap = new Map(); myMap.set(sourceChain, source.provider); myMap.set(targetChain, target.provider); -const optionalParams = {environment: network, sourceChainProvider: source.provider, targetChainProviders: myMap}; +const optionalParams = { + environment: network, + sourceChainProvider: source.provider, + targetChainProviders: myMap, +}; // for signing wormhole messages const guardians = new MockGuardians(GUARDIAN_SET_INDEX, GUARDIAN_KEYS); - // for generating governance wormhole messages -const governance = new GovernanceEmitter( -GOVERNANCE_EMITTER_ADDRESS -); +const governance = new GovernanceEmitter(GOVERNANCE_EMITTER_ADDRESS); -const guardianIndices = ci?[0,1]:[0]; +const guardianIndices = ci ? [0, 1] : [0]; const REASONABLE_GAS_LIMIT = 500000; const TOO_LOW_GAS_LIMIT = 10000; const REASONABLE_GAS_LIMIT_FORWARDS = 900000; -const getStatus = async (txHash: string, _sourceChain?: ChainName): Promise => { +const getStatus = async ( + txHash: string, + _sourceChain?: ChainName +): Promise => { const info = (await relayer.getWormholeRelayerInfo( - _sourceChain || sourceChain, - txHash, - {environment: network, targetChainProviders: myMap, sourceChainProvider: myMap.get(_sourceChain || sourceChain)} - )) as relayer.DeliveryInfo; - return info.targetChainStatus.events[0].status; -} + _sourceChain || sourceChain, + txHash, + { + environment: network, + targetChainProviders: myMap, + sourceChainProvider: myMap.get(_sourceChain || sourceChain), + } + )) as relayer.DeliveryInfo; + return info.targetChainStatus.events[0].status; +}; -const testSend = async (payload: string, sendToSourceChain?: boolean, notEnoughValue?: boolean): Promise => { - const value = await relayer.getPrice(sourceChain, sendToSourceChain ? sourceChain : targetChain, notEnoughValue ? TOO_LOW_GAS_LIMIT : REASONABLE_GAS_LIMIT, optionalParams); - console.log(`Quoted gas delivery fee: ${value}`); - const tx = await source.mockIntegration.sendMessage( - payload, - sendToSourceChain ? source.chainId : target.chainId, - notEnoughValue ? TOO_LOW_GAS_LIMIT : REASONABLE_GAS_LIMIT, - 0, - { value, gasLimit: REASONABLE_GAS_LIMIT } - ); - console.log("Sent delivery request!"); - await tx.wait(); - console.log("Message confirmed!"); - - return tx.hash; -} +const testSend = async ( + payload: string, + sendToSourceChain?: boolean, + notEnoughValue?: boolean +): Promise => { + const value = await relayer.getPrice( + sourceChain, + sendToSourceChain ? sourceChain : targetChain, + notEnoughValue ? TOO_LOW_GAS_LIMIT : REASONABLE_GAS_LIMIT, + optionalParams + ); + console.log(`Quoted gas delivery fee: ${value}`); + const tx = await source.mockIntegration.sendMessage( + payload, + sendToSourceChain ? source.chainId : target.chainId, + notEnoughValue ? TOO_LOW_GAS_LIMIT : REASONABLE_GAS_LIMIT, + 0, + { value, gasLimit: REASONABLE_GAS_LIMIT } + ); + console.log("Sent delivery request!"); + await tx.wait(); + console.log("Message confirmed!"); -const testForward = async (payload1: string, payload2: string, notEnoughExtraForwardingValue?: boolean): Promise => { - const valueNeededOnTargetChain = await relayer.getPrice(targetChain, sourceChain, notEnoughExtraForwardingValue ? TOO_LOW_GAS_LIMIT : REASONABLE_GAS_LIMIT, optionalParams); - const value = await relayer.getPrice(sourceChain, targetChain, REASONABLE_GAS_LIMIT_FORWARDS, {receiverValue: valueNeededOnTargetChain, ...optionalParams}); - console.log(`Quoted gas delivery fee: ${value}`); + return tx.wait(); +}; - const tx = await source.mockIntegration["sendMessageWithForwardedResponse(bytes,bytes,uint16,uint32,uint128)"]( - payload1, - payload2, - target.chainId, - REASONABLE_GAS_LIMIT_FORWARDS, - valueNeededOnTargetChain, - { value: value, gasLimit: REASONABLE_GAS_LIMIT } - ); - console.log("Sent delivery request!"); - await tx.wait(); - console.log("Message confirmed!"); +const testForward = async ( + payload1: string, + payload2: string, + notEnoughExtraForwardingValue?: boolean +): Promise => { + const valueNeededOnTargetChain = await relayer.getPrice( + targetChain, + sourceChain, + notEnoughExtraForwardingValue ? TOO_LOW_GAS_LIMIT : REASONABLE_GAS_LIMIT, + optionalParams + ); + const value = await relayer.getPrice( + sourceChain, + targetChain, + REASONABLE_GAS_LIMIT_FORWARDS, + { receiverValue: valueNeededOnTargetChain, ...optionalParams } + ); + console.log(`Quoted gas delivery fee: ${value}`); - return tx.hash -} + const tx = await source.mockIntegration[ + "sendMessageWithForwardedResponse(bytes,bytes,uint16,uint32,uint128)" + ]( + payload1, + payload2, + target.chainId, + REASONABLE_GAS_LIMIT_FORWARDS, + valueNeededOnTargetChain, + { value: value, gasLimit: REASONABLE_GAS_LIMIT } + ); + console.log("Sent delivery request!"); + await tx.wait(); + console.log("Message confirmed!"); + + return tx.wait(); +}; describe("Wormhole Relayer Tests", () => { - test("Executes a Delivery Success", async () => { - const arbitraryPayload = getArbitraryBytes32() + const arbitraryPayload = getArbitraryBytes32(); console.log(`Sent message: ${arbitraryPayload}`); - - const txHash = await testSend(arbitraryPayload); + + const rx = await testSend(arbitraryPayload); await waitForRelay(); console.log("Checking status using SDK"); - const status = await getStatus(txHash); + const status = await getStatus(rx.transactionHash); expect(status).toBe("Delivery Success"); console.log("Checking if message was relayed"); const message = await target.mockIntegration.getMessage(); expect(message).toBe(arbitraryPayload); + }); + test("Executes a Delivery Success with manual delivery", async () => { + const arbitraryPayload = getArbitraryBytes32(); + console.log(`Sent message: ${arbitraryPayload}`); + const deliverySeq = await Implementation__factory.connect( + CONTRACTS[network][sourceChain].core || "", + source.provider + ).nextSequence(source.wormholeRelayerAddress); + + console.log(`Got delivery seq: ${deliverySeq}`); + const rx = await testSend(arbitraryPayload); + + await sleep(1000); + + const deliveryVaa = await getVAA(getGuardianRPC(network, ci), { + emitterAddress: Buffer.from( + tryNativeToUint8Array(source.wormholeRelayerAddress, "ethereum") + ), + chainId: source.chainId, + sequence: deliverySeq, + }, true); + + console.log(`Got delivery VAA: ${deliveryVaa}`); + const deliveryRx = await deliver( + deliveryVaa, + target.wallet, + getGuardianRPC(network, ci), + network + ); + console.log("Manual delivery tx hash", deliveryRx.transactionHash); + console.log("Manual delivery tx status", deliveryRx.status); + + console.log("Checking status using SDK"); + const status = await getStatus(rx.transactionHash); + expect(status).toBe("Delivery Success"); + + console.log("Checking if message was relayed"); + const message = await target.mockIntegration.getMessage(); + expect(message).toBe(arbitraryPayload); }); test("Executes a Forward Request Success", async () => { - const arbitraryPayload1 = getArbitraryBytes32() - const arbitraryPayload2 = getArbitraryBytes32() - console.log(`Sent message: ${arbitraryPayload1}, expecting ${arbitraryPayload2} to be forwarded`); - - const txHash = await testForward(arbitraryPayload1, arbitraryPayload2); + const arbitraryPayload1 = getArbitraryBytes32(); + const arbitraryPayload2 = getArbitraryBytes32(); + console.log( + `Sent message: ${arbitraryPayload1}, expecting ${arbitraryPayload2} to be forwarded` + ); + + const rx = await testForward(arbitraryPayload1, arbitraryPayload2); await waitForRelay(2); - console.log("Checking status using SDK"); - const status = await getStatus(txHash); + const status = await getStatus(rx.transactionHash); expect(status).toBe("Forward Request Success"); console.log("Checking if message was relayed"); @@ -176,29 +279,47 @@ describe("Wormhole Relayer Tests", () => { console.log("Checking if forward message was relayed back"); const message2 = await source.mockIntegration.getMessage(); expect(message2).toBe(arbitraryPayload2); - - }); test("Executes multiple forwards", async () => { - const arbitraryPayload1 = getArbitraryBytes32() - const arbitraryPayload2 = getArbitraryBytes32() - console.log(`Sent message: ${arbitraryPayload1}, expecting ${arbitraryPayload2} to be forwarded`); - const valueNeededOnTargetChain1 = await relayer.getPrice(targetChain, sourceChain, REASONABLE_GAS_LIMIT, optionalParams); - const valueNeededOnTargetChain2 = await relayer.getPrice(targetChain, targetChain, REASONABLE_GAS_LIMIT, optionalParams); + const arbitraryPayload1 = getArbitraryBytes32(); + const arbitraryPayload2 = getArbitraryBytes32(); + console.log( + `Sent message: ${arbitraryPayload1}, expecting ${arbitraryPayload2} to be forwarded` + ); + const valueNeededOnTargetChain1 = await relayer.getPrice( + targetChain, + sourceChain, + REASONABLE_GAS_LIMIT, + optionalParams + ); + const valueNeededOnTargetChain2 = await relayer.getPrice( + targetChain, + targetChain, + REASONABLE_GAS_LIMIT, + optionalParams + ); - const value = await relayer.getPrice(sourceChain, targetChain, REASONABLE_GAS_LIMIT_FORWARDS, {receiverValue: valueNeededOnTargetChain1.add(valueNeededOnTargetChain2), ...optionalParams}); + const value = await relayer.getPrice( + sourceChain, + targetChain, + REASONABLE_GAS_LIMIT_FORWARDS, + { + receiverValue: valueNeededOnTargetChain1.add(valueNeededOnTargetChain2), + ...optionalParams, + } + ); console.log(`Quoted gas delivery fee: ${value}`); - - const tx = await source.mockIntegration.sendMessageWithMultiForwardedResponse( - arbitraryPayload1, - arbitraryPayload2, - target.chainId, - REASONABLE_GAS_LIMIT_FORWARDS, - valueNeededOnTargetChain1.add(valueNeededOnTargetChain2), - { value: value, gasLimit: REASONABLE_GAS_LIMIT } - ); + const tx = + await source.mockIntegration.sendMessageWithMultiForwardedResponse( + arbitraryPayload1, + arbitraryPayload2, + target.chainId, + REASONABLE_GAS_LIMIT_FORWARDS, + valueNeededOnTargetChain1.add(valueNeededOnTargetChain2), + { value: value, gasLimit: REASONABLE_GAS_LIMIT } + ); console.log("Sent delivery request!"); await tx.wait(); console.log("Message confirmed!"); @@ -206,7 +327,7 @@ describe("Wormhole Relayer Tests", () => { await waitForRelay(2); const status = await getStatus(tx.hash); - console.log(`Status of forward: ${status}`) + console.log(`Status of forward: ${status}`); console.log("Checking if first forward was relayed"); const message1 = await source.mockIntegration.getMessage(); @@ -218,15 +339,17 @@ describe("Wormhole Relayer Tests", () => { }); test("Executes a Forward Request Failure", async () => { - const arbitraryPayload1 = getArbitraryBytes32() - const arbitraryPayload2 = getArbitraryBytes32() - console.log(`Sent message: ${arbitraryPayload1}, expecting ${arbitraryPayload2} to be forwarded (but should fail)`); - - const txHash = await testForward(arbitraryPayload1, arbitraryPayload2, true); + const arbitraryPayload1 = getArbitraryBytes32(); + const arbitraryPayload2 = getArbitraryBytes32(); + console.log( + `Sent message: ${arbitraryPayload1}, expecting ${arbitraryPayload2} to be forwarded (but should fail)` + ); + + const rx = await testForward(arbitraryPayload1, arbitraryPayload2, true); await waitForRelay(); - const status = await getStatus(txHash); + const status = await getStatus(rx.transactionHash); expect(status).toBe("Forward Request Failure"); console.log("Checking if message was relayed (it shouldn't have been!"); @@ -238,18 +361,20 @@ describe("Wormhole Relayer Tests", () => { ); const message2 = await source.mockIntegration.getMessage(); expect(message2).not.toBe(arbitraryPayload2); - - }); - test("Test getPrice in Typescript SDK", async () => { - const price = (await relayer.getPrice(sourceChain, targetChain, 200000, optionalParams)); + const price = await relayer.getPrice( + sourceChain, + targetChain, + 200000, + optionalParams + ); expect(price.toString()).toBe("165000000000000000"); }); test("Executes a delivery with a Cross Chain Refund", async () => { - const arbitraryPayload = getArbitraryBytes32() + const arbitraryPayload = getArbitraryBytes32(); console.log(`Sent message: ${arbitraryPayload}`); const value = await relayer.getPrice( sourceChain, @@ -267,8 +392,8 @@ describe("Wormhole Relayer Tests", () => { target.wormholeRelayerAddress, // This is an address that exists but doesn't implement the IWormhole interface, so should result in Receiver Failure Buffer.from("hi!"), REASONABLE_GAS_LIMIT, - {value, gasLimit: REASONABLE_GAS_LIMIT}, - optionalParams, + { value, gasLimit: REASONABLE_GAS_LIMIT }, + optionalParams ); console.log("Sent delivery request!"); await tx.wait(); @@ -281,7 +406,11 @@ describe("Wormhole Relayer Tests", () => { const status = await getStatus(tx.hash); expect(status).toBe("Receiver Failure"); - const info = (await relayer.getWormholeRelayerInfo(sourceChain, tx.hash, optionalParams)) as relayer.DeliveryInfo; + const info = (await relayer.getWormholeRelayerInfo( + sourceChain, + tx.hash, + optionalParams + )) as relayer.DeliveryInfo; await waitForRelay(); @@ -289,8 +418,11 @@ describe("Wormhole Relayer Tests", () => { console.log("Checking status of refund using SDK"); console.log(relayer.stringifyWormholeRelayerInfo(info)); - const statusOfRefund = await getStatus(info.targetChainStatus.events[0].transactionHash || "", targetChain); - expect(statusOfRefund).toBe("Delivery Success"); + const statusOfRefund = await getStatus( + info.targetChainStatus.events[0].transactionHash || "", + targetChain + ); + expect(statusOfRefund).toBe("Delivery Success"); console.log(`Quoted gas delivery fee: ${value}`); console.log( @@ -310,25 +442,25 @@ describe("Wormhole Relayer Tests", () => { }); test("Executes a Receiver Failure", async () => { - const arbitraryPayload = getArbitraryBytes32() + const arbitraryPayload = getArbitraryBytes32(); console.log(`Sent message: ${arbitraryPayload}`); - - const txHash = await testSend(arbitraryPayload, false, true); + + const rx = await testSend(arbitraryPayload, false, true); await waitForRelay(); const message = await target.mockIntegration.getMessage(); expect(message).not.toBe(arbitraryPayload); - const status = await getStatus(txHash); + const status = await getStatus(rx.transactionHash); expect(status).toBe("Receiver Failure"); }); test("Executes a receiver failure and then redelivery through SDK", async () => { - const arbitraryPayload = getArbitraryBytes32() + const arbitraryPayload = getArbitraryBytes32(); console.log(`Sent message: ${arbitraryPayload}`); - - const txHash = await testSend(arbitraryPayload, false, true); + + const rx = await testSend(arbitraryPayload, false, true); await waitForRelay(); @@ -336,12 +468,21 @@ describe("Wormhole Relayer Tests", () => { expect(message).not.toBe(arbitraryPayload); console.log("Checking status using SDK"); - const status = await getStatus(txHash); + const status = await getStatus(rx.transactionHash); expect(status).toBe("Receiver Failure"); - const value = await relayer.getPrice(sourceChain, targetChain, REASONABLE_GAS_LIMIT, optionalParams); + const value = await relayer.getPrice( + sourceChain, + targetChain, + REASONABLE_GAS_LIMIT, + optionalParams + ); - const info = (await relayer.getWormholeRelayerInfo(sourceChain, txHash, optionalParams)) as relayer.DeliveryInfo; + const info = (await relayer.getWormholeRelayerInfo( + sourceChain, + rx.transactionHash, + optionalParams + )) as relayer.DeliveryInfo; console.log("Redelivering message"); const redeliveryReceipt = await relayer.resend( @@ -381,80 +522,146 @@ describe("Wormhole Relayer Tests", () => { // GOVERNANCE TESTS test("Governance: Test Registering Chain", async () => { - const chain = 24; - const currentAddress = await source.wormholeRelayer.getRegisteredWormholeRelayerContract(chain); - console.log(`For Chain ${source.chainId}, registered chain ${chain} address: ${currentAddress}`); + const currentAddress = + await source.wormholeRelayer.getRegisteredWormholeRelayerContract(chain); + console.log( + `For Chain ${source.chainId}, registered chain ${chain} address: ${currentAddress}` + ); - const expectedNewRegisteredAddress = "0x0000000000000000000000001234567890123456789012345678901234567892"; + const expectedNewRegisteredAddress = + "0x0000000000000000000000001234567890123456789012345678901234567892"; - const timestamp = (await source.wallet.provider.getBlock("latest")).timestamp; - - const firstMessage = governance.publishWormholeRelayerRegisterChain(timestamp, chain, expectedNewRegisteredAddress) - const firstSignedVaa = guardians.addSignatures(firstMessage, guardianIndices); + const timestamp = (await source.wallet.provider.getBlock("latest")) + .timestamp; - let tx = await source.wormholeRelayer.registerWormholeRelayerContract(firstSignedVaa, {gasLimit: REASONABLE_GAS_LIMIT}); + const firstMessage = governance.publishWormholeRelayerRegisterChain( + timestamp, + chain, + expectedNewRegisteredAddress + ); + const firstSignedVaa = guardians.addSignatures( + firstMessage, + guardianIndices + ); + + let tx = await source.wormholeRelayer.registerWormholeRelayerContract( + firstSignedVaa, + { gasLimit: REASONABLE_GAS_LIMIT } + ); await tx.wait(); - const newRegisteredAddress = (await source.wormholeRelayer.getRegisteredWormholeRelayerContract(chain)); + const newRegisteredAddress = + await source.wormholeRelayer.getRegisteredWormholeRelayerContract(chain); expect(newRegisteredAddress).toBe(expectedNewRegisteredAddress); -}) + }); -test("Governance: Test Setting Default Relay Provider", async () => { + test("Governance: Test Setting Default Relay Provider", async () => { + const currentAddress = + await source.wormholeRelayer.getDefaultDeliveryProvider(); + console.log( + `For Chain ${source.chainId}, default relay provider: ${currentAddress}` + ); - const currentAddress = await source.wormholeRelayer.getDefaultDeliveryProvider(); - console.log(`For Chain ${source.chainId}, default relay provider: ${currentAddress}`); + const expectedNewDefaultDeliveryProvider = + "0x1234567890123456789012345678901234567892"; - const expectedNewDefaultDeliveryProvider = "0x1234567890123456789012345678901234567892"; - - const timestamp = (await source.wallet.provider.getBlock("latest")).timestamp; + const timestamp = (await source.wallet.provider.getBlock("latest")) + .timestamp; const chain = source.chainId; - const firstMessage = governance.publishWormholeRelayerSetDefaultDeliveryProvider(timestamp, chain, expectedNewDefaultDeliveryProvider); - const firstSignedVaa = guardians.addSignatures(firstMessage, guardianIndices); + const firstMessage = + governance.publishWormholeRelayerSetDefaultDeliveryProvider( + timestamp, + chain, + expectedNewDefaultDeliveryProvider + ); + const firstSignedVaa = guardians.addSignatures( + firstMessage, + guardianIndices + ); - let tx = await source.wormholeRelayer.setDefaultDeliveryProvider(firstSignedVaa); + let tx = await source.wormholeRelayer.setDefaultDeliveryProvider( + firstSignedVaa + ); await tx.wait(); - const newDefaultDeliveryProvider = (await source.wormholeRelayer.getDefaultDeliveryProvider()); + const newDefaultDeliveryProvider = + await source.wormholeRelayer.getDefaultDeliveryProvider(); expect(newDefaultDeliveryProvider).toBe(expectedNewDefaultDeliveryProvider); - const inverseFirstMessage = governance.publishWormholeRelayerSetDefaultDeliveryProvider(timestamp, chain, currentAddress) - const inverseFirstSignedVaa = guardians.addSignatures(inverseFirstMessage, guardianIndices); + const inverseFirstMessage = + governance.publishWormholeRelayerSetDefaultDeliveryProvider( + timestamp, + chain, + currentAddress + ); + const inverseFirstSignedVaa = guardians.addSignatures( + inverseFirstMessage, + guardianIndices + ); - tx = await source.wormholeRelayer.setDefaultDeliveryProvider(inverseFirstSignedVaa); + tx = await source.wormholeRelayer.setDefaultDeliveryProvider( + inverseFirstSignedVaa + ); await tx.wait(); - const originalDefaultDeliveryProvider = (await source.wormholeRelayer.getDefaultDeliveryProvider()); + const originalDefaultDeliveryProvider = + await source.wormholeRelayer.getDefaultDeliveryProvider(); expect(originalDefaultDeliveryProvider).toBe(currentAddress); + }); + test("Governance: Test Upgrading Contract", async () => { + const IMPLEMENTATION_STORAGE_SLOT = + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; + + const getImplementationAddress = () => + source.provider.getStorageAt( + source.wormholeRelayer.address, + IMPLEMENTATION_STORAGE_SLOT + ); + + console.log( + `Current Implementation address: ${await getImplementationAddress()}` + ); + + const wormholeAddress = CONTRACTS[network][sourceChain].core || ""; + + const newWormholeRelayerImplementationAddress = ( + await new ethers_contracts.WormholeRelayer__factory(source.wallet) + .deploy(wormholeAddress) + .then((x) => x.deployed()) + ).address; + + console.log(`Deployed!`); + console.log( + `New core relayer implementation: ${newWormholeRelayerImplementationAddress}` + ); + + const timestamp = (await source.wallet.provider.getBlock("latest")) + .timestamp; + const chain = source.chainId; + const firstMessage = governance.publishWormholeRelayerUpgradeContract( + timestamp, + chain, + newWormholeRelayerImplementationAddress + ); + const firstSignedVaa = guardians.addSignatures( + firstMessage, + guardianIndices + ); + + let tx = await source.wormholeRelayer.submitContractUpgrade(firstSignedVaa); + + expect( + ethers.utils.getAddress((await getImplementationAddress()).substring(26)) + ).toBe(ethers.utils.getAddress(newWormholeRelayerImplementationAddress)); + }); }); - -test("Governance: Test Upgrading Contract", async () => { - const IMPLEMENTATION_STORAGE_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; - - const getImplementationAddress = () => source.provider.getStorageAt(source.wormholeRelayer.address, IMPLEMENTATION_STORAGE_SLOT); - - console.log(`Current Implementation address: ${(await getImplementationAddress())}`); - - const wormholeAddress = CONTRACTS[network][sourceChain].core || ""; - - const newWormholeRelayerImplementationAddress = (await new ethers_contracts.WormholeRelayer__factory(source.wallet).deploy(wormholeAddress).then((x)=>x.deployed())).address; - - console.log(`Deployed!`); - console.log(`New core relayer implementation: ${newWormholeRelayerImplementationAddress}`); - - const timestamp = (await source.wallet.provider.getBlock("latest")).timestamp; - const chain = source.chainId; - const firstMessage = governance.publishWormholeRelayerUpgradeContract(timestamp, chain, newWormholeRelayerImplementationAddress); - const firstSignedVaa = guardians.addSignatures(firstMessage, guardianIndices); - - let tx = await source.wormholeRelayer.submitContractUpgrade(firstSignedVaa); - - expect(ethers.utils.getAddress((await getImplementationAddress()).substring(26))).toBe(ethers.utils.getAddress(newWormholeRelayerImplementationAddress)); -}); -}); +function sleep(ms: number): Promise { + return new Promise((r) => setTimeout(() => r(), ms)); +} diff --git a/sdk/js/src/relayer/consts.ts b/sdk/js/src/relayer/consts.ts index c0b52193d..9c0346db2 100644 --- a/sdk/js/src/relayer/consts.ts +++ b/sdk/js/src/relayer/consts.ts @@ -6,19 +6,29 @@ type AddressInfo = {wormholeRelayerAddress?: string, mockDeliveryProviderAddress const TESTNET: {[K in ChainName]?: AddressInfo} = { bsc: { - wormholeRelayerAddress: "0x6Bf598B0eb6aef9B163565763Fe50e54d230eD4E", + wormholeRelayerAddress: "0x80aC94316391752A193C1c47E27D382b507c93F3", + mockDeliveryProviderAddress: "0x813AB43ab264362c55BF35A1448d0fd8135049a6", + mockIntegrationAddress: "0xb6A04D6672F005787147472Be20d39741929Aa03", }, polygon: { - wormholeRelayerAddress: "0x0c97Ef9C224b7EB0BA5e4A9fd2740EC3AeAfc9c3", + wormholeRelayerAddress: "0x0591C25ebd0580E0d4F27A82Fc2e24E7489CB5e0", + mockDeliveryProviderAddress: "0xBF684878906629E72079D4f07D75Ef7165238092", + mockIntegrationAddress: "0x3bF0c43d88541BBCF92bE508ec41e540FbF28C56", }, avalanche: { - wormholeRelayerAddress: "0xf4e844a9B75BB532e67E654F7F80C6232e5Ea7a0", + wormholeRelayerAddress: "0xA3cF45939bD6260bcFe3D66bc73d60f19e49a8BB", + mockDeliveryProviderAddress: "0xd5903a063f604D4615E5c2760b7b80D491564BBe", + mockIntegrationAddress: "0x5E52f3eB0774E5e5f37760BD3Fca64951D8F74Ae", }, celo: { - wormholeRelayerAddress: "0xF08B7c0CFf448174a7007CF5f12023C72C0e84f0", + wormholeRelayerAddress: "0x306B68267Deb7c5DfCDa3619E22E9Ca39C374f84", + mockDeliveryProviderAddress: "0x93d56f29542c156B3e36f10dE41124B499664ff7", + mockIntegrationAddress: "0x7f1d8E809aBB3F6Dc9B90F0131C3E8308046E190", }, moonbeam: { - wormholeRelayerAddress: "0xd20d484eC6c57448d6871F91F4527260FD4aC141", + wormholeRelayerAddress: "0x0591C25ebd0580E0d4F27A82Fc2e24E7489CB5e0", + mockDeliveryProviderAddress: "0xBF684878906629E72079D4f07D75Ef7165238092", + mockIntegrationAddress: "0x3bF0c43d88541BBCF92bE508ec41e540FbF28C56", }, } diff --git a/sdk/js/src/relayer/index.ts b/sdk/js/src/relayer/index.ts index 54dd2e510..0a3f3a62c 100644 --- a/sdk/js/src/relayer/index.ts +++ b/sdk/js/src/relayer/index.ts @@ -1,3 +1,3 @@ export * from "./structs"; export * from "./consts"; -export * from "./relayer/relayer"; +export * from "./relayer"; diff --git a/sdk/js/src/relayer/relayer/deliver.ts b/sdk/js/src/relayer/relayer/deliver.ts new file mode 100644 index 000000000..538fad4a9 --- /dev/null +++ b/sdk/js/src/relayer/relayer/deliver.ts @@ -0,0 +1,127 @@ +import { BigNumber, ethers, ContractReceipt } from "ethers"; +import { IWormholeRelayer__factory } from "../../ethers-contracts"; +import { ChainName, toChainName, ChainId, Network } from "../../utils"; +import { SignedVaa, parseVaa } from "../../vaa"; +import { getWormholeRelayerAddress } from "../consts"; +import { + RelayerPayloadId, + DeliveryInstruction, + DeliveryOverrideArgs, + packOverrides, + parseEVMExecutionInfoV1, + parseWormholeRelayerPayloadType, + parseWormholeRelayerSend, + VaaKey, +} from "../structs"; +import { DeliveryTargetInfo, getVAA } from "./helpers"; + +export type DeliveryInfo = { + type: RelayerPayloadId.Delivery; + sourceChain: ChainName; + sourceTransactionHash: string; + sourceDeliverySequenceNumber: number; + deliveryInstruction: DeliveryInstruction; + targetChainStatus: { + chain: ChainName; + events: DeliveryTargetInfo[]; + }; +}; + +export type DeliveryArguments = { + budget: BigNumber; + deliveryInstruction: DeliveryInstruction; + deliveryHash: string; +}; + +export async function deliver( + deliveryVaa: SignedVaa, + signer: ethers.Signer, + wormholeRPCs: string | string[], + environment: Network = "MAINNET", + overrides?: DeliveryOverrideArgs +): Promise { + const { budget, deliveryInstruction, deliveryHash } = + extractDeliveryArguments(deliveryVaa, overrides); + + const additionalVaas = await fetchAdditionalVaas( + wormholeRPCs, + deliveryInstruction.vaaKeys + ); + + const wormholeRelayerAddress = getWormholeRelayerAddress( + toChainName(deliveryInstruction.targetChainId as ChainId), + environment + ); + const wormholeRelayer = IWormholeRelayer__factory.connect( + wormholeRelayerAddress, + signer + ); + const gasEstimate = await wormholeRelayer.estimateGas.deliver( + additionalVaas, + deliveryVaa, + signer.getAddress(), + overrides ? packOverrides(overrides) : new Uint8Array(), + { value: budget } + ); + const tx = await wormholeRelayer.deliver( + additionalVaas, + deliveryVaa, + signer.getAddress(), + overrides ? packOverrides(overrides) : new Uint8Array(), + { value: budget, gasLimit: gasEstimate.mul(2) } + ); + const rx = await tx.wait(); + console.log(`Delivered ${deliveryHash} on ${rx.blockNumber}`); + return rx; +} + +export function deliveryBudget( + delivery: DeliveryInstruction, + overrides?: DeliveryOverrideArgs +): BigNumber { + const receiverValue = overrides?.newReceiverValue + ? overrides.newReceiverValue + : delivery.requestedReceiverValue.add(delivery.extraReceiverValue); + const getMaxRefund = (encodedDeliveryInfo: Buffer) => { + const [deliveryInfo] = parseEVMExecutionInfoV1(encodedDeliveryInfo, 0); + return deliveryInfo.targetChainRefundPerGasUnused.mul( + deliveryInfo.gasLimit + ); + }; + const maxRefund = getMaxRefund( + overrides?.newExecutionInfo + ? overrides.newExecutionInfo + : delivery.encodedExecutionInfo + ); + return receiverValue.add(maxRefund); +} + +export function extractDeliveryArguments( + vaa: SignedVaa, + overrides?: DeliveryOverrideArgs +): DeliveryArguments { + const parsedVaa = parseVaa(vaa); + + const payloadType = parseWormholeRelayerPayloadType(parsedVaa.payload); + if (payloadType !== RelayerPayloadId.Delivery) { + throw new Error( + `Expected delivery payload type, got ${RelayerPayloadId[payloadType]}` + ); + } + const deliveryInstruction = parseWormholeRelayerSend(parsedVaa.payload); + const budget = deliveryBudget(deliveryInstruction, overrides); + return { + budget, + deliveryInstruction: deliveryInstruction, + deliveryHash: parsedVaa.hash.toString("hex"), + }; +} + +export async function fetchAdditionalVaas( + wormholeRPCs: string | string[], + additionalVaaKeys: VaaKey[] +): Promise { + return Promise.all( + additionalVaaKeys.map(async (vaaKey) => getVAA(wormholeRPCs, vaaKey)) + ); +} diff --git a/sdk/js/src/relayer/relayer/helpers.ts b/sdk/js/src/relayer/relayer/helpers.ts index c5acdff4d..d7a8875a0 100644 --- a/sdk/js/src/relayer/relayer/helpers.ts +++ b/sdk/js/src/relayer/relayer/helpers.ts @@ -6,7 +6,9 @@ import { Network, tryNativeToHexString, isChain, - CONTRACTS + CONTRACTS, + getSignedVAAWithRetry, + SignedVaa, } from "../../"; import { BigNumber, ContractReceipt, ethers } from "ethers"; import { getWormholeRelayer, RPCS_BY_CHAIN } from "../consts"; @@ -20,11 +22,17 @@ import { RefundStatus, VaaKey, DeliveryOverrideArgs, - parseForwardFailureError + parseForwardFailureError, } from "../structs"; -import { DeliveryProvider, DeliveryProvider__factory, Implementation__factory, IWormholeRelayerDelivery__factory } from "../../ethers-contracts/"; -import {DeliveryEvent} from "../../ethers-contracts/WormholeRelayer" +import { + DeliveryProvider, + DeliveryProvider__factory, + Implementation__factory, + IWormholeRelayerDelivery__factory, +} from "../../ethers-contracts/"; +import { DeliveryEvent } from "../../ethers-contracts/WormholeRelayer"; import { VaaKeyStruct } from "../../ethers-contracts/IWormholeRelayer.sol/IWormholeRelayer"; +import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport"; export type DeliveryTargetInfo = { status: DeliveryStatus | string; @@ -38,6 +46,31 @@ export type DeliveryTargetInfo = { overrides?: DeliveryOverrideArgs; }; +export async function getVAA( + wormholeRPCs: string[] | string, + vaaKey: VaaKey, + isNode?: boolean +): Promise { + if (typeof wormholeRPCs === "string") { + wormholeRPCs = [wormholeRPCs]; + } + const vaa = await getSignedVAAWithRetry( + wormholeRPCs, + vaaKey.chainId! as ChainId, + vaaKey.emitterAddress!.toString("hex"), + vaaKey.sequence!.toBigInt().toString(), + isNode + ? { + transport: NodeHttpTransport(), + } + : {}, + 2000, + 4 + ); + + return Buffer.from(vaa.vaaBytes); +} + export function parseWormholeLog(log: ethers.providers.Log): { type: RelayerPayloadId; parsed: DeliveryInstruction | string; @@ -57,20 +90,25 @@ export function parseWormholeLog(log: ethers.providers.Log): { } export function printChain(chainId: number) { - if(!(chainId in CHAIN_ID_TO_NAME)) throw Error(`Invalid Chain ID: ${chainId}`); + if (!(chainId in CHAIN_ID_TO_NAME)) + throw Error(`Invalid Chain ID: ${chainId}`); return `${CHAIN_ID_TO_NAME[chainId as ChainId]} (Chain ${chainId})`; } -export function getDefaultProvider(network: Network, chain: ChainName, ci?: boolean) { +export function getDefaultProvider( + network: Network, + chain: ChainName, + ci?: boolean +) { let rpc: string | undefined = ""; - if(ci) { - if(chain == "ethereum") rpc = "http://eth-devnet:8545"; - else if(chain == "bsc") rpc = "http://eth-devnet2:8545"; - else throw Error(`This chain isn't in CI for relayers: ${chain}`) + if (ci) { + if (chain == "ethereum") rpc = "http://eth-devnet:8545"; + else if (chain == "bsc") rpc = "http://eth-devnet2:8545"; + else throw Error(`This chain isn't in CI for relayers: ${chain}`); } else { rpc = RPCS_BY_CHAIN[network][chain]; } - if(!rpc) { + if (!rpc) { throw Error(`No default RPC for chain ${chain} or network ${network}`); } return new ethers.providers.StaticJsonRpcProvider(rpc); @@ -100,7 +138,7 @@ export async function getWormholeRelayerInfoBySourceSequence( blockStartNumber: ethers.providers.BlockTag, blockEndNumber: ethers.providers.BlockTag, targetWormholeRelayerAddress: string -): Promise<{chain: ChainName, events: DeliveryTargetInfo[]}> { +): Promise<{ chain: ChainName; events: DeliveryTargetInfo[] }> { const deliveryEvents = await getWormholeRelayerDeliveryEventsBySourceSequence( environment, targetChain, @@ -116,9 +154,9 @@ export async function getWormholeRelayerInfoBySourceSequence( try { const blockStart = await targetChainProvider.getBlock(blockStartNumber); const blockEnd = await targetChainProvider.getBlock(blockEndNumber); - status = `Delivery didn't happen on ${targetChain} within blocks ${blockStart.number} to ${ - blockEnd.number - } (within times ${new Date( + status = `Delivery didn't happen on ${targetChain} within blocks ${ + blockStart.number + } to ${blockEnd.number} (within times ${new Date( blockStart.timestamp * 1000 ).toString()} to ${new Date(blockEnd.timestamp * 1000).toString()})`; } catch (e) {} @@ -129,12 +167,12 @@ export async function getWormholeRelayerInfoBySourceSequence( sourceChain: sourceChain, sourceVaaSequence, gasUsed: BigNumber.from(0), - refundStatus: RefundStatus.RefundFail + refundStatus: RefundStatus.RefundFail, }); } const targetChainStatus = { chain: targetChain, - events: deliveryEvents + events: deliveryEvents, }; return targetChainStatus; @@ -151,7 +189,7 @@ export async function getWormholeRelayerDeliveryEventsBySourceSequence( targetWormholeRelayerAddress: string ): Promise { const sourceChainId = CHAINS[sourceChain]; - if(!sourceChainId) throw Error(`Invalid source chain: ${sourceChain}`) + if (!sourceChainId) throw Error(`Invalid source chain: ${sourceChain}`); const wormholeRelayer = getWormholeRelayer( targetChain, environment, @@ -165,13 +203,23 @@ export async function getWormholeRelayerDeliveryEventsBySourceSequence( sourceVaaSequence ); - const deliveryEventsPreFilter: DeliveryEvent[] = await wormholeRelayer.queryFilter( - deliveryEvents, - blockStartNumber, - blockEndNumber - ); + const deliveryEventsPreFilter: DeliveryEvent[] = + await wormholeRelayer.queryFilter( + deliveryEvents, + blockStartNumber, + blockEndNumber + ); - const isValid: boolean[] = await Promise.all(deliveryEventsPreFilter.map((deliveryEvent) => areSignaturesValid(deliveryEvent.getTransaction(), targetChain, targetChainProvider, environment))); + const isValid: boolean[] = await Promise.all( + deliveryEventsPreFilter.map((deliveryEvent) => + areSignaturesValid( + deliveryEvent.getTransaction(), + targetChain, + targetChainProvider, + environment + ) + ) + ); // There is a max limit on RPCs sometimes for how many blocks to query return await transformDeliveryEvents( @@ -180,22 +228,36 @@ export async function getWormholeRelayerDeliveryEventsBySourceSequence( ); } -async function areSignaturesValid(transaction: Promise, targetChain: ChainName, targetChainProvider: ethers.providers.Provider, environment: Network) { +async function areSignaturesValid( + transaction: Promise, + targetChain: ChainName, + targetChainProvider: ethers.providers.Provider, + environment: Network +) { const coreAddress = CONTRACTS[environment][targetChain].core; - if(!coreAddress) throw Error(`No Wormhole Address for chain ${targetChain}, network ${environment}`); + if (!coreAddress) + throw Error( + `No Wormhole Address for chain ${targetChain}, network ${environment}` + ); - const wormhole = Implementation__factory.connect(coreAddress, targetChainProvider); - const decodedData = IWormholeRelayerDelivery__factory.createInterface().parseTransaction(await transaction); + const wormhole = Implementation__factory.connect( + coreAddress, + targetChainProvider + ); + const decodedData = + IWormholeRelayerDelivery__factory.createInterface().parseTransaction( + await transaction + ); const vaaIsValid = async (vaa: ethers.utils.BytesLike): Promise => { - const [,result,reason] = await wormhole.parseAndVerifyVM(vaa); - if(!result) console.log(`Invalid vaa! Reason: ${reason}`); + const [, result, reason] = await wormhole.parseAndVerifyVM(vaa); + if (!result) console.log(`Invalid vaa! Reason: ${reason}`); return result; - } + }; const vaas = decodedData.args[0]; - for(let i=0; i { - - return Promise.all( events.map(async (x) => { const status = deliveryStatus(x.args[4]); - if(!isChain(x.args[1])) throw Error(`Invalid source chain id: ${x.args[1]}`); + if (!isChain(x.args[1])) + throw Error(`Invalid source chain id: ${x.args[1]}`); const sourceChain = CHAIN_ID_TO_NAME[x.args[1] as ChainId]; return { status, @@ -237,8 +298,19 @@ async function transformDeliveryEvents( sourceChain, gasUsed: BigNumber.from(x.args[5]), refundStatus: x.args[6], - revertString: (status == DeliveryStatus.ReceiverFailure) ? x.args[7] : (status == DeliveryStatus.ForwardRequestFailure ? parseForwardFailureError(Buffer.from(x.args[7].substring(2), "hex")): undefined), - overridesInfo: (Buffer.from(x.args[8].substring(2), "hex").length > 0) && parseOverrideInfoFromDeliveryEvent(Buffer.from(x.args[8].substring(2), "hex")) + revertString: + status == DeliveryStatus.ReceiverFailure + ? x.args[7] + : status == DeliveryStatus.ForwardRequestFailure + ? parseForwardFailureError( + Buffer.from(x.args[7].substring(2), "hex") + ) + : undefined, + overridesInfo: + Buffer.from(x.args[8].substring(2), "hex").length > 0 && + parseOverrideInfoFromDeliveryEvent( + Buffer.from(x.args[8].substring(2), "hex") + ), }; }) ); @@ -288,9 +360,7 @@ export function getWormholeRelayerLog( } } -export function vaaKeyToVaaKeyStruct( - vaaKey: VaaKey -): VaaKeyStruct { +export function vaaKeyToVaaKeyStruct(vaaKey: VaaKey): VaaKeyStruct { return { chainId: vaaKey.chainId || 0, emitterAddress: diff --git a/sdk/js/src/relayer/relayer/index.ts b/sdk/js/src/relayer/relayer/index.ts new file mode 100644 index 000000000..086b5c210 --- /dev/null +++ b/sdk/js/src/relayer/relayer/index.ts @@ -0,0 +1,5 @@ +export * from "./helpers"; +export * from "./deliver"; +export * from "./info"; +export * from "./resend"; +export * from "./send"; diff --git a/sdk/js/src/relayer/relayer/info.ts b/sdk/js/src/relayer/relayer/info.ts new file mode 100644 index 000000000..7d2668189 --- /dev/null +++ b/sdk/js/src/relayer/relayer/info.ts @@ -0,0 +1,306 @@ +import { + ChainId, + CHAIN_ID_TO_NAME, + ChainName, + isChain, + CONTRACTS, + CHAINS, + tryNativeToHexString, + Network, + ethers_contracts, +} from "../.."; +import { BigNumber, ethers } from "ethers"; +import { getWormholeRelayerAddress } from "../consts"; +import { + RelayerPayloadId, + DeliveryInstruction, + RefundStatus, + parseEVMExecutionInfoV1, +} from "../structs"; +import { + getDefaultProvider, + printChain, + getWormholeRelayerLog, + parseWormholeLog, + getBlockRange, + getWormholeRelayerInfoBySourceSequence, +} from "./helpers"; +import { DeliveryInfo } from "./deliver"; + +export type InfoRequestParams = { + environment?: Network; + sourceChainProvider?: ethers.providers.Provider; + targetChainProviders?: Map; + targetChainBlockRanges?: Map< + ChainName, + [ethers.providers.BlockTag, ethers.providers.BlockTag] + >; + wormholeRelayerWhMessageIndex?: number; + wormholeRelayerAddresses?: Map; +}; + + +export type GetPriceOptParams = { + environment?: Network; + receiverValue?: ethers.BigNumberish; + deliveryProviderAddress?: string; + sourceChainProvider?: ethers.providers.Provider; +}; + +export async function getPriceAndRefundInfo( + sourceChain: ChainName, + targetChain: ChainName, + gasAmount: ethers.BigNumberish, + optionalParams?: GetPriceOptParams +): Promise<[ethers.BigNumber, ethers.BigNumber]> { + const environment = optionalParams?.environment || "MAINNET"; + const sourceChainProvider = + optionalParams?.sourceChainProvider || + getDefaultProvider(environment, sourceChain); + if (!sourceChainProvider) + throw Error( + "No default RPC for this chain; pass in your own provider (as sourceChainProvider)" + ); + const wormholeRelayerAddress = getWormholeRelayerAddress( + sourceChain, + environment + ); + const sourceWormholeRelayer = + ethers_contracts.IWormholeRelayer__factory.connect( + wormholeRelayerAddress, + sourceChainProvider + ); + const deliveryProviderAddress = + optionalParams?.deliveryProviderAddress || + (await sourceWormholeRelayer.getDefaultDeliveryProvider()); + const targetChainId = CHAINS[targetChain]; + const priceAndRefundInfo = await sourceWormholeRelayer[ + "quoteEVMDeliveryPrice(uint16,uint256,uint256,address)" + ]( + targetChainId, + optionalParams?.receiverValue || 0, + gasAmount, + deliveryProviderAddress + ); + return priceAndRefundInfo; +} + +export async function getPrice( + sourceChain: ChainName, + targetChain: ChainName, + gasAmount: ethers.BigNumberish, + optionalParams?: GetPriceOptParams +): Promise { + const priceAndRefundInfo = await getPriceAndRefundInfo( + sourceChain, + targetChain, + gasAmount, + optionalParams + ); + return priceAndRefundInfo[0]; +} + +export async function getWormholeRelayerInfo( + sourceChain: ChainName, + sourceTransaction: string, + infoRequest?: InfoRequestParams +): Promise { + const environment = infoRequest?.environment || "MAINNET"; + const sourceChainProvider = + infoRequest?.sourceChainProvider || + getDefaultProvider(environment, sourceChain); + if (!sourceChainProvider) + throw Error( + "No default RPC for this chain; pass in your own provider (as sourceChainProvider)" + ); + const receipt = await sourceChainProvider.getTransactionReceipt( + sourceTransaction + ); + if (!receipt) throw Error("Transaction has not been mined"); + const bridgeAddress = CONTRACTS[environment][sourceChain].core; + const wormholeRelayerAddress = + infoRequest?.wormholeRelayerAddresses?.get(sourceChain) || + getWormholeRelayerAddress(sourceChain, environment); + if (!bridgeAddress || !wormholeRelayerAddress) { + throw Error( + `Invalid chain ID or network: Chain ${sourceChain}, ${environment}` + ); + } + const deliveryLog = getWormholeRelayerLog( + receipt, + bridgeAddress, + tryNativeToHexString(wormholeRelayerAddress, "ethereum"), + infoRequest?.wormholeRelayerWhMessageIndex + ? infoRequest.wormholeRelayerWhMessageIndex + : 0 + ); + + const { type, parsed } = parseWormholeLog(deliveryLog.log); + + const instruction = parsed as DeliveryInstruction; + + const targetChainId = instruction.targetChainId as ChainId; + if (!isChain(targetChainId)) throw Error(`Invalid Chain: ${targetChainId}`); + const targetChain = CHAIN_ID_TO_NAME[targetChainId]; + const targetChainProvider = + infoRequest?.targetChainProviders?.get(targetChain) || + getDefaultProvider(environment, targetChain); + + if (!targetChainProvider) { + throw Error( + "No default RPC for this chain; pass in your own provider (as targetChainProvider)" + ); + } + const [blockStartNumber, blockEndNumber] = + infoRequest?.targetChainBlockRanges?.get(targetChain) || + getBlockRange(targetChainProvider); + + const targetChainStatus = await getWormholeRelayerInfoBySourceSequence( + environment, + targetChain, + targetChainProvider, + sourceChain, + BigNumber.from(deliveryLog.sequence), + blockStartNumber, + blockEndNumber, + infoRequest?.wormholeRelayerAddresses?.get(targetChain) || + getWormholeRelayerAddress(targetChain, environment) + ); + + return { + type: RelayerPayloadId.Delivery, + sourceChain: sourceChain, + sourceTransactionHash: sourceTransaction, + sourceDeliverySequenceNumber: BigNumber.from( + deliveryLog.sequence + ).toNumber(), + deliveryInstruction: instruction, + targetChainStatus, + }; +} + +export function printWormholeRelayerInfo(info: DeliveryInfo) { + console.log(stringifyWormholeRelayerInfo(info)); +} + +export function stringifyWormholeRelayerInfo(info: DeliveryInfo): string { + let stringifiedInfo = ""; + if ( + info.type == RelayerPayloadId.Delivery && + info.deliveryInstruction.targetAddress.toString("hex") !== + "0000000000000000000000000000000000000000000000000000000000000000" + ) { + stringifiedInfo += `Found delivery request in transaction ${info.sourceTransactionHash} on ${info.sourceChain}\n`; + const numMsgs = info.deliveryInstruction.vaaKeys.length; + + const payload = info.deliveryInstruction.payload.toString("hex"); + if (payload.length > 0) { + stringifiedInfo += `\nPayload to be relayed (as hex string): 0x${payload}`; + } + if (numMsgs > 0) { + stringifiedInfo += `\nThe following ${numMsgs} wormhole messages (VAAs) were ${ + payload.length > 0 ? "also " : "" + }requested to be relayed:\n`; + stringifiedInfo += info.deliveryInstruction.vaaKeys + .map((msgInfo, i) => { + let result = ""; + result += `(VAA ${i}): `; + result += `Message from ${ + msgInfo.chainId ? printChain(msgInfo.chainId) : "" + }, with emitter address ${msgInfo.emitterAddress?.toString( + "hex" + )} and sequence number ${msgInfo.sequence}`; + + return result; + }) + .join(",\n"); + } + if (payload.length == 0 && numMsgs == 0) { + stringifiedInfo += `\nAn empty payload was requested to be sent`; + } + + const instruction = info.deliveryInstruction; + const targetChainName = + CHAIN_ID_TO_NAME[instruction.targetChainId as ChainId]; + stringifiedInfo += `${ + numMsgs == 0 + ? payload.length == 0 + ? "" + : "\n\nPayload was requested to be relayed" + : "\n\nThese were requested to be sent" + } to 0x${instruction.targetAddress.toString("hex")} on ${printChain( + instruction.targetChainId + )}\n`; + const totalReceiverValue = instruction.requestedReceiverValue.add( + instruction.extraReceiverValue + ); + stringifiedInfo += totalReceiverValue.gt(0) + ? `Amount to pass into target address: ${totalReceiverValue} wei of ${targetChainName} currency ${ + instruction.extraReceiverValue.gt(0) + ? `${instruction.requestedReceiverValue} requested, ${instruction.extraReceiverValue} additionally paid for` + : "" + }\n` + : ``; + const [executionInfo] = parseEVMExecutionInfoV1( + instruction.encodedExecutionInfo, + 0 + ); + stringifiedInfo += `Gas limit: ${executionInfo.gasLimit} ${targetChainName} gas\n\n`; + stringifiedInfo += `Refund rate: ${executionInfo.targetChainRefundPerGasUnused} of ${targetChainName} wei per unit of gas unused\n\n`; + stringifiedInfo += info.targetChainStatus.events + + .map( + (e, i) => + `Delivery attempt ${i + 1}: ${ + e.transactionHash + ? ` ${targetChainName} transaction hash: ${e.transactionHash}` + : "" + }\nStatus: ${e.status}\n${ + e.revertString + ? `Failure reason: ${ + e.gasUsed.eq(executionInfo.gasLimit) + ? "Gas limit hit" + : e.revertString + }\n` + : "" + }Gas used: ${e.gasUsed.toString()}\nTransaction fee used: ${executionInfo.targetChainRefundPerGasUnused + .mul(e.gasUsed) + .toString()} wei of ${targetChainName} currency\n}` + ) + .join("\n"); + } else if ( + info.type == RelayerPayloadId.Delivery && + info.deliveryInstruction.targetAddress.toString("hex") === + "0000000000000000000000000000000000000000000000000000000000000000" + ) { + stringifiedInfo += `Found delivery request in transaction ${info.sourceTransactionHash} on ${info.sourceChain}\n`; + + const instruction = info.deliveryInstruction; + const targetChainName = + CHAIN_ID_TO_NAME[instruction.targetChainId as ChainId]; + + stringifiedInfo += `\nA refund of ${ + instruction.extraReceiverValue + } ${targetChainName} wei was requested to be sent to ${targetChainName}, address 0x${info.deliveryInstruction.refundAddress.toString( + "hex" + )}`; + + stringifiedInfo += info.targetChainStatus.events + + .map( + (e, i) => + `Delivery attempt ${i + 1}: ${ + e.transactionHash + ? ` ${targetChainName} transaction hash: ${e.transactionHash}` + : "" + }\nStatus: ${ + e.refundStatus == RefundStatus.RefundSent + ? "Refund Successful" + : "Refund Failed" + }` + ) + .join("\n"); + } + + return stringifiedInfo; +} diff --git a/sdk/js/src/relayer/relayer/relayer.ts b/sdk/js/src/relayer/relayer/relayer.ts deleted file mode 100644 index 27c39e134..000000000 --- a/sdk/js/src/relayer/relayer/relayer.ts +++ /dev/null @@ -1,508 +0,0 @@ -import { - ChainId, - CHAIN_ID_TO_NAME, - ChainName, - isChain, - CONTRACTS, - CHAINS, - tryNativeToHexString, - tryHexToNativeString, - Network, - ethers_contracts, - getSignedVAAWithRetry, - parseVaa, -} from "../../"; -import { BigNumber, ethers } from "ethers"; -import { getWormholeRelayer, getWormholeRelayerAddress } from "../consts"; -import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport"; -import { - RelayerPayloadId, - DeliveryInstruction, - VaaKeyType, - DeliveryStatus, - VaaKey, - parseWormholeRelayerSend, - RefundStatus, - parseEVMExecutionInfoV1 -} from "../structs"; -import { - getDefaultProvider, - printChain, - getWormholeRelayerLog, - parseWormholeLog, - getBlockRange, - getWormholeRelayerInfoBySourceSequence, - vaaKeyToVaaKeyStruct, - getDeliveryProvider, - DeliveryTargetInfo -} from "./helpers"; -import { VaaKeyStruct } from "../../ethers-contracts/IWormholeRelayer.sol/IWormholeRelayer"; - -export type InfoRequestParams = { - environment?: Network; - sourceChainProvider?: ethers.providers.Provider; - targetChainProviders?: Map; - targetChainBlockRanges?: Map< - ChainName, - [ethers.providers.BlockTag, ethers.providers.BlockTag] - >; - wormholeRelayerWhMessageIndex?: number; - wormholeRelayerAddresses?: Map -}; - -export type DeliveryInfo = { - type: RelayerPayloadId.Delivery; - sourceChain: ChainName; - sourceTransactionHash: string; - sourceDeliverySequenceNumber: number; - deliveryInstruction: DeliveryInstruction; - targetChainStatus: { - chain: ChainName; - events: DeliveryTargetInfo[]; - }; -}; - -export function printWormholeRelayerInfo(info: DeliveryInfo) { - console.log(stringifyWormholeRelayerInfo(info)); -} - -export function stringifyWormholeRelayerInfo(info: DeliveryInfo): string { - let stringifiedInfo = ""; - if (info.type == RelayerPayloadId.Delivery && info.deliveryInstruction.targetAddress.toString("hex") !== "0000000000000000000000000000000000000000000000000000000000000000") { - stringifiedInfo += `Found delivery request in transaction ${ - info.sourceTransactionHash - } on ${info.sourceChain}\n`; - const numMsgs = info.deliveryInstruction.vaaKeys.length; - - const payload = info.deliveryInstruction.payload.toString("hex"); - if(payload.length > 0) { - stringifiedInfo += `\nPayload to be relayed (as hex string): 0x${payload}` - } - if(numMsgs > 0) { - stringifiedInfo += `\nThe following ${numMsgs} wormhole messages (VAAs) were ${payload.length > 0 ? 'also ' : ''}requested to be relayed:\n`; - stringifiedInfo += info.deliveryInstruction.vaaKeys.map((msgInfo, i) => { - let result = ""; - result += `(VAA ${i}): `; - result += `Message from ${ - msgInfo.chainId ? printChain(msgInfo.chainId) : "" - }, with emitter address ${msgInfo.emitterAddress?.toString( - "hex" - )} and sequence number ${msgInfo.sequence}`; - - return result; - }).join(",\n"); - } - if(payload.length == 0 && numMsgs == 0) { - stringifiedInfo += `\nAn empty payload was requested to be sent` - } - - const instruction = info.deliveryInstruction; - const targetChainName = CHAIN_ID_TO_NAME[instruction.targetChainId as ChainId]; - stringifiedInfo += `${numMsgs == 0 ? (payload.length == 0 ? '' : '\n\nPayload was requested to be relayed') : '\n\nThese were requested to be sent'} to 0x${instruction.targetAddress.toString( - - "hex" - )} on ${printChain(instruction.targetChainId)}\n`; - const totalReceiverValue = (instruction.requestedReceiverValue.add(instruction.extraReceiverValue)); - stringifiedInfo += totalReceiverValue.gt(0) - ? `Amount to pass into target address: ${totalReceiverValue} wei of ${targetChainName} currency ${instruction.extraReceiverValue.gt(0) ? `${instruction.requestedReceiverValue} requested, ${instruction.extraReceiverValue} additionally paid for` : ""}\n` - : ``; - const [executionInfo,] = parseEVMExecutionInfoV1(instruction.encodedExecutionInfo, 0); - stringifiedInfo += `Gas limit: ${executionInfo.gasLimit} ${targetChainName} gas\n\n`; - stringifiedInfo += `Refund rate: ${executionInfo.targetChainRefundPerGasUnused} of ${targetChainName} wei per unit of gas unused\n\n`; - stringifiedInfo += info.targetChainStatus.events - - .map( - (e, i) => - `Delivery attempt ${i + 1}: ${ - e.transactionHash - ? ` ${targetChainName} transaction hash: ${e.transactionHash}` - : "" - }\nStatus: ${e.status}\n${e.revertString ? `Failure reason: ${e.gasUsed.eq(executionInfo.gasLimit) ? "Gas limit hit" : e.revertString}\n`: ""}Gas used: ${e.gasUsed.toString()}\nTransaction fee used: ${executionInfo.targetChainRefundPerGasUnused.mul(e.gasUsed).toString()} wei of ${targetChainName} currency\n}` - ) - .join("\n"); - } else if (info.type == RelayerPayloadId.Delivery && info.deliveryInstruction.targetAddress.toString("hex") === "0000000000000000000000000000000000000000000000000000000000000000") { - stringifiedInfo += `Found delivery request in transaction ${ - info.sourceTransactionHash - } on ${info.sourceChain}\n`; - - const instruction = info.deliveryInstruction; - const targetChainName = CHAIN_ID_TO_NAME[instruction.targetChainId as ChainId]; - - stringifiedInfo += `\nA refund of ${instruction.extraReceiverValue} ${targetChainName} wei was requested to be sent to ${targetChainName}, address 0x${info.deliveryInstruction.refundAddress.toString("hex")}` - - stringifiedInfo += info.targetChainStatus.events - - .map( - (e, i) => - `Delivery attempt ${i + 1}: ${ - e.transactionHash - ? ` ${targetChainName} transaction hash: ${e.transactionHash}` - : "" - }\nStatus: ${e.refundStatus == RefundStatus.RefundSent ? "Refund Successful" : "Refund Failed"}` - ) - .join("\n"); - } - - return stringifiedInfo; -} - -export type SendOptionalParams = { - environment?: Network; - receiverValue?: ethers.BigNumberish; - paymentForExtraReceiverValue?: ethers.BigNumberish; - additionalVaas?: [ - { - chainId?: ChainId; - emitterAddress: string; - sequenceNumber: ethers.BigNumberish; - } - ]; - deliveryProviderAddress?: string; - consistencyLevel?: ethers.BigNumberish; - refundChainId?: ChainId; - refundAddress?: string; - relayParameters?: ethers.BytesLike; -}; - -export async function sendToEvm( - signer: ethers.Signer, - sourceChain: ChainName, - targetChain: ChainName, - targetAddress: string, - payload: ethers.BytesLike, - gasLimit: BigNumber | number, - overrides?: ethers.PayableOverrides, - sendOptionalParams?: SendOptionalParams, -): Promise { - const sourceChainId = CHAINS[sourceChain]; - const targetChainId = CHAINS[targetChain]; - - const environment = sendOptionalParams?.environment || "MAINNET"; - const wormholeRelayerAddress = getWormholeRelayerAddress( - sourceChain, - environment - ); - const sourceWormholeRelayer = ethers_contracts.IWormholeRelayer__factory.connect( - wormholeRelayerAddress, - signer - ); - - const refundLocationExists = - sendOptionalParams?.refundChainId!== undefined && - sendOptionalParams?.refundAddress !== undefined; - const defaultDeliveryProviderAddress = - await sourceWormholeRelayer.getDefaultDeliveryProvider(); - - // Using the most general 'send' function in IWormholeRelayer - // Inputs: - // targetChainId, targetAddress, refundChainId, refundAddress, maxTransactionFee, receiverValue, payload, vaaKeys, - // consistencyLevel, deliveryProviderAddress, relayParameters - const [deliveryPrice,]: [BigNumber, BigNumber] = await sourceWormholeRelayer["quoteEVMDeliveryPrice(uint16,uint256,uint256,address)"](targetChainId, sendOptionalParams?.receiverValue || 0, gasLimit, sendOptionalParams?.deliveryProviderAddress || defaultDeliveryProviderAddress); - const value = await (overrides?.value || 0); - const totalPrice = deliveryPrice.add(sendOptionalParams?.paymentForExtraReceiverValue || 0); - if(!totalPrice.eq(value)) { - throw new Error(`Expected a payment of ${totalPrice.toString()} wei; received ${value.toString()} wei`); - } - const tx = sourceWormholeRelayer.sendToEvm( - targetChainId, // targetChainId - targetAddress, // targetAddress - payload, - sendOptionalParams?.receiverValue || 0, // receiverValue - sendOptionalParams?.paymentForExtraReceiverValue || 0, // payment for extra receiverValue - gasLimit, - (refundLocationExists && sendOptionalParams?.refundChainId) || sourceChainId, // refundChainId - refundLocationExists && - sendOptionalParams?.refundAddress && - sendOptionalParams?.refundAddress || - signer.getAddress(), // refundAddress - sendOptionalParams?.deliveryProviderAddress || defaultDeliveryProviderAddress, // deliveryProviderAddress - sendOptionalParams?.additionalVaas - ? sendOptionalParams.additionalVaas.map( - (additionalVaa): VaaKeyStruct => ({ - chainId: additionalVaa.chainId || sourceChainId, - emitterAddress: Buffer.from(tryNativeToHexString( - additionalVaa.emitterAddress, - "ethereum" - ), "hex"), - sequence: BigNumber.from(additionalVaa.sequenceNumber || 0) - }) - ) - : [], // vaaKeys - sendOptionalParams?.consistencyLevel || 15, // consistencyLevel - overrides); - return tx; -} - -export type GetPriceOptParams = { - environment?: Network; - receiverValue?: ethers.BigNumberish; - deliveryProviderAddress?: string; - sourceChainProvider?: ethers.providers.Provider; -}; - -export async function getPriceAndRefundInfo( - sourceChain: ChainName, - targetChain: ChainName, - gasAmount: ethers.BigNumberish, - optionalParams?: GetPriceOptParams -): Promise<[ethers.BigNumber, ethers.BigNumber]> { - const environment = optionalParams?.environment || "MAINNET"; - const sourceChainProvider = - optionalParams?.sourceChainProvider || - getDefaultProvider(environment, sourceChain); - if (!sourceChainProvider) - throw Error( - "No default RPC for this chain; pass in your own provider (as sourceChainProvider)" - ); - const wormholeRelayerAddress = getWormholeRelayerAddress( - sourceChain, - environment - ); - const sourceWormholeRelayer = ethers_contracts.IWormholeRelayer__factory.connect( - wormholeRelayerAddress, - sourceChainProvider - ); - const deliveryProviderAddress = - optionalParams?.deliveryProviderAddress || - (await sourceWormholeRelayer.getDefaultDeliveryProvider()); - const targetChainId = CHAINS[targetChain]; - const priceAndRefundInfo = ( - await sourceWormholeRelayer["quoteEVMDeliveryPrice(uint16,uint256,uint256,address)"]( - targetChainId, - optionalParams?.receiverValue || 0, - gasAmount, - deliveryProviderAddress - ) - ) - return priceAndRefundInfo; -} - -export async function getPrice( - sourceChain: ChainName, - targetChain: ChainName, - gasAmount: ethers.BigNumberish, - optionalParams?: GetPriceOptParams -): Promise { - const priceAndRefundInfo = await getPriceAndRefundInfo(sourceChain, targetChain, gasAmount, optionalParams); - return priceAndRefundInfo[0]; -} - - -export async function getWormholeRelayerInfo( - sourceChain: ChainName, - sourceTransaction: string, - infoRequest?: InfoRequestParams -): Promise { - const environment = infoRequest?.environment || "MAINNET"; - const sourceChainProvider = - infoRequest?.sourceChainProvider || - getDefaultProvider(environment, sourceChain); - if (!sourceChainProvider) - throw Error( - "No default RPC for this chain; pass in your own provider (as sourceChainProvider)" - ); - const receipt = await sourceChainProvider.getTransactionReceipt( - sourceTransaction - ); - if (!receipt) throw Error("Transaction has not been mined"); - const bridgeAddress = - CONTRACTS[environment][sourceChain].core; - const wormholeRelayerAddress = infoRequest?.wormholeRelayerAddresses?.get(sourceChain) || getWormholeRelayerAddress( - sourceChain, - environment - ); - if (!bridgeAddress || !wormholeRelayerAddress) { - throw Error( - `Invalid chain ID or network: Chain ${sourceChain}, ${environment}` - ); - } - const deliveryLog = getWormholeRelayerLog( - receipt, - bridgeAddress, - tryNativeToHexString(wormholeRelayerAddress, "ethereum"), - infoRequest?.wormholeRelayerWhMessageIndex - ? infoRequest.wormholeRelayerWhMessageIndex - : 0 - ); - - const { type, parsed } = parseWormholeLog(deliveryLog.log); - - const instruction = parsed as DeliveryInstruction; - - const targetChainId = instruction.targetChainId as ChainId; - if (!isChain(targetChainId)) throw Error(`Invalid Chain: ${targetChainId}`); - const targetChain = CHAIN_ID_TO_NAME[targetChainId]; - const targetChainProvider = - infoRequest?.targetChainProviders?.get(targetChain) || - getDefaultProvider(environment, targetChain); - - if (!targetChainProvider) { - throw Error( - "No default RPC for this chain; pass in your own provider (as targetChainProvider)" - ); - } - const [blockStartNumber, blockEndNumber] = - infoRequest?.targetChainBlockRanges?.get(targetChain) || - getBlockRange(targetChainProvider); - - const targetChainStatus = await getWormholeRelayerInfoBySourceSequence( - environment, - targetChain, - targetChainProvider, - sourceChain, - BigNumber.from(deliveryLog.sequence), - blockStartNumber, - blockEndNumber, - infoRequest?.wormholeRelayerAddresses?.get(targetChain) || getWormholeRelayerAddress( - targetChain, - environment - ) - ); - - return { - type: RelayerPayloadId.Delivery, - sourceChain: sourceChain, - sourceTransactionHash: sourceTransaction, - sourceDeliverySequenceNumber: BigNumber.from( - deliveryLog.sequence - ).toNumber(), - deliveryInstruction: instruction, - targetChainStatus, - }; - - -} - -export async function resendRaw( - signer: ethers.Signer, - sourceChain: ChainName, - targetChain: ChainName, - environment: Network, - vaaKey: VaaKey, - newGasLimit: BigNumber | number, - newReceiverValue: BigNumber | number, - deliveryProviderAddress: string, - overrides?: ethers.PayableOverrides -) { - const provider = signer.provider; - - if (!provider) throw Error("No provider on signer"); - - const wormholeRelayer = getWormholeRelayer(sourceChain, environment, signer); - - return wormholeRelayer.resendToEvm( - vaaKeyToVaaKeyStruct(vaaKey), - CHAINS[targetChain], - newReceiverValue, - newGasLimit, - deliveryProviderAddress, - overrides - ); -} - -export async function resend( - signer: ethers.Signer, - sourceChain: ChainName, - targetChain: ChainName, - environment: Network, - vaaKey: VaaKey, - newGasLimit: BigNumber | number, - newReceiverValue: BigNumber | number, - deliveryProviderAddress: string, - wormholeRPCs: string[], - overrides: ethers.PayableOverrides, - isNode?: boolean, -) { - const sourceChainId = CHAINS[sourceChain]; - const targetChainId = CHAINS[targetChain]; - const originalVAA = await getVAA(wormholeRPCs, vaaKey, isNode); - - if (!originalVAA) throw Error("orignal VAA not found"); - - const originalVAAparsed = parseWormholeRelayerSend( - parseVaa(Buffer.from(originalVAA)).payload - ); - if (!originalVAAparsed) throw Error("orignal VAA not a valid delivery VAA."); - - const [originalExecutionInfo,] = parseEVMExecutionInfoV1(originalVAAparsed.encodedExecutionInfo, 0); - const originalGasLimit = originalExecutionInfo.gasLimit; - const originalRefund = originalExecutionInfo.targetChainRefundPerGasUnused; - const originalReceiverValue = originalVAAparsed.requestedReceiverValue; - const originalTargetChain = originalVAAparsed.targetChainId; - - - - if (originalTargetChain != targetChainId) { - throw Error( - `Target chain of original VAA (${originalTargetChain}) does not match target chain of resend (${targetChainId})` - ); - } - - if (newReceiverValue < originalReceiverValue) { - throw Error( - `New receiver value too low. Minimum is ${originalReceiverValue.toString()}` - ); - } - - if (newGasLimit < originalGasLimit) { - throw Error( - `New gas limit too low. Minimum is ${originalReceiverValue.toString()}` - ); - } - - - - const wormholeRelayer = getWormholeRelayer(sourceChain, environment, signer); - const deliveryProvider = getDeliveryProvider( - deliveryProviderAddress, - signer.provider! - ); - - const [deliveryPrice, refundPerUnitGas]: [BigNumber, BigNumber] = await wormholeRelayer["quoteEVMDeliveryPrice(uint16,uint256,uint256,address)"](targetChainId, newReceiverValue || 0, newGasLimit, deliveryProviderAddress); - const value = await (overrides?.value || 0); - if(!deliveryPrice.eq(value)) { - throw new Error(`Expected a payment of ${deliveryPrice.toString()} wei; received ${value.toString()} wei`); - } - - - if (refundPerUnitGas < originalRefund) { - throw Error( - `New refund per unit gas too low. Minimum is ${originalRefund.toString()}.` - ); - } - - return resendRaw( - signer, - sourceChain, - targetChain, - environment, - vaaKey, - newGasLimit, - newReceiverValue, - deliveryProviderAddress, - overrides - ); -} - -export async function getVAA( - wormholeRPCs: string[], - vaaKey: VaaKey, - isNode?: boolean -): Promise { - - const vaa = await getSignedVAAWithRetry( - wormholeRPCs, - vaaKey.chainId! as ChainId, - vaaKey.emitterAddress!.toString("hex"), - vaaKey.sequence!.toBigInt().toString(), - isNode - ? { - transport: NodeHttpTransport(), - } - : {}, - 2000, - 4 - ); - - return vaa.vaaBytes; -} diff --git a/sdk/js/src/relayer/relayer/resend.ts b/sdk/js/src/relayer/relayer/resend.ts new file mode 100644 index 000000000..e3b965866 --- /dev/null +++ b/sdk/js/src/relayer/relayer/resend.ts @@ -0,0 +1,128 @@ +import { ethers, BigNumber } from "ethers"; +import { ChainName, CHAINS, Network } from "../../utils"; +import { parseVaa } from "../../vaa"; +import { getWormholeRelayer } from "../consts"; +import { + VaaKey, + parseWormholeRelayerSend, + parseEVMExecutionInfoV1, +} from "../structs"; +import { vaaKeyToVaaKeyStruct, getDeliveryProvider, getVAA } from "./helpers"; + +export async function resendRaw( + signer: ethers.Signer, + sourceChain: ChainName, + targetChain: ChainName, + environment: Network, + vaaKey: VaaKey, + newGasLimit: BigNumber | number, + newReceiverValue: BigNumber | number, + deliveryProviderAddress: string, + overrides?: ethers.PayableOverrides +) { + const provider = signer.provider; + + if (!provider) throw Error("No provider on signer"); + + const wormholeRelayer = getWormholeRelayer(sourceChain, environment, signer); + + return wormholeRelayer.resendToEvm( + vaaKeyToVaaKeyStruct(vaaKey), + CHAINS[targetChain], + newReceiverValue, + newGasLimit, + deliveryProviderAddress, + overrides + ); +} + +export async function resend( + signer: ethers.Signer, + sourceChain: ChainName, + targetChain: ChainName, + environment: Network, + vaaKey: VaaKey, + newGasLimit: BigNumber | number, + newReceiverValue: BigNumber | number, + deliveryProviderAddress: string, + wormholeRPCs: string[], + overrides: ethers.PayableOverrides, + isNode?: boolean +) { + const targetChainId = CHAINS[targetChain]; + const originalVAA = await getVAA(wormholeRPCs, vaaKey, isNode); + + if (!originalVAA) throw Error("orignal VAA not found"); + + const originalVAAparsed = parseWormholeRelayerSend( + parseVaa(Buffer.from(originalVAA)).payload + ); + if (!originalVAAparsed) throw Error("orignal VAA not a valid delivery VAA."); + + const [originalExecutionInfo] = parseEVMExecutionInfoV1( + originalVAAparsed.encodedExecutionInfo, + 0 + ); + const originalGasLimit = originalExecutionInfo.gasLimit; + const originalRefund = originalExecutionInfo.targetChainRefundPerGasUnused; + const originalReceiverValue = originalVAAparsed.requestedReceiverValue; + const originalTargetChain = originalVAAparsed.targetChainId; + + if (originalTargetChain != targetChainId) { + throw Error( + `Target chain of original VAA (${originalTargetChain}) does not match target chain of resend (${targetChainId})` + ); + } + + if (newReceiverValue < originalReceiverValue) { + throw Error( + `New receiver value too low. Minimum is ${originalReceiverValue.toString()}` + ); + } + + if (newGasLimit < originalGasLimit) { + throw Error( + `New gas limit too low. Minimum is ${originalReceiverValue.toString()}` + ); + } + + const wormholeRelayer = getWormholeRelayer(sourceChain, environment, signer); + const deliveryProvider = getDeliveryProvider( + deliveryProviderAddress, + signer.provider! + ); + + const [deliveryPrice, refundPerUnitGas]: [BigNumber, BigNumber] = + await wormholeRelayer[ + "quoteEVMDeliveryPrice(uint16,uint256,uint256,address)" + ]( + targetChainId, + newReceiverValue || 0, + newGasLimit, + deliveryProviderAddress + ); + const value = await (overrides?.value || 0); + if (!deliveryPrice.eq(value)) { + throw new Error( + `Expected a payment of ${deliveryPrice.toString()} wei; received ${value.toString()} wei` + ); + } + + if (refundPerUnitGas < originalRefund) { + throw Error( + `New refund per unit gas too low. Minimum is ${originalRefund.toString()}.` + ); + } + + return resendRaw( + signer, + sourceChain, + targetChain, + environment, + vaaKey, + newGasLimit, + newReceiverValue, + deliveryProviderAddress, + overrides + ); +} diff --git a/sdk/js/src/relayer/relayer/send.ts b/sdk/js/src/relayer/relayer/send.ts new file mode 100644 index 000000000..f36ff307e --- /dev/null +++ b/sdk/js/src/relayer/relayer/send.ts @@ -0,0 +1,114 @@ +import { ethers, BigNumber } from "ethers"; +import { ethers_contracts } from "../.."; +import { VaaKeyStruct } from "../../ethers-contracts/MockRelayerIntegration"; +import { + ChainId, + ChainName, + CHAINS, + Network, + tryNativeToHexString, +} from "../../utils"; +import { getWormholeRelayerAddress } from "../consts"; + +export type SendOptionalParams = { + environment?: Network; + receiverValue?: ethers.BigNumberish; + paymentForExtraReceiverValue?: ethers.BigNumberish; + additionalVaas?: [ + { + chainId?: ChainId; + emitterAddress: string; + sequenceNumber: ethers.BigNumberish; + } + ]; + deliveryProviderAddress?: string; + consistencyLevel?: ethers.BigNumberish; + refundChainId?: ChainId; + refundAddress?: string; + relayParameters?: ethers.BytesLike; +}; + +export async function sendToEvm( + signer: ethers.Signer, + sourceChain: ChainName, + targetChain: ChainName, + targetAddress: string, + payload: ethers.BytesLike, + gasLimit: BigNumber | number, + overrides?: ethers.PayableOverrides, + sendOptionalParams?: SendOptionalParams +): Promise { + const sourceChainId = CHAINS[sourceChain]; + const targetChainId = CHAINS[targetChain]; + + const environment = sendOptionalParams?.environment || "MAINNET"; + const wormholeRelayerAddress = getWormholeRelayerAddress( + sourceChain, + environment + ); + const sourceWormholeRelayer = + ethers_contracts.IWormholeRelayer__factory.connect( + wormholeRelayerAddress, + signer + ); + + const refundLocationExists = + sendOptionalParams?.refundChainId !== undefined && + sendOptionalParams?.refundAddress !== undefined; + const defaultDeliveryProviderAddress = + await sourceWormholeRelayer.getDefaultDeliveryProvider(); + + // Using the most general 'send' function in IWormholeRelayer + // Inputs: + // targetChainId, targetAddress, refundChainId, refundAddress, maxTransactionFee, receiverValue, payload, vaaKeys, + // consistencyLevel, deliveryProviderAddress, relayParameters + const [deliveryPrice]: [BigNumber, BigNumber] = await sourceWormholeRelayer[ + "quoteEVMDeliveryPrice(uint16,uint256,uint256,address)" + ]( + targetChainId, + sendOptionalParams?.receiverValue || 0, + gasLimit, + sendOptionalParams?.deliveryProviderAddress || + defaultDeliveryProviderAddress + ); + const value = await (overrides?.value || 0); + const totalPrice = deliveryPrice.add( + sendOptionalParams?.paymentForExtraReceiverValue || 0 + ); + if (!totalPrice.eq(value)) { + throw new Error( + `Expected a payment of ${totalPrice.toString()} wei; received ${value.toString()} wei` + ); + } + const tx = sourceWormholeRelayer.sendToEvm( + targetChainId, // targetChainId + targetAddress, // targetAddress + payload, + sendOptionalParams?.receiverValue || 0, // receiverValue + sendOptionalParams?.paymentForExtraReceiverValue || 0, // payment for extra receiverValue + gasLimit, + (refundLocationExists && sendOptionalParams?.refundChainId) || + sourceChainId, // refundChainId + (refundLocationExists && + sendOptionalParams?.refundAddress && + sendOptionalParams?.refundAddress) || + signer.getAddress(), // refundAddress + sendOptionalParams?.deliveryProviderAddress || + defaultDeliveryProviderAddress, // deliveryProviderAddress + sendOptionalParams?.additionalVaas + ? sendOptionalParams.additionalVaas.map( + (additionalVaa): VaaKeyStruct => ({ + chainId: additionalVaa.chainId || sourceChainId, + emitterAddress: Buffer.from( + tryNativeToHexString(additionalVaa.emitterAddress, "ethereum"), + "hex" + ), + sequence: BigNumber.from(additionalVaa.sequenceNumber || 0), + }) + ) + : [], // vaaKeys + sendOptionalParams?.consistencyLevel || 15, // consistencyLevel + overrides + ); + return tx; +}