diff --git a/src.ts/_tests/test-providers-data.ts b/src.ts/_tests/test-providers-data.ts index 1777e685a1..820f56f8aa 100644 --- a/src.ts/_tests/test-providers-data.ts +++ b/src.ts/_tests/test-providers-data.ts @@ -249,7 +249,7 @@ describe("Test Networks", function() { "base", "base-sepolia", "bnb", "bnbt", "linea", "linea-sepolia", - "matic", "matic-mumbai", "matic-amoy", + "matic", "matic-amoy", "optimism", "optimism-sepolia", "xdai", ]; diff --git a/src.ts/_tests/test-providers-wildcard.ts b/src.ts/_tests/test-providers-wildcard.ts index 0d75e89adf..939051e76a 100644 --- a/src.ts/_tests/test-providers-wildcard.ts +++ b/src.ts/_tests/test-providers-wildcard.ts @@ -24,11 +24,11 @@ describe("Test EIP-2544 ENS wildcards", function() { }); describe("Test ENS-DNS gasless resolver", function() { - it("Resolved almonit.org", async function() { + it("Resolved firefly.app", async function() { this.timeout(10000); const provider = connect("mainnet"); - const addr = await provider.resolveName("almonit.org"); - assert.equal(addr, "0x0D59d0f7DcC0fBF0A3305cE0261863aAf7Ab685c", "addr"); + const addr = await provider.resolveName("firefly.app"); + assert.equal(addr, "0x643aA0A61eADCC9Cc202D1915D942d35D005400C", "addr"); }); }); diff --git a/src.ts/abi/coders/abstract-coder.ts b/src.ts/abi/coders/abstract-coder.ts index 981b2b80a1..3a62e66dd8 100644 --- a/src.ts/abi/coders/abstract-coder.ts +++ b/src.ts/abi/coders/abstract-coder.ts @@ -3,6 +3,7 @@ import { defineProperties, concat, getBytesCopy, getNumber, hexlify, toBeArray, toBigInt, toNumber, assert, assertArgument + /*, isError*/ } from "../../utils/index.js"; import type { BigNumberish, BytesLike } from "../../utils/index.js"; @@ -19,12 +20,44 @@ const passProperties = [ "then" ]; const _guard = { }; +const resultNames: WeakMap> = new WeakMap(); + +function getNames(result: Result): ReadonlyArray { + return resultNames.get(result)!; +} +function setNames(result: Result, names: ReadonlyArray): void { + resultNames.set(result, names); +} + function throwError(name: string, error: Error): never { const wrapped = new Error(`deferred error during ABI decoding triggered accessing ${ name }`); (wrapped).error = error; throw wrapped; } +function toObject(names: ReadonlyArray, items: Result, deep?: boolean): Record | Array { + if (names.indexOf(null) >= 0) { + return items.map((item, index) => { + if (item instanceof Result) { + return toObject(getNames(item), item, deep); + } + return item; + }); + } + + return (>names).reduce((accum, name, index) => { + let item = items.getValue(name); + if (!(name in accum)) { + if (deep && item instanceof Result) { + item = toObject(getNames(item), item, deep); + } + accum[name] = item; + } + return accum; + }, >{ }); +} + + /** * A [[Result]] is a sub-class of Array, which allows accessing any * of its values either positionally by its index or, if keys are @@ -33,6 +66,9 @@ function throwError(name: string, error: Error): never { * @_docloc: api/abi */ export class Result extends Array { + // No longer used; but cannot be removed as it will remove the + // #private field from the .d.ts which may break backwards + // compatibility readonly #names: ReadonlyArray; [ K: string | number ]: any @@ -73,13 +109,17 @@ export class Result extends Array { }, >(new Map())); // Remove any key thats not unique - this.#names = Object.freeze(items.map((item, index) => { + setNames(this, Object.freeze(items.map((item, index) => { const name = names[index]; if (name != null && nameCounts.get(name) === 1) { return name; } return null; - })); + }))); + + // Dummy operations to prevent TypeScript from complaining + this.#names = [ ]; + if (this.#names == null) { void(this.#names); } if (!wrap) { return; } @@ -87,7 +127,7 @@ export class Result extends Array { Object.freeze(this); // Proxy indices and names so we can trap deferred errors - return new Proxy(this, { + const proxy = new Proxy(this, { get: (target, prop, receiver) => { if (typeof(prop) === "string") { @@ -127,6 +167,7 @@ export class Result extends Array { return Reflect.get(target, prop, receiver); } }); + setNames(proxy, getNames(this)); } /** @@ -157,21 +198,14 @@ export class Result extends Array { * any outstanding deferred errors. */ toObject(deep?: boolean): Record { - return this.#names.reduce((accum, name, index) => { - assert(name != null, "value at index ${ index } unnamed", "UNSUPPORTED_OPERATION", { + const names = getNames(this); + return names.reduce((accum, name, index) => { + + assert(name != null, `value at index ${ index } unnamed`, "UNSUPPORTED_OPERATION", { operation: "toObject()" }); - // Add values for names that don't conflict - if (!(name in accum)) { - let child = this.getValue(name); - if (deep && child instanceof Result) { - child = child.toObject(deep); - } - accum[name] = child; - } - - return accum; + return toObject(names, this, deep); }, >{}); } @@ -192,10 +226,12 @@ export class Result extends Array { } if (end > this.length) { end = this.length; } + const _names = getNames(this); + const result: Array = [ ], names: Array = [ ]; for (let i = start; i < end; i++) { result.push(this[i]); - names.push(this.#names[i]); + names.push(_names[i]); } return new Result(_guard, result, names); @@ -205,6 +241,8 @@ export class Result extends Array { * @_ignore */ filter(callback: (el: any, index: number, array: Result) => boolean, thisArg?: any): Result { + const _names = getNames(this); + const result: Array = [ ], names: Array = [ ]; for (let i = 0; i < this.length; i++) { const item = this[i]; @@ -214,7 +252,7 @@ export class Result extends Array { if (callback.call(thisArg, item, i, this)) { result.push(item); - names.push(this.#names[i]); + names.push(_names[i]); } } @@ -248,7 +286,7 @@ export class Result extends Array { * accessible by name. */ getValue(name: string): any { - const index = this.#names.indexOf(name); + const index = getNames(this).indexOf(name); if (index === -1) { return undefined; } const value = this[index]; diff --git a/src.ts/ethers.ts b/src.ts/ethers.ts index a95fe94dbf..c989eeb812 100644 --- a/src.ts/ethers.ts +++ b/src.ts/ethers.ts @@ -164,7 +164,7 @@ export type { TypedDataDomain, TypedDataField } from "./hash/index.js"; export type { Provider, Signer, - AbstractProviderOptions, FallbackProviderOptions, + AbstractProviderOptions, BrowserProviderOptions, FallbackProviderOptions, AbstractProviderPlugin, BlockParams, BlockTag, ContractRunner, DebugEventBrowserProvider, Eip1193Provider, EventFilter, Filter, FilterByBlockHash, GasCostParameters, diff --git a/src.ts/providers/index.ts b/src.ts/providers/index.ts index c0a33dc8aa..47316dd2a1 100644 --- a/src.ts/providers/index.ts +++ b/src.ts/providers/index.ts @@ -114,7 +114,7 @@ export type { } from "./provider.js"; export type { - DebugEventBrowserProvider, Eip1193Provider + BrowserProviderOptions, DebugEventBrowserProvider, Eip1193Provider } from "./provider-browser.js"; export type { FallbackProviderOptions } from "./provider-fallback.js"; diff --git a/src.ts/providers/provider-browser.ts b/src.ts/providers/provider-browser.ts index 8b45957625..9db47045ae 100644 --- a/src.ts/providers/provider-browser.ts +++ b/src.ts/providers/provider-browser.ts @@ -3,10 +3,11 @@ import { assertArgument } from "../utils/index.js"; import { JsonRpcApiPollingProvider } from "./provider-jsonrpc.js"; import type { + JsonRpcApiProviderOptions, JsonRpcError, JsonRpcPayload, JsonRpcResult, JsonRpcSigner } from "./provider-jsonrpc.js"; -import type { Networkish } from "./network.js"; +import type { Network, Networkish } from "./network.js"; /** * The interface to an [[link-eip-1193]] provider, which is a standard @@ -35,6 +36,13 @@ export type DebugEventBrowserProvider = { error: Error }; +export type BrowserProviderOptions = { + polling?: boolean; + staticNetwork?: null | boolean | Network; + + cacheTimeout?: number; + pollingInterval?: number; +}; /** * A **BrowserProvider** is intended to wrap an injected provider which @@ -48,10 +56,15 @@ export class BrowserProvider extends JsonRpcApiPollingProvider { * Connnect to the %%ethereum%% provider, optionally forcing the * %%network%%. */ - constructor(ethereum: Eip1193Provider, network?: Networkish) { + 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, { batchMaxCount: 1 }); + super(network, options); this.#request = async (method: string, params: Array | Record) => { const payload = { method, params }; diff --git a/src.ts/utils/geturl-browser.ts b/src.ts/utils/geturl-browser.ts index a71abb2553..375d804691 100644 --- a/src.ts/utils/geturl-browser.ts +++ b/src.ts/utils/geturl-browser.ts @@ -1,4 +1,4 @@ -import { assert } from "./errors.js"; +import { assert, makeError } from "./errors.js"; import type { FetchGetUrlFunc, FetchRequest, FetchCancelSignal, GetUrlResponse @@ -27,11 +27,11 @@ declare global { function fetch(url: string, init: FetchInit): Promise; } -// @TODO: timeout is completely ignored; start a Promise.any with a reject? - export function createGetUrl(options?: Record): FetchGetUrlFunc { async function getUrl(req: FetchRequest, _signal?: FetchCancelSignal): Promise { + assert(_signal == null || !_signal.cancelled, "request cancelled before sending", "CANCELLED"); + const protocol = req.url.split(":")[0].toLowerCase(); assert(protocol === "http" || protocol === "https", `unsupported protocol ${ protocol }`, "UNSUPPORTED_OPERATION", { @@ -43,21 +43,39 @@ export function createGetUrl(options?: Record): FetchGetUrlFunc { operation: "request" }); - let signal: undefined | AbortSignal = undefined; + let error: null | Error = null; + + const controller = new AbortController(); + + const timer = setTimeout(() => { + error = makeError("request timeout", "TIMEOUT"); + controller.abort(); + }, req.timeout); + if (_signal) { - const controller = new AbortController(); - signal = controller.signal; - _signal.addListener(() => { controller.abort(); }); + _signal.addListener(() => { + error = makeError("request cancelled", "CANCELLED"); + controller.abort(); + }); } const init = { method: req.method, headers: new Headers(Array.from(req)), body: req.body || undefined, - signal + signal: controller.signal }; - const resp = await fetch(req.url, init); + let resp: Awaited>; + try { + resp = await fetch(req.url, init); + } catch (_error) { + clearTimeout(timer); + if (error) { throw error; } + throw _error; + } + + clearTimeout(timer); const headers: Record = { }; resp.headers.forEach((value, key) => { diff --git a/src.ts/utils/geturl.ts b/src.ts/utils/geturl.ts index b7220acd24..571eaaa731 100644 --- a/src.ts/utils/geturl.ts +++ b/src.ts/utils/geturl.ts @@ -2,7 +2,7 @@ import http from "http"; import https from "https"; import { gunzipSync } from "zlib"; -import { assert } from "./errors.js"; +import { assert, makeError } from "./errors.js"; import { getBytes } from "./data.js"; import type { @@ -15,6 +15,8 @@ import type { export function createGetUrl(options?: Record): FetchGetUrlFunc { async function getUrl(req: FetchRequest, signal?: FetchCancelSignal): Promise { + // Make sure we weren't cancelled before sending + assert(signal == null || !signal.cancelled, "request cancelled before sending", "CANCELLED"); const protocol = req.url.split(":")[0].toLowerCase(); @@ -35,6 +37,13 @@ export function createGetUrl(options?: Record): FetchGetUrlFunc { if (options.agent) { reqOptions.agent = options.agent; } } + // Create a Node-specific AbortController, if available + let abort: null | AbortController = null; + try { + abort = new AbortController(); + reqOptions.abort = abort.signal; + } catch (e) { console.log(e); } + const request = ((protocol === "http") ? http: https).request(req.url, reqOptions); request.setTimeout(req.timeout); @@ -45,8 +54,17 @@ export function createGetUrl(options?: Record): FetchGetUrlFunc { request.end(); return new Promise((resolve, reject) => { - // @TODO: Node 15 added AbortSignal; once we drop support for - // Node14, we can add that in here too + + if (signal) { + signal.addListener(() => { + if (abort) { abort.abort(); } + reject(makeError("request cancelled", "CANCELLED")); + }); + } + + request.on("timeout", () => { + reject(makeError("request timeout", "TIMEOUT")); + }); request.once("response", (resp: http.IncomingMessage) => { const statusCode = resp.statusCode || 0;