UNPKG

@wagmi/core

Version:

VanillaJS library for Ethereum

291 lines (270 loc) 9.23 kB
import { type Address, type EIP1193RequestFn, type Hex, RpcRequestError, SwitchChainError, type Transport, UserRejectedRequestError, type WalletCallReceipt, type WalletRpcSchema, custom, fromHex, getAddress, keccak256, numberToHex, stringToHex, } from 'viem' import { rpc } from 'viem/utils' import { ChainNotConfiguredError, ConnectorNotConnectedError, } from '../errors/config.js' import { createConnector } from './createConnector.js' export type MockParameters = { accounts: readonly [Address, ...Address[]] features?: | { defaultConnected?: boolean | undefined connectError?: boolean | Error | undefined switchChainError?: boolean | Error | undefined signMessageError?: boolean | Error | undefined signTypedDataError?: boolean | Error | undefined reconnect?: boolean | undefined watchAssetError?: boolean | Error | undefined } | undefined } mock.type = 'mock' as const export function mock(parameters: MockParameters) { const transactionCache = new Map<Hex, Hex[]>() const features = parameters.features ?? ({ defaultConnected: false } satisfies MockParameters['features']) type Provider = ReturnType< Transport<'custom', unknown, EIP1193RequestFn<WalletRpcSchema>> > type Properties = { connect(parameters?: { chainId?: number | undefined isReconnecting?: boolean | undefined foo?: string | undefined }): Promise<{ accounts: readonly Address[] chainId: number }> } let connected = features.defaultConnected let connectedChainId: number return createConnector<Provider, Properties>((config) => ({ id: 'mock', name: 'Mock Connector', type: mock.type, async setup() { connectedChainId = config.chains[0].id }, async connect({ chainId } = {}) { if (features.connectError) { if (typeof features.connectError === 'boolean') throw new UserRejectedRequestError(new Error('Failed to connect.')) throw features.connectError } const provider = await this.getProvider() const accounts = await provider.request({ method: 'eth_requestAccounts', }) let currentChainId = await this.getChainId() if (chainId && currentChainId !== chainId) { const chain = await this.switchChain!({ chainId }) currentChainId = chain.id } connected = true return { accounts: accounts.map((x) => getAddress(x)), chainId: currentChainId, } }, async disconnect() { connected = false }, async getAccounts() { if (!connected) throw new ConnectorNotConnectedError() const provider = await this.getProvider() const accounts = await provider.request({ method: 'eth_accounts' }) return accounts.map((x) => getAddress(x)) }, async getChainId() { const provider = await this.getProvider() const hexChainId = await provider.request({ method: 'eth_chainId' }) return fromHex(hexChainId, 'number') }, async isAuthorized() { if (!features.reconnect) return false if (!connected) return false const accounts = await this.getAccounts() return !!accounts.length }, async switchChain({ chainId }) { const provider = await this.getProvider() const chain = config.chains.find((x) => x.id === chainId) if (!chain) throw new SwitchChainError(new ChainNotConfiguredError()) await provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: numberToHex(chainId) }], }) return chain }, 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') connected = false }, async getProvider({ chainId } = {}) { const chain = config.chains.find((x) => x.id === chainId) ?? config.chains[0] const url = chain.rpcUrls.default.http[0]! const request: EIP1193RequestFn = async ({ method, params }) => { // eth methods if (method === 'eth_chainId') return numberToHex(connectedChainId) if (method === 'eth_requestAccounts') return parameters.accounts if (method === 'eth_signTypedData_v4') if (features.signTypedDataError) { if (typeof features.signTypedDataError === 'boolean') throw new UserRejectedRequestError( new Error('Failed to sign typed data.'), ) throw features.signTypedDataError } // wallet methods if (method === 'wallet_switchEthereumChain') { if (features.switchChainError) { if (typeof features.switchChainError === 'boolean') throw new UserRejectedRequestError( new Error('Failed to switch chain.'), ) throw features.switchChainError } type Params = [{ chainId: Hex }] connectedChainId = fromHex((params as Params)[0].chainId, 'number') this.onChainChanged(connectedChainId.toString()) return } if (method === 'wallet_watchAsset') { if (features.watchAssetError) { if (typeof features.watchAssetError === 'boolean') throw new UserRejectedRequestError( new Error('Failed to switch chain.'), ) throw features.watchAssetError } return connected } if (method === 'wallet_getCapabilities') return { '0x2105': { paymasterService: { supported: (params as [Hex])[0] === '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', }, sessionKeys: { supported: true, }, }, '0x14A34': { paymasterService: { supported: (params as [Hex])[0] === '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', }, }, } if (method === 'wallet_sendCalls') { const hashes = [] const calls = (params as any)[0].calls for (const call of calls) { const { result, error } = await rpc.http(url, { body: { method: 'eth_sendTransaction', params: [call], }, }) if (error) throw new RpcRequestError({ body: { method, params }, error, url, }) hashes.push(result) } const id = keccak256(stringToHex(JSON.stringify(calls))) transactionCache.set(id, hashes) return id } if (method === 'wallet_getCallsStatus') { const hashes = transactionCache.get((params as any)[0]) if (!hashes) return null const receipts = await Promise.all( hashes.map(async (hash) => { const { result, error } = await rpc.http(url, { body: { method: 'eth_getTransactionReceipt', params: [hash], id: 0, }, }) if (error) throw new RpcRequestError({ body: { method, params }, error, url, }) if (!result) return null return { blockHash: result.blockHash, blockNumber: result.blockNumber, gasUsed: result.gasUsed, logs: result.logs, status: result.status, transactionHash: result.transactionHash, } satisfies WalletCallReceipt }), ) if (receipts.some((x) => !x)) return { status: 'PENDING', receipts: [] } return { status: 'CONFIRMED', receipts } } if (method === 'wallet_showCallsStatus') return // other methods if (method === 'personal_sign') { if (features.signMessageError) { if (typeof features.signMessageError === 'boolean') throw new UserRejectedRequestError( new Error('Failed to sign message.'), ) throw features.signMessageError } // Change `personal_sign` to `eth_sign` and swap params method = 'eth_sign' type Params = [data: Hex, address: Address] params = [(params as Params)[1], (params as Params)[0]] } const body = { method, params } const { error, result } = await rpc.http(url, { body }) if (error) throw new RpcRequestError({ body, error, url }) return result } return custom({ request })({ retryCount: 0 }) }, })) }