UNPKG

@wagmi/connectors

Version:

Collection of connectors for Wagmi

331 lines (307 loc) 10.6 kB
import type { createBaseAccountSDK, ProviderInterface } from '@base-org/account' import { ChainNotConfiguredError, type Connector, createConnector, } from '@wagmi/core' import type { Mutable, Omit } from '@wagmi/core/internal' import { type AddEthereumChainParameter, type Address, getAddress, type Hex, numberToHex, type ProviderRpcError, SwitchChainError, UserRejectedRequestError, } from 'viem' export type BaseAccountParameters = Mutable< Omit< Parameters<typeof createBaseAccountSDK>[0], 'appChainIds' // set via wagmi config > > export function baseAccount(parameters: BaseAccountParameters = {}) { type Provider = ProviderInterface type Properties = { connect<withCapabilities extends boolean = false>(parameters?: { chainId?: number | undefined capabilities?: | { signInWithEthereum?: { chainId?: string | undefined domain?: string | undefined expirationTime?: string | undefined issuedAt?: string | undefined nonce: string notBefore?: string | undefined requestId?: string | undefined resources?: string[] | undefined scheme?: string | undefined statement?: string | undefined uri?: string | undefined version?: string | undefined } [capability: string]: any } | undefined isReconnecting?: boolean | undefined withCapabilities?: withCapabilities | boolean | undefined }): Promise<{ accounts: withCapabilities extends true ? readonly { address: Address capabilities: WalletConnectResponseCapabilities }[] : readonly Address[] chainId: number }> } type WalletConnectResponseCapabilities = { signInWithEthereum?: { message: string; signature: Hex } | undefined [capability: string]: any } let walletProvider: Provider | undefined let accountsChanged: Connector['onAccountsChanged'] | undefined let chainChanged: Connector['onChainChanged'] | undefined let disconnect: Connector['onDisconnect'] | undefined return createConnector<Provider, Properties>((config) => ({ id: 'baseAccount', name: 'Base Account', rdns: 'app.base.account', type: 'baseAccount', async connect({ chainId, withCapabilities, ...rest } = {}) { try { const provider = await this.getProvider() const targetChainId = chainId ?? config.chains[0]?.id if (!targetChainId) throw new ChainNotConfiguredError() let { accounts, currentChainId } = await (async () => { if (rest.isReconnecting) return { accounts: ( (await provider.request({ method: 'eth_accounts', params: [], })) as string[] ).map((x) => ({ address: getAddress(x) })), currentChainId: await this.getChainId(), } const response = (await provider.request({ method: 'wallet_connect', params: [ { capabilities: 'capabilities' in rest && rest.capabilities ? rest.capabilities : {}, chainIds: [ numberToHex(targetChainId), ...config.chains .filter((x) => x.id !== targetChainId) .map((x) => numberToHex(x.id)), ], }, ], })) as { accounts: { address: Address capabilities?: WalletConnectResponseCapabilities | undefined }[] chainIds: Hex[] } const orderedAccounts = (await provider.request({ method: 'eth_accounts', })) as Address[] const accounts = orderedAccounts.map( (account1) => response.accounts.find( (account2) => account2.address === account1, ) ?? { address: account1 }, ) return { accounts: accounts.map((account) => ({ address: getAddress(account.address), capabilities: account.capabilities ?? {}, })), currentChainId: Number(response.chainIds[0]), } })() if (!accountsChanged) { accountsChanged = this.onAccountsChanged.bind(this) provider.on('accountsChanged', accountsChanged) } if (!chainChanged) { chainChanged = this.onChainChanged.bind(this) provider.on('chainChanged', chainChanged) } if (!disconnect) { disconnect = this.onDisconnect.bind(this) provider.on('disconnect', disconnect) } // Switch to chain if provided if (chainId && currentChainId !== chainId) { const chain = await this.switchChain!({ chainId }).catch((error) => { if (error.code === UserRejectedRequestError.code) throw error return { id: currentChainId } }) currentChainId = chain?.id ?? currentChainId } return { // TODO(v3): Make `withCapabilities: true` default behavior accounts: (withCapabilities ? accounts : accounts.map((account) => account.address)) as never, chainId: currentChainId, } } catch (error) { if ( /(user closed modal|accounts received is empty|user denied account|request rejected)/i.test( (error as Error).message, ) ) throw new UserRejectedRequestError(error as Error) throw error } }, async disconnect() { const provider = await this.getProvider() if (accountsChanged) { provider.removeListener('accountsChanged', accountsChanged) accountsChanged = undefined } if (chainChanged) { provider.removeListener('chainChanged', chainChanged) chainChanged = undefined } if (disconnect) { provider.removeListener('disconnect', disconnect) disconnect = undefined } provider.disconnect() }, async getAccounts() { const provider = await this.getProvider() return ( (await provider.request({ method: 'eth_accounts', })) as string[] ).map((x) => getAddress(x)) }, async getChainId() { const provider = await this.getProvider() const chainId = (await provider.request({ method: 'eth_chainId', })) as Hex return Number(chainId) }, async getProvider() { if (!walletProvider) { const preference = (() => { if (typeof parameters.preference === 'string') return { options: parameters.preference } return { ...parameters.preference, options: parameters.preference?.options ?? 'all', } })() const { createBaseAccountSDK } = await (() => { // safe webpack optional peer dependency dynamic import try { return import('@base-org/account') } catch { throw new Error('dependency "@base-org/account" not found') } })() const sdk = createBaseAccountSDK({ ...parameters, appChainIds: config.chains.map((x) => x.id), preference, }) walletProvider = sdk.getProvider() } return walletProvider }, async isAuthorized() { try { const accounts = await this.getAccounts() return !!accounts.length } catch { return false } }, async switchChain({ addEthereumChainParameter, chainId }) { const chain = config.chains.find((chain) => chain.id === chainId) if (!chain) throw new SwitchChainError(new ChainNotConfiguredError()) const provider = await this.getProvider() try { await provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: numberToHex(chain.id) }], }) return chain } catch (error) { // Indicates chain is not added to provider if ((error as ProviderRpcError).code === 4902) { try { let blockExplorerUrls: string[] | undefined if (addEthereumChainParameter?.blockExplorerUrls) blockExplorerUrls = addEthereumChainParameter.blockExplorerUrls else blockExplorerUrls = chain.blockExplorers?.default.url ? [chain.blockExplorers?.default.url] : [] let rpcUrls: readonly string[] if (addEthereumChainParameter?.rpcUrls?.length) rpcUrls = addEthereumChainParameter.rpcUrls else rpcUrls = [chain.rpcUrls.default?.http[0] ?? ''] const addEthereumChain = { blockExplorerUrls, chainId: numberToHex(chainId), chainName: addEthereumChainParameter?.chainName ?? chain.name, iconUrls: addEthereumChainParameter?.iconUrls, nativeCurrency: addEthereumChainParameter?.nativeCurrency ?? chain.nativeCurrency, rpcUrls, } satisfies AddEthereumChainParameter await provider.request({ method: 'wallet_addEthereumChain', params: [addEthereumChain], }) return chain } catch (error) { throw new UserRejectedRequestError(error as Error) } } throw new SwitchChainError(error as Error) } }, onAccountsChanged(accounts) { if (accounts.length === 0) this.onDisconnect() else config.emitter.emit('change', { accounts: accounts.map((x) => getAddress(x)), }) }, onChainChanged(chain) { const chainId = Number(chain) config.emitter.emit('change', { chainId }) }, async onDisconnect(_error) { config.emitter.emit('disconnect') const provider = await this.getProvider() if (accountsChanged) { provider.removeListener('accountsChanged', accountsChanged) accountsChanged = undefined } if (chainChanged) { provider.removeListener('chainChanged', chainChanged) chainChanged = undefined } if (disconnect) { provider.removeListener('disconnect', disconnect) disconnect = undefined } }, })) }