examples: beginnings of an examples codebase
Change-Id: I3c84aa954aef80307dba400df1182489eb6eedb7
This commit is contained in:
parent
d678cb4662
commit
5ed2b2a06d
|
@ -0,0 +1,33 @@
|
|||
# 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.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
|
|
@ -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/"]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"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 -r esm src/runner.js"
|
||||
},
|
||||
"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",
|
||||
"@openzeppelin/contracts": "^4.2.0",
|
||||
"@typechain/ethers-v5": "^7.0.1",
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/long": "^4.0.1",
|
||||
"@types/node": "^16.6.1",
|
||||
"@types/react": "^17.0.19",
|
||||
"copy-dir": "^1.3.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": "^0.0.10",
|
||||
"@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",
|
||||
"@terra-money/wallet-provider": "^2.2.0",
|
||||
"bech32": "^2.0.0",
|
||||
"js-base64": "^3.6.1",
|
||||
"protobufjs": "^6.11.2",
|
||||
"rxjs": "^7.3.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
import {
|
||||
ChainId,
|
||||
parseNFTPayload,
|
||||
parseTransferPayload,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { BigNumber } from "@ethersproject/bignumber";
|
||||
import { formatUnits } from "@ethersproject/units";
|
||||
import { getSignedVAABySequence } from "./core/guardianQuery";
|
||||
import { redeem } from "./core/redeem";
|
||||
|
||||
/*
|
||||
The intent behind this module is to represent a backend process which waits for the guardian network to produce a signedVAA
|
||||
for the bridged tokens, and then submits the VAA to the target chain. This allows the end user to pay fees only in currency from
|
||||
the source chain, rather than on both chains.
|
||||
|
||||
*/
|
||||
export async function relay(
|
||||
sourceChain: ChainId,
|
||||
sequence: string,
|
||||
isNftTransfer: boolean
|
||||
) {
|
||||
//The transaction has already been submitted by the client,
|
||||
//so the relayer first needs to wait for the guardian network to
|
||||
//reach consensus and emit the signedVAA.
|
||||
const vaaBytes = await getSignedVAABySequence(
|
||||
sourceChain, //Emitter address is always the bridge contract address on the source chain
|
||||
sequence,
|
||||
isNftTransfer
|
||||
);
|
||||
|
||||
//The VAA is in the generic format of the Wormhole Core bridge. The VAA payload contains the information needed to redeem the tokens.
|
||||
const transferInformation = await parsePayload(
|
||||
await parseVaa(vaaBytes),
|
||||
isNftTransfer
|
||||
);
|
||||
//If the relayer is unwilling to submit VAAs at a potential monetary loss, it should first assess if this will be a profitable action.
|
||||
const shouldAttempt = await processFee(transferInformation, isNftTransfer);
|
||||
|
||||
if (shouldAttempt) {
|
||||
try {
|
||||
await redeem(transferInformation.targetChain, vaaBytes, isNftTransfer);
|
||||
} catch (e) {
|
||||
//Because VAAs are broadcasted publicly, there is a possibility that the VAA
|
||||
//will be redeemed by a different relayer. This case should be detected separately from
|
||||
//other errors, as it is a do-not-retry scenario.
|
||||
//This error will be deterministic, but dependent upon the implementation
|
||||
//of the specific wallet provider used. As such, the detection of this error
|
||||
//will need to be implemented for each provider separately.
|
||||
if (isAlreadyRedeemedError(e as any)) {
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//This function converts the raw VAA into a useable javascript object.
|
||||
async function parseVaa(bytes: Uint8Array) {
|
||||
//parse_vaa is based on wasm
|
||||
const { parse_vaa } = await import(
|
||||
"@certusone/wormhole-sdk/lib/solana/core/bridge"
|
||||
);
|
||||
return parse_vaa(bytes);
|
||||
}
|
||||
|
||||
//This takes the generic parsedVAA format from the Core Bridge, and parses the payload content into the
|
||||
//protocol specific information of the Token & NFT bridges.
|
||||
//Note: there are an unlimited variety of VAA formats, and it should not be assumed that a random VAA is one of these
|
||||
//two types.
|
||||
async function parsePayload(parsedVaa: any, isNftTransfer: boolean) {
|
||||
const buffered = Buffer.from(new Uint8Array(parsedVaa.payload));
|
||||
return isNftTransfer
|
||||
? parseNFTPayload(buffered)
|
||||
: parseTransferPayload(buffered);
|
||||
}
|
||||
|
||||
//This is a toy function for the purpose of determining a VAA's profitability.
|
||||
async function processFee(transferInformation: any, isNftTransfer: boolean) {
|
||||
if (isNftTransfer) {
|
||||
//NFTs are always relayed at a loss, because there is no fee field on the VAA.
|
||||
return true;
|
||||
}
|
||||
|
||||
const targetAssetDecimals = 8; //This will have to be pulled from either the chain or third party provider
|
||||
const targetAssetUnitPrice = 100; //Accurate price quotes are important for determining profitability.
|
||||
const feeValue =
|
||||
parseFloat(
|
||||
formatUnits(
|
||||
BigNumber.from((transferInformation.fee || BigInt(0)) as bigint),
|
||||
targetAssetDecimals
|
||||
)
|
||||
) * targetAssetUnitPrice;
|
||||
|
||||
const estimatedCurrencyFees = 0.01;
|
||||
const estimatedCurrencyPrice = -1;
|
||||
const transactionCost = estimatedCurrencyFees * estimatedCurrencyPrice;
|
||||
|
||||
return feeValue > transactionCost;
|
||||
}
|
||||
|
||||
const ALREADY_REDEEMED = "Already Redeemed";
|
||||
function isAlreadyRedeemedError(e: Error) {
|
||||
return e?.message === ALREADY_REDEEMED;
|
||||
}
|
|
@ -0,0 +1,224 @@
|
|||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_BSC,
|
||||
CHAIN_ID_ETH,
|
||||
CHAIN_ID_POLYGON,
|
||||
CHAIN_ID_SOLANA,
|
||||
CHAIN_ID_TERRA,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { Signer } from "@ethersproject/abstract-signer";
|
||||
import { clusterApiUrl } from "@solana/web3.js";
|
||||
import { ethers } from "ethers";
|
||||
import { getAddress } from "ethers/lib/utils";
|
||||
|
||||
//Devnet here means the locahost kubernetes environment used by the certusone/wormhole official git repository.
|
||||
//Testnet is the official Wormhole testnet
|
||||
export type Environment = "devnet" | "testnet" | "mainnet";
|
||||
export const CLUSTER: Environment = "devnet" as Environment; //This is the currently selected environment.
|
||||
|
||||
export const SOLANA_HOST = process.env.REACT_APP_SOLANA_API_URL
|
||||
? process.env.REACT_APP_SOLANA_API_URL
|
||||
: CLUSTER === "mainnet"
|
||||
? clusterApiUrl("mainnet-beta")
|
||||
: CLUSTER === "testnet"
|
||||
? clusterApiUrl("testnet")
|
||||
: "http://localhost:8899";
|
||||
|
||||
export const TERRA_HOST =
|
||||
CLUSTER === "mainnet"
|
||||
? {
|
||||
URL: "https://lcd.terra.dev",
|
||||
chainID: "columbus-5",
|
||||
name: "mainnet",
|
||||
}
|
||||
: CLUSTER === "testnet"
|
||||
? {
|
||||
URL: "https://bombay-lcd.terra.dev",
|
||||
chainID: "bombay-12",
|
||||
name: "testnet",
|
||||
}
|
||||
: {
|
||||
URL: "http://localhost:1317",
|
||||
chainID: "columbus-5",
|
||||
name: "localterra",
|
||||
};
|
||||
export const ETH_BRIDGE_ADDRESS = getAddress(
|
||||
CLUSTER === "mainnet"
|
||||
? "0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B"
|
||||
: CLUSTER === "testnet"
|
||||
? "0x44F3e7c20850B3B5f3031114726A9240911D912a"
|
||||
: "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550"
|
||||
);
|
||||
export const ETH_NFT_BRIDGE_ADDRESS = getAddress(
|
||||
CLUSTER === "mainnet"
|
||||
? "0x6FFd7EdE62328b3Af38FCD61461Bbfc52F5651fE"
|
||||
: CLUSTER === "testnet"
|
||||
? "0x26b4afb60d6c903165150c6f0aa14f8016be4aec" // TODO: test address
|
||||
: "0x26b4afb60d6c903165150c6f0aa14f8016be4aec"
|
||||
);
|
||||
export const ETH_TOKEN_BRIDGE_ADDRESS = getAddress(
|
||||
CLUSTER === "mainnet"
|
||||
? "0x3ee18B2214AFF97000D974cf647E7C347E8fa585"
|
||||
: CLUSTER === "testnet"
|
||||
? "0xa6CDAddA6e4B6704705b065E01E52e2486c0FBf6"
|
||||
: "0x0290FB167208Af455bB137780163b7B7a9a10C16"
|
||||
);
|
||||
export const BSC_BRIDGE_ADDRESS = getAddress(
|
||||
CLUSTER === "mainnet"
|
||||
? "0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B"
|
||||
: CLUSTER === "testnet"
|
||||
? "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550" // TODO: test address
|
||||
: "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550"
|
||||
);
|
||||
export const BSC_NFT_BRIDGE_ADDRESS = getAddress(
|
||||
CLUSTER === "mainnet"
|
||||
? "0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE"
|
||||
: CLUSTER === "testnet"
|
||||
? "0x26b4afb60d6c903165150c6f0aa14f8016be4aec" // TODO: test address
|
||||
: "0x26b4afb60d6c903165150c6f0aa14f8016be4aec"
|
||||
);
|
||||
export const BSC_TOKEN_BRIDGE_ADDRESS = getAddress(
|
||||
CLUSTER === "mainnet"
|
||||
? "0xB6F6D86a8f9879A9c87f643768d9efc38c1Da6E7"
|
||||
: CLUSTER === "testnet"
|
||||
? "0x0290FB167208Af455bB137780163b7B7a9a10C16" // TODO: test address
|
||||
: "0x0290FB167208Af455bB137780163b7B7a9a10C16"
|
||||
);
|
||||
export const POLYGON_BRIDGE_ADDRESS = getAddress(
|
||||
CLUSTER === "mainnet"
|
||||
? "0x7A4B5a56256163F07b2C80A7cA55aBE66c4ec4d7"
|
||||
: CLUSTER === "testnet"
|
||||
? "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550" // TODO: test address
|
||||
: "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550"
|
||||
);
|
||||
export const POLYGON_NFT_BRIDGE_ADDRESS = getAddress(
|
||||
CLUSTER === "mainnet"
|
||||
? "0x90BBd86a6Fe93D3bc3ed6335935447E75fAb7fCf"
|
||||
: CLUSTER === "testnet"
|
||||
? "0x26b4afb60d6c903165150c6f0aa14f8016be4aec" // TODO: test address
|
||||
: "0x26b4afb60d6c903165150c6f0aa14f8016be4aec"
|
||||
);
|
||||
export const POLYGON_TOKEN_BRIDGE_ADDRESS = getAddress(
|
||||
CLUSTER === "mainnet"
|
||||
? "0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE"
|
||||
: CLUSTER === "testnet"
|
||||
? "0x0290FB167208Af455bB137780163b7B7a9a10C16" // TODO: test address
|
||||
: "0x0290FB167208Af455bB137780163b7B7a9a10C16"
|
||||
);
|
||||
export const SOL_BRIDGE_ADDRESS =
|
||||
CLUSTER === "mainnet"
|
||||
? "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth"
|
||||
: CLUSTER === "testnet"
|
||||
? "Brdguy7BmNB4qwEbcqqMbyV5CyJd2sxQNUn6NEpMSsUb"
|
||||
: "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o";
|
||||
export const SOL_NFT_BRIDGE_ADDRESS =
|
||||
CLUSTER === "mainnet"
|
||||
? "WnFt12ZrnzZrFZkt2xsNsaNWoQribnuQ5B5FrDbwDhD"
|
||||
: CLUSTER === "testnet"
|
||||
? "NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA" // TODO: test address
|
||||
: "NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA";
|
||||
export const SOL_TOKEN_BRIDGE_ADDRESS =
|
||||
CLUSTER === "mainnet"
|
||||
? "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb"
|
||||
: CLUSTER === "testnet"
|
||||
? "A4Us8EhCC76XdGAN17L4KpRNEK423nMivVHZzZqFqqBg"
|
||||
: "B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE";
|
||||
export const TERRA_BRIDGE_ADDRESS =
|
||||
CLUSTER === "mainnet"
|
||||
? "terra1dq03ugtd40zu9hcgdzrsq6z2z4hwhc9tqk2uy5"
|
||||
: CLUSTER === "testnet"
|
||||
? "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5"
|
||||
: "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5";
|
||||
export const TERRA_TOKEN_BRIDGE_ADDRESS =
|
||||
CLUSTER === "mainnet"
|
||||
? "terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf"
|
||||
: CLUSTER === "testnet"
|
||||
? "terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4"
|
||||
: "terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4";
|
||||
|
||||
export const getBridgeAddressForChain = (chainId: ChainId) =>
|
||||
chainId === CHAIN_ID_SOLANA
|
||||
? SOL_BRIDGE_ADDRESS
|
||||
: chainId === CHAIN_ID_ETH
|
||||
? ETH_BRIDGE_ADDRESS
|
||||
: chainId === CHAIN_ID_BSC
|
||||
? BSC_BRIDGE_ADDRESS
|
||||
: chainId === CHAIN_ID_TERRA
|
||||
? TERRA_BRIDGE_ADDRESS
|
||||
: chainId === CHAIN_ID_POLYGON
|
||||
? POLYGON_BRIDGE_ADDRESS
|
||||
: "";
|
||||
export const getNFTBridgeAddressForChain = (chainId: ChainId) =>
|
||||
chainId === CHAIN_ID_SOLANA
|
||||
? SOL_NFT_BRIDGE_ADDRESS
|
||||
: chainId === CHAIN_ID_ETH
|
||||
? ETH_NFT_BRIDGE_ADDRESS
|
||||
: chainId === CHAIN_ID_BSC
|
||||
? BSC_NFT_BRIDGE_ADDRESS
|
||||
: chainId === CHAIN_ID_POLYGON
|
||||
? POLYGON_NFT_BRIDGE_ADDRESS
|
||||
: "";
|
||||
export const getTokenBridgeAddressForChain = (chainId: ChainId) =>
|
||||
chainId === CHAIN_ID_SOLANA
|
||||
? SOL_TOKEN_BRIDGE_ADDRESS
|
||||
: chainId === CHAIN_ID_ETH
|
||||
? ETH_TOKEN_BRIDGE_ADDRESS
|
||||
: chainId === CHAIN_ID_BSC
|
||||
? BSC_TOKEN_BRIDGE_ADDRESS
|
||||
: chainId === CHAIN_ID_TERRA
|
||||
? TERRA_TOKEN_BRIDGE_ADDRESS
|
||||
: chainId === CHAIN_ID_POLYGON
|
||||
? POLYGON_TOKEN_BRIDGE_ADDRESS
|
||||
: "";
|
||||
|
||||
export const WORMHOLE_RPC_HOSTS =
|
||||
CLUSTER === "mainnet"
|
||||
? [
|
||||
"https://wormhole-v2-mainnet-api.certus.one",
|
||||
"https://wormhole.inotel.ro",
|
||||
"https://wormhole-v2-mainnet-api.mcf.rocks",
|
||||
"https://wormhole-v2-mainnet-api.chainlayer.network",
|
||||
"https://wormhole-v2-mainnet-api.staking.fund",
|
||||
"https://wormhole-v2-mainnet-api.chainlayer.network",
|
||||
]
|
||||
: CLUSTER === "testnet"
|
||||
? [
|
||||
"https://wormhole-v2-testnet-api.certus.one",
|
||||
"https://wormhole-v2-testnet-api.mcf.rocks",
|
||||
"https://wormhole-v2-testnet-api.chainlayer.network",
|
||||
"https://wormhole-v2-testnet-api.staking.fund",
|
||||
"https://wormhole-v2-testnet-api.chainlayer.network",
|
||||
]
|
||||
: ["http://localhost:7071"];
|
||||
|
||||
export const ETH_NODE_URL = "ws://localhost:8545"; //TODO testnet
|
||||
export const POLYGON_NODE_URL = "ws:localhost:0000"; //TODO
|
||||
export const BSC_NODE_URL = "ws://localhost:8545"; //TODO testnet
|
||||
export const ETH_PRIVATE_KEY =
|
||||
"0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d";
|
||||
|
||||
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 function getSignerForChain(chainId: ChainId): Signer {
|
||||
const provider = new ethers.providers.WebSocketProvider(
|
||||
chainId === CHAIN_ID_POLYGON
|
||||
? POLYGON_NODE_URL
|
||||
: chainId === CHAIN_ID_BSC
|
||||
? BSC_NODE_URL
|
||||
: ETH_NODE_URL
|
||||
);
|
||||
const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
|
||||
return signer;
|
||||
}
|
||||
|
||||
export const ETH_TEST_WALLET_PUBLIC_KEY =
|
||||
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1";
|
||||
|
||||
export const SOLANA_TEST_TOKEN = "2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ"; //SOLT on devnet
|
||||
export const SOLANA_TEST_WALLET_PUBLIC_KEY =
|
||||
"6sbzC1eH4FTujJXWj51eQe25cYvr4xfXbJ1vAj7j2k5J";
|
|
@ -0,0 +1,80 @@
|
|||
import {
|
||||
attestFromEth,
|
||||
attestFromSolana,
|
||||
ChainId,
|
||||
CHAIN_ID_SOLANA,
|
||||
CHAIN_ID_TERRA,
|
||||
parseSequenceFromLogEth,
|
||||
parseSequenceFromLogSolana,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { Connection, Keypair } from "@solana/web3.js";
|
||||
import {
|
||||
getBridgeAddressForChain,
|
||||
getSignerForChain,
|
||||
getTokenBridgeAddressForChain,
|
||||
SOLANA_HOST,
|
||||
SOLANA_PRIVATE_KEY,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
} from "../consts";
|
||||
|
||||
/*
|
||||
This function attests the given token and returns the resultant sequence number, which is then used to retrieve the
|
||||
VAA from the guardians.
|
||||
*/
|
||||
export async function attest(
|
||||
originChain: ChainId,
|
||||
originAsset: string
|
||||
): Promise<string> {
|
||||
if (originChain === CHAIN_ID_SOLANA) {
|
||||
return attestSolana(originAsset);
|
||||
} else if (originChain === CHAIN_ID_TERRA) {
|
||||
return attestTerra(originAsset);
|
||||
} else {
|
||||
return attestEvm(originChain, originAsset);
|
||||
}
|
||||
}
|
||||
|
||||
export async function attestEvm(
|
||||
originChain: ChainId,
|
||||
originAsset: string
|
||||
): Promise<string> {
|
||||
const signer = getSignerForChain(originChain);
|
||||
const receipt = await attestFromEth(
|
||||
getTokenBridgeAddressForChain(originChain),
|
||||
signer,
|
||||
originAsset
|
||||
);
|
||||
const sequence = parseSequenceFromLogEth(
|
||||
receipt,
|
||||
getBridgeAddressForChain(originChain)
|
||||
);
|
||||
return sequence;
|
||||
}
|
||||
|
||||
export async function attestTerra(originAsset: string): Promise<string> {
|
||||
//TODO modify bridge_ui to use in-memory signer
|
||||
throw new Error("Unimplemented");
|
||||
}
|
||||
|
||||
export async function attestSolana(originAsset: string): Promise<string> {
|
||||
const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
|
||||
const payerAddress = keypair.publicKey.toString();
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const transaction = await attestFromSolana(
|
||||
connection,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
payerAddress,
|
||||
originAsset
|
||||
);
|
||||
transaction.partialSign(keypair);
|
||||
const txid = await connection.sendRawTransaction(transaction.serialize());
|
||||
await connection.confirmTransaction(txid);
|
||||
const info = await connection.getTransaction(txid);
|
||||
if (!info) {
|
||||
throw new Error("An error occurred while fetching the transaction info");
|
||||
}
|
||||
const sequence = parseSequenceFromLogSolana(info);
|
||||
return sequence;
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_SOLANA,
|
||||
CHAIN_ID_TERRA,
|
||||
getEmitterAddressEth,
|
||||
getEmitterAddressSolana,
|
||||
getEmitterAddressTerra,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import getSignedVAAWithRetry from "@certusone/wormhole-sdk/lib/rpc/getSignedVAAWithRetry";
|
||||
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
|
||||
import {
|
||||
getNFTBridgeAddressForChain,
|
||||
getTokenBridgeAddressForChain,
|
||||
WORMHOLE_RPC_HOSTS,
|
||||
} from "../consts";
|
||||
|
||||
export async function getSignedVAABySequence(
|
||||
chainId: ChainId,
|
||||
sequence: string,
|
||||
isNftTransfer: boolean
|
||||
): Promise<Uint8Array> {
|
||||
//Note, if handed a sequence which doesn't exist or was skipped for consensus this will retry until the timeout.
|
||||
const contractAddress = isNftTransfer
|
||||
? getNFTBridgeAddressForChain(chainId)
|
||||
: getTokenBridgeAddressForChain(chainId);
|
||||
const emitterAddress = await nativeAddressToEmitterAddress(
|
||||
chainId,
|
||||
contractAddress
|
||||
);
|
||||
const { vaaBytes } = await getSignedVAAWithRetry(
|
||||
WORMHOLE_RPC_HOSTS,
|
||||
chainId,
|
||||
emitterAddress,
|
||||
sequence,
|
||||
{
|
||||
transport: NodeHttpTransport(), //This should only be needed when running in node.
|
||||
},
|
||||
1000, //retryTimeout
|
||||
1000 //Maximum retry attempts
|
||||
);
|
||||
|
||||
return vaaBytes;
|
||||
}
|
||||
|
||||
async function nativeAddressToEmitterAddress(
|
||||
chainId: ChainId,
|
||||
address: string
|
||||
): Promise<string> {
|
||||
if (chainId === CHAIN_ID_SOLANA) {
|
||||
return await getEmitterAddressSolana(address);
|
||||
} else if (chainId === CHAIN_ID_TERRA) {
|
||||
return await getEmitterAddressTerra(address);
|
||||
} else {
|
||||
return getEmitterAddressEth(address); //Not a mistake, this one is synchronous.
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_BSC,
|
||||
CHAIN_ID_ETH,
|
||||
CHAIN_ID_POLYGON,
|
||||
CHAIN_ID_SOLANA,
|
||||
CHAIN_ID_TERRA,
|
||||
postVaaSolana,
|
||||
redeemOnEth,
|
||||
redeemOnSolana,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { Signer } from "@ethersproject/abstract-signer";
|
||||
import { Connection, Keypair } from "@solana/web3.js";
|
||||
import {
|
||||
getNFTBridgeAddressForChain,
|
||||
getSignerForChain,
|
||||
getTokenBridgeAddressForChain,
|
||||
SOLANA_HOST,
|
||||
SOLANA_PRIVATE_KEY,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
} from "../consts";
|
||||
|
||||
//This function attempts to redeem the VAA on the target chain.
|
||||
export async function redeem(
|
||||
targetChain: ChainId,
|
||||
signedVaa: Uint8Array,
|
||||
isNftTransfer: boolean
|
||||
) {
|
||||
if (
|
||||
targetChain === CHAIN_ID_ETH ||
|
||||
targetChain === CHAIN_ID_POLYGON ||
|
||||
targetChain === CHAIN_ID_BSC
|
||||
) {
|
||||
redeemEvm(signedVaa, targetChain, isNftTransfer);
|
||||
} else if (targetChain === CHAIN_ID_SOLANA) {
|
||||
redeemSolana(signedVaa, isNftTransfer);
|
||||
} else if (targetChain === CHAIN_ID_TERRA) {
|
||||
redeemTerra(signedVaa);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export async function redeemEvm(
|
||||
signedVAA: Uint8Array,
|
||||
targetChain: ChainId,
|
||||
isNftTransfer: boolean
|
||||
) {
|
||||
const signer: Signer = getSignerForChain(targetChain);
|
||||
try {
|
||||
await redeemOnEth(
|
||||
isNftTransfer
|
||||
? getNFTBridgeAddressForChain(targetChain)
|
||||
: getTokenBridgeAddressForChain(targetChain),
|
||||
signer,
|
||||
signedVAA
|
||||
);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
export async function redeemSolana(
|
||||
signedVAA: Uint8Array,
|
||||
isNftTransfer: boolean
|
||||
) {
|
||||
if (isNftTransfer) {
|
||||
//TODO
|
||||
//Solana redemptions require sending metadata to the chain inside of transactions,
|
||||
//and this in not yet available in the sdk.
|
||||
return;
|
||||
}
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
|
||||
const payerAddress = keypair.publicKey.toString();
|
||||
await postVaaSolana(
|
||||
connection,
|
||||
async (transaction) => {
|
||||
transaction.partialSign(keypair);
|
||||
return transaction;
|
||||
},
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
payerAddress,
|
||||
Buffer.from(signedVAA)
|
||||
);
|
||||
await redeemOnSolana(
|
||||
connection,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
payerAddress,
|
||||
signedVAA
|
||||
);
|
||||
}
|
||||
|
||||
export async function redeemTerra(signedVAA: Uint8Array) {
|
||||
//TODO adapt bridge_ui implementation to use in-memory terra key
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_SOLANA,
|
||||
CHAIN_ID_TERRA,
|
||||
hexToUint8Array,
|
||||
nativeToHexString,
|
||||
parseSequenceFromLogEth,
|
||||
parseSequenceFromLogSolana,
|
||||
transferFromEth,
|
||||
transferFromEthNative,
|
||||
transferFromSolana,
|
||||
transferNativeSol,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { parseUnits } from "@ethersproject/units";
|
||||
import { Connection, Keypair } from "@solana/web3.js";
|
||||
import {
|
||||
getBridgeAddressForChain,
|
||||
getSignerForChain,
|
||||
getTokenBridgeAddressForChain,
|
||||
SOLANA_HOST,
|
||||
SOLANA_PRIVATE_KEY,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
} from "../consts";
|
||||
|
||||
/*
|
||||
This function transfers the given token and returns the resultant sequence number, which is then used to retrieve the
|
||||
VAA from the guardians.
|
||||
*/
|
||||
export async function transferTokens(
|
||||
sourceChain: ChainId,
|
||||
amount: string,
|
||||
targetChain: ChainId,
|
||||
sourceAddress: string,
|
||||
recipientAddress: string,
|
||||
isNativeAsset: boolean,
|
||||
assetAddress?: string,
|
||||
decimals?: number
|
||||
): Promise<string> {
|
||||
//TODO support native assets,
|
||||
//TODO set relayer fee,
|
||||
if (sourceChain === CHAIN_ID_SOLANA) {
|
||||
return transferSolana(
|
||||
amount,
|
||||
targetChain,
|
||||
sourceAddress,
|
||||
recipientAddress,
|
||||
isNativeAsset,
|
||||
assetAddress,
|
||||
decimals
|
||||
);
|
||||
} else if (sourceChain === CHAIN_ID_TERRA) {
|
||||
return transferTerra(
|
||||
amount,
|
||||
targetChain,
|
||||
recipientAddress,
|
||||
isNativeAsset,
|
||||
assetAddress,
|
||||
decimals
|
||||
);
|
||||
} else {
|
||||
return transferEvm(
|
||||
sourceChain,
|
||||
amount,
|
||||
targetChain,
|
||||
recipientAddress,
|
||||
isNativeAsset,
|
||||
assetAddress,
|
||||
decimals
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function transferSolana(
|
||||
amount: string,
|
||||
targetChain: ChainId,
|
||||
sourceAddress: string,
|
||||
recipientAddress: string,
|
||||
isNativeAsset: boolean,
|
||||
assetAddress?: string,
|
||||
decimals?: number
|
||||
): Promise<string> {
|
||||
if (isNativeAsset) {
|
||||
decimals = 9;
|
||||
} else if (!assetAddress || !decimals) {
|
||||
throw new Error("No token specified for transfer.");
|
||||
}
|
||||
const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
|
||||
const payerAddress = keypair.publicKey.toString();
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const amountParsed = parseUnits(amount, decimals).toBigInt();
|
||||
const hexString = nativeToHexString(recipientAddress, targetChain);
|
||||
if (!hexString) {
|
||||
throw new Error("Invalid recipient");
|
||||
}
|
||||
const vaaCompatibleAddress = hexToUint8Array(hexString);
|
||||
const promise = isNativeAsset
|
||||
? transferNativeSol(
|
||||
connection,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
payerAddress,
|
||||
amountParsed,
|
||||
vaaCompatibleAddress,
|
||||
targetChain
|
||||
)
|
||||
: transferFromSolana(
|
||||
connection,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
payerAddress, //Actual SOL fee paying address
|
||||
sourceAddress, //SPL token account
|
||||
assetAddress as string,
|
||||
amountParsed,
|
||||
vaaCompatibleAddress,
|
||||
targetChain
|
||||
//TODO support non-wormhole assets here.
|
||||
// originAddress,
|
||||
// originChain
|
||||
);
|
||||
const transaction = await promise;
|
||||
transaction.partialSign(keypair);
|
||||
const txid = await connection.sendRawTransaction(transaction.serialize());
|
||||
await connection.confirmTransaction(txid);
|
||||
const info = await connection.getTransaction(txid);
|
||||
if (!info) {
|
||||
throw new Error("An error occurred while fetching the transaction info");
|
||||
}
|
||||
const sequence = parseSequenceFromLogSolana(info);
|
||||
return sequence;
|
||||
}
|
||||
export async function transferTerra(
|
||||
amount: string,
|
||||
targetChain: ChainId,
|
||||
recipientAddress: string,
|
||||
isNativeAsset: boolean,
|
||||
assetAddress?: string,
|
||||
decimals?: number
|
||||
): Promise<string> {
|
||||
//TODO modify bridge_ui to use in-memory signer
|
||||
throw new Error("Unimplemented");
|
||||
}
|
||||
|
||||
export async function transferEvm(
|
||||
sourceChain: ChainId,
|
||||
amount: string,
|
||||
targetChain: ChainId,
|
||||
recipientAddress: string,
|
||||
isNativeAsset: boolean,
|
||||
assetAddress?: string,
|
||||
decimals?: number
|
||||
): Promise<string> {
|
||||
if (isNativeAsset) {
|
||||
decimals = 18;
|
||||
} else if (!assetAddress || !decimals) {
|
||||
throw new Error("No token specified for transfer.");
|
||||
}
|
||||
const amountParsed = parseUnits(amount, decimals);
|
||||
const signer = getSignerForChain(sourceChain);
|
||||
const hexString = nativeToHexString(recipientAddress, targetChain);
|
||||
if (!hexString) {
|
||||
throw new Error("Invalid recipient");
|
||||
}
|
||||
const vaaCompatibleAddress = hexToUint8Array(hexString);
|
||||
const receipt = isNativeAsset
|
||||
? await transferFromEthNative(
|
||||
getTokenBridgeAddressForChain(sourceChain),
|
||||
signer,
|
||||
amountParsed,
|
||||
targetChain,
|
||||
vaaCompatibleAddress
|
||||
)
|
||||
: await transferFromEth(
|
||||
getTokenBridgeAddressForChain(sourceChain),
|
||||
signer,
|
||||
assetAddress as string,
|
||||
amountParsed,
|
||||
targetChain,
|
||||
vaaCompatibleAddress
|
||||
);
|
||||
|
||||
return await parseSequenceFromLogEth(
|
||||
receipt,
|
||||
getBridgeAddressForChain(sourceChain)
|
||||
);
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
||||
import { setDefaultWasm } from "@certusone/wormhole-sdk/lib/solana/wasm";
|
||||
import {
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
Token,
|
||||
TOKEN_PROGRAM_ID,
|
||||
} from "@solana/spl-token";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import { relay } from "./basicRelayer";
|
||||
import {
|
||||
ETH_TEST_WALLET_PUBLIC_KEY,
|
||||
SOLANA_TEST_TOKEN,
|
||||
SOLANA_TEST_WALLET_PUBLIC_KEY,
|
||||
} from "./consts";
|
||||
/*
|
||||
The goal of this example program is to demonstrate a common Wormhole token bridge
|
||||
use-case.
|
||||
|
||||
*/
|
||||
import { attest } from "./core/attestation";
|
||||
import { getSignedVAABySequence } from "./core/guardianQuery";
|
||||
import { redeem } from "./core/redeem";
|
||||
import { transferTokens } from "./core/transfer";
|
||||
setDefaultWasm("node");
|
||||
|
||||
/*
|
||||
This example attests a test token on Solana, retrieves the resulting VAA, and then submits it
|
||||
to Ethereum, thereby registering the token on Ethereum.
|
||||
*/
|
||||
export async function attestationExample() {
|
||||
const sequenceNumber = await attest(CHAIN_ID_SOLANA, SOLANA_TEST_TOKEN);
|
||||
|
||||
const signedVaa = await getSignedVAABySequence(
|
||||
CHAIN_ID_SOLANA,
|
||||
sequenceNumber,
|
||||
false
|
||||
);
|
||||
await redeem(CHAIN_ID_ETH, signedVaa, false);
|
||||
}
|
||||
|
||||
export async function transferWithRelayHandoff() {
|
||||
const sourceAddress = (
|
||||
await Token.getAssociatedTokenAddress(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
new PublicKey(SOLANA_TEST_TOKEN),
|
||||
new PublicKey(SOLANA_TEST_WALLET_PUBLIC_KEY)
|
||||
)
|
||||
).toString();
|
||||
|
||||
const sequenceNumber = await transferTokens(
|
||||
CHAIN_ID_SOLANA,
|
||||
"1.0",
|
||||
CHAIN_ID_ETH,
|
||||
sourceAddress,
|
||||
ETH_TEST_WALLET_PUBLIC_KEY,
|
||||
false,
|
||||
SOLANA_TEST_TOKEN,
|
||||
9
|
||||
);
|
||||
|
||||
await relay(CHAIN_ID_SOLANA, sequenceNumber, false);
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import { exit } from "process";
|
||||
import * as examples from "../lib/examples";
|
||||
|
||||
function logWrapper(promise) {
|
||||
return promise.catch((e) => {
|
||||
console.log(e);
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
export async function runAll() {
|
||||
console.log("Attestation example");
|
||||
await logWrapper(examples.attestationExample());
|
||||
console.log("Attestation complete.");
|
||||
console.log();
|
||||
|
||||
console.log("Transfer example");
|
||||
await logWrapper(examples.transferWithRelayHandoff());
|
||||
console.log("Transfer example complete.");
|
||||
console.log();
|
||||
|
||||
console.log("Complete");
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let done = false;
|
||||
runAll().then(
|
||||
() => (done = true),
|
||||
() => (done = true)
|
||||
);
|
||||
function wait() {
|
||||
if (!done) {
|
||||
setTimeout(wait, 1000);
|
||||
} else {
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
wait();
|
|
@ -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__/*"]
|
||||
}
|
Loading…
Reference in New Issue