UNPKG

ethers

Version:

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

1,404 lines (1,393 loc) 1.1 MB
const __$G = (typeof globalThis !== 'undefined' ? globalThis: typeof window !== 'undefined' ? window: typeof global !== 'undefined' ? global: typeof self !== 'undefined' ? self: {}); (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ethers = {})); })(this, (function (exports) { 'use strict'; /* Do NOT modify this file; see /src.ts/_admin/update-version.ts */ /** * The current version of Ethers. */ const version = "6.13.7"; /** * Property helper functions. * * @_subsection api/utils:Properties [about-properties] */ function checkType(value, type, name) { const types = type.split("|").map(t => t.trim()); for (let i = 0; i < types.length; i++) { switch (type) { case "any": return; case "bigint": case "boolean": case "number": case "string": if (typeof (value) === type) { return; } } } const error = new Error(`invalid value for type ${type}`); error.code = "INVALID_ARGUMENT"; error.argument = `value.${name}`; error.value = value; throw error; } /** * Resolves to a new object that is a copy of %%value%%, but with all * values resolved. */ async function resolveProperties(value) { const keys = Object.keys(value); const results = await Promise.all(keys.map((k) => Promise.resolve(value[k]))); return results.reduce((accum, v, index) => { accum[keys[index]] = v; return accum; }, {}); } /** * Assigns the %%values%% to %%target%% as read-only values. * * It %%types%% is specified, the values are checked. */ function defineProperties(target, values, types) { for (let key in values) { let value = values[key]; const type = (types ? types[key] : null); if (type) { checkType(value, type, key); } Object.defineProperty(target, key, { enumerable: true, value, writable: false }); } } /** * All errors in ethers include properties to ensure they are both * human-readable (i.e. ``.message``) and machine-readable (i.e. ``.code``). * * The [[isError]] function can be used to check the error ``code`` and * provide a type guard for the properties present on that error interface. * * @_section: api/utils/errors:Errors [about-errors] */ function stringify$1(value) { if (value == null) { return "null"; } if (Array.isArray(value)) { return "[ " + (value.map(stringify$1)).join(", ") + " ]"; } if (value instanceof Uint8Array) { const HEX = "0123456789abcdef"; let result = "0x"; for (let i = 0; i < value.length; i++) { result += HEX[value[i] >> 4]; result += HEX[value[i] & 0xf]; } return result; } if (typeof (value) === "object" && typeof (value.toJSON) === "function") { return stringify$1(value.toJSON()); } switch (typeof (value)) { case "boolean": case "symbol": return value.toString(); case "bigint": return BigInt(value).toString(); case "number": return (value).toString(); case "string": return JSON.stringify(value); case "object": { const keys = Object.keys(value); keys.sort(); return "{ " + keys.map((k) => `${stringify$1(k)}: ${stringify$1(value[k])}`).join(", ") + " }"; } } return `[ COULD NOT SERIALIZE ]`; } /** * Returns true if the %%error%% matches an error thrown by ethers * that matches the error %%code%%. * * In TypeScript environments, this can be used to check that %%error%% * matches an EthersError type, which means the expected properties will * be set. * * @See [ErrorCodes](api:ErrorCode) * @example * try { * // code.... * } catch (e) { * if (isError(e, "CALL_EXCEPTION")) { * // The Type Guard has validated this object * console.log(e.data); * } * } */ function isError(error, code) { return (error && error.code === code); } /** * Returns true if %%error%% is a [[CallExceptionError]. */ function isCallException(error) { return isError(error, "CALL_EXCEPTION"); } /** * Returns a new Error configured to the format ethers emits errors, with * the %%message%%, [[api:ErrorCode]] %%code%% and additional properties * for the corresponding EthersError. * * Each error in ethers includes the version of ethers, a * machine-readable [[ErrorCode]], and depending on %%code%%, additional * required properties. The error message will also include the %%message%%, * ethers version, %%code%% and all additional properties, serialized. */ function makeError(message, code, info) { let shortMessage = message; { const details = []; if (info) { if ("message" in info || "code" in info || "name" in info) { throw new Error(`value will overwrite populated values: ${stringify$1(info)}`); } for (const key in info) { if (key === "shortMessage") { continue; } const value = (info[key]); // try { details.push(key + "=" + stringify$1(value)); // } catch (error: any) { // console.log("MMM", error.message); // details.push(key + "=[could not serialize object]"); // } } } details.push(`code=${code}`); details.push(`version=${version}`); if (details.length) { message += " (" + details.join(", ") + ")"; } } let error; switch (code) { case "INVALID_ARGUMENT": error = new TypeError(message); break; case "NUMERIC_FAULT": case "BUFFER_OVERRUN": error = new RangeError(message); break; default: error = new Error(message); } defineProperties(error, { code }); if (info) { Object.assign(error, info); } if (error.shortMessage == null) { defineProperties(error, { shortMessage }); } return error; } /** * Throws an EthersError with %%message%%, %%code%% and additional error * %%info%% when %%check%% is falsish.. * * @see [[api:makeError]] */ function assert(check, message, code, info) { if (!check) { throw makeError(message, code, info); } } /** * A simple helper to simply ensuring provided arguments match expected * constraints, throwing if not. * * In TypeScript environments, the %%check%% has been asserted true, so * any further code does not need additional compile-time checks. */ function assertArgument(check, message, name, value) { assert(check, message, "INVALID_ARGUMENT", { argument: name, value: value }); } function assertArgumentCount(count, expectedCount, message) { if (message == null) { message = ""; } if (message) { message = ": " + message; } assert(count >= expectedCount, "missing argument" + message, "MISSING_ARGUMENT", { count: count, expectedCount: expectedCount }); assert(count <= expectedCount, "too many arguments" + message, "UNEXPECTED_ARGUMENT", { count: count, expectedCount: expectedCount }); } const _normalizeForms = ["NFD", "NFC", "NFKD", "NFKC"].reduce((accum, form) => { try { // General test for normalize /* c8 ignore start */ if ("test".normalize(form) !== "test") { throw new Error("bad"); } ; /* c8 ignore stop */ if (form === "NFD") { const check = String.fromCharCode(0xe9).normalize("NFD"); const expected = String.fromCharCode(0x65, 0x0301); /* c8 ignore start */ if (check !== expected) { throw new Error("broken"); } /* c8 ignore stop */ } accum.push(form); } catch (error) { } return accum; }, []); /** * Throws if the normalization %%form%% is not supported. */ function assertNormalize(form) { assert(_normalizeForms.indexOf(form) >= 0, "platform missing String.prototype.normalize", "UNSUPPORTED_OPERATION", { operation: "String.prototype.normalize", info: { form } }); } /** * Many classes use file-scoped values to guard the constructor, * making it effectively private. This facilitates that pattern * by ensuring the %%givenGaurd%% matches the file-scoped %%guard%%, * throwing if not, indicating the %%className%% if provided. */ function assertPrivate(givenGuard, guard, className) { if (className == null) { className = ""; } if (givenGuard !== guard) { let method = className, operation = "new"; if (className) { method += "."; operation += " " + className; } assert(false, `private constructor; use ${method}from* methods`, "UNSUPPORTED_OPERATION", { operation }); } } /** * Some data helpers. * * * @_subsection api/utils:Data Helpers [about-data] */ function _getBytes(value, name, copy) { if (value instanceof Uint8Array) { if (copy) { return new Uint8Array(value); } return value; } if (typeof (value) === "string" && value.match(/^0x(?:[0-9a-f][0-9a-f])*$/i)) { const result = new Uint8Array((value.length - 2) / 2); let offset = 2; for (let i = 0; i < result.length; i++) { result[i] = parseInt(value.substring(offset, offset + 2), 16); offset += 2; } return result; } assertArgument(false, "invalid BytesLike value", name || "value", value); } /** * Get a typed Uint8Array for %%value%%. If already a Uint8Array * the original %%value%% is returned; if a copy is required use * [[getBytesCopy]]. * * @see: getBytesCopy */ function getBytes(value, name) { return _getBytes(value, name, false); } /** * Get a typed Uint8Array for %%value%%, creating a copy if necessary * to prevent any modifications of the returned value from being * reflected elsewhere. * * @see: getBytes */ function getBytesCopy(value, name) { return _getBytes(value, name, true); } /** * Returns true if %%value%% is a valid [[HexString]]. * * If %%length%% is ``true`` or a //number//, it also checks that * %%value%% is a valid [[DataHexString]] of %%length%% (if a //number//) * bytes of data (e.g. ``0x1234`` is 2 bytes). */ function isHexString(value, length) { if (typeof (value) !== "string" || !value.match(/^0x[0-9A-Fa-f]*$/)) { return false; } if (typeof (length) === "number" && value.length !== 2 + 2 * length) { return false; } if (length === true && (value.length % 2) !== 0) { return false; } return true; } /** * Returns true if %%value%% is a valid representation of arbitrary * data (i.e. a valid [[DataHexString]] or a Uint8Array). */ function isBytesLike(value) { return (isHexString(value, true) || (value instanceof Uint8Array)); } const HexCharacters = "0123456789abcdef"; /** * Returns a [[DataHexString]] representation of %%data%%. */ function hexlify(data) { const bytes = getBytes(data); let result = "0x"; for (let i = 0; i < bytes.length; i++) { const v = bytes[i]; result += HexCharacters[(v & 0xf0) >> 4] + HexCharacters[v & 0x0f]; } return result; } /** * Returns a [[DataHexString]] by concatenating all values * within %%data%%. */ function concat(datas) { return "0x" + datas.map((d) => hexlify(d).substring(2)).join(""); } /** * Returns the length of %%data%%, in bytes. */ function dataLength(data) { if (isHexString(data, true)) { return (data.length - 2) / 2; } return getBytes(data).length; } /** * Returns a [[DataHexString]] by slicing %%data%% from the %%start%% * offset to the %%end%% offset. * * By default %%start%% is 0 and %%end%% is the length of %%data%%. */ function dataSlice(data, start, end) { const bytes = getBytes(data); if (end != null && end > bytes.length) { assert(false, "cannot slice beyond data bounds", "BUFFER_OVERRUN", { buffer: bytes, length: bytes.length, offset: end }); } return hexlify(bytes.slice((start == null) ? 0 : start, (end == null) ? bytes.length : end)); } /** * Return the [[DataHexString]] result by stripping all **leading** ** zero bytes from %%data%%. */ function stripZerosLeft(data) { let bytes = hexlify(data).substring(2); while (bytes.startsWith("00")) { bytes = bytes.substring(2); } return "0x" + bytes; } function zeroPad(data, length, left) { const bytes = getBytes(data); assert(length >= bytes.length, "padding exceeds data length", "BUFFER_OVERRUN", { buffer: new Uint8Array(bytes), length: length, offset: length + 1 }); const result = new Uint8Array(length); result.fill(0); if (left) { result.set(bytes, length - bytes.length); } else { result.set(bytes, 0); } return hexlify(result); } /** * Return the [[DataHexString]] of %%data%% padded on the **left** * to %%length%% bytes. * * If %%data%% already exceeds %%length%%, a [[BufferOverrunError]] is * thrown. * * This pads data the same as **values** are in Solidity * (e.g. ``uint128``). */ function zeroPadValue(data, length) { return zeroPad(data, length, true); } /** * Return the [[DataHexString]] of %%data%% padded on the **right** * to %%length%% bytes. * * If %%data%% already exceeds %%length%%, a [[BufferOverrunError]] is * thrown. * * This pads data the same as **bytes** are in Solidity * (e.g. ``bytes16``). */ function zeroPadBytes(data, length) { return zeroPad(data, length, false); } /** * Some mathematic operations. * * @_subsection: api/utils:Math Helpers [about-maths] */ const BN_0$a = BigInt(0); const BN_1$5 = BigInt(1); //const BN_Max256 = (BN_1 << BigInt(256)) - BN_1; // IEEE 754 support 53-bits of mantissa const maxValue = 0x1fffffffffffff; /** * Convert %%value%% from a twos-compliment representation of %%width%% * bits to its value. * * If the highest bit is ``1``, the result will be negative. */ function fromTwos(_value, _width) { const value = getUint(_value, "value"); const width = BigInt(getNumber(_width, "width")); assert((value >> width) === BN_0$a, "overflow", "NUMERIC_FAULT", { operation: "fromTwos", fault: "overflow", value: _value }); // Top bit set; treat as a negative value if (value >> (width - BN_1$5)) { const mask = (BN_1$5 << width) - BN_1$5; return -(((~value) & mask) + BN_1$5); } return value; } /** * Convert %%value%% to a twos-compliment representation of * %%width%% bits. * * The result will always be positive. */ function toTwos(_value, _width) { let value = getBigInt(_value, "value"); const width = BigInt(getNumber(_width, "width")); const limit = (BN_1$5 << (width - BN_1$5)); if (value < BN_0$a) { value = -value; assert(value <= limit, "too low", "NUMERIC_FAULT", { operation: "toTwos", fault: "overflow", value: _value }); const mask = (BN_1$5 << width) - BN_1$5; return ((~value) & mask) + BN_1$5; } else { assert(value < limit, "too high", "NUMERIC_FAULT", { operation: "toTwos", fault: "overflow", value: _value }); } return value; } /** * Mask %%value%% with a bitmask of %%bits%% ones. */ function mask(_value, _bits) { const value = getUint(_value, "value"); const bits = BigInt(getNumber(_bits, "bits")); return value & ((BN_1$5 << bits) - BN_1$5); } /** * Gets a BigInt from %%value%%. If it is an invalid value for * a BigInt, then an ArgumentError will be thrown for %%name%%. */ function getBigInt(value, name) { switch (typeof (value)) { case "bigint": return value; case "number": assertArgument(Number.isInteger(value), "underflow", name || "value", value); assertArgument(value >= -maxValue && value <= maxValue, "overflow", name || "value", value); return BigInt(value); case "string": try { if (value === "") { throw new Error("empty string"); } if (value[0] === "-" && value[1] !== "-") { return -BigInt(value.substring(1)); } return BigInt(value); } catch (e) { assertArgument(false, `invalid BigNumberish string: ${e.message}`, name || "value", value); } } assertArgument(false, "invalid BigNumberish value", name || "value", value); } /** * Returns %%value%% as a bigint, validating it is valid as a bigint * value and that it is positive. */ function getUint(value, name) { const result = getBigInt(value, name); assert(result >= BN_0$a, "unsigned value cannot be negative", "NUMERIC_FAULT", { fault: "overflow", operation: "getUint", value }); return result; } const Nibbles$1 = "0123456789abcdef"; /* * Converts %%value%% to a BigInt. If %%value%% is a Uint8Array, it * is treated as Big Endian data. */ function toBigInt(value) { if (value instanceof Uint8Array) { let result = "0x0"; for (const v of value) { result += Nibbles$1[v >> 4]; result += Nibbles$1[v & 0x0f]; } return BigInt(result); } return getBigInt(value); } /** * Gets a //number// from %%value%%. If it is an invalid value for * a //number//, then an ArgumentError will be thrown for %%name%%. */ function getNumber(value, name) { switch (typeof (value)) { case "bigint": assertArgument(value >= -maxValue && value <= maxValue, "overflow", name || "value", value); return Number(value); case "number": assertArgument(Number.isInteger(value), "underflow", name || "value", value); assertArgument(value >= -maxValue && value <= maxValue, "overflow", name || "value", value); return value; case "string": try { if (value === "") { throw new Error("empty string"); } return getNumber(BigInt(value), name); } catch (e) { assertArgument(false, `invalid numeric string: ${e.message}`, name || "value", value); } } assertArgument(false, "invalid numeric value", name || "value", value); } /** * Converts %%value%% to a number. If %%value%% is a Uint8Array, it * is treated as Big Endian data. Throws if the value is not safe. */ function toNumber(value) { return getNumber(toBigInt(value)); } /** * Converts %%value%% to a Big Endian hexstring, optionally padded to * %%width%% bytes. */ function toBeHex(_value, _width) { const value = getUint(_value, "value"); let result = value.toString(16); if (_width == null) { // Ensure the value is of even length if (result.length % 2) { result = "0" + result; } } else { const width = getNumber(_width, "width"); assert(width * 2 >= result.length, `value exceeds width (${width} bytes)`, "NUMERIC_FAULT", { operation: "toBeHex", fault: "overflow", value: _value }); // Pad the value to the required width while (result.length < (width * 2)) { result = "0" + result; } } return "0x" + result; } /** * Converts %%value%% to a Big Endian Uint8Array. */ function toBeArray(_value) { const value = getUint(_value, "value"); if (value === BN_0$a) { return new Uint8Array([]); } let hex = value.toString(16); if (hex.length % 2) { hex = "0" + hex; } const result = new Uint8Array(hex.length / 2); for (let i = 0; i < result.length; i++) { const offset = i * 2; result[i] = parseInt(hex.substring(offset, offset + 2), 16); } return result; } /** * Returns a [[HexString]] for %%value%% safe to use as a //Quantity//. * * A //Quantity// does not have and leading 0 values unless the value is * the literal value `0x0`. This is most commonly used for JSSON-RPC * numeric values. */ function toQuantity(value) { let result = hexlify(isBytesLike(value) ? value : toBeArray(value)).substring(2); while (result.startsWith("0")) { result = result.substring(1); } if (result === "") { result = "0"; } return "0x" + result; } /** * The [Base58 Encoding](link-base58) scheme allows a **numeric** value * to be encoded as a compact string using a radix of 58 using only * alpha-numeric characters. Confusingly similar characters are omitted * (i.e. ``"l0O"``). * * Note that Base58 encodes a **numeric** value, not arbitrary bytes, * since any zero-bytes on the left would get removed. To mitigate this * issue most schemes that use Base58 choose specific high-order values * to ensure non-zero prefixes. * * @_subsection: api/utils:Base58 Encoding [about-base58] */ const Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; let Lookup = null; function getAlpha(letter) { if (Lookup == null) { Lookup = {}; for (let i = 0; i < Alphabet.length; i++) { Lookup[Alphabet[i]] = BigInt(i); } } const result = Lookup[letter]; assertArgument(result != null, `invalid base58 value`, "letter", letter); return result; } const BN_0$9 = BigInt(0); const BN_58 = BigInt(58); /** * Encode %%value%% as a Base58-encoded string. */ function encodeBase58(_value) { const bytes = getBytes(_value); let value = toBigInt(bytes); let result = ""; while (value) { result = Alphabet[Number(value % BN_58)] + result; value /= BN_58; } // Account for leading padding zeros for (let i = 0; i < bytes.length; i++) { if (bytes[i]) { break; } result = Alphabet[0] + result; } return result; } /** * Decode the Base58-encoded %%value%%. */ function decodeBase58(value) { let result = BN_0$9; for (let i = 0; i < value.length; i++) { result *= BN_58; result += getAlpha(value[i]); } return result; } // utils/base64-browser function decodeBase64(textData) { textData = atob(textData); const data = new Uint8Array(textData.length); for (let i = 0; i < textData.length; i++) { data[i] = textData.charCodeAt(i); } return getBytes(data); } function encodeBase64(_data) { const data = getBytes(_data); let textData = ""; for (let i = 0; i < data.length; i++) { textData += String.fromCharCode(data[i]); } return btoa(textData); } /** * Events allow for applications to use the observer pattern, which * allows subscribing and publishing events, outside the normal * execution paths. * * @_section api/utils/events:Events [about-events] */ /** * When an [[EventEmitterable]] triggers a [[Listener]], the * callback always ahas one additional argument passed, which is * an **EventPayload**. */ class EventPayload { /** * The event filter. */ filter; /** * The **EventEmitterable**. */ emitter; #listener; /** * Create a new **EventPayload** for %%emitter%% with * the %%listener%% and for %%filter%%. */ constructor(emitter, listener, filter) { this.#listener = listener; defineProperties(this, { emitter, filter }); } /** * Unregister the triggered listener for future events. */ async removeListener() { if (this.#listener == null) { return; } await this.emitter.off(this.filter, this.#listener); } } /** * Using strings in Ethereum (or any security-basd system) requires * additional care. These utilities attempt to mitigate some of the * safety issues as well as provide the ability to recover and analyse * strings. * * @_subsection api/utils:Strings and UTF-8 [about-strings] */ function errorFunc(reason, offset, bytes, output, badCodepoint) { assertArgument(false, `invalid codepoint at offset ${offset}; ${reason}`, "bytes", bytes); } function ignoreFunc(reason, offset, bytes, output, badCodepoint) { // If there is an invalid prefix (including stray continuation), skip any additional continuation bytes if (reason === "BAD_PREFIX" || reason === "UNEXPECTED_CONTINUE") { let i = 0; for (let o = offset + 1; o < bytes.length; o++) { if (bytes[o] >> 6 !== 0x02) { break; } i++; } return i; } // This byte runs us past the end of the string, so just jump to the end // (but the first byte was read already read and therefore skipped) if (reason === "OVERRUN") { return bytes.length - offset - 1; } // Nothing to skip return 0; } function replaceFunc(reason, offset, bytes, output, badCodepoint) { // Overlong representations are otherwise "valid" code points; just non-deistingtished if (reason === "OVERLONG") { assertArgument(typeof (badCodepoint) === "number", "invalid bad code point for replacement", "badCodepoint", badCodepoint); output.push(badCodepoint); return 0; } // Put the replacement character into the output output.push(0xfffd); // Otherwise, process as if ignoring errors return ignoreFunc(reason, offset, bytes); } /** * A handful of popular, built-in UTF-8 error handling strategies. * * **``"error"``** - throws on ANY illegal UTF-8 sequence or * non-canonical (overlong) codepoints (this is the default) * * **``"ignore"``** - silently drops any illegal UTF-8 sequence * and accepts non-canonical (overlong) codepoints * * **``"replace"``** - replace any illegal UTF-8 sequence with the * UTF-8 replacement character (i.e. ``"\\ufffd"``) and accepts * non-canonical (overlong) codepoints * * @returns: Record<"error" | "ignore" | "replace", Utf8ErrorFunc> */ const Utf8ErrorFuncs = Object.freeze({ error: errorFunc, ignore: ignoreFunc, replace: replaceFunc }); // http://stackoverflow.com/questions/13356493/decode-utf-8-with-javascript#13691499 function getUtf8CodePoints(_bytes, onError) { if (onError == null) { onError = Utf8ErrorFuncs.error; } const bytes = getBytes(_bytes, "bytes"); const result = []; let i = 0; // Invalid bytes are ignored while (i < bytes.length) { const c = bytes[i++]; // 0xxx xxxx if (c >> 7 === 0) { result.push(c); continue; } // Multibyte; how many bytes left for this character? let extraLength = null; let overlongMask = null; // 110x xxxx 10xx xxxx if ((c & 0xe0) === 0xc0) { extraLength = 1; overlongMask = 0x7f; // 1110 xxxx 10xx xxxx 10xx xxxx } else if ((c & 0xf0) === 0xe0) { extraLength = 2; overlongMask = 0x7ff; // 1111 0xxx 10xx xxxx 10xx xxxx 10xx xxxx } else if ((c & 0xf8) === 0xf0) { extraLength = 3; overlongMask = 0xffff; } else { if ((c & 0xc0) === 0x80) { i += onError("UNEXPECTED_CONTINUE", i - 1, bytes, result); } else { i += onError("BAD_PREFIX", i - 1, bytes, result); } continue; } // Do we have enough bytes in our data? if (i - 1 + extraLength >= bytes.length) { i += onError("OVERRUN", i - 1, bytes, result); continue; } // Remove the length prefix from the char let res = c & ((1 << (8 - extraLength - 1)) - 1); for (let j = 0; j < extraLength; j++) { let nextChar = bytes[i]; // Invalid continuation byte if ((nextChar & 0xc0) != 0x80) { i += onError("MISSING_CONTINUE", i, bytes, result); res = null; break; } res = (res << 6) | (nextChar & 0x3f); i++; } // See above loop for invalid continuation byte if (res === null) { continue; } // Maximum code point if (res > 0x10ffff) { i += onError("OUT_OF_RANGE", i - 1 - extraLength, bytes, result, res); continue; } // Reserved for UTF-16 surrogate halves if (res >= 0xd800 && res <= 0xdfff) { i += onError("UTF16_SURROGATE", i - 1 - extraLength, bytes, result, res); continue; } // Check for overlong sequences (more bytes than needed) if (res <= overlongMask) { i += onError("OVERLONG", i - 1 - extraLength, bytes, result, res); continue; } result.push(res); } return result; } // http://stackoverflow.com/questions/18729405/how-to-convert-utf8-string-to-byte-array /** * Returns the UTF-8 byte representation of %%str%%. * * If %%form%% is specified, the string is normalized. */ function toUtf8Bytes(str, form) { assertArgument(typeof (str) === "string", "invalid string value", "str", str); if (form != null) { assertNormalize(form); str = str.normalize(form); } let result = []; for (let i = 0; i < str.length; i++) { const c = str.charCodeAt(i); if (c < 0x80) { result.push(c); } else if (c < 0x800) { result.push((c >> 6) | 0xc0); result.push((c & 0x3f) | 0x80); } else if ((c & 0xfc00) == 0xd800) { i++; const c2 = str.charCodeAt(i); assertArgument(i < str.length && ((c2 & 0xfc00) === 0xdc00), "invalid surrogate pair", "str", str); // Surrogate Pair const pair = 0x10000 + ((c & 0x03ff) << 10) + (c2 & 0x03ff); result.push((pair >> 18) | 0xf0); result.push(((pair >> 12) & 0x3f) | 0x80); result.push(((pair >> 6) & 0x3f) | 0x80); result.push((pair & 0x3f) | 0x80); } else { result.push((c >> 12) | 0xe0); result.push(((c >> 6) & 0x3f) | 0x80); result.push((c & 0x3f) | 0x80); } } return new Uint8Array(result); } //export function _toUtf8String(codePoints) { return codePoints.map((codePoint) => { if (codePoint <= 0xffff) { return String.fromCharCode(codePoint); } codePoint -= 0x10000; return String.fromCharCode((((codePoint >> 10) & 0x3ff) + 0xd800), ((codePoint & 0x3ff) + 0xdc00)); }).join(""); } /** * Returns the string represented by the UTF-8 data %%bytes%%. * * When %%onError%% function is specified, it is called on UTF-8 * errors allowing recovery using the [[Utf8ErrorFunc]] API. * (default: [error](Utf8ErrorFuncs)) */ function toUtf8String(bytes, onError) { return _toUtf8String(getUtf8CodePoints(bytes, onError)); } /** * Returns the UTF-8 code-points for %%str%%. * * If %%form%% is specified, the string is normalized. */ function toUtf8CodePoints(str, form) { return getUtf8CodePoints(toUtf8Bytes(str, form)); } function createGetUrl(options) { async function getUrl(req, _signal) { 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", { info: { protocol }, operation: "request" }); assert(protocol === "https" || !req.credentials || req.allowInsecureAuthentication, "insecure authorized connections unsupported", "UNSUPPORTED_OPERATION", { operation: "request" }); let error = null; const controller = new AbortController(); const timer = setTimeout(() => { error = makeError("request timeout", "TIMEOUT"); controller.abort(); }, req.timeout); if (_signal) { _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: controller.signal }; let resp; try { resp = await fetch(req.url, init); } catch (_error) { clearTimeout(timer); if (error) { throw error; } throw _error; } clearTimeout(timer); const headers = {}; resp.headers.forEach((value, key) => { headers[key.toLowerCase()] = value; }); const respBody = await resp.arrayBuffer(); const body = (respBody == null) ? null : new Uint8Array(respBody); return { statusCode: resp.status, statusMessage: resp.statusText, headers, body }; } return getUrl; } /** * Fetching content from the web is environment-specific, so Ethers * provides an abstraction that each environment can implement to provide * this service. * * On [Node.js](link-node), the ``http`` and ``https`` libs are used to * create a request object, register event listeners and process data * and populate the [[FetchResponse]]. * * In a browser, the [DOM fetch](link-js-fetch) is used, and the resulting * ``Promise`` is waited on to retrieve the payload. * * The [[FetchRequest]] is responsible for handling many common situations, * such as redirects, server throttling, authentication, etc. * * It also handles common gateways, such as IPFS and data URIs. * * @_section api/utils/fetching:Fetching Web Content [about-fetch] */ const MAX_ATTEMPTS = 12; const SLOT_INTERVAL = 250; // The global FetchGetUrlFunc implementation. let defaultGetUrlFunc = createGetUrl(); const reData = new RegExp("^data:([^;:]*)?(;base64)?,(.*)$", "i"); const reIpfs = new RegExp("^ipfs:/\/(ipfs/)?(.*)$", "i"); // If locked, new Gateways cannot be added let locked$5 = false; // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs async function dataGatewayFunc(url, signal) { try { const match = url.match(reData); if (!match) { throw new Error("invalid data"); } return new FetchResponse(200, "OK", { "content-type": (match[1] || "text/plain"), }, (match[2] ? decodeBase64(match[3]) : unpercent(match[3]))); } catch (error) { return new FetchResponse(599, "BAD REQUEST (invalid data: URI)", {}, null, new FetchRequest(url)); } } /** * Returns a [[FetchGatewayFunc]] for fetching content from a standard * IPFS gateway hosted at %%baseUrl%%. */ function getIpfsGatewayFunc(baseUrl) { async function gatewayIpfs(url, signal) { try { const match = url.match(reIpfs); if (!match) { throw new Error("invalid link"); } return new FetchRequest(`${baseUrl}${match[2]}`); } catch (error) { return new FetchResponse(599, "BAD REQUEST (invalid IPFS URI)", {}, null, new FetchRequest(url)); } } return gatewayIpfs; } const Gateways = { "data": dataGatewayFunc, "ipfs": getIpfsGatewayFunc("https:/\/gateway.ipfs.io/ipfs/") }; const fetchSignals = new WeakMap(); /** * @_ignore */ class FetchCancelSignal { #listeners; #cancelled; constructor(request) { this.#listeners = []; this.#cancelled = false; fetchSignals.set(request, () => { if (this.#cancelled) { return; } this.#cancelled = true; for (const listener of this.#listeners) { setTimeout(() => { listener(); }, 0); } this.#listeners = []; }); } addListener(listener) { assert(!this.#cancelled, "singal already cancelled", "UNSUPPORTED_OPERATION", { operation: "fetchCancelSignal.addCancelListener" }); this.#listeners.push(listener); } get cancelled() { return this.#cancelled; } checkSignal() { assert(!this.cancelled, "cancelled", "CANCELLED", {}); } } // Check the signal, throwing if it is cancelled function checkSignal(signal) { if (signal == null) { throw new Error("missing signal; should not happen"); } signal.checkSignal(); return signal; } /** * Represents a request for a resource using a URI. * * By default, the supported schemes are ``HTTP``, ``HTTPS``, ``data:``, * and ``IPFS:``. * * Additional schemes can be added globally using [[registerGateway]]. * * @example: * req = new FetchRequest("https://www.ricmoo.com") * resp = await req.send() * resp.body.length * //_result: */ class FetchRequest { #allowInsecure; #gzip; #headers; #method; #timeout; #url; #body; #bodyType; #creds; // Hooks #preflight; #process; #retry; #signal; #throttle; #getUrlFunc; /** * The fetch URL to request. */ get url() { return this.#url; } set url(url) { this.#url = String(url); } /** * The fetch body, if any, to send as the request body. //(default: null)// * * When setting a body, the intrinsic ``Content-Type`` is automatically * set and will be used if **not overridden** by setting a custom * header. * * If %%body%% is null, the body is cleared (along with the * intrinsic ``Content-Type``). * * If %%body%% is a string, the intrinsic ``Content-Type`` is set to * ``text/plain``. * * If %%body%% is a Uint8Array, the intrinsic ``Content-Type`` is set to * ``application/octet-stream``. * * If %%body%% is any other object, the intrinsic ``Content-Type`` is * set to ``application/json``. */ get body() { if (this.#body == null) { return null; } return new Uint8Array(this.#body); } set body(body) { if (body == null) { this.#body = undefined; this.#bodyType = undefined; } else if (typeof (body) === "string") { this.#body = toUtf8Bytes(body); this.#bodyType = "text/plain"; } else if (body instanceof Uint8Array) { this.#body = body; this.#bodyType = "application/octet-stream"; } else if (typeof (body) === "object") { this.#body = toUtf8Bytes(JSON.stringify(body)); this.#bodyType = "application/json"; } else { throw new Error("invalid body"); } } /** * Returns true if the request has a body. */ hasBody() { return (this.#body != null); } /** * The HTTP method to use when requesting the URI. If no method * has been explicitly set, then ``GET`` is used if the body is * null and ``POST`` otherwise. */ get method() { if (this.#method) { return this.#method; } if (this.hasBody()) { return "POST"; } return "GET"; } set method(method) { if (method == null) { method = ""; } this.#method = String(method).toUpperCase(); } /** * The headers that will be used when requesting the URI. All * keys are lower-case. * * This object is a copy, so any changes will **NOT** be reflected * in the ``FetchRequest``. * * To set a header entry, use the ``setHeader`` method. */ get headers() { const headers = Object.assign({}, this.#headers); if (this.#creds) { headers["authorization"] = `Basic ${encodeBase64(toUtf8Bytes(this.#creds))}`; } if (this.allowGzip) { headers["accept-encoding"] = "gzip"; } if (headers["content-type"] == null && this.#bodyType) { headers["content-type"] = this.#bodyType; } if (this.body) { headers["content-length"] = String(this.body.length); } return headers; } /** * Get the header for %%key%%, ignoring case. */ getHeader(key) { return this.headers[key.toLowerCase()]; } /** * Set the header for %%key%% to %%value%%. All values are coerced * to a string. */ setHeader(key, value) { this.#headers[String(key).toLowerCase()] = String(value); } /** * Clear all headers, resetting all intrinsic headers. */ clearHeaders() { this.#headers = {}; } [Symbol.iterator]() { const headers = this.headers; const keys = Object.keys(headers); let index = 0; return { next: () => { if (index < keys.length) { const key = keys[index++]; return { value: [key, headers[key]], done: false }; } return { value: undefined, done: true }; } }; } /** * The value that will be sent for the ``Authorization`` header. * * To set the credentials, use the ``setCredentials`` method. */ get credentials() { return this.#creds || null; } /** * Sets an ``Authorization`` for %%username%% with %%password%%. */ setCredentials(username, password) { assertArgument(!username.match(/:/), "invalid basic authentication username", "username", "[REDACTED]"); this.#creds = `${username}:${password}`; } /** * Enable and request gzip-encoded responses. The response will * automatically be decompressed. //(default: true)// */ get allowGzip() { return this.#gzip; } set allowGzip(value) { this.#gzip = !!value; } /** * Allow ``Authentication`` credentials to be sent over insecure * channels. //(default: false)// */ get allowInsecureAuthentication() { return !!this.#allowInsecure; } set allowInsecureAuthentication(value) { this.#allowInsecure = !!value; } /** * The timeout (in milliseconds) to wait for a complete response. * //(default: 5 minutes)// */ get timeout() { return this.#timeout; } set timeout(timeout) { assertArgument(timeout >= 0, "timeout must be non-zero", "timeout", timeout); this.#timeout = timeout; } /** * This function is called prior to each request, for example * during a redirection or retry in case of server throttling. * * This offers an opportunity to populate headers or update * content before sending a request. */ get preflightFunc() { return this.#preflight || null; } set preflightFunc(preflight) { this.#preflight = preflight; } /** * This function is called after each response, offering an * opportunity to provide client-level throttling or updating * response data. * * Any error thrown in this causes the ``send()`` to throw. * * To schedule a retry attempt (assuming the maximum retry limit * has not