UNPKG

ethers

Version:

A complete and compact Ethereum library, for dapps, wallets and any other tools.

335 lines (269 loc) 10.9 kB
import { assertArgument, makeError } from "../utils/index.js"; import { JsonRpcApiPollingProvider } from "./provider-jsonrpc.js"; import type { JsonRpcApiProviderOptions, JsonRpcError, JsonRpcPayload, JsonRpcResult, JsonRpcSigner } from "./provider-jsonrpc.js"; import type { Network, Networkish } from "./network.js"; /** * The interface to an [[link-eip-1193]] provider, which is a standard * used by most injected providers, which the [[BrowserProvider]] accepts * and exposes the API of. */ export interface Eip1193Provider { /** * See [[link-eip-1193]] for details on this method. */ request(request: { method: string, params?: Array<any> | Record<string, any> }): Promise<any>; }; /** * The possible additional events dispatched when using the ``"debug"`` * event on a [[BrowserProvider]]. */ export type DebugEventBrowserProvider = { action: "sendEip1193Payload", payload: { method: string, params: Array<any> } } | { action: "receiveEip1193Result", result: any } | { action: "receiveEip1193Error", error: Error }; /** * Provider info provided by the [[link-eip-6963]] discovery mechanism. */ export interface Eip6963ProviderInfo { uuid: string; name: string; icon: string; rdns: string; } interface Eip6963ProviderDetail { info: Eip6963ProviderInfo; provider: Eip1193Provider; } interface Eip6963Announcement { type: "eip6963:announceProvider"; detail: Eip6963ProviderDetail } export type BrowserProviderOptions = { polling?: boolean; staticNetwork?: null | boolean | Network; cacheTimeout?: number; pollingInterval?: number; providerInfo?: Eip6963ProviderInfo; }; /** * Specifies how [[link-eip-6963]] discovery should proceed. * * See: [[BrowserProvider-discover]] */ export interface BrowserDiscoverOptions { /** * Override provider detection with this provider. */ provider?: Eip1193Provider; /** * Duration to wait to detect providers. (default: 300ms) */ timeout?: number; /** * Return the first detected provider. Otherwise wait for %%timeout%% * and allowing filtering before selecting the desired provider. */ anyProvider?: boolean; /** * Use the provided window context. Useful in non-standard * environments or to hijack where a provider comes from. */ window?: any; /** * Explicitly choose which provider to used once scanning is complete. */ filter?: (found: Array<Eip6963ProviderInfo>) => null | BrowserProvider | Eip6963ProviderInfo; } /** * A **BrowserProvider** is intended to wrap an injected provider which * adheres to the [[link-eip-1193]] standard, which most (if not all) * currently do. */ export class BrowserProvider extends JsonRpcApiPollingProvider { #request: (method: string, params: Array<any> | Record<string, any>) => Promise<any>; #providerInfo: null | Eip6963ProviderInfo; /** * Connect to the %%ethereum%% provider, optionally forcing the * %%network%%. */ constructor(ethereum: Eip1193Provider, network?: Networkish, _options?: BrowserProviderOptions) { // Copy the options const options: JsonRpcApiProviderOptions = Object.assign({ }, ((_options != null) ? _options: { }), { batchMaxCount: 1 }); assertArgument(ethereum && ethereum.request, "invalid EIP-1193 provider", "ethereum", ethereum); super(network, options); this.#providerInfo = null; if (_options && _options.providerInfo) { this.#providerInfo = _options.providerInfo; } this.#request = async (method: string, params: Array<any> | Record<string, any>) => { const payload = { method, params }; this.emit("debug", { action: "sendEip1193Request", payload }); try { const result = await ethereum.request(payload); this.emit("debug", { action: "receiveEip1193Result", result }); return result; } catch (e: any) { const error = new Error(e.message); (<any>error).code = e.code; (<any>error).data = e.data; (<any>error).payload = payload; this.emit("debug", { action: "receiveEip1193Error", error }); throw error; } }; } get providerInfo(): null | Eip6963ProviderInfo { return this.#providerInfo; } async send(method: string, params: Array<any> | Record<string, any>): Promise<any> { await this._start(); return await super.send(method, params); } async _send(payload: JsonRpcPayload | Array<JsonRpcPayload>): Promise<Array<JsonRpcResult | JsonRpcError>> { assertArgument(!Array.isArray(payload), "EIP-1193 does not support batch request", "payload", payload); try { const result = await this.#request(payload.method, payload.params || [ ]); return [ { id: payload.id, result } ]; } catch (e: any) { return [ { id: payload.id, error: { code: e.code, data: e.data, message: e.message } } ]; } } getRpcError(payload: JsonRpcPayload, error: JsonRpcError): Error { error = JSON.parse(JSON.stringify(error)); // EIP-1193 gives us some machine-readable error codes, so rewrite // them into Ethers standard errors. switch (error.error.code || -1) { case 4001: error.error.message = `ethers-user-denied: ${ error.error.message }`; break; case 4200: error.error.message = `ethers-unsupported: ${ error.error.message }`; break; } return super.getRpcError(payload, error); } /** * Resolves to ``true`` if the provider manages the %%address%%. */ async hasSigner(address: number | string): Promise<boolean> { if (address == null) { address = 0; } const accounts = await this.send("eth_accounts", [ ]); if (typeof(address) === "number") { return (accounts.length > address); } address = address.toLowerCase(); return accounts.filter((a: string) => (a.toLowerCase() === address)).length !== 0; } async getSigner(address?: number | string): Promise<JsonRpcSigner> { if (address == null) { address = 0; } if (!(await this.hasSigner(address))) { try { await this.#request("eth_requestAccounts", [ ]); } catch (error: any) { const payload = error.payload; throw this.getRpcError(payload, { id: payload.id, error }); } } return await super.getSigner(address); } /** * Discover and connect to a Provider in the Browser using the * [[link-eip-6963]] discovery mechanism. If no providers are * present, ``null`` is resolved. */ static async discover(options?: BrowserDiscoverOptions): Promise<null | BrowserProvider> { if (options == null) { options = { }; } if (options.provider) { return new BrowserProvider(options.provider); } const context = options.window ? options.window: (typeof(window) !== "undefined") ? window: null; if (context == null) { return null; } const anyProvider = options.anyProvider; if (anyProvider && context.ethereum) { return new BrowserProvider(context.ethereum); } if (!("addEventListener" in context && "dispatchEvent" in context && "removeEventListener" in context)) { return null; } const timeout = options.timeout ? options.timeout: 300; if (timeout === 0) { return null; } return await (new Promise((resolve, reject) => { let found: Array<Eip6963ProviderDetail> = [ ]; const addProvider = (event: Eip6963Announcement) => { found.push(event.detail); if (anyProvider) { finalize(); } }; const finalize = () => { clearTimeout(timer); if (found.length) { // If filtering is provided: if (options && options.filter) { // Call filter, with a copies of found provider infos const filtered = options.filter(found.map(i => Object.assign({ }, (i.info)))); if (filtered == null) { // No provider selected resolve(null); } else if (filtered instanceof BrowserProvider) { // Custom provider created resolve(filtered); } else { // Find the matching provider let match: null | Eip6963ProviderDetail = null; if (filtered.uuid) { const matches = found.filter(f => (filtered.uuid === f.info.uuid)); // @TODO: What should happen if multiple values // for the same UUID? match = matches[0]; } if (match) { const { provider, info } = match; resolve(new BrowserProvider(provider, undefined, { providerInfo: info })); } else { reject(makeError("filter returned unknown info", "UNSUPPORTED_OPERATION", { value: filtered })); } } } else { // Pick the first found provider const { provider, info } = found[0]; resolve(new BrowserProvider(provider, undefined, { providerInfo: info })); } } else { // Nothing found resolve(null); } context.removeEventListener(<any>"eip6963:announceProvider", addProvider); }; const timer = setTimeout(() => { finalize(); }, timeout); context.addEventListener(<any>"eip6963:announceProvider", addProvider); context.dispatchEvent(new Event("eip6963:requestProvider")); })); } }