Merge branch 'cross-chain-swap' of github.com:certusone/wormhole into cross-chain-swap
This commit is contained in:
commit
795ce1177d
30
README.md
30
README.md
|
@ -67,3 +67,33 @@ And finally, start the react app:
|
||||||
npm ci
|
npm ci
|
||||||
npm run start
|
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
|
||||||
|
```
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# TestNet
|
||||||
|
EVM_PRIVATE_KEY=your_key_here
|
||||||
|
EVM_CONTRACT_ADDRESS=0x0290FB167208Af455bB137780163b7B7a9a10C16
|
||||||
|
|
||||||
|
#LOG_DIR=/home/briley/logs
|
||||||
|
LOG_LEVEL=debug
|
|
@ -0,0 +1 @@
|
||||||
|
/lib
|
|
@ -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" ]
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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__/*"]
|
||||||
|
}
|
Loading…
Reference in New Issue