bridge_ui: remove
This commit is contained in:
parent
0384d31a9b
commit
2d2a4b63ac
50
Tiltfile
50
Tiltfile
|
@ -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"],
|
||||
|
|
|
@ -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
|
|
@ -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 .
|
|
@ -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
|
||||
```
|
|
@ -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/],
|
||||
};
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -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 |
|
@ -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 |
|
@ -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"
|
||||
}
|
|
@ -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" }]
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
Binary file not shown.
Before Width: | Height: | Size: 264 KiB |
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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;
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 🎉
|
||||
</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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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);
|
|
@ -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;
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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} 🎉
|
||||
</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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 },
|
||||
};
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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",
|
||||
];
|
|
@ -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;
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue