bridge_ui: terra token picker implementation
Change-Id: I646913489af2011fd2a8ed660b80340168c292e7
This commit is contained in:
parent
fde696d2a8
commit
0f8eb3b933
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -123,7 +123,7 @@ function Target() {
|
|||
disabled={shouldLockFields}
|
||||
chains={chains}
|
||||
/>
|
||||
<KeyAndBalance chainId={targetChain} balance={uiAmountString} />
|
||||
<KeyAndBalance chainId={targetChain} />
|
||||
{readableTargetAddress ? (
|
||||
<>
|
||||
{targetAsset ? (
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue