ethers
Version:
A complete and compact Ethereum library, for dapps, wallets and any other tools.
335 lines (269 loc) • 10.9 kB
text/typescript
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"));
}));
}
}