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" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Wormhole Token Bridge"
/>
<meta name="description" content="Wormhole Token Bridge" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
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.
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.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@300;400;500;700&display=swap" rel="stylesheet">
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Sora:wght@200;300;400;500;700&display=swap"
rel="stylesheet"
/>
<title>Wormhole Token Bridge</title>
</head>
<body>

View File

@ -1,14 +1,22 @@
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
import {
AppBar,
Button,
Container,
Hidden,
IconButton,
Link,
makeStyles,
Tab,
Tabs,
Toolbar,
Tooltip,
Typography,
} 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 {
Link as RouterLink,
NavLink,
@ -19,17 +27,18 @@ import {
import Attest from "./components/Attest";
import Home from "./components/Home";
import Migration from "./components/Migration";
import EthereumQuickMigrate from "./components/Migration/EthereumQuickMigrate";
import NFT from "./components/NFT";
import NFTOriginVerifier from "./components/NFTOriginVerifier";
import Recovery from "./components/Recovery";
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 EthereumQuickMigrate from "./components/Migration/EthereumQuickMigrate";
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
const useStyles = makeStyles((theme) => ({
appBar: {
borderBottom: `1px solid ${theme.palette.divider}`,
background: COLORS.nearBlackWithMinorTransparency,
"& > .MuiToolbar-root": {
margin: "auto",
width: "100%",
@ -40,13 +49,6 @@ const useStyles = makeStyles((theme) => ({
flex: 1,
width: "100vw",
},
logo: {
verticalAlign: "middle",
height: 52,
[theme.breakpoints.down("xs")]: {
height: 42,
},
},
link: {
...theme.typography.body1,
color: theme.palette.text.primary,
@ -58,9 +60,14 @@ const useStyles = makeStyles((theme) => ({
marginLeft: theme.spacing(1),
},
"&.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: {
[theme.breakpoints.up("sm")]: {
margin: theme.spacing(2, 0),
@ -69,70 +76,93 @@ const useStyles = makeStyles((theme) => ({
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() {
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 (
<>
<div className={classes.bg}>
<AppBar position="static" color="inherit" className={classes.appBar}>
<Toolbar>
<RouterLink to="/">
<img
src={wormholeLogo}
alt="Wormhole Logo"
className={classes.logo}
/>
</RouterLink>
<Link
component={RouterLink}
to="/"
className={clsx(classes.link, classes.brandText)}
>
wormhole
</Link>
<div className={classes.spacer} />
<Hidden implementation="css" xsDown>
<div style={{ display: "flex", alignItems: "center" }}>
<Tooltip title="Transfer NFTs to another blockchain">
<Link component={NavLink} to="/nft" className={classes.link}>
NFTs
</Link>
</Tooltip>
<Tooltip title="Transfer tokens to another blockchain">
<Link
component={NavLink}
{isHomepage ? (
<Button
component={RouterLink}
to="/transfer"
className={classes.link}
variant="contained"
color="primary"
size="large"
className={classes.gradientButton}
>
Transfer
</Link>
</Tooltip>
<Tooltip title="Register a new wrapped token">
<Link
component={NavLink}
to="/register"
className={classes.link}
>
Register
</Link>
</Tooltip>
<Tooltip title="View the source code">
<IconButton
href="https://github.com/certusone/wormhole"
target="_blank"
size="small"
className={classes.link}
>
<GitHub />
</IconButton>
</Tooltip>
Transfer Tokens
</Button>
) : (
<Tooltip title="View the FAQ">
<IconButton
<Button
href="https://docs.wormholenetwork.com/wormhole/faqs"
target="_blank"
size="small"
className={classes.link}
variant="outlined"
endIcon={<HelpOutline />}
>
<Help />
</IconButton>
FAQ
</Button>
</Tooltip>
)}
</div>
</Hidden>
<Hidden implementation="css" smUp>
{isHomepage ? (
<Tooltip title="Transfer tokens to another blockchain">
<IconButton
component={NavLink}
@ -143,16 +173,7 @@ function App() {
<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"
@ -160,20 +181,47 @@ function App() {
size="small"
className={classes.link}
>
<Help />
<HelpOutline />
</IconButton>
</Tooltip>
)}
</Hidden>
</Toolbar>
</AppBar>
{CLUSTER === "mainnet" ? null : (
<AppBar position="static" color="secondary">
<AppBar position="static" className={classes.betaBanner}>
<Typography style={{ textAlign: "center" }}>
Caution! You are using the {CLUSTER} build of this app.
</Typography>
</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}>
{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>
<Route exact path="/nft">
<NFT />
@ -184,6 +232,9 @@ function App() {
<Route exact path="/transfer">
<Transfer />
</Route>
<Route exact path="/redeem">
<Recovery />
</Route>
<Route exact path="/register">
<Attest />
</Route>
@ -204,7 +255,7 @@ function App() {
</Route>
</Switch>
</div>
</>
</div>
);
}

View File

@ -31,7 +31,7 @@ export default function CreatePreview() {
}, [dispatch, push]);
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 (
<>

View File

@ -1,6 +1,7 @@
import { makeStyles, MenuItem, TextField } from "@material-ui/core";
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useBetaContext } from "../../contexts/BetaContext";
import {
incrementStep,
setSourceAsset,
@ -12,7 +13,7 @@ import {
selectAttestSourceAsset,
selectAttestSourceChain,
} from "../../store/selectors";
import { CHAINS } from "../../utils/consts";
import { BETA_CHAINS, CHAINS } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
import LowBalanceWarning from "../LowBalanceWarning";
@ -26,6 +27,7 @@ const useStyles = makeStyles((theme) => ({
function Source() {
const classes = useStyles();
const dispatch = useDispatch();
const isBeta = useBetaContext();
const sourceChain = useSelector(selectAttestSourceChain);
const sourceAsset = useSelector(selectAttestSourceAsset);
const isSourceComplete = useSelector(selectAttestIsSourceComplete);
@ -49,12 +51,15 @@ function Source() {
<>
<TextField
select
variant="outlined"
fullWidth
value={sourceChain}
onChange={handleSourceChange}
disabled={shouldLockFields}
>
{CHAINS.map(({ id, name }) => (
{CHAINS.filter(({ id }) =>
isBeta ? true : !BETA_CHAINS.includes(id)
).map(({ id, name }) => (
<MenuItem key={id} value={id}>
{name}
</MenuItem>
@ -63,6 +68,7 @@ function Source() {
<KeyAndBalance chainId={sourceChain} />
<TextField
label="Asset"
variant="outlined"
fullWidth
className={classes.transferField}
value={sourceAsset}

View File

@ -2,6 +2,7 @@ import { makeStyles, MenuItem, TextField, Typography } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useBetaContext } from "../../contexts/BetaContext";
import { EthGasEstimateSummary } from "../../hooks/useTransactionFees";
import { incrementStep, setTargetChain } from "../../store/attestSlice";
import {
@ -10,7 +11,7 @@ import {
selectAttestSourceChain,
selectAttestTargetChain,
} 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 ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
@ -26,6 +27,7 @@ const useStyles = makeStyles((theme) => ({
function Target() {
const classes = useStyles();
const dispatch = useDispatch();
const isBeta = useBetaContext();
const sourceChain = useSelector(selectAttestSourceChain);
const chains = useMemo(
() => CHAINS.filter((c) => c.id !== sourceChain),
@ -47,12 +49,15 @@ function Target() {
<>
<TextField
select
variant="outlined"
fullWidth
value={targetChain}
onChange={handleTargetChange}
disabled={shouldLockFields}
>
{chains.map(({ id, name }) => (
{chains
.filter(({ id }) => (isBeta ? true : !BETA_CHAINS.includes(id)))
.map(({ id, name }) => (
<MenuItem key={id} value={id}>
{name}
</MenuItem>

View File

@ -1,6 +1,5 @@
import {
Container,
makeStyles,
Step,
StepButton,
StepContent,
@ -9,7 +8,6 @@ import {
import { Alert } from "@material-ui/lab";
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { COLORS } from "../../muiTheme";
import { setStep } from "../../store/attestSlice";
import {
selectAttestActiveStep,
@ -27,14 +25,7 @@ import SourcePreview from "./SourcePreview";
import Target from "./Target";
import TargetPreview from "./TargetPreview";
const useStyles = makeStyles(() => ({
rootContainer: {
backgroundColor: COLORS.nearBlackWithMinorTransparency,
},
}));
function Attest() {
const classes = useStyles();
const dispatch = useDispatch();
const activeStep = useSelector(selectAttestActiveStep);
const isSending = useSelector(selectAttestIsSending);
@ -57,11 +48,7 @@ function Attest() {
This form allows you to register a token on a new foreign chain. Tokens
must be registered before they can be transferred.
</Alert>
<Stepper
activeStep={activeStep}
orientation="vertical"
className={classes.rootContainer}
>
<Stepper activeStep={activeStep} orientation="vertical">
<Step
expanded={activeStep >= 0}
disabled={preventNavigation || isCreateComplete}

View File

@ -1,6 +1,5 @@
import { makeStyles } from "@material-ui/core";
import { useRouteMatch } from "react-router";
import holev2 from "../images/holev2.svg";
// import { useRouteMatch } from "react-router";
const useStyles = makeStyles((theme) => ({
holeOuterContainer: {
@ -31,19 +30,11 @@ const useStyles = makeStyles((theme) => ({
const BackgroundImage = () => {
const classes = useStyles();
const isHomepage = useRouteMatch({ path: "/", exact: true });
// const isHomepage = useRouteMatch({ path: "/", exact: true });
return (
<div className={classes.holeOuterContainer}>
<div className={classes.holeInnerContainer}>
<img
src={holev2}
alt=""
className={
classes.holeImage + (isHomepage ? "" : " " + classes.blurred)
}
/>
</div>
<div className={classes.holeInnerContainer}></div>
</div>
);
};

View File

@ -1,5 +1,4 @@
import {
Button,
Card,
Container,
Link,
@ -7,7 +6,6 @@ import {
Typography,
} from "@material-ui/core";
import { Link as RouterLink } from "react-router-dom";
import overview from "../../images/overview2.svg";
import { COLORS } from "../../muiTheme";
const useStyles = makeStyles((theme) => ({
@ -29,7 +27,7 @@ const useStyles = makeStyles((theme) => ({
WebkitTextFillColor: "transparent",
MozBackgroundClip: "text",
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: {
marginBottom: theme.spacing(2),
@ -45,8 +43,7 @@ const useStyles = makeStyles((theme) => ({
maxWidth: "100%",
},
mainCard: {
padding: theme.spacing(1),
borderRadius: "5px",
padding: theme.spacing(8),
backgroundColor: COLORS.nearBlackWithMinorTransparency,
},
spacer: {
@ -60,12 +57,12 @@ function Home() {
<div>
<Container maxWidth="md">
<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>
</Typography>
</div>
</Container>
<Container maxWidth="sm">
<Container maxWidth="md">
<Card className={classes.mainCard}>
<Typography variant="h4" className={classes.description}>
Wormhole v2 is here!
@ -74,26 +71,11 @@ function Home() {
The Wormhole Token Bridge allows you to seamlessly transfer
tokenized assets across Solana and Ethereum.
</Typography>
<Button
component={RouterLink}
to="/transfer"
variant="contained"
color="secondary"
size="large"
className={classes.button}
>
Transfer Tokens
</Button>
<div className={classes.spacer} />
<Typography variant="subtitle1" className={classes.description}>
If you transferred assets using the previous version of Wormhole,
most assets can be migrated to v2 on the{" "}
<Link
component={RouterLink}
to="/transfer"
color="secondary"
noWrap
>
<Link component={RouterLink} to="/transfer" noWrap>
transfer page
</Link>
.
@ -101,23 +83,18 @@ function Home() {
<Typography variant="subtitle1" className={classes.description}>
For assets that don't support the migration, the v1 UI can be found
at{" "}
<Link href="https://v1.wormholebridge.com" color="secondary">
<Link href="https://v1.wormholebridge.com">
v1.wormholebridge.com
</Link>
</Typography>
<Typography variant="subtitle1" className={classes.description}>
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
</Link>
</Typography>
</Card>
</Container>
<Container maxWidth="md">
<div className={classes.centeredContainer}>
<img src={overview} alt="overview" className={classes.overview} />
</div>
</Container>
</div>
);
}

View File

@ -182,6 +182,7 @@ export default function EthereumWorkflow({
{explainerContent}
<div className={classes.spacer} />
<TextField
variant="outlined"
value={migrationAmount}
type="number"
onChange={handleAmountChange}

View File

@ -460,6 +460,7 @@ export default function Workflow({
) : null}
<div className={classes.spacer} />
<TextField
variant="outlined"
value={migrationAmount}
type="number"
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 { Restore, VerifiedUser } from "@material-ui/icons";
import { VerifiedUser } from "@material-ui/icons";
import { Alert } from "@material-ui/lab";
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link } from "react-router-dom";
import { useBetaContext } from "../../contexts/BetaContext";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import { incrementStep, setSourceChain } from "../../store/nftSlice";
import {
@ -13,7 +14,7 @@ import {
selectNFTSourceChain,
selectNFTSourceError,
} 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 ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
@ -25,21 +26,12 @@ const useStyles = makeStyles((theme) => ({
transferField: {
marginTop: theme.spacing(5),
},
buttonWrapper: {
textAlign: "right",
},
nftOriginVerifierButton: {
marginTop: theme.spacing(0.5),
},
}));
function Source({
setIsRecoveryOpen,
}: {
setIsRecoveryOpen: (open: boolean) => void;
}) {
function Source() {
const classes = useStyles();
const dispatch = useDispatch();
const isBeta = useBetaContext();
const sourceChain = useSelector(selectNFTSourceChain);
const uiAmountString = useSelector(selectNFTSourceBalanceString);
const error = useSelector(selectNFTSourceError);
@ -62,39 +54,29 @@ function Source({
Select an NFT to send through the Wormhole NFT Bridge.
<div style={{ flexGrow: 1 }} />
<div>
<div className={classes.buttonWrapper}>
<Button
onClick={() => setIsRecoveryOpen(true)}
size="small"
variant="outlined"
endIcon={<Restore />}
>
Perform Recovery
</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>
</StepDescription>
<TextField
variant="outlined"
select
fullWidth
value={sourceChain}
onChange={handleSourceChange}
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}>
{name}
</MenuItem>

View File

@ -9,6 +9,7 @@ import { PublicKey } from "@solana/web3.js";
import { BigNumber, ethers } from "ethers";
import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useBetaContext } from "../../contexts/BetaContext";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import useSyncTargetAddress from "../../hooks/useSyncTargetAddress";
import { EthGasEstimateSummary } from "../../hooks/useTransactionFees";
@ -26,7 +27,11 @@ import {
selectNFTTargetChain,
selectNFTTargetError,
} 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 ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
@ -46,6 +51,7 @@ const useStyles = makeStyles((theme) => ({
function Target() {
const classes = useStyles();
const dispatch = useDispatch();
const isBeta = useBetaContext();
const sourceChain = useSelector(selectNFTSourceChain);
const chains = useMemo(
() => CHAINS_WITH_NFT_SUPPORT.filter((c) => c.id !== sourceChain),
@ -91,10 +97,13 @@ function Target() {
<TextField
select
fullWidth
variant="outlined"
value={targetChain}
onChange={handleTargetChange}
>
{chains.map(({ id, name }) => (
{chains
.filter(({ id }) => (isBeta ? true : !BETA_CHAINS.includes(id)))
.map(({ id, name }) => (
<MenuItem key={id} value={id}>
{name}
</MenuItem>
@ -104,6 +113,7 @@ function Target() {
<TextField
label="Recipient Address"
fullWidth
variant="outlined"
className={classes.transferField}
value={readableTargetAddress}
disabled={true}
@ -113,12 +123,14 @@ function Target() {
<TextField
label="Token Address"
fullWidth
variant="outlined"
className={classes.transferField}
value={targetAsset || ""}
disabled={true}
/>
{isEVMChain(targetChain) ? (
<TextField
variant="outlined"
label="TokenId"
fullWidth
className={classes.transferField}

View File

@ -1,15 +1,15 @@
import {
Container,
makeStyles,
Step,
StepButton,
StepContent,
Stepper,
} from "@material-ui/core";
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import useCheckIfWormholeWrapped from "../../hooks/useCheckIfWormholeWrapped";
import useFetchTargetAsset from "../../hooks/useFetchTargetAsset";
import { setStep } from "../../store/nftSlice";
import {
selectNFTActiveStep,
selectNFTIsRedeemComplete,
@ -17,8 +17,6 @@ import {
selectNFTIsSendComplete,
selectNFTIsSending,
} from "../../store/selectors";
import { setStep } from "../../store/nftSlice";
import Recovery from "./Recovery";
import Redeem from "./Redeem";
import RedeemPreview from "./RedeemPreview";
import Send from "./Send";
@ -27,19 +25,10 @@ import Source from "./Source";
import SourcePreview from "./SourcePreview";
import Target from "./Target";
import TargetPreview from "./TargetPreview";
import { COLORS } from "../../muiTheme";
const useStyles = makeStyles(() => ({
rootContainer: {
backgroundColor: COLORS.nearBlackWithMinorTransparency,
},
}));
function NFT() {
const classes = useStyles();
useCheckIfWormholeWrapped(true);
useFetchTargetAsset(true);
const [isRecoveryOpen, setIsRecoveryOpen] = useState(false);
const dispatch = useDispatch();
const activeStep = useSelector(selectNFTActiveStep);
const isSending = useSelector(selectNFTIsSending);
@ -58,22 +47,14 @@ function NFT() {
}, [preventNavigation]);
return (
<Container maxWidth="md">
<Stepper
activeStep={activeStep}
orientation="vertical"
className={classes.rootContainer}
>
<Stepper activeStep={activeStep} orientation="vertical">
<Step
expanded={activeStep >= 0}
disabled={preventNavigation || isRedeemComplete}
>
<StepButton onClick={() => dispatch(setStep(0))}>Source</StepButton>
<StepContent>
{activeStep === 0 ? (
<Source setIsRecoveryOpen={setIsRecoveryOpen} />
) : (
<SourcePreview />
)}
{activeStep === 0 ? <Source /> : <SourcePreview />}
</StepContent>
</Step>
<Step
@ -103,11 +84,6 @@ function NFT() {
</StepContent>
</Step>
</Stepper>
<Recovery
open={isRecoveryOpen}
setOpen={setIsRecoveryOpen}
disabled={preventNavigation}
/>
</Container>
);
}

View File

@ -24,12 +24,14 @@ import { Launch } from "@material-ui/icons";
import { Alert } from "@material-ui/lab";
import { Connection } from "@solana/web3.js";
import { useCallback, useEffect, useState } from "react";
import { useBetaContext } from "../contexts/BetaContext";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import useIsWalletReady from "../hooks/useIsWalletReady";
import { getMetaplexData } from "../hooks/useMetaplexData";
import { COLORS } from "../muiTheme";
import { NFTParsedTokenAccount } from "../store/nftSlice";
import {
BETA_CHAINS,
CHAINS_BY_ID,
CHAINS_WITH_NFT_SUPPORT,
getNFTBridgeAddressForChain,
@ -65,11 +67,10 @@ const useStyles = makeStyles((theme) => ({
WebkitTextFillColor: "transparent",
MozBackgroundClip: "text",
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: {
padding: theme.spacing(1),
borderRadius: "5px",
backgroundColor: COLORS.nearBlackWithMinorTransparency,
},
originHeader: {
@ -89,6 +90,7 @@ const useStyles = makeStyles((theme) => ({
export default function NFTOriginVerifier() {
const classes = useStyles();
const isBeta = useBetaContext();
const { provider, signerAddress } = useEthereumProvider();
const [lookupChain, setLookupChain] = useState(CHAIN_ID_ETH);
const { isReady, statusMessage } = useIsWalletReady(lookupChain);
@ -232,7 +234,7 @@ export default function NFTOriginVerifier() {
<div>
<Container maxWidth="md">
<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>
</Typography>
</div>
@ -245,13 +247,16 @@ export default function NFTOriginVerifier() {
</Alert>
<TextField
select
variant="outlined"
label="Chain"
value={lookupChain}
onChange={handleChainChange}
fullWidth
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}>
{name}
</MenuItem>
@ -262,6 +267,7 @@ export default function NFTOriginVerifier() {
) : null}
<TextField
fullWidth
variant="outlined"
margin="normal"
label="Paste an address"
value={lookupAsset}
@ -270,6 +276,7 @@ export default function NFTOriginVerifier() {
{isEVMChain(lookupChain) ? (
<TextField
fullWidth
variant="outlined"
margin="normal"
label="Paste a tokenId"
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 { LinkOff } from "@material-ui/icons";
const useStyles = makeStyles((theme) => ({
button: {
display: "block",
display: "flex",
margin: `${theme.spacing(1)}px auto`,
width: "100%",
maxWidth: 400,
@ -25,11 +26,12 @@ const ToggleConnectedButton = ({
return connected ? (
<Tooltip title={pk}>
<Button
color="secondary"
color="primary"
variant="contained"
size="small"
onClick={disconnect}
className={classes.button}
startIcon={<LinkOff />}
>
Disconnect {pk.substring(0, is0x ? 6 : 3)}...
{pk.substr(pk.length - (is0x ? 4 : 3))}

View File

@ -584,6 +584,7 @@ export default function EthereumSourceTokenSelector(
) : advancedMode ? (
<>
<TextField
variant="outlined"
fullWidth
label="Enter an asset address"
value={advancedModeHolderString}
@ -602,6 +603,7 @@ export default function EthereumSourceTokenSelector(
/>
{nft ? (
<TextField
variant="outlined"
fullWidth
label="Enter a tokenId"
value={advancedModeHolderTokenIdRaw}

View File

@ -113,6 +113,7 @@ export const TokenSelector = (props: TokenSelectorProps) => {
/>
) : (
<TextField
variant="outlined"
placeholder="Asset"
fullWidth
value={"Not Implemented"}

View File

@ -296,6 +296,7 @@ export default function TerraSourceTokenSelector(
<>
<TextField
fullWidth
variant="outlined"
label="Enter an asset address"
value={advancedModeHolderString}
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 { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
import { Restore } from "@material-ui/icons";
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useHistory } from "react-router";
import { useBetaContext } from "../../contexts/BetaContext";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import {
selectTransferAmount,
@ -20,6 +20,7 @@ import {
setSourceChain,
} from "../../store/transferSlice";
import {
BETA_CHAINS,
CHAINS,
ETH_MIGRATION_ASSET_MAP,
MIGRATION_ASSET_MAP,
@ -37,13 +38,10 @@ const useStyles = makeStyles((theme) => ({
},
}));
function Source({
setIsRecoveryOpen,
}: {
setIsRecoveryOpen: (open: boolean) => void;
}) {
function Source() {
const classes = useStyles();
const dispatch = useDispatch();
const isBeta = useBetaContext();
const history = useHistory();
const sourceChain = useSelector(selectTransferSourceChain);
const parsedTokenAccount = useSelector(
@ -92,27 +90,19 @@ function Source({
return (
<>
<StepDescription>
<div style={{ display: "flex", alignItems: "center" }}>
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>
<TextField
select
variant="outlined"
fullWidth
value={sourceChain}
onChange={handleSourceChange}
disabled={shouldLockFields}
>
{CHAINS.map(({ id, name }) => (
{CHAINS.filter(({ id }) =>
isBeta ? true : !BETA_CHAINS.includes(id)
).map(({ id, name }) => (
<MenuItem key={id} value={id}>
{name}
</MenuItem>
@ -143,6 +133,7 @@ function Source({
<LowBalanceWarning chainId={sourceChain} />
{hasParsedTokenAccount ? (
<TextField
variant="outlined"
label="Amount"
type="number"
fullWidth

View File

@ -3,6 +3,7 @@ import { makeStyles, MenuItem, TextField, Typography } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useBetaContext } from "../../contexts/BetaContext";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import useMetadata from "../../hooks/useMetadata";
import useSyncTargetAddress from "../../hooks/useSyncTargetAddress";
@ -20,7 +21,7 @@ import {
UNREGISTERED_ERROR_MESSAGE,
} from "../../store/selectors";
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 ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
@ -45,6 +46,7 @@ const useStyles = makeStyles((theme) => ({
function Target() {
const classes = useStyles();
const dispatch = useDispatch();
const isBeta = useBetaContext();
const sourceChain = useSelector(selectTransferSourceChain);
const chains = useMemo(
() => CHAINS.filter((c) => c.id !== sourceChain),
@ -92,13 +94,16 @@ function Target() {
<>
<StepDescription>Select a recipient chain and address.</StepDescription>
<TextField
variant="outlined"
select
fullWidth
value={targetChain}
onChange={handleTargetChange}
disabled={shouldLockFields}
>
{chains.map(({ id, name }) => (
{chains
.filter(({ id }) => (isBeta ? true : !BETA_CHAINS.includes(id)))
.map(({ id, name }) => (
<MenuItem key={id} value={id}>
{name}
</MenuItem>

View File

@ -10,7 +10,6 @@ import {
ETH_TOKENS_THAT_CAN_BE_SWAPPED_ON_SOLANA,
ETH_TOKENS_THAT_EXIST_ELSEWHERE,
SOLANA_TOKENS_THAT_EXIST_ELSEWHERE,
WETH_ADDRESS,
} from "../../utils/consts";
export default function TokenWarning({
@ -39,10 +38,13 @@ export default function TokenWarning({
);
return tokenConflictingNativeWarning ? (
<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">
As of 2021-09-30, markets for Wormhole v2 wrapped WETH have not yet been
created.
Lido stETH rewards can only be received on Ethereum. Use the value
accruing wrapper token wstETH instead.
</Alert>
) : sourceChain === CHAIN_ID_ETH &&
tokenAddress &&

View File

@ -1,17 +1,15 @@
import {
Container,
makeStyles,
Step,
StepButton,
StepContent,
Stepper,
} from "@material-ui/core";
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import useCheckIfWormholeWrapped from "../../hooks/useCheckIfWormholeWrapped";
import useFetchTargetAsset from "../../hooks/useFetchTargetAsset";
import useGetBalanceEffect from "../../hooks/useGetBalanceEffect";
import { COLORS } from "../../muiTheme";
import {
selectTransferActiveStep,
selectTransferIsRedeemComplete,
@ -20,7 +18,6 @@ import {
selectTransferIsSending,
} from "../../store/selectors";
import { setStep } from "../../store/transferSlice";
import Recovery from "./Recovery";
import Redeem from "./Redeem";
import RedeemPreview from "./RedeemPreview";
import Send from "./Send";
@ -30,18 +27,10 @@ import SourcePreview from "./SourcePreview";
import Target from "./Target";
import TargetPreview from "./TargetPreview";
const useStyles = makeStyles(() => ({
rootContainer: {
backgroundColor: COLORS.nearBlackWithMinorTransparency,
},
}));
function Transfer() {
const classes = useStyles();
useCheckIfWormholeWrapped();
useFetchTargetAsset();
useGetBalanceEffect("target");
const [isRecoveryOpen, setIsRecoveryOpen] = useState(false);
const dispatch = useDispatch();
const activeStep = useSelector(selectTransferActiveStep);
const isSending = useSelector(selectTransferIsSending);
@ -60,22 +49,14 @@ function Transfer() {
}, [preventNavigation]);
return (
<Container maxWidth="md">
<Stepper
activeStep={activeStep}
orientation="vertical"
className={classes.rootContainer}
>
<Stepper activeStep={activeStep} orientation="vertical">
<Step
expanded={activeStep >= 0}
disabled={preventNavigation || isRedeemComplete}
>
<StepButton onClick={() => dispatch(setStep(0))}>Source</StepButton>
<StepContent>
{activeStep === 0 ? (
<Source setIsRecoveryOpen={setIsRecoveryOpen} />
) : (
<SourcePreview />
)}
{activeStep === 0 ? <Source /> : <SourcePreview />}
</StepContent>
</Step>
<Step
@ -107,11 +88,6 @@ function Transfer() {
</StepContent>
</Step>
</Stepper>
<Recovery
open={isRecoveryOpen}
setOpen={setIsRecoveryOpen}
disabled={preventNavigation}
/>
</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 App from "./App";
import BackgroundImage from "./components/BackgroundImage";
import { BetaContextProvider } from "./contexts/BetaContext";
import { EthereumProviderProvider } from "./contexts/EthereumProviderContext";
import { SolanaWalletProvider } from "./contexts/SolanaWalletContext.tsx";
import { TerraWalletProvider } from "./contexts/TerraWalletContext.tsx";
@ -20,6 +21,7 @@ ReactDOM.render(
<CssBaseline />
<ErrorBoundary>
<SnackbarProvider maxSnack={3}>
<BetaContextProvider>
<SolanaWalletProvider>
<EthereumProviderProvider>
<TerraWalletProvider>
@ -30,6 +32,7 @@ ReactDOM.render(
</TerraWalletProvider>
</EthereumProviderProvider>
</SolanaWalletProvider>
</BetaContextProvider>
</SnackbarProvider>
</ErrorBoundary>
</ThemeProvider>

View File

@ -1,13 +1,16 @@
import { createTheme, responsiveFontSizes } from "@material-ui/core";
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)",
green: "#00EFD8",
blue: "#0074FF",
blueWithTransparency: "rgba(0, 116, 255, 0.8)",
greenWithTransparency: "rgba(0,239,216,0.8)",
nearBlack: "#010114",
nearBlackWithMinorTransparency: "rgba(0,0,0,.97)",
lightBlue: "#83b9fc",
nearBlack: "#000008",
nearBlackWithMinorTransparency: "rgba(0,0,0,.25)",
red: "#aa0818",
darkRed: "#810612",
};
export const theme = responsiveFontSizes(
@ -24,31 +27,106 @@ export const theme = responsiveFontSizes(
},
primary: {
main: COLORS.blueWithTransparency, // #0074FF
light: COLORS.lightBlue,
},
secondary: {
main: COLORS.greenWithTransparency, // #00EFD8
light: COLORS.lightGreen,
},
error: {
main: "#FD3503",
main: COLORS.red,
},
},
typography: {
fontFamily: "'Sora', sans-serif",
h1: {
fontWeight: "200",
},
h2: {
fontWeight: "700",
fontWeight: "300",
},
h4: {
fontWeight: "500",
},
},
overrides: {
MuiAccordion: {
root: {
backgroundColor: COLORS.nearBlackWithMinorTransparency,
"&:before": {
display: "none",
},
},
rounded: {
"&:first-child": {
borderTopLeftRadius: "16px",
borderTopRightRadius: "16px",
},
"&:last-child": {
borderBottomLeftRadius: "16px",
borderBottomRightRadius: "16px",
},
},
},
MuiButton: {
root: {
borderRadius: "5px",
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 =
CLUSTER === "mainnet"
? [
// {
// id: CHAIN_ID_BSC,
// name: "Binance Smart Chain",
// },
{
id: CHAIN_ID_ETH,
name: "Ethereum",
@ -30,6 +34,10 @@ export const CHAINS =
id: CHAIN_ID_SOLANA,
name: "Solana",
},
// {
// id: CHAIN_ID_TERRA,
// name: "Terra",
// },
]
: CLUSTER === "testnet"
? [
@ -64,6 +72,8 @@ export const CHAINS =
name: "Terra",
},
];
export const BETA_CHAINS =
CLUSTER === "mainnet" ? [CHAIN_ID_BSC, CHAIN_ID_TERRA] : [];
export const CHAINS_WITH_NFT_SUPPORT = CHAINS.filter(
({ id }) =>
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 =
"terra13nkgqrfymug724h8pprpexqj9h629sa3ncw7sh";
export const TERRA_BRIDGE_ADDRESS =
CLUSTER === "mainnet"
CLUSTER === "mainnet"
? "terra1dq03ugtd40zu9hcgdzrsq6z2z4hwhc9tqk2uy5"
: CLUSTER === "testnet"
? "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5"
: "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5";
export const TERRA_TOKEN_BRIDGE_ADDRESS =
CLUSTER === "mainnet"
CLUSTER === "mainnet"
? "terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf"
: CLUSTER === "testnet"
? "terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4"