Messenger relayer (#22)
* init * Configuration to connect spy to wormhole local validator * plugin + scaffold written * it works * cleanup config * more cleanup * fix spy_guardiand.bash hostname * depend on github relayer engine * clean up
This commit is contained in:
parent
ec13c1d180
commit
7232b5e4a6
File diff suppressed because it is too large
Load Diff
|
@ -8,15 +8,19 @@
|
|||
"dependencies": {
|
||||
"@certusone/wormhole-sdk": "^0.6.0",
|
||||
"@project-serum/anchor": "^0.25.0",
|
||||
"@solana/web3.js": "^1.64.0",
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"byteify": "^2.0.10",
|
||||
"commander": "^9.4.0",
|
||||
"ethers": "^5.6.9",
|
||||
"keccak256": "^1.0.6",
|
||||
"node-fetch": "2",
|
||||
"ts-node": "^10.9.1"
|
||||
"relayer-engine": "wormhole-foundation/relayer-engine#relayer-engine-top-package",
|
||||
"winston": "^3.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.6.4"
|
||||
"@types/node": "^18.6.4",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
import {
|
||||
ActionExecutor,
|
||||
CommonPluginEnv,
|
||||
ContractFilter,
|
||||
Plugin,
|
||||
PluginFactory,
|
||||
Providers,
|
||||
StagingArea,
|
||||
Workflow,
|
||||
} from "relayer-plugin-interface";
|
||||
import * as whSdk from "@certusone/wormhole-sdk";
|
||||
import { Logger } from "winston";
|
||||
import { ethers } from "ethers";
|
||||
import { Connection as SolanaConnection } from "@solana/web3.js";
|
||||
import { BaseVAA } from "./utils";
|
||||
import { TextDecoder } from "util";
|
||||
|
||||
// todo: do we need this in the plugin or just the relayer??
|
||||
whSdk.setDefaultWasm("node");
|
||||
|
||||
function create(
|
||||
commonConfig: CommonPluginEnv,
|
||||
pluginConfig: any,
|
||||
logger: Logger
|
||||
): Plugin {
|
||||
console.log("Creating da plugin...");
|
||||
return new MessengerRelayerPlugin(commonConfig, pluginConfig, logger);
|
||||
}
|
||||
|
||||
export interface MessengerRelayerPluginConfig {
|
||||
registeredContracts: { chainId: whSdk.ChainId; contractAddress: string }[];
|
||||
xDappConfig: XDappConfig;
|
||||
messengerABI: string;
|
||||
}
|
||||
|
||||
type Network = string;
|
||||
|
||||
export interface XDappConfig {
|
||||
networks: Record<
|
||||
Network,
|
||||
{
|
||||
type: string;
|
||||
wormholeChainId: whSdk.ChainId;
|
||||
rpc: string;
|
||||
privateKey: string;
|
||||
bridgeAddress: string;
|
||||
}
|
||||
>;
|
||||
wormhole: {
|
||||
restAddress: string;
|
||||
};
|
||||
}
|
||||
|
||||
// base64 encoded Buffer
|
||||
type VAA = string;
|
||||
|
||||
export class MessengerRelayerPlugin implements Plugin<VAA> {
|
||||
readonly shouldSpy: boolean = true;
|
||||
readonly shouldRest: boolean = false;
|
||||
readonly demoteInProgress: boolean = true;
|
||||
static readonly pluginName: string = "MessengerRelayerPlugin";
|
||||
readonly pluginName = MessengerRelayerPlugin.pluginName;
|
||||
|
||||
constructor(
|
||||
readonly relayerConfig: CommonPluginEnv,
|
||||
readonly pluginConfig: MessengerRelayerPluginConfig,
|
||||
readonly logger: Logger
|
||||
) {
|
||||
this.logger.info("Messenger relayer plugin loaded");
|
||||
// todo: config validation
|
||||
}
|
||||
|
||||
async consumeEvent(
|
||||
vaa: Buffer,
|
||||
stagingArea: { counter?: number }
|
||||
): Promise<{ workflowData?: VAA; nextStagingArea: StagingArea }> {
|
||||
this.logger.info("Got VAA");
|
||||
return {
|
||||
workflowData: vaa.toString("base64"),
|
||||
nextStagingArea: {
|
||||
counter: stagingArea?.counter ? stagingArea.counter + 1 : 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async handleWorkflow(
|
||||
workflow: Workflow<VAA>,
|
||||
providers: Providers,
|
||||
execute: ActionExecutor
|
||||
): Promise<void> {
|
||||
this.logger.info("Handling workflow...");
|
||||
const vaa = Buffer.from(workflow.data, "base64");
|
||||
const parsed = await this.parseVAA(vaa);
|
||||
this.logger.info("Got message: " + Buffer.from(parsed.payload).toString("utf-8"));
|
||||
await Promise.all(
|
||||
this.pluginConfig.registeredContracts.map(async (registeredContract) => {
|
||||
if (registeredContract.chainId === parsed.emitter_chain) {
|
||||
// do not submit to emitting chain
|
||||
return;
|
||||
}
|
||||
if (whSdk.isEVMChain(registeredContract.chainId)) {
|
||||
await this.submitOnEVM(
|
||||
vaa,
|
||||
providers.evm[registeredContract.chainId],
|
||||
execute,
|
||||
registeredContract.contractAddress,
|
||||
registeredContract.chainId
|
||||
);
|
||||
} else if (registeredContract.chainId === whSdk.CHAIN_ID_SOLANA) {
|
||||
await this.submitOnSolana(
|
||||
vaa,
|
||||
providers.solana,
|
||||
execute,
|
||||
registeredContract.contractAddress,
|
||||
registeredContract.chainId
|
||||
);
|
||||
} else {
|
||||
throw new Error("Unsupported Chain: " + registeredContract.chainId);
|
||||
}
|
||||
})
|
||||
);
|
||||
this.logger.info("Message submitted to all chains!!");
|
||||
}
|
||||
|
||||
async submitOnEVM(
|
||||
vaa: Buffer,
|
||||
provider: ethers.providers.Provider,
|
||||
execute: ActionExecutor,
|
||||
contractAddress: string,
|
||||
chainId: whSdk.ChainId
|
||||
) {
|
||||
const messenger = new ethers.Contract(
|
||||
contractAddress,
|
||||
this.pluginConfig.messengerABI,
|
||||
provider
|
||||
);
|
||||
const tx = await execute.onEVM({
|
||||
chainId,
|
||||
f: async ({ wallet }) => {
|
||||
return messenger.connect(wallet).receiveEncodedMsg(vaa);
|
||||
},
|
||||
});
|
||||
await tx.wait();
|
||||
|
||||
const message = await messenger.getCurrentMsg();
|
||||
this.logger.info(`Current message now '${message}' on chain ${chainId}`);
|
||||
}
|
||||
|
||||
async submitOnSolana(
|
||||
vaa: Buffer,
|
||||
provider: SolanaConnection,
|
||||
execute: ActionExecutor,
|
||||
contractAddress: string,
|
||||
chainId: whSdk.ChainId
|
||||
) {
|
||||
throw new Error("Exercise for the reader...");
|
||||
}
|
||||
|
||||
getFilters(): ContractFilter[] {
|
||||
return this.pluginConfig.registeredContracts.map((rc) => ({
|
||||
chainId: rc.chainId,
|
||||
emitterAddress: rc.contractAddress,
|
||||
}));
|
||||
}
|
||||
|
||||
async parseVAA(vaa: Buffer | Uint8Array): Promise<BaseVAA> {
|
||||
try {
|
||||
const { parse_vaa } = await whSdk.importCoreWasm();
|
||||
return parse_vaa(new Uint8Array(vaa)) as BaseVAA;
|
||||
} catch (e) {
|
||||
this.logger.error("Failed to parse vaa");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const factory: PluginFactory = {
|
||||
create,
|
||||
pluginName: MessengerRelayerPlugin.pluginName,
|
||||
};
|
||||
|
||||
export default factory;
|
|
@ -0,0 +1,67 @@
|
|||
import { readdirSync, readFileSync } from "fs";
|
||||
import * as relayerEngine from "relayer-engine";
|
||||
import { EnvType } from "relayer-plugin-interface";
|
||||
import { MessengerRelayerPlugin, XDappConfig } from "./plugin";
|
||||
|
||||
async function main() {
|
||||
const xDappConfig: XDappConfig = JSON.parse(
|
||||
readFileSync("./xdapp.config.json").toString()
|
||||
);
|
||||
const messengerABI = JSON.parse(
|
||||
readFileSync("./chains/evm/out/Messenger.sol/Messenger.json").toString()
|
||||
).abi;
|
||||
|
||||
const registeredContracts = readdirSync("./deployinfo")
|
||||
.filter((fname) => fname.split(".").length === 3)
|
||||
.map((fname) => {
|
||||
const file = JSON.parse(readFileSync(`./deployinfo/${fname}`).toString());
|
||||
const network = fname.split(".")[0];
|
||||
const chainId = xDappConfig.networks[network].wormholeChainId;
|
||||
return { chainId, contractAddress: file.address };
|
||||
});
|
||||
|
||||
const relayerConfig = {
|
||||
envType: EnvType.LOCALHOST,
|
||||
mode: relayerEngine.Mode.BOTH,
|
||||
supportedChains: Object.entries(xDappConfig.networks).map(
|
||||
([networkName, network]) => {
|
||||
return {
|
||||
chainId: network.wormholeChainId,
|
||||
chainName: networkName,
|
||||
nodeUrl: network.rpc,
|
||||
nativeCurrencySymbol: network.wormholeChainId === 1 ? "SOL" : "ETH",
|
||||
bridgeAddress: network.bridgeAddress,
|
||||
};
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
const plugin = new MessengerRelayerPlugin(
|
||||
relayerConfig,
|
||||
{ registeredContracts, xDappConfig, messengerABI },
|
||||
relayerEngine.getLogger()
|
||||
);
|
||||
|
||||
await relayerEngine.run({
|
||||
plugins: [plugin],
|
||||
configs: {
|
||||
executorEnv: {
|
||||
// @ts-ignore
|
||||
privateKeys: Object.fromEntries(
|
||||
Object.values(xDappConfig.networks).map((network) => {
|
||||
return [network.wormholeChainId, [network.privateKey]];
|
||||
})
|
||||
),
|
||||
},
|
||||
listenerEnv: { spyServiceHost: "localhost:7073" },
|
||||
commonEnv: relayerConfig as relayerEngine.CommonEnv,
|
||||
},
|
||||
mode: relayerConfig.mode,
|
||||
envType: relayerConfig.envType,
|
||||
});
|
||||
}
|
||||
|
||||
main().then((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
import * as wh from "@certusone/wormhole-sdk";
|
||||
|
||||
export function assertInt(x: any, fieldName?: string): number {
|
||||
if (!Number.isInteger(x)) {
|
||||
const e = new Error(`Expected field to be integer, found ${x}`) as any;
|
||||
e.fieldName = fieldName;
|
||||
throw e;
|
||||
}
|
||||
return x as number;
|
||||
}
|
||||
|
||||
export function assertArray<T>(x: any, fieldName?: string): T[] {
|
||||
if (!Array.isArray(x)) {
|
||||
const e = new Error(`Expected field to be array, found ${x}`) as any;
|
||||
e.fieldName = fieldName;
|
||||
throw e;
|
||||
}
|
||||
return x as T[];
|
||||
}
|
||||
|
||||
export function assertBool(x: any, fieldName?: string): boolean {
|
||||
if (x !== false && x !== true) {
|
||||
const e = new Error(`Expected field to be boolean, found ${x}`) as any;
|
||||
e.fieldName = fieldName;
|
||||
throw e;
|
||||
}
|
||||
return x as boolean;
|
||||
}
|
||||
|
||||
export function nnull<T>(x: T | undefined | null, errMsg?: string): T {
|
||||
if (x === undefined || x === null) {
|
||||
throw new Error("Found unexpected undefined or null. " + errMsg);
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
export interface BaseVAA {
|
||||
version: number;
|
||||
guardianSetIndex: number;
|
||||
timestamp: number;
|
||||
nonce: number;
|
||||
emitter_chain: wh.ChainId;
|
||||
emitter_address: Uint8Array; // 32 bytes
|
||||
sequence: number;
|
||||
consistency_level: number;
|
||||
payload: Uint8Array;
|
||||
}
|
|
@ -1,7 +1,23 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"types": ["node"],
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true
|
||||
"esModuleInterop": true,
|
||||
"outDir": "lib",
|
||||
"target": "esnext",
|
||||
"module": "CommonJS",
|
||||
"lib": [
|
||||
"es2019"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"noImplicitAny": false,
|
||||
"declaration": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"resolveJsonModule": true,
|
||||
"downlevelIteration": true,
|
||||
"sourceMap": true
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
# Run Guardiand
|
||||
# Run Spy
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
|
|
Loading…
Reference in New Issue