bridge_ui: more safety checks and feedback messages

fixes https://github.com/certusone/wormhole/issues/372
fixes https://github.com/certusone/wormhole/issues/366

Change-Id: Ieefdd2f04e353d4a68204864bfa91e8e8ebafc30
This commit is contained in:
Evan Gray 2021-09-01 03:42:16 -04:00
parent e11e59095f
commit b234c223b8
17 changed files with 373 additions and 87 deletions

View File

@ -50,6 +50,7 @@
"@craco/craco": "^6.2.0",
"@truffle/hdwallet-provider": "^1.4.1",
"@types/node": "^16.6.1",
"@types/react-router-dom": "^5.1.8",
"prettier": "^2.3.2",
"truffle": "^5.4.1",
"wasm-loader": "^1.3.0"
@ -7882,6 +7883,12 @@
"@types/node": "*"
}
},
"node_modules/@types/history": {
"version": "4.7.9",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz",
"integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==",
"dev": true
},
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
@ -8080,6 +8087,27 @@
"@babel/runtime": "^7.9.2"
}
},
"node_modules/@types/react-router": {
"version": "5.1.16",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.16.tgz",
"integrity": "sha512-8d7nR/fNSqlTFGHti0R3F9WwIertOaaA1UEB8/jr5l5mDMOs4CidEgvvYMw4ivqrBK+vtVLxyTj2P+Pr/dtgzg==",
"dev": true,
"dependencies": {
"@types/history": "*",
"@types/react": "*"
}
},
"node_modules/@types/react-router-dom": {
"version": "5.1.8",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.8.tgz",
"integrity": "sha512-03xHyncBzG0PmDmf8pf3rehtjY0NpUj7TIN46FrT5n1ZWHPZvXz32gUyNboJ+xsL8cpg8bQVLcllptcQHvocrw==",
"dev": true,
"dependencies": {
"@types/history": "*",
"@types/react": "*",
"@types/react-router": "*"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz",
@ -45781,6 +45809,12 @@
"@types/node": "*"
}
},
"@types/history": {
"version": "4.7.9",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz",
"integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==",
"dev": true
},
"@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
@ -45988,6 +46022,27 @@
}
}
},
"@types/react-router": {
"version": "5.1.16",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.16.tgz",
"integrity": "sha512-8d7nR/fNSqlTFGHti0R3F9WwIertOaaA1UEB8/jr5l5mDMOs4CidEgvvYMw4ivqrBK+vtVLxyTj2P+Pr/dtgzg==",
"dev": true,
"requires": {
"@types/history": "*",
"@types/react": "*"
}
},
"@types/react-router-dom": {
"version": "5.1.8",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.8.tgz",
"integrity": "sha512-03xHyncBzG0PmDmf8pf3rehtjY0NpUj7TIN46FrT5n1ZWHPZvXz32gUyNboJ+xsL8cpg8bQVLcllptcQHvocrw==",
"dev": true,
"requires": {
"@types/history": "*",
"@types/react": "*",
"@types/react-router": "*"
}
},
"@types/react-transition-group": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz",

View File

@ -69,6 +69,7 @@
"@craco/craco": "^6.2.0",
"@truffle/hdwallet-provider": "^1.4.1",
"@types/node": "^16.6.1",
"@types/react-router-dom": "^5.1.8",
"prettier": "^2.3.2",
"truffle": "^5.4.1",
"wasm-loader": "^1.3.0"

View File

@ -5,6 +5,8 @@ import {
Link,
makeStyles,
Toolbar,
Tooltip,
Typography,
} from "@material-ui/core";
import { GitHub, Publish, Send } from "@material-ui/icons";
import { NavLink, Redirect, Route, Switch } from "react-router-dom";
@ -30,6 +32,9 @@ const useStyles = makeStyles((theme) => ({
...theme.typography.body1,
color: theme.palette.text.primary,
marginLeft: theme.spacing(6),
[theme.breakpoints.down("sm")]: {
marginLeft: theme.spacing(4),
},
[theme.breakpoints.down("xs")]: {
marginLeft: theme.spacing(2),
},
@ -57,39 +62,65 @@ function App() {
<div className={classes.spacer} />
<Hidden implementation="css" xsDown>
<div style={{ display: "flex", alignItems: "center" }}>
<Link component={NavLink} to="/transfer" className={classes.link}>
Transfer
</Link>
<Link component={NavLink} to="/attest" className={classes.link}>
Attest
</Link>
<IconButton
href="https://github.com/certusone/wormhole"
target="_blank"
size="small"
className={classes.link}
>
<GitHub />
</IconButton>
<Tooltip title="Coming Soon">
<Typography
className={classes.link}
style={{ color: "#ffffff80", cursor: "default" }}
>
NFTs
</Typography>
</Tooltip>
<Tooltip title="Transfer tokens to another blockchain">
<Link
component={NavLink}
to="/transfer"
className={classes.link}
>
Transfer
</Link>
</Tooltip>
<Tooltip title="Register a new wrapped token">
<Link
component={NavLink}
to="/register"
className={classes.link}
>
Register
</Link>
</Tooltip>
<Tooltip title="View the source code">
<IconButton
href="https://github.com/certusone/wormhole"
target="_blank"
size="small"
className={classes.link}
>
<GitHub />
</IconButton>
</Tooltip>
</div>
</Hidden>
<Hidden implementation="css" smUp>
<IconButton
component={NavLink}
to="/transfer"
size="small"
className={classes.link}
>
<Send />
</IconButton>
<IconButton
component={NavLink}
to="/attest"
size="small"
className={classes.link}
>
<Publish />
</IconButton>
<Tooltip title="Transfer tokens to another blockchain">
<IconButton
component={NavLink}
to="/transfer"
size="small"
className={classes.link}
>
<Send />
</IconButton>
</Tooltip>
<Tooltip title="Register a new wrapped token">
<IconButton
component={NavLink}
to="/register"
size="small"
className={classes.link}
>
<Publish />
</IconButton>
</Tooltip>
</Hidden>
</Toolbar>
</AppBar>
@ -98,7 +129,7 @@ function App() {
<Route exact path="/transfer">
<Transfer />
</Route>
<Route exact path="/attest">
<Route exact path="/register">
<Attest />
</Route>
<Route>

View File

@ -0,0 +1,29 @@
import { Typography } from "@material-ui/core";
import React from "react";
export default class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error(error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<Typography variant="h5" style={{ textAlign: "center", marginTop: 24 }}>
An unexpected error has occurred. Please refresh the page.
</Typography>
);
}
return this.props.children;
}
}

View File

@ -15,6 +15,7 @@ import Create from "./Create";
import Send from "./Send";
import Source from "./Source";
import Target from "./Target";
import { Alert } from "@material-ui/lab";
// TODO: ensure that both wallets are connected to the same known network
@ -24,6 +25,10 @@ function Attest() {
const signedVAAHex = useSelector(selectAttestSignedVAAHex);
return (
<Container maxWidth="md">
<Alert severity="info">
This form allows you to register a token on a new foreign chain. Tokens
must be registered before they can be transferred.
</Alert>
<Stepper activeStep={activeStep} orientation="vertical">
<Step>
<StepButton onClick={() => dispatch(setStep(0))}>
@ -54,7 +59,7 @@ function Attest() {
onClick={() => dispatch(setStep(3))}
disabled={!signedVAAHex}
>
Create wrapper
Create wrapped token
</StepButton>
<StepContent>
<Create />

View File

@ -1,3 +1,4 @@
import { ChainId, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
import { Typography } from "@material-ui/core";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
@ -5,26 +6,29 @@ import {
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
import { SOLANA_HOST } from "../utils/consts";
import { signSendAndConfirm } from "../utils/solana";
import ButtonWithLoader from "./ButtonWithLoader";
export default function SolanaCreateAssociatedAddress({
mintAddress,
readableTargetAddress,
}: {
mintAddress: string;
readableTargetAddress: string;
}) {
const [isCreating, setIsCreating] = useState(false);
export function useAssociatedAccountExistsState(
targetChain: ChainId,
mintAddress: string | null | undefined,
readableTargetAddress: string
) {
const [associatedAccountExists, setAssociatedAccountExists] = useState(true); // for now, assume it exists until we confirm it doesn't
const solanaWallet = useSolanaWallet();
const solPK = solanaWallet?.publicKey;
useEffect(() => {
setAssociatedAccountExists(true);
if (!mintAddress || !readableTargetAddress || !solPK) return;
if (
targetChain !== CHAIN_ID_SOLANA ||
!mintAddress ||
!readableTargetAddress ||
!solPK
)
return;
let cancelled = false;
(async () => {
// TODO: share connection in context?
@ -52,7 +56,27 @@ export default function SolanaCreateAssociatedAddress({
return () => {
cancelled = true;
};
}, [mintAddress, readableTargetAddress, solPK]);
}, [targetChain, mintAddress, readableTargetAddress, solPK]);
return useMemo(
() => ({ associatedAccountExists, setAssociatedAccountExists }),
[associatedAccountExists]
);
}
export default function SolanaCreateAssociatedAddress({
mintAddress,
readableTargetAddress,
associatedAccountExists,
setAssociatedAccountExists,
}: {
mintAddress: string;
readableTargetAddress: string;
associatedAccountExists: boolean;
setAssociatedAccountExists: (associatedAccountExists: boolean) => void;
}) {
const [isCreating, setIsCreating] = useState(false);
const solanaWallet = useSolanaWallet();
const solPK = solanaWallet?.publicKey;
const handleClick = useCallback(() => {
if (
associatedAccountExists ||
@ -100,6 +124,7 @@ export default function SolanaCreateAssociatedAddress({
})();
}, [
associatedAccountExists,
setAssociatedAccountExists,
mintAddress,
solPK,
readableTargetAddress,

View File

@ -0,0 +1,21 @@
import { makeStyles, Typography } from "@material-ui/core";
import { ReactChild } from "react";
const useStyles = makeStyles((theme) => ({
description: {
marginBottom: theme.spacing(4),
},
}));
export default function StepDescription({
children,
}: {
children: ReactChild;
}) {
const classes = useStyles();
return (
<Typography component="div" variant="body2" className={classes.description}>
{children}
</Typography>
);
}

View File

@ -417,15 +417,20 @@ function RecoveryDialogContent({ onClose }: { onClose: () => void }) {
);
}
export default function Recovery() {
export default function Recovery({
open,
setOpen,
}: {
open: boolean;
setOpen: (open: boolean) => void;
}) {
const classes = useStyles();
const [open, setOpen] = useState(false);
const handleOpenClick = useCallback(() => {
setOpen(true);
}, []);
}, [setOpen]);
const handleCloseClick = useCallback(() => {
setOpen(false);
}, []);
}, [setOpen]);
return (
<>
<Fab className={classes.fab} onClick={handleOpenClick}>

View File

@ -4,6 +4,7 @@ import useIsWalletReady from "../../hooks/useIsWalletReady";
import { selectTransferTargetChain } from "../../store/selectors";
import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
import StepDescription from "../StepDescription";
function Redeem() {
const { handleClick, disabled, showLoader } = useHandleRedeem();
@ -11,6 +12,7 @@ function Redeem() {
const { isReady, statusMessage } = useIsWalletReady(targetChain);
return (
<>
<StepDescription>Receive the tokens on the target chain</StepDescription>
<KeyAndBalance chainId={targetChain} />
<ButtonWithLoader
disabled={!isReady || disabled}

View File

@ -0,0 +1,47 @@
import { Button } from "@material-ui/core";
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useHistory } from "react-router-dom";
import {
setSourceAsset,
setSourceChain,
setStep,
setTargetChain,
} from "../../store/attestSlice";
import {
selectAttestSignedVAAHex,
selectTransferSourceAsset,
selectTransferSourceChain,
selectTransferTargetChain,
} from "../../store/selectors";
export default function RegisterNowButton() {
const dispatch = useDispatch();
const history = useHistory();
const sourceChain = useSelector(selectTransferSourceChain);
const sourceAsset = useSelector(selectTransferSourceAsset);
const targetChain = useSelector(selectTransferTargetChain);
// user might be in the middle of a different attest
const signedVAAHex = useSelector(selectAttestSignedVAAHex);
const canSwitch = sourceAsset && !signedVAAHex;
const handleClick = useCallback(() => {
if (sourceAsset && canSwitch) {
dispatch(setSourceChain(sourceChain));
dispatch(setSourceAsset(sourceAsset));
dispatch(setTargetChain(targetChain));
dispatch(setStep(2));
history.push("/register");
}
}, [dispatch, canSwitch, sourceChain, sourceAsset, targetChain, history]);
if (!canSwitch) return null;
return (
<Button
variant="outlined"
size="small"
style={{ display: "block", margin: "4px auto 0px" }}
onClick={handleClick}
>
Register Now
</Button>
);
}

View File

@ -1,3 +1,4 @@
import { Alert } from "@material-ui/lab";
import { useSelector } from "react-redux";
import { useHandleTransfer } from "../../hooks/useHandleTransfer";
import useIsWalletReady from "../../hooks/useIsWalletReady";
@ -5,8 +6,10 @@ import {
selectTransferSourceChain,
selectTransferTargetError,
} from "../../store/selectors";
import { CHAINS_BY_ID } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
import StepDescription from "../StepDescription";
function Send() {
const { handleClick, disabled, showLoader } = useHandleTransfer();
@ -15,7 +18,14 @@ function Send() {
const { isReady, statusMessage } = useIsWalletReady(sourceChain);
return (
<>
<StepDescription>Transfer the tokens to the worm bridge.</StepDescription>
<KeyAndBalance chainId={sourceChain} />
<Alert severity="warning">
This will initiate the transfer on {CHAINS_BY_ID[sourceChain].name} and
wait for finalization. If you navigate away from this page before
completing Step 4, you will have to perform the recovery workflow to
complete the transfer.
</Alert>
<ButtonWithLoader
disabled={!isReady || disabled}
onClick={handleClick}

View File

@ -1,4 +1,5 @@
import { makeStyles, MenuItem, TextField } from "@material-ui/core";
import { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
import { Restore } from "@material-ui/icons";
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import useIsWalletReady from "../../hooks/useIsWalletReady";
@ -18,6 +19,7 @@ import {
import { CHAINS } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
import StepDescription from "../StepDescription";
import { TokenSelector } from "../TokenSelectors/SourceTokenSelector";
const useStyles = makeStyles((theme) => ({
@ -26,7 +28,11 @@ const useStyles = makeStyles((theme) => ({
},
}));
function Source() {
function Source({
setIsRecoveryOpen,
}: {
setIsRecoveryOpen: (open: boolean) => void;
}) {
const classes = useStyles();
const dispatch = useDispatch();
const sourceChain = useSelector(selectTransferSourceChain);
@ -53,6 +59,20 @@ function Source() {
}, [dispatch]);
return (
<>
<StepDescription>
<div style={{ display: "flex", alignItems: "center" }}>
Select tokens to send through the worm bridge.
<div style={{ flexGrow: 1 }} />
<Button
onClick={() => setIsRecoveryOpen(true)}
size="small"
variant="outlined"
endIcon={<Restore />}
>
Perform Recovery
</Button>
</div>
</StepDescription>
<TextField
select
fullWidth
@ -72,7 +92,6 @@ function Source() {
<TokenSelector disabled={shouldLockFields} />
</div>
) : null}
{/* TODO: token list for eth, check own */}
<TextField
label="Amount"
type="number"

View File

@ -13,13 +13,18 @@ import {
selectTransferTargetBalanceString,
selectTransferTargetChain,
selectTransferTargetError,
UNREGISTERED_ERROR_MESSAGE,
} from "../../store/selectors";
import { incrementStep, setTargetChain } from "../../store/transferSlice";
import { hexToNativeString } from "../../utils/array";
import { CHAINS } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
import SolanaCreateAssociatedAddress from "../SolanaCreateAssociatedAddress";
import SolanaCreateAssociatedAddress, {
useAssociatedAccountExistsState,
} from "../SolanaCreateAssociatedAddress";
import StepDescription from "../StepDescription";
import RegisterNowButton from "./RegisterNowButton";
const useStyles = makeStyles((theme) => ({
transferField: {
@ -45,6 +50,12 @@ function Target() {
const isTargetComplete = useSelector(selectTransferIsTargetComplete);
const shouldLockFields = useSelector(selectTransferShouldLockFields);
const { statusMessage } = useIsWalletReady(targetChain);
const { associatedAccountExists, setAssociatedAccountExists } =
useAssociatedAccountExistsState(
targetChain,
targetAsset,
readableTargetAddress
);
useSyncTargetAddress(!shouldLockFields);
const handleTargetChange = useCallback(
(event) => {
@ -57,6 +68,7 @@ function Target() {
}, [dispatch]);
return (
<>
<StepDescription>Select a recipient chain and address.</StepDescription>
<TextField
select
fullWidth
@ -72,7 +84,7 @@ function Target() {
</TextField>
<KeyAndBalance chainId={targetChain} balance={uiAmountString} />
<TextField
label="Address"
label="Recipient Address"
fullWidth
className={classes.transferField}
value={readableTargetAddress}
@ -82,23 +94,28 @@ function Target() {
<SolanaCreateAssociatedAddress
mintAddress={targetAsset}
readableTargetAddress={readableTargetAddress}
associatedAccountExists={associatedAccountExists}
setAssociatedAccountExists={setAssociatedAccountExists}
/>
) : null}
<TextField
label="Asset"
label="Token Address"
fullWidth
className={classes.transferField}
value={targetAsset || ""}
disabled={true}
/>
<ButtonWithLoader
disabled={!isTargetComplete}
disabled={!isTargetComplete || !associatedAccountExists}
onClick={handleNextClick}
showLoader={false}
error={statusMessage || error}
>
Next
</ButtonWithLoader>
{!statusMessage && error === UNREGISTERED_ERROR_MESSAGE ? (
<RegisterNowButton />
) : null}
</>
);
}

View File

@ -5,13 +5,16 @@ import {
StepContent,
Stepper,
} from "@material-ui/core";
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import useCheckIfWormholeWrapped from "../../hooks/useCheckIfWormholeWrapped";
import useFetchTargetAsset from "../../hooks/useFetchTargetAsset";
import useGetBalanceEffect from "../../hooks/useGetBalanceEffect";
import {
selectTransferActiveStep,
selectTransferSignedVAAHex,
selectTransferIsRedeeming,
selectTransferIsSendComplete,
selectTransferIsSending,
} from "../../store/selectors";
import { setStep } from "../../store/transferSlice";
import Recovery from "./Recovery";
@ -29,26 +32,32 @@ function Transfer() {
useCheckIfWormholeWrapped();
useFetchTargetAsset();
useGetBalanceEffect("target");
const [isRecoveryOpen, setIsRecoveryOpen] = useState(false);
const dispatch = useDispatch();
const activeStep = useSelector(selectTransferActiveStep);
const signedVAAHex = useSelector(selectTransferSignedVAAHex);
const isSending = useSelector(selectTransferIsSending);
const isSendComplete = useSelector(selectTransferIsSendComplete);
const isRedeeming = useSelector(selectTransferIsRedeeming);
const preventNavigation = isSending || isSendComplete || isRedeeming;
useEffect(() => {
if (preventNavigation) {
window.onbeforeunload = () => true;
return () => {
window.onbeforeunload = null;
};
}
}, [preventNavigation]);
return (
<Container maxWidth="md">
<Stepper activeStep={activeStep} orientation="vertical">
<Step>
<StepButton onClick={() => dispatch(setStep(0))}>
Select a source
</StepButton>
<StepButton onClick={() => dispatch(setStep(0))}>Source</StepButton>
<StepContent>
<Source />
<Source setIsRecoveryOpen={setIsRecoveryOpen} />
</StepContent>
</Step>
<Step>
<StepButton onClick={() => dispatch(setStep(1))}>
Select a target
</StepButton>
<StepButton onClick={() => dispatch(setStep(1))}>Target</StepButton>
<StepContent>
<Target />
</StepContent>
@ -64,7 +73,7 @@ function Transfer() {
<Step>
<StepButton
onClick={() => dispatch(setStep(3))}
disabled={!signedVAAHex}
disabled={!isSendComplete}
>
Redeem tokens
</StepButton>
@ -73,7 +82,7 @@ function Transfer() {
</StepContent>
</Step>
</Stepper>
<Recovery />
<Recovery open={isRecoveryOpen} setOpen={setIsRecoveryOpen} />
</Container>
);
}

View File

@ -108,6 +108,7 @@ const getEthereumAccountsCovalent = async (
if (tokens instanceof Array && tokens.length) {
for (const item of tokens) {
// TODO: filter?
if (
item.contract_decimals &&
item.contract_ticker_symbol &&

View File

@ -9,26 +9,31 @@ import RadialGradient from "./components/RadialGradient";
import { EthereumProviderProvider } from "./contexts/EthereumProviderContext";
import { SolanaWalletProvider } from "./contexts/SolanaWalletContext.tsx";
import { TerraWalletProvider } from "./contexts/TerraWalletContext.tsx";
import ErrorBoundary from "./ErrorBoundary";
import { theme } from "./muiTheme";
import { store } from "./store";
ReactDOM.render(
<Provider store={store}>
<ThemeProvider theme={theme}>
<CssBaseline />
<RadialGradient />
<SnackbarProvider maxSnack={3}>
<SolanaWalletProvider>
<EthereumProviderProvider>
<TerraWalletProvider>
<HashRouter>
<App />
</HashRouter>
</TerraWalletProvider>
</EthereumProviderProvider>
</SolanaWalletProvider>
</SnackbarProvider>
</ThemeProvider>
</Provider>,
<ErrorBoundary>
<Provider store={store}>
<ThemeProvider theme={theme}>
<CssBaseline />
<RadialGradient />
<ErrorBoundary>
<SnackbarProvider maxSnack={3}>
<SolanaWalletProvider>
<EthereumProviderProvider>
<TerraWalletProvider>
<HashRouter>
<App />
</HashRouter>
</TerraWalletProvider>
</EthereumProviderProvider>
</SolanaWalletProvider>
</SnackbarProvider>
</ErrorBoundary>
</ThemeProvider>
</Provider>
</ErrorBoundary>,
document.getElementById("root")
);

View File

@ -71,7 +71,9 @@ export const selectTransferIsSending = (state: RootState) =>
state.transfer.isSending;
export const selectTransferIsRedeeming = (state: RootState) =>
state.transfer.isRedeeming;
export const selectTransferSourceError = (state: RootState) => {
export const selectTransferSourceError = (
state: RootState
): string | undefined => {
if (!state.transfer.sourceChain) {
return "Select a source chain";
}
@ -127,6 +129,8 @@ export const selectTransferSourceError = (state: RootState) => {
};
export const selectTransferIsSourceComplete = (state: RootState) =>
!selectTransferSourceError(state);
export const UNREGISTERED_ERROR_MESSAGE =
"Target asset unavailable. Is the token registered?";
export const selectTransferTargetError = (state: RootState) => {
const sourceError = selectTransferSourceError(state);
if (sourceError) {
@ -136,13 +140,13 @@ export const selectTransferTargetError = (state: RootState) => {
return "Select a target chain";
}
if (!state.transfer.targetAsset) {
return "Target asset unavailable. Is the token attested?";
return UNREGISTERED_ERROR_MESSAGE;
}
if (
state.transfer.targetChain === CHAIN_ID_ETH &&
state.transfer.targetAsset === ethers.constants.AddressZero
) {
return "Target asset unavailable. Is the token attested?";
return UNREGISTERED_ERROR_MESSAGE;
}
if (!state.transfer.targetAddressHex) {
return "Target account unavailable";