first k8s deployment w/ relayer engine and redis

This commit is contained in:
Joe Howarth 2023-01-12 17:53:22 -07:00
parent 4a57a6c371
commit 03f09c9a1a
40 changed files with 24714 additions and 167 deletions

17
relayer_engine/Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM node:lts as builder
# Create app directory
WORKDIR /usr/src
# COPY package.json package-lock.json /src/
# ENV NODE_ENV=production
# RUN npm ci
# Install app dependencies
# COPY . /src
COPY node_modules .
COPY . .
CMD [ "npm", "run", "k8s-testnet" ]

4
relayer_engine/build.sh Normal file
View File

@ -0,0 +1,4 @@
#! /usr/bin/sh
cp -r ../sdk ./sdk
cp ../ethereum/ts-scripts/config/testnet/contracts.json .

View File

@ -0,0 +1,34 @@
{
"description": "This file contains the addresses for the contracts on each chain. If useLastRun is true, this file will be ignored, and the addresses will be taken from the lastrun.json of the deployment scripts.",
"useLastRun": false,
"relayProviders": [
{
"chainId": 6,
"address": "0x302f4D287204b8c383a79BA86Ad1fD1F81fb00E2"
},
{
"chainId": 14,
"address": "0x1A7d2aCBa5Ae7ad19e4DA7a512d369CC4aEFe66B"
}
],
"coreRelayers": [
{
"chainId": 6,
"address": "0xDED10060E839c497B8D71C3091f9f24dCe4110cF"
},
{
"chainId": 14,
"address": "0xDED10060E839c497B8D71C3091f9f24dCe4110cF"
}
],
"mockIntegrations": [
{
"chainId": 6,
"address": "0x62C4143AB8BEe162eBF6166a679A746cAE1D1385"
},
{
"chainId": 14,
"address": "0xbeD6e30Ff857944931F3eF0B26EdC0B616e92d57"
}
]
}

View File

@ -0,0 +1,3 @@
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install --set auth.enabled=false --set architecture=standalone redis bitnami/redis

View File

@ -0,0 +1,5 @@
bash deploy-redis.sh
kubectl apply -f ./spy-service.yaml
source ../../pkeys.sh
bash inject-private-keys.sh
kubectl apply -f ./simple-gr.yaml

View File

@ -0,0 +1,5 @@
#! /usr/bin/sh
kubectl create secret generic private-keys \
--from-literal=PRIVATE_KEYS_CHAIN_14=${PRIVATE_KEYS_CHAIN_14} \
--from-literal=PRIVATE_KEYS_CHAIN_6=${PRIVATE_KEYS_CHAIN_6}

View File

@ -0,0 +1,63 @@
apiVersion: v1
kind: Service
metadata:
name: simple-gr
namespace: default
labels:
app: simple-gr
spec:
type: LoadBalancer
selector:
app: simple-gr
ports:
- name: simple-gr
protocol: TCP
port: 8000
targetPort: 8000
# Do we actually need a service for the relayer??
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: simple-gr
namespace: default
labels:
app: simple-gr
spec:
selector:
matchLabels:
app: simple-gr
replicas: 1
template:
metadata:
labels:
app: simple-gr
spec:
restartPolicy: Always
containers:
- name: simple-gr
image: simple-gr:latest
imagePullPolicy: Never
resources:
requests:
cpu: 100m
memory: 100Mi
limits:
cpu: 900m
memory: 900Mi
ports:
- containerPort: 8000
name: simple-gr
env:
- name: PRIVATE_KEYS_CHAIN_14
valueFrom:
secretKeyRef:
name: private-keys
key: PRIVATE_KEYS_CHAIN_14
optional: false
- name: PRIVATE_KEYS_CHAIN_6
valueFrom:
secretKeyRef:
name: private-keys
key: PRIVATE_KEYS_CHAIN_6
optional: false

View File

@ -0,0 +1,56 @@
---
apiVersion: v1
kind: Service
metadata:
name: spy
namespace: default
labels:
app: spy
spec:
type: LoadBalancer
selector:
app: spy
ports:
- port: 7073
targetPort: 7073
name: spy
protocol: TCP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: spy
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: spy
template:
metadata:
labels:
app: spy
spec:
restartPolicy: Always
terminationGracePeriodSeconds: 40
containers:
- name: spy
image: ghcr.io/wormhole-foundation/guardiand:latest
args:
- spy
- --nodeKey
- /node.key
- --spyRPC
- "[::]:7073"
- --network
- /wormhole/testnet/2/1
- --bootstrap
- /dns4/wormhole-testnet-v2-bootstrap.certus.one/udp/8999/quic/p2p/12D3KooWAkB9ynDur1Jtoa97LBUp8RXdhzS5uHgAfdTquJbrbN7i
resources:
limits:
memory: 256Mi
cpu: 500m
requests:
memory: 128Mi
cpu: 250m

View File

@ -1,6 +1,7 @@
{
"mode": "BOTH",
"logLevel": "debug",
"storeType": "InMemory",
"readinessPort": 2000,
"numGuardians": 1,
"supportedChains": [

View File

@ -0,0 +1,21 @@
{
"mode": "BOTH",
"logLevel": "debug",
"storeType": "Redis",
"redisPort": 6379,
"redisHost": "redis-master.default.svc.cluster.local",
"readinessPort": 2000,
"numGuardians": 1,
"supportedChains": [
{
"chainId": 6,
"chainName": "Avalanche",
"nodeUrl": "https://api.avax-test.network/ext/bc/C/rpc"
},
{
"chainId": 14,
"chainName": "Celo",
"nodeUrl": "https://alfajores-forno.celo-testnet.org"
}
]
}

View File

@ -0,0 +1,6 @@
{
"privateKeys": {
"6": [""],
"14": [""]
}
}

View File

@ -0,0 +1,3 @@
{
"spyServiceHost": "spy:7073"
}

View File

@ -1,5 +1,6 @@
{
"mode": "BOTH",
"storeType": "InMemory",
"logLevel": "debug",
"readinessPort": 2000,
"supportedChains": [

File diff suppressed because it is too large Load Diff

View File

@ -5,19 +5,20 @@
"main": "lib/main.js",
"types": "lib/main.d.ts",
"scripts": {
"testnet": "ts-node src/main --testnet",
"testnet-watch": "nodemon src/main --testnet",
"tilt": "ts-node src/main --tilt",
"mainnet": "ts-node src/main --mainnet",
"k8s-testnet": "ts-node src/main --k8s-testnet",
"testnet": "bash build.sh; ts-node src/main --testnet",
"testnet-watch": "bash build.sh; nodemon src/main --testnet",
"tilt": "bash build.sh; ts-node src/main --tilt",
"mainnet": "bash build.sh; ts-node src/main --mainnet",
"typecheck": "tsc --noEmit",
"build": "tsc",
"build": "bash build.sh; tsc",
"watch": "tsc --watch",
"start": "ts-node src/main.ts"
},
"dependencies": {
"@certusone/wormhole-sdk": "^0.9.6",
"@improbable-eng/grpc-web-node-http-transport": "^0.15.0",
"@wormhole-foundation/relayer-engine": "file:../../relayer-engine",
"@wormhole-foundation/relayer-engine": "github:wormhole-foundation/relayer-engine#f6491e6e59e905a9c9590cd8b6f62a58f730b4d6",
"lodash": "^4.17.21",
"ts-retry": "^4.1.1"
},

3
relayer_engine/sdk/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
lib
node_modules
src/ethers-contracts

9651
relayer_engine/sdk/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
{
"name": "generic-relayer-sdk",
"version": "1.0.0",
"description": "",
"main": "networks.js",
"devDependencies": {
"@openzeppelin/contracts": "^4.7.3",
"@poanet/solidity-flattener": "^3.0.8",
"@types/chai": "^4.3.3",
"@types/mocha": "^9.1.1",
"chai": "^4.3.6",
"ethers": "^5.7.1",
"mocha": "^10.0.0",
"ts-mocha": "^10.0.0"
},
"scripts": {
"clean": "rm -rf node_modules src/ethers-contracts",
"build": "bash scripts/make_ethers_types.sh",
"test": "ts-mocha src/__tests__/*.ts --timeout 60000"
},
"author": "",
"license": "ISC",
"dependencies": {
"@certusone/wormhole-sdk": "^0.9.6",
"@improbable-eng/grpc-web-node-http-transport": "^0.15.0",
"@typechain/ethers-v5": "^10.1.0",
"dotenv": "^16.0.2",
"elliptic": "^6.5.4",
"jsonfile": "^6.1.0",
"solc": "^0.8.17",
"ts-node": "^10.9.1",
"typescript": "^4.8.3"
}
}

View File

@ -0,0 +1,6 @@
#!/bin/bash
SRC=$(dirname $0)/../../ethereum/build
DST=$(dirname $0)/../src/ethers-contracts
typechain --target=ethers-v5 --out-dir=$DST $SRC/*/*.json

3
relayer_engine/sdk/sdk/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
lib
node_modules
src/ethers-contracts

9651
relayer_engine/sdk/sdk/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
{
"name": "generic-relayer-sdk",
"version": "1.0.0",
"description": "",
"main": "networks.js",
"devDependencies": {
"@openzeppelin/contracts": "^4.7.3",
"@poanet/solidity-flattener": "^3.0.8",
"@types/chai": "^4.3.3",
"@types/mocha": "^9.1.1",
"chai": "^4.3.6",
"ethers": "^5.7.1",
"mocha": "^10.0.0",
"ts-mocha": "^10.0.0"
},
"scripts": {
"clean": "rm -rf node_modules src/ethers-contracts",
"build": "bash scripts/make_ethers_types.sh",
"test": "ts-mocha src/__tests__/*.ts --timeout 60000"
},
"author": "",
"license": "ISC",
"dependencies": {
"@certusone/wormhole-sdk": "^0.9.6",
"@improbable-eng/grpc-web-node-http-transport": "^0.15.0",
"@typechain/ethers-v5": "^10.1.0",
"dotenv": "^16.0.2",
"elliptic": "^6.5.4",
"jsonfile": "^6.1.0",
"solc": "^0.8.17",
"ts-node": "^10.9.1",
"typescript": "^4.8.3"
}
}

View File

@ -0,0 +1,6 @@
#!/bin/bash
SRC=$(dirname $0)/../../ethereum/build
DST=$(dirname $0)/../src/ethers-contracts
typechain --target=ethers-v5 --out-dir=$DST $SRC/*/*.json

View File

@ -0,0 +1,222 @@
import { expect } from "chai";
import { ethers } from "ethers";
import {
BSC_FORGE_BROADCAST,
BSC_RPC,
WORMHOLE_RPCS,
ETH_FORGE_BROADCAST,
ETH_RPC,
DEPLOYER_PRIVATE_KEY,
ZERO_ADDRESS_BYTES,
TARGET_GAS_LIMIT,
} from "./helpers/consts";
import { RelayerArgs } from "./helpers/structs";
import {
makeCoreRelayerFromForgeBroadcast,
makeGasOracleFromForgeBroadcast,
makeMockRelayerIntegrationFromForgeBroadcast,
resolvePath,
} from "./helpers/utils";
import {
CHAIN_ID_BSC,
CHAIN_ID_ETH,
getSignedBatchVAAWithRetry,
tryNativeToUint8Array,
tryNativeToHexString,
} from "@certusone/wormhole-sdk";
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
describe("ETH <> BSC Generic Relayer Integration Test", () => {
const ethProvider = new ethers.providers.StaticJsonRpcProvider(ETH_RPC);
const bscProvider = new ethers.providers.StaticJsonRpcProvider(BSC_RPC);
// core relayers
const ethCoreRelayer = makeCoreRelayerFromForgeBroadcast(
resolvePath(ETH_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, ethProvider)
);
const bscCoreRelayer = makeCoreRelayerFromForgeBroadcast(
resolvePath(BSC_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, bscProvider)
);
// relayer integrators
const ethRelayerIntegrator = makeMockRelayerIntegrationFromForgeBroadcast(
resolvePath(ETH_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, ethProvider)
);
const bscRelayerIntegrator = makeMockRelayerIntegrationFromForgeBroadcast(
resolvePath(BSC_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, bscProvider)
);
// gas oracles
const ownedGasOracles = [
// eth
makeGasOracleFromForgeBroadcast(
resolvePath(ETH_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, ethProvider)
),
// bsc
makeGasOracleFromForgeBroadcast(
resolvePath(BSC_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, bscProvider)
),
];
const readonlyGasOracles = [
// eth
makeGasOracleFromForgeBroadcast(resolvePath(ETH_FORGE_BROADCAST), ethProvider),
// bsc
makeGasOracleFromForgeBroadcast(resolvePath(BSC_FORGE_BROADCAST), bscProvider),
];
const ethPrice = ethers.utils.parseUnits("2000.00", 8);
const bscPrice = ethers.utils.parseUnits("400.00", 8);
before("Setup Gas Oracle Prices And Register Relayer Contracts", async () => {
// now fetch gas prices from each provider
const gasPrices = await Promise.all(ownedGasOracles.map((oracle) => oracle.provider.getGasPrice()));
const updates = [
{
chainId: CHAIN_ID_ETH,
gasPrice: gasPrices.at(0)!,
nativeCurrencyPrice: ethPrice,
},
{
chainId: CHAIN_ID_BSC,
gasPrice: gasPrices.at(1)!,
nativeCurrencyPrice: bscPrice,
},
];
const oracleTxs = await Promise.all(
ownedGasOracles.map((oracle) => oracle.updatePrices(updates).then((tx: ethers.ContractTransaction) => tx.wait()))
);
// query the core relayer contracts to see if relayers have been registered
const registeredCoreRelayerOnBsc = await bscCoreRelayer.registeredRelayer(CHAIN_ID_ETH);
const registeredCoreRelayerOnEth = await ethCoreRelayer.registeredRelayer(CHAIN_ID_BSC);
// register the core relayer contracts
if (registeredCoreRelayerOnBsc == ZERO_ADDRESS_BYTES) {
await bscCoreRelayer
.registerChain(CHAIN_ID_ETH, tryNativeToUint8Array(ethCoreRelayer.address, CHAIN_ID_ETH))
.then((tx) => tx.wait());
}
if (registeredCoreRelayerOnEth == ZERO_ADDRESS_BYTES) {
await ethCoreRelayer
.registerChain(CHAIN_ID_BSC, tryNativeToUint8Array(bscCoreRelayer.address, CHAIN_ID_BSC))
.then((tx) => tx.wait());
}
});
describe("Send from Ethereum and Deliver to BSC", () => {
// batch Vaa payloads to relay to the target contract
let batchVaaPayloads: ethers.utils.BytesLike[] = [];
// save the batch VAA info
let batchToBscReceipt: ethers.ContractReceipt;
let batchVaaFromEth: ethers.utils.BytesLike;
it("Check Gas Oracles", async () => {
const chainIds = await Promise.all(readonlyGasOracles.map((oracle) => oracle.chainId()));
expect(chainIds.at(0)).is.not.undefined;
expect(chainIds.at(0)!).to.equal(CHAIN_ID_ETH);
expect(chainIds.at(1)).is.not.undefined;
expect(chainIds.at(1)!).to.equal(CHAIN_ID_BSC);
const ethPrices = await Promise.all(readonlyGasOracles.map((oracle) => oracle.gasPrice(CHAIN_ID_ETH)));
const bscPrices = await Promise.all(readonlyGasOracles.map((oracle) => oracle.gasPrice(CHAIN_ID_BSC)));
for (let i = 0; i < 2; ++i) {
expect(ethPrices.at(i)).is.not.undefined;
expect(ethPrices.at(i)?.toString()).to.equal("20000000000");
expect(bscPrices.at(i)).is.not.undefined;
expect(bscPrices.at(i)?.toString()).to.equal("20000000000");
}
});
it("Generate batch VAA with delivery instructions on Ethereum", async () => {
// estimate the relayer cost to relay a batch to BSC
const estimatedGasCost = await ethRelayerIntegrator.estimateRelayCosts(CHAIN_ID_BSC, TARGET_GAS_LIMIT);
// create an array of messages to deliver to the BSC target contract
batchVaaPayloads = [
ethers.utils.hexlify(ethers.utils.toUtf8Bytes("SuperCoolCrossChainStuff0")),
ethers.utils.hexlify(ethers.utils.toUtf8Bytes("SuperCoolCrossChainStuff1")),
ethers.utils.hexlify(ethers.utils.toUtf8Bytes("SuperCoolCrossChainStuff2")),
ethers.utils.hexlify(ethers.utils.toUtf8Bytes("SuperCoolCrossChainStuff3")),
];
const batchVaaConsistencyLevels = [15, 15, 15, 15];
// create relayerArgs interface to call the mock integration contract with
const relayerArgs: RelayerArgs = {
nonce: 69,
targetChainId: CHAIN_ID_BSC,
targetAddress: bscRelayerIntegrator.address,
targetGasLimit: TARGET_GAS_LIMIT,
consistencyLevel: batchVaaConsistencyLevels[0],
};
// call the mock integration contract and send the batch VAA
batchToBscReceipt = await ethRelayerIntegrator
.sendBatchToTargetChain(batchVaaPayloads, batchVaaConsistencyLevels, relayerArgs, {
value: estimatedGasCost,
})
.then((tx) => tx.wait());
});
it("Fetch batch VAA from Ethereum", async () => {
// fetch the batch VAA with getSignedBatchVAAWithRetry
const batchVaaRes = await getSignedBatchVAAWithRetry(
WORMHOLE_RPCS,
CHAIN_ID_ETH,
batchToBscReceipt.transactionHash,
{
transport: NodeHttpTransport(),
}
);
batchVaaFromEth = batchVaaRes.batchVaaBytes;
});
it("Wait for off-chain relayer to deliver the batch VAA to BSC", async () => {
// parse the batch VAA
const parsedBatch = await ethRelayerIntegrator.parseWormholeBatch(batchVaaFromEth);
// Check to see if the batch VAA was delivered by querying the contract
// for the first payload sent in the batch.
let isBatchDelivered: boolean = false;
const targetVm3 = await ethRelayerIntegrator.parseWormholeObservation(parsedBatch.observations[0]);
while (!isBatchDelivered) {
// query the contract to see if the batch was delivered
const storedPayload = await bscRelayerIntegrator.getPayload(targetVm3.hash);
if (storedPayload == targetVm3.payload) {
isBatchDelivered = true;
}
}
// confirm that the remaining payloads are stored in the contract
for (const observation of parsedBatch.observations) {
const vm3 = await bscRelayerIntegrator.parseWormholeObservation(observation);
// skip delivery instructions VM
if (vm3.emitterAddress == "0x" + tryNativeToHexString(ethCoreRelayer.address, CHAIN_ID_ETH)) {
continue;
}
// query the contract to see if the batch was delivered
const storedPayload = await bscRelayerIntegrator.getPayload(vm3.hash);
expect(storedPayload).to.equal(vm3.payload);
// clear the payload from the mock integration contract
await bscRelayerIntegrator.clearPayload(vm3.hash);
const emptyStoredPayload = await bscRelayerIntegrator.getPayload(vm3.hash);
expect(emptyStoredPayload).to.equal("0x");
}
});
});
});

View File

@ -0,0 +1,27 @@
// rpc
export const ETH_RPC = "http://localhost:8545";
export const BSC_RPC = "http://localhost:8546";
export const WORMHOLE_RPCS = ["http://localhost:7071"];
export const ETH_EVM_CHAINID = 1337;
export const BSC_EVM_CHAINID = 1397;
// evm wallets
export const DEPLOYER_PRIVATE_KEY = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"; // account 0
export const EVM_PRIVATE_KEY = "0x6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c"; // account 2
// io
export const ETHEREUM_ROOT = `${__dirname}/../../../../ethereum`; // holy parent directories, batman
export const ETH_FORGE_BROADCAST = `${ETHEREUM_ROOT}/broadcast/deploy_contracts.sol/${ETH_EVM_CHAINID}/run-latest.json`;
export const BSC_FORGE_BROADCAST = `${ETHEREUM_ROOT}/broadcast/deploy_contracts.sol/${BSC_EVM_CHAINID}/run-latest.json`;
// misc
export const ZERO_ADDRESS_BYTES = "0x0000000000000000000000000000000000000000000000000000000000000000";
// the amount of gas that the target relayer contract will invoke the wormhole receiver with
export const TARGET_GAS_LIMIT = 500000; // evm gas units
// wormhole event ABIs
export const WORMHOLE_MESSAGE_EVENT_ABI = [
"event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel)",
];

View File

@ -0,0 +1,24 @@
import { ethers } from "ethers";
export interface RelayerArgs {
nonce: number;
targetChainId: number;
targetAddress: string;
targetGasLimit: number;
consistencyLevel: number;
}
export interface TargetDeliveryParameters {
encodedVM: ethers.utils.BytesLike;
deliveryIndex: number;
targetCallGasOverride: ethers.BigNumber;
}
export interface DeliveryStatus {
payloadId: number;
batchHash: ethers.utils.BytesLike;
emitterAddress: ethers.utils.BytesLike;
sequence: number;
deliveryCount: number;
deliverySuccess: number;
}

View File

@ -0,0 +1,136 @@
import {ethers} from "ethers";
import fs from "fs";
import path from "path";
import {DeliveryStatus} from "./structs";
import {WORMHOLE_RPCS, WORMHOLE_MESSAGE_EVENT_ABI} from "./consts";
import {NodeHttpTransport} from "@improbable-eng/grpc-web-node-http-transport";
import {ChainId, getEmitterAddressEth, getSignedVAAWithRetry} from "@certusone/wormhole-sdk";
import {
GasOracle,
GasOracle__factory,
MockRelayerIntegration,
MockRelayerIntegration__factory,
CoreRelayer,
CoreRelayer__factory,
} from "../../";
export function makeGasOracleFromForgeBroadcast(
broadcastPath: string,
signerOrProvider: ethers.Signer | ethers.providers.Provider
): GasOracle {
const address = getContractAddressFromForgeBroadcast(broadcastPath, "GasOracle");
return GasOracle__factory.connect(address, signerOrProvider);
}
export function makeMockRelayerIntegrationFromForgeBroadcast(
broadcastPath: string,
signerOrProvider: ethers.Signer | ethers.providers.Provider
): MockRelayerIntegration {
const address = getContractAddressFromForgeBroadcast(broadcastPath, "MockRelayerIntegration");
return MockRelayerIntegration__factory.connect(address, signerOrProvider);
}
export function makeCoreRelayerFromForgeBroadcast(
broadcastPath: string,
signerOrProvider: ethers.Signer | ethers.providers.Provider
): CoreRelayer {
const address = getContractAddressFromForgeBroadcast(broadcastPath, "ERC1967Proxy");
return CoreRelayer__factory.connect(address, signerOrProvider);
}
function readForgeBroadcast(broadcastPath: string): any {
if (!fs.existsSync(broadcastPath)) {
throw new Error("broadcastPath does not exist");
}
return JSON.parse(fs.readFileSync(broadcastPath, "utf8"));
}
function getContractAddressFromForgeBroadcast(broadcastPath: string, contractName: string) {
const transactions: any[] = readForgeBroadcast(broadcastPath).transactions;
const result = transactions.find((tx) => tx.contractName == contractName && tx.transactionType == "CREATE");
if (result == undefined) {
throw new Error("transaction.find == undefined");
}
return result.contractAddress;
}
export function resolvePath(fp: string) {
return path.resolve(fp);
}
export async function parseWormholeEventsFromReceipt(
receipt: ethers.ContractReceipt
): Promise<ethers.utils.LogDescription[]> {
// create the wormhole message interface
const wormholeMessageInterface = new ethers.utils.Interface(WORMHOLE_MESSAGE_EVENT_ABI);
// loop through the logs and parse the events that were emitted
const logDescriptions: ethers.utils.LogDescription[] = await Promise.all(
receipt.logs.map(async (log) => {
return wormholeMessageInterface.parseLog(log);
})
);
return logDescriptions;
}
export async function getSignedVaaFromReceiptOnEth(
receipt: ethers.ContractReceipt,
emitterChainId: ChainId,
contractAddress: ethers.BytesLike
): Promise<Uint8Array> {
const messageEvents = await parseWormholeEventsFromReceipt(receipt);
// grab the sequence from the parsed message log
if (messageEvents.length !== 1) {
throw Error("more than one message found in log");
}
const sequence = messageEvents[0].args.sequence;
// fetch the signed VAA
const result = await getSignedVAAWithRetry(
WORMHOLE_RPCS,
emitterChainId,
getEmitterAddressEth(contractAddress),
sequence.toString(),
{
transport: NodeHttpTransport(),
}
);
return result.vaaBytes;
}
export function parseDeliveryStatusVaa(payload: ethers.BytesLike): DeliveryStatus {
// confirm that the payload is formatted correctly
let index: number = 0;
// interface that we will parse the bytes into
let deliveryStatus: DeliveryStatus = {} as DeliveryStatus;
// grab the payloadID = 2
deliveryStatus.payloadId = parseInt(ethers.utils.hexDataSlice(payload, index, index + 1));
index += 1;
// delivery batch hash
deliveryStatus.batchHash = ethers.utils.hexDataSlice(payload, index, index + 32);
index += 32;
// deliveryId emitter address
deliveryStatus.emitterAddress = ethers.utils.hexDataSlice(payload, index, index + 32);
index += 32;
// deliveryId sequence
deliveryStatus.sequence = parseInt(ethers.utils.hexDataSlice(payload, index, index + 8));
index += 8;
// delivery count
deliveryStatus.deliveryCount = parseInt(ethers.utils.hexDataSlice(payload, index, index + 2));
index += 2;
// grab the success boolean
deliveryStatus.deliverySuccess = parseInt(ethers.utils.hexDataSlice(payload, index, index + 1));
index += 1;
return deliveryStatus;
}

View File

@ -0,0 +1,203 @@
import { expect } from "chai";
import { ethers } from "ethers";
import {
BSC_FORGE_BROADCAST,
BSC_RPC,
WORMHOLE_RPCS,
ETH_FORGE_BROADCAST,
ETH_RPC,
DEPLOYER_PRIVATE_KEY,
ZERO_ADDRESS_BYTES,
TARGET_GAS_LIMIT,
} from "../__tests__/helpers/consts";
import { DeliveryStatus, RelayerArgs, TargetDeliveryParameters } from "../__tests__/helpers/structs";
import {
makeCoreRelayerFromForgeBroadcast,
makeGasOracleFromForgeBroadcast,
makeMockRelayerIntegrationFromForgeBroadcast,
resolvePath,
getSignedVaaFromReceiptOnEth,
parseDeliveryStatusVaa,
} from "../__tests__/helpers/utils";
import { CHAIN_ID_BSC, CHAIN_ID_ETH, tryNativeToHexString, getSignedBatchVAAWithRetry } from "@certusone/wormhole-sdk";
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
async function main() {
const ethProvider = new ethers.providers.StaticJsonRpcProvider(ETH_RPC);
const bscProvider = new ethers.providers.StaticJsonRpcProvider(BSC_RPC);
// core relayers
const ethCoreRelayer = makeCoreRelayerFromForgeBroadcast(
resolvePath(ETH_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, ethProvider)
);
const bscCoreRelayer = makeCoreRelayerFromForgeBroadcast(
resolvePath(BSC_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, bscProvider)
);
// relayer integrators
const ethRelayerIntegrator = makeMockRelayerIntegrationFromForgeBroadcast(
resolvePath(ETH_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, ethProvider)
);
const bscRelayerIntegrator = makeMockRelayerIntegrationFromForgeBroadcast(
resolvePath(BSC_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, bscProvider)
);
// gas oracles
const ownedGasOracles = [
// eth
makeGasOracleFromForgeBroadcast(
resolvePath(ETH_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, ethProvider)
),
// bsc
makeGasOracleFromForgeBroadcast(
resolvePath(BSC_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, bscProvider)
),
];
// setup gas oracles and register if needed
{
const ethPrice = ethers.utils.parseUnits("2000.00", 8);
const bscPrice = ethers.utils.parseUnits("400.00", 8);
// now fetch gas prices from each provider
const gasPrices = await Promise.all(ownedGasOracles.map((oracle) => oracle.provider.getGasPrice()));
const updates = [
{
chainId: CHAIN_ID_ETH,
gasPrice: gasPrices.at(0)!,
nativeCurrencyPrice: ethPrice,
},
{
chainId: CHAIN_ID_BSC,
gasPrice: gasPrices.at(1)!,
nativeCurrencyPrice: bscPrice,
},
];
const oracleTxs = await Promise.all(
ownedGasOracles.map((oracle) => oracle.updatePrices(updates).then((tx: ethers.ContractTransaction) => tx.wait()))
);
// query the core relayer contracts to see if relayers have been registered
const registeredCoreRelayerOnBsc = await bscCoreRelayer.registeredRelayer(CHAIN_ID_ETH);
const registeredCoreRelayerOnEth = await ethCoreRelayer.registeredRelayer(CHAIN_ID_BSC);
// register the core relayer contracts
if (registeredCoreRelayerOnBsc == ZERO_ADDRESS_BYTES) {
const bscRegistrationTx = await bscCoreRelayer.registerChain(
CHAIN_ID_ETH,
"0x" + tryNativeToHexString(ethCoreRelayer.address, CHAIN_ID_ETH)
);
await bscRegistrationTx.wait();
}
if (registeredCoreRelayerOnEth == ZERO_ADDRESS_BYTES) {
const ethRegistrationTx = await ethCoreRelayer.registerChain(
CHAIN_ID_BSC,
"0x" + tryNativeToHexString(bscCoreRelayer.address, CHAIN_ID_BSC)
);
await ethRegistrationTx.wait();
}
// Query the mock relayer integration contracts to see if trusted mock relayer
// integration contracts have been registered.
const trustedSenderOnBsc = await bscRelayerIntegrator.trustedSender(CHAIN_ID_ETH);
const trustedSenderOnEth = await ethRelayerIntegrator.trustedSender(CHAIN_ID_BSC);
// register the trusted mock relayer integration contracts
if (trustedSenderOnBsc == ZERO_ADDRESS_BYTES) {
const bscRegistrationTx = await bscRelayerIntegrator.registerTrustedSender(
CHAIN_ID_ETH,
"0x" + tryNativeToHexString(ethRelayerIntegrator.address, CHAIN_ID_ETH)
);
await bscRegistrationTx.wait();
}
if (trustedSenderOnEth == ZERO_ADDRESS_BYTES) {
const ethRegistrationTx = await ethRelayerIntegrator.registerTrustedSender(
CHAIN_ID_BSC,
"0x" + tryNativeToHexString(bscRelayerIntegrator.address, CHAIN_ID_BSC)
);
await ethRegistrationTx.wait();
}
}
{
// batch Vaa payloads to relay to the target contract
let batchVaaPayloads: ethers.utils.BytesLike[] = [];
// REVIEW: these should be removed when the off-chain relayer is implemented
let batchToBscReceipt: ethers.ContractReceipt;
let targetDeliveryParamsOnBsc: TargetDeliveryParameters = {} as TargetDeliveryParameters;
{
// estimate the relayer cost to relay a batch to BSC
const estimatedGasCost: ethers.BigNumber = await ethRelayerIntegrator.estimateRelayCosts(
CHAIN_ID_BSC,
TARGET_GAS_LIMIT
);
// create an array of messages to deliver to the BSC target contract
batchVaaPayloads = [
ethers.utils.hexlify(ethers.utils.toUtf8Bytes("SuperCoolCrossChainStuff0")),
ethers.utils.hexlify(ethers.utils.toUtf8Bytes("SuperCoolCrossChainStuff1")),
ethers.utils.hexlify(ethers.utils.toUtf8Bytes("SuperCoolCrossChainStuff2")),
ethers.utils.hexlify(ethers.utils.toUtf8Bytes("SuperCoolCrossChainStuff3")),
];
const batchVaaConsistencyLevels = [15, 15, 15, 15];
// create relayerArgs interface to call the mock integration contract with
const relayerArgs: RelayerArgs = {
nonce: 69,
targetChainId: CHAIN_ID_BSC,
targetAddress: bscRelayerIntegrator.address,
targetGasLimit: TARGET_GAS_LIMIT,
consistencyLevel: batchVaaConsistencyLevels[0],
deliveryListIndices: [] as number[], // no indices specified for full batch delivery
};
// call the mock integration contract and send the batch VAA
const tx = await ethRelayerIntegrator.sendBatchToTargetChain(
batchVaaPayloads,
batchVaaConsistencyLevels,
relayerArgs,
{
value: estimatedGasCost,
}
);
batchToBscReceipt = await tx.wait();
console.log("emitterChain", CHAIN_ID_ETH, "emitterAddress", ethCoreRelayer.address);
console.log("transaction", batchToBscReceipt.transactionHash);
}
{
// fetch the batch VAA with getSignedBatchVAAWithRetry
const batchVaaRes = await getSignedBatchVAAWithRetry(
WORMHOLE_RPCS,
CHAIN_ID_ETH,
batchToBscReceipt.transactionHash,
{
transport: NodeHttpTransport(),
}
);
const batchVaaFromEth: ethers.utils.BytesLike = batchVaaRes.batchVaaBytes;
console.log("vaa", Buffer.from(batchVaaFromEth as Uint8Array).toString("hex"));
// parse the batch VAA
const parsedBatch = await ethRelayerIntegrator.parseBatchVM(batchVaaFromEth);
console.log("parsed", parsedBatch);
}
}
}
main();

View File

@ -0,0 +1,8 @@
export type { CoreRelayer } from "./ethers-contracts/CoreRelayer"
export { CoreRelayer__factory } from "./ethers-contracts/factories/CoreRelayer__factory"
export type { MockRelayerIntegration } from "./ethers-contracts/MockRelayerIntegration"
export { MockRelayerIntegration__factory } from "./ethers-contracts/factories/MockRelayerIntegration__factory"
export type { IWormhole } from "./ethers-contracts/IWormhole"
export { IWormhole__factory } from "./ethers-contracts/factories/IWormhole__factory"
export type { RelayProvider } from "./ethers-contracts/RelayProvider"
export { RelayProvider__factory } from "./ethers-contracts/factories/RelayProvider__factory"

View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"types": ["mocha", "chai"],
"typeRoots": ["./node_modules/@types"],
"lib": ["es2020"],
"module": "CommonJS",
"target": "es2020",
"esModuleInterop": true,
"strict": true,
"resolveJsonModule": true,
"moduleResolution": "node"
}
}

View File

@ -0,0 +1,222 @@
import { expect } from "chai";
import { ethers } from "ethers";
import {
BSC_FORGE_BROADCAST,
BSC_RPC,
WORMHOLE_RPCS,
ETH_FORGE_BROADCAST,
ETH_RPC,
DEPLOYER_PRIVATE_KEY,
ZERO_ADDRESS_BYTES,
TARGET_GAS_LIMIT,
} from "./helpers/consts";
import { RelayerArgs } from "./helpers/structs";
import {
makeCoreRelayerFromForgeBroadcast,
makeGasOracleFromForgeBroadcast,
makeMockRelayerIntegrationFromForgeBroadcast,
resolvePath,
} from "./helpers/utils";
import {
CHAIN_ID_BSC,
CHAIN_ID_ETH,
getSignedBatchVAAWithRetry,
tryNativeToUint8Array,
tryNativeToHexString,
} from "@certusone/wormhole-sdk";
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
describe("ETH <> BSC Generic Relayer Integration Test", () => {
const ethProvider = new ethers.providers.StaticJsonRpcProvider(ETH_RPC);
const bscProvider = new ethers.providers.StaticJsonRpcProvider(BSC_RPC);
// core relayers
const ethCoreRelayer = makeCoreRelayerFromForgeBroadcast(
resolvePath(ETH_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, ethProvider)
);
const bscCoreRelayer = makeCoreRelayerFromForgeBroadcast(
resolvePath(BSC_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, bscProvider)
);
// relayer integrators
const ethRelayerIntegrator = makeMockRelayerIntegrationFromForgeBroadcast(
resolvePath(ETH_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, ethProvider)
);
const bscRelayerIntegrator = makeMockRelayerIntegrationFromForgeBroadcast(
resolvePath(BSC_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, bscProvider)
);
// gas oracles
const ownedGasOracles = [
// eth
makeGasOracleFromForgeBroadcast(
resolvePath(ETH_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, ethProvider)
),
// bsc
makeGasOracleFromForgeBroadcast(
resolvePath(BSC_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, bscProvider)
),
];
const readonlyGasOracles = [
// eth
makeGasOracleFromForgeBroadcast(resolvePath(ETH_FORGE_BROADCAST), ethProvider),
// bsc
makeGasOracleFromForgeBroadcast(resolvePath(BSC_FORGE_BROADCAST), bscProvider),
];
const ethPrice = ethers.utils.parseUnits("2000.00", 8);
const bscPrice = ethers.utils.parseUnits("400.00", 8);
before("Setup Gas Oracle Prices And Register Relayer Contracts", async () => {
// now fetch gas prices from each provider
const gasPrices = await Promise.all(ownedGasOracles.map((oracle) => oracle.provider.getGasPrice()));
const updates = [
{
chainId: CHAIN_ID_ETH,
gasPrice: gasPrices.at(0)!,
nativeCurrencyPrice: ethPrice,
},
{
chainId: CHAIN_ID_BSC,
gasPrice: gasPrices.at(1)!,
nativeCurrencyPrice: bscPrice,
},
];
const oracleTxs = await Promise.all(
ownedGasOracles.map((oracle) => oracle.updatePrices(updates).then((tx: ethers.ContractTransaction) => tx.wait()))
);
// query the core relayer contracts to see if relayers have been registered
const registeredCoreRelayerOnBsc = await bscCoreRelayer.registeredRelayer(CHAIN_ID_ETH);
const registeredCoreRelayerOnEth = await ethCoreRelayer.registeredRelayer(CHAIN_ID_BSC);
// register the core relayer contracts
if (registeredCoreRelayerOnBsc == ZERO_ADDRESS_BYTES) {
await bscCoreRelayer
.registerChain(CHAIN_ID_ETH, tryNativeToUint8Array(ethCoreRelayer.address, CHAIN_ID_ETH))
.then((tx) => tx.wait());
}
if (registeredCoreRelayerOnEth == ZERO_ADDRESS_BYTES) {
await ethCoreRelayer
.registerChain(CHAIN_ID_BSC, tryNativeToUint8Array(bscCoreRelayer.address, CHAIN_ID_BSC))
.then((tx) => tx.wait());
}
});
describe("Send from Ethereum and Deliver to BSC", () => {
// batch Vaa payloads to relay to the target contract
let batchVaaPayloads: ethers.utils.BytesLike[] = [];
// save the batch VAA info
let batchToBscReceipt: ethers.ContractReceipt;
let batchVaaFromEth: ethers.utils.BytesLike;
it("Check Gas Oracles", async () => {
const chainIds = await Promise.all(readonlyGasOracles.map((oracle) => oracle.chainId()));
expect(chainIds.at(0)).is.not.undefined;
expect(chainIds.at(0)!).to.equal(CHAIN_ID_ETH);
expect(chainIds.at(1)).is.not.undefined;
expect(chainIds.at(1)!).to.equal(CHAIN_ID_BSC);
const ethPrices = await Promise.all(readonlyGasOracles.map((oracle) => oracle.gasPrice(CHAIN_ID_ETH)));
const bscPrices = await Promise.all(readonlyGasOracles.map((oracle) => oracle.gasPrice(CHAIN_ID_BSC)));
for (let i = 0; i < 2; ++i) {
expect(ethPrices.at(i)).is.not.undefined;
expect(ethPrices.at(i)?.toString()).to.equal("20000000000");
expect(bscPrices.at(i)).is.not.undefined;
expect(bscPrices.at(i)?.toString()).to.equal("20000000000");
}
});
it("Generate batch VAA with delivery instructions on Ethereum", async () => {
// estimate the relayer cost to relay a batch to BSC
const estimatedGasCost = await ethRelayerIntegrator.estimateRelayCosts(CHAIN_ID_BSC, TARGET_GAS_LIMIT);
// create an array of messages to deliver to the BSC target contract
batchVaaPayloads = [
ethers.utils.hexlify(ethers.utils.toUtf8Bytes("SuperCoolCrossChainStuff0")),
ethers.utils.hexlify(ethers.utils.toUtf8Bytes("SuperCoolCrossChainStuff1")),
ethers.utils.hexlify(ethers.utils.toUtf8Bytes("SuperCoolCrossChainStuff2")),
ethers.utils.hexlify(ethers.utils.toUtf8Bytes("SuperCoolCrossChainStuff3")),
];
const batchVaaConsistencyLevels = [15, 15, 15, 15];
// create relayerArgs interface to call the mock integration contract with
const relayerArgs: RelayerArgs = {
nonce: 69,
targetChainId: CHAIN_ID_BSC,
targetAddress: bscRelayerIntegrator.address,
targetGasLimit: TARGET_GAS_LIMIT,
consistencyLevel: batchVaaConsistencyLevels[0],
};
// call the mock integration contract and send the batch VAA
batchToBscReceipt = await ethRelayerIntegrator
.sendBatchToTargetChain(batchVaaPayloads, batchVaaConsistencyLevels, relayerArgs, {
value: estimatedGasCost,
})
.then((tx) => tx.wait());
});
it("Fetch batch VAA from Ethereum", async () => {
// fetch the batch VAA with getSignedBatchVAAWithRetry
const batchVaaRes = await getSignedBatchVAAWithRetry(
WORMHOLE_RPCS,
CHAIN_ID_ETH,
batchToBscReceipt.transactionHash,
{
transport: NodeHttpTransport(),
}
);
batchVaaFromEth = batchVaaRes.batchVaaBytes;
});
it("Wait for off-chain relayer to deliver the batch VAA to BSC", async () => {
// parse the batch VAA
const parsedBatch = await ethRelayerIntegrator.parseWormholeBatch(batchVaaFromEth);
// Check to see if the batch VAA was delivered by querying the contract
// for the first payload sent in the batch.
let isBatchDelivered: boolean = false;
const targetVm3 = await ethRelayerIntegrator.parseWormholeObservation(parsedBatch.observations[0]);
while (!isBatchDelivered) {
// query the contract to see if the batch was delivered
const storedPayload = await bscRelayerIntegrator.getPayload(targetVm3.hash);
if (storedPayload == targetVm3.payload) {
isBatchDelivered = true;
}
}
// confirm that the remaining payloads are stored in the contract
for (const observation of parsedBatch.observations) {
const vm3 = await bscRelayerIntegrator.parseWormholeObservation(observation);
// skip delivery instructions VM
if (vm3.emitterAddress == "0x" + tryNativeToHexString(ethCoreRelayer.address, CHAIN_ID_ETH)) {
continue;
}
// query the contract to see if the batch was delivered
const storedPayload = await bscRelayerIntegrator.getPayload(vm3.hash);
expect(storedPayload).to.equal(vm3.payload);
// clear the payload from the mock integration contract
await bscRelayerIntegrator.clearPayload(vm3.hash);
const emptyStoredPayload = await bscRelayerIntegrator.getPayload(vm3.hash);
expect(emptyStoredPayload).to.equal("0x");
}
});
});
});

View File

@ -0,0 +1,27 @@
// rpc
export const ETH_RPC = "http://localhost:8545";
export const BSC_RPC = "http://localhost:8546";
export const WORMHOLE_RPCS = ["http://localhost:7071"];
export const ETH_EVM_CHAINID = 1337;
export const BSC_EVM_CHAINID = 1397;
// evm wallets
export const DEPLOYER_PRIVATE_KEY = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"; // account 0
export const EVM_PRIVATE_KEY = "0x6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c"; // account 2
// io
export const ETHEREUM_ROOT = `${__dirname}/../../../../ethereum`; // holy parent directories, batman
export const ETH_FORGE_BROADCAST = `${ETHEREUM_ROOT}/broadcast/deploy_contracts.sol/${ETH_EVM_CHAINID}/run-latest.json`;
export const BSC_FORGE_BROADCAST = `${ETHEREUM_ROOT}/broadcast/deploy_contracts.sol/${BSC_EVM_CHAINID}/run-latest.json`;
// misc
export const ZERO_ADDRESS_BYTES = "0x0000000000000000000000000000000000000000000000000000000000000000";
// the amount of gas that the target relayer contract will invoke the wormhole receiver with
export const TARGET_GAS_LIMIT = 500000; // evm gas units
// wormhole event ABIs
export const WORMHOLE_MESSAGE_EVENT_ABI = [
"event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel)",
];

View File

@ -0,0 +1,24 @@
import { ethers } from "ethers";
export interface RelayerArgs {
nonce: number;
targetChainId: number;
targetAddress: string;
targetGasLimit: number;
consistencyLevel: number;
}
export interface TargetDeliveryParameters {
encodedVM: ethers.utils.BytesLike;
deliveryIndex: number;
targetCallGasOverride: ethers.BigNumber;
}
export interface DeliveryStatus {
payloadId: number;
batchHash: ethers.utils.BytesLike;
emitterAddress: ethers.utils.BytesLike;
sequence: number;
deliveryCount: number;
deliverySuccess: number;
}

View File

@ -0,0 +1,136 @@
import {ethers} from "ethers";
import fs from "fs";
import path from "path";
import {DeliveryStatus} from "./structs";
import {WORMHOLE_RPCS, WORMHOLE_MESSAGE_EVENT_ABI} from "./consts";
import {NodeHttpTransport} from "@improbable-eng/grpc-web-node-http-transport";
import {ChainId, getEmitterAddressEth, getSignedVAAWithRetry} from "@certusone/wormhole-sdk";
import {
GasOracle,
GasOracle__factory,
MockRelayerIntegration,
MockRelayerIntegration__factory,
CoreRelayer,
CoreRelayer__factory,
} from "../../";
export function makeGasOracleFromForgeBroadcast(
broadcastPath: string,
signerOrProvider: ethers.Signer | ethers.providers.Provider
): GasOracle {
const address = getContractAddressFromForgeBroadcast(broadcastPath, "GasOracle");
return GasOracle__factory.connect(address, signerOrProvider);
}
export function makeMockRelayerIntegrationFromForgeBroadcast(
broadcastPath: string,
signerOrProvider: ethers.Signer | ethers.providers.Provider
): MockRelayerIntegration {
const address = getContractAddressFromForgeBroadcast(broadcastPath, "MockRelayerIntegration");
return MockRelayerIntegration__factory.connect(address, signerOrProvider);
}
export function makeCoreRelayerFromForgeBroadcast(
broadcastPath: string,
signerOrProvider: ethers.Signer | ethers.providers.Provider
): CoreRelayer {
const address = getContractAddressFromForgeBroadcast(broadcastPath, "ERC1967Proxy");
return CoreRelayer__factory.connect(address, signerOrProvider);
}
function readForgeBroadcast(broadcastPath: string): any {
if (!fs.existsSync(broadcastPath)) {
throw new Error("broadcastPath does not exist");
}
return JSON.parse(fs.readFileSync(broadcastPath, "utf8"));
}
function getContractAddressFromForgeBroadcast(broadcastPath: string, contractName: string) {
const transactions: any[] = readForgeBroadcast(broadcastPath).transactions;
const result = transactions.find((tx) => tx.contractName == contractName && tx.transactionType == "CREATE");
if (result == undefined) {
throw new Error("transaction.find == undefined");
}
return result.contractAddress;
}
export function resolvePath(fp: string) {
return path.resolve(fp);
}
export async function parseWormholeEventsFromReceipt(
receipt: ethers.ContractReceipt
): Promise<ethers.utils.LogDescription[]> {
// create the wormhole message interface
const wormholeMessageInterface = new ethers.utils.Interface(WORMHOLE_MESSAGE_EVENT_ABI);
// loop through the logs and parse the events that were emitted
const logDescriptions: ethers.utils.LogDescription[] = await Promise.all(
receipt.logs.map(async (log) => {
return wormholeMessageInterface.parseLog(log);
})
);
return logDescriptions;
}
export async function getSignedVaaFromReceiptOnEth(
receipt: ethers.ContractReceipt,
emitterChainId: ChainId,
contractAddress: ethers.BytesLike
): Promise<Uint8Array> {
const messageEvents = await parseWormholeEventsFromReceipt(receipt);
// grab the sequence from the parsed message log
if (messageEvents.length !== 1) {
throw Error("more than one message found in log");
}
const sequence = messageEvents[0].args.sequence;
// fetch the signed VAA
const result = await getSignedVAAWithRetry(
WORMHOLE_RPCS,
emitterChainId,
getEmitterAddressEth(contractAddress),
sequence.toString(),
{
transport: NodeHttpTransport(),
}
);
return result.vaaBytes;
}
export function parseDeliveryStatusVaa(payload: ethers.BytesLike): DeliveryStatus {
// confirm that the payload is formatted correctly
let index: number = 0;
// interface that we will parse the bytes into
let deliveryStatus: DeliveryStatus = {} as DeliveryStatus;
// grab the payloadID = 2
deliveryStatus.payloadId = parseInt(ethers.utils.hexDataSlice(payload, index, index + 1));
index += 1;
// delivery batch hash
deliveryStatus.batchHash = ethers.utils.hexDataSlice(payload, index, index + 32);
index += 32;
// deliveryId emitter address
deliveryStatus.emitterAddress = ethers.utils.hexDataSlice(payload, index, index + 32);
index += 32;
// deliveryId sequence
deliveryStatus.sequence = parseInt(ethers.utils.hexDataSlice(payload, index, index + 8));
index += 8;
// delivery count
deliveryStatus.deliveryCount = parseInt(ethers.utils.hexDataSlice(payload, index, index + 2));
index += 2;
// grab the success boolean
deliveryStatus.deliverySuccess = parseInt(ethers.utils.hexDataSlice(payload, index, index + 1));
index += 1;
return deliveryStatus;
}

View File

@ -0,0 +1,203 @@
import { expect } from "chai";
import { ethers } from "ethers";
import {
BSC_FORGE_BROADCAST,
BSC_RPC,
WORMHOLE_RPCS,
ETH_FORGE_BROADCAST,
ETH_RPC,
DEPLOYER_PRIVATE_KEY,
ZERO_ADDRESS_BYTES,
TARGET_GAS_LIMIT,
} from "../__tests__/helpers/consts";
import { DeliveryStatus, RelayerArgs, TargetDeliveryParameters } from "../__tests__/helpers/structs";
import {
makeCoreRelayerFromForgeBroadcast,
makeGasOracleFromForgeBroadcast,
makeMockRelayerIntegrationFromForgeBroadcast,
resolvePath,
getSignedVaaFromReceiptOnEth,
parseDeliveryStatusVaa,
} from "../__tests__/helpers/utils";
import { CHAIN_ID_BSC, CHAIN_ID_ETH, tryNativeToHexString, getSignedBatchVAAWithRetry } from "@certusone/wormhole-sdk";
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
async function main() {
const ethProvider = new ethers.providers.StaticJsonRpcProvider(ETH_RPC);
const bscProvider = new ethers.providers.StaticJsonRpcProvider(BSC_RPC);
// core relayers
const ethCoreRelayer = makeCoreRelayerFromForgeBroadcast(
resolvePath(ETH_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, ethProvider)
);
const bscCoreRelayer = makeCoreRelayerFromForgeBroadcast(
resolvePath(BSC_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, bscProvider)
);
// relayer integrators
const ethRelayerIntegrator = makeMockRelayerIntegrationFromForgeBroadcast(
resolvePath(ETH_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, ethProvider)
);
const bscRelayerIntegrator = makeMockRelayerIntegrationFromForgeBroadcast(
resolvePath(BSC_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, bscProvider)
);
// gas oracles
const ownedGasOracles = [
// eth
makeGasOracleFromForgeBroadcast(
resolvePath(ETH_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, ethProvider)
),
// bsc
makeGasOracleFromForgeBroadcast(
resolvePath(BSC_FORGE_BROADCAST),
new ethers.Wallet(DEPLOYER_PRIVATE_KEY, bscProvider)
),
];
// setup gas oracles and register if needed
{
const ethPrice = ethers.utils.parseUnits("2000.00", 8);
const bscPrice = ethers.utils.parseUnits("400.00", 8);
// now fetch gas prices from each provider
const gasPrices = await Promise.all(ownedGasOracles.map((oracle) => oracle.provider.getGasPrice()));
const updates = [
{
chainId: CHAIN_ID_ETH,
gasPrice: gasPrices.at(0)!,
nativeCurrencyPrice: ethPrice,
},
{
chainId: CHAIN_ID_BSC,
gasPrice: gasPrices.at(1)!,
nativeCurrencyPrice: bscPrice,
},
];
const oracleTxs = await Promise.all(
ownedGasOracles.map((oracle) => oracle.updatePrices(updates).then((tx: ethers.ContractTransaction) => tx.wait()))
);
// query the core relayer contracts to see if relayers have been registered
const registeredCoreRelayerOnBsc = await bscCoreRelayer.registeredRelayer(CHAIN_ID_ETH);
const registeredCoreRelayerOnEth = await ethCoreRelayer.registeredRelayer(CHAIN_ID_BSC);
// register the core relayer contracts
if (registeredCoreRelayerOnBsc == ZERO_ADDRESS_BYTES) {
const bscRegistrationTx = await bscCoreRelayer.registerChain(
CHAIN_ID_ETH,
"0x" + tryNativeToHexString(ethCoreRelayer.address, CHAIN_ID_ETH)
);
await bscRegistrationTx.wait();
}
if (registeredCoreRelayerOnEth == ZERO_ADDRESS_BYTES) {
const ethRegistrationTx = await ethCoreRelayer.registerChain(
CHAIN_ID_BSC,
"0x" + tryNativeToHexString(bscCoreRelayer.address, CHAIN_ID_BSC)
);
await ethRegistrationTx.wait();
}
// Query the mock relayer integration contracts to see if trusted mock relayer
// integration contracts have been registered.
const trustedSenderOnBsc = await bscRelayerIntegrator.trustedSender(CHAIN_ID_ETH);
const trustedSenderOnEth = await ethRelayerIntegrator.trustedSender(CHAIN_ID_BSC);
// register the trusted mock relayer integration contracts
if (trustedSenderOnBsc == ZERO_ADDRESS_BYTES) {
const bscRegistrationTx = await bscRelayerIntegrator.registerTrustedSender(
CHAIN_ID_ETH,
"0x" + tryNativeToHexString(ethRelayerIntegrator.address, CHAIN_ID_ETH)
);
await bscRegistrationTx.wait();
}
if (trustedSenderOnEth == ZERO_ADDRESS_BYTES) {
const ethRegistrationTx = await ethRelayerIntegrator.registerTrustedSender(
CHAIN_ID_BSC,
"0x" + tryNativeToHexString(bscRelayerIntegrator.address, CHAIN_ID_BSC)
);
await ethRegistrationTx.wait();
}
}
{
// batch Vaa payloads to relay to the target contract
let batchVaaPayloads: ethers.utils.BytesLike[] = [];
// REVIEW: these should be removed when the off-chain relayer is implemented
let batchToBscReceipt: ethers.ContractReceipt;
let targetDeliveryParamsOnBsc: TargetDeliveryParameters = {} as TargetDeliveryParameters;
{
// estimate the relayer cost to relay a batch to BSC
const estimatedGasCost: ethers.BigNumber = await ethRelayerIntegrator.estimateRelayCosts(
CHAIN_ID_BSC,
TARGET_GAS_LIMIT
);
// create an array of messages to deliver to the BSC target contract
batchVaaPayloads = [
ethers.utils.hexlify(ethers.utils.toUtf8Bytes("SuperCoolCrossChainStuff0")),
ethers.utils.hexlify(ethers.utils.toUtf8Bytes("SuperCoolCrossChainStuff1")),
ethers.utils.hexlify(ethers.utils.toUtf8Bytes("SuperCoolCrossChainStuff2")),
ethers.utils.hexlify(ethers.utils.toUtf8Bytes("SuperCoolCrossChainStuff3")),
];
const batchVaaConsistencyLevels = [15, 15, 15, 15];
// create relayerArgs interface to call the mock integration contract with
const relayerArgs: RelayerArgs = {
nonce: 69,
targetChainId: CHAIN_ID_BSC,
targetAddress: bscRelayerIntegrator.address,
targetGasLimit: TARGET_GAS_LIMIT,
consistencyLevel: batchVaaConsistencyLevels[0],
deliveryListIndices: [] as number[], // no indices specified for full batch delivery
};
// call the mock integration contract and send the batch VAA
const tx = await ethRelayerIntegrator.sendBatchToTargetChain(
batchVaaPayloads,
batchVaaConsistencyLevels,
relayerArgs,
{
value: estimatedGasCost,
}
);
batchToBscReceipt = await tx.wait();
console.log("emitterChain", CHAIN_ID_ETH, "emitterAddress", ethCoreRelayer.address);
console.log("transaction", batchToBscReceipt.transactionHash);
}
{
// fetch the batch VAA with getSignedBatchVAAWithRetry
const batchVaaRes = await getSignedBatchVAAWithRetry(
WORMHOLE_RPCS,
CHAIN_ID_ETH,
batchToBscReceipt.transactionHash,
{
transport: NodeHttpTransport(),
}
);
const batchVaaFromEth: ethers.utils.BytesLike = batchVaaRes.batchVaaBytes;
console.log("vaa", Buffer.from(batchVaaFromEth as Uint8Array).toString("hex"));
// parse the batch VAA
const parsedBatch = await ethRelayerIntegrator.parseBatchVM(batchVaaFromEth);
console.log("parsed", parsedBatch);
}
}
}
main();

View File

@ -0,0 +1,8 @@
export type { CoreRelayer } from "./ethers-contracts/CoreRelayer"
export { CoreRelayer__factory } from "./ethers-contracts/factories/CoreRelayer__factory"
export type { MockRelayerIntegration } from "./ethers-contracts/MockRelayerIntegration"
export { MockRelayerIntegration__factory } from "./ethers-contracts/factories/MockRelayerIntegration__factory"
export type { IWormhole } from "./ethers-contracts/IWormhole"
export { IWormhole__factory } from "./ethers-contracts/factories/IWormhole__factory"
export type { RelayProvider } from "./ethers-contracts/RelayProvider"
export { RelayProvider__factory } from "./ethers-contracts/factories/RelayProvider__factory"

View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"types": ["mocha", "chai"],
"typeRoots": ["./node_modules/@types"],
"lib": ["es2020"],
"module": "CommonJS",
"target": "es2020",
"esModuleInterop": true,
"strict": true,
"resolveJsonModule": true,
"moduleResolution": "node"
}
}

View File

@ -9,14 +9,16 @@ async function main() {
// load plugin config
const envType = selectPluginConfig(process.argv[2] || "")
const pluginConfig = (await relayerEngine.loadFileAndParseToObject(
`./src/plugin/config/${envType.toLowerCase()}.json`
`./src/plugin/config/${envType}.json`
)) as GenericRelayerPluginConfig
const contracts = await relayerEngine.loadFileAndParseToObject(
`../ethereum/ts-scripts/config/${envType
.toLocaleLowerCase()
.replace("devnet", "testnet")}/contracts.json`
`./contracts.json`
)
// const contracts = await relayerEngine.loadFileAndParseToObject(
// `../ethereum/ts-scripts/config/${envType.replace("devnet", "testnet")}/contracts.json`
// )
const supportedChains = pluginConfig.supportedChains as unknown as Record<
any,
ChainInfo
@ -39,16 +41,18 @@ async function main() {
})
}
function selectPluginConfig(flag: string) {
function selectPluginConfig(flag: string): string {
switch (flag) {
case "--testnet":
return relayerEngine.EnvType.DEVNET
return relayerEngine.EnvType.DEVNET.toLowerCase()
case "--mainnet":
return relayerEngine.EnvType.MAINNET
return relayerEngine.EnvType.MAINNET.toLowerCase()
case "--tilt":
return relayerEngine.EnvType.TILT
return relayerEngine.EnvType.TILT.toLowerCase()
case "--k8s-testnet":
return "k8s-testnet"
default:
return relayerEngine.EnvType.TILT
return relayerEngine.EnvType.TILT.toLowerCase()
}
}

View File

@ -0,0 +1,15 @@
{
"shouldSpy": true,
"shouldRest": false,
"logWatcherSleepMs": 300000,
"supportedChains": {
"6": {
"relayerAddress": "0x932848Aed98d8af0f3eA685533966dA4551851db",
"mockIntegrationContractAddress": "0xF5C9730B9F8B4D3a352D0cC6358896B1e56E656C"
},
"14": {
"relayerAddress": "0x06ced51D388A66ff3d968818C4ea58e5CC199B0B",
"mockIntegrationContractAddress": "0xB7078f7384d4bb353A147a1035990eD6CDAF011E"
}
}
}

View File

@ -18,15 +18,14 @@ import {
import * as wh from "@certusone/wormhole-sdk"
import { Logger } from "winston"
import { PluginError } from "./utils"
import { logNearGas, parseSequencesFromLogEth, SignedVaa } from "@certusone/wormhole-sdk"
import { CoreRelayer__factory, IWormhole, IWormhole__factory } from "../../../../sdk/src"
import { SignedVaa } from "@certusone/wormhole-sdk"
import { CoreRelayer__factory, IWormhole, IWormhole__factory } from "../../../sdk/src"
import * as ethers from "ethers"
import { Implementation__factory } from "@certusone/wormhole-sdk/lib/cjs/ethers-contracts"
import { LogMessagePublishedEvent } from "../../../../sdk/src/ethers-contracts/IWormhole"
import { CoreRelayerStructs } from "../../../../sdk/src/ethers-contracts/CoreRelayer"
import { LogMessagePublishedEvent } from "../../../sdk/src/ethers-contracts/IWormhole"
import { CoreRelayerStructs } from "../../../sdk/src/ethers-contracts/CoreRelayer"
import * as _ from "lodash"
import * as grpcWebNodeHttpTransport from "@improbable-eng/grpc-web-node-http-transport"
import { retryAsync } from "ts-retry"
const wormholeRpc = "https://wormhole-v2-testnet-api.certus.one"
@ -335,16 +334,16 @@ export class GenericRelayerPlugin implements Plugin<WorkflowPayload> {
allFetched: false,
}
// const maybeResolvedEntry = await this.fetchEntry(hash, newEntry, this.logger)
// if (maybeResolvedEntry.allFetched) {
// this.logger.info("Resolved entry immediately")
// return {
// workflowData: {
// coreRelayerVaaIndex: maybeResolvedEntry.deliveryVaaIdx,
// vaas: maybeResolvedEntry.vaas.map((v) => v.bytes),
// },
// }
// }
const maybeResolvedEntry = await this.fetchEntry(hash, newEntry, this.logger)
if (maybeResolvedEntry.allFetched) {
this.logger.info("Resolved entry immediately")
return {
workflowData: {
coreRelayerVaaIndex: maybeResolvedEntry.deliveryVaaIdx,
vaas: maybeResolvedEntry.vaas.map((v) => v.bytes),
},
}
}
this.logger.debug(`Entry: ${JSON.stringify(newEntry, undefined, 4)}`)
await db.withKey(