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",
|
||||
"@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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 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 />
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 [open, setOpen] = useState(false);
|
||||
const handleOpenClick = useCallback(() => {
|
||||
setOpen(true);
|
||||
}, []);
|
||||
}, [setOpen]);
|
||||
const handleCloseClick = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, []);
|
||||
}, [setOpen]);
|
||||
return (
|
||||
<>
|
||||
<Fab className={classes.fab} onClick={handleOpenClick}>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 { 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}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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")
|
||||
);
|
||||
|
|
|
@ -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";
|
||||
|
|
Loading…
Reference in New Issue