docs: added jsdocs for utils

This commit is contained in:
Richard Moore 2022-11-27 21:50:34 -05:00
parent 1966c2d6d4
commit fe342ca48a
15 changed files with 762 additions and 240 deletions

@ -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;
}

@ -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");

@ -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<BytesLike>): 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 {

@ -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<T> = Omit<T, "code" | "name" | "message">;
// The type of error to use for various error codes
const ErrorConstructors: Record<string, { new (...args: Array<any>): 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<T extends ErrorCode = ErrorCode> 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<string, any>
}
@ -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<T> =
* @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<K extends ErrorCode, T extends CodedEthersError<K>>(me
const details: Array<string> = [];
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 = <any>(info[<keyof ErrorInfo<T>>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,15 +489,22 @@ export function makeError<K extends ErrorCode, T extends CodedEthersError<K>>(me
}
}
const create = ErrorConstructors[code] || Error;
const error = <T>(new create(message));
defineReadOnly(error, "code", code);
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);
}
if (info) {
for (const key in info) {
defineReadOnly(error, <keyof T>key, <any>(info[<keyof ErrorInfo<T>>key]));
}
}
defineProperties<EthersError>(<EthersError>error, { code });
if (info) { defineProperties<any>(error, info); }
return <T>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) {

@ -1,3 +1,8 @@
/**
* Explain events...
*
* @_section api/utils/events:Events [events]
*/
import { defineProperties } from "./properties.js";
export type Listener = (...args: Array<any>) => void;

@ -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<FetchRequest>;
export type FetchPreflightFunc = (req: FetchRequest) => Promise<FetchRequest>;
/**
* Called on the response, allowing client-based throttling logic or post-processing.
*/
export type FetchProcessFunc = (request: FetchRequest, response: FetchResponse) => Promise<FetchResponse>;
export type FetchProcessFunc = (req: FetchRequest, resp: FetchResponse) => Promise<FetchResponse>;
/**
* Called prior to each retry; return true to retry, false to abort.
*/
export type FetchRetryFunc = (request: FetchRequest, response: FetchResponse, attempt: number) => Promise<boolean>;
export type FetchRetryFunc = (req: FetchRequest, resp: FetchResponse, attempt: number) => Promise<boolean>;
/**
* 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<GetUrlResponse>;
export type FetchGetUrlFunc = (req: FetchRequest, signal?: FetchCancelSignal) => Promise<GetUrlResponse>;
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<FetchResponse> {
async function dataGatewayFunc(url: string, signal?: FetchCancelSignal): Promise<FetchResponse> {
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<Fet
* Returns a [[FetchGatewayFunc]] for fetching content from a standard
* IPFS gateway hosted at %%baseUrl%%.
*/
export function getIpfsGatewayFunc(baseUrl: string): FetchGatewayFunc {
function getIpfsGatewayFunc(baseUrl: string): FetchGatewayFunc {
async function gatewayIpfs(url: string, signal?: FetchCancelSignal): Promise<FetchRequest | FetchResponse> {
try {
const match = url.match(reIpfs);
@ -91,12 +96,15 @@ export function getIpfsGatewayFunc(baseUrl: string): FetchGatewayFunc {
}
const Gateways: Record<string, FetchGatewayFunc> = {
"data": gatewayData,
"data": dataGatewayFunc,
"ipfs": getIpfsGatewayFunc("https:/\/gateway.ipfs.io/ipfs/")
};
const fetchSignals: WeakMap<FetchRequest, () => 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<Record<string, string>> {
get headers(): Record<string, string> {
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<Record<string, string>>;
#headers: Record<string, string>;
#body: null | Readonly<Uint8Array>;
#request: null | FetchRequest;
@ -653,19 +717,20 @@ 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<string, string> { return this.#headers; }
get headers(): Record<string, string> { return Object.assign({ }, this.#headers); }
/**
* The response body.
* The response body, or ``null`` if there was no body.
*/
get body(): null | Readonly<Uint8Array> {
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.
*/
@ -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<Record<string, string>>, 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;
}, <Record<string, string>>{ })));
}, <Record<string, string>>{ });
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);

@ -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);
}

@ -1,18 +1,10 @@
////
export interface Freezable<T> {
clone(): T;
freeze(): Frozen<T>;
isFrozen(): boolean;
}
export type Frozen<T> = Readonly<{
[ P in keyof T ]: T[P] extends (...args: Array<any>) => any ? T[P]:
T[P] extends Freezable<any> ? Frozen<T[P]>:
Readonly<T[P]>;
}>;
/**
* 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

@ -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,7 +142,7 @@ 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.
*/
@ -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([ ]); }

@ -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<T>(value: { [ P in keyof T ]: T[P] | Promise<T[P]>}): Promise<T> {
const keys = Object.keys(value);
const results = await Promise.all(keys.map((k) => Promise.resolve(value[<keyof T>k])));
@ -7,104 +17,44 @@ export async function resolveProperties<T>(value: { [ P in keyof T ]: T[P] | Pro
}, <{ [ P in keyof T]: T[P] }>{ });
}
export function defineReadOnly<T, P extends keyof T>(object: T, name: P, value: T[P]): void {
Object.defineProperty(object, name, {
enumerable: true,
value: value,
writable: false,
});
}
/*
export interface CancellablePromise<T> extends Promise<T> {
cancel(): Promise<void>;
}
export type IsCancelled = () => Promise<boolean>;
export function createPromise<T>(resolve: (isCancelled: IsCancelled, (result: T) => void) => void, reject: (error: Error) => void, isCancelled: IsCancelled): CancellablePromise<T> {
let cancelled = false;
const promise = new Promise((resolve, reject) => {
});
(<CancellablePromise<T>>promise).cancel = function() {
cancelled = true;
};
return (<CancellablePromise<T>>promise);
}
*/
/*
export class A implements Freezable<A> {
foo: number;
constructor(foo: number) {
this.foo = foo;
}
freeze(): Frozen<A> {
Object.freeze(this);
return this;
}
clone(): A {
return new A(this.foo);
}
}
export class B implements Freezable<B> {
a: A;
constructor(a: A) {
this.a = a;
}
freeze(): Frozen<B> {
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<T>(
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); }
}
if (type) { checkType(value, type, key); }
Object.defineProperty(target, key, { enumerable: true, value, writable: false });
}
}

@ -52,7 +52,7 @@ function _encode(object: Array<any> | string): Array<number> {
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";

@ -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<RlpStructuredData>;
export { decodeRlp } from "./rlp-decode.js";
export { encodeRlp } from "./rlp-encode.js";

@ -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";

@ -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<number>, output: Array<number>, 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<number>, badCodepoint?: number) => number;
function errorFunc(reason: Utf8ErrorReason, offset: number, bytes: ArrayLike<number>, output: Array<number>, badCodepoint?: number): number {
function errorFunc(reason: Utf8ErrorReason, offset: number, bytes: Uint8Array, output: Array<number>, badCodepoint?: number): number {
assertArgument(false, `invalid codepoint at offset ${ offset }; ${ reason }`, "bytes", bytes);
}
function ignoreFunc(reason: Utf8ErrorReason, offset: number, bytes: ArrayLike<number>, output: Array<number>, badCodepoint?: number): number {
function ignoreFunc(reason: Utf8ErrorReason, offset: number, bytes: Uint8Array, output: Array<number>, 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<nu
return 0;
}
function replaceFunc(reason: Utf8ErrorReason, offset: number, bytes: ArrayLike<number>, output: Array<number>, badCodepoint?: number): number {
function replaceFunc(reason: Utf8ErrorReason, offset: number, bytes: Uint8Array, output: Array<number>, 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<nu
}
// 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.
*/
export function toUtf8Bytes(str: string, form?: UnicodeNormalizationForm): Uint8Array {
if (form != null) {
@ -233,7 +274,8 @@ export function toUtf8Bytes(str: string, form?: UnicodeNormalizationForm): Uint8
return new Uint8Array(result);
};
export function _toUtf8String(codePoints: Array<number>): string {
//export
function _toUtf8String(codePoints: Array<number>): string {
return codePoints.map((codePoint) => {
if (codePoint <= 0xffff) {
return String.fromCharCode(codePoint);
@ -246,10 +288,22 @@ export function _toUtf8String(codePoints: Array<number>): 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<number> {
return getUtf8CodePoints(toUtf8Bytes(str, form));
}

31
src.ts/utils/uuid.ts Normal file

@ -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("-");
}