bridge_ui: rework of target section on transfer screen

fixes https://github.com/certusone/wormhole/issues/404

Change-Id: I405802f3a6e1695e8d1517bd834e820d6369fa05
This commit is contained in:
Chase Moran 2021-09-23 17:29:48 -04:00 committed by Evan Gray
parent 66d75c97a3
commit 77ecc035a3
3 changed files with 298 additions and 14 deletions

View File

@ -8,6 +8,7 @@ import { Alert } from "@material-ui/lab";
import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import useMetadata from "../../hooks/useMetadata";
import useSyncTargetAddress from "../../hooks/useSyncTargetAddress";
import { EthGasEstimateSummary } from "../../hooks/useTransactionFees";
import {
@ -20,12 +21,14 @@ import {
selectTransferTargetChain,
selectTransferTargetError,
UNREGISTERED_ERROR_MESSAGE,
selectTransferAmount,
} from "../../store/selectors";
import { incrementStep, setTargetChain } from "../../store/transferSlice";
import { CHAINS, CHAINS_BY_ID } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
import LowBalanceWarning from "../LowBalanceWarning";
import SmartAddress from "../SmartAddress";
import SolanaCreateAssociatedAddress, {
useAssociatedAccountExistsState,
} from "../SolanaCreateAssociatedAddress";
@ -53,9 +56,21 @@ function Target() {
const targetChain = useSelector(selectTransferTargetChain);
const targetAddressHex = useSelector(selectTransferTargetAddressHex);
const targetAsset = useSelector(selectTransferTargetAsset);
const targetAssetArrayed = useMemo(
() => (targetAsset ? [targetAsset] : []),
[targetAsset]
);
const metadata = useMetadata(targetChain, targetAssetArrayed);
const tokenName =
(targetAsset && metadata.data?.get(targetAsset)?.tokenName) || undefined;
const symbol =
(targetAsset && metadata.data?.get(targetAsset)?.symbol) || undefined;
const logo =
(targetAsset && metadata.data?.get(targetAsset)?.logo) || undefined;
const readableTargetAddress =
hexToNativeString(targetAddressHex, targetChain) || "";
const uiAmountString = useSelector(selectTransferTargetBalanceString);
const transferAmount = useSelector(selectTransferAmount);
const error = useSelector(selectTransferTargetError);
const isTargetComplete = useSelector(selectTransferIsTargetComplete);
const shouldLockFields = useSelector(selectTransferShouldLockFields);
@ -93,13 +108,37 @@ function Target() {
))}
</TextField>
<KeyAndBalance chainId={targetChain} balance={uiAmountString} />
<TextField
label="Recipient Address"
fullWidth
className={classes.transferField}
value={readableTargetAddress}
disabled={true}
/>
{readableTargetAddress ? (
<>
{targetAsset ? (
<div className={classes.transferField}>
<Typography variant="subtitle2">Bridged tokens:</Typography>
<Typography component="div">
<SmartAddress
chainId={targetChain}
address={targetAsset}
symbol={symbol}
tokenName={tokenName}
logo={logo}
variant="h6"
/>
{`(Amount: ${transferAmount})`}
</Typography>
</div>
) : null}
<div className={classes.transferField}>
<Typography variant="subtitle2">Sent to:</Typography>
<Typography component="div">
<SmartAddress
chainId={targetChain}
address={readableTargetAddress}
variant="h6"
/>
{`(Current balance: ${uiAmountString || "0"})`}
</Typography>
</div>
</>
) : null}
{targetChain === CHAIN_ID_SOLANA && targetAsset ? (
<SolanaCreateAssociatedAddress
mintAddress={targetAsset}
@ -108,13 +147,6 @@ function Target() {
setAssociatedAccountExists={setAssociatedAccountExists}
/>
) : null}
<TextField
label="Token Address"
fullWidth
className={classes.transferField}
value={targetAsset || ""}
disabled={true}
/>
<Alert severity="info" className={classes.alert}>
<Typography>
You will have to pay transaction fees on{" "}

View File

@ -0,0 +1,102 @@
import { CHAIN_ID_ETH } from "@certusone/wormhole-sdk";
import { ethers } from "@certusone/wormhole-sdk/node_modules/ethers";
import { useEffect, useMemo, useState } from "react";
import {
Provider,
useEthereumProvider,
} from "../contexts/EthereumProviderContext";
import { DataWrapper } from "../store/helpers";
import useIsWalletReady from "./useIsWalletReady";
export type EthMetadata = {
symbol?: string;
logo?: string;
tokenName?: string;
decimals?: number;
};
const ERC20_BASIC_ABI = [
"function name() view returns (string name)",
"function symbol() view returns (string symbol)",
"function decimals() view returns (uint8 decimals)",
];
const handleError = () => {
return undefined;
};
const fetchSingleMetadata = async (
address: string,
provider: Provider
): Promise<EthMetadata> => {
const contract = new ethers.Contract(address, ERC20_BASIC_ABI, provider);
const [name, symbol, decimals] = await Promise.all([
contract.name().catch(handleError),
contract.symbol().catch(handleError),
contract.decimals().catch(handleError),
]);
return { tokenName: name, symbol, decimals };
};
const fetchEthMetadata = async (addresses: string[], provider: Provider) => {
const promises: Promise<EthMetadata>[] = [];
addresses.forEach((address) => {
promises.push(fetchSingleMetadata(address, provider));
});
const resultsArray = await Promise.all(promises);
const output = new Map<string, EthMetadata>();
addresses.forEach((address, index) => {
output.set(address, resultsArray[index]);
});
return output;
};
function useEthMetadata(
addresses: string[]
): DataWrapper<Map<string, EthMetadata>> {
const { isReady } = useIsWalletReady(CHAIN_ID_ETH);
const { provider } = useEthereumProvider();
const [isFetching, setIsFetching] = useState(false);
const [error, setError] = useState("");
const [data, setData] = useState<Map<string, EthMetadata> | null>(null);
useEffect(() => {
let cancelled = false;
if (addresses.length && provider && isReady) {
setIsFetching(true);
setError("");
setData(null);
fetchEthMetadata(addresses, provider).then(
(results) => {
if (!cancelled) {
setData(results);
setIsFetching(false);
}
},
() => {
if (!cancelled) {
setError("Could not retrieve contract metadata");
setIsFetching(false);
}
}
);
}
return () => {
cancelled = true;
};
}, [addresses, provider, isReady]);
return useMemo(
() => ({
data,
isFetching,
error,
receivedAt: null,
}),
[data, isFetching, error]
);
}
export default useEthMetadata;

View File

@ -0,0 +1,150 @@
import {
ChainId,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
} from "@certusone/wormhole-sdk";
import { TokenInfo } from "@solana/spl-token-registry";
import { useMemo } from "react";
import { DataWrapper, getEmptyDataWrapper } from "../store/helpers";
import { Metadata } from "../utils/metaplex";
import useEthMetadata, { EthMetadata } from "./useEthMetadata";
import useMetaplexData from "./useMetaplexData";
import useSolanaTokenMap from "./useSolanaTokenMap";
import useTerraTokenMap, { TerraTokenMap } from "./useTerraTokenMap";
export type GenericMetadata = {
symbol?: string;
logo?: string;
tokenName?: string;
decimals?: number;
//TODO more items
};
const constructSolanaMetadata = (
addresses: string[],
solanaTokenMap: DataWrapper<TokenInfo[]>,
metaplexData: DataWrapper<Map<string, Metadata | undefined> | undefined>
) => {
const isFetching = solanaTokenMap.isFetching || metaplexData?.isFetching;
const error = solanaTokenMap.error || metaplexData?.isFetching;
const receivedAt = solanaTokenMap.receivedAt && metaplexData?.receivedAt;
const data = new Map<string, GenericMetadata>();
addresses.forEach((address) => {
const metaplex = metaplexData?.data?.get(address);
const tokenInfo = solanaTokenMap.data?.find((x) => x.address === address);
//Both this and the token picker, at present, give priority to the tokenmap
const obj = {
symbol: tokenInfo?.symbol || metaplex?.data.symbol || undefined,
logo: tokenInfo?.logoURI || metaplex?.data.uri || undefined, //TODO is URI on metaplex actually the logo? If not, where is it?
tokenName: tokenInfo?.name || metaplex?.data.name || undefined,
decimals: tokenInfo?.decimals || undefined, //TODO decimals are actually on the mint, not the metaplex account.
};
data.set(address, obj);
});
return {
isFetching,
error,
receivedAt,
data,
};
};
const constructTerraMetadata = (
addresses: string[],
tokenMap: DataWrapper<TerraTokenMap>
) => {
const isFetching = tokenMap.isFetching;
const error = tokenMap.error;
const receivedAt = tokenMap.receivedAt;
const data = new Map<string, GenericMetadata>();
addresses.forEach((address) => {
const meta = tokenMap.data?.mainnet[address];
const obj = {
symbol: meta?.symbol || undefined,
logo: meta?.icon || undefined,
tokenName: meta?.token || undefined,
decimals: undefined, //TODO find a way to get this on terra
};
data.set(address, obj);
});
return {
isFetching,
error,
receivedAt,
data,
};
};
const constructEthMetadata = (
addresses: string[],
metadataMap: DataWrapper<Map<string, EthMetadata> | null>
) => {
const isFetching = metadataMap.isFetching;
const error = metadataMap.error;
const receivedAt = metadataMap.receivedAt;
const data = new Map<string, GenericMetadata>();
addresses.forEach((address) => {
const meta = metadataMap.data?.get(address);
const obj = {
symbol: meta?.symbol || undefined,
logo: meta?.logo || undefined,
tokenName: meta?.tokenName || undefined,
decimals: meta?.decimals,
};
data.set(address, obj);
});
return {
isFetching,
error,
receivedAt,
data,
};
};
export default function useMetadata(
chainId: ChainId,
addresses: string[]
): DataWrapper<Map<string, GenericMetadata>> {
const terraTokenMap = useTerraTokenMap();
const solanaTokenMap = useSolanaTokenMap();
const solanaAddresses = useMemo(() => {
return chainId === CHAIN_ID_SOLANA ? addresses : [];
}, [chainId, addresses]);
const terraAddresses = useMemo(() => {
return chainId === CHAIN_ID_TERRA ? addresses : [];
}, [chainId, addresses]);
const ethereumAddresses = useMemo(() => {
return chainId === CHAIN_ID_ETH ? addresses : [];
}, [chainId, addresses]);
const metaplexData = useMetaplexData(solanaAddresses);
const ethMetadata = useEthMetadata(ethereumAddresses);
const output: DataWrapper<Map<string, GenericMetadata>> = useMemo(
() =>
chainId === CHAIN_ID_SOLANA
? constructSolanaMetadata(solanaAddresses, solanaTokenMap, metaplexData)
: chainId === CHAIN_ID_ETH
? constructEthMetadata(ethereumAddresses, ethMetadata)
: chainId === CHAIN_ID_TERRA
? constructTerraMetadata(terraAddresses, terraTokenMap)
: getEmptyDataWrapper(),
[
chainId,
solanaAddresses,
solanaTokenMap,
metaplexData,
ethereumAddresses,
ethMetadata,
terraAddresses,
terraTokenMap,
]
);
return output;
}