فهرست منبع

bridge_ui: support multiple evm bridges

Change-Id: I3c416955e3e01707eec29404a483b1c223bffef4
Evan Gray 4 سال پیش
والد
کامیت
4bdb714594
43فایلهای تغییر یافته به همراه600 افزوده شده و 316 حذف شده
  1. 6 3
      bridge_ui/src/components/Attest/Target.tsx
  2. 2 2
      bridge_ui/src/components/KeyAndBalance.tsx
  3. 7 11
      bridge_ui/src/components/LowBalanceWarning.tsx
  4. 22 18
      bridge_ui/src/components/NFT/Recovery.tsx
  5. 8 10
      bridge_ui/src/components/NFT/Source.tsx
  6. 11 14
      bridge_ui/src/components/NFT/Target.tsx
  7. 3 2
      bridge_ui/src/components/NFT/WaitingForWalletMessage.tsx
  8. 21 11
      bridge_ui/src/components/NFTOriginVerifier.tsx
  9. 5 7
      bridge_ui/src/components/ShowTx.tsx
  10. 5 7
      bridge_ui/src/components/SmartAddress.tsx
  11. 13 6
      bridge_ui/src/components/TokenSelectors/EthereumSourceTokenSelector.tsx
  12. 21 0
      bridge_ui/src/components/TokenSelectors/NFTViewer.tsx
  13. 10 12
      bridge_ui/src/components/TokenSelectors/SourceTokenSelector.tsx
  14. 3 2
      bridge_ui/src/components/TransactionProgress.tsx
  15. 12 5
      bridge_ui/src/components/Transfer/AddToMetamask.tsx
  16. 20 14
      bridge_ui/src/components/Transfer/Recovery.tsx
  17. 7 2
      bridge_ui/src/components/Transfer/Redeem.tsx
  18. 2 3
      bridge_ui/src/components/Transfer/Send.tsx
  19. 5 8
      bridge_ui/src/components/Transfer/Target.tsx
  20. 12 17
      bridge_ui/src/hooks/useAllowance.ts
  21. 12 10
      bridge_ui/src/hooks/useCheckIfWormholeWrapped.ts
  22. 15 13
      bridge_ui/src/hooks/useEvmMetadata.ts
  23. 15 8
      bridge_ui/src/hooks/useFetchTargetAsset.ts
  24. 18 5
      bridge_ui/src/hooks/useGetBalanceEffect.ts
  25. 74 11
      bridge_ui/src/hooks/useGetSourceParsedTokenAccounts.ts
  26. 19 12
      bridge_ui/src/hooks/useHandleAttest.ts
  27. 9 7
      bridge_ui/src/hooks/useHandleCreateWrapped.ts
  28. 11 9
      bridge_ui/src/hooks/useHandleNFTRedeem.ts
  29. 20 13
      bridge_ui/src/hooks/useHandleNFTTransfer.ts
  30. 20 10
      bridge_ui/src/hooks/useHandleRedeem.ts
  31. 22 15
      bridge_ui/src/hooks/useHandleTransfer.ts
  32. 12 10
      bridge_ui/src/hooks/useIsWalletReady.ts
  33. 6 6
      bridge_ui/src/hooks/useMetadata.ts
  34. 4 4
      bridge_ui/src/hooks/useSyncTargetAddress.ts
  35. 8 5
      bridge_ui/src/hooks/useTransactionFees.tsx
  36. 12 0
      bridge_ui/src/icons/bsc.svg
  37. 3 2
      bridge_ui/src/store/selectors.ts
  38. 106 7
      bridge_ui/src/utils/consts.ts
  39. 6 0
      bridge_ui/src/utils/ethereum.ts
  40. 1 1
      devnet/eth-devnet.yaml
  41. 4 3
      sdk/js/src/nft_bridge/getOriginalAsset.ts
  42. 6 10
      sdk/js/src/token_bridge/getOriginalAsset.ts
  43. 2 1
      sdk/js/src/utils/array.ts

+ 6 - 3
bridge_ui/src/components/Attest/Target.tsx

@@ -1,4 +1,3 @@
-import { CHAIN_ID_ETH } from "@certusone/wormhole-sdk";
 import { makeStyles, MenuItem, TextField, Typography } from "@material-ui/core";
 import { Alert } from "@material-ui/lab";
 import { useCallback, useMemo } from "react";
@@ -12,6 +11,7 @@ import {
   selectAttestTargetChain,
 } from "../../store/selectors";
 import { CHAINS, CHAINS_BY_ID } from "../../utils/consts";
+import { isEVMChain } from "../../utils/ethereum";
 import ButtonWithLoader from "../ButtonWithLoader";
 import KeyAndBalance from "../KeyAndBalance";
 import LowBalanceWarning from "../LowBalanceWarning";
@@ -64,8 +64,11 @@ function Target() {
           You will have to pay transaction fees on{" "}
           {CHAINS_BY_ID[targetChain].name} to attest this token.{" "}
         </Typography>
-        {targetChain === CHAIN_ID_ETH && (
-          <EthGasEstimateSummary methodType="createWrapped" />
+        {isEVMChain(targetChain) && (
+          <EthGasEstimateSummary
+            methodType="createWrapped"
+            chainId={targetChain}
+          />
         )}
       </Alert>
       <LowBalanceWarning chainId={targetChain} />

+ 2 - 2
bridge_ui/src/components/KeyAndBalance.tsx

@@ -1,10 +1,10 @@
 import {
   ChainId,
-  CHAIN_ID_ETH,
   CHAIN_ID_SOLANA,
   CHAIN_ID_TERRA,
 } from "@certusone/wormhole-sdk";
 import { Typography } from "@material-ui/core";
+import { isEVMChain } from "../utils/ethereum";
 import EthereumSignerKey from "./EthereumSignerKey";
 import SolanaWalletKey from "./SolanaWalletKey";
 import TerraWalletKey from "./TerraWalletKey";
@@ -17,7 +17,7 @@ function KeyAndBalance({
   balance?: string;
 }) {
   const balanceString = balance ? "Balance: " + balance : balance;
-  if (chainId === CHAIN_ID_ETH) {
+  if (isEVMChain(chainId)) {
     return (
       <>
         <EthereumSignerKey />

+ 7 - 11
bridge_ui/src/components/LowBalanceWarning.tsx

@@ -1,13 +1,9 @@
-import {
-  ChainId,
-  CHAIN_ID_ETH,
-  CHAIN_ID_SOLANA,
-} from "@certusone/wormhole-sdk";
-import { Typography } from "@material-ui/core";
+import { ChainId } from "@certusone/wormhole-sdk";
+import { makeStyles, Typography } from "@material-ui/core";
 import { Alert } from "@material-ui/lab";
-import { makeStyles } from "@material-ui/core";
-import useTransactionFees from "../hooks/useTransactionFees";
 import useIsWalletReady from "../hooks/useIsWalletReady";
+import useTransactionFees from "../hooks/useTransactionFees";
+import { getDefaultNativeCurrencySymbol } from "../utils/consts";
 
 const useStyles = makeStyles((theme) => ({
   alert: {
@@ -24,9 +20,9 @@ function LowBalanceWarning({ chainId }: { chainId: ChainId }) {
     isReady &&
     transactionFeeWarning.balanceString &&
     transactionFeeWarning.isSufficientBalance === false;
-  const warningMessage = `This wallet has a very low ${
-    chainId === CHAIN_ID_SOLANA ? "SOL" : chainId === CHAIN_ID_ETH ? "ETH" : ""
-  } balance and may not be able to pay for the upcoming transaction fees.`;
+  const warningMessage = `This wallet has a very low ${getDefaultNativeCurrencySymbol(
+    chainId
+  )} balance and may not be able to pay for the upcoming transaction fees.`;
 
   const content = (
     <Alert severity="warning" className={classes.alert}>

+ 22 - 18
bridge_ui/src/components/NFT/Recovery.tsx

@@ -1,6 +1,5 @@
 import {
-  CHAIN_ID_BSC,
-  CHAIN_ID_ETH,
+  ChainId,
   CHAIN_ID_SOLANA,
   getEmitterAddressEth,
   getEmitterAddressSolana,
@@ -41,13 +40,14 @@ import {
   selectNFTSourceChain,
 } from "../../store/selectors";
 import {
-  CHAINS,
-  ETH_BRIDGE_ADDRESS,
-  ETH_NFT_BRIDGE_ADDRESS,
+  CHAINS_WITH_NFT_SUPPORT,
+  getBridgeAddressForChain,
+  getNFTBridgeAddressForChain,
   SOLANA_HOST,
   SOL_NFT_BRIDGE_ADDRESS,
   WORMHOLE_RPC_HOSTS,
 } from "../../utils/consts";
+import { isEVMChain } from "../../utils/ethereum";
 import { getSignedVAAWithRetry } from "../../utils/getSignedVAAWithRetry";
 import parseError from "../../utils/parseError";
 import KeyAndBalance from "../KeyAndBalance";
@@ -60,17 +60,23 @@ const useStyles = makeStyles((theme) => ({
   },
 }));
 
-async function eth(
+async function evm(
   provider: ethers.providers.Web3Provider,
   tx: string,
-  enqueueSnackbar: any
+  enqueueSnackbar: any,
+  chainId: ChainId
 ) {
   try {
     const receipt = await provider.getTransactionReceipt(tx);
-    const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
-    const emitterAddress = getEmitterAddressEth(ETH_NFT_BRIDGE_ADDRESS);
+    const sequence = parseSequenceFromLogEth(
+      receipt,
+      getBridgeAddressForChain(chainId)
+    );
+    const emitterAddress = getEmitterAddressEth(
+      getNFTBridgeAddressForChain(chainId)
+    );
     const { vaaBytes } = await getSignedVAAWithRetry(
-      CHAIN_ID_ETH,
+      chainId,
       emitterAddress,
       sequence.toString(),
       WORMHOLE_RPC_HOSTS.length
@@ -137,14 +143,15 @@ function RecoveryDialogContent({
   useEffect(() => {
     if (recoverySourceTx) {
       let cancelled = false;
-      if (recoverySourceChain === CHAIN_ID_ETH && provider) {
+      if (isEVMChain(recoverySourceChain) && provider) {
         setRecoverySourceTxError("");
         setRecoverySourceTxIsLoading(true);
         (async () => {
-          const { vaa, error } = await eth(
+          const { vaa, error } = await evm(
             provider,
             recoverySourceTx,
-            enqueueSnackbar
+            enqueueSnackbar,
+            recoverySourceChain
           );
           if (!cancelled) {
             setRecoverySourceTxIsLoading(false);
@@ -259,16 +266,13 @@ function RecoveryDialogContent({
           fullWidth
           margin="normal"
         >
-          {CHAINS.filter(
-            ({ id }) => id === CHAIN_ID_ETH || id === CHAIN_ID_SOLANA
-          ).map(({ id, name }) => (
+          {CHAINS_WITH_NFT_SUPPORT.map(({ id, name }) => (
             <MenuItem key={id} value={id}>
               {name}
             </MenuItem>
           ))}
         </TextField>
-        {recoverySourceChain === CHAIN_ID_ETH ||
-        recoverySourceChain === CHAIN_ID_BSC ? (
+        {isEVMChain(recoverySourceChain) ? (
           <KeyAndBalance chainId={recoverySourceChain} />
         ) : null}
         <TextField

+ 8 - 10
bridge_ui/src/components/NFT/Source.tsx

@@ -1,9 +1,11 @@
-import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
 import { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
 import { Restore, VerifiedUser } from "@material-ui/icons";
+import { Alert } from "@material-ui/lab";
 import { useCallback } from "react";
 import { useDispatch, useSelector } from "react-redux";
+import { Link } from "react-router-dom";
 import useIsWalletReady from "../../hooks/useIsWalletReady";
+import { incrementStep, setSourceChain } from "../../store/nftSlice";
 import {
   selectNFTIsSourceComplete,
   selectNFTShouldLockFields,
@@ -11,15 +13,13 @@ import {
   selectNFTSourceChain,
   selectNFTSourceError,
 } from "../../store/selectors";
-import { incrementStep, setSourceChain } from "../../store/nftSlice";
-import { CHAINS } from "../../utils/consts";
+import { CHAINS_WITH_NFT_SUPPORT } from "../../utils/consts";
+import { isEVMChain } from "../../utils/ethereum";
 import ButtonWithLoader from "../ButtonWithLoader";
 import KeyAndBalance from "../KeyAndBalance";
+import LowBalanceWarning from "../LowBalanceWarning";
 import StepDescription from "../StepDescription";
 import { TokenSelector } from "../TokenSelectors/SourceTokenSelector";
-import { Alert } from "@material-ui/lab";
-import LowBalanceWarning from "../LowBalanceWarning";
-import { Link } from "react-router-dom";
 
 const useStyles = makeStyles((theme) => ({
   transferField: {
@@ -94,15 +94,13 @@ function Source({
         onChange={handleSourceChange}
         disabled={shouldLockFields}
       >
-        {CHAINS.filter(
-          ({ id }) => id === CHAIN_ID_ETH || id === CHAIN_ID_SOLANA
-        ).map(({ id, name }) => (
+        {CHAINS_WITH_NFT_SUPPORT.map(({ id, name }) => (
           <MenuItem key={id} value={id}>
             {name}
           </MenuItem>
         ))}
       </TextField>
-      {sourceChain === CHAIN_ID_ETH ? (
+      {isEVMChain(sourceChain) ? (
         <Alert severity="info">
           Only NFTs which implement ERC-721 are supported.
         </Alert>

+ 11 - 14
bridge_ui/src/components/NFT/Target.tsx

@@ -1,5 +1,4 @@
 import {
-  CHAIN_ID_ETH,
   CHAIN_ID_SOLANA,
   hexToNativeString,
   hexToUint8Array,
@@ -27,7 +26,8 @@ import {
   selectNFTTargetChain,
   selectNFTTargetError,
 } from "../../store/selectors";
-import { CHAINS, CHAINS_BY_ID } from "../../utils/consts";
+import { CHAINS_BY_ID, CHAINS_WITH_NFT_SUPPORT } from "../../utils/consts";
+import { isEVMChain } from "../../utils/ethereum";
 import ButtonWithLoader from "../ButtonWithLoader";
 import KeyAndBalance from "../KeyAndBalance";
 import LowBalanceWarning from "../LowBalanceWarning";
@@ -48,7 +48,7 @@ function Target() {
   const dispatch = useDispatch();
   const sourceChain = useSelector(selectNFTSourceChain);
   const chains = useMemo(
-    () => CHAINS.filter((c) => c.id !== sourceChain),
+    () => CHAINS_WITH_NFT_SUPPORT.filter((c) => c.id !== sourceChain),
     [sourceChain]
   );
   const targetChain = useSelector(selectNFTTargetChain);
@@ -93,15 +93,12 @@ function Target() {
         fullWidth
         value={targetChain}
         onChange={handleTargetChange}
-        disabled={true}
       >
-        {chains
-          .filter(({ id }) => id === CHAIN_ID_ETH || id === CHAIN_ID_SOLANA)
-          .map(({ id, name }) => (
-            <MenuItem key={id} value={id}>
-              {name}
-            </MenuItem>
-          ))}
+        {chains.map(({ id, name }) => (
+          <MenuItem key={id} value={id}>
+            {name}
+          </MenuItem>
+        ))}
       </TextField>
       <KeyAndBalance chainId={targetChain} balance={uiAmountString} />
       <TextField
@@ -120,7 +117,7 @@ function Target() {
             value={targetAsset || ""}
             disabled={true}
           />
-          {targetChain === CHAIN_ID_ETH ? (
+          {isEVMChain(targetChain) ? (
             <TextField
               label="TokenId"
               fullWidth
@@ -136,8 +133,8 @@ function Target() {
           You will have to pay transaction fees on{" "}
           {CHAINS_BY_ID[targetChain].name} to redeem your NFT.
         </Typography>
-        {targetChain === CHAIN_ID_ETH && (
-          <EthGasEstimateSummary methodType="nft" />
+        {isEVMChain(targetChain) && (
+          <EthGasEstimateSummary methodType="nft" chainId={targetChain} />
         )}
       </Alert>
       <LowBalanceWarning chainId={targetChain} />

+ 3 - 2
bridge_ui/src/components/NFT/WaitingForWalletMessage.tsx

@@ -1,4 +1,4 @@
-import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
+import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
 import { makeStyles, Typography } from "@material-ui/core";
 import { useSelector } from "react-redux";
 import {
@@ -9,6 +9,7 @@ import {
   selectNFTTargetChain,
   selectNFTTransferTx,
 } from "../../store/selectors";
+import { isEVMChain } from "../../utils/ethereum";
 import { WAITING_FOR_WALLET_AND_CONF } from "../Transfer/WaitingForWalletMessage";
 
 const useStyles = makeStyles((theme) => ({
@@ -33,7 +34,7 @@ export default function WaitingForWalletMessage() {
       {WAITING_FOR_WALLET_AND_CONF}{" "}
       {targetChain === CHAIN_ID_SOLANA && isRedeeming
         ? "Note: there will be several transactions"
-        : sourceChain === CHAIN_ID_ETH && isSending
+        : isEVMChain(sourceChain) && isSending
         ? "Note: there will be two transactions"
         : null}
     </Typography>

+ 21 - 11
bridge_ui/src/components/NFTOriginVerifier.tsx

@@ -30,15 +30,16 @@ import { getMetaplexData } from "../hooks/useMetaplexData";
 import { COLORS } from "../muiTheme";
 import { NFTParsedTokenAccount } from "../store/nftSlice";
 import {
-  CHAINS,
   CHAINS_BY_ID,
-  ETH_NFT_BRIDGE_ADDRESS,
+  CHAINS_WITH_NFT_SUPPORT,
+  getNFTBridgeAddressForChain,
   SOLANA_HOST,
   SOL_NFT_BRIDGE_ADDRESS,
 } from "../utils/consts";
 import {
   ethNFTToNFTParsedTokenAccount,
   getEthereumNFT,
+  isEVMChain,
   isNFT,
   isValidEthereumAddress,
 } from "../utils/ethereum";
@@ -119,7 +120,7 @@ export default function NFTOriginVerifier() {
       isReady &&
       provider &&
       signerAddress &&
-      lookupChain === CHAIN_ID_ETH &&
+      isEVMChain(lookupChain) &&
       lookupAsset &&
       lookupTokenId
     ) {
@@ -136,10 +137,11 @@ export default function NFTOriginVerifier() {
                 signerAddress
               );
               const info = await getOriginalAssetEth(
-                ETH_NFT_BRIDGE_ADDRESS,
+                getNFTBridgeAddressForChain(lookupChain),
                 provider,
                 lookupAsset,
-                lookupTokenId
+                lookupTokenId,
+                lookupChain
               );
               if (!cancelled) {
                 setIsLoading(false);
@@ -225,7 +227,7 @@ export default function NFTOriginVerifier() {
       originInfo.chainId
     );
   const displayError =
-    (lookupChain === CHAIN_ID_ETH && statusMessage) || lookupError;
+    (isEVMChain(lookupChain) && statusMessage) || lookupError;
   return (
     <div>
       <Container maxWidth="md">
@@ -249,15 +251,13 @@ export default function NFTOriginVerifier() {
             fullWidth
             margin="normal"
           >
-            {CHAINS.filter(
-              ({ id }) => id === CHAIN_ID_ETH || id === CHAIN_ID_SOLANA
-            ).map(({ id, name }) => (
+            {CHAINS_WITH_NFT_SUPPORT.map(({ id, name }) => (
               <MenuItem key={id} value={id}>
                 {name}
               </MenuItem>
             ))}
           </TextField>
-          {lookupChain === CHAIN_ID_ETH || lookupChain === CHAIN_ID_BSC ? (
+          {isEVMChain(lookupChain) ? (
             <KeyAndBalance chainId={lookupChain} />
           ) : null}
           <TextField
@@ -267,7 +267,7 @@ export default function NFTOriginVerifier() {
             value={lookupAsset}
             onChange={handleAssetChange}
           />
-          {lookupChain === CHAIN_ID_ETH ? (
+          {isEVMChain(lookupChain) ? (
             <TextField
               fullWidth
               margin="normal"
@@ -318,6 +318,16 @@ export default function NFTOriginVerifier() {
                   >
                     View on Solscan
                   </Button>
+                ) : originInfo.chainId === CHAIN_ID_BSC ? (
+                  <Button
+                    href={`https://bscscan.com/token/${readableAddress}?a=${originInfo.tokenId}`}
+                    target="_blank"
+                    endIcon={<Launch />}
+                    className={classes.viewButton}
+                    variant="outlined"
+                  >
+                    View on BscScan
+                  </Button>
                 ) : (
                   <Button
                     href={`https://opensea.io/assets/${readableAddress}/${originInfo.tokenId}`}

+ 5 - 7
bridge_ui/src/components/ShowTx.tsx

@@ -1,12 +1,13 @@
 import {
   ChainId,
+  CHAIN_ID_BSC,
   CHAIN_ID_ETH,
   CHAIN_ID_SOLANA,
   CHAIN_ID_TERRA,
 } from "@certusone/wormhole-sdk";
 import { Button, makeStyles, Typography } from "@material-ui/core";
 import { Transaction } from "../store/transferSlice";
-import { CLUSTER } from "../utils/consts";
+import { CLUSTER, getExplorerName } from "../utils/consts";
 
 const useStyles = makeStyles((theme) => ({
   tx: {
@@ -36,6 +37,8 @@ export default function ShowTx({
       ? `https://${CLUSTER === "testnet" ? "goerli." : ""}etherscan.io/tx/${
           tx?.id
         }`
+      : chainId === CHAIN_ID_BSC
+      ? `https://bscscan.com/tx/${tx?.id}`
       : chainId === CHAIN_ID_SOLANA
       ? `https://explorer.solana.com/tx/${tx?.id}${
           CLUSTER === "testnet"
@@ -53,12 +56,7 @@ export default function ShowTx({
             : "columbus-5"
         }/tx/${tx?.id}`
       : undefined;
-  const explorerName =
-    chainId === CHAIN_ID_ETH
-      ? "Etherscan"
-      : chainId === CHAIN_ID_TERRA
-      ? "Finder"
-      : "Explorer";
+  const explorerName = getExplorerName(chainId);
 
   return (
     <div className={classes.tx}>

+ 5 - 7
bridge_ui/src/components/SmartAddress.tsx

@@ -1,5 +1,6 @@
 import {
   ChainId,
+  CHAIN_ID_BSC,
   CHAIN_ID_ETH,
   CHAIN_ID_SOLANA,
   CHAIN_ID_TERRA,
@@ -10,7 +11,7 @@ import { withStyles } from "@material-ui/styles";
 import clsx from "clsx";
 import useCopyToClipboard from "../hooks/useCopyToClipboard";
 import { ParsedTokenAccount } from "../store/transferSlice";
-import { CLUSTER } from "../utils/consts";
+import { CLUSTER, getExplorerName } from "../utils/consts";
 import { shortenAddress } from "../utils/solana";
 
 const useStyles = makeStyles((theme) => ({
@@ -86,6 +87,8 @@ export default function SmartAddress({
     ? `https://${
         CLUSTER === "testnet" ? "goerli." : ""
       }etherscan.io/address/${useableAddress}`
+    : chainId === CHAIN_ID_BSC
+    ? `https://bscscan.com/address/${useableAddress}`
     : chainId === CHAIN_ID_SOLANA
     ? `https://explorer.solana.com/address/${useableAddress}${
         CLUSTER === "testnet"
@@ -103,12 +106,7 @@ export default function SmartAddress({
           : "columbus-5"
       }/address/${useableAddress}`
     : undefined;
-  const explorerName =
-    chainId === CHAIN_ID_ETH
-      ? "Etherscan"
-      : chainId === CHAIN_ID_TERRA
-      ? "Finder"
-      : "Explorer";
+  const explorerName = getExplorerName(chainId);
 
   const copyToClipboard = useCopyToClipboard(useableAddress);
 

+ 13 - 6
bridge_ui/src/components/TokenSelectors/EthereumSourceTokenSelector.tsx

@@ -30,7 +30,7 @@ import { NFTParsedTokenAccount } from "../../store/nftSlice";
 import NFTViewer from "./NFTViewer";
 import { useDebounce } from "use-debounce/lib";
 import RefreshButtonWrapper from "./RefreshButtonWrapper";
-import { CHAIN_ID_ETH } from "@certusone/wormhole-sdk";
+import { ChainId, CHAIN_ID_ETH } from "@certusone/wormhole-sdk";
 import { sortParsedTokenAccounts } from "../../utils/sort";
 
 const useStyles = makeStyles((theme) =>
@@ -83,7 +83,10 @@ const getLogo = (account: ParsedTokenAccount | null) => {
   return account.logo;
 };
 
-const isWormholev1 = (provider: any, address: string) => {
+const isWormholev1 = (provider: any, address: string, chainId: ChainId) => {
+  if (chainId !== CHAIN_ID_ETH) {
+    return Promise.resolve(false);
+  }
   const connection = WormholeAbi__factory.connect(
     WORMHOLE_V1_ETH_ADDRESS,
     provider
@@ -102,6 +105,7 @@ type EthereumSourceTokenSelectorProps = {
   tokenAccounts: DataWrapper<ParsedTokenAccount[]> | undefined;
   disabled: boolean;
   resetAccounts: (() => void) | undefined;
+  chainId: ChainId;
   nft?: boolean;
 };
 
@@ -186,6 +190,7 @@ export default function EthereumSourceTokenSelector(
     tokenAccounts,
     disabled,
     resetAccounts,
+    chainId,
     nft,
   } = props;
   const classes = useStyles();
@@ -258,7 +263,7 @@ export default function EthereumSourceTokenSelector(
         onChange(autocompleteHolder);
         return;
       }
-      isWormholev1(provider, autocompleteHolder.mintKey).then(
+      isWormholev1(provider, autocompleteHolder.mintKey, chainId).then(
         (result) => {
           if (!cancelled) {
             result
@@ -282,7 +287,7 @@ export default function EthereumSourceTokenSelector(
         cancelled = true;
       };
     }
-  }, [autocompleteHolder, provider, advancedMode, onChange, nft]);
+  }, [autocompleteHolder, provider, advancedMode, onChange, nft, chainId]);
 
   //This effect watches the advancedModeString, and checks that the selected asset is valid before putting
   // it on the state.
@@ -353,7 +358,8 @@ export default function EthereumSourceTokenSelector(
           //Validate that the token is not a wormhole v1 asset
           const isWormholePromise = isWormholev1(
             provider,
-            advancedModeHolderString
+            advancedModeHolderString,
+            chainId
           ).then(
             (result) => {
               if (result && !cancelled) {
@@ -430,6 +436,7 @@ export default function EthereumSourceTokenSelector(
     onChange,
     nft,
     advancedModeHolderTokenId,
+    chainId,
   ]);
 
   const handleClick = useCallback(() => {
@@ -563,7 +570,7 @@ export default function EthereumSourceTokenSelector(
   const content = value ? (
     <>
       {nft ? (
-        <NFTViewer value={value} chainId={CHAIN_ID_ETH} />
+        <NFTViewer value={value} chainId={chainId} />
       ) : (
         <RefreshButtonWrapper callback={resetAccountWrapper}>
           <Typography>

+ 21 - 0
bridge_ui/src/components/TokenSelectors/NFTViewer.tsx

@@ -13,10 +13,12 @@ import { NFTParsedTokenAccount } from "../../store/nftSlice";
 import clsx from "clsx";
 import {
   ChainId,
+  CHAIN_ID_BSC,
   CHAIN_ID_ETH,
   CHAIN_ID_SOLANA,
 } from "@certusone/wormhole-sdk";
 import SmartAddress from "../SmartAddress";
+import bscIcon from "../../icons/bsc.svg";
 import ethIcon from "../../icons/eth.svg";
 import solanaIcon from "../../icons/solana.svg";
 import useCopyToClipboard from "../../hooks/useCopyToClipboard";
@@ -53,6 +55,18 @@ const LogoIcon = ({ chainId }: { chainId: ChainId }) =>
       src={ethIcon}
       alt="Ethereum"
     />
+  ) : chainId === CHAIN_ID_BSC ? (
+    <Avatar
+      style={{
+        backgroundColor: "rgb(20, 21, 26)",
+        height: "1em",
+        width: "1em",
+        marginLeft: "4px",
+        padding: "2px",
+      }}
+      src={bscIcon}
+      alt="Binance Smart Chain"
+    />
   ) : null;
 
 const useStyles = makeStyles((theme) => ({
@@ -127,6 +141,12 @@ const useStyles = makeStyles((theme) => ({
     background:
       "linear-gradient(160deg, rgba(69,74,117,1) 0%, rgba(138,146,178,1) 33%, rgba(69,74,117,1) 66%, rgba(98,104,143,1) 100%)",
   },
+  bsc: {
+    // color from binance background rgb(20, 21, 26), 2 and 1 tint lighter
+    backgroundColor: "#F0B90B",
+    background:
+      "linear-gradient(160deg, rgb(20, 21, 26) 0%, #4A4D57 33%, rgb(20, 21, 26) 66%, #2C2F3B 100%)",
+  },
   solana: {
     // colors from https://solana.com/branding/new/exchange/exchange-sq-black.svg
     backgroundColor: "rgb(153,69,255)",
@@ -211,6 +231,7 @@ export default function NFTViewer({
       <div
         className={clsx(classes.cardInset, {
           [classes.eth]: chainId === CHAIN_ID_ETH,
+          [classes.bsc]: chainId === CHAIN_ID_BSC,
           [classes.solana]: chainId === CHAIN_ID_SOLANA,
         })}
       >

+ 10 - 12
bridge_ui/src/components/TokenSelectors/SourceTokenSelector.tsx

@@ -1,14 +1,14 @@
 //import Autocomplete from '@material-ui/lab/Autocomplete';
-import {
-  CHAIN_ID_ETH,
-  CHAIN_ID_SOLANA,
-  CHAIN_ID_TERRA,
-} from "@certusone/wormhole-sdk";
+import { CHAIN_ID_SOLANA, CHAIN_ID_TERRA } from "@certusone/wormhole-sdk";
 import { TextField, Typography } from "@material-ui/core";
 import { useCallback } from "react";
 import { useDispatch, useSelector } from "react-redux";
 import useGetSourceParsedTokens from "../../hooks/useGetSourceParsedTokenAccounts";
 import useIsWalletReady from "../../hooks/useIsWalletReady";
+import {
+  setSourceParsedTokenAccount as setNFTSourceParsedTokenAccount,
+  setSourceWalletAddress as setNFTSourceWalletAddress,
+} from "../../store/nftSlice";
 import {
   selectNFTSourceChain,
   selectNFTSourceParsedTokenAccount,
@@ -20,14 +20,11 @@ import {
   setSourceParsedTokenAccount as setTransferSourceParsedTokenAccount,
   setSourceWalletAddress as setTransferSourceWalletAddress,
 } from "../../store/transferSlice";
-import {
-  setSourceParsedTokenAccount as setNFTSourceParsedTokenAccount,
-  setSourceWalletAddress as setNFTSourceWalletAddress,
-} from "../../store/nftSlice";
+import { isEVMChain } from "../../utils/ethereum";
 import EthereumSourceTokenSelector from "./EthereumSourceTokenSelector";
+import RefreshButtonWrapper from "./RefreshButtonWrapper";
 import SolanaSourceTokenSelector from "./SolanaSourceTokenSelector";
 import TerraSourceTokenSelector from "./TerraSourceTokenSelector";
-import RefreshButtonWrapper from "./RefreshButtonWrapper";
 
 type TokenSelectorProps = {
   disabled: boolean;
@@ -78,7 +75,7 @@ export const TokenSelector = (props: TokenSelectorProps) => {
 
   //This is only for errors so bad that we shouldn't even mount the component
   const fatalError =
-    lookupChain !== CHAIN_ID_ETH &&
+    isEVMChain(lookupChain) &&
     lookupChain !== CHAIN_ID_TERRA &&
     maps?.tokenAccounts?.error; //Terra & ETH can proceed because it has advanced mode
 
@@ -96,7 +93,7 @@ export const TokenSelector = (props: TokenSelectorProps) => {
       resetAccounts={maps?.resetAccounts}
       nft={nft}
     />
-  ) : lookupChain === CHAIN_ID_ETH ? (
+  ) : isEVMChain(lookupChain) ? (
     <EthereumSourceTokenSelector
       value={sourceParsedTokenAccount || null}
       disabled={disabled}
@@ -104,6 +101,7 @@ export const TokenSelector = (props: TokenSelectorProps) => {
       covalent={maps?.covalent || undefined}
       tokenAccounts={maps?.tokenAccounts}
       resetAccounts={maps?.resetAccounts}
+      chainId={lookupChain}
       nft={nft}
     />
   ) : lookupChain === CHAIN_ID_TERRA ? (

+ 3 - 2
bridge_ui/src/components/TransactionProgress.tsx

@@ -9,6 +9,7 @@ import { useEffect, useState } from "react";
 import { useEthereumProvider } from "../contexts/EthereumProviderContext";
 import { Transaction } from "../store/transferSlice";
 import { CHAINS_BY_ID, SOLANA_HOST } from "../utils/consts";
+import { isEVMChain } from "../utils/ethereum";
 
 const useStyles = makeStyles((theme) => ({
   root: {
@@ -34,7 +35,7 @@ export default function TransactionProgress({
   const [currentBlock, setCurrentBlock] = useState(0);
   useEffect(() => {
     if (isSendComplete || !tx) return;
-    if (chainId === CHAIN_ID_ETH && provider) {
+    if (isEVMChain(chainId) && provider) {
       let cancelled = false;
       (async () => {
         while (!cancelled) {
@@ -73,7 +74,7 @@ export default function TransactionProgress({
     chainId === CHAIN_ID_SOLANA ? 32 : chainId === CHAIN_ID_ETH ? 15 : 1;
   if (
     !isSendComplete &&
-    (chainId === CHAIN_ID_SOLANA || chainId === CHAIN_ID_ETH) &&
+    (chainId === CHAIN_ID_SOLANA || isEVMChain(chainId)) &&
     blockDiff !== undefined
   ) {
     return (

+ 12 - 5
bridge_ui/src/components/Transfer/AddToMetamask.tsx

@@ -1,4 +1,3 @@
-import { CHAIN_ID_ETH } from "@certusone/wormhole-sdk";
 import { Button, makeStyles } from "@material-ui/core";
 import detectEthereumProvider from "@metamask/detect-provider";
 import { useCallback } from "react";
@@ -8,9 +7,11 @@ import {
   selectTransferTargetAsset,
   selectTransferTargetChain,
 } from "../../store/selectors";
+import { getEvmChainId } from "../../utils/consts";
 import {
   ethTokenToParsedTokenAccount,
   getEthereumToken,
+  isEVMChain,
 } from "../../utils/ethereum";
 
 const useStyles = makeStyles((theme) => ({
@@ -24,9 +25,14 @@ export default function AddToMetamask() {
   const classes = useStyles();
   const targetChain = useSelector(selectTransferTargetChain);
   const targetAsset = useSelector(selectTransferTargetAsset);
-  const { provider, signerAddress } = useEthereumProvider();
+  const {
+    provider,
+    signerAddress,
+    chainId: evmChainId,
+  } = useEthereumProvider();
+  const hasCorrectEvmNetwork = evmChainId === getEvmChainId(targetChain);
   const handleClick = useCallback(() => {
-    if (provider && targetAsset && signerAddress) {
+    if (provider && targetAsset && signerAddress && hasCorrectEvmNetwork) {
       (async () => {
         try {
           const token = await getEthereumToken(targetAsset, provider);
@@ -52,11 +58,12 @@ export default function AddToMetamask() {
         }
       })();
     }
-  }, [provider, targetAsset, signerAddress]);
+  }, [provider, targetAsset, signerAddress, hasCorrectEvmNetwork]);
   return provider &&
     signerAddress &&
     targetAsset &&
-    targetChain === CHAIN_ID_ETH ? (
+    isEVMChain(targetChain) &&
+    hasCorrectEvmNetwork ? (
     <Button
       onClick={handleClick}
       size="small"

+ 20 - 14
bridge_ui/src/components/Transfer/Recovery.tsx

@@ -1,6 +1,5 @@
 import {
-  CHAIN_ID_BSC,
-  CHAIN_ID_ETH,
+  ChainId,
   CHAIN_ID_SOLANA,
   CHAIN_ID_TERRA,
   getEmitterAddressEth,
@@ -50,14 +49,15 @@ import {
 } from "../../store/transferSlice";
 import {
   CHAINS,
-  ETH_BRIDGE_ADDRESS,
-  ETH_TOKEN_BRIDGE_ADDRESS,
+  getBridgeAddressForChain,
+  getTokenBridgeAddressForChain,
   SOLANA_HOST,
   SOL_TOKEN_BRIDGE_ADDRESS,
   TERRA_HOST,
   TERRA_TOKEN_BRIDGE_ADDRESS,
   WORMHOLE_RPC_HOSTS,
 } from "../../utils/consts";
+import { isEVMChain } from "../../utils/ethereum";
 import { getSignedVAAWithRetry } from "../../utils/getSignedVAAWithRetry";
 import parseError from "../../utils/parseError";
 import KeyAndBalance from "../KeyAndBalance";
@@ -70,17 +70,23 @@ const useStyles = makeStyles((theme) => ({
   },
 }));
 
-async function eth(
+async function evm(
   provider: ethers.providers.Web3Provider,
   tx: string,
-  enqueueSnackbar: any
+  enqueueSnackbar: any,
+  chainId: ChainId
 ) {
   try {
     const receipt = await provider.getTransactionReceipt(tx);
-    const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
-    const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
+    const sequence = parseSequenceFromLogEth(
+      receipt,
+      getBridgeAddressForChain(chainId)
+    );
+    const emitterAddress = getEmitterAddressEth(
+      getTokenBridgeAddressForChain(chainId)
+    );
     const { vaaBytes } = await getSignedVAAWithRetry(
-      CHAIN_ID_ETH,
+      chainId,
       emitterAddress,
       sequence.toString(),
       WORMHOLE_RPC_HOSTS.length
@@ -172,14 +178,15 @@ function RecoveryDialogContent({
   useEffect(() => {
     if (recoverySourceTx) {
       let cancelled = false;
-      if (recoverySourceChain === CHAIN_ID_ETH && provider) {
+      if (isEVMChain(recoverySourceChain) && provider) {
         setRecoverySourceTxError("");
         setRecoverySourceTxIsLoading(true);
         (async () => {
-          const { vaa, error } = await eth(
+          const { vaa, error } = await evm(
             provider,
             recoverySourceTx,
-            enqueueSnackbar
+            enqueueSnackbar,
+            recoverySourceChain
           );
           if (!cancelled) {
             setRecoverySourceTxIsLoading(false);
@@ -315,8 +322,7 @@ function RecoveryDialogContent({
             </MenuItem>
           ))}
         </TextField>
-        {recoverySourceChain === CHAIN_ID_ETH ||
-        recoverySourceChain === CHAIN_ID_BSC ? (
+        {isEVMChain(recoverySourceChain) ? (
           <KeyAndBalance chainId={recoverySourceChain} />
         ) : null}
         <TextField

+ 7 - 2
bridge_ui/src/components/Transfer/Redeem.tsx

@@ -1,4 +1,5 @@
 import {
+  CHAIN_ID_BSC,
   CHAIN_ID_ETH,
   CHAIN_ID_SOLANA,
   WSOL_ADDRESS,
@@ -12,7 +13,7 @@ import {
   selectTransferTargetAsset,
   selectTransferTargetChain,
 } from "../../store/selectors";
-import { WETH_ADDRESS } from "../../utils/consts";
+import { WBNB_ADDRESS, WETH_ADDRESS } from "../../utils/consts";
 import ButtonWithLoader from "../ButtonWithLoader";
 import KeyAndBalance from "../KeyAndBalance";
 import StepDescription from "../StepDescription";
@@ -29,11 +30,15 @@ function Redeem() {
     targetChain === CHAIN_ID_ETH &&
     targetAsset &&
     targetAsset.toLowerCase() === WETH_ADDRESS.toLowerCase();
+  const isBscNative =
+    targetChain === CHAIN_ID_BSC &&
+    targetAsset &&
+    targetAsset.toLowerCase() === WBNB_ADDRESS.toLowerCase();
   const isSolNative =
     targetChain === CHAIN_ID_SOLANA &&
     targetAsset &&
     targetAsset === WSOL_ADDRESS;
-  const isNativeEligible = isEthNative || isSolNative;
+  const isNativeEligible = isEthNative || isBscNative || isSolNative;
   const [useNativeRedeem, setUseNativeRedeem] = useState(true);
   const toggleNativeRedeem = useCallback(() => {
     setUseNativeRedeem(!useNativeRedeem);

+ 2 - 3
bridge_ui/src/components/Transfer/Send.tsx

@@ -1,4 +1,3 @@
-import { CHAIN_ID_ETH } from "@certusone/wormhole-sdk";
 import { Checkbox, FormControlLabel } from "@material-ui/core";
 import { Alert } from "@material-ui/lab";
 import { ethers } from "ethers";
@@ -19,6 +18,7 @@ import {
   selectTransferTransferTx,
 } from "../../store/selectors";
 import { CHAINS_BY_ID } from "../../utils/consts";
+import { isEVMChain } from "../../utils/ethereum";
 import ButtonWithLoader from "../ButtonWithLoader";
 import KeyAndBalance from "../KeyAndBalance";
 import ShowTx from "../ShowTx";
@@ -70,8 +70,7 @@ function Send() {
     approveAmount,
   } = useAllowance(sourceChain, sourceAsset, sourceAmountParsed || undefined);
 
-  const approveButtonNeeded =
-    sourceChain === CHAIN_ID_ETH && !sufficientAllowance;
+  const approveButtonNeeded = isEVMChain(sourceChain) && !sufficientAllowance;
   const notOne = shouldApproveUnlimited || sourceAmountParsed !== oneParsed;
   const isDisabled =
     !isReady ||

+ 5 - 8
bridge_ui/src/components/Transfer/Target.tsx

@@ -1,8 +1,4 @@
-import {
-  CHAIN_ID_SOLANA,
-  CHAIN_ID_ETH,
-  hexToNativeString,
-} from "@certusone/wormhole-sdk";
+import { CHAIN_ID_SOLANA, hexToNativeString } from "@certusone/wormhole-sdk";
 import { makeStyles, MenuItem, TextField, Typography } from "@material-ui/core";
 import { Alert } from "@material-ui/lab";
 import { useCallback, useMemo } from "react";
@@ -12,6 +8,7 @@ import useMetadata from "../../hooks/useMetadata";
 import useSyncTargetAddress from "../../hooks/useSyncTargetAddress";
 import { EthGasEstimateSummary } from "../../hooks/useTransactionFees";
 import {
+  selectTransferAmount,
   selectTransferIsTargetComplete,
   selectTransferShouldLockFields,
   selectTransferSourceChain,
@@ -21,10 +18,10 @@ import {
   selectTransferTargetChain,
   selectTransferTargetError,
   UNREGISTERED_ERROR_MESSAGE,
-  selectTransferAmount,
 } from "../../store/selectors";
 import { incrementStep, setTargetChain } from "../../store/transferSlice";
 import { CHAINS, CHAINS_BY_ID } from "../../utils/consts";
+import { isEVMChain } from "../../utils/ethereum";
 import ButtonWithLoader from "../ButtonWithLoader";
 import KeyAndBalance from "../KeyAndBalance";
 import LowBalanceWarning from "../LowBalanceWarning";
@@ -152,8 +149,8 @@ function Target() {
           You will have to pay transaction fees on{" "}
           {CHAINS_BY_ID[targetChain].name} to redeem your tokens.
         </Typography>
-        {targetChain === CHAIN_ID_ETH && (
-          <EthGasEstimateSummary methodType="transfer" />
+        {isEVMChain(targetChain) && (
+          <EthGasEstimateSummary methodType="transfer" chainId={targetChain} />
         )}
       </Alert>
       <LowBalanceWarning chainId={targetChain} />

+ 12 - 17
bridge_ui/src/hooks/useAllowance.ts

@@ -1,16 +1,12 @@
-import {
-  approveEth,
-  ChainId,
-  CHAIN_ID_ETH,
-  getAllowanceEth,
-} from "@certusone/wormhole-sdk";
+import { approveEth, ChainId, getAllowanceEth } from "@certusone/wormhole-sdk";
 import { BigNumber } from "ethers";
 import { useEffect, useMemo, useState } from "react";
 import { useDispatch, useSelector } from "react-redux";
 import { useEthereumProvider } from "../contexts/EthereumProviderContext";
 import { selectTransferIsApproving } from "../store/selectors";
 import { setIsApproving } from "../store/transferSlice";
-import { ETH_TOKEN_BRIDGE_ADDRESS } from "../utils/consts";
+import { getTokenBridgeAddressForChain } from "../utils/consts";
+import { isEVMChain } from "../utils/ethereum";
 
 export default function useAllowance(
   chainId: ChainId,
@@ -23,19 +19,18 @@ export default function useAllowance(
   const isApproveProcessing = useSelector(selectTransferIsApproving);
   const { signer } = useEthereumProvider();
   const sufficientAllowance =
-    chainId !== CHAIN_ID_ETH ||
+    !isEVMChain(chainId) ||
     (allowance && transferAmount && allowance >= transferAmount);
 
   useEffect(() => {
     let cancelled = false;
-    if (
-      chainId === CHAIN_ID_ETH &&
-      tokenAddress &&
-      signer &&
-      !isApproveProcessing
-    ) {
+    if (isEVMChain(chainId) && tokenAddress && signer && !isApproveProcessing) {
       setIsAllowanceFetching(true);
-      getAllowanceEth(ETH_TOKEN_BRIDGE_ADDRESS, tokenAddress, signer).then(
+      getAllowanceEth(
+        getTokenBridgeAddressForChain(chainId),
+        tokenAddress,
+        signer
+      ).then(
         (result) => {
           if (!cancelled) {
             setIsAllowanceFetching(false);
@@ -57,14 +52,14 @@ export default function useAllowance(
   }, [chainId, tokenAddress, signer, isApproveProcessing]);
 
   const approveAmount: (amount: BigInt) => Promise<any> = useMemo(() => {
-    return chainId !== CHAIN_ID_ETH || !tokenAddress || !signer
+    return !isEVMChain(chainId) || !tokenAddress || !signer
       ? (amount: BigInt) => {
           return Promise.resolve();
         }
       : (amount: BigInt) => {
           dispatch(setIsApproving(true));
           return approveEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
+            getTokenBridgeAddressForChain(chainId),
             tokenAddress,
             signer,
             BigNumber.from(amount)

+ 12 - 10
bridge_ui/src/hooks/useCheckIfWormholeWrapped.ts

@@ -1,13 +1,12 @@
 import {
   ChainId,
-  CHAIN_ID_ETH,
   CHAIN_ID_SOLANA,
   CHAIN_ID_TERRA,
   getOriginalAssetEth,
   getOriginalAssetSol,
   getOriginalAssetTerra,
-  WormholeWrappedInfo,
   uint8ArrayToHex,
+  WormholeWrappedInfo,
 } from "@certusone/wormhole-sdk";
 import {
   getOriginalAssetEth as getOriginalAssetEthNFT,
@@ -18,6 +17,7 @@ import { LCDClient } from "@terra-money/terra.js";
 import { useEffect } from "react";
 import { useDispatch, useSelector } from "react-redux";
 import { useEthereumProvider } from "../contexts/EthereumProviderContext";
+import { setSourceWormholeWrappedInfo as setNFTSourceWormholeWrappedInfo } from "../store/nftSlice";
 import {
   selectNFTSourceAsset,
   selectNFTSourceChain,
@@ -25,16 +25,16 @@ import {
   selectTransferSourceAsset,
   selectTransferSourceChain,
 } from "../store/selectors";
-import { setSourceWormholeWrappedInfo as setNFTSourceWormholeWrappedInfo } from "../store/nftSlice";
 import { setSourceWormholeWrappedInfo as setTransferSourceWormholeWrappedInfo } from "../store/transferSlice";
 import {
-  ETH_NFT_BRIDGE_ADDRESS,
-  ETH_TOKEN_BRIDGE_ADDRESS,
+  getNFTBridgeAddressForChain,
+  getTokenBridgeAddressForChain,
   SOLANA_HOST,
   SOL_NFT_BRIDGE_ADDRESS,
   SOL_TOKEN_BRIDGE_ADDRESS,
   TERRA_HOST,
 } from "../utils/consts";
+import { isEVMChain } from "../utils/ethereum";
 
 export interface StateSafeWormholeWrappedInfo {
   isWrapped: boolean;
@@ -74,19 +74,21 @@ function useCheckIfWormholeWrapped(nft?: boolean) {
     dispatch(setSourceWormholeWrappedInfo(undefined));
     let cancelled = false;
     (async () => {
-      if (sourceChain === CHAIN_ID_ETH && provider && sourceAsset) {
+      if (isEVMChain(sourceChain) && provider && sourceAsset) {
         const wrappedInfo = makeStateSafe(
           await (nft
             ? getOriginalAssetEthNFT(
-                ETH_NFT_BRIDGE_ADDRESS,
+                getNFTBridgeAddressForChain(sourceChain),
                 provider,
                 sourceAsset,
-                tokenId
+                tokenId,
+                sourceChain
               )
             : getOriginalAssetEth(
-                ETH_TOKEN_BRIDGE_ADDRESS,
+                getTokenBridgeAddressForChain(sourceChain),
                 provider,
-                sourceAsset
+                sourceAsset,
+                sourceChain
               ))
         );
         if (!cancelled) {

+ 15 - 13
bridge_ui/src/hooks/useEthMetadata.ts → bridge_ui/src/hooks/useEvmMetadata.ts

@@ -1,4 +1,4 @@
-import { CHAIN_ID_ETH } from "@certusone/wormhole-sdk";
+import { ChainId } from "@certusone/wormhole-sdk";
 import { ethers } from "@certusone/wormhole-sdk/node_modules/ethers";
 import { useEffect, useMemo, useState } from "react";
 import {
@@ -6,9 +6,10 @@ import {
   useEthereumProvider,
 } from "../contexts/EthereumProviderContext";
 import { DataWrapper } from "../store/helpers";
+import { isEVMChain } from "../utils/ethereum";
 import useIsWalletReady from "./useIsWalletReady";
 
-export type EthMetadata = {
+export type EvmMetadata = {
   symbol?: string;
   logo?: string;
   tokenName?: string;
@@ -28,7 +29,7 @@ const handleError = () => {
 const fetchSingleMetadata = async (
   address: string,
   provider: Provider
-): Promise<EthMetadata> => {
+): Promise<EvmMetadata> => {
   const contract = new ethers.Contract(address, ERC20_BASIC_ABI, provider);
   const [name, symbol, decimals] = await Promise.all([
     contract.name().catch(handleError),
@@ -39,12 +40,12 @@ const fetchSingleMetadata = async (
 };
 
 const fetchEthMetadata = async (addresses: string[], provider: Provider) => {
-  const promises: Promise<EthMetadata>[] = [];
+  const promises: Promise<EvmMetadata>[] = [];
   addresses.forEach((address) => {
     promises.push(fetchSingleMetadata(address, provider));
   });
   const resultsArray = await Promise.all(promises);
-  const output = new Map<string, EthMetadata>();
+  const output = new Map<string, EvmMetadata>();
   addresses.forEach((address, index) => {
     output.set(address, resultsArray[index]);
   });
@@ -52,19 +53,20 @@ const fetchEthMetadata = async (addresses: string[], provider: Provider) => {
   return output;
 };
 
-function useEthMetadata(
-  addresses: string[]
-): DataWrapper<Map<string, EthMetadata>> {
-  const { isReady } = useIsWalletReady(CHAIN_ID_ETH);
+function useEvmMetadata(
+  addresses: string[],
+  chainId: ChainId
+): DataWrapper<Map<string, EvmMetadata>> {
+  const { isReady } = useIsWalletReady(chainId);
   const { provider } = useEthereumProvider();
 
   const [isFetching, setIsFetching] = useState(false);
   const [error, setError] = useState("");
-  const [data, setData] = useState<Map<string, EthMetadata> | null>(null);
+  const [data, setData] = useState<Map<string, EvmMetadata> | null>(null);
 
   useEffect(() => {
     let cancelled = false;
-    if (addresses.length && provider && isReady) {
+    if (addresses.length && provider && isReady && isEVMChain(chainId)) {
       setIsFetching(true);
       setError("");
       setData(null);
@@ -86,7 +88,7 @@ function useEthMetadata(
     return () => {
       cancelled = true;
     };
-  }, [addresses, provider, isReady]);
+  }, [addresses, provider, isReady, chainId]);
 
   return useMemo(
     () => ({
@@ -99,4 +101,4 @@ function useEthMetadata(
   );
 }
 
-export default useEthMetadata;
+export default useEvmMetadata;

+ 15 - 8
bridge_ui/src/hooks/useFetchTargetAsset.ts

@@ -1,5 +1,4 @@
 import {
-  CHAIN_ID_ETH,
   CHAIN_ID_SOLANA,
   CHAIN_ID_TERRA,
   getForeignAssetEth,
@@ -33,14 +32,16 @@ import {
 } from "../store/selectors";
 import { setTargetAsset as setTransferTargetAsset } from "../store/transferSlice";
 import {
-  ETH_NFT_BRIDGE_ADDRESS,
-  ETH_TOKEN_BRIDGE_ADDRESS,
+  getEvmChainId,
+  getNFTBridgeAddressForChain,
+  getTokenBridgeAddressForChain,
   SOLANA_HOST,
   SOL_NFT_BRIDGE_ADDRESS,
   SOL_TOKEN_BRIDGE_ADDRESS,
   TERRA_HOST,
   TERRA_TOKEN_BRIDGE_ADDRESS,
 } from "../utils/consts";
+import { isEVMChain } from "../utils/ethereum";
 
 function useFetchTargetAsset(nft?: boolean) {
   const dispatch = useDispatch();
@@ -61,32 +62,35 @@ function useFetchTargetAsset(nft?: boolean) {
     nft ? selectNFTTargetChain : selectTransferTargetChain
   );
   const setTargetAsset = nft ? setNFTTargetAsset : setTransferTargetAsset;
-  const { provider } = useEthereumProvider();
+  const { provider, chainId: evmChainId } = useEthereumProvider();
+  const correctEvmNetwork = getEvmChainId(targetChain);
+  const hasCorrectEvmNetwork = evmChainId === correctEvmNetwork;
   useEffect(() => {
     if (isSourceAssetWormholeWrapped && originChain === targetChain) {
       dispatch(setTargetAsset(hexToNativeString(originAsset, originChain)));
       return;
     }
     // TODO: loading state, error state
-    dispatch(setTargetAsset(undefined));
     let cancelled = false;
     (async () => {
       if (
-        targetChain === CHAIN_ID_ETH &&
+        isEVMChain(targetChain) &&
         provider &&
+        hasCorrectEvmNetwork &&
         originChain &&
         originAsset
       ) {
+        dispatch(setTargetAsset(undefined));
         try {
           const asset = await (nft
             ? getForeignAssetEthNFT(
-                ETH_NFT_BRIDGE_ADDRESS,
+                getNFTBridgeAddressForChain(targetChain),
                 provider,
                 originChain,
                 hexToUint8Array(originAsset)
               )
             : getForeignAssetEth(
-                ETH_TOKEN_BRIDGE_ADDRESS,
+                getTokenBridgeAddressForChain(targetChain),
                 provider,
                 originChain,
                 hexToUint8Array(originAsset)
@@ -102,6 +106,7 @@ function useFetchTargetAsset(nft?: boolean) {
         }
       }
       if (targetChain === CHAIN_ID_SOLANA && originChain && originAsset) {
+        dispatch(setTargetAsset(undefined));
         try {
           const connection = new Connection(SOLANA_HOST, "confirmed");
           const asset = await (nft
@@ -128,6 +133,7 @@ function useFetchTargetAsset(nft?: boolean) {
         }
       }
       if (targetChain === CHAIN_ID_TERRA && originChain && originAsset) {
+        dispatch(setTargetAsset(undefined));
         try {
           const lcd = new LCDClient(TERRA_HOST);
           const asset = await getForeignAssetTerra(
@@ -160,6 +166,7 @@ function useFetchTargetAsset(nft?: boolean) {
     nft,
     setTargetAsset,
     tokenId,
+    hasCorrectEvmNetwork,
   ]);
 }
 

+ 18 - 5
bridge_ui/src/hooks/useGetBalanceEffect.ts

@@ -1,12 +1,11 @@
 import {
-  CHAIN_ID_ETH,
   CHAIN_ID_SOLANA,
   CHAIN_ID_TERRA,
   TokenImplementation__factory,
 } from "@certusone/wormhole-sdk";
 import { Connection, PublicKey } from "@solana/web3.js";
-import { useConnectedWallet } from "@terra-money/wallet-provider";
 import { LCDClient } from "@terra-money/terra.js";
+import { useConnectedWallet } from "@terra-money/wallet-provider";
 import { formatUnits } from "ethers/lib/utils";
 import { useEffect } from "react";
 import { useDispatch, useSelector } from "react-redux";
@@ -22,9 +21,12 @@ import {
   setSourceParsedTokenAccount,
   setTargetParsedTokenAccount,
 } from "../store/transferSlice";
-import { SOLANA_HOST, TERRA_HOST } from "../utils/consts";
+import { getEvmChainId, SOLANA_HOST, TERRA_HOST } from "../utils/consts";
+import { isEVMChain } from "../utils/ethereum";
 import { createParsedTokenAccount } from "./useGetSourceParsedTokenAccounts";
 
+// TODO: we only ever use this for target, could clean up and rename
+
 /**
  * Fetches the balance of an asset for the connected wallet
  * @param sourceOrTarget determines whether this will fetch balance for the source or target account. Not intended to be switched on the same hook!
@@ -48,7 +50,12 @@ function useGetBalanceEffect(sourceOrTarget: "source" | "target") {
   const solanaWallet = useSolanaWallet();
   const solPK = solanaWallet?.publicKey;
   const terraWallet = useConnectedWallet();
-  const { provider, signerAddress } = useEthereumProvider();
+  const {
+    provider,
+    signerAddress,
+    chainId: evmChainId,
+  } = useEthereumProvider();
+  const hasCorrectEvmNetwork = evmChainId === getEvmChainId(lookupChain);
   useEffect(() => {
     // source is now handled by getsourceparsedtokenaccounts
     if (sourceOrTarget === "source") return;
@@ -127,7 +134,12 @@ function useGetBalanceEffect(sourceOrTarget: "source" | "target") {
           }
         });
     }
-    if (lookupChain === CHAIN_ID_ETH && provider && signerAddress) {
+    if (
+      isEVMChain(lookupChain) &&
+      provider &&
+      signerAddress &&
+      hasCorrectEvmNetwork
+    ) {
       const token = TokenImplementation__factory.connect(lookupAsset, provider);
       token
         .decimals()
@@ -170,6 +182,7 @@ function useGetBalanceEffect(sourceOrTarget: "source" | "target") {
     solPK,
     sourceOrTarget,
     terraWallet,
+    hasCorrectEvmNetwork,
   ]);
 }
 

+ 74 - 11
bridge_ui/src/hooks/useGetSourceParsedTokenAccounts.ts

@@ -1,4 +1,6 @@
 import {
+  ChainId,
+  CHAIN_ID_BSC,
   CHAIN_ID_ETH,
   CHAIN_ID_SOLANA,
   CHAIN_ID_TERRA,
@@ -53,9 +55,12 @@ import {
 import {
   COVALENT_GET_TOKENS_URL,
   SOLANA_HOST,
+  WBNB_ADDRESS,
+  WBNB_DECIMALS,
   WETH_ADDRESS,
   WETH_DECIMALS,
 } from "../utils/consts";
+import { isEVMChain } from "../utils/ethereum";
 import {
   ExtractedMintInfo,
   extractMintInfo,
@@ -205,6 +210,29 @@ const createNativeEthParsedTokenAccount = (
       });
 };
 
+const createNativeBscParsedTokenAccount = (
+  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
+          WBNB_ADDRESS, //Mint key, On the other side this will be WBNB, so this is hopefully a white lie.
+          balanceInWei.toString(), //amount, in wei
+          WBNB_DECIMALS, //Luckily both BNB and WBNB 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.
+          "BNB", //A white lie for display purposes
+          "Binance Coin", //A white lie for display purposes
+          undefined, //TODO logo
+          true //isNativeAsset
+        );
+      });
+};
+
 const createNFTParsedTokenAccountFromCovalent = (
   walletAddress: string,
   covalent: CovalentData,
@@ -266,9 +294,10 @@ export type CovalentNFTData = {
 
 const getEthereumAccountsCovalent = async (
   walletAddress: string,
-  nft?: boolean
+  nft: boolean,
+  chainId: ChainId
 ): Promise<CovalentData[]> => {
-  const url = COVALENT_GET_TOKENS_URL(CHAIN_ID_ETH, walletAddress, nft);
+  const url = COVALENT_GET_TOKENS_URL(chainId, walletAddress, nft);
 
   try {
     const output = [] as CovalentData[];
@@ -387,12 +416,11 @@ function useGetAvailableTokens(nft: boolean = false) {
   const selectedSourceWalletAddress = useSelector(
     nft ? selectNFTSourceWalletAddress : selectSourceWalletAddress
   );
-  const currentSourceWalletAddress: string | undefined =
-    lookupChain === CHAIN_ID_ETH
-      ? signerAddress
-      : lookupChain === CHAIN_ID_SOLANA
-      ? solPK?.toString()
-      : undefined;
+  const currentSourceWalletAddress: string | undefined = isEVMChain(lookupChain)
+    ? signerAddress
+    : lookupChain === CHAIN_ID_SOLANA
+    ? solPK?.toString()
+    : undefined;
 
   const resetSourceAccounts = useCallback(() => {
     dispatch(
@@ -539,15 +567,50 @@ function useGetAvailableTokens(nft: boolean = false) {
     };
   }, [lookupChain, provider, signerAddress, nft, ethNativeAccount]);
 
+  //Binance Smart Chain native asset load
+  useEffect(() => {
+    let cancelled = false;
+    if (
+      signerAddress &&
+      lookupChain === CHAIN_ID_BSC &&
+      !ethNativeAccount &&
+      !nft
+    ) {
+      setEthNativeAccountLoading(true);
+      createNativeBscParsedTokenAccount(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 BSC balance.");
+          }
+        }
+      );
+    }
+
+    return () => {
+      cancelled = true;
+    };
+  }, [lookupChain, provider, signerAddress, nft, ethNativeAccount]);
+
   //Ethereum covalent accounts load
   useEffect(() => {
     //const testWallet = "0xf60c2ea62edbfe808163751dd0d8693dcb30019c";
     // const nftTestWallet1 = "0x3f304c6721f35ff9af00fd32650c8e0a982180ab";
     // const nftTestWallet2 = "0x98ed231428088eb440e8edb5cc8d66dcf913b86e";
     // const nftTestWallet3 = "0xb1fadf677a7e9b90e9d4f31c8ffb3dc18c138c6f";
+    // const nftBscTestWallet1 = "0x5f464a652bd1991df0be37979b93b3306d64a909";
     let cancelled = false;
     const walletAddress = signerAddress;
-    if (walletAddress && lookupChain === CHAIN_ID_ETH && !covalent) {
+    if (walletAddress && isEVMChain(lookupChain) && !covalent) {
       //TODO less cancel
       !cancelled && setCovalentLoading(true);
       !cancelled &&
@@ -556,7 +619,7 @@ function useGetAvailableTokens(nft: boolean = false) {
             ? fetchSourceParsedTokenAccountsNFT()
             : fetchSourceParsedTokenAccounts()
         );
-      getEthereumAccountsCovalent(walletAddress, nft).then(
+      getEthereumAccountsCovalent(walletAddress, nft, lookupChain).then(
         (accounts) => {
           !cancelled && setCovalentLoading(false);
           !cancelled && setCovalentError(undefined);
@@ -639,7 +702,7 @@ function useGetAvailableTokens(nft: boolean = false) {
         },
         resetAccounts: resetSourceAccounts,
       }
-    : lookupChain === CHAIN_ID_ETH
+    : isEVMChain(lookupChain)
     ? {
         tokenAccounts: ethAccounts,
         covalent: {

+ 19 - 12
bridge_ui/src/hooks/useHandleAttest.ts

@@ -2,7 +2,7 @@ import {
   attestFromEth,
   attestFromSolana,
   attestFromTerra,
-  CHAIN_ID_ETH,
+  ChainId,
   CHAIN_ID_SOLANA,
   CHAIN_ID_TERRA,
   getEmitterAddressEth,
@@ -19,10 +19,10 @@ import {
   ConnectedWallet,
   useConnectedWallet,
 } from "@terra-money/wallet-provider";
+import { Signer } from "ethers";
 import { useSnackbar } from "notistack";
 import { useCallback, useMemo } from "react";
 import { useDispatch, useSelector } from "react-redux";
-import { Signer } from "ethers";
 import { useEthereumProvider } from "../contexts/EthereumProviderContext";
 import { useSolanaWallet } from "../contexts/SolanaWalletContext";
 import {
@@ -38,28 +38,30 @@ import {
   selectAttestSourceChain,
 } from "../store/selectors";
 import {
-  ETH_BRIDGE_ADDRESS,
-  ETH_TOKEN_BRIDGE_ADDRESS,
+  getBridgeAddressForChain,
+  getTokenBridgeAddressForChain,
   SOLANA_HOST,
   SOL_BRIDGE_ADDRESS,
   SOL_TOKEN_BRIDGE_ADDRESS,
   TERRA_TOKEN_BRIDGE_ADDRESS,
 } from "../utils/consts";
+import { isEVMChain } from "../utils/ethereum";
 import { getSignedVAAWithRetry } from "../utils/getSignedVAAWithRetry";
 import parseError from "../utils/parseError";
 import { signSendAndConfirm } from "../utils/solana";
 import { waitForTerraExecution } from "../utils/terra";
 
-async function eth(
+async function evm(
   dispatch: any,
   enqueueSnackbar: any,
   signer: Signer,
-  sourceAsset: string
+  sourceAsset: string,
+  chainId: ChainId
 ) {
   dispatch(setIsSending(true));
   try {
     const receipt = await attestFromEth(
-      ETH_TOKEN_BRIDGE_ADDRESS,
+      getTokenBridgeAddressForChain(chainId),
       signer,
       sourceAsset
     );
@@ -67,11 +69,16 @@ async function eth(
       setAttestTx({ id: receipt.transactionHash, block: receipt.blockNumber })
     );
     enqueueSnackbar("Transaction confirmed", { variant: "success" });
-    const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
-    const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
+    const sequence = parseSequenceFromLogEth(
+      receipt,
+      getBridgeAddressForChain(chainId)
+    );
+    const emitterAddress = getEmitterAddressEth(
+      getTokenBridgeAddressForChain(chainId)
+    );
     enqueueSnackbar("Fetching VAA", { variant: "info" });
     const { vaaBytes } = await getSignedVAAWithRetry(
-      CHAIN_ID_ETH,
+      chainId,
       emitterAddress,
       sequence
     );
@@ -180,8 +187,8 @@ export function useHandleAttest() {
   const terraWallet = useConnectedWallet();
   const disabled = !isTargetComplete || isSending || isSendComplete;
   const handleAttestClick = useCallback(() => {
-    if (sourceChain === CHAIN_ID_ETH && !!signer) {
-      eth(dispatch, enqueueSnackbar, signer, sourceAsset);
+    if (isEVMChain(sourceChain) && !!signer) {
+      evm(dispatch, enqueueSnackbar, signer, sourceAsset, sourceChain);
     } else if (sourceChain === CHAIN_ID_SOLANA && !!solanaWallet && !!solPK) {
       solana(dispatch, enqueueSnackbar, solPK, sourceAsset, solanaWallet);
     } else if (sourceChain === CHAIN_ID_TERRA && !!terraWallet) {

+ 9 - 7
bridge_ui/src/hooks/useHandleCreateWrapped.ts

@@ -1,5 +1,5 @@
 import {
-  CHAIN_ID_ETH,
+  ChainId,
   CHAIN_ID_SOLANA,
   CHAIN_ID_TERRA,
   createWrappedOnEth,
@@ -26,25 +26,27 @@ import {
   selectAttestTargetChain,
 } from "../store/selectors";
 import {
-  ETH_TOKEN_BRIDGE_ADDRESS,
+  getTokenBridgeAddressForChain,
   SOLANA_HOST,
   SOL_BRIDGE_ADDRESS,
   SOL_TOKEN_BRIDGE_ADDRESS,
   TERRA_TOKEN_BRIDGE_ADDRESS,
 } from "../utils/consts";
+import { isEVMChain } from "../utils/ethereum";
 import parseError from "../utils/parseError";
 import { signSendAndConfirm } from "../utils/solana";
 
-async function eth(
+async function evm(
   dispatch: any,
   enqueueSnackbar: any,
   signer: Signer,
-  signedVAA: Uint8Array
+  signedVAA: Uint8Array,
+  chainId: ChainId
 ) {
   dispatch(setIsCreating(true));
   try {
     const receipt = await createWrappedOnEth(
-      ETH_TOKEN_BRIDGE_ADDRESS,
+      getTokenBridgeAddressForChain(chainId),
       signer,
       signedVAA
     );
@@ -133,8 +135,8 @@ export function useHandleCreateWrapped() {
   const { signer } = useEthereumProvider();
   const terraWallet = useConnectedWallet();
   const handleCreateClick = useCallback(() => {
-    if (targetChain === CHAIN_ID_ETH && !!signer && !!signedVAA) {
-      eth(dispatch, enqueueSnackbar, signer, signedVAA);
+    if (isEVMChain(targetChain) && !!signer && !!signedVAA) {
+      evm(dispatch, enqueueSnackbar, signer, signedVAA, targetChain);
     } else if (
       targetChain === CHAIN_ID_SOLANA &&
       !!solanaWallet &&

+ 11 - 9
bridge_ui/src/hooks/useHandleNFTRedeem.ts

@@ -1,10 +1,10 @@
 import {
-  CHAIN_ID_ETH,
+  ChainId,
   CHAIN_ID_SOLANA,
   getClaimAddressSolana,
-  postVaaSolana,
-  parseNFTPayload,
   hexToUint8Array,
+  parseNFTPayload,
+  postVaaSolana,
 } from "@certusone/wormhole-sdk";
 import {
   createMetaOnSolana,
@@ -25,26 +25,28 @@ import { useSolanaWallet } from "../contexts/SolanaWalletContext";
 import { setIsRedeeming, setRedeemTx } from "../store/nftSlice";
 import { selectNFTIsRedeeming, selectNFTTargetChain } from "../store/selectors";
 import {
-  ETH_NFT_BRIDGE_ADDRESS,
+  getNFTBridgeAddressForChain,
   SOLANA_HOST,
   SOL_BRIDGE_ADDRESS,
   SOL_NFT_BRIDGE_ADDRESS,
 } from "../utils/consts";
+import { isEVMChain } from "../utils/ethereum";
 import { getMetadataAddress } from "../utils/metaplex";
 import parseError from "../utils/parseError";
 import { signSendAndConfirm } from "../utils/solana";
 import useNFTSignedVAA from "./useNFTSignedVAA";
 
-async function eth(
+async function evm(
   dispatch: any,
   enqueueSnackbar: any,
   signer: Signer,
-  signedVAA: Uint8Array
+  signedVAA: Uint8Array,
+  chainId: ChainId
 ) {
   dispatch(setIsRedeeming(true));
   try {
     const receipt = await redeemOnEth(
-      ETH_NFT_BRIDGE_ADDRESS,
+      getNFTBridgeAddressForChain(chainId),
       signer,
       signedVAA
     );
@@ -142,8 +144,8 @@ export function useHandleNFTRedeem() {
   const signedVAA = useNFTSignedVAA();
   const isRedeeming = useSelector(selectNFTIsRedeeming);
   const handleRedeemClick = useCallback(() => {
-    if (targetChain === CHAIN_ID_ETH && !!signer && signedVAA) {
-      eth(dispatch, enqueueSnackbar, signer, signedVAA);
+    if (isEVMChain(targetChain) && !!signer && signedVAA) {
+      evm(dispatch, enqueueSnackbar, signer, signedVAA, targetChain);
     } else if (
       targetChain === CHAIN_ID_SOLANA &&
       !!solanaWallet &&

+ 20 - 13
bridge_ui/src/hooks/useHandleNFTTransfer.ts

@@ -1,12 +1,11 @@
 import {
   ChainId,
-  CHAIN_ID_ETH,
   CHAIN_ID_SOLANA,
   getEmitterAddressEth,
   getEmitterAddressSolana,
+  hexToUint8Array,
   parseSequenceFromLogEth,
   parseSequenceFromLogSolana,
-  hexToUint8Array,
   uint8ArrayToHex,
 } from "@certusone/wormhole-sdk";
 import {
@@ -40,30 +39,32 @@ import {
   selectNFTTargetChain,
 } from "../store/selectors";
 import {
-  ETH_BRIDGE_ADDRESS,
-  ETH_NFT_BRIDGE_ADDRESS,
+  getBridgeAddressForChain,
+  getNFTBridgeAddressForChain,
   SOLANA_HOST,
   SOL_BRIDGE_ADDRESS,
   SOL_NFT_BRIDGE_ADDRESS,
 } from "../utils/consts";
+import { isEVMChain } from "../utils/ethereum";
 import { getSignedVAAWithRetry } from "../utils/getSignedVAAWithRetry";
 import parseError from "../utils/parseError";
 import { signSendAndConfirm } from "../utils/solana";
 import useNFTTargetAddressHex from "./useNFTTargetAddress";
 
-async function eth(
+async function evm(
   dispatch: any,
   enqueueSnackbar: any,
   signer: Signer,
   tokenAddress: string,
   tokenId: string,
   recipientChain: ChainId,
-  recipientAddress: Uint8Array
+  recipientAddress: Uint8Array,
+  chainId: ChainId
 ) {
   dispatch(setIsSending(true));
   try {
     const receipt = await transferFromEth(
-      ETH_NFT_BRIDGE_ADDRESS,
+      getNFTBridgeAddressForChain(chainId),
       signer,
       tokenAddress,
       tokenId,
@@ -74,11 +75,16 @@ async function eth(
       setTransferTx({ id: receipt.transactionHash, block: receipt.blockNumber })
     );
     enqueueSnackbar("Transaction confirmed", { variant: "success" });
-    const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
-    const emitterAddress = getEmitterAddressEth(ETH_NFT_BRIDGE_ADDRESS);
+    const sequence = parseSequenceFromLogEth(
+      receipt,
+      getBridgeAddressForChain(chainId)
+    );
+    const emitterAddress = getEmitterAddressEth(
+      getNFTBridgeAddressForChain(chainId)
+    );
     enqueueSnackbar("Fetching VAA", { variant: "info" });
     const { vaaBytes } = await getSignedVAAWithRetry(
-      CHAIN_ID_ETH,
+      chainId,
       emitterAddress,
       sequence.toString()
     );
@@ -178,20 +184,21 @@ export function useHandleNFTTransfer() {
   const handleTransferClick = useCallback(() => {
     // TODO: we should separate state for transaction vs fetching vaa
     if (
-      sourceChain === CHAIN_ID_ETH &&
+      isEVMChain(sourceChain) &&
       !!signer &&
       !!sourceAsset &&
       !!sourceTokenId &&
       !!targetAddress
     ) {
-      eth(
+      evm(
         dispatch,
         enqueueSnackbar,
         signer,
         sourceAsset,
         sourceTokenId,
         targetChain,
-        targetAddress
+        targetAddress,
+        sourceChain
       );
     } else if (
       sourceChain === CHAIN_ID_SOLANA &&

+ 20 - 10
bridge_ui/src/hooks/useHandleRedeem.ts

@@ -1,5 +1,5 @@
 import {
-  CHAIN_ID_ETH,
+  ChainId,
   CHAIN_ID_SOLANA,
   CHAIN_ID_TERRA,
   postVaaSolana,
@@ -28,27 +28,37 @@ import {
 } from "../store/selectors";
 import { setIsRedeeming, setRedeemTx } from "../store/transferSlice";
 import {
-  ETH_TOKEN_BRIDGE_ADDRESS,
+  getTokenBridgeAddressForChain,
   SOLANA_HOST,
   SOL_BRIDGE_ADDRESS,
   SOL_TOKEN_BRIDGE_ADDRESS,
   TERRA_TOKEN_BRIDGE_ADDRESS,
 } from "../utils/consts";
+import { isEVMChain } from "../utils/ethereum";
 import parseError from "../utils/parseError";
 import { signSendAndConfirm } from "../utils/solana";
 
-async function eth(
+async function evm(
   dispatch: any,
   enqueueSnackbar: any,
   signer: Signer,
   signedVAA: Uint8Array,
-  isNative: boolean
+  isNative: boolean,
+  chainId: ChainId
 ) {
   dispatch(setIsRedeeming(true));
   try {
     const receipt = isNative
-      ? await redeemOnEthNative(ETH_TOKEN_BRIDGE_ADDRESS, signer, signedVAA)
-      : await redeemOnEth(ETH_TOKEN_BRIDGE_ADDRESS, signer, signedVAA);
+      ? await redeemOnEthNative(
+          getTokenBridgeAddressForChain(chainId),
+          signer,
+          signedVAA
+        )
+      : await redeemOnEth(
+          getTokenBridgeAddressForChain(chainId),
+          signer,
+          signedVAA
+        );
     dispatch(
       setRedeemTx({ id: receipt.transactionHash, block: receipt.blockNumber })
     );
@@ -144,8 +154,8 @@ export function useHandleRedeem() {
   const signedVAA = useTransferSignedVAA();
   const isRedeeming = useSelector(selectTransferIsRedeeming);
   const handleRedeemClick = useCallback(() => {
-    if (targetChain === CHAIN_ID_ETH && !!signer && signedVAA) {
-      eth(dispatch, enqueueSnackbar, signer, signedVAA, false);
+    if (isEVMChain(targetChain) && !!signer && signedVAA) {
+      evm(dispatch, enqueueSnackbar, signer, signedVAA, false, targetChain);
     } else if (
       targetChain === CHAIN_ID_SOLANA &&
       !!solanaWallet &&
@@ -179,8 +189,8 @@ export function useHandleRedeem() {
   ]);
 
   const handleRedeemNativeClick = useCallback(() => {
-    if (targetChain === CHAIN_ID_ETH && !!signer && signedVAA) {
-      eth(dispatch, enqueueSnackbar, signer, signedVAA, true);
+    if (isEVMChain(targetChain) && !!signer && signedVAA) {
+      evm(dispatch, enqueueSnackbar, signer, signedVAA, true, targetChain);
     } else if (
       targetChain === CHAIN_ID_SOLANA &&
       !!solanaWallet &&

+ 22 - 15
bridge_ui/src/hooks/useHandleTransfer.ts

@@ -1,20 +1,19 @@
 import {
   ChainId,
-  CHAIN_ID_ETH,
   CHAIN_ID_SOLANA,
   CHAIN_ID_TERRA,
   getEmitterAddressEth,
   getEmitterAddressSolana,
   getEmitterAddressTerra,
+  hexToUint8Array,
   parseSequenceFromLogEth,
   parseSequenceFromLogSolana,
   parseSequenceFromLogTerra,
   transferFromEth,
   transferFromEthNative,
   transferFromSolana,
-  transferNativeSol,
   transferFromTerra,
-  hexToUint8Array,
+  transferNativeSol,
   uint8ArrayToHex,
 } from "@certusone/wormhole-sdk";
 import { WalletContextState } from "@solana/wallet-adapter-react";
@@ -48,20 +47,21 @@ import {
   setTransferTx,
 } from "../store/transferSlice";
 import {
-  ETH_BRIDGE_ADDRESS,
-  ETH_TOKEN_BRIDGE_ADDRESS,
+  getBridgeAddressForChain,
+  getTokenBridgeAddressForChain,
   SOLANA_HOST,
   SOL_BRIDGE_ADDRESS,
   SOL_TOKEN_BRIDGE_ADDRESS,
   TERRA_TOKEN_BRIDGE_ADDRESS,
 } from "../utils/consts";
+import { isEVMChain } from "../utils/ethereum";
 import { getSignedVAAWithRetry } from "../utils/getSignedVAAWithRetry";
 import parseError from "../utils/parseError";
 import { signSendAndConfirm } from "../utils/solana";
 import { waitForTerraExecution } from "../utils/terra";
 import useTransferTargetAddressHex from "./useTransferTargetAddress";
 
-async function eth(
+async function evm(
   dispatch: any,
   enqueueSnackbar: any,
   signer: Signer,
@@ -70,21 +70,22 @@ async function eth(
   amount: string,
   recipientChain: ChainId,
   recipientAddress: Uint8Array,
-  isNative: boolean
+  isNative: boolean,
+  chainId: ChainId
 ) {
   dispatch(setIsSending(true));
   try {
     const amountParsed = parseUnits(amount, decimals);
     const receipt = isNative
       ? await transferFromEthNative(
-          ETH_TOKEN_BRIDGE_ADDRESS,
+          getTokenBridgeAddressForChain(chainId),
           signer,
           amountParsed,
           recipientChain,
           recipientAddress
         )
       : await transferFromEth(
-          ETH_TOKEN_BRIDGE_ADDRESS,
+          getTokenBridgeAddressForChain(chainId),
           signer,
           tokenAddress,
           amountParsed,
@@ -95,11 +96,16 @@ async function eth(
       setTransferTx({ id: receipt.transactionHash, block: receipt.blockNumber })
     );
     enqueueSnackbar("Transaction confirmed", { variant: "success" });
-    const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
-    const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
+    const sequence = parseSequenceFromLogEth(
+      receipt,
+      getBridgeAddressForChain(chainId)
+    );
+    const emitterAddress = getEmitterAddressEth(
+      getTokenBridgeAddressForChain(chainId)
+    );
     enqueueSnackbar("Fetching VAA", { variant: "info" });
     const { vaaBytes } = await getSignedVAAWithRetry(
-      CHAIN_ID_ETH,
+      chainId,
       emitterAddress,
       sequence.toString()
     );
@@ -262,13 +268,13 @@ export function useHandleTransfer() {
   const handleTransferClick = useCallback(() => {
     // TODO: we should separate state for transaction vs fetching vaa
     if (
-      sourceChain === CHAIN_ID_ETH &&
+      isEVMChain(sourceChain) &&
       !!signer &&
       !!sourceAsset &&
       decimals !== undefined &&
       !!targetAddress
     ) {
-      eth(
+      evm(
         dispatch,
         enqueueSnackbar,
         signer,
@@ -277,7 +283,8 @@ export function useHandleTransfer() {
         amount,
         targetChain,
         targetAddress,
-        isNative
+        isNative,
+        sourceChain
       );
     } else if (
       sourceChain === CHAIN_ID_SOLANA &&

+ 12 - 10
bridge_ui/src/hooks/useIsWalletReady.ts

@@ -1,6 +1,5 @@
 import {
   ChainId,
-  CHAIN_ID_ETH,
   CHAIN_ID_SOLANA,
   CHAIN_ID_TERRA,
 } from "@certusone/wormhole-sdk";
@@ -9,7 +8,8 @@ import { useConnectedWallet } from "@terra-money/wallet-provider";
 import { useMemo } from "react";
 import { useEthereumProvider } from "../contexts/EthereumProviderContext";
 import { useSolanaWallet } from "../contexts/SolanaWalletContext";
-import { CLUSTER, ETH_NETWORK_CHAIN_ID } from "../utils/consts";
+import { CLUSTER, getEvmChainId } from "../utils/consts";
+import { isEVMChain } from "../utils/ethereum";
 
 const createWalletStatus = (
   isReady: boolean,
@@ -33,10 +33,11 @@ function useIsWalletReady(chainId: ChainId): {
   const {
     provider,
     signerAddress,
-    chainId: ethChainId,
+    chainId: evmChainId,
   } = useEthereumProvider();
   const hasEthInfo = !!provider && !!signerAddress;
-  const hasCorrectEthNetwork = ethChainId === ETH_NETWORK_CHAIN_ID;
+  const correctEvmNetwork = getEvmChainId(chainId);
+  const hasCorrectEvmNetwork = evmChainId === correctEvmNetwork;
 
   return useMemo(() => {
     if (
@@ -50,20 +51,20 @@ function useIsWalletReady(chainId: ChainId): {
     if (chainId === CHAIN_ID_SOLANA && solPK) {
       return createWalletStatus(true, undefined, solPK.toString());
     }
-    if (chainId === CHAIN_ID_ETH && hasEthInfo && signerAddress) {
-      if (hasCorrectEthNetwork) {
+    if (isEVMChain(chainId) && hasEthInfo && signerAddress) {
+      if (hasCorrectEvmNetwork) {
         return createWalletStatus(true, undefined, signerAddress);
       } else {
-        if (provider) {
+        if (provider && correctEvmNetwork) {
           try {
             provider.send("wallet_switchEthereumChain", [
-              { chainId: hexStripZeros(hexlify(ETH_NETWORK_CHAIN_ID)) },
+              { chainId: hexStripZeros(hexlify(correctEvmNetwork)) },
             ]);
           } catch (e) {}
         }
         return createWalletStatus(
           false,
-          `Wallet is not connected to ${CLUSTER}. Expected Chain ID: ${ETH_NETWORK_CHAIN_ID}`,
+          `Wallet is not connected to ${CLUSTER}. Expected Chain ID: ${correctEvmNetwork}`,
           undefined
         );
       }
@@ -75,7 +76,8 @@ function useIsWalletReady(chainId: ChainId): {
     hasTerraWallet,
     solPK,
     hasEthInfo,
-    hasCorrectEthNetwork,
+    correctEvmNetwork,
+    hasCorrectEvmNetwork,
     provider,
     signerAddress,
     terraWallet,

+ 6 - 6
bridge_ui/src/hooks/useMetadata.ts

@@ -1,14 +1,14 @@
 import {
   ChainId,
-  CHAIN_ID_ETH,
   CHAIN_ID_SOLANA,
   CHAIN_ID_TERRA,
 } from "@certusone/wormhole-sdk";
 import { TokenInfo } from "@solana/spl-token-registry";
 import { useMemo } from "react";
 import { DataWrapper, getEmptyDataWrapper } from "../store/helpers";
+import { isEVMChain } from "../utils/ethereum";
 import { Metadata } from "../utils/metaplex";
-import useEthMetadata, { EthMetadata } from "./useEthMetadata";
+import useEvmMetadata, { EvmMetadata } from "./useEvmMetadata";
 import useMetaplexData from "./useMetaplexData";
 import useSolanaTokenMap from "./useSolanaTokenMap";
 import useTerraTokenMap, { TerraTokenMap } from "./useTerraTokenMap";
@@ -80,7 +80,7 @@ const constructTerraMetadata = (
 
 const constructEthMetadata = (
   addresses: string[],
-  metadataMap: DataWrapper<Map<string, EthMetadata> | null>
+  metadataMap: DataWrapper<Map<string, EvmMetadata> | null>
 ) => {
   const isFetching = metadataMap.isFetching;
   const error = metadataMap.error;
@@ -119,17 +119,17 @@ export default function useMetadata(
     return chainId === CHAIN_ID_TERRA ? addresses : [];
   }, [chainId, addresses]);
   const ethereumAddresses = useMemo(() => {
-    return chainId === CHAIN_ID_ETH ? addresses : [];
+    return isEVMChain(chainId) ? addresses : [];
   }, [chainId, addresses]);
 
   const metaplexData = useMetaplexData(solanaAddresses);
-  const ethMetadata = useEthMetadata(ethereumAddresses);
+  const ethMetadata = useEvmMetadata(ethereumAddresses, chainId);
 
   const output: DataWrapper<Map<string, GenericMetadata>> = useMemo(
     () =>
       chainId === CHAIN_ID_SOLANA
         ? constructSolanaMetadata(solanaAddresses, solanaTokenMap, metaplexData)
-        : chainId === CHAIN_ID_ETH
+        : isEVMChain(chainId)
         ? constructEthMetadata(ethereumAddresses, ethMetadata)
         : chainId === CHAIN_ID_TERRA
         ? constructTerraMetadata(terraAddresses, terraTokenMap)

+ 4 - 4
bridge_ui/src/hooks/useSyncTargetAddress.ts

@@ -1,8 +1,7 @@
 import {
-  CHAIN_ID_ETH,
+  canonicalAddress,
   CHAIN_ID_SOLANA,
   CHAIN_ID_TERRA,
-  canonicalAddress,
   uint8ArrayToHex,
 } from "@certusone/wormhole-sdk";
 import { arrayify, zeroPad } from "@ethersproject/bytes";
@@ -17,6 +16,7 @@ import { useEffect } from "react";
 import { useDispatch, useSelector } from "react-redux";
 import { useEthereumProvider } from "../contexts/EthereumProviderContext";
 import { useSolanaWallet } from "../contexts/SolanaWalletContext";
+import { setTargetAddressHex as setNFTTargetAddressHex } from "../store/nftSlice";
 import {
   selectNFTTargetAsset,
   selectNFTTargetChain,
@@ -24,8 +24,8 @@ import {
   selectTransferTargetChain,
   selectTransferTargetParsedTokenAccount,
 } from "../store/selectors";
-import { setTargetAddressHex as setNFTTargetAddressHex } from "../store/nftSlice";
 import { setTargetAddressHex as setTransferTargetAddressHex } from "../store/transferSlice";
+import { isEVMChain } from "../utils/ethereum";
 
 function useSyncTargetAddress(shouldFire: boolean, nft?: boolean) {
   const dispatch = useDispatch();
@@ -49,7 +49,7 @@ function useSyncTargetAddress(shouldFire: boolean, nft?: boolean) {
   useEffect(() => {
     if (shouldFire) {
       let cancelled = false;
-      if (targetChain === CHAIN_ID_ETH && signerAddress) {
+      if (isEVMChain(targetChain) && signerAddress) {
         dispatch(
           setTargetAddressHex(
             uint8ArrayToHex(zeroPad(arrayify(signerAddress), 32))

+ 8 - 5
bridge_ui/src/hooks/useTransactionFees.tsx

@@ -11,7 +11,7 @@ import { LocalGasStation } from "@material-ui/icons";
 import { Connection, PublicKey } from "@solana/web3.js";
 import { useCallback, useEffect, useMemo, useState } from "react";
 import { useEthereumProvider } from "../contexts/EthereumProviderContext";
-import { SOLANA_HOST } from "../utils/consts";
+import { getDefaultNativeCurrencySymbol, SOLANA_HOST } from "../utils/consts";
 import { getMultipleAccountsRPC } from "../utils/solana";
 import useIsWalletReady from "./useIsWalletReady";
 
@@ -142,9 +142,9 @@ export default function useTransactionFees(chainId: ChainId) {
   return results;
 }
 
-export function useEthereumGasPrice(contract: MethodType) {
+export function useEthereumGasPrice(contract: MethodType, chainId: ChainId) {
   const { provider } = useEthereumProvider();
-  const { isReady } = useIsWalletReady(CHAIN_ID_ETH);
+  const { isReady } = useIsWalletReady(chainId);
   const [estimateResults, setEstimateResults] = useState<GasEstimate | null>(
     null
   );
@@ -168,10 +168,12 @@ export function useEthereumGasPrice(contract: MethodType) {
 
 export function EthGasEstimateSummary({
   methodType,
+  chainId,
 }: {
   methodType: MethodType;
+  chainId: ChainId;
 }) {
-  const estimate = useEthereumGasPrice(methodType);
+  const estimate = useEthereumGasPrice(methodType, chainId);
   if (!estimate) {
     return null;
   }
@@ -192,7 +194,8 @@ export function EthGasEstimateSummary({
       </div>
       <div>&nbsp;&nbsp;&nbsp;</div>
       <div>
-        Est. Fees: {estimate.lowEstimate} - {estimate.highEstimate} ETH
+        Est. Fees: {estimate.lowEstimate} - {estimate.highEstimate}{" "}
+        {getDefaultNativeCurrencySymbol(chainId)}
       </div>
     </Typography>
   );

+ 12 - 0
bridge_ui/src/icons/bsc.svg

@@ -0,0 +1,12 @@
+<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M20.3025 0L9.67897 6.12683L13.5847 8.39024L20.3025 4.52683L27.0204 8.39024L30.9261 6.12683L20.3025 0Z" fill="#F0B90B"/>
+<path d="M27.0204 11.5902L30.9261 13.8537V18.3805L24.2083 22.2439V29.9707L20.3025 32.2341L16.3968 29.9707V22.2439L9.67897 18.3805V13.8537L13.5847 11.5902L20.3025 15.4537L27.0204 11.5902Z" fill="#F0B90B"/>
+<path d="M30.9261 21.5805V26.1073L27.0204 28.3707V23.8439L30.9261 21.5805Z" fill="#F0B90B"/>
+<path d="M26.9814 31.5707L33.6992 27.7073V19.9805L37.605 17.7171V29.9707L26.9814 36.0976V31.5707Z" fill="#F0B90B"/>
+<path d="M33.6992 12.2537L29.7935 9.99025L33.6992 7.72683L37.605 9.99025V14.5171L33.6992 16.7805V12.2537Z" fill="#F0B90B"/>
+<path d="M16.3968 37.7366V33.2098L20.3025 35.4732L24.2083 33.2098V37.7366L20.3025 40L16.3968 37.7366Z" fill="#F0B90B"/>
+<path d="M13.5847 28.3707L9.67897 26.1073V21.5805L13.5847 23.8439V28.3707Z" fill="#F0B90B"/>
+<path d="M20.3025 12.2537L16.3968 9.99025L20.3025 7.72683L24.2083 9.99025L20.3025 12.2537Z" fill="#F0B90B"/>
+<path d="M10.8116 9.99025L6.90586 12.2537V16.7805L3.00012 14.5171V9.99025L6.90586 7.72683L10.8116 9.99025Z" fill="#F0B90B"/>
+<path d="M3.00012 17.7171L6.90586 19.9805V27.7073L13.6237 31.5707V36.0976L3.00012 29.9707V17.7171Z" fill="#F0B90B"/>
+</svg>

+ 3 - 2
bridge_ui/src/store/selectors.ts

@@ -1,7 +1,8 @@
-import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
+import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
 import { ethers } from "ethers";
 import { parseUnits } from "ethers/lib/utils";
 import { RootState } from ".";
+import { isEVMChain } from "../utils/ethereum";
 
 /*
  * Attest
@@ -259,7 +260,7 @@ export const selectTransferTargetError = (state: RootState) => {
     return UNREGISTERED_ERROR_MESSAGE;
   }
   if (
-    state.transfer.targetChain === CHAIN_ID_ETH &&
+    isEVMChain(state.transfer.targetChain) &&
     state.transfer.targetAsset === ethers.constants.AddressZero
   ) {
     return UNREGISTERED_ERROR_MESSAGE;

+ 106 - 7
bridge_ui/src/utils/consts.ts

@@ -64,11 +64,33 @@ export const CHAINS =
           name: "Terra",
         },
       ];
+export const CHAINS_WITH_NFT_SUPPORT = CHAINS.filter(
+  ({ id }) =>
+    id === CHAIN_ID_ETH || id === CHAIN_ID_BSC || id === CHAIN_ID_SOLANA
+);
 export type ChainsById = { [key in ChainId]: ChainInfo };
 export const CHAINS_BY_ID: ChainsById = CHAINS.reduce((obj, chain) => {
   obj[chain.id] = chain;
   return obj;
 }, {} as ChainsById);
+export const getDefaultNativeCurrencySymbol = (chainId: ChainId) =>
+  chainId === CHAIN_ID_SOLANA
+    ? "SOL"
+    : chainId === CHAIN_ID_ETH
+    ? "ETH"
+    : chainId === CHAIN_ID_BSC
+    ? "BNB"
+    : chainId === CHAIN_ID_TERRA
+    ? "LUNA"
+    : "";
+export const getExplorerName = (chainId: ChainId) =>
+  chainId === CHAIN_ID_ETH
+    ? "Etherscan"
+    : chainId === CHAIN_ID_BSC
+    ? "BscScan"
+    : chainId === CHAIN_ID_TERRA
+    ? "Finder"
+    : "Explorer";
 export const WORMHOLE_RPC_HOSTS =
   CLUSTER === "mainnet"
     ? [
@@ -81,11 +103,19 @@ export const WORMHOLE_RPC_HOSTS =
     ? [
         "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.chainlayer.network",
       ]
     : ["http://localhost:7071"];
 export const ETH_NETWORK_CHAIN_ID =
   CLUSTER === "mainnet" ? 1 : CLUSTER === "testnet" ? 5 : 1337;
+export const BSC_NETWORK_CHAIN_ID =
+  CLUSTER === "mainnet" ? 56 : CLUSTER === "testnet" ? 97 : 1397;
+export const getEvmChainId = (chainId: ChainId) =>
+  chainId === CHAIN_ID_ETH
+    ? ETH_NETWORK_CHAIN_ID
+    : chainId === CHAIN_ID_BSC
+    ? BSC_NETWORK_CHAIN_ID
+    : undefined;
 export const SOLANA_HOST = process.env.REACT_APP_SOLANA_API_URL
   ? process.env.REACT_APP_SOLANA_API_URL
   : CLUSTER === "mainnet"
@@ -127,6 +157,27 @@ export const ETH_TOKEN_BRIDGE_ADDRESS = getAddress(
     ? "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 SOL_BRIDGE_ADDRESS =
   CLUSTER === "mainnet"
     ? "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth"
@@ -151,24 +202,64 @@ export const SOL_CUSTODY_ADDRESS =
 export const TERRA_TEST_TOKEN_ADDRESS =
   "terra13nkgqrfymug724h8pprpexqj9h629sa3ncw7sh";
 export const TERRA_BRIDGE_ADDRESS =
-  "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5";
+CLUSTER === "mainnet"
+    ? "terra1dq03ugtd40zu9hcgdzrsq6z2z4hwhc9tqk2uy5"
+    : CLUSTER === "testnet"
+    ? "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5"
+    : "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5";
 export const TERRA_TOKEN_BRIDGE_ADDRESS =
-  "terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4";
+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
+    : "";
+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
+    : "";
+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
+    : "";
 
 export const COVALENT_API_KEY = process.env.REACT_APP_COVALENT_API_KEY
   ? process.env.REACT_APP_COVALENT_API_KEY
   : "";
 
 export const COVALENT_ETHEREUM_MAINNET = "1";
+export const COVALENT_BSC_MAINNET = "56";
 export const COVALENT_GET_TOKENS_URL = (
   chainId: ChainId,
   walletAddress: string,
   nft?: boolean
 ) => {
-  let chainNum = "";
-  if (chainId === CHAIN_ID_ETH) {
-    chainNum = COVALENT_ETHEREUM_MAINNET;
-  }
+  const chainNum =
+    chainId === CHAIN_ID_ETH
+      ? COVALENT_ETHEREUM_MAINNET
+      : chainId === CHAIN_ID_BSC
+      ? COVALENT_BSC_MAINNET
+      : "";
   // https://www.covalenthq.com/docs/api/#get-/v1/{chain_id}/address/{address}/balances_v2/
   return `https://api.covalenthq.com/v1/${chainNum}/address/${walletAddress}/balances_v2/?key=${COVALENT_API_KEY}${
     nft ? "&nft=true" : ""
@@ -183,6 +274,14 @@ export const WETH_ADDRESS =
     : "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E";
 export const WETH_DECIMALS = 18;
 
+export const WBNB_ADDRESS =
+  CLUSTER === "mainnet"
+    ? "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"
+    : CLUSTER === "testnet"
+    ? ""
+    : "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E";
+export const WBNB_DECIMALS = 18;
+
 export const WORMHOLE_V1_ETH_ADDRESS =
   CLUSTER === "mainnet"
     ? "0xf92cD566Ea4864356C5491c177A430C222d7e678"

+ 6 - 0
bridge_ui/src/utils/ethereum.ts

@@ -1,4 +1,7 @@
 import {
+  ChainId,
+  CHAIN_ID_BSC,
+  CHAIN_ID_ETH,
   NFTImplementation,
   NFTImplementation__factory,
   TokenImplementation,
@@ -11,6 +14,9 @@ import {
   createParsedTokenAccount,
 } from "../hooks/useGetSourceParsedTokenAccounts";
 
+export const isEVMChain = (chainId: ChainId) =>
+  chainId === CHAIN_ID_ETH || chainId === CHAIN_ID_BSC;
+
 //This is a valuable intermediate step to the parsed token account, as the token has metadata information on it.
 export async function getEthereumToken(
   tokenAddress: string,

+ 1 - 1
devnet/eth-devnet.yaml

@@ -106,7 +106,7 @@ spec:
             - --deterministic
             - --time="1970-01-01T00:00:00+00:00"
             - --host=0.0.0.0
-            - -i 1337
+            - --chainId=1397
           ports:
             - containerPort: 8545
               name: rpc

+ 4 - 3
sdk/js/src/nft_bridge/getOriginalAsset.ts

@@ -2,7 +2,7 @@ import { Connection, PublicKey } from "@solana/web3.js";
 import { BigNumber, ethers } from "ethers";
 import { arrayify, zeroPad } from "ethers/lib/utils";
 import { TokenImplementation__factory } from "../ethers-contracts";
-import { ChainId, CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../utils";
+import { ChainId, CHAIN_ID_SOLANA } from "../utils";
 import { getIsWrappedAssetEth } from "./getIsWrappedAsset";
 
 export interface WormholeWrappedNFTInfo {
@@ -23,7 +23,8 @@ export async function getOriginalAssetEth(
   tokenBridgeAddress: string,
   provider: ethers.providers.Web3Provider,
   wrappedAddress: string,
-  tokenId: string
+  tokenId: string,
+  lookupChainId: ChainId
 ): Promise<WormholeWrappedNFTInfo> {
   const isWrapped = await getIsWrappedAssetEth(
     tokenBridgeAddress,
@@ -49,7 +50,7 @@ export async function getOriginalAssetEth(
   }
   return {
     isWrapped: false,
-    chainId: CHAIN_ID_ETH,
+    chainId: lookupChainId,
     assetAddress: zeroPad(arrayify(wrappedAddress), 32),
     tokenId,
   };

+ 6 - 10
sdk/js/src/token_bridge/getOriginalAsset.ts

@@ -1,16 +1,11 @@
 import { Connection, PublicKey } from "@solana/web3.js";
+import { LCDClient } from "@terra-money/terra.js";
 import { ethers } from "ethers";
 import { arrayify, zeroPad } from "ethers/lib/utils";
 import { TokenImplementation__factory } from "../ethers-contracts";
-import {
-  ChainId,
-  CHAIN_ID_ETH,
-  CHAIN_ID_SOLANA,
-  CHAIN_ID_TERRA,
-} from "../utils";
-import { getIsWrappedAssetEth } from "./getIsWrappedAsset";
-import { LCDClient } from "@terra-money/terra.js";
 import { buildNativeId, canonicalAddress, isNativeDenom } from "../terra";
+import { ChainId, CHAIN_ID_SOLANA, CHAIN_ID_TERRA } from "../utils";
+import { getIsWrappedAssetEth } from "./getIsWrappedAsset";
 
 export interface WormholeWrappedInfo {
   isWrapped: boolean;
@@ -28,7 +23,8 @@ export interface WormholeWrappedInfo {
 export async function getOriginalAssetEth(
   tokenBridgeAddress: string,
   provider: ethers.providers.Web3Provider,
-  wrappedAddress: string
+  wrappedAddress: string,
+  lookupChainId: ChainId
 ): Promise<WormholeWrappedInfo> {
   const isWrapped = await getIsWrappedAssetEth(
     tokenBridgeAddress,
@@ -50,7 +46,7 @@ export async function getOriginalAssetEth(
   }
   return {
     isWrapped: false,
-    chainId: CHAIN_ID_ETH,
+    chainId: lookupChainId,
     assetAddress: zeroPad(arrayify(wrappedAddress), 32),
   };
 }

+ 2 - 1
sdk/js/src/utils/array.ts

@@ -1,5 +1,6 @@
 import {
   ChainId,
+  CHAIN_ID_BSC,
   CHAIN_ID_ETH,
   CHAIN_ID_SOLANA,
   CHAIN_ID_TERRA,
@@ -21,7 +22,7 @@ export const hexToNativeString = (h: string | undefined, c: ChainId) => {
       ? undefined
       : c === CHAIN_ID_SOLANA
       ? new PublicKey(hexToUint8Array(h)).toString()
-      : c === CHAIN_ID_ETH
+      : c === CHAIN_ID_ETH || c === CHAIN_ID_BSC
       ? hexZeroPad(hexValue(hexToUint8Array(h)), 20)
       : c === CHAIN_ID_TERRA
       ? isHexNativeTerra(h)