├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── task.md └── workflows │ ├── build.yaml │ ├── prettier.yml │ └── test.yaml ├── .gitignore ├── .nvmrc ├── .prettierrc.json ├── Dockerfile ├── LEGAL.txt ├── LICENSE ├── README.md ├── config-overrides.js ├── package-lock.json ├── package.json ├── public ├── index.html ├── relayerExample.json └── robots.txt ├── src ├── App.js ├── ErrorBoundary.js ├── __tests__ │ ├── sdk.js │ └── unit.ts ├── components │ ├── AlgorandWalletKey.tsx │ ├── AptosConnectWalletDialog.tsx │ ├── AptosWalletKey.tsx │ ├── Attest │ │ ├── Create.tsx │ │ ├── CreatePreview.tsx │ │ ├── Send.tsx │ │ ├── SendPreview.tsx │ │ ├── Source.tsx │ │ ├── SourcePreview.tsx │ │ ├── Target.tsx │ │ ├── TargetPreview.tsx │ │ ├── WaitingForWalletMessage.tsx │ │ └── index.tsx │ ├── ButtonWithLoader.tsx │ ├── ChainSelect.tsx │ ├── ChainSelectArrow.tsx │ ├── ChainWarningMessage.tsx │ ├── EthereumSignerKey.tsx │ ├── EvmConnectWalletDialog.tsx │ ├── FeeMethodSelector.tsx │ ├── Footer.tsx │ ├── HeaderText.tsx │ ├── InjectiveConnectWalletDialog.tsx │ ├── InjectiveWalletKey.tsx │ ├── KeyAndBalance.tsx │ ├── LowBalanceWarning.tsx │ ├── NFT │ │ ├── Redeem.tsx │ │ ├── RedeemPreview.tsx │ │ ├── Send.tsx │ │ ├── SendPreview.tsx │ │ ├── Source.tsx │ │ ├── SourcePreview.tsx │ │ ├── Target.tsx │ │ ├── TargetPreview.tsx │ │ ├── WaitingForWalletMessage.tsx │ │ └── index.tsx │ ├── NFTOriginVerifier.tsx │ ├── NearWalletKey.tsx │ ├── NumberTextField.tsx │ ├── Recovery.tsx │ ├── RelaySelector.tsx │ ├── SeiConnectWalletDialog.tsx │ ├── SeiWalletKey.tsx │ ├── ShowTx.tsx │ ├── SmartAddress.tsx │ ├── SolanaConnectWalletDialog.tsx │ ├── SolanaCreateAssociatedAddress.tsx │ ├── SolanaTPSWarning.tsx │ ├── SolanaWalletKey.tsx │ ├── StepDescription.tsx │ ├── SuiConnectWalletDialog.tsx │ ├── SuiWalletKey.tsx │ ├── TerraConnectWalletDialog.tsx │ ├── TerraFeeDenomPicker.tsx │ ├── TerraWalletKey.tsx │ ├── ToggleConnectedButton.tsx │ ├── TokenOriginVerifier.tsx │ ├── TokenSelectors │ │ ├── AlgoTokenPicker.tsx │ │ ├── AptosTokenPicker.tsx │ │ ├── EvmTokenPicker.tsx │ │ ├── InjectiveTokenPicker.tsx │ │ ├── NFTViewer.tsx │ │ ├── NearTokenPicker.tsx │ │ ├── OffsetButton.tsx │ │ ├── RefreshButtonWrapper.tsx │ │ ├── SeiTokenPicker.tsx │ │ ├── SolanaTokenPicker.tsx │ │ ├── SourceTokenSelector.tsx │ │ ├── SuiTokenPicker.tsx │ │ ├── TerraTokenPicker.tsx │ │ ├── TokenPicker.tsx │ │ └── XplaTokenPicker.tsx │ ├── TransactionProgress.tsx │ ├── Transfer │ │ ├── AddToMetamask.tsx │ │ ├── PendingVAAWarning.tsx │ │ ├── Redeem.tsx │ │ ├── RedeemPreview.tsx │ │ ├── RegisterNowButton.tsx │ │ ├── Send.tsx │ │ ├── SendConfirmationDialog.tsx │ │ ├── SendPreview.tsx │ │ ├── Source.tsx │ │ ├── SourceAssetWarning.tsx │ │ ├── SourcePreview.tsx │ │ ├── Target.tsx │ │ ├── TargetPreview.tsx │ │ ├── TransferLimitedWarning.tsx │ │ ├── WaitingForWalletMessage.tsx │ │ └── index.tsx │ ├── USDC │ │ └── index.tsx │ ├── UnwrapNative.tsx │ ├── WithdrawTokensTerra.tsx │ ├── XplaConnectWalletDialog.tsx │ └── XplaWalletKey.tsx ├── config.ts ├── contexts │ ├── AlgorandWalletContext.tsx │ ├── AptosWalletContext.tsx │ ├── EthereumProviderContext.tsx │ ├── InjectiveWalletContext.tsx │ ├── NearWalletContext.tsx │ ├── SolanaWalletContext.tsx │ ├── SuiWalletContext.tsx │ ├── TerraWalletContext.tsx │ └── XplaWalletContext.tsx ├── hooks │ ├── useAcalaRelayerInfo.ts │ ├── useAlgoMetadata.ts │ ├── useAllowance.ts │ ├── useAptosMetadata.ts │ ├── useAptosNativeBalance.ts │ ├── useAttestSignedVAA.ts │ ├── useCheckIfWormholeWrapped.ts │ ├── useCopyToClipboard.tsx │ ├── useEthereumMigratorInformation.tsx │ ├── useEvmMetadata.ts │ ├── useFetchForeignAsset.ts │ ├── useFetchTargetAsset.ts │ ├── useGetIsTransferCompleted.ts │ ├── useGetSourceParsedTokenAccounts.ts │ ├── useGetTargetParsedTokenAccounts.ts │ ├── useHandleAttest.tsx │ ├── useHandleCreateWrapped.tsx │ ├── useHandleNFTRedeem.tsx │ ├── useHandleNFTTransfer.tsx │ ├── useHandleRedeem.tsx │ ├── useHandleTransfer.tsx │ ├── useInjectiveMetadata.ts │ ├── useInjectiveNativeBalances.ts │ ├── useIsTransferLimited.ts │ ├── useIsWalletReady.ts │ ├── useMetadata.ts │ ├── useMetaplexData.ts │ ├── useNFTSignedVAA.ts │ ├── useNFTTargetAddress.ts │ ├── useNativeSuiBalance.ts │ ├── useNearMetadata.ts │ ├── useOriginalAsset.ts │ ├── useRelayerInfo.ts │ ├── useRelayersAvailable.ts │ ├── useSeiMetadata.ts │ ├── useSeiNativeBalances.ts │ ├── useSolanaTokenMap.ts │ ├── useSuiMetadata.ts │ ├── useSyncTargetAddress.ts │ ├── useTerraMetadata.ts │ ├── useTerraNativeBalances.ts │ ├── useTerraTokenMap.ts │ ├── useTransactionFees.tsx │ ├── useTransferSignedVAA.ts │ ├── useTransferTargetAddress.ts │ ├── useXplaMetadata.ts │ └── useXplaNativeBalances.ts ├── icons │ ├── Discord.svg │ ├── Docs.svg │ ├── Github.svg │ ├── Medium.svg │ ├── Telegram.svg │ ├── Twitter.svg │ ├── acala.svg │ ├── algorand.svg │ ├── aptos.svg │ ├── arbitrum.svg │ ├── aurora.svg │ ├── avax.svg │ ├── base.svg │ ├── bnb.svg │ ├── bsc.svg │ ├── celo.svg │ ├── eth.svg │ ├── fantom.svg │ ├── injective.svg │ ├── karura.svg │ ├── keplr.svg │ ├── klaytn.svg │ ├── metamask-fox.svg │ ├── moonbeam.svg │ ├── near.svg │ ├── neon.svg │ ├── oasis-network-rose-logo.svg │ ├── optimism.svg │ ├── polygon.svg │ ├── sei.svg │ ├── solana.svg │ ├── sui.svg │ ├── terra.svg │ ├── terra2.svg │ ├── usdc.svg │ ├── walletconnect.svg │ ├── wormhole.svg │ └── xpla.svg ├── index.js ├── muiTheme.js ├── react-app-env.d.ts ├── store │ ├── attestSlice.ts │ ├── feeSlice.ts │ ├── helpers.ts │ ├── index.ts │ ├── nftSlice.ts │ ├── selectors.ts │ ├── tokenSlice.ts │ ├── transferSlice.ts │ ├── usdcSelectors.ts │ └── usdcSlice.ts └── utils │ ├── SolanaPriceStore.ts │ ├── algorand.ts │ ├── aptos.ts │ ├── balancePretty.ts │ ├── consts.ts │ ├── ethereum.ts │ ├── getSignedVAAWithRetry.ts │ ├── injective.ts │ ├── karura.ts │ ├── metaMaskChainParameters.ts │ ├── metaplex.ts │ ├── near.ts │ ├── parseError.ts │ ├── pushToClipboard.ts │ ├── sei.ts │ ├── sleep.ts │ ├── solana.ts │ ├── sort.ts │ ├── sui.ts │ ├── terra.ts │ └── xpla.ts └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Submit a bug ticket if you have an issue. If you're a user, check out the Wormhole Discord server below for faster assistance. 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | --- 8 | 9 | 10 | ## Description and context 11 | 12 | 13 | 14 | ## Steps to reproduce 15 | 16 | 17 | 18 | 1. 19 | 2. 20 | 3. 21 | 22 | ## Experienced behavior 23 | 24 | 25 | 26 | ## Expected behavior 27 | 28 | 29 | 30 | ## Solution recommendation 31 | 32 | 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Wormhole Official Discord 4 | url: https://discord.gg/wormholecrypto 5 | about: If you're a user, this is the fastest way to get help. Do not give your wallet private key or mnemonic words to anyone. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/task.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Task 3 | about: Create a regular work item to be picked up by a contributor. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Description and context 11 | 12 | 13 | 14 | 15 | 16 | ## Definition of done 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | build-and-deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 🛎️ 11 | uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 16 15 | cache: "npm" 16 | - name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built. 17 | run: | 18 | npm ci 19 | IFS='/' read -a strarr <<< $GITHUB_REPOSITORY && sed -i "`wc -l < package.json`i\,\"homepage\":\"https://${strarr[0]}.github.io/${strarr[1]}\"" package.json 20 | npm run build 21 | - name: Deploy 🚀 22 | uses: JamesIves/github-pages-deploy-action@v4.2.5 23 | with: 24 | branch: gh-pages # The branch the action should deploy to. 25 | folder: build # The folder the action should deploy. 26 | -------------------------------------------------------------------------------- /.github/workflows/prettier.yml: -------------------------------------------------------------------------------- 1 | name: Prettier Check 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | prettier: 7 | runs-on: "ubuntu-latest" 8 | steps: 9 | - name: Checkout repository 10 | uses: actions/checkout@v3 11 | - name: Run prettier 12 | run: npx prettier --check ./src 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | - uses: actions/setup-node@v3 10 | with: 11 | node-version: 16 12 | cache: "npm" 13 | - run: npm ci 14 | - run: npm run build 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # ethereum contracts 26 | /contracts 27 | /src/ethers-contracts -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.20.0 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "quoteProps": "as-needed", 8 | "jsxSingleQuote": false, 9 | "trailingComma": "es5", 10 | "bracketSpacing": true, 11 | "bracketSameLine": false, 12 | "arrowParens": "always" 13 | } 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker.io/docker/dockerfile:1.3@sha256:42399d4635eddd7a9b8a24be879d2f9a930d0ed040a61324cfdf59ef1357b3b2 2 | 3 | # Derivative of ethereum/Dockerfile, look there for an explanation on how it works. 4 | FROM node:16-alpine@sha256:f21f35732964a96306a84a8c4b5a829f6d3a0c5163237ff4b6b8b34f8d70064b 5 | 6 | RUN mkdir -p /app 7 | WORKDIR /app 8 | 9 | COPY bridge_ui/package.json bridge_ui/package-lock.json ./ 10 | RUN --mount=type=cache,uid=1000,gid=1000,target=/home/node/.npm \ 11 | npm ci 12 | COPY bridge_ui . 13 | -------------------------------------------------------------------------------- /LEGAL.txt: -------------------------------------------------------------------------------- 1 | The information and materials available through this repo (collectively, “Materials”) are available on an “AI IS” basis without warranties of any kind, either express or implied, including, but not limited to, warranties of merchantability, title, fitness for a particular purpose and non-infringement. You assume all risks associated with using the Materials, and digital assets and decentralized systems generally, including but not limited to, that: (a) digital assets are highly volatile; (b) using digital assets is inherently risky due to both features of such assets and the potential unauthorized acts of third parties; (c) you may not have ready access to digital assets; and (d) there is a risk of losing your digital assets or those owned by a third party. You agree that you will have no recourse against anyone else for any losses due to the use of the Materials. For example, these losses may arise from or relate to: (i) incorrect information; (ii) software or network failures; (iii) corrupted files; (iv) unauthorized access; (v) errors, mistakes, or inaccuracies; or (vi) third-party activities. 2 | 3 | You are solely responsible for using the Materials in compliance with applicable laws and regulations, including but not limited to, export control or sanctions laws of any applicable jurisdiction. You should be aware that U.S. export control and sanctions laws prohibit U.S. persons (and other persons that are subject to such laws) from transacting with persons in certain countries and territories or that are on the Specially Designated Nationals list. Accordingly, you must avoid providing the Materials to sanctioned persons because such activity could be a violation of U.S. export controls and sanctions law. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Wormhole Project Contributors 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example Token Bridge UI · [![GitHub license](https://img.shields.io/badge/license-Apache2.0-blue.svg)](https://github.com/wormhole-foundation/example-token-bridge-ui/blob/main/LICENSE) ![Build](https://github.com/wormhole-foundation/example-token-bridge-ui/actions/workflows/build.yaml/badge.svg) 2 | 3 | This app serves as a testnet and local devnet UI for the example token bridge contracts. 4 | 5 | View at https://wormhole-foundation.github.io/example-token-bridge-ui/ 6 | 7 | ## Install 8 | 9 | ```bash 10 | npm ci 11 | ``` 12 | 13 | ## Develop 14 | 15 | If using the node version specified in `.nvmrc`, run with 16 | 17 | ```bash 18 | npm start 19 | ``` 20 | 21 | If on latest LTS (v18.16.0), run with 22 | 23 | ```bash 24 | NODE_OPTIONS=--openssl-legacy-provider npm start 25 | ``` 26 | 27 | *Note: the above issue should be resolved after updating to the latest mui + react versions* 28 | 29 | ## Build 30 | 31 | ```bash 32 | npm run build 33 | ``` 34 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | const { ProvidePlugin } = require("webpack"); 2 | 3 | module.exports = function override(config, env) { 4 | return { 5 | ...config, 6 | module: { 7 | ...config.module, 8 | rules: [ 9 | ...config.module.rules, 10 | { 11 | test: /\.m?js$/, 12 | enforce: "pre", 13 | use: ["source-map-loader"], 14 | resolve: { 15 | fullySpecified: false, 16 | fallback: { 17 | "react/jsx-runtime": "react/jsx-runtime.js", 18 | "react/jsx-dev-runtime": "react/jsx-dev-runtime.js", 19 | }, 20 | }, 21 | }, 22 | { 23 | test: /\.wasm$/, 24 | type: "webassembly/async", 25 | }, 26 | ], 27 | }, 28 | plugins: [ 29 | ...config.plugins, 30 | new ProvidePlugin({ 31 | Buffer: ["buffer", "Buffer"], 32 | process: "process/browser", 33 | }), 34 | ], 35 | resolve: { 36 | ...config.resolve, 37 | fallback: { 38 | assert: "assert", 39 | buffer: "buffer", 40 | console: "console-browserify", 41 | constants: "constants-browserify", 42 | crypto: "crypto-browserify", 43 | domain: "domain-browser", 44 | events: "events", 45 | fs: false, 46 | http: "stream-http", 47 | https: "https-browserify", 48 | os: "os-browserify/browser", 49 | path: "path-browserify", 50 | punycode: "punycode", 51 | process: "process/browser", 52 | querystring: "querystring-es3", 53 | stream: "stream-browserify", 54 | _stream_duplex: "readable-stream/duplex", 55 | _stream_passthrough: "readable-stream/passthrough", 56 | _stream_readable: "readable-stream/readable", 57 | _stream_transform: "readable-stream/transform", 58 | _stream_writable: "readable-stream/writable", 59 | string_decoder: "string_decoder", 60 | sys: "util", 61 | timers: "timers-browserify", 62 | tty: "tty-browserify", 63 | url: "url", 64 | util: "util", 65 | vm: "vm-browserify", 66 | zlib: "browserify-zlib", 67 | }, 68 | }, 69 | experiments: { 70 | asyncWebAssembly: true, 71 | }, 72 | ignoreWarnings: [/Failed to parse source map/], 73 | }; 74 | }; 75 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 15 | Token Bridge 16 | 17 | 18 | 19 |
20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /public/relayerExample.json: -------------------------------------------------------------------------------- 1 | { 2 | "supportedTokens": [ 3 | { 4 | "chainId": 1, 5 | "address": "So11111111111111111111111111111111111111112", 6 | "coingeckoId": "solana" 7 | }, 8 | { 9 | "chainId": 2, 10 | "address": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E", 11 | "coingeckoId": "ethereum" 12 | }, 13 | { 14 | "chainId": 3, 15 | "address": "uluna", 16 | "coingeckoId": "terra-luna" 17 | }, 18 | { 19 | "chainId": 4, 20 | "address": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E", 21 | "coingeckoId": "binancecoin" 22 | } 23 | ], 24 | "relayers": [{ "name": "localhostRelayer", "url": "http://localhost:4201" }] 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import { Typography } from "@material-ui/core"; 2 | import React from "react"; 3 | 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static getDerivedStateFromError(error) { 11 | return { hasError: true }; 12 | } 13 | 14 | componentDidCatch(error, errorInfo) { 15 | console.error(error, errorInfo); 16 | } 17 | 18 | render() { 19 | if (this.state.hasError) { 20 | return ( 21 | 22 | An unexpected error has occurred. Please refresh the page. 23 | 24 | ); 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/__tests__/sdk.js: -------------------------------------------------------------------------------- 1 | const { describe, expect, it } = require("@jest/globals"); 2 | const fs = require("fs"); 3 | 4 | describe("SDK installation", () => { 5 | it("does not import from file path", () => { 6 | const packageFile = fs.readFileSync("./package.json"); 7 | const packageObj = JSON.parse(packageFile.toString()); 8 | 9 | const sdkInstallation = 10 | packageObj?.dependencies?.["@certusone/wormhole-sdk"]; 11 | expect(sdkInstallation && !sdkInstallation.includes("file")).toBe(true); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/__tests__/unit.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "@jest/globals"; 2 | import { balancePretty } from "../utils/balancePretty"; 3 | 4 | describe("Unit Tests", () => { 5 | describe("balancePretty() tests", () => { 6 | it("9.99 => 9.99", () => { 7 | expect(balancePretty("9.99")).toBe("9.99"); 8 | }); 9 | it("123456.789 => 123456.78", () => { 10 | expect(balancePretty("123456.789")).toBe("123456.7"); 11 | }); 12 | it("1234567.8912 => 1.23 M", () => { 13 | expect(balancePretty("1234567.891")).toBe("1.23 M"); 14 | }); 15 | it("123999.8912 => 1.23 M", () => { 16 | expect(balancePretty("1239999.8912")).toBe("1.23 M"); 17 | }); 18 | it("981234567.8912 => 981.23 M", () => { 19 | expect(balancePretty("981234567.891")).toBe("981.23 M"); 20 | }); 21 | it("9876543210.8912 => 9.87 B", () => { 22 | expect(balancePretty("9876543210.8912")).toBe("9.87 B"); 23 | }); 24 | it("219876543210.8912 => 219.87 B", () => { 25 | expect(balancePretty("219876543210.8912")).toBe("219.87 B"); 26 | }); 27 | it("219876543210 => 219.87 B", () => { 28 | expect(balancePretty("219876543210")).toBe("219.87 B"); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/components/AlgorandWalletKey.tsx: -------------------------------------------------------------------------------- 1 | import { useAlgorandContext } from "../contexts/AlgorandWalletContext"; 2 | import ToggleConnectedButton from "./ToggleConnectedButton"; 3 | 4 | const AlgorandWalletKey = () => { 5 | const { connect, disconnect, accounts } = useAlgorandContext(); 6 | 7 | return ( 8 | <> 9 | 15 | 16 | ); 17 | }; 18 | 19 | export default AlgorandWalletKey; 20 | -------------------------------------------------------------------------------- /src/components/AptosWalletKey.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | import { useAptosContext } from "../contexts/AptosWalletContext"; 3 | import AptosConnectWalletDialog from "./AptosConnectWalletDialog"; 4 | import ToggleConnectedButton from "./ToggleConnectedButton"; 5 | 6 | const AptosWalletKey = () => { 7 | const { connected, disconnect, account } = useAptosContext(); 8 | const address = account?.address?.toString(); 9 | const [isDialogOpen, setIsDialogOpen] = useState(false); 10 | 11 | const connect = useCallback(() => { 12 | setIsDialogOpen(true); 13 | }, [setIsDialogOpen]); 14 | 15 | const closeDialog = useCallback(() => { 16 | setIsDialogOpen(false); 17 | }, [setIsDialogOpen]); 18 | return ( 19 | <> 20 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default AptosWalletKey; 32 | -------------------------------------------------------------------------------- /src/components/Attest/Create.tsx: -------------------------------------------------------------------------------- 1 | import { isTerraChain } from "@certusone/wormhole-sdk"; 2 | import { CircularProgress, makeStyles } from "@material-ui/core"; 3 | import { useSelector } from "react-redux"; 4 | import useFetchForeignAsset from "../../hooks/useFetchForeignAsset"; 5 | import { useHandleCreateWrapped } from "../../hooks/useHandleCreateWrapped"; 6 | import useIsWalletReady from "../../hooks/useIsWalletReady"; 7 | import { 8 | selectAttestSourceAsset, 9 | selectAttestSourceChain, 10 | selectAttestTargetChain, 11 | } from "../../store/selectors"; 12 | import ButtonWithLoader from "../ButtonWithLoader"; 13 | import KeyAndBalance from "../KeyAndBalance"; 14 | import TerraFeeDenomPicker from "../TerraFeeDenomPicker"; 15 | import WaitingForWalletMessage from "./WaitingForWalletMessage"; 16 | 17 | const useStyles = makeStyles((theme) => ({ 18 | alignCenter: { 19 | margin: "0 auto", 20 | display: "block", 21 | textAlign: "center", 22 | }, 23 | spacer: { 24 | height: theme.spacing(2), 25 | }, 26 | })); 27 | 28 | function Create() { 29 | const classes = useStyles(); 30 | const targetChain = useSelector(selectAttestTargetChain); 31 | const originAsset = useSelector(selectAttestSourceAsset); 32 | const originChain = useSelector(selectAttestSourceChain); 33 | const { isReady, statusMessage } = useIsWalletReady(targetChain); 34 | const foreignAssetInfo = useFetchForeignAsset( 35 | originChain, 36 | originAsset, 37 | targetChain 38 | ); 39 | const shouldUpdate = foreignAssetInfo.data?.doesExist; 40 | const foreignAddress = foreignAssetInfo.data?.address; 41 | const error = foreignAssetInfo.error || statusMessage; 42 | const { handleClick, disabled, showLoader } = useHandleCreateWrapped( 43 | shouldUpdate || false, 44 | foreignAddress 45 | ); 46 | 47 | return ( 48 | <> 49 | 50 | {isTerraChain(targetChain) && ( 51 | 52 | )} 53 | {foreignAssetInfo.isFetching ? ( 54 | <> 55 |
56 | 57 | 58 | ) : ( 59 | <> 60 | 66 | {shouldUpdate ? "Update" : "Create"} 67 | 68 | 69 | 70 | )} 71 | 72 | ); 73 | } 74 | 75 | export default Create; 76 | -------------------------------------------------------------------------------- /src/components/Attest/CreatePreview.tsx: -------------------------------------------------------------------------------- 1 | import { Link, makeStyles, Typography } from "@material-ui/core"; 2 | import { useCallback } from "react"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { 5 | selectAttestCreateTx, 6 | selectAttestTargetChain, 7 | } from "../../store/selectors"; 8 | import { reset } from "../../store/attestSlice"; 9 | import ButtonWithLoader from "../ButtonWithLoader"; 10 | import ShowTx from "../ShowTx"; 11 | import { useHistory } from "react-router"; 12 | import { getHowToAddToTokenListUrl } from "../../utils/consts"; 13 | import { Alert } from "@material-ui/lab"; 14 | 15 | const useStyles = makeStyles((theme) => ({ 16 | description: { 17 | textAlign: "center", 18 | }, 19 | alert: { 20 | marginTop: theme.spacing(1), 21 | }, 22 | })); 23 | 24 | export default function CreatePreview() { 25 | const { push } = useHistory(); 26 | const classes = useStyles(); 27 | const dispatch = useDispatch(); 28 | const targetChain = useSelector(selectAttestTargetChain); 29 | const createTx = useSelector(selectAttestCreateTx); 30 | const handleResetClick = useCallback(() => { 31 | dispatch(reset()); 32 | }, [dispatch]); 33 | const handleReturnClick = useCallback(() => { 34 | dispatch(reset()); 35 | push("/transfer"); 36 | }, [dispatch, push]); 37 | 38 | const explainerString = 39 | "Success! The create wrapped transaction was submitted."; 40 | const howToAddToTokenListUrl = getHowToAddToTokenListUrl(targetChain); 41 | 42 | return ( 43 | <> 44 | 49 | {explainerString} 50 | 51 | {createTx ? : null} 52 | {howToAddToTokenListUrl ? ( 53 | 54 | Remember to add the token to the{" "} 55 | 60 | token list 61 | 62 | {"."} 63 | 64 | ) : null} 65 | 66 | Attest Another Token! 67 | 68 | 69 | Return to Transfer 70 | 71 | 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/components/Attest/SendPreview.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles, Typography } from "@material-ui/core"; 2 | import { useSelector } from "react-redux"; 3 | import { 4 | selectAttestSourceChain, 5 | selectAttestAttestTx, 6 | } from "../../store/selectors"; 7 | import ShowTx from "../ShowTx"; 8 | 9 | const useStyles = makeStyles((theme) => ({ 10 | description: { 11 | textAlign: "center", 12 | }, 13 | tx: { 14 | marginTop: theme.spacing(1), 15 | textAlign: "center", 16 | }, 17 | viewButton: { 18 | marginTop: theme.spacing(1), 19 | }, 20 | })); 21 | 22 | export default function SendPreview() { 23 | const classes = useStyles(); 24 | const sourceChain = useSelector(selectAttestSourceChain); 25 | const attestTx = useSelector(selectAttestAttestTx); 26 | 27 | const explainerString = "The token has been attested!"; 28 | 29 | return ( 30 | <> 31 | 36 | {explainerString} 37 | 38 | {attestTx ? : null} 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Attest/Source.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles, TextField } from "@material-ui/core"; 2 | import { useCallback } from "react"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { 5 | incrementStep, 6 | setSourceAsset, 7 | setSourceChain, 8 | } from "../../store/attestSlice"; 9 | import { 10 | selectAttestIsSourceComplete, 11 | selectAttestShouldLockFields, 12 | selectAttestSourceAsset, 13 | selectAttestSourceChain, 14 | } from "../../store/selectors"; 15 | import { CHAINS } from "../../utils/consts"; 16 | import ButtonWithLoader from "../ButtonWithLoader"; 17 | import ChainSelect from "../ChainSelect"; 18 | import KeyAndBalance from "../KeyAndBalance"; 19 | import LowBalanceWarning from "../LowBalanceWarning"; 20 | 21 | const useStyles = makeStyles((theme) => ({ 22 | transferField: { 23 | marginTop: theme.spacing(5), 24 | }, 25 | })); 26 | 27 | function Source() { 28 | const classes = useStyles(); 29 | const dispatch = useDispatch(); 30 | const sourceChain = useSelector(selectAttestSourceChain); 31 | const sourceAsset = useSelector(selectAttestSourceAsset); 32 | const isSourceComplete = useSelector(selectAttestIsSourceComplete); 33 | const shouldLockFields = useSelector(selectAttestShouldLockFields); 34 | const handleSourceChange = useCallback( 35 | (event) => { 36 | dispatch(setSourceChain(event.target.value)); 37 | }, 38 | [dispatch] 39 | ); 40 | const handleAssetChange = useCallback( 41 | (event) => { 42 | dispatch(setSourceAsset(event.target.value)); 43 | }, 44 | [dispatch] 45 | ); 46 | const handleNextClick = useCallback(() => { 47 | dispatch(incrementStep()); 48 | }, [dispatch]); 49 | return ( 50 | <> 51 | 60 | 61 | 70 | 71 | 76 | Next 77 | 78 | 79 | ); 80 | } 81 | 82 | export default Source; 83 | -------------------------------------------------------------------------------- /src/components/Attest/SourcePreview.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles, Typography } from "@material-ui/core"; 2 | import { useSelector } from "react-redux"; 3 | import { 4 | selectAttestSourceAsset, 5 | selectAttestSourceChain, 6 | } from "../../store/selectors"; 7 | import { CHAINS_BY_ID } from "../../utils/consts"; 8 | import SmartAddress from "../SmartAddress"; 9 | 10 | const useStyles = makeStyles((theme) => ({ 11 | description: { 12 | textAlign: "center", 13 | }, 14 | })); 15 | 16 | export default function SourcePreview() { 17 | const classes = useStyles(); 18 | const sourceChain = useSelector(selectAttestSourceChain); 19 | const sourceAsset = useSelector(selectAttestSourceAsset); 20 | 21 | const explainerContent = 22 | sourceChain && sourceAsset ? ( 23 | <> 24 | You will attest 25 | 26 | on {CHAINS_BY_ID[sourceChain].name} 27 | 28 | ) : ( 29 | "" 30 | ); 31 | 32 | return ( 33 | 38 | {explainerContent} 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Attest/Target.tsx: -------------------------------------------------------------------------------- 1 | import { isEVMChain } from "@certusone/wormhole-sdk"; 2 | import { makeStyles, Typography } from "@material-ui/core"; 3 | import { Alert } from "@material-ui/lab"; 4 | import { useCallback, useMemo } from "react"; 5 | import { useDispatch, useSelector } from "react-redux"; 6 | import { GasEstimateSummary } from "../../hooks/useTransactionFees"; 7 | import { incrementStep, setTargetChain } from "../../store/attestSlice"; 8 | import { 9 | selectAttestIsTargetComplete, 10 | selectAttestShouldLockFields, 11 | selectAttestSourceChain, 12 | selectAttestTargetChain, 13 | } from "../../store/selectors"; 14 | import { CHAINS, CHAINS_BY_ID } from "../../utils/consts"; 15 | import ButtonWithLoader from "../ButtonWithLoader"; 16 | import ChainSelect from "../ChainSelect"; 17 | import KeyAndBalance from "../KeyAndBalance"; 18 | import LowBalanceWarning from "../LowBalanceWarning"; 19 | 20 | const useStyles = makeStyles((theme) => ({ 21 | alert: { 22 | marginTop: theme.spacing(1), 23 | marginBottom: theme.spacing(1), 24 | }, 25 | })); 26 | 27 | function Target() { 28 | const classes = useStyles(); 29 | const dispatch = useDispatch(); 30 | const sourceChain = useSelector(selectAttestSourceChain); 31 | const chains = useMemo( 32 | () => CHAINS.filter((c) => c.id !== sourceChain), 33 | [sourceChain] 34 | ); 35 | const targetChain = useSelector(selectAttestTargetChain); 36 | const isTargetComplete = useSelector(selectAttestIsTargetComplete); 37 | const shouldLockFields = useSelector(selectAttestShouldLockFields); 38 | const handleTargetChange = useCallback( 39 | (event) => { 40 | dispatch(setTargetChain(event.target.value)); 41 | }, 42 | [dispatch] 43 | ); 44 | const handleNextClick = useCallback(() => { 45 | dispatch(incrementStep()); 46 | }, [dispatch]); 47 | return ( 48 | <> 49 | 58 | 59 | 60 | 61 | You will have to pay transaction fees on{" "} 62 | {CHAINS_BY_ID[targetChain].name} to attest this token.{" "} 63 | 64 | {isEVMChain(targetChain) && ( 65 | 69 | )} 70 | 71 | 72 | 77 | Next 78 | 79 | 80 | ); 81 | } 82 | 83 | export default Target; 84 | -------------------------------------------------------------------------------- /src/components/Attest/TargetPreview.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles, Typography } from "@material-ui/core"; 2 | import { useSelector } from "react-redux"; 3 | import { selectAttestTargetChain } from "../../store/selectors"; 4 | import { CHAINS_BY_ID } from "../../utils/consts"; 5 | 6 | const useStyles = makeStyles((theme) => ({ 7 | description: { 8 | textAlign: "center", 9 | }, 10 | })); 11 | 12 | export default function TargetPreview() { 13 | const classes = useStyles(); 14 | const targetChain = useSelector(selectAttestTargetChain); 15 | 16 | const explainerString = `to ${CHAINS_BY_ID[targetChain].name}`; 17 | 18 | return ( 19 | 24 | {explainerString} 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/components/Attest/WaitingForWalletMessage.tsx: -------------------------------------------------------------------------------- 1 | import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk"; 2 | import { makeStyles, Typography } from "@material-ui/core"; 3 | import { useSelector } from "react-redux"; 4 | import { 5 | selectAttestAttestTx, 6 | selectAttestCreateTx, 7 | selectAttestIsCreating, 8 | selectAttestIsSending, 9 | selectAttestTargetChain, 10 | } from "../../store/selectors"; 11 | import { WAITING_FOR_WALLET_AND_CONF } from "../Transfer/WaitingForWalletMessage"; 12 | 13 | const useStyles = makeStyles((theme) => ({ 14 | message: { 15 | color: theme.palette.warning.light, 16 | marginTop: theme.spacing(1), 17 | textAlign: "center", 18 | }, 19 | })); 20 | 21 | export default function WaitingForWalletMessage() { 22 | const classes = useStyles(); 23 | const isSending = useSelector(selectAttestIsSending); 24 | const attestTx = useSelector(selectAttestAttestTx); 25 | const targetChain = useSelector(selectAttestTargetChain); 26 | const isCreating = useSelector(selectAttestIsCreating); 27 | const createTx = useSelector(selectAttestCreateTx); 28 | const showWarning = (isSending && !attestTx) || (isCreating && !createTx); 29 | return showWarning ? ( 30 | 31 | {WAITING_FOR_WALLET_AND_CONF}{" "} 32 | {targetChain === CHAIN_ID_SOLANA && isCreating 33 | ? "Note: there will be several transactions" 34 | : null} 35 | 36 | ) : null; 37 | } 38 | -------------------------------------------------------------------------------- /src/components/ButtonWithLoader.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | CircularProgress, 4 | makeStyles, 5 | Typography, 6 | } from "@material-ui/core"; 7 | import { ReactChild } from "react"; 8 | 9 | const useStyles = makeStyles((theme) => ({ 10 | root: { 11 | position: "relative", 12 | }, 13 | button: { 14 | marginTop: theme.spacing(2), 15 | width: "100%", 16 | }, 17 | loader: { 18 | position: "absolute", 19 | bottom: 0, 20 | left: "50%", 21 | marginLeft: -12, 22 | marginBottom: 6, 23 | }, 24 | error: { 25 | marginTop: theme.spacing(1), 26 | textAlign: "center", 27 | }, 28 | })); 29 | 30 | export default function ButtonWithLoader({ 31 | disabled, 32 | onClick, 33 | showLoader, 34 | error, 35 | children, 36 | }: { 37 | disabled?: boolean; 38 | onClick: () => void; 39 | showLoader?: boolean; 40 | error?: string; 41 | children: ReactChild; 42 | }) { 43 | const classes = useStyles(); 44 | return ( 45 | <> 46 |
47 | 56 | {showLoader ? ( 57 | 62 | ) : null} 63 |
64 | {error ? ( 65 | 66 | {error} 67 | 68 | ) : null} 69 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /src/components/ChainSelect.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ListItemIcon, 3 | ListItemText, 4 | makeStyles, 5 | MenuItem, 6 | OutlinedTextFieldProps, 7 | TextField, 8 | } from "@material-ui/core"; 9 | import clsx from "clsx"; 10 | import { ChainInfo } from "../utils/consts"; 11 | 12 | const useStyles = makeStyles((theme) => ({ 13 | select: { 14 | "& .MuiSelect-root": { 15 | display: "flex", 16 | alignItems: "center", 17 | }, 18 | }, 19 | listItemIcon: { 20 | minWidth: 40, 21 | }, 22 | icon: { 23 | height: 24, 24 | maxWidth: 24, 25 | }, 26 | })); 27 | 28 | const createChainMenuItem = ({ id, name, logo }: ChainInfo, classes: any) => ( 29 | 30 | 31 | {name} 32 | 33 | {name} 34 | 35 | ); 36 | 37 | interface ChainSelectProps extends OutlinedTextFieldProps { 38 | chains: ChainInfo[]; 39 | } 40 | 41 | export default function ChainSelect({ chains, ...rest }: ChainSelectProps) { 42 | const classes = useStyles(); 43 | 44 | return ( 45 | 46 | {chains.map((chain) => createChainMenuItem(chain, classes))} 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/components/ChainSelectArrow.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton } from "@material-ui/core"; 2 | import { ArrowForward, SwapHoriz } from "@material-ui/icons"; 3 | import { useState } from "react"; 4 | 5 | export default function ChainSelectArrow({ 6 | onClick, 7 | disabled, 8 | }: { 9 | onClick: () => void; 10 | disabled: boolean; 11 | }) { 12 | const [showSwap, setShowSwap] = useState(false); 13 | 14 | return ( 15 | { 18 | setShowSwap(true); 19 | }} 20 | onMouseLeave={() => { 21 | setShowSwap(false); 22 | }} 23 | disabled={disabled} 24 | > 25 | {showSwap ? : } 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/components/ChainWarningMessage.tsx: -------------------------------------------------------------------------------- 1 | import { ChainId } from "@certusone/wormhole-sdk"; 2 | import { Link, makeStyles, Typography } from "@material-ui/core"; 3 | import { Alert } from "@material-ui/lab"; 4 | import { useMemo } from "react"; 5 | import { CHAIN_CONFIG_MAP } from "../config"; 6 | 7 | const useStyles = makeStyles((theme) => ({ 8 | alert: { 9 | marginTop: theme.spacing(1), 10 | marginBottom: theme.spacing(1), 11 | }, 12 | })); 13 | 14 | export default function ChainWarningMessage({ chainId }: { chainId: ChainId }) { 15 | const classes = useStyles(); 16 | 17 | const warningMessage = useMemo(() => { 18 | return CHAIN_CONFIG_MAP[chainId]?.warningMessage; 19 | }, [chainId]); 20 | 21 | if (warningMessage === undefined) { 22 | return null; 23 | } 24 | 25 | return ( 26 | 27 | {warningMessage.text} 28 | {warningMessage.link ? ( 29 | 30 | 31 | {warningMessage.link.text} 32 | 33 | 34 | ) : null} 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/components/EthereumSignerKey.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | import { Typography } from "@material-ui/core"; 3 | import { useEthereumProvider } from "../contexts/EthereumProviderContext"; 4 | import ToggleConnectedButton from "./ToggleConnectedButton"; 5 | import EvmConnectWalletDialog from "./EvmConnectWalletDialog"; 6 | import { ChainId } from "@certusone/wormhole-sdk"; 7 | 8 | const EthereumSignerKey = ({ chainId }: { chainId: ChainId }) => { 9 | const { disconnect, signerAddress, providerError } = useEthereumProvider(); 10 | 11 | const [isDialogOpen, setIsDialogOpen] = useState(false); 12 | 13 | const openDialog = useCallback(() => { 14 | setIsDialogOpen(true); 15 | }, [setIsDialogOpen]); 16 | 17 | const closeDialog = useCallback(() => { 18 | setIsDialogOpen(false); 19 | }, [setIsDialogOpen]); 20 | 21 | return ( 22 | <> 23 | 29 | 34 | {providerError ? ( 35 | 36 | {providerError} 37 | 38 | ) : null} 39 | 40 | ); 41 | }; 42 | 43 | export default EthereumSignerKey; 44 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { Button, makeStyles, Typography } from "@material-ui/core"; 2 | 3 | const useStyles = makeStyles((theme) => ({ 4 | footer: { 5 | textAlign: "center", 6 | borderTop: "1px solid #585587", 7 | position: "relative", 8 | maxWidth: 1100, 9 | margin: "80px auto 0px", 10 | paddingTop: theme.spacing(2), 11 | paddingBottom: theme.spacing(6.5), 12 | [theme.breakpoints.up("md")]: { 13 | paddingBottom: theme.spacing(12), 14 | }, 15 | }, 16 | button: { 17 | textTransform: "none", 18 | margin: theme.spacing(1), 19 | }, 20 | })); 21 | 22 | export default function Footer() { 23 | const classes = useStyles(); 24 | return ( 25 |
26 | 27 | This Interface is only intended as a developmental tool. For more 28 | information, visit the sites below: 29 | 30 | 31 | 42 | 43 | 54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/components/HeaderText.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles, Typography } from "@material-ui/core"; 2 | import clsx from "clsx"; 3 | import { ReactChild } from "react"; 4 | 5 | const useStyles = makeStyles((theme) => ({ 6 | centeredContainer: { 7 | marginBottom: theme.spacing(4), 8 | textAlign: "center", 9 | width: "100%", 10 | }, 11 | linearGradient: { 12 | WebkitBackgroundClip: "text", 13 | backgroundClip: "text", 14 | WebkitTextFillColor: "transparent", 15 | MozBackgroundClip: "text", 16 | MozTextFillColor: "transparent", 17 | }, 18 | subtitle: { 19 | marginTop: theme.spacing(2), 20 | }, 21 | })); 22 | 23 | export default function HeaderText({ 24 | children, 25 | white, 26 | small, 27 | subtitle, 28 | }: { 29 | children: ReactChild; 30 | white?: boolean; 31 | small?: boolean; 32 | subtitle?: ReactChild; 33 | }) { 34 | const classes = useStyles(); 35 | return ( 36 |
37 | 42 | {children} 43 | 44 | {subtitle ? ( 45 | 46 | {subtitle} 47 | 48 | ) : null} 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/components/InjectiveWalletKey.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from "react"; 2 | import { useInjectiveContext } from "../contexts/InjectiveWalletContext"; 3 | import InjectiveConnectWalletDialog from "./InjectiveConnectWalletDialog"; 4 | import ToggleConnectedButton from "./ToggleConnectedButton"; 5 | 6 | const InjectiveWalletKey = () => { 7 | const { disconnect, address } = useInjectiveContext(); 8 | const [isDialogOpen, setIsDialogOpen] = useState(false); 9 | 10 | const connect = useCallback(() => { 11 | setIsDialogOpen(true); 12 | }, [setIsDialogOpen]); 13 | 14 | const closeDialog = useCallback(() => { 15 | setIsDialogOpen(false); 16 | }, [setIsDialogOpen]); 17 | 18 | return ( 19 | <> 20 | 26 | 30 | 31 | ); 32 | }; 33 | 34 | export default InjectiveWalletKey; 35 | -------------------------------------------------------------------------------- /src/components/KeyAndBalance.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ChainId, 3 | CHAIN_ID_ALGORAND, 4 | CHAIN_ID_APTOS, 5 | CHAIN_ID_INJECTIVE, 6 | CHAIN_ID_NEAR, 7 | CHAIN_ID_SOLANA, 8 | CHAIN_ID_SUI, 9 | CHAIN_ID_XPLA, 10 | isEVMChain, 11 | isTerraChain, 12 | CHAIN_ID_SEI, 13 | } from "@certusone/wormhole-sdk"; 14 | import AlgorandWalletKey from "./AlgorandWalletKey"; 15 | import AptosWalletKey from "./AptosWalletKey"; 16 | import EthereumSignerKey from "./EthereumSignerKey"; 17 | import InjectiveWalletKey from "./InjectiveWalletKey"; 18 | import NearWalletKey from "./NearWalletKey"; 19 | import SolanaWalletKey from "./SolanaWalletKey"; 20 | import SuiWalletKey from "./SuiWalletKey"; 21 | import TerraWalletKey from "./TerraWalletKey"; 22 | import XplaWalletKey from "./XplaWalletKey"; 23 | import SeiWalletKey from "./SeiWalletKey"; 24 | 25 | function KeyAndBalance({ chainId }: { chainId: ChainId }) { 26 | if (isEVMChain(chainId)) { 27 | return ; 28 | } 29 | if (chainId === CHAIN_ID_SOLANA) { 30 | return ; 31 | } 32 | if (isTerraChain(chainId)) { 33 | return ; 34 | } 35 | if (chainId === CHAIN_ID_ALGORAND) { 36 | return ; 37 | } 38 | if (chainId === CHAIN_ID_XPLA) { 39 | return ; 40 | } 41 | if (chainId === CHAIN_ID_APTOS) { 42 | return ; 43 | } 44 | if (chainId === CHAIN_ID_INJECTIVE) { 45 | return ; 46 | } 47 | if (chainId === CHAIN_ID_SEI) { 48 | return ; 49 | } 50 | if (chainId === CHAIN_ID_NEAR) { 51 | return ; 52 | } 53 | if (chainId === CHAIN_ID_SUI) { 54 | return ; 55 | } 56 | return null; 57 | } 58 | 59 | export default KeyAndBalance; 60 | -------------------------------------------------------------------------------- /src/components/LowBalanceWarning.tsx: -------------------------------------------------------------------------------- 1 | import { ChainId, isTerraChain } from "@certusone/wormhole-sdk"; 2 | import { makeStyles, Typography } from "@material-ui/core"; 3 | import { Alert } from "@material-ui/lab"; 4 | import { useSelector } from "react-redux"; 5 | import useIsWalletReady from "../hooks/useIsWalletReady"; 6 | import useTransactionFees from "../hooks/useTransactionFees"; 7 | import { selectTransferUseRelayer } from "../store/selectors"; 8 | import { getDefaultNativeCurrencySymbol } from "../utils/consts"; 9 | 10 | const useStyles = makeStyles((theme) => ({ 11 | alert: { 12 | marginTop: theme.spacing(1), 13 | marginBottom: theme.spacing(1), 14 | }, 15 | })); 16 | 17 | function LowBalanceWarning({ chainId }: { chainId: ChainId }) { 18 | const classes = useStyles(); 19 | const { isReady } = useIsWalletReady(chainId); 20 | const transactionFeeWarning = useTransactionFees(chainId); 21 | const relayerSelected = !!useSelector(selectTransferUseRelayer); 22 | const terraChain = isTerraChain(chainId); 23 | 24 | const displayWarning = 25 | isReady && 26 | !relayerSelected && 27 | (terraChain || transactionFeeWarning.balanceString) && 28 | transactionFeeWarning.isSufficientBalance === false; 29 | 30 | const warningMessage = terraChain 31 | ? "This wallet may not have sufficient funds to pay for the upcoming transaction fees." 32 | : `This wallet has a very low ${getDefaultNativeCurrencySymbol( 33 | chainId 34 | )} balance and may not be able to pay for the upcoming transaction fees.`; 35 | 36 | const content = ( 37 | 38 | {warningMessage} 39 | {!terraChain ? ( 40 | 41 | {"Current balance: " + transactionFeeWarning.balanceString} 42 | 43 | ) : null} 44 | 45 | ); 46 | 47 | return displayWarning ? content : null; 48 | } 49 | 50 | export default LowBalanceWarning; 51 | -------------------------------------------------------------------------------- /src/components/NFT/Redeem.tsx: -------------------------------------------------------------------------------- 1 | import { isTerraChain } from "@certusone/wormhole-sdk"; 2 | import { useSelector } from "react-redux"; 3 | import { useHandleNFTRedeem } from "../../hooks/useHandleNFTRedeem"; 4 | import useIsWalletReady from "../../hooks/useIsWalletReady"; 5 | import { selectNFTTargetChain } from "../../store/selectors"; 6 | import ButtonWithLoader from "../ButtonWithLoader"; 7 | import KeyAndBalance from "../KeyAndBalance"; 8 | import StepDescription from "../StepDescription"; 9 | import TerraFeeDenomPicker from "../TerraFeeDenomPicker"; 10 | import WaitingForWalletMessage from "./WaitingForWalletMessage"; 11 | 12 | function Redeem() { 13 | const { handleClick, disabled, showLoader } = useHandleNFTRedeem(); 14 | const targetChain = useSelector(selectNFTTargetChain); 15 | const { isReady, statusMessage } = useIsWalletReady(targetChain); 16 | return ( 17 | <> 18 | Receive the NFT on the target chain 19 | 20 | {isTerraChain(targetChain) && ( 21 | 22 | )} 23 | 29 | Redeem 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | export default Redeem; 37 | -------------------------------------------------------------------------------- /src/components/NFT/RedeemPreview.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles, Typography } from "@material-ui/core"; 2 | import { useCallback } from "react"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { selectNFTRedeemTx, selectNFTTargetChain } from "../../store/selectors"; 5 | import { reset } from "../../store/nftSlice"; 6 | import ButtonWithLoader from "../ButtonWithLoader"; 7 | import ShowTx from "../ShowTx"; 8 | 9 | const useStyles = makeStyles((theme) => ({ 10 | description: { 11 | textAlign: "center", 12 | }, 13 | })); 14 | 15 | export default function RedeemPreview() { 16 | const classes = useStyles(); 17 | const dispatch = useDispatch(); 18 | const targetChain = useSelector(selectNFTTargetChain); 19 | const redeemTx = useSelector(selectNFTRedeemTx); 20 | const handleResetClick = useCallback(() => { 21 | dispatch(reset()); 22 | }, [dispatch]); 23 | 24 | const explainerString = 25 | "Success! The redeem transaction was submitted. The NFT will become available once the transaction confirms."; 26 | 27 | return ( 28 | <> 29 | 34 | {explainerString} 35 | 36 | {redeemTx ? : null} 37 | 38 | Transfer Another NFT! 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/components/NFT/Send.tsx: -------------------------------------------------------------------------------- 1 | import { isTerraChain } from "@certusone/wormhole-sdk"; 2 | import { Alert } from "@material-ui/lab"; 3 | import { useSelector } from "react-redux"; 4 | import { useHandleNFTTransfer } from "../../hooks/useHandleNFTTransfer"; 5 | import useIsWalletReady from "../../hooks/useIsWalletReady"; 6 | import { 7 | selectNFTSourceWalletAddress, 8 | selectNFTSourceChain, 9 | selectNFTTargetError, 10 | selectNFTTransferTx, 11 | selectNFTIsSendComplete, 12 | } from "../../store/selectors"; 13 | import { CHAINS_BY_ID } from "../../utils/consts"; 14 | import ButtonWithLoader from "../ButtonWithLoader"; 15 | import KeyAndBalance from "../KeyAndBalance"; 16 | import ShowTx from "../ShowTx"; 17 | import StepDescription from "../StepDescription"; 18 | import TerraFeeDenomPicker from "../TerraFeeDenomPicker"; 19 | import TransactionProgress from "../TransactionProgress"; 20 | import WaitingForWalletMessage from "./WaitingForWalletMessage"; 21 | 22 | function Send() { 23 | const { handleClick, disabled, showLoader } = useHandleNFTTransfer(); 24 | const sourceChain = useSelector(selectNFTSourceChain); 25 | const error = useSelector(selectNFTTargetError); 26 | const { isReady, statusMessage, walletAddress } = 27 | useIsWalletReady(sourceChain); 28 | const sourceWalletAddress = useSelector(selectNFTSourceWalletAddress); 29 | const transferTx = useSelector(selectNFTTransferTx); 30 | const isSendComplete = useSelector(selectNFTIsSendComplete); 31 | //The chain ID compare is handled implicitly, as the isWalletReady hook should report !isReady if the wallet is on the wrong chain. 32 | const isWrongWallet = 33 | sourceWalletAddress && 34 | walletAddress && 35 | sourceWalletAddress !== walletAddress; 36 | const isDisabled = !isReady || isWrongWallet || disabled; 37 | const errorMessage = isWrongWallet 38 | ? "A different wallet is connected than in Step 1." 39 | : statusMessage || error || undefined; 40 | return ( 41 | <> 42 | 43 | Transfer the NFT to the Wormhole Token Bridge. 44 | 45 | 46 | {isTerraChain(sourceChain) && ( 47 | 48 | )} 49 | 50 | This will initiate the transfer on {CHAINS_BY_ID[sourceChain].name} and 51 | wait for finalization. If you navigate away from this page before 52 | completing Step 4, you will have to perform the recovery workflow to 53 | complete the transfer. 54 | 55 | 61 | Transfer 62 | 63 | 64 | {transferTx ? : null} 65 | 70 | 71 | ); 72 | } 73 | 74 | export default Send; 75 | -------------------------------------------------------------------------------- /src/components/NFT/SendPreview.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles, Typography } from "@material-ui/core"; 2 | import { useSelector } from "react-redux"; 3 | import { 4 | selectNFTSourceChain, 5 | selectNFTTransferTx, 6 | } from "../../store/selectors"; 7 | import ShowTx from "../ShowTx"; 8 | 9 | const useStyles = makeStyles((theme) => ({ 10 | description: { 11 | textAlign: "center", 12 | }, 13 | tx: { 14 | marginTop: theme.spacing(1), 15 | textAlign: "center", 16 | }, 17 | viewButton: { 18 | marginTop: theme.spacing(1), 19 | }, 20 | })); 21 | 22 | export default function SendPreview() { 23 | const classes = useStyles(); 24 | const sourceChain = useSelector(selectNFTSourceChain); 25 | const transferTx = useSelector(selectNFTTransferTx); 26 | 27 | const explainerString = "The NFT has been sent!"; 28 | 29 | return ( 30 | <> 31 | 36 | {explainerString} 37 | 38 | {transferTx ? : null} 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/NFT/SourcePreview.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles, Typography } from "@material-ui/core"; 2 | import { useSelector } from "react-redux"; 3 | import { 4 | selectNFTSourceChain, 5 | selectNFTSourceParsedTokenAccount, 6 | } from "../../store/selectors"; 7 | import { CHAINS_BY_ID } from "../../utils/consts"; 8 | import SmartAddress from "../SmartAddress"; 9 | import NFTViewer from "../TokenSelectors/NFTViewer"; 10 | 11 | const useStyles = makeStyles((theme) => ({ 12 | description: { 13 | textAlign: "center", 14 | }, 15 | })); 16 | 17 | export default function SourcePreview() { 18 | const classes = useStyles(); 19 | const sourceChain = useSelector(selectNFTSourceChain); 20 | const sourceParsedTokenAccount = useSelector( 21 | selectNFTSourceParsedTokenAccount 22 | ); 23 | 24 | const explainerContent = 25 | sourceChain && sourceParsedTokenAccount ? ( 26 | <> 27 | You will transfer 1 NFT of 28 | 32 | from 33 | 37 | on {CHAINS_BY_ID[sourceChain].name} 38 | 39 | ) : ( 40 | "" 41 | ); 42 | 43 | return ( 44 | <> 45 | 50 | {explainerContent} 51 | 52 | {sourceParsedTokenAccount ? ( 53 | 54 | ) : null} 55 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /src/components/NFT/TargetPreview.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles, Typography } from "@material-ui/core"; 2 | import { useSelector } from "react-redux"; 3 | import { 4 | selectNFTTargetAddressHex, 5 | selectNFTTargetChain, 6 | } from "../../store/selectors"; 7 | import { hexToNativeString } from "@certusone/wormhole-sdk"; 8 | import { CHAINS_BY_ID } from "../../utils/consts"; 9 | import SmartAddress from "../SmartAddress"; 10 | 11 | const useStyles = makeStyles((theme) => ({ 12 | description: { 13 | textAlign: "center", 14 | }, 15 | })); 16 | 17 | export default function TargetPreview() { 18 | const classes = useStyles(); 19 | const targetChain = useSelector(selectNFTTargetChain); 20 | const targetAddress = useSelector(selectNFTTargetAddressHex); 21 | const targetAddressNative = hexToNativeString(targetAddress, targetChain); 22 | 23 | const explainerContent = 24 | targetChain && targetAddressNative ? ( 25 | <> 26 | to 27 | 28 | on {CHAINS_BY_ID[targetChain].name} 29 | 30 | ) : ( 31 | "" 32 | ); 33 | 34 | return ( 35 | 40 | {explainerContent} 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/NFT/WaitingForWalletMessage.tsx: -------------------------------------------------------------------------------- 1 | import { CHAIN_ID_SOLANA, isEVMChain } from "@certusone/wormhole-sdk"; 2 | import { makeStyles, Typography } from "@material-ui/core"; 3 | import { useSelector } from "react-redux"; 4 | import { 5 | selectNFTIsRedeeming, 6 | selectNFTIsSending, 7 | selectNFTRedeemTx, 8 | selectNFTSourceChain, 9 | selectNFTTargetChain, 10 | selectNFTTransferTx, 11 | } from "../../store/selectors"; 12 | import { WAITING_FOR_WALLET_AND_CONF } from "../Transfer/WaitingForWalletMessage"; 13 | 14 | const useStyles = makeStyles((theme) => ({ 15 | message: { 16 | color: theme.palette.warning.light, 17 | marginTop: theme.spacing(1), 18 | textAlign: "center", 19 | }, 20 | })); 21 | 22 | export default function WaitingForWalletMessage() { 23 | const classes = useStyles(); 24 | const sourceChain = useSelector(selectNFTSourceChain); 25 | const isSending = useSelector(selectNFTIsSending); 26 | const transferTx = useSelector(selectNFTTransferTx); 27 | const targetChain = useSelector(selectNFTTargetChain); 28 | const isRedeeming = useSelector(selectNFTIsRedeeming); 29 | const redeemTx = useSelector(selectNFTRedeemTx); 30 | const showWarning = (isSending && !transferTx) || (isRedeeming && !redeemTx); 31 | return showWarning ? ( 32 | 33 | {WAITING_FOR_WALLET_AND_CONF}{" "} 34 | {targetChain === CHAIN_ID_SOLANA && isRedeeming 35 | ? "Note: there will be several transactions" 36 | : isEVMChain(sourceChain) && isSending 37 | ? "Note: there will be two transactions" 38 | : null} 39 | 40 | ) : null; 41 | } 42 | -------------------------------------------------------------------------------- /src/components/NearWalletKey.tsx: -------------------------------------------------------------------------------- 1 | import { useNearContext } from "../contexts/NearWalletContext"; 2 | import ToggleConnectedButton from "./ToggleConnectedButton"; 3 | 4 | const NearWalletKey = () => { 5 | const { connect, disconnect, accountId: activeAccount } = useNearContext(); 6 | 7 | return ( 8 | 14 | ); 15 | }; 16 | 17 | export default NearWalletKey; 18 | -------------------------------------------------------------------------------- /src/components/NumberTextField.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | InputAdornment, 4 | TextField, 5 | TextFieldProps, 6 | } from "@material-ui/core"; 7 | 8 | export default function NumberTextField({ 9 | onMaxClick, 10 | ...props 11 | }: TextFieldProps & { onMaxClick?: () => void }) { 12 | return ( 13 | 19 | 26 | 27 | ) : undefined, 28 | ...(props?.InputProps || {}), 29 | }} 30 | > 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/components/RelaySelector.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | CircularProgress, 3 | makeStyles, 4 | MenuItem, 5 | TextField, 6 | Typography, 7 | } from "@material-ui/core"; 8 | import { useCallback } from "react"; 9 | import useRelayersAvailable, { Relayer } from "../hooks/useRelayersAvailable"; 10 | 11 | const useStyles = makeStyles((theme) => ({ 12 | mainContainer: { 13 | textAlign: "center", 14 | }, 15 | })); 16 | 17 | export default function RelaySelector({ 18 | selectedValue, 19 | onChange, 20 | }: { 21 | selectedValue: Relayer | null; 22 | onChange: (newValue: Relayer | null) => void; 23 | }) { 24 | const classes = useStyles(); 25 | const availableRelayers = useRelayersAvailable(true); 26 | 27 | const loader = ( 28 |
29 | 30 | Loading available relayers 31 |
32 | ); 33 | 34 | const onChangeWrapper = useCallback( 35 | (event) => { 36 | console.log(event, "event in selector"); 37 | event.target.value 38 | ? onChange( 39 | availableRelayers?.data?.relayers?.find( 40 | (x) => x.url === event.target.value 41 | ) || null 42 | ) 43 | : onChange(null); 44 | }, 45 | [onChange, availableRelayers] 46 | ); 47 | 48 | console.log("selectedValue in relay selector", selectedValue); 49 | 50 | const selector = ( 51 | 58 | {availableRelayers.data?.relayers?.map((item) => ( 59 | 60 | {item.name} 61 | 62 | ))} 63 | 64 | ); 65 | 66 | const error = ( 67 | 68 | No relayers are available at this time. 69 | 70 | ); 71 | 72 | return ( 73 |
74 | {availableRelayers.data?.relayers?.length 75 | ? selector 76 | : availableRelayers.isFetching 77 | ? loader 78 | : error} 79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/components/SeiConnectWalletDialog.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog, 3 | DialogTitle, 4 | Divider, 5 | IconButton, 6 | List, 7 | ListItem, 8 | ListItemText, 9 | makeStyles, 10 | } from "@material-ui/core"; 11 | import CloseIcon from "@material-ui/icons/Close"; 12 | import { useWallet } from "@sei-js/react"; 13 | import { useCallback } from "react"; 14 | 15 | const useStyles = makeStyles((theme) => ({ 16 | flexTitle: { 17 | display: "flex", 18 | alignItems: "center", 19 | "& > div": { 20 | flexGrow: 1, 21 | marginRight: theme.spacing(4), 22 | }, 23 | "& > button": { 24 | marginRight: theme.spacing(-1), 25 | }, 26 | }, 27 | icon: { 28 | height: 24, 29 | width: 24, 30 | }, 31 | })); 32 | 33 | const WalletOptions = ({ 34 | walletInfo, 35 | connect, 36 | isInstalled, 37 | onClose, 38 | }: { 39 | walletInfo: string; 40 | connect: (wallet: any) => void; 41 | isInstalled: boolean; 42 | onClose: () => void; 43 | }) => { 44 | const name = `${walletInfo.charAt(0).toUpperCase()}${walletInfo.slice(1)}`; 45 | 46 | const handleClick = useCallback(() => { 47 | connect(walletInfo); 48 | onClose(); 49 | }, [connect, onClose, walletInfo]); 50 | 51 | return ( 52 | 53 | {isInstalled ? name : `Install ${name}`} 54 | 55 | ); 56 | }; 57 | 58 | const SeiConnectWalletDialog = ({ 59 | isOpen, 60 | onClose, 61 | }: { 62 | isOpen: boolean; 63 | onClose: () => void; 64 | }) => { 65 | const { 66 | connect, 67 | installedWallets: installedWalletKeys, 68 | supportedWallets: supportedWalletKeys, 69 | } = useWallet(); 70 | const classes = useStyles(); 71 | 72 | const installedWallets = installedWalletKeys.map((walletInfo) => ( 73 | 80 | )); 81 | 82 | const undetectedWallets = supportedWalletKeys 83 | .filter((walletInfo) => !installedWalletKeys.includes(walletInfo)) 84 | .map((walletInfo) => ( 85 | 92 | )); 93 | 94 | return ( 95 | 96 | 97 |
98 |
Select your wallet
99 | 100 | 101 | 102 |
103 |
104 | 105 | {installedWallets} 106 | {!!installedWallets.length && !!undetectedWallets.length && ( 107 | 108 | )} 109 | {undetectedWallets} 110 | 111 |
112 | ); 113 | }; 114 | 115 | export default SeiConnectWalletDialog; 116 | -------------------------------------------------------------------------------- /src/components/SeiWalletKey.tsx: -------------------------------------------------------------------------------- 1 | import { useWallet } from "@sei-js/react"; 2 | import { useCallback, useState } from "react"; 3 | import SeiConnectWalletDialog from "./SeiConnectWalletDialog"; 4 | import ToggleConnectedButton from "./ToggleConnectedButton"; 5 | 6 | const SeiWalletKey = () => { 7 | const { disconnect, accounts } = useWallet(); 8 | const address = accounts.length ? accounts[0].address : null; 9 | const [isDialogOpen, setIsDialogOpen] = useState(false); 10 | 11 | const connect = useCallback(() => { 12 | setIsDialogOpen(true); 13 | }, [setIsDialogOpen]); 14 | 15 | const closeDialog = useCallback(() => { 16 | setIsDialogOpen(false); 17 | }, [setIsDialogOpen]); 18 | 19 | return ( 20 | <> 21 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default SeiWalletKey; 33 | -------------------------------------------------------------------------------- /src/components/SolanaTPSWarning.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles } from "@material-ui/core"; 2 | import { Alert } from "@material-ui/lab"; 3 | import { Connection } from "@solana/web3.js"; 4 | import numeral from "numeral"; 5 | import { useEffect, useState } from "react"; 6 | import { SOLANA_HOST } from "../utils/consts"; 7 | 8 | const useStyles = makeStyles((theme) => ({ 9 | alert: { 10 | marginTop: theme.spacing(1), 11 | marginBottom: theme.spacing(1), 12 | }, 13 | })); 14 | 15 | export default function SolanaTPSWarning() { 16 | const classes = useStyles(); 17 | const [tps, setTps] = useState(null); 18 | useEffect(() => { 19 | let cancelled = false; 20 | let interval = setInterval(() => { 21 | (async () => { 22 | try { 23 | const connection = new Connection(SOLANA_HOST); 24 | const samples = await connection.getRecentPerformanceSamples(1); 25 | if (samples.length >= 1) { 26 | let short = samples 27 | .filter((sample) => sample.numTransactions !== 0) 28 | .map( 29 | (sample) => sample.numTransactions / sample.samplePeriodSecs 30 | ); 31 | const avgTps = short[0]; 32 | if (!cancelled) { 33 | setTps(avgTps); 34 | } 35 | } 36 | } catch (e) {} 37 | })(); 38 | }, 5000); 39 | return () => { 40 | cancelled = true; 41 | clearInterval(interval); 42 | }; 43 | }, []); 44 | return tps !== null && tps < 1500 ? ( 45 | {`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( 50 | tps 51 | ).format("0,0")}`} 52 | ) : null; 53 | } 54 | -------------------------------------------------------------------------------- /src/components/SolanaWalletKey.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo, useState } from "react"; 2 | import { useSolanaWallet } from "../contexts/SolanaWalletContext"; 3 | import SolanaConnectWalletDialog from "./SolanaConnectWalletDialog"; 4 | import ToggleConnectedButton from "./ToggleConnectedButton"; 5 | 6 | const SolanaWalletKey = () => { 7 | const { publicKey, wallet, disconnect } = useSolanaWallet(); 8 | const [isDialogOpen, setIsDialogOpen] = useState(false); 9 | 10 | const openDialog = useCallback(() => { 11 | setIsDialogOpen(true); 12 | }, [setIsDialogOpen]); 13 | 14 | const closeDialog = useCallback(() => { 15 | setIsDialogOpen(false); 16 | }, [setIsDialogOpen]); 17 | 18 | const publicKeyBase58 = useMemo(() => { 19 | return publicKey?.toBase58() || ""; 20 | }, [publicKey]); 21 | 22 | return ( 23 | <> 24 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default SolanaWalletKey; 37 | -------------------------------------------------------------------------------- /src/components/StepDescription.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles, Typography } from "@material-ui/core"; 2 | import { ReactChild } from "react"; 3 | 4 | const useStyles = makeStyles((theme) => ({ 5 | description: { 6 | marginBottom: theme.spacing(4), 7 | }, 8 | })); 9 | 10 | export default function StepDescription({ 11 | children, 12 | }: { 13 | children: ReactChild; 14 | }) { 15 | const classes = useStyles(); 16 | return ( 17 | 18 | {children} 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/SuiConnectWalletDialog.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog, 3 | DialogTitle, 4 | IconButton, 5 | List, 6 | ListItem, 7 | ListItemIcon, 8 | ListItemText, 9 | makeStyles, 10 | } from "@material-ui/core"; 11 | import CloseIcon from "@material-ui/icons/Close"; 12 | import { useWallet } from "@suiet/wallet-kit"; 13 | import { useCallback } from "react"; 14 | 15 | const useStyles = makeStyles((theme) => ({ 16 | flexTitle: { 17 | display: "flex", 18 | alignItems: "center", 19 | "& > div": { 20 | flexGrow: 1, 21 | marginRight: theme.spacing(4), 22 | }, 23 | "& > button": { 24 | marginRight: theme.spacing(-1), 25 | }, 26 | }, 27 | icon: { 28 | height: 24, 29 | width: 24, 30 | }, 31 | })); 32 | 33 | const WalletOptions = ({ 34 | name, 35 | select, 36 | onClose, 37 | icon, 38 | }: { 39 | name: string; 40 | select: (name: string) => void; 41 | onClose: () => void; 42 | icon: string; 43 | }) => { 44 | const classes = useStyles(); 45 | const handleClick = useCallback(() => { 46 | select(name); 47 | onClose(); 48 | }, [select, onClose, name]); 49 | 50 | return ( 51 | 52 | 53 | {name} 54 | 55 | {name} 56 | 57 | ); 58 | }; 59 | 60 | const SuiConnectWalletDialog = ({ 61 | isOpen, 62 | onClose, 63 | }: { 64 | isOpen: boolean; 65 | onClose: () => void; 66 | }) => { 67 | const { allAvailableWallets, select } = useWallet(); 68 | const classes = useStyles(); 69 | const filteredConnections = allAvailableWallets.map(({ name, iconUrl }) => ( 70 | 77 | )); 78 | return ( 79 | 80 | 81 |
82 |
Select your wallet
83 | 84 | 85 | 86 |
87 |
88 | {filteredConnections} 89 |
90 | ); 91 | }; 92 | 93 | export default SuiConnectWalletDialog; 94 | -------------------------------------------------------------------------------- /src/components/SuiWalletKey.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | import SuiConnectWalletDialog from "./SuiConnectWalletDialog"; 3 | import ToggleConnectedButton from "./ToggleConnectedButton"; 4 | import { useWallet } from "@suiet/wallet-kit"; 5 | 6 | const SuiWalletKey = () => { 7 | const { connected, address, disconnect } = useWallet(); 8 | const [isDialogOpen, setIsDialogOpen] = useState(false); 9 | 10 | const connect = useCallback(() => { 11 | setIsDialogOpen(true); 12 | }, [setIsDialogOpen]); 13 | 14 | const closeDialog = useCallback(() => { 15 | setIsDialogOpen(false); 16 | }, [setIsDialogOpen]); 17 | 18 | const onDisconnect = useCallback(() => { 19 | disconnect().catch((e) => console.error(e)); 20 | }, [disconnect]); 21 | 22 | return ( 23 | <> 24 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default SuiWalletKey; 36 | -------------------------------------------------------------------------------- /src/components/TerraWalletKey.tsx: -------------------------------------------------------------------------------- 1 | import { useConnectedWallet, useWallet } from "@terra-money/wallet-provider"; 2 | import { useCallback, useState } from "react"; 3 | import TerraConnectWalletDialog from "./TerraConnectWalletDialog"; 4 | import ToggleConnectedButton from "./ToggleConnectedButton"; 5 | 6 | const TerraWalletKey = () => { 7 | const wallet = useWallet(); 8 | const connectedWallet = useConnectedWallet(); 9 | 10 | const [isDialogOpen, setIsDialogOpen] = useState(false); 11 | 12 | const connect = useCallback(() => { 13 | setIsDialogOpen(true); 14 | }, [setIsDialogOpen]); 15 | 16 | const closeDialog = useCallback(() => { 17 | setIsDialogOpen(false); 18 | }, [setIsDialogOpen]); 19 | 20 | return ( 21 | <> 22 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default TerraWalletKey; 34 | -------------------------------------------------------------------------------- /src/components/ToggleConnectedButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, makeStyles, Tooltip } from "@material-ui/core"; 2 | import { LinkOff } from "@material-ui/icons"; 3 | 4 | const useStyles = makeStyles((theme) => ({ 5 | button: { 6 | display: "flex", 7 | margin: `${theme.spacing(1)}px auto`, 8 | width: "100%", 9 | maxWidth: 400, 10 | }, 11 | icon: { 12 | height: 24, 13 | width: 24, 14 | }, 15 | })); 16 | 17 | const ToggleConnectedButton = ({ 18 | connect, 19 | disconnect, 20 | connected, 21 | pk, 22 | walletIcon, 23 | }: { 24 | connect(): any; 25 | disconnect(): any; 26 | connected: boolean; 27 | pk: string; 28 | walletIcon?: string; 29 | }) => { 30 | const classes = useStyles(); 31 | const is0x = pk.startsWith("0x"); 32 | return connected ? ( 33 | 34 | 51 | 52 | ) : ( 53 | 62 | ); 63 | }; 64 | 65 | export default ToggleConnectedButton; 66 | -------------------------------------------------------------------------------- /src/components/TokenSelectors/OffsetButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, makeStyles } from "@material-ui/core"; 2 | import { ReactChild } from "react"; 3 | 4 | const useStyles = makeStyles((theme) => ({ 5 | offsetButton: { display: "block", marginLeft: "auto", marginTop: 8 }, 6 | })); 7 | 8 | export default function OffsetButton({ 9 | onClick, 10 | disabled, 11 | children, 12 | }: { 13 | onClick: () => void; 14 | disabled?: boolean; 15 | children: ReactChild; 16 | }) { 17 | const classes = useStyles(); 18 | return ( 19 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/components/TokenSelectors/RefreshButtonWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createStyles, 3 | IconButton, 4 | makeStyles, 5 | Tooltip, 6 | } from "@material-ui/core"; 7 | import RefreshIcon from "@material-ui/icons/Refresh"; 8 | 9 | const useStyles = makeStyles(() => 10 | createStyles({ 11 | inlineContentWrapper: { 12 | display: "inline-block", 13 | flexGrow: 1, 14 | }, 15 | flexWrapper: { 16 | "& > *": { 17 | margin: ".5rem", 18 | }, 19 | display: "flex", 20 | alignItems: "center", 21 | }, 22 | }) 23 | ); 24 | 25 | export default function RefreshButtonWrapper({ 26 | children, 27 | callback, 28 | }: { 29 | children: JSX.Element; 30 | callback: () => any; 31 | }) { 32 | const classes = useStyles(); 33 | 34 | const refreshWrapper = ( 35 |
36 |
{children}
37 | 38 | 39 | 40 | 41 | 42 |
43 | ); 44 | 45 | return refreshWrapper; 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Transfer/PendingVAAWarning.tsx: -------------------------------------------------------------------------------- 1 | import { ChainId } from "@certusone/wormhole-sdk"; 2 | import { Link, makeStyles } from "@material-ui/core"; 3 | import { Alert } from "@material-ui/lab"; 4 | import { CHAINS_BY_ID } from "../../utils/consts"; 5 | 6 | const useStyles = makeStyles((theme) => ({ 7 | alert: { 8 | marginTop: theme.spacing(1), 9 | marginBottom: theme.spacing(1), 10 | }, 11 | })); 12 | 13 | const PendingVAAWarning = ({ sourceChain }: { sourceChain: ChainId }) => { 14 | const classes = useStyles(); 15 | const chainName = CHAINS_BY_ID[sourceChain]?.name || "unknown"; 16 | const message = `The daily notional value limit for transfers on ${chainName} has been exceeded. As 17 | a result, the VAA for this transfer is pending. If you have any questions, 18 | please open a support ticket on `; 19 | return ( 20 | 21 | {message} 22 | 27 | https://discord.gg/wormholecrypto 28 | 29 | {"."} 30 | 31 | ); 32 | }; 33 | 34 | export default PendingVAAWarning; 35 | -------------------------------------------------------------------------------- /src/components/Transfer/RedeemPreview.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles, Typography } from "@material-ui/core"; 2 | import { useCallback } from "react"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { 5 | selectTransferRedeemTx, 6 | selectTransferTargetChain, 7 | } from "../../store/selectors"; 8 | import { reset } from "../../store/transferSlice"; 9 | import ButtonWithLoader from "../ButtonWithLoader"; 10 | import ShowTx from "../ShowTx"; 11 | import AddToMetamask from "./AddToMetamask"; 12 | 13 | const useStyles = makeStyles((theme) => ({ 14 | description: { 15 | textAlign: "center", 16 | marginBottom: theme.spacing(2), 17 | }, 18 | })); 19 | 20 | export default function RedeemPreview({ 21 | overrideExplainerString, 22 | }: { 23 | overrideExplainerString?: string; 24 | }) { 25 | const classes = useStyles(); 26 | const dispatch = useDispatch(); 27 | const targetChain = useSelector(selectTransferTargetChain); 28 | const redeemTx = useSelector(selectTransferRedeemTx); 29 | const handleResetClick = useCallback(() => { 30 | dispatch(reset()); 31 | }, [dispatch]); 32 | 33 | const explainerString = 34 | overrideExplainerString || 35 | "Success! The redeem transaction was submitted. The tokens will become available once the transaction confirms."; 36 | 37 | return ( 38 | <> 39 | 44 | {explainerString} 45 | 46 | {redeemTx ? : null} 47 | 48 | 49 | Transfer More Tokens! 50 | 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/components/Transfer/SendPreview.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles, Typography } from "@material-ui/core"; 2 | import { useSelector } from "react-redux"; 3 | import { 4 | selectTransferSourceChain, 5 | selectTransferTransferTx, 6 | } from "../../store/selectors"; 7 | import ShowTx from "../ShowTx"; 8 | 9 | const useStyles = makeStyles((theme) => ({ 10 | description: { 11 | textAlign: "center", 12 | }, 13 | tx: { 14 | marginTop: theme.spacing(1), 15 | textAlign: "center", 16 | }, 17 | viewButton: { 18 | marginTop: theme.spacing(1), 19 | }, 20 | })); 21 | 22 | export default function SendPreview() { 23 | const classes = useStyles(); 24 | const sourceChain = useSelector(selectTransferSourceChain); 25 | const transferTx = useSelector(selectTransferTransferTx); 26 | 27 | const explainerString = "The tokens have entered the bridge!"; 28 | 29 | return ( 30 | <> 31 | 36 | {explainerString} 37 | 38 | {transferTx ? : null} 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Transfer/SourceAssetWarning.tsx: -------------------------------------------------------------------------------- 1 | import { ChainId, CHAIN_ID_POLYGON, isEVMChain } from "@certusone/wormhole-sdk"; 2 | import { makeStyles, Typography } from "@material-ui/core"; 3 | import { Alert } from "@material-ui/lab"; 4 | import { POLYGON_TERRA_WRAPPED_TOKENS } from "../../utils/consts"; 5 | 6 | const useStyles = makeStyles((theme) => ({ 7 | container: { 8 | marginTop: theme.spacing(2), 9 | marginBottom: theme.spacing(2), 10 | }, 11 | alert: { 12 | marginTop: theme.spacing(1), 13 | marginBottom: theme.spacing(1), 14 | }, 15 | })); 16 | 17 | function PolygonTerraWrappedWarning() { 18 | const classes = useStyles(); 19 | return ( 20 | 21 | 22 | This is a Shuttle-wrapped asset from Polygon! Transferring it will 23 | result in a double wrapped (Portal-wrapped Shuttle-wrapped) asset, which 24 | has no liquid markets. 25 | 26 | 27 | ); 28 | } 29 | 30 | export default function SoureAssetWarning({ 31 | sourceChain, 32 | sourceAsset, 33 | }: { 34 | sourceChain?: ChainId; 35 | sourceAsset?: string; 36 | originChain?: ChainId; 37 | targetChain?: ChainId; 38 | targetAsset?: string; 39 | }) { 40 | if (!(sourceChain && sourceAsset)) { 41 | return null; 42 | } 43 | 44 | const searchableAddress = isEVMChain(sourceChain) 45 | ? sourceAsset.toLowerCase() 46 | : sourceAsset; 47 | const showPolygonTerraWrappedWarning = 48 | sourceChain === CHAIN_ID_POLYGON && 49 | POLYGON_TERRA_WRAPPED_TOKENS.includes(searchableAddress); 50 | 51 | return ( 52 | <> 53 | {showPolygonTerraWrappedWarning ? : null} 54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Transfer/SourcePreview.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles, Typography } from "@material-ui/core"; 2 | import numeral from "numeral"; 3 | import { useSelector } from "react-redux"; 4 | import { 5 | selectSourceWalletAddress, 6 | selectTransferAmount, 7 | selectTransferRelayerFee, 8 | selectTransferSourceChain, 9 | selectTransferSourceParsedTokenAccount, 10 | } from "../../store/selectors"; 11 | import { CHAINS_BY_ID } from "../../utils/consts"; 12 | import SmartAddress from "../SmartAddress"; 13 | 14 | const useStyles = makeStyles((theme) => ({ 15 | description: { 16 | textAlign: "center", 17 | }, 18 | })); 19 | 20 | export default function SourcePreview() { 21 | const classes = useStyles(); 22 | const sourceChain = useSelector(selectTransferSourceChain); 23 | const sourceParsedTokenAccount = useSelector( 24 | selectTransferSourceParsedTokenAccount 25 | ); 26 | const sourceWalletAddress = useSelector(selectSourceWalletAddress); 27 | const sourceAmount = useSelector(selectTransferAmount); 28 | const relayerFee = useSelector(selectTransferRelayerFee); 29 | 30 | const explainerContent = 31 | sourceChain && sourceParsedTokenAccount ? ( 32 | <> 33 | 34 | You will transfer {sourceAmount}{" "} 35 | {relayerFee 36 | ? `(+~${numeral(relayerFee).format("0.00")} relayer fee)` 37 | : ""} 38 | 39 | 44 | {sourceWalletAddress ? ( 45 | <> 46 | from 47 | 48 | 49 | ) : null} 50 | on {CHAINS_BY_ID[sourceChain].name} 51 | 52 | ) : ( 53 | "" 54 | ); 55 | 56 | return ( 57 | <> 58 | 63 | {explainerContent} 64 | 65 | 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/components/Transfer/TargetPreview.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles, Typography } from "@material-ui/core"; 2 | import { CHAINS_BY_ID } from "../../utils/consts"; 3 | import SmartAddress from "../SmartAddress"; 4 | import { useTargetInfo } from "./Target"; 5 | 6 | const useStyles = makeStyles((theme) => ({ 7 | description: { 8 | textAlign: "center", 9 | }, 10 | })); 11 | 12 | export default function TargetPreview() { 13 | const classes = useStyles(); 14 | const { 15 | targetChain, 16 | readableTargetAddress, 17 | targetAsset, 18 | symbol, 19 | tokenName, 20 | logo, 21 | } = useTargetInfo(); 22 | 23 | const explainerContent = 24 | targetChain && readableTargetAddress ? ( 25 | <> 26 | {targetAsset ? ( 27 | <> 28 | and receive 29 | 37 | 38 | ) : null} 39 | to 40 | 41 | on {CHAINS_BY_ID[targetChain].name} 42 | 43 | ) : ( 44 | "" 45 | ); 46 | 47 | return ( 48 | 53 | {explainerContent} 54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Transfer/TransferLimitedWarning.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles } from "@material-ui/core"; 2 | import { Alert } from "@material-ui/lab"; 3 | import { IsTransferLimitedResult } from "../../hooks/useIsTransferLimited"; 4 | import { 5 | CHAINS_BY_ID, 6 | USD_NUMBER_FORMATTER as USD_FORMATTER, 7 | } from "../../utils/consts"; 8 | 9 | const useStyles = makeStyles((theme) => ({ 10 | alert: { 11 | marginTop: theme.spacing(1), 12 | marginBottom: theme.spacing(1), 13 | }, 14 | })); 15 | 16 | const TransferLimitedWarning = ({ 17 | isTransferLimited, 18 | }: { 19 | isTransferLimited: IsTransferLimitedResult; 20 | }) => { 21 | const classes = useStyles(); 22 | if ( 23 | isTransferLimited.isLimited && 24 | isTransferLimited.reason && 25 | isTransferLimited.limits 26 | ) { 27 | const chainName = 28 | CHAINS_BY_ID[isTransferLimited.limits.chainId]?.name || "unknown"; 29 | const message = 30 | isTransferLimited.reason === "EXCEEDS_MAX_NOTIONAL" 31 | ? `This transfer's estimated notional value would exceed the notional value limit for transfers on ${chainName} (${USD_FORMATTER.format( 32 | isTransferLimited.limits.chainNotionalLimit 33 | )}) and may be subject to a 24 hour delay.` 34 | : isTransferLimited.reason === "EXCEEDS_LARGE_TRANSFER_LIMIT" 35 | ? `This transfer's estimated notional value may exceed the notional value for large transfers on ${chainName} (${USD_FORMATTER.format( 36 | isTransferLimited.limits.chainBigTransactionSize 37 | )}) and may be subject to a 24 hour delay.` 38 | : isTransferLimited.reason === "EXCEEDS_REMAINING_NOTIONAL" 39 | ? `This transfer's estimated notional value may exceed the remaining notional value available for transfers on ${chainName} (${USD_FORMATTER.format( 40 | isTransferLimited.limits.chainRemainingAvailableNotional 41 | )}) and may be subject to a delay.` 42 | : ""; 43 | return ( 44 | 45 | {message} 46 | 47 | ); 48 | } 49 | return null; 50 | }; 51 | 52 | export default TransferLimitedWarning; 53 | -------------------------------------------------------------------------------- /src/components/Transfer/WaitingForWalletMessage.tsx: -------------------------------------------------------------------------------- 1 | import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk"; 2 | import { makeStyles, Typography } from "@material-ui/core"; 3 | import { useSelector } from "react-redux"; 4 | import { 5 | selectTransferIsApproving, 6 | selectTransferIsRedeeming, 7 | selectTransferIsSending, 8 | selectTransferRedeemTx, 9 | selectTransferTargetChain, 10 | selectTransferTransferTx, 11 | } from "../../store/selectors"; 12 | 13 | const useStyles = makeStyles((theme) => ({ 14 | message: { 15 | color: theme.palette.warning.light, 16 | marginTop: theme.spacing(1), 17 | textAlign: "center", 18 | }, 19 | })); 20 | 21 | export const WAITING_FOR_WALLET_AND_CONF = 22 | "Waiting for wallet approval (likely in a popup) and confirmation..."; 23 | 24 | export default function WaitingForWalletMessage() { 25 | const classes = useStyles(); 26 | const isApproving = useSelector(selectTransferIsApproving); 27 | const isSending = useSelector(selectTransferIsSending); 28 | const transferTx = useSelector(selectTransferTransferTx); 29 | const targetChain = useSelector(selectTransferTargetChain); 30 | const isRedeeming = useSelector(selectTransferIsRedeeming); 31 | const redeemTx = useSelector(selectTransferRedeemTx); 32 | const showWarning = 33 | isApproving || (isSending && !transferTx) || (isRedeeming && !redeemTx); 34 | return showWarning ? ( 35 | 36 | {WAITING_FOR_WALLET_AND_CONF}{" "} 37 | {targetChain === CHAIN_ID_SOLANA && isRedeeming 38 | ? "Note: there will be several transactions" 39 | : null} 40 | 41 | ) : null; 42 | } 43 | -------------------------------------------------------------------------------- /src/components/XplaWalletKey.tsx: -------------------------------------------------------------------------------- 1 | import { useConnectedWallet, useWallet } from "@xpla/wallet-provider"; 2 | import { useCallback, useState } from "react"; 3 | import XplaConnectWalletDialog from "./XplaConnectWalletDialog"; 4 | import ToggleConnectedButton from "./ToggleConnectedButton"; 5 | 6 | const XplaWalletKey = () => { 7 | const wallet = useWallet(); 8 | const connectedWallet = useConnectedWallet(); 9 | 10 | const [isDialogOpen, setIsDialogOpen] = useState(false); 11 | 12 | const connect = useCallback(() => { 13 | setIsDialogOpen(true); 14 | }, [setIsDialogOpen]); 15 | 16 | const closeDialog = useCallback(() => { 17 | setIsDialogOpen(false); 18 | }, [setIsDialogOpen]); 19 | 20 | return ( 21 | <> 22 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default XplaWalletKey; 34 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from "@certusone/wormhole-sdk"; 2 | 3 | export type DisableTransfers = boolean | "to" | "from"; 4 | 5 | export interface WarningMessage { 6 | text: string; 7 | link?: { 8 | url: string; 9 | text: string; 10 | }; 11 | } 12 | 13 | export interface ChainConfig { 14 | disableTransfers?: DisableTransfers; 15 | warningMessage?: WarningMessage; 16 | } 17 | 18 | export type ChainConfigMap = { 19 | [key in ChainId]?: ChainConfig; 20 | }; 21 | 22 | export const CHAIN_CONFIG_MAP: ChainConfigMap = { 23 | // [CHAIN_ID_POLYGON]: { 24 | // disableTransfers: true, 25 | // warningMessage: { 26 | // text: "Polygon is currently experiencing partial downtime. As a precautionary measure, Wormhole Network and Portal have paused Polygon support until the network has been fully restored.", 27 | // link: { 28 | // url: "https://twitter.com/0xPolygonDevs", 29 | // text: "Follow @0xPolygonDevs for updates", 30 | // }, 31 | // }, 32 | // } as ChainConfig, 33 | }; 34 | -------------------------------------------------------------------------------- /src/contexts/AlgorandWalletContext.tsx: -------------------------------------------------------------------------------- 1 | import MyAlgoConnect, { Accounts } from "@randlabs/myalgo-connect"; 2 | import { 3 | createContext, 4 | ReactChildren, 5 | useCallback, 6 | useContext, 7 | useMemo, 8 | useState, 9 | } from "react"; 10 | 11 | interface IAlgorandContext { 12 | connect(): void; 13 | disconnect(): void; 14 | accounts: Accounts[]; 15 | } 16 | 17 | const AlgorandContext = createContext({ 18 | connect: () => {}, 19 | disconnect: () => {}, 20 | accounts: [], 21 | }); 22 | 23 | export const AlgorandContextProvider = ({ 24 | children, 25 | }: { 26 | children: ReactChildren; 27 | }) => { 28 | const myAlgoConnect = useMemo(() => new MyAlgoConnect(), []); 29 | const [accounts, setAccounts] = useState([]); 30 | const connect = useCallback(() => { 31 | let cancelled = false; 32 | (async () => { 33 | const accounts = await myAlgoConnect.connect({ 34 | shouldSelectOneAccount: true, 35 | }); 36 | if (!cancelled) { 37 | setAccounts(accounts); 38 | } 39 | })(); 40 | return () => { 41 | cancelled = true; 42 | }; 43 | }, [myAlgoConnect]); 44 | const disconnect = useCallback(() => { 45 | setAccounts([]); 46 | }, []); 47 | const value = useMemo( 48 | () => ({ 49 | connect, 50 | disconnect, 51 | accounts, 52 | }), 53 | [connect, disconnect, accounts] 54 | ); 55 | 56 | return ( 57 | 58 | {children} 59 | 60 | ); 61 | }; 62 | 63 | export const useAlgorandContext = () => { 64 | return useContext(AlgorandContext); 65 | }; 66 | -------------------------------------------------------------------------------- /src/contexts/AptosWalletContext.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AptosSnapAdapter, 3 | AptosWalletAdapter, 4 | BitkeepWalletAdapter, 5 | BloctoWalletAdapter, 6 | FewchaWalletAdapter, 7 | FletchWalletAdapter, 8 | MartianWalletAdapter, 9 | NightlyWalletAdapter, 10 | PontemWalletAdapter, 11 | RiseWalletAdapter, 12 | SpikaWalletAdapter, 13 | TokenPocketWalletAdapter, 14 | useWallet, 15 | WalletProvider, 16 | } from "@manahippo/aptos-wallet-adapter"; 17 | import { ReactChildren, useMemo } from "react"; 18 | 19 | export const useAptosContext = useWallet; 20 | 21 | export const AptosWalletProvider = ({ 22 | children, 23 | }: { 24 | children: ReactChildren; 25 | }) => { 26 | const wallets = useMemo( 27 | () => [ 28 | new AptosWalletAdapter(), 29 | new MartianWalletAdapter(), 30 | new RiseWalletAdapter(), 31 | new NightlyWalletAdapter(), 32 | new PontemWalletAdapter(), 33 | new FletchWalletAdapter(), 34 | new FewchaWalletAdapter(), 35 | new SpikaWalletAdapter(), 36 | new AptosSnapAdapter(), 37 | new BitkeepWalletAdapter(), 38 | new TokenPocketWalletAdapter(), 39 | new BloctoWalletAdapter(), 40 | ], 41 | [] 42 | ); 43 | return ( 44 | { 47 | console.log("wallet errors: ", error); 48 | }} 49 | > 50 | {children} 51 | 52 | ); 53 | }; 54 | 55 | export default AptosWalletProvider; 56 | -------------------------------------------------------------------------------- /src/contexts/SolanaWalletContext.tsx: -------------------------------------------------------------------------------- 1 | import { Adapter, WalletAdapterNetwork } from "@solana/wallet-adapter-base"; 2 | import { 3 | ConnectionProvider, 4 | WalletProvider, 5 | useWallet, 6 | } from "@solana/wallet-adapter-react"; 7 | import { 8 | PhantomWalletAdapter, 9 | SolflareWalletAdapter, 10 | SolletWalletAdapter, 11 | CloverWalletAdapter, 12 | Coin98WalletAdapter, 13 | SlopeWalletAdapter, 14 | SolongWalletAdapter, 15 | TorusWalletAdapter, 16 | SolletExtensionWalletAdapter, 17 | ExodusWalletAdapter, 18 | BackpackWalletAdapter, 19 | NightlyWalletAdapter, 20 | BloctoWalletAdapter, 21 | } from "@solana/wallet-adapter-wallets"; 22 | import { FC, useMemo } from "react"; 23 | import { CLUSTER, SOLANA_HOST } from "../utils/consts"; 24 | 25 | export const SolanaWalletProvider: FC = (props) => { 26 | const wallets = useMemo(() => { 27 | const wallets: Adapter[] = [ 28 | new PhantomWalletAdapter(), 29 | new SolflareWalletAdapter(), 30 | new BackpackWalletAdapter(), 31 | new NightlyWalletAdapter(), 32 | new SolletWalletAdapter(), 33 | new SolletExtensionWalletAdapter(), 34 | new CloverWalletAdapter(), 35 | new Coin98WalletAdapter(), 36 | new SlopeWalletAdapter(), 37 | new SolongWalletAdapter(), 38 | new TorusWalletAdapter(), 39 | new ExodusWalletAdapter(), 40 | ]; 41 | if (CLUSTER === "testnet") { 42 | wallets.push( 43 | new BloctoWalletAdapter({ network: WalletAdapterNetwork.Devnet }) 44 | ); 45 | } 46 | return wallets; 47 | }, []); 48 | 49 | return ( 50 | 51 | 52 | {props.children} 53 | 54 | 55 | ); 56 | }; 57 | 58 | export const useSolanaWallet = useWallet; 59 | -------------------------------------------------------------------------------- /src/contexts/SuiWalletContext.tsx: -------------------------------------------------------------------------------- 1 | import { WalletProvider } from "@suiet/wallet-kit"; 2 | import { ReactChildren } from "react"; 3 | 4 | // export const useSuiContext = () => { 5 | // const [accounts, setAccounts] = useState([]); 6 | // const { select, wallets, connected, disconnect, getAccounts } = useWallet(); 7 | 8 | // useEffect(() => { 9 | // let isCancelled = false; 10 | // if (wallet) { 11 | // wallet.getAccounts().then((accounts) => { 12 | // if (!isCancelled) { 13 | // setAccounts(accounts); 14 | // } 15 | // }); 16 | // } 17 | // return () => { 18 | // isCancelled = true; 19 | // }; 20 | // }, [wallet]); 21 | 22 | // return { 23 | // wallet, 24 | // accounts, 25 | // select, 26 | // wallets, 27 | // connected, 28 | // disconnect, 29 | // getAccounts, 30 | // }; 31 | // }; 32 | 33 | export const SuiWalletProvider = ({ 34 | children, 35 | }: { 36 | children: ReactChildren; 37 | }) => { 38 | return {children}; 39 | }; 40 | 41 | export default SuiWalletProvider; 42 | -------------------------------------------------------------------------------- /src/contexts/TerraWalletContext.tsx: -------------------------------------------------------------------------------- 1 | import { NetworkInfo, WalletProvider } from "@terra-money/wallet-provider"; 2 | import { ReactChildren } from "react"; 3 | 4 | const testnet: NetworkInfo = { 5 | name: "testnet", 6 | chainID: "pisco-1", 7 | lcd: "https://pisco-lcd.terra.dev", 8 | walletconnectID: 0, 9 | }; 10 | 11 | const walletConnectChainIds: Record = { 12 | 0: testnet, 13 | }; 14 | 15 | export const TerraWalletProvider = ({ 16 | children, 17 | }: { 18 | children: ReactChildren; 19 | }) => { 20 | return ( 21 | 25 | {children} 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/contexts/XplaWalletContext.tsx: -------------------------------------------------------------------------------- 1 | import { NetworkInfo, WalletProvider } from "@xpla/wallet-provider"; 2 | import { ReactChildren } from "react"; 3 | 4 | const testnet: NetworkInfo = { 5 | name: "testnet", 6 | chainID: "cube_47-5", 7 | lcd: "https://cube-lcd.xpla.dev", 8 | walletconnectID: 0, 9 | }; 10 | 11 | const walletConnectChainIds: Record = { 12 | 0: testnet, 13 | }; 14 | 15 | export const XplaWalletProvider = ({ 16 | children, 17 | }: { 18 | children: ReactChildren; 19 | }) => { 20 | return ( 21 | 25 | {children} 26 | 27 | ); 28 | }; 29 | 30 | export default XplaWalletProvider; 31 | -------------------------------------------------------------------------------- /src/hooks/useAlgoMetadata.ts: -------------------------------------------------------------------------------- 1 | import { Algodv2 } from "algosdk"; 2 | import { useEffect, useMemo, useState } from "react"; 3 | import { DataWrapper } from "../store/helpers"; 4 | import { ALGORAND_HOST, ALGO_DECIMALS } from "../utils/consts"; 5 | 6 | export type AlgoMetadata = { 7 | symbol?: string; 8 | tokenName?: string; 9 | decimals: number; 10 | }; 11 | 12 | export const fetchSingleMetadata = async ( 13 | address: string, 14 | algodClient: Algodv2 15 | ): Promise => { 16 | const assetId = parseInt(address); 17 | if (assetId === 0) { 18 | return { 19 | tokenName: "Algo", 20 | symbol: "ALGO", 21 | decimals: ALGO_DECIMALS, 22 | }; 23 | } 24 | const assetInfo = await algodClient.getAssetByID(assetId).do(); 25 | return { 26 | tokenName: assetInfo.params.name, 27 | symbol: assetInfo.params["unit-name"], 28 | decimals: assetInfo.params.decimals, 29 | }; 30 | }; 31 | 32 | const fetchAlgoMetadata = async (addresses: string[]) => { 33 | const algodClient = new Algodv2( 34 | ALGORAND_HOST.algodToken, 35 | ALGORAND_HOST.algodServer, 36 | ALGORAND_HOST.algodPort 37 | ); 38 | const promises: Promise[] = []; 39 | addresses.forEach((address) => { 40 | promises.push(fetchSingleMetadata(address, algodClient)); 41 | }); 42 | const resultsArray = await Promise.all(promises); 43 | const output = new Map(); 44 | addresses.forEach((address, index) => { 45 | output.set(address, resultsArray[index]); 46 | }); 47 | 48 | return output; 49 | }; 50 | 51 | function useAlgoMetadata( 52 | addresses: string[] 53 | ): DataWrapper> { 54 | const [isFetching, setIsFetching] = useState(false); 55 | const [error, setError] = useState(""); 56 | const [data, setData] = useState | null>(null); 57 | 58 | useEffect(() => { 59 | let cancelled = false; 60 | if (addresses.length) { 61 | setIsFetching(true); 62 | setError(""); 63 | setData(null); 64 | fetchAlgoMetadata(addresses).then( 65 | (results) => { 66 | if (!cancelled) { 67 | setData(results); 68 | setIsFetching(false); 69 | } 70 | }, 71 | () => { 72 | if (!cancelled) { 73 | setError("Could not retrieve contract metadata"); 74 | setIsFetching(false); 75 | } 76 | } 77 | ); 78 | } 79 | return () => { 80 | cancelled = true; 81 | }; 82 | }, [addresses]); 83 | 84 | return useMemo( 85 | () => ({ 86 | data, 87 | isFetching, 88 | error, 89 | receivedAt: null, 90 | }), 91 | [data, isFetching, error] 92 | ); 93 | } 94 | 95 | export default useAlgoMetadata; 96 | -------------------------------------------------------------------------------- /src/hooks/useAptosMetadata.ts: -------------------------------------------------------------------------------- 1 | import { ensureHexPrefix } from "@certusone/wormhole-sdk"; 2 | import { AptosClient } from "aptos"; 3 | import { useEffect, useMemo, useState } from "react"; 4 | import { DataWrapper } from "../store/helpers"; 5 | import { getAptosClient } from "../utils/aptos"; 6 | 7 | export type AptosMetadata = { 8 | symbol?: string; 9 | tokenName?: string; 10 | decimals: number; 11 | }; 12 | 13 | export type AptosCoinResourceReturn = { 14 | decimals: number; 15 | name: string; 16 | supply: any; 17 | symbol: string; 18 | }; 19 | 20 | export const fetchSingleMetadata = async ( 21 | address: string, 22 | client: AptosClient 23 | ): Promise => { 24 | const coinType = `0x1::coin::CoinInfo<${ensureHexPrefix(address)}>`; 25 | const assetInfo = ( 26 | await client.getAccountResource(address.split("::")[0], coinType) 27 | ).data as AptosCoinResourceReturn; 28 | return { 29 | tokenName: assetInfo.name, 30 | symbol: assetInfo.symbol, 31 | decimals: assetInfo.decimals, 32 | }; 33 | }; 34 | 35 | const fetchAptosMetadata = async (addresses: string[]) => { 36 | const client = getAptosClient(); 37 | const promises: Promise[] = []; 38 | addresses.forEach((address) => { 39 | promises.push(fetchSingleMetadata(address, client)); 40 | }); 41 | const resultsArray = await Promise.all(promises); 42 | const output = new Map(); 43 | addresses.forEach((address, index) => { 44 | output.set(address, resultsArray[index]); 45 | }); 46 | 47 | return output; 48 | }; 49 | 50 | function useAptosMetadata( 51 | addresses: string[] 52 | ): DataWrapper> { 53 | const [isFetching, setIsFetching] = useState(false); 54 | const [error, setError] = useState(""); 55 | const [data, setData] = useState | null>(null); 56 | 57 | useEffect(() => { 58 | let cancelled = false; 59 | if (addresses.length) { 60 | setIsFetching(true); 61 | setError(""); 62 | setData(null); 63 | fetchAptosMetadata(addresses).then( 64 | (results) => { 65 | if (!cancelled) { 66 | setData(results); 67 | setIsFetching(false); 68 | } 69 | }, 70 | () => { 71 | if (!cancelled) { 72 | setError("Could not retrieve contract metadata"); 73 | setIsFetching(false); 74 | } 75 | } 76 | ); 77 | } 78 | return () => { 79 | cancelled = true; 80 | }; 81 | }, [addresses]); 82 | 83 | return useMemo( 84 | () => ({ 85 | data, 86 | isFetching, 87 | error, 88 | receivedAt: null, 89 | }), 90 | [data, isFetching, error] 91 | ); 92 | } 93 | 94 | export default useAptosMetadata; 95 | -------------------------------------------------------------------------------- /src/hooks/useAptosNativeBalance.ts: -------------------------------------------------------------------------------- 1 | import { AptosAccount, CoinClient } from "aptos"; 2 | import { MutableRefObject, useEffect, useMemo, useState } from "react"; 3 | import { getAptosClient } from "../utils/aptos"; 4 | 5 | export default function useAptosNativeBalance( 6 | address?: string, 7 | refreshRef?: MutableRefObject<() => void> 8 | ) { 9 | const [isLoading, setIsLoading] = useState(true); 10 | const [balance, setBalance] = useState(undefined); 11 | const [refresh, setRefresh] = useState(false); 12 | useEffect(() => { 13 | if (refreshRef) { 14 | refreshRef.current = () => { 15 | setRefresh(true); 16 | }; 17 | } 18 | }, [refreshRef]); 19 | useEffect(() => { 20 | setRefresh(false); 21 | if (address) { 22 | setIsLoading(true); 23 | setBalance(undefined); 24 | const account = new AptosAccount(undefined, address); 25 | const client = getAptosClient(); 26 | const coinClient = new CoinClient(client); 27 | coinClient 28 | .checkBalance(account) 29 | .then((value) => { 30 | setIsLoading(false); 31 | setBalance(value); 32 | }) 33 | .catch((e) => { 34 | console.error(e); 35 | setIsLoading(false); 36 | setBalance(undefined); 37 | }); 38 | } else { 39 | setIsLoading(false); 40 | setBalance(undefined); 41 | } 42 | }, [address, refresh]); 43 | const value = useMemo(() => ({ isLoading, balance }), [isLoading, balance]); 44 | return value; 45 | } 46 | -------------------------------------------------------------------------------- /src/hooks/useAttestSignedVAA.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { useSelector } from "react-redux"; 3 | import { selectAttestSignedVAAHex } from "../store/selectors"; 4 | import { hexToUint8Array } from "@certusone/wormhole-sdk"; 5 | 6 | export default function useAttestSignedVAA() { 7 | const signedVAAHex = useSelector(selectAttestSignedVAAHex); 8 | const signedVAA = useMemo( 9 | () => (signedVAAHex ? hexToUint8Array(signedVAAHex) : undefined), 10 | [signedVAAHex] 11 | ); 12 | return signedVAA; 13 | } 14 | -------------------------------------------------------------------------------- /src/hooks/useCopyToClipboard.tsx: -------------------------------------------------------------------------------- 1 | import { Alert } from "@material-ui/lab"; 2 | import { useSnackbar } from "notistack"; 3 | import { useCallback } from "react"; 4 | import pushToClipboard from "../utils/pushToClipboard"; 5 | 6 | export default function useCopyToClipboard(content: string) { 7 | const { enqueueSnackbar } = useSnackbar(); 8 | return useCallback(() => { 9 | pushToClipboard(content)?.then(() => { 10 | enqueueSnackbar(null, { 11 | content: Copied., 12 | }); 13 | }); 14 | }, [content, enqueueSnackbar]); 15 | } 16 | -------------------------------------------------------------------------------- /src/hooks/useInjectiveMetadata.ts: -------------------------------------------------------------------------------- 1 | import { parseSmartContractStateResponse } from "@certusone/wormhole-sdk"; 2 | import { ChainGrpcWasmApi } from "@injectivelabs/sdk-ts"; 3 | import { useLayoutEffect, useMemo, useState } from "react"; 4 | import { DataWrapper } from "../store/helpers"; 5 | import { getInjectiveWasmClient } from "../utils/injective"; 6 | 7 | export type InjectiveMetadata = { 8 | symbol?: string; 9 | logo?: string; 10 | tokenName?: string; 11 | decimals?: number; 12 | }; 13 | 14 | const fetchSingleMetadata = async (address: string, client: ChainGrpcWasmApi) => 15 | client 16 | .fetchSmartContractState( 17 | address, 18 | Buffer.from(JSON.stringify({ token_info: {} })).toString("base64") 19 | ) 20 | .then((data) => { 21 | const parsed = parseSmartContractStateResponse(data); 22 | return { 23 | symbol: parsed.symbol, 24 | tokenName: parsed.name, 25 | decimals: parsed.decimals, 26 | } as InjectiveMetadata; 27 | }); 28 | 29 | const fetchInjectiveMetadata = async (addresses: string[]) => { 30 | const client = getInjectiveWasmClient(); 31 | const promises: Promise[] = []; 32 | addresses.forEach((address) => { 33 | promises.push(fetchSingleMetadata(address, client)); 34 | }); 35 | const resultsArray = await Promise.all(promises); 36 | const output = new Map(); 37 | addresses.forEach((address, index) => { 38 | output.set(address, resultsArray[index]); 39 | }); 40 | 41 | return output; 42 | }; 43 | 44 | const useInjectiveMetadata = ( 45 | addresses: string[] 46 | ): DataWrapper> => { 47 | const [isFetching, setIsFetching] = useState(false); 48 | const [error, setError] = useState(""); 49 | const [data, setData] = useState | null>(null); 50 | 51 | useLayoutEffect(() => { 52 | let cancelled = false; 53 | if (addresses.length) { 54 | setIsFetching(true); 55 | setError(""); 56 | setData(null); 57 | fetchInjectiveMetadata(addresses).then( 58 | (results) => { 59 | if (!cancelled) { 60 | setData(results); 61 | setIsFetching(false); 62 | } 63 | }, 64 | () => { 65 | if (!cancelled) { 66 | setError("Could not retrieve contract metadata"); 67 | setIsFetching(false); 68 | } 69 | } 70 | ); 71 | } 72 | return () => { 73 | cancelled = true; 74 | }; 75 | }, [addresses]); 76 | 77 | return useMemo( 78 | () => ({ 79 | data, 80 | isFetching, 81 | error, 82 | receivedAt: null, 83 | }), 84 | [data, isFetching, error] 85 | ); 86 | }; 87 | 88 | export default useInjectiveMetadata; 89 | -------------------------------------------------------------------------------- /src/hooks/useInjectiveNativeBalances.ts: -------------------------------------------------------------------------------- 1 | import { MutableRefObject, useEffect, useMemo, useState } from "react"; 2 | import { getInjectiveBankClient } from "../utils/injective"; 3 | 4 | export interface InjectiveNativeBalances { 5 | [index: string]: string; 6 | } 7 | 8 | export default function useInjectiveNativeBalances( 9 | walletAddress?: string, 10 | refreshRef?: MutableRefObject<() => void> 11 | ) { 12 | const [isLoading, setIsLoading] = useState(true); 13 | const [balances, setBalances] = useState( 14 | {} 15 | ); 16 | const [refresh, setRefresh] = useState(false); 17 | useEffect(() => { 18 | if (refreshRef) { 19 | refreshRef.current = () => { 20 | setRefresh(true); 21 | }; 22 | } 23 | }, [refreshRef]); 24 | useEffect(() => { 25 | setRefresh(false); 26 | if (walletAddress) { 27 | setIsLoading(true); 28 | setBalances(undefined); 29 | const client = getInjectiveBankClient(); 30 | client 31 | .fetchBalances(walletAddress) 32 | .then(({ balances }) => { 33 | const nativeBalances = balances.reduce((obj, { denom, amount }) => { 34 | obj[denom] = amount; 35 | return obj; 36 | }, {} as InjectiveNativeBalances); 37 | setIsLoading(false); 38 | setBalances(nativeBalances); 39 | }) 40 | .catch((e) => { 41 | console.error(e); 42 | setIsLoading(false); 43 | setBalances(undefined); 44 | }); 45 | } else { 46 | setIsLoading(false); 47 | setBalances(undefined); 48 | } 49 | }, [walletAddress, refresh]); 50 | const value = useMemo(() => ({ isLoading, balances }), [isLoading, balances]); 51 | return value; 52 | } 53 | -------------------------------------------------------------------------------- /src/hooks/useNFTSignedVAA.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { useSelector } from "react-redux"; 3 | import { selectNFTSignedVAAHex } from "../store/selectors"; 4 | import { hexToUint8Array } from "@certusone/wormhole-sdk"; 5 | 6 | export default function useNFTSignedVAA() { 7 | const signedVAAHex = useSelector(selectNFTSignedVAAHex); 8 | const signedVAA = useMemo( 9 | () => (signedVAAHex ? hexToUint8Array(signedVAAHex) : undefined), 10 | [signedVAAHex] 11 | ); 12 | return signedVAA; 13 | } 14 | -------------------------------------------------------------------------------- /src/hooks/useNFTTargetAddress.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { useSelector } from "react-redux"; 3 | import { selectNFTTargetAddressHex } from "../store/selectors"; 4 | import { hexToUint8Array } from "@certusone/wormhole-sdk"; 5 | 6 | export default function useNFTTargetAddressHex() { 7 | const targetAddressHex = useSelector(selectNFTTargetAddressHex); 8 | const targetAddress = useMemo( 9 | () => (targetAddressHex ? hexToUint8Array(targetAddressHex) : undefined), 10 | [targetAddressHex] 11 | ); 12 | return targetAddress; 13 | } 14 | -------------------------------------------------------------------------------- /src/hooks/useNativeSuiBalance.ts: -------------------------------------------------------------------------------- 1 | import { MutableRefObject, useEffect, useMemo, useState } from "react"; 2 | import { getSuiProvider } from "../utils/sui"; 3 | 4 | export default function useSuiNativeBalance( 5 | address?: string, 6 | refreshRef?: MutableRefObject<() => void> 7 | ) { 8 | const [isLoading, setIsLoading] = useState(true); 9 | const [balance, setBalance] = useState(undefined); 10 | const [refresh, setRefresh] = useState(false); 11 | useEffect(() => { 12 | if (refreshRef) { 13 | refreshRef.current = () => { 14 | setRefresh(true); 15 | }; 16 | } 17 | }, [refreshRef]); 18 | useEffect(() => { 19 | setRefresh(false); 20 | if (address) { 21 | setIsLoading(true); 22 | setBalance(undefined); 23 | const provider = getSuiProvider(); 24 | provider 25 | .getBalance({ 26 | owner: address, 27 | }) 28 | .then((coinBalance) => { 29 | setIsLoading(false); 30 | setBalance(BigInt(coinBalance.totalBalance)); 31 | }) 32 | .catch((e) => { 33 | console.error(e); 34 | setIsLoading(false); 35 | setBalance(undefined); 36 | }); 37 | } else { 38 | setIsLoading(false); 39 | setBalance(undefined); 40 | } 41 | }, [address, refresh]); 42 | const value = useMemo(() => ({ isLoading, balance }), [isLoading, balance]); 43 | return value; 44 | } 45 | -------------------------------------------------------------------------------- /src/hooks/useNearMetadata.ts: -------------------------------------------------------------------------------- 1 | import { Account } from "near-api-js"; 2 | import { useEffect, useMemo, useState } from "react"; 3 | import { useNearContext } from "../contexts/NearWalletContext"; 4 | import { DataWrapper } from "../store/helpers"; 5 | import { makeNearAccount } from "../utils/near"; 6 | import { AlgoMetadata } from "./useAlgoMetadata"; 7 | 8 | export const fetchSingleMetadata = async ( 9 | address: string, 10 | account: Account 11 | ): Promise => { 12 | const assetInfo = await account.viewFunction(address, "ft_metadata"); 13 | return { 14 | tokenName: assetInfo.name, 15 | symbol: assetInfo.symbol, 16 | decimals: assetInfo.decimals, 17 | }; 18 | }; 19 | 20 | const fetchNearMetadata = async ( 21 | addresses: string[], 22 | nearAccountId: string 23 | ) => { 24 | const account = await makeNearAccount(nearAccountId); 25 | const promises: Promise[] = []; 26 | addresses.forEach((address) => { 27 | promises.push(fetchSingleMetadata(address, account)); 28 | }); 29 | const resultsArray = await Promise.all(promises); 30 | const output = new Map(); 31 | addresses.forEach((address, index) => { 32 | output.set(address, resultsArray[index]); 33 | }); 34 | 35 | return output; 36 | }; 37 | 38 | function useNearMetadata( 39 | addresses: string[] 40 | ): DataWrapper> { 41 | const { accountId: nearAccountId } = useNearContext(); 42 | const [isFetching, setIsFetching] = useState(false); 43 | const [error, setError] = useState(""); 44 | const [data, setData] = useState | null>(null); 45 | 46 | useEffect(() => { 47 | let cancelled = false; 48 | if (addresses.length && nearAccountId) { 49 | setIsFetching(true); 50 | setError(""); 51 | setData(null); 52 | fetchNearMetadata(addresses, nearAccountId).then( 53 | (results) => { 54 | if (!cancelled) { 55 | setData(results); 56 | setIsFetching(false); 57 | } 58 | }, 59 | () => { 60 | if (!cancelled) { 61 | setError("Could not retrieve contract metadata"); 62 | setIsFetching(false); 63 | } 64 | } 65 | ); 66 | } 67 | return () => { 68 | cancelled = true; 69 | }; 70 | }, [addresses, nearAccountId]); 71 | 72 | return useMemo( 73 | () => ({ 74 | data, 75 | isFetching, 76 | error, 77 | receivedAt: null, 78 | }), 79 | [data, isFetching, error] 80 | ); 81 | } 82 | 83 | export default useNearMetadata; 84 | -------------------------------------------------------------------------------- /src/hooks/useRelayersAvailable.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from "@certusone/wormhole-sdk"; 2 | import { Dispatch } from "@reduxjs/toolkit"; 3 | import axios from "axios"; 4 | import { useEffect } from "react"; 5 | import { useDispatch, useSelector } from "react-redux"; 6 | import { DataWrapper } from "../store/helpers"; 7 | import { selectRelayerTokenInfo } from "../store/selectors"; 8 | import { 9 | errorRelayerTokenInfo, 10 | fetchRelayerTokenInfo, 11 | receiveRelayerTokenInfo, 12 | } from "../store/tokenSlice"; 13 | import { RELAYER_INFO_URL } from "../utils/consts"; 14 | 15 | export type RelayToken = { 16 | chainId?: ChainId; 17 | address?: string; 18 | coingeckoId?: string; 19 | }; 20 | export type Relayer = { 21 | name?: string; 22 | url?: string; 23 | }; 24 | export type FeeScheduleEntryFlat = { 25 | type: "flat"; 26 | feeUsd: number; 27 | }; 28 | export type FeeScheduleEntryPercent = { 29 | type: "percent"; 30 | feePercent: number; 31 | gasEstimate: number; 32 | }; 33 | export type FeeSchedule = { 34 | // ChainId as a string 35 | [key: string]: FeeScheduleEntryFlat | FeeScheduleEntryPercent; 36 | }; 37 | export type RelayerTokenInfo = { 38 | supportedTokens?: RelayToken[]; 39 | relayers?: Relayer[]; 40 | feeSchedule?: FeeSchedule; 41 | }; 42 | 43 | const useRelayersAvailable = ( 44 | shouldFire: boolean 45 | ): DataWrapper => { 46 | const relayerTokenInfo = useSelector(selectRelayerTokenInfo); 47 | // console.log("relayerTokenInfo", relayerTokenInfo); 48 | const dispatch = useDispatch(); 49 | const internalShouldFire = 50 | shouldFire && 51 | (relayerTokenInfo.data === undefined || 52 | (relayerTokenInfo.data === null && !relayerTokenInfo.isFetching)); 53 | 54 | useEffect(() => { 55 | if (internalShouldFire) { 56 | getRelayersAvailable(dispatch); 57 | } 58 | }, [internalShouldFire, dispatch]); 59 | 60 | return relayerTokenInfo; 61 | }; 62 | 63 | const getRelayersAvailable = (dispatch: Dispatch) => { 64 | dispatch(fetchRelayerTokenInfo()); 65 | axios.get(RELAYER_INFO_URL).then( 66 | (response) => { 67 | dispatch(receiveRelayerTokenInfo(response.data as RelayerTokenInfo)); 68 | }, 69 | (error) => { 70 | dispatch( 71 | errorRelayerTokenInfo("Failed to retrieve the relayer token info.") 72 | ); 73 | } 74 | ); 75 | }; 76 | 77 | export default useRelayersAvailable; 78 | -------------------------------------------------------------------------------- /src/hooks/useSeiMetadata.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useMemo, useState } from "react"; 2 | import { DataWrapper } from "../store/helpers"; 3 | import { CosmWasmClient, getSeiWasmClient } from "../utils/sei"; 4 | 5 | export type SeiMetadata = { 6 | symbol?: string; 7 | logo?: string; 8 | tokenName?: string; 9 | decimals?: number; 10 | }; 11 | 12 | const fetchSingleMetadata = async (address: string, client: CosmWasmClient) => 13 | client 14 | .queryContractSmart(address, { 15 | token_info: {}, 16 | }) 17 | .then( 18 | ({ symbol, name: tokenName, decimals }: any) => 19 | ({ 20 | symbol, 21 | tokenName, 22 | decimals, 23 | } as SeiMetadata) 24 | ); 25 | 26 | const fetchSeiMetadata = async ( 27 | addresses: string[], 28 | client: CosmWasmClient 29 | ) => { 30 | const promises: Promise[] = []; 31 | addresses.forEach((address) => { 32 | promises.push(fetchSingleMetadata(address, client)); 33 | }); 34 | const resultsArray = await Promise.all(promises); 35 | const output = new Map(); 36 | addresses.forEach((address, index) => { 37 | output.set(address, resultsArray[index]); 38 | }); 39 | 40 | return output; 41 | }; 42 | 43 | const useSeiMetadata = ( 44 | addresses: string[] 45 | ): DataWrapper> => { 46 | const [isFetching, setIsFetching] = useState(false); 47 | const [error, setError] = useState(""); 48 | const [data, setData] = useState | null>(null); 49 | 50 | useLayoutEffect(() => { 51 | let cancelled = false; 52 | if (addresses.length) { 53 | setIsFetching(true); 54 | setError(""); 55 | setData(null); 56 | getSeiWasmClient().then( 57 | (client) => 58 | fetchSeiMetadata(addresses, client).then( 59 | (results) => { 60 | if (!cancelled) { 61 | setData(results); 62 | setIsFetching(false); 63 | } 64 | }, 65 | () => { 66 | if (!cancelled) { 67 | setError("Could not retrieve contract metadata"); 68 | setIsFetching(false); 69 | } 70 | } 71 | ), 72 | () => { 73 | if (!cancelled) { 74 | setError("Could not retrieve contract metadata"); 75 | setIsFetching(false); 76 | } 77 | } 78 | ); 79 | } 80 | return () => { 81 | cancelled = true; 82 | }; 83 | }, [addresses]); 84 | 85 | return useMemo( 86 | () => ({ 87 | data, 88 | isFetching, 89 | error, 90 | receivedAt: null, 91 | }), 92 | [data, isFetching, error] 93 | ); 94 | }; 95 | 96 | export default useSeiMetadata; 97 | -------------------------------------------------------------------------------- /src/hooks/useSolanaTokenMap.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch } from "@reduxjs/toolkit"; 2 | import { ENV, TokenInfo, TokenListProvider } from "@solana/spl-token-registry"; 3 | import { useEffect } from "react"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | import { DataWrapper } from "../store/helpers"; 6 | import { selectSolanaTokenMap } from "../store/selectors"; 7 | import { 8 | errorSolanaTokenMap, 9 | fetchSolanaTokenMap, 10 | receiveSolanaTokenMap, 11 | } from "../store/tokenSlice"; 12 | import { CLUSTER } from "../utils/consts"; 13 | 14 | const environment = CLUSTER === "testnet" ? ENV.Testnet : ENV.MainnetBeta; 15 | 16 | const useSolanaTokenMap = (): DataWrapper => { 17 | const tokenMap = useSelector(selectSolanaTokenMap); 18 | const dispatch = useDispatch(); 19 | const shouldFire = 20 | tokenMap.data === undefined || 21 | (tokenMap.data === null && !tokenMap.isFetching); 22 | 23 | useEffect(() => { 24 | if (shouldFire) { 25 | getSolanaTokenMap(dispatch); 26 | } 27 | }, [dispatch, shouldFire]); 28 | 29 | return tokenMap; 30 | }; 31 | 32 | const getSolanaTokenMap = (dispatch: Dispatch) => { 33 | dispatch(fetchSolanaTokenMap()); 34 | 35 | new TokenListProvider().resolve().then( 36 | (tokens) => { 37 | const tokenList = tokens.filterByChainId(environment).getList(); 38 | dispatch(receiveSolanaTokenMap(tokenList)); 39 | }, 40 | (error) => { 41 | console.error(error); 42 | dispatch(errorSolanaTokenMap("Failed to retrieve the Solana token map.")); 43 | } 44 | ); 45 | }; 46 | 47 | export default useSolanaTokenMap; 48 | -------------------------------------------------------------------------------- /src/hooks/useSuiMetadata.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcProvider } from "@mysten/sui.js"; 2 | import { useLayoutEffect, useMemo, useState } from "react"; 3 | import { DataWrapper } from "../store/helpers"; 4 | import { getSuiProvider } from "../utils/sui"; 5 | 6 | export type SuiMetadata = { 7 | symbol?: string; 8 | logo?: string; 9 | tokenName?: string; 10 | decimals?: number; 11 | }; 12 | 13 | const fetchSingleMetadata = async ( 14 | coinType: string, 15 | provider: JsonRpcProvider 16 | ) => 17 | provider.getCoinMetadata({ coinType }).then((response) => { 18 | if (!response) { 19 | throw new Error("Error fetching metdata"); 20 | } 21 | return { 22 | symbol: response.symbol, 23 | tokenName: response.name, 24 | decimals: response.decimals, 25 | } as SuiMetadata; 26 | }); 27 | 28 | const fetchSuiMetadata = async (addresses: string[]) => { 29 | const provider = getSuiProvider(); 30 | const promises: Promise[] = []; 31 | addresses.forEach((address) => { 32 | promises.push(fetchSingleMetadata(address, provider)); 33 | }); 34 | const resultsArray = await Promise.all(promises); 35 | const output = new Map(); 36 | addresses.forEach((address, index) => { 37 | output.set(address, resultsArray[index]); 38 | }); 39 | 40 | return output; 41 | }; 42 | 43 | const useSuiMetadata = ( 44 | addresses: string[] 45 | ): DataWrapper> => { 46 | const [isFetching, setIsFetching] = useState(false); 47 | const [error, setError] = useState(""); 48 | const [data, setData] = useState | null>(null); 49 | 50 | useLayoutEffect(() => { 51 | let cancelled = false; 52 | if (addresses.length) { 53 | setIsFetching(true); 54 | setError(""); 55 | setData(null); 56 | fetchSuiMetadata(addresses).then( 57 | (results) => { 58 | if (!cancelled) { 59 | setData(results); 60 | setIsFetching(false); 61 | } 62 | }, 63 | () => { 64 | if (!cancelled) { 65 | setError("Could not retrieve contract metadata"); 66 | setIsFetching(false); 67 | } 68 | } 69 | ); 70 | } 71 | return () => { 72 | cancelled = true; 73 | }; 74 | }, [addresses]); 75 | 76 | return useMemo( 77 | () => ({ 78 | data, 79 | isFetching, 80 | error, 81 | receivedAt: null, 82 | }), 83 | [data, isFetching, error] 84 | ); 85 | }; 86 | 87 | export default useSuiMetadata; 88 | -------------------------------------------------------------------------------- /src/hooks/useTerraMetadata.ts: -------------------------------------------------------------------------------- 1 | import { TerraChainId } from "@certusone/wormhole-sdk"; 2 | import { LCDClient } from "@terra-money/terra.js"; 3 | import { useLayoutEffect, useMemo, useState } from "react"; 4 | import { DataWrapper } from "../store/helpers"; 5 | import { getTerraConfig } from "../utils/consts"; 6 | 7 | export type TerraMetadata = { 8 | symbol?: string; 9 | logo?: string; 10 | tokenName?: string; 11 | decimals?: number; 12 | }; 13 | 14 | const fetchSingleMetadata = async (address: string, lcd: LCDClient) => 15 | lcd.wasm 16 | .contractQuery(address, { 17 | token_info: {}, 18 | }) 19 | .then( 20 | ({ symbol, name: tokenName, decimals }: any) => 21 | ({ 22 | symbol, 23 | tokenName, 24 | decimals, 25 | } as TerraMetadata) 26 | ); 27 | 28 | const fetchTerraMetadata = async ( 29 | addresses: string[], 30 | chainId: TerraChainId 31 | ) => { 32 | const lcd = new LCDClient(getTerraConfig(chainId)); 33 | const promises: Promise[] = []; 34 | addresses.forEach((address) => { 35 | promises.push(fetchSingleMetadata(address, lcd)); 36 | }); 37 | const resultsArray = await Promise.all(promises); 38 | const output = new Map(); 39 | addresses.forEach((address, index) => { 40 | output.set(address, resultsArray[index]); 41 | }); 42 | 43 | return output; 44 | }; 45 | 46 | const useTerraMetadata = ( 47 | addresses: string[], 48 | chainId: TerraChainId 49 | ): DataWrapper> => { 50 | const [isFetching, setIsFetching] = useState(false); 51 | const [error, setError] = useState(""); 52 | const [data, setData] = useState | null>(null); 53 | 54 | useLayoutEffect(() => { 55 | let cancelled = false; 56 | if (addresses.length) { 57 | setIsFetching(true); 58 | setError(""); 59 | setData(null); 60 | fetchTerraMetadata(addresses, chainId).then( 61 | (results) => { 62 | if (!cancelled) { 63 | setData(results); 64 | setIsFetching(false); 65 | } 66 | }, 67 | () => { 68 | if (!cancelled) { 69 | setError("Could not retrieve contract metadata"); 70 | setIsFetching(false); 71 | } 72 | } 73 | ); 74 | } 75 | return () => { 76 | cancelled = true; 77 | }; 78 | }, [addresses, chainId]); 79 | 80 | return useMemo( 81 | () => ({ 82 | data, 83 | isFetching, 84 | error, 85 | receivedAt: null, 86 | }), 87 | [data, isFetching, error] 88 | ); 89 | }; 90 | 91 | export default useTerraMetadata; 92 | -------------------------------------------------------------------------------- /src/hooks/useTerraNativeBalances.ts: -------------------------------------------------------------------------------- 1 | import { TerraChainId } from "@certusone/wormhole-sdk"; 2 | import { LCDClient } from "@terra-money/terra.js"; 3 | import { MutableRefObject, useEffect, useMemo, useState } from "react"; 4 | import { getTerraConfig } from "../utils/consts"; 5 | 6 | export interface TerraNativeBalances { 7 | [index: string]: string; 8 | } 9 | 10 | export default function useTerraNativeBalances( 11 | chainId: TerraChainId, 12 | walletAddress?: string, 13 | refreshRef?: MutableRefObject<() => void> 14 | ) { 15 | const [isLoading, setIsLoading] = useState(true); 16 | const [balances, setBalances] = useState({}); 17 | const [refresh, setRefresh] = useState(false); 18 | useEffect(() => { 19 | if (refreshRef) { 20 | refreshRef.current = () => { 21 | setRefresh(true); 22 | }; 23 | } 24 | }, [refreshRef]); 25 | useEffect(() => { 26 | setRefresh(false); 27 | if (walletAddress) { 28 | setIsLoading(true); 29 | setBalances(undefined); 30 | const lcd = new LCDClient(getTerraConfig(chainId)); 31 | lcd.bank 32 | .balance(walletAddress) 33 | .then(([coins]) => { 34 | // coins doesn't support reduce 35 | const balancePairs = coins.map(({ amount, denom }) => [ 36 | denom, 37 | amount, 38 | ]); 39 | const balance = balancePairs.reduce((obj, current) => { 40 | obj[current[0].toString()] = current[1].toString(); 41 | return obj; 42 | }, {} as TerraNativeBalances); 43 | setIsLoading(false); 44 | setBalances(balance); 45 | }) 46 | .catch((e) => { 47 | setIsLoading(false); 48 | setBalances(undefined); 49 | }); 50 | } else { 51 | setIsLoading(false); 52 | setBalances(undefined); 53 | } 54 | }, [walletAddress, refresh, chainId]); 55 | const value = useMemo(() => ({ isLoading, balances }), [isLoading, balances]); 56 | return value; 57 | } 58 | -------------------------------------------------------------------------------- /src/hooks/useTerraTokenMap.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch } from "@reduxjs/toolkit"; 2 | import axios from "axios"; 3 | import { useEffect } from "react"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | import { DataWrapper } from "../store/helpers"; 6 | import { selectTerraTokenMap } from "../store/selectors"; 7 | import { 8 | errorTerraTokenMap, 9 | fetchTerraTokenMap, 10 | receiveTerraTokenMap, 11 | } from "../store/tokenSlice"; 12 | import { TERRA_TOKEN_METADATA_URL } from "../utils/consts"; 13 | 14 | export type TerraTokenMetadata = { 15 | protocol: string; 16 | symbol: string; 17 | token: string; 18 | icon: string; 19 | name?: string; 20 | balance?: string; // populated by native tokens, could move to a type that extends this 21 | }; 22 | 23 | export type TerraTokenMap = { 24 | mainnet: { 25 | [address: string]: TerraTokenMetadata; 26 | }; 27 | classic: { 28 | [address: string]: TerraTokenMetadata; 29 | }; 30 | }; 31 | 32 | const useTerraTokenMap = (shouldFire: boolean): DataWrapper => { 33 | const terraTokenMap = useSelector(selectTerraTokenMap); 34 | const dispatch = useDispatch(); 35 | const internalShouldFire = 36 | shouldFire && 37 | (terraTokenMap.data === undefined || 38 | (terraTokenMap.data === null && !terraTokenMap.isFetching)); 39 | 40 | useEffect(() => { 41 | if (internalShouldFire) { 42 | getTerraTokenMap(dispatch); 43 | } 44 | }, [internalShouldFire, dispatch]); 45 | 46 | return terraTokenMap; 47 | }; 48 | 49 | const getTerraTokenMap = (dispatch: Dispatch) => { 50 | dispatch(fetchTerraTokenMap()); 51 | axios.get(TERRA_TOKEN_METADATA_URL).then( 52 | (response) => { 53 | dispatch(receiveTerraTokenMap(response.data as TerraTokenMap)); 54 | }, 55 | (error) => { 56 | dispatch(errorTerraTokenMap("Failed to retrieve the Terra Token List.")); 57 | } 58 | ); 59 | }; 60 | 61 | export default useTerraTokenMap; 62 | -------------------------------------------------------------------------------- /src/hooks/useTransferSignedVAA.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { useSelector } from "react-redux"; 3 | import { selectTransferSignedVAAHex } from "../store/selectors"; 4 | import { hexToUint8Array } from "@certusone/wormhole-sdk"; 5 | 6 | export default function useTransferSignedVAA() { 7 | const signedVAAHex = useSelector(selectTransferSignedVAAHex); 8 | const signedVAA = useMemo( 9 | () => (signedVAAHex ? hexToUint8Array(signedVAAHex) : undefined), 10 | [signedVAAHex] 11 | ); 12 | return signedVAA; 13 | } 14 | -------------------------------------------------------------------------------- /src/hooks/useTransferTargetAddress.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { useSelector } from "react-redux"; 3 | import { selectTransferTargetAddressHex } from "../store/selectors"; 4 | import { hexToUint8Array } from "@certusone/wormhole-sdk"; 5 | 6 | export default function useTransferTargetAddressHex() { 7 | const targetAddressHex = useSelector(selectTransferTargetAddressHex); 8 | const targetAddress = useMemo( 9 | () => (targetAddressHex ? hexToUint8Array(targetAddressHex) : undefined), 10 | [targetAddressHex] 11 | ); 12 | return targetAddress; 13 | } 14 | -------------------------------------------------------------------------------- /src/hooks/useXplaMetadata.ts: -------------------------------------------------------------------------------- 1 | import { LCDClient } from "@xpla/xpla.js"; 2 | import { useLayoutEffect, useMemo, useState } from "react"; 3 | import { DataWrapper } from "../store/helpers"; 4 | import { XPLA_LCD_CLIENT_CONFIG } from "../utils/consts"; 5 | 6 | export type XplaMetadata = { 7 | symbol?: string; 8 | logo?: string; 9 | tokenName?: string; 10 | decimals?: number; 11 | }; 12 | 13 | const fetchSingleMetadata = async (address: string, lcd: LCDClient) => 14 | lcd.wasm 15 | .contractQuery(address, { 16 | token_info: {}, 17 | }) 18 | .then( 19 | ({ symbol, name: tokenName, decimals }: any) => 20 | ({ 21 | symbol, 22 | tokenName, 23 | decimals, 24 | } as XplaMetadata) 25 | ); 26 | 27 | const fetchXplaMetadata = async (addresses: string[]) => { 28 | const lcd = new LCDClient(XPLA_LCD_CLIENT_CONFIG); 29 | const promises: Promise[] = []; 30 | addresses.forEach((address) => { 31 | promises.push(fetchSingleMetadata(address, lcd)); 32 | }); 33 | const resultsArray = await Promise.all(promises); 34 | const output = new Map(); 35 | addresses.forEach((address, index) => { 36 | output.set(address, resultsArray[index]); 37 | }); 38 | 39 | return output; 40 | }; 41 | 42 | const useXplaMetadata = ( 43 | addresses: string[] 44 | ): DataWrapper> => { 45 | const [isFetching, setIsFetching] = useState(false); 46 | const [error, setError] = useState(""); 47 | const [data, setData] = useState | null>(null); 48 | 49 | useLayoutEffect(() => { 50 | let cancelled = false; 51 | if (addresses.length) { 52 | setIsFetching(true); 53 | setError(""); 54 | setData(null); 55 | fetchXplaMetadata(addresses).then( 56 | (results) => { 57 | if (!cancelled) { 58 | setData(results); 59 | setIsFetching(false); 60 | } 61 | }, 62 | () => { 63 | if (!cancelled) { 64 | setError("Could not retrieve contract metadata"); 65 | setIsFetching(false); 66 | } 67 | } 68 | ); 69 | } 70 | return () => { 71 | cancelled = true; 72 | }; 73 | }, [addresses]); 74 | 75 | return useMemo( 76 | () => ({ 77 | data, 78 | isFetching, 79 | error, 80 | receivedAt: null, 81 | }), 82 | [data, isFetching, error] 83 | ); 84 | }; 85 | 86 | export default useXplaMetadata; 87 | -------------------------------------------------------------------------------- /src/hooks/useXplaNativeBalances.ts: -------------------------------------------------------------------------------- 1 | import { LCDClient } from "@xpla/xpla.js"; 2 | import { MutableRefObject, useEffect, useMemo, useState } from "react"; 3 | import { XPLA_LCD_CLIENT_CONFIG } from "../utils/consts"; 4 | 5 | export interface XplaNativeBalances { 6 | [index: string]: string; 7 | } 8 | 9 | export default function useXplaNativeBalances( 10 | walletAddress?: string, 11 | refreshRef?: MutableRefObject<() => void> 12 | ) { 13 | const [isLoading, setIsLoading] = useState(true); 14 | const [balances, setBalances] = useState({}); 15 | const [refresh, setRefresh] = useState(false); 16 | useEffect(() => { 17 | if (refreshRef) { 18 | refreshRef.current = () => { 19 | setRefresh(true); 20 | }; 21 | } 22 | }, [refreshRef]); 23 | useEffect(() => { 24 | setRefresh(false); 25 | if (walletAddress) { 26 | setIsLoading(true); 27 | setBalances(undefined); 28 | const lcd = new LCDClient(XPLA_LCD_CLIENT_CONFIG); 29 | lcd.bank 30 | .balance(walletAddress) 31 | .then(([coins]) => { 32 | // coins doesn't support reduce 33 | const balancePairs = coins.map(({ amount, denom }) => [ 34 | denom, 35 | amount, 36 | ]); 37 | const balance = balancePairs.reduce((obj, current) => { 38 | obj[current[0].toString()] = current[1].toString(); 39 | return obj; 40 | }, {} as XplaNativeBalances); 41 | setIsLoading(false); 42 | setBalances(balance); 43 | }) 44 | .catch((e) => { 45 | console.error(e); 46 | setIsLoading(false); 47 | setBalances(undefined); 48 | }); 49 | } else { 50 | setIsLoading(false); 51 | setBalances(undefined); 52 | } 53 | }, [walletAddress, refresh]); 54 | const value = useMemo(() => ({ isLoading, balances }), [isLoading, balances]); 55 | return value; 56 | } 57 | -------------------------------------------------------------------------------- /src/icons/Discord.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/Docs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/Github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/Medium.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/Telegram.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/Twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/acala.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/algorand.svg: -------------------------------------------------------------------------------- 1 | ALGO_Logos_190320 -------------------------------------------------------------------------------- /src/icons/aptos.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/arbitrum.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/icons/aurora.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/icons/avax.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/base.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/icons/bnb.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/bsc.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/celo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | Artboard 1 11 | 13 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/icons/eth.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/fantom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/injective.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/icons/karura.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/klaytn.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 12 | 14 | 17 | 19 | 20 | -------------------------------------------------------------------------------- /src/icons/metamask-fox.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/near.svg: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/oasis-network-rose-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/optimism.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/polygon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/sei.svg: -------------------------------------------------------------------------------- 1 | 7 | 14 | 15 | -------------------------------------------------------------------------------- /src/icons/solana.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/sui.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/terra.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/terra2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/usdc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/walletconnect.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/xpla.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { CssBaseline } from "@material-ui/core"; 2 | import { ThemeProvider } from "@material-ui/core/styles"; 3 | import { SeiWalletProvider } from "@sei-js/react"; 4 | import { SnackbarProvider } from "notistack"; 5 | import ReactDOM from "react-dom"; 6 | import { Provider } from "react-redux"; 7 | import { HashRouter } from "react-router-dom"; 8 | import App from "./App"; 9 | import ErrorBoundary from "./ErrorBoundary"; 10 | import { AlgorandContextProvider } from "./contexts/AlgorandWalletContext"; 11 | import AptosWalletProvider from "./contexts/AptosWalletContext"; 12 | import { EthereumProviderProvider } from "./contexts/EthereumProviderContext"; 13 | import InjectiveWalletProvider from "./contexts/InjectiveWalletContext"; 14 | import { NearContextProvider } from "./contexts/NearWalletContext"; 15 | import { SolanaWalletProvider } from "./contexts/SolanaWalletContext.tsx"; 16 | import SuiWalletProvider from "./contexts/SuiWalletContext"; 17 | import { TerraWalletProvider } from "./contexts/TerraWalletContext.tsx"; 18 | import XplaWalletProvider from "./contexts/XplaWalletContext"; 19 | import { theme } from "./muiTheme"; 20 | import { store } from "./store"; 21 | import { SEI_CHAIN_CONFIGURATION } from "./utils/consts"; 22 | 23 | ReactDOM.render( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | , 60 | document.getElementById("root") 61 | ); 62 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/store/feeSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | import { TERRA_DEFAULT_FEE_DENOM } from "../utils/consts"; 3 | 4 | export interface FeeSliceState { 5 | terraFeeDenom: string; 6 | } 7 | 8 | const initialState: FeeSliceState = { 9 | terraFeeDenom: TERRA_DEFAULT_FEE_DENOM, 10 | }; 11 | 12 | export const feeSlice = createSlice({ 13 | name: "fee", 14 | initialState, 15 | reducers: { 16 | setTerraFeeDenom: (state, action: PayloadAction) => { 17 | state.terraFeeDenom = action.payload; 18 | }, 19 | reset: () => initialState, 20 | }, 21 | }); 22 | 23 | export const { setTerraFeeDenom, reset } = feeSlice.actions; 24 | 25 | export default feeSlice.reducer; 26 | -------------------------------------------------------------------------------- /src/store/helpers.ts: -------------------------------------------------------------------------------- 1 | export type DataWrapper = { 2 | data: T | null; 3 | error: any | null; 4 | isFetching: boolean; 5 | receivedAt: string | null; 6 | //possibly invalidate 7 | }; 8 | 9 | export function getEmptyDataWrapper() { 10 | return { 11 | data: null, 12 | error: null, 13 | isFetching: false, 14 | receivedAt: null, 15 | }; 16 | } 17 | 18 | export function receiveDataWrapper(data: T): DataWrapper { 19 | return { 20 | data, 21 | error: null, 22 | isFetching: false, 23 | receivedAt: new Date().toISOString(), 24 | }; 25 | } 26 | 27 | export function errorDataWrapper(error: string): DataWrapper { 28 | return { 29 | data: null, 30 | error, 31 | isFetching: false, 32 | receivedAt: null, 33 | }; 34 | } 35 | 36 | export function fetchDataWrapper() { 37 | return { 38 | data: null, 39 | error: null, 40 | isFetching: true, 41 | receivedAt: null, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import attestReducer from "./attestSlice"; 3 | import nftReducer from "./nftSlice"; 4 | import transferReducer from "./transferSlice"; 5 | import tokenReducer from "./tokenSlice"; 6 | import feeReducer from "./feeSlice"; 7 | import usdcReducer from "./usdcSlice"; 8 | 9 | export const store = configureStore({ 10 | reducer: { 11 | attest: attestReducer, 12 | nft: nftReducer, 13 | transfer: transferReducer, 14 | tokens: tokenReducer, 15 | fee: feeReducer, 16 | usdc: usdcReducer, 17 | }, 18 | }); 19 | 20 | // Infer the `RootState` and `AppDispatch` types from the store itself 21 | export type RootState = ReturnType; 22 | // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} 23 | export type AppDispatch = typeof store.dispatch; 24 | -------------------------------------------------------------------------------- /src/store/tokenSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | import { TokenInfo } from "@solana/spl-token-registry"; 3 | import { RelayerTokenInfo } from "../hooks/useRelayersAvailable"; 4 | import { TerraTokenMap } from "../hooks/useTerraTokenMap"; 5 | import { 6 | DataWrapper, 7 | errorDataWrapper, 8 | fetchDataWrapper, 9 | getEmptyDataWrapper, 10 | receiveDataWrapper, 11 | } from "./helpers"; 12 | 13 | export interface TokenMetadataState { 14 | solanaTokenMap: DataWrapper; 15 | terraTokenMap: DataWrapper; //TODO make a decent type for this. 16 | relayerTokenInfo: DataWrapper; 17 | } 18 | 19 | const initialState: TokenMetadataState = { 20 | solanaTokenMap: getEmptyDataWrapper(), 21 | terraTokenMap: getEmptyDataWrapper(), 22 | relayerTokenInfo: getEmptyDataWrapper(), 23 | }; 24 | 25 | export const tokenSlice = createSlice({ 26 | name: "tokenInfos", 27 | initialState, 28 | reducers: { 29 | receiveSolanaTokenMap: (state, action: PayloadAction) => { 30 | state.solanaTokenMap = receiveDataWrapper(action.payload); 31 | }, 32 | fetchSolanaTokenMap: (state) => { 33 | state.solanaTokenMap = fetchDataWrapper(); 34 | }, 35 | errorSolanaTokenMap: (state, action: PayloadAction) => { 36 | state.solanaTokenMap = errorDataWrapper(action.payload); 37 | }, 38 | 39 | receiveTerraTokenMap: (state, action: PayloadAction) => { 40 | state.terraTokenMap = receiveDataWrapper(action.payload); 41 | }, 42 | fetchTerraTokenMap: (state) => { 43 | state.terraTokenMap = fetchDataWrapper(); 44 | }, 45 | errorTerraTokenMap: (state, action: PayloadAction) => { 46 | state.terraTokenMap = errorDataWrapper(action.payload); 47 | }, 48 | 49 | receiveRelayerTokenInfo: ( 50 | state, 51 | action: PayloadAction 52 | ) => { 53 | state.relayerTokenInfo = receiveDataWrapper(action.payload); 54 | }, 55 | fetchRelayerTokenInfo: (state) => { 56 | state.relayerTokenInfo = fetchDataWrapper(); 57 | }, 58 | errorRelayerTokenInfo: (state, action: PayloadAction) => { 59 | state.relayerTokenInfo = errorDataWrapper(action.payload); 60 | }, 61 | 62 | reset: () => initialState, 63 | }, 64 | }); 65 | 66 | export const { 67 | receiveSolanaTokenMap, 68 | fetchSolanaTokenMap, 69 | errorSolanaTokenMap, 70 | receiveTerraTokenMap, 71 | fetchTerraTokenMap, 72 | errorTerraTokenMap, 73 | receiveRelayerTokenInfo, 74 | fetchRelayerTokenInfo, 75 | errorRelayerTokenInfo, 76 | reset, 77 | } = tokenSlice.actions; 78 | 79 | export default tokenSlice.reducer; 80 | -------------------------------------------------------------------------------- /src/store/usdcSelectors.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from "."; 2 | 3 | export const selectSourceChain = (state: RootState) => state.usdc.sourceChain; 4 | 5 | export const selectTargetChain = (state: RootState) => state.usdc.targetChain; 6 | 7 | export const selectBalance = (state: RootState) => state.usdc.balance; 8 | 9 | export const selectRelayerFee = (state: RootState) => state.usdc.relayerFee; 10 | 11 | export const selectMaxSwapAmount = (state: RootState) => 12 | state.usdc.maxSwapAmount; 13 | 14 | export const selectEstimatedSwapAmount = (state: RootState) => 15 | state.usdc.estimatedSwapAmount; 16 | 17 | export const selectAmount = (state: RootState) => state.usdc.amount; 18 | 19 | export const selectShouldRelay = (state: RootState) => state.usdc.shouldRelay; 20 | 21 | export const selectToNativeAmount = (state: RootState) => 22 | state.usdc.toNativeAmount; 23 | 24 | export const selectIsSending = (state: RootState) => state.usdc.isSending; 25 | 26 | export const selectSourceTxHash = (state: RootState) => state.usdc.sourceTxHash; 27 | 28 | export const selectSourceTxConfirmed = (state: RootState) => 29 | state.usdc.sourceTxConfirmed; 30 | 31 | export const selectTransferInfo = (state: RootState) => state.usdc.transferInfo; 32 | 33 | export const selectIsRedeeming = (state: RootState) => state.usdc.isRedeeming; 34 | 35 | export const selectIsRedeemComplete = (state: RootState) => 36 | state.usdc.isRedeemComplete; 37 | 38 | export const selectTargetTxHash = (state: RootState) => state.usdc.targetTxHash; 39 | 40 | export const selectAllowanceError = (state: RootState) => 41 | state.usdc.allowanceError; 42 | 43 | export const selectShouldApproveUnlimited = (state: RootState) => 44 | state.usdc.shouldApproveUnlimited; 45 | -------------------------------------------------------------------------------- /src/utils/SolanaPriceStore.ts: -------------------------------------------------------------------------------- 1 | import { MARKETS } from "@project-serum/serum"; 2 | import { Connection, PublicKey } from "@solana/web3.js"; 3 | 4 | export interface Markets { 5 | [coin: string]: { 6 | publicKey?: PublicKey; 7 | name: string; 8 | deprecated?: boolean; 9 | }; 10 | } 11 | 12 | export const serumMarkets = (() => { 13 | const m: Markets = {}; 14 | MARKETS.forEach((market) => { 15 | const coin = market.name.split("/")[0]; 16 | if (m[coin]) { 17 | // Only override a market if it's not deprecated . 18 | if (!m.deprecated) { 19 | m[coin] = { 20 | publicKey: market.address, 21 | name: market.name.split("/").join(""), 22 | }; 23 | } 24 | } else { 25 | m[coin] = { 26 | publicKey: market.address, 27 | name: market.name.split("/").join(""), 28 | }; 29 | } 30 | }); 31 | 32 | m["USDC"] = m["USDT"]; 33 | 34 | return m; 35 | })(); 36 | 37 | // Create a cached API wrapper to avoid rate limits. 38 | class PriceStore { 39 | cache: Map; 40 | 41 | constructor() { 42 | this.cache = new Map(); 43 | } 44 | 45 | async getPrice( 46 | connection: Connection, 47 | marketName: string 48 | ): Promise { 49 | return new Promise((resolve, reject) => { 50 | if (this.cache.get(marketName) === undefined) { 51 | fetch(`https://serum-api.bonfida.com/orderbooks/${marketName}`).then( 52 | (resp) => { 53 | resp.json().then((resp) => { 54 | if (resp.data.asks === null || resp.data.bids === null) { 55 | resolve(undefined); 56 | } else if ( 57 | resp.data.asks.length === 0 && 58 | resp.data.bids.length === 0 59 | ) { 60 | resolve(undefined); 61 | } else if (resp.data.asks.length === 0) { 62 | resolve(resp.data.bids[0].price); 63 | } else if (resp.data.bids.length === 0) { 64 | resolve(resp.data.asks[0].price); 65 | } else { 66 | const mid = 67 | (resp.data.asks[0].price + resp.data.bids[0].price) / 2.0; 68 | this.cache.set(marketName, mid); 69 | resolve(this.cache.get(marketName)); 70 | } 71 | }); 72 | } 73 | ); 74 | } else { 75 | return resolve(this.cache.get(marketName)); 76 | } 77 | }); 78 | } 79 | } 80 | 81 | export const priceStore = new PriceStore(); 82 | -------------------------------------------------------------------------------- /src/utils/algorand.ts: -------------------------------------------------------------------------------- 1 | import { TransactionSignerPair } from "@certusone/wormhole-sdk/lib/esm/algorand"; 2 | import MyAlgoConnect from "@randlabs/myalgo-connect"; 3 | import { Algodv2, assignGroupID, waitForConfirmation } from "algosdk"; 4 | import { ALGORAND_WAIT_FOR_CONFIRMATIONS } from "./consts"; 5 | 6 | export async function signSendAndConfirmAlgorand( 7 | algodClient: Algodv2, 8 | txs: TransactionSignerPair[] 9 | ) { 10 | const myAlgoConnect = new MyAlgoConnect(); 11 | assignGroupID(txs.map((tx) => tx.tx)); 12 | const signedTxns: Uint8Array[] = []; 13 | const lsigSignedTxns: Uint8Array[] = []; 14 | const walletUnsignedTxns: Uint8Array[] = []; 15 | // sign all the lsigs 16 | for (const lsigTx of txs) { 17 | if (lsigTx.signer) { 18 | lsigSignedTxns.push(await lsigTx.signer.signTxn(lsigTx.tx)); 19 | } 20 | } 21 | // assemble the txs for the wallet to sign 22 | for (const walletTx of txs) { 23 | if (!walletTx.signer) { 24 | walletUnsignedTxns.push(walletTx.tx.toByte()); 25 | } 26 | } 27 | const walletSignedTxns = await myAlgoConnect.signTransaction( 28 | walletUnsignedTxns 29 | ); 30 | let lsigIdx = 0; 31 | let walletIdx = 0; 32 | for (const originalTx of txs) { 33 | if (originalTx.signer) { 34 | signedTxns.push(lsigSignedTxns[lsigIdx++]); 35 | } else { 36 | signedTxns.push(walletSignedTxns[walletIdx++].blob); 37 | } 38 | } 39 | await algodClient.sendRawTransaction(signedTxns).do(); 40 | const result = await waitForConfirmation( 41 | algodClient, 42 | txs[txs.length - 1].tx.txID(), 43 | ALGORAND_WAIT_FOR_CONFIRMATIONS 44 | ); 45 | return result; 46 | } 47 | -------------------------------------------------------------------------------- /src/utils/aptos.ts: -------------------------------------------------------------------------------- 1 | import { CHAIN_ID_APTOS } from "@certusone/wormhole-sdk"; 2 | import { AptosClient, Types } from "aptos"; 3 | import { hexZeroPad } from "ethers/lib/utils"; 4 | import { APTOS_URL, getBridgeAddressForChain } from "./consts"; 5 | 6 | export enum AptosNetwork { 7 | Testnet = "Testnet", 8 | Mainnet = "Mainnet", 9 | Devnet = "Devnet", 10 | Localhost = "Localhost", 11 | } 12 | 13 | export const getAptosClient = () => new AptosClient(APTOS_URL); 14 | 15 | export const getEmitterAddressAndSequenceFromResult = ( 16 | result: Types.UserTransaction 17 | ): { emitterAddress: string; sequence: string } => { 18 | const data = result.events.find( 19 | (e) => 20 | e.type === 21 | `${getBridgeAddressForChain(CHAIN_ID_APTOS)}::state::WormholeMessage` 22 | )?.data; 23 | const emitterAddress = hexZeroPad( 24 | `0x${parseInt(data?.sender).toString(16)}`, 25 | 32 26 | ).substring(2); 27 | const sequence = data?.sequence; 28 | return { 29 | emitterAddress, 30 | sequence, 31 | }; 32 | }; 33 | 34 | export async function waitForSignAndSubmitTransaction( 35 | payload: any, 36 | signAndSubmitTransaction: ( 37 | transaction: Types.TransactionPayload, 38 | options?: any 39 | ) => Promise<{ 40 | hash: string; 41 | }> 42 | ): Promise { 43 | // The wallets do not handle Uint8Array serialization' 44 | if (payload?.arguments) { 45 | payload.arguments = payload.arguments.map((a: any) => 46 | a instanceof Uint8Array ? Array.from(a) : a 47 | ); 48 | } 49 | try { 50 | let hash = ""; 51 | hash = (await signAndSubmitTransaction(payload)).hash; 52 | if (!hash) { 53 | throw new Error("Invalid hash"); 54 | } 55 | const client = getAptosClient(); 56 | await client.waitForTransaction(hash); 57 | return hash; 58 | } catch (e) { 59 | throw e; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/utils/balancePretty.ts: -------------------------------------------------------------------------------- 1 | export const balancePretty = (uiString: string) => { 2 | const numberString = uiString.split(".")[0]; 3 | const nsLen = numberString.length; 4 | if (nsLen > 9) { 5 | // Billion case 6 | const num = numberString.substring(0, nsLen - 9); 7 | const fract = numberString.substring(nsLen - 9, nsLen - 9 + 2); 8 | return num + "." + fract + " B"; 9 | } else if (nsLen > 6) { 10 | // Million case 11 | const num = numberString.substring(0, nsLen - 6); 12 | const fract = numberString.substring(nsLen - 6, nsLen - 6 + 2); 13 | return num + "." + fract + " M"; 14 | } else if (uiString.length > 8) { 15 | return uiString.substring(0, 8); 16 | } else { 17 | return uiString; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/utils/ethereum.ts: -------------------------------------------------------------------------------- 1 | import { ethers_contracts } from "@certusone/wormhole-sdk"; 2 | import { ethers } from "ethers"; 3 | import { arrayify, formatUnits } from "ethers/lib/utils"; 4 | import { 5 | createNFTParsedTokenAccount, 6 | createParsedTokenAccount, 7 | } from "../hooks/useGetSourceParsedTokenAccounts"; 8 | 9 | //This is a valuable intermediate step to the parsed token account, as the token has metadata information on it. 10 | export async function getEthereumToken( 11 | tokenAddress: string, 12 | provider: ethers.providers.Web3Provider | ethers.providers.JsonRpcProvider 13 | ) { 14 | const token = ethers_contracts.TokenImplementation__factory.connect( 15 | tokenAddress, 16 | provider 17 | ); 18 | return token; 19 | } 20 | 21 | export async function ethTokenToParsedTokenAccount( 22 | token: ethers_contracts.TokenImplementation, 23 | signerAddress: string 24 | ) { 25 | const decimals = await token.decimals(); 26 | const balance = await token.balanceOf(signerAddress); 27 | const symbol = await token.symbol(); 28 | const name = await token.name(); 29 | return createParsedTokenAccount( 30 | signerAddress, 31 | token.address, 32 | balance.toString(), 33 | decimals, 34 | Number(formatUnits(balance, decimals)), 35 | formatUnits(balance, decimals), 36 | symbol, 37 | name 38 | ); 39 | } 40 | 41 | //This is a valuable intermediate step to the parsed token account, as the token has metadata information on it. 42 | export async function getEthereumNFT( 43 | tokenAddress: string, 44 | provider: ethers.providers.Web3Provider 45 | ) { 46 | const token = ethers_contracts.NFTImplementation__factory.connect( 47 | tokenAddress, 48 | provider 49 | ); 50 | return token; 51 | } 52 | 53 | export async function isNFT(token: ethers_contracts.NFTImplementation) { 54 | const erc721 = "0x80ac58cd"; 55 | const erc721metadata = "0x5b5e139f"; 56 | const supportsErc721 = await token.supportsInterface(arrayify(erc721)); 57 | const supportsErc721Metadata = await token.supportsInterface( 58 | arrayify(erc721metadata) 59 | ); 60 | return supportsErc721 && supportsErc721Metadata; 61 | } 62 | 63 | export async function ethNFTToNFTParsedTokenAccount( 64 | token: ethers_contracts.NFTImplementation, 65 | tokenId: string, 66 | signerAddress: string 67 | ) { 68 | const decimals = 0; 69 | const balance = (await token.ownerOf(tokenId)) === signerAddress ? 1 : 0; 70 | const symbol = await token.symbol(); 71 | const name = await token.name(); 72 | const uri = await token.tokenURI(tokenId); 73 | return createNFTParsedTokenAccount( 74 | signerAddress, 75 | token.address, 76 | balance.toString(), 77 | decimals, 78 | Number(formatUnits(balance, decimals)), 79 | formatUnits(balance, decimals), 80 | tokenId, 81 | symbol, 82 | name, 83 | uri 84 | ); 85 | } 86 | 87 | export function isValidEthereumAddress(address: string) { 88 | return ethers.utils.isAddress(address); 89 | } 90 | -------------------------------------------------------------------------------- /src/utils/getSignedVAAWithRetry.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChainId, 3 | ChainName, 4 | getGovernorIsVAAEnqueued, 5 | getSignedVAA, 6 | } from "@certusone/wormhole-sdk"; 7 | import { WORMHOLE_RPC_HOSTS } from "./consts"; 8 | 9 | export interface GetSignedVAAWithRetryResult { 10 | vaaBytes: Uint8Array | undefined; 11 | isPending: boolean; 12 | } 13 | 14 | export const getSignedVAAWithRetry = async ( 15 | emitterChain: ChainId | ChainName, 16 | emitterAddress: string, 17 | sequence: string, 18 | retryAttempts?: number 19 | ): Promise => { 20 | let currentWormholeRpcHost = -1; 21 | const getNextRpcHost = () => 22 | ++currentWormholeRpcHost % WORMHOLE_RPC_HOSTS.length; 23 | let attempts = 0; 24 | while (true) { 25 | attempts++; 26 | await new Promise((resolve) => setTimeout(resolve, 1000)); 27 | const rpcHost = WORMHOLE_RPC_HOSTS[getNextRpcHost()]; 28 | const results = await Promise.allSettled([ 29 | getSignedVAA(rpcHost, emitterChain, emitterAddress, sequence), 30 | getGovernorIsVAAEnqueued(rpcHost, emitterChain, emitterAddress, sequence), 31 | ]); 32 | if (results[0].status === "fulfilled") { 33 | return { vaaBytes: results[0].value.vaaBytes, isPending: false }; 34 | } 35 | if (results[1].status === "fulfilled" && results[1].value.isEnqueued) { 36 | return { vaaBytes: undefined, isPending: true }; 37 | } 38 | if (retryAttempts !== undefined && attempts > retryAttempts) { 39 | throw new Error(results[0].reason); 40 | } 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /src/utils/injective.ts: -------------------------------------------------------------------------------- 1 | import { cosmos, isNativeDenomInjective } from "@certusone/wormhole-sdk"; 2 | import { 3 | ChainGrpcBankApi, 4 | ChainGrpcWasmApi, 5 | Msgs, 6 | TxGrpcClient, 7 | } from "@injectivelabs/sdk-ts"; 8 | import { MsgBroadcaster, WalletStrategy } from "@injectivelabs/wallet-ts"; 9 | import { INJECTIVE_NETWORK, INJECTIVE_NETWORK_INFO } from "./consts"; 10 | 11 | export const NATIVE_INJECTIVE_DECIMALS = 18; 12 | 13 | export const INJECTIVE_NATIVE_DENOM = "inj"; 14 | 15 | export const getInjectiveWasmClient = () => 16 | new ChainGrpcWasmApi(INJECTIVE_NETWORK_INFO.grpc); 17 | 18 | export const getInjectiveBankClient = () => 19 | new ChainGrpcBankApi(INJECTIVE_NETWORK_INFO.grpc); 20 | 21 | export const getInjectiveTxClient = () => 22 | new TxGrpcClient(INJECTIVE_NETWORK_INFO.grpc); 23 | 24 | export const isValidInjectiveAddress = (address: string) => { 25 | if (isNativeDenomInjective(address)) { 26 | return true; 27 | } 28 | try { 29 | const startsWithInj = address && address.startsWith("inj"); 30 | const isParsable = cosmos.canonicalAddress(address); 31 | const isLengthOk = isParsable.length === 20; 32 | return !!(startsWithInj && isParsable && isLengthOk); 33 | } catch (error) { 34 | return false; 35 | } 36 | }; 37 | 38 | export const formatNativeDenom = (denom: string) => 39 | denom === INJECTIVE_NATIVE_DENOM ? "INJ" : ""; 40 | 41 | export const broadcastInjectiveTx = async ( 42 | walletStrategy: WalletStrategy, 43 | walletAddress: string, 44 | msgs: Msgs | Msgs[], 45 | memo: string = "" 46 | ) => { 47 | const client = getInjectiveTxClient(); 48 | const network = INJECTIVE_NETWORK; 49 | const broadcaster = new MsgBroadcaster({ 50 | network, 51 | walletStrategy, 52 | simulateTx: true, 53 | }); 54 | const txResponse = await broadcaster.broadcast({ 55 | //@ts-ignore 56 | msgs, 57 | address: walletAddress, 58 | memo, 59 | }); 60 | const tx = await client.fetchTxPoll(txResponse.txHash); 61 | if (!tx) { 62 | throw new Error("Unable to fetch transaction"); 63 | } 64 | if (tx.code !== 0) { 65 | throw new Error(`Transaction failed: ${tx.rawLog}`); 66 | } 67 | return tx; 68 | }; 69 | -------------------------------------------------------------------------------- /src/utils/karura.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export async function getKaruraGasParams(rpc: string): Promise<{ 4 | gasPrice: number; 5 | gasLimit: number; 6 | }> { 7 | const gasLimit = 21000000; 8 | const storageLimit = 64001; 9 | const res = ( 10 | await axios.post(rpc, { 11 | id: 0, 12 | jsonrpc: "2.0", 13 | method: "eth_getEthGas", 14 | params: [ 15 | { 16 | gasLimit, 17 | storageLimit, 18 | }, 19 | ], 20 | }) 21 | ).data.result; 22 | 23 | return { 24 | gasLimit: parseInt(res.gasLimit, 16), 25 | gasPrice: parseInt(res.gasPrice, 16), 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/near.ts: -------------------------------------------------------------------------------- 1 | import { Wallet } from "@near-wallet-selector/core/lib/wallet"; 2 | import { Account, connect } from "near-api-js"; 3 | import { FunctionCallOptions } from "near-api-js/lib/account"; 4 | import { 5 | FinalExecutionOutcome, 6 | JsonRpcProvider, 7 | } from "@certusone/wormhole-sdk/node_modules/near-api-js/lib/providers"; //from "near-api-js/lib/providers"; 8 | import { getNearConnectionConfig } from "./consts"; 9 | 10 | export const makeNearAccount = async (senderAddr: string) => 11 | await (await connect(getNearConnectionConfig())).account(senderAddr); 12 | 13 | export const makeNearProvider = () => 14 | new JsonRpcProvider({ url: getNearConnectionConfig().nodeUrl }); 15 | 16 | export const signAndSendTransactions = async ( 17 | account: Account, 18 | wallet: Wallet, 19 | messages: FunctionCallOptions[] 20 | ): Promise => { 21 | // the browser wallet's signAndSendTransactions call navigates away from the page which is incompatible with the current app design 22 | if (wallet.type === "browser" && account) { 23 | let lastReceipt: FinalExecutionOutcome | null = null; 24 | for (const message of messages) { 25 | lastReceipt = await account.functionCall(message); 26 | } 27 | if (!lastReceipt) { 28 | throw new Error("An error occurred while fetching the transaction info"); 29 | } 30 | return lastReceipt; 31 | } 32 | const receipts = await wallet.signAndSendTransactions({ 33 | transactions: messages.map((options) => ({ 34 | signerId: wallet.id, 35 | receiverId: options.contractId, 36 | actions: [ 37 | { 38 | type: "FunctionCall", 39 | params: { 40 | methodName: options.methodName, 41 | args: options.args, 42 | gas: options.gas?.toString() || "0", 43 | deposit: options.attachedDeposit?.toString() || "0", 44 | }, 45 | }, 46 | ], 47 | })), 48 | }); 49 | if (!receipts || receipts.length === 0) { 50 | throw new Error("An error occurred while fetching the transaction info"); 51 | } 52 | return receipts[receipts.length - 1]; 53 | }; 54 | 55 | export async function lookupHash( 56 | account: Account, 57 | tokenBridge: string, 58 | hash: string 59 | ): Promise<[boolean, string]> { 60 | return await account.viewFunction(tokenBridge, "hash_lookup", { 61 | hash, 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /src/utils/parseError.ts: -------------------------------------------------------------------------------- 1 | const MM_ERR_WITH_INFO_START = 2 | "VM Exception while processing transaction: revert "; 3 | const parseError = (e: any) => 4 | e?.data?.message?.startsWith(MM_ERR_WITH_INFO_START) 5 | ? e.data.message.replace(MM_ERR_WITH_INFO_START, "") 6 | : e?.response?.data?.error // terra error 7 | ? e.response.data.error 8 | : e?.message 9 | ? e.message 10 | : "An unknown error occurred"; 11 | export default parseError; 12 | -------------------------------------------------------------------------------- /src/utils/pushToClipboard.ts: -------------------------------------------------------------------------------- 1 | export default function pushToClipboard(content: any) { 2 | if (!navigator.clipboard) { 3 | // Clipboard API not available 4 | return; 5 | } 6 | return navigator.clipboard.writeText(content); 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/sleep.ts: -------------------------------------------------------------------------------- 1 | export function sleep(ms: number) { 2 | return new Promise((resolve) => setTimeout(resolve, ms)); 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/solana.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from "@ethersproject/bignumber"; 2 | import { MintLayout } from "@solana/spl-token"; 3 | import { WalletContextState } from "@solana/wallet-adapter-react"; 4 | import { 5 | AccountInfo, 6 | Connection, 7 | PublicKey, 8 | Transaction, 9 | } from "@solana/web3.js"; 10 | 11 | export async function signSendAndConfirm( 12 | wallet: WalletContextState, 13 | connection: Connection, 14 | transaction: Transaction 15 | ) { 16 | if (!wallet.signTransaction) { 17 | throw new Error("wallet.signTransaction is undefined"); 18 | } 19 | const signed = await wallet.signTransaction(transaction); 20 | const txid = await connection.sendRawTransaction(signed.serialize()); 21 | await connection.confirmTransaction(txid); 22 | return txid; 23 | } 24 | 25 | export interface ExtractedMintInfo { 26 | mintAuthority?: string; 27 | supply?: string; 28 | } 29 | 30 | export function extractMintInfo( 31 | account: AccountInfo 32 | ): ExtractedMintInfo { 33 | const data = Buffer.from(account.data); 34 | const mintInfo = MintLayout.decode(data); 35 | 36 | const uintArray = mintInfo?.mintAuthority; 37 | const pubkey = new PublicKey(uintArray); 38 | const supply = BigNumber.from(mintInfo?.supply.reverse()).toString(); 39 | const output = { 40 | mintAuthority: pubkey?.toString(), 41 | supply: supply.toString(), 42 | }; 43 | 44 | return output; 45 | } 46 | 47 | export async function getMultipleAccountsRPC( 48 | connection: Connection, 49 | pubkeys: PublicKey[] 50 | ): Promise<(AccountInfo | null)[]> { 51 | return getMultipleAccounts(connection, pubkeys, "confirmed"); 52 | } 53 | 54 | export const getMultipleAccounts = async ( 55 | connection: any, 56 | pubkeys: PublicKey[], 57 | commitment: string 58 | ) => { 59 | return ( 60 | await Promise.all( 61 | chunks(pubkeys, 99).map((chunk) => 62 | connection.getMultipleAccountsInfo(chunk, commitment) 63 | ) 64 | ) 65 | ).flat(); 66 | }; 67 | 68 | export function chunks(array: T[], size: number): T[][] { 69 | return Array.apply( 70 | 0, 71 | new Array(Math.ceil(array.length / size)) 72 | ).map((_, index) => array.slice(index * size, (index + 1) * size)); 73 | } 74 | 75 | export function shortenAddress(address: string) { 76 | return address.length > 10 77 | ? `${address.slice(0, 4)}...${address.slice(-4)}` 78 | : address; 79 | } 80 | -------------------------------------------------------------------------------- /src/utils/sort.ts: -------------------------------------------------------------------------------- 1 | import { ParsedTokenAccount } from "../store/transferSlice"; 2 | 3 | export const sortParsedTokenAccounts = ( 4 | a: ParsedTokenAccount, 5 | b: ParsedTokenAccount 6 | ) => 7 | a.isNativeAsset && !b.isNativeAsset 8 | ? -1 9 | : !a.isNativeAsset && b.isNativeAsset 10 | ? 1 11 | : a.symbol && b.symbol 12 | ? a.symbol.localeCompare(b.symbol) 13 | : a.symbol 14 | ? -1 15 | : b.symbol 16 | ? 1 17 | : 0; 18 | -------------------------------------------------------------------------------- /src/utils/sui.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcProvider } from "@mysten/sui.js"; 2 | import { SUI_CONNECTION } from "./consts"; 3 | 4 | export const getSuiProvider = () => new JsonRpcProvider(SUI_CONNECTION); 5 | -------------------------------------------------------------------------------- /src/utils/xpla.ts: -------------------------------------------------------------------------------- 1 | import { LCDClient, isTxError } from "@xpla/xpla.js"; 2 | import { ConnectedWallet, TxResult } from "@xpla/wallet-provider"; 3 | import axios from "axios"; 4 | import { 5 | XPLA_GAS_PRICES_URL, 6 | XPLA_LCD_CLIENT_CONFIG, 7 | XPLA_NATIVE_DENOM, 8 | } from "./consts"; 9 | import { cosmos, isNativeDenomXpla } from "@certusone/wormhole-sdk"; 10 | 11 | export const NATIVE_XPLA_DECIMALS = 18; 12 | 13 | export async function waitForXplaExecution(transaction: TxResult) { 14 | const lcd = new LCDClient(XPLA_LCD_CLIENT_CONFIG); 15 | let info; 16 | while (!info) { 17 | await new Promise((resolve) => setTimeout(resolve, 1000)); 18 | try { 19 | info = await lcd.tx.txInfo(transaction.result.txhash); 20 | } catch (e) { 21 | console.error(e); 22 | } 23 | } 24 | if (isTxError(info)) { 25 | throw new Error( 26 | `Tx ${transaction.result.txhash}: error code ${info.code}: ${info.raw_log}` 27 | ); 28 | } 29 | return info; 30 | } 31 | 32 | export async function postWithFeesXpla( 33 | wallet: ConnectedWallet, 34 | msgs: any[], 35 | memo: string 36 | ) { 37 | // don't try/catch, let errors propagate 38 | const lcd = new LCDClient(XPLA_LCD_CLIENT_CONFIG); 39 | //Thus, we are going to pull it directly from the current FCD. 40 | const gasPrices = await axios 41 | .get(XPLA_GAS_PRICES_URL) 42 | .then((result) => result.data); 43 | 44 | const account = await lcd.auth.accountInfo(wallet.walletAddress); 45 | 46 | const feeDenoms = [XPLA_NATIVE_DENOM]; 47 | 48 | const feeEstimate = await lcd.tx.estimateFee( 49 | [ 50 | { 51 | sequenceNumber: account.getSequenceNumber(), 52 | publicKey: account.getPublicKey(), 53 | }, 54 | ], 55 | { 56 | msgs: [...msgs], 57 | memo, 58 | feeDenoms, 59 | gasPrices, 60 | } 61 | ); 62 | 63 | const result = await wallet.post({ 64 | msgs: [...msgs], 65 | memo, 66 | feeDenoms, 67 | gasPrices, 68 | fee: feeEstimate, 69 | }); 70 | 71 | return result; 72 | } 73 | 74 | export const isValidXplaAddress = (address: string) => { 75 | if (isNativeDenomXpla(address)) { 76 | return true; 77 | } 78 | try { 79 | const startsWithXpla = address && address.startsWith("xpla"); 80 | const isParseable = cosmos.canonicalAddress(address); 81 | const isLengthOk = isParseable.length === 32; 82 | return !!(startsWithXpla && isParseable && isLengthOk); 83 | } catch (error) { 84 | return false; 85 | } 86 | }; 87 | 88 | export const formatNativeDenom = (denom: string) => 89 | denom === XPLA_NATIVE_DENOM ? "XPLA" : ""; 90 | 91 | export const XPLA_NATIVE_TOKEN_ICON = 92 | "https://assets.xpla.io/icon/svg/XPLA.svg"; 93 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "downlevelIteration": true, 21 | "noEmit": true, 22 | "jsx": "react-jsx" 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } 28 | --------------------------------------------------------------------------------