├── src ├── vite-env.d.ts ├── config │ ├── index.ts │ ├── connectors │ │ ├── index.ts │ │ └── NetworkConnector.ts │ └── constants │ │ ├── rpc.ts │ │ ├── chainId.ts │ │ └── wallets.ts ├── main.tsx ├── utils │ ├── isAddress.ts │ └── index.ts ├── index.css ├── hooks │ ├── useActiveWeb3React.ts │ ├── useContract.ts │ ├── useInactiveListener.ts │ └── useEagerConnect.ts ├── page │ └── Home.tsx ├── App.tsx ├── favicon.svg ├── components │ └── Web3ReactManager │ │ └── index.tsx └── logo.svg ├── tsconfig.node.json ├── README.md ├── vite.config.ts ├── .gitignore ├── index.html ├── tsconfig.json └── package.json /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | export const NetworkContextName = "NETWORK"; 2 | -------------------------------------------------------------------------------- /src/config/connectors/index.ts: -------------------------------------------------------------------------------- 1 | export const connectorLocalStorageKey = "connectorId"; 2 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import "./index.css"; 5 | 6 | ReactDOM.render(, document.getElementById("root")); 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.主要技术栈是 React17+Vite+ethers+web3-react 4 | 5 | 2.config文件夹下是配置文件 主要配置的是连接钱包的参数与常量 6 | 7 | 3.hooks里主要是合约调用的逻辑 8 | 9 | 4.components里的Web3ReactManager是钱包连接逻辑 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/utils/isAddress.ts: -------------------------------------------------------------------------------- 1 | import { getAddress } from "@ethersproject/address"; 2 | 3 | export function isAddress(value: any): string | false { 4 | try { 5 | return getAddress(value); 6 | } catch { 7 | return false; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import * as path from "path"; 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | resolve: { 8 | alias: { 9 | "@": path.join(__dirname, "src"), 10 | }, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcSigner, Web3Provider } from "@ethersproject/providers"; 2 | 3 | // account is not optional 4 | export function getSigner(library: Web3Provider, account: string): JsonRpcSigner { 5 | return library.getSigner(account).connectUnchecked(); 6 | } 7 | 8 | // account is optional 9 | export function getProviderOrSigner(library: Web3Provider, account?: string): Web3Provider | JsonRpcSigner { 10 | return account ? getSigner(library, account) : library; 11 | } 12 | -------------------------------------------------------------------------------- /src/hooks/useActiveWeb3React.ts: -------------------------------------------------------------------------------- 1 | import { Web3Provider } from "@ethersproject/providers"; 2 | import { useWeb3React } from "@web3-react/core"; 3 | import { Web3ReactContextInterface } from "@web3-react/core/dist/types"; 4 | import { NetworkContextName } from "@/config/index"; 5 | 6 | export function useActiveWeb3React(): Web3ReactContextInterface { 7 | const context = useWeb3React(); 8 | const contextNetwork = useWeb3React(NetworkContextName); 9 | 10 | return context.active ? { ...context } : { ...contextNetwork }; 11 | } 12 | -------------------------------------------------------------------------------- /src/page/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useEffect } from "react"; 3 | import { injected } from "@/config/constants/wallets"; 4 | import { useActiveWeb3React } from "@/hooks/useActiveWeb3React"; 5 | import { connectorLocalStorageKey } from "@/config/connectors/index"; 6 | 7 | export default function Home() { 8 | const { account, chainId, error, activate } = useActiveWeb3React(); 9 | 10 | useEffect(() => { 11 | console.log(window.localStorage.getItem(connectorLocalStorageKey)); 12 | 13 | activate(injected, undefined, true).catch(() => { 14 | activate(injected); 15 | }); 16 | }, []); 17 | return
{account}
; 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "baseUrl": "./", 19 | "paths": { 20 | "@/*": ["src/*"] 21 | } 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { HashRouter as Router, Switch, Route } from "react-router-dom"; 2 | import { Web3Provider } from "@ethersproject/providers"; 3 | import { Web3ReactProvider, createWeb3ReactRoot } from "@web3-react/core"; 4 | import Web3ReactManager from "@/components/Web3ReactManager/index"; 5 | 6 | import { NetworkContextName } from "@/config"; 7 | 8 | import Home from "@/page/Home"; 9 | 10 | export function getLibrary(provider: any): Web3Provider { 11 | const library = new Web3Provider(provider); 12 | library.pollingInterval = 15000; 13 | return library; 14 | } 15 | const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName); 16 | 17 | function App() { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | export default App; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-react-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@ethersproject/address": "^5.6.1", 12 | "@ethersproject/constants": "^5.6.1", 13 | "@ethersproject/contracts": "^5.6.2", 14 | "@ethersproject/providers": "^5.6.8", 15 | "@web3-react/abstract-connector": "^6.0.7", 16 | "@web3-react/core": "^6.1.9", 17 | "@web3-react/injected-connector": "^6.0.7", 18 | "ethers": "^5.6.9", 19 | "events": "^3.3.0", 20 | "react": "^17.0.2", 21 | "react-dom": "^17.0.2", 22 | "react-router-dom": "5", 23 | "web3modal": "^1.9.8" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^18.0.1", 27 | "@types/react": "^17.0.20", 28 | "@types/react-dom": "^17.0.9", 29 | "@types/react-router-dom": "^5.3.3", 30 | "@vitejs/plugin-react": "^1.3.0", 31 | "typescript": "^4.6.3", 32 | "vite": "^2.9.9" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/hooks/useContract.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { useActiveWeb3React } from "@/hooks/useActiveWeb3React"; 3 | import { JsonRpcSigner, Web3Provider } from "@ethersproject/providers"; 4 | import { AddressZero } from "@ethersproject/constants"; 5 | import { isAddress } from "@/utils/isAddress"; 6 | import { getProviderOrSigner } from "@/utils"; 7 | import { Contract } from "@ethersproject/contracts"; 8 | 9 | // export const useExampleContract = (address: string, withSignerIfPossible = true) => { 10 | // return useContract(address, ContractAbi, withSignerIfPossible); 11 | // }; 12 | 13 | // Multiple chains 14 | 15 | // export const useBatchTransfer = (withSignerIfPossible?: boolean) => { 16 | // const { chainId } = useActiveWeb3React(); 17 | // return useContract(getContractAddress(chainId), ContractAbi, withSignerIfPossible); 18 | // }; 19 | 20 | export function useContract(address: string | undefined, ABI: any, withSignerIfPossible = true): Contract | null { 21 | const { library, account } = useActiveWeb3React(); 22 | return useMemo(() => { 23 | if (!address || address === AddressZero || !ABI || !library) return null; 24 | try { 25 | return getContract(address, ABI, library, withSignerIfPossible && account ? account : undefined); 26 | } catch (error) { 27 | console.error("Failed to get contract", error); 28 | return null; 29 | } 30 | }, [address, ABI, library, withSignerIfPossible, account]); 31 | } 32 | 33 | export function getContract(address: string, ABI: any, library: Web3Provider, account?: string): Contract { 34 | if (!isAddress(address) || address === AddressZero) { 35 | throw Error(`Invalid 'address' parameter '${address}'.`); 36 | } 37 | return new Contract(address, ABI, getProviderOrSigner(library, account)); 38 | } 39 | -------------------------------------------------------------------------------- /src/hooks/useInactiveListener.ts: -------------------------------------------------------------------------------- 1 | import { useWeb3React as useWeb3ReactCore } from "@web3-react/core"; 2 | import { useEffect } from "react"; 3 | 4 | import { injected } from "@/config/constants/wallets"; 5 | 6 | /** 7 | * Use for network and injected - logs user in 8 | * and out after checking what network theyre on 9 | */ 10 | function useInactiveListener(suppress = false) { 11 | const { active, error, activate } = useWeb3ReactCore(); // specifically using useWeb3React because of what this hook does 12 | 13 | useEffect(() => { 14 | const { ethereum } = window; 15 | 16 | if (ethereum && ethereum.on && !active && !error && !suppress) { 17 | const handleChainChanged = () => { 18 | // eat errors 19 | activate(injected, undefined, true).catch((error) => { 20 | console.error("Failed to activate after chain changed", error); 21 | }); 22 | }; 23 | 24 | const handleAccountsChanged = (accounts: string[]) => { 25 | if (accounts.length > 0) { 26 | // eat errors 27 | activate(injected, undefined, true).catch((error) => { 28 | console.error("Failed to activate after accounts changed", error); 29 | }); 30 | } 31 | }; 32 | 33 | ethereum.on("chainChanged", handleChainChanged); 34 | ethereum.on("accountsChanged", handleAccountsChanged); 35 | 36 | return () => { 37 | if (ethereum.removeListener) { 38 | ethereum.removeListener("chainChanged", handleChainChanged); 39 | ethereum.removeListener("accountsChanged", handleAccountsChanged); 40 | } 41 | }; 42 | } 43 | return undefined; 44 | }, [active, error, suppress, activate]); 45 | } 46 | 47 | export default useInactiveListener; 48 | -------------------------------------------------------------------------------- /src/hooks/useEagerConnect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useWeb3React as useWeb3ReactCore } from "@web3-react/core"; 3 | import { injected } from "@/config/constants/wallets"; 4 | import { isMobile } from "web3modal"; 5 | import { connectorLocalStorageKey } from "@/config/connectors/index"; 6 | export function useEagerConnect() { 7 | const { activate, active } = useWeb3ReactCore(); // specifically using useWeb3ReactCore because of what this hook does 8 | const [tried, setTried] = useState(false); 9 | 10 | useEffect(() => { 11 | injected.isAuthorized().then((isAuthorized: any) => { 12 | const hasSignedIn = window.localStorage.getItem(connectorLocalStorageKey); 13 | if (isAuthorized && hasSignedIn) { 14 | activate(injected, undefined, true) 15 | // .then(() => window.ethereum.removeAllListeners(['networkChanged'])) 16 | .catch(() => { 17 | setTried(true); 18 | }); 19 | // @ts-ignore TYPE NEEDS FIXING 20 | window.ethereum.removeAllListeners(["networkChanged"]); 21 | } else { 22 | if (isMobile() && window.ethereum && hasSignedIn) { 23 | activate(injected, undefined, true) 24 | // .then(() => window.ethereum.removeAllListeners(['networkChanged'])) 25 | .catch(() => { 26 | setTried(true); 27 | }); 28 | // @ts-ignore TYPE NEEDS FIXING 29 | window.ethereum.removeAllListeners(["networkChanged"]); 30 | } else { 31 | setTried(true); 32 | } 33 | } 34 | }); 35 | }, [activate]); 36 | 37 | useEffect(() => { 38 | if (active) { 39 | setTried(true); 40 | } 41 | }, [active]); 42 | 43 | return tried; 44 | } 45 | 46 | export default useEagerConnect; 47 | -------------------------------------------------------------------------------- /src/components/Web3ReactManager/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useWeb3React } from "@web3-react/core"; 3 | import { network } from "@/config/constants/wallets"; 4 | import { NetworkContextName } from "@/config/index"; 5 | 6 | import useEagerConnect from "@/hooks/useEagerConnect"; 7 | import useInactiveListener from "@/hooks/useInactiveListener"; 8 | 9 | export default function Web3ReactManager({ children }: { children: JSX.Element }) { 10 | const { active } = useWeb3React(); 11 | const { active: networkActive, error: networkError, activate: activateNetwork } = useWeb3React(NetworkContextName); 12 | 13 | // try to eagerly connect to an injected provider, if it exists and has granted access already 14 | const triedEager = useEagerConnect(); 15 | 16 | // after eagerly trying injected, if the network connect ever isn't active or in an error state, activate itd 17 | useEffect(() => { 18 | if (triedEager && !networkActive && !networkError && !active) { 19 | activateNetwork(network); 20 | } 21 | }, [triedEager, networkActive, networkError, activateNetwork, active]); 22 | 23 | // when there's no account connected, react to logins (broadly speaking) on the injected provider, if it exists 24 | useInactiveListener(!triedEager); 25 | 26 | // handle delayed loader state 27 | const [showLoader, setShowLoader] = useState(false); 28 | useEffect(() => { 29 | const timeout = setTimeout(() => { 30 | setShowLoader(true); 31 | }, 600); 32 | 33 | return () => { 34 | clearTimeout(timeout); 35 | }; 36 | }, []); 37 | 38 | // on page load, do nothing until we've tried to connect to the injected connector 39 | if (!triedEager) { 40 | return null; 41 | } 42 | 43 | // if the account context isn't active, and there's an error on the network context, it's an irrecoverable error 44 | if (!active && networkError) { 45 | return
unknownError
; 46 | } 47 | 48 | // if neither context is active, spin 49 | if (!active && !networkActive) { 50 | return showLoader ?
Loader
: null; 51 | } 52 | 53 | return children; 54 | } 55 | -------------------------------------------------------------------------------- /src/config/constants/rpc.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from "./chainId"; 2 | 3 | const RPC = { 4 | [ChainId.ETHEREUM]: "https://api.sushirelay.com/v1", 5 | // [ChainId.ETHEREUM]: 'https://eth-mainnet.alchemyapi.io/v2/HNQXSfiUcPjfpDBQaWYXjqlhTr1cEY9c', 6 | // [ChainId.MAINNET]: 'https://eth-mainnet.alchemyapi.io/v2/q1gSNoSMEzJms47Qn93f9-9Xg5clkmEC', 7 | // [ChainId.ROPSTEN]: "https://eth-ropsten.alchemyapi.io/v2/cidKix2Xr-snU3f6f6Zjq_rYdalKKHmW", 8 | [ChainId.RINKEBY]: "https://eth-rinkeby.alchemyapi.io/v2/XVLwDlhGP6ApBXFz_lfv0aZ6VmurWhYD", 9 | // [ChainId.GÖRLI]: "https://eth-goerli.alchemyapi.io/v2/Dkk5d02QjttYEoGmhZnJG37rKt8Yl3Im", 10 | // [ChainId.KOVAN]: "https://eth-kovan.alchemyapi.io/v2/wnW2uNdwqMPes-BCf9lTWb9UHL9QP2dp", 11 | // [ChainId.FANTOM]: "https://rpcapi.fantom.network", 12 | // [ChainId.FANTOM_TESTNET]: "https://rpc.testnet.fantom.network", 13 | // [ChainId.MATIC]: "https://polygon-rpc.com/", 14 | // [ChainId.MATIC_TESTNET]: "https://rpc-mumbai.matic.today", 15 | // [ChainId.XDAI]: "https://rpc.xdaichain.com", 16 | // [ChainId.BSC]: "https://bsc-dataseed.binance.org/", 17 | // [ChainId.BSC_TESTNET]: "https://data-seed-prebsc-2-s3.binance.org:8545", 18 | // [ChainId.MOONBEAM_TESTNET]: "https://rpc.testnet.moonbeam.network", 19 | // [ChainId.AVALANCHE]: "https://api.avax.network/ext/bc/C/rpc", 20 | // [ChainId.AVALANCHE_TESTNET]: "https://api.avax-test.network/ext/bc/C/rpc", 21 | // [ChainId.HECO]: "https://http-mainnet.hecochain.com", 22 | // [ChainId.HECO_TESTNET]: "https://http-testnet.hecochain.com", 23 | // [ChainId.HARMONY]: "https://api.harmony.one", 24 | // [ChainId.HARMONY_TESTNET]: "https://api.s0.b.hmny.io", 25 | // [ChainId.OKEX]: "https://exchainrpc.okex.org", 26 | // [ChainId.OKEX_TESTNET]: "https://exchaintestrpc.okex.org", 27 | // [ChainId.ARBITRUM]: "https://arb1.arbitrum.io/rpc", 28 | // [ChainId.PALM]: "https://palm-mainnet.infura.io/v3/da5fbfafcca14b109e2665290681e267", 29 | // [ChainId.FUSE]: "https://rpc.fuse.io", 30 | // [ChainId.CELO]: "https://forno.celo.org", 31 | // [ChainId.MOONRIVER]: "https://rpc.moonriver.moonbeam.network", 32 | // [ChainId.TELOS]: "https://mainnet.telos.net/evm", 33 | // [ChainId.MOONBEAM]: "https://rpc.api.moonbeam.network", 34 | }; 35 | 36 | export default RPC; 37 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/config/constants/chainId.ts: -------------------------------------------------------------------------------- 1 | // export const ChainId = { 2 | // 1: "ETHEREUM", 3 | // 3: "ROPSTEN", 4 | // 4: "RINKEBY", 5 | // 5: "GÖRLI", 6 | // 40: "TELOS", 7 | // 42: "KOVAN", 8 | // 56: "BSC", 9 | // 65: "OKEX_TESTNET", 10 | // 66: "OKEX", 11 | // 97: "BSC_TESTNET", 12 | // 100: "XDAI", 13 | // 122: "FUSE", 14 | // 128: "HECO", 15 | // 137: "MATIC", 16 | // 250: "FANTOM", 17 | // 256: "HECO_TESTNET", 18 | // 1284: "MOONBEAM", 19 | // 1285: "MOONRIVER", 20 | // 1287: "MOONBEAM_TESTNET", 21 | // 4002: "FANTOM_TESTNET", 22 | // 31337: "HARDHAT", 23 | // 42161: "ARBITRUM", 24 | // 42220: "CELO", 25 | // 43113: "AVALANCHE_TESTNET", 26 | // 43114: "AVALANCHE", 27 | // 80001: "MATIC_TESTNET", 28 | // 1666600000: "HARMONY", 29 | // 1666700000: "HARMONY_TESTNET", 30 | // 11297108099: "PALM_TESTNET", 31 | // 11297108109: "PALM", 32 | // 79377087078960: "ARBITRUM_TESTNET", 33 | // ARBITRUM: 42161, 34 | // ARBITRUM_TESTNET: 79377087078960, 35 | // AVALANCHE: 43114, 36 | // AVALANCHE_TESTNET: 43113, 37 | // BSC: 56, 38 | // BSC_TESTNET: 97, 39 | // CELO: 42220, 40 | // ETHEREUM: 1, 41 | // FANTOM: 250, 42 | // FANTOM_TESTNET: 4002, 43 | // FUSE: 122, 44 | // GÖRLI: 5, 45 | // HARDHAT: 31337, 46 | // HARMONY: 1666600000, 47 | // HARMONY_TESTNET: 1666700000, 48 | // HECO: 128, 49 | // HECO_TESTNET: 256, 50 | // KOVAN: 42, 51 | // MATIC: 137, 52 | // MATIC_TESTNET: 80001, 53 | // MOONBEAM: 1284, 54 | // MOONBEAM_TESTNET: 1287, 55 | // MOONRIVER: 1285, 56 | // OKEX: 66, 57 | // OKEX_TESTNET: 65, 58 | // PALM: 11297108109, 59 | // PALM_TESTNET: 11297108099, 60 | // RINKEBY: 4, 61 | // ROPSTEN: 3, 62 | // TELOS: 40, 63 | // XDAI: 100, 64 | // }; 65 | 66 | export enum ChainId { 67 | ETHEREUM = 1, 68 | // ROPSTEN = 3, 69 | RINKEBY = 4, 70 | // GÖRLI = 5, 71 | // KOVAN = 42, 72 | // MATIC = 137, 73 | // MATIC_TESTNET = 80001, 74 | // FANTOM = 250, 75 | // FANTOM_TESTNET = 4002, 76 | // XDAI = 100, 77 | // BSC = 56, 78 | // BSC_TESTNET = 97, 79 | // ARBITRUM = 42161, 80 | // ARBITRUM_TESTNET = 79377087078960, 81 | // MOONBEAM_TESTNET = 1287, 82 | // AVALANCHE = 43114, 83 | // AVALANCHE_TESTNET = 43113, 84 | // HECO = 128, 85 | // HECO_TESTNET = 256, 86 | // HARMONY = 1666600000, 87 | // HARMONY_TESTNET = 1666700000, 88 | // OKEX = 66, 89 | // OKEX_TESTNET = 65, 90 | // CELO = 42220, 91 | // PALM = 11297108109, 92 | // PALM_TESTNET = 11297108099, 93 | // MOONRIVER = 1285, 94 | // FUSE = 122, 95 | // TELOS = 40, 96 | // HARDHAT = 31337, 97 | // MOONBEAM = 1284, 98 | } 99 | -------------------------------------------------------------------------------- /src/config/connectors/NetworkConnector.ts: -------------------------------------------------------------------------------- 1 | import { ConnectorUpdate } from "@web3-react/types"; 2 | import { AbstractConnector } from "@web3-react/abstract-connector"; 3 | import invariant from "tiny-invariant"; 4 | 5 | interface NetworkConnectorArguments { 6 | urls: { [chainId: number]: string }; 7 | defaultChainId?: number; 8 | } 9 | 10 | // taken from ethers.js, compatible interface with web3 provider 11 | type AsyncSendable = { 12 | isMetaMask?: boolean; 13 | host?: string; 14 | path?: string; 15 | sendAsync?: (request: any, callback: (error: any, response: any) => void) => void; 16 | send?: (request: any, callback: (error: any, response: any) => void) => void; 17 | }; 18 | 19 | class RequestError extends Error { 20 | constructor(message: string, public code: number, public data?: unknown) { 21 | super(message); 22 | } 23 | } 24 | 25 | interface BatchItem { 26 | request: { jsonrpc: "2.0"; id: number; method: string; params: unknown }; 27 | resolve: (result: any) => void; 28 | reject: (error: Error) => void; 29 | } 30 | 31 | class MiniRpcProvider implements AsyncSendable { 32 | public readonly isMetaMask: false = false; 33 | 34 | public readonly chainId: number; 35 | 36 | public readonly url: string; 37 | 38 | public readonly host: string; 39 | 40 | public readonly path: string; 41 | 42 | public readonly batchWaitTimeMs: number; 43 | 44 | private nextId = 1; 45 | 46 | private batchTimeoutId: ReturnType | null = null; 47 | 48 | private batch: BatchItem[] = []; 49 | 50 | constructor(chainId: number, url: string, batchWaitTimeMs?: number) { 51 | this.chainId = chainId; 52 | this.url = url; 53 | const parsed = new URL(url); 54 | this.host = parsed.host; 55 | this.path = parsed.pathname; // how long to wait to batch calls 56 | this.batchWaitTimeMs = batchWaitTimeMs ?? 50; 57 | } 58 | 59 | public readonly clearBatch = async () => { 60 | // console.info('Clearing batch', this.batch) 61 | const { batch } = this; 62 | this.batch = []; 63 | this.batchTimeoutId = null; 64 | let response: Response; 65 | try { 66 | response = await fetch(this.url, { 67 | method: "POST", 68 | headers: { "content-type": "application/json", accept: "application/json" }, 69 | body: JSON.stringify(batch.map((item) => item.request)), 70 | }); 71 | } catch (error) { 72 | batch.forEach(({ reject }) => reject(new Error("Failed to send batch call"))); 73 | return; 74 | } 75 | 76 | if (!response.ok) { 77 | batch.forEach(({ reject }) => 78 | reject(new RequestError(`${response.status}: ${response.statusText}`, -32000)), 79 | ); 80 | return; 81 | } 82 | 83 | let json; 84 | try { 85 | json = await response.json(); 86 | } catch (error) { 87 | batch.forEach(({ reject }) => reject(new Error("Failed to parse JSON response"))); 88 | return; 89 | } 90 | const byKey = batch.reduce<{ [id: number]: BatchItem }>((memo, current) => { 91 | memo[current.request.id] = current; 92 | return memo; 93 | }, {}); // eslint-disable-next-line no-restricted-syntax 94 | for (const result of json) { 95 | const { 96 | resolve, 97 | reject, 98 | request: { method }, 99 | } = byKey[result.id]; 100 | if (resolve) { 101 | if ("error" in result) { 102 | reject(new RequestError(result?.error?.message, result?.error?.code, result?.error?.data)); 103 | } else if ("result" in result) { 104 | resolve(result.result); 105 | } else { 106 | reject( 107 | new RequestError(`Received unexpected JSON-RPC response to ${method} request.`, -32000, result), 108 | ); 109 | } 110 | } 111 | } 112 | }; 113 | 114 | public readonly sendAsync = ( 115 | request: { jsonrpc: "2.0"; id: number | string | null; method: string; params?: any }, 116 | callback: (error: any, response: any) => void, 117 | ): void => { 118 | this.request(request.method, request.params) 119 | .then((result) => callback(null, { jsonrpc: "2.0", id: request.id, result })) 120 | .catch((error) => callback(error, null)); 121 | }; 122 | 123 | public readonly request = async ( 124 | method: string | { method: string; params: unknown[] }, 125 | params?: any, 126 | ): Promise => { 127 | if (typeof method !== "string") { 128 | return this.request(method.method, method.params); 129 | } 130 | if (method === "eth_chainId") { 131 | return `0x${this.chainId.toString(16)}`; 132 | } 133 | const promise = new Promise((resolve, reject) => { 134 | this.batch.push({ 135 | request: { 136 | jsonrpc: "2.0", 137 | id: this.nextId++, 138 | method, 139 | params, 140 | }, 141 | resolve, 142 | reject, 143 | }); 144 | }); 145 | this.batchTimeoutId = this.batchTimeoutId ?? setTimeout(this.clearBatch, this.batchWaitTimeMs); 146 | return promise; 147 | }; 148 | } 149 | 150 | export class NetworkConnector extends AbstractConnector { 151 | private readonly providers: { [chainId: number]: MiniRpcProvider }; 152 | 153 | private currentChainId: number; 154 | 155 | constructor({ urls, defaultChainId }: NetworkConnectorArguments) { 156 | invariant( 157 | defaultChainId || Object.keys(urls).length === 1, 158 | "defaultChainId is a required argument with >1 url", 159 | ); 160 | super({ supportedChainIds: Object.keys(urls).map((k): number => Number(k)) }); 161 | 162 | this.currentChainId = defaultChainId || Number(Object.keys(urls)[0]); 163 | this.providers = Object.keys(urls).reduce<{ [chainId: number]: MiniRpcProvider }>((accumulator, chainId) => { 164 | accumulator[Number(chainId)] = new MiniRpcProvider(Number(chainId), urls[Number(chainId)]); 165 | return accumulator; 166 | }, {}); 167 | } 168 | 169 | public get provider(): MiniRpcProvider { 170 | return this.providers[this.currentChainId]; 171 | } 172 | 173 | public async activate(): Promise { 174 | return { provider: this.providers[this.currentChainId], chainId: this.currentChainId, account: null }; 175 | } 176 | 177 | public async getProvider(): Promise { 178 | return this.providers[this.currentChainId]; 179 | } 180 | 181 | public async getChainId(): Promise { 182 | return this.currentChainId; 183 | } 184 | 185 | public async getAccount(): Promise { 186 | return null; 187 | } 188 | 189 | public deactivate() { 190 | return null; 191 | } 192 | } 193 | 194 | export default NetworkConnector; 195 | -------------------------------------------------------------------------------- /src/config/constants/wallets.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from "./chainId"; 2 | import { AbstractConnector } from "@web3-react/abstract-connector"; 3 | import { InjectedConnector } from "@web3-react/injected-connector"; 4 | import { NetworkConnector } from "@/config/connectors/NetworkConnector"; 5 | import RPC from "./rpc"; 6 | 7 | const supportedChainIds = Object.values(ChainId) as number[]; 8 | 9 | export const network = new NetworkConnector({ 10 | defaultChainId: 4, 11 | urls: RPC, 12 | }); 13 | 14 | export const injected = new InjectedConnector({ 15 | supportedChainIds, 16 | }); 17 | 18 | export interface WalletInfo { 19 | connector?: (() => Promise) | AbstractConnector; 20 | name: string; 21 | iconName: string; 22 | description: string; 23 | href: string | null; 24 | color: string; 25 | primary?: true; 26 | mobile?: true; 27 | mobileOnly?: true; 28 | } 29 | 30 | export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = { 31 | INJECTED: { 32 | connector: injected, 33 | name: "Injected", 34 | iconName: "injected.svg", 35 | description: "Injected web3 provider.", 36 | href: null, 37 | color: "#010101", 38 | primary: true, 39 | }, 40 | METAMASK: { 41 | connector: injected, 42 | name: "MetaMask", 43 | iconName: "metamask.png", 44 | description: "Easy-to-use browser extension.", 45 | href: null, 46 | color: "#E8831D", 47 | }, 48 | METAMASK_MOBILE: { 49 | name: "MetaMask", 50 | iconName: "metamask.png", 51 | description: "Open in MetaMask app.", 52 | href: "https://metamask.app.link/dapp/app.sushi.com", 53 | color: "#E8831D", 54 | mobile: true, 55 | mobileOnly: true, 56 | }, 57 | // WALLET_CONNECT: { 58 | // connector: async () => { 59 | // const WalletConnectConnector = (await import("@web3-react/walletconnect-connector")).WalletConnectConnector; 60 | // return new WalletConnectConnector({ 61 | // rpc: RPC, 62 | // bridge: "https://bridge.walletconnect.org", 63 | // qrcode: true, 64 | // supportedChainIds, 65 | // }); 66 | // }, 67 | // name: "WalletConnect", 68 | // iconName: "wallet-connect.svg", 69 | // description: "Connect to Trust Wallet, Rainbow Wallet and more...", 70 | // href: null, 71 | // color: "#4196FC", 72 | // mobile: true, 73 | // }, 74 | // KEYSTONE: { 75 | // connector: async () => { 76 | // const KeystoneConnector = (await import("@keystonehq/keystone-connector")).KeystoneConnector; 77 | // return new KeystoneConnector({ 78 | // chainId: 1, 79 | // url: RPC[ChainId.ETHEREUM], 80 | // }); 81 | // }, 82 | // name: "Keystone", 83 | // iconName: "keystone.png", 84 | // description: "Connect to Keystone hardware wallet.", 85 | // href: null, 86 | // color: "#4196FC", 87 | // mobile: true, 88 | // }, 89 | // LATTICE: { 90 | // connector: async () => { 91 | // const LatticeConnector = (await import("@web3-react/lattice-connector")).LatticeConnector; 92 | // return new LatticeConnector({ 93 | // chainId: 1, 94 | // url: RPC[ChainId.ETHEREUM], 95 | // appName: "SushiSwap", 96 | // }); 97 | // }, 98 | // name: "Lattice", 99 | // iconName: "lattice.png", 100 | // description: "Connect to GridPlus Wallet.", 101 | // href: null, 102 | // color: "#40a9ff", 103 | // mobile: true, 104 | // }, 105 | // WALLET_LINK: { 106 | // connector: async () => { 107 | // const WalletLinkConnector = (await import("@web3-react/walletlink-connector")).WalletLinkConnector; 108 | // return new WalletLinkConnector({ 109 | // url: RPC[ChainId.ETHEREUM], 110 | // appName: "SushiSwap", 111 | // appLogoUrl: "https://raw.githubusercontent.com/sushiswap/art/master/sushi/logo-256x256.png", 112 | // darkMode: true, 113 | // }); 114 | // }, 115 | // name: "Coinbase Wallet", 116 | // iconName: "coinbase.svg", 117 | // description: "Use Coinbase Wallet app on mobile device", 118 | // href: null, 119 | // color: "#315CF5", 120 | // }, 121 | // COINBASE_LINK: { 122 | // name: "Open in Coinbase Wallet", 123 | // iconName: "coinbase.svg", 124 | // description: "Open in Coinbase Wallet app.", 125 | // href: "https://go.cb-w.com", 126 | // color: "#315CF5", 127 | // mobile: true, 128 | // mobileOnly: true, 129 | // }, 130 | // FORTMATIC: { 131 | // connector: async () => { 132 | // const FortmaticConnector = (await import("@web3-react/fortmatic-connector")).FortmaticConnector; 133 | // return new FortmaticConnector({ 134 | // apiKey: process.env.NEXT_PUBLIC_FORTMATIC_API_KEY ?? "", 135 | // chainId: 1, 136 | // }); 137 | // }, 138 | // name: "Fortmatic", 139 | // iconName: "fortmatic.png", 140 | // description: "Login using Fortmatic hosted wallet", 141 | // href: null, 142 | // color: "#6748FF", 143 | // mobile: true, 144 | // }, 145 | // Portis: { 146 | // connector: async () => { 147 | // const PortisConnector = (await import("@web3-react/portis-connector")).PortisConnector; 148 | // return new PortisConnector({ 149 | // dAppId: process.env.NEXT_PUBLIC_PORTIS_ID ?? "", 150 | // networks: [1], 151 | // }); 152 | // }, 153 | // name: "Portis", 154 | // iconName: "portis.png", 155 | // description: "Login using Portis hosted wallet", 156 | // href: null, 157 | // color: "#4A6C9B", 158 | // mobile: true, 159 | // }, 160 | // Torus: { 161 | // connector: async () => { 162 | // const TorusConnector = (await import("@web3-react/torus-connector")).TorusConnector; 163 | // return new TorusConnector({ 164 | // chainId: 1, 165 | // }); 166 | // }, 167 | // name: "Torus", 168 | // iconName: "torus.png", 169 | // description: "Login using Torus hosted wallet", 170 | // href: null, 171 | // color: "#315CF5", 172 | // mobile: true, 173 | // }, 174 | // Binance: { 175 | // connector: async () => { 176 | // const BscConnector = (await import("@binance-chain/bsc-connector")).BscConnector; 177 | // return new BscConnector({ 178 | // supportedChainIds: [56], 179 | // }); 180 | // }, 181 | // name: "Binance", 182 | // iconName: "bsc.jpg", 183 | // description: "Login using Binance hosted wallet", 184 | // href: null, 185 | // color: "#F0B90B", 186 | // mobile: true, 187 | // }, 188 | // Clover: { 189 | // connector: async () => { 190 | // const CloverConnector = (await import("@clover-network/clover-connector")).CloverConnector; 191 | // return new CloverConnector({ 192 | // supportedChainIds: [1], 193 | // }); 194 | // }, 195 | // name: "Clover", 196 | // iconName: "clover.svg", 197 | // description: "Login using Clover hosted wallet", 198 | // href: null, 199 | // color: "#269964", 200 | // }, 201 | }; 202 | --------------------------------------------------------------------------------