bridge_ui: confidence enhancement
Change-Id: Ied0f0fe641a89bb99c9b6a5d0e1400e70b2c8d77
This commit is contained in:
parent
4bdb714594
commit
461e8f256e
|
@ -5,10 +5,7 @@
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta
|
<meta name="description" content="Wormhole Token Bridge" />
|
||||||
name="description"
|
|
||||||
content="Wormhole Token Bridge"
|
|
||||||
/>
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
<!--
|
<!--
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
@ -24,9 +21,12 @@
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
-->
|
-->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@300;400;500;700&display=swap" rel="stylesheet">
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Sora:wght@200;300;400;500;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
<title>Wormhole Token Bridge</title>
|
<title>Wormhole Token Bridge</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -1,14 +1,22 @@
|
||||||
|
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
||||||
import {
|
import {
|
||||||
AppBar,
|
AppBar,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
Hidden,
|
Hidden,
|
||||||
IconButton,
|
IconButton,
|
||||||
Link,
|
Link,
|
||||||
makeStyles,
|
makeStyles,
|
||||||
|
Tab,
|
||||||
|
Tabs,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
import { GitHub, Help, Publish, Send } from "@material-ui/icons";
|
import { HelpOutline, Send } from "@material-ui/icons";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { useHistory, useLocation, useRouteMatch } from "react-router";
|
||||||
import {
|
import {
|
||||||
Link as RouterLink,
|
Link as RouterLink,
|
||||||
NavLink,
|
NavLink,
|
||||||
|
@ -19,17 +27,18 @@ import {
|
||||||
import Attest from "./components/Attest";
|
import Attest from "./components/Attest";
|
||||||
import Home from "./components/Home";
|
import Home from "./components/Home";
|
||||||
import Migration from "./components/Migration";
|
import Migration from "./components/Migration";
|
||||||
|
import EthereumQuickMigrate from "./components/Migration/EthereumQuickMigrate";
|
||||||
import NFT from "./components/NFT";
|
import NFT from "./components/NFT";
|
||||||
import NFTOriginVerifier from "./components/NFTOriginVerifier";
|
import NFTOriginVerifier from "./components/NFTOriginVerifier";
|
||||||
|
import Recovery from "./components/Recovery";
|
||||||
import Transfer from "./components/Transfer";
|
import Transfer from "./components/Transfer";
|
||||||
import wormholeLogo from "./icons/wormhole.svg";
|
import { useBetaContext } from "./contexts/BetaContext";
|
||||||
|
import { COLORS } from "./muiTheme";
|
||||||
import { CLUSTER } from "./utils/consts";
|
import { CLUSTER } from "./utils/consts";
|
||||||
import EthereumQuickMigrate from "./components/Migration/EthereumQuickMigrate";
|
|
||||||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
appBar: {
|
appBar: {
|
||||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
background: COLORS.nearBlackWithMinorTransparency,
|
||||||
"& > .MuiToolbar-root": {
|
"& > .MuiToolbar-root": {
|
||||||
margin: "auto",
|
margin: "auto",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
@ -40,13 +49,6 @@ const useStyles = makeStyles((theme) => ({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
width: "100vw",
|
width: "100vw",
|
||||||
},
|
},
|
||||||
logo: {
|
|
||||||
verticalAlign: "middle",
|
|
||||||
height: 52,
|
|
||||||
[theme.breakpoints.down("xs")]: {
|
|
||||||
height: 42,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
link: {
|
link: {
|
||||||
...theme.typography.body1,
|
...theme.typography.body1,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
|
@ -58,9 +60,14 @@ const useStyles = makeStyles((theme) => ({
|
||||||
marginLeft: theme.spacing(1),
|
marginLeft: theme.spacing(1),
|
||||||
},
|
},
|
||||||
"&.active": {
|
"&.active": {
|
||||||
color: theme.palette.secondary.light,
|
color: theme.palette.primary.light,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
bg: {
|
||||||
|
minHeight: "100vh",
|
||||||
|
background:
|
||||||
|
"linear-gradient(160deg, rgba(69,74,117,.1) 0%, rgba(138,146,178,.1) 33%, rgba(69,74,117,.1) 66%, rgba(98,104,143,.1) 100%), linear-gradient(45deg, rgba(153,69,255,.1) 0%, rgba(121,98,231,.1) 20%, rgba(0,209,140,.1) 100%)",
|
||||||
|
},
|
||||||
content: {
|
content: {
|
||||||
[theme.breakpoints.up("sm")]: {
|
[theme.breakpoints.up("sm")]: {
|
||||||
margin: theme.spacing(2, 0),
|
margin: theme.spacing(2, 0),
|
||||||
|
@ -69,57 +76,104 @@ const useStyles = makeStyles((theme) => ({
|
||||||
margin: theme.spacing(4, 0),
|
margin: theme.spacing(4, 0),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
brandText: {
|
||||||
|
...theme.typography.h5,
|
||||||
|
[theme.breakpoints.down("xs")]: {
|
||||||
|
fontSize: 22,
|
||||||
|
},
|
||||||
|
fontWeight: "500",
|
||||||
|
background: `linear-gradient(160deg, rgba(255,255,255,1) 0%, rgba(255,255,255,0.5) 100%);`,
|
||||||
|
WebkitBackgroundClip: "text",
|
||||||
|
backgroundClip: "text",
|
||||||
|
WebkitTextFillColor: "transparent",
|
||||||
|
MozBackgroundClip: "text",
|
||||||
|
MozTextFillColor: "transparent",
|
||||||
|
letterSpacing: "3px",
|
||||||
|
},
|
||||||
|
gradientButton: {
|
||||||
|
backgroundImage: `linear-gradient(45deg, ${COLORS.blue} 0%, ${COLORS.nearBlack}20 50%, ${COLORS.blue}30 62%, ${COLORS.nearBlack}50 120%)`,
|
||||||
|
transition: "0.75s",
|
||||||
|
backgroundSize: "200% auto",
|
||||||
|
boxShadow: "0 0 20px #222",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundPosition:
|
||||||
|
"right center" /* change the direction of the change here */,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
betaBanner: {
|
||||||
|
background: `linear-gradient(to left, ${COLORS.blue}40, ${COLORS.green}40);`,
|
||||||
|
padding: theme.spacing(1, 0),
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
const isBeta = useBetaContext();
|
||||||
|
const isHomepage = useRouteMatch({ path: "/", exact: true });
|
||||||
|
const isOriginVerifier = useRouteMatch({
|
||||||
|
path: "/nft-origin-verifier",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
const { push } = useHistory();
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const handleTabChange = useCallback(
|
||||||
|
(event, value) => {
|
||||||
|
push(value);
|
||||||
|
},
|
||||||
|
[push]
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={classes.bg}>
|
||||||
<AppBar position="static" color="inherit" className={classes.appBar}>
|
<AppBar position="static" color="inherit" className={classes.appBar}>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<RouterLink to="/">
|
<Link
|
||||||
<img
|
component={RouterLink}
|
||||||
src={wormholeLogo}
|
to="/"
|
||||||
alt="Wormhole Logo"
|
className={clsx(classes.link, classes.brandText)}
|
||||||
className={classes.logo}
|
>
|
||||||
/>
|
wormhole
|
||||||
</RouterLink>
|
</Link>
|
||||||
<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" }}>
|
||||||
<Tooltip title="Transfer NFTs to another blockchain">
|
{isHomepage ? (
|
||||||
<Link component={NavLink} to="/nft" className={classes.link}>
|
<Button
|
||||||
NFTs
|
component={RouterLink}
|
||||||
</Link>
|
to="/transfer"
|
||||||
</Tooltip>
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
className={classes.gradientButton}
|
||||||
|
>
|
||||||
|
Transfer Tokens
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Tooltip title="View the FAQ">
|
||||||
|
<Button
|
||||||
|
href="https://docs.wormholenetwork.com/wormhole/faqs"
|
||||||
|
target="_blank"
|
||||||
|
variant="outlined"
|
||||||
|
endIcon={<HelpOutline />}
|
||||||
|
>
|
||||||
|
FAQ
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Hidden>
|
||||||
|
<Hidden implementation="css" smUp>
|
||||||
|
{isHomepage ? (
|
||||||
<Tooltip title="Transfer tokens to another blockchain">
|
<Tooltip title="Transfer tokens to another blockchain">
|
||||||
<Link
|
<IconButton
|
||||||
component={NavLink}
|
component={NavLink}
|
||||||
to="/transfer"
|
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"
|
size="small"
|
||||||
className={classes.link}
|
className={classes.link}
|
||||||
>
|
>
|
||||||
<GitHub />
|
<Send />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
) : (
|
||||||
<Tooltip title="View the FAQ">
|
<Tooltip title="View the FAQ">
|
||||||
<IconButton
|
<IconButton
|
||||||
href="https://docs.wormholenetwork.com/wormhole/faqs"
|
href="https://docs.wormholenetwork.com/wormhole/faqs"
|
||||||
|
@ -127,53 +181,47 @@ function App() {
|
||||||
size="small"
|
size="small"
|
||||||
className={classes.link}
|
className={classes.link}
|
||||||
>
|
>
|
||||||
<Help />
|
<HelpOutline />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
)}
|
||||||
</Hidden>
|
|
||||||
<Hidden implementation="css" smUp>
|
|
||||||
<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>
|
|
||||||
<Tooltip title="View the FAQ">
|
|
||||||
<IconButton
|
|
||||||
href="https://docs.wormholenetwork.com/wormhole/faqs"
|
|
||||||
target="_blank"
|
|
||||||
size="small"
|
|
||||||
className={classes.link}
|
|
||||||
>
|
|
||||||
<Help />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</Hidden>
|
</Hidden>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
{CLUSTER === "mainnet" ? null : (
|
{CLUSTER === "mainnet" ? null : (
|
||||||
<AppBar position="static" color="secondary">
|
<AppBar position="static" className={classes.betaBanner}>
|
||||||
<Typography style={{ textAlign: "center" }}>
|
<Typography style={{ textAlign: "center" }}>
|
||||||
Caution! You are using the {CLUSTER} build of this app.
|
Caution! You are using the {CLUSTER} build of this app.
|
||||||
</Typography>
|
</Typography>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
)}
|
)}
|
||||||
|
{isBeta ? (
|
||||||
|
<AppBar position="static" className={classes.betaBanner}>
|
||||||
|
<Typography style={{ textAlign: "center" }}>
|
||||||
|
Caution! You have enabled the beta. Enter the secret code again to
|
||||||
|
disable.
|
||||||
|
</Typography>
|
||||||
|
</AppBar>
|
||||||
|
) : null}
|
||||||
<div className={classes.content}>
|
<div className={classes.content}>
|
||||||
|
{isHomepage || isOriginVerifier ? null : (
|
||||||
|
<Container maxWidth="md" style={{ paddingBottom: 24 }}>
|
||||||
|
<Tabs
|
||||||
|
value={
|
||||||
|
["/transfer", "/nft", "/redeem"].includes(pathname)
|
||||||
|
? pathname
|
||||||
|
: "/transfer"
|
||||||
|
}
|
||||||
|
variant="fullWidth"
|
||||||
|
onChange={handleTabChange}
|
||||||
|
indicatorColor="primary"
|
||||||
|
>
|
||||||
|
<Tab label="Tokens" value="/transfer" />
|
||||||
|
<Tab label="NFTs" value="/nft" />
|
||||||
|
<Tab label="Redeem" value="/redeem" to="/redeem" />
|
||||||
|
</Tabs>
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/nft">
|
<Route exact path="/nft">
|
||||||
<NFT />
|
<NFT />
|
||||||
|
@ -184,6 +232,9 @@ function App() {
|
||||||
<Route exact path="/transfer">
|
<Route exact path="/transfer">
|
||||||
<Transfer />
|
<Transfer />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route exact path="/redeem">
|
||||||
|
<Recovery />
|
||||||
|
</Route>
|
||||||
<Route exact path="/register">
|
<Route exact path="/register">
|
||||||
<Attest />
|
<Attest />
|
||||||
</Route>
|
</Route>
|
||||||
|
@ -204,7 +255,7 @@ function App() {
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ export default function CreatePreview() {
|
||||||
}, [dispatch, push]);
|
}, [dispatch, push]);
|
||||||
|
|
||||||
const explainerString =
|
const explainerString =
|
||||||
"Success! The redeem transaction was submitted. The tokens will become available once the transaction confirms.";
|
"Success! The create wrapped transaction was submitted.";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { makeStyles, MenuItem, TextField } from "@material-ui/core";
|
import { makeStyles, MenuItem, TextField } from "@material-ui/core";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { useBetaContext } from "../../contexts/BetaContext";
|
||||||
import {
|
import {
|
||||||
incrementStep,
|
incrementStep,
|
||||||
setSourceAsset,
|
setSourceAsset,
|
||||||
|
@ -12,7 +13,7 @@ import {
|
||||||
selectAttestSourceAsset,
|
selectAttestSourceAsset,
|
||||||
selectAttestSourceChain,
|
selectAttestSourceChain,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
import { CHAINS } from "../../utils/consts";
|
import { BETA_CHAINS, CHAINS } from "../../utils/consts";
|
||||||
import ButtonWithLoader from "../ButtonWithLoader";
|
import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
import LowBalanceWarning from "../LowBalanceWarning";
|
import LowBalanceWarning from "../LowBalanceWarning";
|
||||||
|
@ -26,6 +27,7 @@ const useStyles = makeStyles((theme) => ({
|
||||||
function Source() {
|
function Source() {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const isBeta = useBetaContext();
|
||||||
const sourceChain = useSelector(selectAttestSourceChain);
|
const sourceChain = useSelector(selectAttestSourceChain);
|
||||||
const sourceAsset = useSelector(selectAttestSourceAsset);
|
const sourceAsset = useSelector(selectAttestSourceAsset);
|
||||||
const isSourceComplete = useSelector(selectAttestIsSourceComplete);
|
const isSourceComplete = useSelector(selectAttestIsSourceComplete);
|
||||||
|
@ -49,12 +51,15 @@ function Source() {
|
||||||
<>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
|
variant="outlined"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={sourceChain}
|
value={sourceChain}
|
||||||
onChange={handleSourceChange}
|
onChange={handleSourceChange}
|
||||||
disabled={shouldLockFields}
|
disabled={shouldLockFields}
|
||||||
>
|
>
|
||||||
{CHAINS.map(({ id, name }) => (
|
{CHAINS.filter(({ id }) =>
|
||||||
|
isBeta ? true : !BETA_CHAINS.includes(id)
|
||||||
|
).map(({ id, name }) => (
|
||||||
<MenuItem key={id} value={id}>
|
<MenuItem key={id} value={id}>
|
||||||
{name}
|
{name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -63,6 +68,7 @@ function Source() {
|
||||||
<KeyAndBalance chainId={sourceChain} />
|
<KeyAndBalance chainId={sourceChain} />
|
||||||
<TextField
|
<TextField
|
||||||
label="Asset"
|
label="Asset"
|
||||||
|
variant="outlined"
|
||||||
fullWidth
|
fullWidth
|
||||||
className={classes.transferField}
|
className={classes.transferField}
|
||||||
value={sourceAsset}
|
value={sourceAsset}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { makeStyles, MenuItem, TextField, Typography } from "@material-ui/core";
|
||||||
import { Alert } from "@material-ui/lab";
|
import { Alert } from "@material-ui/lab";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { useBetaContext } from "../../contexts/BetaContext";
|
||||||
import { EthGasEstimateSummary } from "../../hooks/useTransactionFees";
|
import { EthGasEstimateSummary } from "../../hooks/useTransactionFees";
|
||||||
import { incrementStep, setTargetChain } from "../../store/attestSlice";
|
import { incrementStep, setTargetChain } from "../../store/attestSlice";
|
||||||
import {
|
import {
|
||||||
|
@ -10,7 +11,7 @@ import {
|
||||||
selectAttestSourceChain,
|
selectAttestSourceChain,
|
||||||
selectAttestTargetChain,
|
selectAttestTargetChain,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
import { CHAINS, CHAINS_BY_ID } from "../../utils/consts";
|
import { BETA_CHAINS, CHAINS, CHAINS_BY_ID } from "../../utils/consts";
|
||||||
import { isEVMChain } from "../../utils/ethereum";
|
import { isEVMChain } from "../../utils/ethereum";
|
||||||
import ButtonWithLoader from "../ButtonWithLoader";
|
import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
|
@ -26,6 +27,7 @@ const useStyles = makeStyles((theme) => ({
|
||||||
function Target() {
|
function Target() {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const isBeta = useBetaContext();
|
||||||
const sourceChain = useSelector(selectAttestSourceChain);
|
const sourceChain = useSelector(selectAttestSourceChain);
|
||||||
const chains = useMemo(
|
const chains = useMemo(
|
||||||
() => CHAINS.filter((c) => c.id !== sourceChain),
|
() => CHAINS.filter((c) => c.id !== sourceChain),
|
||||||
|
@ -47,16 +49,19 @@ function Target() {
|
||||||
<>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
|
variant="outlined"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={targetChain}
|
value={targetChain}
|
||||||
onChange={handleTargetChange}
|
onChange={handleTargetChange}
|
||||||
disabled={shouldLockFields}
|
disabled={shouldLockFields}
|
||||||
>
|
>
|
||||||
{chains.map(({ id, name }) => (
|
{chains
|
||||||
<MenuItem key={id} value={id}>
|
.filter(({ id }) => (isBeta ? true : !BETA_CHAINS.includes(id)))
|
||||||
{name}
|
.map(({ id, name }) => (
|
||||||
</MenuItem>
|
<MenuItem key={id} value={id}>
|
||||||
))}
|
{name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
<KeyAndBalance chainId={targetChain} />
|
<KeyAndBalance chainId={targetChain} />
|
||||||
<Alert severity="info" className={classes.alert}>
|
<Alert severity="info" className={classes.alert}>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
makeStyles,
|
|
||||||
Step,
|
Step,
|
||||||
StepButton,
|
StepButton,
|
||||||
StepContent,
|
StepContent,
|
||||||
|
@ -9,7 +8,6 @@ import {
|
||||||
import { Alert } from "@material-ui/lab";
|
import { Alert } from "@material-ui/lab";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { COLORS } from "../../muiTheme";
|
|
||||||
import { setStep } from "../../store/attestSlice";
|
import { setStep } from "../../store/attestSlice";
|
||||||
import {
|
import {
|
||||||
selectAttestActiveStep,
|
selectAttestActiveStep,
|
||||||
|
@ -27,14 +25,7 @@ import SourcePreview from "./SourcePreview";
|
||||||
import Target from "./Target";
|
import Target from "./Target";
|
||||||
import TargetPreview from "./TargetPreview";
|
import TargetPreview from "./TargetPreview";
|
||||||
|
|
||||||
const useStyles = makeStyles(() => ({
|
|
||||||
rootContainer: {
|
|
||||||
backgroundColor: COLORS.nearBlackWithMinorTransparency,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
function Attest() {
|
function Attest() {
|
||||||
const classes = useStyles();
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const activeStep = useSelector(selectAttestActiveStep);
|
const activeStep = useSelector(selectAttestActiveStep);
|
||||||
const isSending = useSelector(selectAttestIsSending);
|
const isSending = useSelector(selectAttestIsSending);
|
||||||
|
@ -57,11 +48,7 @@ function Attest() {
|
||||||
This form allows you to register a token on a new foreign chain. Tokens
|
This form allows you to register a token on a new foreign chain. Tokens
|
||||||
must be registered before they can be transferred.
|
must be registered before they can be transferred.
|
||||||
</Alert>
|
</Alert>
|
||||||
<Stepper
|
<Stepper activeStep={activeStep} orientation="vertical">
|
||||||
activeStep={activeStep}
|
|
||||||
orientation="vertical"
|
|
||||||
className={classes.rootContainer}
|
|
||||||
>
|
|
||||||
<Step
|
<Step
|
||||||
expanded={activeStep >= 0}
|
expanded={activeStep >= 0}
|
||||||
disabled={preventNavigation || isCreateComplete}
|
disabled={preventNavigation || isCreateComplete}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { makeStyles } from "@material-ui/core";
|
import { makeStyles } from "@material-ui/core";
|
||||||
import { useRouteMatch } from "react-router";
|
// import { useRouteMatch } from "react-router";
|
||||||
import holev2 from "../images/holev2.svg";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
holeOuterContainer: {
|
holeOuterContainer: {
|
||||||
|
@ -31,19 +30,11 @@ const useStyles = makeStyles((theme) => ({
|
||||||
|
|
||||||
const BackgroundImage = () => {
|
const BackgroundImage = () => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const isHomepage = useRouteMatch({ path: "/", exact: true });
|
// const isHomepage = useRouteMatch({ path: "/", exact: true });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.holeOuterContainer}>
|
<div className={classes.holeOuterContainer}>
|
||||||
<div className={classes.holeInnerContainer}>
|
<div className={classes.holeInnerContainer}></div>
|
||||||
<img
|
|
||||||
src={holev2}
|
|
||||||
alt=""
|
|
||||||
className={
|
|
||||||
classes.holeImage + (isHomepage ? "" : " " + classes.blurred)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
Card,
|
Card,
|
||||||
Container,
|
Container,
|
||||||
Link,
|
Link,
|
||||||
|
@ -7,7 +6,6 @@ import {
|
||||||
Typography,
|
Typography,
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import overview from "../../images/overview2.svg";
|
|
||||||
import { COLORS } from "../../muiTheme";
|
import { COLORS } from "../../muiTheme";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
@ -29,7 +27,7 @@ const useStyles = makeStyles((theme) => ({
|
||||||
WebkitTextFillColor: "transparent",
|
WebkitTextFillColor: "transparent",
|
||||||
MozBackgroundClip: "text",
|
MozBackgroundClip: "text",
|
||||||
MozTextFillColor: "transparent",
|
MozTextFillColor: "transparent",
|
||||||
filter: `drop-shadow( 0px 0px 8px ${COLORS.nearBlack}) drop-shadow( 0px 0px 14px ${COLORS.nearBlack}) drop-shadow( 0px 0px 24px ${COLORS.nearBlack})`,
|
// filter: `drop-shadow( 0px 0px 8px ${COLORS.nearBlack}) drop-shadow( 0px 0px 14px ${COLORS.nearBlack}) drop-shadow( 0px 0px 24px ${COLORS.nearBlack})`,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2),
|
||||||
|
@ -45,8 +43,7 @@ const useStyles = makeStyles((theme) => ({
|
||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
},
|
},
|
||||||
mainCard: {
|
mainCard: {
|
||||||
padding: theme.spacing(1),
|
padding: theme.spacing(8),
|
||||||
borderRadius: "5px",
|
|
||||||
backgroundColor: COLORS.nearBlackWithMinorTransparency,
|
backgroundColor: COLORS.nearBlackWithMinorTransparency,
|
||||||
},
|
},
|
||||||
spacer: {
|
spacer: {
|
||||||
|
@ -60,12 +57,12 @@ function Home() {
|
||||||
<div>
|
<div>
|
||||||
<Container maxWidth="md">
|
<Container maxWidth="md">
|
||||||
<div className={classes.centeredContainer}>
|
<div className={classes.centeredContainer}>
|
||||||
<Typography variant="h2" component="h1" className={classes.header}>
|
<Typography variant="h1" className={classes.header}>
|
||||||
<span className={classes.linearGradient}>The Portal is Open</span>
|
<span className={classes.linearGradient}>The Portal is Open</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
<Container maxWidth="sm">
|
<Container maxWidth="md">
|
||||||
<Card className={classes.mainCard}>
|
<Card className={classes.mainCard}>
|
||||||
<Typography variant="h4" className={classes.description}>
|
<Typography variant="h4" className={classes.description}>
|
||||||
Wormhole v2 is here!
|
Wormhole v2 is here!
|
||||||
|
@ -74,26 +71,11 @@ function Home() {
|
||||||
The Wormhole Token Bridge allows you to seamlessly transfer
|
The Wormhole Token Bridge allows you to seamlessly transfer
|
||||||
tokenized assets across Solana and Ethereum.
|
tokenized assets across Solana and Ethereum.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button
|
|
||||||
component={RouterLink}
|
|
||||||
to="/transfer"
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
size="large"
|
|
||||||
className={classes.button}
|
|
||||||
>
|
|
||||||
Transfer Tokens
|
|
||||||
</Button>
|
|
||||||
<div className={classes.spacer} />
|
<div className={classes.spacer} />
|
||||||
<Typography variant="subtitle1" className={classes.description}>
|
<Typography variant="subtitle1" className={classes.description}>
|
||||||
If you transferred assets using the previous version of Wormhole,
|
If you transferred assets using the previous version of Wormhole,
|
||||||
most assets can be migrated to v2 on the{" "}
|
most assets can be migrated to v2 on the{" "}
|
||||||
<Link
|
<Link component={RouterLink} to="/transfer" noWrap>
|
||||||
component={RouterLink}
|
|
||||||
to="/transfer"
|
|
||||||
color="secondary"
|
|
||||||
noWrap
|
|
||||||
>
|
|
||||||
transfer page
|
transfer page
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
|
@ -101,23 +83,18 @@ function Home() {
|
||||||
<Typography variant="subtitle1" className={classes.description}>
|
<Typography variant="subtitle1" className={classes.description}>
|
||||||
For assets that don't support the migration, the v1 UI can be found
|
For assets that don't support the migration, the v1 UI can be found
|
||||||
at{" "}
|
at{" "}
|
||||||
<Link href="https://v1.wormholebridge.com" color="secondary">
|
<Link href="https://v1.wormholebridge.com">
|
||||||
v1.wormholebridge.com
|
v1.wormholebridge.com
|
||||||
</Link>
|
</Link>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="subtitle1" className={classes.description}>
|
<Typography variant="subtitle1" className={classes.description}>
|
||||||
To learn more about the Wormhole Protocol that powers it, visit{" "}
|
To learn more about the Wormhole Protocol that powers it, visit{" "}
|
||||||
<Link href="https://wormholenetwork.com/en/" color="secondary">
|
<Link href="https://wormholenetwork.com/en/">
|
||||||
wormholenetwork.com
|
wormholenetwork.com
|
||||||
</Link>
|
</Link>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Card>
|
</Card>
|
||||||
</Container>
|
</Container>
|
||||||
<Container maxWidth="md">
|
|
||||||
<div className={classes.centeredContainer}>
|
|
||||||
<img src={overview} alt="overview" className={classes.overview} />
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -182,6 +182,7 @@ export default function EthereumWorkflow({
|
||||||
{explainerContent}
|
{explainerContent}
|
||||||
<div className={classes.spacer} />
|
<div className={classes.spacer} />
|
||||||
<TextField
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
value={migrationAmount}
|
value={migrationAmount}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={handleAmountChange}
|
onChange={handleAmountChange}
|
||||||
|
|
|
@ -460,6 +460,7 @@ export default function Workflow({
|
||||||
) : null}
|
) : null}
|
||||||
<div className={classes.spacer} />
|
<div className={classes.spacer} />
|
||||||
<TextField
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
value={migrationAmount}
|
value={migrationAmount}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={handleAmountChange}
|
onChange={handleAmountChange}
|
||||||
|
|
|
@ -1,462 +0,0 @@
|
||||||
import {
|
|
||||||
ChainId,
|
|
||||||
CHAIN_ID_SOLANA,
|
|
||||||
getEmitterAddressEth,
|
|
||||||
getEmitterAddressSolana,
|
|
||||||
hexToNativeString,
|
|
||||||
hexToUint8Array,
|
|
||||||
parseNFTPayload,
|
|
||||||
parseSequenceFromLogEth,
|
|
||||||
parseSequenceFromLogSolana,
|
|
||||||
uint8ArrayToHex,
|
|
||||||
} from "@certusone/wormhole-sdk";
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
CircularProgress,
|
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogContent,
|
|
||||||
DialogTitle,
|
|
||||||
Divider,
|
|
||||||
Fab,
|
|
||||||
makeStyles,
|
|
||||||
MenuItem,
|
|
||||||
TextField,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import { Restore } from "@material-ui/icons";
|
|
||||||
import { Alert } from "@material-ui/lab";
|
|
||||||
import { Connection } from "@solana/web3.js";
|
|
||||||
import { ethers } from "ethers";
|
|
||||||
import { useSnackbar } from "notistack";
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
|
||||||
import { setSignedVAAHex, setStep, setTargetChain } from "../../store/nftSlice";
|
|
||||||
import {
|
|
||||||
selectNFTSignedVAAHex,
|
|
||||||
selectNFTSourceChain,
|
|
||||||
} from "../../store/selectors";
|
|
||||||
import {
|
|
||||||
CHAINS_WITH_NFT_SUPPORT,
|
|
||||||
getBridgeAddressForChain,
|
|
||||||
getNFTBridgeAddressForChain,
|
|
||||||
SOLANA_HOST,
|
|
||||||
SOL_NFT_BRIDGE_ADDRESS,
|
|
||||||
WORMHOLE_RPC_HOSTS,
|
|
||||||
} from "../../utils/consts";
|
|
||||||
import { isEVMChain } from "../../utils/ethereum";
|
|
||||||
import { getSignedVAAWithRetry } from "../../utils/getSignedVAAWithRetry";
|
|
||||||
import parseError from "../../utils/parseError";
|
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
fab: {
|
|
||||||
position: "fixed",
|
|
||||||
bottom: theme.spacing(2),
|
|
||||||
right: theme.spacing(2),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
async function evm(
|
|
||||||
provider: ethers.providers.Web3Provider,
|
|
||||||
tx: string,
|
|
||||||
enqueueSnackbar: any,
|
|
||||||
chainId: ChainId
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const receipt = await provider.getTransactionReceipt(tx);
|
|
||||||
const sequence = parseSequenceFromLogEth(
|
|
||||||
receipt,
|
|
||||||
getBridgeAddressForChain(chainId)
|
|
||||||
);
|
|
||||||
const emitterAddress = getEmitterAddressEth(
|
|
||||||
getNFTBridgeAddressForChain(chainId)
|
|
||||||
);
|
|
||||||
const { vaaBytes } = await getSignedVAAWithRetry(
|
|
||||||
chainId,
|
|
||||||
emitterAddress,
|
|
||||||
sequence.toString(),
|
|
||||||
WORMHOLE_RPC_HOSTS.length
|
|
||||||
);
|
|
||||||
return { vaa: uint8ArrayToHex(vaaBytes), error: null };
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
enqueueSnackbar(parseError(e), { variant: "error" });
|
|
||||||
return { vaa: null, error: parseError(e) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function solana(tx: string, enqueueSnackbar: any) {
|
|
||||||
try {
|
|
||||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
|
||||||
const info = await connection.getTransaction(tx);
|
|
||||||
if (!info) {
|
|
||||||
throw new Error("An error occurred while fetching the transaction info");
|
|
||||||
}
|
|
||||||
const sequence = parseSequenceFromLogSolana(info);
|
|
||||||
const emitterAddress = await getEmitterAddressSolana(
|
|
||||||
SOL_NFT_BRIDGE_ADDRESS
|
|
||||||
);
|
|
||||||
const { vaaBytes } = await getSignedVAAWithRetry(
|
|
||||||
CHAIN_ID_SOLANA,
|
|
||||||
emitterAddress,
|
|
||||||
sequence.toString(),
|
|
||||||
WORMHOLE_RPC_HOSTS.length
|
|
||||||
);
|
|
||||||
return { vaa: uint8ArrayToHex(vaaBytes), error: null };
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
enqueueSnackbar(parseError(e), { variant: "error" });
|
|
||||||
return { vaa: null, error: parseError(e) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function RecoveryDialogContent({
|
|
||||||
onClose,
|
|
||||||
disabled,
|
|
||||||
}: {
|
|
||||||
onClose: () => void;
|
|
||||||
disabled: boolean;
|
|
||||||
}) {
|
|
||||||
const { enqueueSnackbar } = useSnackbar();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { provider } = useEthereumProvider();
|
|
||||||
const currentSourceChain = useSelector(selectNFTSourceChain);
|
|
||||||
const [recoverySourceChain, setRecoverySourceChain] =
|
|
||||||
useState(currentSourceChain);
|
|
||||||
const [recoverySourceTx, setRecoverySourceTx] = useState("");
|
|
||||||
const [recoverySourceTxIsLoading, setRecoverySourceTxIsLoading] =
|
|
||||||
useState(false);
|
|
||||||
const [recoverySourceTxError, setRecoverySourceTxError] = useState("");
|
|
||||||
const currentSignedVAA = useSelector(selectNFTSignedVAAHex);
|
|
||||||
const [recoverySignedVAA, setRecoverySignedVAA] = useState(currentSignedVAA);
|
|
||||||
const [recoveryParsedVAA, setRecoveryParsedVAA] = useState<any>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
if (!recoverySignedVAA) {
|
|
||||||
setRecoverySourceTx("");
|
|
||||||
setRecoverySourceChain(currentSourceChain);
|
|
||||||
}
|
|
||||||
}, [recoverySignedVAA, currentSourceChain]);
|
|
||||||
useEffect(() => {
|
|
||||||
if (recoverySourceTx) {
|
|
||||||
let cancelled = false;
|
|
||||||
if (isEVMChain(recoverySourceChain) && provider) {
|
|
||||||
setRecoverySourceTxError("");
|
|
||||||
setRecoverySourceTxIsLoading(true);
|
|
||||||
(async () => {
|
|
||||||
const { vaa, error } = await evm(
|
|
||||||
provider,
|
|
||||||
recoverySourceTx,
|
|
||||||
enqueueSnackbar,
|
|
||||||
recoverySourceChain
|
|
||||||
);
|
|
||||||
if (!cancelled) {
|
|
||||||
setRecoverySourceTxIsLoading(false);
|
|
||||||
if (vaa) {
|
|
||||||
setRecoverySignedVAA(vaa);
|
|
||||||
}
|
|
||||||
if (error) {
|
|
||||||
setRecoverySourceTxError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
} else if (recoverySourceChain === CHAIN_ID_SOLANA) {
|
|
||||||
setRecoverySourceTxError("");
|
|
||||||
setRecoverySourceTxIsLoading(true);
|
|
||||||
(async () => {
|
|
||||||
const { vaa, error } = await solana(
|
|
||||||
recoverySourceTx,
|
|
||||||
enqueueSnackbar
|
|
||||||
);
|
|
||||||
if (!cancelled) {
|
|
||||||
setRecoverySourceTxIsLoading(false);
|
|
||||||
if (vaa) {
|
|
||||||
setRecoverySignedVAA(vaa);
|
|
||||||
}
|
|
||||||
if (error) {
|
|
||||||
setRecoverySourceTxError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [recoverySourceChain, recoverySourceTx, provider, enqueueSnackbar]);
|
|
||||||
useEffect(() => {
|
|
||||||
setRecoverySignedVAA(currentSignedVAA);
|
|
||||||
}, [currentSignedVAA]);
|
|
||||||
const handleSourceChainChange = useCallback((event) => {
|
|
||||||
setRecoverySourceTx("");
|
|
||||||
setRecoverySourceChain(event.target.value);
|
|
||||||
}, []);
|
|
||||||
const handleSourceTxChange = useCallback((event) => {
|
|
||||||
setRecoverySourceTx(event.target.value.trim());
|
|
||||||
}, []);
|
|
||||||
const handleSignedVAAChange = useCallback((event) => {
|
|
||||||
setRecoverySignedVAA(event.target.value.trim());
|
|
||||||
}, []);
|
|
||||||
useEffect(() => {
|
|
||||||
let cancelled = false;
|
|
||||||
if (recoverySignedVAA) {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const { parse_vaa } = await import(
|
|
||||||
"@certusone/wormhole-sdk/lib/solana/core/bridge"
|
|
||||||
);
|
|
||||||
const parsedVAA = parse_vaa(hexToUint8Array(recoverySignedVAA));
|
|
||||||
if (!cancelled) {
|
|
||||||
setRecoveryParsedVAA(parsedVAA);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
if (!cancelled) {
|
|
||||||
setRecoveryParsedVAA(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [recoverySignedVAA]);
|
|
||||||
const parsedPayload = useMemo(
|
|
||||||
() =>
|
|
||||||
recoveryParsedVAA?.payload
|
|
||||||
? parseNFTPayload(
|
|
||||||
Buffer.from(new Uint8Array(recoveryParsedVAA.payload))
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
[recoveryParsedVAA]
|
|
||||||
);
|
|
||||||
const parsedPayloadTargetChain = parsedPayload?.targetChain;
|
|
||||||
const enableRecovery = recoverySignedVAA && parsedPayloadTargetChain;
|
|
||||||
const handleRecoverClick = useCallback(() => {
|
|
||||||
if (enableRecovery && recoverySignedVAA && parsedPayloadTargetChain) {
|
|
||||||
// TODO: make recovery reducer
|
|
||||||
dispatch(setSignedVAAHex(recoverySignedVAA));
|
|
||||||
dispatch(setTargetChain(parsedPayloadTargetChain));
|
|
||||||
dispatch(setStep(3));
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
dispatch,
|
|
||||||
enableRecovery,
|
|
||||||
recoverySignedVAA,
|
|
||||||
parsedPayloadTargetChain,
|
|
||||||
onClose,
|
|
||||||
]);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DialogContent>
|
|
||||||
<Alert severity="info">
|
|
||||||
If you have sent your tokens but have not redeemed them, you may paste
|
|
||||||
in the Source Transaction ID (from Step 3) to resume your transfer.
|
|
||||||
</Alert>
|
|
||||||
<TextField
|
|
||||||
select
|
|
||||||
label="Source Chain"
|
|
||||||
disabled={!!recoverySignedVAA}
|
|
||||||
value={recoverySourceChain}
|
|
||||||
onChange={handleSourceChainChange}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
>
|
|
||||||
{CHAINS_WITH_NFT_SUPPORT.map(({ id, name }) => (
|
|
||||||
<MenuItem key={id} value={id}>
|
|
||||||
{name}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</TextField>
|
|
||||||
{isEVMChain(recoverySourceChain) ? (
|
|
||||||
<KeyAndBalance chainId={recoverySourceChain} />
|
|
||||||
) : null}
|
|
||||||
<TextField
|
|
||||||
label="Source Tx (paste here)"
|
|
||||||
disabled={!!recoverySignedVAA || recoverySourceTxIsLoading}
|
|
||||||
value={recoverySourceTx}
|
|
||||||
onChange={handleSourceTxChange}
|
|
||||||
error={!!recoverySourceTxError}
|
|
||||||
helperText={recoverySourceTxError}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<Box position="relative">
|
|
||||||
<Box mt={4}>
|
|
||||||
<Typography>or</Typography>
|
|
||||||
</Box>
|
|
||||||
<TextField
|
|
||||||
label="Signed VAA (Hex)"
|
|
||||||
disabled={recoverySourceTxIsLoading}
|
|
||||||
value={recoverySignedVAA || ""}
|
|
||||||
onChange={handleSignedVAAChange}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
{recoverySourceTxIsLoading ? (
|
|
||||||
<Box
|
|
||||||
position="absolute"
|
|
||||||
style={{
|
|
||||||
top: 0,
|
|
||||||
right: 0,
|
|
||||||
left: 0,
|
|
||||||
bottom: 0,
|
|
||||||
backgroundColor: "rgba(0,0,0,0.5)",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CircularProgress />
|
|
||||||
</Box>
|
|
||||||
) : null}
|
|
||||||
</Box>
|
|
||||||
<Box my={4}>
|
|
||||||
<Divider />
|
|
||||||
</Box>
|
|
||||||
<TextField
|
|
||||||
label="Emitter Chain"
|
|
||||||
disabled
|
|
||||||
value={recoveryParsedVAA?.emitter_chain || ""}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Emitter Address"
|
|
||||||
disabled
|
|
||||||
value={
|
|
||||||
(recoveryParsedVAA &&
|
|
||||||
hexToNativeString(
|
|
||||||
recoveryParsedVAA.emitter_address,
|
|
||||||
recoveryParsedVAA.emitter_chain
|
|
||||||
)) ||
|
|
||||||
""
|
|
||||||
}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Sequence"
|
|
||||||
disabled
|
|
||||||
value={recoveryParsedVAA?.sequence || ""}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Timestamp"
|
|
||||||
disabled
|
|
||||||
value={
|
|
||||||
(recoveryParsedVAA &&
|
|
||||||
new Date(recoveryParsedVAA.timestamp * 1000).toLocaleString()) ||
|
|
||||||
""
|
|
||||||
}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<Box my={4}>
|
|
||||||
<Divider />
|
|
||||||
</Box>
|
|
||||||
<TextField
|
|
||||||
label="Origin Chain"
|
|
||||||
disabled
|
|
||||||
value={parsedPayload?.originChain.toString() || ""}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Origin Token Address"
|
|
||||||
disabled
|
|
||||||
value={
|
|
||||||
(parsedPayload &&
|
|
||||||
hexToNativeString(
|
|
||||||
parsedPayload.originAddress,
|
|
||||||
parsedPayload.originChain
|
|
||||||
)) ||
|
|
||||||
""
|
|
||||||
}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Origin Token ID"
|
|
||||||
disabled
|
|
||||||
value={parsedPayload?.tokenId || ""}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Target Chain"
|
|
||||||
disabled
|
|
||||||
value={parsedPayload?.targetChain.toString() || ""}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Target Address"
|
|
||||||
disabled
|
|
||||||
value={
|
|
||||||
(parsedPayload &&
|
|
||||||
hexToNativeString(
|
|
||||||
parsedPayload.targetAddress,
|
|
||||||
parsedPayload.targetChain
|
|
||||||
)) ||
|
|
||||||
""
|
|
||||||
}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<Box my={4}>
|
|
||||||
<Divider />
|
|
||||||
</Box>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={onClose} variant="outlined" color="default">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleRecoverClick}
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
disabled={!enableRecovery || disabled}
|
|
||||||
>
|
|
||||||
Recover
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Recovery({
|
|
||||||
open,
|
|
||||||
setOpen,
|
|
||||||
disabled,
|
|
||||||
}: {
|
|
||||||
open: boolean;
|
|
||||||
setOpen: (open: boolean) => void;
|
|
||||||
disabled: boolean;
|
|
||||||
}) {
|
|
||||||
const classes = useStyles();
|
|
||||||
const handleOpenClick = useCallback(() => {
|
|
||||||
setOpen(true);
|
|
||||||
}, [setOpen]);
|
|
||||||
const handleCloseClick = useCallback(() => {
|
|
||||||
setOpen(false);
|
|
||||||
}, [setOpen]);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Tooltip title="Open Recovery Dialog">
|
|
||||||
<Fab className={classes.fab} onClick={handleOpenClick}>
|
|
||||||
<Restore />
|
|
||||||
</Fab>
|
|
||||||
</Tooltip>
|
|
||||||
<Dialog open={open} onClose={handleCloseClick} maxWidth="md" fullWidth>
|
|
||||||
<DialogTitle>Recovery</DialogTitle>
|
|
||||||
<RecoveryDialogContent onClose={handleCloseClick} disabled={disabled} />
|
|
||||||
</Dialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
|
import { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
|
||||||
import { Restore, VerifiedUser } from "@material-ui/icons";
|
import { VerifiedUser } from "@material-ui/icons";
|
||||||
import { Alert } from "@material-ui/lab";
|
import { Alert } from "@material-ui/lab";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { useBetaContext } from "../../contexts/BetaContext";
|
||||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||||
import { incrementStep, setSourceChain } from "../../store/nftSlice";
|
import { incrementStep, setSourceChain } from "../../store/nftSlice";
|
||||||
import {
|
import {
|
||||||
|
@ -13,7 +14,7 @@ import {
|
||||||
selectNFTSourceChain,
|
selectNFTSourceChain,
|
||||||
selectNFTSourceError,
|
selectNFTSourceError,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
import { CHAINS_WITH_NFT_SUPPORT } from "../../utils/consts";
|
import { BETA_CHAINS, CHAINS_WITH_NFT_SUPPORT } from "../../utils/consts";
|
||||||
import { isEVMChain } from "../../utils/ethereum";
|
import { isEVMChain } from "../../utils/ethereum";
|
||||||
import ButtonWithLoader from "../ButtonWithLoader";
|
import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
|
@ -25,21 +26,12 @@ const useStyles = makeStyles((theme) => ({
|
||||||
transferField: {
|
transferField: {
|
||||||
marginTop: theme.spacing(5),
|
marginTop: theme.spacing(5),
|
||||||
},
|
},
|
||||||
buttonWrapper: {
|
|
||||||
textAlign: "right",
|
|
||||||
},
|
|
||||||
nftOriginVerifierButton: {
|
|
||||||
marginTop: theme.spacing(0.5),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function Source({
|
function Source() {
|
||||||
setIsRecoveryOpen,
|
|
||||||
}: {
|
|
||||||
setIsRecoveryOpen: (open: boolean) => void;
|
|
||||||
}) {
|
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const isBeta = useBetaContext();
|
||||||
const sourceChain = useSelector(selectNFTSourceChain);
|
const sourceChain = useSelector(selectNFTSourceChain);
|
||||||
const uiAmountString = useSelector(selectNFTSourceBalanceString);
|
const uiAmountString = useSelector(selectNFTSourceBalanceString);
|
||||||
const error = useSelector(selectNFTSourceError);
|
const error = useSelector(selectNFTSourceError);
|
||||||
|
@ -62,39 +54,29 @@ function Source({
|
||||||
Select an NFT to send through the Wormhole NFT Bridge.
|
Select an NFT to send through the Wormhole NFT Bridge.
|
||||||
<div style={{ flexGrow: 1 }} />
|
<div style={{ flexGrow: 1 }} />
|
||||||
<div>
|
<div>
|
||||||
<div className={classes.buttonWrapper}>
|
<Button
|
||||||
<Button
|
component={Link}
|
||||||
onClick={() => setIsRecoveryOpen(true)}
|
to="/nft-origin-verifier"
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
endIcon={<Restore />}
|
endIcon={<VerifiedUser />}
|
||||||
>
|
>
|
||||||
Perform Recovery
|
NFT Origin Verifier
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
<div className={classes.buttonWrapper}>
|
|
||||||
<Button
|
|
||||||
component={Link}
|
|
||||||
to="/nft-origin-verifier"
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
endIcon={<VerifiedUser />}
|
|
||||||
className={classes.nftOriginVerifierButton}
|
|
||||||
>
|
|
||||||
NFT Origin Verifier
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</StepDescription>
|
</StepDescription>
|
||||||
<TextField
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
select
|
select
|
||||||
fullWidth
|
fullWidth
|
||||||
value={sourceChain}
|
value={sourceChain}
|
||||||
onChange={handleSourceChange}
|
onChange={handleSourceChange}
|
||||||
disabled={shouldLockFields}
|
disabled={shouldLockFields}
|
||||||
>
|
>
|
||||||
{CHAINS_WITH_NFT_SUPPORT.map(({ id, name }) => (
|
{CHAINS_WITH_NFT_SUPPORT.filter(({ id }) =>
|
||||||
|
isBeta ? true : !BETA_CHAINS.includes(id)
|
||||||
|
).map(({ id, name }) => (
|
||||||
<MenuItem key={id} value={id}>
|
<MenuItem key={id} value={id}>
|
||||||
{name}
|
{name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { PublicKey } from "@solana/web3.js";
|
||||||
import { BigNumber, ethers } from "ethers";
|
import { BigNumber, ethers } from "ethers";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { useBetaContext } from "../../contexts/BetaContext";
|
||||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||||
import useSyncTargetAddress from "../../hooks/useSyncTargetAddress";
|
import useSyncTargetAddress from "../../hooks/useSyncTargetAddress";
|
||||||
import { EthGasEstimateSummary } from "../../hooks/useTransactionFees";
|
import { EthGasEstimateSummary } from "../../hooks/useTransactionFees";
|
||||||
|
@ -26,7 +27,11 @@ import {
|
||||||
selectNFTTargetChain,
|
selectNFTTargetChain,
|
||||||
selectNFTTargetError,
|
selectNFTTargetError,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
import { CHAINS_BY_ID, CHAINS_WITH_NFT_SUPPORT } from "../../utils/consts";
|
import {
|
||||||
|
BETA_CHAINS,
|
||||||
|
CHAINS_BY_ID,
|
||||||
|
CHAINS_WITH_NFT_SUPPORT,
|
||||||
|
} from "../../utils/consts";
|
||||||
import { isEVMChain } from "../../utils/ethereum";
|
import { isEVMChain } from "../../utils/ethereum";
|
||||||
import ButtonWithLoader from "../ButtonWithLoader";
|
import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
|
@ -46,6 +51,7 @@ const useStyles = makeStyles((theme) => ({
|
||||||
function Target() {
|
function Target() {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const isBeta = useBetaContext();
|
||||||
const sourceChain = useSelector(selectNFTSourceChain);
|
const sourceChain = useSelector(selectNFTSourceChain);
|
||||||
const chains = useMemo(
|
const chains = useMemo(
|
||||||
() => CHAINS_WITH_NFT_SUPPORT.filter((c) => c.id !== sourceChain),
|
() => CHAINS_WITH_NFT_SUPPORT.filter((c) => c.id !== sourceChain),
|
||||||
|
@ -91,19 +97,23 @@ function Target() {
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
fullWidth
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
value={targetChain}
|
value={targetChain}
|
||||||
onChange={handleTargetChange}
|
onChange={handleTargetChange}
|
||||||
>
|
>
|
||||||
{chains.map(({ id, name }) => (
|
{chains
|
||||||
<MenuItem key={id} value={id}>
|
.filter(({ id }) => (isBeta ? true : !BETA_CHAINS.includes(id)))
|
||||||
{name}
|
.map(({ id, name }) => (
|
||||||
</MenuItem>
|
<MenuItem key={id} value={id}>
|
||||||
))}
|
{name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
<KeyAndBalance chainId={targetChain} balance={uiAmountString} />
|
<KeyAndBalance chainId={targetChain} balance={uiAmountString} />
|
||||||
<TextField
|
<TextField
|
||||||
label="Recipient Address"
|
label="Recipient Address"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
className={classes.transferField}
|
className={classes.transferField}
|
||||||
value={readableTargetAddress}
|
value={readableTargetAddress}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
|
@ -113,12 +123,14 @@ function Target() {
|
||||||
<TextField
|
<TextField
|
||||||
label="Token Address"
|
label="Token Address"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
className={classes.transferField}
|
className={classes.transferField}
|
||||||
value={targetAsset || ""}
|
value={targetAsset || ""}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
/>
|
/>
|
||||||
{isEVMChain(targetChain) ? (
|
{isEVMChain(targetChain) ? (
|
||||||
<TextField
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
label="TokenId"
|
label="TokenId"
|
||||||
fullWidth
|
fullWidth
|
||||||
className={classes.transferField}
|
className={classes.transferField}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
makeStyles,
|
|
||||||
Step,
|
Step,
|
||||||
StepButton,
|
StepButton,
|
||||||
StepContent,
|
StepContent,
|
||||||
Stepper,
|
Stepper,
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect } 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 { setStep } from "../../store/nftSlice";
|
||||||
import {
|
import {
|
||||||
selectNFTActiveStep,
|
selectNFTActiveStep,
|
||||||
selectNFTIsRedeemComplete,
|
selectNFTIsRedeemComplete,
|
||||||
|
@ -17,8 +17,6 @@ import {
|
||||||
selectNFTIsSendComplete,
|
selectNFTIsSendComplete,
|
||||||
selectNFTIsSending,
|
selectNFTIsSending,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
import { setStep } from "../../store/nftSlice";
|
|
||||||
import Recovery from "./Recovery";
|
|
||||||
import Redeem from "./Redeem";
|
import Redeem from "./Redeem";
|
||||||
import RedeemPreview from "./RedeemPreview";
|
import RedeemPreview from "./RedeemPreview";
|
||||||
import Send from "./Send";
|
import Send from "./Send";
|
||||||
|
@ -27,19 +25,10 @@ import Source from "./Source";
|
||||||
import SourcePreview from "./SourcePreview";
|
import SourcePreview from "./SourcePreview";
|
||||||
import Target from "./Target";
|
import Target from "./Target";
|
||||||
import TargetPreview from "./TargetPreview";
|
import TargetPreview from "./TargetPreview";
|
||||||
import { COLORS } from "../../muiTheme";
|
|
||||||
|
|
||||||
const useStyles = makeStyles(() => ({
|
|
||||||
rootContainer: {
|
|
||||||
backgroundColor: COLORS.nearBlackWithMinorTransparency,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
function NFT() {
|
function NFT() {
|
||||||
const classes = useStyles();
|
|
||||||
useCheckIfWormholeWrapped(true);
|
useCheckIfWormholeWrapped(true);
|
||||||
useFetchTargetAsset(true);
|
useFetchTargetAsset(true);
|
||||||
const [isRecoveryOpen, setIsRecoveryOpen] = useState(false);
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const activeStep = useSelector(selectNFTActiveStep);
|
const activeStep = useSelector(selectNFTActiveStep);
|
||||||
const isSending = useSelector(selectNFTIsSending);
|
const isSending = useSelector(selectNFTIsSending);
|
||||||
|
@ -58,22 +47,14 @@ function NFT() {
|
||||||
}, [preventNavigation]);
|
}, [preventNavigation]);
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="md">
|
<Container maxWidth="md">
|
||||||
<Stepper
|
<Stepper activeStep={activeStep} orientation="vertical">
|
||||||
activeStep={activeStep}
|
|
||||||
orientation="vertical"
|
|
||||||
className={classes.rootContainer}
|
|
||||||
>
|
|
||||||
<Step
|
<Step
|
||||||
expanded={activeStep >= 0}
|
expanded={activeStep >= 0}
|
||||||
disabled={preventNavigation || isRedeemComplete}
|
disabled={preventNavigation || isRedeemComplete}
|
||||||
>
|
>
|
||||||
<StepButton onClick={() => dispatch(setStep(0))}>Source</StepButton>
|
<StepButton onClick={() => dispatch(setStep(0))}>Source</StepButton>
|
||||||
<StepContent>
|
<StepContent>
|
||||||
{activeStep === 0 ? (
|
{activeStep === 0 ? <Source /> : <SourcePreview />}
|
||||||
<Source setIsRecoveryOpen={setIsRecoveryOpen} />
|
|
||||||
) : (
|
|
||||||
<SourcePreview />
|
|
||||||
)}
|
|
||||||
</StepContent>
|
</StepContent>
|
||||||
</Step>
|
</Step>
|
||||||
<Step
|
<Step
|
||||||
|
@ -103,11 +84,6 @@ function NFT() {
|
||||||
</StepContent>
|
</StepContent>
|
||||||
</Step>
|
</Step>
|
||||||
</Stepper>
|
</Stepper>
|
||||||
<Recovery
|
|
||||||
open={isRecoveryOpen}
|
|
||||||
setOpen={setIsRecoveryOpen}
|
|
||||||
disabled={preventNavigation}
|
|
||||||
/>
|
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,12 +24,14 @@ import { Launch } from "@material-ui/icons";
|
||||||
import { Alert } from "@material-ui/lab";
|
import { Alert } from "@material-ui/lab";
|
||||||
import { Connection } from "@solana/web3.js";
|
import { Connection } from "@solana/web3.js";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { useBetaContext } from "../contexts/BetaContext";
|
||||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||||
import useIsWalletReady from "../hooks/useIsWalletReady";
|
import useIsWalletReady from "../hooks/useIsWalletReady";
|
||||||
import { getMetaplexData } from "../hooks/useMetaplexData";
|
import { getMetaplexData } from "../hooks/useMetaplexData";
|
||||||
import { COLORS } from "../muiTheme";
|
import { COLORS } from "../muiTheme";
|
||||||
import { NFTParsedTokenAccount } from "../store/nftSlice";
|
import { NFTParsedTokenAccount } from "../store/nftSlice";
|
||||||
import {
|
import {
|
||||||
|
BETA_CHAINS,
|
||||||
CHAINS_BY_ID,
|
CHAINS_BY_ID,
|
||||||
CHAINS_WITH_NFT_SUPPORT,
|
CHAINS_WITH_NFT_SUPPORT,
|
||||||
getNFTBridgeAddressForChain,
|
getNFTBridgeAddressForChain,
|
||||||
|
@ -65,11 +67,10 @@ const useStyles = makeStyles((theme) => ({
|
||||||
WebkitTextFillColor: "transparent",
|
WebkitTextFillColor: "transparent",
|
||||||
MozBackgroundClip: "text",
|
MozBackgroundClip: "text",
|
||||||
MozTextFillColor: "transparent",
|
MozTextFillColor: "transparent",
|
||||||
filter: `drop-shadow( 0px 0px 8px ${COLORS.nearBlack}) drop-shadow( 0px 0px 14px ${COLORS.nearBlack}) drop-shadow( 0px 0px 24px ${COLORS.nearBlack})`,
|
// filter: `drop-shadow( 0px 0px 8px ${COLORS.nearBlack}) drop-shadow( 0px 0px 14px ${COLORS.nearBlack}) drop-shadow( 0px 0px 24px ${COLORS.nearBlack})`,
|
||||||
},
|
},
|
||||||
mainCard: {
|
mainCard: {
|
||||||
padding: theme.spacing(1),
|
padding: theme.spacing(1),
|
||||||
borderRadius: "5px",
|
|
||||||
backgroundColor: COLORS.nearBlackWithMinorTransparency,
|
backgroundColor: COLORS.nearBlackWithMinorTransparency,
|
||||||
},
|
},
|
||||||
originHeader: {
|
originHeader: {
|
||||||
|
@ -89,6 +90,7 @@ const useStyles = makeStyles((theme) => ({
|
||||||
|
|
||||||
export default function NFTOriginVerifier() {
|
export default function NFTOriginVerifier() {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
const isBeta = useBetaContext();
|
||||||
const { provider, signerAddress } = useEthereumProvider();
|
const { provider, signerAddress } = useEthereumProvider();
|
||||||
const [lookupChain, setLookupChain] = useState(CHAIN_ID_ETH);
|
const [lookupChain, setLookupChain] = useState(CHAIN_ID_ETH);
|
||||||
const { isReady, statusMessage } = useIsWalletReady(lookupChain);
|
const { isReady, statusMessage } = useIsWalletReady(lookupChain);
|
||||||
|
@ -232,7 +234,7 @@ export default function NFTOriginVerifier() {
|
||||||
<div>
|
<div>
|
||||||
<Container maxWidth="md">
|
<Container maxWidth="md">
|
||||||
<div className={classes.centeredContainer}>
|
<div className={classes.centeredContainer}>
|
||||||
<Typography variant="h2" component="h1" className={classes.header}>
|
<Typography variant="h1" className={classes.header}>
|
||||||
<span className={classes.linearGradient}>NFT Origin Verifier</span>
|
<span className={classes.linearGradient}>NFT Origin Verifier</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
|
@ -245,13 +247,16 @@ export default function NFTOriginVerifier() {
|
||||||
</Alert>
|
</Alert>
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
|
variant="outlined"
|
||||||
label="Chain"
|
label="Chain"
|
||||||
value={lookupChain}
|
value={lookupChain}
|
||||||
onChange={handleChainChange}
|
onChange={handleChainChange}
|
||||||
fullWidth
|
fullWidth
|
||||||
margin="normal"
|
margin="normal"
|
||||||
>
|
>
|
||||||
{CHAINS_WITH_NFT_SUPPORT.map(({ id, name }) => (
|
{CHAINS_WITH_NFT_SUPPORT.filter(({ id }) =>
|
||||||
|
isBeta ? true : !BETA_CHAINS.includes(id)
|
||||||
|
).map(({ id, name }) => (
|
||||||
<MenuItem key={id} value={id}>
|
<MenuItem key={id} value={id}>
|
||||||
{name}
|
{name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -262,6 +267,7 @@ export default function NFTOriginVerifier() {
|
||||||
) : null}
|
) : null}
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
label="Paste an address"
|
label="Paste an address"
|
||||||
value={lookupAsset}
|
value={lookupAsset}
|
||||||
|
@ -270,6 +276,7 @@ export default function NFTOriginVerifier() {
|
||||||
{isEVMChain(lookupChain) ? (
|
{isEVMChain(lookupChain) ? (
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
label="Paste a tokenId"
|
label="Paste a tokenId"
|
||||||
value={lookupTokenId}
|
value={lookupTokenId}
|
||||||
|
|
|
@ -0,0 +1,557 @@
|
||||||
|
import {
|
||||||
|
ChainId,
|
||||||
|
CHAIN_ID_SOLANA,
|
||||||
|
CHAIN_ID_TERRA,
|
||||||
|
getEmitterAddressEth,
|
||||||
|
getEmitterAddressSolana,
|
||||||
|
getEmitterAddressTerra,
|
||||||
|
hexToNativeString,
|
||||||
|
hexToUint8Array,
|
||||||
|
parseNFTPayload,
|
||||||
|
parseSequenceFromLogEth,
|
||||||
|
parseSequenceFromLogSolana,
|
||||||
|
parseSequenceFromLogTerra,
|
||||||
|
parseTransferPayload,
|
||||||
|
uint8ArrayToHex,
|
||||||
|
} from "@certusone/wormhole-sdk";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
CircularProgress,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
makeStyles,
|
||||||
|
MenuItem,
|
||||||
|
TextField,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { ExpandMore } from "@material-ui/icons";
|
||||||
|
import { Alert } from "@material-ui/lab";
|
||||||
|
import { Connection } from "@solana/web3.js";
|
||||||
|
import { LCDClient } from "@terra-money/terra.js";
|
||||||
|
import { ethers } from "ethers";
|
||||||
|
import { useSnackbar } from "notistack";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { useHistory } from "react-router";
|
||||||
|
import { useBetaContext } from "../contexts/BetaContext";
|
||||||
|
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||||
|
import { COLORS } from "../muiTheme";
|
||||||
|
import {
|
||||||
|
setSignedVAAHex as setNFTSignedVAAHex,
|
||||||
|
setStep as setNFTStep,
|
||||||
|
setTargetChain as setNFTTargetChain,
|
||||||
|
} from "../store/nftSlice";
|
||||||
|
import {
|
||||||
|
setSignedVAAHex,
|
||||||
|
setStep,
|
||||||
|
setTargetChain,
|
||||||
|
} from "../store/transferSlice";
|
||||||
|
import {
|
||||||
|
BETA_CHAINS,
|
||||||
|
CHAINS,
|
||||||
|
CHAINS_WITH_NFT_SUPPORT,
|
||||||
|
getBridgeAddressForChain,
|
||||||
|
getNFTBridgeAddressForChain,
|
||||||
|
getTokenBridgeAddressForChain,
|
||||||
|
SOLANA_HOST,
|
||||||
|
SOL_NFT_BRIDGE_ADDRESS,
|
||||||
|
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||||
|
TERRA_HOST,
|
||||||
|
TERRA_TOKEN_BRIDGE_ADDRESS,
|
||||||
|
WORMHOLE_RPC_HOSTS,
|
||||||
|
} from "../utils/consts";
|
||||||
|
import { isEVMChain } from "../utils/ethereum";
|
||||||
|
import { getSignedVAAWithRetry } from "../utils/getSignedVAAWithRetry";
|
||||||
|
import parseError from "../utils/parseError";
|
||||||
|
import ButtonWithLoader from "./ButtonWithLoader";
|
||||||
|
import KeyAndBalance from "./KeyAndBalance";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
mainCard: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
backgroundColor: COLORS.nearBlackWithMinorTransparency,
|
||||||
|
},
|
||||||
|
advancedContainer: {
|
||||||
|
padding: theme.spacing(2, 0),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
async function evm(
|
||||||
|
provider: ethers.providers.Web3Provider,
|
||||||
|
tx: string,
|
||||||
|
enqueueSnackbar: any,
|
||||||
|
chainId: ChainId,
|
||||||
|
nft: boolean
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const receipt = await provider.getTransactionReceipt(tx);
|
||||||
|
const sequence = parseSequenceFromLogEth(
|
||||||
|
receipt,
|
||||||
|
getBridgeAddressForChain(chainId)
|
||||||
|
);
|
||||||
|
const emitterAddress = getEmitterAddressEth(
|
||||||
|
nft
|
||||||
|
? getNFTBridgeAddressForChain(chainId)
|
||||||
|
: getTokenBridgeAddressForChain(chainId)
|
||||||
|
);
|
||||||
|
const { vaaBytes } = await getSignedVAAWithRetry(
|
||||||
|
chainId,
|
||||||
|
emitterAddress,
|
||||||
|
sequence.toString(),
|
||||||
|
WORMHOLE_RPC_HOSTS.length
|
||||||
|
);
|
||||||
|
return { vaa: uint8ArrayToHex(vaaBytes), error: null };
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
enqueueSnackbar(parseError(e), { variant: "error" });
|
||||||
|
return { vaa: null, error: parseError(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function solana(tx: string, enqueueSnackbar: any, nft: boolean) {
|
||||||
|
try {
|
||||||
|
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||||
|
const info = await connection.getTransaction(tx);
|
||||||
|
if (!info) {
|
||||||
|
throw new Error("An error occurred while fetching the transaction info");
|
||||||
|
}
|
||||||
|
const sequence = parseSequenceFromLogSolana(info);
|
||||||
|
const emitterAddress = await getEmitterAddressSolana(
|
||||||
|
nft ? SOL_NFT_BRIDGE_ADDRESS : SOL_TOKEN_BRIDGE_ADDRESS
|
||||||
|
);
|
||||||
|
const { vaaBytes } = await getSignedVAAWithRetry(
|
||||||
|
CHAIN_ID_SOLANA,
|
||||||
|
emitterAddress,
|
||||||
|
sequence.toString(),
|
||||||
|
WORMHOLE_RPC_HOSTS.length
|
||||||
|
);
|
||||||
|
return { vaa: uint8ArrayToHex(vaaBytes), error: null };
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
enqueueSnackbar(parseError(e), { variant: "error" });
|
||||||
|
return { vaa: null, error: parseError(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function terra(tx: string, enqueueSnackbar: any) {
|
||||||
|
try {
|
||||||
|
const lcd = new LCDClient(TERRA_HOST);
|
||||||
|
const info = await lcd.tx.txInfo(tx);
|
||||||
|
const sequence = parseSequenceFromLogTerra(info);
|
||||||
|
if (!sequence) {
|
||||||
|
throw new Error("Sequence not found");
|
||||||
|
}
|
||||||
|
const emitterAddress = await getEmitterAddressTerra(
|
||||||
|
TERRA_TOKEN_BRIDGE_ADDRESS
|
||||||
|
);
|
||||||
|
const { vaaBytes } = await getSignedVAAWithRetry(
|
||||||
|
CHAIN_ID_TERRA,
|
||||||
|
emitterAddress,
|
||||||
|
sequence,
|
||||||
|
WORMHOLE_RPC_HOSTS.length
|
||||||
|
);
|
||||||
|
return { vaa: uint8ArrayToHex(vaaBytes), error: null };
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
enqueueSnackbar(parseError(e), { variant: "error" });
|
||||||
|
return { vaa: null, error: parseError(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Recovery() {
|
||||||
|
const classes = useStyles();
|
||||||
|
const isBeta = useBetaContext();
|
||||||
|
const { push } = useHistory();
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { provider } = useEthereumProvider();
|
||||||
|
const [type, setType] = useState("Token");
|
||||||
|
const isNFT = type === "NFT";
|
||||||
|
const [recoverySourceChain, setRecoverySourceChain] =
|
||||||
|
useState(CHAIN_ID_SOLANA);
|
||||||
|
const [recoverySourceTx, setRecoverySourceTx] = useState("");
|
||||||
|
const [recoverySourceTxIsLoading, setRecoverySourceTxIsLoading] =
|
||||||
|
useState(false);
|
||||||
|
const [recoverySourceTxError, setRecoverySourceTxError] = useState("");
|
||||||
|
const [recoverySignedVAA, setRecoverySignedVAA] = useState("");
|
||||||
|
const [recoveryParsedVAA, setRecoveryParsedVAA] = useState<any>(null);
|
||||||
|
const parsedPayload = useMemo(() => {
|
||||||
|
try {
|
||||||
|
return recoveryParsedVAA?.payload
|
||||||
|
? isNFT
|
||||||
|
? parseNFTPayload(
|
||||||
|
Buffer.from(new Uint8Array(recoveryParsedVAA.payload))
|
||||||
|
)
|
||||||
|
: parseTransferPayload(
|
||||||
|
Buffer.from(new Uint8Array(recoveryParsedVAA.payload))
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [recoveryParsedVAA, isNFT]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (recoverySourceTx) {
|
||||||
|
let cancelled = false;
|
||||||
|
if (isEVMChain(recoverySourceChain) && provider) {
|
||||||
|
setRecoverySourceTxError("");
|
||||||
|
setRecoverySourceTxIsLoading(true);
|
||||||
|
(async () => {
|
||||||
|
const { vaa, error } = await evm(
|
||||||
|
provider,
|
||||||
|
recoverySourceTx,
|
||||||
|
enqueueSnackbar,
|
||||||
|
recoverySourceChain,
|
||||||
|
isNFT
|
||||||
|
);
|
||||||
|
if (!cancelled) {
|
||||||
|
setRecoverySourceTxIsLoading(false);
|
||||||
|
if (vaa) {
|
||||||
|
setRecoverySignedVAA(vaa);
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
setRecoverySourceTxError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
} else if (recoverySourceChain === CHAIN_ID_SOLANA) {
|
||||||
|
setRecoverySourceTxError("");
|
||||||
|
setRecoverySourceTxIsLoading(true);
|
||||||
|
(async () => {
|
||||||
|
const { vaa, error } = await solana(
|
||||||
|
recoverySourceTx,
|
||||||
|
enqueueSnackbar,
|
||||||
|
isNFT
|
||||||
|
);
|
||||||
|
if (!cancelled) {
|
||||||
|
setRecoverySourceTxIsLoading(false);
|
||||||
|
if (vaa) {
|
||||||
|
setRecoverySignedVAA(vaa);
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
setRecoverySourceTxError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
} else if (recoverySourceChain === CHAIN_ID_TERRA) {
|
||||||
|
setRecoverySourceTxError("");
|
||||||
|
setRecoverySourceTxIsLoading(true);
|
||||||
|
(async () => {
|
||||||
|
const { vaa, error } = await terra(recoverySourceTx, enqueueSnackbar);
|
||||||
|
if (!cancelled) {
|
||||||
|
setRecoverySourceTxIsLoading(false);
|
||||||
|
if (vaa) {
|
||||||
|
setRecoverySignedVAA(vaa);
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
setRecoverySourceTxError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [recoverySourceChain, recoverySourceTx, provider, enqueueSnackbar, isNFT]);
|
||||||
|
const handleTypeChange = useCallback((event) => {
|
||||||
|
setRecoverySourceChain((prevChain) =>
|
||||||
|
event.target.value === "NFT" &&
|
||||||
|
!CHAINS_WITH_NFT_SUPPORT.find((chain) => chain.id === prevChain)
|
||||||
|
? CHAIN_ID_SOLANA
|
||||||
|
: prevChain
|
||||||
|
);
|
||||||
|
setType(event.target.value);
|
||||||
|
}, []);
|
||||||
|
const handleSourceChainChange = useCallback((event) => {
|
||||||
|
setRecoverySourceTx("");
|
||||||
|
setRecoverySourceChain(event.target.value);
|
||||||
|
}, []);
|
||||||
|
const handleSourceTxChange = useCallback((event) => {
|
||||||
|
setRecoverySourceTx(event.target.value.trim());
|
||||||
|
}, []);
|
||||||
|
const handleSignedVAAChange = useCallback((event) => {
|
||||||
|
setRecoverySignedVAA(event.target.value.trim());
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
if (recoverySignedVAA) {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const { parse_vaa } = await import(
|
||||||
|
"@certusone/wormhole-sdk/lib/solana/core/bridge"
|
||||||
|
);
|
||||||
|
const parsedVAA = parse_vaa(hexToUint8Array(recoverySignedVAA));
|
||||||
|
if (!cancelled) {
|
||||||
|
setRecoveryParsedVAA(parsedVAA);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
if (!cancelled) {
|
||||||
|
setRecoveryParsedVAA(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [recoverySignedVAA]);
|
||||||
|
const parsedPayloadTargetChain = parsedPayload?.targetChain;
|
||||||
|
const enableRecovery = recoverySignedVAA && parsedPayloadTargetChain;
|
||||||
|
const handleRecoverClick = useCallback(() => {
|
||||||
|
if (enableRecovery && recoverySignedVAA && parsedPayloadTargetChain) {
|
||||||
|
// TODO: make recovery reducer
|
||||||
|
if (isNFT) {
|
||||||
|
dispatch(setNFTSignedVAAHex(recoverySignedVAA));
|
||||||
|
dispatch(setNFTTargetChain(parsedPayloadTargetChain));
|
||||||
|
dispatch(setNFTStep(3));
|
||||||
|
push("/nft");
|
||||||
|
} else {
|
||||||
|
dispatch(setSignedVAAHex(recoverySignedVAA));
|
||||||
|
dispatch(setTargetChain(parsedPayloadTargetChain));
|
||||||
|
dispatch(setStep(3));
|
||||||
|
push("/transfer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
dispatch,
|
||||||
|
enableRecovery,
|
||||||
|
recoverySignedVAA,
|
||||||
|
parsedPayloadTargetChain,
|
||||||
|
isNFT,
|
||||||
|
push,
|
||||||
|
]);
|
||||||
|
return (
|
||||||
|
<Container maxWidth="md">
|
||||||
|
<Card className={classes.mainCard}>
|
||||||
|
<Alert severity="info">
|
||||||
|
If you have sent your tokens but have not redeemed them, you may paste
|
||||||
|
in the Source Transaction ID (from Step 3) to resume your transfer.
|
||||||
|
</Alert>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
variant="outlined"
|
||||||
|
label="Type"
|
||||||
|
disabled={!!recoverySignedVAA}
|
||||||
|
value={type}
|
||||||
|
onChange={handleTypeChange}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
>
|
||||||
|
<MenuItem value="Token">Token</MenuItem>
|
||||||
|
<MenuItem value="NFT">NFT</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
variant="outlined"
|
||||||
|
label="Source Chain"
|
||||||
|
disabled={!!recoverySignedVAA}
|
||||||
|
value={recoverySourceChain}
|
||||||
|
onChange={handleSourceChainChange}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
>
|
||||||
|
{(isNFT ? CHAINS_WITH_NFT_SUPPORT : CHAINS)
|
||||||
|
.filter(({ id }) => (isBeta ? true : !BETA_CHAINS.includes(id)))
|
||||||
|
.map(({ id, name }) => (
|
||||||
|
<MenuItem key={id} value={id}>
|
||||||
|
{name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
{isEVMChain(recoverySourceChain) ? (
|
||||||
|
<KeyAndBalance chainId={recoverySourceChain} />
|
||||||
|
) : null}
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
label="Source Tx (paste here)"
|
||||||
|
disabled={!!recoverySignedVAA || recoverySourceTxIsLoading}
|
||||||
|
value={recoverySourceTx}
|
||||||
|
onChange={handleSourceTxChange}
|
||||||
|
error={!!recoverySourceTxError}
|
||||||
|
helperText={recoverySourceTxError}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<ButtonWithLoader
|
||||||
|
onClick={handleRecoverClick}
|
||||||
|
disabled={!enableRecovery}
|
||||||
|
showLoader={recoverySourceTxIsLoading}
|
||||||
|
>
|
||||||
|
Recover
|
||||||
|
</ButtonWithLoader>
|
||||||
|
<div className={classes.advancedContainer}>
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||||
|
Advanced
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<div>
|
||||||
|
<Box position="relative">
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
label="Signed VAA (Hex)"
|
||||||
|
disabled={recoverySourceTxIsLoading}
|
||||||
|
value={recoverySignedVAA || ""}
|
||||||
|
onChange={handleSignedVAAChange}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
{recoverySourceTxIsLoading ? (
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
style={{
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: "rgba(0,0,0,0.5)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
<Box my={4}>
|
||||||
|
<Divider />
|
||||||
|
</Box>
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
label="Emitter Chain"
|
||||||
|
disabled
|
||||||
|
value={recoveryParsedVAA?.emitter_chain || ""}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
label="Emitter Address"
|
||||||
|
disabled
|
||||||
|
value={
|
||||||
|
(recoveryParsedVAA &&
|
||||||
|
hexToNativeString(
|
||||||
|
recoveryParsedVAA.emitter_address,
|
||||||
|
recoveryParsedVAA.emitter_chain
|
||||||
|
)) ||
|
||||||
|
""
|
||||||
|
}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
label="Sequence"
|
||||||
|
disabled
|
||||||
|
value={recoveryParsedVAA?.sequence || ""}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
label="Timestamp"
|
||||||
|
disabled
|
||||||
|
value={
|
||||||
|
(recoveryParsedVAA &&
|
||||||
|
new Date(
|
||||||
|
recoveryParsedVAA.timestamp * 1000
|
||||||
|
).toLocaleString()) ||
|
||||||
|
""
|
||||||
|
}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<Box my={4}>
|
||||||
|
<Divider />
|
||||||
|
</Box>
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
label="Origin Chain"
|
||||||
|
disabled
|
||||||
|
value={parsedPayload?.originChain.toString() || ""}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
label="Origin Token Address"
|
||||||
|
disabled
|
||||||
|
value={
|
||||||
|
(parsedPayload &&
|
||||||
|
hexToNativeString(
|
||||||
|
parsedPayload.originAddress,
|
||||||
|
parsedPayload.originChain
|
||||||
|
)) ||
|
||||||
|
""
|
||||||
|
}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
{isNFT ? (
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
label="Origin Token ID"
|
||||||
|
disabled
|
||||||
|
// @ts-ignore
|
||||||
|
value={parsedPayload?.tokenId || ""}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
label="Target Chain"
|
||||||
|
disabled
|
||||||
|
value={parsedPayload?.targetChain.toString() || ""}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
label="Target Chain"
|
||||||
|
disabled
|
||||||
|
value={parsedPayload?.targetChain.toString() || ""}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
label="Target Address"
|
||||||
|
disabled
|
||||||
|
value={
|
||||||
|
(parsedPayload &&
|
||||||
|
hexToNativeString(
|
||||||
|
parsedPayload.targetAddress,
|
||||||
|
parsedPayload.targetChain
|
||||||
|
)) ||
|
||||||
|
""
|
||||||
|
}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
{isNFT ? null : (
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
label="Amount"
|
||||||
|
disabled
|
||||||
|
// @ts-ignore
|
||||||
|
value={parsedPayload?.amount.toString() || ""}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
import { Button, makeStyles, Tooltip } from "@material-ui/core";
|
import { Button, makeStyles, Tooltip } from "@material-ui/core";
|
||||||
|
import { LinkOff } from "@material-ui/icons";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
button: {
|
button: {
|
||||||
display: "block",
|
display: "flex",
|
||||||
margin: `${theme.spacing(1)}px auto`,
|
margin: `${theme.spacing(1)}px auto`,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
maxWidth: 400,
|
maxWidth: 400,
|
||||||
|
@ -25,11 +26,12 @@ const ToggleConnectedButton = ({
|
||||||
return connected ? (
|
return connected ? (
|
||||||
<Tooltip title={pk}>
|
<Tooltip title={pk}>
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="primary"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={disconnect}
|
onClick={disconnect}
|
||||||
className={classes.button}
|
className={classes.button}
|
||||||
|
startIcon={<LinkOff />}
|
||||||
>
|
>
|
||||||
Disconnect {pk.substring(0, is0x ? 6 : 3)}...
|
Disconnect {pk.substring(0, is0x ? 6 : 3)}...
|
||||||
{pk.substr(pk.length - (is0x ? 4 : 3))}
|
{pk.substr(pk.length - (is0x ? 4 : 3))}
|
||||||
|
|
|
@ -584,6 +584,7 @@ export default function EthereumSourceTokenSelector(
|
||||||
) : advancedMode ? (
|
) : advancedMode ? (
|
||||||
<>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Enter an asset address"
|
label="Enter an asset address"
|
||||||
value={advancedModeHolderString}
|
value={advancedModeHolderString}
|
||||||
|
@ -602,6 +603,7 @@ export default function EthereumSourceTokenSelector(
|
||||||
/>
|
/>
|
||||||
{nft ? (
|
{nft ? (
|
||||||
<TextField
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Enter a tokenId"
|
label="Enter a tokenId"
|
||||||
value={advancedModeHolderTokenIdRaw}
|
value={advancedModeHolderTokenIdRaw}
|
||||||
|
|
|
@ -113,6 +113,7 @@ export const TokenSelector = (props: TokenSelectorProps) => {
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TextField
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
placeholder="Asset"
|
placeholder="Asset"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={"Not Implemented"}
|
value={"Not Implemented"}
|
||||||
|
|
|
@ -296,6 +296,7 @@ export default function TerraSourceTokenSelector(
|
||||||
<>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
label="Enter an asset address"
|
label="Enter an asset address"
|
||||||
value={advancedModeHolderString}
|
value={advancedModeHolderString}
|
||||||
onChange={handleOnChange}
|
onChange={handleOnChange}
|
||||||
|
|
|
@ -1,512 +0,0 @@
|
||||||
import {
|
|
||||||
ChainId,
|
|
||||||
CHAIN_ID_SOLANA,
|
|
||||||
CHAIN_ID_TERRA,
|
|
||||||
getEmitterAddressEth,
|
|
||||||
getEmitterAddressSolana,
|
|
||||||
getEmitterAddressTerra,
|
|
||||||
hexToNativeString,
|
|
||||||
hexToUint8Array,
|
|
||||||
parseSequenceFromLogEth,
|
|
||||||
parseSequenceFromLogSolana,
|
|
||||||
parseSequenceFromLogTerra,
|
|
||||||
parseTransferPayload,
|
|
||||||
uint8ArrayToHex,
|
|
||||||
} from "@certusone/wormhole-sdk";
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
CircularProgress,
|
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogContent,
|
|
||||||
DialogTitle,
|
|
||||||
Divider,
|
|
||||||
Fab,
|
|
||||||
makeStyles,
|
|
||||||
MenuItem,
|
|
||||||
TextField,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import { Restore } from "@material-ui/icons";
|
|
||||||
import { Alert } from "@material-ui/lab";
|
|
||||||
import { Connection } from "@solana/web3.js";
|
|
||||||
import { LCDClient } from "@terra-money/terra.js";
|
|
||||||
import { ethers } from "ethers";
|
|
||||||
import { useSnackbar } from "notistack";
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
|
||||||
import {
|
|
||||||
selectTransferSignedVAAHex,
|
|
||||||
selectTransferSourceChain,
|
|
||||||
} from "../../store/selectors";
|
|
||||||
import {
|
|
||||||
setSignedVAAHex,
|
|
||||||
setStep,
|
|
||||||
setTargetChain,
|
|
||||||
} from "../../store/transferSlice";
|
|
||||||
import {
|
|
||||||
CHAINS,
|
|
||||||
getBridgeAddressForChain,
|
|
||||||
getTokenBridgeAddressForChain,
|
|
||||||
SOLANA_HOST,
|
|
||||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
|
||||||
TERRA_HOST,
|
|
||||||
TERRA_TOKEN_BRIDGE_ADDRESS,
|
|
||||||
WORMHOLE_RPC_HOSTS,
|
|
||||||
} from "../../utils/consts";
|
|
||||||
import { isEVMChain } from "../../utils/ethereum";
|
|
||||||
import { getSignedVAAWithRetry } from "../../utils/getSignedVAAWithRetry";
|
|
||||||
import parseError from "../../utils/parseError";
|
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
fab: {
|
|
||||||
position: "fixed",
|
|
||||||
bottom: theme.spacing(2),
|
|
||||||
right: theme.spacing(2),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
async function evm(
|
|
||||||
provider: ethers.providers.Web3Provider,
|
|
||||||
tx: string,
|
|
||||||
enqueueSnackbar: any,
|
|
||||||
chainId: ChainId
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const receipt = await provider.getTransactionReceipt(tx);
|
|
||||||
const sequence = parseSequenceFromLogEth(
|
|
||||||
receipt,
|
|
||||||
getBridgeAddressForChain(chainId)
|
|
||||||
);
|
|
||||||
const emitterAddress = getEmitterAddressEth(
|
|
||||||
getTokenBridgeAddressForChain(chainId)
|
|
||||||
);
|
|
||||||
const { vaaBytes } = await getSignedVAAWithRetry(
|
|
||||||
chainId,
|
|
||||||
emitterAddress,
|
|
||||||
sequence.toString(),
|
|
||||||
WORMHOLE_RPC_HOSTS.length
|
|
||||||
);
|
|
||||||
return { vaa: uint8ArrayToHex(vaaBytes), error: null };
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
enqueueSnackbar(parseError(e), { variant: "error" });
|
|
||||||
return { vaa: null, error: parseError(e) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function solana(tx: string, enqueueSnackbar: any) {
|
|
||||||
try {
|
|
||||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
|
||||||
const info = await connection.getTransaction(tx);
|
|
||||||
if (!info) {
|
|
||||||
throw new Error("An error occurred while fetching the transaction info");
|
|
||||||
}
|
|
||||||
const sequence = parseSequenceFromLogSolana(info);
|
|
||||||
const emitterAddress = await getEmitterAddressSolana(
|
|
||||||
SOL_TOKEN_BRIDGE_ADDRESS
|
|
||||||
);
|
|
||||||
const { vaaBytes } = await getSignedVAAWithRetry(
|
|
||||||
CHAIN_ID_SOLANA,
|
|
||||||
emitterAddress,
|
|
||||||
sequence.toString(),
|
|
||||||
WORMHOLE_RPC_HOSTS.length
|
|
||||||
);
|
|
||||||
return { vaa: uint8ArrayToHex(vaaBytes), error: null };
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
enqueueSnackbar(parseError(e), { variant: "error" });
|
|
||||||
return { vaa: null, error: parseError(e) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function terra(tx: string, enqueueSnackbar: any) {
|
|
||||||
try {
|
|
||||||
const lcd = new LCDClient(TERRA_HOST);
|
|
||||||
const info = await lcd.tx.txInfo(tx);
|
|
||||||
const sequence = parseSequenceFromLogTerra(info);
|
|
||||||
if (!sequence) {
|
|
||||||
throw new Error("Sequence not found");
|
|
||||||
}
|
|
||||||
const emitterAddress = await getEmitterAddressTerra(
|
|
||||||
TERRA_TOKEN_BRIDGE_ADDRESS
|
|
||||||
);
|
|
||||||
const { vaaBytes } = await getSignedVAAWithRetry(
|
|
||||||
CHAIN_ID_TERRA,
|
|
||||||
emitterAddress,
|
|
||||||
sequence,
|
|
||||||
WORMHOLE_RPC_HOSTS.length
|
|
||||||
);
|
|
||||||
return { vaa: uint8ArrayToHex(vaaBytes), error: null };
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
enqueueSnackbar(parseError(e), { variant: "error" });
|
|
||||||
return { vaa: null, error: parseError(e) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function RecoveryDialogContent({
|
|
||||||
onClose,
|
|
||||||
disabled,
|
|
||||||
}: {
|
|
||||||
onClose: () => void;
|
|
||||||
disabled: boolean;
|
|
||||||
}) {
|
|
||||||
const { enqueueSnackbar } = useSnackbar();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { provider } = useEthereumProvider();
|
|
||||||
const currentSourceChain = useSelector(selectTransferSourceChain);
|
|
||||||
const [recoverySourceChain, setRecoverySourceChain] =
|
|
||||||
useState(currentSourceChain);
|
|
||||||
const [recoverySourceTx, setRecoverySourceTx] = useState("");
|
|
||||||
const [recoverySourceTxIsLoading, setRecoverySourceTxIsLoading] =
|
|
||||||
useState(false);
|
|
||||||
const [recoverySourceTxError, setRecoverySourceTxError] = useState("");
|
|
||||||
const currentSignedVAA = useSelector(selectTransferSignedVAAHex);
|
|
||||||
const [recoverySignedVAA, setRecoverySignedVAA] = useState(currentSignedVAA);
|
|
||||||
const [recoveryParsedVAA, setRecoveryParsedVAA] = useState<any>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
if (!recoverySignedVAA) {
|
|
||||||
setRecoverySourceTx("");
|
|
||||||
setRecoverySourceChain(currentSourceChain);
|
|
||||||
}
|
|
||||||
}, [recoverySignedVAA, currentSourceChain]);
|
|
||||||
useEffect(() => {
|
|
||||||
if (recoverySourceTx) {
|
|
||||||
let cancelled = false;
|
|
||||||
if (isEVMChain(recoverySourceChain) && provider) {
|
|
||||||
setRecoverySourceTxError("");
|
|
||||||
setRecoverySourceTxIsLoading(true);
|
|
||||||
(async () => {
|
|
||||||
const { vaa, error } = await evm(
|
|
||||||
provider,
|
|
||||||
recoverySourceTx,
|
|
||||||
enqueueSnackbar,
|
|
||||||
recoverySourceChain
|
|
||||||
);
|
|
||||||
if (!cancelled) {
|
|
||||||
setRecoverySourceTxIsLoading(false);
|
|
||||||
if (vaa) {
|
|
||||||
setRecoverySignedVAA(vaa);
|
|
||||||
}
|
|
||||||
if (error) {
|
|
||||||
setRecoverySourceTxError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
} else if (recoverySourceChain === CHAIN_ID_SOLANA) {
|
|
||||||
setRecoverySourceTxError("");
|
|
||||||
setRecoverySourceTxIsLoading(true);
|
|
||||||
(async () => {
|
|
||||||
const { vaa, error } = await solana(
|
|
||||||
recoverySourceTx,
|
|
||||||
enqueueSnackbar
|
|
||||||
);
|
|
||||||
if (!cancelled) {
|
|
||||||
setRecoverySourceTxIsLoading(false);
|
|
||||||
if (vaa) {
|
|
||||||
setRecoverySignedVAA(vaa);
|
|
||||||
}
|
|
||||||
if (error) {
|
|
||||||
setRecoverySourceTxError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
} else if (recoverySourceChain === CHAIN_ID_TERRA) {
|
|
||||||
setRecoverySourceTxError("");
|
|
||||||
setRecoverySourceTxIsLoading(true);
|
|
||||||
(async () => {
|
|
||||||
const { vaa, error } = await terra(recoverySourceTx, enqueueSnackbar);
|
|
||||||
if (!cancelled) {
|
|
||||||
setRecoverySourceTxIsLoading(false);
|
|
||||||
if (vaa) {
|
|
||||||
setRecoverySignedVAA(vaa);
|
|
||||||
}
|
|
||||||
if (error) {
|
|
||||||
setRecoverySourceTxError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [recoverySourceChain, recoverySourceTx, provider, enqueueSnackbar]);
|
|
||||||
useEffect(() => {
|
|
||||||
setRecoverySignedVAA(currentSignedVAA);
|
|
||||||
}, [currentSignedVAA]);
|
|
||||||
const handleSourceChainChange = useCallback((event) => {
|
|
||||||
setRecoverySourceTx("");
|
|
||||||
setRecoverySourceChain(event.target.value);
|
|
||||||
}, []);
|
|
||||||
const handleSourceTxChange = useCallback((event) => {
|
|
||||||
setRecoverySourceTx(event.target.value.trim());
|
|
||||||
}, []);
|
|
||||||
const handleSignedVAAChange = useCallback((event) => {
|
|
||||||
setRecoverySignedVAA(event.target.value.trim());
|
|
||||||
}, []);
|
|
||||||
useEffect(() => {
|
|
||||||
let cancelled = false;
|
|
||||||
if (recoverySignedVAA) {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const { parse_vaa } = await import(
|
|
||||||
"@certusone/wormhole-sdk/lib/solana/core/bridge"
|
|
||||||
);
|
|
||||||
const parsedVAA = parse_vaa(hexToUint8Array(recoverySignedVAA));
|
|
||||||
if (!cancelled) {
|
|
||||||
setRecoveryParsedVAA(parsedVAA);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
if (!cancelled) {
|
|
||||||
setRecoveryParsedVAA(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [recoverySignedVAA]);
|
|
||||||
const parsedPayload = useMemo(
|
|
||||||
() =>
|
|
||||||
recoveryParsedVAA?.payload
|
|
||||||
? parseTransferPayload(
|
|
||||||
Buffer.from(new Uint8Array(recoveryParsedVAA.payload))
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
[recoveryParsedVAA]
|
|
||||||
);
|
|
||||||
const parsedPayloadTargetChain = parsedPayload?.targetChain;
|
|
||||||
const enableRecovery = recoverySignedVAA && parsedPayloadTargetChain;
|
|
||||||
const handleRecoverClick = useCallback(() => {
|
|
||||||
if (enableRecovery && recoverySignedVAA && parsedPayloadTargetChain) {
|
|
||||||
// TODO: make recovery reducer
|
|
||||||
dispatch(setSignedVAAHex(recoverySignedVAA));
|
|
||||||
dispatch(setTargetChain(parsedPayloadTargetChain));
|
|
||||||
dispatch(setStep(3));
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
dispatch,
|
|
||||||
enableRecovery,
|
|
||||||
recoverySignedVAA,
|
|
||||||
parsedPayloadTargetChain,
|
|
||||||
onClose,
|
|
||||||
]);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DialogContent>
|
|
||||||
<Alert severity="info">
|
|
||||||
If you have sent your tokens but have not redeemed them, you may paste
|
|
||||||
in the Source Transaction ID (from Step 3) to resume your transfer.
|
|
||||||
</Alert>
|
|
||||||
<TextField
|
|
||||||
select
|
|
||||||
label="Source Chain"
|
|
||||||
disabled={!!recoverySignedVAA}
|
|
||||||
value={recoverySourceChain}
|
|
||||||
onChange={handleSourceChainChange}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
>
|
|
||||||
{CHAINS.map(({ id, name }) => (
|
|
||||||
<MenuItem key={id} value={id}>
|
|
||||||
{name}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</TextField>
|
|
||||||
{isEVMChain(recoverySourceChain) ? (
|
|
||||||
<KeyAndBalance chainId={recoverySourceChain} />
|
|
||||||
) : null}
|
|
||||||
<TextField
|
|
||||||
label="Source Tx (paste here)"
|
|
||||||
disabled={!!recoverySignedVAA || recoverySourceTxIsLoading}
|
|
||||||
value={recoverySourceTx}
|
|
||||||
onChange={handleSourceTxChange}
|
|
||||||
error={!!recoverySourceTxError}
|
|
||||||
helperText={recoverySourceTxError}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<Box position="relative">
|
|
||||||
<Box mt={4}>
|
|
||||||
<Typography>or</Typography>
|
|
||||||
</Box>
|
|
||||||
<TextField
|
|
||||||
label="Signed VAA (Hex)"
|
|
||||||
disabled={recoverySourceTxIsLoading}
|
|
||||||
value={recoverySignedVAA || ""}
|
|
||||||
onChange={handleSignedVAAChange}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
{recoverySourceTxIsLoading ? (
|
|
||||||
<Box
|
|
||||||
position="absolute"
|
|
||||||
style={{
|
|
||||||
top: 0,
|
|
||||||
right: 0,
|
|
||||||
left: 0,
|
|
||||||
bottom: 0,
|
|
||||||
backgroundColor: "rgba(0,0,0,0.5)",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CircularProgress />
|
|
||||||
</Box>
|
|
||||||
) : null}
|
|
||||||
</Box>
|
|
||||||
<Box my={4}>
|
|
||||||
<Divider />
|
|
||||||
</Box>
|
|
||||||
<TextField
|
|
||||||
label="Emitter Chain"
|
|
||||||
disabled
|
|
||||||
value={recoveryParsedVAA?.emitter_chain || ""}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Emitter Address"
|
|
||||||
disabled
|
|
||||||
value={
|
|
||||||
(recoveryParsedVAA &&
|
|
||||||
hexToNativeString(
|
|
||||||
recoveryParsedVAA.emitter_address,
|
|
||||||
recoveryParsedVAA.emitter_chain
|
|
||||||
)) ||
|
|
||||||
""
|
|
||||||
}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Sequence"
|
|
||||||
disabled
|
|
||||||
value={recoveryParsedVAA?.sequence || ""}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Timestamp"
|
|
||||||
disabled
|
|
||||||
value={
|
|
||||||
(recoveryParsedVAA &&
|
|
||||||
new Date(recoveryParsedVAA.timestamp * 1000).toLocaleString()) ||
|
|
||||||
""
|
|
||||||
}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<Box my={4}>
|
|
||||||
<Divider />
|
|
||||||
</Box>
|
|
||||||
<TextField
|
|
||||||
label="Origin Chain"
|
|
||||||
disabled
|
|
||||||
value={parsedPayload?.originChain.toString() || ""}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Origin Token Address"
|
|
||||||
disabled
|
|
||||||
value={
|
|
||||||
(parsedPayload &&
|
|
||||||
hexToNativeString(
|
|
||||||
parsedPayload.originAddress,
|
|
||||||
parsedPayload.originChain
|
|
||||||
)) ||
|
|
||||||
""
|
|
||||||
}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Target Chain"
|
|
||||||
disabled
|
|
||||||
value={parsedPayload?.targetChain.toString() || ""}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Target Address"
|
|
||||||
disabled
|
|
||||||
value={
|
|
||||||
(parsedPayload &&
|
|
||||||
hexToNativeString(
|
|
||||||
parsedPayload.targetAddress,
|
|
||||||
parsedPayload.targetChain
|
|
||||||
)) ||
|
|
||||||
""
|
|
||||||
}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Amount"
|
|
||||||
disabled
|
|
||||||
value={parsedPayload?.amount.toString() || ""}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<Box my={4}>
|
|
||||||
<Divider />
|
|
||||||
</Box>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={onClose} variant="outlined" color="default">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleRecoverClick}
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
disabled={!enableRecovery || disabled}
|
|
||||||
>
|
|
||||||
Recover
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Recovery({
|
|
||||||
open,
|
|
||||||
setOpen,
|
|
||||||
disabled,
|
|
||||||
}: {
|
|
||||||
open: boolean;
|
|
||||||
setOpen: (open: boolean) => void;
|
|
||||||
disabled: boolean;
|
|
||||||
}) {
|
|
||||||
const classes = useStyles();
|
|
||||||
const handleOpenClick = useCallback(() => {
|
|
||||||
setOpen(true);
|
|
||||||
}, [setOpen]);
|
|
||||||
const handleCloseClick = useCallback(() => {
|
|
||||||
setOpen(false);
|
|
||||||
}, [setOpen]);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Tooltip title="Open Recovery Dialog">
|
|
||||||
<Fab className={classes.fab} onClick={handleOpenClick}>
|
|
||||||
<Restore />
|
|
||||||
</Fab>
|
|
||||||
</Tooltip>
|
|
||||||
<Dialog open={open} onClose={handleCloseClick} maxWidth="md" fullWidth>
|
|
||||||
<DialogTitle>Recovery</DialogTitle>
|
|
||||||
<RecoveryDialogContent onClose={handleCloseClick} disabled={disabled} />
|
|
||||||
</Dialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
||||||
import { Button, 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 { useHistory } from "react-router";
|
import { useHistory } from "react-router";
|
||||||
|
import { useBetaContext } from "../../contexts/BetaContext";
|
||||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||||
import {
|
import {
|
||||||
selectTransferAmount,
|
selectTransferAmount,
|
||||||
|
@ -20,6 +20,7 @@ import {
|
||||||
setSourceChain,
|
setSourceChain,
|
||||||
} from "../../store/transferSlice";
|
} from "../../store/transferSlice";
|
||||||
import {
|
import {
|
||||||
|
BETA_CHAINS,
|
||||||
CHAINS,
|
CHAINS,
|
||||||
ETH_MIGRATION_ASSET_MAP,
|
ETH_MIGRATION_ASSET_MAP,
|
||||||
MIGRATION_ASSET_MAP,
|
MIGRATION_ASSET_MAP,
|
||||||
|
@ -37,13 +38,10 @@ 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 isBeta = useBetaContext();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const sourceChain = useSelector(selectTransferSourceChain);
|
const sourceChain = useSelector(selectTransferSourceChain);
|
||||||
const parsedTokenAccount = useSelector(
|
const parsedTokenAccount = useSelector(
|
||||||
|
@ -92,27 +90,19 @@ function Source({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StepDescription>
|
<StepDescription>
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
Select tokens to send through the Wormhole Token Bridge.
|
||||||
Select tokens to send through the Wormhole Token Bridge.
|
|
||||||
<div style={{ flexGrow: 1 }} />
|
|
||||||
<Button
|
|
||||||
onClick={() => setIsRecoveryOpen(true)}
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
endIcon={<Restore />}
|
|
||||||
>
|
|
||||||
Perform Recovery
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</StepDescription>
|
</StepDescription>
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
|
variant="outlined"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={sourceChain}
|
value={sourceChain}
|
||||||
onChange={handleSourceChange}
|
onChange={handleSourceChange}
|
||||||
disabled={shouldLockFields}
|
disabled={shouldLockFields}
|
||||||
>
|
>
|
||||||
{CHAINS.map(({ id, name }) => (
|
{CHAINS.filter(({ id }) =>
|
||||||
|
isBeta ? true : !BETA_CHAINS.includes(id)
|
||||||
|
).map(({ id, name }) => (
|
||||||
<MenuItem key={id} value={id}>
|
<MenuItem key={id} value={id}>
|
||||||
{name}
|
{name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -143,6 +133,7 @@ function Source({
|
||||||
<LowBalanceWarning chainId={sourceChain} />
|
<LowBalanceWarning chainId={sourceChain} />
|
||||||
{hasParsedTokenAccount ? (
|
{hasParsedTokenAccount ? (
|
||||||
<TextField
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
label="Amount"
|
label="Amount"
|
||||||
type="number"
|
type="number"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { makeStyles, MenuItem, TextField, Typography } from "@material-ui/core";
|
||||||
import { Alert } from "@material-ui/lab";
|
import { Alert } from "@material-ui/lab";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { useBetaContext } from "../../contexts/BetaContext";
|
||||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||||
import useMetadata from "../../hooks/useMetadata";
|
import useMetadata from "../../hooks/useMetadata";
|
||||||
import useSyncTargetAddress from "../../hooks/useSyncTargetAddress";
|
import useSyncTargetAddress from "../../hooks/useSyncTargetAddress";
|
||||||
|
@ -20,7 +21,7 @@ import {
|
||||||
UNREGISTERED_ERROR_MESSAGE,
|
UNREGISTERED_ERROR_MESSAGE,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
import { incrementStep, setTargetChain } from "../../store/transferSlice";
|
import { incrementStep, setTargetChain } from "../../store/transferSlice";
|
||||||
import { CHAINS, CHAINS_BY_ID } from "../../utils/consts";
|
import { BETA_CHAINS, CHAINS, CHAINS_BY_ID } from "../../utils/consts";
|
||||||
import { isEVMChain } from "../../utils/ethereum";
|
import { isEVMChain } from "../../utils/ethereum";
|
||||||
import ButtonWithLoader from "../ButtonWithLoader";
|
import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
|
@ -45,6 +46,7 @@ const useStyles = makeStyles((theme) => ({
|
||||||
function Target() {
|
function Target() {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const isBeta = useBetaContext();
|
||||||
const sourceChain = useSelector(selectTransferSourceChain);
|
const sourceChain = useSelector(selectTransferSourceChain);
|
||||||
const chains = useMemo(
|
const chains = useMemo(
|
||||||
() => CHAINS.filter((c) => c.id !== sourceChain),
|
() => CHAINS.filter((c) => c.id !== sourceChain),
|
||||||
|
@ -92,17 +94,20 @@ function Target() {
|
||||||
<>
|
<>
|
||||||
<StepDescription>Select a recipient chain and address.</StepDescription>
|
<StepDescription>Select a recipient chain and address.</StepDescription>
|
||||||
<TextField
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
select
|
select
|
||||||
fullWidth
|
fullWidth
|
||||||
value={targetChain}
|
value={targetChain}
|
||||||
onChange={handleTargetChange}
|
onChange={handleTargetChange}
|
||||||
disabled={shouldLockFields}
|
disabled={shouldLockFields}
|
||||||
>
|
>
|
||||||
{chains.map(({ id, name }) => (
|
{chains
|
||||||
<MenuItem key={id} value={id}>
|
.filter(({ id }) => (isBeta ? true : !BETA_CHAINS.includes(id)))
|
||||||
{name}
|
.map(({ id, name }) => (
|
||||||
</MenuItem>
|
<MenuItem key={id} value={id}>
|
||||||
))}
|
{name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
<KeyAndBalance chainId={targetChain} balance={uiAmountString} />
|
<KeyAndBalance chainId={targetChain} balance={uiAmountString} />
|
||||||
{readableTargetAddress ? (
|
{readableTargetAddress ? (
|
||||||
|
|
|
@ -10,7 +10,6 @@ import {
|
||||||
ETH_TOKENS_THAT_CAN_BE_SWAPPED_ON_SOLANA,
|
ETH_TOKENS_THAT_CAN_BE_SWAPPED_ON_SOLANA,
|
||||||
ETH_TOKENS_THAT_EXIST_ELSEWHERE,
|
ETH_TOKENS_THAT_EXIST_ELSEWHERE,
|
||||||
SOLANA_TOKENS_THAT_EXIST_ELSEWHERE,
|
SOLANA_TOKENS_THAT_EXIST_ELSEWHERE,
|
||||||
WETH_ADDRESS,
|
|
||||||
} from "../../utils/consts";
|
} from "../../utils/consts";
|
||||||
|
|
||||||
export default function TokenWarning({
|
export default function TokenWarning({
|
||||||
|
@ -39,10 +38,13 @@ export default function TokenWarning({
|
||||||
);
|
);
|
||||||
return tokenConflictingNativeWarning ? (
|
return tokenConflictingNativeWarning ? (
|
||||||
<Alert severity="warning">{tokenConflictingNativeWarning}</Alert>
|
<Alert severity="warning">{tokenConflictingNativeWarning}</Alert>
|
||||||
) : sourceChain === CHAIN_ID_ETH && tokenAddress === WETH_ADDRESS ? (
|
) : sourceChain === CHAIN_ID_ETH &&
|
||||||
|
tokenAddress &&
|
||||||
|
getAddress(tokenAddress) ===
|
||||||
|
getAddress("0xae7ab96520de3a18e5e111b5eaab095312d7fe84") ? ( // stETH (Lido)
|
||||||
<Alert severity="warning">
|
<Alert severity="warning">
|
||||||
As of 2021-09-30, markets for Wormhole v2 wrapped WETH have not yet been
|
Lido stETH rewards can only be received on Ethereum. Use the value
|
||||||
created.
|
accruing wrapper token wstETH instead.
|
||||||
</Alert>
|
</Alert>
|
||||||
) : sourceChain === CHAIN_ID_ETH &&
|
) : sourceChain === CHAIN_ID_ETH &&
|
||||||
tokenAddress &&
|
tokenAddress &&
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
makeStyles,
|
|
||||||
Step,
|
Step,
|
||||||
StepButton,
|
StepButton,
|
||||||
StepContent,
|
StepContent,
|
||||||
Stepper,
|
Stepper,
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect } 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 { COLORS } from "../../muiTheme";
|
|
||||||
import {
|
import {
|
||||||
selectTransferActiveStep,
|
selectTransferActiveStep,
|
||||||
selectTransferIsRedeemComplete,
|
selectTransferIsRedeemComplete,
|
||||||
|
@ -20,7 +18,6 @@ import {
|
||||||
selectTransferIsSending,
|
selectTransferIsSending,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
import { setStep } from "../../store/transferSlice";
|
import { setStep } from "../../store/transferSlice";
|
||||||
import Recovery from "./Recovery";
|
|
||||||
import Redeem from "./Redeem";
|
import Redeem from "./Redeem";
|
||||||
import RedeemPreview from "./RedeemPreview";
|
import RedeemPreview from "./RedeemPreview";
|
||||||
import Send from "./Send";
|
import Send from "./Send";
|
||||||
|
@ -30,18 +27,10 @@ import SourcePreview from "./SourcePreview";
|
||||||
import Target from "./Target";
|
import Target from "./Target";
|
||||||
import TargetPreview from "./TargetPreview";
|
import TargetPreview from "./TargetPreview";
|
||||||
|
|
||||||
const useStyles = makeStyles(() => ({
|
|
||||||
rootContainer: {
|
|
||||||
backgroundColor: COLORS.nearBlackWithMinorTransparency,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
function Transfer() {
|
function Transfer() {
|
||||||
const classes = useStyles();
|
|
||||||
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 isSending = useSelector(selectTransferIsSending);
|
const isSending = useSelector(selectTransferIsSending);
|
||||||
|
@ -60,22 +49,14 @@ function Transfer() {
|
||||||
}, [preventNavigation]);
|
}, [preventNavigation]);
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="md">
|
<Container maxWidth="md">
|
||||||
<Stepper
|
<Stepper activeStep={activeStep} orientation="vertical">
|
||||||
activeStep={activeStep}
|
|
||||||
orientation="vertical"
|
|
||||||
className={classes.rootContainer}
|
|
||||||
>
|
|
||||||
<Step
|
<Step
|
||||||
expanded={activeStep >= 0}
|
expanded={activeStep >= 0}
|
||||||
disabled={preventNavigation || isRedeemComplete}
|
disabled={preventNavigation || isRedeemComplete}
|
||||||
>
|
>
|
||||||
<StepButton onClick={() => dispatch(setStep(0))}>Source</StepButton>
|
<StepButton onClick={() => dispatch(setStep(0))}>Source</StepButton>
|
||||||
<StepContent>
|
<StepContent>
|
||||||
{activeStep === 0 ? (
|
{activeStep === 0 ? <Source /> : <SourcePreview />}
|
||||||
<Source setIsRecoveryOpen={setIsRecoveryOpen} />
|
|
||||||
) : (
|
|
||||||
<SourcePreview />
|
|
||||||
)}
|
|
||||||
</StepContent>
|
</StepContent>
|
||||||
</Step>
|
</Step>
|
||||||
<Step
|
<Step
|
||||||
|
@ -107,11 +88,6 @@ function Transfer() {
|
||||||
</StepContent>
|
</StepContent>
|
||||||
</Step>
|
</Step>
|
||||||
</Stepper>
|
</Stepper>
|
||||||
<Recovery
|
|
||||||
open={isRecoveryOpen}
|
|
||||||
setOpen={setIsRecoveryOpen}
|
|
||||||
disabled={preventNavigation}
|
|
||||||
/>
|
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
import React, { ReactChildren, useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const BetaContext = React.createContext<boolean>(false);
|
||||||
|
|
||||||
|
export const BetaContextProvider = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactChildren;
|
||||||
|
}) => {
|
||||||
|
const [isBetaEnabled, setIsBetaEnabled] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let userEntered = [];
|
||||||
|
const secretSequence = [
|
||||||
|
"38",
|
||||||
|
"38",
|
||||||
|
"40",
|
||||||
|
"40",
|
||||||
|
"37",
|
||||||
|
"39",
|
||||||
|
"37",
|
||||||
|
"39",
|
||||||
|
"66",
|
||||||
|
"65",
|
||||||
|
];
|
||||||
|
const secretListener = (event: KeyboardEvent) => {
|
||||||
|
const k = event.keyCode.toString();
|
||||||
|
if (k === secretSequence[userEntered.length]) {
|
||||||
|
userEntered.push(k);
|
||||||
|
if (userEntered.length === secretSequence.length) {
|
||||||
|
userEntered = [];
|
||||||
|
setIsBetaEnabled((prev) => !prev);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
userEntered = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", secretListener);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", secretListener);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BetaContext.Provider value={isBetaEnabled}>
|
||||||
|
{children}
|
||||||
|
</BetaContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBetaContext = () => {
|
||||||
|
return useContext(BetaContext);
|
||||||
|
};
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 288.9 274" style="enable-background:new 0 0 288.9 274;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#2849A9;}
|
||||||
|
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#5795ED;}
|
||||||
|
</style>
|
||||||
|
<path class="st0" d="M151.1,0.3c33.7,0,64.9,12.1,88.7,32.9c31.8,24.5,22.6,113.9-9.6,90.3c-70.8-0.3-202.4-38.2-163.2-90.3
|
||||||
|
c4-5.3,9-9.6,14.5-13.7h-0.3c0.9-0.5,1.9-1,2.8-1.6c0.9-0.5,1.9-1.1,2.8-1.6h0c2.8-1.6,5.6-3.1,8.7-4.3
|
||||||
|
C112.5,4.6,131.3,0.3,151.1,0.3z M174.9,272.8c-14.2,0.9-42.6-21.4-50.7-50.9c-15.1-55.9,107.2-84.4,118.7-85.4
|
||||||
|
c31.2,0.9,38.9,38.2,16.1,76.7C229.3,262.6,175.5,272.8,174.9,272.8z"/>
|
||||||
|
<path class="st1" d="M14.8,77.9c9.9,2.8,70.5-16.5,88.4-43.8c0.3-0.3,14.2-21.7-12.7-22c-3.1,0-11.7,0.3-20.1,5.3
|
||||||
|
c-4,2.5-7.7,5-11.4,7.8c-5.8,4.3-11.3,9.5-16.5,14.4h0l-0.2,0.2c-5.3,5-10.2,10.9-14.5,16.8c-4.3,5.9-8.3,12.4-11.7,18.9
|
||||||
|
c-0.2,0.5-0.4,0.9-0.6,1.2C15.2,77,15,77.4,14.8,77.9z M86.5,272.8c1.9-2.8,3.1-36.6,1.9-45.3c-1.2-8.7-4-26.4-20.7-55.6
|
||||||
|
c-2.8-4.7-16.1-26.4-26-39.7c-5.6-7.8-11.7-15-17.8-22.3h0c-5.1-6-10.2-12.1-15-18.4c-0.3,0.8-0.5,1.5-0.8,2.2s-0.5,1.4-0.8,2.2
|
||||||
|
c-2.5,7.1-4.3,14.6-5.6,22.4S0,133.8,0,141.8c0,8.1,0.6,15.8,1.9,23.6s3.4,15.2,5.6,22.4c2.2,7.1,5.3,14.3,8.7,20.8
|
||||||
|
s7.4,13,11.7,18.9c4.3,5.9,9.3,11.5,14.5,16.8c4.9,5.3,10.8,10.2,16.7,14.6h0h0c4.6,3.1,9.3,6.2,13.9,9c8.5,5,11.7,5,13.4,5
|
||||||
|
C86.4,272.8,86.4,272.8,86.5,272.8z M288.9,141.8c0,18.9-3.7,36.9-10.2,53.4c-15.7,17-115.3-20.7-130.8-26.6c-1.2-0.5-2-0.7-2-0.8
|
||||||
|
c-15.8-6.8-63.3-27.9-67.7-60.8c-6.2-47.5,89.6-80.7,131.9-82c4.9,0,20.4,0.3,29.4,7.5C269.8,59.2,288.9,98.4,288.9,141.8z
|
||||||
|
M188.8,260.1c-3.7,12.1,10.2,16.5,22.6,10.6c24.7-13,45.1-33.2,59-57.1c0.9-1.2,0-2.5-1.5-2.2C255.6,212.6,195.6,236.5,188.8,260.1
|
||||||
|
z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 1.0 MiB |
|
@ -6,6 +6,7 @@ import { Provider } from "react-redux";
|
||||||
import { HashRouter } from "react-router-dom";
|
import { HashRouter } from "react-router-dom";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import BackgroundImage from "./components/BackgroundImage";
|
import BackgroundImage from "./components/BackgroundImage";
|
||||||
|
import { BetaContextProvider } from "./contexts/BetaContext";
|
||||||
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";
|
||||||
|
@ -20,16 +21,18 @@ ReactDOM.render(
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<SnackbarProvider maxSnack={3}>
|
<SnackbarProvider maxSnack={3}>
|
||||||
<SolanaWalletProvider>
|
<BetaContextProvider>
|
||||||
<EthereumProviderProvider>
|
<SolanaWalletProvider>
|
||||||
<TerraWalletProvider>
|
<EthereumProviderProvider>
|
||||||
<HashRouter>
|
<TerraWalletProvider>
|
||||||
<BackgroundImage />
|
<HashRouter>
|
||||||
<App />
|
<BackgroundImage />
|
||||||
</HashRouter>
|
<App />
|
||||||
</TerraWalletProvider>
|
</HashRouter>
|
||||||
</EthereumProviderProvider>
|
</TerraWalletProvider>
|
||||||
</SolanaWalletProvider>
|
</EthereumProviderProvider>
|
||||||
|
</SolanaWalletProvider>
|
||||||
|
</BetaContextProvider>
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
import { createTheme, responsiveFontSizes } from "@material-ui/core";
|
import { createTheme, responsiveFontSizes } from "@material-ui/core";
|
||||||
|
|
||||||
export const COLORS = {
|
export const COLORS = {
|
||||||
|
blue: "#1975e6",
|
||||||
|
blueWithTransparency: "rgba(25, 117, 230, 0.8)",
|
||||||
|
green: "#0ac2af",
|
||||||
|
greenWithTransparency: "rgba(10, 194, 175, 0.8)",
|
||||||
lightGreen: "rgba(51, 242, 223, 1)",
|
lightGreen: "rgba(51, 242, 223, 1)",
|
||||||
green: "#00EFD8",
|
lightBlue: "#83b9fc",
|
||||||
blue: "#0074FF",
|
nearBlack: "#000008",
|
||||||
blueWithTransparency: "rgba(0, 116, 255, 0.8)",
|
nearBlackWithMinorTransparency: "rgba(0,0,0,.25)",
|
||||||
greenWithTransparency: "rgba(0,239,216,0.8)",
|
red: "#aa0818",
|
||||||
nearBlack: "#010114",
|
darkRed: "#810612",
|
||||||
nearBlackWithMinorTransparency: "rgba(0,0,0,.97)",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const theme = responsiveFontSizes(
|
export const theme = responsiveFontSizes(
|
||||||
|
@ -24,31 +27,106 @@ export const theme = responsiveFontSizes(
|
||||||
},
|
},
|
||||||
primary: {
|
primary: {
|
||||||
main: COLORS.blueWithTransparency, // #0074FF
|
main: COLORS.blueWithTransparency, // #0074FF
|
||||||
|
light: COLORS.lightBlue,
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
main: COLORS.greenWithTransparency, // #00EFD8
|
main: COLORS.greenWithTransparency, // #00EFD8
|
||||||
light: COLORS.lightGreen,
|
light: COLORS.lightGreen,
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
main: "#FD3503",
|
main: COLORS.red,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
typography: {
|
typography: {
|
||||||
fontFamily: "'Sora', sans-serif",
|
fontFamily: "'Sora', sans-serif",
|
||||||
|
h1: {
|
||||||
|
fontWeight: "200",
|
||||||
|
},
|
||||||
h2: {
|
h2: {
|
||||||
fontWeight: "700",
|
fontWeight: "300",
|
||||||
},
|
},
|
||||||
h4: {
|
h4: {
|
||||||
fontWeight: "500",
|
fontWeight: "500",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
overrides: {
|
overrides: {
|
||||||
|
MuiAccordion: {
|
||||||
|
root: {
|
||||||
|
backgroundColor: COLORS.nearBlackWithMinorTransparency,
|
||||||
|
"&:before": {
|
||||||
|
display: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rounded: {
|
||||||
|
"&:first-child": {
|
||||||
|
borderTopLeftRadius: "16px",
|
||||||
|
borderTopRightRadius: "16px",
|
||||||
|
},
|
||||||
|
"&:last-child": {
|
||||||
|
borderBottomLeftRadius: "16px",
|
||||||
|
borderBottomRightRadius: "16px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
MuiButton: {
|
MuiButton: {
|
||||||
root: {
|
root: {
|
||||||
borderRadius: "5px",
|
borderRadius: "5px",
|
||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
MuiLink: {
|
||||||
|
root: {
|
||||||
|
color: COLORS.lightBlue,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiPaper: {
|
||||||
|
rounded: {
|
||||||
|
borderRadius: "16px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiStepper: {
|
||||||
|
root: {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiStep: {
|
||||||
|
root: {
|
||||||
|
backgroundColor: COLORS.nearBlackWithMinorTransparency,
|
||||||
|
borderRadius: "16px",
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiStepConnector: {
|
||||||
|
lineVertical: {
|
||||||
|
borderLeftWidth: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiStepContent: {
|
||||||
|
root: {
|
||||||
|
borderLeftWidth: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiStepLabel: {
|
||||||
|
label: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "300",
|
||||||
|
"&.MuiStepLabel-active": {
|
||||||
|
fontWeight: "300",
|
||||||
|
},
|
||||||
|
"&.MuiStepLabel-completed": {
|
||||||
|
fontWeight: "300",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiTab: {
|
||||||
|
root: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: "300",
|
||||||
|
padding: 12,
|
||||||
|
textTransform: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -22,6 +22,10 @@ export interface ChainInfo {
|
||||||
export const CHAINS =
|
export const CHAINS =
|
||||||
CLUSTER === "mainnet"
|
CLUSTER === "mainnet"
|
||||||
? [
|
? [
|
||||||
|
// {
|
||||||
|
// id: CHAIN_ID_BSC,
|
||||||
|
// name: "Binance Smart Chain",
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
id: CHAIN_ID_ETH,
|
id: CHAIN_ID_ETH,
|
||||||
name: "Ethereum",
|
name: "Ethereum",
|
||||||
|
@ -30,6 +34,10 @@ export const CHAINS =
|
||||||
id: CHAIN_ID_SOLANA,
|
id: CHAIN_ID_SOLANA,
|
||||||
name: "Solana",
|
name: "Solana",
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// id: CHAIN_ID_TERRA,
|
||||||
|
// name: "Terra",
|
||||||
|
// },
|
||||||
]
|
]
|
||||||
: CLUSTER === "testnet"
|
: CLUSTER === "testnet"
|
||||||
? [
|
? [
|
||||||
|
@ -64,6 +72,8 @@ export const CHAINS =
|
||||||
name: "Terra",
|
name: "Terra",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
export const BETA_CHAINS =
|
||||||
|
CLUSTER === "mainnet" ? [CHAIN_ID_BSC, CHAIN_ID_TERRA] : [];
|
||||||
export const CHAINS_WITH_NFT_SUPPORT = CHAINS.filter(
|
export const CHAINS_WITH_NFT_SUPPORT = CHAINS.filter(
|
||||||
({ id }) =>
|
({ id }) =>
|
||||||
id === CHAIN_ID_ETH || id === CHAIN_ID_BSC || id === CHAIN_ID_SOLANA
|
id === CHAIN_ID_ETH || id === CHAIN_ID_BSC || id === CHAIN_ID_SOLANA
|
||||||
|
@ -202,13 +212,13 @@ export const SOL_CUSTODY_ADDRESS =
|
||||||
export const TERRA_TEST_TOKEN_ADDRESS =
|
export const TERRA_TEST_TOKEN_ADDRESS =
|
||||||
"terra13nkgqrfymug724h8pprpexqj9h629sa3ncw7sh";
|
"terra13nkgqrfymug724h8pprpexqj9h629sa3ncw7sh";
|
||||||
export const TERRA_BRIDGE_ADDRESS =
|
export const TERRA_BRIDGE_ADDRESS =
|
||||||
CLUSTER === "mainnet"
|
CLUSTER === "mainnet"
|
||||||
? "terra1dq03ugtd40zu9hcgdzrsq6z2z4hwhc9tqk2uy5"
|
? "terra1dq03ugtd40zu9hcgdzrsq6z2z4hwhc9tqk2uy5"
|
||||||
: CLUSTER === "testnet"
|
: CLUSTER === "testnet"
|
||||||
? "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5"
|
? "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5"
|
||||||
: "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5";
|
: "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5";
|
||||||
export const TERRA_TOKEN_BRIDGE_ADDRESS =
|
export const TERRA_TOKEN_BRIDGE_ADDRESS =
|
||||||
CLUSTER === "mainnet"
|
CLUSTER === "mainnet"
|
||||||
? "terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf"
|
? "terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf"
|
||||||
: CLUSTER === "testnet"
|
: CLUSTER === "testnet"
|
||||||
? "terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4"
|
? "terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4"
|
||||||
|
|
Loading…
Reference in New Issue