From a3c6b1d6b67dca01093fc88de3a4004866577830 Mon Sep 17 00:00:00 2001 From: Taneeya <2240351540@qq.com> Date: Thu, 31 Aug 2023 15:19:45 +0800 Subject: [PATCH] add CLV wallet connector --- .gitignore | 1 + example/components/Card.tsx | 3 +- example/components/ConnectWithSelect.tsx | 3 +- example/components/ProviderExample.tsx | 5 +- .../connectorCards/ClvWalletCard.tsx | 40 ++++ example/connectors/clvWallet.ts | 4 + example/pages/index.tsx | 2 + example/utils.ts | 2 + packages/clvWallet/README.md | 1 + packages/clvWallet/package.json | 32 +++ packages/clvWallet/src/index.spec.ts | 66 ++++++ packages/clvWallet/src/index.ts | 202 ++++++++++++++++++ packages/clvWallet/tsconfig.json | 7 + 13 files changed, 365 insertions(+), 3 deletions(-) create mode 100644 example/components/connectorCards/ClvWalletCard.tsx create mode 100644 example/connectors/clvWallet.ts create mode 100644 packages/clvWallet/README.md create mode 100644 packages/clvWallet/package.json create mode 100644 packages/clvWallet/src/index.spec.ts create mode 100644 packages/clvWallet/src/index.ts create mode 100644 packages/clvWallet/tsconfig.json diff --git a/.gitignore b/.gitignore index f0f1b52a0..0024f7f36 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ packages/*/dist/ example/.next/ .env.local +.idea/ diff --git a/example/components/Card.tsx b/example/components/Card.tsx index 64582b5c6..2c5a56993 100644 --- a/example/components/Card.tsx +++ b/example/components/Card.tsx @@ -1,6 +1,7 @@ import type { CoinbaseWallet } from '@web3-react/coinbase-wallet' import type { Web3ReactHooks } from '@web3-react/core' import type { GnosisSafe } from '@web3-react/gnosis-safe' +import type { ClvWallet } from '@web3-react/clvWallet' import type { MetaMask } from '@web3-react/metamask' import type { Network } from '@web3-react/network' import type { WalletConnect } from '@web3-react/walletconnect' @@ -13,7 +14,7 @@ import { ConnectWithSelect } from './ConnectWithSelect' import { Status } from './Status' interface Props { - connector: MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network | GnosisSafe + connector: ClvWallet | MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network | GnosisSafe activeChainId: ReturnType chainIds?: ReturnType[] isActivating: ReturnType diff --git a/example/components/ConnectWithSelect.tsx b/example/components/ConnectWithSelect.tsx index 3568c6574..b4c38345b 100644 --- a/example/components/ConnectWithSelect.tsx +++ b/example/components/ConnectWithSelect.tsx @@ -1,6 +1,7 @@ import type { CoinbaseWallet } from '@web3-react/coinbase-wallet' import type { Web3ReactHooks } from '@web3-react/core' import { GnosisSafe } from '@web3-react/gnosis-safe' +import { ClvWallet } from '@web3-react/clvWallet' import type { MetaMask } from '@web3-react/metamask' import { Network } from '@web3-react/network' import { WalletConnect } from '@web3-react/walletconnect' @@ -48,7 +49,7 @@ export function ConnectWithSelect({ error, setError, }: { - connector: MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network | GnosisSafe + connector: ClvWallet | MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network | GnosisSafe activeChainId: ReturnType chainIds?: ReturnType[] isActivating: ReturnType diff --git a/example/components/ProviderExample.tsx b/example/components/ProviderExample.tsx index 8721d3318..bee863360 100644 --- a/example/components/ProviderExample.tsx +++ b/example/components/ProviderExample.tsx @@ -1,18 +1,21 @@ import type { CoinbaseWallet } from '@web3-react/coinbase-wallet' import { useWeb3React, Web3ReactHooks, Web3ReactProvider } from '@web3-react/core' +import type { ClvWallet } from '@web3-react/clvWallet' import type { MetaMask } from '@web3-react/metamask' import type { Network } from '@web3-react/network' import type { WalletConnect } from '@web3-react/walletconnect' import type { WalletConnect as WalletConnectV2 } from '@web3-react/walletconnect-v2' import { coinbaseWallet, hooks as coinbaseWalletHooks } from '../connectors/coinbaseWallet' +import { hooks as clvWalletHooks, clvWallet } from '../connectors/clvWallet' import { hooks as metaMaskHooks, metaMask } from '../connectors/metaMask' import { hooks as networkHooks, network } from '../connectors/network' import { hooks as walletConnectHooks, walletConnect } from '../connectors/walletConnect' import { hooks as walletConnectV2Hooks, walletConnectV2 } from '../connectors/walletConnectV2' import { getName } from '../utils' -const connectors: [MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network, Web3ReactHooks][] = [ +const connectors: [ClvWallet | MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network, Web3ReactHooks][] = [ + [clvWallet, clvWalletHooks], [metaMask, metaMaskHooks], [walletConnect, walletConnectHooks], [walletConnectV2, walletConnectV2Hooks], diff --git a/example/components/connectorCards/ClvWalletCard.tsx b/example/components/connectorCards/ClvWalletCard.tsx new file mode 100644 index 000000000..7497db735 --- /dev/null +++ b/example/components/connectorCards/ClvWalletCard.tsx @@ -0,0 +1,40 @@ +import { useEffect, useState } from 'react' +import { hooks, clvWallet } from '../../connectors/clvWallet' +import { Card } from '../Card' + + +const { useChainId, useAccounts, useIsActivating, useIsActive, useProvider, useENSNames } = hooks + +export default function ClvWalletCard() { + const chainId = useChainId() + const accounts = useAccounts() + const isActivating = useIsActivating() + + const isActive = useIsActive() + + const provider = useProvider() + const ENSNames = useENSNames(provider) + + const [error, setError] = useState(undefined) + + // attempt to connect eagerly on mount + useEffect(() => { + clvWallet.connectEagerly().catch((error) => { + console.debug('Failed to connect eagerly to clvWallet', error) + }) + }, []) + + return ( + + ) +} diff --git a/example/connectors/clvWallet.ts b/example/connectors/clvWallet.ts new file mode 100644 index 000000000..8ca4d4414 --- /dev/null +++ b/example/connectors/clvWallet.ts @@ -0,0 +1,4 @@ +import { initializeConnector } from '@web3-react/core' +import { ClvWallet } from '@web3-react/clvWallet' + +export const [clvWallet, hooks] = initializeConnector((actions) => new ClvWallet({ actions })) diff --git a/example/pages/index.tsx b/example/pages/index.tsx index baefa90fd..0f304f3c2 100644 --- a/example/pages/index.tsx +++ b/example/pages/index.tsx @@ -1,6 +1,7 @@ import CoinbaseWalletCard from '../components/connectorCards/CoinbaseWalletCard' import GnosisSafeCard from '../components/connectorCards/GnosisSafeCard' import MetaMaskCard from '../components/connectorCards/MetaMaskCard' +import ClvWalletCard from '../components/connectorCards/ClvWalletCard' import NetworkCard from '../components/connectorCards/NetworkCard' import WalletConnectV2Card from '../components/connectorCards/WalletConnectV2Card' import ProviderExample from '../components/ProviderExample' @@ -10,6 +11,7 @@ export default function Home() { <>
+ diff --git a/example/utils.ts b/example/utils.ts index 68e79460a..e275effbc 100644 --- a/example/utils.ts +++ b/example/utils.ts @@ -1,5 +1,6 @@ import { CoinbaseWallet } from '@web3-react/coinbase-wallet' import { GnosisSafe } from '@web3-react/gnosis-safe' +import { ClvWallet } from '@web3-react/clvWallet' import { MetaMask } from '@web3-react/metamask' import { Network } from '@web3-react/network' import type { Connector } from '@web3-react/types' @@ -7,6 +8,7 @@ import { WalletConnect as WalletConnect } from '@web3-react/walletconnect' import { WalletConnect as WalletConnectV2 } from '@web3-react/walletconnect-v2' export function getName(connector: Connector) { + if (connector instanceof ClvWallet) return 'ClvWallet' if (connector instanceof MetaMask) return 'MetaMask' if (connector instanceof WalletConnectV2) return 'WalletConnect V2' if (connector instanceof WalletConnect) return 'WalletConnect' diff --git a/packages/clvWallet/README.md b/packages/clvWallet/README.md new file mode 100644 index 000000000..76ac99adc --- /dev/null +++ b/packages/clvWallet/README.md @@ -0,0 +1 @@ +# @web3-react/clvWallet diff --git a/packages/clvWallet/package.json b/packages/clvWallet/package.json new file mode 100644 index 000000000..771eb766b --- /dev/null +++ b/packages/clvWallet/package.json @@ -0,0 +1,32 @@ +{ + "name": "@web3-react/clvWallet", + "keywords": [ + "web3-react", + "clvWallet" + ], + "author": "Noah Zinsmeister ", + "license": "GPL-3.0-or-later", + "repository": "github:Uniswap/web3-react", + "publishConfig": { + "access": "public" + }, + "version": "8.2.3", + "files": [ + "dist/*" + ], + "type": "commonjs", + "types": "./dist/index.d.ts", + "main": "./dist/index.js", + "exports": "./dist/index.js", + "scripts": { + "prebuild": "rm -rf dist", + "build": "tsc", + "start": "tsc --watch" + }, + "dependencies": { + "@web3-react/types": "^8.2.2" + }, + "devDependencies": { + "@web3-react/store": "^8.2.2" + } +} diff --git a/packages/clvWallet/src/index.spec.ts b/packages/clvWallet/src/index.spec.ts new file mode 100644 index 000000000..c2d982200 --- /dev/null +++ b/packages/clvWallet/src/index.spec.ts @@ -0,0 +1,66 @@ +import { createWeb3ReactStoreAndActions } from '@web3-react/store' +import type { Actions, Web3ReactStore } from '@web3-react/types' +import {ClvWallet} from '.' +import { MockEIP1193Provider } from '@web3-react/core' + +const chainId = '0x1' +const accounts: string[] = ['0x0000000000000000000000000000000000000000'] + +describe('ClvWallet', () => { + let mockProvider: MockEIP1193Provider + + beforeEach(() => { + mockProvider = new MockEIP1193Provider() + }) + + beforeEach(() => { + ;(window as any).ethereum = mockProvider + }) + + let store: Web3ReactStore + let connector: ClvWallet + + beforeEach(() => { + let actions: Actions + ;[store, actions] = createWeb3ReactStoreAndActions() + connector = new ClvWallet({ actions }) + }) + + test('#connectEagerly', async () => { + mockProvider.chainId = chainId + mockProvider.accounts = accounts + + await connector.connectEagerly() + + expect(mockProvider.eth_requestAccounts).not.toHaveBeenCalled() + expect(mockProvider.eth_accounts).toHaveBeenCalled() + expect(mockProvider.eth_chainId).toHaveBeenCalled() + expect(mockProvider.eth_chainId.mock.invocationCallOrder[0]) + .toBeGreaterThan(mockProvider.eth_accounts.mock.invocationCallOrder[0]) + + expect(store.getState()).toEqual({ + chainId: Number.parseInt(chainId, 16), + accounts, + activating: false, + }) + }) + + test('#activate', async () => { + mockProvider.chainId = chainId + mockProvider.accounts = accounts + + await connector.activate() + + expect(mockProvider.eth_requestAccounts).toHaveBeenCalled() + expect(mockProvider.eth_accounts).not.toHaveBeenCalled() + expect(mockProvider.eth_chainId).toHaveBeenCalled() + expect(mockProvider.eth_chainId.mock.invocationCallOrder[0]) + .toBeGreaterThan(mockProvider.eth_requestAccounts.mock.invocationCallOrder[0]) + + expect(store.getState()).toEqual({ + chainId: Number.parseInt(chainId, 16), + accounts, + activating: false, + }) + }) +}) diff --git a/packages/clvWallet/src/index.ts b/packages/clvWallet/src/index.ts new file mode 100644 index 000000000..0a8c86e95 --- /dev/null +++ b/packages/clvWallet/src/index.ts @@ -0,0 +1,202 @@ +import type { + Actions, + AddEthereumChainParameter, + Provider, + ProviderConnectInfo, + ProviderRpcError, + WatchAssetParameters, +} from '@web3-react/types' +import { Connector } from '@web3-react/types' + +type ClvWalletProvider = Provider & { + isClover?: boolean + isConnected?: () => boolean + providers?: ClvWalletProvider[] +} + +export class NoClvWalletError extends Error { + public constructor() { + super('ClvWallet not installed') + this.name = NoClvWalletError.name + Object.setPrototypeOf(this, NoClvWalletError.prototype) + } +} + +function parseChainId(chainId: string) { + return Number.parseInt(chainId, 16) +} + +/** + * @param options - Options to pass to `@clvWallet/detect-provider` + * @param onError - Handler to report errors thrown from eventListeners. + */ +export interface ClvWalletConstructorArgs { + actions: Actions + options?: Parameters[0] + onError?: (error: Error) => void +} + +export class ClvWallet extends Connector { + /** {@inheritdoc Connector.provider} */ + public provider?: ClvWalletProvider + + private readonly options?: Parameters[0] + private eagerConnection?: Promise + + constructor({ actions, options, onError }: ClvWalletConstructorArgs) { + super(actions, onError) + this.options = options + } + + private async isomorphicInitialize(): Promise { + if (this.eagerConnection) return + + const windowObj: any = window + const provider = windowObj.clover as any + if (provider) { + this.provider = provider as ClvWalletProvider + + // handle the case when e.g. clvWallet and coinbase wallet are both installed + if (this.provider.providers?.length) { + this.provider = this.provider.providers.find((p) => p.isClover) ?? this.provider.providers[0] + } + + this.provider.on('connect', ({ chainId }: ProviderConnectInfo): void => { + this.actions.update({ chainId: parseChainId(chainId) }) + }) + + this.provider.on('disconnect', (error: ProviderRpcError): void => { + // 1013 indicates that ClvWallet is attempting to reestablish the connection + // https://github.com/ClvWallet/providers/releases/tag/v8.0.0 + if (error.code === 1013) { + console.debug('ClvWallet logged connection error 1013: "Try again later"') + return + } + this.actions.resetState() + this.onError?.(error) + }) + + this.provider.on('chainChanged', (chainId: string): void => { + this.actions.update({ chainId: parseChainId(chainId) }) + }) + + this.provider.on('accountsChanged', (accounts: string[]): void => { + if (accounts.length === 0) { + // handle this edge case by disconnecting + this.actions.resetState() + } else { + this.actions.update({ accounts }) + } + }) + } + } + + /** {@inheritdoc Connector.connectEagerly} */ + public async connectEagerly(): Promise { + const cancelActivation = this.actions.startActivation() + + try { + await this.isomorphicInitialize() + if (!this.provider) return cancelActivation() + + // Wallets may resolve eth_chainId and hang on eth_accounts pending user interaction, which may include changing + // chains; they should be requested serially, with accounts first, so that the chainId can settle. + const accounts = (await this.provider.request({ method: 'eth_accounts' })) as string[] + if (!accounts.length) throw new Error('No accounts returned') + const chainId = (await this.provider.request({ method: 'eth_chainId' })) as string + this.actions.update({ chainId: parseChainId(chainId), accounts }) + } catch (error) { + console.debug('Could not connect eagerly', error) + // we should be able to use `cancelActivation` here, but on mobile, clvWallet emits a 'connect' + // event, meaning that chainId is updated, and cancelActivation doesn't work because an intermediary + // update has occurred, so we reset state instead + this.actions.resetState() + } + } + + /** + * Initiates a connection. + * + * @param desiredChainIdOrChainParameters - If defined, indicates the desired chain to connect to. If the user is + * already connected to this chain, no additional steps will be taken. Otherwise, the user will be prompted to switch + * to the chain, if one of two conditions is met: either they already have it added in their extension, or the + * argument is of type AddEthereumChainParameter, in which case the user will be prompted to add the chain with the + * specified parameters first, before being prompted to switch. + */ + public async activate(desiredChainIdOrChainParameters?: number | AddEthereumChainParameter): Promise { + let cancelActivation: () => void + if (!this.provider?.isConnected?.()) cancelActivation = this.actions.startActivation() + + return this.isomorphicInitialize() + .then(async () => { + if (!this.provider) throw new NoClvWalletError() + + // Wallets may resolve eth_chainId and hang on eth_accounts pending user interaction, which may include changing + // chains; they should be requested serially, with accounts first, so that the chainId can settle. + const accounts = (await this.provider.request({ method: 'eth_requestAccounts' })) as string[] + const chainId = (await this.provider.request({ method: 'eth_chainId' })) as string + const receivedChainId = parseChainId(chainId) + const desiredChainId = + typeof desiredChainIdOrChainParameters === 'number' + ? desiredChainIdOrChainParameters + : desiredChainIdOrChainParameters?.chainId + + // if there's no desired chain, or it's equal to the received, update + if (!desiredChainId || receivedChainId === desiredChainId) + return this.actions.update({ chainId: receivedChainId, accounts }) + + const desiredChainIdHex = `0x${desiredChainId.toString(16)}` + + // if we're here, we can try to switch networks + return this.provider + .request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: desiredChainIdHex }], + }) + .catch((error: ProviderRpcError) => { + // https://github.com/ClvWallet/clvWallet-mobile/issues/3312#issuecomment-1065923294 + const errorCode = (error.data as any)?.originalError?.code || error.code + + // 4902 indicates that the chain has not been added to ClvWallet and wallet_addEthereumChain needs to be called + // https://docs.clvWallet.io/guide/rpc-api.html#wallet-switchethereumchain + if (errorCode === 4902 && typeof desiredChainIdOrChainParameters !== 'number') { + if (!this.provider) throw new Error('No provider') + // if we're here, we can try to add a new network + return this.provider.request({ + method: 'wallet_addEthereumChain', + params: [{ ...desiredChainIdOrChainParameters, chainId: desiredChainIdHex }], + }) + } + + throw error + }) + .then(() => this.activate(desiredChainId)) + }) + .catch((error) => { + cancelActivation?.() + throw error + }) + } + + public async watchAsset({ address, symbol, decimals, image }: WatchAssetParameters): Promise { + if (!this.provider) throw new Error('No provider') + + return this.provider + .request({ + method: 'wallet_watchAsset', + params: { + type: 'ERC20', // Initially only supports ERC20, but eventually more! + options: { + address, // The address that the token is at. + symbol, // A ticker symbol or shorthand, up to 5 chars. + decimals, // The number of decimals in the token + image, // A string url of the token logo + }, + }, + }) + .then((success) => { + if (!success) throw new Error('Rejected') + return true + }) + } +} diff --git a/packages/clvWallet/tsconfig.json b/packages/clvWallet/tsconfig.json new file mode 100644 index 000000000..67531bbb0 --- /dev/null +++ b/packages/clvWallet/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./src"], + "compilerOptions": { + "outDir": "./dist" + } +}