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:
parent
e11e59095f
commit
b234c223b8
|
@ -50,6 +50,7 @@
|
||||||
"@craco/craco": "^6.2.0",
|
"@craco/craco": "^6.2.0",
|
||||||
"@truffle/hdwallet-provider": "^1.4.1",
|
"@truffle/hdwallet-provider": "^1.4.1",
|
||||||
"@types/node": "^16.6.1",
|
"@types/node": "^16.6.1",
|
||||||
|
"@types/react-router-dom": "^5.1.8",
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.3.2",
|
||||||
"truffle": "^5.4.1",
|
"truffle": "^5.4.1",
|
||||||
"wasm-loader": "^1.3.0"
|
"wasm-loader": "^1.3.0"
|
||||||
|
@ -7882,6 +7883,12 @@
|
||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/@types/hoist-non-react-statics": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
|
"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"
|
"@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": {
|
"node_modules/@types/react-transition-group": {
|
||||||
"version": "4.4.2",
|
"version": "4.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz",
|
||||||
|
@ -45781,6 +45809,12 @@
|
||||||
"@types/node": "*"
|
"@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": {
|
"@types/hoist-non-react-statics": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
|
"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": {
|
"@types/react-transition-group": {
|
||||||
"version": "4.4.2",
|
"version": "4.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz",
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"@craco/craco": "^6.2.0",
|
"@craco/craco": "^6.2.0",
|
||||||
"@truffle/hdwallet-provider": "^1.4.1",
|
"@truffle/hdwallet-provider": "^1.4.1",
|
||||||
"@types/node": "^16.6.1",
|
"@types/node": "^16.6.1",
|
||||||
|
"@types/react-router-dom": "^5.1.8",
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.3.2",
|
||||||
"truffle": "^5.4.1",
|
"truffle": "^5.4.1",
|
||||||
"wasm-loader": "^1.3.0"
|
"wasm-loader": "^1.3.0"
|
||||||
|
|
|
@ -5,6 +5,8 @@ import {
|
||||||
Link,
|
Link,
|
||||||
makeStyles,
|
makeStyles,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
import { GitHub, Publish, Send } from "@material-ui/icons";
|
import { GitHub, Publish, Send } from "@material-ui/icons";
|
||||||
import { NavLink, Redirect, Route, Switch } from "react-router-dom";
|
import { NavLink, Redirect, Route, Switch } from "react-router-dom";
|
||||||
|
@ -30,6 +32,9 @@ const useStyles = makeStyles((theme) => ({
|
||||||
...theme.typography.body1,
|
...theme.typography.body1,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
marginLeft: theme.spacing(6),
|
marginLeft: theme.spacing(6),
|
||||||
|
[theme.breakpoints.down("sm")]: {
|
||||||
|
marginLeft: theme.spacing(4),
|
||||||
|
},
|
||||||
[theme.breakpoints.down("xs")]: {
|
[theme.breakpoints.down("xs")]: {
|
||||||
marginLeft: theme.spacing(2),
|
marginLeft: theme.spacing(2),
|
||||||
},
|
},
|
||||||
|
@ -57,12 +62,33 @@ function App() {
|
||||||
<div className={classes.spacer} />
|
<div className={classes.spacer} />
|
||||||
<Hidden implementation="css" xsDown>
|
<Hidden implementation="css" xsDown>
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
<Link component={NavLink} to="/transfer" className={classes.link}>
|
<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
|
Transfer
|
||||||
</Link>
|
</Link>
|
||||||
<Link component={NavLink} to="/attest" className={classes.link}>
|
</Tooltip>
|
||||||
Attest
|
<Tooltip title="Register a new wrapped token">
|
||||||
|
<Link
|
||||||
|
component={NavLink}
|
||||||
|
to="/register"
|
||||||
|
className={classes.link}
|
||||||
|
>
|
||||||
|
Register
|
||||||
</Link>
|
</Link>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="View the source code">
|
||||||
<IconButton
|
<IconButton
|
||||||
href="https://github.com/certusone/wormhole"
|
href="https://github.com/certusone/wormhole"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -71,9 +97,11 @@ function App() {
|
||||||
>
|
>
|
||||||
<GitHub />
|
<GitHub />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</Hidden>
|
</Hidden>
|
||||||
<Hidden implementation="css" smUp>
|
<Hidden implementation="css" smUp>
|
||||||
|
<Tooltip title="Transfer tokens to another blockchain">
|
||||||
<IconButton
|
<IconButton
|
||||||
component={NavLink}
|
component={NavLink}
|
||||||
to="/transfer"
|
to="/transfer"
|
||||||
|
@ -82,14 +110,17 @@ function App() {
|
||||||
>
|
>
|
||||||
<Send />
|
<Send />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Register a new wrapped token">
|
||||||
<IconButton
|
<IconButton
|
||||||
component={NavLink}
|
component={NavLink}
|
||||||
to="/attest"
|
to="/register"
|
||||||
size="small"
|
size="small"
|
||||||
className={classes.link}
|
className={classes.link}
|
||||||
>
|
>
|
||||||
<Publish />
|
<Publish />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
</Hidden>
|
</Hidden>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
|
@ -98,7 +129,7 @@ function App() {
|
||||||
<Route exact path="/transfer">
|
<Route exact path="/transfer">
|
||||||
<Transfer />
|
<Transfer />
|
||||||
</Route>
|
</Route>
|
||||||
<Route exact path="/attest">
|
<Route exact path="/register">
|
||||||
<Attest />
|
<Attest />
|
||||||
</Route>
|
</Route>
|
||||||
<Route>
|
<Route>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import Create from "./Create";
|
||||||
import Send from "./Send";
|
import Send from "./Send";
|
||||||
import Source from "./Source";
|
import Source from "./Source";
|
||||||
import Target from "./Target";
|
import Target from "./Target";
|
||||||
|
import { Alert } from "@material-ui/lab";
|
||||||
|
|
||||||
// TODO: ensure that both wallets are connected to the same known network
|
// TODO: ensure that both wallets are connected to the same known network
|
||||||
|
|
||||||
|
@ -24,6 +25,10 @@ function Attest() {
|
||||||
const signedVAAHex = useSelector(selectAttestSignedVAAHex);
|
const signedVAAHex = useSelector(selectAttestSignedVAAHex);
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="md">
|
<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">
|
<Stepper activeStep={activeStep} orientation="vertical">
|
||||||
<Step>
|
<Step>
|
||||||
<StepButton onClick={() => dispatch(setStep(0))}>
|
<StepButton onClick={() => dispatch(setStep(0))}>
|
||||||
|
@ -54,7 +59,7 @@ function Attest() {
|
||||||
onClick={() => dispatch(setStep(3))}
|
onClick={() => dispatch(setStep(3))}
|
||||||
disabled={!signedVAAHex}
|
disabled={!signedVAAHex}
|
||||||
>
|
>
|
||||||
Create wrapper
|
Create wrapped token
|
||||||
</StepButton>
|
</StepButton>
|
||||||
<StepContent>
|
<StepContent>
|
||||||
<Create />
|
<Create />
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { ChainId, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
||||||
import { Typography } from "@material-ui/core";
|
import { Typography } from "@material-ui/core";
|
||||||
import {
|
import {
|
||||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
|
@ -5,26 +6,29 @@ import {
|
||||||
TOKEN_PROGRAM_ID,
|
TOKEN_PROGRAM_ID,
|
||||||
} from "@solana/spl-token";
|
} from "@solana/spl-token";
|
||||||
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
|
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 { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||||
import { SOLANA_HOST } from "../utils/consts";
|
import { SOLANA_HOST } from "../utils/consts";
|
||||||
import { signSendAndConfirm } from "../utils/solana";
|
import { signSendAndConfirm } from "../utils/solana";
|
||||||
import ButtonWithLoader from "./ButtonWithLoader";
|
import ButtonWithLoader from "./ButtonWithLoader";
|
||||||
|
|
||||||
export default function SolanaCreateAssociatedAddress({
|
export function useAssociatedAccountExistsState(
|
||||||
mintAddress,
|
targetChain: ChainId,
|
||||||
readableTargetAddress,
|
mintAddress: string | null | undefined,
|
||||||
}: {
|
readableTargetAddress: string
|
||||||
mintAddress: string;
|
) {
|
||||||
readableTargetAddress: string;
|
|
||||||
}) {
|
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
|
||||||
const [associatedAccountExists, setAssociatedAccountExists] = useState(true); // for now, assume it exists until we confirm it doesn't
|
const [associatedAccountExists, setAssociatedAccountExists] = useState(true); // for now, assume it exists until we confirm it doesn't
|
||||||
const solanaWallet = useSolanaWallet();
|
const solanaWallet = useSolanaWallet();
|
||||||
const solPK = solanaWallet?.publicKey;
|
const solPK = solanaWallet?.publicKey;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAssociatedAccountExists(true);
|
setAssociatedAccountExists(true);
|
||||||
if (!mintAddress || !readableTargetAddress || !solPK) return;
|
if (
|
||||||
|
targetChain !== CHAIN_ID_SOLANA ||
|
||||||
|
!mintAddress ||
|
||||||
|
!readableTargetAddress ||
|
||||||
|
!solPK
|
||||||
|
)
|
||||||
|
return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
(async () => {
|
(async () => {
|
||||||
// TODO: share connection in context?
|
// TODO: share connection in context?
|
||||||
|
@ -52,7 +56,27 @@ export default function SolanaCreateAssociatedAddress({
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
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(() => {
|
const handleClick = useCallback(() => {
|
||||||
if (
|
if (
|
||||||
associatedAccountExists ||
|
associatedAccountExists ||
|
||||||
|
@ -100,6 +124,7 @@ export default function SolanaCreateAssociatedAddress({
|
||||||
})();
|
})();
|
||||||
}, [
|
}, [
|
||||||
associatedAccountExists,
|
associatedAccountExists,
|
||||||
|
setAssociatedAccountExists,
|
||||||
mintAddress,
|
mintAddress,
|
||||||
solPK,
|
solPK,
|
||||||
readableTargetAddress,
|
readableTargetAddress,
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 classes = useStyles();
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const handleOpenClick = useCallback(() => {
|
const handleOpenClick = useCallback(() => {
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}, []);
|
}, [setOpen]);
|
||||||
const handleCloseClick = useCallback(() => {
|
const handleCloseClick = useCallback(() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}, []);
|
}, [setOpen]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Fab className={classes.fab} onClick={handleOpenClick}>
|
<Fab className={classes.fab} onClick={handleOpenClick}>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||||
import { selectTransferTargetChain } from "../../store/selectors";
|
import { selectTransferTargetChain } from "../../store/selectors";
|
||||||
import ButtonWithLoader from "../ButtonWithLoader";
|
import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
|
import StepDescription from "../StepDescription";
|
||||||
|
|
||||||
function Redeem() {
|
function Redeem() {
|
||||||
const { handleClick, disabled, showLoader } = useHandleRedeem();
|
const { handleClick, disabled, showLoader } = useHandleRedeem();
|
||||||
|
@ -11,6 +12,7 @@ function Redeem() {
|
||||||
const { isReady, statusMessage } = useIsWalletReady(targetChain);
|
const { isReady, statusMessage } = useIsWalletReady(targetChain);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<StepDescription>Receive the tokens on the target chain</StepDescription>
|
||||||
<KeyAndBalance chainId={targetChain} />
|
<KeyAndBalance chainId={targetChain} />
|
||||||
<ButtonWithLoader
|
<ButtonWithLoader
|
||||||
disabled={!isReady || disabled}
|
disabled={!isReady || disabled}
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Alert } from "@material-ui/lab";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { useHandleTransfer } from "../../hooks/useHandleTransfer";
|
import { useHandleTransfer } from "../../hooks/useHandleTransfer";
|
||||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||||
|
@ -5,8 +6,10 @@ import {
|
||||||
selectTransferSourceChain,
|
selectTransferSourceChain,
|
||||||
selectTransferTargetError,
|
selectTransferTargetError,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
|
import { CHAINS_BY_ID } from "../../utils/consts";
|
||||||
import ButtonWithLoader from "../ButtonWithLoader";
|
import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
|
import StepDescription from "../StepDescription";
|
||||||
|
|
||||||
function Send() {
|
function Send() {
|
||||||
const { handleClick, disabled, showLoader } = useHandleTransfer();
|
const { handleClick, disabled, showLoader } = useHandleTransfer();
|
||||||
|
@ -15,7 +18,14 @@ function Send() {
|
||||||
const { isReady, statusMessage } = useIsWalletReady(sourceChain);
|
const { isReady, statusMessage } = useIsWalletReady(sourceChain);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<StepDescription>Transfer the tokens to the worm bridge.</StepDescription>
|
||||||
<KeyAndBalance chainId={sourceChain} />
|
<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
|
<ButtonWithLoader
|
||||||
disabled={!isReady || disabled}
|
disabled={!isReady || disabled}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
|
|
@ -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 { useCallback } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||||
|
@ -18,6 +19,7 @@ import {
|
||||||
import { CHAINS } from "../../utils/consts";
|
import { CHAINS } from "../../utils/consts";
|
||||||
import ButtonWithLoader from "../ButtonWithLoader";
|
import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
|
import StepDescription from "../StepDescription";
|
||||||
import { TokenSelector } from "../TokenSelectors/SourceTokenSelector";
|
import { TokenSelector } from "../TokenSelectors/SourceTokenSelector";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
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 classes = useStyles();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const sourceChain = useSelector(selectTransferSourceChain);
|
const sourceChain = useSelector(selectTransferSourceChain);
|
||||||
|
@ -53,6 +59,20 @@ function Source() {
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
return (
|
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
|
<TextField
|
||||||
select
|
select
|
||||||
fullWidth
|
fullWidth
|
||||||
|
@ -72,7 +92,6 @@ function Source() {
|
||||||
<TokenSelector disabled={shouldLockFields} />
|
<TokenSelector disabled={shouldLockFields} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{/* TODO: token list for eth, check own */}
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Amount"
|
label="Amount"
|
||||||
type="number"
|
type="number"
|
||||||
|
|
|
@ -13,13 +13,18 @@ import {
|
||||||
selectTransferTargetBalanceString,
|
selectTransferTargetBalanceString,
|
||||||
selectTransferTargetChain,
|
selectTransferTargetChain,
|
||||||
selectTransferTargetError,
|
selectTransferTargetError,
|
||||||
|
UNREGISTERED_ERROR_MESSAGE,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
import { incrementStep, setTargetChain } from "../../store/transferSlice";
|
import { incrementStep, setTargetChain } from "../../store/transferSlice";
|
||||||
import { hexToNativeString } from "../../utils/array";
|
import { hexToNativeString } from "../../utils/array";
|
||||||
import { CHAINS } from "../../utils/consts";
|
import { CHAINS } from "../../utils/consts";
|
||||||
import ButtonWithLoader from "../ButtonWithLoader";
|
import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
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) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
transferField: {
|
transferField: {
|
||||||
|
@ -45,6 +50,12 @@ function Target() {
|
||||||
const isTargetComplete = useSelector(selectTransferIsTargetComplete);
|
const isTargetComplete = useSelector(selectTransferIsTargetComplete);
|
||||||
const shouldLockFields = useSelector(selectTransferShouldLockFields);
|
const shouldLockFields = useSelector(selectTransferShouldLockFields);
|
||||||
const { statusMessage } = useIsWalletReady(targetChain);
|
const { statusMessage } = useIsWalletReady(targetChain);
|
||||||
|
const { associatedAccountExists, setAssociatedAccountExists } =
|
||||||
|
useAssociatedAccountExistsState(
|
||||||
|
targetChain,
|
||||||
|
targetAsset,
|
||||||
|
readableTargetAddress
|
||||||
|
);
|
||||||
useSyncTargetAddress(!shouldLockFields);
|
useSyncTargetAddress(!shouldLockFields);
|
||||||
const handleTargetChange = useCallback(
|
const handleTargetChange = useCallback(
|
||||||
(event) => {
|
(event) => {
|
||||||
|
@ -57,6 +68,7 @@ function Target() {
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<StepDescription>Select a recipient chain and address.</StepDescription>
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
fullWidth
|
fullWidth
|
||||||
|
@ -72,7 +84,7 @@ function Target() {
|
||||||
</TextField>
|
</TextField>
|
||||||
<KeyAndBalance chainId={targetChain} balance={uiAmountString} />
|
<KeyAndBalance chainId={targetChain} balance={uiAmountString} />
|
||||||
<TextField
|
<TextField
|
||||||
label="Address"
|
label="Recipient Address"
|
||||||
fullWidth
|
fullWidth
|
||||||
className={classes.transferField}
|
className={classes.transferField}
|
||||||
value={readableTargetAddress}
|
value={readableTargetAddress}
|
||||||
|
@ -82,23 +94,28 @@ function Target() {
|
||||||
<SolanaCreateAssociatedAddress
|
<SolanaCreateAssociatedAddress
|
||||||
mintAddress={targetAsset}
|
mintAddress={targetAsset}
|
||||||
readableTargetAddress={readableTargetAddress}
|
readableTargetAddress={readableTargetAddress}
|
||||||
|
associatedAccountExists={associatedAccountExists}
|
||||||
|
setAssociatedAccountExists={setAssociatedAccountExists}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<TextField
|
<TextField
|
||||||
label="Asset"
|
label="Token Address"
|
||||||
fullWidth
|
fullWidth
|
||||||
className={classes.transferField}
|
className={classes.transferField}
|
||||||
value={targetAsset || ""}
|
value={targetAsset || ""}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
/>
|
/>
|
||||||
<ButtonWithLoader
|
<ButtonWithLoader
|
||||||
disabled={!isTargetComplete}
|
disabled={!isTargetComplete || !associatedAccountExists}
|
||||||
onClick={handleNextClick}
|
onClick={handleNextClick}
|
||||||
showLoader={false}
|
showLoader={false}
|
||||||
error={statusMessage || error}
|
error={statusMessage || error}
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
</ButtonWithLoader>
|
</ButtonWithLoader>
|
||||||
|
{!statusMessage && error === UNREGISTERED_ERROR_MESSAGE ? (
|
||||||
|
<RegisterNowButton />
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,16 @@ import {
|
||||||
StepContent,
|
StepContent,
|
||||||
Stepper,
|
Stepper,
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import useCheckIfWormholeWrapped from "../../hooks/useCheckIfWormholeWrapped";
|
import useCheckIfWormholeWrapped from "../../hooks/useCheckIfWormholeWrapped";
|
||||||
import useFetchTargetAsset from "../../hooks/useFetchTargetAsset";
|
import useFetchTargetAsset from "../../hooks/useFetchTargetAsset";
|
||||||
import useGetBalanceEffect from "../../hooks/useGetBalanceEffect";
|
import useGetBalanceEffect from "../../hooks/useGetBalanceEffect";
|
||||||
import {
|
import {
|
||||||
selectTransferActiveStep,
|
selectTransferActiveStep,
|
||||||
selectTransferSignedVAAHex,
|
selectTransferIsRedeeming,
|
||||||
|
selectTransferIsSendComplete,
|
||||||
|
selectTransferIsSending,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
import { setStep } from "../../store/transferSlice";
|
import { setStep } from "../../store/transferSlice";
|
||||||
import Recovery from "./Recovery";
|
import Recovery from "./Recovery";
|
||||||
|
@ -29,26 +32,32 @@ function Transfer() {
|
||||||
useCheckIfWormholeWrapped();
|
useCheckIfWormholeWrapped();
|
||||||
useFetchTargetAsset();
|
useFetchTargetAsset();
|
||||||
useGetBalanceEffect("target");
|
useGetBalanceEffect("target");
|
||||||
|
const [isRecoveryOpen, setIsRecoveryOpen] = useState(false);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const activeStep = useSelector(selectTransferActiveStep);
|
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 (
|
return (
|
||||||
<Container maxWidth="md">
|
<Container maxWidth="md">
|
||||||
<Stepper activeStep={activeStep} orientation="vertical">
|
<Stepper activeStep={activeStep} orientation="vertical">
|
||||||
<Step>
|
<Step>
|
||||||
<StepButton onClick={() => dispatch(setStep(0))}>
|
<StepButton onClick={() => dispatch(setStep(0))}>Source</StepButton>
|
||||||
Select a source
|
|
||||||
</StepButton>
|
|
||||||
<StepContent>
|
<StepContent>
|
||||||
<Source />
|
<Source setIsRecoveryOpen={setIsRecoveryOpen} />
|
||||||
</StepContent>
|
</StepContent>
|
||||||
</Step>
|
</Step>
|
||||||
<Step>
|
<Step>
|
||||||
<StepButton onClick={() => dispatch(setStep(1))}>
|
<StepButton onClick={() => dispatch(setStep(1))}>Target</StepButton>
|
||||||
Select a target
|
|
||||||
</StepButton>
|
|
||||||
<StepContent>
|
<StepContent>
|
||||||
<Target />
|
<Target />
|
||||||
</StepContent>
|
</StepContent>
|
||||||
|
@ -64,7 +73,7 @@ function Transfer() {
|
||||||
<Step>
|
<Step>
|
||||||
<StepButton
|
<StepButton
|
||||||
onClick={() => dispatch(setStep(3))}
|
onClick={() => dispatch(setStep(3))}
|
||||||
disabled={!signedVAAHex}
|
disabled={!isSendComplete}
|
||||||
>
|
>
|
||||||
Redeem tokens
|
Redeem tokens
|
||||||
</StepButton>
|
</StepButton>
|
||||||
|
@ -73,7 +82,7 @@ function Transfer() {
|
||||||
</StepContent>
|
</StepContent>
|
||||||
</Step>
|
</Step>
|
||||||
</Stepper>
|
</Stepper>
|
||||||
<Recovery />
|
<Recovery open={isRecoveryOpen} setOpen={setIsRecoveryOpen} />
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,6 +108,7 @@ const getEthereumAccountsCovalent = async (
|
||||||
|
|
||||||
if (tokens instanceof Array && tokens.length) {
|
if (tokens instanceof Array && tokens.length) {
|
||||||
for (const item of tokens) {
|
for (const item of tokens) {
|
||||||
|
// TODO: filter?
|
||||||
if (
|
if (
|
||||||
item.contract_decimals &&
|
item.contract_decimals &&
|
||||||
item.contract_ticker_symbol &&
|
item.contract_ticker_symbol &&
|
||||||
|
|
|
@ -9,14 +9,17 @@ import RadialGradient from "./components/RadialGradient";
|
||||||
import { EthereumProviderProvider } from "./contexts/EthereumProviderContext";
|
import { EthereumProviderProvider } from "./contexts/EthereumProviderContext";
|
||||||
import { SolanaWalletProvider } from "./contexts/SolanaWalletContext.tsx";
|
import { SolanaWalletProvider } from "./contexts/SolanaWalletContext.tsx";
|
||||||
import { TerraWalletProvider } from "./contexts/TerraWalletContext.tsx";
|
import { TerraWalletProvider } from "./contexts/TerraWalletContext.tsx";
|
||||||
|
import ErrorBoundary from "./ErrorBoundary";
|
||||||
import { theme } from "./muiTheme";
|
import { theme } from "./muiTheme";
|
||||||
import { store } from "./store";
|
import { store } from "./store";
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
|
<ErrorBoundary>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<RadialGradient />
|
<RadialGradient />
|
||||||
|
<ErrorBoundary>
|
||||||
<SnackbarProvider maxSnack={3}>
|
<SnackbarProvider maxSnack={3}>
|
||||||
<SolanaWalletProvider>
|
<SolanaWalletProvider>
|
||||||
<EthereumProviderProvider>
|
<EthereumProviderProvider>
|
||||||
|
@ -28,7 +31,9 @@ ReactDOM.render(
|
||||||
</EthereumProviderProvider>
|
</EthereumProviderProvider>
|
||||||
</SolanaWalletProvider>
|
</SolanaWalletProvider>
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</Provider>,
|
</Provider>
|
||||||
|
</ErrorBoundary>,
|
||||||
document.getElementById("root")
|
document.getElementById("root")
|
||||||
);
|
);
|
||||||
|
|
|
@ -71,7 +71,9 @@ export const selectTransferIsSending = (state: RootState) =>
|
||||||
state.transfer.isSending;
|
state.transfer.isSending;
|
||||||
export const selectTransferIsRedeeming = (state: RootState) =>
|
export const selectTransferIsRedeeming = (state: RootState) =>
|
||||||
state.transfer.isRedeeming;
|
state.transfer.isRedeeming;
|
||||||
export const selectTransferSourceError = (state: RootState) => {
|
export const selectTransferSourceError = (
|
||||||
|
state: RootState
|
||||||
|
): string | undefined => {
|
||||||
if (!state.transfer.sourceChain) {
|
if (!state.transfer.sourceChain) {
|
||||||
return "Select a source chain";
|
return "Select a source chain";
|
||||||
}
|
}
|
||||||
|
@ -127,6 +129,8 @@ export const selectTransferSourceError = (state: RootState) => {
|
||||||
};
|
};
|
||||||
export const selectTransferIsSourceComplete = (state: RootState) =>
|
export const selectTransferIsSourceComplete = (state: RootState) =>
|
||||||
!selectTransferSourceError(state);
|
!selectTransferSourceError(state);
|
||||||
|
export const UNREGISTERED_ERROR_MESSAGE =
|
||||||
|
"Target asset unavailable. Is the token registered?";
|
||||||
export const selectTransferTargetError = (state: RootState) => {
|
export const selectTransferTargetError = (state: RootState) => {
|
||||||
const sourceError = selectTransferSourceError(state);
|
const sourceError = selectTransferSourceError(state);
|
||||||
if (sourceError) {
|
if (sourceError) {
|
||||||
|
@ -136,13 +140,13 @@ export const selectTransferTargetError = (state: RootState) => {
|
||||||
return "Select a target chain";
|
return "Select a target chain";
|
||||||
}
|
}
|
||||||
if (!state.transfer.targetAsset) {
|
if (!state.transfer.targetAsset) {
|
||||||
return "Target asset unavailable. Is the token attested?";
|
return UNREGISTERED_ERROR_MESSAGE;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
state.transfer.targetChain === CHAIN_ID_ETH &&
|
state.transfer.targetChain === CHAIN_ID_ETH &&
|
||||||
state.transfer.targetAsset === ethers.constants.AddressZero
|
state.transfer.targetAsset === ethers.constants.AddressZero
|
||||||
) {
|
) {
|
||||||
return "Target asset unavailable. Is the token attested?";
|
return UNREGISTERED_ERROR_MESSAGE;
|
||||||
}
|
}
|
||||||
if (!state.transfer.targetAddressHex) {
|
if (!state.transfer.targetAddressHex) {
|
||||||
return "Target account unavailable";
|
return "Target account unavailable";
|
||||||
|
|
Loading…
Reference in New Issue