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_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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
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>
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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 ? (
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue