UNPKG

@wagmi/core

Version:

VanillaJS library for Ethereum

435 lines (391 loc) 13.8 kB
import type { Provider as AccountsProvider, Rpc as AccountsRpc, dangerous_secp256k1 as accountsDangerousSecp256k1, dialog as accountsDialog, webAuthn as accountsWebAuthn, } from 'accounts' import { type Address, numberToHex, type ProviderConnectInfo, type RpcError, SwitchChainError, UserRejectedRequestError, withRetry, } from 'viem' import { parseAccount } from 'viem/utils' import { createConnector } from '../connectors/createConnector.js' import type { Connector } from '../createConfig.js' import { ChainNotConfiguredError } from '../errors/config.js' type AccountsModule = { dialog: typeof accountsDialog Provider: typeof AccountsProvider dangerous_secp256k1: typeof accountsDangerousSecp256k1 webAuthn: typeof accountsWebAuthn } type AccountsDialogParameters = NonNullable< Parameters<typeof accountsDialog>[0] > type AccountsProviderParameters = NonNullable< Parameters<typeof AccountsProvider.create>[0] > type AccountsAdapter = NonNullable<AccountsProviderParameters['adapter']> type AccountsDangerousSecp256k1Parameters = NonNullable< Parameters<typeof accountsDangerousSecp256k1>[0] > type AccountsWebAuthnParameters = NonNullable< Parameters<typeof accountsWebAuthn>[0] > type Provider = Pick< ReturnType<typeof AccountsProvider.create>, 'getAccount' | 'getClient' | 'on' | 'removeListener' | 'request' > type AccountsConnectParameters = NonNullable< AccountsRpc.wallet_connect.Decoded['params'] >[0] type CapabilitiesRequest = AccountsConnectParameters['capabilities'] type InternalAccount = AccountsRpc.wallet_connect.Encoded['returns']['accounts'][number] const tempoWalletIcon = 'data:image/svg+xml,<svg width="269" height="269" viewBox="0 0 269 269" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="269" height="269" fill="black"/><path d="M123.273 190.794H93.445L121.09 105.318H85.7334L93.445 80.2642H191.95L184.238 105.318H150.773L123.273 190.794Z" fill="white"/></svg>' /** @deprecated use `tempoWallet.Parameters` instead */ export type TempoWalletParameters = tempoWallet.Parameters /** * Connector for the Tempo Wallet dialog. */ export function tempoWallet(parameters: tempoWallet.Parameters = {}) { const { dialog: dialogOption, host, icon = tempoWalletIcon, name, rdns, theme, ...providerParameters } = parameters return _setup({ createAdapter(accounts) { return accounts.dialog({ dialog: dialogOption, host, icon, name, rdns, theme, }) }, icon, id: rdns ?? 'xyz.tempo', name: name ?? 'Tempo Wallet', providerParameters, rdns: rdns ?? 'xyz.tempo', type: 'injected', }) } export declare namespace tempoWallet { export type Parameters = Omit< AccountsProviderParameters, 'adapter' | 'chains' > & AccountsDialogParameters export type ConnectParameters<withCapabilities extends boolean = false> = setup.ConnectParameters<withCapabilities> export type ConnectReturnType<withCapabilities extends boolean = false> = setup.ConnectReturnType<withCapabilities> } /** @deprecated use `webAuthn.Parameters` instead */ export type WebAuthnParameters = webAuthn.Parameters webAuthn.type = 'webAuthn' as const /** * Connector for a WebAuthn EOA. */ export function webAuthn(parameters: webAuthn.Parameters = {}) { const { authUrl, ceremony, icon, name, rdns, ...providerParameters } = parameters return _setup({ createAdapter(accounts) { return ceremony ? accounts.webAuthn({ ceremony, icon, name, rdns }) : accounts.webAuthn({ authUrl, icon, name, rdns }) }, icon, id: 'webAuthn', name: name ?? 'EOA (WebAuthn)', providerParameters, rdns, type: 'webAuthn', }) } export declare namespace webAuthn { export type Parameters = AccountsWebAuthnParameters & Omit<AccountsProviderParameters, 'adapter' | 'chains'> } /** @deprecated use `dangerous_secp256k1.Parameters` instead */ export type Dangerous_Secp256k1Parameters = dangerous_secp256k1.Parameters dangerous_secp256k1.type = 'dangerous_secp256k1' as const /** * Connector for a Secp256k1 EOA. * * WARNING: NOT RECOMMENDED FOR PRODUCTION USAGE. * This connector stores private keys in clear text, and are bound to the session * length of the storage used. */ export function dangerous_secp256k1( parameters: dangerous_secp256k1.Parameters = {}, ) { const { icon, name, privateKey, rdns, ...providerParameters } = parameters return _setup({ createAdapter(accounts) { return accounts.dangerous_secp256k1({ icon, name, privateKey, rdns }) }, icon, id: 'secp256k1', name: name ?? 'EOA (Secp256k1)', providerParameters, rdns, type: 'secp256k1', }) } export declare namespace dangerous_secp256k1 { export type Parameters = AccountsDangerousSecp256k1Parameters & Omit<AccountsProviderParameters, 'adapter' | 'chains'> } function _setup(parameters: setup.Parameters) { type Properties = { connect<withCapabilities extends boolean = false>( parameters?: setup.ConnectParameters<withCapabilities>, ): Promise<setup.ConnectReturnType<withCapabilities>> } return createConnector<Provider, Properties>((config) => { const chains = config.chains let providerPromise: Promise<Provider> | undefined let accountsChanged: Connector['onAccountsChanged'] | undefined let chainChanged: ((chain: string) => void) | undefined let connect: ((connectInfo: ProviderConnectInfo) => void) | undefined let disconnect: ((error?: Error | undefined) => void) | undefined async function getAccountsModule() { return await import('accounts').catch(() => { throw new Error('dependency "accounts" not found') }) } async function getProvider() { providerPromise ??= (async () => { const accounts = await getAccountsModule() return accounts.Provider.create({ ...parameters.providerParameters, adapter: parameters.createAdapter(accounts), chains: config.chains as never, transports: config.transports as never, }) as unknown as Provider })() return await providerPromise } return { icon: parameters.icon, id: parameters.id, name: parameters.name, rdns: parameters.rdns, type: parameters.type, async connect(connectParameters = {}) { const { chainId, isReconnecting, withCapabilities } = connectParameters const capabilities = 'capabilities' in connectParameters ? connectParameters.capabilities : undefined let accounts: readonly InternalAccount[] = [] let currentChainId: number | undefined if (isReconnecting) { accounts = await this.getAccounts() .then((accounts) => accounts.map((address) => ({ address, capabilities: {} })), ) .catch(() => []) } try { if (!accounts.length && !isReconnecting) { const provider = await getProvider() const response = (await provider.request({ method: 'wallet_connect', params: [ { ...(chainId ? { chainId } : {}), ...(capabilities ? { capabilities } : {}), }, ] as never, })) as AccountsRpc.wallet_connect.Encoded['returns'] accounts = response.accounts } currentChainId ??= await this.getChainId() if (!currentChainId) throw new ChainNotConfiguredError() const provider = await getProvider() if (connect) { provider.removeListener('connect', connect) connect = undefined } if (!accountsChanged) { accountsChanged = this.onAccountsChanged.bind(this) provider.on('accountsChanged', accountsChanged as never) } if (!chainChanged) { chainChanged = this.onChainChanged.bind(this) provider.on('chainChanged', chainChanged) } if (!disconnect) { disconnect = this.onDisconnect.bind(this) provider.on('disconnect', disconnect) } return { accounts: (withCapabilities ? accounts : accounts.map((account) => account.address)) as never, chainId: currentChainId, } } catch (error) { const rpcError = error as RpcError if (rpcError.code === UserRejectedRequestError.code) throw new UserRejectedRequestError(rpcError) throw rpcError } }, async disconnect() { const provider = await getProvider() if (chainChanged) { provider.removeListener('chainChanged', chainChanged) chainChanged = undefined } if (disconnect) { provider.removeListener('disconnect', disconnect) disconnect = undefined } if (!connect) { connect = this.onConnect?.bind(this) if (connect) provider.on('connect', connect) } await provider.request({ method: 'wallet_disconnect' }) }, async getAccounts() { const provider = await getProvider() return await provider.request({ method: 'eth_accounts' }) }, async getChainId() { const provider = await getProvider() return Number(await provider.request({ method: 'eth_chainId' })) }, async getClient({ chainId } = {}) { const provider = await getProvider() // Always provide a JSON-RPC account; the SDK provider performs // access key orchestration internally before signing. const { address } = provider.getAccount({ accessKey: false }) return Object.assign(provider.getClient({ chainId }), { account: parseAccount(address), }) as never }, async getProvider() { return await getProvider() }, async isAuthorized() { try { const accounts = await withRetry(() => this.getAccounts()) return !!accounts.length } catch { return false } }, onAccountsChanged(accounts) { config.emitter.emit('change', { accounts: accounts as readonly Address[], }) }, onChainChanged(chain) { config.emitter.emit('change', { chainId: Number(chain) }) }, async onConnect(connectInfo) { const accounts = await this.getAccounts() if (accounts.length === 0) return const chainId = Number(connectInfo.chainId) config.emitter.emit('connect', { accounts, chainId }) const provider = await getProvider() if (connect) { provider.removeListener('connect', connect) connect = undefined } if (!accountsChanged) { accountsChanged = this.onAccountsChanged.bind(this) provider.on('accountsChanged', accountsChanged as never) } if (!chainChanged) { chainChanged = this.onChainChanged.bind(this) provider.on('chainChanged', chainChanged) } if (!disconnect) { disconnect = this.onDisconnect.bind(this) provider.on('disconnect', disconnect) } }, async onDisconnect(_error) { const provider = await getProvider() config.emitter.emit('disconnect') if (chainChanged) { provider.removeListener('chainChanged', chainChanged) chainChanged = undefined } if (disconnect) { provider.removeListener('disconnect', disconnect) disconnect = undefined } if (!connect) { connect = this.onConnect?.bind(this) if (connect) provider.on('connect', connect) } }, async setup() { if (!connect) { const provider = await getProvider() connect = this.onConnect?.bind(this) if (connect) provider.on('connect', connect) } }, async switchChain({ chainId }) { const chain = chains.find((chain) => chain.id === chainId) if (!chain) throw new SwitchChainError(new ChainNotConfiguredError()) const provider = await getProvider() await provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: numberToHex(chainId) }], }) return chain }, } }) } export declare namespace setup { export type Parameters = { /** Builds the underlying `accounts` adapter (dialog, webAuthn, secp256k1) the connector wraps. */ createAdapter: (accounts: AccountsModule) => AccountsAdapter /** Optional connector icon (data URL or remote URL). Surfaced via EIP-6963 announcement. */ icon?: string | undefined /** Connector ID. */ id: string /** Human-readable connector name. */ name: string /** Forwarded to `Provider.create`. */ providerParameters: Omit<AccountsProviderParameters, 'adapter' | 'chains'> /** EIP-6963 reverse-DNS ID(s) for the connector. */ rdns?: string | readonly string[] | undefined /** Connector type. */ type: string } export type ConnectParameters<withCapabilities extends boolean = false> = { capabilities?: CapabilitiesRequest | undefined chainId?: number | undefined isReconnecting?: boolean | undefined withCapabilities?: withCapabilities | boolean | undefined } export type ConnectReturnType<withCapabilities extends boolean = false> = { accounts: withCapabilities extends true ? readonly InternalAccount[] : readonly Address[] chainId: number } }