From 17edf358478619420f7aea0c39ef7c68ce638676 Mon Sep 17 00:00:00 2001 From: okxweb3team001 Date: Tue, 9 Jul 2024 22:36:33 +0800 Subject: [PATCH] add okx wallet --- example/components/Card.tsx | 3 +- example/components/ConnectWithSelect.tsx | 3 +- example/components/ProviderExample.tsx | 8 +- .../connectorCards/OKXWalletCard.tsx | 38 ++++ example/connectors/okxWallet.ts | 5 + example/pages/index.tsx | 2 + example/utils.ts | 2 + packages/okx/README.md | 1 + packages/okx/package.json | 37 ++++ packages/okx/src/index.spec.ts | 66 ++++++ packages/okx/src/index.ts | 197 ++++++++++++++++++ packages/okx/tsconfig.json | 7 + 12 files changed, 366 insertions(+), 3 deletions(-) create mode 100644 example/components/connectorCards/OKXWalletCard.tsx create mode 100644 example/connectors/okxWallet.ts create mode 100644 packages/okx/README.md create mode 100644 packages/okx/package.json create mode 100644 packages/okx/src/index.spec.ts create mode 100644 packages/okx/src/index.ts create mode 100644 packages/okx/tsconfig.json diff --git a/example/components/Card.tsx b/example/components/Card.tsx index 64582b5c6..1071d07b9 100644 --- a/example/components/Card.tsx +++ b/example/components/Card.tsx @@ -5,6 +5,7 @@ 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 type { OKXWallet } from '../../packages/okx/dist/index' import { getName } from '../utils' import { Accounts } from './Accounts' @@ -13,7 +14,7 @@ import { ConnectWithSelect } from './ConnectWithSelect' import { Status } from './Status' interface Props { - connector: MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network | GnosisSafe + connector: MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network | GnosisSafe | OKXWallet activeChainId: ReturnType chainIds?: ReturnType[] isActivating: ReturnType diff --git a/example/components/ConnectWithSelect.tsx b/example/components/ConnectWithSelect.tsx index 3568c6574..17de7c01e 100644 --- a/example/components/ConnectWithSelect.tsx +++ b/example/components/ConnectWithSelect.tsx @@ -6,6 +6,7 @@ import { Network } from '@web3-react/network' import { WalletConnect } from '@web3-react/walletconnect' import { WalletConnect as WalletConnectV2 } from '@web3-react/walletconnect-v2' import { useCallback, useEffect, useState } from 'react' +import { OKXWallet } from '../../packages/okx/dist/index' import { CHAINS, getAddChainParameters } from '../chains' @@ -48,7 +49,7 @@ export function ConnectWithSelect({ error, setError, }: { - connector: MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network | GnosisSafe + connector: MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network | GnosisSafe | OKXWallet activeChainId: ReturnType chainIds?: ReturnType[] isActivating: ReturnType diff --git a/example/components/ProviderExample.tsx b/example/components/ProviderExample.tsx index 8721d3318..a72e2d76f 100644 --- a/example/components/ProviderExample.tsx +++ b/example/components/ProviderExample.tsx @@ -4,20 +4,26 @@ 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 { OKXWallet } from '../../packages/okx/dist/index' import { coinbaseWallet, hooks as coinbaseWalletHooks } from '../connectors/coinbaseWallet' 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 { hooks as okxHooks, okxWallet } from '../connectors/okxWallet' import { getName } from '../utils' -const connectors: [MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network, Web3ReactHooks][] = [ +const connectors: [ + MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network | OKXWallet, + Web3ReactHooks +][] = [ [metaMask, metaMaskHooks], [walletConnect, walletConnectHooks], [walletConnectV2, walletConnectV2Hooks], [coinbaseWallet, coinbaseWalletHooks], [network, networkHooks], + [okxWallet, okxHooks], ] function Child() { diff --git a/example/components/connectorCards/OKXWalletCard.tsx b/example/components/connectorCards/OKXWalletCard.tsx new file mode 100644 index 000000000..1c27289f4 --- /dev/null +++ b/example/components/connectorCards/OKXWalletCard.tsx @@ -0,0 +1,38 @@ +import { useEffect, useState } from 'react' +import { hooks, okxWallet } from '../../connectors/okxWallet' +import { Card } from '../Card' + +const { useChainId, useAccounts, useIsActivating, useIsActive, useProvider, useENSNames } = hooks + +export default function OKXWalletCard() { + const chainId = useChainId() + const accounts = useAccounts() + const isActivating = useIsActivating() + + const isActive = useIsActive() + + const provider = useProvider() + const ENSNames = useENSNames(provider) + + const [error, setError] = useState(undefined) + + useEffect(() => { + okxWallet.connectEagerly().catch((error) => { + console.debug('Failed to connect eagerly to OKX Wallet', error) + }) + }, []) + + return ( + + ) +} diff --git a/example/connectors/okxWallet.ts b/example/connectors/okxWallet.ts new file mode 100644 index 000000000..0bc5944b8 --- /dev/null +++ b/example/connectors/okxWallet.ts @@ -0,0 +1,5 @@ +import { initializeConnector } from '@web3-react/core' + +import { OKXWallet } from '../../packages/okx/dist/index' + +export const [okxWallet, hooks] = initializeConnector((actions) => new OKXWallet({ actions })) \ No newline at end of file diff --git a/example/pages/index.tsx b/example/pages/index.tsx index baefa90fd..3fe41ee83 100644 --- a/example/pages/index.tsx +++ b/example/pages/index.tsx @@ -4,6 +4,7 @@ import MetaMaskCard from '../components/connectorCards/MetaMaskCard' import NetworkCard from '../components/connectorCards/NetworkCard' import WalletConnectV2Card from '../components/connectorCards/WalletConnectV2Card' import ProviderExample from '../components/ProviderExample' +import OKXWalletCard from '../components/connectorCards/OKXWalletCard' export default function Home() { return ( @@ -15,6 +16,7 @@ export default function Home() { + ) diff --git a/example/utils.ts b/example/utils.ts index 68e79460a..8d9461ade 100644 --- a/example/utils.ts +++ b/example/utils.ts @@ -5,6 +5,7 @@ import { Network } from '@web3-react/network' import type { Connector } from '@web3-react/types' import { WalletConnect as WalletConnect } from '@web3-react/walletconnect' import { WalletConnect as WalletConnectV2 } from '@web3-react/walletconnect-v2' +import { OKXWallet } from '../packages/okx/dist/index' export function getName(connector: Connector) { if (connector instanceof MetaMask) return 'MetaMask' @@ -13,5 +14,6 @@ export function getName(connector: Connector) { if (connector instanceof CoinbaseWallet) return 'Coinbase Wallet' if (connector instanceof Network) return 'Network' if (connector instanceof GnosisSafe) return 'Gnosis Safe' + if (connector instanceof OKXWallet) return 'OKX Wallet' return 'Unknown' } diff --git a/packages/okx/README.md b/packages/okx/README.md new file mode 100644 index 000000000..a79bdee49 --- /dev/null +++ b/packages/okx/README.md @@ -0,0 +1 @@ +# @web3-react/okx diff --git a/packages/okx/package.json b/packages/okx/package.json new file mode 100644 index 000000000..54d0450ca --- /dev/null +++ b/packages/okx/package.json @@ -0,0 +1,37 @@ +{ + "name": "@web3-react/okx", + "keywords": [ + "web3-react", + "coinbase-wallet" + ], + "author": "", + "license": "GPL-3.0-or-later", + "repository": "github:Uniswap/web3-react", + "publishConfig": { + "access": "public" + }, + "version": "8.2.2", + "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": { + "@okxweb3/coin-base": "^1.0.8", + "@okxweb3/coin-ethereum": "^1.0.3", + "@web3-react/types": "^8.2.2" + }, + "peerDependencies": { + "@coinbase/wallet-sdk": "^3.0.4" + }, + "devDependencies": { + "@web3-react/store": "^8.2.2" + } +} diff --git a/packages/okx/src/index.spec.ts b/packages/okx/src/index.spec.ts new file mode 100644 index 000000000..7cd3af5a6 --- /dev/null +++ b/packages/okx/src/index.spec.ts @@ -0,0 +1,66 @@ +import { createWeb3ReactStoreAndActions } from '@web3-react/store' +import type { Actions, Web3ReactStore } from '@web3-react/types' +import { OKXWallet } from '.' +import { MockEIP1193Provider } from '@web3-react/core' + +const chainId = '0x1' +const accounts: string[] = ['0x0000000000000000000000000000000000000000'] + +describe('OKXWallet', () => { + let mockProvider: MockEIP1193Provider + + beforeEach(() => { + mockProvider = new MockEIP1193Provider() + }) + + beforeEach(() => { + ;(window as any).ethereum = mockProvider + }) + + let store: Web3ReactStore + let connector: OKXWallet + + beforeEach(() => { + let actions: Actions + ;[store, actions] = createWeb3ReactStoreAndActions() + connector = new OKXWallet({ 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, + }) + }) +}) \ No newline at end of file diff --git a/packages/okx/src/index.ts b/packages/okx/src/index.ts new file mode 100644 index 000000000..7f7fd4b92 --- /dev/null +++ b/packages/okx/src/index.ts @@ -0,0 +1,197 @@ +import type { + Actions, + AddEthereumChainParameter, + Provider, + ProviderConnectInfo, + ProviderRpcError, + WatchAssetParameters, +} from '@web3-react/types' +import { Connector } from '@web3-react/types' + +type OKXWalletProvider = Provider & { + __OKX?: boolean + isConnected?: () => boolean + providers?: OKXWalletProvider[] +} + +export class NoOKXWalletError extends Error { + public constructor() { + super('OKX Wallet not installed') + this.name = NoOKXWalletError.name + Object.setPrototypeOf(this, NoOKXWalletError.prototype) + } +} + +function parseChainId(chainId: string) { + return Number.parseInt(chainId, 16) +} + +/** + * @param options - Options to pass to `@OKXWallet/detect-provider` + * @param onError - Handler to report errors thrown from eventListeners. + */ +export interface OKXWalletConstructorArgs { + actions: Actions + options?: Parameters[0] + onError?: (error: Error) => void +} + +export class OKXWallet extends Connector { + /** {@inheritdoc Connector.provider} */ + public provider?: OKXWalletProvider + + private readonly options?: Parameters[0] + private eagerConnection?: Promise + + constructor({ actions, options, onError }: OKXWalletConstructorArgs) { + super(actions, onError) + this.options = options + } + + private async isomorphicInitialize(): Promise { + if (this.eagerConnection) return + + const windowObj: any = window + const provider = windowObj.ethereum as any + if (provider) { + this.provider = provider as OKXWalletProvider + + // handle the case when e.g. OKXWallet and coinbase wallet are both installed + if (this.provider.providers?.length) { + this.provider = this.provider.providers.find((p) => p.__OKX) ?? this.provider.providers[0] + } + + this.provider.on('connect', ({ chainId }: ProviderConnectInfo): void => { + this.actions.update({ chainId: parseChainId(chainId) }) + }) + + this.provider.on('disconnect', (error: ProviderRpcError): void => { + if (error.code === 1013) { + console.debug('OKX Wallet 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) { + 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, OKXWallet 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 NoOKXWalletError() + + // 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) => { + const errorCode = (error.data as any)?.originalError?.code || error.code + + // 4902 indicates that the chain has not been added to OKXWallet and wallet_addEthereumChain needs to be called + 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', + options: { + address, + symbol, + decimals, + image, + }, + }, + }) + .then((success) => { + if (!success) throw new Error('Rejected') + return true + }) + } +} diff --git a/packages/okx/tsconfig.json b/packages/okx/tsconfig.json new file mode 100644 index 000000000..67531bbb0 --- /dev/null +++ b/packages/okx/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./src"], + "compilerOptions": { + "outDir": "./dist" + } +}