examples: most basic rest relayer

Change-Id: I207644487224586ba064f9d5b6f0b547abe8cd96
This commit is contained in:
Evan Gray 2021-11-21 19:18:56 -05:00
parent f085e4e15c
commit acf186c783
22 changed files with 11821 additions and 0 deletions

View File

@ -0,0 +1,3 @@
ETH_NODE_URL=ws://localhost:8545
ETH_PRIVATE_KEY=0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d
ETH_TOKEN_BRIDGE_ADDRESS=0x0290FB167208Af455bB137780163b7B7a9a10C16

34
examples/rest_relayer/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# ethereum contracts
/contracts
/src/ethers-contracts
# tsproto output
/src/proto
# build
/lib

View File

@ -0,0 +1,8 @@
{
"transform": {
"^.+\\.(t|j)sx?$": "ts-jest"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"],
"transformIgnorePatterns": ["/node_modules/"]
}

11516
examples/rest_relayer/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,54 @@
{
"name": "@certusone/wormhole-examples",
"version": "0.0.1",
"description": "SDK for interacting with Wormhole",
"homepage": "https://wormholenetwork.com",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
"lib/**/*"
],
"repository": "https://github.com/certusone/wormhole/tree/dev.v2/examples",
"scripts": {
"build": "tsc",
"start": "npm run build && node lib/index.js",
"test": "jest --config jestconfig.json --verbose"
},
"keywords": [
"wormhole",
"bridge",
"token",
"sdk",
"solana",
"ethereum",
"terra",
"bsc"
],
"author": "certusone",
"license": "Apache-2.0",
"devDependencies": {
"@improbable-eng/grpc-web-node-http-transport": "^0.15.0",
"@types/jest": "^27.0.2",
"@types/long": "^4.0.1",
"@types/node": "^16.6.1",
"axios": "^0.24.0",
"esm": "^3.2.25",
"ethers": "^5.4.4",
"jest": "^27.3.1",
"prettier": "^2.3.2",
"ts-jest": "^27.0.7",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"typescript": "^4.3.5"
},
"dependencies": {
"@certusone/wormhole-sdk": "file:../../sdk/js",
"@improbable-eng/grpc-web": "^0.14.0",
"@solana/spl-token": "^0.1.8",
"@solana/web3.js": "^1.24.0",
"@terra-money/terra.js": "^2.0.14",
"body-parser": "^1.19.0",
"dotenv": "^10.0.0",
"express": "^4.17.1"
}
}

View File

@ -0,0 +1,36 @@
import { describe, expect, it } from "@jest/globals";
import { Connection, PublicKey } from "@solana/web3.js";
// see devnet.md
export const ETH_NODE_URL = "ws://localhost:8545";
export const ETH_PRIVATE_KEY =
"0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d";
export const ETH_PUBLIC_KEY = "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1";
export const ETH_CORE_BRIDGE_ADDRESS =
"0xC89Ce4735882C9F0f0FE26686c53074E09B0D550";
export const ETH_TOKEN_BRIDGE_ADDRESS =
"0x0290FB167208Af455bB137780163b7B7a9a10C16";
export const SOLANA_HOST = "http://localhost:8899";
export const SOLANA_PRIVATE_KEY = new Uint8Array([
14, 173, 153, 4, 176, 224, 201, 111, 32, 237, 183, 185, 159, 247, 22, 161, 89,
84, 215, 209, 212, 137, 10, 92, 157, 49, 29, 192, 101, 164, 152, 70, 87, 65,
8, 174, 214, 157, 175, 126, 98, 90, 54, 24, 100, 177, 247, 77, 19, 112, 47,
44, 165, 109, 233, 102, 14, 86, 109, 29, 134, 145, 132, 141,
]);
export const SOLANA_CORE_BRIDGE_ADDRESS =
"Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o";
export const SOLANA_TOKEN_BRIDGE_ADDRESS =
"B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE";
export const TEST_ERC20 = "0x2D8BE6BF0baA74e0A907016679CaE9190e80dD0A";
export const TEST_SOLANA_TOKEN = "2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ";
export const WORMHOLE_RPC_HOSTS = ["http://localhost:7071"];
describe("consts should exist", () => {
it("has Solana test token", () => {
expect.assertions(1);
const connection = new Connection(SOLANA_HOST, "confirmed");
return expect(
connection.getAccountInfo(new PublicKey(TEST_SOLANA_TOKEN))
).resolves.toBeTruthy();
});
});

View File

@ -0,0 +1,106 @@
import {
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
getEmitterAddressSolana,
hexToUint8Array,
nativeToHexString,
parseSequenceFromLogSolana,
transferFromSolana,
uint8ArrayToHex,
} from "@certusone/wormhole-sdk";
import getSignedVAAWithRetry from "@certusone/wormhole-sdk/lib/cjs/rpc/getSignedVAAWithRetry";
import { setDefaultWasm } from "@certusone/wormhole-sdk/lib/cjs/solana/wasm";
import { parseUnits } from "@ethersproject/units";
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
import { jest, test } from "@jest/globals";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
Token,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
import axios from "axios";
import {
ETH_PUBLIC_KEY,
SOLANA_CORE_BRIDGE_ADDRESS,
SOLANA_HOST,
SOLANA_PRIVATE_KEY,
SOLANA_TOKEN_BRIDGE_ADDRESS,
TEST_SOLANA_TOKEN,
WORMHOLE_RPC_HOSTS,
} from "./consts";
const RELAYER_URL = "http://localhost:3001/relay";
setDefaultWasm("node");
jest.setTimeout(60000);
test("Send Solana SPL to Ethereum", (done) => {
(async () => {
try {
const targetAddress = ETH_PUBLIC_KEY;
// create a keypair for Solana
const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
const payerAddress = keypair.publicKey.toString();
// find the associated token account
const fromAddress = (
await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
new PublicKey(TEST_SOLANA_TOKEN),
keypair.publicKey
)
).toString();
// transfer the test token
const connection = new Connection(SOLANA_HOST, "confirmed");
const amount = parseUnits("1", 9).toBigInt();
const transaction = await transferFromSolana(
connection,
SOLANA_CORE_BRIDGE_ADDRESS,
SOLANA_TOKEN_BRIDGE_ADDRESS,
payerAddress,
fromAddress,
TEST_SOLANA_TOKEN,
amount,
hexToUint8Array(nativeToHexString(targetAddress, CHAIN_ID_ETH) || ""),
CHAIN_ID_ETH
);
// sign, send, and confirm transaction
transaction.partialSign(keypair);
const txid = await connection.sendRawTransaction(transaction.serialize());
console.log("Solana transaction:", txid);
await connection.confirmTransaction(txid);
const info = await connection.getTransaction(txid);
if (!info) {
throw new Error(
"An error occurred while fetching the transaction info"
);
}
// get the sequence from the logs (needed to fetch the vaa)
const sequence = parseSequenceFromLogSolana(info);
const emitterAddress = await getEmitterAddressSolana(
SOLANA_TOKEN_BRIDGE_ADDRESS
);
// poll until the guardian(s) witness and sign the vaa
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
CHAIN_ID_SOLANA,
emitterAddress,
sequence,
{
transport: NodeHttpTransport(),
}
);
const result = await axios.post(RELAYER_URL, {
chainId: CHAIN_ID_ETH,
signedVAA: uint8ArrayToHex(signedVAA),
});
console.log(result);
done();
} catch (e) {
console.error(e);
done("An error occurred while trying to send from Solana to Ethereum");
}
})();
});

View File

@ -0,0 +1,51 @@
import {
CHAIN_ID_ETH,
hexToUint8Array,
redeemOnEth,
} from "@certusone/wormhole-sdk";
import { ethers } from "ethers";
require("dotenv").config();
if (!process.env.ETH_NODE_URL) {
console.error("Missing environment variable ETH_NODE_URL");
process.exit(1);
}
if (!process.env.ETH_PRIVATE_KEY) {
console.error("Missing environment variable ETH_PRIVATE_KEY");
process.exit(1);
}
if (!process.env.ETH_TOKEN_BRIDGE_ADDRESS) {
console.error("Missing environment variable ETH_TOKEN_BRIDGE_ADDRESS");
process.exit(1);
}
const SUPPORTED_CHAINS = [CHAIN_ID_ETH];
const express = require("express");
const app = express();
const bodyParser = require("body-parser");
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.post("/relay", async (req, res) => {
console.log(req.body);
const chainId = req.body?.chainId;
if (!SUPPORTED_CHAINS.includes(chainId)) {
res.status(400).json({ error: "Unsupported chainId" });
return;
}
const signedVAA = req.body?.signedVAA;
if (!signedVAA) {
res.status(400).json({ error: "signedVAA is required" });
}
const provider = new ethers.providers.WebSocketProvider(
process.env.ETH_NODE_URL
);
const signer = new ethers.Wallet(process.env.ETH_PRIVATE_KEY, provider);
const receipt = await redeemOnEth(
process.env.ETH_TOKEN_BRIDGE_ADDRESS,
signer,
hexToUint8Array(signedVAA)
);
provider.destroy();
res.status(200).json(receipt);
});
app.listen(3001, () => {
console.log("Server running on port 3001");
});

View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"outDir": "lib",
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"lib": ["es2019"],
"skipLibCheck": true,
"allowJs": true
},
"include": ["src"],
"exclude": ["node_modules", "**/__tests__/*"]
}