examples: beginnings of an examples codebase

Change-Id: I3c84aa954aef80307dba400df1182489eb6eedb7
This commit is contained in:
Chase Moran 2021-11-18 01:13:25 -05:00 committed by Evan Gray
parent d678cb4662
commit 5ed2b2a06d
13 changed files with 16053 additions and 0 deletions

33
examples/.gitignore vendored Normal file
View File

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

8
examples/jestconfig.json Normal file
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/"]
}

15093
examples/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

58
examples/package.json Normal file
View File

@ -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"
}
}

View File

@ -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;
}

224
examples/src/consts.ts Normal file
View File

@ -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";

View File

@ -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;
}

View File

@ -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.
}
}

View File

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

View File

@ -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)
);
}

63
examples/src/examples.ts Normal file
View File

@ -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);
}

39
examples/src/runner.js Normal file
View File

@ -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();

13
examples/tsconfig.json Normal file
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__/*"]
}