diff --git a/src.ts/utils/base58.ts b/src.ts/utils/base58.ts index 987fc7e73..b00e30afd 100644 --- a/src.ts/utils/base58.ts +++ b/src.ts/utils/base58.ts @@ -1,7 +1,20 @@ +/** + * 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 [base58] + */ import { getBytes } from "./data.js"; import { assertArgument } from "./errors.js"; -import { toBigInt, toHex } from "./maths.js"; +import { toBigInt } from "./maths.js"; import type { BytesLike } from "./index.js"; @@ -26,7 +39,7 @@ const BN_0 = BigInt(0); const BN_58 = BigInt(58); /** - * Encode %%value%% as Base58-encoded data. + * Encode %%value%% as a Base58-encoded string. */ export function encodeBase58(_value: BytesLike): string { let value = toBigInt(getBytes(_value)); @@ -41,11 +54,11 @@ export function encodeBase58(_value: BytesLike): string { /** * Decode the Base58-encoded %%value%%. */ -export function decodeBase58(value: string): Uint8Array { +export function decodeBase58(value: string): bigint { let result = BN_0; for (let i = 0; i < value.length; i++) { result *= BN_58; result += getAlpha(value[i]); } - return getBytes(toHex(result)); + return result; } diff --git a/src.ts/utils/base64.ts b/src.ts/utils/base64.ts index 74ed13cbb..351df99a5 100644 --- a/src.ts/utils/base64.ts +++ b/src.ts/utils/base64.ts @@ -1,17 +1,25 @@ +/** + * [Base64 encoding](link-wiki-base64) using 6-bit words to encode + * arbitrary bytes into a string using 65 printable symbols, the + * upper-case and lower-case alphabet, the digits ``0`` through ``9``, + * ``"+"`` and ``"/"`` with the ``"="`` used for padding. + * + * @_subsection: api/utils:Base64 Encoding [base64] + */ import { getBytes, getBytesCopy } from "./data.js"; import type { BytesLike } from "./data.js"; /** - * Decodes the base-64 encoded %%base64Data%%. + * Decodes the base-64 encoded %%value%%. */ -export function decodeBase64(base64Data: string): Uint8Array { - return getBytesCopy(Buffer.from(base64Data, "base64")); +export function decodeBase64(value: string): Uint8Array { + return getBytesCopy(Buffer.from(value, "base64")); }; /** - * Encodes %%data%% as base-64 encoded data. + * Encodes %%data%% as a base-64 encoded string. */ export function encodeBase64(data: BytesLike): string { return Buffer.from(getBytes(data)).toString("base64"); diff --git a/src.ts/utils/data.ts b/src.ts/utils/data.ts index 9fb5bd540..ba1d47f65 100644 --- a/src.ts/utils/data.ts +++ b/src.ts/utils/data.ts @@ -1,7 +1,29 @@ +/** + * Some data helpers. + * + * + * @_subsection api/utils:Data Helpers [data] + */ import { assert, assertArgument } from "./errors.js"; +/** + * A [[HexString]] whose length is even, which ensures it is a valid + * representation of binary data. + */ +export type DataHexString = string; -export type BytesLike = string | Uint8Array; +/** + * A string which is prefixed with ``0x`` and followed by any number + * of case-agnostic hexadecimal characters. + * + * It must match the regular expression ``/0x[0-9A-Fa-f]*\/``. + */ +export type HexString = string; + +/** + * An object that can be used to represent binary data. + */ +export type BytesLike = DataHexString | Uint8Array; function _getBytes(value: BytesLike, name?: string, copy?: boolean): Uint8Array { if (value instanceof Uint8Array) { @@ -46,13 +68,10 @@ export function getBytesCopy(value: BytesLike, name?: string): Uint8Array { /** - * Returns true if %%value%% is a valid [[HexString]], with additional - * optional constraints depending on %%length%%. + * Returns true if %%value%% is a valid [[HexString]]. * - * If %%length%% is //true//, then %%value%% must additionally be a valid - * [[HexDataString]] (i.e. even length). - * - * If %%length%% is //a number//, then %%value%% must represent that many + * 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). */ export function isHexString(value: any, length?: number | boolean): value is `0x${ string }` { @@ -68,7 +87,7 @@ export function isHexString(value: any, length?: number | boolean): value is `0x /** * Returns true if %%value%% is a valid representation of arbitrary - * data (i.e. a valid [[HexDataString]] or a Uint8Array). + * data (i.e. a valid [[DataHexString]] or a Uint8Array). */ export function isBytesLike(value: any): value is BytesLike { return (isHexString(value, true) || (value instanceof Uint8Array)); @@ -77,7 +96,7 @@ export function isBytesLike(value: any): value is BytesLike { const HexCharacters: string = "0123456789abcdef"; /** - * Returns a [[HexDataString]] representation of %%data%%. + * Returns a [[DataHexString]] representation of %%data%%. */ export function hexlify(data: BytesLike): string { const bytes = getBytes(data); @@ -91,7 +110,7 @@ export function hexlify(data: BytesLike): string { } /** - * Returns a [[HexDataString]] by concatenating all values + * Returns a [[DataHexString]] by concatenating all values * within %%data%%. */ export function concat(datas: ReadonlyArray): string { @@ -107,7 +126,7 @@ export function dataLength(data: BytesLike): number { } /** - * Returns a [[HexDataString]] by slicing %%data%% from the %%start%% + * 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%%. @@ -123,7 +142,7 @@ export function dataSlice(data: BytesLike, start?: number, end?: number): string } /** - * Return the [[HexDataString]] result by stripping all **leading** + * Return the [[DataHexString]] result by stripping all **leading** ** zero bytes from %%data%%. */ export function stripZerosLeft(data: BytesLike): string { @@ -152,7 +171,7 @@ function zeroPad(data: BytesLike, length: number, left: boolean): string { } /** - * Return the [[HexDataString]] of %%data%% padded on the **left** + * Return the [[DataHexString]] of %%data%% padded on the **left** * to %%length%% bytes. */ export function zeroPadValue(data: BytesLike, length: number): string { @@ -160,7 +179,7 @@ export function zeroPadValue(data: BytesLike, length: number): string { } /** - * Return the [[HexDataString]] of %%data%% padded on the **right** + * Return the [[DataHexString]] of %%data%% padded on the **right** * to %%length%% bytes. */ export function zeroPadBytes(data: BytesLike, length: number): string { diff --git a/src.ts/utils/errors.ts b/src.ts/utils/errors.ts index 6f854edce..49357370c 100644 --- a/src.ts/utils/errors.ts +++ b/src.ts/utils/errors.ts @@ -1,6 +1,12 @@ +/** + * About Errors. + * + * @_section: api/utils/errors:Errors [errors] + */ + import { version } from "../_version.js"; -import { defineReadOnly } from "./properties.js"; +import { defineProperties } from "./properties.js"; import type { TransactionRequest, TransactionReceipt, TransactionResponse @@ -11,11 +17,46 @@ import type { FetchRequest, FetchResponse } from "./fetch.js"; export type ErrorInfo = Omit; -// The type of error to use for various error codes -const ErrorConstructors: Record): Error }> = { }; -ErrorConstructors.INVALID_ARGUMENT = TypeError; -ErrorConstructors.NUMERIC_FAULT = RangeError; -ErrorConstructors.BUFFER_OVERRUN = RangeError; + +function stringify(value: any): any { + if (value == null) { return "null"; } + + if (Array.isArray(value)) { + return "[ " + (value.map(stringify)).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(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(k) }: ${ stringify(value[k]) }`).join(", ") + " }"; + } + } + + return `[ COULD NOT SERIALIZE ]`; +} /** * All errors emitted by ethers have an **ErrorCode** to help @@ -59,15 +100,39 @@ export interface EthersError extends Error { // Generic Errors +/** + * This Error is a catch-all for when there is no way for Ethers to + * know what the underlying problem is. + */ export interface UnknownError extends EthersError<"UNKNOWN_ERROR"> { [ key: string ]: any; } +/** + * This Error is mostly used as a stub for functionality that is + * intended for the future, but is currently not implemented. + */ export interface NotImplementedError extends EthersError<"NOT_IMPLEMENTED"> { + /** + * The attempted operation. + */ operation: string; } +/** + * This Error indicates that the attempted operation is not supported. + * + * This could range from a specifc JSON-RPC end-point not supporting + * a feature to a specific configuration of an object prohibiting the + * operation. + * + * For example, a [[Wallet]] with no connected [[Provider]] is unable + * to send a transaction. + */ export interface UnsupportedOperationError extends EthersError<"UNSUPPORTED_OPERATION"> { + /** + * The attempted operation. + */ operation: string; } @@ -75,45 +140,134 @@ export interface NetworkError extends EthersError<"NETWORK_ERROR"> { event: string; } +/** + * This Error indicates there was a problem fetching a resource from + * a server. + */ export interface ServerError extends EthersError<"SERVER_ERROR"> { + /** + * The requested resource. + */ request: FetchRequest | string; + + /** + * The response received from the server, if available. + */ response?: FetchResponse; } +/** + * This Error indicates that the timeout duration has expired and + * that the operation has been implicitly cancelled. + * + * The side-effect of the operation may still occur, as this + * generally means a request has been sent and there has simply + * been no response to indicate whether it was processed or not. + */ export interface TimeoutError extends EthersError<"TIMEOUT"> { + /** + * The attempted operation. + */ operation: string; + + /** + * The reason. + */ reason: string; + + /** + * The resource request, if available. + */ request?: FetchRequest; } +/** + * This Error indicates that a provided set of data cannot + * be correctly interpretted. + */ export interface BadDataError extends EthersError<"BAD_DATA"> { + /** + * The data. + */ value: any; } +/** + * This Error indicates that the operation was cancelled by a + * programmatic call, for example to ``cancel()``. + */ export interface CancelledError extends EthersError<"CANCELLED"> { } // Operational Errors +/** + * This Error indicates an attempt was made to read outside the bounds + * of protected data. + * + * Most operations in Ethers are protected by bounds checks, to mitigate + * exploits when parsing data. + */ export interface BufferOverrunError extends EthersError<"BUFFER_OVERRUN"> { + /** + * The buffer that was overrun. + */ buffer: Uint8Array; + + /** + * The length of the buffer. + */ length: number; + + /** + * The offset that was requested. + */ offset: number; } +/** + * This Error indicates an operation which would result in incorrect + * arithmetic output has occurred. + * + * For example, trying to divide by zero or using a ``uint8`` to store + * a negative value. + */ export interface NumericFaultError extends EthersError<"NUMERIC_FAULT"> { + /** + * The attempted operation. + */ operation: string; + + /** + * The fault reported. + */ fault: string; + + /** + * The value the operation was attempted against. + */ value: any; } // Argument Errors +/** + * This Error indicates an incorrect type or value was passed to + * a function or method. + */ export interface InvalidArgumentError extends EthersError<"INVALID_ARGUMENT"> { + /** + * The name of the argument. + */ argument: string; + + /** + * The value that was provided. + */ value: any; + info?: Record } @@ -168,6 +322,7 @@ export interface CallExceptionError extends EthersError<"CALL_EXCEPTION"> { // transaction: any;//ErrorTransaction; //} + export interface InsufficientFundsError extends EthersError<"INSUFFICIENT_FUNDS"> { transaction: TransactionRequest; } @@ -193,12 +348,41 @@ export interface TransactionReplacedError extends EthersError<"TRANSACTION_REPLA receipt: TransactionReceipt; } +/** + * This Error indicates an ENS name was used, but the name has not + * been configured. + * + * This could indicate an ENS name is unowned or that the current + * address being pointed to is the [[Zero]]. + */ export interface UnconfiguredNameError extends EthersError<"UNCONFIGURED_NAME"> { + /** + * The ENS name that was requested + */ value: string; } +/** + * This Error indicates a request was rejected by the user. + * + * In most clients (such as MetaMask), when an operation requires user + * authorization (such as ``signer.sendTransaction``), the client + * presents a dialog box to the user. If the user denies the request + * this error is thrown. + */ export interface ActionRejectedError extends EthersError<"ACTION_REJECTED"> { + /** + * The requested action. + */ action: "requestAccess" | "sendTransaction" | "signMessage" | "signTransaction" | "signTypedData" | "unknown", + + /** + * The reason the action was rejected. + * + * If there is already a pending request, some clients may indicate + * there is already a ``"pending"`` action. This prevents an app + * from spamming the user. + */ reason: "expired" | "rejected" | "pending" } @@ -252,7 +436,7 @@ export type CodedEthersError = * @See [ErrorCodes](api:ErrorCode) * @example * try { - * // code.... + * / / code.... * } catch (e) { * if (isError(e, "CALL_EXCEPTION")) { * console.log(e.data); @@ -285,15 +469,16 @@ export function makeError>(me const details: Array = []; if (info) { if ("message" in info || "code" in info || "name" in info) { - throw new Error(`value will overwrite populated values: ${ JSON.stringify(info) }`); + throw new Error(`value will overwrite populated values: ${ stringify(info) }`); } for (const key in info) { const value = (info[>key]); - try { - details.push(key + "=" + JSON.stringify(value)); - } catch (error) { - details.push(key + "=[could not serialize object]"); - } +// try { + details.push(key + "=" + stringify(value)); +// } catch (error: any) { +// console.log("MMM", error.message); +// details.push(key + "=[could not serialize object]"); +// } } } details.push(`code=${ code }`); @@ -304,16 +489,23 @@ export function makeError>(me } } - const create = ErrorConstructors[code] || Error; - const error = (new create(message)); - defineReadOnly(error, "code", code); - - if (info) { - for (const key in info) { - defineReadOnly(error, key, (info[>key])); - } + 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) { defineProperties(error, info); } + return error; } @@ -339,7 +531,8 @@ export function assertArgument(check: unknown, message: string, name: string, va assert(check, message, "INVALID_ARGUMENT", { argument: name, value: value }); } -export function assertArgumentCount(count: number, expectedCount: number, message: string = ""): void { +export function assertArgumentCount(count: number, expectedCount: number, message?: string): void { + if (message == null) { message = ""; } if (message) { message = ": " + message; } assert(count >= expectedCount, "missing arguemnt" + message, "MISSING_ARGUMENT", { @@ -389,7 +582,8 @@ export function assertNormalize(form: string): void { * by ensuring the %%givenGaurd%% matches the file-scoped %%guard%%, * throwing if not, indicating the %%className%% if provided. */ -export function assertPrivate(givenGuard: any, guard: any, className: string = ""): void { +export function assertPrivate(givenGuard: any, guard: any, className?: string): void { + if (className == null) { className = ""; } if (givenGuard !== guard) { let method = className, operation = "new"; if (className) { diff --git a/src.ts/utils/events.ts b/src.ts/utils/events.ts index 6c2a57e9f..85eebc7f2 100644 --- a/src.ts/utils/events.ts +++ b/src.ts/utils/events.ts @@ -1,3 +1,8 @@ +/** + * Explain events... + * + * @_section api/utils/events:Events [events] + */ import { defineProperties } from "./properties.js"; export type Listener = (...args: Array) => void; diff --git a/src.ts/utils/fetch.ts b/src.ts/utils/fetch.ts index 023c2c28e..bef1c7be5 100644 --- a/src.ts/utils/fetch.ts +++ b/src.ts/utils/fetch.ts @@ -1,3 +1,8 @@ +/** + * Explain fetching here... + * + * @_section api/utils/fetching:Fetching Web Content [fetching] + */ import { decodeBase64, encodeBase64 } from "./base64.js"; import { hexlify } from "./data.js"; import { assert, assertArgument } from "./errors.js"; @@ -21,17 +26,17 @@ export type FetchThrottleParams = { /** * Called before any network request, allowing updated headers (e.g. Bearer tokens), etc. */ -export type FetchPreflightFunc = (request: FetchRequest) => Promise; +export type FetchPreflightFunc = (req: FetchRequest) => Promise; /** * Called on the response, allowing client-based throttling logic or post-processing. */ -export type FetchProcessFunc = (request: FetchRequest, response: FetchResponse) => Promise; +export type FetchProcessFunc = (req: FetchRequest, resp: FetchResponse) => Promise; /** * Called prior to each retry; return true to retry, false to abort. */ -export type FetchRetryFunc = (request: FetchRequest, response: FetchResponse, attempt: number) => Promise; +export type FetchRetryFunc = (req: FetchRequest, resp: FetchResponse, attempt: number) => Promise; /** * Called on Gateway URLs. @@ -44,7 +49,7 @@ export type FetchGatewayFunc = (url: string, signal?: FetchCancelSignal) => Prom * and in the browser ``fetch`` is used. If you wish to use Axios, this is * how you would register it. */ -export type FetchGetUrlFunc = (request: FetchRequest, signal?: FetchCancelSignal) => Promise; +export type FetchGetUrlFunc = (req: FetchRequest, signal?: FetchCancelSignal) => Promise; const MAX_ATTEMPTS = 12; @@ -60,7 +65,7 @@ const reIpfs = new RegExp("^ipfs:/\/(ipfs/)?(.*)$", "i"); let locked = false; // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs -async function gatewayData(url: string, signal?: FetchCancelSignal): Promise { +async function dataGatewayFunc(url: string, signal?: FetchCancelSignal): Promise { try { const match = url.match(reData); if (!match) { throw new Error("invalid data"); } @@ -76,7 +81,7 @@ async function gatewayData(url: string, signal?: FetchCancelSignal): Promise { try { const match = url.match(reIpfs); @@ -91,12 +96,15 @@ export function getIpfsGatewayFunc(baseUrl: string): FetchGatewayFunc { } const Gateways: Record = { - "data": gatewayData, + "data": dataGatewayFunc, "ipfs": getIpfsGatewayFunc("https:/\/gateway.ipfs.io/ipfs/") }; const fetchSignals: WeakMap void> = new WeakMap(); +/** + * @_ignore + */ export class FetchCancelSignal { #listeners: Array<() => void>; #cancelled: boolean; @@ -141,8 +149,10 @@ function checkSignal(signal?: FetchCancelSignal): FetchCancelSignal { /** * Represents a request for a resource using a URI. * - * Requests can occur over http/https, data: URI or any - * URI scheme registered via the static [[register]] method. + * By default, the supported schemes are ``HTTP``, ``HTTPS``, ``data:``, + * and ``IPFS:``. + * + * Additional schemes can be added globally using [[registerGateway]]. */ export class FetchRequest implements Iterable<[ key: string, value: string ]> { #allowInsecure: boolean; @@ -174,7 +184,7 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { } /** - * The fetch body, if any, to send as the request body. + * 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 @@ -237,9 +247,15 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { } /** - * The headers that will be used when requesting the URI. + * The headers that will be used when requesting the URI. All + * keys are lower-case. + * + * This object is a copy, so any chnages will **NOT** be reflected + * in the ``FetchRequest``. + * + * To set a header entry, use the ``setHeader`` method. */ - get headers(): Readonly> { + get headers(): Record { const headers = Object.assign({ }, this.#headers); if (this.#creds) { @@ -255,11 +271,11 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { } if (this.body) { headers["content-length"] = String(this.body.length); } - return Object.freeze(headers); + return headers; } /** - * Get the header for %%key%%. + * Get the header for %%key%%, ignoring case. */ getHeader(key: string): string { return this.headers[key.toLowerCase()]; @@ -274,7 +290,7 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { } /** - * Clear all headers. + * Clear all headers, resetting all intrinsic headers. */ clearHeaders(): void { this.#headers = { }; @@ -299,6 +315,8 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { /** * The value that will be sent for the ``Authorization`` header. + * + * To set the credentials, use the ``setCredentials`` method. */ get credentials(): null | string { return this.#creds || null; @@ -313,7 +331,8 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { } /** - * Allow gzip-encoded responses. + * Enable and request gzip-encoded responses. The response will + * automatically be decompressed. //(default: true)// */ get allowGzip(): boolean { return this.#gzip; @@ -324,7 +343,7 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { /** * Allow ``Authentication`` credentials to be sent over insecure - * channels. + * channels. //(default: false)// */ get allowInsecureAuthentication(): boolean { return !!this.#allowInsecure; @@ -335,6 +354,7 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { /** * The timeout (in milliseconds) to wait for a complere response. + * //(default: 5 minutes)// */ get timeout(): number { return this.#timeout; } set timeout(timeout: number) { @@ -383,11 +403,17 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { this.#retry = retry; } + /** + * Create a new FetchRequest instance with default values. + * + * Once created, each property may be set before issuing a + * ``.send()`` to make teh request. + */ constructor(url: string) { this.#url = String(url); this.#allowInsecure = false; - this.#gzip = false; + this.#gzip = true; this.#headers = { }; this.#method = ""; this.#timeout = 300000; @@ -398,6 +424,10 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { }; } + /** + * Update the throttle parameters used to determine maximum + * attempts and exponential-backoff properties. + */ setThrottleParams(params: FetchThrottleParams): void { if (params.slotInterval != null) { this.#throttle.slotInterval = params.slotInterval; @@ -600,7 +630,12 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { } /** - * Set the FetchGatewayFunc for %%scheme%% to %%func%%. + * Use the %%func%% when fetching URIs using %%scheme%%. + * + * This method affects all requests globally. + * + * If [[lockConfig]] has been called, no change is made and this + * throws. */ static registerGateway(scheme: string, func: FetchGatewayFunc): void { scheme = scheme.toLowerCase(); @@ -612,12 +647,41 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { } /** - * Set a custom function for fetching HTTP and HTTPS requests. + * Use %%getUrl%% when fetching URIs over HTTP and HTTPS requests. + * + * This method affects all requests globally. + * + * If [[lockConfig]] has been called, no change is made and this + * throws. */ static registerGetUrl(getUrl: FetchGetUrlFunc): void { if (locked) { throw new Error("gateways locked"); } getUrlFunc = getUrl; } + + /** + * Creates a function that can "fetch" data URIs. + * + * Note that this is automatically done internally to support + * data URIs, so it is not necessary to register it. + * + * This is not generally something that is needed, but may + * be useful in a wrapper to perfom custom data URI functionality. + */ + static createDataGateway(): FetchGatewayFunc { + return dataGatewayFunc; + } + + /** + * Creates a function that will fetch IPFS (unvalidated) from + * a custom gateway baseUrl. + * + * The default IPFS gateway used internally is + * ``"https:/\/gateway.ipfs.io/ipfs/"``. + */ + static createIpfsGatewayFunc(baseUrl: string): FetchGatewayFunc { + return getIpfsGatewayFunc(baseUrl); + } } @@ -632,7 +696,7 @@ interface ThrottleError extends Error { export class FetchResponse implements Iterable<[ key: string, value: string ]> { #statusCode: number; #statusMessage: string; - #headers: Readonly>; + #headers: Record; #body: null | Readonly; #request: null | FetchRequest; @@ -653,21 +717,22 @@ export class FetchResponse implements Iterable<[ key: string, value: string ]> { get statusMessage(): string { return this.#statusMessage; } /** - * The response headers. + * The response headers. All keys are lower-case. */ - get headers(): Record { return this.#headers; } + get headers(): Record { return Object.assign({ }, this.#headers); } /** - * The response body. + * The response body, or ``null`` if there was no body. */ get body(): null | Readonly { return (this.#body == null) ? null: new Uint8Array(this.#body); } /** - * The response body as a UTF-8 encoded string. + * The response body as a UTF-8 encoded string, or the empty + * string (i.e. ``""``) if there was no body. * - * An error is thrown if the body is invalid UTF-8 data. + * An error is thrown if the body is invalid UTF-8 data. */ get bodyText(): string { try { @@ -682,7 +747,8 @@ export class FetchResponse implements Iterable<[ key: string, value: string ]> { /** * The response body, decoded as JSON. * - * An error is thrown if the body is invalid JSON-encoded data. + * An error is thrown if the body is invalid JSON-encoded data + * or if there was no body. */ get bodyJson(): any { try { @@ -714,10 +780,10 @@ export class FetchResponse implements Iterable<[ key: string, value: string ]> { constructor(statusCode: number, statusMessage: string, headers: Readonly>, body: null | Uint8Array, request?: FetchRequest) { this.#statusCode = statusCode; this.#statusMessage = statusMessage; - this.#headers = Object.freeze(Object.assign({ }, Object.keys(headers).reduce((accum, k) => { + this.#headers = Object.keys(headers).reduce((accum, k) => { accum[k.toLowerCase()] = String(headers[k]); return accum; - }, >{ }))); + }, >{ }); this.#body = ((body == null) ? null: new Uint8Array(body)); this.#request = (request || null); @@ -744,8 +810,9 @@ export class FetchResponse implements Iterable<[ key: string, value: string ]> { } /** - * If called within the [[processFunc]], causes the request to - * retry as if throttled for %%stall%% milliseconds. + * If called within a [request.processFunc](FetchRequest-processFunc) + * call, causes the request to retry as if throttled for %%stall%% + * milliseconds. */ throwThrottleError(message?: string, stall?: number): never { if (stall == null) { @@ -762,7 +829,7 @@ export class FetchResponse implements Iterable<[ key: string, value: string ]> { } /** - * Get the header value for %%key%%. + * Get the header value for %%key%%, ignoring case. */ getHeader(key: string): string { return this.headers[key.toLowerCase()]; @@ -781,7 +848,7 @@ export class FetchResponse implements Iterable<[ key: string, value: string ]> { get request(): null | FetchRequest { return this.#request; } /** - * Returns true if this response was a success statuscode. + * Returns true if this response was a success statusCode. */ ok(): boolean { return (this.#error.message === "" && this.statusCode >= 200 && this.statusCode < 300); diff --git a/src.ts/utils/fixednumber.ts b/src.ts/utils/fixednumber.ts index 6345ffe87..3f1e6215e 100644 --- a/src.ts/utils/fixednumber.ts +++ b/src.ts/utils/fixednumber.ts @@ -1,3 +1,8 @@ +/** + * About fixed-point math... + * + * @_section: api/utils/fixed-point-math:Fixed-Point Maths [fixed-point-math] + */ import { getBytes } from "./data.js"; import { assert, assertArgument, assertPrivate } from "./errors.js"; import { getBigInt, getNumber, fromTwos, toBigInt, toHex, toTwos } from "./maths.js"; @@ -28,6 +33,12 @@ function getMultiplier(decimals: number): bigint { return BigInt("1" + zeros.substring(0, decimals)); } +/** + * Returns the fixed-point string representation of %%value%% to + * divided by %%decimal%% places. + * + * @param {Numeric = 18} decimals + */ export function formatFixed(_value: BigNumberish, _decimals?: Numeric): string { if (_decimals == null) { _decimals = 18; } @@ -58,24 +69,29 @@ export function formatFixed(_value: BigNumberish, _decimals?: Numeric): string { return result; } -export function parseFixed(value: string, _decimals: Numeric): bigint { +/** + * Returns the value of %%value%% multiplied by %%decimal%% places. + * + * @param {Numeric = 18} decimals + */ +export function parseFixed(str: string, _decimals: Numeric): bigint { if (_decimals == null) { _decimals = 18; } const decimals = getNumber(_decimals, "decimals"); const multiplier = getMultiplier(decimals); - assertArgument(typeof(value) === "string" && value.match(/^-?[0-9.]+$/), - "invalid decimal value", "value", value); + assertArgument(typeof(str) === "string" && str.match(/^-?[0-9.]+$/), + "invalid decimal value", "str", str); // Is it negative? - const negative = (value.substring(0, 1) === "-"); - if (negative) { value = value.substring(1); } + const negative = (str.substring(0, 1) === "-"); + if (negative) { str = str.substring(1); } - assertArgument(value !== ".", "missing value", "value", value); + assertArgument(str !== ".", "missing value", "str", str); // Split it into a whole and fractional part - const comps = value.split("."); - assertArgument(comps.length <= 2, "too many decimal points", "value", value); + const comps = str.split("."); + assertArgument(comps.length <= 2, "too many decimal points", "str", str); let whole = (comps[0] || "0"), fraction = (comps[1] || "0"); @@ -105,14 +121,41 @@ export function parseFixed(value: string, _decimals: Numeric): bigint { return wei; } +/** + * A FixedFormat encapsulates the properties required to describe + * a fixed-point arithmetic field. + */ export class FixedFormat { + /** + * If true, negative values are permitted, otherwise only + * positive values and zero are allowed. + */ readonly signed: boolean; + + /** + * The number of bits available to store the value in the + * fixed-point arithmetic field. + */ readonly width: number; + + /** + * The number of decimal places in the fixed-point arithment field. + */ readonly decimals: number; + + /** + * A human-readable representation of the fixed-point arithmetic field. + */ readonly name: string; + /** + * @private + */ readonly _multiplier: bigint; + /** + * @private + */ constructor(guard: any, signed: boolean, width: number, decimals: number) { assertPrivate(guard, _guard, "FixedFormat"); @@ -127,6 +170,25 @@ export class FixedFormat { Object.freeze(this); } + /** + * Returns a new FixedFormat for %%value%%. + * + * If %%value%% is specified as a ``number``, the bit-width is + * 128 bits and %%value%% is used for the ``decimals``. + * + * A string %%value%% may begin with ``fixed`` or ``ufixed`` + * for signed and unsigned respectfully. If no other properties + * are specified, the bit-width is 128-bits with 18 decimals. + * + * To specify the bit-width and demicals, append them separated + * by an ``"x"`` to the %%value%%. + * + * For example, ``ufixed128x18`` describes an unsigned, 128-bit + * wide format with 18 decimals. + * + * If %%value%% is an other object, its properties for ``signed``, + * ``width`` and ``decimals`` are checked. + */ static from(value: any): FixedFormat { if (value instanceof FixedFormat) { return value; } @@ -170,16 +232,26 @@ export class FixedFormat { } /** - * Fixed Number class + * A FixedNumber represents a value over its [[FixedFormat]] + * arithmetic field. + * + * A FixedNumber can be used to perform math, losslessly, on + * values which have decmial places. */ export class FixedNumber { readonly format: FixedFormat; + /** + * @private + */ readonly _isFixedNumber: boolean; //#hex: string; #value: string; + /** + * @private + */ constructor(guard: any, hex: string, value: string, format?: FixedFormat) { assertPrivate(guard, _guard, "FixedNumber"); @@ -198,7 +270,7 @@ export class FixedNumber { } /** - * Returns a new [[FixedNumber]] with the result of this added + * Returns a new [[FixedNumber]] with the result of %%this%% added * to %%other%%. */ addUnsafe(other: FixedNumber): FixedNumber { @@ -208,6 +280,10 @@ export class FixedNumber { return FixedNumber.fromValue(a + b, this.format.decimals, this.format); } + /** + * Returns a new [[FixedNumber]] with the result of %%other%% subtracted + * %%this%%. + */ subUnsafe(other: FixedNumber): FixedNumber { this.#checkFormat(other); const a = parseFixed(this.#value, this.format.decimals); @@ -215,6 +291,10 @@ export class FixedNumber { return FixedNumber.fromValue(a - b, this.format.decimals, this.format); } + /** + * Returns a new [[FixedNumber]] with the result of %%this%% multiplied + * by %%other%%. + */ mulUnsafe(other: FixedNumber): FixedNumber { this.#checkFormat(other); const a = parseFixed(this.#value, this.format.decimals); @@ -222,6 +302,10 @@ export class FixedNumber { return FixedNumber.fromValue((a * b) / this.format._multiplier, this.format.decimals, this.format); } + /** + * Returns a new [[FixedNumber]] with the result of %%this%% divided + * by %%other%%. + */ divUnsafe(other: FixedNumber): FixedNumber { this.#checkFormat(other); const a = parseFixed(this.#value, this.format.decimals); @@ -229,6 +313,12 @@ export class FixedNumber { return FixedNumber.fromValue((a * this.format._multiplier) / b, this.format.decimals, this.format); } + /** + * Returns a new [[FixedNumber]] which is the largest **integer** + * that is less than or equal to %%this%%. + * + * The decimal component of the result will always be ``0``. + */ floor(): FixedNumber { const comps = this.toString().split("."); if (comps.length === 1) { comps.push("0"); } @@ -243,6 +333,12 @@ export class FixedNumber { return result; } + /** + * Returns a new [[FixedNumber]] which is the smallest **integer** + * that is greater than or equal to %%this%%. + * + * The decimal component of the result will always be ``0``. + */ ceiling(): FixedNumber { const comps = this.toString().split("."); if (comps.length === 1) { comps.push("0"); } @@ -257,7 +353,14 @@ export class FixedNumber { return result; } - // @TODO: Support other rounding algorithms + /** + * Returns a new [[FixedNumber]] with the decimal component + * rounded up on ties. + * + * The decimal component of the result will always be ``0``. + * + * @param {number = 0} decimals + */ round(decimals?: number): FixedNumber { if (decimals == null) { decimals = 0; } @@ -276,14 +379,23 @@ export class FixedNumber { return this.mulUnsafe(factor).addUnsafe(bump).floor().divUnsafe(factor); } + /** + * Returns true if %%this%% is equal to ``0``. + */ isZero(): boolean { return (this.#value === "0.0" || this.#value === "0"); } + /** + * Returns true if %%this%% is less than ``0``. + */ isNegative(): boolean { return (this.#value[0] === "-"); } + /** + * Returns the string representation of %%this%%. + */ toString(): string { return this.#value; } toHexString(_width: Numeric): string { @@ -300,19 +412,45 @@ export class FixedNumber { */ } + /** + * Returns a float approximation. + * + * Due to IEEE 754 precission (or lack thereof), this function + * can only return an approximation and most values will contain + * rounding errors. + */ toUnsafeFloat(): number { return parseFloat(this.toString()); } + /** + * Return a new [[FixedNumber]] with the same value but has had + * its field set to %%format%%. + * + * This will throw if the value cannot fit into %%format%%. + */ toFormat(format: FixedFormat | string): FixedNumber { return FixedNumber.fromString(this.#value, format); } - - static fromValue(value: BigNumberish, decimals: number = 0, format: FixedFormat | string | number = "fixed"): FixedNumber { + /** + * Creates a new [[FixedNumber]] for %%value%% multiplied by + * %%decimal%% places with %%format%%. + * + * @param {number = 0} decimals + * @param {FixedFormat | string | number = "fixed"} format + */ + static fromValue(value: BigNumberish, decimals?: number, format?: FixedFormat | string | number): FixedNumber { + if (decimals == null) { decimals = 0; } + if (format == null) { format = "fixed"; } return FixedNumber.fromString(formatFixed(value, decimals), FixedFormat.from(format)); } - - static fromString(value: string, format: FixedFormat | string | number = "fixed"): FixedNumber { + /** + * Creates a new [[FixedNumber]] for %%value%% with %%format%%. + * + * @param {FixedFormat | string | number = "fixed"} format + */ + static fromString(value: string, format?: FixedFormat | string | number): FixedNumber { + if (format == null) { format = "fixed"; } const fixedFormat = FixedFormat.from(format); const numeric = parseFixed(value, fixedFormat.decimals); @@ -332,7 +470,12 @@ export class FixedNumber { return new FixedNumber(_guard, hex, decimal, fixedFormat); } - static fromBytes(_value: BytesLike, format: FixedFormat | string | number = "fixed"): FixedNumber { + /** + * Creates a new [[FixedNumber]] with the big-endian representation + * %%value%% with %%format%%. + */ + static fromBytes(_value: BytesLike, format?: FixedFormat | string | number): FixedNumber { + if (format == null) { format = "fixed"; } const value = getBytes(_value, "value"); const fixedFormat = FixedFormat.from(format); @@ -349,6 +492,9 @@ export class FixedNumber { return new FixedNumber(_guard, hex, decimal, fixedFormat); } + /** + * Creates a new [[FixedNumber]]. + */ static from(value: any, format?: FixedFormat | string | number): FixedNumber { if (typeof(value) === "string") { return FixedNumber.fromString(value, format); @@ -370,6 +516,9 @@ export class FixedNumber { assertArgument(false, "invalid FixedNumber value", "value", value); } + /** + * Returns true if %%value%% is a [[FixedNumber]]. + */ static isFixedNumber(value: any): value is FixedNumber { return !!(value && value._isFixedNumber); } diff --git a/src.ts/utils/index.ts b/src.ts/utils/index.ts index cabc5c930..befc59501 100644 --- a/src.ts/utils/index.ts +++ b/src.ts/utils/index.ts @@ -1,18 +1,10 @@ - -//// - -export interface Freezable { - clone(): T; - freeze(): Frozen; - isFrozen(): boolean; -} - -export type Frozen = Readonly<{ - [ P in keyof T ]: T[P] extends (...args: Array) => any ? T[P]: - T[P] extends Freezable ? Frozen: - Readonly; -}>; - +/** + * There are many simple utilities required to interact with + * Ethereum and to simplify the library, without increasing + * the library dependencies for simple functions. + * + * @_section api/utils:Utilities [utils] + */ export { decodeBase58, encodeBase58 } from "./base58.js"; @@ -31,7 +23,6 @@ export { export { EventPayload } from "./events.js"; export { - getIpfsGatewayFunc, FetchRequest, FetchResponse, FetchCancelSignal, } from "./fetch.js"; @@ -42,13 +33,11 @@ export { getBigInt, getNumber, toBigInt, toNumber, toHex, toArray, toQuantity } from "./maths.js"; -export { resolveProperties, defineReadOnly, defineProperties} from "./properties.js"; +export { resolveProperties, defineProperties} from "./properties.js"; export { decodeRlp } from "./rlp-decode.js"; export { encodeRlp } from "./rlp-encode.js"; -export { getStore, setStore} from "./storage.js"; - export { formatEther, parseEther, formatUnits, parseUnits } from "./units.js"; export { @@ -59,6 +48,7 @@ export { Utf8ErrorFuncs, } from "./utf8.js"; +export { uuidV4 } from "./uuid.js"; ///////////////////////////// // Types diff --git a/src.ts/utils/maths.ts b/src.ts/utils/maths.ts index ddf37097e..fced344bc 100644 --- a/src.ts/utils/maths.ts +++ b/src.ts/utils/maths.ts @@ -1,3 +1,8 @@ +/** + * Some mathematic operations. + * + * @_subsection: api/utils:Math Helpers [maths] + */ import { hexlify, isBytesLike } from "./data.js"; import { assertArgument } from "./errors.js"; @@ -21,7 +26,10 @@ const BN_1 = BigInt(1); const maxValue = 0x1fffffffffffff; /** - * Convert %%value%% from a twos-compliment value of %%width%% bits. + * Convert %%value%% from a twos-compliment representation of %%width%% + * bits to its value. + * + * If the highest bit is ``1``, the result will be negative. */ export function fromTwos(_value: BigNumberish, _width: Numeric): bigint { const value = getBigInt(_value, "value"); @@ -37,7 +45,10 @@ export function fromTwos(_value: BigNumberish, _width: Numeric): bigint { } /** - * Convert %%value%% to a twos-compliment value of %%width%% bits. + * Convert %%value%% to a twos-compliment representation of + * %%width%% bits. + * + * The result will always be positive. */ export function toTwos(_value: BigNumberish, _width: Numeric): bigint { const value = getBigInt(_value, "value"); @@ -48,6 +59,7 @@ export function toTwos(_value: BigNumberish, _width: Numeric): bigint { return ((~(-value)) & mask) + BN_1; } + return value; } @@ -73,6 +85,7 @@ export function getBigInt(value: BigNumberish, name?: string): bigint { return BigInt(value); case "string": try { + if (value === "") { throw new Error("empty string"); } if (value[0] === "-" && value[1] !== "-") { return -BigInt(value.substring(1)); } @@ -119,6 +132,7 @@ export function getNumber(value: BigNumberish, name?: string): number { return value; case "string": try { + if (value === "") { throw new Error("empty string"); } return getNumber(BigInt(value), name); } catch(e: any) { assertArgument(false, `invalid numeric string: ${ e.message }`, name || "value", value); @@ -128,9 +142,9 @@ export function getNumber(value: BigNumberish, name?: string): number { } -/* - * Converts %%value%% to a number. If %%value%% is a Uint8Array, it - * is treated as Big Endian data. Throws if the value is not safe. +/** + * Converts %%value%% to a number. If %%value%% is a Uint8Array, it + * is treated as Big Endian data. Throws if the value is not safe. */ export function toNumber(value: BigNumberish | Uint8Array): number { return getNumber(toBigInt(value)); @@ -142,7 +156,7 @@ export function toNumber(value: BigNumberish | Uint8Array): number { */ export function toHex(_value: BigNumberish, _width?: Numeric): string { const value = getBigInt(_value, "value"); - if (value < 0) { throw new Error("cannot convert negative value to hex"); } + assertArgument(value >= 0, "cannot toHex negative value", "value", _value); let result = value.toString(16); @@ -151,7 +165,7 @@ export function toHex(_value: BigNumberish, _width?: Numeric): string { if (result.length % 2) { result = "0" + result; } } else { const width = getNumber(_width, "width"); - if (width * 2 < result.length) { throw new Error(`value ${ value } exceeds width ${ width }`); } + assertArgument(width * 2 >= result.length, `value exceeds width`, "[ value, width ]", [ _value, _width ]); // Pad the value to the required width while (result.length < (width * 2)) { result = "0" + result; } @@ -166,7 +180,7 @@ export function toHex(_value: BigNumberish, _width?: Numeric): string { */ export function toArray(_value: BigNumberish): Uint8Array { const value = getBigInt(_value, "value"); - if (value < 0) { throw new Error("cannot convert negative value to hex"); } + assertArgument(value >= 0, "cannot toArray negative value", "value", _value); if (value === BN_0) { return new Uint8Array([ ]); } diff --git a/src.ts/utils/properties.ts b/src.ts/utils/properties.ts index bbaac7f71..f6bfe773b 100644 --- a/src.ts/utils/properties.ts +++ b/src.ts/utils/properties.ts @@ -1,3 +1,13 @@ +/** + * Property helper functions. + * + * @_subsection api/utils:Properties [properties] + */ + +/** + * Resolves to a new object that is a copy of %%value%%, but with all + * values resolved. + */ export async function resolveProperties(value: { [ P in keyof T ]: T[P] | Promise}): Promise { const keys = Object.keys(value); const results = await Promise.all(keys.map((k) => Promise.resolve(value[k]))); @@ -7,104 +17,44 @@ export async function resolveProperties(value: { [ P in keyof T ]: T[P] | Pro }, <{ [ P in keyof T]: T[P] }>{ }); } -export function defineReadOnly(object: T, name: P, value: T[P]): void { - Object.defineProperty(object, name, { - enumerable: true, - value: value, - writable: false, - }); -} - -/* -export interface CancellablePromise extends Promise { - cancel(): Promise; -} -export type IsCancelled = () => Promise; - -export function createPromise(resolve: (isCancelled: IsCancelled, (result: T) => void) => void, reject: (error: Error) => void, isCancelled: IsCancelled): CancellablePromise { - let cancelled = false; - - const promise = new Promise((resolve, reject) => { - - }); - - (>promise).cancel = function() { - cancelled = true; - }; - - return (>promise); -} -*/ -/* -export class A implements Freezable { - foo: number; - constructor(foo: number) { - this.foo = foo; - } - freeze(): Frozen { - Object.freeze(this); - return this; - } - clone(): A { - return new A(this.foo); - } -} - -export class B implements Freezable { - a: A; - constructor(a: A) { - this.a = a; - } - freeze(): Frozen { - this.a.freeze(); - Object.freeze(this); - return this; - } - clone(): B { - return new B(this.a); - } -} - -export function test() { - const a = new A(123); - const b = new B(a); - b.a = new A(234); - const b2 = b.freeze(); - b2.a.foo = 123; // = a; -} -*/ - -function checkType(value: any, type: string): void { +function checkType(value: any, type: string, name: string): void { 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; } } } - throw new Error("invalid value for type"); + + const error: any = new Error(`invalid value for type ${ type }`); + error.code = "INVALID_ARGUMENT"; + error.argument = `value.${ name }`; + error.value = value; + + throw error; } +/** + * Assigns the %%values%% to %%target%% as read-only values. + * + * It %%types%% is specified, the values are checked. + */ export function defineProperties( target: T, - values: { [ K in keyof T ]?: undefined | T[K] }, - types?: { [ K in keyof T ]?: string }, - defaults?: { [ K in keyof T ]?: T[K] }): void { + values: { [ K in keyof T ]?: T[K] }, + types?: { [ K in keyof T ]?: string }): void { for (let key in values) { let value = values[key]; - const fallback = (defaults ? defaults[key]: undefined); - if (fallback !== undefined) { - value = fallback; - } else { - const type = (types ? types[key]: null); - if (type) { checkType(value, type); } - } + const type = (types ? types[key]: null); + if (type) { checkType(value, type, key); } + Object.defineProperty(target, key, { enumerable: true, value, writable: false }); } } diff --git a/src.ts/utils/rlp-encode.ts b/src.ts/utils/rlp-encode.ts index 8fca87aff..9800e4b2f 100644 --- a/src.ts/utils/rlp-encode.ts +++ b/src.ts/utils/rlp-encode.ts @@ -52,7 +52,7 @@ function _encode(object: Array | string): Array { const nibbles = "0123456789abcdef"; /** - * Encodes %%object%% as an RLP-encoded [[HexDataString]]. + * Encodes %%object%% as an RLP-encoded [[DataHexString]]. */ export function encodeRlp(object: RlpStructuredData): string { let result = "0x"; diff --git a/src.ts/utils/rlp.ts b/src.ts/utils/rlp.ts index d455a7f9e..dbd423eeb 100644 --- a/src.ts/utils/rlp.ts +++ b/src.ts/utils/rlp.ts @@ -1,8 +1,15 @@ +/** + * The [[link-rlp]] (RLP) encoding is used throughout Ethereum + * to serialize nested structures of Arrays and data. + * + * @_subsection api/utils:Recursive-Length Prefix [rlp] + */ + +export { decodeRlp } from "./rlp-decode.js"; +export { encodeRlp } from "./rlp-encode.js"; /** * An RLP-encoded structure. */ export type RlpStructuredData = string | Array; -export { decodeRlp } from "./rlp-decode.js"; -export { encodeRlp } from "./rlp-encode.js"; diff --git a/src.ts/utils/units.ts b/src.ts/utils/units.ts index 217dbc5a9..d7fc9f2e6 100644 --- a/src.ts/utils/units.ts +++ b/src.ts/utils/units.ts @@ -1,3 +1,24 @@ +/** + * Most interactions with Ethereum requires integer values, which use + * the smallest magnitude unit. + * + * For example, imagine dealing with dollars and cents. Since dollars + * are divisible, non-integer values are possible, such as ``$10.77``. + * By using the smallest indivisible unit (i.e. cents), the value can + * be kept as the integer ``1077``. + * + * When receiving decimal input from the user (as a decimal string), + * the value should be converted to an integer and when showing a user + * a value, the integer value should be converted to a decimal string. + * + * This creates a clear distinction, between values to be used by code + * (integers) and values used for display logic to users (decimals). + * + * The native unit in Ethereum, //ether// is divisible to 18 decimal places, + * where each individual unit is called a //wei//. + * + * @_subsection api/utils:Unit Conversion [units] + */ import { formatFixed, parseFixed } from "./fixednumber.js"; import { assertArgument } from "./errors.js"; diff --git a/src.ts/utils/utf8.ts b/src.ts/utils/utf8.ts index 938c7c067..8fee360e2 100644 --- a/src.ts/utils/utf8.ts +++ b/src.ts/utils/utf8.ts @@ -1,3 +1,11 @@ +/** + * 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 [strings] + */ import { getBytes } from "./data.js"; import { assertArgument, assertNormalize } from "./errors.js"; @@ -6,49 +14,75 @@ import type { BytesLike } from "./index.js"; /////////////////////////////// +/** + * The stanard normalization forms. + */ export type UnicodeNormalizationForm = "NFC" | "NFD" | "NFKC" | "NFKD"; -export type Utf8ErrorReason = - // A continuation byte was present where there was nothing to continue - // - offset = the index the codepoint began in - "UNEXPECTED_CONTINUE" | - - // An invalid (non-continuation) byte to start a UTF-8 codepoint was found - // - offset = the index the codepoint began in - "BAD_PREFIX" | - - // The string is too short to process the expected codepoint - // - offset = the index the codepoint began in - "OVERRUN" | - - // A missing continuation byte was expected but not found - // - offset = the index the continuation byte was expected at - "MISSING_CONTINUE" | - - // The computed code point is outside the range for UTF-8 - // - offset = start of this codepoint - // - badCodepoint = the computed codepoint; outside the UTF-8 range - "OUT_OF_RANGE" | - - // UTF-8 strings may not contain UTF-16 surrogate pairs - // - offset = start of this codepoint - // - badCodepoint = the computed codepoint; inside the UTF-16 surrogate range - "UTF16_SURROGATE" | - - // The string is an overlong representation - // - offset = start of this codepoint - // - badCodepoint = the computed codepoint; already bounds checked - "OVERLONG"; +/** + * When using the UTF-8 error API the following errors can be intercepted + * and processed as the %%reason%% passed to the [[Utf8ErrorFunc]]. + * + * **``"UNEXPECTED_CONTINUE"``** - a continuation byte was present where there + * was nothing to continue. + * + * **``"BAD_PREFIX"``** - an invalid (non-continuation) byte to start a + * UTF-8 codepoint was found. + * + * **``"OVERRUN"``** - the string is too short to process the expected + * codepoint length. + * + * **``"MISSING_CONTINUE"``** - a missing continuation byte was expected but + * not found. The %%offset%% indicates the index the continuation byte + * was expected at. + * + * **``"OUT_OF_RANGE"``** - the computed code point is outside the range + * for UTF-8. The %%badCodepoint%% indicates the computed codepoint, which was + * outside the valid UTF-8 range. + * + * **``"UTF16_SURROGATE"``** - the UTF-8 strings contained a UTF-16 surrogate + * pair. The %%badCodepoint%% is the computed codepoint, which was inside the + * UTF-16 surrogate range. + * + * **``"OVERLONG"``** - the string is an overlong representation. The + * %%badCodepoint%% indicates the computed codepoint, which has already + * been bounds checked. + * + * + * @returns string + */ +export type Utf8ErrorReason = "UNEXPECTED_CONTINUE" | "BAD_PREFIX" | "OVERRUN" | + "MISSING_CONTINUE" | "OUT_OF_RANGE" | "UTF16_SURROGATE" | "OVERLONG"; -export type Utf8ErrorFunc = (reason: Utf8ErrorReason, offset: number, bytes: ArrayLike, output: Array, badCodepoint?: number) => number; +/** + * A callback that can be used with [[toUtf8String]] to analysis or + * recovery from invalid UTF-8 data. + * + * Parsing UTF-8 data is done through a simple Finite-State Machine (FSM) + * which calls the ``Utf8ErrorFunc`` if a fault is detected. + * + * The %%reason%% indicates where in the FSM execution the fault + * occurred and the %%offset%% indicates where the input failed. + * + * The %%bytes%% represents the raw UTF-8 data that was provided and + * %%output%% is the current array of UTF-8 code-points, which may + * be updated by the ``Utf8ErrorFunc``. + * + * The value of the %%badCodepoint%% depends on the %%reason%%. See + * [[Utf8ErrorReason]] for details. + * + * The function should return the number of bytes that should be skipped + * when control resumes to the FSM. + */ +export type Utf8ErrorFunc = (reason: Utf8ErrorReason, offset: number, bytes: Uint8Array, output: Array, badCodepoint?: number) => number; -function errorFunc(reason: Utf8ErrorReason, offset: number, bytes: ArrayLike, output: Array, badCodepoint?: number): number { +function errorFunc(reason: Utf8ErrorReason, offset: number, bytes: Uint8Array, output: Array, badCodepoint?: number): number { assertArgument(false, `invalid codepoint at offset ${ offset }; ${ reason }`, "bytes", bytes); } -function ignoreFunc(reason: Utf8ErrorReason, offset: number, bytes: ArrayLike, output: Array, badCodepoint?: number): number { +function ignoreFunc(reason: Utf8ErrorReason, offset: number, bytes: Uint8Array, output: Array, badCodepoint?: number): number { // If there is an invalid prefix (including stray continuation), skip any additional continuation bytes if (reason === "BAD_PREFIX" || reason === "UNEXPECTED_CONTINUE") { @@ -70,11 +104,12 @@ function ignoreFunc(reason: Utf8ErrorReason, offset: number, bytes: ArrayLike, output: Array, badCodepoint?: number): number { +function replaceFunc(reason: Utf8ErrorReason, offset: number, bytes: Uint8Array, output: Array, badCodepoint?: number): number { // Overlong representations are otherwise "valid" code points; just non-deistingtished if (reason === "OVERLONG") { - output.push((badCodepoint != null) ? badCodepoint: -1); + assertArgument(typeof(badCodepoint) === "number", "invalid bad code point for replacement", "badCodepoint", badCodepoint); + output.push(badCodepoint); return 0; } @@ -191,6 +226,12 @@ function getUtf8CodePoints(_bytes: BytesLike, onError?: Utf8ErrorFunc): Array): string { +//export +function _toUtf8String(codePoints: Array): string { return codePoints.map((codePoint) => { if (codePoint <= 0xffff) { return String.fromCharCode(codePoint); @@ -246,10 +288,22 @@ export function _toUtf8String(codePoints: Array): string { }).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-error)) + */ export function toUtf8String(bytes: BytesLike, onError?: Utf8ErrorFunc): string { return _toUtf8String(getUtf8CodePoints(bytes, onError)); } +/** + * Returns the UTF-8 code-points for %%str%%. + * + * If %%form%% is specified, the string is normalized. + */ export function toUtf8CodePoints(str: string, form?: UnicodeNormalizationForm): Array { return getUtf8CodePoints(toUtf8Bytes(str, form)); } diff --git a/src.ts/utils/uuid.ts b/src.ts/utils/uuid.ts new file mode 100644 index 000000000..18f987527 --- /dev/null +++ b/src.ts/utils/uuid.ts @@ -0,0 +1,31 @@ +import { getBytes, hexlify } from "./data.js"; + +import type { BytesLike } from "./index.js"; + +/** + * Returns the version 4 [[link-uuid]] for the %%randomBytes%%. + * + * @see: https://www.ietf.org/rfc/rfc4122.txt (Section 4.4) + */ +export function uuidV4(randomBytes: BytesLike): string { + const bytes = getBytes(randomBytes, "randomBytes"); + + // Section: 4.1.3: + // - time_hi_and_version[12:16] = 0b0100 + bytes[6] = (bytes[6] & 0x0f) | 0x40; + + // Section 4.4 + // - clock_seq_hi_and_reserved[6] = 0b0 + // - clock_seq_hi_and_reserved[7] = 0b1 + bytes[8] = (bytes[8] & 0x3f) | 0x80; + + const value = hexlify(bytes); + + return [ + value.substring(2, 10), + value.substring(10, 14), + value.substring(14, 18), + value.substring(18, 22), + value.substring(22, 34), + ].join("-"); +}