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_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 (
<>
<TerraWalletKey />
<Typography>{balanceString}</Typography>
</>
);
}

View File

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

View File

@ -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}
/>
<KeyAndBalance chainId={targetChain} balance={uiAmountString} />
<KeyAndBalance chainId={targetChain} />
<TextField
label="Recipient Address"
fullWidth

View File

@ -24,7 +24,7 @@ import { isEVMChain } from "../../utils/ethereum";
import EvmTokenPicker from "./EvmTokenPicker";
import RefreshButtonWrapper from "./RefreshButtonWrapper";
import SolanaTokenPicker from "./SolanaTokenPicker";
import TerraSourceTokenSelector from "./TerraSourceTokenSelector";
import TerraTokenPicker from "./TerraTokenPicker";
type TokenSelectorProps = {
disabled: boolean;
@ -104,11 +104,12 @@ export const TokenSelector = (props: TokenSelectorProps) => {
nft={nft}
/>
) : lookupChain === CHAIN_ID_TERRA ? (
<TerraSourceTokenSelector
<TerraTokenPicker
value={sourceParsedTokenAccount || null}
disabled={disabled}
onChange={handleOnChange}
resetAccounts={maps?.resetAccounts}
tokenAccounts={maps?.tokenAccounts}
/>
) : (
<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) {
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 = (
<div className={classes.alignCenter}>
<CircularProgress />
<Typography variant="body2" color="error">
{loadingError || selectionError}
</Typography>

View File

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

View File

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