Start of swap_relayer

Change-Id: Icec1a280198a443d525304cff036ad003e66856d
This commit is contained in:
Bruce Riley 2022-01-20 16:52:49 +00:00
parent da50adb5e4
commit b6a71c3e8c
8 changed files with 12724 additions and 0 deletions

View File

@ -67,3 +67,33 @@ And finally, start the react app:
npm ci
npm run start
```
### Running the swap relayer
You need to have a spy_guardian running in TestNet. If there is not already one running, you can build the docker image and start it as follows:
#### Build the spy_guardian docker container if you don't already have it.
```
$ cd swap_relayer
$ docker build -f Dockerfile.spy_guardian -t spy_guardian .
```
#### Start the spy_guardian docker container in TestNet.
```
$ docker run --platform linux/amd64 --network=host spy_guardian \
--bootstrap /dns4/wormhole-testnet-v2-bootstrap.certus.one/udp/8999/quic/p2p/12D3KooWBY9ty9CXLBXGQzMuqkziLntsVcyz4pk1zWaJRvJn6Mmt \
--network /wormhole/testnet/2/1 \
--spyRPC "[::]:7073"
```
#### Start the swap relayer
```
$ cd swap_relayer
$ # Edit the .env.sample, be sure to set a valid wallet private key.
$ npm ci
$ npm run build
$ npm run start
```

16
swap_relayer/.env.sample Normal file
View File

@ -0,0 +1,16 @@
# TestNet
SPY_SERVICE_HOST=localhost:7073
SPY_SERVICE_FILTERS=[{"chain_id":2,"emitter_address":"0x000000000000000000000000f890982f9310df57d00f659cf4fd87e65aded8d7"},{"chain_id":5,"emitter_address":"0x000000000000000000000000377d55a7928c046e18eebb61977e714d2a76472a"}]
EVM_CHAIN_ID=2
EVM_NODE_URL=ws://localhost:8545
# DevNet
EVM_PRIVATE_KEY=0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d
# TestNet (public key is 0xD09Ef93392eD15d77971EA815418eF69ce84137F)
#EVM_PRIVATE_KEY=0x181ef1fecd6841fe36b7c9a4f983d790a6e53b01569cdbc24d3511eef16aa8d5
EVM_CONTRACT_ADDRESS=0x0290FB167208Af455bB137780163b7B7a9a10C16
#LOG_DIR=/home/briley/logs
LOG_LEVEL=debug

1
swap_relayer/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/lib

View File

@ -0,0 +1,30 @@
FROM docker.io/golang:1.17.0-alpine as builder
RUN apk add --no-cache git gcc linux-headers alpine-sdk bash
WORKDIR /app
RUN git clone https://github.com/certusone/wormhole.git
WORKDIR /app/wormhole/tools
RUN CGO_ENABLED=0 ./build.sh
WORKDIR /app/wormhole
RUN tools/bin/buf lint && tools/bin/buf generate
WORKDIR /app/wormhole/node/tools
RUN go build -mod=readonly -o /dlv github.com/go-delve/delve/cmd/dlv
WORKDIR /app/wormhole/node
RUN go build -race -gcflags="all=-N -l" -mod=readonly -o /guardiand github.com/certusone/wormhole/node
FROM docker.io/golang:1.17.0-alpine
WORKDIR /app
COPY --from=builder /guardiand /app/guardiand
ENV PATH="/app:${PATH}"
RUN addgroup -S pyth -g 10001 && adduser -S pyth -G pyth -u 10001
RUN chown -R pyth:pyth .
USER pyth
ENTRYPOINT [ "guardiand", "spy", "--nodeKey", "/tmp/node.key" ]

12273
swap_relayer/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

35
swap_relayer/package.json Normal file
View File

@ -0,0 +1,35 @@
{
"name": "swap_relay",
"version": "1.0.0",
"description": "Swap listener and relayer demo",
"main": "index.js",
"scripts": {
"build": "tsc",
"start": "node lib/index.js"
},
"author": "",
"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",
"winston": "^3.3.3"
},
"dependencies": {
"@certusone/wormhole-sdk": "^0.1.4",
"@certusone/wormhole-spydk": "^0.0.1",
"body-parser": "^1.19.0",
"cors": "^2.8.5",
"dotenv": "^10.0.0"
}
}

326
swap_relayer/src/index.ts Normal file
View File

@ -0,0 +1,326 @@
import {
ChainId,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
hexToUint8Array,
uint8ArrayToHex,
getEmitterAddressEth,
getEmitterAddressSolana,
getEmitterAddressTerra,
getIsTransferCompletedEth,
redeemOnEth,
} from "@certusone/wormhole-sdk";
import {
importCoreWasm,
setDefaultWasm,
} from "@certusone/wormhole-sdk/lib/cjs/solana/wasm";
import {
createSpyRPCServiceClient,
subscribeSignedVAA,
} from "@certusone/wormhole-spydk";
let logger: any;
let configFile: string = ".env.sample";
if (process.env.SWAP_RELAY_CONFIG) {
configFile = process.env.SWAP_RELAY_CONFIG;
}
console.log("Loading config file [%s]", configFile);
require("dotenv").config({ path: configFile });
initLogger();
type OurEnvironment = {
spy_host: string;
spy_filters: string;
target_chain_id: number;
target_node_url: string;
target_private_key: string;
target_contract_address: string;
};
setDefaultWasm("node");
let success: boolean;
let env: OurEnvironment;
[success, env] = loadConfig();
if (success) {
logger.info(
"swap_relay starting up, will listen for signed VAAs from [" +
env.spy_host +
"]"
);
logger.info(
"will relay to EVM chainId: [" +
env.target_chain_id +
"], nodeUrl: [" +
env.target_node_url +
"], contractAddress: [" +
env.target_contract_address +
"]"
);
spy_listen();
}
function loadConfig(): [boolean, OurEnvironment] {
if (!process.env.SPY_SERVICE_HOST) {
logger.error("Missing environment variable SPY_SERVICE_HOST");
return [false, undefined];
}
if (!process.env.EVM_CHAIN_ID) {
logger.error("Missing environment variable EVM_CHAIN_ID");
return [false, undefined];
}
if (!process.env.EVM_NODE_URL) {
logger.error("Missing environment variable EVM_NODE_URL");
return [false, undefined];
}
if (!process.env.EVM_PRIVATE_KEY) {
logger.error("Missing environment variable EVM_PRIVATE_KEY");
return [false, undefined];
}
if (!process.env.EVM_CONTRACT_ADDRESS) {
logger.error("Missing environment variable EVM_CONTRACT_ADDRESS");
return [false, undefined];
}
return [
true,
{
spy_host: process.env.SPY_SERVICE_HOST,
spy_filters: process.env.SPY_SERVICE_FILTERS,
target_chain_id: parseInt(process.env.EVM_CHAIN_ID),
target_node_url: process.env.EVM_NODE_URL,
target_private_key: process.env.EVM_PRIVATE_KEY,
target_contract_address: process.env.EVM_CONTRACT_ADDRESS,
},
];
}
async function spy_listen() {
(async () => {
var filter = {};
if (env.spy_filters) {
const parsedJsonFilters = eval(env.spy_filters);
var myFilters = [];
for (var i = 0; i < parsedJsonFilters.length; i++) {
var myChainId = parseInt(parsedJsonFilters[i].chain_id) as ChainId;
var myEmitterAddress = await encodeEmitterAddress(
myChainId,
parsedJsonFilters[i].emitter_address
);
var myEmitterFilter = {
emitterFilter: {
chainId: myChainId,
emitterAddress: myEmitterAddress,
},
};
logger.info(
"adding filter: chainId: [" +
myEmitterFilter.emitterFilter.chainId +
"], emitterAddress: [" +
myEmitterFilter.emitterFilter.emitterAddress +
"]"
);
myFilters.push(myEmitterFilter);
}
logger.info("setting " + myFilters.length + " filters");
filter = {
filters: myFilters,
};
} else {
logger.info("processing all signed VAAs");
}
const client = createSpyRPCServiceClient(env.spy_host);
const stream = await subscribeSignedVAA(client, filter);
stream.on("data", ({ vaaBytes }) => {
processVaa(vaaBytes);
});
logger.info("swap_relay waiting for transfer signed VAAs");
})();
}
async function encodeEmitterAddress(
myChainId,
emitterAddressStr
): Promise<string> {
if (myChainId === CHAIN_ID_SOLANA) {
return await getEmitterAddressSolana(emitterAddressStr);
}
if (myChainId === CHAIN_ID_TERRA) {
return await getEmitterAddressTerra(emitterAddressStr);
}
return getEmitterAddressEth(emitterAddressStr);
}
type Type3Payload = {
contractAddress: string;
relayerFee: ethers.BigNumber;
swapFunctionType: number;
swapCurrencyType: number;
};
async function processVaa(vaaBytes) {
logger.debug("processVaa");
const { parse_vaa } = await importCoreWasm();
const parsedVAA = parse_vaa(hexToUint8Array(vaaBytes));
logger.debug("processVaa: parsedVAA: %o", parsedVAA);
let emitter_chain_id: number = parsedVAA.emitter_chain;
let emitter_address: string = uint8ArrayToHex(parsedVAA.emitter_address);
let sequence: number = parsedVAA.sequence;
let payload_type: number = parsedVAA.payload[0];
let t3Payload = decodeSignedVAAPayloadType3(parsedVAA);
if (t3Payload) {
logger.info(
"relaying type 3: emitter: [" +
emitter_chain_id +
":" +
emitter_address +
"], seqNum: " +
sequence +
", contractAddress: [" +
t3Payload.contractAddress +
"], relayerFee: [" +
t3Payload.relayerFee +
"], swapFunctionType: [" +
t3Payload.swapFunctionType +
"], swapCurrencyType: [" +
t3Payload.swapCurrencyType +
"]"
);
try {
//await relayVaa(vaaBytes);
} catch (e) {
logger.error("failed to relay type 3 vaa: %o", e);
}
} else {
logger.info(
"dropping vaa: emitter: [" +
emitter_chain_id +
":" +
emitter_address +
"], seqNum: " +
sequence +
" payloadType: " +
payload_type
);
}
}
function decodeSignedVAAPayloadType3(parsedVAA: any): Type3Payload {
const payload = Buffer.from(new Uint8Array(parsedVAA.payload));
const version = payload.readUInt8(0);
// if (version !== 1) {
// return undefined;
// }
// return true;
if (version !== 3) {
return undefined;
}
return {
contractAddress: payload.slice(79, 79 + 20).toString("hex"),
relayerFee: ethers.BigNumber.from(payload.slice(101, 101 + 32)),
swapFunctionType: payload.readUInt8(260),
swapCurrencyType: payload.readUInt8(261),
};
}
import { ethers } from "ethers";
async function relayVaa(vaaBytes: string) {
const signedVaaArray = hexToUint8Array(vaaBytes);
const provider = new ethers.providers.WebSocketProvider(env.target_node_url);
const signer = new ethers.Wallet(env.target_private_key, provider);
const receipt = await redeemOnEth(
env.target_contract_address,
signer,
signedVaaArray
);
let success = await getIsTransferCompletedEth(
env.target_contract_address,
provider,
signedVaaArray
);
provider.destroy();
logger.info(
"redeemed on evm: success: " + success + ", receipt: %o",
receipt
);
}
///////////////////////////////// Start of logger stuff ///////////////////////////////////////////
function initLogger() {
const winston = require("winston");
let useConsole: boolean = true;
let logFileName: string = "";
if (process.env.LOG_DIR) {
useConsole = false;
logFileName =
process.env.LOG_DIR + "/swap_relay." + new Date().toISOString() + ".log";
}
let logLevel = "info";
if (process.env.LOG_LEVEL) {
logLevel = process.env.LOG_LEVEL;
}
let transport: any;
if (useConsole) {
console.log("swap_relay is logging to the console at level [%s]", logLevel);
transport = new winston.transports.Console({
level: logLevel,
});
} else {
console.log(
"swap_relay is logging to [%s] at level [%s]",
logFileName,
logLevel
);
transport = new winston.transports.File({
filename: logFileName,
level: logLevel,
});
}
const logConfiguration = {
transports: [transport],
format: winston.format.combine(
winston.format.splat(),
winston.format.simple(),
winston.format.timestamp({
format: "YYYY-MM-DD HH:mm:ss.SSS",
}),
winston.format.printf(
(info: any) => `${[info.timestamp]}|${info.level}|${info.message}`
)
),
};
logger = winston.createLogger(logConfiguration);
}

View File

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