From 8e251f4acc2a8c0809a2e18808da8e46b7a50835 Mon Sep 17 00:00:00 2001 From: chase-45 Date: Wed, 8 Sep 2021 15:57:35 -0400 Subject: [PATCH] bridge_ui: reset button in picker for tokens fixes https://github.com/certusone/wormhole/issues/362 fixes https://github.com/certusone/wormhole/issues/394 Change-Id: Ib3619ac95e1bfda4b5d1b58840304d867f81305e --- .../EthereumSourceTokenSelector.tsx | 74 +++++--- .../TokenSelectors/RefreshButtonWrapper.tsx | 45 +++++ .../SolanaSourceTokenSelector.tsx | 14 +- .../TokenSelectors/SourceTokenSelector.tsx | 5 +- .../TerraSourceTokenSelector.tsx | 81 ++++++--- .../hooks/useGetSourceParsedTokenAccounts.ts | 170 ++++++++++-------- 6 files changed, 265 insertions(+), 124 deletions(-) create mode 100644 bridge_ui/src/components/TokenSelectors/RefreshButtonWrapper.tsx diff --git a/bridge_ui/src/components/TokenSelectors/EthereumSourceTokenSelector.tsx b/bridge_ui/src/components/TokenSelectors/EthereumSourceTokenSelector.tsx index af7ae996..08de3f39 100644 --- a/bridge_ui/src/components/TokenSelectors/EthereumSourceTokenSelector.tsx +++ b/bridge_ui/src/components/TokenSelectors/EthereumSourceTokenSelector.tsx @@ -1,3 +1,4 @@ +import { WormholeAbi__factory } from "@certusone/wormhole-sdk/lib/ethers-contracts/abi"; import { CircularProgress, createStyles, @@ -11,6 +12,7 @@ import { useEthereumProvider } from "../../contexts/EthereumProviderContext"; import { CovalentData } from "../../hooks/useGetSourceParsedTokenAccounts"; import { DataWrapper } from "../../store/helpers"; import { ParsedTokenAccount } from "../../store/transferSlice"; +import { WORMHOLE_V1_ETH_ADDRESS } from "../../utils/consts"; import { ethNFTToNFTParsedTokenAccount, ethTokenToParsedTokenAccount, @@ -21,11 +23,10 @@ import { } from "../../utils/ethereum"; import { shortenAddress } from "../../utils/solana"; import OffsetButton from "./OffsetButton"; -import { WormholeAbi__factory } from "@certusone/wormhole-sdk/lib/ethers-contracts/abi"; -import { WORMHOLE_V1_ETH_ADDRESS } from "../../utils/consts"; import { NFTParsedTokenAccount } from "../../store/nftSlice"; import NFTViewer from "./NFTViewer"; import { useDebounce } from "use-debounce/lib"; +import RefreshButtonWrapper from "./RefreshButtonWrapper"; const useStyles = makeStyles(() => createStyles({ @@ -56,6 +57,7 @@ type EthereumSourceTokenSelectorProps = { covalent: DataWrapper | undefined; tokenAccounts: DataWrapper | undefined; disabled: boolean; + resetAccounts: (() => void) | undefined; nft?: boolean; }; @@ -116,7 +118,15 @@ const renderNFTAccount = ( export default function EthereumSourceTokenSelector( props: EthereumSourceTokenSelectorProps ) { - const { value, onChange, covalent, tokenAccounts, disabled, nft } = props; + const { + value, + onChange, + covalent, + tokenAccounts, + disabled, + resetAccounts, + nft, + } = props; const classes = useStyles(); const [advancedMode, setAdvancedMode] = useState(false); const [advancedModeLoading, setAdvancedModeLoading] = useState(false); @@ -139,6 +149,14 @@ export default function EthereumSourceTokenSelector( // const wrappedTestToken = "0x8bf3c393b588bb6ad021e154654493496139f06d"; // const notWrappedTestToken = "0xaaaebe6fe48e54f431b0c390cfaf0b017d09d42d"; + const resetAccountWrapper = useCallback(() => { + setAdvancedModeHolderString(""); + setAutocompleteHolder(null); + setAdvancedModeError(""); + setAutocompleteError(""); + resetAccounts && resetAccounts(); + }, [resetAccounts]); + useEffect(() => { //If we receive a push from our parent, usually on component mount, we set our internal value to synchronize //This also kicks off the metadata load. @@ -394,6 +412,8 @@ export default function EthereumSourceTokenSelector( setAdvancedModeHolderString(""); setAdvancedModeError(""); setAdvancedModeSymbol(""); + setAutocompleteHolder(null); + setAutocompleteError(""); setAdvancedMode(!advancedMode); }; @@ -412,7 +432,7 @@ export default function EthereumSourceTokenSelector( autoSelect blurOnSelect clearOnBlur - fullWidth={false} + fullWidth={true} filterOptions={nft ? filterConfigNFT : filterConfig} value={autocompleteHolder} onChange={(event, newValue) => { @@ -454,15 +474,18 @@ export default function EthereumSourceTokenSelector( })`; }} /> - {autocompleteError && ( - {autocompleteError} - )} ); const advancedModeToggleButton = ( - {advancedMode ? "Toggle Token Picker" : "Toggle Override"} + {advancedMode ? "Toggle Token Picker" : "Toggle Manual Entry"} + + ); + + const clearButton = ( + + Clear ); @@ -473,16 +496,12 @@ export default function EthereumSourceTokenSelector( {nft ? ( ) : ( - {(symbol ? symbol + " " : "") + value.mintKey} + + + {(symbol ? symbol + " " : "") + value.mintKey} + + )} - - Clear - - {!advancedMode && autocompleteError ? ( - {autocompleteError} - ) : advancedMode && advancedModeError ? ( - {advancedModeError} - ) : null} ) : advancedMode ? ( <> @@ -496,7 +515,11 @@ export default function EthereumSourceTokenSelector( !isValidEthereumAddress(advancedModeHolderString)) || !!advancedModeError } - helperText={advancedModeError === "" ? undefined : advancedModeError} + helperText={ + advancedModeHolderString && + !isValidEthereumAddress(advancedModeHolderString) && + "Invalid Ethereum address" + } disabled={disabled || advancedModeLoading} /> {nft ? ( @@ -515,13 +538,20 @@ export default function EthereumSourceTokenSelector( {nft ? "Loading (this may take a while)..." : "Loading..."} ) : ( - autoComplete + + {autoComplete} + ); return ( - + <> {content} - {!value && advancedModeToggleButton} - + {!advancedMode && autocompleteError ? ( + {autocompleteError} + ) : advancedMode && advancedModeError ? ( + {advancedModeError} + ) : null} + {value ? clearButton : advancedModeToggleButton} + ); } diff --git a/bridge_ui/src/components/TokenSelectors/RefreshButtonWrapper.tsx b/bridge_ui/src/components/TokenSelectors/RefreshButtonWrapper.tsx new file mode 100644 index 00000000..d527d941 --- /dev/null +++ b/bridge_ui/src/components/TokenSelectors/RefreshButtonWrapper.tsx @@ -0,0 +1,45 @@ +import { + createStyles, + IconButton, + makeStyles, + Tooltip, +} from "@material-ui/core"; +import RefreshIcon from "@material-ui/icons/Refresh"; + +const useStyles = makeStyles(() => + createStyles({ + inlineContentWrapper: { + display: "inline-block", + flexGrow: 1, + }, + flexWrapper: { + "& > *": { + margin: ".5rem", + }, + display: "flex", + }, + }) +); + +export default function RefreshButtonWrapper({ + children, + callback, +}: { + children: JSX.Element; + callback: () => any; +}) { + const classes = useStyles(); + + const refreshWrapper = ( +
+
{children}
+ + + + + +
+ ); + + return refreshWrapper; +} diff --git a/bridge_ui/src/components/TokenSelectors/SolanaSourceTokenSelector.tsx b/bridge_ui/src/components/TokenSelectors/SolanaSourceTokenSelector.tsx index ba82dd8c..3d49bcb0 100644 --- a/bridge_ui/src/components/TokenSelectors/SolanaSourceTokenSelector.tsx +++ b/bridge_ui/src/components/TokenSelectors/SolanaSourceTokenSelector.tsx @@ -9,6 +9,7 @@ import { ParsedTokenAccount } from "../../store/transferSlice"; import { WORMHOLE_V1_MINT_AUTHORITY } from "../../utils/consts"; import { Metadata } from "../../utils/metaplex"; import { shortenAddress } from "../../utils/solana"; +import RefreshButtonWrapper from "./RefreshButtonWrapper"; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -33,15 +34,18 @@ type SolanaSourceTokenSelectorProps = { metaplexData: any; //DataWrapper<(Metadata | undefined)[]> | undefined | null; disabled: boolean; mintAccounts: DataWrapper> | undefined; + resetAccounts: (() => void) | undefined; nft?: boolean; }; export default function SolanaSourceTokenSelector( props: SolanaSourceTokenSelectorProps ) { - const { value, onChange, disabled, nft } = props; + const { value, onChange, disabled, resetAccounts, nft } = props; const classes = useStyles(); + const resetAccountWrapper = resetAccounts || (() => {}); //This should never happen. + const memoizedTokenMap: Map = useMemo(() => { const output = new Map(); @@ -245,9 +249,15 @@ export default function SolanaSourceTokenSelector( /> ); + const wrappedContent = ( + + {autoComplete} + + ); + return ( - {isLoading ? : autoComplete} + {isLoading ? : wrappedContent} {error && {error}} ); diff --git a/bridge_ui/src/components/TokenSelectors/SourceTokenSelector.tsx b/bridge_ui/src/components/TokenSelectors/SourceTokenSelector.tsx index af52b21f..41ff0793 100644 --- a/bridge_ui/src/components/TokenSelectors/SourceTokenSelector.tsx +++ b/bridge_ui/src/components/TokenSelectors/SourceTokenSelector.tsx @@ -91,6 +91,7 @@ export const TokenSelector = (props: TokenSelectorProps) => { solanaTokenMap={maps?.tokenMap} metaplexData={maps?.metaplex} mintAccounts={maps?.mintAccounts} + resetAccounts={maps?.resetAccounts} nft={nft} /> ) : lookupChain === CHAIN_ID_ETH ? ( @@ -99,7 +100,8 @@ export const TokenSelector = (props: TokenSelectorProps) => { disabled={disabled} onChange={handleOnChange} covalent={maps?.covalent || undefined} - tokenAccounts={maps?.tokenAccounts} //TODO standardize + tokenAccounts={maps?.tokenAccounts} + resetAccounts={maps?.resetAccounts} nft={nft} /> ) : lookupChain === CHAIN_ID_TERRA ? ( @@ -108,6 +110,7 @@ export const TokenSelector = (props: TokenSelectorProps) => { disabled={disabled} onChange={handleOnChange} tokenMap={maps?.terraTokenMap} + resetAccounts={maps?.resetAccounts} /> ) : ( createStyles({ @@ -43,6 +45,7 @@ type TerraSourceTokenSelectorProps = { onChange: (newValue: ParsedTokenAccount | null) => void; disabled: boolean; tokenMap: DataWrapper | undefined; //TODO better type + resetAccounts: (() => void) | undefined; }; //TODO move elsewhere @@ -87,12 +90,28 @@ export default function TerraSourceTokenSelector( props: TerraSourceTokenSelectorProps ) { const classes = useStyles(); - const { onChange, value, disabled, tokenMap } = props; + const { onChange, value, disabled, tokenMap, resetAccounts } = props; const [advancedMode, setAdvancedMode] = useState(false); const [advancedModeHolderString, setAdvancedModeHolderString] = useState(""); const [advancedModeError, setAdvancedModeError] = useState(""); const terraWallet = useConnectedWallet(); + const [autocompleteString, setAutocompleteString] = useState(""); + + const handleAutocompleteChange = useCallback( + (event) => { + setAutocompleteString(event?.target?.value); + }, + [setAutocompleteString] + ); + + const resetAccountWrapper = useCallback(() => { + setAdvancedModeHolderString(""); + setAdvancedModeError(""); + setAutocompleteString(""); + resetAccounts && resetAccounts(); + }, [resetAccounts]); + const isLoading = tokenMap?.isFetching || false; const terraTokenArray = useMemo(() => { @@ -102,7 +121,7 @@ export default function TerraSourceTokenSelector( }, [props.tokenMap]); const valueToOption = (fromProps: ParsedTokenAccount | undefined | null) => { - if (!fromProps) return undefined; + if (!fromProps) return null; else { return terraTokenArray.find((x) => x.token === fromProps.mintKey); } @@ -113,7 +132,7 @@ export default function TerraSourceTokenSelector( }, [onChange]); const handleOnChange = useCallback( - (event) => setAdvancedModeHolderString(event.target.value), + (event) => setAdvancedModeHolderString(event?.target?.value), [] ); @@ -128,7 +147,7 @@ export default function TerraSourceTokenSelector( onChange(result); }, (error) => { - setAdvancedModeError("Unable to retrieve this address."); + setAdvancedModeError("Unable to retrieve that address."); } ); setAdvancedModeError(""); @@ -172,10 +191,12 @@ export default function TerraSourceTokenSelector( const advancedModeToggleButton = ( - {advancedMode ? "Toggle Token Picker" : "Toggle Override"} + {advancedMode ? "Toggle Token Picker" : "Toggle Manual Entry"} ); + const selectedValue = valueToOption(value); + const autoComplete = ( <> { handleConfirm(newValue?.token); }} + inputValue={autocompleteString} + onInputChange={handleAutocompleteChange} disabled={disabled} noOptionsText={"No CW20 tokens found at the moment."} options={terraTokenArray} @@ -199,18 +222,18 @@ export default function TerraSourceTokenSelector( renderOption={renderOption} getOptionLabel={renderOptionLabel} /> - {advancedModeError && ( - {advancedModeError} - )} ); + const clearButton = ( + + Clear + + ); + const content = value ? ( <> {value.mintKey} - - Clear - ) : !advancedMode ? ( autoComplete @@ -223,21 +246,37 @@ export default function TerraSourceTokenSelector( onChange={handleOnChange} disabled={disabled} error={advancedModeHolderString !== "" && !!advancedModeError} - helperText={advancedModeError === "" ? undefined : advancedModeError} /> - handleConfirm(advancedModeHolderString)} - disabled={disabled} - > - Confirm - ); + const wrappedContent = ( + + {content} + + ); + + const confirmButton = ( + handleConfirm(advancedModeHolderString)} + disabled={disabled} + > + Confirm + + ); + return ( - {content} - {!value && !isLoading && advancedModeToggleButton} + {isLoading && } + {wrappedContent} + {advancedModeError && ( + {advancedModeError} + )} +
+ {advancedMode && !value && confirmButton} + {!value && !isLoading && advancedModeToggleButton} + {value && clearButton} +
); } diff --git a/bridge_ui/src/hooks/useGetSourceParsedTokenAccounts.ts b/bridge_ui/src/hooks/useGetSourceParsedTokenAccounts.ts index 7ce83230..41db7c26 100644 --- a/bridge_ui/src/hooks/useGetSourceParsedTokenAccounts.ts +++ b/bridge_ui/src/hooks/useGetSourceParsedTokenAccounts.ts @@ -14,7 +14,7 @@ import { } from "@solana/web3.js"; import axios from "axios"; import { formatUnits } from "ethers/lib/utils"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useEthereumProvider } from "../contexts/EthereumProviderContext"; import { useSolanaWallet } from "../contexts/SolanaWalletContext"; @@ -381,6 +381,28 @@ function useGetAvailableTokens(nft: boolean = false) { ? solPK?.toString() : undefined; + const resetSourceAccounts = useCallback(() => { + dispatch( + nft + ? setSourceWalletAddressNFT(undefined) + : setSourceWalletAddress(undefined) + ); + dispatch( + nft + ? setSourceParsedTokenAccountNFT(undefined) + : setSourceParsedTokenAccount(undefined) + ); + dispatch( + nft + ? setSourceParsedTokenAccountsNFT(undefined) + : setSourceParsedTokenAccounts(undefined) + ); + !nft && dispatch(setAmount("")); + setCovalent(undefined); //These need to be included in the reset because they have balances on them. + setCovalentLoading(false); + setCovalentError(""); + }, [setCovalent, dispatch, nft]); + //TODO this useEffect could be somewhere else in the codebase //It resets the SourceParsedTokens accounts when the wallet changes useEffect(() => { @@ -389,26 +411,16 @@ function useGetAvailableTokens(nft: boolean = false) { currentSourceWalletAddress !== undefined && currentSourceWalletAddress !== selectedSourceWalletAddress ) { - dispatch( - nft - ? setSourceWalletAddressNFT(undefined) - : setSourceWalletAddress(undefined) - ); - dispatch( - nft - ? setSourceParsedTokenAccountNFT(undefined) - : setSourceParsedTokenAccount(undefined) - ); - dispatch( - nft - ? setSourceParsedTokenAccountsNFT(undefined) - : setSourceParsedTokenAccounts(undefined) - ); - !nft && dispatch(setAmount("")); + resetSourceAccounts(); return; } else { } - }, [selectedSourceWalletAddress, currentSourceWalletAddress, dispatch, nft]); + }, [ + selectedSourceWalletAddress, + currentSourceWalletAddress, + dispatch, + resetSourceAccounts, + ]); // Solana metaplex load useEffect(() => { @@ -526,69 +538,68 @@ function useGetAvailableTokens(nft: boolean = false) { // const nftTestWallet2 = "0x98ed231428088eb440e8edb5cc8d66dcf913b86e"; let cancelled = false; const walletAddress = signerAddress; - if (!walletAddress || lookupChain !== CHAIN_ID_ETH) { - return; - } - //TODO less cancel - !cancelled && setCovalentLoading(true); - !cancelled && - dispatch( - nft - ? fetchSourceParsedTokenAccountsNFT() - : fetchSourceParsedTokenAccounts() - ); - getEthereumAccountsCovalent(walletAddress, nft).then( - (accounts) => { - !cancelled && setCovalentLoading(false); - !cancelled && setCovalentError(undefined); - !cancelled && setCovalent(accounts); - !cancelled && - dispatch( - nft - ? receiveSourceParsedTokenAccountsNFT( - accounts.reduce((arr, current) => { - if (current.nft_data) { - current.nft_data.forEach((x) => - arr.push( - createNFTParsedTokenAccountFromCovalent( - walletAddress, - current, - x + if (walletAddress && lookupChain === CHAIN_ID_ETH && !tokenAccounts.data) { + //TODO less cancel + !cancelled && setCovalentLoading(true); + !cancelled && + dispatch( + nft + ? fetchSourceParsedTokenAccountsNFT() + : fetchSourceParsedTokenAccounts() + ); + getEthereumAccountsCovalent(walletAddress, nft).then( + (accounts) => { + !cancelled && setCovalentLoading(false); + !cancelled && setCovalentError(undefined); + !cancelled && setCovalent(accounts); + !cancelled && + dispatch( + nft + ? receiveSourceParsedTokenAccountsNFT( + accounts.reduce((arr, current) => { + if (current.nft_data) { + current.nft_data.forEach((x) => + arr.push( + createNFTParsedTokenAccountFromCovalent( + walletAddress, + current, + x + ) ) - ) - ); - } - return arr; - }, [] as NFTParsedTokenAccount[]) - ) - : receiveSourceParsedTokenAccounts( - accounts.map((x) => - createParsedTokenAccountFromCovalent(walletAddress, x) + ); + } + return arr; + }, [] as NFTParsedTokenAccount[]) ) - ) - ); - }, - () => { - !cancelled && - dispatch( - nft - ? errorSourceParsedTokenAccountsNFT( - "Cannot load your Ethereum NFTs at the moment." - ) - : errorSourceParsedTokenAccounts( - "Cannot load your Ethereum tokens at the moment." - ) - ); - !cancelled && - setCovalentError("Cannot load your Ethereum tokens at the moment."); - !cancelled && setCovalentLoading(false); - } - ); + : receiveSourceParsedTokenAccounts( + accounts.map((x) => + createParsedTokenAccountFromCovalent(walletAddress, x) + ) + ) + ); + }, + () => { + !cancelled && + dispatch( + nft + ? errorSourceParsedTokenAccountsNFT( + "Cannot load your Ethereum NFTs at the moment." + ) + : errorSourceParsedTokenAccounts( + "Cannot load your Ethereum tokens at the moment." + ) + ); + !cancelled && + setCovalentError("Cannot load your Ethereum tokens at the moment."); + !cancelled && setCovalentLoading(false); + } + ); - return () => { - cancelled = true; - }; - }, [lookupChain, provider, signerAddress, dispatch, nft]); + return () => { + cancelled = true; + }; + } + }, [lookupChain, provider, signerAddress, dispatch, nft, tokenAccounts.data]); //Terra accounts load //At present, we don't have any mechanism for doing this. @@ -640,6 +651,7 @@ function useGetAvailableTokens(nft: boolean = false) { error: solanaMintAccountsError, receivedAt: null, //TODO }, + resetAccounts: resetSourceAccounts, } : lookupChain === CHAIN_ID_ETH ? { @@ -650,10 +662,12 @@ function useGetAvailableTokens(nft: boolean = false) { error: covalentError, receivedAt: null, //TODO }, + resetAccounts: resetSourceAccounts, } : lookupChain === CHAIN_ID_TERRA ? { terraTokenMap: terraTokenMap, + resetAccounts: resetSourceAccounts, } : undefined; }