|
1 |
| -import { Connector, type ConnectorData, type WalletClient } from 'wagmi'; |
| 1 | +import { ChainNotConfiguredError, createConnector } from 'wagmi'; |
2 | 2 | import {
|
3 |
| - createWalletClient, |
4 |
| - custom, |
5 | 3 | getAddress,
|
6 |
| - type Address, |
7 | 4 | UserRejectedRequestError,
|
8 | 5 | numberToHex,
|
9 | 6 | ProviderRpcError,
|
10 |
| - SwitchChainError, |
11 |
| - type Chain |
| 7 | + SwitchChainError |
12 | 8 | } from 'viem';
|
13 | 9 | import { WalletMobileSDKEVMProvider, configure } from '@coinbase/wallet-mobile-sdk';
|
14 | 10 | import type { WalletMobileSDKProviderOptions } from '@coinbase/wallet-mobile-sdk/build/WalletMobileSDKEVMProvider';
|
15 | 11 |
|
16 |
| -const ADD_ETH_CHAIN_METHOD = 'wallet_addEthereumChain'; |
17 |
| -const SWITCH_ETH_CHAIN_METHOD = 'wallet_switchEthereumChain'; |
18 |
| - |
19 |
| -type CoinbaseConnectorOptions = WalletMobileSDKProviderOptions & { |
| 12 | +type CoinbaseConnectorParameters = WalletMobileSDKProviderOptions & { |
20 | 13 | redirect: string;
|
21 | 14 | };
|
22 | 15 |
|
23 |
| -export class CoinbaseConnector extends Connector< |
24 |
| - WalletMobileSDKEVMProvider, |
25 |
| - CoinbaseConnectorOptions |
26 |
| -> { |
27 |
| - readonly id = 'coinbaseWallet'; |
28 |
| - readonly name = 'Coinbase Wallet'; |
29 |
| - readonly ready = true; |
30 |
| - |
31 |
| - private _provider?: WalletMobileSDKEVMProvider; |
32 |
| - private _initProviderPromise?: Promise<void>; |
33 |
| - |
34 |
| - constructor(config: { chains?: Chain[]; options: CoinbaseConnectorOptions }) { |
35 |
| - super(config); |
36 |
| - this._createProvider(); |
37 |
| - } |
38 |
| - |
39 |
| - override connect = async ( |
40 |
| - config?: { chainId?: number | undefined } | undefined |
41 |
| - ): Promise<Required<ConnectorData>> => { |
42 |
| - try { |
43 |
| - await this._setupListeners(); |
| 16 | +type Provider = WalletMobileSDKEVMProvider; |
| 17 | + |
| 18 | +coinbaseConnector.type = 'coinbaseWallet' as const; |
| 19 | +export function coinbaseConnector(parameters: CoinbaseConnectorParameters) { |
| 20 | + let _provider: Provider; |
| 21 | + |
| 22 | + return createConnector<Provider>(config => ({ |
| 23 | + id: 'coinbaseWallet', |
| 24 | + name: 'Coinbase Wallet', |
| 25 | + type: coinbaseConnector.type, |
| 26 | + async setup() {}, |
| 27 | + async connect({ chainId } = {}) { |
| 28 | + try { |
| 29 | + const provider = await this.getProvider(); |
| 30 | + let accounts; |
| 31 | + const isConnected = provider.connected; |
| 32 | + |
| 33 | + if (!isConnected) { |
| 34 | + accounts = ( |
| 35 | + (await provider.request({ |
| 36 | + method: 'eth_requestAccounts' |
| 37 | + })) as string[] |
| 38 | + ).map(getAddress); |
| 39 | + } else { |
| 40 | + accounts = provider.selectedAddress ? [getAddress(provider.selectedAddress)] : []; |
| 41 | + } |
| 42 | + |
| 43 | + provider.on('accountsChanged', this.onAccountsChanged); |
| 44 | + provider.on('chainChanged', this.onChainChanged); |
| 45 | + provider.on('disconnect', this.onDisconnect.bind(this)); |
| 46 | + |
| 47 | + // Switch to chain if provided |
| 48 | + let currentChainId = await this.getChainId(); |
| 49 | + if (chainId && currentChainId !== chainId) { |
| 50 | + const chain = await this.switchChain!({ chainId }).catch(() => ({ |
| 51 | + id: currentChainId |
| 52 | + })); |
| 53 | + currentChainId = chain?.id ?? currentChainId; |
| 54 | + } |
| 55 | + |
| 56 | + return { accounts, chainId: currentChainId }; |
| 57 | + } catch (error) { |
| 58 | + if (/(Error error 0|User rejected the request)/i.test((error as Error).message)) |
| 59 | + throw new UserRejectedRequestError(error as Error); |
| 60 | + |
| 61 | + if (/(Error error 5|Could not open wallet)/i.test((error as Error).message)) |
| 62 | + throw new Error(`Wallet not found. SDK Error: ${(error as Error).message}`); |
| 63 | + |
| 64 | + throw error; |
| 65 | + } |
| 66 | + }, |
| 67 | + async disconnect() { |
44 | 68 | const provider = await this.getProvider();
|
45 | 69 |
|
46 |
| - const isConnected = provider.connected; |
| 70 | + provider.removeListener('accountsChanged', this.onAccountsChanged); |
| 71 | + provider.removeListener('chainChanged', this.onChainChanged); |
| 72 | + provider.removeListener('disconnect', this.onDisconnect.bind(this)); |
47 | 73 |
|
48 |
| - if (!isConnected) { |
49 |
| - await provider.request({ |
50 |
| - method: 'eth_requestAccounts', |
51 |
| - params: [] |
| 74 | + provider.disconnect(); |
| 75 | + }, |
| 76 | + async getAccounts() { |
| 77 | + const provider = await this.getProvider(); |
| 78 | + |
| 79 | + return ( |
| 80 | + await provider.request<string[]>({ |
| 81 | + method: 'eth_accounts' |
| 82 | + }) |
| 83 | + ).map(getAddress); |
| 84 | + }, |
| 85 | + async getChainId() { |
| 86 | + const provider = await this.getProvider(); |
| 87 | + |
| 88 | + return Number(provider.chainId); |
| 89 | + }, |
| 90 | + async getProvider({ chainId } = {}) { |
| 91 | + function initProvider() { |
| 92 | + configure({ |
| 93 | + callbackURL: new URL(parameters.redirect), |
| 94 | + hostURL: new URL('https://wallet.coinbase.com/wsegue'), |
| 95 | + hostPackageName: 'org.toshi' |
52 | 96 | });
|
| 97 | + |
| 98 | + return new WalletMobileSDKEVMProvider({ ...parameters }); |
| 99 | + } |
| 100 | + |
| 101 | + if (!_provider) { |
| 102 | + _provider = initProvider(); |
53 | 103 | }
|
54 | 104 |
|
55 |
| - const address = provider.selectedAddress!; |
56 |
| - const chainId = config?.chainId; |
| 105 | + if (chainId) { |
| 106 | + await this.switchChain?.({ chainId }); |
| 107 | + } |
57 | 108 |
|
58 |
| - this.emit('message', { type: 'connecting' }); |
| 109 | + return _provider; |
| 110 | + }, |
| 111 | + async isAuthorized() { |
| 112 | + try { |
| 113 | + const accounts = await this.getAccounts(); |
59 | 114 |
|
60 |
| - // Switch to chain if provided |
61 |
| - let id = await this.getChainId(); |
62 |
| - let unsupported = this.isChainUnsupported(id); |
63 |
| - if (chainId && id !== chainId) { |
64 |
| - const chain = await this.switchChain(chainId); |
65 |
| - id = chain.id; |
66 |
| - unsupported = this.isChainUnsupported(id); |
| 115 | + return !!accounts.length; |
| 116 | + } catch { |
| 117 | + return false; |
67 | 118 | }
|
| 119 | + }, |
| 120 | + async switchChain({ chainId }) { |
| 121 | + const chain = config.chains.find(c => c.id === chainId); |
| 122 | + if (!chain) throw new SwitchChainError(new ChainNotConfiguredError()); |
68 | 123 |
|
69 |
| - return { |
70 |
| - account: address as `0x${string}`, |
71 |
| - chain: { id, unsupported } |
72 |
| - }; |
73 |
| - } catch (error) { |
74 |
| - if (/(Error error 0|User rejected the request)/i.test((error as Error).message)) |
75 |
| - throw new UserRejectedRequestError(error as Error); |
| 124 | + const provider = await this.getProvider(); |
| 125 | + const chainId_ = numberToHex(chain.id); |
76 | 126 |
|
77 |
| - if (/(Error error 5|Could not open wallet)/i.test((error as Error).message)) |
78 |
| - throw new Error(`Wallet not found. SDK Error: ${(error as Error).message}`); |
| 127 | + try { |
| 128 | + await provider.request({ |
| 129 | + method: 'wallet_switchEthereumChain', |
| 130 | + params: [{ chainId: chainId_ }] |
| 131 | + }); |
79 | 132 |
|
80 |
| - throw error; |
81 |
| - } |
82 |
| - }; |
83 |
| - |
84 |
| - override disconnect = async (): Promise<void> => { |
85 |
| - if (!this._provider) return; |
86 |
| - |
87 |
| - const provider = await this.getProvider(); |
88 |
| - this._removeListeners(); |
89 |
| - provider.disconnect(); |
90 |
| - }; |
91 |
| - |
92 |
| - override async getAccount(): Promise<`0x${string}`> { |
93 |
| - const provider = await this.getProvider(); |
94 |
| - const accounts = await provider.request<Address[]>({ |
95 |
| - method: 'eth_accounts' |
96 |
| - }); |
97 |
| - |
98 |
| - return getAddress(accounts[0] as string); |
99 |
| - } |
100 |
| - |
101 |
| - override getChainId = async (): Promise<number> => { |
102 |
| - const provider = await this.getProvider(); |
103 |
| - |
104 |
| - return this._normalizeChainId(provider.chainId); |
105 |
| - }; |
106 |
| - |
107 |
| - override getProvider = async ({ chainId }: { chainId?: number } = {}) => { |
108 |
| - if (!this._provider) await this._createProvider(); |
109 |
| - if (chainId) await this.switchChain(chainId); |
110 |
| - |
111 |
| - return this._provider!; |
112 |
| - }; |
113 |
| - |
114 |
| - override getWalletClient = async ({ |
115 |
| - chainId |
116 |
| - }: { chainId?: number } = {}): Promise<WalletClient> => { |
117 |
| - const [provider, account] = await Promise.all([this.getProvider(), this.getAccount()]); |
118 |
| - const chain = this.chains.find(x => x.id === chainId); |
119 |
| - if (!provider) throw new Error('provider is required.'); |
120 |
| - |
121 |
| - // @ts-ignore |
122 |
| - return createWalletClient({ |
123 |
| - account, |
124 |
| - chain, |
125 |
| - transport: custom(provider) |
126 |
| - }); |
127 |
| - }; |
128 |
| - |
129 |
| - override isAuthorized = async (): Promise<boolean> => { |
130 |
| - try { |
131 |
| - const account = await this.getAccount(); |
132 |
| - |
133 |
| - return !!account; |
134 |
| - } catch { |
135 |
| - return false; |
136 |
| - } |
137 |
| - }; |
138 |
| - |
139 |
| - override switchChain = async (chainId: number) => { |
140 |
| - const provider = await this.getProvider(); |
141 |
| - const id = numberToHex(chainId); |
142 |
| - const chain = this.chains.find(_chain => _chain.id === chainId); |
143 |
| - if (!chain) |
144 |
| - throw new SwitchChainError( |
145 |
| - new Error(`Chain "${chainId}" not configured for connector "${this.id}".`) |
146 |
| - ); |
147 |
| - |
148 |
| - try { |
149 |
| - await provider.request({ |
150 |
| - method: SWITCH_ETH_CHAIN_METHOD, |
151 |
| - params: [{ chainId: id }] |
152 |
| - }); |
153 |
| - |
154 |
| - return chain; |
155 |
| - } catch (error) { |
156 |
| - // Indicates chain is not added to provider |
157 |
| - if ((error as ProviderRpcError).code === 4902) { |
158 |
| - try { |
159 |
| - await provider.request({ |
160 |
| - method: ADD_ETH_CHAIN_METHOD, |
161 |
| - params: [ |
162 |
| - { |
163 |
| - chainId: id, |
164 |
| - chainName: chain.name, |
165 |
| - nativeCurrency: chain.nativeCurrency, |
166 |
| - rpcUrls: [chain.rpcUrls.public?.http[0] ?? ''], |
167 |
| - blockExplorerUrls: this.getBlockExplorerUrls(chain) |
168 |
| - } |
169 |
| - ] |
170 |
| - }); |
171 |
| - |
172 |
| - return chain; |
173 |
| - } catch (e) { |
174 |
| - throw new UserRejectedRequestError(e as Error); |
| 133 | + return chain; |
| 134 | + } catch (error) { |
| 135 | + // Indicates chain is not added to provider |
| 136 | + if ((error as ProviderRpcError).code === 4902) { |
| 137 | + try { |
| 138 | + await provider.request({ |
| 139 | + method: 'wallet_addEthereumChain', |
| 140 | + params: [ |
| 141 | + { |
| 142 | + chainId: chainId_, |
| 143 | + chainName: chain.name, |
| 144 | + nativeCurrency: chain.nativeCurrency, |
| 145 | + rpcUrls: [chain.rpcUrls.default?.http[0] ?? ''], |
| 146 | + blockExplorerUrls: [chain.blockExplorers?.default.url] |
| 147 | + } |
| 148 | + ] |
| 149 | + }); |
| 150 | + |
| 151 | + return chain; |
| 152 | + } catch (e) { |
| 153 | + throw new UserRejectedRequestError(e as Error); |
| 154 | + } |
175 | 155 | }
|
| 156 | + |
| 157 | + throw new SwitchChainError(error as Error); |
176 | 158 | }
|
| 159 | + }, |
| 160 | + onAccountsChanged(accounts) { |
| 161 | + if (accounts.length === 0) config.emitter.emit('disconnect'); |
| 162 | + else config.emitter.emit('change', { accounts: accounts.map(getAddress) }); |
| 163 | + }, |
| 164 | + onChainChanged(chain) { |
| 165 | + const chainId = Number(chain); |
| 166 | + config.emitter.emit('change', { chainId }); |
| 167 | + }, |
| 168 | + async onDisconnect(_error) { |
| 169 | + config.emitter.emit('disconnect'); |
177 | 170 |
|
178 |
| - throw new SwitchChainError(error as Error); |
179 |
| - } |
180 |
| - }; |
181 |
| - |
182 |
| - protected override onAccountsChanged = (accounts: `0x${string}`[]): void => { |
183 |
| - if (accounts.length === 0) this.emit('disconnect'); |
184 |
| - else this.emit('change', { account: getAddress(accounts[0] as string) }); |
185 |
| - }; |
186 |
| - |
187 |
| - protected override onChainChanged = (chain: string | number): void => { |
188 |
| - const id = Number(chain); |
189 |
| - const unsupported = this.isChainUnsupported(id); |
190 |
| - this.emit('change', { chain: { id, unsupported } }); |
191 |
| - }; |
192 |
| - |
193 |
| - protected override onDisconnect = (): void => { |
194 |
| - this.emit('disconnect'); |
195 |
| - }; |
196 |
| - |
197 |
| - private async _createProvider() { |
198 |
| - if (!this._initProviderPromise) { |
199 |
| - this._initProviderPromise = this._initProvider(); |
| 171 | + const provider = await this.getProvider(); |
| 172 | + provider.removeListener('accountsChanged', this.onAccountsChanged); |
| 173 | + provider.removeListener('chainChanged', this.onChainChanged); |
| 174 | + provider.removeListener('disconnect', this.onDisconnect.bind(this)); |
200 | 175 | }
|
201 |
| - |
202 |
| - return this._initProviderPromise; |
203 |
| - } |
204 |
| - |
205 |
| - private _initProvider = async () => { |
206 |
| - configure({ |
207 |
| - callbackURL: new URL(this.options.redirect), |
208 |
| - hostURL: new URL('https://wallet.coinbase.com/wsegue'), // Don't change -> Coinbase url |
209 |
| - hostPackageName: 'org.toshi' // Don't change -> Coinbase wallet scheme |
210 |
| - }); |
211 |
| - |
212 |
| - this._provider = new WalletMobileSDKEVMProvider({ ...this.options }); |
213 |
| - }; |
214 |
| - |
215 |
| - private _setupListeners = async () => { |
216 |
| - const provider = await this.getProvider(); |
217 |
| - this._removeListeners(); |
218 |
| - provider.on('accountsChanged', this.onAccountsChanged); |
219 |
| - provider.on('chainChanged', this.onChainChanged); |
220 |
| - provider.on('disconnect', this.onDisconnect); |
221 |
| - }; |
222 |
| - |
223 |
| - private _removeListeners = () => { |
224 |
| - if (!this._provider) return; |
225 |
| - |
226 |
| - this._provider.removeListener('accountsChanged', this.onAccountsChanged); |
227 |
| - this._provider.removeListener('chainChanged', this.onChainChanged); |
228 |
| - this._provider.removeListener('disconnect', this.onDisconnect); |
229 |
| - }; |
230 |
| - |
231 |
| - private _normalizeChainId = (chainId: string | number | bigint) => { |
232 |
| - if (typeof chainId === 'string') |
233 |
| - return Number.parseInt(chainId, chainId.trim().substring(0, 2) === '0x' ? 16 : 10); |
234 |
| - if (typeof chainId === 'bigint') return Number(chainId); |
235 |
| - |
236 |
| - return chainId; |
237 |
| - }; |
| 176 | + })); |
238 | 177 | }
|
0 commit comments