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:
parent
66d75c97a3
commit
77ecc035a3
|
@ -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{" "}
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue