Переглянути джерело

bridge_ui: weth functionality

Change-Id: I1c6dc5f502232c32f4219a9a3be61f203c7be22a
Chase Moran 4 роки тому
батько
коміт
b39d72e32f

+ 28 - 14
bridge_ui/src/components/TokenSelectors/EthereumSourceTokenSelector.tsx

@@ -43,6 +43,20 @@ const useStyles = makeStyles(() =>
   })
 );
 
+const getSymbol = (account: ParsedTokenAccount | null) => {
+  if (!account) {
+    return undefined;
+  }
+  return account.symbol;
+};
+
+const getLogo = (account: ParsedTokenAccount | null) => {
+  if (!account) {
+    return undefined;
+  }
+  return account.logo;
+};
+
 const isWormholev1 = (provider: any, address: string) => {
   const connection = WormholeAbi__factory.connect(
     WORMHOLE_V1_ETH_ADDRESS,
@@ -67,8 +81,8 @@ const renderAccount = (
   classes: any
 ) => {
   const mintPrettyString = shortenAddress(account.mintKey);
-  const uri = covalentData?.logo_url;
-  const symbol = covalentData?.contract_ticker_symbol || "Unknown";
+  const uri = getLogo(account);
+  const symbol = getSymbol(account) || "Unknown";
   return (
     <div className={classes.tokenOverviewContainer}>
       <div>
@@ -78,7 +92,11 @@ const renderAccount = (
         <Typography variant="subtitle1">{symbol}</Typography>
       </div>
       <div>
-        <Typography variant="body1">{mintPrettyString}</Typography>
+        {
+          <Typography variant="body1">
+            {account.isNativeAsset ? "Native" : mintPrettyString}
+          </Typography>
+        }
       </div>
       <div>
         <Typography variant="body2">{"Balance"}</Typography>
@@ -193,6 +211,10 @@ export default function EthereumSourceTokenSelector(
         onChange(autocompleteHolder);
         return;
       }
+      if (autocompleteHolder.isNativeAsset) {
+        onChange(autocompleteHolder);
+        return;
+      }
       isWormholev1(provider, autocompleteHolder.mintKey).then(
         (result) => {
           if (!cancelled) {
@@ -376,16 +398,6 @@ export default function EthereumSourceTokenSelector(
     []
   );
 
-  const getSymbol = (account: ParsedTokenAccount | null) => {
-    if (!account) {
-      return undefined;
-    }
-    const item = covalent?.data?.find(
-      (x) => x.contract_address === account.mintKey
-    );
-    return item ? item.contract_ticker_symbol : undefined;
-  };
-
   const filterConfig = createFilterOptions({
     matchFrom: "any",
     stringify: (option: ParsedTokenAccount) => {
@@ -498,7 +510,9 @@ export default function EthereumSourceTokenSelector(
       ) : (
         <RefreshButtonWrapper callback={resetAccountWrapper}>
           <Typography>
-            {(symbol ? symbol + " " : "") + value.mintKey}
+            {value.isNativeAsset
+              ? value.symbol
+              : (symbol ? symbol + " " : "") + value.mintKey}
           </Typography>
         </RefreshButtonWrapper>
       )}

+ 37 - 3
bridge_ui/src/components/Transfer/Redeem.tsx

@@ -1,23 +1,57 @@
+import { CHAIN_ID_ETH } from "@certusone/wormhole-sdk";
+import { Checkbox, FormControlLabel } from "@material-ui/core";
+import { useCallback, useState } from "react";
 import { useSelector } from "react-redux";
 import { useHandleRedeem } from "../../hooks/useHandleRedeem";
 import useIsWalletReady from "../../hooks/useIsWalletReady";
-import { selectTransferTargetChain } from "../../store/selectors";
+import {
+  selectTransferTargetAsset,
+  selectTransferTargetChain,
+} from "../../store/selectors";
+import { WETH_ADDRESS } from "../../utils/consts";
 import ButtonWithLoader from "../ButtonWithLoader";
 import KeyAndBalance from "../KeyAndBalance";
 import StepDescription from "../StepDescription";
 import WaitingForWalletMessage from "./WaitingForWalletMessage";
 
 function Redeem() {
-  const { handleClick, disabled, showLoader } = useHandleRedeem();
+  const { handleClick, handleNativeClick, disabled, showLoader } =
+    useHandleRedeem();
   const targetChain = useSelector(selectTransferTargetChain);
+  const targetAssetHex = useSelector(selectTransferTargetAsset);
   const { isReady, statusMessage } = useIsWalletReady(targetChain);
+  //TODO better check, probably involving a hook & the VAA
+  const isNativeEligible =
+    targetChain === CHAIN_ID_ETH &&
+    targetAssetHex &&
+    targetAssetHex.toLowerCase() === WETH_ADDRESS.toLowerCase();
+  const [useNativeRedeem, setUseNativeRedeem] = useState(true);
+  const toggleNativeRedeem = useCallback(() => {
+    setUseNativeRedeem(!useNativeRedeem);
+  }, [useNativeRedeem]);
+
   return (
     <>
       <StepDescription>Receive the tokens on the target chain</StepDescription>
       <KeyAndBalance chainId={targetChain} />
+      {isNativeEligible && (
+        <FormControlLabel
+          control={
+            <Checkbox
+              checked={useNativeRedeem}
+              onChange={toggleNativeRedeem}
+              color="primary"
+            />
+          }
+          label="Automatically unwrap to native currency"
+        />
+      )}
+
       <ButtonWithLoader
         disabled={!isReady || disabled}
-        onClick={handleClick}
+        onClick={
+          isNativeEligible && useNativeRedeem ? handleNativeClick : handleClick
+        }
         showLoader={showLoader}
         error={statusMessage}
       >

+ 13 - 8
bridge_ui/src/components/Transfer/SourcePreview.tsx

@@ -25,15 +25,20 @@ export default function SourcePreview() {
 
   const plural = parseInt(sourceAmount) !== 1;
 
+  const tokenExplainer = !sourceParsedTokenAccount
+    ? ""
+    : sourceParsedTokenAccount.isNativeAsset
+    ? sourceParsedTokenAccount.symbol
+    : `token${plural ? "s" : ""} of ${
+        sourceParsedTokenAccount.symbol ||
+        shortenAddress(sourceParsedTokenAccount.mintKey)
+      }`;
+
   const explainerString = sourceParsedTokenAccount
-    ? `You will transfer ${sourceAmount} token${
-        plural ? "s" : ""
-      } of ${shortenAddress(
-        sourceParsedTokenAccount?.mintKey
-      )}, from ${shortenAddress(sourceParsedTokenAccount?.publicKey)} on ${
-        CHAINS_BY_ID[sourceChain].name
-      }`
-    : "Step complete.";
+    ? `You will transfer ${sourceAmount} ${tokenExplainer}, from ${shortenAddress(
+        sourceParsedTokenAccount?.publicKey
+      )} on ${CHAINS_BY_ID[sourceChain].name}`
+    : "";
 
   return (
     <>

+ 101 - 7
bridge_ui/src/hooks/useGetSourceParsedTokenAccounts.ts

@@ -1,8 +1,10 @@
 import {
+  Bridge__factory,
   CHAIN_ID_ETH,
   CHAIN_ID_SOLANA,
   CHAIN_ID_TERRA,
 } from "@certusone/wormhole-sdk";
+import { ethers } from "@certusone/wormhole-sdk/node_modules/ethers";
 import { Dispatch } from "@reduxjs/toolkit";
 import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
 import {
@@ -13,9 +15,12 @@ import {
 } from "@solana/web3.js";
 import axios from "axios";
 import { formatUnits } from "ethers/lib/utils";
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useMemo, useState } from "react";
 import { useDispatch, useSelector } from "react-redux";
-import { useEthereumProvider } from "../contexts/EthereumProviderContext";
+import {
+  Provider,
+  useEthereumProvider,
+} from "../contexts/EthereumProviderContext";
 import { useSolanaWallet } from "../contexts/SolanaWalletContext";
 import {
   errorSourceParsedTokenAccounts as errorSourceParsedTokenAccountsNFT,
@@ -44,7 +49,13 @@ import {
   setSourceParsedTokenAccounts,
   setSourceWalletAddress,
 } from "../store/transferSlice";
-import { COVALENT_GET_TOKENS_URL, SOLANA_HOST } from "../utils/consts";
+import {
+  COVALENT_GET_TOKENS_URL,
+  ETH_TOKEN_BRIDGE_ADDRESS,
+  SOLANA_HOST,
+  WETH_ADDRESS,
+  WETH_DECIMALS,
+} from "../utils/consts";
 import {
   extractMintAuthorityInfo,
   getMultipleAccountsRPC,
@@ -59,7 +70,8 @@ export function createParsedTokenAccount(
   uiAmountString: string,
   symbol?: string,
   name?: string,
-  logo?: string
+  logo?: string,
+  isNativeAsset?: boolean
 ): ParsedTokenAccount {
   return {
     publicKey: publicKey,
@@ -71,6 +83,7 @@ export function createParsedTokenAccount(
     symbol,
     name,
     logo,
+    isNativeAsset,
   };
 }
 
@@ -139,6 +152,29 @@ const createParsedTokenAccountFromCovalent = (
   };
 };
 
+const createNativeEthParsedTokenAccount = (
+  provider: Provider,
+  signerAddress: string | undefined
+) => {
+  return !(provider && signerAddress)
+    ? Promise.reject()
+    : provider.getBalance(signerAddress).then((balanceInWei) => {
+        const balanceInEth = ethers.utils.formatEther(balanceInWei);
+        return createParsedTokenAccount(
+          signerAddress, //public key
+          WETH_ADDRESS, //Mint key, On the other side this will be WETH, so this is hopefully a white lie.
+          balanceInWei.toString(), //amount, in wei
+          WETH_DECIMALS, //Luckily both ETH and WETH have 18 decimals, so this should not be an issue.
+          parseFloat(balanceInEth), //This loses precision, but is a limitation of the current datamodel. This field is essentially deprecated
+          balanceInEth.toString(), //This is the actual display field, which has full precision.
+          "ETH", //A white lie for display purposes
+          "Ethereum", //A white lie for display purposes
+          undefined, //TODO logo
+          true //isNativeAsset
+        );
+      });
+};
+
 const createNFTParsedTokenAccountFromCovalent = (
   walletAddress: string,
   covalent: CovalentData,
@@ -282,7 +318,7 @@ function useGetAvailableTokens(nft: boolean = false) {
   );
   const solanaWallet = useSolanaWallet();
   const solPK = solanaWallet?.publicKey;
-  const { provider, signerAddress } = useEthereumProvider();
+  const { provider, signer, signerAddress } = useEthereumProvider();
 
   const [covalent, setCovalent] = useState<any>(undefined);
   const [covalentLoading, setCovalentLoading] = useState(false);
@@ -290,6 +326,12 @@ function useGetAvailableTokens(nft: boolean = false) {
     undefined
   );
 
+  const [ethNativeAccount, setEthNativeAccount] = useState<any>(undefined);
+  const [ethNativeAccountLoading, setEthNativeAccountLoading] = useState(false);
+  const [ethNativeAccountError, setEthNativeAccountError] = useState<
+    string | undefined
+  >(undefined);
+
   const [solanaMintAccounts, setSolanaMintAccounts] = useState<any>(undefined);
   const [solanaMintAccountsLoading, setSolanaMintAccountsLoading] =
     useState(false);
@@ -327,6 +369,10 @@ function useGetAvailableTokens(nft: boolean = false) {
     setCovalent(undefined); //These need to be included in the reset because they have balances on them.
     setCovalentLoading(false);
     setCovalentError("");
+
+    setEthNativeAccount(undefined);
+    setEthNativeAccountLoading(false);
+    setEthNativeAccountError("");
   }, [setCovalent, dispatch, nft]);
 
   //TODO this useEffect could be somewhere else in the codebase
@@ -410,7 +456,41 @@ function useGetAvailableTokens(nft: boolean = false) {
     return () => (cancelled = true);
   }, [tokenAccounts.data, lookupChain]);
 
-  //Ethereum accounts load
+  //Ethereum native asset load
+  useEffect(() => {
+    let cancelled = false;
+    if (
+      signerAddress &&
+      lookupChain === CHAIN_ID_ETH &&
+      !ethNativeAccount &&
+      !nft
+    ) {
+      setEthNativeAccountLoading(true);
+      createNativeEthParsedTokenAccount(provider, signerAddress).then(
+        (result) => {
+          console.log("create native account returned with value", result);
+          if (!cancelled) {
+            setEthNativeAccount(result);
+            setEthNativeAccountLoading(false);
+            setEthNativeAccountError("");
+          }
+        },
+        (error) => {
+          if (!cancelled) {
+            setEthNativeAccount(undefined);
+            setEthNativeAccountLoading(false);
+            setEthNativeAccountError("Unable to retrieve your ETH balance.");
+          }
+        }
+      );
+    }
+
+    return () => {
+      cancelled = true;
+    };
+  }, [lookupChain, provider, signerAddress, nft, ethNativeAccount]);
+
+  //Ethereum covalent accounts load
   useEffect(() => {
     //const testWallet = "0xf60c2ea62edbfe808163751dd0d8693dcb30019c";
     // const nftTestWallet1 = "0x3f304c6721f35ff9af00fd32650c8e0a982180ab";
@@ -484,6 +564,20 @@ function useGetAvailableTokens(nft: boolean = false) {
   //At present, we don't have any mechanism for doing this.
   useEffect(() => {}, []);
 
+  const ethAccounts = useMemo(() => {
+    const output = { ...tokenAccounts };
+    output.data = output.data?.slice() || [];
+    output.isFetching = output.isFetching || ethNativeAccountLoading;
+    output.error = output.error || ethNativeAccountError;
+    ethNativeAccount && output.data && output.data.unshift(ethNativeAccount);
+    return output;
+  }, [
+    ethNativeAccount,
+    ethNativeAccountLoading,
+    ethNativeAccountError,
+    tokenAccounts,
+  ]);
+
   return lookupChain === CHAIN_ID_SOLANA
     ? {
         tokenAccounts: tokenAccounts,
@@ -497,7 +591,7 @@ function useGetAvailableTokens(nft: boolean = false) {
       }
     : lookupChain === CHAIN_ID_ETH
     ? {
-        tokenAccounts: tokenAccounts,
+        tokenAccounts: ethAccounts,
         covalent: {
           data: covalent,
           isFetching: covalentLoading,

+ 44 - 8
bridge_ui/src/hooks/useHandleRedeem.ts

@@ -4,6 +4,7 @@ import {
   CHAIN_ID_TERRA,
   postVaaSolana,
   redeemOnEth,
+  redeemOnEthNative,
   redeemOnSolana,
   redeemOnTerra,
 } from "@certusone/wormhole-sdk";
@@ -39,15 +40,14 @@ async function eth(
   dispatch: any,
   enqueueSnackbar: any,
   signer: Signer,
-  signedVAA: Uint8Array
+  signedVAA: Uint8Array,
+  isNative: boolean
 ) {
   dispatch(setIsRedeeming(true));
   try {
-    const receipt = await redeemOnEth(
-      ETH_TOKEN_BRIDGE_ADDRESS,
-      signer,
-      signedVAA
-    );
+    const receipt = isNative
+      ? await redeemOnEthNative(ETH_TOKEN_BRIDGE_ADDRESS, signer, signedVAA)
+      : await redeemOnEth(ETH_TOKEN_BRIDGE_ADDRESS, signer, signedVAA);
     dispatch(
       setRedeemTx({ id: receipt.transactionHash, block: receipt.blockNumber })
     );
@@ -135,7 +135,7 @@ export function useHandleRedeem() {
   const isRedeeming = useSelector(selectTransferIsRedeeming);
   const handleRedeemClick = useCallback(() => {
     if (targetChain === CHAIN_ID_ETH && !!signer && signedVAA) {
-      eth(dispatch, enqueueSnackbar, signer, signedVAA);
+      eth(dispatch, enqueueSnackbar, signer, signedVAA, false);
     } else if (
       targetChain === CHAIN_ID_SOLANA &&
       !!solanaWallet &&
@@ -166,12 +166,48 @@ export function useHandleRedeem() {
     solPK,
     terraWallet,
   ]);
+
+  const handleRedeemNativeClick = useCallback(() => {
+    if (targetChain === CHAIN_ID_ETH && !!signer && signedVAA) {
+      eth(dispatch, enqueueSnackbar, signer, signedVAA, true);
+    } else if (
+      targetChain === CHAIN_ID_SOLANA &&
+      !!solanaWallet &&
+      !!solPK &&
+      signedVAA
+    ) {
+      solana(
+        dispatch,
+        enqueueSnackbar,
+        solanaWallet,
+        solPK.toString(),
+        signedVAA
+      );
+    } else if (targetChain === CHAIN_ID_TERRA && !!terraWallet && signedVAA) {
+      terra(dispatch, enqueueSnackbar, terraWallet, signedVAA); //TODO isNative = true
+    } else {
+      // enqueueSnackbar("Redeeming on this chain is not yet supported", {
+      //   variant: "error",
+      // });
+    }
+  }, [
+    dispatch,
+    enqueueSnackbar,
+    targetChain,
+    signer,
+    signedVAA,
+    solanaWallet,
+    solPK,
+    terraWallet,
+  ]);
+
   return useMemo(
     () => ({
+      handleNativeClick: handleRedeemNativeClick,
       handleClick: handleRedeemClick,
       disabled: !!isRedeeming,
       showLoader: !!isRedeeming,
     }),
-    [handleRedeemClick, isRedeeming]
+    [handleRedeemClick, isRedeeming, handleRedeemNativeClick]
   );
 }

+ 23 - 10
bridge_ui/src/hooks/useHandleTransfer.ts

@@ -10,6 +10,7 @@ import {
   parseSequenceFromLogSolana,
   parseSequenceFromLogTerra,
   transferFromEth,
+  transferFromEthNative,
   transferFromSolana,
   transferFromTerra,
 } from "@certusone/wormhole-sdk";
@@ -66,19 +67,28 @@ async function eth(
   decimals: number,
   amount: string,
   recipientChain: ChainId,
-  recipientAddress: Uint8Array
+  recipientAddress: Uint8Array,
+  isNative: boolean
 ) {
   dispatch(setIsSending(true));
   try {
     const amountParsed = parseUnits(amount, decimals);
-    const receipt = await transferFromEth(
-      ETH_TOKEN_BRIDGE_ADDRESS,
-      signer,
-      tokenAddress,
-      amountParsed,
-      recipientChain,
-      recipientAddress
-    );
+    const receipt = isNative
+      ? await transferFromEthNative(
+          ETH_TOKEN_BRIDGE_ADDRESS,
+          signer,
+          amountParsed,
+          recipientChain,
+          recipientAddress
+        )
+      : await transferFromEth(
+          ETH_TOKEN_BRIDGE_ADDRESS,
+          signer,
+          tokenAddress,
+          amountParsed,
+          recipientChain,
+          recipientAddress
+        );
     dispatch(
       setTransferTx({ id: receipt.transactionHash, block: receipt.blockNumber })
     );
@@ -237,6 +247,7 @@ export function useHandleTransfer() {
   );
   const sourceTokenPublicKey = sourceParsedTokenAccount?.publicKey;
   const decimals = sourceParsedTokenAccount?.decimals;
+  const isNative = sourceParsedTokenAccount?.isNativeAsset || false;
   const disabled = !isTargetComplete || isSending || isSendComplete;
   const handleTransferClick = useCallback(() => {
     // TODO: we should separate state for transaction vs fetching vaa
@@ -255,7 +266,8 @@ export function useHandleTransfer() {
         decimals,
         amount,
         targetChain,
-        targetAddress
+        targetAddress,
+        isNative
       );
     } else if (
       sourceChain === CHAIN_ID_SOLANA &&
@@ -318,6 +330,7 @@ export function useHandleTransfer() {
     targetAddress,
     originAsset,
     originChain,
+    isNative,
   ]);
   return useMemo(
     () => ({

+ 1 - 0
bridge_ui/src/store/transferSlice.ts

@@ -27,6 +27,7 @@ export interface ParsedTokenAccount {
   symbol?: string;
   name?: string;
   logo?: string;
+  isNativeAsset?: boolean;
 }
 
 export interface Transaction {

+ 8 - 0
bridge_ui/src/utils/consts.ts

@@ -162,6 +162,14 @@ export const COVALENT_GET_TOKENS_URL = (
   }`;
 };
 
+export const WETH_ADDRESS =
+  CLUSTER === "mainnet"
+    ? "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
+    : CLUSTER === "testnet"
+    ? ""
+    : "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E";
+export const WETH_DECIMALS = 18;
+
 export const WORMHOLE_V1_ETH_ADDRESS =
   CLUSTER === "mainnet"
     ? "0xf92cD566Ea4864356C5491c177A430C222d7e678"

+ 3 - 2
docs/devnet.md

@@ -3,8 +3,9 @@
 | Label              | Network         | Address                                                                                               | Note                                                                                                                                                           |
 | -------------      | :-------------: | -----:                                                                                                | :-----                                                                                                                                                         |
 | Test Wallet        | ETH             | 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1                                                            | Key: `0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d` Mnemonic `myth like bonus scare over problem client lizard pioneer submit female collect`                                                                                 |
-| Test ERC20         | ETH             | 0x0E696947A06550DEf604e82C26fd9E493e576337                                                            | Tokens minted to Test Wallet                                                                                                                                   |
-| Test NFT           | ETH             | 0xA94B7f0465E98609391C623d0560C5720a3f2D33                                                            | One minted to Test Wallet                                                                                                                                      |
+| Test ERC20         | ETH             | 0x2D8BE6BF0baA74e0A907016679CaE9190e80dD0A                                                            | Tokens minted to Test Wallet                                                                                                                                   |
+| Test NFT           | ETH             | 0x5b9b42d6e4B2e4Bf8d42Eba32D46918e10899B66                                                            | One minted to Test Wallet          
+| Test WETH          | ETH             | 0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E                                                            | Tokens minted to Test Wallet  
 | Bridge Core        | ETH             | 0xC89Ce4735882C9F0f0FE26686c53074E09B0D550                                                            |                                                                                                                                                                |
 | Token Bridge       | ETH             | 0x0290FB167208Af455bB137780163b7B7a9a10C16                                                            |                                                                                                                                                                |
 | NFT Bridge         | ETH             | 0x26b4afb60d6c903165150c6f0aa14f8016be4aec                                                            |                                                                                                                                                                |

+ 1 - 1
ethereum/.env.test

@@ -8,7 +8,7 @@ INIT_GOV_CONTRACT=0x000000000000000000000000000000000000000000000000000000000000
 BRIDGE_INIT_CHAIN_ID=0x02
 BRIDGE_INIT_GOV_CHAIN_ID=0x1
 BRIDGE_INIT_GOV_CONTRACT=0x0000000000000000000000000000000000000000000000000000000000000004
-BRIDGE_INIT_WETH=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
+BRIDGE_INIT_WETH=0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E
 
 #Pyth Migrations
 PYTH_INIT_CHAIN_ID=0x2

+ 50 - 0
ethereum/scripts/deploy_test_token.js

@@ -3,10 +3,52 @@
 const ERC20 = artifacts.require("ERC20PresetMinterPauser");
 const ERC721 = artifacts.require("ERC721PresetMinterPauserAutoId");
 
+const interateToStandardTransactionCount = async () => {
+  const accounts = await web3.eth.getAccounts();
+
+  const transactionCount = await web3.eth.getTransactionCount(
+    accounts[0],
+    "latest"
+  );
+  console.log(
+    "transaction count prior to test token deploys: ",
+    transactionCount
+  );
+
+  const transactionsToBurn = 32 - transactionCount;
+  const promises = [];
+  for (let i = 0; i < transactionsToBurn; i++) {
+    promises.push(
+      web3.eth.sendTransaction({
+        to: accounts[0],
+        from: accounts[0],
+        value: 530,
+      })
+    );
+  }
+
+  await Promise.all(promises);
+
+  const burnCount = await web3.eth.getTransactionCount(accounts[0], "latest");
+
+  console.log("transaction count after burn: ", burnCount);
+
+  return Promise.resolve();
+};
+
 module.exports = async function(callback) {
   try {
     const accounts = await web3.eth.getAccounts();
 
+    //Contracts deployed via this script deploy to an address which is determined by the number of transactions
+    //which have been performed on the chain.
+    //This is, however, variable. For example, if you optionally deploy the pyth contracts, more transactions are
+    //performed than if you didn't.
+    //In order to make sure the test contracts deploy to a location
+    //which is deterministic with regard to other environment conditions, we fire bogus transactions up to a safe
+    //count, currently 32, before deploying the test contracts.
+    await interateToStandardTransactionCount();
+
     // deploy token contract
     const tokenAddress = (await ERC20.new("Ethereum Test Token", "TKN"))
       .address;
@@ -39,6 +81,14 @@ module.exports = async function(callback) {
 
     console.log("NFT deployed at: " + nftAddress);
 
+    const MockWETH9 = await artifacts.require("MockWETH9");
+    //WETH deploy
+    // deploy token contract
+    const wethAddress = (await MockWETH9.new()).address;
+    const wethToken = new web3.eth.Contract(MockWETH9.abi, wethAddress);
+
+    console.log("WETH token deployed at: " + wethAddress);
+
     callback();
   } catch (e) {
     callback(e);

+ 11 - 0
sdk/js/src/token_bridge/redeem.ts

@@ -17,6 +17,17 @@ export async function redeemOnEth(
   return receipt;
 }
 
+export async function redeemOnEthNative(
+  tokenBridgeAddress: string,
+  signer: ethers.Signer,
+  signedVAA: Uint8Array
+) {
+  const bridge = Bridge__factory.connect(tokenBridgeAddress, signer);
+  const v = await bridge.completeTransferAndUnwrapETH(signedVAA);
+  const receipt = await v.wait();
+  return receipt;
+}
+
 export async function redeemOnTerra(
   tokenBridgeAddress: string,
   walletAddress: string,

+ 22 - 2
sdk/js/src/token_bridge/transfer.ts

@@ -39,8 +39,6 @@ export async function transferFromEth(
   recipientChain: ChainId,
   recipientAddress: Uint8Array
 ) {
-  //TODO: should we check if token attestation exists on the target chain
-  const token = TokenImplementation__factory.connect(tokenAddress, signer);
   const fee = 0; // for now, this won't do anything, we may add later
   const bridge = Bridge__factory.connect(tokenBridgeAddress, signer);
   const v = await bridge.transferTokens(
@@ -55,6 +53,28 @@ export async function transferFromEth(
   return receipt;
 }
 
+export async function transferFromEthNative(
+  tokenBridgeAddress: string,
+  signer: ethers.Signer,
+  amount: ethers.BigNumberish,
+  recipientChain: ChainId,
+  recipientAddress: Uint8Array
+) {
+  const fee = 0; // for now, this won't do anything, we may add later
+  const bridge = Bridge__factory.connect(tokenBridgeAddress, signer);
+  const v = await bridge.wrapAndTransferETH(
+    recipientChain,
+    recipientAddress,
+    fee,
+    createNonce(),
+    {
+      value: amount,
+    }
+  );
+  const receipt = await v.wait();
+  return receipt;
+}
+
 export async function transferFromTerra(
   walletAddress: string,
   tokenBridgeAddress: string,