From 0f8eb3b9332c62016b7d4cad3bb04fc819a6a16a Mon Sep 17 00:00:00 2001 From: Chase Moran Date: Tue, 19 Oct 2021 11:48:46 -0400 Subject: [PATCH] bridge_ui: terra token picker implementation Change-Id: I646913489af2011fd2a8ed660b80340168c292e7 --- bridge_ui/src/components/KeyAndBalance.tsx | 11 +- bridge_ui/src/components/NFT/Source.tsx | 2 +- bridge_ui/src/components/NFT/Target.tsx | 4 +- .../TokenSelectors/SourceTokenSelector.tsx | 5 +- .../TokenSelectors/TerraTokenPicker.tsx | 166 ++++++++++++++++++ .../components/TokenSelectors/TokenPicker.tsx | 7 +- bridge_ui/src/components/Transfer/Source.tsx | 2 +- bridge_ui/src/components/Transfer/Target.tsx | 2 +- bridge_ui/src/utils/terra.ts | 20 ++- 9 files changed, 198 insertions(+), 21 deletions(-) create mode 100644 bridge_ui/src/components/TokenSelectors/TerraTokenPicker.tsx diff --git a/bridge_ui/src/components/KeyAndBalance.tsx b/bridge_ui/src/components/KeyAndBalance.tsx index cc1d01e9e..f8b9ec714 100644 --- a/bridge_ui/src/components/KeyAndBalance.tsx +++ b/bridge_ui/src/components/KeyAndBalance.tsx @@ -3,20 +3,12 @@ import { 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"; -function KeyAndBalance({ - chainId, - balance, -}: { - chainId: ChainId; - balance?: string; -}) { - const balanceString = balance ? "Balance: " + balance : balance; +function KeyAndBalance({ chainId }: { chainId: ChainId }) { if (isEVMChain(chainId)) { return ( <> @@ -35,7 +27,6 @@ function KeyAndBalance({ return ( <> - {balanceString} ); } diff --git a/bridge_ui/src/components/NFT/Source.tsx b/bridge_ui/src/components/NFT/Source.tsx index ba931fc2e..4fffc2b3c 100644 --- a/bridge_ui/src/components/NFT/Source.tsx +++ b/bridge_ui/src/components/NFT/Source.tsx @@ -79,7 +79,7 @@ function Source() { Only NFTs which implement ERC-721 are supported. ) : null} - + {isReady || uiAmountString ? (
diff --git a/bridge_ui/src/components/NFT/Target.tsx b/bridge_ui/src/components/NFT/Target.tsx index 177f418c3..25c44c993 100644 --- a/bridge_ui/src/components/NFT/Target.tsx +++ b/bridge_ui/src/components/NFT/Target.tsx @@ -22,7 +22,6 @@ import { selectNFTSourceChain, selectNFTTargetAddressHex, selectNFTTargetAsset, - selectNFTTargetBalanceString, selectNFTTargetChain, selectNFTTargetError, } from "../../store/selectors"; @@ -71,7 +70,6 @@ function Target() { } const readableTargetAddress = hexToNativeString(targetAddressHex, targetChain) || ""; - const uiAmountString = useSelector(selectNFTTargetBalanceString); const error = useSelector(selectNFTTargetError); const isTargetComplete = useSelector(selectNFTIsTargetComplete); const shouldLockFields = useSelector(selectNFTShouldLockFields); @@ -97,7 +95,7 @@ function Target() { onChange={handleTargetChange} chains={chains} /> - + { nft={nft} /> ) : lookupChain === CHAIN_ID_TERRA ? ( - ) : ( void; + tokenAccounts: DataWrapper | undefined; + disabled: boolean; + resetAccounts: (() => void) | undefined; +}; + +const returnsFalse = () => false; + +export default function TerraTokenPicker(props: TerraTokenPickerProps) { + const { value, onChange, disabled } = props; + const { walletAddress } = useIsWalletReady(CHAIN_ID_TERRA); + const nativeRefresh = useRef<() => void>(() => {}); + const { balances, isLoading: nativeIsLoading } = useTerraNativeBalances( + walletAddress, + nativeRefresh + ); + + const resetAccountWrapper = useCallback(() => { + //we can currently skip calling this as we don't read from sourceParsedTokenAccounts + //resetAccounts && resetAccounts(); + nativeRefresh.current(); + }, []); + const isLoading = nativeIsLoading; // || (tokenMap?.isFetching || false); + + const onChangeWrapper = useCallback( + async (account: NFTParsedTokenAccount | null) => { + if (account === null) { + onChange(null); + return Promise.resolve(); + } + onChange(account); + return Promise.resolve(); + }, + [onChange] + ); + + const terraTokenArray = useMemo(() => { + const balancesItems = + balances && walletAddress + ? Object.keys(balances).map((denom) => + // ({ + // protocol: "native", + // symbol: formatNativeDenom(denom), + // token: denom, + // icon: getNativeTerraIcon(formatNativeDenom(denom)), + // balance: balances[denom], + // } as TerraTokenMetadata) + + //TODO support non-natives in the SUPPORTED_TERRA_TOKENS + //This token account makes a lot of assumptions + createParsedTokenAccount( + walletAddress, + denom, + balances[denom], //amount + NATIVE_TERRA_DECIMALS, //TODO actually get decimals rather than hardcode + 0, //uiAmount is unused + formatUnits(balances[denom], NATIVE_TERRA_DECIMALS), //uiAmountString + formatNativeDenom(denom), // symbol + undefined, //name + getNativeTerraIcon(formatNativeDenom(denom)), //logo + true //is native asset + ) + ) + : []; + return balancesItems.filter((metadata) => + SUPPORTED_TERRA_TOKENS.includes(metadata.mintKey) + ); + // const values = tokenMap.data?.mainnet; + // const tokenMapItems = Object.values(values || {}) || []; + // return [...balancesItems, ...tokenMapItems]; + }, [ + walletAddress, + balances, + // tokenMap + ]); + + //TODO this only supports non-native assets. Native assets come from the hook. + //TODO correlate against token list to get metadata + const lookupTerraAddress = useCallback( + (lookupAsset: string) => { + if (!walletAddress) { + return Promise.reject("Wallet not connected"); + } + const lcd = new LCDClient(TERRA_HOST); + return lcd.wasm + .contractQuery(lookupAsset, { + token_info: {}, + }) + .then((info: any) => + lcd.wasm + .contractQuery(lookupAsset, { + balance: { + address: walletAddress, + }, + }) + .then((balance: any) => { + if (balance && info) { + return createParsedTokenAccount( + walletAddress, + lookupAsset, + balance.balance.toString(), + info.decimals, + Number(formatUnits(balance.balance, info.decimals)), + formatUnits(balance.balance, info.decimals) + ); + } else { + throw new Error("Failed to retrieve Terra account."); + } + }) + ) + .catch(() => { + return Promise.reject(); + }); + }, + [walletAddress] + ); + + const isSearchableAddress = useCallback((address: string) => { + return isValidTerraAddress(address) && !isNativeDenom(address); + }, []); + + const RenderComp = useCallback( + ({ account }: { account: NFTParsedTokenAccount }) => { + return BasicAccountRender(account, returnsFalse, false); + }, + [] + ); + + return ( + + ); +} diff --git a/bridge_ui/src/components/TokenSelectors/TokenPicker.tsx b/bridge_ui/src/components/TokenSelectors/TokenPicker.tsx index ef1a31c25..9c20dc7a3 100644 --- a/bridge_ui/src/components/TokenSelectors/TokenPicker.tsx +++ b/bridge_ui/src/components/TokenSelectors/TokenPicker.tsx @@ -282,12 +282,15 @@ export default function TokenPicker({ if (useTokenId && !tokenIdHolderString) { return; } + setLoadingError(""); let cancelled = false; if (isValidAddress(holderString)) { const option = localFind(holderString, tokenIdHolderString); if (option) { handleSelectOption(option); - return; + return () => { + cancelled = true; + }; } setLocalLoading(true); setLoadingError(""); @@ -311,6 +314,7 @@ export default function TokenPicker({ } ); } + return () => (cancelled = true); }, [ holderString, isValidAddress, @@ -336,7 +340,6 @@ export default function TokenPicker({ const displayLocalError = (
- {loadingError || selectionError} diff --git a/bridge_ui/src/components/Transfer/Source.tsx b/bridge_ui/src/components/Transfer/Source.tsx index 56bfec274..feea71f4f 100644 --- a/bridge_ui/src/components/Transfer/Source.tsx +++ b/bridge_ui/src/components/Transfer/Source.tsx @@ -112,7 +112,7 @@ function Source() { disabled={shouldLockFields} chains={CHAINS} /> - + {isReady || uiAmountString ? (
diff --git a/bridge_ui/src/components/Transfer/Target.tsx b/bridge_ui/src/components/Transfer/Target.tsx index ac64471a0..e7c5dad8f 100644 --- a/bridge_ui/src/components/Transfer/Target.tsx +++ b/bridge_ui/src/components/Transfer/Target.tsx @@ -123,7 +123,7 @@ function Target() { disabled={shouldLockFields} chains={chains} /> - + {readableTargetAddress ? ( <> {targetAsset ? ( diff --git a/bridge_ui/src/utils/terra.ts b/bridge_ui/src/utils/terra.ts index 138d275f9..954465579 100644 --- a/bridge_ui/src/utils/terra.ts +++ b/bridge_ui/src/utils/terra.ts @@ -1,4 +1,8 @@ -import { isNativeTerra } from "@certusone/wormhole-sdk"; +import { + canonicalAddress, + isNativeDenom, + isNativeTerra, +} from "@certusone/wormhole-sdk"; import { formatUnits } from "@ethersproject/units"; import { LCDClient } from "@terra-money/terra.js"; import { TxResult } from "@terra-money/wallet-provider"; @@ -37,3 +41,17 @@ export async function waitForTerraExecution(transaction: TxResult) { } return info; } + +export const isValidTerraAddress = (address: string) => { + if (isNativeDenom(address)) { + return true; + } + try { + const startsWithTerra = address && address.startsWith("terra"); + const isParseable = canonicalAddress(address); + const isLength20 = isParseable.length === 20; + return !!(startsWithTerra && isParseable && isLength20); + } catch (error) { + return false; + } +};