From f288a44790c5ec33f4a3eb85e8e9bc9f40171ccc Mon Sep 17 00:00:00 2001 From: spacemandev Date: Fri, 5 Aug 2022 02:01:31 -0500 Subject: [PATCH] added js examples for sdk --- src/SUMMARY.md | 6 +- .../portal/sdkjs/evm-solana-transfer.md | 110 +++++++++ src/development/portal/sdkjs/overview.md | 9 + .../portal/sdkjs/polygon-oasis-relayer.md | 218 ++++++++++++++++++ 4 files changed, 340 insertions(+), 3 deletions(-) create mode 100644 src/development/portal/sdkjs/evm-solana-transfer.md create mode 100644 src/development/portal/sdkjs/overview.md create mode 100644 src/development/portal/sdkjs/polygon-oasis-relayer.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 1e69249..f5e1cd8 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -62,9 +62,9 @@ - [EVM]() - [Attesting](./development/portal/evm/attestingToken.md) - [Transfer Tokens](./development/portal/evm/tokenTransfer.md) - - [Portal JS Client Transfers]() - - [EVM to Solana Transfer]() - - [Polygon to Oasis with Relayers]() + - [Portal JS SDK](./development/portal/sdkjs/overview.md) + - [EVM to Solana Transfer](./development/portal/sdkjs/evm-solana-transfer.md) + - [Polygon to Oasis with Relayers](./development/portal/sdkjs/polygon-oasis-relayer.md) - [Portal Payloads]() --- diff --git a/src/development/portal/sdkjs/evm-solana-transfer.md b/src/development/portal/sdkjs/evm-solana-transfer.md new file mode 100644 index 0000000..9bed871 --- /dev/null +++ b/src/development/portal/sdkjs/evm-solana-transfer.md @@ -0,0 +1,110 @@ +# EVM to Solana Token Transfer + +A cornerstone of cross chain apps (xDapps) is the ability to move tokens from one chain to another. Wormhole’s APIs make that a breeze. + +Let’s do a simple programmatic transfer from Eth to Solana. First, we need to figure out what address on Solana where we are sending the tokens. Unlike EVM chains where the address is just the wallet address, we need to send the tokens to our recipient address associated token account for that token. We can use a couple helper functions from the Wormhole SDK to make this possible. + +```ts +import { + Token, + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID +} from '@solana/spl-token'; +import { + getForeignAssetSolana, + hexToUint8Array, + nativeToHexString, + CHAIN_ID_ETH, +} from '@certusone/wormhole-sdk'; + +const SOLANA_TOKEN_BRIDGE_ADDRESS = "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb"; +// determine destination address - an associated token account +const solanaMintKey = new PublicKey( + (await getForeignAssetSolana( + connection, + SOLANA_TOKEN_BRIDGE_ADDRESS, + CHAIN_ID_ETH, + hexToUint8Array(nativeToHexString(tokenAddress, CHAIN_ID_ETH) || "") + )) || "" +); +const recipientAddress = await Token.getAssociatedTokenAddress( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + solanaMintKey, + recipientWalletAddress +); +``` + +After we have the receipt token account on Solana, we can come back and submit the transfer message on Ethereum. This will output a log that contains a sequence number (A nonce for the message) and an emitter address (the ETH Token Bridge Address as bytes) . The sequence number and emitter address will be used to fetch a VAA after it’s been signed by Guardians. + +```ts +import { + trasnferFromEth, + parseSequenceFromLogEth, + getEmitterAddressEth, + CHAIN_ID_SOLANA, +} from '@certusone/wormhole-sdk'; + +const ETH_TOKEN_BRIDGE_ADDRESS = "0x3ee18B2214AFF97000D974cf647E7C347E8fa585"; + +// Submit transaction - results in a Wormhole message being published +const receipt = await transferFromEth( + ETH_TOKEN_BRIDGE_ADDRESS, + signer, + tokenAddress, + amount, + CHAIN_ID_SOLANA, + recipientAddress +); +// Get the sequence number and emitter address required to fetch the signedVAA of our message +const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS); +const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS); +``` + +Once the Guardians have signed the token message, we can fetch it to use in the redeem step. If you’re a developer, you might run this as an automatic process through an application specific relayer (more on that in a later thread!) + +```ts +import { + getSignedVAA, +} from '@certusone/wormhole-sdk'; + +// Fetch the signedVAA from the Wormhole Network (this may require retries while you wait for confirmation) +const { signedVAA } = await getSignedVAA( + WORMHOLE_RPC_HOST, + CHAIN_ID_ETH, + emitterAddress, + sequence +); +``` + +Then we can post the VAA to Solana to mint the tokens. Because of the compute limit on Solana, we split the signature verification and token claim into steps. First we'll verify all the signatures and create a claim account for the Token. + +```ts +const SOL_BRIDGE_ADDRESS = "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth"; +// On Solana, we have to post the signedVAA ourselves +await postVaaSolana( + connection, // Solana Mainnet Connection + wallet, //Solana Wallet Signer + SOL_BRIDGE_ADDRESS, + payerAddress, + signedVAA +); +``` + +Finally we can claim the tokens + +```ts +// Finally, redeem on Solana +const transaction = await redeemOnSolana( + connection, + SOL_BRIDGE_ADDRESS, + SOL_TOKEN_BRIDGE_ADDRESS, + payerAddress, + signedVAA, + isSolanaNative, + mintAddress +); +const signed = await wallet.signTransaction(transaction); +const txid = await connection.sendRawTransaction(signed.serialize()); +await connection.confirmTransaction(txid); +``` \ No newline at end of file diff --git a/src/development/portal/sdkjs/overview.md b/src/development/portal/sdkjs/overview.md new file mode 100644 index 0000000..f60f0f3 --- /dev/null +++ b/src/development/portal/sdkjs/overview.md @@ -0,0 +1,9 @@ +# Portal JS SDK Overview + +For applications that only need to interact with the Core and Token Bridge contracts off chain, there is a Wormhole JS SDK provided. + +It can be installed using npm like so: + +```sh +npm i @certusone/wormholesdk +``` \ No newline at end of file diff --git a/src/development/portal/sdkjs/polygon-oasis-relayer.md b/src/development/portal/sdkjs/polygon-oasis-relayer.md new file mode 100644 index 0000000..ee6f7e1 --- /dev/null +++ b/src/development/portal/sdkjs/polygon-oasis-relayer.md @@ -0,0 +1,218 @@ +# Polygon to Oasis with Relayers + +In this example we’ll fetch the fee schedule and attach a relayer fee onto our transaction. This is a non trivial example as we’ll also use Polygon as a source chain, which has some quirks when it comes to gas estimation. In the future, this whole process is being simplified, so check back in the future for hopefully much simpler version of this example. + +For this example, we’ll need a couple of packages: + +```bash +npm i --save @certusone/wormhole-sdk ethers node-fetch +``` + +Then let's get started writing some code: + +```ts +import { BigNumber, ethers } from "ethers"; +import fetch from "node-fetch"; +import { + getEmitterAddressEth, + hexToUint8Array, + nativeToHexString, + parseSequenceFromLogEth, + CHAIN_ID_POLYGON, + CHAIN_ID_OASIS, + transferFromEthNative, + getIsTransferCompletedEth, + setDefaultWasm +} from "@certusone/wormhole-sdk"; +``` + +### Setup the Polygon and Oasis Wallets + +First, let us set up the two wallets we’ll be sending and receiving from. While we are instantiating both wallets with their private keys, we only need the Public key of the receiving wallet for this example. + +```ts +const EmeraldWallet = new ethers.Wallet( + privatekey_emerald, + new ethers.providers.JsonRpcProvider("https://emerald.oasis.dev") +); +const PolygonWallet = new ethers.Wallet( + privatekey_polygon, + new ethers.providers.JsonRpcProvider("https://polygon-rpc.com/") +); +``` + +### Fetch the fee schedule +Next, we’ll fetch the fee schedule for the Portal Token Bridge relayer. This fee schedule will give us the minimum fee for each recipient chain that the relayer will accept. As long as we attach at least that fee in the relayer fee, we can be fairly confident that the relayer will pick up the transaction and relay it to the recipient chain. The fee will cover the gas cost for the relayer along with a little extra to make it worth their time to run the relayer service. + +We will also define the transfer amount in this step. The fee schedule will either return a flat fee in USD for the recipient chain, or a percentage fee (usually only for Ethereum). Either way, we’ll need to calculate the fee in in BigNumber format (no decimals). + +For example, 1 MATIC on Polygon is 1e18 wei, or 1000000000000000000 wei. Because EVM has a hard time with floating point math, we have to do all our transactions in this small unit, to avoid decimal numbers. + +```ts +const transferAmount = BigNumber.from("1000000000000000000"); // We are sending 1 MATIC over the wall to Oasis +const relayerFeeSchedule = await (await fetch( + "https://raw.githubusercontent.com/certusone/wormhole-relayer-list/main/relayer.json" +)).json(); +``` + +The fee schedule has the following interface: + +```ts +export interface RelayerFeeSchedule { + supportedTokens: ChainAddress[]; + relayers: Relayer[]; + feeSchedule: FeeSchedule; +} + +interface ChainAddress { + chainId: number; + address: string; + coingeckoId: string; +} + +interface Relayer { + name: string; + url: string; +} + +interface FeeSchedule { + [chainId: string]: { + type: "flat" | "percent"; + feeUsd?: number; + feePercent?: number; + gasEstimate?: number; + }; +} +``` + +After we’ve fetched the fee schedule, we need to find the fee in Wei that needs to be paid to the Relayer. At the time of writing, Oasis has a flat fee of $0.50, so to calculate how much MATIC we need to pay for the $0.50 fee, we need to fetch the MATIC price. Let’s use the free CoinGecko api: + +```ts +let feeWei: number; +if (relayerFeeSchedule.feeSchedule[CHAIN_ID_OASIS].type == "flat") { + const feeUsd = relayerFeeSchedule.feeSchedule[CHAIN_ID_OASIS].feeUsd + const MATIC_PRICE = ( + await ( + await fetch( + "https://api.coingecko.com/api/v3/simple/token_price/polygon-pos?contract_addresses=0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270&vs_currencies=usd" + ) + ).json() + )["0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270"]["usd"]; + + feeWei = (feeUsd / MATIC_PRICE) * 1e18; +} else if (relayerFeeSchedule.feeSchedule[CHAIN_ID_OASIS].type == "percent") { + let feeWei = (relayerFeeSchedule.feeSchedule[CHAIN_ID_OASIS].feePercent /100) * transferAmount.toNumber(); +} +``` + +### Add override for gas estimation for Polygon +Because the source chain is Polygon, we need to do this additional step to overestimate the gas. This is because ethers library has some problems with fee estimation after EIP-1559. + +```ts +let overrides; +let feeData = await PolygonWallet.provider.getFeeData(); +overrides = { + maxFeePerGas: feeData.maxFeePerGas?.mul(50) || undefined, + maxPriorityFeePerGas: + feeData.maxPriorityFeePerGas?.mul(50) || undefined, +}; +``` + +### Emit Portal Message +Now we have all the pieces we need to emit a Portal Bridge message with a relay fee attached. We do this using the transferFromEthNative() method. EthNative is used because we’re transferring the native token of the Polygon network rather than an ERC20 token. + +```ts +const POLYGON_TOKEN_BRIDGE = "0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE"; + +const receipt = await transferFromEthNative( + POLYGON_TOKEN_BRIDGE, + PolygonWallet, + transferAmount, + CHAIN_ID_OASIS, + hexToUint8Array( + nativeToHexString( + await EmeraldWallet.getAddress(), + CHAIN_ID_OASIS + ) || "" + ), + BigNumber.from(feeWei.toString()), + overrides +); +console.log("Receipt: ", receipt); + +const POLYGON_CORE_BRIDGE_ADDRESS = + "0x7A4B5a56256163F07b2C80A7cA55aBE66c4ec4d7"; +const sequence = parseSequenceFromLogEth( + receipt, + POLYGON_CORE_BRIDGE_ADDRESS +); +const emitterAddress = getEmitterAddressEth(POLYGON_TOKEN_BRIDGE); +console.log("Sequence: ", sequence); +console.log("EmitterAddress: ", emitterAddress); +``` + +Let’s walk through each of the arguments of this function and what they mean. + +`POLYGON_TOKEN_BRIDGE` is the address of the Portal Token Bridge on the Polygon network. You can find it, amongst other addresses on the Deployment Info page. + +`PolygonWallet` is a signer you get from the Ethers library that holds a private key that can sign transactions, + +`transferAmount` is a BigNumber that contains the amount to transfer in the smallest unit of the network. + +`CHAIN_ID_OASIS` is a constant that identifies the target chain + +`hexToUint8Array()` translates the target publickey into a wormhole public key. + +`BigNumber.from(feeWei.toString())` identifies the fee in smallest unit of the network for the relayer. + +`overrides` are used if we need to override the gas cost, which we need to do for Polygon. + +### Check VAA was signed + +Wait 15 min for finality on Polygon and check to see if was submitted. If successful you’ll be able to fetch a base64 encoded vaaBytes. We need this in the next step where we check if the transaction was successfully relayed. + +```ts +await new Promise((r) => setTimeout(r, 900000)); //15m in seconds +const WORMHOLE_RPC = "https://wormhole-v2-mainnet-api.certus.one"; +let vaaBytes = undefined; +while (!vaaBytes) { + try { + vaaBytes = ( + await ( + await fetch( + `${WORMHOLE_RPC}/v1/signed_vaa/${CHAIN_ID_POLYGON}/${emitterAddress}/${sequence}` + ) + ).json() + ).vaaBytes; + } catch (e) { + await new Promise((r) => setTimeout(r, 5000)); + } +} +console.log("VAA Bytes: ", vaaBytes); +``` + +### Check if the transfer was completed + +In the final step we use the getIsTransferCompletedEth() method to check if the transfer was completed on the Oasis Emerald chain. If it’s not, we wait 5 seconds and check again. + +```ts +setDefaultWasm("node"); //only needed if running in node.js +const EMERALD_TOKEN_BRIDGE = "0x5848C791e09901b40A9Ef749f2a6735b418d7564"; +let transferCompleted = await getIsTransferCompletedEth( + EMERALD_TOKEN_BRIDGE, + EmeraldWallet.provider, + vaaBytes +); +while (!transferCompleted) { + await new Promise((r) => setTimeout(r, 5000)); + transferCompleted = await getIsTransferCompletedEth( + EMERALD_TOKEN_BRIDGE, + EmeraldWallet.provider, + vaaBytes + ); +} + +console.log("VAA Relayed!"); +``` + +And that's it! You've successfully programmatically relayed a transaction!