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