bridge_ui: terra token picker implementation

Change-Id: I646913489af2011fd2a8ed660b80340168c292e7
This commit is contained in:
Chase Moran 2021-10-19 11:48:46 -04:00 committed by Evan Gray
parent fde696d2a8
commit 0f8eb3b933
9 changed files with 198 additions and 21 deletions

View File

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

View File

@ -79,7 +79,7 @@ function Source() {
Only NFTs which implement ERC-721 are supported. Only NFTs which implement ERC-721 are supported.
</Alert> </Alert>
) : null} ) : null}
<KeyAndBalance chainId={sourceChain} balance={uiAmountString} /> <KeyAndBalance chainId={sourceChain} />
{isReady || uiAmountString ? ( {isReady || uiAmountString ? (
<div className={classes.transferField}> <div className={classes.transferField}>
<TokenSelector disabled={shouldLockFields} nft={true} /> <TokenSelector disabled={shouldLockFields} nft={true} />

View File

@ -22,7 +22,6 @@ import {
selectNFTSourceChain, selectNFTSourceChain,
selectNFTTargetAddressHex, selectNFTTargetAddressHex,
selectNFTTargetAsset, selectNFTTargetAsset,
selectNFTTargetBalanceString,
selectNFTTargetChain, selectNFTTargetChain,
selectNFTTargetError, selectNFTTargetError,
} from "../../store/selectors"; } from "../../store/selectors";
@ -71,7 +70,6 @@ function Target() {
} }
const readableTargetAddress = const readableTargetAddress =
hexToNativeString(targetAddressHex, targetChain) || ""; hexToNativeString(targetAddressHex, targetChain) || "";
const uiAmountString = useSelector(selectNFTTargetBalanceString);
const error = useSelector(selectNFTTargetError); const error = useSelector(selectNFTTargetError);
const isTargetComplete = useSelector(selectNFTIsTargetComplete); const isTargetComplete = useSelector(selectNFTIsTargetComplete);
const shouldLockFields = useSelector(selectNFTShouldLockFields); const shouldLockFields = useSelector(selectNFTShouldLockFields);
@ -97,7 +95,7 @@ function Target() {
onChange={handleTargetChange} onChange={handleTargetChange}
chains={chains} chains={chains}
/> />
<KeyAndBalance chainId={targetChain} balance={uiAmountString} /> <KeyAndBalance chainId={targetChain} />
<TextField <TextField
label="Recipient Address" label="Recipient Address"
fullWidth fullWidth

View File

@ -24,7 +24,7 @@ import { isEVMChain } from "../../utils/ethereum";
import EvmTokenPicker from "./EvmTokenPicker"; import EvmTokenPicker from "./EvmTokenPicker";
import RefreshButtonWrapper from "./RefreshButtonWrapper"; import RefreshButtonWrapper from "./RefreshButtonWrapper";
import SolanaTokenPicker from "./SolanaTokenPicker"; import SolanaTokenPicker from "./SolanaTokenPicker";
import TerraSourceTokenSelector from "./TerraSourceTokenSelector"; import TerraTokenPicker from "./TerraTokenPicker";
type TokenSelectorProps = { type TokenSelectorProps = {
disabled: boolean; disabled: boolean;
@ -104,11 +104,12 @@ export const TokenSelector = (props: TokenSelectorProps) => {
nft={nft} nft={nft}
/> />
) : lookupChain === CHAIN_ID_TERRA ? ( ) : lookupChain === CHAIN_ID_TERRA ? (
<TerraSourceTokenSelector <TerraTokenPicker
value={sourceParsedTokenAccount || null} value={sourceParsedTokenAccount || null}
disabled={disabled} disabled={disabled}
onChange={handleOnChange} onChange={handleOnChange}
resetAccounts={maps?.resetAccounts} resetAccounts={maps?.resetAccounts}
tokenAccounts={maps?.tokenAccounts}
/> />
) : ( ) : (
<TextField <TextField

View File

@ -0,0 +1,166 @@
import { CHAIN_ID_TERRA, isNativeDenom } from "@certusone/wormhole-sdk";
import { formatUnits } from "@ethersproject/units";
import { LCDClient } from "@terra-money/terra.js";
import React, { useCallback, useMemo, useRef } from "react";
import { createParsedTokenAccount } from "../../hooks/useGetSourceParsedTokenAccounts";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import useTerraNativeBalances from "../../hooks/useTerraNativeBalances";
import { DataWrapper } from "../../store/helpers";
import { NFTParsedTokenAccount } from "../../store/nftSlice";
import { ParsedTokenAccount } from "../../store/transferSlice";
import { SUPPORTED_TERRA_TOKENS, TERRA_HOST } from "../../utils/consts";
import {
formatNativeDenom,
getNativeTerraIcon,
isValidTerraAddress,
NATIVE_TERRA_DECIMALS,
} from "../../utils/terra";
import TokenPicker, { BasicAccountRender } from "./TokenPicker";
type TerraTokenPickerProps = {
value: ParsedTokenAccount | null;
onChange: (newValue: ParsedTokenAccount | null) => void;
tokenAccounts: DataWrapper<ParsedTokenAccount[]> | 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 (
<TokenPicker
value={value}
options={terraTokenArray || []}
RenderOption={RenderComp}
onChange={onChangeWrapper}
isValidAddress={isSearchableAddress}
getAddress={lookupTerraAddress}
disabled={disabled}
resetAccounts={resetAccountWrapper}
error={""}
showLoader={isLoading}
nft={false}
chainId={CHAIN_ID_TERRA}
/>
);
}

View File

@ -282,12 +282,15 @@ export default function TokenPicker({
if (useTokenId && !tokenIdHolderString) { if (useTokenId && !tokenIdHolderString) {
return; return;
} }
setLoadingError("");
let cancelled = false; let cancelled = false;
if (isValidAddress(holderString)) { if (isValidAddress(holderString)) {
const option = localFind(holderString, tokenIdHolderString); const option = localFind(holderString, tokenIdHolderString);
if (option) { if (option) {
handleSelectOption(option); handleSelectOption(option);
return; return () => {
cancelled = true;
};
} }
setLocalLoading(true); setLocalLoading(true);
setLoadingError(""); setLoadingError("");
@ -311,6 +314,7 @@ export default function TokenPicker({
} }
); );
} }
return () => (cancelled = true);
}, [ }, [
holderString, holderString,
isValidAddress, isValidAddress,
@ -336,7 +340,6 @@ export default function TokenPicker({
const displayLocalError = ( const displayLocalError = (
<div className={classes.alignCenter}> <div className={classes.alignCenter}>
<CircularProgress />
<Typography variant="body2" color="error"> <Typography variant="body2" color="error">
{loadingError || selectionError} {loadingError || selectionError}
</Typography> </Typography>

View File

@ -112,7 +112,7 @@ function Source() {
disabled={shouldLockFields} disabled={shouldLockFields}
chains={CHAINS} chains={CHAINS}
/> />
<KeyAndBalance chainId={sourceChain} balance={uiAmountString} /> <KeyAndBalance chainId={sourceChain} />
{isReady || uiAmountString ? ( {isReady || uiAmountString ? (
<div className={classes.transferField}> <div className={classes.transferField}>
<TokenSelector disabled={shouldLockFields} /> <TokenSelector disabled={shouldLockFields} />

View File

@ -123,7 +123,7 @@ function Target() {
disabled={shouldLockFields} disabled={shouldLockFields}
chains={chains} chains={chains}
/> />
<KeyAndBalance chainId={targetChain} balance={uiAmountString} /> <KeyAndBalance chainId={targetChain} />
{readableTargetAddress ? ( {readableTargetAddress ? (
<> <>
{targetAsset ? ( {targetAsset ? (

View File

@ -1,4 +1,8 @@
import { isNativeTerra } from "@certusone/wormhole-sdk"; import {
canonicalAddress,
isNativeDenom,
isNativeTerra,
} from "@certusone/wormhole-sdk";
import { formatUnits } from "@ethersproject/units"; import { formatUnits } from "@ethersproject/units";
import { LCDClient } from "@terra-money/terra.js"; import { LCDClient } from "@terra-money/terra.js";
import { TxResult } from "@terra-money/wallet-provider"; import { TxResult } from "@terra-money/wallet-provider";
@ -37,3 +41,17 @@ export async function waitForTerraExecution(transaction: TxResult) {
} }
return info; 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;
}
};