bridge_ui: remove

This commit is contained in:
Christine Eun 2022-08-25 20:21:50 +00:00 committed by Evan Gray
parent 0384d31a9b
commit 2d2a4b63ac
242 changed files with 0 additions and 113246 deletions

View File

@ -45,11 +45,9 @@ config.define_bool("solana", False, "Enable Solana component")
config.define_bool("terra_classic", False, "Enable Terra Classic component")
config.define_bool("terra2", False, "Enable Terra 2 component")
config.define_bool("explorer", False, "Enable explorer component")
config.define_bool("bridge_ui", False, "Enable bridge UI component")
config.define_bool("spy_relayer", False, "Enable spy relayer")
config.define_bool("e2e", False, "Enable E2E testing stack")
config.define_bool("ci_tests", False, "Enable tests runner component")
config.define_bool("bridge_ui_hot", False, "Enable hot loading bridge_ui")
config.define_bool("guardiand_debug", False, "Enable dlv endpoint for guardiand")
config.define_bool("node_metrics", False, "Enable Prometheus & Grafana for Guardian metrics")
config.define_bool("guardiand_governor", False, "Enable chain governor in guardiand")
@ -68,7 +66,6 @@ terra_classic = cfg.get("terra_classic", True)
terra2 = cfg.get("terra2", True)
ci = cfg.get("ci", False)
explorer = cfg.get("explorer", ci)
bridge_ui = cfg.get("bridge_ui", ci)
spy_relayer = cfg.get("spy_relayer", ci)
e2e = cfg.get("e2e", ci)
ci_tests = cfg.get("ci_tests", ci)
@ -76,8 +73,6 @@ guardiand_debug = cfg.get("guardiand_debug", False)
node_metrics = cfg.get("node_metrics", False)
guardiand_governor = cfg.get("guardiand_governor", False)
bridge_ui_hot = not ci
if cfg.get("manual", False):
trigger_mode = TRIGGER_MODE_MANUAL
else:
@ -484,36 +479,6 @@ if evm2:
trigger_mode = trigger_mode,
)
if bridge_ui:
entrypoint = "npm run build && /app/node_modules/.bin/serve -s build -n"
live_update = []
if bridge_ui_hot:
entrypoint = "npm start"
live_update = [
sync("./bridge_ui/public", "/app/public"),
sync("./bridge_ui/src", "/app/src"),
]
docker_build(
ref = "bridge-ui",
context = ".",
only = ["./bridge_ui"],
dockerfile = "bridge_ui/Dockerfile",
entrypoint = entrypoint,
live_update = live_update,
)
k8s_yaml_with_ns("devnet/bridge-ui.yaml")
k8s_resource(
"bridge-ui",
resource_deps = [],
port_forwards = [
port_forward(3000, name = "Bridge UI [:3000]", host = webHost),
],
labels = ["portal"],
trigger_mode = trigger_mode,
)
if ci_tests:
local_resource(
@ -527,16 +492,6 @@ if ci_tests:
trigger_mode = trigger_mode,
)
docker_build(
ref = "bridge-ui-test-image",
context = ".",
dockerfile = "testing/Dockerfile.bridge_ui.test",
only = [],
live_update = [
sync("./testing", "/app/testing"),
sync("./bridge_ui/src", "/app/bridge_ui/src"),
],
)
docker_build(
ref = "sdk-test-image",
context = ".",
@ -561,11 +516,6 @@ if ci_tests:
k8s_yaml_with_ns("devnet/tests.yaml")
# separate resources to parallelize docker builds
k8s_resource(
"bridge-ui-ci-tests",
labels = ["ci"],
trigger_mode = trigger_mode,
)
k8s_resource(
"sdk-ci-tests",
labels = ["ci"],

View File

27
bridge_ui/.gitignore vendored
View File

@ -1,27 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# ethereum contracts
/contracts
/src/ethers-contracts

View File

@ -1,12 +0,0 @@
# syntax=docker.io/docker/dockerfile:1.3@sha256:42399d4635eddd7a9b8a24be879d2f9a930d0ed040a61324cfdf59ef1357b3b2
# Derivative of ethereum/Dockerfile, look there for an explanation on how it works.
FROM node:16-alpine@sha256:f21f35732964a96306a84a8c4b5a829f6d3a0c5163237ff4b6b8b34f8d70064b
RUN mkdir -p /app
WORKDIR /app
COPY bridge_ui/package.json bridge_ui/package-lock.json ./
RUN --mount=type=cache,uid=1000,gid=1000,target=/home/node/.npm \
npm ci
COPY bridge_ui .

View File

@ -1,50 +0,0 @@
# Example Token Bridge UI
## Prerequisites
- NodeJS v14+
- NPM v7.18+
## Install
```bash
npm ci
```
## Develop
```bash
npm start
```
## Build for local tilt network
```bash
npm run build
```
## Build for testnet
```bash
REACT_APP_CLUSTER=testnet npm run build
```
## Build for mainnet
```bash
REACT_APP_CLUSTER=mainnet REACT_APP_COVALENT_API_KEY=YOUR_API_KEY REACT_APP_SOLANA_API_URL=YOUR_CUSTOM_RPC npm run build
```
## Test Server
```bash
npx serve -s build
```
## Environment Variables (optional)
Create `.env` from the sample file, then add your Covalent API key:
```bash
cp .env.sample .env
```

View File

@ -1,67 +0,0 @@
const { ProvidePlugin } = require("webpack");
module.exports = function override(config, env) {
return {
...config,
module: {
...config.module,
rules: [
...config.module.rules,
{
test: /\.js$/,
enforce: "pre",
use: ["source-map-loader"],
},
{
test: /\.wasm$/,
type: "webassembly/async",
},
],
},
plugins: [
...config.plugins,
new ProvidePlugin({
Buffer: ["buffer", "Buffer"],
process: "process/browser",
}),
],
resolve: {
...config.resolve,
fallback: {
assert: "assert",
buffer: "buffer",
console: "console-browserify",
constants: "constants-browserify",
crypto: "crypto-browserify",
domain: "domain-browser",
events: "events",
fs: false,
http: "stream-http",
https: "https-browserify",
os: "os-browserify/browser",
path: "path-browserify",
punycode: "punycode",
process: "process/browser",
querystring: "querystring-es3",
stream: "stream-browserify",
_stream_duplex: "readable-stream/duplex",
_stream_passthrough: "readable-stream/passthrough",
_stream_readable: "readable-stream/readable",
_stream_transform: "readable-stream/transform",
_stream_writable: "readable-stream/writable",
string_decoder: "string_decoder",
sys: "util",
timers: "timers-browserify",
tty: "tty-browserify",
url: "url",
util: "util",
vm: "vm-browserify",
zlib: "browserify-zlib",
},
},
experiments: {
asyncWebAssembly: true,
},
ignoreWarnings: [/Failed to parse source map/],
};
};

81873
bridge_ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,106 +0,0 @@
{
"name": "test_ui",
"version": "0.1.0",
"private": true,
"dependencies": {
"@certusone/wormhole-sdk": "^0.6.2",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60",
"@metamask/detect-provider": "^1.2.0",
"@project-serum/serum": "^0.13.60",
"@randlabs/myalgo-connect": "^1.1.3",
"@reduxjs/toolkit": "^1.6.1",
"@solana/spl-token": "^0.1.6",
"@solana/spl-token-registry": "^0.2.216",
"@solana/wallet-adapter-base": "^0.9.3",
"@solana/wallet-adapter-react": "^0.15.3",
"@solana/wallet-adapter-react-ui": "^0.9.5",
"@solana/wallet-adapter-wallets": "^0.16.1",
"@solana/web3.js": "^1.35.1",
"@terra-money/wallet-provider": "^3.9.4",
"@walletconnect/web3-provider": "^1.7.8",
"algosdk": "^1.15.0",
"axios": "^0.21.1",
"bech32": "^1.1.4",
"bn.js": "^5.1.3",
"borsh": "^0.4.0",
"bs58": "^4.0.1",
"clsx": "^1.1.1",
"ethers": "^5.6.8",
"js-base64": "^3.6.1",
"luxon": "^2.3.1",
"notistack": "^1.0.10",
"numeral": "^2.0.6",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-redux": "^7.2.4",
"react-router-dom": "^5.2.0",
"react-scripts": "5.0.0",
"react-table": "^7.7.0",
"recharts": "^2.1.9",
"redux": "^3.7.2",
"use-debounce": "^7.0.0"
},
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@truffle/hdwallet-provider": "^1.4.1",
"@types/luxon": "^2.3.1",
"@types/node": "^16.6.1",
"@types/numeral": "^2.0.2",
"@types/react-router-dom": "^5.1.8",
"assert": "^2.0.0",
"babel-jest": "^26.6.0",
"browserify-zlib": "^0.2.0",
"buffer": "^6.0.3",
"console-browserify": "^1.2.0",
"constants-browserify": "^1.0.0",
"crypto-browserify": "^3.12.0",
"domain-browser": "^4.22.0",
"events": "^3.3.0",
"https-browserify": "^1.0.0",
"jest": "^26.6.0",
"jest-watch-typeahead": "^0.6.4",
"os-browserify": "^0.3.0",
"path-browserify": "^1.0.1",
"prettier": "^2.3.2",
"process": "^0.11.10",
"punycode": "^2.1.1",
"querystring-es3": "^0.2.1",
"react-app-rewired": "^2.2.1",
"readable-stream": "^3.6.0",
"serve": "^13.0.2",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
"string_decoder": "^1.3.0",
"timers-browserify": "^2.0.12",
"truffle": "^5.4.1",
"tty-browserify": "^0.0.1",
"url": "^0.11.0",
"util": "^0.12.4",
"vm-browserify": "^1.1.2"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,61 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<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="Portal is a bridge that offers unlimited transfers across chains for tokens and NFTs wrapped by Wormhole."
/>
<meta property="og:title" content="Portal Token Bridge" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://portalbridge.com" />
<meta property="og:image" content="%PUBLIC_URL%/wormhole.png" />
<meta property="og:image:alt" content="Wormhole logo" />
<meta
property="og:description"
content="Portal is a bridge that offers unlimited transfers across chains for tokens and NFTs wrapped by Wormhole."
/>
<meta name="twitter:site" content="@portalbridge_" />
<meta name="twitter:creator" content="@portalbridge_" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
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=Poppins:wght@200;300;400;500;600;700&display=swap"
rel="stylesheet"
/>
<title>Portal Token Bridge</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

View File

@ -1,25 +0,0 @@
{
"short_name": "worm.to",
"name": "Wormhole Token Bridge",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#010114",
"background_color": "#010114"
}

View File

@ -1,25 +0,0 @@
{
"supportedTokens": [
{
"chainId": 1,
"address": "So11111111111111111111111111111111111111112",
"coingeckoId": "solana"
},
{
"chainId": 2,
"address": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E",
"coingeckoId": "ethereum"
},
{
"chainId": 3,
"address": "uluna",
"coingeckoId": "terra-luna"
},
{
"chainId": 4,
"address": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E",
"coingeckoId": "binancecoin"
}
],
"relayers": [{ "name": "localhostRelayer", "url": "http://localhost:4201" }]
}

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

View File

@ -1,362 +0,0 @@
import {
CHAIN_ID_BSC,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
} from "@certusone/wormhole-sdk";
import {
AppBar,
Container,
Hidden,
IconButton,
Link,
makeStyles,
Tab,
Tabs,
Toolbar,
Tooltip,
Typography,
} from "@material-ui/core";
import { HelpOutline } from "@material-ui/icons";
import { useCallback } from "react";
import { useHistory, useLocation } from "react-router";
import {
Link as RouterLink,
NavLink,
Redirect,
Route,
Switch,
} from "react-router-dom";
import Attest from "./components/Attest";
import Footer from "./components/Footer";
import HeaderText from "./components/HeaderText";
import Migration from "./components/Migration";
import EvmQuickMigrate from "./components/Migration/EvmQuickMigrate";
import SolanaQuickMigrate from "./components/Migration/SolanaQuickMigrate";
import NFT from "./components/NFT";
import NFTOriginVerifier from "./components/NFTOriginVerifier";
import Recovery from "./components/Recovery";
import Stats from "./components/Stats";
import CustodyAddresses from "./components/Stats/CustodyAddresses";
import TokenOriginVerifier from "./components/TokenOriginVerifier";
import Transfer from "./components/Transfer";
import UnwrapNative from "./components/UnwrapNative";
import WithdrawTokensTerra from "./components/WithdrawTokensTerra";
import { useBetaContext } from "./contexts/BetaContext";
import Portal from "./icons/portal_logo_w.svg";
import { CLUSTER } from "./utils/consts";
const useStyles = makeStyles((theme) => ({
appBar: {
background: "transparent",
marginTop: theme.spacing(2),
"& > .MuiToolbar-root": {
margin: "auto",
width: "100%",
maxWidth: 1440,
},
},
spacer: {
flex: 1,
width: "100vw",
},
link: {
...theme.typography.body2,
fontWeight: 600,
fontFamily: "Suisse BP Intl, sans-serif",
color: "white",
marginLeft: theme.spacing(4),
textUnderlineOffset: "6px",
[theme.breakpoints.down("sm")]: {
marginLeft: theme.spacing(2.5),
},
[theme.breakpoints.down("xs")]: {
marginLeft: theme.spacing(1),
},
"&.active": {
textDecoration: "underline",
},
},
bg: {
// 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%)",
display: "flex",
flexDirection: "column",
minHeight: "100vh",
position: "relative",
overflow: "hidden",
},
brandLink: {
display: "inline-flex",
alignItems: "center",
"&:hover": {
textDecoration: "none",
},
},
iconButton: {
[theme.breakpoints.up("md")]: {
marginRight: theme.spacing(2.5),
},
[theme.breakpoints.down("sm")]: {
marginRight: theme.spacing(2.5),
},
[theme.breakpoints.down("xs")]: {
marginRight: theme.spacing(1),
},
},
betaBanner: {
backgroundColor: "rgba(0,0,0,0.75)",
padding: theme.spacing(1, 0),
},
wormholeIcon: {
height: 68,
"&:hover": {
filter: "contrast(1)",
},
verticalAlign: "middle",
marginRight: theme.spacing(1),
display: "inline-block",
},
gradientRight: {
position: "absolute",
top: "72px",
right: "-1000px",
width: "1757px",
height: "1506px",
background:
"radial-gradient(closest-side at 50% 50%, #FFCE00 0%, #FFCE0000 100%)",
opacity: "0.2",
transform: "matrix(0.87, 0.48, -0.48, 0.87, 0, 0)",
zIndex: "-1",
pointerEvent: "none",
[theme.breakpoints.down("sm")]: {
display: "none",
},
},
gradientLeft: {
top: "-530px",
left: "-350px",
width: "1379px",
height: "1378px",
position: "absolute",
background:
"radial-gradient(closest-side at 50% 50%, #F44B1B 0%, #F44B1B00 100%)",
opacity: "0.2",
zIndex: "-1",
pointerEvent: "none",
},
gradientLeft2: {
bottom: "-330px",
left: "-350px",
width: "1379px",
height: "1378px",
position: "absolute",
background:
"radial-gradient(closest-side at 50% 50%, #F44B1B 0%, #F44B1B00 100%)",
opacity: "0.2",
zIndex: "-1",
pointerEvent: "none",
[theme.breakpoints.down("sm")]: {
display: "none",
},
},
gradientRight2: {
position: "absolute",
bottom: "-900px",
right: "-1000px",
width: "1757px",
height: "1506px",
background:
"radial-gradient(closest-side at 50% 50%, #FFCE00 0%, #FFCE0000 100%)",
opacity: "0.24",
transform: "matrix(0.87, 0.48, -0.48, 0.87, 0, 0);",
zIndex: "-1",
pointerEvent: "none",
[theme.breakpoints.down("sm")]: {
display: "none",
},
},
}));
function App() {
const classes = useStyles();
const isBeta = useBetaContext();
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}
elevation={0}
>
<Toolbar>
<Link
component={RouterLink}
to="/transfer"
className={classes.brandLink}
>
<img src={Portal} alt="Portal" className={classes.wormholeIcon} />
</Link>
<div className={classes.spacer} />
<Hidden implementation="css" xsDown>
<div style={{ display: "flex", alignItems: "center" }}>
<Link
component={NavLink}
to="/transfer"
color="inherit"
className={classes.link}
>
Bridge
</Link>
<Link
href="https://docs.wormholenetwork.com/wormhole/faqs"
target="_blank"
rel="noopener noreferrer"
color="inherit"
className={classes.link}
>
FAQ
</Link>
<Link
href="https://wormholenetwork.com/"
target="_blank"
rel="noopener noreferrer"
color="inherit"
className={classes.link}
>
Wormhole
</Link>
</div>
</Hidden>
<Hidden implementation="css" smUp>
<Tooltip title="View the FAQ">
<IconButton
href="https://docs.wormholenetwork.com/wormhole/faqs"
target="_blank"
rel="noopener noreferrer"
size="small"
className={classes.link}
>
<HelpOutline />
</IconButton>
</Tooltip>
</Hidden>
</Toolbar>
</AppBar>
{CLUSTER === "mainnet" ? null : (
<AppBar position="static" className={classes.betaBanner} elevation={0}>
<Typography style={{ textAlign: "center" }}>
Caution! You are using the {CLUSTER} build of this app.
</Typography>
</AppBar>
)}
{isBeta ? (
<AppBar position="static" className={classes.betaBanner} elevation={0}>
<Typography style={{ textAlign: "center" }}>
Caution! You have enabled the beta. Enter the secret code again to
disable.
</Typography>
</AppBar>
) : null}
{["/transfer", "/nft", "/redeem"].includes(pathname) ? (
<Container maxWidth="md" style={{ paddingBottom: 24 }}>
<HeaderText
white
subtitle={
<>
<Typography>
Portal is a bridge that offers unlimited transfers across
chains for tokens and NFTs wrapped by Wormhole.
</Typography>
<Typography>
Unlike many other bridges, you avoid double wrapping and never
have to retrace your steps.
</Typography>
</>
}
>
Token Bridge
</HeaderText>
<Tabs
value={pathname}
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>
) : null}
<Switch>
<Route exact path="/transfer">
<Transfer />
</Route>
<Route exact path="/nft">
<NFT />
</Route>
<Route exact path="/redeem">
<Recovery />
</Route>
<Route exact path="/nft-origin-verifier">
<NFTOriginVerifier />
</Route>
<Route exact path="/token-origin-verifier">
<TokenOriginVerifier />
</Route>
<Route exact path="/register">
<Attest />
</Route>
<Route exact path="/migrate/Solana/:legacyAsset/:fromTokenAccount">
<Migration chainId={CHAIN_ID_SOLANA} />
</Route>
<Route exact path="/migrate/Ethereum/:legacyAsset/">
<Migration chainId={CHAIN_ID_ETH} />
</Route>
<Route exact path="/migrate/BinanceSmartChain/:legacyAsset/">
<Migration chainId={CHAIN_ID_BSC} />
</Route>
<Route exact path="/migrate/Ethereum/">
<EvmQuickMigrate chainId={CHAIN_ID_ETH} />
</Route>
<Route exact path="/migrate/BinanceSmartChain/">
<EvmQuickMigrate chainId={CHAIN_ID_BSC} />
</Route>
<Route exact path="/migrate/Solana/">
<SolanaQuickMigrate />
</Route>
<Route exact path="/stats">
<Stats />
</Route>
<Route exact path="/withdraw-tokens-terra">
<WithdrawTokensTerra />
</Route>
<Route exact path="/unwrap-native">
<UnwrapNative />
</Route>
<Route exact path="/custody-addresses">
<CustodyAddresses />
</Route>
<Route>
<Redirect to="/transfer" />
</Route>
</Switch>
<div className={classes.spacer} />
<div className={classes.gradientRight}></div>
<div className={classes.gradientRight2}></div>
<div className={classes.gradientLeft}></div>
<div className={classes.gradientLeft2}></div>
<Footer />
</div>
);
}
export default App;

View File

@ -1,29 +0,0 @@
import { Typography } from "@material-ui/core";
import React from "react";
export default class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error(error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<Typography variant="h5" style={{ textAlign: "center", marginTop: 24 }}>
An unexpected error has occurred. Please refresh the page.
</Typography>
);
}
return this.props.children;
}
}

View File

@ -1,13 +0,0 @@
const { describe, expect, it } = require("@jest/globals");
const fs = require("fs");
describe("SDK installation", () => {
it("does not import from file path", () => {
const packageFile = fs.readFileSync("./package.json");
const packageObj = JSON.parse(packageFile.toString());
const sdkInstallation =
packageObj?.dependencies?.["@certusone/wormhole-sdk"];
expect(sdkInstallation && !sdkInstallation.includes("file")).toBe(true);
});
});

View File

@ -1,31 +0,0 @@
import { describe, expect, it } from "@jest/globals";
import { balancePretty } from "../utils/balancePretty";
describe("Unit Tests", () => {
describe("balancePretty() tests", () => {
it("9.99 => 9.99", () => {
expect(balancePretty("9.99")).toBe("9.99");
});
it("123456.789 => 123456.78", () => {
expect(balancePretty("123456.789")).toBe("123456.7");
});
it("1234567.8912 => 1.23 M", () => {
expect(balancePretty("1234567.891")).toBe("1.23 M");
});
it("123999.8912 => 1.23 M", () => {
expect(balancePretty("1239999.8912")).toBe("1.23 M");
});
it("981234567.8912 => 981.23 M", () => {
expect(balancePretty("981234567.891")).toBe("981.23 M");
});
it("9876543210.8912 => 9.87 B", () => {
expect(balancePretty("9876543210.8912")).toBe("9.87 B");
});
it("219876543210.8912 => 219.87 B", () => {
expect(balancePretty("219876543210.8912")).toBe("219.87 B");
});
it("219876543210 => 219.87 B", () => {
expect(balancePretty("219876543210")).toBe("219.87 B");
});
});
});

View File

@ -1,19 +0,0 @@
import { useAlgorandContext } from "../contexts/AlgorandWalletContext";
import ToggleConnectedButton from "./ToggleConnectedButton";
const AlgorandWalletKey = () => {
const { connect, disconnect, accounts } = useAlgorandContext();
return (
<>
<ToggleConnectedButton
connect={connect}
disconnect={disconnect}
connected={!!accounts[0]}
pk={accounts[0]?.address || ""}
/>
</>
);
};
export default AlgorandWalletKey;

View File

@ -1,73 +0,0 @@
import { isTerraChain } from "@certusone/wormhole-sdk";
import { CircularProgress, makeStyles } from "@material-ui/core";
import { useSelector } from "react-redux";
import useFetchForeignAsset from "../../hooks/useFetchForeignAsset";
import { useHandleCreateWrapped } from "../../hooks/useHandleCreateWrapped";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import {
selectAttestSourceAsset,
selectAttestSourceChain,
selectAttestTargetChain,
} from "../../store/selectors";
import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
import TerraFeeDenomPicker from "../TerraFeeDenomPicker";
import WaitingForWalletMessage from "./WaitingForWalletMessage";
const useStyles = makeStyles((theme) => ({
alignCenter: {
margin: "0 auto",
display: "block",
textAlign: "center",
},
spacer: {
height: theme.spacing(2),
},
}));
function Create() {
const classes = useStyles();
const targetChain = useSelector(selectAttestTargetChain);
const originAsset = useSelector(selectAttestSourceAsset);
const originChain = useSelector(selectAttestSourceChain);
const { isReady, statusMessage } = useIsWalletReady(targetChain);
const foreignAssetInfo = useFetchForeignAsset(
originChain,
originAsset,
targetChain
);
const shouldUpdate = foreignAssetInfo.data?.doesExist;
const error = foreignAssetInfo.error || statusMessage;
const { handleClick, disabled, showLoader } = useHandleCreateWrapped(
shouldUpdate || false
);
return (
<>
<KeyAndBalance chainId={targetChain} />
{isTerraChain(targetChain) && (
<TerraFeeDenomPicker disabled={disabled} chainId={targetChain} />
)}
{foreignAssetInfo.isFetching ? (
<>
<div className={classes.spacer} />
<CircularProgress className={classes.alignCenter} />
</>
) : (
<>
<ButtonWithLoader
disabled={!isReady || disabled}
onClick={handleClick}
showLoader={showLoader}
error={error}
>
{shouldUpdate ? "Update" : "Create"}
</ButtonWithLoader>
<WaitingForWalletMessage />
</>
)}
</>
);
}
export default Create;

View File

@ -1,73 +0,0 @@
import { Link, makeStyles, Typography } from "@material-ui/core";
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
selectAttestCreateTx,
selectAttestTargetChain,
} from "../../store/selectors";
import { reset } from "../../store/attestSlice";
import ButtonWithLoader from "../ButtonWithLoader";
import ShowTx from "../ShowTx";
import { useHistory } from "react-router";
import { getHowToAddToTokenListUrl } from "../../utils/consts";
import { Alert } from "@material-ui/lab";
const useStyles = makeStyles((theme) => ({
description: {
textAlign: "center",
},
alert: {
marginTop: theme.spacing(1),
},
}));
export default function CreatePreview() {
const { push } = useHistory();
const classes = useStyles();
const dispatch = useDispatch();
const targetChain = useSelector(selectAttestTargetChain);
const createTx = useSelector(selectAttestCreateTx);
const handleResetClick = useCallback(() => {
dispatch(reset());
}, [dispatch]);
const handleReturnClick = useCallback(() => {
dispatch(reset());
push("/transfer");
}, [dispatch, push]);
const explainerString =
"Success! The create wrapped transaction was submitted.";
const howToAddToTokenListUrl = getHowToAddToTokenListUrl(targetChain);
return (
<>
<Typography
component="div"
variant="subtitle2"
className={classes.description}
>
{explainerString}
</Typography>
{createTx ? <ShowTx chainId={targetChain} tx={createTx} /> : null}
{howToAddToTokenListUrl ? (
<Alert severity="info" variant="outlined" className={classes.alert}>
Remember to add the token to the{" "}
<Link
href={howToAddToTokenListUrl}
target="_blank"
rel="noopener noreferrer"
>
token list
</Link>
{"."}
</Alert>
) : null}
<ButtonWithLoader onClick={handleResetClick}>
Attest Another Token!
</ButtonWithLoader>
<ButtonWithLoader onClick={handleReturnClick}>
Return to Transfer
</ButtonWithLoader>
</>
);
}

View File

@ -1,88 +0,0 @@
import { CHAIN_ID_SOLANA, isTerraChain } from "@certusone/wormhole-sdk";
import { Alert } from "@material-ui/lab";
import { Link, makeStyles } from "@material-ui/core";
import { useMemo } from "react";
import { useSelector } from "react-redux";
import { useHandleAttest } from "../../hooks/useHandleAttest";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import useMetaplexData from "../../hooks/useMetaplexData";
import {
selectAttestAttestTx,
selectAttestIsSendComplete,
selectAttestSourceAsset,
selectAttestSourceChain,
} from "../../store/selectors";
import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
import TransactionProgress from "../TransactionProgress";
import WaitingForWalletMessage from "./WaitingForWalletMessage";
import { SOLANA_TOKEN_METADATA_PROGRAM_URL } from "../../utils/consts";
import TerraFeeDenomPicker from "../TerraFeeDenomPicker";
const useStyles = makeStyles((theme) => ({
alert: {
marginTop: theme.spacing(1),
},
}));
const SolanaTokenMetadataWarning = () => {
const sourceAsset = useSelector(selectAttestSourceAsset);
const sourceAssetArrayed = useMemo(() => {
return [sourceAsset];
}, [sourceAsset]);
const metaplexData = useMetaplexData(sourceAssetArrayed);
const classes = useStyles();
if (metaplexData.isFetching || metaplexData.error) {
return null;
}
return !metaplexData.data?.get(sourceAsset) ? (
<Alert severity="warning" variant="outlined" className={classes.alert}>
This token is missing on-chain (Metaplex) metadata. Without it, the
wrapped token's name and symbol will be empty. See the{" "}
<Link
href={SOLANA_TOKEN_METADATA_PROGRAM_URL}
target="_blank"
rel="noopener noreferrer"
>
metaplex repository
</Link>{" "}
for details.
</Alert>
) : null;
};
function Send() {
const { handleClick, disabled, showLoader } = useHandleAttest();
const sourceChain = useSelector(selectAttestSourceChain);
const attestTx = useSelector(selectAttestAttestTx);
const isSendComplete = useSelector(selectAttestIsSendComplete);
const { isReady, statusMessage } = useIsWalletReady(sourceChain);
return (
<>
<KeyAndBalance chainId={sourceChain} />
{isTerraChain(sourceChain) && (
<TerraFeeDenomPicker disabled={disabled} chainId={sourceChain} />
)}
<ButtonWithLoader
disabled={!isReady || disabled}
onClick={handleClick}
showLoader={showLoader}
error={statusMessage}
>
Attest
</ButtonWithLoader>
{sourceChain === CHAIN_ID_SOLANA && <SolanaTokenMetadataWarning />}
<WaitingForWalletMessage />
<TransactionProgress
chainId={sourceChain}
tx={attestTx}
isSendComplete={isSendComplete}
/>
</>
);
}
export default Send;

View File

@ -1,41 +0,0 @@
import { makeStyles, Typography } from "@material-ui/core";
import { useSelector } from "react-redux";
import {
selectAttestSourceChain,
selectAttestAttestTx,
} from "../../store/selectors";
import ShowTx from "../ShowTx";
const useStyles = makeStyles((theme) => ({
description: {
textAlign: "center",
},
tx: {
marginTop: theme.spacing(1),
textAlign: "center",
},
viewButton: {
marginTop: theme.spacing(1),
},
}));
export default function SendPreview() {
const classes = useStyles();
const sourceChain = useSelector(selectAttestSourceChain);
const attestTx = useSelector(selectAttestAttestTx);
const explainerString = "The token has been attested!";
return (
<>
<Typography
component="div"
variant="subtitle2"
className={classes.description}
>
{explainerString}
</Typography>
{attestTx ? <ShowTx chainId={sourceChain} tx={attestTx} /> : null}
</>
);
}

View File

@ -1,82 +0,0 @@
import { makeStyles, TextField } from "@material-ui/core";
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
incrementStep,
setSourceAsset,
setSourceChain,
} from "../../store/attestSlice";
import {
selectAttestIsSourceComplete,
selectAttestShouldLockFields,
selectAttestSourceAsset,
selectAttestSourceChain,
} from "../../store/selectors";
import { CHAINS } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader";
import ChainSelect from "../ChainSelect";
import KeyAndBalance from "../KeyAndBalance";
import LowBalanceWarning from "../LowBalanceWarning";
const useStyles = makeStyles((theme) => ({
transferField: {
marginTop: theme.spacing(5),
},
}));
function Source() {
const classes = useStyles();
const dispatch = useDispatch();
const sourceChain = useSelector(selectAttestSourceChain);
const sourceAsset = useSelector(selectAttestSourceAsset);
const isSourceComplete = useSelector(selectAttestIsSourceComplete);
const shouldLockFields = useSelector(selectAttestShouldLockFields);
const handleSourceChange = useCallback(
(event) => {
dispatch(setSourceChain(event.target.value));
},
[dispatch]
);
const handleAssetChange = useCallback(
(event) => {
dispatch(setSourceAsset(event.target.value));
},
[dispatch]
);
const handleNextClick = useCallback(() => {
dispatch(incrementStep());
}, [dispatch]);
return (
<>
<ChainSelect
select
variant="outlined"
fullWidth
value={sourceChain}
onChange={handleSourceChange}
disabled={shouldLockFields}
chains={CHAINS}
/>
<KeyAndBalance chainId={sourceChain} />
<TextField
label="Asset"
variant="outlined"
fullWidth
className={classes.transferField}
value={sourceAsset}
onChange={handleAssetChange}
disabled={shouldLockFields}
/>
<LowBalanceWarning chainId={sourceChain} />
<ButtonWithLoader
disabled={!isSourceComplete}
onClick={handleNextClick}
showLoader={false}
>
Next
</ButtonWithLoader>
</>
);
}
export default Source;

View File

@ -1,41 +0,0 @@
import { makeStyles, Typography } from "@material-ui/core";
import { useSelector } from "react-redux";
import {
selectAttestSourceAsset,
selectAttestSourceChain,
} from "../../store/selectors";
import { CHAINS_BY_ID } from "../../utils/consts";
import SmartAddress from "../SmartAddress";
const useStyles = makeStyles((theme) => ({
description: {
textAlign: "center",
},
}));
export default function SourcePreview() {
const classes = useStyles();
const sourceChain = useSelector(selectAttestSourceChain);
const sourceAsset = useSelector(selectAttestSourceAsset);
const explainerContent =
sourceChain && sourceAsset ? (
<>
<span>You will attest</span>
<SmartAddress chainId={sourceChain} address={sourceAsset} isAsset />
<span>on {CHAINS_BY_ID[sourceChain].name}</span>
</>
) : (
""
);
return (
<Typography
component="div"
variant="subtitle2"
className={classes.description}
>
{explainerContent}
</Typography>
);
}

View File

@ -1,83 +0,0 @@
import { isEVMChain } from "@certusone/wormhole-sdk";
import { makeStyles, Typography } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { GasEstimateSummary } from "../../hooks/useTransactionFees";
import { incrementStep, setTargetChain } from "../../store/attestSlice";
import {
selectAttestIsTargetComplete,
selectAttestShouldLockFields,
selectAttestSourceChain,
selectAttestTargetChain,
} from "../../store/selectors";
import { CHAINS, CHAINS_BY_ID } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader";
import ChainSelect from "../ChainSelect";
import KeyAndBalance from "../KeyAndBalance";
import LowBalanceWarning from "../LowBalanceWarning";
const useStyles = makeStyles((theme) => ({
alert: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
}));
function Target() {
const classes = useStyles();
const dispatch = useDispatch();
const sourceChain = useSelector(selectAttestSourceChain);
const chains = useMemo(
() => CHAINS.filter((c) => c.id !== sourceChain),
[sourceChain]
);
const targetChain = useSelector(selectAttestTargetChain);
const isTargetComplete = useSelector(selectAttestIsTargetComplete);
const shouldLockFields = useSelector(selectAttestShouldLockFields);
const handleTargetChange = useCallback(
(event) => {
dispatch(setTargetChain(event.target.value));
},
[dispatch]
);
const handleNextClick = useCallback(() => {
dispatch(incrementStep());
}, [dispatch]);
return (
<>
<ChainSelect
select
variant="outlined"
fullWidth
value={targetChain}
onChange={handleTargetChange}
disabled={shouldLockFields}
chains={chains}
/>
<KeyAndBalance chainId={targetChain} />
<Alert severity="info" variant="outlined" className={classes.alert}>
<Typography>
You will have to pay transaction fees on{" "}
{CHAINS_BY_ID[targetChain].name} to attest this token.{" "}
</Typography>
{isEVMChain(targetChain) && (
<GasEstimateSummary
methodType="createWrapped"
chainId={targetChain}
/>
)}
</Alert>
<LowBalanceWarning chainId={targetChain} />
<ButtonWithLoader
disabled={!isTargetComplete}
onClick={handleNextClick}
showLoader={false}
>
Next
</ButtonWithLoader>
</>
);
}
export default Target;

View File

@ -1,27 +0,0 @@
import { makeStyles, Typography } from "@material-ui/core";
import { useSelector } from "react-redux";
import { selectAttestTargetChain } from "../../store/selectors";
import { CHAINS_BY_ID } from "../../utils/consts";
const useStyles = makeStyles((theme) => ({
description: {
textAlign: "center",
},
}));
export default function TargetPreview() {
const classes = useStyles();
const targetChain = useSelector(selectAttestTargetChain);
const explainerString = `to ${CHAINS_BY_ID[targetChain].name}`;
return (
<Typography
component="div"
variant="subtitle2"
className={classes.description}
>
{explainerString}
</Typography>
);
}

View File

@ -1,37 +0,0 @@
import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
import { makeStyles, Typography } from "@material-ui/core";
import { useSelector } from "react-redux";
import {
selectAttestAttestTx,
selectAttestCreateTx,
selectAttestIsCreating,
selectAttestIsSending,
selectAttestTargetChain,
} from "../../store/selectors";
import { WAITING_FOR_WALLET_AND_CONF } from "../Transfer/WaitingForWalletMessage";
const useStyles = makeStyles((theme) => ({
message: {
color: theme.palette.warning.light,
marginTop: theme.spacing(1),
textAlign: "center",
},
}));
export default function WaitingForWalletMessage() {
const classes = useStyles();
const isSending = useSelector(selectAttestIsSending);
const attestTx = useSelector(selectAttestAttestTx);
const targetChain = useSelector(selectAttestTargetChain);
const isCreating = useSelector(selectAttestIsCreating);
const createTx = useSelector(selectAttestCreateTx);
const showWarning = (isSending && !attestTx) || (isCreating && !createTx);
return showWarning ? (
<Typography className={classes.message} variant="body2">
{WAITING_FOR_WALLET_AND_CONF}{" "}
{targetChain === CHAIN_ID_SOLANA && isCreating
? "Note: there will be several transactions"
: null}
</Typography>
) : null;
}

View File

@ -1,101 +0,0 @@
import {
Container,
Step,
StepButton,
StepContent,
Stepper,
} from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { setStep } from "../../store/attestSlice";
import {
selectAttestActiveStep,
selectAttestIsCreateComplete,
selectAttestIsCreating,
selectAttestIsSendComplete,
selectAttestIsSending,
} from "../../store/selectors";
import HeaderText from "../HeaderText";
import Create from "./Create";
import CreatePreview from "./CreatePreview";
import Send from "./Send";
import SendPreview from "./SendPreview";
import Source from "./Source";
import SourcePreview from "./SourcePreview";
import Target from "./Target";
import TargetPreview from "./TargetPreview";
function Attest() {
const dispatch = useDispatch();
const activeStep = useSelector(selectAttestActiveStep);
const isSending = useSelector(selectAttestIsSending);
const isSendComplete = useSelector(selectAttestIsSendComplete);
const isCreating = useSelector(selectAttestIsCreating);
const isCreateComplete = useSelector(selectAttestIsCreateComplete);
const preventNavigation =
(isSending || isSendComplete || isCreating) && !isCreateComplete;
useEffect(() => {
if (preventNavigation) {
window.onbeforeunload = () => true;
return () => {
window.onbeforeunload = null;
};
}
}, [preventNavigation]);
return (
<Container maxWidth="md">
<HeaderText white>Token Registration</HeaderText>
<Alert severity="info">
This form allows you to register a token on a new foreign chain. Tokens
must be registered before they can be transferred.
</Alert>
<Stepper activeStep={activeStep} orientation="vertical">
<Step
expanded={activeStep >= 0}
disabled={preventNavigation || isCreateComplete}
>
<StepButton onClick={() => dispatch(setStep(0))} icon={null}>
1. Source
</StepButton>
<StepContent>
{activeStep === 0 ? <Source /> : <SourcePreview />}
</StepContent>
</Step>
<Step
expanded={activeStep >= 1}
disabled={preventNavigation || isCreateComplete}
>
<StepButton onClick={() => dispatch(setStep(1))} icon={null}>
2. Target
</StepButton>
<StepContent>
{activeStep === 1 ? <Target /> : <TargetPreview />}
</StepContent>
</Step>
<Step expanded={activeStep >= 2} disabled={isSendComplete}>
<StepButton onClick={() => dispatch(setStep(2))} icon={null}>
3. Send attestation
</StepButton>
<StepContent>
{activeStep === 2 ? <Send /> : <SendPreview />}
</StepContent>
</Step>
<Step expanded={activeStep >= 3}>
<StepButton
onClick={() => dispatch(setStep(3))}
disabled={!isSendComplete}
icon={null}
>
4. Create wrapped token
</StepButton>
<StepContent>
{isCreateComplete ? <CreatePreview /> : <Create />}
</StepContent>
</Step>
</Stepper>
</Container>
);
}
export default Attest;

View File

@ -1,41 +0,0 @@
import { makeStyles } from "@material-ui/core";
// import { useRouteMatch } from "react-router";
const useStyles = makeStyles((theme) => ({
holeOuterContainer: {
maxWidth: "100%",
width: "100%",
position: "relative",
},
holeInnerContainer: {
position: "absolute",
zIndex: -1,
left: "50%",
transform: "translate(-50%, 0)",
width: "100%",
maxWidth: "100%",
overflow: "hidden",
display: "flex",
justifyContent: "center",
},
holeImage: {
width: "max(1200px, 100vw)",
maxWidth: "1600px",
},
blurred: {
filter: "blur(2px)",
opacity: ".9",
},
}));
const BackgroundImage = () => {
const classes = useStyles();
return (
<div className={classes.holeOuterContainer}>
<div className={classes.holeInnerContainer}></div>
</div>
);
};
export default BackgroundImage;

View File

@ -1,71 +0,0 @@
import {
Button,
CircularProgress,
makeStyles,
Typography,
} from "@material-ui/core";
import { ReactChild } from "react";
const useStyles = makeStyles((theme) => ({
root: {
position: "relative",
},
button: {
marginTop: theme.spacing(2),
width: "100%",
},
loader: {
position: "absolute",
bottom: 0,
left: "50%",
marginLeft: -12,
marginBottom: 6,
},
error: {
marginTop: theme.spacing(1),
textAlign: "center",
},
}));
export default function ButtonWithLoader({
disabled,
onClick,
showLoader,
error,
children,
}: {
disabled?: boolean;
onClick: () => void;
showLoader?: boolean;
error?: string;
children: ReactChild;
}) {
const classes = useStyles();
return (
<>
<div className={classes.root}>
<Button
color="primary"
variant="contained"
className={classes.button}
disabled={disabled}
onClick={onClick}
>
{children}
</Button>
{showLoader ? (
<CircularProgress
size={24}
color="inherit"
className={classes.loader}
/>
) : null}
</div>
{error ? (
<Typography variant="body2" color="error" className={classes.error}>
{error}
</Typography>
) : null}
</>
);
}

View File

@ -1,56 +0,0 @@
import {
ListItemIcon,
ListItemText,
makeStyles,
MenuItem,
OutlinedTextFieldProps,
TextField,
} from "@material-ui/core";
import clsx from "clsx";
import { useMemo } from "react";
import { useBetaContext } from "../contexts/BetaContext";
import { BETA_CHAINS, ChainInfo } from "../utils/consts";
const useStyles = makeStyles((theme) => ({
select: {
"& .MuiSelect-root": {
display: "flex",
alignItems: "center",
},
},
listItemIcon: {
minWidth: 40,
},
icon: {
height: 24,
maxWidth: 24,
},
}));
const createChainMenuItem = ({ id, name, logo }: ChainInfo, classes: any) => (
<MenuItem key={id} value={id}>
<ListItemIcon className={classes.listItemIcon}>
<img src={logo} alt={name} className={classes.icon} />
</ListItemIcon>
<ListItemText>{name}</ListItemText>
</MenuItem>
);
interface ChainSelectProps extends OutlinedTextFieldProps {
chains: ChainInfo[];
}
export default function ChainSelect({ chains, ...rest }: ChainSelectProps) {
const classes = useStyles();
const isBeta = useBetaContext();
const filteredChains = useMemo(
() =>
chains.filter(({ id }) => (isBeta ? true : !BETA_CHAINS.includes(id))),
[chains, isBeta]
);
return (
<TextField {...rest} className={clsx(classes.select, rest.className)}>
{filteredChains.map((chain) => createChainMenuItem(chain, classes))}
</TextField>
);
}

View File

@ -1,28 +0,0 @@
import { IconButton } from "@material-ui/core";
import { ArrowForward, SwapHoriz } from "@material-ui/icons";
import { useState } from "react";
export default function ChainSelectArrow({
onClick,
disabled,
}: {
onClick: () => void;
disabled: boolean;
}) {
const [showSwap, setShowSwap] = useState(false);
return (
<IconButton
onClick={onClick}
onMouseEnter={() => {
setShowSwap(true);
}}
onMouseLeave={() => {
setShowSwap(false);
}}
disabled={disabled}
>
{showSwap ? <SwapHoriz /> : <ArrowForward />}
</IconButton>
);
}

View File

@ -1,37 +0,0 @@
import { ChainId } from "@certusone/wormhole-sdk";
import { Link, makeStyles, Typography } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { useMemo } from "react";
import { CHAIN_CONFIG_MAP } from "../config";
const useStyles = makeStyles((theme) => ({
alert: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
}));
export default function ChainWarningMessage({ chainId }: { chainId: ChainId }) {
const classes = useStyles();
const warningMessage = useMemo(() => {
return CHAIN_CONFIG_MAP[chainId]?.warningMessage;
}, [chainId]);
if (warningMessage === undefined) {
return null;
}
return (
<Alert variant="outlined" severity="warning" className={classes.alert}>
{warningMessage.text}
{warningMessage.link ? (
<Typography component="div">
<Link href={warningMessage.link.url} target="_blank" rel="noreferrer">
{warningMessage.link.text}
</Link>
</Typography>
) : null}
</Alert>
);
}

View File

@ -1,43 +0,0 @@
import { useCallback, useState } from "react";
import { Typography } from "@material-ui/core";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import ToggleConnectedButton from "./ToggleConnectedButton";
import EvmConnectWalletDialog from "./EvmConnectWalletDialog";
import { ChainId } from "@certusone/wormhole-sdk";
const EthereumSignerKey = ({ chainId }: { chainId: ChainId }) => {
const { disconnect, signerAddress, providerError } = useEthereumProvider();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const openDialog = useCallback(() => {
setIsDialogOpen(true);
}, [setIsDialogOpen]);
const closeDialog = useCallback(() => {
setIsDialogOpen(false);
}, [setIsDialogOpen]);
return (
<>
<ToggleConnectedButton
connect={openDialog}
disconnect={disconnect}
connected={!!signerAddress}
pk={signerAddress || ""}
/>
<EvmConnectWalletDialog
isOpen={isDialogOpen}
onClose={closeDialog}
chainId={chainId}
/>
{providerError ? (
<Typography variant="body2" color="error">
{providerError}
</Typography>
) : null}
</>
);
};
export default EthereumSignerKey;

View File

@ -1,120 +0,0 @@
import { ChainId } from "@certusone/wormhole-sdk";
import {
Dialog,
DialogTitle,
IconButton,
List,
ListItem,
ListItemIcon,
ListItemText,
makeStyles,
} from "@material-ui/core";
import CloseIcon from "@material-ui/icons/Close";
import { useCallback } from "react";
import {
Connection,
ConnectType,
useEthereumProvider,
} from "../contexts/EthereumProviderContext";
import { getEvmChainId } from "../utils/consts";
import { EVM_RPC_MAP } from "../utils/metaMaskChainParameters";
const useStyles = makeStyles((theme) => ({
flexTitle: {
display: "flex",
alignItems: "center",
"& > div": {
flexGrow: 1,
marginRight: theme.spacing(4),
},
"& > button": {
marginRight: theme.spacing(-1),
},
},
icon: {
height: 24,
width: 24,
},
}));
const WalletOptions = ({
connection,
connect,
onClose,
}: {
connection: Connection;
connect: (connectType: ConnectType) => void;
onClose: () => void;
}) => {
const classes = useStyles();
const handleClick = useCallback(() => {
connect(connection.connectType);
onClose();
}, [connect, connection, onClose]);
return (
<ListItem button onClick={handleClick}>
<ListItemIcon>
<img
src={connection.icon}
alt={connection.name}
className={classes.icon}
/>
</ListItemIcon>
<ListItemText>{connection.name}</ListItemText>
</ListItem>
);
};
const EvmConnectWalletDialog = ({
isOpen,
onClose,
chainId,
}: {
isOpen: boolean;
onClose: () => void;
chainId: ChainId;
}) => {
const { availableConnections, connect } = useEthereumProvider();
const classes = useStyles();
const availableWallets = availableConnections
.filter((connection) => {
if (connection.connectType === ConnectType.METAMASK) {
return true;
} else if (connection.connectType === ConnectType.WALLETCONNECT) {
const evmChainId = getEvmChainId(chainId);
// WalletConnect requires a rpc provider
return (
evmChainId !== undefined && EVM_RPC_MAP[evmChainId] !== undefined
);
} else {
return false;
}
})
.map((connection) => (
<WalletOptions
connection={connection}
connect={connect}
onClose={onClose}
key={connection.name}
/>
));
return (
<Dialog open={isOpen} onClose={onClose}>
<DialogTitle>
<div className={classes.flexTitle}>
<div>Select your wallet</div>
<IconButton onClick={onClose}>
<CloseIcon />
</IconButton>
</div>
</DialogTitle>
<List>{availableWallets}</List>
</Dialog>
);
};
export default EvmConnectWalletDialog;

View File

@ -1,335 +0,0 @@
import {
CHAIN_ID_ACALA,
CHAIN_ID_KARURA,
CHAIN_ID_TERRA,
hexToNativeAssetString,
isEVMChain,
isTerraChain,
} from "@certusone/wormhole-sdk";
import {
Card,
Checkbox,
Chip,
makeStyles,
Typography,
} from "@material-ui/core";
import clsx from "clsx";
import { parseUnits } from "ethers/lib/utils";
import { useCallback, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import SmartAddress from "../components/SmartAddress";
import { useAcalaRelayerInfo } from "../hooks/useAcalaRelayerInfo";
import useRelayerInfo from "../hooks/useRelayerInfo";
import { GasEstimateSummary } from "../hooks/useTransactionFees";
import { COLORS } from "../muiTheme";
import {
selectTransferAmount,
selectTransferOriginAsset,
selectTransferOriginChain,
selectTransferSourceChain,
selectTransferSourceParsedTokenAccount,
selectTransferTargetChain,
selectTransferUseRelayer,
} from "../store/selectors";
import { setRelayerFee, setUseRelayer } from "../store/transferSlice";
import { CHAINS_BY_ID, getDefaultNativeCurrencySymbol } from "../utils/consts";
const useStyles = makeStyles((theme) => ({
feeSelectorContainer: {
marginTop: "2rem",
textAlign: "center",
},
title: {
margin: theme.spacing(2),
},
optionCardBase: {
display: "flex",
margin: theme.spacing(2),
alignItems: "center",
justifyContent: "space-between",
padding: theme.spacing(1),
background: COLORS.nearBlackWithMinorTransparency,
"& > *": {
margin: ".5rem",
},
border: "1px solid " + COLORS.nearBlackWithMinorTransparency,
},
alignCenterContainer: {
alignItems: "center",
display: "flex",
"& > *": {
margin: "0rem 1rem 0rem 1rem",
},
},
optionCardSelectable: {
"&:hover": {
cursor: "pointer",
boxShadow: "inset 0 0 100px 100px rgba(255, 255, 255, 0.1)",
},
},
optionCardSelected: {
border: "1px solid " + COLORS.blue,
},
inlineBlock: {
display: "inline-block",
},
alignLeft: {
textAlign: "left",
},
betaLabel: {
color: COLORS.white,
background: "linear-gradient(20deg, #f44b1b 0%, #eeb430 100%)",
marginLeft: theme.spacing(1),
fontSize: "120%",
},
}));
function FeeMethodSelector() {
const classes = useStyles();
const originAsset = useSelector(selectTransferOriginAsset);
const originChain = useSelector(selectTransferOriginChain);
const targetChain = useSelector(selectTransferTargetChain);
const transferAmount = useSelector(selectTransferAmount);
const relayerInfo = useRelayerInfo(originChain, originAsset, targetChain);
const sourceParsedTokenAccount = useSelector(
selectTransferSourceParsedTokenAccount
);
const sourceDecimals = sourceParsedTokenAccount?.decimals;
let vaaNormalizedAmount: string | undefined = undefined;
if (transferAmount && sourceDecimals !== undefined) {
try {
vaaNormalizedAmount = parseUnits(
transferAmount,
Math.min(sourceDecimals, 8)
).toString();
} catch (e) {}
}
const sourceSymbol = sourceParsedTokenAccount?.symbol;
const acalaRelayerInfo = useAcalaRelayerInfo(
targetChain,
vaaNormalizedAmount,
originChain ? hexToNativeAssetString(originAsset, originChain) : undefined
);
const sourceChain = useSelector(selectTransferSourceChain);
const dispatch = useDispatch();
const relayerSelected = !!useSelector(selectTransferUseRelayer);
// console.log("relayer info in fee method selector", relayerInfo);
const relayerEligible =
relayerInfo.data &&
relayerInfo.data.isRelayable &&
relayerInfo.data.feeFormatted &&
relayerInfo.data.feeUsd;
const targetIsAcala =
targetChain === CHAIN_ID_ACALA || targetChain === CHAIN_ID_KARURA;
const acalaRelayerEligible = acalaRelayerInfo.data?.shouldRelay;
const chooseAcalaRelayer = useCallback(() => {
if (targetIsAcala && acalaRelayerEligible) {
dispatch(setUseRelayer(true));
dispatch(setRelayerFee(undefined));
}
}, [dispatch, targetIsAcala, acalaRelayerEligible]);
const chooseRelayer = useCallback(() => {
if (relayerEligible) {
dispatch(setUseRelayer(true));
dispatch(setRelayerFee(relayerInfo.data?.feeFormatted));
}
}, [relayerInfo, dispatch, relayerEligible]);
const chooseManual = useCallback(() => {
dispatch(setUseRelayer(false));
dispatch(setRelayerFee(undefined));
}, [dispatch]);
useEffect(() => {
if (targetIsAcala) {
if (acalaRelayerEligible) {
chooseAcalaRelayer();
} else {
chooseManual();
}
} else if (relayerInfo.data?.isRelayable === true) {
chooseRelayer();
} else if (relayerInfo.data?.isRelayable === false) {
chooseManual();
}
//If it's undefined / null it's still loading, so no action is taken.
}, [
relayerInfo,
chooseRelayer,
chooseManual,
targetIsAcala,
acalaRelayerEligible,
chooseAcalaRelayer,
]);
const acalaRelayerContent = (
<Card
className={
classes.optionCardBase +
" " +
(relayerSelected ? classes.optionCardSelected : "") +
" " +
(acalaRelayerEligible ? classes.optionCardSelectable : "")
}
onClick={chooseAcalaRelayer}
>
<div className={classes.alignCenterContainer}>
<Checkbox
checked={relayerSelected}
disabled={!acalaRelayerEligible}
onClick={chooseAcalaRelayer}
className={classes.inlineBlock}
/>
<div className={clsx(classes.inlineBlock, classes.alignLeft)}>
{acalaRelayerEligible ? (
<div>
<Typography variant="body1">
{CHAINS_BY_ID[targetChain].name}
</Typography>
<Typography variant="body2" color="textSecondary">
{CHAINS_BY_ID[targetChain].name} pays gas for you &#127881;
</Typography>
</div>
) : (
<>
<Typography color="textSecondary" variant="body2">
{"Automatic redeem is unavailable for this token."}
</Typography>
<div />
</>
)}
</div>
</div>
{acalaRelayerEligible ? (
<>
<div></div>
<div></div>
</>
) : null}
</Card>
);
const relayerContent = (
<Card
className={
classes.optionCardBase +
" " +
(relayerSelected ? classes.optionCardSelected : "") +
" " +
(relayerEligible ? classes.optionCardSelectable : "")
}
onClick={chooseRelayer}
>
<div className={classes.alignCenterContainer}>
<Checkbox
checked={relayerSelected}
disabled={!relayerEligible}
onClick={chooseRelayer}
className={classes.inlineBlock}
/>
<div className={clsx(classes.inlineBlock, classes.alignLeft)}>
{relayerEligible ? (
<div>
<Typography variant="body1">Automatic Payment</Typography>
<Typography variant="body2" color="textSecondary">
{`Pay with additional ${
sourceSymbol ? sourceSymbol : "tokens"
} and use a relayer`}
</Typography>
</div>
) : (
<>
<Typography color="textSecondary" variant="body2">
{"Automatic redeem is unavailable for this token."}
</Typography>
<div />
</>
)}
</div>
</div>
{/* TODO fixed number of decimals on these strings */}
{relayerEligible ? (
<>
<div>
<Chip label="Beta" className={classes.betaLabel} />
</div>
<div>
<div>
<Typography className={classes.inlineBlock}>
{/* Transfers are max 8 decimals */}
{parseFloat(relayerInfo.data?.feeFormatted || "0").toFixed(
Math.min(sourceParsedTokenAccount?.decimals || 8, 8)
)}
</Typography>
<SmartAddress
chainId={sourceChain}
parsedTokenAccount={sourceParsedTokenAccount}
isAsset
/>
</div>{" "}
<Typography>{`($ ${relayerInfo.data?.feeUsd})`}</Typography>
</div>
</>
) : null}
</Card>
);
const manualRedeemContent = (
<Card
className={
classes.optionCardBase +
" " +
classes.optionCardSelectable +
" " +
(!relayerSelected ? classes.optionCardSelected : "")
}
onClick={chooseManual}
>
<div className={classes.alignCenterContainer}>
<Checkbox
checked={!relayerSelected}
onClick={chooseManual}
className={classes.inlineBlock}
/>
<div className={clsx(classes.inlineBlock, classes.alignLeft)}>
<Typography variant="body1">{"Manual Payment"}</Typography>
<Typography variant="body2" color="textSecondary">
{`Pay with your own ${
isTerraChain(targetChain)
? "funds"
: getDefaultNativeCurrencySymbol(targetChain)
} on ${CHAINS_BY_ID[targetChain]?.name || "target chain"}`}
</Typography>
</div>
</div>
{(isEVMChain(targetChain) || targetChain === CHAIN_ID_TERRA) && (
<GasEstimateSummary
methodType="transfer"
chainId={targetChain}
priceQuote={relayerInfo.data?.targetNativeAssetPriceQuote}
/>
)}
</Card>
);
return (
<div className={classes.feeSelectorContainer}>
<Typography
className={classes.title}
variant="subtitle2"
color="textSecondary"
>
How would you like to pay the target chain fees?
</Typography>
{targetIsAcala ? acalaRelayerContent : relayerContent}
{manualRedeemContent}
</div>
);
}
export default FeeMethodSelector;

View File

@ -1,84 +0,0 @@
import { makeStyles, Typography } from "@material-ui/core";
const useStyles = makeStyles((theme) => ({
footer: {
position: "relative",
},
container: {
maxWidth: 1100,
margin: "0px auto",
paddingTop: theme.spacing(11),
paddingBottom: theme.spacing(6.5),
[theme.breakpoints.up("md")]: {
paddingBottom: theme.spacing(12),
},
},
flex: {
display: "flex",
flexDirection: "column",
alignItems: "center",
marginLeft: theme.spacing(3.5),
marginRight: theme.spacing(3.5),
borderTop: "1px solid #585587",
paddingTop: theme.spacing(7),
[theme.breakpoints.up("md")]: {
flexWrap: "wrap",
flexDirection: "row",
alignItems: "unset",
},
},
spacer: {
flexGrow: 1,
},
linkStyle: {
color: "white",
display: "block",
marginRight: theme.spacing(0),
marginBottom: theme.spacing(1.5),
fontSize: 14,
textUnderlineOffset: "6px",
[theme.breakpoints.up("md")]: {
marginRight: theme.spacing(7.5),
},
},
linkActiveStyle: { textDecoration: "underline" },
wormholeIcon: {
height: 68,
marginTop: -24,
},
}));
export default function Footer() {
const classes = useStyles();
return (
<footer className={classes.footer}>
<div className={classes.container}>
<div className={classes.flex}>
<div className={classes.spacer} />
<Typography variant="body2">
This Interface is an open source software portal to Wormhole, a
cross chain messaging protocol. THIS INTERFACE AND THE WORMHOLE
PROTOCOL ARE PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT
WARRANTIES OF ANY KIND. By using or accessing this Interface or
Wormhole, you agree that no developer or entity involved in
creating, deploying, maintaining, operating this Interface or
Wormhole, or causing or supporting any of the foregoing, will be
liable in any manner for any claims or damages whatsoever associated
with your use, inability to use, or your interaction with other
users of, this Interface or Wormhole, or this Interface or Wormhole
themselves, including any direct, indirect, incidental, special,
exemplary, punitive or consequential damages, or loss of profits,
cryptocurrencies, tokens, or anything else of value. By using or
accessing this Interface, you represent that you are not subject to
sanctions or otherwise designated on any list of prohibited or
restricted parties or excluded or denied persons, including but not
limited to the lists maintained by the United States' Department of
Treasury's Office of Foreign Assets Control, the United Nations
Security Council, the European Union or its Member States, or any
other government authority.
</Typography>
</div>
</div>
</footer>
);
}

View File

@ -1,53 +0,0 @@
import { makeStyles, Typography } from "@material-ui/core";
import clsx from "clsx";
import { ReactChild } from "react";
import { COLORS } from "../muiTheme";
const useStyles = makeStyles((theme) => ({
centeredContainer: {
marginBottom: theme.spacing(16),
textAlign: "center",
width: "100%",
},
linearGradient: {
background: `linear-gradient(to left, ${COLORS.blue}, ${COLORS.green});`,
WebkitBackgroundClip: "text",
backgroundClip: "text",
WebkitTextFillColor: "transparent",
MozBackgroundClip: "text",
MozTextFillColor: "transparent",
},
subtitle: {
marginTop: theme.spacing(2),
},
}));
export default function HeaderText({
children,
white,
small,
subtitle,
}: {
children: ReactChild;
white?: boolean;
small?: boolean;
subtitle?: ReactChild;
}) {
const classes = useStyles();
return (
<div className={classes.centeredContainer}>
<Typography
variant={small ? "h2" : "h1"}
component="h1"
className={clsx({ [classes.linearGradient]: !white })}
>
{children}
</Typography>
{subtitle ? (
<Typography component="div" className={classes.subtitle}>
{subtitle}
</Typography>
) : null}
</div>
);
}

View File

@ -1,29 +0,0 @@
import {
ChainId,
CHAIN_ID_ALGORAND,
CHAIN_ID_SOLANA,
isEVMChain,
isTerraChain,
} from "@certusone/wormhole-sdk";
import AlgorandWalletKey from "./AlgorandWalletKey";
import EthereumSignerKey from "./EthereumSignerKey";
import SolanaWalletKey from "./SolanaWalletKey";
import TerraWalletKey from "./TerraWalletKey";
function KeyAndBalance({ chainId }: { chainId: ChainId }) {
if (isEVMChain(chainId)) {
return <EthereumSignerKey chainId={chainId} />;
}
if (chainId === CHAIN_ID_SOLANA) {
return <SolanaWalletKey />;
}
if (isTerraChain(chainId)) {
return <TerraWalletKey />;
}
if (chainId === CHAIN_ID_ALGORAND) {
return <AlgorandWalletKey />;
}
return null;
}
export default KeyAndBalance;

View File

@ -1,50 +0,0 @@
import { ChainId, isTerraChain } from "@certusone/wormhole-sdk";
import { makeStyles, Typography } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { useSelector } from "react-redux";
import useIsWalletReady from "../hooks/useIsWalletReady";
import useTransactionFees from "../hooks/useTransactionFees";
import { selectTransferUseRelayer } from "../store/selectors";
import { getDefaultNativeCurrencySymbol } from "../utils/consts";
const useStyles = makeStyles((theme) => ({
alert: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
}));
function LowBalanceWarning({ chainId }: { chainId: ChainId }) {
const classes = useStyles();
const { isReady } = useIsWalletReady(chainId);
const transactionFeeWarning = useTransactionFees(chainId);
const relayerSelected = !!useSelector(selectTransferUseRelayer);
const terraChain = isTerraChain(chainId);
const displayWarning =
isReady &&
!relayerSelected &&
(terraChain || transactionFeeWarning.balanceString) &&
transactionFeeWarning.isSufficientBalance === false;
const warningMessage = terraChain
? "This wallet may not have sufficient funds to pay for the upcoming transaction fees."
: `This wallet has a very low ${getDefaultNativeCurrencySymbol(
chainId
)} balance and may not be able to pay for the upcoming transaction fees.`;
const content = (
<Alert severity="warning" variant="outlined" className={classes.alert}>
<Typography variant="body1">{warningMessage}</Typography>
{!terraChain ? (
<Typography variant="body1">
{"Current balance: " + transactionFeeWarning.balanceString}
</Typography>
) : null}
</Alert>
);
return displayWarning ? content : null;
}
export default LowBalanceWarning;

View File

@ -1,391 +0,0 @@
import { ChainId, TokenImplementation__factory } from "@certusone/wormhole-sdk";
import { Signer } from "@ethersproject/abstract-signer";
import { getAddress } from "@ethersproject/address";
import { BigNumber } from "@ethersproject/bignumber";
import {
CircularProgress,
Container,
makeStyles,
Paper,
Typography,
} from "@material-ui/core";
import ArrowRightAltIcon from "@material-ui/icons/ArrowRightAlt";
import { Alert } from "@material-ui/lab";
import { parseUnits } from "ethers/lib/utils";
import { useSnackbar } from "notistack";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
import useEthereumMigratorInformation from "../../hooks/useEthereumMigratorInformation";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import { COLORS } from "../../muiTheme";
import { CHAINS_BY_ID, getMigrationAssetMap } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader";
import EthereumSignerKey from "../EthereumSignerKey";
import HeaderText from "../HeaderText";
import ShowTx from "../ShowTx";
import SmartAddress from "../SmartAddress";
const useStyles = makeStyles((theme) => ({
spacer: {
height: "2rem",
},
containerDiv: {
textAlign: "center",
padding: theme.spacing(2),
},
lineItem: {
display: "flex",
flexWrap: "nowrap",
justifyContent: "space-between",
"& > *": {
alignSelf: "flex-start",
width: "max-content",
},
},
flexGrow: {
flewGrow: 1,
},
mainPaper: {
backgroundColor: COLORS.whiteWithTransparency,
textAlign: "center",
padding: "2rem",
"& > h, p ": {
margin: ".5rem",
},
},
hidden: {
display: "none",
},
divider: {
margin: "2rem 0rem 2rem 0rem",
},
balance: {
display: "inline-block",
},
convertButton: {
alignSelf: "flex-end",
},
}));
//TODO move elsewhere
export const compareWithDecimalOffset = (
valueA: string,
decimalsA: number,
valueB: string,
decimalsB: number
) => {
//find which is larger, and offset by that amount
const decimalsBasis = decimalsA > decimalsB ? decimalsA : decimalsB;
const normalizedA = parseUnits(valueA, decimalsBasis).toBigInt();
const normalizedB = parseUnits(valueB, decimalsBasis).toBigInt();
if (normalizedA < normalizedB) {
return -1;
} else if (normalizedA === normalizedB) {
return 0;
} else {
return 1;
}
};
function EvmMigrationLineItem({
chainId,
migratorAddress,
onLoadComplete,
}: {
chainId: ChainId;
migratorAddress: string;
onLoadComplete: () => void;
}) {
const classes = useStyles();
const { enqueueSnackbar } = useSnackbar();
const { signer, signerAddress } = useEthereumProvider();
const poolInfo = useEthereumMigratorInformation(
migratorAddress,
signer,
signerAddress,
false
);
const [loaded, setLoaded] = useState(false);
const [migrationIsProcessing, setMigrationIsProcessing] = useState(false);
const [transaction, setTransaction] = useState("");
const [error, setError] = useState("");
const fromSymbol = poolInfo?.data?.fromSymbol;
const toSymbol = poolInfo?.data?.toSymbol;
const sufficientPoolBalance =
poolInfo.data &&
compareWithDecimalOffset(
poolInfo.data.fromWalletBalance,
poolInfo.data.fromDecimals,
poolInfo.data.toPoolBalance,
poolInfo.data.toDecimals
) !== 1;
useEffect(() => {
if (!loaded && (poolInfo.data || poolInfo.error)) {
onLoadComplete();
setLoaded(true);
}
}, [loaded, poolInfo, onLoadComplete]);
//TODO use transaction loader
const migrateTokens = useCallback(async () => {
if (!poolInfo.data) {
enqueueSnackbar(null, {
content: <Alert severity="error">Could not migrate the tokens.</Alert>,
}); //Should never be hit
return;
}
try {
const migrationAmountAbs = parseUnits(
poolInfo.data.fromWalletBalance,
poolInfo.data.fromDecimals
);
setMigrationIsProcessing(true);
await poolInfo.data.fromToken.approve(
poolInfo.data.migrator.address,
migrationAmountAbs
);
const transaction = await poolInfo.data.migrator.migrate(
migrationAmountAbs
);
await transaction.wait();
setTransaction(transaction.hash);
enqueueSnackbar(null, {
content: (
<Alert severity="success">Successfully migrated the tokens.</Alert>
),
});
setMigrationIsProcessing(false);
} catch (e) {
console.error(e);
enqueueSnackbar(null, {
content: <Alert severity="error">Could not migrate the tokens.</Alert>,
});
setMigrationIsProcessing(false);
setError("Failed to send the transaction.");
}
}, [poolInfo.data, enqueueSnackbar]);
if (!poolInfo.data) {
return null;
} else if (transaction) {
return (
<div className={classes.lineItem}>
<div>
<Typography variant="body2" color="textSecondary">
Successfully migrated your tokens. They will become available once
this transaction confirms.
</Typography>
<ShowTx chainId={chainId} tx={{ id: transaction, block: 1 }} />
</div>
</div>
);
} else {
return (
<div className={classes.lineItem}>
<div>
<Typography variant="body2" color="textSecondary">
Current Token
</Typography>
<Typography className={classes.balance}>
{poolInfo.data.fromWalletBalance}
</Typography>
<SmartAddress
chainId={chainId}
address={poolInfo.data.fromAddress}
symbol={fromSymbol || undefined}
isAsset
/>
</div>
<div>
<Typography variant="body2" color="textSecondary">
will become
</Typography>
<ArrowRightAltIcon fontSize="large" />
</div>
<div>
<Typography variant="body2" color="textSecondary">
Wormhole Token
</Typography>
<Typography className={classes.balance}>
{poolInfo.data.fromWalletBalance}
</Typography>
<SmartAddress
chainId={chainId}
address={poolInfo.data.toAddress}
symbol={toSymbol || undefined}
isAsset
/>
</div>
<div className={classes.convertButton}>
<ButtonWithLoader
showLoader={migrationIsProcessing}
onClick={migrateTokens}
error={
error
? error
: !sufficientPoolBalance
? "The swap pool has insufficient funds."
: ""
}
disabled={!sufficientPoolBalance || migrationIsProcessing}
>
Convert
</ButtonWithLoader>
</div>
</div>
);
}
}
const getAddressBalances = async (
signer: Signer,
signerAddress: string,
addresses: string[]
): Promise<Map<string, BigNumber | null>> => {
try {
const promises: Promise<any>[] = [];
const output = new Map<string, BigNumber | null>();
addresses.forEach((address) => {
const factory = TokenImplementation__factory.connect(address, signer);
promises.push(
factory.balanceOf(signerAddress).then(
(result) => {
output.set(address, result);
},
(error) => {
output.set(address, null);
}
)
);
});
await Promise.all(promises);
return output;
} catch (e) {
return Promise.reject("Unable to retrieve token balances.");
}
};
export default function EvmQuickMigrate({ chainId }: { chainId: ChainId }) {
const classes = useStyles();
const { signer, signerAddress } = useEthereumProvider();
const { isReady } = useIsWalletReady(chainId);
const migrationMap = useMemo(() => getMigrationAssetMap(chainId), [chainId]);
const eligibleTokens = useMemo(
() => Array.from(migrationMap.keys()),
[migrationMap]
);
const [migrators, setMigrators] = useState<string[] | null>(null);
const [migratorsError, setMigratorsError] = useState("");
const [migratorsLoading, setMigratorsLoading] = useState(false);
//This is for a callback into the line items, so a loader can be displayed while
//they are loading
//TODO don't just swallow loading errors.
const [migratorsFinishedLoading, setMigratorsFinishedLoading] = useState(0);
const reportLoadComplete = useCallback(() => {
setMigratorsFinishedLoading((prevState) => prevState + 1);
}, []);
const isLoading =
migratorsLoading ||
(migrators &&
migrators.length &&
migratorsFinishedLoading < migrators.length);
useEffect(() => {
if (isReady && signer && signerAddress) {
let cancelled = false;
setMigratorsLoading(true);
setMigratorsError("");
getAddressBalances(signer, signerAddress, eligibleTokens).then(
(result) => {
if (!cancelled) {
const migratorAddresses = [];
for (const tokenAddress of result.keys()) {
if (result.get(tokenAddress) && result.get(tokenAddress)?.gt(0)) {
const migratorAddress = migrationMap.get(
getAddress(tokenAddress)
);
if (migratorAddress) {
migratorAddresses.push(migratorAddress);
}
}
}
setMigratorsFinishedLoading(0);
setMigrators(migratorAddresses);
setMigratorsLoading(false);
}
},
(error) => {
if (!cancelled) {
setMigratorsLoading(false);
setMigratorsError(
"Failed to retrieve available token information."
);
}
}
);
return () => {
cancelled = true;
};
}
}, [isReady, signer, signerAddress, eligibleTokens, migrationMap]);
const hasEligibleAssets = migrators && migrators.length > 0;
const chainName = CHAINS_BY_ID[chainId]?.name;
const content = (
<div className={classes.containerDiv}>
<Typography variant="h5">
{`This page allows you to convert certain wrapped tokens ${
chainName ? "on " + chainName : ""
} into
Wormhole V2 tokens.`}
</Typography>
<EthereumSignerKey chainId={chainId} />
{!isReady ? (
<Typography variant="body1">Please connect your wallet.</Typography>
) : migratorsError ? (
<Typography variant="h6">{migratorsError}</Typography>
) : (
<>
<div className={classes.spacer} />
<CircularProgress className={isLoading ? "" : classes.hidden} />
<div className={!isLoading ? "" : classes.hidden}>
<Typography>
{hasEligibleAssets
? "You have some assets that are eligible for migration! Click the 'Convert' button to swap them for Wormhole tokens."
: "You don't have any assets eligible for migration."}
</Typography>
<div className={classes.spacer} />
{migrators?.map((address) => {
return (
<EvmMigrationLineItem
key={address}
chainId={chainId}
migratorAddress={address}
onLoadComplete={reportLoadComplete}
/>
);
})}
</div>
</>
)}
</div>
);
return (
<Container maxWidth="md">
<HeaderText
white
subtitle="Convert assets from other bridges to Wormhole V2 tokens"
>
Migrate Assets
</HeaderText>
<Paper className={classes.mainPaper}>{content}</Paper>
</Container>
);
}

View File

@ -1,246 +0,0 @@
import { ChainId } from "@certusone/wormhole-sdk";
import { CircularProgress, makeStyles, Typography } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { parseUnits } from "ethers/lib/utils";
import { useSnackbar } from "notistack";
import { useCallback, useState } from "react";
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
import useEthereumMigratorInformation from "../../hooks/useEthereumMigratorInformation";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import ButtonWithLoader from "../ButtonWithLoader";
import EthereumSignerKey from "../EthereumSignerKey";
import NumberTextField from "../NumberTextField";
import ShowTx from "../ShowTx";
import SmartAddress from "../SmartAddress";
const useStyles = makeStyles((theme) => ({
spacer: {
height: "2rem",
},
containerDiv: {
textAlign: "center",
padding: theme.spacing(2),
},
}));
export default function EvmWorkflow({
chainId,
migratorAddress,
}: {
chainId: ChainId;
migratorAddress: string;
}) {
const classes = useStyles();
const { enqueueSnackbar } = useSnackbar();
const { signer, signerAddress } = useEthereumProvider();
const { isReady } = useIsWalletReady(chainId);
const [toggleRefresh, setToggleRefresh] = useState(false);
const forceRefresh = useCallback(
() => setToggleRefresh((prevState) => !prevState),
[]
);
const poolInfo = useEthereumMigratorInformation(
migratorAddress,
signer,
signerAddress,
toggleRefresh
);
const fromWalletBalance = poolInfo.data?.fromWalletBalance;
const [migrationAmount, setMigrationAmount] = useState("");
const [migrationIsProcessing, setMigrationIsProcessing] = useState(false);
const [error, setError] = useState("");
const [transaction, setTransaction] = useState<string | null>(null);
const fromParse = (amount: string) => {
try {
if (!poolInfo.data?.fromDecimals || !migrationAmount) {
return BigInt(0);
}
return parseUnits(amount, poolInfo.data.fromDecimals).toBigInt();
} catch (e) {
return BigInt(0);
}
};
const hasRequisiteData = poolInfo.data;
const amountGreaterThanZero = fromParse(migrationAmount) > BigInt(0);
const sufficientFromTokens =
fromWalletBalance &&
migrationAmount &&
fromParse(migrationAmount) <= fromParse(fromWalletBalance);
const sufficientPoolBalance =
poolInfo.data?.toPoolBalance &&
migrationAmount &&
parseFloat(migrationAmount) <= parseFloat(poolInfo.data.toPoolBalance);
const isReadyToTransfer =
isReady &&
amountGreaterThanZero &&
sufficientFromTokens &&
sufficientPoolBalance &&
hasRequisiteData;
const getNotReadyCause = () => {
if (!isReady) {
return "Connect your wallet to proceed.";
} else if (poolInfo.error) {
return "Unable to retrieve necessary information. This asset may not be supported.";
} else if (!migrationAmount) {
return "Enter an amount to transfer.";
} else if (!amountGreaterThanZero) {
return "The transfer amount must be greater than zero.";
} else if (!sufficientFromTokens) {
return "There are not sufficient funds in your wallet for this transfer.";
} else if (!sufficientPoolBalance) {
return "There are not sufficient funds in the pool for this transfer.";
} else {
return "";
}
};
const handleAmountChange = useCallback(
(event) => setMigrationAmount(event.target.value),
[setMigrationAmount]
);
const handleMaxClick = useCallback(() => {
if (fromWalletBalance) {
setMigrationAmount(fromWalletBalance);
}
}, [fromWalletBalance]);
const migrateTokens = useCallback(async () => {
if (!poolInfo.data) {
enqueueSnackbar(null, {
content: <Alert severity="error">Could not migrate the tokens.</Alert>,
}); //Should never be hit
return;
}
try {
setMigrationIsProcessing(true);
setError("");
await poolInfo.data.fromToken.approve(
poolInfo.data.migrator.address,
parseUnits(migrationAmount, poolInfo.data.fromDecimals)
);
const transaction = await poolInfo.data.migrator.migrate(
parseUnits(migrationAmount, poolInfo.data.fromDecimals)
);
await transaction.wait();
setTransaction(transaction.hash);
forceRefresh();
enqueueSnackbar(null, {
content: (
<Alert severity="success">Successfully migrated the tokens.</Alert>
),
});
setMigrationIsProcessing(false);
} catch (e) {
console.error(e);
enqueueSnackbar(null, {
content: <Alert severity="error">Could not migrate the tokens.</Alert>,
});
setMigrationIsProcessing(false);
setError("Failed to send the transaction.");
}
}, [poolInfo.data, migrationAmount, enqueueSnackbar, forceRefresh]);
//TODO tokenName
const toTokenPretty = (
<SmartAddress
chainId={chainId}
address={poolInfo.data?.toAddress}
symbol={poolInfo.data?.toSymbol}
isAsset
/>
);
const fromTokenPretty = (
<SmartAddress
chainId={chainId}
address={poolInfo.data?.fromAddress}
symbol={poolInfo.data?.fromSymbol}
isAsset
/>
);
const poolPretty = (
<SmartAddress chainId={chainId} address={poolInfo.data?.poolAddress} />
);
const fatalError = poolInfo.error
? "Unable to retrieve necessary information. This asset may not be supported."
: null;
const explainerContent = (
<div>
<Typography>This action will convert</Typography>
<Typography variant="h6">
{fromTokenPretty} {`(Balance: ${fromWalletBalance || ""})`}
</Typography>
<div className={classes.spacer} />
<Typography>to</Typography>
<Typography variant="h6">
{toTokenPretty} {`(Balance: ${poolInfo.data?.toWalletBalance || ""})`}
</Typography>
<div className={classes.spacer} />
<Typography>Utilizing this pool</Typography>
<Typography variant="h6">
{poolPretty} {`(Balance: ${poolInfo.data?.toPoolBalance || ""})`}
</Typography>
</div>
);
const mainWorkflow = (
<>
{explainerContent}
<div className={classes.spacer} />
<NumberTextField
variant="outlined"
value={migrationAmount}
onChange={handleAmountChange}
label={"Amount"}
disabled={!!migrationIsProcessing || !!transaction}
onMaxClick={fromWalletBalance ? handleMaxClick : undefined}
/>
{!transaction && (
<ButtonWithLoader
disabled={!isReadyToTransfer || migrationIsProcessing}
showLoader={migrationIsProcessing}
onClick={migrateTokens}
>
{migrationAmount && isReadyToTransfer
? "Migrate " + migrationAmount + " Tokens"
: "Migrate"}
</ButtonWithLoader>
)}
{(error || !isReadyToTransfer) && (
<Typography color="error">{error || getNotReadyCause()}</Typography>
)}
{transaction ? (
<>
<Typography>
Successfully migrated your tokens! They will be available once this
transaction confirms.
</Typography>
<ShowTx tx={{ id: transaction, block: 1 }} chainId={chainId} />
</>
) : null}
</>
);
return (
<div className={classes.containerDiv}>
<EthereumSignerKey chainId={chainId} />
{!isReady ? (
<Typography variant="body1">Please connect your wallet.</Typography>
) : poolInfo.isLoading ? (
<CircularProgress />
) : fatalError ? (
<Typography variant="h6">{fatalError}</Typography>
) : (
mainWorkflow
)}
</div>
);
}

View File

@ -1,381 +0,0 @@
import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
import {
CircularProgress,
Container,
makeStyles,
Paper,
Typography,
} from "@material-ui/core";
import ArrowRightAltIcon from "@material-ui/icons/ArrowRightAlt";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import {
AccountInfo,
Connection,
ParsedAccountData,
PublicKey,
} from "@solana/web3.js";
import { useCallback, useEffect, useMemo, useState } from "react";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import useSolanaMigratorInformation from "../../hooks/useSolanaMigratorInformation";
import { COLORS } from "../../muiTheme";
import {
CHAINS_BY_ID,
getMigrationAssetMap,
SOLANA_HOST,
} from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader";
import HeaderText from "../HeaderText";
import ShowTx from "../ShowTx";
import SmartAddress from "../SmartAddress";
import SolanaCreateAssociatedAddress from "../SolanaCreateAssociatedAddress";
import SolanaWalletKey from "../SolanaWalletKey";
const useStyles = makeStyles((theme) => ({
spacer: {
height: "2rem",
},
containerDiv: {
textAlign: "center",
padding: theme.spacing(2),
},
centered: {
textAlign: "center",
},
lineItem: {
display: "flex",
flexWrap: "nowrap",
justifyContent: "space-between",
"& > *": {
alignSelf: "flex-start",
width: "max-content",
},
},
flexGrow: {
flewGrow: 1,
},
mainPaper: {
backgroundColor: COLORS.whiteWithTransparency,
textAlign: "center",
padding: "2rem",
"& > h, p ": {
margin: ".5rem",
},
},
hidden: {
display: "none",
},
divider: {
margin: "2rem 0rem 2rem 0rem",
},
balance: {
display: "inline-block",
},
convertButton: {
alignSelf: "flex-end",
},
}));
function SolanaMigrationLineItem({
migratorInfo,
onLoadComplete,
}: {
migratorInfo: DefaultAssociatedTokenAccountInfo;
onLoadComplete: () => void;
}) {
const classes = useStyles();
const poolInfo = useSolanaMigratorInformation(
migratorInfo.fromMintKey,
migratorInfo.toMintKey,
migratorInfo.defaultFromTokenAccount
);
const [migrationIsProcessing, setMigrationIsProcessing] = useState(false);
const [transaction, setTransaction] = useState("");
const [migrationError, setMigrationError] = useState("");
const handleMigrateClick = useCallback(() => {
if (!poolInfo.data) {
return;
}
setMigrationIsProcessing(true);
setMigrationError("");
poolInfo.data
.migrateTokens(poolInfo.data.fromAssociatedTokenAccountBalance)
.then((result) => {
setMigrationIsProcessing(false);
setTransaction(result);
})
.catch((e) => {
setMigrationError("Unable to perform migration.");
setMigrationIsProcessing(false);
});
}, [poolInfo.data]);
const precheckError =
poolInfo.data &&
poolInfo.data.getNotReadyCause(
poolInfo.data.fromAssociatedTokenAccountBalance
);
useEffect(() => {
if (poolInfo.data || poolInfo.error) {
onLoadComplete();
}
}, [poolInfo, onLoadComplete]);
if (!poolInfo.data) {
return (
<div className={classes.centered}>
<div>
<Typography variant="body2" color="textSecondary">
Failed to load migration information for token
</Typography>
<SmartAddress
chainId={CHAIN_ID_SOLANA}
address={migratorInfo.fromMintKey}
isAsset
/>
</div>
</div>
);
} else if (transaction) {
return (
<div className={classes.centered}>
<div>
<Typography variant="body2" color="textSecondary">
Successfully migrated your tokens. They will become available once
this transaction confirms.
</Typography>
<ShowTx
chainId={CHAIN_ID_SOLANA}
tx={{ id: transaction, block: 1 }}
/>
</div>
</div>
);
} else {
return (
<div className={classes.lineItem}>
<div>
<Typography variant="body2" color="textSecondary">
Current Token
</Typography>
<Typography className={classes.balance}>
{poolInfo.data.fromAssociatedTokenAccountBalance}
</Typography>
<SmartAddress
chainId={CHAIN_ID_SOLANA}
address={poolInfo.data.fromAssociatedTokenAccount}
symbol={poolInfo.data.fromSymbol || undefined}
tokenName={poolInfo.data.fromName || undefined}
/>
</div>
<div>
<Typography variant="body2" color="textSecondary">
will become
</Typography>
<ArrowRightAltIcon fontSize="large" />
</div>
<div>
<Typography variant="body2" color="textSecondary">
Wormhole Token
</Typography>
<Typography className={classes.balance}>
{poolInfo.data.fromAssociatedTokenAccountBalance}
</Typography>
<SmartAddress
chainId={CHAIN_ID_SOLANA}
address={poolInfo.data.toAssociatedTokenAccount}
symbol={poolInfo.data.toSymbol || undefined}
tokenName={poolInfo.data.toName || undefined}
/>
</div>
{!poolInfo.data.toAssociatedTokenAccountExists ? (
<div className={classes.convertButton}>
<SolanaCreateAssociatedAddress
mintAddress={migratorInfo.toMintKey}
readableTargetAddress={poolInfo.data?.toAssociatedTokenAccount}
associatedAccountExists={
poolInfo.data.toAssociatedTokenAccountExists
}
setAssociatedAccountExists={poolInfo.data.setToTokenAccountExists}
/>
</div>
) : (
<div className={classes.convertButton}>
<ButtonWithLoader
showLoader={migrationIsProcessing}
onClick={handleMigrateClick}
error={
poolInfo.error
? poolInfo.error
: migrationError
? migrationError
: precheckError
? precheckError
: ""
}
disabled={
!!poolInfo.error || !!precheckError || migrationIsProcessing
}
>
Convert
</ButtonWithLoader>
</div>
)}
</div>
);
}
}
type DefaultAssociatedTokenAccountInfo = {
fromMintKey: string;
toMintKey: string;
defaultFromTokenAccount: string;
fromAccountInfo: AccountInfo<ParsedAccountData> | null;
};
const getTokenBalances = async (
walletAddress: string,
migrationMap: Map<string, string>
): Promise<DefaultAssociatedTokenAccountInfo[]> => {
try {
const connection = new Connection(SOLANA_HOST);
const output: DefaultAssociatedTokenAccountInfo[] = [];
const tokenAccounts = await connection.getParsedTokenAccountsByOwner(
new PublicKey(walletAddress),
{ programId: TOKEN_PROGRAM_ID },
"confirmed"
);
tokenAccounts.value.forEach((item) => {
if (
item.account != null &&
item.account.data?.parsed?.info?.tokenAmount?.uiAmountString &&
item.account.data?.parsed.info?.tokenAmount?.amount !== "0"
) {
const fromMintKey = item.account.data.parsed.info.mint;
const toMintKey = migrationMap.get(fromMintKey);
if (toMintKey) {
output.push({
fromMintKey,
toMintKey: toMintKey,
defaultFromTokenAccount: item.pubkey.toString(),
fromAccountInfo: item.account,
});
}
}
});
return output;
} catch (e) {
console.error(e);
return Promise.reject("Unable to retrieve token balances.");
}
};
export default function SolanaQuickMigrate() {
const chainId = CHAIN_ID_SOLANA;
const classes = useStyles();
const { isReady, walletAddress } = useIsWalletReady(chainId);
const migrationMap = useMemo(() => getMigrationAssetMap(chainId), [chainId]);
const [migrators, setMigrators] = useState<
DefaultAssociatedTokenAccountInfo[] | null
>(null);
const [migratorsError, setMigratorsError] = useState("");
const [migratorsLoading, setMigratorsLoading] = useState(false);
//This is for a callback into the line items, so a loader can be displayed while
//they are loading
//TODO don't just swallow loading errors.
const [migratorsFinishedLoading, setMigratorsFinishedLoading] = useState(0);
const reportLoadComplete = useCallback(() => {
setMigratorsFinishedLoading((prevState) => prevState + 1);
}, []);
const isLoading =
migratorsLoading ||
(migrators &&
migrators.length &&
migratorsFinishedLoading < migrators.length);
useEffect(() => {
if (isReady && walletAddress) {
let cancelled = false;
setMigratorsLoading(true);
setMigratorsError("");
getTokenBalances(walletAddress, migrationMap).then(
(result) => {
if (!cancelled) {
setMigratorsFinishedLoading(0);
setMigrators(result.filter((x) => x.fromAccountInfo && x));
setMigratorsLoading(false);
}
},
(error) => {
if (!cancelled) {
setMigratorsLoading(false);
setMigratorsError(
"Failed to retrieve available token information."
);
}
}
);
return () => {
cancelled = true;
};
}
}, [isReady, walletAddress, migrationMap]);
const hasEligibleAssets = migrators && migrators.length > 0;
const chainName = CHAINS_BY_ID[chainId]?.name;
const content = (
<div className={classes.containerDiv}>
<Typography variant="h5">
{`This page allows you to convert certain wrapped tokens ${
chainName ? "on " + chainName : ""
} into
Wormhole V2 tokens.`}
</Typography>
<SolanaWalletKey />
{!isReady ? (
<Typography variant="body1">Please connect your wallet.</Typography>
) : migratorsError ? (
<Typography variant="h6">{migratorsError}</Typography>
) : (
<>
<div className={classes.spacer} />
<CircularProgress className={isLoading ? "" : classes.hidden} />
<div className={!isLoading ? "" : classes.hidden}>
<Typography>
{hasEligibleAssets
? "You have some assets that are eligible for migration! Click the 'Convert' button to swap them for Wormhole tokens."
: "You don't have any assets eligible for migration."}
</Typography>
<div className={classes.spacer} />
{migrators?.map((info) => {
return (
<SolanaMigrationLineItem
migratorInfo={info}
onLoadComplete={reportLoadComplete}
/>
);
})}
</div>
</>
)}
</div>
);
return (
<Container maxWidth="md">
<HeaderText
white
subtitle="Convert assets from other bridges to Wormhole V2 tokens"
>
Migrate Assets
</HeaderText>
<Paper className={classes.mainPaper}>{content}</Paper>
</Container>
);
}

View File

@ -1,507 +0,0 @@
import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
import migrateTokensTx from "@certusone/wormhole-sdk/lib/esm/migration/migrateTokens";
import getPoolAddress from "@certusone/wormhole-sdk/lib/esm/migration/poolAddress";
import getToCustodyAddress from "@certusone/wormhole-sdk/lib/esm/migration/toCustodyAddress";
import { makeStyles, Typography } from "@material-ui/core";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
Token,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { Connection, PublicKey } from "@solana/web3.js";
import { parseUnits } from "ethers/lib/utils";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import useMetaplexData from "../../hooks/useMetaplexData";
import useSolanaTokenMap from "../../hooks/useSolanaTokenMap";
import { COLORS } from "../../muiTheme";
import { MIGRATION_PROGRAM_ADDRESS, SOLANA_HOST } from "../../utils/consts";
import { getMultipleAccounts, signSendAndConfirm } from "../../utils/solana";
import ButtonWithLoader from "../ButtonWithLoader";
import NumberTextField from "../NumberTextField";
import ShowTx from "../ShowTx";
import SmartAddress from "../SmartAddress";
import SolanaCreateAssociatedAddress, {
useAssociatedAccountExistsState,
} from "../SolanaCreateAssociatedAddress";
import SolanaWalletKey from "../SolanaWalletKey";
const useStyles = makeStyles(() => ({
mainPaper: {
backgroundColor: COLORS.whiteWithTransparency,
textAlign: "center",
padding: "2rem",
"& > h, p ": {
margin: ".5rem",
},
},
divider: {
margin: "2rem 0rem 2rem 0rem",
},
spacer: {
height: "2rem",
},
}));
//TODO move to utils/solana
const getDecimals = async (
connection: Connection,
mint: string,
setter: (decimals: number | undefined) => void
) => {
setter(undefined);
if (mint) {
try {
const pk = new PublicKey(mint);
const info = await connection.getParsedAccountInfo(pk);
// @ts-ignore
const decimals = info.value?.data.parsed.info.decimals;
setter(decimals);
} catch (e) {
console.log(`Unable to determine decimals of ${mint}`);
}
}
};
//TODO move to utils/solana
const getBalance = async (
connection: Connection,
address: string | undefined,
setter: (balance: string | undefined) => void
) => {
setter(undefined);
if (address) {
try {
const pk = new PublicKey(address);
const info = await connection.getParsedAccountInfo(pk);
// @ts-ignore
const balance = info.value?.data.parsed.info.tokenAmount.uiAmountString;
setter(balance);
} catch (e) {
console.log(`Unable to determine balance of ${address}`);
}
}
};
export default function Workflow({
fromMint,
toMint,
fromTokenAccount,
}: {
fromMint: string;
toMint: string;
fromTokenAccount: string;
}) {
const classes = useStyles();
const connection = useMemo(
() => new Connection(SOLANA_HOST, "confirmed"),
[]
);
const wallet = useSolanaWallet();
const { isReady } = useIsWalletReady(CHAIN_ID_SOLANA);
const solanaTokenMap = useSolanaTokenMap();
const metaplexArray = useMemo(() => [fromMint, toMint], [fromMint, toMint]);
const metaplexData = useMetaplexData(metaplexArray);
const [poolAddress, setPoolAddress] = useState("");
const [poolExists, setPoolExists] = useState<boolean | undefined>(undefined);
const [fromTokenAccountBalance, setFromTokenAccountBalance] = useState<
string | undefined
>(undefined);
const [toTokenAccount, setToTokenAccount] = useState<string | undefined>(
undefined
);
const [toTokenAccountBalance, setToTokenAccountBalance] = useState<
string | undefined
>(undefined);
const [fromMintDecimals, setFromMintDecimals] = useState<number | undefined>(
undefined
);
const {
associatedAccountExists: fromTokenAccountExists,
//setAssociatedAccountExists: setFromTokenAccountExists,
} = useAssociatedAccountExistsState(
CHAIN_ID_SOLANA,
fromMint,
fromTokenAccount
);
const {
associatedAccountExists: toTokenAccountExists,
setAssociatedAccountExists: setToTokenAccountExists,
} = useAssociatedAccountExistsState(CHAIN_ID_SOLANA, toMint, toTokenAccount);
const [toCustodyAddress, setToCustodyAddress] = useState<string | undefined>(
undefined
);
const [toCustodyBalance, setToCustodyBalance] = useState<string | undefined>(
undefined
);
const [migrationAmount, setMigrationAmount] = useState("");
const [migrationIsProcessing, setMigrationIsProcessing] = useState(false);
const [error, setError] = useState("");
const [transaction, setTransaction] = useState<string | null>(null);
/* Effects
*/
useEffect(() => {
getDecimals(connection, fromMint, setFromMintDecimals);
}, [connection, fromMint]);
//Retrieve user balance when fromTokenAccount changes
useEffect(() => {
// TODO: cancellable
if (fromTokenAccount && fromTokenAccountExists) {
getBalance(connection, fromTokenAccount, setFromTokenAccountBalance);
} else {
setFromTokenAccountBalance(undefined);
}
}, [
connection,
fromTokenAccountExists,
fromTokenAccount,
setFromTokenAccountBalance,
]);
useEffect(() => {
// TODO: cancellable
if (toTokenAccount && toTokenAccountExists) {
getBalance(connection, toTokenAccount, setToTokenAccountBalance);
} else {
setToTokenAccountBalance(undefined);
}
}, [
connection,
toTokenAccountExists,
toTokenAccount,
setFromTokenAccountBalance,
]);
useEffect(() => {
// TODO: cancellable
if (toCustodyAddress) {
getBalance(connection, toCustodyAddress, setToCustodyBalance);
} else {
setToCustodyBalance(undefined);
}
}, [connection, toCustodyAddress, setToCustodyBalance]);
//Retrieve pool address on selectedTokens change
useEffect(() => {
if (toMint && fromMint) {
setPoolAddress("");
setPoolExists(undefined);
getPoolAddress(MIGRATION_PROGRAM_ADDRESS, fromMint, toMint).then(
(result) => {
const key = new PublicKey(result).toString();
setPoolAddress(key);
},
(error) => console.log("Could not calculate pool address.")
);
}
}, [toMint, fromMint, setPoolAddress]);
//Retrieve the poolAccount every time the pool address changes.
useEffect(() => {
if (poolAddress) {
setPoolExists(undefined);
try {
getMultipleAccounts(
connection,
[new PublicKey(poolAddress)],
"confirmed"
).then((result) => {
if (result.length && result[0] !== null) {
setPoolExists(true);
} else if (result.length && result[0] === null) {
setPoolExists(false);
setError("There is no swap pool for this token.");
} else {
setError(
"unexpected error in fetching pool address. Please reload and try again"
);
}
});
} catch (e) {
setError("Could not fetch pool address");
}
}
}, [connection, poolAddress]);
//Set relevant information derived from poolAddress
useEffect(() => {
if (poolAddress) {
getToCustodyAddress(MIGRATION_PROGRAM_ADDRESS, poolAddress)
.then((result: any) =>
setToCustodyAddress(new PublicKey(result).toString())
)
.catch((e) => {
setToCustodyAddress(undefined);
});
} else {
setToCustodyAddress(undefined);
}
}, [poolAddress]);
useEffect(() => {
if (wallet?.publicKey && toMint) {
Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
new PublicKey(toMint),
wallet?.publicKey || new PublicKey([])
).then(
(result) => {
setToTokenAccount(result.toString());
},
(error) => {}
);
}
}, [toMint, wallet?.publicKey]);
/*
End effects
*/
const migrateTokens = useCallback(async () => {
try {
setError("");
const instruction = await migrateTokensTx(
connection,
wallet?.publicKey?.toString() || "",
MIGRATION_PROGRAM_ADDRESS,
fromMint,
toMint,
fromTokenAccount || "",
toTokenAccount || "",
parseUnits(migrationAmount, fromMintDecimals).toBigInt()
);
setMigrationIsProcessing(true);
signSendAndConfirm(wallet, connection, instruction).then(
(transaction: any) => {
setMigrationIsProcessing(false);
setTransaction(transaction);
},
(error) => {
console.log(error);
setError("Could not complete the migrateTokens transaction.");
setMigrationIsProcessing(false);
}
);
} catch (e) {
console.log(e);
setError("Could not complete the migrateTokens transaction.");
setMigrationIsProcessing(false);
}
}, [
connection,
fromMint,
fromTokenAccount,
migrationAmount,
toMint,
toTokenAccount,
wallet,
fromMintDecimals,
]);
const fromParse = (amount: string) => {
try {
return parseUnits(amount, fromMintDecimals).toBigInt();
} catch (e) {
return BigInt(0);
}
};
const hasRequisiteData = fromMint && toMint && poolAddress && poolExists;
const accountsReady =
fromTokenAccountExists && toTokenAccountExists && poolExists;
const amountGreaterThanZero = fromParse(migrationAmount) > BigInt(0);
const sufficientFromTokens =
fromTokenAccountBalance &&
migrationAmount &&
fromParse(migrationAmount) <= fromParse(fromTokenAccountBalance);
const sufficientPoolBalance =
toCustodyBalance &&
migrationAmount &&
parseFloat(migrationAmount) <= parseFloat(toCustodyBalance);
const isReadyToTransfer =
isReady &&
amountGreaterThanZero &&
sufficientFromTokens &&
sufficientPoolBalance &&
accountsReady &&
hasRequisiteData;
const getNotReadyCause = () => {
if (!fromMint || !toMint || !poolAddress || !poolExists) {
return "This asset is not supported.";
} else if (!isReady) {
return "Wallet is not connected.";
} else if (!toTokenAccountExists || !fromTokenAccountExists) {
return "You have not created the necessary token accounts.";
} else if (!migrationAmount) {
return "Enter an amount to transfer.";
} else if (!amountGreaterThanZero) {
return "Enter an amount greater than zero.";
} else if (!sufficientFromTokens) {
return "There are not sufficient funds in your wallet for this transfer.";
} else if (!sufficientPoolBalance) {
return "There are not sufficient funds in the pool for this transfer.";
} else {
return "";
}
};
const handleAmountChange = useCallback(
(event) => setMigrationAmount(event.target.value),
[setMigrationAmount]
);
const handleMaxClick = useCallback(() => {
if (fromTokenAccountBalance) {
setMigrationAmount(fromTokenAccountBalance);
}
}, [fromTokenAccountBalance]);
const getMetadata = (address: string) => {
const tokenMapItem = solanaTokenMap.data?.find(
(x) => x.address === address
);
const metaplexItem = metaplexData.data?.get(address);
return {
symbol: tokenMapItem?.symbol || metaplexItem?.data?.symbol || undefined,
name: tokenMapItem?.name || metaplexItem?.data?.name || undefined,
logo: tokenMapItem?.logoURI || metaplexItem?.data?.uri || undefined,
};
};
const toMetadata = getMetadata(toMint);
const fromMetadata = getMetadata(fromMint);
const toMintPretty = (
<SmartAddress
chainId={CHAIN_ID_SOLANA}
address={toMint}
symbol={toMetadata?.symbol}
tokenName={toMetadata?.name}
isAsset
/>
);
const fromMintPretty = (
<SmartAddress
chainId={CHAIN_ID_SOLANA}
address={fromMint}
symbol={fromMetadata?.symbol}
tokenName={fromMetadata?.name}
isAsset
/>
);
return (
<div>
<SolanaWalletKey />
<div className={classes.spacer} />
{fromTokenAccount && toTokenAccount ? (
<>
<Typography variant="body2" component="div">
<span>This will migrate</span>
{fromMintPretty}
<span>tokens in this account:</span>
</Typography>
<Typography variant="h5">
<SmartAddress
address={fromTokenAccount}
chainId={CHAIN_ID_SOLANA}
/>
{`(Balance: ${fromTokenAccountBalance}${
fromMetadata.symbol && " " + fromMetadata.symbol
})`}
</Typography>
<div className={classes.spacer} />
<Typography variant="body2" component="div">
<span>into </span>
{toMintPretty}
<span> tokens in this account:</span>
</Typography>
<Typography
variant="h5"
color={toTokenAccountExists ? "textPrimary" : "textSecondary"}
>
<SmartAddress address={toTokenAccount} chainId={CHAIN_ID_SOLANA} />
<span>
{toTokenAccountExists
? ` (Balance: ${toTokenAccountBalance}${
(toMetadata.symbol && " " + toMetadata.symbol) || ""
})`
: " (Not created yet)"}
</span>
</Typography>
<SolanaCreateAssociatedAddress
mintAddress={toMint}
readableTargetAddress={toTokenAccount}
associatedAccountExists={toTokenAccountExists}
setAssociatedAccountExists={setToTokenAccountExists}
/>
{poolAddress && toCustodyAddress && toCustodyBalance ? (
<>
<div className={classes.spacer} />
<Typography variant="body2" component="div">
<span>Using pool </span>
<SmartAddress address={poolAddress} chainId={CHAIN_ID_SOLANA} />
<span> holding tokens in this account:</span>
</Typography>
<Typography variant="h5">
<SmartAddress
address={toCustodyAddress}
chainId={CHAIN_ID_SOLANA}
/>
<span>{` (Balance: ${toCustodyBalance}${
toMetadata.symbol && " " + toMetadata.symbol
})`}</span>
</Typography>
</>
) : null}
</>
) : null}
<div className={classes.spacer} />
<NumberTextField
variant="outlined"
value={migrationAmount}
onChange={handleAmountChange}
label={"Amount"}
disabled={!!migrationIsProcessing || !!transaction}
onMaxClick={fromTokenAccountBalance ? handleMaxClick : undefined}
/>
{!transaction && (
<ButtonWithLoader
disabled={!isReadyToTransfer || migrationIsProcessing}
showLoader={migrationIsProcessing}
onClick={migrateTokens}
>
{migrationAmount && isReadyToTransfer
? "Migrate " + migrationAmount + " Tokens"
: "Migrate"}
</ButtonWithLoader>
)}
{(error || !isReadyToTransfer) && (
<Typography color="error">{error || getNotReadyCause()}</Typography>
)}
{transaction ? (
<>
<Typography>
Successfully migrated your tokens! They will be available once this
transaction confirms.
</Typography>
<ShowTx
tx={{ id: transaction, block: 1 }}
chainId={CHAIN_ID_SOLANA}
/>
</>
) : null}
</div>
);
}

View File

@ -1,130 +0,0 @@
import {
ChainId,
CHAIN_ID_BSC,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
} from "@certusone/wormhole-sdk";
import { getAddress } from "@ethersproject/address";
import { Container, makeStyles, Paper, Typography } from "@material-ui/core";
import { PublicKey } from "@solana/web3.js";
import { withRouter } from "react-router";
import { RouteComponentProps } from "react-router-dom";
import { COLORS } from "../../muiTheme";
import { getMigrationAssetMap, MIGRATION_ASSET_MAP } from "../../utils/consts";
import HeaderText from "../HeaderText";
import EvmWorkflow from "./EvmWorkflow";
import SolanaWorkflow from "./SolanaWorkflow";
const useStyles = makeStyles(() => ({
mainPaper: {
backgroundColor: COLORS.whiteWithTransparency,
textAlign: "center",
padding: "2rem",
"& > h, p ": {
margin: ".5rem",
},
},
divider: {
margin: "2rem 0rem 2rem 0rem",
},
spacer: {
height: "2rem",
},
}));
interface RouteParams {
legacyAsset: string;
fromTokenAccount: string;
}
interface Migration extends RouteComponentProps<RouteParams> {
chainId: ChainId;
}
const SolanaRoot: React.FC<Migration> = (props) => {
const legacyAsset: string = props.match.params.legacyAsset;
const fromTokenAccount: string = props.match.params.fromTokenAccount;
const targetAsset: string | undefined = MIGRATION_ASSET_MAP.get(legacyAsset);
let fromMint: string | undefined = "";
let toMint: string | undefined = "";
let fromTokenAcct: string | undefined = "";
try {
fromMint = legacyAsset && new PublicKey(legacyAsset).toString();
toMint = targetAsset && new PublicKey(targetAsset).toString();
fromTokenAcct =
fromTokenAccount && new PublicKey(fromTokenAccount).toString();
} catch (e) {}
let content = null;
if (!fromMint || !toMint) {
content = (
<Typography style={{ textAlign: "center" }}>
This asset is not eligible for migration.
</Typography>
);
} else if (!fromTokenAcct) {
content = (
<Typography style={{ textAlign: "center" }}>
Invalid token account.
</Typography>
);
} else {
content = (
<SolanaWorkflow
fromMint={fromMint}
toMint={toMint}
fromTokenAccount={fromTokenAcct}
/>
);
}
return content;
};
const EthereumRoot: React.FC<Migration> = (props) => {
const legacyAsset: string = props.match.params.legacyAsset;
const assetMap = getMigrationAssetMap(props.chainId);
const targetPool = assetMap.get(getAddress(legacyAsset));
let content = null;
if (!legacyAsset || !targetPool) {
content = (
<Typography style={{ textAlign: "center" }}>
This asset is not eligible for migration.
</Typography>
);
} else {
content = (
<EvmWorkflow migratorAddress={targetPool} chainId={props.chainId} />
);
}
return content;
};
const MigrationRoot: React.FC<Migration> = (props) => {
const classes = useStyles();
let content = null;
if (props.chainId === CHAIN_ID_SOLANA) {
content = <SolanaRoot {...props} />;
} else if (props.chainId === CHAIN_ID_ETH || props.chainId === CHAIN_ID_BSC) {
content = <EthereumRoot {...props} />;
}
return (
<Container maxWidth="md">
<HeaderText
white
subtitle="Convert assets from other bridges to Wormhole V2 tokens"
>
Migrate Assets
</HeaderText>
<Paper className={classes.mainPaper}>{content}</Paper>
</Container>
);
};
export default withRouter(MigrationRoot);

View File

@ -1,44 +0,0 @@
import {
CHAIN_ID_SOLANA,
isTerraChain,
} from "@certusone/wormhole-sdk";
import { useSelector } from "react-redux";
import { useHandleNFTRedeem } from "../../hooks/useHandleNFTRedeem";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import { selectNFTTargetChain } from "../../store/selectors";
import { CLUSTER } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
import SolanaTPSWarning from "../SolanaTPSWarning";
import StepDescription from "../StepDescription";
import TerraFeeDenomPicker from "../TerraFeeDenomPicker";
import WaitingForWalletMessage from "./WaitingForWalletMessage";
function Redeem() {
const { handleClick, disabled, showLoader } = useHandleNFTRedeem();
const targetChain = useSelector(selectNFTTargetChain);
const { isReady, statusMessage } = useIsWalletReady(targetChain);
return (
<>
<StepDescription>Receive the NFT on the target chain</StepDescription>
<KeyAndBalance chainId={targetChain} />
{isTerraChain(targetChain) && (
<TerraFeeDenomPicker disabled={disabled} chainId={targetChain} />
)}
{targetChain === CHAIN_ID_SOLANA && CLUSTER === "mainnet" && (
<SolanaTPSWarning />
)}
<ButtonWithLoader
disabled={!isReady || disabled}
onClick={handleClick}
showLoader={showLoader}
error={statusMessage}
>
Redeem
</ButtonWithLoader>
<WaitingForWalletMessage />
</>
);
}
export default Redeem;

View File

@ -1,42 +0,0 @@
import { makeStyles, Typography } from "@material-ui/core";
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { selectNFTRedeemTx, selectNFTTargetChain } from "../../store/selectors";
import { reset } from "../../store/nftSlice";
import ButtonWithLoader from "../ButtonWithLoader";
import ShowTx from "../ShowTx";
const useStyles = makeStyles((theme) => ({
description: {
textAlign: "center",
},
}));
export default function RedeemPreview() {
const classes = useStyles();
const dispatch = useDispatch();
const targetChain = useSelector(selectNFTTargetChain);
const redeemTx = useSelector(selectNFTRedeemTx);
const handleResetClick = useCallback(() => {
dispatch(reset());
}, [dispatch]);
const explainerString =
"Success! The redeem transaction was submitted. The NFT will become available once the transaction confirms.";
return (
<>
<Typography
component="div"
variant="subtitle2"
className={classes.description}
>
{explainerString}
</Typography>
{redeemTx ? <ShowTx chainId={targetChain} tx={redeemTx} /> : null}
<ButtonWithLoader onClick={handleResetClick}>
Transfer Another NFT!
</ButtonWithLoader>
</>
);
}

View File

@ -1,81 +0,0 @@
import {
CHAIN_ID_SOLANA,
isTerraChain,
} from "@certusone/wormhole-sdk";
import { Alert } from "@material-ui/lab";
import { useSelector } from "react-redux";
import { useHandleNFTTransfer } from "../../hooks/useHandleNFTTransfer";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import {
selectNFTSourceWalletAddress,
selectNFTSourceChain,
selectNFTTargetError,
selectNFTTransferTx,
selectNFTIsSendComplete,
} from "../../store/selectors";
import { CHAINS_BY_ID, CLUSTER } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
import ShowTx from "../ShowTx";
import SolanaTPSWarning from "../SolanaTPSWarning";
import StepDescription from "../StepDescription";
import TerraFeeDenomPicker from "../TerraFeeDenomPicker";
import TransactionProgress from "../TransactionProgress";
import WaitingForWalletMessage from "./WaitingForWalletMessage";
function Send() {
const { handleClick, disabled, showLoader } = useHandleNFTTransfer();
const sourceChain = useSelector(selectNFTSourceChain);
const error = useSelector(selectNFTTargetError);
const { isReady, statusMessage, walletAddress } =
useIsWalletReady(sourceChain);
const sourceWalletAddress = useSelector(selectNFTSourceWalletAddress);
const transferTx = useSelector(selectNFTTransferTx);
const isSendComplete = useSelector(selectNFTIsSendComplete);
//The chain ID compare is handled implicitly, as the isWalletReady hook should report !isReady if the wallet is on the wrong chain.
const isWrongWallet =
sourceWalletAddress &&
walletAddress &&
sourceWalletAddress !== walletAddress;
const isDisabled = !isReady || isWrongWallet || disabled;
const errorMessage = isWrongWallet
? "A different wallet is connected than in Step 1."
: statusMessage || error || undefined;
return (
<>
<StepDescription>
Transfer the NFT to the Wormhole Token Bridge.
</StepDescription>
<KeyAndBalance chainId={sourceChain} />
{isTerraChain(sourceChain) && (
<TerraFeeDenomPicker disabled={disabled} chainId={sourceChain} />
)}
<Alert severity="info" variant="outlined">
This will initiate the transfer on {CHAINS_BY_ID[sourceChain].name} and
wait for finalization. If you navigate away from this page before
completing Step 4, you will have to perform the recovery workflow to
complete the transfer.
</Alert>
{sourceChain === CHAIN_ID_SOLANA && CLUSTER === "mainnet" && (
<SolanaTPSWarning />
)}
<ButtonWithLoader
disabled={isDisabled}
onClick={handleClick}
showLoader={showLoader}
error={errorMessage}
>
Transfer
</ButtonWithLoader>
<WaitingForWalletMessage />
{transferTx ? <ShowTx chainId={sourceChain} tx={transferTx} /> : null}
<TransactionProgress
chainId={sourceChain}
tx={transferTx}
isSendComplete={isSendComplete}
/>
</>
);
}
export default Send;

View File

@ -1,41 +0,0 @@
import { makeStyles, Typography } from "@material-ui/core";
import { useSelector } from "react-redux";
import {
selectNFTSourceChain,
selectNFTTransferTx,
} from "../../store/selectors";
import ShowTx from "../ShowTx";
const useStyles = makeStyles((theme) => ({
description: {
textAlign: "center",
},
tx: {
marginTop: theme.spacing(1),
textAlign: "center",
},
viewButton: {
marginTop: theme.spacing(1),
},
}));
export default function SendPreview() {
const classes = useStyles();
const sourceChain = useSelector(selectNFTSourceChain);
const transferTx = useSelector(selectNFTTransferTx);
const explainerString = "The NFT has been sent!";
return (
<>
<Typography
component="div"
variant="subtitle2"
className={classes.description}
>
{explainerString}
</Typography>
{transferTx ? <ShowTx chainId={sourceChain} tx={transferTx} /> : null}
</>
);
}

View File

@ -1,119 +0,0 @@
import { CHAIN_ID_SOLANA, isEVMChain } from "@certusone/wormhole-sdk";
import { Button, makeStyles } from "@material-ui/core";
import { VerifiedUser } from "@material-ui/icons";
import { Alert } from "@material-ui/lab";
import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link } from "react-router-dom";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import { incrementStep, setSourceChain } from "../../store/nftSlice";
import {
selectNFTIsSourceComplete,
selectNFTShouldLockFields,
selectNFTSourceBalanceString,
selectNFTSourceChain,
selectNFTSourceError,
} from "../../store/selectors";
import {
CHAINS_WITH_NFT_SUPPORT,
CLUSTER,
getIsTransferDisabled,
} from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader";
import ChainSelect from "../ChainSelect";
import KeyAndBalance from "../KeyAndBalance";
import LowBalanceWarning from "../LowBalanceWarning";
import SolanaTPSWarning from "../SolanaTPSWarning";
import StepDescription from "../StepDescription";
import { TokenSelector } from "../TokenSelectors/SourceTokenSelector";
import ChainWarningMessage from "../ChainWarningMessage";
const useStyles = makeStyles((theme) => ({
transferField: {
marginTop: theme.spacing(5),
},
}));
function Source() {
const classes = useStyles();
const dispatch = useDispatch();
const sourceChain = useSelector(selectNFTSourceChain);
const uiAmountString = useSelector(selectNFTSourceBalanceString);
const error = useSelector(selectNFTSourceError);
const isSourceComplete = useSelector(selectNFTIsSourceComplete);
const shouldLockFields = useSelector(selectNFTShouldLockFields);
const { isReady, statusMessage } = useIsWalletReady(sourceChain);
const handleSourceChange = useCallback(
(event) => {
dispatch(setSourceChain(event.target.value));
},
[dispatch]
);
const handleNextClick = useCallback(() => {
dispatch(incrementStep());
}, [dispatch]);
const isTransferDisabled = useMemo(() => {
return getIsTransferDisabled(sourceChain, true);
}, [sourceChain]);
return (
<>
<StepDescription>
<div style={{ display: "flex", alignItems: "center" }}>
Select an NFT to send through the Wormhole NFT Bridge.
<div style={{ flexGrow: 1 }} />
<div>
<Button
component={Link}
to="/nft-origin-verifier"
size="small"
variant="outlined"
startIcon={<VerifiedUser />}
>
NFT Origin Verifier
</Button>
</div>
</div>
</StepDescription>
<ChainSelect
variant="outlined"
select
fullWidth
value={sourceChain}
onChange={handleSourceChange}
disabled={shouldLockFields}
chains={CHAINS_WITH_NFT_SUPPORT}
/>
{isEVMChain(sourceChain) ? (
<Alert severity="info" variant="outlined">
Only NFTs which implement ERC-721 are supported.
</Alert>
) : null}
{sourceChain === CHAIN_ID_SOLANA ? (
<Alert severity="info" variant="outlined">
Only NFTs with a supply of 1 are supported.
</Alert>
) : null}
<KeyAndBalance chainId={sourceChain} />
{isReady || uiAmountString ? (
<div className={classes.transferField}>
<TokenSelector disabled={shouldLockFields} nft={true} />
</div>
) : null}
<LowBalanceWarning chainId={sourceChain} />
{sourceChain === CHAIN_ID_SOLANA && CLUSTER === "mainnet" && (
<SolanaTPSWarning />
)}
<ChainWarningMessage chainId={sourceChain} />
<ButtonWithLoader
disabled={!isSourceComplete || isTransferDisabled}
onClick={handleNextClick}
showLoader={false}
error={statusMessage || error}
>
Next
</ButtonWithLoader>
</>
);
}
export default Source;

View File

@ -1,57 +0,0 @@
import { makeStyles, Typography } from "@material-ui/core";
import { useSelector } from "react-redux";
import {
selectNFTSourceChain,
selectNFTSourceParsedTokenAccount,
} from "../../store/selectors";
import { CHAINS_BY_ID } from "../../utils/consts";
import SmartAddress from "../SmartAddress";
import NFTViewer from "../TokenSelectors/NFTViewer";
const useStyles = makeStyles((theme) => ({
description: {
textAlign: "center",
},
}));
export default function SourcePreview() {
const classes = useStyles();
const sourceChain = useSelector(selectNFTSourceChain);
const sourceParsedTokenAccount = useSelector(
selectNFTSourceParsedTokenAccount
);
const explainerContent =
sourceChain && sourceParsedTokenAccount ? (
<>
<span>You will transfer 1 NFT of</span>
<SmartAddress
chainId={sourceChain}
parsedTokenAccount={sourceParsedTokenAccount}
/>
<span>from</span>
<SmartAddress
chainId={sourceChain}
address={sourceParsedTokenAccount?.publicKey}
/>
<span>on {CHAINS_BY_ID[sourceChain].name}</span>
</>
) : (
""
);
return (
<>
<Typography
component="div"
variant="subtitle2"
className={classes.description}
>
{explainerContent}
</Typography>
{sourceParsedTokenAccount ? (
<NFTViewer value={sourceParsedTokenAccount} chainId={sourceChain} />
) : null}
</>
);
}

View File

@ -1,165 +0,0 @@
import {
CHAIN_ID_SOLANA,
hexToNativeString,
hexToUint8Array,
isEVMChain,
} from "@certusone/wormhole-sdk";
import { makeStyles, TextField, Typography } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { PublicKey } from "@solana/web3.js";
import { BigNumber, ethers } from "ethers";
import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import useSyncTargetAddress from "../../hooks/useSyncTargetAddress";
import { GasEstimateSummary } from "../../hooks/useTransactionFees";
import { incrementStep, setTargetChain } from "../../store/nftSlice";
import {
selectNFTIsTargetComplete,
selectNFTOriginAsset,
selectNFTOriginChain,
selectNFTOriginTokenId,
selectNFTShouldLockFields,
selectNFTSourceChain,
selectNFTTargetAddressHex,
selectNFTTargetAsset,
selectNFTTargetChain,
selectNFTTargetError,
} from "../../store/selectors";
import {
CHAINS_BY_ID,
CHAINS_WITH_NFT_SUPPORT,
CLUSTER,
getIsTransferDisabled,
} from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader";
import ChainSelect from "../ChainSelect";
import KeyAndBalance from "../KeyAndBalance";
import LowBalanceWarning from "../LowBalanceWarning";
import SolanaTPSWarning from "../SolanaTPSWarning";
import StepDescription from "../StepDescription";
import ChainWarningMessage from "../ChainWarningMessage";
const useStyles = makeStyles((theme) => ({
transferField: {
marginTop: theme.spacing(5),
},
alert: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
}));
function Target() {
const classes = useStyles();
const dispatch = useDispatch();
const sourceChain = useSelector(selectNFTSourceChain);
const chains = useMemo(
() => CHAINS_WITH_NFT_SUPPORT.filter((c) => c.id !== sourceChain),
[sourceChain]
);
const targetChain = useSelector(selectNFTTargetChain);
const targetAddressHex = useSelector(selectNFTTargetAddressHex);
const targetAsset = useSelector(selectNFTTargetAsset);
const originChain = useSelector(selectNFTOriginChain);
const originAsset = useSelector(selectNFTOriginAsset);
const originTokenId = useSelector(selectNFTOriginTokenId);
let tokenId;
try {
tokenId =
originChain === CHAIN_ID_SOLANA && originAsset
? BigNumber.from(
new PublicKey(hexToUint8Array(originAsset)).toBytes()
).toString()
: originTokenId;
} catch (e) {
tokenId = originTokenId;
}
const readableTargetAddress =
hexToNativeString(targetAddressHex, targetChain) || "";
const error = useSelector(selectNFTTargetError);
const isTargetComplete = useSelector(selectNFTIsTargetComplete);
const shouldLockFields = useSelector(selectNFTShouldLockFields);
const { statusMessage } = useIsWalletReady(targetChain);
useSyncTargetAddress(!shouldLockFields, true);
const handleTargetChange = useCallback(
(event) => {
dispatch(setTargetChain(event.target.value));
},
[dispatch]
);
const handleNextClick = useCallback(() => {
dispatch(incrementStep());
}, [dispatch]);
const isTransferDisabled = useMemo(() => {
return getIsTransferDisabled(targetChain, false);
}, [targetChain]);
return (
<>
<StepDescription>Select a recipient chain and address.</StepDescription>
<ChainSelect
select
fullWidth
variant="outlined"
value={targetChain}
onChange={handleTargetChange}
chains={chains}
/>
<KeyAndBalance chainId={targetChain} />
<TextField
label="Recipient Address"
fullWidth
variant="outlined"
className={classes.transferField}
value={readableTargetAddress}
disabled={true}
/>
{targetAsset !== ethers.constants.AddressZero ? (
<>
<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}
value={tokenId || ""}
disabled={true}
/>
) : null}
</>
) : null}
<Alert severity="info" variant="outlined" className={classes.alert}>
<Typography>
You will have to pay transaction fees on{" "}
{CHAINS_BY_ID[targetChain].name} to redeem your NFT.
</Typography>
{isEVMChain(targetChain) && (
<GasEstimateSummary methodType="nft" chainId={targetChain} />
)}
</Alert>
<LowBalanceWarning chainId={targetChain} />
{targetChain === CHAIN_ID_SOLANA && CLUSTER === "mainnet" && (
<SolanaTPSWarning />
)}
<ChainWarningMessage chainId={targetChain} />
<ButtonWithLoader
disabled={!isTargetComplete || isTransferDisabled} //|| !associatedAccountExists}
onClick={handleNextClick}
showLoader={false}
error={statusMessage || error}
>
Next
</ButtonWithLoader>
</>
);
}
export default Target;

View File

@ -1,43 +0,0 @@
import { makeStyles, Typography } from "@material-ui/core";
import { useSelector } from "react-redux";
import {
selectNFTTargetAddressHex,
selectNFTTargetChain,
} from "../../store/selectors";
import { hexToNativeString } from "@certusone/wormhole-sdk";
import { CHAINS_BY_ID } from "../../utils/consts";
import SmartAddress from "../SmartAddress";
const useStyles = makeStyles((theme) => ({
description: {
textAlign: "center",
},
}));
export default function TargetPreview() {
const classes = useStyles();
const targetChain = useSelector(selectNFTTargetChain);
const targetAddress = useSelector(selectNFTTargetAddressHex);
const targetAddressNative = hexToNativeString(targetAddress, targetChain);
const explainerContent =
targetChain && targetAddressNative ? (
<>
<span>to</span>
<SmartAddress chainId={targetChain} address={targetAddressNative} />
<span>on {CHAINS_BY_ID[targetChain].name}</span>
</>
) : (
""
);
return (
<Typography
component="div"
variant="subtitle2"
className={classes.description}
>
{explainerContent}
</Typography>
);
}

View File

@ -1,41 +0,0 @@
import { CHAIN_ID_SOLANA, isEVMChain } from "@certusone/wormhole-sdk";
import { makeStyles, Typography } from "@material-ui/core";
import { useSelector } from "react-redux";
import {
selectNFTIsRedeeming,
selectNFTIsSending,
selectNFTRedeemTx,
selectNFTSourceChain,
selectNFTTargetChain,
selectNFTTransferTx,
} from "../../store/selectors";
import { WAITING_FOR_WALLET_AND_CONF } from "../Transfer/WaitingForWalletMessage";
const useStyles = makeStyles((theme) => ({
message: {
color: theme.palette.warning.light,
marginTop: theme.spacing(1),
textAlign: "center",
},
}));
export default function WaitingForWalletMessage() {
const classes = useStyles();
const sourceChain = useSelector(selectNFTSourceChain);
const isSending = useSelector(selectNFTIsSending);
const transferTx = useSelector(selectNFTTransferTx);
const targetChain = useSelector(selectNFTTargetChain);
const isRedeeming = useSelector(selectNFTIsRedeeming);
const redeemTx = useSelector(selectNFTRedeemTx);
const showWarning = (isSending && !transferTx) || (isRedeeming && !redeemTx);
return showWarning ? (
<Typography className={classes.message} variant="body2">
{WAITING_FOR_WALLET_AND_CONF}{" "}
{targetChain === CHAIN_ID_SOLANA && isRedeeming
? "Note: there will be several transactions"
: isEVMChain(sourceChain) && isSending
? "Note: there will be two transactions"
: null}
</Typography>
) : null;
}

View File

@ -1,134 +0,0 @@
import { ChainId } from "@certusone/wormhole-sdk";
import {
Container,
Step,
StepButton,
StepContent,
Stepper,
} from "@material-ui/core";
import { useEffect, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useLocation } from "react-router";
import useCheckIfWormholeWrapped from "../../hooks/useCheckIfWormholeWrapped";
import useFetchTargetAsset from "../../hooks/useFetchTargetAsset";
import { setSourceChain, setStep, setTargetChain } from "../../store/nftSlice";
import {
selectNFTActiveStep,
selectNFTIsRedeemComplete,
selectNFTIsRedeeming,
selectNFTIsSendComplete,
selectNFTIsSending,
} from "../../store/selectors";
import { CHAINS_WITH_NFT_SUPPORT } from "../../utils/consts";
import Redeem from "./Redeem";
import RedeemPreview from "./RedeemPreview";
import Send from "./Send";
import SendPreview from "./SendPreview";
import Source from "./Source";
import SourcePreview from "./SourcePreview";
import Target from "./Target";
import TargetPreview from "./TargetPreview";
function NFT() {
useCheckIfWormholeWrapped(true);
useFetchTargetAsset(true);
const dispatch = useDispatch();
const activeStep = useSelector(selectNFTActiveStep);
const isSending = useSelector(selectNFTIsSending);
const isSendComplete = useSelector(selectNFTIsSendComplete);
const isRedeeming = useSelector(selectNFTIsRedeeming);
const isRedeemComplete = useSelector(selectNFTIsRedeemComplete);
const preventNavigation =
(isSending || isSendComplete || isRedeeming) && !isRedeemComplete;
const { search } = useLocation();
const query = useMemo(() => new URLSearchParams(search), [search]);
const pathSourceChain = query.get("sourceChain");
const pathTargetChain = query.get("targetChain");
//This effect initializes the state based on the path params
useEffect(() => {
if (!pathSourceChain && !pathTargetChain) {
return;
}
try {
const sourceChain: ChainId | undefined = CHAINS_WITH_NFT_SUPPORT.find(
(x) => parseFloat(pathSourceChain || "") === x.id
)?.id;
const targetChain: ChainId | undefined = CHAINS_WITH_NFT_SUPPORT.find(
(x) => parseFloat(pathTargetChain || "") === x.id
)?.id;
if (sourceChain === targetChain) {
return;
}
if (sourceChain) {
dispatch(setSourceChain(sourceChain));
}
if (targetChain) {
dispatch(setTargetChain(targetChain));
}
} catch (e) {
console.error("Invalid path params specified.");
}
}, [pathSourceChain, pathTargetChain, dispatch]);
useEffect(() => {
if (preventNavigation) {
window.onbeforeunload = () => true;
return () => {
window.onbeforeunload = null;
};
}
}, [preventNavigation]);
return (
<Container maxWidth="md">
<Stepper activeStep={activeStep} orientation="vertical">
<Step
expanded={activeStep >= 0}
disabled={preventNavigation || isRedeemComplete}
>
<StepButton onClick={() => dispatch(setStep(0))} icon={null}>
1. Source
</StepButton>
<StepContent>
{activeStep === 0 ? <Source /> : <SourcePreview />}
</StepContent>
</Step>
<Step
expanded={activeStep >= 1}
disabled={preventNavigation || isRedeemComplete || activeStep === 0}
>
<StepButton onClick={() => dispatch(setStep(1))} icon={null}>
2. Target
</StepButton>
<StepContent>
{activeStep === 1 ? <Target /> : <TargetPreview />}
</StepContent>
</Step>
<Step expanded={activeStep >= 2} disabled={isSendComplete}>
<StepButton disabled icon={null}>
3. Send NFT
</StepButton>
<StepContent>
{activeStep === 2 ? <Send /> : <SendPreview />}
</StepContent>
</Step>
<Step expanded={activeStep >= 3} completed={isRedeemComplete}>
<StepButton
onClick={() => dispatch(setStep(3))}
disabled={!isSendComplete || isRedeemComplete}
icon={null}
>
4. Redeem NFT
</StepButton>
<StepContent>
{isRedeemComplete ? <RedeemPreview /> : <Redeem />}
</StepContent>
</Step>
</Stepper>
</Container>
);
}
export default NFT;

View File

@ -1,388 +0,0 @@
import {
ChainId,
CHAIN_ID_AURORA,
CHAIN_ID_AVAX,
CHAIN_ID_BSC,
CHAIN_ID_ETH,
CHAIN_ID_FANTOM,
CHAIN_ID_OASIS,
CHAIN_ID_POLYGON,
CHAIN_ID_SOLANA,
hexToNativeAssetString,
isEVMChain,
uint8ArrayToHex,
} from "@certusone/wormhole-sdk";
import {
getOriginalAssetEth,
getOriginalAssetSol,
WormholeWrappedNFTInfo,
} from "@certusone/wormhole-sdk/lib/esm/nft_bridge";
import {
Button,
Card,
CircularProgress,
Container,
makeStyles,
MenuItem,
TextField,
Typography,
} from "@material-ui/core";
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,
SOLANA_HOST,
SOL_NFT_BRIDGE_ADDRESS,
} from "../utils/consts";
import {
ethNFTToNFTParsedTokenAccount,
getEthereumNFT,
isNFT,
isValidEthereumAddress,
} from "../utils/ethereum";
import HeaderText from "./HeaderText";
import KeyAndBalance from "./KeyAndBalance";
import NFTViewer from "./TokenSelectors/NFTViewer";
const useStyles = makeStyles((theme) => ({
mainCard: {
padding: "32px 32px 16px",
backgroundColor: COLORS.whiteWithTransparency,
},
originHeader: {
marginTop: theme.spacing(4),
},
viewButtonWrapper: {
textAlign: "center",
},
viewButton: {
marginTop: theme.spacing(1),
},
loaderWrapper: {
margin: theme.spacing(2),
textAlign: "center",
},
}));
export default function NFTOriginVerifier() {
const classes = useStyles();
const isBeta = useBetaContext();
const { provider, signerAddress } = useEthereumProvider();
const [lookupChain, setLookupChain] = useState<ChainId>(CHAIN_ID_ETH);
const { isReady, statusMessage } = useIsWalletReady(lookupChain);
const [lookupAsset, setLookupAsset] = useState("");
const [lookupTokenId, setLookupTokenId] = useState("");
const [lookupError, setLookupError] = useState("");
const [parsedTokenAccount, setParsedTokenAccount] = useState<
NFTParsedTokenAccount | undefined
>(undefined);
const [originInfo, setOriginInfo] = useState<
WormholeWrappedNFTInfo | undefined
>(undefined);
const [isLoading, setIsLoading] = useState(false);
const handleChainChange = useCallback((event) => {
setLookupChain(event.target.value);
}, []);
const handleAssetChange = useCallback((event) => {
setLookupAsset(event.target.value);
}, []);
const handleTokenIdChange = useCallback((event) => {
setLookupTokenId(event.target.value);
}, []);
useEffect(() => {
let cancelled = false;
setLookupError("");
setParsedTokenAccount(undefined);
setOriginInfo(undefined);
if (
isReady &&
provider &&
signerAddress &&
isEVMChain(lookupChain) &&
lookupAsset &&
lookupTokenId
) {
if (isValidEthereumAddress(lookupAsset)) {
(async () => {
setIsLoading(true);
try {
const token = await getEthereumNFT(lookupAsset, provider);
const result = await isNFT(token);
if (result) {
const newParsedTokenAccount = await ethNFTToNFTParsedTokenAccount(
token,
lookupTokenId,
signerAddress
);
const info = await getOriginalAssetEth(
getNFTBridgeAddressForChain(lookupChain),
provider,
lookupAsset,
lookupTokenId,
lookupChain
);
if (!cancelled) {
setIsLoading(false);
setParsedTokenAccount(newParsedTokenAccount);
setOriginInfo(info);
}
} else if (!cancelled) {
setIsLoading(false);
setLookupError(
"This token does not support ERC-165, ERC-721, and ERC-721 metadata"
);
}
} catch (e) {
console.error(e);
if (!cancelled) {
setIsLoading(false);
setLookupError(
"This token does not support ERC-165, ERC-721, and ERC-721 metadata"
);
}
}
})();
} else {
setLookupError("Invalid address");
}
} else if (lookupChain === CHAIN_ID_SOLANA && lookupAsset) {
(async () => {
try {
setIsLoading(true);
const [metadata] = await getMetaplexData([lookupAsset]);
if (metadata) {
const connection = new Connection(SOLANA_HOST, "confirmed");
const info = await getOriginalAssetSol(
connection,
SOL_NFT_BRIDGE_ADDRESS,
lookupAsset
);
if (!cancelled) {
setIsLoading(false);
setParsedTokenAccount({
amount: "0",
decimals: 0,
mintKey: lookupAsset,
publicKey: "",
uiAmount: 0,
uiAmountString: "0",
uri: metadata.data.uri,
});
setOriginInfo(info);
}
} else {
if (!cancelled) {
setIsLoading(false);
setLookupError("Error fetching metadata");
}
}
} catch (e) {
console.error(e);
if (!cancelled) {
setIsLoading(false);
setLookupError("Invalid token");
}
}
})();
}
return () => {
cancelled = true;
};
}, [
isReady,
provider,
signerAddress,
lookupChain,
lookupAsset,
lookupTokenId,
]);
const readableAddress =
originInfo &&
originInfo.chainId &&
originInfo.assetAddress &&
hexToNativeAssetString(
uint8ArrayToHex(originInfo.assetAddress),
originInfo.chainId
);
const displayError =
(isEVMChain(lookupChain) && statusMessage) || lookupError;
return (
<div>
<Container maxWidth="md">
<HeaderText white>NFT Origin Verifier</HeaderText>
</Container>
<Container maxWidth="sm">
<Card className={classes.mainCard}>
<Alert severity="info" variant="outlined">
This page allows you to find where a Wormhole-bridged NFT was
originally minted so you can verify its authenticity.
</Alert>
<TextField
select
variant="outlined"
label="Chain"
value={lookupChain}
onChange={handleChainChange}
fullWidth
margin="normal"
>
{CHAINS_WITH_NFT_SUPPORT.filter(({ id }) =>
isBeta ? true : !BETA_CHAINS.includes(id)
).map(({ id, name }) => (
<MenuItem key={id} value={id}>
{name}
</MenuItem>
))}
</TextField>
{isEVMChain(lookupChain) ? (
<KeyAndBalance chainId={lookupChain} />
) : null}
<TextField
fullWidth
variant="outlined"
margin="normal"
label="Paste an address"
value={lookupAsset}
onChange={handleAssetChange}
/>
{isEVMChain(lookupChain) ? (
<TextField
fullWidth
variant="outlined"
margin="normal"
label="Paste a tokenId"
value={lookupTokenId}
onChange={handleTokenIdChange}
/>
) : null}
{displayError ? (
<Typography align="center" color="error">
{displayError}
</Typography>
) : null}
{isLoading ? (
<div className={classes.loaderWrapper}>
<CircularProgress />
</div>
) : null}
{parsedTokenAccount ? (
<NFTViewer value={parsedTokenAccount} chainId={lookupChain} />
) : null}
{originInfo ? (
<>
<Typography
variant="h5"
gutterBottom
className={classes.originHeader}
>
Origin Info
</Typography>
<Typography variant="body2" gutterBottom>
Chain: {CHAINS_BY_ID[originInfo.chainId].name}
</Typography>
<Typography variant="body2" gutterBottom>
Address: {readableAddress}
</Typography>
{originInfo.chainId === CHAIN_ID_SOLANA ? null : (
<Typography variant="body2" gutterBottom>
Token ID: {originInfo.tokenId}
</Typography>
)}
<div className={classes.viewButtonWrapper}>
{originInfo.chainId === CHAIN_ID_SOLANA ? (
<Button
href={`https://solscan.io/token/${readableAddress}`}
target="_blank"
rel="noopener noreferrer"
startIcon={<Launch />}
className={classes.viewButton}
variant="outlined"
>
View on Solscan
</Button>
) : originInfo.chainId === CHAIN_ID_BSC ? (
<Button
href={`https://bscscan.com/token/${readableAddress}?a=${originInfo.tokenId}`}
target="_blank"
rel="noopener noreferrer"
startIcon={<Launch />}
className={classes.viewButton}
variant="outlined"
>
View on BscScan
</Button>
) : originInfo.chainId === CHAIN_ID_POLYGON ? (
<Button
href={`https://opensea.io/assets/matic/${readableAddress}/${originInfo.tokenId}`}
target="_blank"
rel="noopener noreferrer"
startIcon={<Launch />}
className={classes.viewButton}
variant="outlined"
>
View on OpenSea
</Button>
) : originInfo.chainId === CHAIN_ID_AVAX ? (
<Button
href={`https://snowtrace.io/token/${readableAddress}?a=${originInfo.tokenId}`}
target="_blank"
rel="noopener noreferrer"
startIcon={<Launch />}
className={classes.viewButton}
variant="outlined"
>
View on Snowtrace
</Button>
) : originInfo.chainId === CHAIN_ID_AURORA ? (
<Button
href={`https://aurorascan.dev/token/${readableAddress}?a=${originInfo.tokenId}`}
target="_blank"
rel="noopener noreferrer"
startIcon={<Launch />}
className={classes.viewButton}
variant="outlined"
>
View on Explorer
</Button>
) : originInfo.chainId === CHAIN_ID_FANTOM ? (
<Button
href={`https://ftmscan.com/token/${readableAddress}?a=${originInfo.tokenId}`}
target="_blank"
rel="noopener noreferrer"
startIcon={<Launch />}
className={classes.viewButton}
variant="outlined"
>
View on FTMScan
</Button>
) : originInfo.chainId === CHAIN_ID_OASIS ? null : (
<Button
href={`https://opensea.io/assets/${readableAddress}/${originInfo.tokenId}`}
target="_blank"
rel="noopener noreferrer"
startIcon={<Launch />}
className={classes.viewButton}
variant="outlined"
>
View on OpenSea
</Button>
)}
</div>
</>
) : null}
</Card>
</Container>
</div>
);
}

View File

@ -1,32 +0,0 @@
import {
Button,
InputAdornment,
TextField,
TextFieldProps,
} from "@material-ui/core";
export default function NumberTextField({
onMaxClick,
...props
}: TextFieldProps & { onMaxClick?: () => void }) {
return (
<TextField
type="number"
{...props}
InputProps={{
endAdornment: onMaxClick ? (
<InputAdornment position="end">
<Button
onClick={onMaxClick}
disabled={props.disabled}
variant="outlined"
>
Max
</Button>
</InputAdornment>
) : undefined,
...(props?.InputProps || {}),
}}
></TextField>
);
}

View File

@ -1,888 +0,0 @@
import {
ChainId,
CHAIN_ID_ACALA,
CHAIN_ID_ALGORAND,
CHAIN_ID_KARURA,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA2,
getEmitterAddressAlgorand,
getEmitterAddressEth,
getEmitterAddressSolana,
getEmitterAddressTerra,
hexToNativeAssetString,
hexToNativeString,
hexToUint8Array,
importCoreWasm,
isEVMChain,
isTerraChain,
parseNFTPayload,
parseSequenceFromLogAlgorand,
parseSequenceFromLogEth,
parseSequenceFromLogSolana,
parseSequenceFromLogTerra,
parseTransferPayload,
TerraChainId,
uint8ArrayToHex,
} from "@certusone/wormhole-sdk";
import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Card,
CircularProgress,
Container,
Divider,
makeStyles,
MenuItem,
TextField,
Typography,
} 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 algosdk from "algosdk";
import axios from "axios";
import { ethers } from "ethers";
import { useSnackbar } from "notistack";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useDispatch } from "react-redux";
import { useHistory, useLocation } from "react-router";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useAcalaRelayerInfo } from "../hooks/useAcalaRelayerInfo";
import useIsWalletReady from "../hooks/useIsWalletReady";
import useRelayersAvailable, { Relayer } from "../hooks/useRelayersAvailable";
import { COLORS } from "../muiTheme";
import { setRecoveryVaa as setRecoveryNFTVaa } from "../store/nftSlice";
import { setRecoveryVaa } from "../store/transferSlice";
import {
ALGORAND_HOST,
ALGORAND_TOKEN_BRIDGE_ID,
CHAINS,
CHAINS_BY_ID,
CHAINS_WITH_NFT_SUPPORT,
getBridgeAddressForChain,
getNFTBridgeAddressForChain,
getTokenBridgeAddressForChain,
RELAY_URL_EXTENSION,
SOLANA_HOST,
SOL_NFT_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
getTerraConfig,
WORMHOLE_RPC_HOSTS,
} from "../utils/consts";
import { getSignedVAAWithRetry } from "../utils/getSignedVAAWithRetry";
import parseError from "../utils/parseError";
import { queryExternalId } from "../utils/terra";
import ButtonWithLoader from "./ButtonWithLoader";
import ChainSelect from "./ChainSelect";
import KeyAndBalance from "./KeyAndBalance";
import RelaySelector from "./RelaySelector";
import PendingVAAWarning from "./Transfer/PendingVAAWarning";
const useStyles = makeStyles((theme) => ({
mainCard: {
padding: "32px 32px 16px",
backgroundColor: COLORS.whiteWithTransparency,
},
advancedContainer: {
padding: theme.spacing(2, 0),
},
relayAlert: {
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
"& > .MuiAlert-message": {
width: "100%",
},
},
}));
async function fetchSignedVAA(
chainId: ChainId,
emitterAddress: string,
sequence: string
) {
const { vaaBytes, isPending } = await getSignedVAAWithRetry(
chainId,
emitterAddress,
sequence,
WORMHOLE_RPC_HOSTS.length
);
return {
vaa: vaaBytes ? uint8ArrayToHex(vaaBytes) : undefined,
isPending,
error: null,
};
}
function handleError(e: any, enqueueSnackbar: any) {
console.error(e);
enqueueSnackbar(null, {
content: <Alert severity="error">{parseError(e)}</Alert>,
});
return { vaa: null, isPending: false, error: parseError(e) };
}
async function algo(tx: string, enqueueSnackbar: any) {
try {
const algodClient = new algosdk.Algodv2(
ALGORAND_HOST.algodToken,
ALGORAND_HOST.algodServer,
ALGORAND_HOST.algodPort
);
const pendingInfo = await algodClient
.pendingTransactionInformation(tx)
.do();
let confirmedTxInfo: Record<string, any> | undefined = undefined;
// This is the code from waitForConfirmation
if (pendingInfo !== undefined) {
if (
pendingInfo["confirmed-round"] !== null &&
pendingInfo["confirmed-round"] > 0
) {
//Got the completed Transaction
confirmedTxInfo = pendingInfo;
}
}
if (!confirmedTxInfo) {
throw new Error("Transaction not found or not confirmed");
}
const sequence = parseSequenceFromLogAlgorand(confirmedTxInfo);
if (!sequence) {
throw new Error("Sequence not found");
}
const emitterAddress = getEmitterAddressAlgorand(ALGORAND_TOKEN_BRIDGE_ID);
return await fetchSignedVAA(CHAIN_ID_ALGORAND, emitterAddress, sequence);
} catch (e) {
return handleError(e, enqueueSnackbar);
}
}
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)
);
return await fetchSignedVAA(chainId, emitterAddress, sequence);
} catch (e) {
return handleError(e, enqueueSnackbar);
}
}
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
);
return await fetchSignedVAA(CHAIN_ID_SOLANA, emitterAddress, sequence);
} catch (e) {
return handleError(e, enqueueSnackbar);
}
}
async function terra(tx: string, enqueueSnackbar: any, chainId: TerraChainId) {
try {
const lcd = new LCDClient(getTerraConfig(chainId));
const info = await lcd.tx.txInfo(tx);
const sequence = parseSequenceFromLogTerra(info);
if (!sequence) {
throw new Error("Sequence not found");
}
const emitterAddress = await getEmitterAddressTerra(
getTokenBridgeAddressForChain(chainId)
);
return await fetchSignedVAA(chainId, emitterAddress, sequence);
} catch (e) {
return handleError(e, enqueueSnackbar);
}
}
function RelayerRecovery({
parsedPayload,
signedVaa,
onClick,
}: {
parsedPayload: any;
signedVaa: string;
onClick: () => void;
}) {
const classes = useStyles();
const relayerInfo = useRelayersAvailable(true);
const [selectedRelayer, setSelectedRelayer] = useState<Relayer | null>(null);
const [isAttemptingToSchedule, setIsAttemptingToSchedule] = useState(false);
const { enqueueSnackbar } = useSnackbar();
console.log(parsedPayload, relayerInfo, "in recovery relayer");
const fee =
(parsedPayload && parsedPayload.fee && parseInt(parsedPayload.fee)) || null;
//This check is probably more sophisticated in the future. Possibly a net call.
const isEligible =
fee &&
fee > 0 &&
relayerInfo?.data?.relayers?.length &&
relayerInfo?.data?.relayers?.length > 0;
const handleRelayerChange = useCallback(
(relayer: Relayer | null) => {
setSelectedRelayer(relayer);
},
[setSelectedRelayer]
);
const handleGo = useCallback(async () => {
console.log("handle go", selectedRelayer, parsedPayload);
if (!(selectedRelayer && selectedRelayer.url)) {
return;
}
setIsAttemptingToSchedule(true);
axios
.get(
selectedRelayer.url +
RELAY_URL_EXTENSION +
encodeURIComponent(
Buffer.from(hexToUint8Array(signedVaa)).toString("base64")
)
)
.then(
() => {
setIsAttemptingToSchedule(false);
onClick();
},
(error) => {
setIsAttemptingToSchedule(false);
enqueueSnackbar(null, {
content: (
<Alert severity="error">
{"Relay request rejected. Error: " + error.message}
</Alert>
),
});
}
);
}, [selectedRelayer, enqueueSnackbar, onClick, signedVaa, parsedPayload]);
if (!isEligible) {
return null;
}
return (
<Alert variant="outlined" severity="info" className={classes.relayAlert}>
<Typography>{"This transaction is eligible to be relayed"}</Typography>
<RelaySelector
selectedValue={selectedRelayer}
onChange={handleRelayerChange}
/>
<ButtonWithLoader
disabled={!selectedRelayer}
onClick={handleGo}
showLoader={isAttemptingToSchedule}
>
Request Relay
</ButtonWithLoader>
</Alert>
);
}
function AcalaRelayerRecovery({
parsedPayload,
signedVaa,
onClick,
isNFT,
}: {
parsedPayload: any;
signedVaa: string;
onClick: () => void;
isNFT: boolean;
}) {
const classes = useStyles();
const originChain: ChainId = parsedPayload?.originChain;
const originAsset = parsedPayload?.originAddress;
const targetChain: ChainId = parsedPayload?.targetChain;
const amount =
parsedPayload && "amount" in parsedPayload
? parsedPayload.amount.toString()
: "";
const shouldCheck =
parsedPayload &&
originChain &&
originAsset &&
signedVaa &&
targetChain &&
!isNFT &&
(targetChain === CHAIN_ID_ACALA || targetChain === CHAIN_ID_KARURA);
const acalaRelayerInfo = useAcalaRelayerInfo(
targetChain,
amount,
hexToNativeAssetString(originAsset, originChain),
false
);
const enabled = shouldCheck && acalaRelayerInfo.data?.shouldRelay;
return enabled ? (
<Alert variant="outlined" severity="info" className={classes.relayAlert}>
<Typography>
This transaction is eligible to be relayed by{" "}
{CHAINS_BY_ID[targetChain].name} &#127881;
</Typography>
<ButtonWithLoader onClick={onClick}>Request Relay</ButtonWithLoader>
</Alert>
) : null;
}
export default function Recovery() {
const classes = useStyles();
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<ChainId>(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 [isVAAPending, setIsVAAPending] = useState(false);
const [terra2TokenId, setTerra2TokenId] = useState("");
const { isReady, statusMessage } = useIsWalletReady(recoverySourceChain);
const walletConnectError =
isEVMChain(recoverySourceChain) && !isReady ? statusMessage : "";
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(() => {
let cancelled = false;
if (parsedPayload && parsedPayload.targetChain === CHAIN_ID_TERRA2) {
(async () => {
const tokenId = await queryExternalId(parsedPayload.originAddress);
if (!cancelled) {
setTerra2TokenId(tokenId || "");
}
})();
}
return () => {
cancelled = true;
};
}, [parsedPayload]);
const { search } = useLocation();
const query = useMemo(() => new URLSearchParams(search), [search]);
const pathSourceChain = query.get("sourceChain");
const pathSourceTransaction = query.get("transactionId");
//This effect initializes the state based on the path params.
useEffect(() => {
if (!pathSourceChain && !pathSourceTransaction) {
return;
}
try {
const sourceChain: ChainId =
CHAINS_BY_ID[parseFloat(pathSourceChain || "") as ChainId]?.id;
if (sourceChain) {
setRecoverySourceChain(sourceChain);
}
if (pathSourceTransaction) {
setRecoverySourceTx(pathSourceTransaction);
}
} catch (e) {
console.error(e);
console.error("Invalid path params specified.");
}
}, [pathSourceChain, pathSourceTransaction]);
useEffect(() => {
if (recoverySourceTx && (!isEVMChain(recoverySourceChain) || isReady)) {
let cancelled = false;
if (isEVMChain(recoverySourceChain) && provider) {
setRecoverySourceTxError("");
setRecoverySourceTxIsLoading(true);
(async () => {
const { vaa, isPending, error } = await evm(
provider,
recoverySourceTx,
enqueueSnackbar,
recoverySourceChain,
isNFT
);
if (!cancelled) {
setRecoverySourceTxIsLoading(false);
if (vaa) {
setRecoverySignedVAA(vaa);
}
if (error) {
setRecoverySourceTxError(error);
}
setIsVAAPending(isPending);
}
})();
} else if (recoverySourceChain === CHAIN_ID_SOLANA) {
setRecoverySourceTxError("");
setRecoverySourceTxIsLoading(true);
(async () => {
const { vaa, isPending, error } = await solana(
recoverySourceTx,
enqueueSnackbar,
isNFT
);
if (!cancelled) {
setRecoverySourceTxIsLoading(false);
if (vaa) {
setRecoverySignedVAA(vaa);
}
if (error) {
setRecoverySourceTxError(error);
}
setIsVAAPending(isPending);
}
})();
} else if (isTerraChain(recoverySourceChain)) {
setRecoverySourceTxError("");
setRecoverySourceTxIsLoading(true);
setTerra2TokenId("");
(async () => {
const { vaa, isPending, error } = await terra(
recoverySourceTx,
enqueueSnackbar,
recoverySourceChain
);
if (!cancelled) {
setRecoverySourceTxIsLoading(false);
if (vaa) {
setRecoverySignedVAA(vaa);
}
if (error) {
setRecoverySourceTxError(error);
}
setIsVAAPending(isPending);
}
})();
} else if (recoverySourceChain === CHAIN_ID_ALGORAND) {
setRecoverySourceTxError("");
setRecoverySourceTxIsLoading(true);
(async () => {
const { vaa, isPending, error } = await algo(
recoverySourceTx,
enqueueSnackbar
);
if (!cancelled) {
setRecoverySourceTxIsLoading(false);
if (vaa) {
setRecoverySignedVAA(vaa);
}
if (error) {
setRecoverySourceTxError(error);
}
setIsVAAPending(isPending);
}
})();
}
return () => {
cancelled = true;
};
}
}, [
recoverySourceChain,
recoverySourceTx,
provider,
enqueueSnackbar,
isNFT,
isReady,
]);
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 importCoreWasm();
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 handleRecoverClickBase = useCallback(
(useRelayer: boolean) => {
if (enableRecovery && recoverySignedVAA && parsedPayloadTargetChain) {
// TODO: make recovery reducer
if (isNFT) {
dispatch(
setRecoveryNFTVaa({
vaa: recoverySignedVAA,
parsedPayload: {
targetChain: parsedPayload.targetChain,
targetAddress: parsedPayload.targetAddress,
originChain: parsedPayload.originChain,
originAddress: parsedPayload.originAddress,
},
})
);
push("/nft");
} else {
dispatch(
setRecoveryVaa({
vaa: recoverySignedVAA,
useRelayer,
parsedPayload: {
targetChain: parsedPayload.targetChain,
targetAddress: parsedPayload.targetAddress,
originChain: parsedPayload.originChain,
originAddress: parsedPayload.originAddress,
amount:
"amount" in parsedPayload
? parsedPayload.amount.toString()
: "",
},
})
);
push("/transfer");
}
}
},
[
dispatch,
enableRecovery,
recoverySignedVAA,
parsedPayloadTargetChain,
parsedPayload,
isNFT,
push,
]
);
const handleRecoverClick = useCallback(() => {
handleRecoverClickBase(false);
}, [handleRecoverClickBase]);
const handleRecoverWithRelayerClick = useCallback(() => {
handleRecoverClickBase(true);
}, [handleRecoverClickBase]);
return (
<Container maxWidth="md">
<Card className={classes.mainCard}>
<Alert severity="info" variant="outlined">
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>
<ChainSelect
select
variant="outlined"
label="Source Chain"
disabled={!!recoverySignedVAA}
value={recoverySourceChain}
onChange={handleSourceChainChange}
fullWidth
margin="normal"
chains={isNFT ? CHAINS_WITH_NFT_SUPPORT : CHAINS}
/>
{isEVMChain(recoverySourceChain) ? (
<KeyAndBalance chainId={recoverySourceChain} />
) : null}
<TextField
variant="outlined"
label="Source Tx (paste here)"
disabled={
!!recoverySignedVAA ||
recoverySourceTxIsLoading ||
!!walletConnectError
}
value={recoverySourceTx}
onChange={handleSourceTxChange}
error={!!recoverySourceTxError || !!walletConnectError}
helperText={recoverySourceTxError || walletConnectError}
fullWidth
margin="normal"
/>
<RelayerRecovery
parsedPayload={parsedPayload}
signedVaa={recoverySignedVAA}
onClick={handleRecoverWithRelayerClick}
/>
<AcalaRelayerRecovery
parsedPayload={parsedPayload}
signedVaa={recoverySignedVAA}
onClick={handleRecoverWithRelayerClick}
isNFT={isNFT}
/>
<ButtonWithLoader
onClick={handleRecoverClick}
disabled={!enableRecovery}
showLoader={recoverySourceTxIsLoading}
>
Recover
</ButtonWithLoader>
{isVAAPending && <PendingVAAWarning />}
<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"
/>
<TextField
variant="outlined"
label="Guardian Set"
disabled
value={recoveryParsedVAA?.guardian_set_index || ""}
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
? parsedPayload.targetChain === CHAIN_ID_TERRA2
? terra2TokenId
: hexToNativeAssetString(
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 Address"
disabled
value={
(parsedPayload &&
hexToNativeString(
parsedPayload.targetAddress,
parsedPayload.targetChain
)) ||
""
}
fullWidth
margin="normal"
/>
{isNFT ? null : (
<>
<TextField
variant="outlined"
label="Amount"
disabled
value={
parsedPayload && "amount" in parsedPayload
? parsedPayload.amount.toString()
: ""
}
fullWidth
margin="normal"
/>
<TextField
variant="outlined"
label="Relayer Fee"
disabled
value={
parsedPayload && "fee" in parsedPayload
? parsedPayload.fee.toString()
: ""
}
fullWidth
margin="normal"
/>
</>
)}
</div>
</AccordionDetails>
</Accordion>
</div>
</Card>
</Container>
);
}

View File

@ -1,81 +0,0 @@
import {
CircularProgress,
makeStyles,
MenuItem,
TextField,
Typography,
} from "@material-ui/core";
import { useCallback } from "react";
import useRelayersAvailable, { Relayer } from "../hooks/useRelayersAvailable";
const useStyles = makeStyles((theme) => ({
mainContainer: {
textAlign: "center",
},
}));
export default function RelaySelector({
selectedValue,
onChange,
}: {
selectedValue: Relayer | null;
onChange: (newValue: Relayer | null) => void;
}) {
const classes = useStyles();
const availableRelayers = useRelayersAvailable(true);
const loader = (
<div>
<CircularProgress></CircularProgress>
<Typography>Loading available relayers</Typography>
</div>
);
const onChangeWrapper = useCallback(
(event) => {
console.log(event, "event in selector");
event.target.value
? onChange(
availableRelayers?.data?.relayers?.find(
(x) => x.url === event.target.value
) || null
)
: onChange(null);
},
[onChange, availableRelayers]
);
console.log("selectedValue in relay selector", selectedValue);
const selector = (
<TextField
onChange={onChangeWrapper}
value={selectedValue ? selectedValue.url : ""}
label="Select a relayer"
select
fullWidth
>
{availableRelayers.data?.relayers?.map((item) => (
<MenuItem key={item.url} value={item.url}>
{item.name}
</MenuItem>
))}
</TextField>
);
const error = (
<Typography variant="body2" color="textSecondary">
No relayers are available at this time.
</Typography>
);
return (
<div className={classes.mainContainer}>
{availableRelayers.data?.relayers?.length
? selector
: availableRelayers.isFetching
? loader
: error}
</div>
);
}

View File

@ -1,149 +0,0 @@
import {
ChainId,
CHAIN_ID_ALGORAND,
CHAIN_ID_AURORA,
CHAIN_ID_AVAX,
CHAIN_ID_BSC,
CHAIN_ID_CELO,
CHAIN_ID_ETH,
CHAIN_ID_ETHEREUM_ROPSTEN,
CHAIN_ID_FANTOM,
CHAIN_ID_KLAYTN,
CHAIN_ID_KARURA,
CHAIN_ID_OASIS,
CHAIN_ID_POLYGON,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
CHAIN_ID_ACALA,
isTerraChain,
CHAIN_ID_TERRA2,
} from "@certusone/wormhole-sdk";
import { Button, makeStyles, Typography } from "@material-ui/core";
import { Transaction } from "../store/transferSlice";
import { CLUSTER, getExplorerName } from "../utils/consts";
const useStyles = makeStyles((theme) => ({
tx: {
marginTop: theme.spacing(1),
textAlign: "center",
},
viewButton: {
marginTop: theme.spacing(1),
},
}));
export default function ShowTx({
chainId,
tx,
}: {
chainId: ChainId;
tx: Transaction;
}) {
const classes = useStyles();
const showExplorerLink =
CLUSTER === "testnet" ||
CLUSTER === "mainnet" ||
(CLUSTER === "devnet" &&
(chainId === CHAIN_ID_SOLANA || isTerraChain(chainId)));
const explorerAddress =
chainId === CHAIN_ID_ETH
? `https://${CLUSTER === "testnet" ? "goerli." : ""}etherscan.io/tx/${
tx?.id
}`
: chainId === CHAIN_ID_ETHEREUM_ROPSTEN
? `https://${CLUSTER === "testnet" ? "ropsten." : ""}etherscan.io/tx/${
tx?.id
}`
: chainId === CHAIN_ID_BSC
? `https://${CLUSTER === "testnet" ? "testnet." : ""}bscscan.com/tx/${
tx?.id
}`
: chainId === CHAIN_ID_POLYGON
? `https://${CLUSTER === "testnet" ? "mumbai." : ""}polygonscan.com/tx/${
tx?.id
}`
: chainId === CHAIN_ID_AVAX
? `https://${CLUSTER === "testnet" ? "testnet." : ""}snowtrace.io/tx/${
tx?.id
}`
: chainId === CHAIN_ID_OASIS
? `https://${
CLUSTER === "testnet" ? "testnet." : ""
}explorer.emerald.oasis.dev/tx/${tx?.id}`
: chainId === CHAIN_ID_AURORA
? `https://${CLUSTER === "testnet" ? "testnet." : ""}aurorascan.dev/tx/${
tx?.id
}`
: chainId === CHAIN_ID_FANTOM
? `https://${CLUSTER === "testnet" ? "testnet." : ""}ftmscan.com/tx/${
tx?.id
}`
: chainId === CHAIN_ID_KLAYTN
? `https://${CLUSTER === "testnet" ? "baobab." : ""}scope.klaytn.com/tx/${
tx?.id
}`
: chainId === CHAIN_ID_CELO
? `https://${
CLUSTER === "testnet"
? "alfajores-blockscout.celo-testnet.org"
: "explorer.celo.org"
}/tx/${tx?.id}`
: chainId === CHAIN_ID_KARURA
? `https://${
CLUSTER === "testnet"
? "blockscout.karura-dev.aca-dev.network"
: "blockscout.karura.network"
}/tx/${tx?.id}`
: chainId === CHAIN_ID_ACALA
? `https://${
CLUSTER === "testnet"
? "blockscout.acala-dev.aca-dev.network"
: "blockscout.acala.network"
}/tx/${tx?.id}`
: chainId === CHAIN_ID_SOLANA
? `https://solscan.io/tx/${tx?.id}${
CLUSTER === "testnet"
? "?cluster=devnet"
: CLUSTER === "devnet"
? "?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899"
: ""
}`
: chainId === CHAIN_ID_TERRA
? CLUSTER === "mainnet"
? `https://finder.terra.money/columbus-5/tx/${tx?.id}`
: undefined
: chainId === CHAIN_ID_TERRA2
? `https://finder.terra.money/${
CLUSTER === "devnet"
? "localterra"
: CLUSTER === "testnet"
? "pisco-1"
: "phoenix-1"
}/tx/${tx?.id}`
: chainId === CHAIN_ID_ALGORAND
? `https://${CLUSTER === "testnet" ? "testnet." : ""}algoexplorer.io/tx/${
tx?.id
}`
: undefined;
const explorerName = getExplorerName(chainId);
return (
<div className={classes.tx}>
<Typography noWrap component="div" variant="body2">
{tx.id}
</Typography>
{showExplorerLink && explorerAddress ? (
<Button
href={explorerAddress}
target="_blank"
rel="noopener noreferrer"
size="small"
variant="outlined"
className={classes.viewButton}
>
View on {explorerName}
</Button>
) : null}
</div>
);
}

View File

@ -1,256 +0,0 @@
import {
ChainId,
CHAIN_ID_ALGORAND,
CHAIN_ID_AURORA,
CHAIN_ID_AVAX,
CHAIN_ID_BSC,
CHAIN_ID_CELO,
CHAIN_ID_ETH,
CHAIN_ID_ETHEREUM_ROPSTEN,
CHAIN_ID_FANTOM,
CHAIN_ID_KLAYTN,
CHAIN_ID_KARURA,
CHAIN_ID_OASIS,
CHAIN_ID_POLYGON,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
isNativeDenom,
CHAIN_ID_ACALA,
isTerraChain,
CHAIN_ID_TERRA2,
TerraChainId,
} from "@certusone/wormhole-sdk";
import { Button, makeStyles, Tooltip, Typography } from "@material-ui/core";
import { FileCopy, OpenInNew } from "@material-ui/icons";
import { withStyles } from "@material-ui/styles";
import clsx from "clsx";
import { ReactChild } from "react";
import useCopyToClipboard from "../hooks/useCopyToClipboard";
import { ParsedTokenAccount } from "../store/transferSlice";
import { CLUSTER, getExplorerName } from "../utils/consts";
import { shortenAddress } from "../utils/solana";
import { formatNativeDenom } from "../utils/terra";
const useStyles = makeStyles((theme) => ({
mainTypog: {
display: "inline-block",
marginLeft: theme.spacing(1),
marginRight: theme.spacing(1),
textDecoration: "underline",
textUnderlineOffset: "2px",
},
noGutter: {
marginLeft: 0,
marginRight: 0,
},
noUnderline: {
textDecoration: "none",
},
buttons: {
marginLeft: ".5rem",
marginRight: ".5rem",
},
}));
const tooltipStyles = {
tooltip: {
minWidth: "max-content",
textAlign: "center",
"& > *": {
margin: ".25rem",
},
},
};
// @ts-ignore
const StyledTooltip = withStyles(tooltipStyles)(Tooltip);
export default function SmartAddress({
chainId,
parsedTokenAccount,
address,
symbol,
tokenName,
variant,
noGutter,
noUnderline,
extraContent,
isAsset,
}: {
chainId: ChainId;
parsedTokenAccount?: ParsedTokenAccount;
address?: string;
logo?: string;
tokenName?: string;
symbol?: string;
variant?: any;
noGutter?: boolean;
noUnderline?: boolean;
extraContent?: ReactChild;
isAsset?: boolean;
}) {
const classes = useStyles();
const isNativeTerra = isTerraChain(chainId) && isNativeDenom(address);
const useableAddress = parsedTokenAccount?.mintKey || address || "";
const useableSymbol = isNativeTerra
? formatNativeDenom(address || "", chainId as TerraChainId)
: parsedTokenAccount?.symbol || symbol || "";
// const useableLogo = logo || isNativeTerra ? getNativeTerraIcon(useableSymbol) : null
const isNative = parsedTokenAccount?.isNativeAsset || isNativeTerra || false;
const addressShort = shortenAddress(useableAddress) || "";
const useableName = isNative
? "Native Currency"
: parsedTokenAccount?.name
? parsedTokenAccount.name
: tokenName
? tokenName
: "";
const explorerAddress = isNative
? null
: chainId === CHAIN_ID_ETH
? `https://${CLUSTER === "testnet" ? "goerli." : ""}etherscan.io/${
isAsset ? "token" : "address"
}/${useableAddress}`
: chainId === CHAIN_ID_ETHEREUM_ROPSTEN
? `https://${CLUSTER === "testnet" ? "ropsten." : ""}etherscan.io/${
isAsset ? "token" : "address"
}/${useableAddress}`
: chainId === CHAIN_ID_BSC
? `https://${CLUSTER === "testnet" ? "testnet." : ""}bscscan.com/${
isAsset ? "token" : "address"
}/${useableAddress}`
: chainId === CHAIN_ID_POLYGON
? `https://${CLUSTER === "testnet" ? "mumbai." : ""}polygonscan.com/${
isAsset ? "token" : "address"
}/${useableAddress}`
: chainId === CHAIN_ID_AVAX
? `https://${CLUSTER === "testnet" ? "testnet." : ""}snowtrace.io/${
isAsset ? "token" : "address"
}/${useableAddress}`
: chainId === CHAIN_ID_OASIS
? `https://${
CLUSTER === "testnet" ? "testnet." : ""
}explorer.emerald.oasis.dev/${
isAsset ? "token" : "address"
}/${useableAddress}`
: chainId === CHAIN_ID_AURORA
? `https://${CLUSTER === "testnet" ? "testnet." : ""}aurorascan.dev/${
isAsset ? "token" : "address"
}/${useableAddress}`
: chainId === CHAIN_ID_FANTOM
? `https://${CLUSTER === "testnet" ? "testnet." : ""}ftmscan.com/${
isAsset ? "token" : "address"
}/${useableAddress}`
: chainId === CHAIN_ID_KLAYTN
? `https://${CLUSTER === "testnet" ? "baobab." : ""}scope.klaytn.com/${
isAsset ? "token" : "address"
}/${useableAddress}`
: chainId === CHAIN_ID_CELO
? `https://${
CLUSTER === "testnet"
? "alfajores-blockscout.celo-testnet.org"
: "explorer.celo.org"
}/address/${useableAddress}`
: chainId === CHAIN_ID_KARURA
? `https://${
CLUSTER === "testnet"
? "blockscout.karura-dev.aca-dev.network"
: "blockscout.karura.network"
}/${isAsset ? "token" : "address"}/${useableAddress}`
: chainId === CHAIN_ID_ACALA
? `https://${
CLUSTER === "testnet"
? "blockscout.acala-dev.aca-dev.network"
: "blockscout.acala.network"
}/${isAsset ? "token" : "address"}/${useableAddress}`
: chainId === CHAIN_ID_SOLANA
? `https://solscan.io/address/${useableAddress}${
CLUSTER === "testnet"
? "?cluster=devnet"
: CLUSTER === "devnet"
? "?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899"
: ""
}`
: chainId === CHAIN_ID_TERRA
? CLUSTER === "mainnet"
? `https://finder.terra.money/columbus-5/address/${useableAddress}`
: undefined
: chainId === CHAIN_ID_TERRA2
? `https://finder.terra.money/${
CLUSTER === "devnet"
? "localterra"
: CLUSTER === "testnet"
? "pisco-1"
: "phoenix-1"
}/address/${useableAddress}`
: chainId === CHAIN_ID_ALGORAND
? `https://${CLUSTER === "testnet" ? "testnet." : ""}algoexplorer.io/${
isAsset ? "asset" : "address"
}/${useableAddress}`
: undefined;
const explorerName = getExplorerName(chainId);
const copyToClipboard = useCopyToClipboard(useableAddress);
const explorerButton = !explorerAddress ? null : (
<Button
size="small"
variant="outlined"
startIcon={<OpenInNew />}
className={classes.buttons}
href={explorerAddress}
target="_blank"
rel="noopener noreferrer"
>
{"View on " + explorerName}
</Button>
);
//TODO add icon here
const copyButton = isNative ? null : (
<Button
size="small"
variant="outlined"
startIcon={<FileCopy />}
onClick={copyToClipboard}
className={classes.buttons}
>
Copy
</Button>
);
const tooltipContent = (
<>
{useableName && <Typography>{useableName}</Typography>}
{useableSymbol && !isNative && (
<Typography noWrap variant="body2">
{addressShort}
</Typography>
)}
<div>
{explorerButton}
{copyButton}
</div>
{extraContent ? extraContent : null}
</>
);
return (
<StyledTooltip
title={tooltipContent}
interactive={true}
className={classes.mainTypog}
>
<Typography
variant={variant || "body1"}
className={clsx(classes.mainTypog, {
[classes.noGutter]: noGutter,
[classes.noUnderline]: noUnderline,
})}
component="div"
>
{useableSymbol || addressShort}
</Typography>
</StyledTooltip>
);
}

View File

@ -1,139 +0,0 @@
import { useCallback, useMemo } from "react";
import {
Dialog,
DialogTitle,
Divider,
IconButton,
List,
ListItem,
ListItemIcon,
ListItemText,
makeStyles,
} from "@material-ui/core";
import CloseIcon from "@material-ui/icons/Close";
import { WalletName, WalletReadyState } from "@solana/wallet-adapter-base";
import { useWallet, Wallet } from "@solana/wallet-adapter-react";
const useStyles = makeStyles((theme) => ({
flexTitle: {
display: "flex",
alignItems: "center",
"& > div": {
flexGrow: 1,
marginRight: theme.spacing(4),
},
"& > button": {
marginRight: theme.spacing(-1),
},
},
icon: {
height: 24,
width: 24,
},
}));
const DetectedWalletListItem = ({
wallet,
select,
onClose,
}: {
wallet: Wallet;
select: (walletName: WalletName) => void;
onClose: () => void;
}) => {
const handleWalletClick = useCallback(() => {
select(wallet.adapter.name);
onClose();
}, [select, onClose, wallet]);
return (
<ListItem button onClick={handleWalletClick}>
<WalletListItem wallet={wallet} text={wallet.adapter.name} />
</ListItem>
);
};
const WalletListItem = ({ wallet, text }: { wallet: Wallet; text: string }) => {
const classes = useStyles();
return (
<>
<ListItemIcon>
<img
src={wallet.adapter.icon}
alt={wallet.adapter.name}
className={classes.icon}
/>
</ListItemIcon>
<ListItemText>{text}</ListItemText>
</>
);
};
const SolanaConnectWalletDialog = ({
isOpen,
onClose,
}: {
isOpen: boolean;
onClose: () => void;
}) => {
const classes = useStyles();
const { wallets, select } = useWallet();
const [detected, undetected] = useMemo(() => {
const detected: Wallet[] = [];
const undetected: Wallet[] = [];
for (const wallet of wallets) {
if (
wallet.readyState === WalletReadyState.Installed ||
wallet.readyState === WalletReadyState.Loadable
) {
detected.push(wallet);
} else if (wallet.readyState === WalletReadyState.NotDetected) {
undetected.push(wallet);
}
}
return [detected, undetected];
}, [wallets]);
return (
<Dialog open={isOpen} onClose={onClose}>
<DialogTitle>
<div className={classes.flexTitle}>
<div>Select your wallet</div>
<IconButton onClick={onClose}>
<CloseIcon />
</IconButton>
</div>
</DialogTitle>
<List>
{detected.map((wallet) => (
<DetectedWalletListItem
wallet={wallet}
select={select}
onClose={onClose}
key={wallet.adapter.name}
/>
))}
{undetected && <Divider variant="middle" />}
{undetected.map((wallet) => (
<ListItem
button
onClick={onClose}
component="a"
key={wallet.adapter.name}
href={wallet.adapter.url}
target="_blank"
rel="noreferrer"
>
<WalletListItem
wallet={wallet}
text={"Install " + wallet.adapter.name}
/>
</ListItem>
))}
</List>
</Dialog>
);
};
export default SolanaConnectWalletDialog;

View File

@ -1,317 +0,0 @@
import {
ChainId,
CHAIN_ID_SOLANA,
getForeignAssetSolana,
hexToNativeAssetString,
hexToNativeString,
hexToUint8Array,
} from "@certusone/wormhole-sdk";
import { Button, Typography } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
Token,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
import { useSnackbar } from "notistack";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useSelector } from "react-redux";
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
import {
selectTransferOriginAsset,
selectTransferOriginChain,
selectTransferTargetAddressHex,
} from "../store/selectors";
import { SOLANA_HOST, SOL_TOKEN_BRIDGE_ADDRESS } from "../utils/consts";
import parseError from "../utils/parseError";
import { signSendAndConfirm } from "../utils/solana";
import ButtonWithLoader from "./ButtonWithLoader";
import SmartAddress from "./SmartAddress";
export function useAssociatedAccountExistsState(
targetChain: ChainId,
mintAddress: string | null | undefined,
readableTargetAddress: string | undefined
) {
const [associatedAccountExists, setAssociatedAccountExists] = useState(true); // for now, assume it exists until we confirm it doesn't
const solanaWallet = useSolanaWallet();
const solPK = solanaWallet?.publicKey;
useEffect(() => {
setAssociatedAccountExists(true);
if (
targetChain !== CHAIN_ID_SOLANA ||
!mintAddress ||
!readableTargetAddress ||
!solPK
)
return;
let cancelled = false;
(async () => {
const connection = new Connection(SOLANA_HOST, "confirmed");
const mintPublicKey = new PublicKey(mintAddress);
const payerPublicKey = new PublicKey(solPK); // currently assumes the wallet is the owner
const associatedAddress = await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
mintPublicKey,
payerPublicKey
);
const match = associatedAddress.toString() === readableTargetAddress;
if (match) {
const associatedAddressInfo = await connection.getAccountInfo(
associatedAddress
);
if (!associatedAddressInfo) {
if (!cancelled) {
setAssociatedAccountExists(false);
}
}
}
})();
return () => {
cancelled = true;
};
}, [targetChain, mintAddress, readableTargetAddress, solPK]);
return useMemo(
() => ({ associatedAccountExists, setAssociatedAccountExists }),
[associatedAccountExists]
);
}
export default function SolanaCreateAssociatedAddress({
mintAddress,
readableTargetAddress,
associatedAccountExists,
setAssociatedAccountExists,
}: {
mintAddress: string;
readableTargetAddress: string;
associatedAccountExists: boolean;
setAssociatedAccountExists: (associatedAccountExists: boolean) => void;
}) {
const [isCreating, setIsCreating] = useState(false);
const solanaWallet = useSolanaWallet();
const solPK = solanaWallet?.publicKey;
const handleClick = useCallback(() => {
if (
associatedAccountExists ||
!mintAddress ||
!readableTargetAddress ||
!solPK
)
return;
(async () => {
const connection = new Connection(SOLANA_HOST, "confirmed");
const mintPublicKey = new PublicKey(mintAddress);
const payerPublicKey = new PublicKey(solPK); // currently assumes the wallet is the owner
const associatedAddress = await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
mintPublicKey,
payerPublicKey
);
const match = associatedAddress.toString() === readableTargetAddress;
if (match) {
const associatedAddressInfo = await connection.getAccountInfo(
associatedAddress
);
if (!associatedAddressInfo) {
setIsCreating(true);
const transaction = new Transaction().add(
await Token.createAssociatedTokenAccountInstruction(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
mintPublicKey,
associatedAddress,
payerPublicKey, // owner
payerPublicKey // payer
)
);
const { blockhash } = await connection.getRecentBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = new PublicKey(payerPublicKey);
await signSendAndConfirm(solanaWallet, connection, transaction);
setIsCreating(false);
setAssociatedAccountExists(true);
} else {
console.log("Account already exists.");
}
}
})();
}, [
associatedAccountExists,
setAssociatedAccountExists,
mintAddress,
solPK,
readableTargetAddress,
solanaWallet,
]);
if (associatedAccountExists) return null;
return (
<>
<Typography color="error" variant="body2">
This associated token account doesn't exist.
</Typography>
<ButtonWithLoader
disabled={
!mintAddress || !readableTargetAddress || !solPK || isCreating
}
onClick={handleClick}
showLoader={isCreating}
>
Create Associated Token Account
</ButtonWithLoader>
</>
);
}
export function SolanaCreateAssociatedAddressAlternate() {
const { enqueueSnackbar } = useSnackbar();
const originChain = useSelector(selectTransferOriginChain);
const originAsset = useSelector(selectTransferOriginAsset);
const addressHex = useSelector(selectTransferTargetAddressHex);
const base58TargetAddress = useMemo(
() => hexToNativeString(addressHex, CHAIN_ID_SOLANA) || "",
[addressHex]
);
const base58OriginAddress = useMemo(
() => hexToNativeAssetString(originAsset, CHAIN_ID_SOLANA) || "",
[originAsset]
);
const connection = useMemo(() => new Connection(SOLANA_HOST), []);
const [targetAsset, setTargetAsset] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
if (!(originChain && originAsset && addressHex && base58TargetAddress)) {
setTargetAsset(null);
} else if (originChain === CHAIN_ID_SOLANA && base58OriginAddress) {
setTargetAsset(base58OriginAddress);
} else {
getForeignAssetSolana(
connection,
SOL_TOKEN_BRIDGE_ADDRESS,
originChain,
hexToUint8Array(originAsset)
).then((result) => {
if (!cancelled) {
setTargetAsset(result);
}
});
}
return () => {
cancelled = true;
};
}, [
originChain,
originAsset,
addressHex,
base58TargetAddress,
connection,
base58OriginAddress,
]);
const { associatedAccountExists, setAssociatedAccountExists } =
useAssociatedAccountExistsState(
CHAIN_ID_SOLANA,
targetAsset,
base58TargetAddress
);
const solanaWallet = useSolanaWallet();
const solPK = solanaWallet?.publicKey;
const handleForceCreateClick = useCallback(() => {
if (!targetAsset || !base58TargetAddress || !solPK) return;
(async () => {
const connection = new Connection(SOLANA_HOST, "confirmed");
const mintPublicKey = new PublicKey(targetAsset);
const payerPublicKey = new PublicKey(solPK); // currently assumes the wallet is the owner
const associatedAddress = await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
mintPublicKey,
payerPublicKey
);
const match = associatedAddress.toString() === base58TargetAddress;
if (match) {
try {
const transaction = new Transaction().add(
await Token.createAssociatedTokenAccountInstruction(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
mintPublicKey,
associatedAddress,
payerPublicKey, // owner
payerPublicKey // payer
)
);
const { blockhash } = await connection.getRecentBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = new PublicKey(payerPublicKey);
await signSendAndConfirm(solanaWallet, connection, transaction);
setAssociatedAccountExists(true);
enqueueSnackbar(null, {
content: (
<Alert severity="success">
Successfully created associated token account
</Alert>
),
});
} catch (e) {
enqueueSnackbar(null, {
content: <Alert severity="error">{parseError(e)}</Alert>,
});
}
} else {
enqueueSnackbar(null, {
content: (
<Alert severity="error">
Derived address does not match the target address. Do you have the
same wallet connected?
</Alert>
),
});
}
})();
}, [
setAssociatedAccountExists,
targetAsset,
solPK,
base58TargetAddress,
solanaWallet,
enqueueSnackbar,
]);
return targetAsset ? (
<div style={{ textAlign: "center" }}>
<Typography variant="subtitle2">Recipient Address:</Typography>
<Typography component="div">
<SmartAddress
chainId={CHAIN_ID_SOLANA}
address={base58TargetAddress}
variant="h6"
extraContent={
<Button
size="small"
variant="outlined"
onClick={handleForceCreateClick}
disabled={!targetAsset || !base58TargetAddress || !solPK}
>
Force Create Account
</Button>
}
/>
</Typography>
{associatedAccountExists ? null : (
<SolanaCreateAssociatedAddress
mintAddress={targetAsset}
readableTargetAddress={base58TargetAddress}
associatedAccountExists={associatedAccountExists}
setAssociatedAccountExists={setAssociatedAccountExists}
/>
)}
</div>
) : null;
}

View File

@ -1,53 +0,0 @@
import { makeStyles } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { Connection } from "@solana/web3.js";
import numeral from "numeral";
import { useEffect, useState } from "react";
import { SOLANA_HOST } from "../utils/consts";
const useStyles = makeStyles((theme) => ({
alert: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
}));
export default function SolanaTPSWarning() {
const classes = useStyles();
const [tps, setTps] = useState<number | null>(null);
useEffect(() => {
let cancelled = false;
let interval = setInterval(() => {
(async () => {
try {
const connection = new Connection(SOLANA_HOST);
const samples = await connection.getRecentPerformanceSamples(1);
if (samples.length >= 1) {
let short = samples
.filter((sample) => sample.numTransactions !== 0)
.map(
(sample) => sample.numTransactions / sample.samplePeriodSecs
);
const avgTps = short[0];
if (!cancelled) {
setTps(avgTps);
}
}
} catch (e) {}
})();
}, 5000);
return () => {
cancelled = true;
clearInterval(interval);
};
}, []);
return tps !== null && tps < 1500 ? (
<Alert
variant="outlined"
severity="warning"
className={classes.alert}
>{`WARNING! The Solana Transactions Per Second (TPS) is below 1500. This is a sign of network congestion. Proceed with caution as you may have difficulty submitting transactions and the guardians may have difficulty witnessing them (this could lead to processing delays). Current TPS: ${numeral(
tps
).format("0,0")}`}</Alert>
) : null;
}

View File

@ -1,36 +0,0 @@
import { useCallback, useMemo, useState } from "react";
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
import SolanaConnectWalletDialog from "./SolanaConnectWalletDialog";
import ToggleConnectedButton from "./ToggleConnectedButton";
const SolanaWalletKey = () => {
const { publicKey, wallet, disconnect } = useSolanaWallet();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const openDialog = useCallback(() => {
setIsDialogOpen(true);
}, [setIsDialogOpen]);
const closeDialog = useCallback(() => {
setIsDialogOpen(false);
}, [setIsDialogOpen]);
const publicKeyBase58 = useMemo(() => {
return publicKey?.toBase58() || "";
}, [publicKey]);
return (
<>
<ToggleConnectedButton
connect={openDialog}
disconnect={disconnect}
connected={!!wallet?.adapter.connected}
pk={publicKeyBase58}
walletIcon={wallet?.adapter.icon}
/>
<SolanaConnectWalletDialog isOpen={isDialogOpen} onClose={closeDialog} />
</>
);
};
export default SolanaWalletKey;

View File

@ -1,46 +0,0 @@
import { makeStyles, Typography } from "@material-ui/core";
import { formatDate } from "./utils";
const useStyles = makeStyles(() => ({
container: {
padding: "16px",
minWidth: "214px",
background: "rgba(255, 255, 255, 0.95)",
borderRadius: "4px",
},
titleText: {
color: "#21227E",
fontSize: "24px",
fontWeight: 500,
},
ruler: {
height: "3px",
backgroundImage: "linear-gradient(90deg, #F44B1B 0%, #EEB430 100%)",
},
valueText: {
color: "#404040",
fontSize: "18px",
fontWeight: 500,
},
}));
const CustomTooltip = ({ active, payload, title, valueFormatter }: any) => {
const classes = useStyles();
if (active && payload && payload.length) {
return (
<div className={classes.container}>
<Typography className={classes.titleText}>{title}</Typography>
<hr className={classes.ruler}></hr>
<Typography className={classes.valueText}>
{valueFormatter(payload[0].value)}
</Typography>
<Typography className={classes.valueText}>
{formatDate(payload[0].payload.date)}
</Typography>
</div>
);
}
return null;
};
export default CustomTooltip;

View File

@ -1,123 +0,0 @@
import { ChainId } from "@certusone/wormhole-sdk";
import { makeStyles, Grid, Typography } from "@material-ui/core";
import {
getChainShortName,
CHAINS_BY_ID,
COLOR_BY_CHAIN_ID,
} from "../../../utils/consts";
import { formatDate } from "./utils";
const useStyles = makeStyles(() => ({
container: {
padding: "16px",
minWidth: "214px",
background: "rgba(255, 255, 255, 0.95)",
borderRadius: "4px",
},
titleText: {
color: "#21227E",
fontSize: "24px",
fontWeight: 500,
},
row: {
display: "flex",
alignItems: "center",
marginBottom: "8px",
},
ruler: {
height: "3px",
backgroundColor: "#374B92",
},
valueText: {
color: "#404040",
fontSize: "18px",
fontWeight: 500,
},
icon: {
width: "24px",
height: "24px",
},
}));
const MultiChainTooltip = ({ active, payload, title, valueFormatter }: any) => {
const classes = useStyles();
if (active && payload && payload.length) {
if (payload.length === 1) {
const chainId = +payload[0].dataKey.split(".")[1] as ChainId;
const chainShortName = getChainShortName(chainId);
const data = payload.find((data: any) => data.name === chainShortName);
if (data) {
return (
<div className={classes.container}>
<Grid container alignItems="center">
<img
className={classes.icon}
src={CHAINS_BY_ID[chainId]?.logo}
alt={chainShortName}
/>
<Typography
display="inline"
className={classes.titleText}
style={{ marginLeft: "8px" }}
>
{chainShortName}
</Typography>
</Grid>
<hr
className={classes.ruler}
style={{ backgroundColor: COLOR_BY_CHAIN_ID[chainId] }}
></hr>
<Typography className={classes.valueText}>
{valueFormatter(data.value)}
</Typography>
<Typography className={classes.valueText}>
{formatDate(data.payload.date)}
</Typography>
</div>
);
}
} else {
return (
<div className={classes.container}>
<Typography noWrap className={classes.titleText}>
{title}
</Typography>
<Typography className={classes.valueText}>
{formatDate(payload[0].payload.date)}
</Typography>
<hr className={classes.ruler}></hr>
{payload.map((data: any) => {
return (
<div key={data.name} className={classes.row}>
<div
style={{
width: "24px",
height: "24px",
backgroundColor: data.stroke,
}}
/>
<Typography
display="inline"
className={classes.valueText}
style={{ marginLeft: "8px", marginRight: "8px" }}
>
{data.name}
</Typography>
<Typography
display="inline"
className={classes.valueText}
style={{ marginLeft: "auto" }}
>
{valueFormatter(data.value)}
</Typography>
</div>
);
})}
</div>
);
}
}
return null;
};
export default MultiChainTooltip;

View File

@ -1,64 +0,0 @@
import {
AreaChart,
Area,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { formatTVL, createCumulativeTVLChartData } from "./utils";
import { NotionalTVLCumulative } from "../../../hooks/useCumulativeTVL";
import { useMemo } from "react";
import { TimeFrame } from "./TimeFrame";
import CustomTooltip from "./CustomTooltip";
import { useTheme, useMediaQuery } from "@material-ui/core";
const TVLAreaChart = ({
cumulativeTVL,
timeFrame,
}: {
cumulativeTVL: NotionalTVLCumulative;
timeFrame: TimeFrame;
}) => {
const data = useMemo(() => {
return createCumulativeTVLChartData(cumulativeTVL, timeFrame);
}, [cumulativeTVL, timeFrame]);
const theme = useTheme();
const isXSmall = useMediaQuery(theme.breakpoints.down("xs"));
return (
<ResponsiveContainer height={452}>
<AreaChart data={data}>
<XAxis
dataKey="date"
tickFormatter={timeFrame.tickFormatter}
tick={{ fill: "white" }}
interval={!isXSmall ? timeFrame.interval : undefined}
axisLine={false}
tickLine={false}
dy={16}
padding={{ right: 32 }}
/>
<YAxis
tickFormatter={formatTVL}
tick={{ fill: "white" }}
axisLine={false}
tickLine={false}
/>
<Tooltip
content={<CustomTooltip title="TVL" valueFormatter={formatTVL} />}
/>
<defs>
<linearGradient id="gradient" gradientTransform="rotate(100)">
<stop offset="0%" stopColor="#FF2B57" />
<stop offset="100%" stopColor="#5EA1EC" />
</linearGradient>
</defs>
<Area dataKey="totalTVL" fill="url(#gradient)" stroke="#405BBC" />
</AreaChart>
</ResponsiveContainer>
);
};
export default TVLAreaChart;

View File

@ -1,116 +0,0 @@
import { ChainId, CHAIN_ID_ETH } from "@certusone/wormhole-sdk";
import {
Button,
makeStyles,
Typography,
useMediaQuery,
useTheme,
} from "@material-ui/core";
import { ArrowForward } from "@material-ui/icons";
import { useCallback, useMemo, useState } from "react";
import { NotionalTVL } from "../../../hooks/useTVL";
import { ChainInfo, getChainShortName } from "../../../utils/consts";
import { createChainTVLChartData, formatTVL } from "./utils";
const useStyles = makeStyles(() => ({
table: {
borderSpacing: "16px",
overflowX: "auto",
display: "block",
},
button: {
height: "30px",
textTransform: "none",
width: "150px",
fontSize: "12px",
},
}));
const TVLBarChart = ({
tvl,
onChainSelected,
}: {
tvl: NotionalTVL;
onChainSelected: (chainInfo: ChainInfo) => void;
}) => {
const classes = useStyles();
const [mouseOverChainId, setMouseOverChainId] =
useState<ChainId>(CHAIN_ID_ETH);
const chainTVLs = useMemo(() => {
return createChainTVLChartData(tvl);
}, [tvl]);
const handleClick = useCallback(
(chainInfo: ChainInfo) => {
onChainSelected(chainInfo);
},
[onChainSelected]
);
const handleMouseOver = useCallback((chainId: ChainId) => {
setMouseOverChainId(chainId);
}, []);
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("sm"));
return (
<table className={classes.table}>
<tbody>
{chainTVLs.map((chainTVL) => (
<tr
key={chainTVL.chainInfo.id}
onMouseOver={() => handleMouseOver(chainTVL.chainInfo.id)}
>
<td style={{ textAlign: "right" }}>
<Typography noWrap display="inline">
{getChainShortName(chainTVL.chainInfo.id)}
</Typography>
</td>
<td>
<img
src={chainTVL.chainInfo.logo}
alt={""}
width={24}
height={24}
/>
</td>
<td width="100%">
<div
style={{
height: 30,
width: `${chainTVL.tvlRatio}%`,
backgroundImage:
"linear-gradient(90deg, #F44B1B 0%, #EEB430 100%)",
}}
></div>
</td>
<td>
<Typography noWrap display="inline">
{formatTVL(chainTVL.tvl)}
</Typography>
</td>
<td>
{isSmall || mouseOverChainId === chainTVL.chainInfo.id ? (
<Button
variant="outlined"
endIcon={<ArrowForward />}
onClick={() => handleClick(chainTVL.chainInfo)}
className={classes.button}
>
View assets
</Button>
) : (
<div style={{ width: 150 }} />
)}
</td>
</tr>
))}
</tbody>
</table>
);
};
export default TVLBarChart;

View File

@ -1,87 +0,0 @@
import { ChainId } from "@certusone/wormhole-sdk";
import { useTheme, useMediaQuery } from "@material-ui/core";
import { useMemo } from "react";
import {
Legend,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { NotionalTVLCumulative } from "../../../hooks/useCumulativeTVL";
import { COLOR_BY_CHAIN_ID, getChainShortName } from "../../../utils/consts";
import MultiChainTooltip from "./MultiChainTooltip";
import { TimeFrame } from "./TimeFrame";
import {
formatTVL,
createCumulativeTVLChartData,
renderLegendText,
} from "./utils";
const TVLLineChart = ({
cumulativeTVL,
timeFrame,
selectedChains,
}: {
cumulativeTVL: NotionalTVLCumulative;
timeFrame: TimeFrame;
selectedChains: ChainId[];
}) => {
const data = useMemo(() => {
return createCumulativeTVLChartData(cumulativeTVL, timeFrame);
}, [cumulativeTVL, timeFrame]);
const theme = useTheme();
const isXSmall = useMediaQuery(theme.breakpoints.down("xs"));
return (
<ResponsiveContainer height={452}>
<LineChart data={data}>
<XAxis
dataKey="date"
tickFormatter={timeFrame.tickFormatter}
tick={{ fill: "white" }}
interval={!isXSmall ? timeFrame.interval : undefined}
axisLine={false}
tickLine={false}
dy={16}
padding={{ right: 32 }}
/>
<YAxis
tickFormatter={formatTVL}
tick={{ fill: "white" }}
axisLine={false}
tickLine={false}
/>
<Tooltip
content={
<MultiChainTooltip
title="Multiple Chains"
valueFormatter={formatTVL}
/>
}
/>
{selectedChains.map((chainId) => (
<Line
dataKey={`tvlByChain.${chainId}`}
name={getChainShortName(chainId)}
stroke={COLOR_BY_CHAIN_ID[chainId]}
strokeWidth="4"
dot={false}
key={chainId}
/>
))}
<Legend
iconType="square"
iconSize={32}
formatter={renderLegendText}
wrapperStyle={{ paddingTop: 24 }}
/>
</LineChart>
</ResponsiveContainer>
);
};
export default TVLLineChart;

View File

@ -1,142 +0,0 @@
import { makeStyles } from "@material-ui/core";
import numeral from "numeral";
import { useMemo } from "react";
import { createTVLArray, NotionalTVL } from "../../../hooks/useTVL";
import { ChainInfo } from "../../../utils/consts";
import SmartAddress from "../../SmartAddress";
import MuiReactTable from "../tableComponents/MuiReactTable";
import { formatTVL } from "./utils";
const useStyles = makeStyles((theme) => ({
logoPositioner: {
height: "30px",
width: "30px",
maxWidth: "30px",
marginRight: theme.spacing(1),
display: "flex",
alignItems: "center",
},
logo: {
maxHeight: "100%",
maxWidth: "100%",
},
tokenContainer: {
display: "flex",
justifyContent: "flex-start",
alignItems: "center",
},
}));
const TVLTable = ({
chainInfo,
tvl,
}: {
chainInfo: ChainInfo;
tvl: NotionalTVL;
}) => {
const classes = useStyles();
const chainTVL = useMemo(() => {
return createTVLArray(tvl).filter((x) => x.originChainId === chainInfo.id);
}, [chainInfo, tvl]);
const sortTokens = useMemo(() => {
return (rowA: any, rowB: any) => {
if (rowA.isGrouped && rowB.isGrouped) {
return rowA.values.assetAddress > rowB.values.assetAddress ? 1 : -1;
} else if (rowA.isGrouped && !rowB.isGrouped) {
return 1;
} else if (!rowA.isGrouped && rowB.isGrouped) {
return -1;
} else if (rowA.original.symbol && !rowB.original.symbol) {
return 1;
} else if (rowB.original.symbol && !rowA.original.symbol) {
return -1;
} else if (rowA.original.symbol && rowB.original.symbol) {
return rowA.original.symbol > rowB.original.symbol ? 1 : -1;
} else {
return rowA.original.assetAddress > rowB.original.assetAddress ? 1 : -1;
}
};
}, []);
const tvlColumns = useMemo(() => {
return [
{
Header: "Token",
id: "assetAddress",
sortType: sortTokens,
disableGroupBy: true,
accessor: (value: any) => ({
chainId: value.originChainId,
symbol: value.symbol,
name: value.name,
logo: value.logo,
assetAddress: value.assetAddress,
}),
Cell: (value: any) => (
<div className={classes.tokenContainer}>
<div className={classes.logoPositioner}>
{value.row?.original?.logo ? (
<img
src={value.row?.original?.logo}
alt=""
className={classes.logo}
/>
) : null}
</div>
<SmartAddress
chainId={value.row?.original?.originChainId}
address={value.row?.original?.assetAddress}
symbol={value.row?.original?.symbol}
tokenName={value.row?.original?.name}
isAsset
/>
</div>
),
},
{
Header: "Quantity",
accessor: "amount",
disableGroupBy: true,
Cell: (value: any) =>
value.row?.original?.amount !== undefined
? numeral(value.row?.original?.amount).format("0,0.00")
: "",
},
{
Header: "Unit Price",
accessor: "quotePrice",
disableGroupBy: true,
Cell: (value: any) =>
value.row?.original?.quotePrice !== undefined
? numeral(value.row?.original?.quotePrice).format("0,0.00")
: "",
},
{
Header: "Value (USD)",
id: "totalValue",
accessor: "totalValue",
disableGroupBy: true,
Cell: (value: any) =>
value.row?.original?.totalValue !== undefined
? formatTVL(value.row?.original?.totalValue)
: "",
},
];
}, [
classes.logo,
classes.tokenContainer,
classes.logoPositioner,
sortTokens,
]);
return (
<MuiReactTable
columns={tvlColumns}
data={chainTVL || []}
skipPageReset={false}
initialState={{ sortBy: [{ id: "totalValue", desc: true }] }}
/>
);
};
export default TVLTable;

View File

@ -1,34 +0,0 @@
import { DurationLike } from "luxon";
import { formatTickDay, formatTickMonth } from "./utils";
export interface TimeFrame {
interval?: number;
duration?: DurationLike;
tickFormatter: (value: any, index: number) => string;
}
export const TIME_FRAMES: { [key: string]: TimeFrame } = {
"7 days": {
duration: { days: 7 },
tickFormatter: formatTickDay,
},
"30 days": {
duration: { days: 30 },
tickFormatter: formatTickDay,
},
"3 months": {
duration: { months: 3 },
tickFormatter: formatTickDay,
},
"6 months": {
duration: { months: 6 },
interval: 30,
tickFormatter: formatTickMonth,
},
"1 year": {
duration: { years: 1 },
interval: 30,
tickFormatter: formatTickMonth,
},
"All time": { interval: 30, tickFormatter: formatTickMonth },
};

View File

@ -1,69 +0,0 @@
import { useTheme, useMediaQuery } from "@material-ui/core";
import { useCallback } from "react";
import {
Area,
AreaChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import CustomTooltip from "./CustomTooltip";
import { TimeFrame } from "./TimeFrame";
import { formatTransactionCount, TransactionData } from "./utils";
const TransactionsAreaChart = ({
transactionData,
timeFrame,
}: {
transactionData: TransactionData[];
timeFrame: TimeFrame;
}) => {
const formatValue = useCallback((value: number) => {
return `${formatTransactionCount(value)} transactions`;
}, []);
const theme = useTheme();
const isXSmall = useMediaQuery(theme.breakpoints.down("xs"));
return (
<ResponsiveContainer height={452}>
<AreaChart data={transactionData}>
<XAxis
dataKey="date"
tickFormatter={timeFrame.tickFormatter}
tick={{ fill: "white" }}
interval={!isXSmall ? timeFrame.interval : undefined}
axisLine={false}
tickLine={false}
dy={16}
padding={{ right: 32 }}
/>
<YAxis
tickFormatter={formatTransactionCount}
tick={{ fill: "white" }}
axisLine={false}
tickLine={false}
/>
<Tooltip
content={
<CustomTooltip title="All chains" valueFormatter={formatValue} />
}
/>
<defs>
<linearGradient id="gradient" gradientTransform="rotate(100)">
<stop offset="0%" stopColor="#FF2B57" />
<stop offset="100%" stopColor="#5EA1EC" />
</linearGradient>
</defs>
<Area
dataKey="totalTransactions"
stroke="#405BBC"
fill="url(#gradient)"
/>
</AreaChart>
</ResponsiveContainer>
);
};
export default TransactionsAreaChart;

View File

@ -1,86 +0,0 @@
import { ChainId } from "@certusone/wormhole-sdk";
import { useTheme, useMediaQuery } from "@material-ui/core";
import { useCallback } from "react";
import {
ResponsiveContainer,
LineChart,
XAxis,
YAxis,
Line,
Legend,
Tooltip,
} from "recharts";
import { COLOR_BY_CHAIN_ID, getChainShortName } from "../../../utils/consts";
import MultiChainTooltip from "./MultiChainTooltip";
import { TimeFrame } from "./TimeFrame";
import {
formatTransactionCount,
renderLegendText,
TransactionData,
} from "./utils";
const TransactionsLineChart = ({
transactionData,
timeFrame,
chains,
}: {
transactionData: TransactionData[];
timeFrame: TimeFrame;
chains: ChainId[];
}) => {
const formatValue = useCallback((value: number) => {
return `${formatTransactionCount(value)} transactions`;
}, []);
const theme = useTheme();
const isXSmall = useMediaQuery(theme.breakpoints.down("xs"));
return (
<ResponsiveContainer height={452}>
<LineChart data={transactionData}>
<XAxis
dataKey="date"
tickFormatter={timeFrame.tickFormatter}
tick={{ fill: "white" }}
interval={!isXSmall ? timeFrame.interval : undefined}
axisLine={false}
tickLine={false}
dy={16}
padding={{ right: 32 }}
/>
<YAxis
tickFormatter={formatTransactionCount}
tick={{ fill: "white" }}
axisLine={false}
tickLine={false}
/>
<Tooltip
content={
<MultiChainTooltip
title="Multiple Chains"
valueFormatter={formatValue}
/>
}
/>
{chains.map((chainId) => (
<Line
dataKey={`transactionsByChain.${chainId}`}
name={getChainShortName(chainId)}
stroke={COLOR_BY_CHAIN_ID[chainId]}
strokeWidth="4"
dot={false}
key={chainId}
/>
))}
<Legend
iconType="square"
iconSize={32}
formatter={renderLegendText}
wrapperStyle={{ paddingTop: 24 }}
/>
</LineChart>
</ResponsiveContainer>
);
};
export default TransactionsLineChart;

View File

@ -1,64 +0,0 @@
import { useTheme, useMediaQuery } from "@material-ui/core";
import {
Area,
AreaChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import CustomTooltip from "./CustomTooltip";
import { TimeFrame } from "./TimeFrame";
import { TransferChartData, formatTVL } from "./utils";
const VolumeAreaChart = ({
transferData,
timeFrame,
}: {
transferData: TransferChartData[];
timeFrame: TimeFrame;
}) => {
const theme = useTheme();
const isXSmall = useMediaQuery(theme.breakpoints.down("xs"));
return (
<ResponsiveContainer height={452}>
<AreaChart data={transferData}>
<XAxis
dataKey="date"
tickFormatter={timeFrame.tickFormatter}
tick={{ fill: "white" }}
interval={!isXSmall ? timeFrame.interval : undefined}
axisLine={false}
tickLine={false}
dy={16}
padding={{ right: 32 }}
/>
<YAxis
tickFormatter={formatTVL}
tick={{ fill: "white" }}
axisLine={false}
tickLine={false}
/>
<Tooltip
content={
<CustomTooltip title="All chains" valueFormatter={formatTVL} />
}
/>
<defs>
<linearGradient id="gradient" gradientTransform="rotate(100)">
<stop offset="0%" stopColor="#FF2B57" />
<stop offset="100%" stopColor="#5EA1EC" />
</linearGradient>
</defs>
<Area
dataKey="totalTransferred"
stroke="#405BBC"
fill="url(#gradient)"
/>
</AreaChart>
</ResponsiveContainer>
);
};
export default VolumeAreaChart;

View File

@ -1,77 +0,0 @@
import { ChainId } from "@certusone/wormhole-sdk";
import { useTheme, useMediaQuery } from "@material-ui/core";
import {
Legend,
ResponsiveContainer,
LineChart,
XAxis,
YAxis,
Line,
Tooltip,
} from "recharts";
import { COLOR_BY_CHAIN_ID, getChainShortName } from "../../../utils/consts";
import MultiChainTooltip from "./MultiChainTooltip";
import { TimeFrame } from "./TimeFrame";
import { formatTVL, renderLegendText, TransferChartData } from "./utils";
const VolumeLineChart = ({
transferData,
timeFrame,
chains,
}: {
transferData: TransferChartData[];
timeFrame: TimeFrame;
chains: ChainId[];
}) => {
const theme = useTheme();
const isXSmall = useMediaQuery(theme.breakpoints.down("xs"));
return (
<ResponsiveContainer height={452}>
<LineChart data={transferData}>
<XAxis
dataKey="date"
tickFormatter={timeFrame.tickFormatter}
tick={{ fill: "white" }}
interval={!isXSmall ? timeFrame.interval : undefined}
axisLine={false}
tickLine={false}
dy={16}
padding={{ right: 32 }}
/>
<YAxis
tickFormatter={formatTVL}
tick={{ fill: "white" }}
axisLine={false}
tickLine={false}
/>
<Tooltip
content={
<MultiChainTooltip
title="Multiple Chains"
valueFormatter={formatTVL}
/>
}
/>
{chains.map((chainId) => (
<Line
dataKey={`transferredByChain.${chainId}`}
name={getChainShortName(chainId)}
stroke={COLOR_BY_CHAIN_ID[chainId]}
strokeWidth="4"
dot={false}
key={chainId}
/>
))}
<Legend
iconType="square"
iconSize={32}
formatter={renderLegendText}
wrapperStyle={{ paddingTop: 24 }}
/>
</LineChart>
</ResponsiveContainer>
);
};
export default VolumeLineChart;

View File

@ -1,202 +0,0 @@
import { ChainId } from "@certusone/wormhole-sdk";
import {
Typography,
makeStyles,
Grid,
useMediaQuery,
useTheme,
} from "@material-ui/core";
import { useMemo, useState } from "react";
import {
ResponsiveContainer,
BarChart,
XAxis,
YAxis,
Tooltip,
Bar,
Legend,
} from "recharts";
import {
CHAINS_BY_ID,
COLOR_BY_CHAIN_ID,
getChainShortName,
} from "../../../utils/consts";
import { TimeFrame } from "./TimeFrame";
import {
formatDate,
TransferChartData,
formatTVL,
renderLegendText,
} from "./utils";
const useStyles = makeStyles(() => ({
tooltipContainer: {
padding: "16px",
minWidth: "214px",
background: "rgba(255, 255, 255, 0.95)",
borderRadius: "4px",
},
tooltipTitleText: {
color: "#21227E",
fontSize: "24px",
fontWeight: 500,
marginLeft: "8px",
},
tooltipRuler: {
height: "3px",
},
tooltipValueText: {
color: "#404040",
fontSize: "18px",
fontWeight: 500,
},
tooltipIcon: {
width: "24px",
height: "24px",
},
}));
interface BarData {
date: Date;
volume: {
[chainId: string]: number;
};
volumePercent: {
[chainId: string]: number;
};
}
const createBarData = (
transferData: TransferChartData[],
selectedChains: ChainId[]
) => {
return transferData.reduce<BarData[]>((barData, transfer) => {
const data: BarData = {
date: transfer.date,
volume: {},
volumePercent: {},
};
const totalVolume = Object.entries(transfer.transferredByChain).reduce(
(totalVolume, [chainId, volume]) => {
if (selectedChains.indexOf(+chainId as ChainId) > -1) {
data.volume[chainId] = volume;
return totalVolume + volume;
}
return totalVolume;
},
0
);
if (totalVolume > 0) {
Object.keys(data.volume).forEach((chainId) => {
data.volumePercent[chainId] =
(data.volume[chainId] / totalVolume) * 100;
});
}
barData.push(data);
return barData;
}, []);
};
const CustomTooltip = ({ active, payload, chainId }: any) => {
const classes = useStyles();
if (active && payload && payload.length && chainId) {
const chainShortName = getChainShortName(chainId);
const data = payload.find((data: any) => data.name === chainShortName);
if (data) {
return (
<div className={classes.tooltipContainer}>
<Grid container alignItems="center">
<img
className={classes.tooltipIcon}
src={CHAINS_BY_ID[chainId as ChainId]?.logo}
alt={chainShortName}
/>
<Typography display="inline" className={classes.tooltipTitleText}>
{chainShortName}
</Typography>
</Grid>
<hr
className={classes.tooltipRuler}
style={{ backgroundColor: COLOR_BY_CHAIN_ID[chainId as ChainId] }}
></hr>
<Typography
className={classes.tooltipValueText}
>{`${data.value.toFixed(1)}%`}</Typography>
<Typography className={classes.tooltipValueText}>
{formatTVL(data.payload.volume[chainId])}
</Typography>
<Typography className={classes.tooltipValueText}>
{formatDate(data.payload.date)}
</Typography>
</div>
);
}
}
return null;
};
const VolumeStackedBarChart = ({
transferData,
timeFrame,
selectedChains,
}: {
transferData: TransferChartData[];
timeFrame: TimeFrame;
selectedChains: ChainId[];
}) => {
const [hoverChainId, setHoverChainId] = useState<ChainId | null>(null);
const barData = useMemo(() => {
return createBarData(transferData, selectedChains);
}, [transferData, selectedChains]);
const theme = useTheme();
const isXSmall = useMediaQuery(theme.breakpoints.down("xs"));
return (
<ResponsiveContainer height={452}>
<BarChart data={barData}>
<XAxis
dataKey="date"
tickFormatter={timeFrame.tickFormatter}
tick={{ fill: "white" }}
interval={!isXSmall ? timeFrame.interval : undefined}
axisLine={false}
tickLine={false}
dy={16}
padding={{ right: 32 }}
/>
<YAxis
tickFormatter={(tick) => `${tick}%`}
ticks={[0, 25, 50, 75, 100]}
domain={[0, 100]}
tick={{ fill: "white" }}
axisLine={false}
tickLine={false}
/>
<Tooltip
content={<CustomTooltip chainId={hoverChainId} barData={barData} />}
cursor={{ fill: "transparent" }}
/>
{selectedChains.map((chainId) => (
<Bar
dataKey={`volumePercent.${chainId}`}
name={getChainShortName(chainId)}
fill={COLOR_BY_CHAIN_ID[chainId]}
key={chainId}
stackId="a"
onMouseOver={() => setHoverChainId(chainId)}
/>
))}
<Legend
iconType="square"
iconSize={32}
formatter={renderLegendText}
wrapperStyle={{ paddingTop: 24 }}
/>
</BarChart>
</ResponsiveContainer>
);
};
export default VolumeStackedBarChart;

View File

@ -1,204 +0,0 @@
import { NotionalTVLCumulative } from "../../../hooks/useCumulativeTVL";
import { NotionalTransferredFrom } from "../../../hooks/useNotionalTransferred";
import { TimeFrame } from "./TimeFrame";
import { DateTime } from "luxon";
import { Totals } from "../../../hooks/useTransactionTotals";
import {
ChainInfo,
CHAINS_BY_ID,
VAA_EMITTER_ADDRESSES,
} from "../../../utils/consts";
import { NotionalTVL } from "../../../hooks/useTVL";
import { ChainId } from "@certusone/wormhole-sdk";
export const formatTVL = (tvl: number) => {
const [divisor, unit, fractionDigits] =
tvl < 1e3
? [1, "", 0]
: tvl < 1e6
? [1e3, "K", 0]
: tvl < 1e9
? [1e6, "M", 0]
: [1e9, "B", 2];
return `$${(tvl / divisor).toFixed(fractionDigits)} ${unit}`;
};
export const formatDate = (date: Date) => {
return date.toLocaleString("en-US", {
day: "numeric",
month: "long",
year: "numeric",
timeZone: "UTC",
});
};
export const formatTickDay = (date: Date) => {
return date.toLocaleString("en-US", {
day: "numeric",
month: "short",
year: "numeric",
timeZone: "UTC",
});
};
export const formatTickMonth = (date: Date) => {
return date.toLocaleString("en-US", {
month: "short",
year: "numeric",
timeZone: "UTC",
});
};
export const formatTransactionCount = (transactionCount: number) => {
return transactionCount.toLocaleString("en-US");
};
export const renderLegendText = (value: any) => {
return <span style={{ color: "white", margin: "8px" }}>{value}</span>;
};
export const getStartDate = (timeFrame: TimeFrame) => {
return timeFrame.duration
? DateTime.now().toUTC().minus(timeFrame.duration).toJSDate()
: undefined;
};
export interface CumulativeTVLChartData {
date: Date;
totalTVL: number;
tvlByChain: {
[chainId: string]: number;
};
}
export const createCumulativeTVLChartData = (
cumulativeTVL: NotionalTVLCumulative,
timeFrame: TimeFrame
) => {
const startDate = getStartDate(timeFrame);
return Object.entries(cumulativeTVL.DailyLocked)
.reduce<CumulativeTVLChartData[]>(
(chartData, [dateString, chainsAssets]) => {
const date = new Date(dateString);
if (!startDate || date >= startDate) {
const data: CumulativeTVLChartData = {
date: date,
totalTVL: 0,
tvlByChain: {},
};
Object.entries(chainsAssets).forEach(([chainId, lockedAssets]) => {
const notional = lockedAssets["*"].Notional;
if (chainId === "*") {
data.totalTVL = notional;
} else {
data.tvlByChain[chainId] = notional;
}
});
chartData.push(data);
}
return chartData;
},
[]
)
.sort((a, z) => a.date.getTime() - z.date.getTime());
};
export interface TransferChartData {
date: Date;
totalTransferred: number;
transferredByChain: {
[chainId: string]: number;
};
}
export const createTransferChartData = (
notionalTransferredFrom: NotionalTransferredFrom,
timeFrame: TimeFrame
) => {
const startDate = getStartDate(timeFrame);
return Object.keys(notionalTransferredFrom.Daily)
.sort()
.reduce<TransferChartData[]>((chartData, dateString) => {
const transferFromData = notionalTransferredFrom.Daily[dateString];
const data: TransferChartData = {
date: new Date(dateString),
totalTransferred: 0,
transferredByChain: {},
};
Object.entries(transferFromData).forEach(([chainId, amount]) => {
if (chainId === "*") {
data.totalTransferred = amount;
} else {
data.transferredByChain[chainId] = amount;
}
});
chartData.push(data);
return chartData;
}, [])
.filter((value) => !startDate || startDate <= value.date);
};
export interface TransactionData {
date: Date;
totalTransactions: number;
transactionsByChain: {
[chainId: string]: number;
};
}
export const createTransactionData = (totals: Totals, timeFrame: TimeFrame) => {
const startDate = getStartDate(timeFrame);
return Object.keys(totals.DailyTotals)
.sort()
.reduce<TransactionData[]>((chartData, dateString) => {
const groupByKeys = totals.DailyTotals[dateString];
const data: TransactionData = {
date: new Date(dateString),
totalTransactions: 0,
transactionsByChain: {},
};
VAA_EMITTER_ADDRESSES.forEach((address) => {
const count = groupByKeys[address] || 0;
data.totalTransactions += count;
const chainId = address.slice(0, address.indexOf(":"));
if (data.transactionsByChain[chainId] === undefined) {
data.transactionsByChain[chainId] = 0;
}
data.transactionsByChain[chainId] += count;
});
chartData.push(data);
return chartData;
}, [])
.filter((value) => !startDate || startDate <= value.date);
};
export interface ChainTVLChartData {
chainInfo: ChainInfo;
tvl: number;
tvlRatio: number;
}
export const createChainTVLChartData = (tvl: NotionalTVL) => {
let maxTVL = 0;
const chainTVLs = Object.entries(tvl.AllTime)
.reduce<ChainTVLChartData[]>((chartData, [chainId, assets]) => {
const chainInfo = CHAINS_BY_ID[+chainId as ChainId];
if (chainInfo !== undefined) {
const tvl = assets["*"].Notional;
chartData.push({
chainInfo: chainInfo,
tvl: tvl,
tvlRatio: 0,
});
maxTVL = Math.max(maxTVL, tvl);
}
return chartData;
}, [])
.sort((a, z) => z.tvl - a.tvl);
if (maxTVL > 0) {
chainTVLs.forEach((chainTVL) => {
chainTVL.tvlRatio = (chainTVL.tvl / maxTVL) * 100;
});
}
return chainTVLs;
};

View File

@ -1,187 +0,0 @@
import {
CHAIN_ID_AURORA,
CHAIN_ID_AVAX,
CHAIN_ID_BSC,
CHAIN_ID_ETH,
CHAIN_ID_FANTOM,
CHAIN_ID_OASIS,
CHAIN_ID_POLYGON,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
CHAIN_ID_TERRA2,
} from "@certusone/wormhole-sdk";
import { Container, makeStyles, Paper, Typography } from "@material-ui/core";
import { useMemo } from "react";
import { COLORS } from "../../muiTheme";
import {
getNFTBridgeAddressForChain,
getTokenBridgeAddressForChain,
SOL_CUSTODY_ADDRESS,
SOL_NFT_CUSTODY_ADDRESS,
} from "../../utils/consts";
import HeaderText from "../HeaderText";
import SmartAddress from "../SmartAddress";
import MuiReactTable from "./tableComponents/MuiReactTable";
const useStyles = makeStyles((theme) => ({
flexBox: {
display: "flex",
alignItems: "flex-end",
marginBottom: theme.spacing(4),
textAlign: "left",
[theme.breakpoints.down("sm")]: {
flexDirection: "column",
alignItems: "unset",
},
},
grower: {
flexGrow: 1,
},
explainerContainer: {},
mainPaper: {
backgroundColor: COLORS.whiteWithTransparency,
padding: "2rem",
"& > h, & > p ": {
margin: ".5rem",
},
marginBottom: theme.spacing(8),
},
}));
const CustodyAddresses: React.FC<any> = () => {
const classes = useStyles();
const data = useMemo(() => {
return [
{
chainName: "Ethereum",
chainId: CHAIN_ID_ETH,
tokenAddress: getTokenBridgeAddressForChain(CHAIN_ID_ETH),
nftAddress: getNFTBridgeAddressForChain(CHAIN_ID_ETH),
},
{
chainName: "Solana",
chainId: CHAIN_ID_SOLANA,
tokenAddress: SOL_CUSTODY_ADDRESS,
nftAddress: SOL_NFT_CUSTODY_ADDRESS,
},
{
chainName: "Binance Smart Chain",
chainId: CHAIN_ID_BSC,
tokenAddress: getTokenBridgeAddressForChain(CHAIN_ID_BSC),
nftAddress: getNFTBridgeAddressForChain(CHAIN_ID_BSC),
},
{
chainName: "Terra Classic",
chainId: CHAIN_ID_TERRA,
tokenAddress: getTokenBridgeAddressForChain(CHAIN_ID_TERRA),
nftAddress: null,
},
{
chainName: "Polygon",
chainId: CHAIN_ID_POLYGON,
tokenAddress: getTokenBridgeAddressForChain(CHAIN_ID_POLYGON),
nftAddress: getNFTBridgeAddressForChain(CHAIN_ID_POLYGON),
},
{
chainName: "Avalanche",
chainId: CHAIN_ID_AVAX,
tokenAddress: getTokenBridgeAddressForChain(CHAIN_ID_AVAX),
nftAddress: getNFTBridgeAddressForChain(CHAIN_ID_AVAX),
},
{
chainName: "Oasis",
chainId: CHAIN_ID_OASIS,
tokenAddress: getTokenBridgeAddressForChain(CHAIN_ID_OASIS),
nftAddress: getNFTBridgeAddressForChain(CHAIN_ID_OASIS),
},
{
chainName: "Fantom",
chainId: CHAIN_ID_FANTOM,
tokenAddress: getTokenBridgeAddressForChain(CHAIN_ID_FANTOM),
nftAddress: getNFTBridgeAddressForChain(CHAIN_ID_FANTOM),
},
{
chainName: "Aurora",
chainId: CHAIN_ID_AURORA,
tokenAddress: getTokenBridgeAddressForChain(CHAIN_ID_AURORA),
nftAddress: getNFTBridgeAddressForChain(CHAIN_ID_AURORA),
},
{
chainName: "Terra",
chainId: CHAIN_ID_TERRA2,
tokenAddress: getTokenBridgeAddressForChain(CHAIN_ID_TERRA2),
nftAddress: null,
},
];
}, []);
const tvlColumns = useMemo(() => {
return [
{ Header: "Chain", accessor: "chainName", disableGroupBy: true },
{
Header: "Token Address",
id: "tokenAddress",
accessor: "address",
disableGroupBy: true,
Cell: (value: any) =>
value.row?.original?.tokenAddress && value.row?.original?.chainId ? (
<SmartAddress
chainId={value.row?.original?.chainId}
address={value.row?.original?.tokenAddress}
/>
) : (
""
),
},
{
Header: "NFT Address",
id: "nftAddress",
accessor: "address",
disableGroupBy: true,
Cell: (value: any) =>
value.row?.original?.nftAddress && value.row?.original?.chainId ? (
<SmartAddress
chainId={value.row?.original?.chainId}
address={value.row?.original?.nftAddress}
/>
) : (
""
),
},
];
}, []);
const header = (
<div className={classes.flexBox}>
<div className={classes.explainerContainer}>
<Typography variant="h4">Custody Addresses</Typography>
<Typography variant="subtitle1" color="textSecondary">
These are the custody addresses which hold collateralized assets for
the token bridge.
</Typography>
</div>
<div className={classes.grower} />
</div>
);
const table = (
<MuiReactTable
columns={tvlColumns}
data={data || []}
skipPageReset={false}
initialState={{}}
/>
);
return (
<Container maxWidth="lg">
<Container maxWidth="md">
<HeaderText white>Custody</HeaderText>
</Container>
{header}
<Paper className={classes.mainPaper}>{table}</Paper>
</Container>
);
};
export default CustodyAddresses;

View File

@ -1,293 +0,0 @@
import {
Button,
CircularProgress,
makeStyles,
Paper,
Typography,
} from "@material-ui/core";
import clsx from "clsx";
import numeral from "numeral";
import { useCallback, useEffect, useMemo, useState } from "react";
import useNFTTVL from "../../hooks/useNFTTVL";
import {
BETA_CHAINS,
CHAINS_WITH_NFT_SUPPORT,
getNFTBridgeAddressForChain,
} from "../../utils/consts";
import NFTViewer from "../TokenSelectors/NFTViewer";
import MuiReactTable from "./tableComponents/MuiReactTable";
import {
//DENY_LIST,
ALLOW_LIST,
} from "./nftLists";
import { COLORS } from "../../muiTheme";
const useStyles = makeStyles((theme) => ({
logoPositioner: {
height: "30px",
width: "30px",
maxWidth: "30px",
marginRight: theme.spacing(1),
display: "flex",
alignItems: "center",
},
logo: {
maxHeight: "100%",
maxWidth: "100%",
},
tokenContainer: {
display: "flex",
justifyContent: "flex-start",
alignItems: "center",
},
flexBox: {
display: "flex",
alignItems: "flex-end",
marginBottom: theme.spacing(4),
textAlign: "left",
[theme.breakpoints.down("sm")]: {
flexDirection: "column",
alignItems: "unset",
},
},
grower: {
flexGrow: 1,
},
explainerContainer: {},
totalContainer: {
display: "flex",
alignItems: "flex-end",
paddingBottom: 1, // line up with left text bottom
[theme.breakpoints.down("sm")]: {
marginTop: theme.spacing(1),
},
},
totalValue: {
marginLeft: theme.spacing(0.5),
marginBottom: "-.125em", // line up number with label
},
tableBox: {
display: "flex",
justifyContent: "center",
"& > *": {
margin: theme.spacing(1),
},
flexWrap: "wrap",
},
randomButton: {
margin: "0px auto 8px",
display: "block",
},
randomNftContainer: {
minHeight: "550px",
maxWidth: "100%",
},
alignCenter: {
margin: "0 auto",
display: "block",
},
tableContainer: {
flexGrow: 1,
width: "fit-content",
maxWidth: "100%",
},
mainPaper: {
backgroundColor: COLORS.whiteWithTransparency,
padding: "2rem",
"& > h, & > p ": {
margin: ".5rem",
},
marginBottom: theme.spacing(2),
},
}));
const NFTStats: React.FC<any> = () => {
const classes = useStyles();
const nftTVL = useNFTTVL();
//Disable this to quickly turn off
//TODO also change what data is fetched off this
const enableRandomNFT = true;
const [randomNumber, setRandomNumber] = useState<number | null>(null);
const randomNft = useMemo(
() =>
(randomNumber !== null && nftTVL.data && nftTVL.data[randomNumber]) ||
null,
[randomNumber, nftTVL.data]
);
const genRandomNumber = useCallback(() => {
if (!nftTVL || !nftTVL.data || !nftTVL.data?.length || nftTVL.isFetching) {
setRandomNumber(null);
} else {
let found = false;
let nextNumber = Math.floor(Math.random() * nftTVL.data.length);
while (!found) {
if (!nftTVL.data) {
return null;
}
const item = nftTVL?.data[nextNumber]?.mintKey?.toLowerCase() || null;
if (ALLOW_LIST.find((x) => x.toLowerCase() === item)) {
found = true;
} else {
nextNumber = Math.floor(Math.random() * nftTVL.data.length);
}
}
setRandomNumber(nextNumber);
}
}, [nftTVL]);
useEffect(() => {
genRandomNumber();
}, [nftTVL.isFetching, genRandomNumber]);
const data = useMemo(() => {
const output: any[] = [];
if (nftTVL.data && !nftTVL.isFetching) {
CHAINS_WITH_NFT_SUPPORT.filter(
(chain) => !BETA_CHAINS.find((x) => x === chain.id)
).forEach((chain) => {
output.push({
nfts: nftTVL?.data?.filter((x) => x.chainId === chain.id),
chainName: chain.name,
chainId: chain.id,
chainLogo: chain.logo,
contractAddress: getNFTBridgeAddressForChain(chain.id),
});
});
}
return output;
}, [nftTVL]);
//Generate allow list
// useEffect(() => {
// const output: string[] = [];
// if (nftTVL.data) {
// nftTVL.data.forEach((item) => {
// if (
// !DENY_LIST.find((x) => x.toLowerCase() === item.mintKey.toLowerCase())
// ) {
// if (!output.includes(item.mintKey)) {
// output.push(item.mintKey);
// }
// }
// });
// }
// console.log(JSON.stringify(output));
// }, [nftTVL.data]);
const tvlColumns = useMemo(() => {
return [
{ Header: "Chain", accessor: "chainName", disableGroupBy: true },
// {
// Header: "Address",
// accessor: "contractAddress",
// disableGroupBy: true,
// Cell: (value: any) =>
// value.row?.original?.contractAddress &&
// value.row?.original?.chainId ? (
// <SmartAddress
// chainId={value.row?.original?.chainId}
// address={value.row?.original?.contractAddress}
// />
// ) : (
// ""
// ),
// },
{
Header: "NFTs Locked",
id: "nftCount",
accessor: "nftCount",
align: "right",
disableGroupBy: true,
Cell: (value: any) =>
value.row?.original?.nfts?.length !== undefined
? numeral(value.row?.original?.nfts?.length).format("0 a")
: "",
},
];
}, []);
const header = (
<div className={classes.flexBox}>
<div className={classes.explainerContainer}>
<Typography variant="h4">Total NFTs Locked</Typography>
<Typography variant="subtitle1" color="textSecondary">
These NFTs are currently locked by the NFT Bridge contracts.
</Typography>
</div>
<div className={classes.grower} />
{!nftTVL.isFetching ? (
<div
className={clsx(classes.explainerContainer, classes.totalContainer)}
>
<Typography
variant="body2"
color="textSecondary"
component="div"
noWrap
>
{"Total "}
</Typography>
<Typography
variant="h3"
component="div"
noWrap
className={classes.totalValue}
>
{nftTVL.data?.length || "0"}
</Typography>
</div>
) : null}
</div>
);
const table = (
<MuiReactTable
columns={tvlColumns}
data={data || []}
skipPageReset={false}
initialState={{ sortBy: [{ id: "nftCount", desc: true }] }}
/>
);
const randomNFTContent =
enableRandomNFT && randomNft ? (
<div className={classes.randomNftContainer}>
<Button
className={classes.randomButton}
variant="outlined"
onClick={genRandomNumber}
>
Load Random Wormhole NFT
</Button>
<NFTViewer chainId={randomNft.chainId} value={randomNft} />
</div>
) : null;
// const allNfts =
// nftTVL?.data?.map((thing) => (
// <NFTViewer chainId={thing.chainId} value={thing} />
// )) || [];
return (
<>
{header}
<Paper className={classes.mainPaper}>
{nftTVL.isFetching ? (
<CircularProgress className={classes.alignCenter} />
) : (
<div className={classes.tableBox}>
<div className={classes.tableContainer}>{table}</div>
{randomNFTContent}
</div>
)}
{/* {allNfts} */}
</Paper>
</>
);
};
export default NFTStats;

View File

@ -1,298 +0,0 @@
import {
Button,
Checkbox,
CircularProgress,
FormControl,
ListItemText,
makeStyles,
MenuItem,
Paper,
Select,
TextField,
Tooltip,
Typography,
withStyles,
} from "@material-ui/core";
import { ToggleButton, ToggleButtonGroup } from "@material-ui/lab";
import { useCallback, useMemo, useState } from "react";
import TVLAreaChart from "./Charts/TVLAreaChart";
import useCumulativeTVL from "../../hooks/useCumulativeTVL";
import { TIME_FRAMES } from "./Charts/TimeFrame";
import TVLLineChart from "./Charts/TVLLineChart";
import { ChainInfo, CHAINS_BY_ID } from "../../utils/consts";
import { ChainId } from "@certusone/wormhole-sdk";
import { COLORS } from "../../muiTheme";
import TVLBarChart from "./Charts/TVLBarChart";
import TVLTable from "./Charts/TVLTable";
import useTVL from "../../hooks/useTVL";
import { ArrowBack, InfoOutlined } from "@material-ui/icons";
const useStyles = makeStyles((theme) => ({
description: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: "16px",
[theme.breakpoints.down("xs")]: {
flexDirection: "column",
},
},
displayBy: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
flexWrap: "wrap",
marginBottom: "16px",
[theme.breakpoints.down("xs")]: {
justifyContent: "center",
columnGap: 8,
rowGap: 8,
},
},
mainPaper: {
display: "flex",
flexDirection: "column",
backgroundColor: COLORS.whiteWithTransparency,
padding: "2rem",
marginBottom: theme.spacing(8),
borderRadius: 8,
},
toggleButton: {
textTransform: "none",
},
tooltip: {
margin: 8,
},
alignCenter: {
margin: "0 auto",
display: "block",
},
}));
const tooltipStyles = {
tooltip: {
minWidth: "max-content",
borderRadius: "4px",
backgroundColor: "#5EA1EC",
color: "#0F0C48",
fontSize: "14px",
},
};
const StyledTooltip = withStyles(tooltipStyles)(Tooltip);
const DISPLAY_BY_VALUES = ["Time", "Chain"];
const TVLStats = () => {
const classes = useStyles();
const [displayBy, setDisplayBy] = useState(DISPLAY_BY_VALUES[0]);
const [timeFrame, setTimeFrame] = useState("All time");
const [selectedChains, setSelectedChains] = useState<ChainId[]>([]);
const [selectedChainDetail, setSelectedChainDetail] =
useState<ChainInfo | null>(null);
const cumulativeTVL = useCumulativeTVL();
const tvl = useTVL();
const tvlAllTime = useMemo(() => {
return tvl.data
? new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
}).format(
tvl.data.AllTime[selectedChainDetail?.id || "*"]["*"].Notional || 0
)
: "";
}, [selectedChainDetail, tvl]);
const availableChains = useMemo(() => {
const chainIds = cumulativeTVL.data
? Object.keys(
Object.values(cumulativeTVL.data.DailyLocked)[0] || {}
).reduce<ChainId[]>((chainIds, key) => {
if (key !== "*") {
const chainId = parseInt(key) as ChainId;
if (CHAINS_BY_ID[chainId]) {
chainIds.push(chainId);
}
}
return chainIds;
}, [])
: [];
setSelectedChains(chainIds);
return chainIds;
}, [cumulativeTVL]);
const handleDisplayByChange = useCallback((event, nextValue) => {
if (nextValue) {
setDisplayBy(nextValue);
}
}, []);
const handleTimeFrameChange = useCallback(
(event) => setTimeFrame(event.target.value),
[]
);
const handleSelectedChainsChange = useCallback(
(event) => {
const value = event.target.value;
if (value[value.length - 1] === "all") {
setSelectedChains((prevValue) =>
prevValue.length === availableChains.length ? [] : availableChains
);
} else {
setSelectedChains(value);
}
},
[availableChains]
);
const handleChainDetailSelected = useCallback((chainInfo: ChainInfo) => {
setSelectedChainDetail(chainInfo);
}, []);
const allChainsSelected = selectedChains.length === availableChains.length;
const tvlText =
"Total Value Locked" +
(selectedChainDetail ? ` on ${selectedChainDetail?.name}` : "");
const tooltipText = selectedChainDetail
? `Total Value Locked on ${selectedChainDetail?.name}`
: "USD equivalent value of all assets locked in Portal";
return (
<>
<div className={classes.description}>
<Typography variant="h3">
{tvlText}
<StyledTooltip title={tooltipText} className={classes.tooltip}>
<InfoOutlined />
</StyledTooltip>
</Typography>
<Typography variant="h3">{tvlAllTime}</Typography>
</div>
<div className={classes.displayBy}>
{!selectedChainDetail ? (
<div>
<Typography display="inline" style={{ marginRight: "8px" }}>
Display by
</Typography>
<ToggleButtonGroup
value={displayBy}
exclusive
onChange={handleDisplayByChange}
>
{DISPLAY_BY_VALUES.map((value) => (
<ToggleButton
key={value}
value={value}
className={classes.toggleButton}
>
{value}
</ToggleButton>
))}
</ToggleButtonGroup>
</div>
) : null}
{displayBy === "Time" && !selectedChainDetail ? (
<div>
<FormControl>
<Select
multiple
variant="outlined"
value={selectedChains}
onChange={handleSelectedChainsChange}
renderValue={(selected: any) =>
selected.length === availableChains.length
? "All chains"
: selected.length > 1
? `${selected.length} chains`
: //@ts-ignore
CHAINS_BY_ID[selected[0]]?.name
}
MenuProps={{ getContentAnchorEl: null }} // hack to prevent popup menu from moving
style={{ minWidth: 128 }}
>
<MenuItem value="all">
<Checkbox
checked={availableChains.length > 0 && allChainsSelected}
indeterminate={
selectedChains.length > 0 &&
selectedChains.length < availableChains.length
}
/>
<ListItemText primary="All chains" />
</MenuItem>
{availableChains.map((option) => (
<MenuItem key={option} value={option}>
<Checkbox checked={selectedChains.indexOf(option) > -1} />
<ListItemText primary={CHAINS_BY_ID[option]?.name} />
</MenuItem>
))}
</Select>
</FormControl>
<TextField
select
variant="outlined"
value={timeFrame}
onChange={handleTimeFrameChange}
style={{ marginLeft: 8 }}
>
{Object.keys(TIME_FRAMES).map((timeFrame) => (
<MenuItem key={timeFrame} value={timeFrame}>
{timeFrame}
</MenuItem>
))}
</TextField>
</div>
) : selectedChainDetail ? (
<Button
startIcon={<ArrowBack />}
onClick={() => {
setSelectedChainDetail(null);
}}
>
Back to all chains
</Button>
) : null}
</div>
<Paper className={classes.mainPaper}>
{displayBy === "Time" ? (
cumulativeTVL.data ? (
allChainsSelected ? (
<TVLAreaChart
cumulativeTVL={cumulativeTVL.data}
timeFrame={TIME_FRAMES[timeFrame]}
/>
) : (
<TVLLineChart
cumulativeTVL={cumulativeTVL.data}
timeFrame={TIME_FRAMES[timeFrame]}
selectedChains={selectedChains}
/>
)
) : (
<CircularProgress className={classes.alignCenter} />
)
) : tvl.data ? (
selectedChainDetail ? (
<TVLTable chainInfo={selectedChainDetail} tvl={tvl.data} />
) : (
<TVLBarChart
tvl={tvl.data}
onChainSelected={handleChainDetailSelected}
/>
)
) : (
<CircularProgress className={classes.alignCenter} />
)}
</Paper>
</>
);
};
export default TVLStats;

View File

@ -1,158 +0,0 @@
import {
CircularProgress,
Link,
makeStyles,
Paper,
Typography,
} from "@material-ui/core";
import clsx from "clsx";
import numeral from "numeral";
import useTransactionCount from "../../hooks/useTransactionCount";
import { COLORS } from "../../muiTheme";
import { WORMHOLE_EXPLORER_BASE } from "../../utils/consts";
const useStyles = makeStyles((theme) => ({
logoPositioner: {
height: "30px",
width: "30px",
maxWidth: "30px",
marginRight: theme.spacing(1),
display: "flex",
alignItems: "center",
},
logo: {
maxHeight: "100%",
maxWidth: "100%",
},
tokenContainer: {
display: "flex",
justifyContent: "flex-start",
alignItems: "center",
},
flexBox: {
display: "flex",
alignItems: "flex-end",
marginBottom: theme.spacing(4),
textAlign: "left",
[theme.breakpoints.down("sm")]: {
flexDirection: "column",
alignItems: "unset",
},
},
grower: {
flexGrow: 1,
},
alignCenter: {
margin: "0 auto",
display: "block",
textAlign: "center",
},
totalsBox: {
display: "flex",
flexWrap: "wrap",
width: "100%",
justifyContent: "space-evenly",
alignItems: "center",
},
totalContainer: {
paddingLeft: theme.spacing(1),
paddingRight: theme.spacing(1),
textAlign: "center",
marginBottom: theme.spacing(2),
},
totalValue: {
fontWeight: 600,
fontFamily: "Suisse BP Intl, sans-serif",
},
typog: {
marginTop: theme.spacing(3),
},
mainPaper: {
backgroundColor: COLORS.whiteWithTransparency,
padding: "2rem",
"& > h, & > p ": {
margin: ".5rem",
},
marginBottom: theme.spacing(8),
},
}));
const TransactionMetrics: React.FC<any> = () => {
const transactionCount = useTransactionCount();
const classes = useStyles();
const isFetching = transactionCount.isFetching;
const header = (
<div className={classes.flexBox}>
<div>
<Typography variant="h4">Transaction Count</Typography>
<Typography variant="subtitle1" color="textSecondary">
This is how many transactions the Token Bridge has processed.
</Typography>
</div>
<div className={classes.grower} />
</div>
);
const content = (
<div className={classes.totalsBox}>
<div className={classes.totalContainer}>
<Typography variant="subtitle2" component="div" noWrap>
{"Last 48 Hours"}
</Typography>
<Typography
variant="h2"
component="div"
noWrap
className={classes.totalValue}
>
{numeral(transactionCount.data?.total48h || 0).format("0,0")}
</Typography>
</div>
<div className={classes.totalContainer}>
<Typography variant="subtitle2" component="div" noWrap>
{"All Time"}
</Typography>
<Typography
variant="h2"
component="div"
noWrap
className={classes.totalValue}
>
{numeral(transactionCount.data?.totalAllTime || 0).format("0,0")}
</Typography>
</div>
</div>
);
const networkExplorer = (
<Typography
variant="subtitle1"
className={clsx(classes.alignCenter, classes.typog)}
>
To see metrics for the entire Wormhole Network (not just this bridge),
check out the{" "}
<Link href={WORMHOLE_EXPLORER_BASE} target="_blank">
Wormhole Network Explorer
</Link>
</Typography>
);
return (
<>
{header}
<Paper className={classes.mainPaper}>
{isFetching ? (
<CircularProgress className={classes.alignCenter} />
) : (
<>
{content}
{networkExplorer}
</>
)}
</Paper>
</>
);
};
export default TransactionMetrics;

View File

@ -1,319 +0,0 @@
import { ChainId } from "@certusone/wormhole-sdk";
import {
Checkbox,
CircularProgress,
FormControl,
ListItemText,
makeStyles,
MenuItem,
Paper,
Select,
TextField,
Tooltip,
Typography,
withStyles,
} from "@material-ui/core";
import { InfoOutlined } from "@material-ui/icons";
import { ToggleButton, ToggleButtonGroup } from "@material-ui/lab";
import { useCallback, useMemo, useState } from "react";
import useNotionalTransferred from "../../hooks/useNotionalTransferred";
import { COLORS } from "../../muiTheme";
import { CHAINS_BY_ID } from "../../utils/consts";
import { TIME_FRAMES } from "./Charts/TimeFrame";
import {
createTransferChartData,
createTransactionData,
formatTransactionCount,
} from "./Charts/utils";
import VolumeAreaChart from "./Charts/VolumeAreaChart";
import VolumeStackedBarChart from "./Charts/VolumeStackedBarChart";
import VolumeLineChart from "./Charts/VolumeLineChart";
import TransactionsAreaChart from "./Charts/TransactionsAreaChart";
import TransactionsLineChart from "./Charts/TransactionsLineChart";
import useTransactionTotals from "../../hooks/useTransactionTotals";
const DISPLAY_BY_VALUES = ["Dollar", "Percent", "Transactions"];
const useStyles = makeStyles((theme) => ({
description: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: "16px",
[theme.breakpoints.down("xs")]: {
flexDirection: "column",
},
},
displayBy: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
flexWrap: "wrap",
marginBottom: "16px",
[theme.breakpoints.down("xs")]: {
justifyContent: "center",
columnGap: 8,
rowGap: 8,
},
},
mainPaper: {
display: "flex",
flexDirection: "column",
backgroundColor: COLORS.whiteWithTransparency,
padding: "2rem",
marginBottom: theme.spacing(8),
borderRadius: 8,
},
toggleButton: {
textTransform: "none",
},
tooltip: {
margin: 8,
},
alignCenter: {
margin: "0 auto",
display: "block",
},
}));
const tooltipStyles = {
tooltip: {
minWidth: "max-content",
borderRadius: "4px",
backgroundColor: "#5EA1EC",
color: "#0F0C48",
fontSize: "14px",
},
};
const StyledTooltip = withStyles(tooltipStyles)(Tooltip);
const VolumeStats = () => {
const classes = useStyles();
const [displayBy, setDisplayBy] = useState(DISPLAY_BY_VALUES[0]);
const [timeFrame, setTimeFrame] = useState("All time");
const [selectedChains, setSelectedChains] = useState<ChainId[]>([]);
const notionalTransferred = useNotionalTransferred();
const [transferData, transferredAllTime] = useMemo(() => {
const transferData = notionalTransferred.data
? createTransferChartData(
notionalTransferred.data,
TIME_FRAMES[timeFrame]
)
: [];
const transferredAllTime = transferData.reduce((sum, value) => {
return sum + value.totalTransferred;
}, 0);
const transferredAllTimeString = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
}).format(transferredAllTime);
return [transferData, transferredAllTimeString];
}, [notionalTransferred, timeFrame]);
const transactionTotals = useTransactionTotals();
const [transactionData, transactionsAllTime] = useMemo(() => {
const transactionData = transactionTotals.data
? createTransactionData(transactionTotals.data, TIME_FRAMES[timeFrame])
: [];
const transactionsAllTime = formatTransactionCount(
transactionData.reduce((sum, value) => {
return sum + value.totalTransactions;
}, 0)
);
return [transactionData, transactionsAllTime];
}, [transactionTotals, timeFrame]);
const availableChains = useMemo(() => {
const chainIds = notionalTransferred.data
? Object.keys(
Object.values(notionalTransferred.data.Daily)[0] || {}
).reduce<ChainId[]>((chainIds, key) => {
if (key !== "*") {
const chainId = parseInt(key) as ChainId;
if (CHAINS_BY_ID[chainId] !== undefined) {
chainIds.push(chainId);
}
}
return chainIds;
}, [])
: [];
setSelectedChains(chainIds);
return chainIds;
}, [notionalTransferred]);
const handleDisplayByChange = useCallback((event, nextValue) => {
if (nextValue !== null) {
setDisplayBy(nextValue);
}
}, []);
const handleTimeFrameChange = useCallback(
(event) => setTimeFrame(event.target.value),
[]
);
const handleSelectedChainsChange = useCallback(
(event) => {
const value = event.target.value;
if (value[value.length - 1] === "all") {
setSelectedChains((prevValue) =>
prevValue.length === availableChains.length ? [] : availableChains
);
} else {
setSelectedChains(value);
}
},
[availableChains]
);
const allChainsSelected = selectedChains.length === availableChains.length;
return (
<>
<div className={classes.description}>
<Typography variant="h3">
{displayBy === "Transactions"
? "Transaction Count"
: "Outbound Volume"}
<StyledTooltip
title={
displayBy === "Transactions"
? "Total number of transactions the Token Bridge has processed"
: "Amount of assets bridged through Portal in the outbound direction"
}
className={classes.tooltip}
>
<InfoOutlined />
</StyledTooltip>
</Typography>
<Typography variant="h3">
{displayBy === "Transactions"
? transactionsAllTime
: transferredAllTime}
</Typography>
</div>
<div className={classes.displayBy}>
<div>
<Typography display="inline" style={{ marginRight: "8px" }}>
Display by
</Typography>
<ToggleButtonGroup
value={displayBy}
exclusive
onChange={handleDisplayByChange}
>
{DISPLAY_BY_VALUES.map((value) => (
<ToggleButton
key={value}
value={value}
className={classes.toggleButton}
>
{value}
</ToggleButton>
))}
</ToggleButtonGroup>
</div>
<div>
<FormControl>
<Select
multiple
variant="outlined"
value={selectedChains}
onChange={handleSelectedChainsChange}
renderValue={(selected: any) =>
selected.length === availableChains.length
? "All chains"
: selected.length > 1
? `${selected.length} chains`
: //@ts-ignore
CHAINS_BY_ID[selected[0]]?.name
}
MenuProps={{ getContentAnchorEl: null }} // hack to prevent popup menu from moving
style={{ minWidth: 128 }}
>
<MenuItem value="all">
<Checkbox
checked={availableChains.length > 0 && allChainsSelected}
indeterminate={
selectedChains.length > 0 &&
selectedChains.length < availableChains.length
}
/>
<ListItemText primary="All chains" />
</MenuItem>
{availableChains.map((option) => (
<MenuItem key={option} value={option}>
<Checkbox checked={selectedChains.indexOf(option) > -1} />
<ListItemText primary={CHAINS_BY_ID[option]?.name} />
</MenuItem>
))}
</Select>
</FormControl>
<TextField
select
variant="outlined"
value={timeFrame}
onChange={handleTimeFrameChange}
style={{ marginLeft: 8 }}
>
{Object.keys(TIME_FRAMES).map((timeFrame) => (
<MenuItem key={timeFrame} value={timeFrame}>
{timeFrame}
</MenuItem>
))}
</TextField>
</div>
</div>
<Paper className={classes.mainPaper}>
{displayBy === "Dollar" ? (
notionalTransferred.data ? (
allChainsSelected ? (
<VolumeAreaChart
transferData={transferData}
timeFrame={TIME_FRAMES[timeFrame]}
/>
) : (
<VolumeLineChart
transferData={transferData}
timeFrame={TIME_FRAMES[timeFrame]}
chains={selectedChains}
/>
)
) : (
<CircularProgress className={classes.alignCenter} />
)
) : displayBy === "Percent" ? (
<VolumeStackedBarChart
transferData={transferData}
timeFrame={TIME_FRAMES[timeFrame]}
selectedChains={selectedChains}
/>
) : transactionTotals.data ? (
allChainsSelected ? (
<TransactionsAreaChart
transactionData={transactionData}
timeFrame={TIME_FRAMES[timeFrame]}
/>
) : (
<TransactionsLineChart
transactionData={transactionData}
timeFrame={TIME_FRAMES[timeFrame]}
chains={selectedChains}
/>
)
) : (
<CircularProgress className={classes.alignCenter} />
)}
</Paper>
</>
);
};
export default VolumeStats;

View File

@ -1,18 +0,0 @@
import { Container } from "@material-ui/core";
import HeaderText from "../HeaderText";
import TVLStats from "./TVLStats";
import VolumeStats from "./VolumeStats";
const StatsRoot = () => {
return (
<Container maxWidth="lg">
<Container maxWidth="md">
<HeaderText white>Stats</HeaderText>
</Container>
<TVLStats />
<VolumeStats />
</Container>
);
};
export default StatsRoot;

View File

@ -1,283 +0,0 @@
export const DENY_LIST = [
"D9cX654dGb4GFzqq3RY7rhZbRkQqUkfggDZdnYxqv97g",
"0xfeA43A080297B02F2eBB88a27Cb0FA6DB1b33B1d",
"GUSNP2z7nXjMpmYWeFbS819VwHb5tp7VoGbC6iGHtXmE",
"ERfkrhhgBB6zRo3x4XAcQKjhxu1JdX1PBfdj1RbHsfc7",
"f8DrFDG8dd6kV34WdjoEZd9vZin2pJAck8eMFUXZv1G",
"0x72b6dc1003e154ac71c76d3795a3829cfd5e33b9",
];
export const ALLOW_LIST: string[] = [
"0x915bcb55faf663429fcc1efeb4e346703a91e4b2",
"0xf642d8a98845a25844d3911fa1da1d70587c0acc",
"0x1c60841b70821dca733c9b1a26dbe1a33338bd43",
"0x5603172654f37b509d74c6fe3a3fc087104100d9",
"0x60f80121c31a0d46b5279700f9df786054aa5ee5",
"0xd136ad9eb0a53633156972e3f746ab10d6dced7d",
"0x68d5d4ff0274dd95760e300ef16b81c5eed09842",
"0x4961db588dd962abb20927aa38fa33e5225b3be2",
"0x2e956ed3d7337f4ed4316a6e8f2edf74bf84bb54",
"0xb411d36e034d87558c3f96636dd7f7b62e093a1e",
"0x7592e2f251a7f7da27211625d652092769f43a60",
"0x21b999ec13828edefa243675c64324298c03b831",
"0xad5f6cdda157694439ef9f6dd409424321c74628",
"0x9cf63efbe189091b7e3d364c7f6cfbe06997872b",
"0x7581f8e289f00591818f6c467939da7f9ab5a777",
"0x93b1077d163365e05c9421e8f72547e2fe1e0a5b",
"0x85d39cea74b0baba54d7fd0df42dd3e6e39b1625",
"0x2fb5b875a85713cba312c097d16838c242cadaec",
"0x1e988ba4692e52bc50b375bcc8585b95c48aad77",
"0x0ae3c3a1504e41a6877de1b854c000ec64894bea",
"0xc6904fb685b4dfbdb98a5b70e40863cd9aef33dc",
"0xd58434f33a20661f186ff67626ea6bdf41b80bca",
"0x9bb7da8f68f15ece32fe323246e7a1abac6d895d",
"0xb7f7f6c52f2e2fdb1963eab30438024864c313f6",
"0x2a281305a50627a22ec3e7d82ae656adfee6d964",
"0x346868f7e783e8206335bb14f74ba59a87c44f35",
"0x549d38f104ac46d856c1b2bf2a20d170efdb2a8d",
"0x138ff21a21dfc06fbfccf15f2d9fd290a660e152",
"0x15533781a650f0c34f587cdb60965cdfd16ff624",
"0xaae71bbbaa359be0d81d5cbc9b1e88a8b7c58a94",
"0x4721d66937b16274fac603509e9d61c5372ff220",
"0x72dcccb74cade0aca67739fe0a7956c5dead4a8a",
"0x07fe07226a376e8b74e4da2094537843fdf16318",
"0x1ddb2c0897daf18632662e71fdd2dbdc0eb3a9ec",
"0x2d956093d27621ec0c4628b77eaeac6c734da02c",
"0x312b151a0e87785649ed835d946c2b0de5745c30",
"0x32afc8dc2ff4af284fa5341954050f917357a5f1",
"0x4ba782b05c7d580ab6b896c6a63b8e5de53738b3",
"0x5bc94e9347f3b9be8415bdfd24af16666704e44f",
"0x5ddff6f22ee5df31403b9de994b4c70c8755a8ac",
"0x61f4a37676700f6e9bcbaeb05ff6c2f701c1c702",
"0xcb1f79791088d0f4397c35ec241f91c3727a6dba",
"0xd279d7e46f73961812c4853e065d0096a2657a71",
"0xd901b0618ce2b7f61457ced5621bea4820954ce4",
"0xdf7952b35f24acf7fc0487d01c8d5690a60dba07",
"0xf5db804101d8600c26598a1ba465166c33cdaa4b",
"0x299b6f57922533e1dd9edb8fe76ef632fb2b081e",
"0x36a8377e2bb3ec7d6b0f1675e243e542eb6a4764",
"0x4173eff368153f1f1c87be05226ebca9e5f5748b",
"0x684cd10b02cdade20f1858c6315052d66d1eafc2",
"0x7227e371540cf7b8e512544ba6871472031f3335",
"0xd317cfff093c08a43062b39075e51ac2060317f2",
"FxN1Q3vzdUmkx3qVgsnLiQtRWfYK1MAejEjret9dY68E",
"4V8LnMchSg7LpgNNJxGrcJaheeW5eSiJKRuocxf4YvXn",
"9ayoVUQVpHRcDyrdJ1dDUmkxtY74SYgEo7fMRSbW7GQ6",
"83ENiVFRQvVLAsP2gp4pW9EDphsiorgXaZjSuaWwjQEq",
"2nVCGdoR6xehmoPfTAof7xSvdnGaEKhvXi48BeaXbxNW",
"3mUeDoxSzSpBudaWVygwszkVSHUkeJ7q4C5DGjhi9twL",
"J1JLJjYotfACqTQ8g1vWUW9BHRSkJ3NxTqJiukf3aTmn",
"CECj5imCnAv7avNjSETanhRMxWz5yR2ZN5jfFqAijzmK",
"GHVaMJPwJYrqDaHCGLksvsfGoNaS6NVHL5wc31osASYZ",
"85d8VHCBpER7NeT8Quzcos7iGoZouLkaZNfqfQ5VN3kP",
"EBDEioh6L5UZxuyPPZ9gY3H6fsRqmPEvSXL2EKv588qz",
"BZM9yMv9CXZDYV2JGNWSdNJc6xWGfEHY4vUWX9rc4W4n",
"4cc5svLcu1xyYCGcfwDop64ZMs6WJiCY6JrKF711GDLu",
"8cWC1faRe7fkTMu5wAVQoSbkYChJUNTTCneLhZprqMBj",
"F14j998PJo8CdNU4aS7vEWqm8duNuZAZ6bgrAJhJHPNw",
"DZUjZ8p3QtuSnozqFsuHYTjPFCWkgkgWhD7b71AvbKEC",
"AtjDYmFmnjhh8VMJHAEY3CYMdWCPEDzJzxjjTrMrcY4K",
"GwBPGaMyR8cDKVuGgFUdLqevDkMbKNNKRerhvqJD6hL3",
"B3fYjDpfJ9q7YJfnrjeFgALaiLn5DaobPje3VQZ8zk4a",
"3KeSd9UcWFWq7DQnUWT9tXkgb54yUUCynLzJZkaMoqEX",
"XUhiSfF7NN3s8T4gZByoae7sKKuLfffkgF14G6PYHPH",
"2BLYg54F2ejko6Dz6gDPDDGUMxFPHe4v6oNTsshRnS9S",
"HEr369zRti7fShFufrxg7zpmGsRcWey8XNoGdr6bfFHp",
"997VfMfoCp6uh8ZjJZeSm7yDEP9x2iJVPqrLk2M6XBx4",
"BuUUVQeiCNoChQ2CRAgVuXCS56ZF5m8AtGen8LtcjdTv",
"DHUFjTnK94Yx9MybaosFxFFa4pmvxoo2toHxDXZisthU",
"73nxe85cw4XqQ2o5znPCZDVXL5zf3wpM59boCqnhRzqz",
"BbMsktYU3XDZNTB2jNU75RZPak2pu75jDRudHtjbLBMD",
"2K8p6q9KVkDBcedrXpVgAkr4AH9BAZf2BvvXkQvgC3L1",
"G1k5umwEmgziVLAetgmuGRw6sBRodRCGviRNT5HW3CK6",
"2XNvSJhJjpXBbvmhrnbx3BNMTC3JhAmyC11nkmWkTtuM",
"FBoetbUNf5GVJfnCXDf5uo9HMVwmvuSYBMmEVKTKDFd3",
"8TgS1Z1H3YM2qer6wQfVgfs1zrrkEhN8jwqZwEkuaoQs",
"2P2hiNdVyn6BhC4zhGiT1Ct2Jm47F8eShfkwwEDeerCu",
"8nTjurPHAGSttLJHhfPws5wqnPF2ogNg2BWGyJV9Tpt",
"gJbDs6Mp78jMV2vuwZei6oRaPfVQycYxvZXkc8emKoi",
"55VXhAfPKEG5LhQyakH3eUrxy3zrC6pryQSPy5SMDpRW",
"FKxYYQNyigzTfyig4Gyvxkcq9wFb4FEQPq9KvNLoMQ2R",
"9fhR7uHDtJEWBzE9hiwCemggMiKiZGzMmMS22mG3DNuN",
"Cjduv1s4nUkt8i7syFr8yjUvYa5GiNASVKPnNorPSjyo",
"5Gak12U6pxnyh8akeDY4jdaUBJ6FziEVSTDFDSX97acs",
"8Urs8EzwgvNSyRXgSVkuBAD4B5odLfKih6rTn75YnXes",
"Ft4y4KR1Vf45js7RBWbuT7nTFaZmty11sCTGcfJ4fAsx",
"4iKnL1CTvA3yrcchB17vz3yBSmaj2aLvhxPhtBEXvtdd",
"2KfyLN3iyBHYGajiHzEj75aZqeNM22FA9jh8QaKQbwD2",
"5tBZ5feptkoEWiGhisi1y81gzYg3PAJ9v5QbY6uJ4P9a",
"AJnfVpnmtN3oo5jMT1Rxr9VQdVxLUuF6U35oSFRMtten",
"8TEPXUw4vtZuy2xizHfDcccCfsNMxpADrEBw9FJXe3tJ",
"CVsnX3yUwQoe2WappLEhBbu4vxZAE6Bym33UkKuYgKqf",
"8TxaeiYDUhVL6cJVxHeS5Gx6UpE3G4zsQJMaejHttb5x",
"5usxCLasTHRAJqC2kzqboAzQQA4jibxWPCbb3Xtc5r7z",
"FsBzFoC4YYeAvdRX1d4AmYcwtZbcp3ctGs137AsGzLRe",
"CYPz33SnBXkCf1SfsN5yWk5jRCU7r4m4m236K7LYQYUn",
"DmGzNiorJqWac5cNejfUkk3vxYPkbarsB29y3ZdZ2chi",
"6Z551xuQRipV5tfiLawgALuufBtMYRhHiL8Mco1uoQHp",
"8QFELiySN2tQutqamND7v557BpuE3Nu4h5KLxnaLa92K",
"HciHdyzYoCuhFCgYCvCYsXssJuxkHhE68anbgBvqsa4Y",
"AsVWRy4KfRErkcyif5ZXXvVdTeeWLKJaLo5ZKAfYxuFe",
"DNKy4mE6onmzDYYGd4vP9BuJByDoPYdoFKvHUtveCarn",
"EBms1LbV3o6h4eCTAzyZ4xoBmJ1T4SuhQLLguCJ88LBA",
"7xaMEDQ1gTJhXTZ1LYgoMBEeGXiZNmY3QhPxcnBKjkcS",
"71hXBp4hbpYrVSsKMuFACF5UJMBasCmPybpN3dyNdFxJ",
"EstAdgD62RLLPCsLVgJWxxQAeQAJ8iX7Yid9EJp99CQ7",
"HHodYwygcTYuxBNrQx1aLwfQLkJaAMcioXvMs7LNK6Jf",
"AkydeQuFwTsWj8YrmKdhV4TE1w5Nc9DF5ipgfNGs2k8q",
"AmJ52MAQXAjtFkFdAYkLWKZU3zp2BDryLcbQQ1hMSBcp",
"BQHvhpAwZr83joHaEJrKUY4Lij4ZXqKgi9MzeBeiatbX",
"7oEGhi9YJECDHvfbp41spBwGU1pEfgS5mHo8cXGwfxug",
"364ue6kuFJM6reWX7Lz8LwCxA7TeVbWuMXBFrv1EZYWS",
"88R54Zx8TLM4roQPTrHSpA5BfLFQVu5CcH9DyxWmfxDV",
"F11NLrf3w5WuR8kyPFfgpdG9qMM7QoS9JdkMyFu9B6z",
"DzgpaoTtvcxmfAuceZR3Q4xAJjjHCcFURJ4orMiAP1oB",
"8iNwAG4LCFoxZWmpAPq2AUXmdBAeeGNgxUZnRNJ2oaxZ",
"Jzv1Tp99guHWs4WxmcSV3ty3UHhqwv312Wb7A59Cm6B",
"HqTSsezCJ49VjpcRvGtN4WbLHgywkpqrQZAPPR4sid7P",
"7b9xH9DZ5EjehncbST6Cw2cVVVmJccYsoYa7CmVxV74w",
"FE5zmTuD1zaaDheJf8HxDJS8HFVHaFTHbeonxqrBMYhm",
"JD9NiPVbSHGAanmxnXCcmYaXfa56rWwzvB6TVAXkirYS",
"F2kkKqoUr8gCfjXTUknLQZ8TEvedkNA5s6Ne6XS4CPQf",
"38QnWX1xq83uspnp9nY6cEhdcRcx1dw5VJwxudZ7ugDM",
"Gzkr9pHFCQ3WqkyRMZobPSXZNheFQKYrjJCVs1Xmhj6R",
"jfLuStusES8VBAHqFSBB9XXX5vQjjWxZGyEYdDRS2yz",
"9dFPAaubJzhHFpsc8s4qbSepMFEKecaQYHrUKQMRoZcs",
"6yCBHAmvBAZkFSXi7u8Sj7n9pNPuDFrGahNnLxpAk9YJ",
"A2SgMawSkCjdhQpBypwRpCSryAPa6B65dRetu3YGX4ay",
"9wPZRx3jrYQ2U3AwrxRJ8dQXFgVt8Qc5BxjD2reAD81",
"C6FGVFzzwGqxEGnEqQva4zZ6tdVXHDY4KsPuDFMKQipA",
"28awX2aLeyDik4aYtLe3xgvx5rnA9PeJrUmEepbps1XU",
"7B3esUC5uCw3yLcpvU91q3poeVENLpyfLU7SCZxy7YnF",
"5qoDyfChkPokzd5BXGcbNmqFYwF3dYerLfnN2ujxTCjU",
"5K5hobUAQLYTiKMGqJBJauZWbQJHLnjmwfbYRu3Kdt59",
"pavej29HVNfxHEkDxR9fE8zgCozmFuCFFCk7u9dUfci",
"4hKuAuo8dXgkRLfz78qNHNGs188epLoVmcKPKuDDKFNj",
"AzX2c2bFMeZcnj1Y6DokKAacDnsRzAo6pyVJmMiVUuVQ",
"D6uV9j31HV2yv6Cg6uuJuAeSMVdJ7PpVrzRtm1d5N3pV",
"2VNQusXvHEjMBTkY4gYUUhvdkCMc9DADSSxbdS7sJ1CK",
"Cug1VKTudanAUbUMh4sw1bicJgcfXqs8x4tjHHPtqkEA",
"HQnijpcihTRLRaQpywJfo4cM5pZHBffvTATFiomE8g8u",
"73kJkeEFQYPxT3cHDRG8iiyvUhmSBY2Nc33JpB6DFK2R",
"4rZifPfY8DdPkWv4mqvctLgcGaBScBQHMCPguZQC28C4",
"GZivmjaWrg2Va9seuxY4D7SGPP8WnKsKwwc5a87hMAgn",
"9rMUmiujvmthZoVBaFff2Jf8YW2cX4SzWDK2nritZXK3",
"CkAD1HLVnqhfMeCXzKuEuhjFd4FmTXRtCVe535jJKVH2",
"8tfmWiyX4efiXb3P9ti4GfCZZ5FNcwbRZgzfA688K9tC",
"HXrkcTcRNF6EnvfFaXMpqbSeik7fgbn9DLraBaNmRD5u",
"59j1gA2ANhYkzvvCZauoisnpfnVk6UbycFHMNBij2jy",
"5Dot4FyLAdx9LkxrmG4zi6nrRQQcPZjvBAjZ5VmvYWpp",
"BbL5V1TJuLnMonChPVAfP3uarM5G2XKCfcEaTxzXEqPK",
"4hmEUPcgHimQ1vNrr3WNGYb1RezmRLD6fVv9TVwAU9yj",
"Gnmnh3LNaaQ1UmnQMkdGTH7ja2aTHQyxkS7tCfYkxvtD",
"9BUEx9ULoTQhs3QVzyQvSecxfbTPV8PcuHkHeuRE8LF8",
"6UF8mqUg1aGakkW2yS4oRPiX1YNBJiqxcoyN9u6pBYo9",
"7vEHTQ9mmy7T7fkhns3fBFrWqRhXPTZUPgn64QxVirhY",
"71HGRqeeDwcXJs9CxzPyTurLNdwYwsBBb17CmxoW4VTD",
"5v2fhVgjH2Xv2v78ysSZWvhZ5wu8ycD4pJE6LRBbaf1G",
"7rjPaxZ5p5jzZFhEZwVFCsLwvxppmJ8nmsrgxmRMeKMq",
"7JHruL5Baoqj66RniPDpB1SiKz8jegNuiKLxcvCDVYCa",
"3xA3wJk9YpgwxNuSc9GHQ6E1Wt8zeKC5oK8yQLg2JzrF",
"3c7r6ZtTDxN1fxc1ixM8Ay26hTyfnucs3yx42vVH4LM3",
"4jsaQWBKn3aSpZbwboxiNRRiagY2xdkoSomfoBRDrdcs",
"B9txgFSjNXjFQv5VTCu2vhMyCEWnyp9UQWpeZzTLdeKP",
"96GjikTDDt1FuznnmwE9rzKi2i7gZ3gYwW7g2daW8s2r",
"C4otrRtEHVeeTkkmMy6WkLkMEbtK49BUgkfYXMNiJhGq",
"Dtesdhkc5WFDHvjy7JzrpSL7xbN7DHsQhrK5i8vgzogi",
"Cf2fzzcUhBqtXokdEEg4HVuc4dNmgrsz2J8QsvPwMnFU",
"DoqovGZ86RjsFZd3TGaLDwaU8mXmw3C195BfV31UuG85",
"Au3h7j9twciHo6YQ9vznx97rC1D82dcqdBgBqkzwKMGm",
"93N68g43aiDTJTTQdJL5djSBytTCSqL62woXubFrX2vJ",
"6WwzhMYbAWem22ZcVcS2NohsccjmrWZbq1aHDySABTZ2",
"DGkYVSFoq5cafZJZboHPuzdrKtVYrm4R5oS18nEuZaGu",
"8MLm9nLqLsDQgoN7nHkUoeu2oqe8J3cuZKc2KCX8Zde2",
"9Zn1R4fdAHZSrq6hrXdQWdRVJhAUEtpU9gT26KRgRjCb",
"6j53Ln8GvosfZXar1WCNrmURVxjR8GV9Es2HE6fTvkz9",
"9Kevoh8H31NQV8kB89aocRTrkt2UGqbs7Ani73WkACrf",
"8MTsYKzp4qNEFXzM6nWhRk759qqSMbur5DXjbWwDNFCj",
"CcCVqUaMsKNcQwWHHA3H5yydjcspcD3LhBYP9aYrmweK",
"FCiSbFdcZSpx1YZFfUCV56KQ34C8CJY9A3JrnMGopNip",
"5R5cr6dq3v25Z4Cq7qM2dG4hxN5gbY5eUKzETRnBYXUf",
"DeQDKoAuR1BYvspX8dHehyua3VRuCmD2zbEvZ97NLjas",
"C6nyuPc5wZLB1c3rFvchbF4aLZBn4yujtDyeqsJDhXSL",
"7vzFUmX9qZiznTikem4Eu2v2aa1BkqDsuT3gvYLq2rRT",
"Nf2WRYpG8hquo3BsvioYxKZmDECvuS7HmDhHrpQrdYv",
"Fz5jKMiXi5SN4TqQ12fYm49QFxJkeLoyYEZWVwRwyBCC",
"FCs3xYBoo55HenGpeNETpuWjuPfzegHhDQFzEuu3HWuM",
"CC56nm4aRSd7RzcnrX9VH5VXnfj5Bq1XLGRxXy9oDm2p",
"BLJg1tgRZdzPxXhJngtbCnuJ8PtsVpFjFcoUby2NXxjH",
"4Y3Lfzeh9m45GomuKpqjj6bVRLpNnn7wyosPzRyx8bQW",
"8GCrwThSdG2JUXwwisFtwEs5gkGGxjNKQYArRtq6bEPc",
"78wF4F64K31GbbC9jhi3me4kSqznGtf6KHFLTUTkrEjB",
"Cy57RjcnLmWhY2ccjEwcFu6SvoySbq6mevDhY54DxVLh",
"FVTRVzRagRDQj7Lp2n1GyRJ85TNSErhYk4xf283Svnrn",
"rMH5QQxx9WNZYeCgTEr7TogXT1rnATDaibWcvibHxRp",
"ENFgBr93NsoG9QHUTHbfEobWtnFmV1fEsAuiaNq1Qxiv",
"454LshEu93z3auzq2cMbA7PsSw9v8zhuvQRGkfP6eGMx",
"aHe1GAdAVgijtp55eUmPe6DdUYonRVw6Xdq1QpTmVeM",
"4BG5Yo4o2nHGDgA1AqH94oUk2CouLuGCSK7ZjqHQaT2J",
"CCDKm4AEVxeeNVdYNM7X8CBQmtNpySEhBvp1yQ93jjdY",
"27XFvenPT5LpkJ8FVJdHf3CPaARqgCWm1EsSRt2HrDcH",
"DqH8Q3e5Bts3rCsC6UpTP18Hf1fD7dfGaP71uR6HpxRg",
"FPfgDLbWDjSx1FnEdeM5DWqJNpRqv5oDqpmvW18Ef5QQ",
"6u1TPKaTboYQ8gU7DF7AgEp5cP9N3FTuxxsu5oWi5uJ5",
"CCg1C49hNR91425PeS1HatXobQkAniJ775N6pjWAiqp2",
"ENxmdaEsTWXkufsTLngp3sZ89dFZWNoke7F95aVE67Kz",
"39h7MdBeuMFvcBz5FKGQBqnqR7BSAfp4ob8qLphLc4yo",
"CRGEVCx5HrRjsbX71RM8acuydKHiXwRwWysF9BJvYTG9",
"3mzktRHfhxjPirgHF9sjbXf2kwW1RZSudaRpyc478BAA",
"9J7F9nx2UyVYPuTk21ni2jxprgBpzfMpN7HxJqgKUWxd",
"3K8NyBHkMdWX672bqqtJC5mZASvqCHtZXPRMhKz8iP2x",
"65sAs4U5puLv3UC8f5y1DVCGAhpMtTRUoKpMdUMFY1sY",
"Dxb5mPASvxsoDRhHZEu2arYRqvYByYgyvrtUYxnFJsGg",
"GF2hTcNvU7HFj5682KBVpffoS223aVhYaaCcM9ngxehs",
"BBF6JkFMRLMqW5F4Ri8VUNnAK5T2n9MLmRhtuXQNJmZh",
"8x8J5MGViuLTtxwjwnrLM29USZqx7TcbGvqdaNfA1m8X",
"U4Babgbjm7ciqth5XDJppzVk7WhGnHNZUGvPL2ox8HB",
"3Y9f4MxYEvjWfiBJexTSnRQcKVD8e1aSUs4Z6FZYj5K1",
"CZZ5B1Az9kW2nyRHM5mABS2NWuStoHCPcfh75KFo2UsN",
"tRKmpQWu48phJXR7W4d2skG9W4nvb5cb976bkGQYAuN",
"3iAabhw1Rdu2HimbzT9gDCDcuod7TmBtcZwFWxGQyFfQ",
"5TTRcMNMwhiGUZ8JtTnqB6sW3g6P4fAFhWLsV2kJWEsg",
"8B8VaqcxunjTnbq4mGhPW3C2eHuByWVBRjqKthwX2aRQ",
"9hi9yukj5ZjtD2Z73zRLca8kARChJmgEaMtYefx454Q3",
"DTrqt4Wb1H16hdrLwEfFkNVwGYWPqnghb3pREEZejhtS",
"EitHiKHGWWadEhZahZLHURVoxPqAUofKLEXfYU9NsvMp",
"4ZscrWACeLLYrsFLQjX16bdjCqLy6vkKuHa2w3fsakDe",
"FuPR7rUMzphpQ3YhmENUJUt5gvtDpJDbUqSa6FA82U6p",
"2o9C56tqnc54QF54MxGn38DNNEa9vAMow3bc5VMqogMV",
"Ffgr3YTKvhWkdv2pSW2F5VEVvLQvnatje1ookcuJpBAF",
"J9rKCnTuERL9G95FmFQnYDeLVYrNa7pQBiL13oqGgH3t",
"5FJeEJR8576YxXFdGRAu4NBBFcyfmtjsZrXHSsnzNPdS",
"779kNT4696bMaAkeAHxezAuniTrVjfvrp3vABvhdPGWs",
"4fA5U4w4DfmNojhKKBCVHeAY5VQ4dYpx5uFQSsrknSFA",
"BU2oyceVSZXYzxURwhAvfULBSe59v22LQfuYgS585L1g",
"3BApsMpiSaHfF2a59aW9XAPfGQ8WMaQd7Q1F9rTbhGqv",
"5oWeivCzcQrcmueDbUAfPQRfjGeDqkVDxErchHUz8NCh",
"2rrZbBRpt5o7GKnTHusTqRANsj1vs5t4WC9CQPt2cujb",
"CbN1vKsz99qbQidVR5RfBLmrxPoYbJguEAnsFi5BN5WU",
"CTwRHSdPNGucwLuLqUMSc5ddJbnSN7GjKFYfm91wJHVo",
"6NKWLHZENHKvditMFu7z97qJypmJTsz1PbjFVpsmrXMY",
"5D2kc7J4RNcMzfoAQz1E4NWjLS7GzTnJKvTV3HiospCZ",
"9hrjJvaYETxp6VcRRX4yzT8jtH6gzoVtFUqdaCksisx4",
"2kUMSNYvpWXqrpSV2L1GV5UWYH3CFXF7F9QbGkgv9kNw",
"8nRkgYkufaLQVtuaaPGwrKJBSS7tu55YWx3EiRufD9uU",
"7aLAz1NE2hx7adjrgDs87GT49gwTkmW2n8gHSRJUVzHa",
"G2kuL2iKEdYmZBnM1a8rA5j5si4AqySJUq3ujPaJxkDk",
"FQJUJSHQACz7WLHVM7iS2dKnZVWwFHTuwUdtByLMxXF8",
"AhtJTzNB7zwLB7RxqbYcpAKBeEfxYrczz12Nn4FgVb2y",
"7zEHcoELyPvmipqXL297edddhxLfx25UGTvY8dMrZqzT",
"38mqzHtHLmUMCZyuZKEQB8Zkg2fGkToYw4gVwPC3NoH3",
"3kL3aPezAbZxzamMurWvjMpXkVQgLAgrh9wT1CeS3CnK",
"EB7hzkjRjV3AFCfQDuPK8joneM6dW2a96rJCHvc7p1fW",
"AVbSKg271GmzqrtPmU5fRza9uR2HGLhTy3yiM1mMPTpw",
"CoVJmrq8KeRZGAcKa6h6mueQsFotVkibehWeKGCZBXe",
"5bwpMvAA65frL4A1B3nYQcQjLHGUzHhkHX5FV4CVi1s8",
"Kyi9YmFqBARjW8KwS192hyrfzwyQbCR6utt5yr6hoNg",
"ESrJ2tXdmkGv81FNJmJcezunQLz3fP6WFzfmZd6d4FPn",
"8qMsDP7hFWZAKGA2taL7u8rERH4te4D4i8cxzHKtAMcA",
"5c1ymadNzqM9cE8K8gqDguNYg2jWnGcNBV5S3tGGdhM2",
"Gx4Y9M67BPsAViveoBcm4JocVh82S7pZsaXc4PH9EibE",
"5Xb7BoMNP19cXPgLdzrAkJCarznkYAfpmyyCvynJam7K",
"EZH44QW2BzUijN79rrngRUoGHCJGWitabxkVkT82XpeA",
];

View File

@ -1,174 +0,0 @@
import { IconButton } from "@material-ui/core";
import MaUTable from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TablePagination from "@material-ui/core/TablePagination";
import TableRow from "@material-ui/core/TableRow";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import {
AddCircleOutline,
KeyboardArrowDown,
KeyboardArrowRight,
RemoveCircleOutline,
} from "@material-ui/icons";
import React from "react";
import {
useExpanded,
useGlobalFilter,
useGroupBy,
usePagination,
useSortBy,
useTable,
} from "react-table";
import TablePaginationActions from "./TablePaginationActions";
const stopProp = (e) => {
e.stopPropagation();
};
const EnhancedTable = ({ columns, data, skipPageReset, initialState = {} }) => {
const {
getTableProps,
headerGroups,
prepareRow,
page,
gotoPage,
setPageSize,
rows,
state: { pageIndex, pageSize },
} = useTable(
{
columns,
data,
autoResetPage: !skipPageReset,
initialState,
},
useGlobalFilter,
useGroupBy,
useSortBy,
useExpanded,
usePagination
);
const handlePageChange = (event, newPage) => {
gotoPage(newPage);
};
const handleRowsPerPageChange = (event) => {
setPageSize(Number(event.target.value));
};
// Render the UI for your table
return (
<>
<TableContainer>
<MaUTable {...getTableProps()}>
<TableHead>
{headerGroups.map((headerGroup) => (
<TableRow {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column) => (
<TableCell
{...(column.id === "selection"
? column.getHeaderProps()
: column.getHeaderProps(column.getSortByToggleProps()))}
align={
// TODO: better way to get column?
columns.find((c) => c.Header === column.Header)?.align ||
"left"
}
>
{column.id !== "selection" ? (
<TableSortLabel
active={column.isSorted}
// react-table has a unsorted state which is not treated here
direction={column.isSortedDesc ? "desc" : "asc"}
>
{column.render("Header")}
</TableSortLabel>
) : (
column.render("Header")
)}
{column.canGroupBy ? (
// If the column can be grouped, let's add a toggle
<span onClick={stopProp}>
<IconButton
size="small"
{...column.getGroupByToggleProps()}
>
{column.isGrouped ? (
<RemoveCircleOutline fontSize="inherit" />
) : (
<AddCircleOutline fontSize="inherit" />
)}
</IconButton>
</span>
) : null}
</TableCell>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{page.map((row, i) => {
prepareRow(row);
return (
<TableRow {...row.getRowProps()}>
{row.cells.map((cell) => {
return (
<TableCell
{...cell.getCellProps()}
align={cell.column.align || "left"}
>
{cell.isGrouped ? (
// If it's a grouped cell, add an expander and row count
<>
<IconButton
size="small"
{...row.getToggleRowExpandedProps()}
>
{row.isExpanded ? (
<KeyboardArrowDown fontSize="inherit" />
) : (
<KeyboardArrowRight fontSize="inherit" />
)}
</IconButton>{" "}
{cell.render("Cell")} ({row.subRows.length})
</>
) : cell.isAggregated ? (
// If the cell is aggregated, use the Aggregated
// renderer for cell
cell.render("Aggregated")
) : cell.isPlaceholder ? null : ( // For cells with repeated values, render null
// Otherwise, just render the regular cell
cell.render("Cell")
)}
</TableCell>
);
})}
</TableRow>
);
})}
</TableBody>
</MaUTable>
</TableContainer>
<TablePagination
component="div"
rowsPerPageOptions={[5, 10, 25, { label: "All", value: rows.length }]}
count={rows.length}
rowsPerPage={pageSize}
page={pageIndex}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true,
}}
onPageChange={handlePageChange}
onRowsPerPageChange={handleRowsPerPageChange}
ActionsComponent={TablePaginationActions}
/>
</>
);
};
export default EnhancedTable;

View File

@ -1,88 +0,0 @@
import React from "react";
import FirstPageIcon from "@material-ui/icons/FirstPage";
import IconButton from "@material-ui/core/IconButton";
import KeyboardArrowLeft from "@material-ui/icons/KeyboardArrowLeft";
import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight";
import LastPageIcon from "@material-ui/icons/LastPage";
import { makeStyles, useTheme } from "@material-ui/core/styles";
import PropTypes from "prop-types";
const useStyles = makeStyles((theme) => ({
root: {
flexShrink: 0,
marginLeft: theme.spacing(1),
},
}));
const TablePaginationActions = (props) => {
const classes = useStyles();
const theme = useTheme();
const { count, page, rowsPerPage, onPageChange } = props;
const handleFirstPageButtonClick = (event) => {
onPageChange(event, 0);
};
const handleBackButtonClick = (event) => {
onPageChange(event, page - 1);
};
const handleNextButtonClick = (event) => {
onPageChange(event, page + 1);
};
const handleLastPageButtonClick = (event) => {
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
};
return (
<div className={classes.root}>
<IconButton
onClick={handleFirstPageButtonClick}
disabled={page === 0}
aria-label="first page"
>
{theme.direction === "rtl" ? <LastPageIcon /> : <FirstPageIcon />}
</IconButton>
<IconButton
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
>
{theme.direction === "rtl" ? (
<KeyboardArrowRight />
) : (
<KeyboardArrowLeft />
)}
</IconButton>
<IconButton
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="next page"
>
{theme.direction === "rtl" ? (
<KeyboardArrowLeft />
) : (
<KeyboardArrowRight />
)}
</IconButton>
<IconButton
onClick={handleLastPageButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
>
{theme.direction === "rtl" ? <FirstPageIcon /> : <LastPageIcon />}
</IconButton>
</div>
);
};
TablePaginationActions.propTypes = {
count: PropTypes.number.isRequired,
onPageChange: PropTypes.func.isRequired,
page: PropTypes.number.isRequired,
rowsPerPage: PropTypes.number.isRequired,
};
export default TablePaginationActions;

View File

@ -1,21 +0,0 @@
import { makeStyles, Typography } from "@material-ui/core";
import { ReactChild } from "react";
const useStyles = makeStyles((theme) => ({
description: {
marginBottom: theme.spacing(4),
},
}));
export default function StepDescription({
children,
}: {
children: ReactChild;
}) {
const classes = useStyles();
return (
<Typography component="div" variant="body2" className={classes.description}>
{children}
</Typography>
);
}

View File

@ -1,129 +0,0 @@
import {
Dialog,
DialogTitle,
Divider,
IconButton,
List,
ListItem,
ListItemIcon,
ListItemText,
makeStyles,
} from "@material-ui/core";
import CloseIcon from "@material-ui/icons/Close";
import { ConnectType, useWallet } from "@terra-money/wallet-provider";
import { useCallback } from "react";
const useStyles = makeStyles((theme) => ({
flexTitle: {
display: "flex",
alignItems: "center",
"& > div": {
flexGrow: 1,
marginRight: theme.spacing(4),
},
"& > button": {
marginRight: theme.spacing(-1),
},
},
icon: {
height: 24,
width: 24,
},
}));
const WalletOptions = ({
type,
identifier,
connect,
onClose,
icon,
name,
}: {
type: ConnectType;
identifier: string;
connect: (
type: ConnectType | undefined,
identifier: string | undefined
) => void;
onClose: () => void;
icon: string;
name: string;
}) => {
const classes = useStyles();
const handleClick = useCallback(() => {
connect(type, identifier);
onClose();
}, [connect, onClose, type, identifier]);
return (
<ListItem button onClick={handleClick}>
<ListItemIcon>
<img src={icon} alt={name} className={classes.icon} />
</ListItemIcon>
<ListItemText>{name}</ListItemText>
</ListItem>
);
};
const TerraConnectWalletDialog = ({
isOpen,
onClose,
}: {
isOpen: boolean;
onClose: () => void;
}) => {
const { availableConnections, availableInstallations, connect } = useWallet();
const classes = useStyles();
const filteredConnections = availableConnections
.filter(({ type }) => type !== ConnectType.READONLY)
.map(({ type, name, icon, identifier = "" }) => (
<WalletOptions
type={type}
identifier={identifier}
connect={connect}
onClose={onClose}
icon={icon}
name={name}
key={"connection-" + type + identifier}
/>
));
const filteredInstallations = availableInstallations
.filter(({ type }) => type !== ConnectType.READONLY)
.map(({ type, name, icon, url, identifier = "" }) => (
<ListItem
button
component="a"
onClick={onClose}
key={"install-" + type + identifier}
href={url}
target="_blank"
rel="noreferrer"
>
<ListItemIcon>
<img src={icon} alt={name} className={classes.icon} />
</ListItemIcon>
<ListItemText>{"Install " + name}</ListItemText>
</ListItem>
));
return (
<Dialog open={isOpen} onClose={onClose}>
<DialogTitle>
<div className={classes.flexTitle}>
<div>Select your wallet</div>
<IconButton onClick={onClose}>
<CloseIcon />
</IconButton>
</div>
</DialogTitle>
<List>
{filteredConnections}
{filteredInstallations && <Divider variant="middle" />}
{filteredInstallations}
</List>
</Dialog>
);
};
export default TerraConnectWalletDialog;

View File

@ -1,112 +0,0 @@
import {
MenuItem,
makeStyles,
TextField,
Typography,
ListItemIcon,
} from "@material-ui/core";
import { useConnectedWallet } from "@terra-money/wallet-provider";
import { useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { setTerraFeeDenom } from "../store/feeSlice";
import { selectTerraFeeDenom } from "../store/selectors";
import useTerraNativeBalances from "../hooks/useTerraNativeBalances";
import { formatNativeDenom, getNativeTerraIcon } from "../utils/terra";
import { TerraChainId } from "@certusone/wormhole-sdk";
const useStyles = makeStyles((theme) => ({
feePickerContainer: {
display: "flex",
flexDirection: "column",
margin: `${theme.spacing(1)}px auto`,
maxWidth: 200,
width: "100%",
},
select: {
"& .MuiSelect-root": {
display: "flex",
alignItems: "center",
},
},
listItemIcon: {
minWidth: 40,
},
icon: {
height: 24,
maxWidth: 24,
},
}));
type TerraFeeDenomPickerProps = {
disabled: boolean;
chainId: TerraChainId;
};
export default function TerraFeeDenomPicker(props: TerraFeeDenomPickerProps) {
const terraFeeDenom = useSelector(selectTerraFeeDenom);
const wallet = useConnectedWallet();
const { balances } = useTerraNativeBalances(
props.chainId,
wallet?.walletAddress
);
const dispatch = useDispatch();
const classes = useStyles();
const feeDenomItems = useMemo(() => {
const items = [];
if (balances) {
for (const [denom, amount] of Object.entries(balances)) {
if (amount === "0") continue;
const symbol = formatNativeDenom(denom, props.chainId);
if (symbol) {
items.push({
denom,
symbol,
icon: getNativeTerraIcon(symbol),
});
}
}
}
// prevent an out-of-range value from being selected
if (!items.find((item) => item.denom === terraFeeDenom)) {
const symbol = formatNativeDenom(terraFeeDenom, props.chainId);
items.push({
denom: terraFeeDenom,
symbol,
icon: getNativeTerraIcon(symbol),
});
}
return items;
}, [balances, terraFeeDenom, props.chainId]);
return (
<div className={classes.feePickerContainer}>
<Typography variant="caption">Fee Denomination</Typography>
<TextField
variant="outlined"
size="small"
select
fullWidth
value={terraFeeDenom}
onChange={(event) => dispatch(setTerraFeeDenom(event.target.value))}
disabled={props.disabled}
className={classes.select}
>
{feeDenomItems.map((item) => {
return (
<MenuItem key={item.denom} value={item.denom}>
<ListItemIcon>
<img
src={item.icon}
alt={item.symbol}
className={classes.icon}
/>
</ListItemIcon>
{item.symbol}
</MenuItem>
);
})}
</TextField>
</div>
);
}

View File

@ -1,33 +0,0 @@
import { useConnectedWallet, useWallet } from "@terra-money/wallet-provider";
import { useCallback, useState } from "react";
import TerraConnectWalletDialog from "./TerraConnectWalletDialog";
import ToggleConnectedButton from "./ToggleConnectedButton";
const TerraWalletKey = () => {
const wallet = useWallet();
const connectedWallet = useConnectedWallet();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const connect = useCallback(() => {
setIsDialogOpen(true);
}, [setIsDialogOpen]);
const closeDialog = useCallback(() => {
setIsDialogOpen(false);
}, [setIsDialogOpen]);
return (
<>
<ToggleConnectedButton
connect={connect}
disconnect={wallet.disconnect}
connected={!!connectedWallet}
pk={connectedWallet?.terraAddress || ""}
/>
<TerraConnectWalletDialog isOpen={isDialogOpen} onClose={closeDialog} />
</>
);
};
export default TerraWalletKey;

View File

@ -1,65 +0,0 @@
import { Button, makeStyles, Tooltip } from "@material-ui/core";
import { LinkOff } from "@material-ui/icons";
const useStyles = makeStyles((theme) => ({
button: {
display: "flex",
margin: `${theme.spacing(1)}px auto`,
width: "100%",
maxWidth: 400,
},
icon: {
height: 24,
width: 24,
},
}));
const ToggleConnectedButton = ({
connect,
disconnect,
connected,
pk,
walletIcon,
}: {
connect(): any;
disconnect(): any;
connected: boolean;
pk: string;
walletIcon?: string;
}) => {
const classes = useStyles();
const is0x = pk.startsWith("0x");
return connected ? (
<Tooltip title={pk}>
<Button
color="primary"
variant="contained"
size="small"
onClick={disconnect}
className={classes.button}
startIcon={
walletIcon ? (
<img className={classes.icon} src={walletIcon} alt="Wallet" />
) : (
<LinkOff />
)
}
>
Disconnect {pk.substring(0, is0x ? 6 : 3)}...
{pk.substr(pk.length - (is0x ? 4 : 3))}
</Button>
</Tooltip>
) : (
<Button
color="primary"
variant="contained"
size="small"
onClick={connect}
className={classes.button}
>
Connect
</Button>
);
};
export default ToggleConnectedButton;

View File

@ -1,381 +0,0 @@
import {
ChainId,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
isEVMChain,
nativeToHexString,
} from "@certusone/wormhole-sdk";
import {
Card,
CircularProgress,
Container,
makeStyles,
MenuItem,
TextField,
Typography,
} from "@material-ui/core";
import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
import { useCallback, useMemo, useState } from "react";
import { useBetaContext } from "../contexts/BetaContext";
import useFetchForeignAsset, {
ForeignAssetInfo,
} from "../hooks/useFetchForeignAsset";
import useIsWalletReady from "../hooks/useIsWalletReady";
import useMetadata from "../hooks/useMetadata";
import useOriginalAsset, { OriginalAssetInfo } from "../hooks/useOriginalAsset";
import { COLORS } from "../muiTheme";
import { BETA_CHAINS, CHAINS, CHAINS_BY_ID } from "../utils/consts";
import HeaderText from "./HeaderText";
import KeyAndBalance from "./KeyAndBalance";
import SmartAddress from "./SmartAddress";
import { RegisterNowButtonCore } from "./Transfer/RegisterNowButton";
const useStyles = makeStyles((theme) => ({
flexBox: {
display: "flex",
width: "100%",
justifyContent: "center",
"& > *": {
margin: theme.spacing(2),
},
},
mainCard: {
padding: "32px 32px 16px",
backgroundColor: COLORS.whiteWithTransparency,
},
spacer: {
height: theme.spacing(3),
},
centered: {
textAlign: "center",
},
arrowIcon: {
margin: "0 auto",
fontSize: "70px",
},
resultContainer: {
margin: theme.spacing(2),
},
}));
function PrimaryAssetInfomation({
lookupChain,
lookupAsset,
originChain,
originAsset,
showLoader,
}: {
lookupChain: ChainId;
lookupAsset: string;
originChain: ChainId;
originAsset: string;
showLoader: boolean;
}) {
const classes = useStyles();
const tokenArray = useMemo(() => [originAsset], [originAsset]);
const metadata = useMetadata(originChain, tokenArray);
const nativeContent = (
<div>
<Typography>{`This is not a Portal wrapped token.`}</Typography>
</div>
);
const wrapped = (
<div>
<Typography>{`This is wrapped by Portal! Here is the original token: `}</Typography>
<div className={classes.flexBox}>
<Typography>{`Chain: ${CHAINS_BY_ID[originChain].name}`}</Typography>
<div>
<Typography component="div">
{"Token: "}
<SmartAddress
address={originAsset}
chainId={originChain}
symbol={metadata.data?.get(originAsset)?.symbol}
tokenName={metadata.data?.get(originAsset)?.tokenName}
isAsset
/>
</Typography>
</div>
</div>
</div>
);
return lookupChain === originChain ? nativeContent : wrapped;
}
function SecondaryAssetInformation({
chainId,
foreignAssetInfo,
originAssetInfo,
}: {
chainId: ChainId;
foreignAssetInfo?: ForeignAssetInfo;
originAssetInfo?: OriginalAssetInfo;
}) {
const classes = useStyles();
const tokenArray: string[] = useMemo(() => {
//Saved to a variable to help typescript cope
const originAddress = originAssetInfo?.originAddress;
return originAddress && chainId === originAssetInfo?.originChain
? [originAddress]
: foreignAssetInfo?.address
? [foreignAssetInfo?.address]
: [];
}, [foreignAssetInfo, originAssetInfo, chainId]);
const metadata = useMetadata(chainId, tokenArray);
//TODO when this is the origin chain
return !originAssetInfo ? null : chainId === originAssetInfo.originChain ? (
<div>
<Typography>{`Transferring to ${CHAINS_BY_ID[chainId].name} will unwrap the token:`}</Typography>
<div className={classes.resultContainer}>
<SmartAddress
chainId={chainId}
address={originAssetInfo.originAddress || undefined}
symbol={
metadata.data?.get(originAssetInfo.originAddress || "")?.symbol ||
undefined
}
tokenName={
metadata.data?.get(originAssetInfo.originAddress || "")
?.tokenName || undefined
}
isAsset
/>
</div>
</div>
) : !foreignAssetInfo ? null : foreignAssetInfo.doesExist === false ? (
<div>
<Typography>{`This token has not yet been registered on ${CHAINS_BY_ID[chainId].name}`}</Typography>
<RegisterNowButtonCore
originChain={originAssetInfo?.originChain || undefined}
originAsset={
nativeToHexString(
originAssetInfo?.originAddress || undefined,
originAssetInfo?.originChain || CHAIN_ID_SOLANA // this should exist
) || undefined
}
targetChain={chainId}
/>
</div>
) : (
<div>
<Typography>When bridged, this asset becomes: </Typography>
<div className={classes.resultContainer}>
<SmartAddress
chainId={chainId}
address={foreignAssetInfo.address || undefined}
symbol={
metadata.data?.get(foreignAssetInfo.address || "")?.symbol ||
undefined
}
tokenName={
metadata.data?.get(foreignAssetInfo.address || "")?.tokenName ||
undefined
}
isAsset
/>
</div>
</div>
);
}
export default function TokenOriginVerifier() {
const classes = useStyles();
const isBeta = useBetaContext();
const [primaryLookupChain, setPrimaryLookupChain] = useState(CHAIN_ID_SOLANA);
const [primaryLookupAsset, setPrimaryLookupAsset] = useState("");
const [secondaryLookupChain, setSecondaryLookupChain] =
useState<ChainId>(CHAIN_ID_ETH);
const primaryLookupChainOptions = useMemo(
() => (isBeta ? CHAINS.filter((x) => !BETA_CHAINS.includes(x.id)) : CHAINS),
[isBeta]
);
const secondaryLookupChainOptions = useMemo(
() =>
isBeta
? CHAINS.filter(
(x) => !BETA_CHAINS.includes(x.id) && x.id !== primaryLookupChain
)
: CHAINS.filter((x) => x.id !== primaryLookupChain),
[isBeta, primaryLookupChain]
);
const handlePrimaryLookupChainChange = useCallback(
(e) => {
setPrimaryLookupChain(e.target.value);
if (secondaryLookupChain === e.target.value) {
setSecondaryLookupChain(
e.target.value === CHAIN_ID_SOLANA ? CHAIN_ID_ETH : CHAIN_ID_SOLANA
);
}
setPrimaryLookupAsset("");
},
[secondaryLookupChain]
);
const handleSecondaryLookupChainChange = useCallback((e) => {
setSecondaryLookupChain(e.target.value);
}, []);
const handlePrimaryLookupAssetChange = useCallback((event) => {
setPrimaryLookupAsset(event.target.value);
}, []);
const originInfo = useOriginalAsset(
primaryLookupChain,
primaryLookupAsset,
false
);
const foreignAssetInfo = useFetchForeignAsset(
originInfo.data?.originChain || 1,
originInfo.data?.originAddress || "",
secondaryLookupChain
);
const primaryWalletIsActive = !originInfo.data;
const secondaryWalletIsActive = !primaryWalletIsActive;
const primaryWallet = useIsWalletReady(
primaryLookupChain,
primaryWalletIsActive
);
const secondaryWallet = useIsWalletReady(
secondaryLookupChain,
secondaryWalletIsActive
);
const primaryWalletError =
isEVMChain(primaryLookupChain) &&
primaryLookupAsset &&
!originInfo.data &&
!originInfo.error &&
(!primaryWallet.isReady ? primaryWallet.statusMessage : "");
const originError = originInfo.error;
const primaryError = primaryWalletError || originError;
const secondaryWalletError =
isEVMChain(secondaryLookupChain) &&
originInfo.data?.originAddress &&
originInfo.data?.originChain &&
!foreignAssetInfo.data &&
(!secondaryWallet.isReady ? secondaryWallet.statusMessage : "");
const foreignError = foreignAssetInfo.error;
const secondaryError = secondaryWalletError || foreignError;
const primaryContent = (
<>
<Typography variant="h5">Source Information</Typography>
<Typography variant="body1" color="textSecondary">
Enter a token from any supported chain to get started.
</Typography>
<div className={classes.spacer} />
<TextField
select
variant="outlined"
label="Chain"
value={primaryLookupChain}
onChange={handlePrimaryLookupChainChange}
fullWidth
margin="normal"
>
{primaryLookupChainOptions.map(({ id, name }) => (
<MenuItem key={id} value={id}>
{name}
</MenuItem>
))}
</TextField>
<TextField
fullWidth
variant="outlined"
margin="normal"
label="Paste an address"
value={primaryLookupAsset}
onChange={handlePrimaryLookupAssetChange}
/>
<div className={classes.centered}>
{isEVMChain(primaryLookupChain) ? (
<KeyAndBalance chainId={primaryLookupChain} />
) : null}
{primaryError ? (
<Typography color="error">{primaryError}</Typography>
) : null}
<div className={classes.spacer} />
{originInfo.isFetching ? (
<CircularProgress />
) : originInfo.data?.originChain && originInfo.data.originAddress ? (
<PrimaryAssetInfomation
lookupAsset={primaryLookupAsset}
lookupChain={primaryLookupChain}
originChain={originInfo.data.originChain}
originAsset={originInfo.data.originAddress}
showLoader={originInfo.isFetching}
/>
) : null}
</div>
</>
);
const secondaryContent = originInfo.data ? (
<>
<Typography variant="h5">Bridge Results</Typography>
<Typography variant="body1" color="textSecondary">
Select a chain to see the result of bridging this token.
</Typography>
<div className={classes.spacer} />
<TextField
select
variant="outlined"
label="Other Chain"
value={secondaryLookupChain}
onChange={handleSecondaryLookupChainChange}
fullWidth
margin="normal"
>
{secondaryLookupChainOptions.map(({ id, name }) => (
<MenuItem key={id} value={id}>
{name}
</MenuItem>
))}
</TextField>
<div className={classes.centered}>
{isEVMChain(secondaryLookupChain) ? (
<KeyAndBalance chainId={secondaryLookupChain} />
) : null}
{secondaryError ? (
<Typography color="error">{secondaryError}</Typography>
) : null}
<div className={classes.spacer} />
{foreignAssetInfo.isFetching ? (
<CircularProgress />
) : originInfo.data?.originChain && originInfo.data.originAddress ? (
<SecondaryAssetInformation
foreignAssetInfo={foreignAssetInfo.data || undefined}
originAssetInfo={originInfo.data || undefined}
chainId={secondaryLookupChain}
/>
) : null}
</div>
</>
) : null;
const content = (
<div>
<Container maxWidth="md" className={classes.centered}>
<HeaderText white>Token Origin Verifier</HeaderText>
</Container>
<Container maxWidth="sm">
<Card className={classes.mainCard}>{primaryContent}</Card>
{secondaryContent ? (
<>
<div className={classes.centered}>
<ArrowDropDownIcon className={classes.arrowIcon} />
</div>
<Card className={classes.mainCard}>{secondaryContent}</Card>
</>
) : null}
</Container>
</div>
);
return content;
}

View File

@ -1,126 +0,0 @@
import { ChainId, CHAIN_ID_ALGORAND } from "@certusone/wormhole-sdk";
import { formatUnits } from "@ethersproject/units";
import { Algodv2 } from "algosdk";
import React, { useCallback } from "react";
import { fetchSingleMetadata } from "../../hooks/useAlgoMetadata";
import { createParsedTokenAccount } from "../../hooks/useGetSourceParsedTokenAccounts";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import { DataWrapper } from "../../store/helpers";
import { NFTParsedTokenAccount } from "../../store/nftSlice";
import { ParsedTokenAccount } from "../../store/transferSlice";
import { ALGORAND_HOST } from "../../utils/consts";
import TokenPicker, { BasicAccountRender } from "./TokenPicker";
type AlgoTokenPickerProps = {
value: ParsedTokenAccount | null;
onChange: (newValue: ParsedTokenAccount | null) => void;
tokenAccounts: DataWrapper<ParsedTokenAccount[]> | undefined;
disabled: boolean;
resetAccounts: (() => void) | undefined;
};
const returnsFalse = () => false;
export default function AlgoTokenPicker(props: AlgoTokenPickerProps) {
const { value, onChange, disabled, tokenAccounts, resetAccounts } = props;
const { walletAddress } = useIsWalletReady(CHAIN_ID_ALGORAND);
const resetAccountWrapper = useCallback(() => {
resetAccounts && resetAccounts();
}, [resetAccounts]);
const isLoading = tokenAccounts?.isFetching || false;
const onChangeWrapper = useCallback(
async (account: NFTParsedTokenAccount | null) => {
if (account === null) {
onChange(null);
return Promise.resolve();
}
onChange(account);
return Promise.resolve();
},
[onChange]
);
const lookupAlgoAddress = useCallback(
(lookupAsset: string) => {
if (!walletAddress) {
return Promise.reject("Wallet not connected");
}
const algodClient = new Algodv2(
ALGORAND_HOST.algodToken,
ALGORAND_HOST.algodServer,
ALGORAND_HOST.algodPort
);
return fetchSingleMetadata(lookupAsset, algodClient)
.then((metadata) => {
return algodClient
.accountInformation(walletAddress)
.do()
.then((accountInfo) => {
for (const asset of accountInfo.assets) {
const assetId = asset["asset-id"];
if (assetId.toString() === lookupAsset) {
const amount = asset.amount;
return createParsedTokenAccount(
walletAddress,
assetId.toString(),
amount,
metadata.decimals,
parseFloat(formatUnits(amount, metadata.decimals)),
formatUnits(amount, metadata.decimals).toString(),
metadata.symbol,
metadata.tokenName,
undefined,
false
);
}
}
return Promise.reject();
})
.catch(() => Promise.reject());
})
.catch(() => Promise.reject());
},
[walletAddress]
);
const isSearchableAddress = useCallback(
(address: string, chainId: ChainId) => {
if (address.length === 0) {
return false;
}
try {
parseInt(address);
return true;
} catch (e) {
return false;
}
},
[]
);
const RenderComp = useCallback(
({ account }: { account: NFTParsedTokenAccount }) => {
return BasicAccountRender(account, returnsFalse, false);
},
[]
);
return (
<TokenPicker
value={value}
options={tokenAccounts?.data || []}
RenderOption={RenderComp}
onChange={onChangeWrapper}
isValidAddress={isSearchableAddress}
getAddress={lookupAlgoAddress}
disabled={disabled}
resetAccounts={resetAccountWrapper}
error={""}
showLoader={isLoading}
nft={false}
chainId={CHAIN_ID_ALGORAND}
/>
);
}

View File

@ -1,190 +0,0 @@
import {
ChainId,
CHAIN_ID_ETH,
NFTImplementation,
TokenImplementation,
} from "@certusone/wormhole-sdk";
import { WormholeAbi__factory } from "@certusone/wormhole-sdk/lib/esm/ethers-contracts/abi";
import { getAddress as getEthAddress } from "@ethersproject/address";
import React, { useCallback } from "react";
import { useSelector } from "react-redux";
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import { DataWrapper } from "../../store/helpers";
import { NFTParsedTokenAccount } from "../../store/nftSlice";
import {
selectNFTSourceParsedTokenAccount,
selectTransferSourceParsedTokenAccount,
} from "../../store/selectors";
import { ParsedTokenAccount } from "../../store/transferSlice";
import {
getMigrationAssetMap,
WORMHOLE_V1_ETH_ADDRESS,
} from "../../utils/consts";
import {
ethNFTToNFTParsedTokenAccount,
ethTokenToParsedTokenAccount,
getEthereumNFT,
getEthereumToken,
isValidEthereumAddress,
} from "../../utils/ethereum";
import TokenPicker, { BasicAccountRender } from "./TokenPicker";
const isWormholev1 = (provider: any, address: string, chainId: ChainId) => {
if (chainId !== CHAIN_ID_ETH) {
return Promise.resolve(false);
}
const connection = WormholeAbi__factory.connect(
WORMHOLE_V1_ETH_ADDRESS,
provider
);
return connection.isWrappedAsset(address);
};
type EthereumSourceTokenSelectorProps = {
value: ParsedTokenAccount | null;
onChange: (newValue: ParsedTokenAccount | null) => void;
tokenAccounts: DataWrapper<ParsedTokenAccount[]> | undefined;
disabled: boolean;
resetAccounts: (() => void) | undefined;
chainId: ChainId;
nft?: boolean;
};
export default function EvmTokenPicker(
props: EthereumSourceTokenSelectorProps
) {
const {
value,
onChange,
tokenAccounts,
disabled,
resetAccounts,
chainId,
nft,
} = props;
const { provider, signerAddress } = useEthereumProvider();
const { isReady } = useIsWalletReady(chainId);
const selectedTokenAccount: NFTParsedTokenAccount | undefined = useSelector(
nft
? selectNFTSourceParsedTokenAccount
: selectTransferSourceParsedTokenAccount
);
const shouldDisplayBalance = useCallback(
(tokenAccount: NFTParsedTokenAccount) => {
const selectedMintMatch =
selectedTokenAccount &&
selectedTokenAccount.mintKey.toLowerCase() ===
tokenAccount.mintKey.toLowerCase();
//added just in case we start displaying NFT balances again.
const selectedTokenIdMatch =
selectedTokenAccount &&
selectedTokenAccount.tokenId === tokenAccount.tokenId;
return !!(
tokenAccount.isNativeAsset || //The native asset amount isn't taken from covalent, so can be trusted.
(selectedMintMatch && selectedTokenIdMatch)
);
},
[selectedTokenAccount]
);
const isMigrationEligible = useCallback(
(address: string) => {
const assetMap = getMigrationAssetMap(chainId);
return !!assetMap.get(getEthAddress(address));
},
[chainId]
);
const getAddress: (
address: string,
tokenId?: string
) => Promise<NFTParsedTokenAccount> = useCallback(
async (address: string, tokenId?: string) => {
if (provider && signerAddress && isReady) {
try {
const tokenAccount = await (nft
? getEthereumNFT(address, provider)
: getEthereumToken(address, provider));
if (!tokenAccount) {
return Promise.reject("Could not find the specified token.");
}
if (nft && !tokenId) {
return Promise.reject("Token ID is required.");
} else if (nft && tokenId) {
return ethNFTToNFTParsedTokenAccount(
tokenAccount as NFTImplementation,
tokenId,
signerAddress
);
} else {
return ethTokenToParsedTokenAccount(
tokenAccount as TokenImplementation,
signerAddress
);
}
} catch (e) {
return Promise.reject("Unable to retrive the specific token.");
}
} else {
return Promise.reject({ error: "Wallet is not connected." });
}
},
[isReady, nft, provider, signerAddress]
);
const onChangeWrapper = useCallback(
async (account: NFTParsedTokenAccount | null) => {
if (account === null) {
onChange(null);
return Promise.resolve();
}
let v1 = false;
try {
v1 = await isWormholev1(provider, account.mintKey, chainId);
} catch (e) {
//For now, just swallow this one.
}
const migration = isMigrationEligible(account.mintKey);
if (v1 === true && !migration) {
throw new Error(
"Wormhole v1 assets cannot be transferred with this bridge."
);
}
onChange(account);
return Promise.resolve();
},
[chainId, onChange, provider, isMigrationEligible]
);
const RenderComp = useCallback(
({ account }: { account: NFTParsedTokenAccount }) => {
return BasicAccountRender(
account,
isMigrationEligible,
nft || false,
shouldDisplayBalance
);
},
[nft, isMigrationEligible, shouldDisplayBalance]
);
return (
<TokenPicker
value={value}
options={tokenAccounts?.data || []}
RenderOption={RenderComp}
useTokenId={nft}
onChange={onChangeWrapper}
isValidAddress={isValidEthereumAddress}
getAddress={getAddress}
disabled={disabled}
resetAccounts={resetAccounts}
error={""}
showLoader={tokenAccounts?.isFetching}
nft={nft || false}
chainId={chainId}
/>
);
}

View File

@ -1,488 +0,0 @@
import {
Avatar,
Card,
CardContent,
CardMedia,
makeStyles,
Tooltip,
Typography,
} from "@material-ui/core";
import axios from "axios";
import { useCallback, useEffect, useLayoutEffect, useState } from "react";
import { NFTParsedTokenAccount } from "../../store/nftSlice";
import clsx from "clsx";
import {
ChainId,
CHAIN_ID_AVAX,
CHAIN_ID_BSC,
CHAIN_ID_ETH,
CHAIN_ID_ETHEREUM_ROPSTEN,
CHAIN_ID_POLYGON,
CHAIN_ID_SOLANA,
CHAIN_ID_OASIS,
CHAIN_ID_FANTOM,
} from "@certusone/wormhole-sdk";
import SmartAddress from "../SmartAddress";
import avaxIcon from "../../icons/avax.svg";
import bscIcon from "../../icons/bsc.svg";
import ethIcon from "../../icons/eth.svg";
import fantomIcon from "../../icons/fantom.svg";
import solanaIcon from "../../icons/solana.svg";
import polygonIcon from "../../icons/polygon.svg";
import oasisIcon from "../../icons/oasis-network-rose-logo.svg";
import useCopyToClipboard from "../../hooks/useCopyToClipboard";
import { Skeleton } from "@material-ui/lab";
import Wormhole from "../../icons/wormhole-network.svg";
const safeIPFS = (uri: string) =>
uri.startsWith("ipfs://ipfs/")
? uri.replace("ipfs://", "https://ipfs.io/")
: uri.startsWith("ipfs://")
? uri.replace("ipfs://", "https://ipfs.io/ipfs/")
: uri.startsWith("https://cloudflare-ipfs.com/ipfs/") // no CORS support?
? uri.replace("https://cloudflare-ipfs.com/ipfs/", "https://ipfs.io/ipfs/")
: uri;
const LogoIcon = ({ chainId }: { chainId: ChainId }) =>
chainId === CHAIN_ID_SOLANA ? (
<Avatar
style={{
backgroundColor: "black",
height: "1em",
width: "1em",
marginLeft: "4px",
padding: "4px",
}}
src={solanaIcon}
alt="Solana"
/>
) : chainId === CHAIN_ID_ETH || chainId === CHAIN_ID_ETHEREUM_ROPSTEN ? (
<Avatar
style={{
backgroundColor: "white",
height: "1em",
width: "1em",
marginLeft: "4px",
}}
src={ethIcon}
alt="Ethereum"
/>
) : chainId === CHAIN_ID_BSC ? (
<Avatar
style={{
backgroundColor: "rgb(20, 21, 26)",
height: "1em",
width: "1em",
marginLeft: "4px",
padding: "2px",
}}
src={bscIcon}
alt="Binance Smart Chain"
/>
) : chainId === CHAIN_ID_POLYGON ? (
<Avatar
style={{
backgroundColor: "black",
height: "1em",
width: "1em",
marginLeft: "4px",
padding: "3px",
}}
src={polygonIcon}
alt="Polygon"
/>
) : chainId === CHAIN_ID_AVAX ? (
<Avatar
style={{
backgroundColor: "black",
height: "1em",
width: "1em",
marginLeft: "4px",
padding: "3px",
}}
src={avaxIcon}
alt="Avalanche"
/>
) : chainId === CHAIN_ID_OASIS ? (
<Avatar
style={{
backgroundColor: "black",
height: "1em",
width: "1em",
marginLeft: "4px",
padding: "3px",
}}
src={oasisIcon}
alt="Oasis"
/>
) : chainId === CHAIN_ID_FANTOM ? (
<Avatar
style={{
backgroundColor: "black",
height: "1em",
width: "1em",
marginLeft: "4px",
padding: "3px",
}}
src={fantomIcon}
alt="Fantom"
/>
) : null;
const useStyles = makeStyles((theme) => ({
card: {
borderRadius: 9,
maxWidth: "100%",
width: 400,
margin: `${theme.spacing(1)}px auto`,
padding: 8,
position: "relative",
zIndex: 1,
transition: "background-position 1s, transform 0.25s",
"&:hover": {
backgroundPosition: "right center",
transform: "scale(1.25)",
},
backgroundSize: "200% auto",
backgroundColor: "#ffb347",
background:
"linear-gradient(to right, #ffb347 0%, #ffcc33 51%, #ffb347 100%)",
},
silverBorder: {
backgroundColor: "#D9D8D6",
backgroundSize: "200% auto",
background:
"linear-gradient(to bottom right, #757F9A 0%, #D7DDE8 51%, #757F9A 100%)",
"&:hover": {
backgroundPosition: "right center",
},
},
cardInset: {},
textContent: {
background: "transparent",
paddingTop: 4,
paddingBottom: 2,
display: "flex",
},
detailsContent: {
background: "transparent",
paddingTop: 4,
paddingBottom: 2,
"&:last-child": {
//override rule
paddingBottom: 2,
},
},
title: {
flex: 1,
},
description: {
padding: theme.spacing(0.5, 0, 1),
},
tokenId: {
fontSize: "8px",
},
mediaContent: {
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
background: "transparent",
margin: theme.spacing(0, 2),
"& > img, & > video": {
border: "1px solid #ffb347",
},
},
silverMediaBorder: {
"& > img, & > video": {
borderColor: "#D7DDE8",
},
},
// thanks https://cssgradient.io/ and https://htmlcolorcodes.com/color-picker/
eth: {
// colors from https://en.wikipedia.org/wiki/Ethereum#/media/File:Ethereum-icon-purple.svg
backgroundColor: "rgb(69,74,117)",
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%)",
},
bsc: {
// color from binance background rgb(20, 21, 26), 2 and 1 tint lighter
backgroundColor: "#F0B90B",
background:
"linear-gradient(160deg, rgb(20, 21, 26) 0%, #4A4D57 33%, rgb(20, 21, 26) 66%, #2C2F3B 100%)",
},
polygon: {
// color from polygon logo #8247E5 down to 30 lightness
backgroundColor: "#0F0323",
background:
"linear-gradient(160deg, #0F0323 0%, #250957 33%, #0F0323 66%, #0F0323 100%)",
},
solana: {
// colors from https://solana.com/branding/new/exchange/exchange-sq-black.svg
backgroundColor: "rgb(153,69,255)",
background:
"linear-gradient(45deg, rgba(153,69,255,1) 0%, rgba(121,98,231,1) 20%, rgba(0,209,140,1) 100%)",
},
hidden: {
display: "none",
},
skeleton: {
height: "500px",
width: "400px",
maxWidth: "100%",
borderRadius: 9,
display: "grid",
placeItems: "center",
position: "absolute",
},
wormholeIcon: {
height: 48,
width: 48,
filter: "contrast(0)",
transition: "filter 0.5s",
"&:hover": {
filter: "contrast(1)",
},
verticalAlign: "middle",
marginRight: theme.spacing(1),
zIndex: 10,
},
wormholePositioner: {
display: "grid",
placeItems: "center",
position: "relative",
height: "500px",
width: "400px",
maxWidth: "100%",
margin: `${theme.spacing(1)}px auto`,
},
}));
const ViewerLoader = () => {
const classes = useStyles();
return (
<div className={classes.wormholePositioner}>
<Skeleton variant="rect" animation="wave" className={classes.skeleton} />
<img src={Wormhole} alt="Wormhole" className={classes.wormholeIcon} />
</div>
);
};
export default function NFTViewer({
value,
chainId,
}: {
value: NFTParsedTokenAccount;
chainId: ChainId;
}) {
const uri = safeIPFS(value.uri || "");
const [metadata, setMetadata] = useState({
uri,
image: value.image,
animation_url: value.animation_url,
nftName: value.nftName,
description: value.description,
isLoading: !!uri,
});
const [isMediaLoading, setIsMediaLoading] = useState(false);
const onLoad = useCallback(() => {
setIsMediaLoading(false);
}, []);
const isLoading = isMediaLoading || metadata.isLoading;
useEffect(() => {
setMetadata((m) =>
m.uri === uri
? m
: {
uri,
image: value.image,
animation_url: value.animation_url,
nftName: value.nftName,
description: value.description,
isLoading: !!uri,
}
);
}, [value, uri]);
useEffect(() => {
if (uri) {
let cancelled = false;
(async () => {
try {
const result = await axios.get(uri);
if (!cancelled && result && result.data) {
// support returns with nested data (e.g. {status: 10000, result: {data: {...}}})
const data = result.data.result?.data || result.data;
setMetadata({
uri,
image:
data.image ||
data.image_url ||
data.big_image ||
data.small_image,
animation_url: data.animation_url,
nftName: data.name,
description: data.description,
isLoading: false,
});
} else if (!cancelled) {
setMetadata((m) => ({ ...m, isLoading: false }));
}
} catch (e) {
if (!cancelled) {
setMetadata((m) => ({ ...m, isLoading: false }));
}
}
})();
return () => {
cancelled = true;
};
}
}, [uri]);
const classes = useStyles();
const animLower = metadata.animation_url?.toLowerCase();
// const has3DModel = animLower?.endsWith('gltf') || animLower?.endsWith('glb')
const hasVideo =
!animLower?.startsWith("ipfs://") && // cloudflare ipfs doesn't support streaming video
(animLower?.endsWith("webm") ||
animLower?.endsWith("mp4") ||
animLower?.endsWith("mov") ||
animLower?.endsWith("m4v") ||
animLower?.endsWith("ogv") ||
animLower?.endsWith("ogg"));
const hasAudio =
animLower?.endsWith("mp3") ||
animLower?.endsWith("flac") ||
animLower?.endsWith("wav") ||
animLower?.endsWith("oga");
const hasImage = metadata.image;
const copyTokenId = useCopyToClipboard(value.tokenId || "");
const videoSrc = hasVideo && safeIPFS(metadata.animation_url || "");
const imageSrc = hasImage && safeIPFS(metadata.image || "");
const audioSrc = hasAudio && safeIPFS(metadata.animation_url || "");
//set loading when the media src changes
useLayoutEffect(() => {
if (videoSrc || imageSrc || audioSrc) {
setIsMediaLoading(true);
} else {
setIsMediaLoading(false);
}
}, [videoSrc, imageSrc, audioSrc]);
const image = (
<img
src={imageSrc}
alt={metadata.nftName || ""}
style={{ maxWidth: "100%" }}
onLoad={onLoad}
onError={onLoad}
/>
);
const media = (
<>
{hasVideo ? (
<video
autoPlay
controls
loop
style={{ maxWidth: "100%" }}
onLoadedData={onLoad}
onError={onLoad}
>
<source src={videoSrc || ""} />
{image}
</video>
) : hasImage ? (
image
) : null}
{hasAudio ? (
<audio
controls
src={audioSrc || ""}
onLoadedData={onLoad}
onError={onLoad}
/>
) : null}
</>
);
return (
<>
<div className={!isLoading ? classes.hidden : ""}>
<ViewerLoader />
</div>
<Card
className={clsx(classes.card, {
[classes.silverBorder]:
chainId === CHAIN_ID_SOLANA ||
chainId === CHAIN_ID_POLYGON ||
chainId === CHAIN_ID_AVAX,
[classes.hidden]: isLoading,
})}
elevation={10}
>
<div
className={clsx(classes.cardInset, {
[classes.eth]:
chainId === CHAIN_ID_ETH ||
chainId === CHAIN_ID_ETHEREUM_ROPSTEN ||
chainId === CHAIN_ID_AVAX || //TODO: give avax it's own bg
chainId === CHAIN_ID_OASIS || //TODO: give oasis it's own bg
chainId === CHAIN_ID_FANTOM, //TODO: give fantom it's own bg
[classes.bsc]: chainId === CHAIN_ID_BSC,
[classes.solana]: chainId === CHAIN_ID_SOLANA,
[classes.polygon]: chainId === CHAIN_ID_POLYGON,
})}
>
<CardContent className={classes.textContent}>
{metadata.nftName ? (
<Typography className={classes.title}>
{metadata.nftName}
</Typography>
) : (
<div className={classes.title} />
)}
<SmartAddress
chainId={chainId}
parsedTokenAccount={value}
noGutter
noUnderline
/>
<LogoIcon chainId={chainId} />
</CardContent>
<CardMedia
className={clsx(classes.mediaContent, {
[classes.silverMediaBorder]:
chainId === CHAIN_ID_SOLANA ||
chainId === CHAIN_ID_POLYGON ||
chainId === CHAIN_ID_OASIS ||
chainId === CHAIN_ID_AVAX,
})}
>
{media}
</CardMedia>
<CardContent className={classes.detailsContent}>
{metadata.description ? (
<Typography variant="body2" className={classes.description}>
{metadata.description}
</Typography>
) : null}
{value.tokenId ? (
<Typography className={classes.tokenId} align="right">
<Tooltip title="Copy" arrow>
<span onClick={copyTokenId}>
{value.tokenId.length > 18
? `#${value.tokenId.substr(0, 16)}...`
: `#${value.tokenId}`}
</span>
</Tooltip>
</Typography>
) : null}
</CardContent>
</div>
</Card>
</>
);
}

View File

@ -1,28 +0,0 @@
import { Button, makeStyles } from "@material-ui/core";
import { ReactChild } from "react";
const useStyles = makeStyles((theme) => ({
offsetButton: { display: "block", marginLeft: "auto", marginTop: 8 },
}));
export default function OffsetButton({
onClick,
disabled,
children,
}: {
onClick: () => void;
disabled?: boolean;
children: ReactChild;
}) {
const classes = useStyles();
return (
<Button
onClick={onClick}
disabled={disabled}
variant="outlined"
className={classes.offsetButton}
>
{children}
</Button>
);
}

Some files were not shown because too many files have changed in this diff Show More