bridge_ui: QoL improvements

Change-Id: I4221ff5d7757e099b8dad55de6ca2765137e5e38
This commit is contained in:
Evan Gray 2021-09-18 13:04:43 -04:00
parent 03a373676b
commit 26b6ee22bb
15 changed files with 268 additions and 79 deletions

View File

@ -63,7 +63,7 @@ export default function ButtonWithLoader({
) : null} ) : null}
</div> </div>
{error ? ( {error ? (
<Typography color="error" className={classes.error}> <Typography variant="body2" color="error" className={classes.error}>
{error} {error}
</Typography> </Typography>
) : null} ) : null}

View File

@ -5,7 +5,6 @@ import {
CHAIN_ID_SOLANA, CHAIN_ID_SOLANA,
getEmitterAddressEth, getEmitterAddressEth,
getEmitterAddressSolana, getEmitterAddressSolana,
getSignedVAA,
parseSequenceFromLogEth, parseSequenceFromLogEth,
parseSequenceFromLogSolana, parseSequenceFromLogSolana,
} from "@certusone/wormhole-sdk"; } from "@certusone/wormhole-sdk";
@ -31,11 +30,11 @@ import { BigNumber, ethers } from "ethers";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { useEthereumProvider } from "../../contexts/EthereumProviderContext"; import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
import { setSignedVAAHex, setStep, setTargetChain } from "../../store/nftSlice";
import { import {
selectNFTSignedVAAHex, selectNFTSignedVAAHex,
selectNFTSourceChain, selectNFTSourceChain,
} from "../../store/selectors"; } from "../../store/selectors";
import { setSignedVAAHex, setStep, setTargetChain } from "../../store/nftSlice";
import { import {
hexToNativeString, hexToNativeString,
hexToUint8Array, hexToUint8Array,
@ -49,9 +48,9 @@ import {
SOL_NFT_BRIDGE_ADDRESS, SOL_NFT_BRIDGE_ADDRESS,
WORMHOLE_RPC_HOSTS, WORMHOLE_RPC_HOSTS,
} from "../../utils/consts"; } from "../../utils/consts";
import KeyAndBalance from "../KeyAndBalance"; import { getSignedVAAWithRetry } from "../../utils/getSignedVAAWithRetry";
import { METADATA_REPLACE } from "../../utils/metaplex"; import { METADATA_REPLACE } from "../../utils/metaplex";
import { getNextRpcHost } from "../../utils/getSignedVAAWithRetry"; import KeyAndBalance from "../KeyAndBalance";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
fab: { fab: {
@ -66,11 +65,11 @@ async function eth(provider: ethers.providers.Web3Provider, tx: string) {
const receipt = await provider.getTransactionReceipt(tx); const receipt = await provider.getTransactionReceipt(tx);
const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS); const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
const emitterAddress = getEmitterAddressEth(ETH_NFT_BRIDGE_ADDRESS); const emitterAddress = getEmitterAddressEth(ETH_NFT_BRIDGE_ADDRESS);
const { vaaBytes } = await getSignedVAA( const { vaaBytes } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS[getNextRpcHost()],
CHAIN_ID_ETH, CHAIN_ID_ETH,
emitterAddress, emitterAddress,
sequence.toString() sequence.toString(),
WORMHOLE_RPC_HOSTS.length
); );
return uint8ArrayToHex(vaaBytes); return uint8ArrayToHex(vaaBytes);
} catch (e) { } catch (e) {
@ -90,11 +89,11 @@ async function solana(tx: string) {
const emitterAddress = await getEmitterAddressSolana( const emitterAddress = await getEmitterAddressSolana(
SOL_NFT_BRIDGE_ADDRESS SOL_NFT_BRIDGE_ADDRESS
); );
const { vaaBytes } = await getSignedVAA( const { vaaBytes } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS[getNextRpcHost()],
CHAIN_ID_SOLANA, CHAIN_ID_SOLANA,
emitterAddress, emitterAddress,
sequence.toString() sequence.toString(),
WORMHOLE_RPC_HOSTS.length
); );
return uint8ArrayToHex(vaaBytes); return uint8ArrayToHex(vaaBytes);
} catch (e) { } catch (e) {
@ -199,10 +198,10 @@ function RecoveryDialogContent({
setRecoverySourceChain(event.target.value); setRecoverySourceChain(event.target.value);
}, []); }, []);
const handleSourceTxChange = useCallback((event) => { const handleSourceTxChange = useCallback((event) => {
setRecoverySourceTx(event.target.value); setRecoverySourceTx(event.target.value.trim());
}, []); }, []);
const handleSignedVAAChange = useCallback((event) => { const handleSignedVAAChange = useCallback((event) => {
setRecoverySignedVAA(event.target.value); setRecoverySignedVAA(event.target.value.trim());
}, []); }, []);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;

View File

@ -40,7 +40,7 @@ function Send() {
Transfer the NFT to the Wormhole Token Bridge. Transfer the NFT to the Wormhole Token Bridge.
</StepDescription> </StepDescription>
<KeyAndBalance chainId={sourceChain} /> <KeyAndBalance chainId={sourceChain} />
<Alert severity="warning"> <Alert severity="info">
This will initiate the transfer on {CHAINS_BY_ID[sourceChain].name} and This will initiate the transfer on {CHAINS_BY_ID[sourceChain].name} and
wait for finalization. If you navigate away from this page before wait for finalization. If you navigate away from this page before
completing Step 4, you will have to perform the recovery workflow to completing Step 4, you will have to perform the recovery workflow to

View File

@ -25,7 +25,10 @@ export default function ShowTx({
tx: Transaction; tx: Transaction;
}) { }) {
const classes = useStyles(); const classes = useStyles();
const showExplorerLink = CLUSTER === "testnet" || CLUSTER === "mainnet"; const showExplorerLink =
CLUSTER === "testnet" ||
CLUSTER === "mainnet" ||
(CLUSTER === "devnet" && chainId === CHAIN_ID_SOLANA);
const explorerAddress = const explorerAddress =
chainId === CHAIN_ID_ETH chainId === CHAIN_ID_ETH
? `https://${CLUSTER === "testnet" ? "goerli." : ""}etherscan.io/tx/${ ? `https://${CLUSTER === "testnet" ? "goerli." : ""}etherscan.io/tx/${
@ -33,7 +36,11 @@ export default function ShowTx({
}` }`
: chainId === CHAIN_ID_SOLANA : chainId === CHAIN_ID_SOLANA
? `https://explorer.solana.com/tx/${tx?.id}${ ? `https://explorer.solana.com/tx/${tx?.id}${
CLUSTER === "testnet" ? "?cluster=testnet" : "" CLUSTER === "testnet"
? "?cluster=testnet"
: CLUSTER === "devnet"
? "?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899"
: ""
}` }`
: undefined; : undefined;
const explorerName = chainId === CHAIN_ID_ETH ? "Etherscan" : "Explorer"; const explorerName = chainId === CHAIN_ID_ETH ? "Etherscan" : "Explorer";

View File

@ -0,0 +1,69 @@
import { CHAIN_ID_ETH } from "@certusone/wormhole-sdk";
import { Button, makeStyles } from "@material-ui/core";
import detectEthereumProvider from "@metamask/detect-provider";
import { useCallback } from "react";
import { useSelector } from "react-redux";
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
import {
selectTransferTargetAsset,
selectTransferTargetChain,
} from "../../store/selectors";
import {
ethTokenToParsedTokenAccount,
getEthereumToken,
} from "../../utils/ethereum";
const useStyles = makeStyles((theme) => ({
addButton: {
display: "block",
margin: `${theme.spacing(1)}px auto 0px`,
},
}));
export default function AddToMetamask() {
const classes = useStyles();
const targetChain = useSelector(selectTransferTargetChain);
const targetAsset = useSelector(selectTransferTargetAsset);
const { provider, signerAddress } = useEthereumProvider();
const handleClick = useCallback(() => {
if (provider && targetAsset && signerAddress) {
(async () => {
try {
const token = await getEthereumToken(targetAsset, provider);
const { symbol, decimals } = await ethTokenToParsedTokenAccount(
token,
signerAddress
);
const ethereum = (await detectEthereumProvider()) as any;
ethereum.request({
method: "wallet_watchAsset",
params: {
type: "ERC20", // In the future, other standards will be supported
options: {
address: targetAsset, // The address of the token contract
symbol, // A ticker symbol or shorthand, up to 5 characters
decimals, // The number of token decimals
// image: string; // A string url of the token logo
},
},
});
} catch (e) {
console.error(e);
}
})();
}
}, [provider, targetAsset, signerAddress]);
return provider &&
signerAddress &&
targetAsset &&
targetChain === CHAIN_ID_ETH ? (
<Button
onClick={handleClick}
size="small"
variant="outlined"
className={classes.addButton}
>
Add to Metamask
</Button>
) : null;
}

View File

@ -7,7 +7,6 @@ import {
getEmitterAddressEth, getEmitterAddressEth,
getEmitterAddressSolana, getEmitterAddressSolana,
getEmitterAddressTerra, getEmitterAddressTerra,
getSignedVAA,
parseSequenceFromLogEth, parseSequenceFromLogEth,
parseSequenceFromLogSolana, parseSequenceFromLogSolana,
parseSequenceFromLogTerra, parseSequenceFromLogTerra,
@ -15,6 +14,7 @@ import {
import { import {
Box, Box,
Button, Button,
CircularProgress,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
@ -32,6 +32,7 @@ import { Alert } from "@material-ui/lab";
import { Connection } from "@solana/web3.js"; import { Connection } from "@solana/web3.js";
import { LCDClient } from "@terra-money/terra.js"; import { LCDClient } from "@terra-money/terra.js";
import { BigNumber, ethers } from "ethers"; import { BigNumber, ethers } from "ethers";
import { useSnackbar } from "notistack";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { useEthereumProvider } from "../../contexts/EthereumProviderContext"; import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
@ -59,7 +60,8 @@ import {
TERRA_TOKEN_BRIDGE_ADDRESS, TERRA_TOKEN_BRIDGE_ADDRESS,
WORMHOLE_RPC_HOSTS, WORMHOLE_RPC_HOSTS,
} from "../../utils/consts"; } from "../../utils/consts";
import { getNextRpcHost } from "../../utils/getSignedVAAWithRetry"; import { getSignedVAAWithRetry } from "../../utils/getSignedVAAWithRetry";
import parseError from "../../utils/parseError";
import KeyAndBalance from "../KeyAndBalance"; import KeyAndBalance from "../KeyAndBalance";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
@ -70,25 +72,30 @@ const useStyles = makeStyles((theme) => ({
}, },
})); }));
async function eth(provider: ethers.providers.Web3Provider, tx: string) { async function eth(
provider: ethers.providers.Web3Provider,
tx: string,
enqueueSnackbar: any
) {
try { try {
const receipt = await provider.getTransactionReceipt(tx); const receipt = await provider.getTransactionReceipt(tx);
const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS); const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS); const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
const { vaaBytes } = await getSignedVAA( const { vaaBytes } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS[getNextRpcHost()],
CHAIN_ID_ETH, CHAIN_ID_ETH,
emitterAddress, emitterAddress,
sequence.toString() sequence.toString(),
WORMHOLE_RPC_HOSTS.length
); );
return uint8ArrayToHex(vaaBytes); return { vaa: uint8ArrayToHex(vaaBytes), error: null };
} catch (e) { } catch (e) {
console.error(e); console.error(e);
enqueueSnackbar(parseError(e), { variant: "error" });
return { vaa: null, error: parseError(e) };
} }
return "";
} }
async function solana(tx: string) { async function solana(tx: string, enqueueSnackbar: any) {
try { try {
const connection = new Connection(SOLANA_HOST, "confirmed"); const connection = new Connection(SOLANA_HOST, "confirmed");
const info = await connection.getTransaction(tx); const info = await connection.getTransaction(tx);
@ -99,20 +106,21 @@ async function solana(tx: string) {
const emitterAddress = await getEmitterAddressSolana( const emitterAddress = await getEmitterAddressSolana(
SOL_TOKEN_BRIDGE_ADDRESS SOL_TOKEN_BRIDGE_ADDRESS
); );
const { vaaBytes } = await getSignedVAA( const { vaaBytes } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS[getNextRpcHost()],
CHAIN_ID_SOLANA, CHAIN_ID_SOLANA,
emitterAddress, emitterAddress,
sequence.toString() sequence.toString(),
WORMHOLE_RPC_HOSTS.length
); );
return uint8ArrayToHex(vaaBytes); return { vaa: uint8ArrayToHex(vaaBytes), error: null };
} catch (e) { } catch (e) {
console.error(e); console.error(e);
enqueueSnackbar(parseError(e), { variant: "error" });
return { vaa: null, error: parseError(e) };
} }
return "";
} }
async function terra(tx: string) { async function terra(tx: string, enqueueSnackbar: any) {
try { try {
const lcd = new LCDClient(TERRA_HOST); const lcd = new LCDClient(TERRA_HOST);
const info = await lcd.tx.txInfo(tx); const info = await lcd.tx.txInfo(tx);
@ -123,17 +131,18 @@ async function terra(tx: string) {
const emitterAddress = await getEmitterAddressTerra( const emitterAddress = await getEmitterAddressTerra(
TERRA_TOKEN_BRIDGE_ADDRESS TERRA_TOKEN_BRIDGE_ADDRESS
); );
const { vaaBytes } = await getSignedVAA( const { vaaBytes } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS[getNextRpcHost()],
CHAIN_ID_TERRA, CHAIN_ID_TERRA,
emitterAddress, emitterAddress,
sequence sequence,
WORMHOLE_RPC_HOSTS.length
); );
return uint8ArrayToHex(vaaBytes); return { vaa: uint8ArrayToHex(vaaBytes), error: null };
} catch (e) { } catch (e) {
console.error(e); console.error(e);
enqueueSnackbar(parseError(e), { variant: "error" });
return { vaa: null, error: parseError(e) };
} }
return "";
} }
// 0 u256 amount // 0 u256 amount
@ -159,12 +168,16 @@ function RecoveryDialogContent({
onClose: () => void; onClose: () => void;
disabled: boolean; disabled: boolean;
}) { }) {
const { enqueueSnackbar } = useSnackbar();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { provider } = useEthereumProvider(); const { provider } = useEthereumProvider();
const currentSourceChain = useSelector(selectTransferSourceChain); const currentSourceChain = useSelector(selectTransferSourceChain);
const [recoverySourceChain, setRecoverySourceChain] = const [recoverySourceChain, setRecoverySourceChain] =
useState(currentSourceChain); useState(currentSourceChain);
const [recoverySourceTx, setRecoverySourceTx] = useState(""); const [recoverySourceTx, setRecoverySourceTx] = useState("");
const [recoverySourceTxIsLoading, setRecoverySourceTxIsLoading] =
useState(false);
const [recoverySourceTxError, setRecoverySourceTxError] = useState("");
const currentSignedVAA = useSelector(selectTransferSignedVAAHex); const currentSignedVAA = useSelector(selectTransferSignedVAAHex);
const [recoverySignedVAA, setRecoverySignedVAA] = useState(currentSignedVAA); const [recoverySignedVAA, setRecoverySignedVAA] = useState(currentSignedVAA);
const [recoveryParsedVAA, setRecoveryParsedVAA] = useState<any>(null); const [recoveryParsedVAA, setRecoveryParsedVAA] = useState<any>(null);
@ -178,24 +191,55 @@ function RecoveryDialogContent({
if (recoverySourceTx) { if (recoverySourceTx) {
let cancelled = false; let cancelled = false;
if (recoverySourceChain === CHAIN_ID_ETH && provider) { if (recoverySourceChain === CHAIN_ID_ETH && provider) {
setRecoverySourceTxError("");
setRecoverySourceTxIsLoading(true);
(async () => { (async () => {
const vaa = await eth(provider, recoverySourceTx); const { vaa, error } = await eth(
provider,
recoverySourceTx,
enqueueSnackbar
);
if (!cancelled) { if (!cancelled) {
setRecoverySignedVAA(vaa); setRecoverySourceTxIsLoading(false);
if (vaa) {
setRecoverySignedVAA(vaa);
}
if (error) {
setRecoverySourceTxError(error);
}
} }
})(); })();
} else if (recoverySourceChain === CHAIN_ID_SOLANA) { } else if (recoverySourceChain === CHAIN_ID_SOLANA) {
setRecoverySourceTxError("");
setRecoverySourceTxIsLoading(true);
(async () => { (async () => {
const vaa = await solana(recoverySourceTx); const { vaa, error } = await solana(
recoverySourceTx,
enqueueSnackbar
);
if (!cancelled) { if (!cancelled) {
setRecoverySignedVAA(vaa); setRecoverySourceTxIsLoading(false);
if (vaa) {
setRecoverySignedVAA(vaa);
}
if (error) {
setRecoverySourceTxError(error);
}
} }
})(); })();
} else if (recoverySourceChain === CHAIN_ID_TERRA) { } else if (recoverySourceChain === CHAIN_ID_TERRA) {
setRecoverySourceTxError("");
setRecoverySourceTxIsLoading(true);
(async () => { (async () => {
const vaa = await terra(recoverySourceTx); const { vaa, error } = await terra(recoverySourceTx, enqueueSnackbar);
if (!cancelled) { if (!cancelled) {
setRecoverySignedVAA(vaa); setRecoverySourceTxIsLoading(false);
if (vaa) {
setRecoverySignedVAA(vaa);
}
if (error) {
setRecoverySourceTxError(error);
}
} }
})(); })();
} }
@ -203,7 +247,7 @@ function RecoveryDialogContent({
cancelled = true; cancelled = true;
}; };
} }
}, [recoverySourceChain, recoverySourceTx, provider]); }, [recoverySourceChain, recoverySourceTx, provider, enqueueSnackbar]);
useEffect(() => { useEffect(() => {
setRecoverySignedVAA(currentSignedVAA); setRecoverySignedVAA(currentSignedVAA);
}, [currentSignedVAA]); }, [currentSignedVAA]);
@ -212,10 +256,10 @@ function RecoveryDialogContent({
setRecoverySourceChain(event.target.value); setRecoverySourceChain(event.target.value);
}, []); }, []);
const handleSourceTxChange = useCallback((event) => { const handleSourceTxChange = useCallback((event) => {
setRecoverySourceTx(event.target.value); setRecoverySourceTx(event.target.value.trim());
}, []); }, []);
const handleSignedVAAChange = useCallback((event) => { const handleSignedVAAChange = useCallback((event) => {
setRecoverySignedVAA(event.target.value); setRecoverySignedVAA(event.target.value.trim());
}, []); }, []);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@ -292,23 +336,45 @@ function RecoveryDialogContent({
<KeyAndBalance chainId={recoverySourceChain} /> <KeyAndBalance chainId={recoverySourceChain} />
) : null} ) : null}
<TextField <TextField
label="Source Tx" label="Source Tx (paste here)"
disabled={!!recoverySignedVAA} disabled={!!recoverySignedVAA || recoverySourceTxIsLoading}
value={recoverySourceTx} value={recoverySourceTx}
onChange={handleSourceTxChange} onChange={handleSourceTxChange}
error={!!recoverySourceTxError}
helperText={recoverySourceTxError}
fullWidth fullWidth
margin="normal" margin="normal"
/> />
<Box mt={4}> <Box position="relative">
<Typography>or</Typography> <Box mt={4}>
<Typography>or</Typography>
</Box>
<TextField
label="Signed VAA (Hex)"
disabled={recoverySourceTxIsLoading}
value={recoverySignedVAA || ""}
onChange={handleSignedVAAChange}
fullWidth
margin="normal"
/>
{recoverySourceTxIsLoading ? (
<Box
position="absolute"
style={{
top: 0,
right: 0,
left: 0,
bottom: 0,
backgroundColor: "rgba(0,0,0,0.5)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<CircularProgress />
</Box>
) : null}
</Box> </Box>
<TextField
label="Signed VAA (Hex)"
value={recoverySignedVAA || ""}
onChange={handleSignedVAAChange}
fullWidth
margin="normal"
/>
<Box my={4}> <Box my={4}>
<Divider /> <Divider />
</Box> </Box>

View File

@ -8,6 +8,7 @@ import {
import { reset } from "../../store/transferSlice"; import { reset } from "../../store/transferSlice";
import ButtonWithLoader from "../ButtonWithLoader"; import ButtonWithLoader from "../ButtonWithLoader";
import ShowTx from "../ShowTx"; import ShowTx from "../ShowTx";
import AddToMetamask from "./AddToMetamask";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
description: { description: {
@ -37,6 +38,7 @@ export default function RedeemPreview() {
{explainerString} {explainerString}
</Typography> </Typography>
{redeemTx ? <ShowTx chainId={targetChain} tx={redeemTx} /> : null} {redeemTx ? <ShowTx chainId={targetChain} tx={redeemTx} /> : null}
<AddToMetamask />
<ButtonWithLoader onClick={handleResetClick}> <ButtonWithLoader onClick={handleResetClick}>
Transfer More Tokens! Transfer More Tokens!
</ButtonWithLoader> </ButtonWithLoader>

View File

@ -21,6 +21,7 @@ import {
import { CHAINS_BY_ID } from "../../utils/consts"; import { CHAINS_BY_ID } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader"; import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance"; import KeyAndBalance from "../KeyAndBalance";
import ShowTx from "../ShowTx";
import StepDescription from "../StepDescription"; import StepDescription from "../StepDescription";
import TransactionProgress from "../TransactionProgress"; import TransactionProgress from "../TransactionProgress";
import WaitingForWalletMessage from "./WaitingForWalletMessage"; import WaitingForWalletMessage from "./WaitingForWalletMessage";
@ -111,7 +112,7 @@ function Send() {
Transfer the tokens to the Wormhole Token Bridge. Transfer the tokens to the Wormhole Token Bridge.
</StepDescription> </StepDescription>
<KeyAndBalance chainId={sourceChain} /> <KeyAndBalance chainId={sourceChain} />
<Alert severity="warning"> <Alert severity="info">
This will initiate the transfer on {CHAINS_BY_ID[sourceChain].name} and This will initiate the transfer on {CHAINS_BY_ID[sourceChain].name} and
wait for finalization. If you navigate away from this page before wait for finalization. If you navigate away from this page before
completing Step 4, you will have to perform the recovery workflow to completing Step 4, you will have to perform the recovery workflow to
@ -153,6 +154,7 @@ function Send() {
</ButtonWithLoader> </ButtonWithLoader>
)} )}
<WaitingForWalletMessage /> <WaitingForWalletMessage />
{transferTx ? <ShowTx chainId={sourceChain} tx={transferTx} /> : null}
<TransactionProgress <TransactionProgress
chainId={sourceChain} chainId={sourceChain}
tx={transferTx} tx={transferTx}

View File

@ -5,7 +5,6 @@ import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { useHistory } from "react-router"; import { useHistory } from "react-router";
import useIsWalletReady from "../../hooks/useIsWalletReady"; import useIsWalletReady from "../../hooks/useIsWalletReady";
import useTokenBlacklistWarning from "../../hooks/useTokenBlacklistWarning";
import { import {
selectTransferAmount, selectTransferAmount,
selectTransferIsSourceComplete, selectTransferIsSourceComplete,
@ -25,6 +24,7 @@ import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance"; import KeyAndBalance from "../KeyAndBalance";
import StepDescription from "../StepDescription"; import StepDescription from "../StepDescription";
import { TokenSelector } from "../TokenSelectors/SourceTokenSelector"; import { TokenSelector } from "../TokenSelectors/SourceTokenSelector";
import TokenBlacklistWarning from "./TokenBlacklistWarning";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
transferField: { transferField: {
@ -55,10 +55,6 @@ function Source({
const isSourceComplete = useSelector(selectTransferIsSourceComplete); const isSourceComplete = useSelector(selectTransferIsSourceComplete);
const shouldLockFields = useSelector(selectTransferShouldLockFields); const shouldLockFields = useSelector(selectTransferShouldLockFields);
const { isReady, statusMessage } = useIsWalletReady(sourceChain); const { isReady, statusMessage } = useIsWalletReady(sourceChain);
const tokenBlacklistWarning = useTokenBlacklistWarning(
sourceChain,
parsedTokenAccount?.mintKey
);
const handleMigrationClick = useCallback(() => { const handleMigrationClick = useCallback(() => {
parsedTokenAccount?.mintKey && parsedTokenAccount?.mintKey &&
history.push("/migrate/" + parsedTokenAccount.mintKey); history.push("/migrate/" + parsedTokenAccount.mintKey);
@ -124,6 +120,11 @@ function Source({
</Button> </Button>
) : ( ) : (
<> <>
<TokenBlacklistWarning
sourceChain={sourceChain}
tokenAddress={parsedTokenAccount?.mintKey}
symbol={parsedTokenAccount?.symbol}
/>
{hasParsedTokenAccount ? ( {hasParsedTokenAccount ? (
<TextField <TextField
label="Amount" label="Amount"
@ -139,7 +140,7 @@ function Source({
disabled={!isSourceComplete} disabled={!isSourceComplete}
onClick={handleNextClick} onClick={handleNextClick}
showLoader={false} showLoader={false}
error={statusMessage || error || tokenBlacklistWarning} error={statusMessage || error}
> >
Next Next
</ButtonWithLoader> </ButtonWithLoader>

View File

@ -7,6 +7,7 @@ import {
} from "../../store/selectors"; } from "../../store/selectors";
import { CHAINS_BY_ID } from "../../utils/consts"; import { CHAINS_BY_ID } from "../../utils/consts";
import { shortenAddress } from "../../utils/solana"; import { shortenAddress } from "../../utils/solana";
import TokenBlacklistWarning from "./TokenBlacklistWarning";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
description: { description: {
@ -35,12 +36,19 @@ export default function SourcePreview() {
: "Step complete."; : "Step complete.";
return ( return (
<Typography <>
component="div" <Typography
variant="subtitle2" component="div"
className={classes.description} variant="subtitle2"
> className={classes.description}
{explainerString} >
</Typography> {explainerString}
</Typography>
<TokenBlacklistWarning
sourceChain={sourceChain}
tokenAddress={sourceParsedTokenAccount?.mintKey}
symbol={sourceParsedTokenAccount?.symbol}
/>
</>
); );
} }

View File

@ -0,0 +1,22 @@
import { ChainId } from "@certusone/wormhole-sdk";
import { Alert } from "@material-ui/lab";
import useTokenBlacklistWarning from "../../hooks/useTokenBlacklistWarning";
export default function TokenBlacklistWarning({
sourceChain,
tokenAddress,
symbol,
}: {
sourceChain: ChainId;
tokenAddress: string | undefined;
symbol: string | undefined;
}) {
const tokenBlacklistWarning = useTokenBlacklistWarning(
sourceChain,
tokenAddress,
symbol
);
return tokenBlacklistWarning ? (
<Alert severity="warning">{tokenBlacklistWarning}</Alert>
) : null;
}

View File

@ -21,7 +21,7 @@ import {
import { useSnackbar } from "notistack"; import { useSnackbar } from "notistack";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { Signer } from "../../../sdk/js/node_modules/ethers/lib"; import { Signer } from "ethers";
import { useEthereumProvider } from "../contexts/EthereumProviderContext"; import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useSolanaWallet } from "../contexts/SolanaWalletContext"; import { useSolanaWallet } from "../contexts/SolanaWalletContext";
import { import {

View File

@ -11,7 +11,8 @@ import {
export default function useTokenBlacklistWarning( export default function useTokenBlacklistWarning(
chainId: ChainId, chainId: ChainId,
tokenAddress: string | undefined tokenAddress: string | undefined,
symbol: string | undefined
) { ) {
return useMemo( return useMemo(
() => () =>
@ -20,8 +21,12 @@ export default function useTokenBlacklistWarning(
SOLANA_TOKENS_THAT_EXIST_ELSEWHERE.includes(tokenAddress)) || SOLANA_TOKENS_THAT_EXIST_ELSEWHERE.includes(tokenAddress)) ||
(chainId === CHAIN_ID_ETH && (chainId === CHAIN_ID_ETH &&
ETH_TOKENS_THAT_EXIST_ELSEWHERE.includes(tokenAddress))) ETH_TOKENS_THAT_EXIST_ELSEWHERE.includes(tokenAddress)))
? "This token exists on multiple chains! Bridging the token via Wormhole will produce a wrapped version which might have no liquidity on the target chain." ? `Bridging ${
symbol ? symbol : "the token"
} via Wormhole will not produce native ${
symbol ? symbol : "assets"
}. It will produce a wrapped version which might have no liquidity or utility on the target chain.`
: undefined, : undefined,
[chainId, tokenAddress] [chainId, tokenAddress, symbol]
); );
} }

View File

@ -197,11 +197,12 @@ export const SOLANA_TOKENS_THAT_EXIST_ELSEWHERE = [
"ArUkYE2XDKzqy77PRRGjo4wREWwqk6RXTfM9NeqzPvjU", // renDOGE "ArUkYE2XDKzqy77PRRGjo4wREWwqk6RXTfM9NeqzPvjU", // renDOGE
"E99CQ2gFMmbiyK2bwiaFNWUUmwz4r8k2CVEFxwuvQ7ue", // renZEC "E99CQ2gFMmbiyK2bwiaFNWUUmwz4r8k2CVEFxwuvQ7ue", // renZEC
"De2bU64vsXKU9jq4bCjeDxNRGPn8nr3euaTK8jBYmD3J", // renFIL "De2bU64vsXKU9jq4bCjeDxNRGPn8nr3euaTK8jBYmD3J", // renFIL
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", // USDT
]; ];
export const ETH_TOKENS_THAT_EXIST_ELSEWHERE = [ export const ETH_TOKENS_THAT_EXIST_ELSEWHERE = [
getAddress("0x476c5E26a75bd202a9683ffD34359C0CC15be0fF"), // SRM getAddress("0x476c5E26a75bd202a9683ffD34359C0CC15be0fF"), // SRM
getAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), // USDC getAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), // USDC
getAddress("0x818fc6c2ec5986bc6e2cbf00939d90556ab12ce5"), // KIN getAddress("0x818fc6c2ec5986bc6e2cbf00939d90556ab12ce5"), // KIN
getAddress("0xeb4c2781e4eba804ce9a9803c67d0893436bb27d"), // renBTC getAddress("0xeb4c2781e4eba804ce9a9803c67d0893436bb27d"), // renBTC
getAddress("0x52d87F22192131636F93c5AB18d0127Ea52CB641"), // renLUNA getAddress("0x52d87F22192131636F93c5AB18d0127Ea52CB641"), // renLUNA
getAddress("0x459086f2376525bdceba5bdda135e4e9d3fef5bf"), // renBCH getAddress("0x459086f2376525bdceba5bdda135e4e9d3fef5bf"), // renBCH
@ -209,6 +210,7 @@ export const ETH_TOKENS_THAT_EXIST_ELSEWHERE = [
getAddress("0x3832d2F059E55934220881F831bE501D180671A7"), // renDOGE getAddress("0x3832d2F059E55934220881F831bE501D180671A7"), // renDOGE
getAddress("0x1c5db575e2ff833e46a2e9864c22f4b22e0b37c2"), // renZEC getAddress("0x1c5db575e2ff833e46a2e9864c22f4b22e0b37c2"), // renZEC
getAddress("0xD5147bc8e386d91Cc5DBE72099DAC6C9b99276F5"), // renFIL getAddress("0xD5147bc8e386d91Cc5DBE72099DAC6C9b99276F5"), // renFIL
getAddress("0xdac17f958d2ee523a2206206994597c13d831ec7"), // USDT
]; ];
export const MIGRATION_PROGRAM_ADDRESS = export const MIGRATION_PROGRAM_ADDRESS =

View File

@ -9,10 +9,13 @@ export const getNextRpcHost = () =>
export async function getSignedVAAWithRetry( export async function getSignedVAAWithRetry(
emitterChain: ChainId, emitterChain: ChainId,
emitterAddress: string, emitterAddress: string,
sequence: string sequence: string,
retryAttempts?: number
) { ) {
let result; let result;
let attempts = 0;
while (!result) { while (!result) {
attempts++;
await new Promise((resolve) => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
try { try {
result = await getSignedVAA( result = await getSignedVAA(
@ -22,7 +25,10 @@ export async function getSignedVAAWithRetry(
sequence sequence
); );
} catch (e) { } catch (e) {
console.log(e); console.log(`Attempt ${attempts}: `, e);
if (retryAttempts !== undefined && attempts > retryAttempts) {
throw e;
}
} }
} }
return result; return result;