Forráskód Böngészése

bridge_ui: confidence enhancement

Change-Id: Ied0f0fe641a89bb99c9b6a5d0e1400e70b2c8d77
Evan Gray 4 éve
szülő
commit
461e8f256e
31 módosított fájl, 1006 hozzáadás és 1287 törlés
  1. 7 7
      bridge_ui/public/index.html
  2. 133 82
      bridge_ui/src/App.js
  3. 1 1
      bridge_ui/src/components/Attest/CreatePreview.tsx
  4. 8 2
      bridge_ui/src/components/Attest/Source.tsx
  5. 11 6
      bridge_ui/src/components/Attest/Target.tsx
  6. 1 14
      bridge_ui/src/components/Attest/index.tsx
  7. 3 12
      bridge_ui/src/components/BackgroundImage.tsx
  8. 7 30
      bridge_ui/src/components/Home/index.tsx
  9. 1 0
      bridge_ui/src/components/Migration/EthereumWorkflow.tsx
  10. 1 0
      bridge_ui/src/components/Migration/SolanaWorkflow.tsx
  11. 0 462
      bridge_ui/src/components/NFT/Recovery.tsx
  12. 18 36
      bridge_ui/src/components/NFT/Source.tsx
  13. 18 6
      bridge_ui/src/components/NFT/Target.tsx
  14. 4 28
      bridge_ui/src/components/NFT/index.tsx
  15. 11 4
      bridge_ui/src/components/NFTOriginVerifier.tsx
  16. 557 0
      bridge_ui/src/components/Recovery.tsx
  17. 4 2
      bridge_ui/src/components/ToggleConnectedButton.tsx
  18. 2 0
      bridge_ui/src/components/TokenSelectors/EthereumSourceTokenSelector.tsx
  19. 1 0
      bridge_ui/src/components/TokenSelectors/SourceTokenSelector.tsx
  20. 1 0
      bridge_ui/src/components/TokenSelectors/TerraSourceTokenSelector.tsx
  21. 0 512
      bridge_ui/src/components/Transfer/Recovery.tsx
  22. 10 19
      bridge_ui/src/components/Transfer/Source.tsx
  23. 11 6
      bridge_ui/src/components/Transfer/Target.tsx
  24. 6 4
      bridge_ui/src/components/Transfer/TokenWarning.tsx
  25. 3 27
      bridge_ui/src/components/Transfer/index.tsx
  26. 53 0
      bridge_ui/src/contexts/BetaContext.tsx
  27. 23 0
      bridge_ui/src/icons/terra.svg
  28. 0 7
      bridge_ui/src/images/holev2.svg
  29. 13 10
      bridge_ui/src/index.js
  30. 86 8
      bridge_ui/src/muiTheme.js
  31. 12 2
      bridge_ui/src/utils/consts.ts

+ 7 - 7
bridge_ui/public/index.html

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

+ 133 - 82
bridge_ui/src/App.js

@@ -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,57 +76,104 @@ 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">
+                  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">
                 <IconButton
-                  href="https://github.com/certusone/wormhole"
-                  target="_blank"
+                  component={NavLink}
+                  to="/transfer"
                   size="small"
                   className={classes.link}
                 >
-                  <GitHub />
+                  <Send />
                 </IconButton>
               </Tooltip>
+            ) : (
               <Tooltip title="View the FAQ">
                 <IconButton
                   href="https://docs.wormholenetwork.com/wormhole/faqs"
@@ -127,53 +181,47 @@ function App() {
                   size="small"
                   className={classes.link}
                 >
-                  <Help />
+                  <HelpOutline />
                 </IconButton>
               </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>
         </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>
   );
 }
 

+ 1 - 1
bridge_ui/src/components/Attest/CreatePreview.tsx

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

+ 8 - 2
bridge_ui/src/components/Attest/Source.tsx

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

+ 11 - 6
bridge_ui/src/components/Attest/Target.tsx

@@ -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,16 +49,19 @@ function Target() {
     <>
       <TextField
         select
+        variant="outlined"
         fullWidth
         value={targetChain}
         onChange={handleTargetChange}
         disabled={shouldLockFields}
       >
-        {chains.map(({ id, name }) => (
-          <MenuItem key={id} value={id}>
-            {name}
-          </MenuItem>
-        ))}
+        {chains
+          .filter(({ id }) => (isBeta ? true : !BETA_CHAINS.includes(id)))
+          .map(({ id, name }) => (
+            <MenuItem key={id} value={id}>
+              {name}
+            </MenuItem>
+          ))}
       </TextField>
       <KeyAndBalance chainId={targetChain} />
       <Alert severity="info" className={classes.alert}>

+ 1 - 14
bridge_ui/src/components/Attest/index.tsx

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

+ 3 - 12
bridge_ui/src/components/BackgroundImage.tsx

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

+ 7 - 30
bridge_ui/src/components/Home/index.tsx

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

+ 1 - 0
bridge_ui/src/components/Migration/EthereumWorkflow.tsx

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

+ 1 - 0
bridge_ui/src/components/Migration/SolanaWorkflow.tsx

@@ -460,6 +460,7 @@ export default function Workflow({
       ) : null}
       <div className={classes.spacer} />
       <TextField
+        variant="outlined"
         value={migrationAmount}
         type="number"
         onChange={handleAmountChange}

+ 0 - 462
bridge_ui/src/components/NFT/Recovery.tsx

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

+ 18 - 36
bridge_ui/src/components/NFT/Source.tsx

@@ -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>
+            <Button
+              component={Link}
+              to="/nft-origin-verifier"
+              size="small"
+              variant="outlined"
+              endIcon={<VerifiedUser />}
+            >
+              NFT Origin Verifier
+            </Button>
           </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>

+ 18 - 6
bridge_ui/src/components/NFT/Target.tsx

@@ -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,19 +97,23 @@ function Target() {
       <TextField
         select
         fullWidth
+        variant="outlined"
         value={targetChain}
         onChange={handleTargetChange}
       >
-        {chains.map(({ id, name }) => (
-          <MenuItem key={id} value={id}>
-            {name}
-          </MenuItem>
-        ))}
+        {chains
+          .filter(({ id }) => (isBeta ? true : !BETA_CHAINS.includes(id)))
+          .map(({ id, name }) => (
+            <MenuItem key={id} value={id}>
+              {name}
+            </MenuItem>
+          ))}
       </TextField>
       <KeyAndBalance chainId={targetChain} balance={uiAmountString} />
       <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}

+ 4 - 28
bridge_ui/src/components/NFT/index.tsx

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

+ 11 - 4
bridge_ui/src/components/NFTOriginVerifier.tsx

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

+ 557 - 0
bridge_ui/src/components/Recovery.tsx

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

+ 4 - 2
bridge_ui/src/components/ToggleConnectedButton.tsx

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

+ 2 - 0
bridge_ui/src/components/TokenSelectors/EthereumSourceTokenSelector.tsx

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

+ 1 - 0
bridge_ui/src/components/TokenSelectors/SourceTokenSelector.tsx

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

+ 1 - 0
bridge_ui/src/components/TokenSelectors/TerraSourceTokenSelector.tsx

@@ -296,6 +296,7 @@ export default function TerraSourceTokenSelector(
     <>
       <TextField
         fullWidth
+        variant="outlined"
         label="Enter an asset address"
         value={advancedModeHolderString}
         onChange={handleOnChange}

+ 0 - 512
bridge_ui/src/components/Transfer/Recovery.tsx

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

+ 10 - 19
bridge_ui/src/components/Transfer/Source.tsx

@@ -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>
+        Select tokens to send through the Wormhole Token Bridge.
       </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

+ 11 - 6
bridge_ui/src/components/Transfer/Target.tsx

@@ -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,17 +94,20 @@ 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 }) => (
-          <MenuItem key={id} value={id}>
-            {name}
-          </MenuItem>
-        ))}
+        {chains
+          .filter(({ id }) => (isBeta ? true : !BETA_CHAINS.includes(id)))
+          .map(({ id, name }) => (
+            <MenuItem key={id} value={id}>
+              {name}
+            </MenuItem>
+          ))}
       </TextField>
       <KeyAndBalance chainId={targetChain} balance={uiAmountString} />
       {readableTargetAddress ? (

+ 6 - 4
bridge_ui/src/components/Transfer/TokenWarning.tsx

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

+ 3 - 27
bridge_ui/src/components/Transfer/index.tsx

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

+ 53 - 0
bridge_ui/src/contexts/BetaContext.tsx

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

+ 23 - 0
bridge_ui/src/icons/terra.svg

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

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 7
bridge_ui/src/images/holev2.svg


+ 13 - 10
bridge_ui/src/index.js

@@ -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,16 +21,18 @@ ReactDOM.render(
         <CssBaseline />
         <ErrorBoundary>
           <SnackbarProvider maxSnack={3}>
-            <SolanaWalletProvider>
-              <EthereumProviderProvider>
-                <TerraWalletProvider>
-                  <HashRouter>
-                    <BackgroundImage />
-                    <App />
-                  </HashRouter>
-                </TerraWalletProvider>
-              </EthereumProviderProvider>
-            </SolanaWalletProvider>
+            <BetaContextProvider>
+              <SolanaWalletProvider>
+                <EthereumProviderProvider>
+                  <TerraWalletProvider>
+                    <HashRouter>
+                      <BackgroundImage />
+                      <App />
+                    </HashRouter>
+                  </TerraWalletProvider>
+                </EthereumProviderProvider>
+              </SolanaWalletProvider>
+            </BetaContextProvider>
           </SnackbarProvider>
         </ErrorBoundary>
       </ThemeProvider>

+ 86 - 8
bridge_ui/src/muiTheme.js

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

+ 12 - 2
bridge_ui/src/utils/consts.ts

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

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott