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 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