bridge_ui: confidence enhancement

Change-Id: Ied0f0fe641a89bb99c9b6a5d0e1400e70b2c8d77
This commit is contained in:
Evan Gray 2021-10-06 18:02:56 -04:00
parent 4bdb714594
commit 461e8f256e
31 changed files with 1006 additions and 5020 deletions

View File

@ -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>

View File

@ -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>
); );
} }

View File

@ -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 (
<> <>

View File

@ -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}

View File

@ -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}>

View File

@ -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}

View File

@ -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>
); );
}; };

View File

@ -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>
); );
} }

View File

@ -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}

View File

@ -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}

View File

@ -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>
</>
);
}

View File

@ -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>

View File

@ -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}

View File

@ -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>
); );
} }

View File

@ -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}

View File

@ -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>
);
}

View File

@ -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))}

View File

@ -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}

View File

@ -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"}

View File

@ -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}

View File

@ -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>
</>
);
}

View File

@ -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

View File

@ -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 ? (

View File

@ -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 &&

View File

@ -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>
); );
} }

View File

@ -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);
};

View File

@ -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

View File

@ -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>

View File

@ -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",
},
},
}, },
}) })
); );

View File

@ -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"