Эх сурвалжийг харах

bridge_ui: set target address in state

Change-Id: Ie2f87582ffdc8da53ccc0f34721c3985d7807933
Evan Gray 4 жил өмнө
parent
commit
c33f3c0cb9

+ 23 - 6
bridge_ui/package-lock.json

@@ -29,6 +29,7 @@
         "@solana/wallet-base": "^0.0.1",
         "@solana/web3.js": "^1.22.0",
         "@terra-money/wallet-provider": "^1.4.0-alpha.1",
+        "bech32": "^1.1.4",
         "ethers": "^5.4.1",
         "js-base64": "^3.6.1",
         "notistack": "^1.0.10",
@@ -57,6 +58,9 @@
         "@improbable-eng/grpc-web": "^0.14.0",
         "@solana/spl-token": "^0.1.8",
         "@solana/web3.js": "^1.24.0",
+        "@terra-money/terra.js": "^1.8.10",
+        "@terra-money/wallet-provider": "^1.2.4",
+        "js-base64": "^3.6.1",
         "protobufjs": "^6.11.2",
         "rxjs": "^7.3.0"
       },
@@ -65,6 +69,7 @@
         "@typechain/ethers-v5": "^7.0.1",
         "@types/long": "^4.0.1",
         "@types/node": "^16.6.1",
+        "@types/react": "^17.0.19",
         "copy-dir": "^1.3.0",
         "ethers": "^5.4.4",
         "prettier": "^2.3.2",
@@ -36739,6 +36744,7 @@
       "resolved": "https://registry.npmjs.org/wasm-dce/-/wasm-dce-1.0.2.tgz",
       "integrity": "sha512-Fq1+nu43ybsjSnBquLrW/cULmKs61qbv9k8ep13QUe0nABBezMoNAA+j6QY66MW0/eoDVDp1rjXDqQ2VKyS/Xg==",
       "dev": true,
+      "peer": true,
       "dependencies": {
         "@babel/core": "^7.0.0-beta.39",
         "@babel/traverse": "^7.0.0-beta.39",
@@ -36752,6 +36758,7 @@
       "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.47.tgz",
       "integrity": "sha512-+rq2cr4GDhtToEzKFD6KZZMDBXhjFAr9JjPw9pAppZACeEWqNM294j+NdBzkSHYXwzzBmVjZ3nEVJlOhbR2gOQ==",
       "dev": true,
+      "peer": true,
       "bin": {
         "babylon": "bin/babylon.js"
       },
@@ -37587,6 +37594,7 @@
       "resolved": "https://registry.npmjs.org/webassembly-floating-point-hex-parser/-/webassembly-floating-point-hex-parser-0.1.2.tgz",
       "integrity": "sha512-TUf1H++8U10+stJbFydnvrpG5Sznz5Rilez/oZlV5zI0C/e4cSxd8rALAJ8VpTvjVWxLmL3SVSJUK6Ap9AoiNg==",
       "dev": true,
+      "peer": true,
       "engines": {
         "node": "*"
       }
@@ -37596,6 +37604,7 @@
       "resolved": "https://registry.npmjs.org/webassembly-interpreter/-/webassembly-interpreter-0.0.30.tgz",
       "integrity": "sha512-+Jdy2piEvz9T5j751mOE8+rBO12p+nNW6Fg4kJZ+zP1oUfsm+151sbAbM8AFxWTURmWCGP+r8Lxwfv3pzN1bCQ==",
       "dev": true,
+      "peer": true,
       "dependencies": {
         "@babel/code-frame": "^7.0.0-beta.36",
         "long": "^3.2.0",
@@ -37615,6 +37624,7 @@
       "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz",
       "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=",
       "dev": true,
+      "peer": true,
       "engines": {
         "node": ">=0.6"
       }
@@ -41022,11 +41032,15 @@
         "@openzeppelin/contracts": "^4.2.0",
         "@solana/spl-token": "^0.1.8",
         "@solana/web3.js": "^1.24.0",
+        "@terra-money/terra.js": "^1.8.10",
+        "@terra-money/wallet-provider": "^1.2.4",
         "@typechain/ethers-v5": "^7.0.1",
         "@types/long": "^4.0.1",
         "@types/node": "^16.6.1",
+        "@types/react": "^17.0.19",
         "copy-dir": "^1.3.0",
         "ethers": "^5.4.4",
+        "js-base64": "^3.6.1",
         "prettier": "^2.3.2",
         "protobufjs": "^6.11.2",
         "rxjs": "^7.3.0",
@@ -48382,7 +48396,6 @@
       "dev": true,
       "optional": true,
       "requires": {
-        "bitcore-lib": "^8.25.10",
         "unorm": "^1.4.1"
       }
     },
@@ -69246,6 +69259,7 @@
       "resolved": "https://registry.npmjs.org/wasm-dce/-/wasm-dce-1.0.2.tgz",
       "integrity": "sha512-Fq1+nu43ybsjSnBquLrW/cULmKs61qbv9k8ep13QUe0nABBezMoNAA+j6QY66MW0/eoDVDp1rjXDqQ2VKyS/Xg==",
       "dev": true,
+      "peer": true,
       "requires": {
         "@babel/core": "^7.0.0-beta.39",
         "@babel/traverse": "^7.0.0-beta.39",
@@ -69258,7 +69272,8 @@
           "version": "7.0.0-beta.47",
           "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.47.tgz",
           "integrity": "sha512-+rq2cr4GDhtToEzKFD6KZZMDBXhjFAr9JjPw9pAppZACeEWqNM294j+NdBzkSHYXwzzBmVjZ3nEVJlOhbR2gOQ==",
-          "dev": true
+          "dev": true,
+          "peer": true
         }
       }
     },
@@ -69268,8 +69283,7 @@
       "integrity": "sha512-R4s75XH+o8qM+WaRrAU9S2rbAMDzob18/S3V8R9ZoFpZkPWLAohWWlzWAp1ybeTkOuuku/X1zJtxiV0pBYxZww==",
       "dev": true,
       "requires": {
-        "loader-utils": "^1.1.0",
-        "wasm-dce": "^1.0.0"
+        "loader-utils": "^1.1.0"
       },
       "dependencies": {
         "json5": {
@@ -69991,13 +70005,15 @@
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/webassembly-floating-point-hex-parser/-/webassembly-floating-point-hex-parser-0.1.2.tgz",
       "integrity": "sha512-TUf1H++8U10+stJbFydnvrpG5Sznz5Rilez/oZlV5zI0C/e4cSxd8rALAJ8VpTvjVWxLmL3SVSJUK6Ap9AoiNg==",
-      "dev": true
+      "dev": true,
+      "peer": true
     },
     "webassembly-interpreter": {
       "version": "0.0.30",
       "resolved": "https://registry.npmjs.org/webassembly-interpreter/-/webassembly-interpreter-0.0.30.tgz",
       "integrity": "sha512-+Jdy2piEvz9T5j751mOE8+rBO12p+nNW6Fg4kJZ+zP1oUfsm+151sbAbM8AFxWTURmWCGP+r8Lxwfv3pzN1bCQ==",
       "dev": true,
+      "peer": true,
       "requires": {
         "@babel/code-frame": "^7.0.0-beta.36",
         "long": "^3.2.0",
@@ -70008,7 +70024,8 @@
           "version": "3.2.0",
           "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz",
           "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=",
-          "dev": true
+          "dev": true,
+          "peer": true
         }
       }
     },

+ 1 - 0
bridge_ui/package.json

@@ -23,6 +23,7 @@
     "@solana/wallet-base": "^0.0.1",
     "@solana/web3.js": "^1.22.0",
     "@terra-money/wallet-provider": "^1.4.0-alpha.1",
+    "bech32": "^1.1.4",
     "ethers": "^5.4.1",
     "js-base64": "^3.6.1",
     "notistack": "^1.0.10",

+ 1 - 1
bridge_ui/src/components/Attest/Source.tsx

@@ -63,7 +63,7 @@ function Source() {
       </TextField>
       <KeyAndBalance chainId={sourceChain} />
       <TextField
-        placeholder="Asset"
+        label="Asset"
         fullWidth
         className={classes.transferField}
         value={sourceAsset}

+ 2 - 2
bridge_ui/src/components/Transfer/Source.tsx

@@ -75,7 +75,7 @@ function Source() {
       <KeyAndBalance chainId={sourceChain} balance={uiAmountString} />
       {/* TODO: token list for eth, check own */}
       <TextField
-        placeholder="Asset"
+        label="Asset"
         fullWidth
         className={classes.transferField}
         value={sourceAsset}
@@ -83,7 +83,7 @@ function Source() {
         disabled={shouldLockFields}
       />
       <TextField
-        placeholder="Amount"
+        label="Amount"
         type="number"
         fullWidth
         className={classes.transferField}

+ 12 - 1
bridge_ui/src/components/Transfer/Target.tsx

@@ -3,11 +3,13 @@ import { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
 import { PublicKey } from "@solana/web3.js";
 import { useCallback, useMemo } from "react";
 import { useDispatch, useSelector } from "react-redux";
+import useSyncTargetAddress from "../../hooks/useSyncTargetAddress";
 import {
   selectTransferIsSourceAssetWormholeWrapped,
   selectTransferIsTargetComplete,
   selectTransferShouldLockFields,
   selectTransferSourceChain,
+  selectTransferTargetAddressHex,
   selectTransferTargetAsset,
   selectTransferTargetBalanceString,
   selectTransferTargetChain,
@@ -32,6 +34,7 @@ function Target() {
     [sourceChain]
   );
   const targetChain = useSelector(selectTransferTargetChain);
+  const targetAddressHex = useSelector(selectTransferTargetAddressHex); // TODO: make readable
   const targetAsset = useSelector(selectTransferTargetAsset);
   const isSourceAssetWormholeWrapped = useSelector(
     selectTransferIsSourceAssetWormholeWrapped
@@ -47,6 +50,7 @@ function Target() {
   const uiAmountString = useSelector(selectTransferTargetBalanceString);
   const isTargetComplete = useSelector(selectTransferIsTargetComplete);
   const shouldLockFields = useSelector(selectTransferShouldLockFields);
+  useSyncTargetAddress(!shouldLockFields);
   const handleTargetChange = useCallback(
     (event) => {
       dispatch(setTargetChain(event.target.value));
@@ -76,7 +80,14 @@ function Target() {
       </TextField>
       <KeyAndBalance chainId={targetChain} balance={uiAmountString} />
       <TextField
-        placeholder="Asset"
+        label="Address"
+        fullWidth
+        className={classes.transferField}
+        value={targetAddressHex || ""}
+        disabled={true}
+      />
+      <TextField
+        label="Asset"
         fullWidth
         className={classes.transferField}
         value={readableTargetAsset}

+ 19 - 56
bridge_ui/src/hooks/useHandleTransfer.ts

@@ -11,22 +11,17 @@ import {
   transferFromEth,
   transferFromSolana,
 } from "@certusone/wormhole-sdk";
-import {
-  ASSOCIATED_TOKEN_PROGRAM_ID,
-  Token,
-  TOKEN_PROGRAM_ID,
-} from "@solana/spl-token";
 import { WalletContextState } from "@solana/wallet-adapter-react";
-import { Connection, PublicKey } from "@solana/web3.js";
+import { Connection } from "@solana/web3.js";
 import { MsgExecuteContract } from "@terra-money/terra.js";
 import {
   ConnectedWallet,
   useConnectedWallet,
 } from "@terra-money/wallet-provider";
 import { Signer } from "ethers";
-import { arrayify, parseUnits, zeroPad } from "ethers/lib/utils";
+import { parseUnits, zeroPad } from "ethers/lib/utils";
 import { useSnackbar } from "notistack";
-import { useCallback, useEffect, useMemo, useRef } from "react";
+import { useCallback, useMemo } from "react";
 import { useDispatch, useSelector } from "react-redux";
 import { useEthereumProvider } from "../contexts/EthereumProviderContext";
 import { useSolanaWallet } from "../contexts/SolanaWalletContext";
@@ -40,9 +35,7 @@ import {
   selectTransferSourceAsset,
   selectTransferSourceChain,
   selectTransferSourceParsedTokenAccount,
-  selectTransferTargetAsset,
   selectTransferTargetChain,
-  selectTransferTargetParsedTokenAccount,
 } from "../store/selectors";
 import { setIsSending, setSignedVAAHex } from "../store/transferSlice";
 import { hexToUint8Array, uint8ArrayToHex } from "../utils/array";
@@ -56,6 +49,7 @@ import {
 } from "../utils/consts";
 import { getSignedVAAWithRetry } from "../utils/getSignedVAAWithRetry";
 import { signSendAndConfirm } from "../utils/solana";
+import useTransferTargetAddressHex from "./useTransferTargetAddress";
 
 async function eth(
   dispatch: any,
@@ -104,8 +98,8 @@ async function solana(
   mintAddress: string,
   amount: string,
   decimals: number,
-  targetAddressStr: string,
   targetChain: ChainId,
+  targetAddress: Uint8Array,
   originAddressStr?: string,
   originChain?: ChainId
 ) {
@@ -114,7 +108,6 @@ async function solana(
     //TODO: check if token attestation exists on the target chain
     // TODO: share connection in context?
     const connection = new Connection(SOLANA_HOST, "confirmed");
-    const targetAddress = zeroPad(arrayify(targetAddressStr), 32);
     const amountParsed = parseUnits(amount, decimals).toBigInt();
     const originAddress = originAddressStr
       ? zeroPad(hexToUint8Array(originAddressStr), 32)
@@ -164,8 +157,8 @@ async function terra(
   wallet: ConnectedWallet,
   asset: string,
   amount: string,
-  targetAddressStr: string,
-  targetChain: ChainId
+  targetChain: ChainId,
+  targetAddress: Uint8Array
 ) {
   dispatch(setIsSending(true));
   try {
@@ -180,7 +173,7 @@ async function terra(
               asset: asset,
               amount: amount,
               recipient_chain: targetChain,
-              recipient: targetAddressStr,
+              recipient: targetAddress,
               fee: 1000,
               nonce: 0,
             },
@@ -221,11 +214,11 @@ export function useHandleTransfer() {
   const originAsset = useSelector(selectTransferOriginAsset);
   const amount = useSelector(selectTransferAmount);
   const targetChain = useSelector(selectTransferTargetChain);
-  const targetAsset = useSelector(selectTransferTargetAsset);
+  const targetAddress = useTransferTargetAddressHex();
   const isTargetComplete = useSelector(selectTransferIsTargetComplete);
   const isSending = useSelector(selectTransferIsSending);
   const isSendComplete = useSelector(selectTransferIsSendComplete);
-  const { signer, signerAddress } = useEthereumProvider();
+  const { signer } = useEthereumProvider();
   const solanaWallet = useSolanaWallet();
   const solPK = solanaWallet?.publicKey;
   const terraWallet = useConnectedWallet();
@@ -234,37 +227,7 @@ export function useHandleTransfer() {
   );
   const sourceTokenPublicKey = sourceParsedTokenAccount?.publicKey;
   const decimals = sourceParsedTokenAccount?.decimals;
-  const targetParsedTokenAccount = useSelector(
-    selectTransferTargetParsedTokenAccount
-  );
   const disabled = !isTargetComplete || isSending || isSendComplete;
-  // TODO: we probably shouldn't get here if we don't have this public key
-  // TODO: also this is just for solana... send help(ers)
-  const targetTokenAccountPublicKey = targetParsedTokenAccount?.publicKey;
-  // TODO: AVOID THIS DANGEROUS CACOPHONY
-  const tpkRef = useRef<undefined | Uint8Array>(undefined);
-  useEffect(() => {
-    (async () => {
-      if (targetChain === CHAIN_ID_SOLANA) {
-        tpkRef.current = targetTokenAccountPublicKey
-          ? zeroPad(new PublicKey(targetTokenAccountPublicKey).toBytes(), 32) // use the target's TokenAccount if it exists
-          : solPK && targetAsset // otherwise, use the associated token account (which we create in the case it doesn't exist)
-          ? zeroPad(
-              (
-                await Token.getAssociatedTokenAddress(
-                  ASSOCIATED_TOKEN_PROGRAM_ID,
-                  TOKEN_PROGRAM_ID,
-                  new PublicKey(targetAsset),
-                  solPK
-                )
-              ).toBytes(),
-              32
-            )
-          : undefined;
-      } else tpkRef.current = undefined;
-    })();
-  }, [targetChain, solPK, targetAsset, targetTokenAccountPublicKey]);
-  // TODO: dynamically get "to" wallet
   const handleTransferClick = useCallback(() => {
     // TODO: we should separate state for transaction vs fetching vaa
     // TODO: more generic way of calling these
@@ -272,7 +235,7 @@ export function useHandleTransfer() {
       sourceChain === CHAIN_ID_ETH &&
       !!signer &&
       decimals !== undefined &&
-      !!tpkRef.current
+      !!targetAddress
     ) {
       eth(
         dispatch,
@@ -282,14 +245,14 @@ export function useHandleTransfer() {
         decimals,
         amount,
         targetChain,
-        tpkRef.current
+        targetAddress
       );
     } else if (
       sourceChain === CHAIN_ID_SOLANA &&
       !!solanaWallet &&
       !!solPK &&
       !!sourceTokenPublicKey &&
-      !!signerAddress &&
+      !!targetAddress &&
       decimals !== undefined
     ) {
       solana(
@@ -299,10 +262,10 @@ export function useHandleTransfer() {
         solPK.toString(),
         sourceTokenPublicKey,
         sourceAsset,
-        amount, //TODO: avoid decimals, pass in parsed amount
+        amount,
         decimals,
-        signerAddress,
         targetChain,
+        targetAddress,
         originAsset,
         originChain
       );
@@ -310,7 +273,7 @@ export function useHandleTransfer() {
       sourceChain === CHAIN_ID_TERRA &&
       !!terraWallet &&
       decimals !== undefined &&
-      !!signerAddress
+      !!targetAddress
     ) {
       terra(
         dispatch,
@@ -318,8 +281,8 @@ export function useHandleTransfer() {
         terraWallet,
         sourceAsset,
         amount,
-        signerAddress, // TODO: only works for Eth
-        targetChain
+        targetChain,
+        targetAddress
       );
     } else {
       // enqueueSnackbar("Transfers from this chain are not yet supported", {
@@ -331,7 +294,6 @@ export function useHandleTransfer() {
     enqueueSnackbar,
     sourceChain,
     signer,
-    signerAddress,
     solanaWallet,
     solPK,
     terraWallet,
@@ -340,6 +302,7 @@ export function useHandleTransfer() {
     amount,
     decimals,
     targetChain,
+    targetAddress,
     originAsset,
     originChain,
   ]);

+ 110 - 0
bridge_ui/src/hooks/useSyncTargetAddress.ts

@@ -0,0 +1,110 @@
+import {
+  CHAIN_ID_ETH,
+  CHAIN_ID_SOLANA,
+  CHAIN_ID_TERRA,
+} from "@certusone/wormhole-sdk";
+import { arrayify, zeroPad } from "@ethersproject/bytes";
+import {
+  ASSOCIATED_TOKEN_PROGRAM_ID,
+  Token,
+  TOKEN_PROGRAM_ID,
+} from "@solana/spl-token";
+import { PublicKey } from "@solana/web3.js";
+import { useConnectedWallet } from "@terra-money/wallet-provider";
+import { useEffect } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { useEthereumProvider } from "../contexts/EthereumProviderContext";
+import { useSolanaWallet } from "../contexts/SolanaWalletContext";
+import {
+  selectTransferTargetAsset,
+  selectTransferTargetChain,
+  selectTransferTargetParsedTokenAccount,
+} from "../store/selectors";
+import { setTargetAddressHex } from "../store/transferSlice";
+import { uint8ArrayToHex } from "../utils/array";
+import bech32 from "bech32";
+
+function useSyncTargetAddress(shouldFire: boolean) {
+  const dispatch = useDispatch();
+  const targetChain = useSelector(selectTransferTargetChain);
+  const { signerAddress } = useEthereumProvider();
+  const solanaWallet = useSolanaWallet();
+  const solPK = solanaWallet?.publicKey;
+  const targetAsset = useSelector(selectTransferTargetAsset);
+  const targetParsedTokenAccount = useSelector(
+    selectTransferTargetParsedTokenAccount
+  );
+  const targetTokenAccountPublicKey = targetParsedTokenAccount?.publicKey;
+  const terraWallet = useConnectedWallet();
+  useEffect(() => {
+    if (shouldFire) {
+      let cancelled = false;
+      if (targetChain === CHAIN_ID_ETH && signerAddress) {
+        dispatch(
+          setTargetAddressHex(
+            uint8ArrayToHex(zeroPad(arrayify(signerAddress), 32))
+          )
+        );
+      }
+      // TODO: have the user explicitly select an account on solana
+      else if (targetChain === CHAIN_ID_SOLANA && targetTokenAccountPublicKey) {
+        // use the target's TokenAccount if it exists
+        dispatch(
+          setTargetAddressHex(
+            uint8ArrayToHex(
+              zeroPad(new PublicKey(targetTokenAccountPublicKey).toBytes(), 32)
+            )
+          )
+        );
+      } else if (targetChain === CHAIN_ID_SOLANA && solPK && targetAsset) {
+        // otherwise, use the associated token account (which we create in the case it doesn't exist)
+        (async () => {
+          const associatedTokenAccount = await Token.getAssociatedTokenAddress(
+            ASSOCIATED_TOKEN_PROGRAM_ID,
+            TOKEN_PROGRAM_ID,
+            new PublicKey(targetAsset),
+            solPK
+          );
+          if (!cancelled) {
+            dispatch(
+              setTargetAddressHex(
+                uint8ArrayToHex(zeroPad(associatedTokenAccount.toBytes(), 32))
+              )
+            );
+          }
+        })();
+      } else if (
+        targetChain === CHAIN_ID_TERRA &&
+        terraWallet &&
+        terraWallet.walletAddress
+      ) {
+        dispatch(
+          setTargetAddressHex(
+            uint8ArrayToHex(
+              zeroPad(
+                new Uint8Array(bech32.decode(terraWallet.walletAddress).words),
+                32
+              )
+            )
+          )
+        );
+      } else {
+        dispatch(setTargetAddressHex(undefined));
+      }
+      return () => {
+        cancelled = true;
+      };
+    }
+  }, [
+    dispatch,
+    shouldFire,
+    targetChain,
+    signerAddress,
+    solPK,
+    targetAsset,
+    targetTokenAccountPublicKey,
+    terraWallet,
+  ]);
+}
+
+export default useSyncTargetAddress;

+ 13 - 0
bridge_ui/src/hooks/useTransferTargetAddress.ts

@@ -0,0 +1,13 @@
+import { useMemo } from "react";
+import { useSelector } from "react-redux";
+import { selectTransferTargetAddressHex } from "../store/selectors";
+import { hexToUint8Array } from "../utils/array";
+
+export default function useTransferTargetAddressHex() {
+  const targetAddressHex = useSelector(selectTransferTargetAddressHex);
+  const targetAddress = useMemo(
+    () => (targetAddressHex ? hexToUint8Array(targetAddressHex) : undefined),
+    [targetAddressHex]
+  );
+  return targetAddress;
+}

+ 4 - 5
bridge_ui/src/store/selectors.ts

@@ -57,6 +57,8 @@ export const selectTransferSourceBalanceString = (state: RootState) =>
 export const selectTransferAmount = (state: RootState) => state.transfer.amount;
 export const selectTransferTargetChain = (state: RootState) =>
   state.transfer.targetChain;
+export const selectTransferTargetAddressHex = (state: RootState) =>
+  state.transfer.targetAddressHex;
 export const selectTransferTargetAsset = (state: RootState) =>
   state.transfer.targetAsset;
 export const selectTransferTargetParsedTokenAccount = (state: RootState) =>
@@ -96,11 +98,8 @@ export const selectTransferIsTargetComplete = (state: RootState) =>
   !!state.transfer.targetChain &&
   !!state.transfer.targetAsset &&
   (state.transfer.targetChain !== CHAIN_ID_ETH ||
-    state.transfer.targetAsset !== ethers.constants.AddressZero); //&&
-// Associated Token Account exists
-// (state.transfer.targetChain !== CHAIN_ID_SOLANA ||
-//   (!!state.transfer.targetParsedTokenAccount &&
-//     !!state.transfer.targetParsedTokenAccount.publicKey));
+    state.transfer.targetAsset !== ethers.constants.AddressZero) &&
+  !!state.transfer.targetAddressHex;
 export const selectTransferIsSendComplete = (state: RootState) =>
   !!selectTransferSignedVAAHex(state);
 export const selectTransferShouldLockFields = (state: RootState) =>

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

@@ -34,6 +34,7 @@ export interface TransferState {
   sourceParsedTokenAccount: ParsedTokenAccount | undefined;
   amount: string;
   targetChain: ChainId;
+  targetAddressHex: string | undefined;
   targetAsset: string | null | undefined;
   targetParsedTokenAccount: ParsedTokenAccount | undefined;
   signedVAAHex: string | undefined;
@@ -51,6 +52,7 @@ const initialState: TransferState = {
   originAsset: undefined,
   amount: "",
   targetChain: CHAIN_ID_ETH,
+  targetAddressHex: undefined,
   targetAsset: undefined,
   targetParsedTokenAccount: undefined,
   signedVAAHex: undefined,
@@ -86,6 +88,7 @@ export const transferSlice = createSlice({
       }
       if (state.targetChain === action.payload) {
         state.targetChain = prevSourceChain;
+        state.targetAddressHex = undefined;
       }
     },
     setSourceAsset: (state, action: PayloadAction<string>) => {
@@ -117,6 +120,8 @@ export const transferSlice = createSlice({
     setTargetChain: (state, action: PayloadAction<ChainId>) => {
       const prevTargetChain = state.targetChain;
       state.targetChain = action.payload;
+      state.targetAddressHex = undefined;
+      // targetAsset is handled by useFetchTargetAsset
       if (state.sourceChain === action.payload) {
         state.sourceChain = prevTargetChain;
         state.activeStep = 0;
@@ -132,6 +137,9 @@ export const transferSlice = createSlice({
         }
       }
     },
+    setTargetAddressHex: (state, action: PayloadAction<string | undefined>) => {
+      state.targetAddressHex = action.payload;
+    },
     setTargetAsset: (
       state,
       action: PayloadAction<string | null | undefined>
@@ -169,6 +177,7 @@ export const {
   setSourceParsedTokenAccount,
   setAmount,
   setTargetChain,
+  setTargetAddressHex,
   setTargetAsset,
   setTargetParsedTokenAccount,
   setSignedVAAHex,