diff --git a/src/_tests/test-abi.ts b/src/_tests/test-abi.ts index 9c47ed2f..817716d0 100644 --- a/src/_tests/test-abi.ts +++ b/src/_tests/test-abi.ts @@ -3,7 +3,7 @@ import { loadTests } from './utils.js'; import type { TestCaseAbi, TestCaseAbiVerbose } from './types.js'; -import { AbiCoder, Interface, decodeBytes32String, encodeBytes32String } from '../index.js'; +import { AbiCoder, Interface, decodeBytes32, encodeBytes32 } from '../index.js'; function equal(actual: any, expected: TestCaseAbiVerbose): void { switch (expected.type) { @@ -62,8 +62,8 @@ describe('Test Bytes32 strings', function () { for (const { name, str, expected } of tests) { it(`encodes and decodes Bytes32 strings: ${name}`, function () { - const bytes32 = encodeBytes32String(str); - const decoded = decodeBytes32String(bytes32); + const bytes32 = encodeBytes32(str); + const decoded = decodeBytes32(bytes32); assert.equal(bytes32, expected, 'formatted correctly'); assert.equal(decoded, str, 'parsed correctly'); }); diff --git a/src/_tests/test-providers-data.ts b/src/_tests/test-providers-data.ts index e3f735ba..c1c377e3 100644 --- a/src/_tests/test-providers-data.ts +++ b/src/_tests/test-providers-data.ts @@ -123,7 +123,7 @@ async function sendTransaction(to: string) { } = { from: wallet.address, to, - value: quais.parseEther('0.1'), // Sending 0.1 ether + value: quais.parseQuai('0.1'), // Sending 0.1 ether gasPrice: gas * 2, maxFeePerGas: quais.parseUnits('20', 'gwei'), maxPriorityFeePerGas: quais.parseUnits('20', 'gwei'), diff --git a/src/_tests/test-utils-misc.ts b/src/_tests/test-utils-misc.ts index e90a409a..b097324b 100644 --- a/src/_tests/test-utils-misc.ts +++ b/src/_tests/test-utils-misc.ts @@ -1,6 +1,7 @@ import assert from 'assert'; -import { decodeBase64, encodeBase64, defineProperties, isError, toUtf8Bytes } from '../index.js'; +import { decodeBase64, encodeBase64, isError, toUtf8Bytes } from '../index.js'; +import { defineProperties } from '../utils/index.js'; describe('Base64 Coding', function () { const tests = [ diff --git a/src/_tests/test-utils-units.ts b/src/_tests/test-utils-units.ts index d82bd593..32281d3a 100644 --- a/src/_tests/test-utils-units.ts +++ b/src/_tests/test-utils-units.ts @@ -2,7 +2,7 @@ import assert from 'assert'; import { loadTests } from './utils.js'; -import { formatEther, formatUnits, parseEther, parseUnits } from '../index.js'; +import { formatQuai, formatUnits, parseQuai, parseUnits } from '../index.js'; import type { TestCaseUnit } from './types.js'; @@ -28,7 +28,7 @@ describe('Tests unit conversion', function () { it(`converts wei to ${unit} string: ${test.name}`, function () { const wei = BigInt(test.wei); if (decimals === 18) { - assert.equal(formatEther(wei), str, 'formatEther'); + assert.equal(formatQuai(wei), str, 'formatQuai'); assert.equal(formatUnits(wei), str, 'formatUnits'); } assert.equal(formatUnits(wei, unit), str, `formatUnits(${unit})`); @@ -45,7 +45,7 @@ describe('Tests unit conversion', function () { it(`converts ${format} string to wei: ${test.name}`, function () { const wei = BigInt(test.wei); if (decimals === 18) { - assert.equal(parseEther(str), wei, 'parseEther'); + assert.equal(parseQuai(str), wei, 'parseQuai'); assert.equal(parseUnits(str), wei, 'parseUnits'); } assert.equal(parseUnits(str, unit), wei, `parseUnits(${unit})`); diff --git a/src/_tests/test-utils-utf8.ts b/src/_tests/test-utils-utf8.ts index 2648127d..2f88b0fa 100644 --- a/src/_tests/test-utils-utf8.ts +++ b/src/_tests/test-utils-utf8.ts @@ -1,6 +1,7 @@ import assert from 'assert'; -import { toUtf8Bytes, toUtf8CodePoints, toUtf8String, Utf8ErrorFuncs } from '../index.js'; +import { toUtf8Bytes, toUtf8CodePoints, toUtf8String } from '../index.js'; +import { Utf8ErrorFuncs } from '../encoding/index.js'; export type TestCaseBadString = { name: string; diff --git a/src/abi/index.ts b/src/abi/index.ts index 997f5dbe..5258c3f8 100644 --- a/src/abi/index.ts +++ b/src/abi/index.ts @@ -9,8 +9,6 @@ export { AbiCoder } from './abi-coder.js'; -export { decodeBytes32String, encodeBytes32String } from './bytes32.js'; - export { ConstructorFragment, ErrorFragment, diff --git a/src/address/checks.ts b/src/address/checks.ts index b3245302..9a0a2053 100644 --- a/src/address/checks.ts +++ b/src/address/checks.ts @@ -127,3 +127,31 @@ export function validateAddress(address: string): void { ); assertArgument(formatMixedCaseChecksumAddress(address) === address, 'invalid address checksum', 'address', address); } + +/** + * Checks whether a given address is in the Qi ledger scope by checking the 9th bit of the address. + * + * @category Address + * @param {string} address - The address to check + * + * @returns {boolean} True if the address is in the Qi ledger scope, false otherwise. + */ +export function isQiAddress(address: string): boolean { + const secondByte = address.substring(4, 6); + const binaryString = parseInt(secondByte, 16).toString(2).padStart(8, '0'); + const isUTXO = binaryString[0] === '1'; + + return isUTXO; +} + +/** + * Checks whether a given address is in the Quai ledger scope by checking the 9th bit of the address. + * + * @category Address + * @param {string} address - The address to check + * + * @returns {boolean} True if the address is in the Quai ledger scope, false otherwise. + */ +export function isQuaiAddress(address: string): boolean { + return !isQiAddress(address); +} diff --git a/src/address/index.ts b/src/address/index.ts index 8025f07d..bfa50e4a 100644 --- a/src/address/index.ts +++ b/src/address/index.ts @@ -31,8 +31,8 @@ export interface Addressable { */ export type AddressLike = string | Promise | Addressable; -export { getAddress, computeAddress, recoverAddress, formatMixedCaseChecksumAddress } from './address.js'; +export { getAddress, computeAddress, recoverAddress, formatMixedCaseChecksumAddress, getContractAddress } from './address.js'; export { getCreateAddress, getCreate2Address } from './contract-address.js'; -export { isAddressable, isAddress, resolveAddress, validateAddress } from './checks.js'; +export { isAddressable, isAddress, resolveAddress, validateAddress, isQuaiAddress, isQiAddress } from './checks.js'; diff --git a/src/constants/shards.ts b/src/constants/shards.ts index bc7da889..8702cc26 100644 --- a/src/constants/shards.ts +++ b/src/constants/shards.ts @@ -1,5 +1,12 @@ import { ZoneData } from './zones.js'; +/** + * A shard represents a chain within the Quai network hierarchy. A shard refer to the Prime chain, a region under the + * Prime chain, or a Zone within a region. The value is a hexadecimal string representing the encoded value of the + * shard. Read more [here](https://github.com/quai-network/qips/blob/master/qip-0002.md). + * + * @category Constants + */ export enum Shard { Cyprus = '0x0', Cyprus1 = '0x00', diff --git a/src/constants/zones.ts b/src/constants/zones.ts index 0599c62e..6cd47857 100644 --- a/src/constants/zones.ts +++ b/src/constants/zones.ts @@ -1,3 +1,10 @@ +/** + * A zone is the lowest level shard within the Quai network hierarchy. Zones are the only shards in the network that + * accept user transactions. The value is a hexadecimal string representing the encoded value of the zone. Read more + * [here](https://github.com/quai-network/qips/blob/master/qip-0002.md). + * + * @category Constants + */ export enum Zone { Cyprus1 = '0x00', Cyprus2 = '0x01', diff --git a/src/contract/contract.ts b/src/contract/contract.ts index eea6097f..f0fab227 100644 --- a/src/contract/contract.ts +++ b/src/contract/contract.ts @@ -50,34 +50,68 @@ import { toShard, Zone } from '../constants/index.js'; const BN_0 = BigInt(0); +/** + * Interface for a contract runner that can call transactions. + * @interface + */ interface ContractRunnerCaller extends ContractRunner { call: (tx: TransactionRequest) => Promise; } +/** + * Interface for a contract runner that can estimate gas. + * @interface + */ interface ContractRunnerEstimater extends ContractRunner { estimateGas: (tx: TransactionRequest) => Promise; } +/** + * Interface for a contract runner that can send transactions. + * @interface + */ interface ContractRunnerSender extends ContractRunner { sendTransaction: (tx: TransactionRequest) => Promise; } +/** + * Check if the value can call transactions. + * @param {any} value - The value to check. + * @returns {value is ContractRunnerCaller} True if the value can call transactions. + */ function canCall(value: any): value is ContractRunnerCaller { return value && typeof value.call === 'function'; } +/** + * Check if the value can estimate gas. + * @param {any} value - The value to check. + * @returns {value is ContractRunnerEstimater} True if the value can estimate gas. + */ function canEstimate(value: any): value is ContractRunnerEstimater { return value && typeof value.estimateGas === 'function'; } +/** + * Check if the value can send transactions. + * @param {any} value - The value to check. + * @returns {value is ContractRunnerSender} True if the value can send transactions. + */ function canSend(value: any): value is ContractRunnerSender { return value && typeof value.sendTransaction === 'function'; } +/** + * Class representing a prepared topic filter. + * @implements {DeferredTopicFilter} + */ class PreparedTopicFilter implements DeferredTopicFilter { #filter: Promise; readonly fragment!: EventFragment; + /** + * @ignore + */ constructor(contract: BaseContract, fragment: EventFragment, args: Array) { defineProperties(this, { fragment }); if (fragment.inputs.length < args.length) { @@ -108,18 +142,21 @@ class PreparedTopicFilter implements DeferredTopicFilter { })(); } + /** + * Get the topic filter. + * @returns {Promise} The topic filter. + */ getTopicFilter(): Promise { return this.#filter; } } -// A = Arguments passed in as a tuple -// R = The result type of the call (i.e. if only one return type, -// the qualified type, otherwise Result) -// D = The type the default call will return (i.e. R for view/pure, -// TransactionResponse otherwise) -//export interface ContractMethod = Array, R = any, D extends R | ContractTransactionResponse = ContractTransactionResponse> { - +/** + * Get the runner for a specific feature. + * @param {any} value - The value to check. + * @param {keyof ContractRunner} feature - The feature to check for. + * @returns {null | T} The runner if available, otherwise null. + */ function getRunner(value: any, feature: keyof ContractRunner): null | T { if (value == null) { return null; @@ -133,6 +170,11 @@ function getRunner(value: any, feature: keyof Contract return null; } +/** + * Get the provider from a contract runner. + * @param {null | ContractRunner} value - The contract runner. + * @returns {null | Provider} The provider if available, otherwise null. + */ function getProvider(value: null | ContractRunner): null | Provider { if (value == null) { return null; @@ -142,6 +184,11 @@ function getProvider(value: null | ContractRunner): null | Provider { /** * @ignore + * Copy overrides and validate them. + * @param {any} arg - The argument containing overrides. + * @param {Array} [allowed] - The allowed override keys. + * @returns {Promise>} The copied and validated overrides. + * @throws {Error} If the overrides are invalid. */ export async function copyOverrides( arg: any, @@ -177,6 +224,11 @@ export async function copyOverrides( /** * @ignore + * Resolve arguments for a contract runner. + * @param {null | ContractRunner} _runner - The contract runner. + * @param {ReadonlyArray} inputs - The input parameter types. + * @param {Array} args - The arguments to resolve. + * @returns {Promise>} The resolved arguments. */ export async function resolveArgs( _runner: null | ContractRunner, @@ -197,7 +249,18 @@ export async function resolveArgs( ); } +/** + * Build a wrapped fallback method for a contract. + * @param {BaseContract} contract - The contract instance. + * @returns {WrappedFallback} The wrapped fallback method. + */ function buildWrappedFallback(contract: BaseContract): WrappedFallback { + /** + * Populate a transaction with overrides. + * @param {Omit} [overrides] - The transaction overrides. + * @returns {Promise} The populated transaction. + * @throws {Error} If the overrides are invalid. + */ const populateTransaction = async function ( overrides?: Omit, ): Promise { @@ -248,6 +311,12 @@ function buildWrappedFallback(contract: BaseContract): WrappedFallback { return tx; }; + /** + * Perform a static call with the given overrides. + * @param {Omit} [overrides] - The transaction overrides. + * @returns {Promise} The result of the static call. + * @throws {Error} If the call fails. + */ const staticCall = async function (overrides?: Omit): Promise { const runner = getRunner(contract.runner, 'call'); assert(canCall(runner), 'contract runner does not support calling', 'UNSUPPORTED_OPERATION', { @@ -266,6 +335,12 @@ function buildWrappedFallback(contract: BaseContract): WrappedFallback { } }; + /** + * Send a transaction with the given overrides. + * @param {Omit} [overrides] - The transaction overrides. + * @returns {Promise} The transaction response. + * @throws {Error} If the transaction fails. + */ const send = async function (overrides?: Omit): Promise { const runner = contract.runner; assert(canSend(runner), 'contract runner does not support sending transactions', 'UNSUPPORTED_OPERATION', { @@ -279,6 +354,12 @@ function buildWrappedFallback(contract: BaseContract): WrappedFallback { return new ContractTransactionResponse(contract.interface, provider, tx); }; + /** + * Estimate the gas required for a transaction with the given overrides. + * @param {Omit} [overrides] - The transaction overrides. + * @returns {Promise} The estimated gas. + * @throws {Error} If the gas estimation fails. + */ const estimateGas = async function (overrides?: Omit): Promise { const runner = getRunner(contract.runner, 'estimateGas'); assert(canEstimate(runner), 'contract runner does not support gas estimation', 'UNSUPPORTED_OPERATION', { @@ -288,6 +369,12 @@ function buildWrappedFallback(contract: BaseContract): WrappedFallback { return await runner.estimateGas(await populateTransaction(overrides)); }; + /** + * Send a transaction with the given overrides. + * @param {Omit} [overrides] - The transaction overrides. + * @returns {Promise} The transaction response. + * @throws {Error} If the transaction fails. + */ const method = async (overrides?: Omit) => { return await send(overrides); }; @@ -304,11 +391,23 @@ function buildWrappedFallback(contract: BaseContract): WrappedFallback { return method; } +/** + * Build a wrapped method for a contract. + * @param {BaseContract} contract - The contract instance. + * @param {string} key - The method key. + * @returns {BaseContractMethod} The wrapped method. + */ function buildWrappedMethod< A extends Array = Array, R = any, D extends R | ContractTransactionResponse = ContractTransactionResponse, >(contract: BaseContract, key: string): BaseContractMethod { + /** + * Get the function fragment for the given arguments. + * @param {...ContractMethodArgs} args - The method arguments. + * @returns {FunctionFragment} The function fragment. + * @throws {Error} If no matching fragment is found. + */ const getFragment = function (...args: ContractMethodArgs): FunctionFragment { const fragment = contract.interface.getFunction(key, args); assert(fragment, 'no matching fragment', 'UNSUPPORTED_OPERATION', { @@ -318,6 +417,12 @@ function buildWrappedMethod< return fragment; }; + /** + * Populate a transaction with the given arguments. + * @param {...ContractMethodArgs} args - The method arguments. + * @returns {Promise} The populated transaction. + * @throws {Error} If the arguments are invalid. + */ const populateTransaction = async function (...args: ContractMethodArgs): Promise { const fragment = getFragment(...args); @@ -351,6 +456,12 @@ function buildWrappedMethod< }); }; + /** + * Perform a static call with the given arguments. + * @param {...ContractMethodArgs} args - The method arguments. + * @returns {Promise} The result of the static call. + * @throws {Error} If the call fails. + */ const staticCall = async function (...args: ContractMethodArgs): Promise { const result = await staticCallResult(...args); if (result.length === 1) { @@ -359,6 +470,12 @@ function buildWrappedMethod< return (result); }; + /** + * Send a transaction with the given arguments. + * @param {...ContractMethodArgs} args - The method arguments. + * @returns {Promise} The transaction response. + * @throws {Error} If the transaction fails. + */ const send = async function (...args: ContractMethodArgs): Promise { const runner = contract.runner; assert(canSend(runner), 'contract runner does not support sending transactions', 'UNSUPPORTED_OPERATION', { @@ -376,6 +493,12 @@ function buildWrappedMethod< return new ContractTransactionResponse(contract.interface, provider, tx); }; + /** + * Estimate the gas required for a transaction with the given arguments. + * @param {...ContractMethodArgs} args - The method arguments. + * @returns {Promise} The estimated gas. + * @throws {Error} If the gas estimation fails. + */ const estimateGas = async function (...args: ContractMethodArgs): Promise { const runner = getRunner(contract.runner, 'estimateGas'); assert(canEstimate(runner), 'contract runner does not support gas estimation', 'UNSUPPORTED_OPERATION', { @@ -385,6 +508,12 @@ function buildWrappedMethod< return await runner.estimateGas(await populateTransaction(...args)); }; + /** + * Perform a static call and return the result with the given arguments. + * @param {...ContractMethodArgs} args - The method arguments. + * @returns {Promise} The result of the static call. + * @throws {Error} If the call fails. + */ const staticCallResult = async function (...args: ContractMethodArgs): Promise { const runner = getRunner(contract.runner, 'call'); assert(canCall(runner), 'contract runner does not support calling', 'UNSUPPORTED_OPERATION', { @@ -409,6 +538,12 @@ function buildWrappedMethod< return contract.interface.decodeFunctionResult(fragment, result); }; + /** + * Send a transaction or perform a static call based on the method arguments. + * @param {...ContractMethodArgs} args - The method arguments. + * @returns {Promise} The result of the method call. + * @throws {Error} If the method call fails. + */ const method = async (...args: ContractMethodArgs) => { const fragment = getFragment(...args); if (fragment.constant) { @@ -448,7 +583,19 @@ function buildWrappedMethod< return >method; } +/** + * Build a wrapped event for a contract. + * @param {BaseContract} contract - The contract instance. + * @param {string} key - The event key. + * @returns {ContractEvent} The wrapped event. + */ function buildWrappedEvent = Array>(contract: BaseContract, key: string): ContractEvent { + /** + * Get the event fragment for the given arguments. + * @param {...ContractEventArgs} args - The event arguments. + * @returns {EventFragment} The event fragment. + * @throws {Error} If no matching fragment is found. + */ const getFragment = function (...args: ContractEventArgs): EventFragment { const fragment = contract.interface.getEvent(key, args); @@ -460,6 +607,11 @@ function buildWrappedEvent = Array>(contract: BaseCont return fragment; }; + /** + * Create a prepared topic filter for the event. + * @param {...ContractMethodArgs} args - The event arguments. + * @returns {PreparedTopicFilter} The prepared topic filter. + */ const method = function (...args: ContractMethodArgs): PreparedTopicFilter { return new PreparedTopicFilter(contract, getFragment(...args), args); }; @@ -515,14 +667,29 @@ type Internal = { const internalValues: WeakMap = new WeakMap(); +/** + * Set internal values for a contract. + * @param {BaseContract} contract - The contract instance. + * @param {Internal} values - The internal values. + */ function setInternal(contract: BaseContract, values: Internal): void { internalValues.set(contract[internal], values); } +/** + * Get internal values for a contract. + * @param {BaseContract} contract - The contract instance. + * @returns {Internal} The internal values. + */ function getInternal(contract: BaseContract): Internal { return internalValues.get(contract[internal]) as Internal; } +/** + * Check if a value is a deferred topic filter. + * @param {any} value - The value to check. + * @returns {value is DeferredTopicFilter} True if the value is a deferred topic filter. + */ function isDeferred(value: any): value is DeferredTopicFilter { return ( value && @@ -533,6 +700,13 @@ function isDeferred(value: any): value is DeferredTopicFilter { ); } +/** + * Get subscription information for an event. + * @param {BaseContract} contract - The contract instance. + * @param {ContractEventName} event - The event name. + * @returns {Promise<{ fragment: null | EventFragment; tag: string; topics: TopicFilter }>} The subscription information. + * @throws {Error} If the event name is unknown. + */ async function getSubInfo( contract: BaseContract, event: ContractEventName, @@ -617,11 +791,25 @@ async function getSubInfo( return { fragment, tag, topics }; } +/** + * Check if a contract has a subscription for an event. + * @param {BaseContract} contract - The contract instance. + * @param {ContractEventName} event - The event name. + * @returns {Promise} The subscription if available, otherwise null. + */ async function hasSub(contract: BaseContract, event: ContractEventName): Promise { const { subs } = getInternal(contract); return subs.get((await getSubInfo(contract, event)).tag) || null; } +/** + * Get a subscription for an event. + * @param {BaseContract} contract - The contract instance. + * @param {string} operation - The operation name. + * @param {ContractEventName} event - The event name. + * @returns {Promise} The subscription. + * @throws {Error} If the contract runner does not support subscribing. + */ async function getSub(contract: BaseContract, operation: string, event: ContractEventName): Promise { // Make sure our runner can actually subscribe to events const provider = getProvider(contract.runner); @@ -683,14 +871,25 @@ async function getSub(contract: BaseContract, operation: string, event: Contract } return sub; } - -// We use this to ensure one emit resolves before firing the next to -// ensure correct ordering (note this cannot throw and just adds the -// notice to the event queu using setTimeout). +/** + * We use this to ensure one emit resolves before firing the next to + * ensure correct ordering (note this cannot throw and just adds the + * notice to the event queue using setTimeout). + */ let lastEmit: Promise = Promise.resolve(); type PayloadFunc = (listener: null | Listener) => ContractUnknownEventPayload; +/** + * Emit an event with the given arguments and payload function. + * + * @param {BaseContract} contract - The contract instance. + * @param {ContractEventName} event - The event name. + * @param {Array} args - The arguments to pass to the listeners. + * @param {null | PayloadFunc} payloadFunc - The payload function. + * @returns {Promise} Resolves to true if any listeners were called. + * @ignore + */ async function _emit( contract: BaseContract, event: ContractEventName, @@ -725,6 +924,15 @@ async function _emit( return count > 0; } +/** + * Emit an event with the given arguments and payload function. + * + * @param {BaseContract} contract - The contract instance. + * @param {ContractEventName} event - The event name. + * @param {Array} args - The arguments to pass to the listeners. + * @param {null | PayloadFunc} payloadFunc - The payload function. + * @returns {Promise} Resolves to true if any listeners were called. + */ async function emit( contract: BaseContract, event: ContractEventName, @@ -742,7 +950,6 @@ async function emit( } const passProperties = ['then']; - /** * Creates a new contract connected to target with the abi and optionally connected to a runner to perform operations on * behalf of. @@ -754,7 +961,7 @@ export class BaseContract implements Addressable, EventEmitterable} The resolved address. */ async getAddress(): Promise { return await getInternal(this).addrPromise; @@ -929,6 +1146,9 @@ export class BaseContract implements Addressable, EventEmitterable} The deployed bytecode or null. + * @throws {Error} If the runner does not support .provider. */ async getDeployedCode(): Promise { const provider = getProvider(this.runner); @@ -945,9 +1165,12 @@ export class BaseContract implements Addressable, EventEmitterable} The contract instance. + * @throws {Error} If the contract runner does not support .provider. */ async waitForDeployment(): Promise { - // We have the deployement transaction; just use that (throws if deployement fails) + // We have the deployment transaction; just use that (throws if deployment fails) const deployTx = this.deploymentTransaction(); if (deployTx) { await deployTx.wait(); @@ -1308,3 +1531,5 @@ function _ContractBase(): new ( * @category Contract */ export class Contract extends _ContractBase() {} + + diff --git a/src/contract/factory.ts b/src/contract/factory.ts index 2872a2df..a096e0ac 100644 --- a/src/contract/factory.ts +++ b/src/contract/factory.ts @@ -7,12 +7,12 @@ import type { InterfaceAbi } from '../abi/index.js'; import { validateAddress } from '../address/index.js'; import type { Addressable } from '../address/index.js'; import type { BytesLike } from '../utils/index.js'; -import { getZoneForAddress, isQiAddress } from '../utils/index.js'; +import { getZoneForAddress, } from '../utils/index.js'; import type { ContractInterface, ContractMethodArgs, ContractDeployTransaction, ContractRunner } from './types.js'; import type { ContractTransactionResponse } from './wrappers.js'; import { Wallet } from '../wallet/index.js'; import { randomBytes } from '../crypto/index.js'; -import { getContractAddress } from '../address/address.js'; +import { getContractAddress, isQiAddress } from '../address/index.js'; import { getStatic } from '../utils/properties.js'; import { QuaiTransactionRequest } from '../providers/provider.js'; diff --git a/src/contract/types.ts b/src/contract/types.ts index d8b84ce0..e7195903 100644 --- a/src/contract/types.ts +++ b/src/contract/types.ts @@ -19,7 +19,7 @@ import type { * EventPayload as a single parameter, which includes a `.signature` property that can be used to further filter the * event. * - * {@link TopicFilter | **TopicFilter} - A filter defined using the standard Ethereum API which provides the specific + * {@link TopicFilter | **TopicFilter**} - A filter defined using the standard Ethereum API which provides the specific * topic hash or topic hashes to watch for along with any additional values to filter by. This will only pass a single * parameter to the listener, the EventPayload which will include additional details to refine by, such as the event * name and signature. @@ -47,7 +47,15 @@ export interface ContractInterface { * @category Contract */ export interface DeferredTopicFilter { + /** + * Get the topic filter. + * @returns {Promise} A promise resolving to the topic filter. + */ getTopicFilter(): Promise; + + /** + * The fragment of the event. + */ fragment: EventFragment; } @@ -118,6 +126,11 @@ export interface BaseContractMethod< R = any, D extends R | ContractTransactionResponse = R | ContractTransactionResponse, > { + /** + * Call the contract method with arguments. + * @param {...ContractMethodArgs} args - The arguments to call the method with. + * @returns {Promise} A promise resolving to the result of the call. + */ (...args: ContractMethodArgs): Promise; /** @@ -134,7 +147,6 @@ export interface BaseContractMethod< * Returns the fragment constrained by `args`. This can be used to resolve ambiguous method names. * * @param {ContractMethodArgs} args - The arguments to constrain the fragment by. - * * @returns {FunctionFragment} The constrained fragment. */ getFragment(...args: ContractMethodArgs): FunctionFragment; @@ -143,7 +155,6 @@ export interface BaseContractMethod< * Returns a populated transaction that can be used to perform the contract method with `args`. * * @param {ContractMethodArgs} args - The arguments to populate the transaction with. - * * @returns {Promise} A promise resolving to the populated transaction. */ populateTransaction(...args: ContractMethodArgs): Promise; @@ -155,7 +166,6 @@ export interface BaseContractMethod< * will be returned. * * @param {ContractMethodArgs} args - The arguments to call the method with. - * * @returns {Promise} A promise resolving to the result of the static call. */ staticCall(...args: ContractMethodArgs): Promise; @@ -164,7 +174,6 @@ export interface BaseContractMethod< * Send a transaction for the contract method with `args`. * * @param {ContractMethodArgs} args - The arguments to call the method with. - * * @returns {Promise} A promise resolving to the transaction response. */ send(...args: ContractMethodArgs): Promise; @@ -173,7 +182,6 @@ export interface BaseContractMethod< * Estimate the gas to send the contract method with `args`. * * @param {ContractMethodArgs} args - The arguments to call the method with. - * * @returns {Promise} A promise resolving to the estimated gas. */ estimateGas(...args: ContractMethodArgs): Promise; @@ -182,7 +190,6 @@ export interface BaseContractMethod< * Call the contract method with `args` and return the Result without any dereferencing. * * @param {ContractMethodArgs} args - The arguments to call the method with. - * * @returns {Promise} A promise resolving to the Result of the static call. */ staticCallResult(...args: ContractMethodArgs): Promise; @@ -200,7 +207,7 @@ export interface ContractMethod< > extends BaseContractMethod {} /** - * A pure of view method on a Contract. + * A pure or view method on a Contract. * * @category Contract */ @@ -219,6 +226,11 @@ export type ContractEventArgs> = { [I in keyof A]?: A[I] | * @category Contract */ export interface ContractEvent = Array> { + /** + * Create a deferred topic filter for the event. + * @param {...ContractEventArgs} args - The arguments to create the filter with. + * @returns {DeferredTopicFilter} The deferred topic filter. + */ (...args: ContractEventArgs): DeferredTopicFilter; /** @@ -235,7 +247,6 @@ export interface ContractEvent = Array> { * Returns the fragment constrained by `args`. This can be used to resolve ambiguous event names. * * @param {ContractEventArgs} args - The arguments to constrain the fragment by. - * * @returns {EventFragment} The constrained fragment. */ getFragment(...args: ContractEventArgs): EventFragment; @@ -247,6 +258,11 @@ export interface ContractEvent = Array> { * @category Contract */ export interface WrappedFallback { + /** + * Call the fallback method. + * @param {Omit} [overrides] - The transaction overrides. + * @returns {Promise} A promise resolving to the transaction response. + */ (overrides?: Omit): Promise; /** @@ -254,8 +270,7 @@ export interface WrappedFallback { * * For non-receive fallback, `data` may be overridden. * - * @param {Omit} overrides - The transaction overrides. - * + * @param {Omit} [overrides] - The transaction overrides. * @returns {Promise} A promise resolving to the populated transaction. */ populateTransaction(overrides?: Omit): Promise; @@ -265,9 +280,8 @@ export interface WrappedFallback { * * For non-receive fallback, `data` may be overridden. * - * @param {Omit} overrides - The transaction overrides. - * - * @returns {Promise} A promise resolving to the transaction response. + * @param {Omit} [overrides] - The transaction overrides. + * @returns {Promise} A promise resolving to the result of the call. */ staticCall(overrides?: Omit): Promise; @@ -276,8 +290,7 @@ export interface WrappedFallback { * * For non-receive fallback, `data` may be overridden. * - * @param {Omit} overrides - The transaction overrides. - * + * @param {Omit} [overrides] - The transaction overrides. * @returns {Promise} A promise resolving to the transaction response. */ send(overrides?: Omit): Promise; @@ -287,8 +300,7 @@ export interface WrappedFallback { * * For non-receive fallback, `data` may be overridden. * - * @param {Omit} overrides - The transaction overrides. - * + * @param {Omit} [overrides] - The transaction overrides. * @returns {Promise} A promise resolving to the estimated gas. */ estimateGas(overrides?: Omit): Promise; @@ -318,7 +330,6 @@ export interface ContractRunner { * Required to estimate gas. * * @param {TransactionRequest} tx - The transaction object. - * * @returns {Promise} A promise resolving to the estimated gas. */ estimateGas?: (tx: TransactionRequest) => Promise; @@ -327,7 +338,6 @@ export interface ContractRunner { * Required for pure, view or static calls to contracts. * * @param {QuaiTransactionRequest} tx - The transaction object. - * * @returns {Promise} A promise resolving to the result of the call. */ call?: (tx: QuaiTransactionRequest) => Promise; @@ -336,7 +346,6 @@ export interface ContractRunner { * Required for state mutating calls * * @param {TransactionRequest} tx - The transaction object. - * * @returns {Promise} A promise resolving to the transaction response. */ sendTransaction?: (tx: TransactionRequest) => Promise; diff --git a/src/abi/bytes32.ts b/src/encoding/bytes32.ts similarity index 78% rename from src/abi/bytes32.ts rename to src/encoding/bytes32.ts index 275b3783..084fc8e5 100644 --- a/src/abi/bytes32.ts +++ b/src/encoding/bytes32.ts @@ -5,21 +5,20 @@ */ import { getBytes, zeroPadBytes } from '../utils/index.js'; -import { toUtf8Bytes, toUtf8String, } from '../encoding/index.js'; - +import { toUtf8Bytes, toUtf8String } from './index.js'; import type { BytesLike } from '../utils/index.js'; /** - * Encodes a string as a Bytes32 string. + * Encodes a string as a Bytes32 string. This is used to encode ABI data. * - * @category Application Binary Interface + * @category Encoding * @param {string} text - The string to encode. * * @returns {string} The Bytes32-encoded string. * @throws {Error} If the string is too long to fit in a Bytes32 format. */ -export function encodeBytes32String(text: string): string { +export function encodeBytes32(text: string): string { // Get the bytes const bytes = toUtf8Bytes(text); @@ -33,15 +32,15 @@ export function encodeBytes32String(text: string): string { } /** - * Decodes a Bytes32-encoded string into a regular string. + * Decodes a Bytes32-encoded string into a regular string. This is used to decode ABI-encoded data. * - * @category Application Binary Interface + * @category Encoding * @param {BytesLike} _bytes - The Bytes32-encoded data. * * @returns {string} The decoded string. * @throws {Error} If the input is not exactly 32 bytes long or lacks a null terminator. */ -export function decodeBytes32String(_bytes: BytesLike): string { +export function decodeBytes32(_bytes: BytesLike): string { const data = getBytes(_bytes, 'bytes'); // Must be 32 bytes with a null-termination diff --git a/src/encoding/index.ts b/src/encoding/index.ts index d422cffb..c354d8b0 100644 --- a/src/encoding/index.ts +++ b/src/encoding/index.ts @@ -1,3 +1,4 @@ +export { decodeBytes32, encodeBytes32 } from './bytes32.js'; export { decodeBase58, encodeBase58 } from './base58.js'; export { decodeBase64, encodeBase64 } from './base64.js'; export { encodeProtoTransaction, encodeProtoWorkObject } from './proto-encode.js'; diff --git a/src/providers/abstract-provider.ts b/src/providers/abstract-provider.ts index f0a4299e..61d4bc80 100644 --- a/src/providers/abstract-provider.ts +++ b/src/providers/abstract-provider.ts @@ -4,13 +4,14 @@ * purposes. */ -// @TODO -// Event coalescence -// When we register an event with an async value (e.g. address is a Signer), -// we need to add it immeidately for the Event API, but also -// need time to resolve the address. Upon resolving the address, we need to -// migrate the listener to the static event. We also need to maintain a map -// of Signer to address so we can sync respond to listenerCount. +/** + * @todo Event coalescence + * When we register an event with an async value (e.g. address is a Signer), + * we need to add it immediately for the Event API, but also + * need time to resolve the address. Upon resolving the address, we need to + * migrate the listener to the static event. We also need to maintain a map + * of Signer to address so we can sync respond to listenerCount. + */ import { computeAddress, resolveAddress } from '../address/index.js'; import { Shard, toShard, toZone, Zone } from '../constants/index.js'; @@ -80,13 +81,26 @@ import { QuaiTransactionResponseParams } from './formatting.js'; type Timer = ReturnType; -// Constants +/** + * Constants + */ const BN_2 = BigInt(2); +/** + * Check if a value is a Promise. + * @param {any} value - The value to check. + * @returns {boolean} True if the value is a Promise, false otherwise. + */ function isPromise(value: any): value is Promise { return value && typeof value.then === 'function'; } +/** + * Get a tag string based on a prefix and value. + * @param {string} prefix - The prefix for the tag. + * @param {any} value - The value to include in the tag. + * @returns {string} The generated tag. + */ function getTag(prefix: string, value: any): string { return ( prefix + @@ -121,7 +135,7 @@ function getTag(prefix: string, value: any): string { } /** - * The value passed to the {@link AbstractProvider._getSubscriber | **AbstractProvider._getSubscriber} method. + * The value passed to the {@link AbstractProvider._getSubscriber | **AbstractProvider._getSubscriber**} method. * * Only developers sub-classing {@link AbstractProvider | **AbstractProvider**} will care about this, if they are * modifying a low-level feature of how subscriptions operate. @@ -199,12 +213,13 @@ export interface Subscriber { */ export class UnmanagedSubscriber implements Subscriber { /** - * The name fof the event. + * The name of the event. */ name!: string; /** * Create a new UnmanagedSubscriber with `name`. + * @param {string} name - The name of the event. */ constructor(name: string) { defineProperties(this, { name }); @@ -213,7 +228,9 @@ export class UnmanagedSubscriber implements Subscriber { start(): void {} stop(): void {} - // TODO: `dropWhilePaused` is not used, remove or re-write + /** + * @todo `dropWhilePaused` is not used, remove or re-write + */ // eslint-disable-next-line @typescript-eslint/no-unused-vars pause(dropWhilePaused?: boolean): void {} resume(): void {} @@ -224,23 +241,35 @@ type Sub = { nameMap: Map; addressableMap: WeakMap; listeners: Array<{ listener: Listener; once: boolean }>; - // @TODO: get rid of this, as it is (and has to be) + // @todo get rid of this, as it is (and has to be) // tracked in subscriber started: boolean; subscriber: Subscriber; }; +/** + * Create a deep copy of a value. + * @param {T} value - The value to copy. + * @returns {T} The copied value. + */ function copy(value: T): T { return JSON.parse(JSON.stringify(value)); } +/** + * Remove duplicates and sort an array of strings. + * @param {Array} items - The array of strings. + * @returns {Array} The concisified array. + */ function concisify(items: Array): Array { items = Array.from(new Set(items).values()); items.sort(); return items; } -// TODO: `provider` is not used, remove or re-write +/** + * @todo `provider` is not used, remove or re-write + */ // eslint-disable-next-line @typescript-eslint/no-unused-vars async function getSubscription(_event: ProviderEvent, provider: AbstractProvider): Promise { if (_event == null) { @@ -273,7 +302,7 @@ async function getSubscription(_event: ProviderEvent, provider: AbstractProvider if ((_event).orphan) { const event = _event; - // @TODO: Should lowercase and whatnot things here instead of copy... + // @todo Should lowercase and whatnot things here instead of copy... return { type: 'orphan', tag: getTag('orphan', event), filter: copy(event) }; } @@ -325,29 +354,12 @@ async function getSubscription(_event: ProviderEvent, provider: AbstractProvider assertArgument(false, 'unknown ProviderEvent', 'event', _event); } -function getTime(): number { - return new Date().getTime(); -} - /** - * An **AbstractPlugin** is used to provide additional internal services to an - * {@link AbstractProvider | **AbstractProvider**} without adding backwards-incompatible changes to method signatures or - * other internal and complex logic. - * - * @category Providers + * Get the current time in milliseconds. + * @returns {number} The current time in milliseconds. */ -export interface AbstractProviderPlugin { - /** - * The reverse domain notation of the plugin. - */ - readonly name: string; - - /** - * Creates a new instance of the plugin, connected to `provider`. - * - * @param {AbstractProvider} provider - The provider to connect to. - */ - connect(provider: AbstractProvider): AbstractProviderPlugin; +function getTime(): number { + return new Date().getTime(); } /** @@ -582,7 +594,6 @@ export class AbstractProvider implements Provider { _urlMap: Map; #connect: FetchRequest[]; #subs: Map; - #plugins: Map; // null=unpaused, true=paused+dropWhilePaused, false=paused #pausedState: null | boolean; @@ -632,7 +643,6 @@ export class AbstractProvider implements Provider { this.#performCache = new Map(); this.#subs = new Map(); - this.#plugins = new Map(); this.#pausedState = null; this.#destroyed = false; @@ -644,6 +654,11 @@ export class AbstractProvider implements Provider { this._urlMap = new Map(); } + /** + * Initialize the URL map with the provided URLs. + * @param {U} urls - The URLs to initialize the map with. + * @returns {Promise} A promise that resolves when the map is initialized. + */ async initUrlMap(urls: U): Promise { if (urls instanceof FetchRequest) { urls.url = urls.url.split(':')[0] + ':' + urls.url.split(':')[1] + ':9001'; @@ -677,24 +692,51 @@ export class AbstractProvider implements Provider { } } + /** + * Get the list of connected FetchRequests. + * @returns {FetchRequest[]} The list of connected FetchRequests. + */ get connect(): FetchRequest[] { return this.#connect; } + /** + * Get the zone from an address. + * @param {AddressLike} _address - The address to get the zone from. + * @returns {Promise} A promise that resolves to the zone. + */ async zoneFromAddress(_address: AddressLike): Promise { const address: string | Promise = this._getAddress(_address); return toZone((await address).slice(0, 4)); } + /** + * Get the shard from a hash. + * @param {string} hash - The hash to get the shard from. + * @returns {Shard} The shard. + */ shardFromHash(hash: string): Shard { return toShard(hash.slice(0, 4)); } + /** + * Get the latest Quai rate for a zone. + * @param {Zone} zone - The zone to get the rate for. + * @param {number} [amt=1] - The amount to get the rate for. + * @returns {Promise} A promise that resolves to the latest Quai rate. + */ async getLatestQuaiRate(zone: Zone, amt: number = 1): Promise { const blockNumber = await this.getBlockNumber(toShard(zone)); return this.getQuaiRateAtBlock(zone, blockNumber, amt); } + /** + * Get the Quai rate at a specific block. + * @param {Zone} zone - The zone to get the rate for. + * @param {BlockTag} blockTag - The block tag to get the rate at. + * @param {number} [amt=1] - The amount to get the rate for. + * @returns {Promise} A promise that resolves to the Quai rate at the specified block. + */ async getQuaiRateAtBlock(zone: Zone, blockTag: BlockTag, amt: number = 1): Promise { let resolvedBlockTag = this._getBlockTag(toShard(zone), blockTag); if (typeof resolvedBlockTag !== 'string') { @@ -709,17 +751,34 @@ export class AbstractProvider implements Provider { }); } + /** + * Get the protocol expansion number. + * @returns {Promise} A promise that resolves to the protocol expansion number. + */ async getProtocolExpansionNumber(): Promise { return await this.#perform({ method: 'getProtocolExpansionNumber', }); } + /** + * Get the latest Qi rate for a zone. + * @param {Zone} zone - The zone to get the rate for. + * @param {number} [amt=1] - The amount to get the rate for. + * @returns {Promise} A promise that resolves to the latest Qi rate. + */ async getLatestQiRate(zone: Zone, amt: number = 1): Promise { const blockNumber = await this.getBlockNumber(toShard(zone)); return this.getQiRateAtBlock(zone, blockNumber, amt); } + /** + * Get the Qi rate at a specific block. + * @param {Zone} zone - The zone to get the rate for. + * @param {BlockTag} blockTag - The block tag to get the rate at. + * @param {number} [amt=1] - The amount to get the rate for. + * @returns {Promise} A promise that resolves to the Qi rate at the specified block. + */ async getQiRateAtBlock(zone: Zone, blockTag: BlockTag, amt: number = 1): Promise { let resolvedBlockTag = this._getBlockTag(toShard(zone), blockTag); if (typeof resolvedBlockTag !== 'string') { @@ -734,6 +793,10 @@ export class AbstractProvider implements Provider { }); } + /** + * Get the polling interval. + * @returns {number} The polling interval. + */ get pollingInterval(): number { return this.#options.pollingInterval; } @@ -741,45 +804,17 @@ export class AbstractProvider implements Provider { /** * Returns `this`, to allow an **AbstractProvider** to implement the [Contract Runner](../classes/ContractRunner) * interface. + * @returns {this} The provider instance. */ get provider(): this { return this; } /** - * Returns all the registered plug-ins. - * - * @returns {AbstractProviderPlugin[]} An array of all the registered plug-ins. - */ - get plugins(): Array { - return Array.from(this.#plugins.values()); - } - - /** - * Attach a new plug-in. - * - * @param {AbstractProviderPlugin} plugin - The plug-in to attach. - */ - attachPlugin(plugin: AbstractProviderPlugin): this { - if (this.#plugins.get(plugin.name)) { - throw new Error(`cannot replace existing plugin: ${plugin.name} `); - } - this.#plugins.set(plugin.name, plugin.connect(this)); - return this; - } - - /** - * Get a plugin by name. - * - * @param {string} name - The name of the plugin to get. - * - * @returns {AbstractProviderPlugin | null} The plugin, or `null` if not found. + * Shares multiple identical requests made during the same 250ms. + * @param {PerformActionRequest} req - The request to perform. + * @returns {Promise} A promise that resolves to the result of the operation. */ - getPlugin(name: string): null | T { - return this.#plugins.get(name) || null; - } - - // Shares multiple identical requests made during the same 250ms async #perform(req: PerformActionRequest): Promise { const timeout = this.#options.cacheTimeout; // Caching disabled @@ -815,7 +850,7 @@ export class AbstractProvider implements Provider { * * @returns {Block} The wrapped block. */ - // TODO: `newtork` is not used, remove or re-write + // @todo `network` is not used, remove or re-write // eslint-disable-next-line @typescript-eslint/no-unused-vars _wrapBlock(value: BlockParams, network: Network): Block { // Handle known node by -> remove null values from the number array @@ -834,7 +869,7 @@ export class AbstractProvider implements Provider { * * @returns {Log} The wrapped log. */ - // TODO: `newtork` is not used, remove or re-write + // @todo `network` is not used, remove or re-write // eslint-disable-next-line @typescript-eslint/no-unused-vars _wrapLog(value: LogParams, network: Network): Log { return new Log(formatLog(value), this); @@ -849,7 +884,7 @@ export class AbstractProvider implements Provider { * * @returns {TransactionReceipt} The wrapped transaction receipt. */ - // TODO: `newtork` is not used, remove or re-write + // @todo `network` is not used, remove or re-write // eslint-disable-next-line @typescript-eslint/no-unused-vars _wrapTransactionReceipt(value: TransactionReceiptParams, network: Network): TransactionReceipt { return new TransactionReceipt(formatTransactionReceipt(value), this); @@ -1374,7 +1409,6 @@ export class AbstractProvider implements Provider { } async #getBlock(shard: Shard, block: BlockTag | string, includeTransactions: boolean): Promise { - // @TODO: Add CustomBlockPlugin check if (isHexString(block, 32)) { return await this.#perform({ method: 'getBlock', @@ -1915,3 +1949,4 @@ export class AbstractProvider implements Provider { } } } + diff --git a/src/providers/index.ts b/src/providers/index.ts index 14358672..da015214 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -27,10 +27,8 @@ export { } from './provider-socket.js'; export type { - AbstractProviderOptions, Subscription, Subscriber, - AbstractProviderPlugin, PerformActionFilter, PerformActionTransaction, PerformActionRequest, diff --git a/src/providers/network.ts b/src/providers/network.ts index a66d1374..7c14c409 100644 --- a/src/providers/network.ts +++ b/src/providers/network.ts @@ -1,5 +1,6 @@ /** * A **Network** encapsulates the various properties required to interact with a specific chain. + * @category Providers */ import { getBigInt, assertArgument } from '../utils/index.js'; @@ -7,9 +8,9 @@ import { getBigInt, assertArgument } from '../utils/index.js'; import type { BigNumberish } from '../utils/index.js'; /** - * A Networkish can be used to allude to a Network, by specifing: + * A Networkish can be used to allude to a Network, by specifying: * - * - A [Network](../classes/Network) object + * - A {@link Network} object * - A well-known (or registered) network name * - A well-known (or registered) chain ID * - An object with sufficient details to describe a network @@ -39,6 +40,8 @@ export class Network { /** * Creates a new **Network** for `name` and `chainId`. + * @param {string} name - The network name. + * @param {BigNumberish} chainId - The network chain ID. */ constructor(name: string, chainId: BigNumberish) { this.#name = name; @@ -47,6 +50,7 @@ export class Network { /** * Returns a JSON-compatible representation of a Network. + * @returns {Object} The JSON representation of the network. */ toJSON(): any { return { name: this.name, chainId: String(this.chainId) }; @@ -55,21 +59,33 @@ export class Network { /** * The network common name. * - * This is the canonical name, as networks migh have multiple names. + * This is the canonical name, as networks might have multiple names. + * @returns {string} The network name. */ get name(): string { return this.#name; } + + /** + * Sets the network name. + * @param {string} value - The new network name. + */ set name(value: string) { this.#name = value; } /** * The network chain ID. + * @returns {bigint} The network chain ID. */ get chainId(): bigint { return this.#chainId; } + + /** + * Sets the network chain ID. + * @param {BigNumberish} value - The new network chain ID. + */ set chainId(value: BigNumberish) { this.#chainId = getBigInt(value, 'chainId'); } @@ -81,8 +97,8 @@ export class Network { * This method does not currently check for additional properties, such as plug-in compatibility. * * @param {Networkish} other - The network to compare. - * * @returns {boolean} True if the networks match. + * @ignore */ matches(other: Networkish): boolean { if (other == null) { @@ -124,6 +140,7 @@ export class Network { /** * Create a copy of this Network. + * @returns {Network} A new Network instance. */ clone(): Network { const clone = new Network(this.name, this.chainId); @@ -133,9 +150,9 @@ export class Network { /** * Returns a new Network for the `network` name or chainId. * - * @param {Networkish} network - The network to get. - * + * @param {Networkish} [network] - The network to get. * @returns {Network} The Network instance. + * @throws {Error} If the network is invalid. */ static from(network?: Networkish): Network { // Default network @@ -187,7 +204,7 @@ export class Network { * * @param {string | number | bigint} nameOrChainId - The name or chain ID to register. * @param {() => Network} networkFunc - The function to create the Network. - * @throws If a network is already registered for `nameOrChainId`. + * @throws {Error} If a network is already registered for `nameOrChainId`. */ static register(nameOrChainId: string | number | bigint, networkFunc: () => Network): void { if (typeof nameOrChainId === 'number') { diff --git a/src/providers/provider-browser.ts b/src/providers/provider-browser.ts index d44066a7..4cc7b730 100644 --- a/src/providers/provider-browser.ts +++ b/src/providers/provider-browser.ts @@ -9,11 +9,16 @@ import type { Networkish } from './network.js'; * The interface to an [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) provider, which is a standard used by most * injected providers, which the {@link BrowserProvider | **BrowserProvider**} accepts and exposes the API of. * + * @interface * @category Providers */ export interface Eip1193Provider { /** * See [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) for details on this method. + * @param {Object} request - The request object. + * @param {string} request.method - The method name. + * @param {Array | Record} [request.params] - The parameters for the method. + * @returns {Promise} The result of the request. */ request(request: { method: string; params?: Array | Record }): Promise; } @@ -22,6 +27,12 @@ export interface Eip1193Provider { * The possible additional events dispatched when using the `"debug"` event on a * {@link BrowserProvider | **BrowserProvider**}. * + * @property {string} action - The action type. + * @property {Object} payload - The payload of the action. + * @property {string} payload.method - The method name. + * @property {Array} payload.params - The parameters for the method. + * @property {any} result - The result of the action. + * @property {Error} error - The error object. * @category Providers */ export type DebugEventBrowserProvider = @@ -42,13 +53,18 @@ export type DebugEventBrowserProvider = * A **BrowserProvider** is intended to wrap an injected provider which adheres to the * [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) standard, which most (if not all) currently do. * + * @class + * @extends JsonRpcApiProvider * @category Providers */ export class BrowserProvider extends JsonRpcApiProvider { #request: (method: string, params: Array | Record) => Promise; /** - * Connnect to the `ethereum` provider, optionally forcing the `network`. + * Connect to the `ethereum` provider, optionally forcing the `network`. + * @constructor + * @param {Eip1193Provider} ethereum - The EIP-1193 provider. + * @param {Networkish} [network] - The network to connect to. */ constructor(ethereum: Eip1193Provider, network?: Networkish) { super(network, { batchMaxCount: 1 }); @@ -71,6 +87,11 @@ export class BrowserProvider extends JsonRpcApiProvider { }; } + /** + * Resolves to `true` if the provider manages the `address`. + * @param {number | string} address - The address to check. + * @returns {Promise} Resolves to `true` if the provider manages the `address`. + */ async hasSigner(address: number | string): Promise { if (address == null) { address = 0; @@ -85,12 +106,24 @@ export class BrowserProvider extends JsonRpcApiProvider { return accounts.filter((a: string) => a.toLowerCase() === address).length !== 0; } + /** + * Sends a JSON-RPC request. + * @param {string} method - The method name. + * @param {Array | Record} params - The parameters for the method. + * @returns {Promise} The result of the request. + */ async send(method: string, params: Array | Record): Promise { await this._start(); return await super.send(method, params); } + /** + * Sends a JSON-RPC payload. + * @param {JsonRpcPayload | Array} payload - The JSON-RPC payload. + * @returns {Promise>} The result of the request. + * @private + */ async _send(payload: JsonRpcPayload | Array): Promise> { assertArgument(!Array.isArray(payload), 'EIP-1193 does not support batch request', 'payload', payload); @@ -107,6 +140,12 @@ export class BrowserProvider extends JsonRpcApiProvider { } } + /** + * Gets the RPC error. + * @param {JsonRpcPayload} payload - The JSON-RPC payload. + * @param {JsonRpcError} error - The JSON-RPC error. + * @returns {Error} The RPC error. + */ getRpcError(payload: JsonRpcPayload, error: JsonRpcError): Error { error = JSON.parse(JSON.stringify(error)); @@ -124,6 +163,11 @@ export class BrowserProvider extends JsonRpcApiProvider { return super.getRpcError(payload, error); } + /** + * Gets the signer for the given address. + * @param {number | string} [address] - The address to get the signer for. + * @returns {Promise} The signer for the address. + */ async getSigner(address?: number | string): Promise { if (address == null) { address = 0; @@ -131,9 +175,7 @@ export class BrowserProvider extends JsonRpcApiProvider { if (!(await this.hasSigner(address))) { try { - //const resp = await this.#request('quai_requestAccounts', []); - //console.log("RESP", resp); } catch (error: any) { const payload = error.payload; throw this.getRpcError(payload, { id: payload.id, error }); @@ -142,12 +184,5 @@ export class BrowserProvider extends JsonRpcApiProvider { return await super.getSigner(address); } - - /** - * Resolves to `true` if the provider manages the `address`. - * - * @param {number | string} address - The address to check. - * - * @returns {Promise} Resolves to `true` if the provider manages the `address`. - */ } + diff --git a/src/providers/provider-jsonrpc.ts b/src/providers/provider-jsonrpc.ts index 66146248..2ac3c775 100644 --- a/src/providers/provider-jsonrpc.ts +++ b/src/providers/provider-jsonrpc.ts @@ -2,9 +2,9 @@ * One of the most common ways to interact with the blockchain is by a node running a JSON-RPC interface which can be * connected to, based on the transport, using: * - * - HTTP or HTTPS - [JsonRpcProvider](../classes/JsonRpcProvider) - * - WebSocket - [WebSocketProvider](../classes/WebSocketProvider) - * - IPC - [IpcSocketProvider](../classes/IpcSocketProvider) + * - HTTP or HTTPS - {@link JsonRpcProvider | **JsonRpcProvider**} + * - WebSocket - {@link WebSocketProvider | **WebSocketProvider**} + * - IPC - {@link IpcSocketProvider | **IpcSocketProvider**} */ // @TODO: @@ -47,6 +47,14 @@ import { addressFromTransactionRequest } from './provider.js'; type Timer = ReturnType; const Primitive = 'bigint,boolean,function,number,string,symbol'.split(/,/g); + +/** + * Deeply copies a value. + * + * @param {T} value - The value to copy. + * @returns {T} The copied value. + * @ignore + */ function deepCopy(value: T): T { if (value == null || Primitive.indexOf(typeof value) >= 0) { return value; @@ -74,6 +82,13 @@ function deepCopy(value: T): T { throw new Error(`should not happen: ${value} (${typeof value})`); } +/** + * Stalls execution for a specified duration. + * + * @param {number} duration - The duration to stall in milliseconds. + * @returns {Promise} A promise that resolves after the duration. + * @ignore + */ function stall(duration: number): Promise { return new Promise((resolve) => { setTimeout(resolve, duration); @@ -146,7 +161,7 @@ export type JsonRpcError = { }; /** - * When subscribing to the `"debug"` event, the [[Listener]] will receive this object as the first parameter. + * When subscribing to the `"debug"` event, the {@link Listener | **Listener**} will receive this object as the first parameter. * * @category Providers * @todo Listener is no longer exported, either remove the link or rework the comment @@ -166,7 +181,7 @@ export type DebugEventJsonRpcApiProvider = }; /** - * Options for configuring a {@link JsonRpcApiProvider | **JsonRpcApiProvider**}. Much of this is targetted towards + * Options for configuring a {@link JsonRpcApiProvider | **JsonRpcApiProvider**}. Much of this is targeted towards * sub-classes, which often will not expose any of these options to their consumers. * * **`polling`** - use the polling strategy is used immediately for events; otherwise, attempt to use filters and fall @@ -188,7 +203,7 @@ export type DebugEventJsonRpcApiProvider = * **`batchMaxCount`** - maximum number of requests to allow in a batch. If `batchMaxCount = 1`, then batching is * disabled. (default: `100`) * - * **`cacheTimeout`** - passed as [AbstractProviderOptions](../types-aliases/AbstractProviderOptions). + * **`cacheTimeout`** - passed as {@link AbstractProviderOptions | **AbstractProviderOptions**}. * * @category Providers */ @@ -293,16 +308,33 @@ export interface QuaiJsonRpcTransactionRequest extends AbstractJsonRpcTransactio // @TODO: Unchecked Signers +/** + * A signer that uses JSON-RPC to sign transactions and messages. + * + * @category Providers + */ export class JsonRpcSigner extends AbstractSigner { address!: string; + /** + * Creates a new JsonRpcSigner instance. + * + * @param {JsonRpcApiProvider} provider - The JSON-RPC provider. + * @param {string} address - The address of the signer. + */ constructor(provider: JsonRpcApiProvider, address: string) { super(provider); address = getAddress(address); defineProperties(this, { address }); } - // TODO: `provider` is passed in, but not used, remove? + /** + * Connects the signer to a provider. + * + * @param {null | Provider} provider - The provider to connect to. + * @returns {Signer} The connected signer. + * @throws {Error} If the signer cannot be reconnected. + */ // eslint-disable-next-line @typescript-eslint/no-unused-vars connect(provider: null | Provider): Signer { assert(false, 'cannot reconnect JsonRpcSigner', 'UNSUPPORTED_OPERATION', { @@ -310,17 +342,33 @@ export class JsonRpcSigner extends AbstractSigner { }); } + /** + * Gets the address of the signer. + * + * @returns {Promise} The address of the signer. + */ async getAddress(): Promise { return this.address; } - // JSON-RPC will automatially fill in nonce, etc. so we just check from + /** + * Populates a Quai transaction. + * + * @param {QuaiTransactionRequest} tx - The transaction request. + * @returns {Promise} The populated transaction. + * @ignore + */ async populateQuaiTransaction(tx: QuaiTransactionRequest): Promise { return (await this.populateCall(tx)) as QuaiTransactionLike; } - // Returns just the hash of the transaction after sent, which is what - // the bare JSON-RPC API does; + /** + * Sends an unchecked transaction. + * + * @param {TransactionRequest} _tx - The transaction request. + * @returns {Promise} The transaction hash. + * @ignore + */ async sendUncheckedTransaction(_tx: TransactionRequest): Promise { const tx = deepCopy(_tx); @@ -377,6 +425,13 @@ export class JsonRpcSigner extends AbstractSigner { return this.provider.send('quai_sendTransaction', [hexTx]); } + /** + * Sends a transaction. + * + * @param {TransactionRequest} tx - The transaction request. + * @returns {Promise} The transaction response. + * @throws {Error} If the transaction cannot be sent. + */ async sendTransaction(tx: TransactionRequest): Promise { const zone = await this.zoneFromAddress(addressFromTransactionRequest(tx)); // This cannot be mined any earlier than any recent block @@ -451,6 +506,13 @@ export class JsonRpcSigner extends AbstractSigner { }); } + /** + * Signs a transaction. + * + * @param {TransactionRequest} _tx - The transaction request. + * @returns {Promise} The signed transaction. + * @throws {Error} If the transaction cannot be signed. + */ async signTransaction(_tx: TransactionRequest): Promise { const tx = deepCopy(_tx); @@ -475,11 +537,25 @@ export class JsonRpcSigner extends AbstractSigner { return await this.provider.send('quai_signTransaction', [hexTx]); } + /** + * Signs a message. + * + * @param {string | Uint8Array} _message - The message to sign. + * @returns {Promise} The signed message. + */ async signMessage(_message: string | Uint8Array): Promise { const message = typeof _message === 'string' ? toUtf8Bytes(_message) : _message; return await this.provider.send('personal_sign', [hexlify(message), this.address.toLowerCase()]); } + /** + * Signs typed data. + * + * @param {TypedDataDomain} domain - The domain of the typed data. + * @param {Record>} types - The types of the typed data. + * @param {Record} _value - The value of the typed data. + * @returns {Promise} The signed typed data. + */ async signTypedData( domain: TypedDataDomain, types: Record>, @@ -493,11 +569,23 @@ export class JsonRpcSigner extends AbstractSigner { ]); } + /** + * Unlocks the account. + * + * @param {string} password - The password to unlock the account. + * @returns {Promise} True if the account is unlocked, false otherwise. + */ async unlock(password: string): Promise { return this.provider.send('personal_unlockAccount', [this.address.toLowerCase(), password, null]); } - // https://github.com/ethereum/wiki/wiki/JSON-RPC#quai_sign + /** + * Signs a message using the legacy method. + * + * @param {string | Uint8Array} _message - The message to sign. + * @returns {Promise} The signed message. + * @ignore + */ async _legacySignMessage(_message: string | Uint8Array): Promise { const message = typeof _message === 'string' ? toUtf8Bytes(_message) : _message; return await this.provider.send('quai_sign', [this.address.toLowerCase(), hexlify(message)]); @@ -541,6 +629,11 @@ export abstract class JsonRpcApiProvider extends AbstractProvi initPromise?: Promise; + /** + * Schedules the draining of the payload queue. + * + * @ignore + */ #scheduleDrain(): void { if (this.#drainTimer) { return; @@ -646,6 +739,12 @@ export abstract class JsonRpcApiProvider extends AbstractProvi }, stallTime); } + /** + * Creates a new JsonRpcApiProvider instance. + * + * @param {Networkish} [network] - The network to connect to. + * @param {JsonRpcApiProviderOptions} [options] - The options for the provider. + */ constructor(network?: Networkish, options?: JsonRpcApiProviderOptions) { super(network, options); @@ -693,6 +792,9 @@ export abstract class JsonRpcApiProvider extends AbstractProvi * Returns the value associated with the option `key`. * * Sub-classes can use this to inquire about configuration options. + * + * @param {keyof JsonRpcApiProviderOptions} key - The option key. + * @returns {JsonRpcApiProviderOptions[key]} The option value. */ _getOption(key: K): JsonRpcApiProviderOptions[K] { return this.#options[key]; @@ -701,6 +803,9 @@ export abstract class JsonRpcApiProvider extends AbstractProvi /** * Gets the {@link Network | **Network**} this provider has committed to. On each call, the network is detected, and * if it has changed, the call will reject. + * + * @returns {Network} The network. + * @throws {Error} If the network is not available yet. */ get _network(): Network { assert(this.#network, 'network is not available yet', 'NETWORK_ERROR'); @@ -711,6 +816,11 @@ export abstract class JsonRpcApiProvider extends AbstractProvi * Sends a JSON-RPC `payload` (or a batch) to the underlying channel. * * Sub-classes **MUST** override this. + * + * @param {JsonRpcPayload | Array} payload - The JSON-RPC payload. + * @param {Shard} [shard] - The shard to send the request to. + * @returns {Promise>} The JSON-RPC result. + * @throws {Error} If the request fails. */ abstract _send( payload: JsonRpcPayload | Array, @@ -722,6 +832,10 @@ export abstract class JsonRpcApiProvider extends AbstractProvi * * Sub-classes may override this to modify behavior of actions, and should generally call `super._perform` as a * fallback. + * + * @param {PerformActionRequest} req - The request to perform. + * @returns {Promise} The result of the request. + * @throws {Error} If the request fails. */ async _perform(req: PerformActionRequest): Promise { // Legacy networks do not like the type field being passed along (which @@ -760,6 +874,9 @@ export abstract class JsonRpcApiProvider extends AbstractProvi * * Keep in mind that {@link JsonRpcApiProvider.send | **send**} may only be used once * {@link JsonRpcApiProvider.ready | **ready**}, otherwise the _send primitive must be used instead. + * + * @returns {Promise} The detected network. + * @throws {Error} If network detection fails. */ async _detectNetwork(): Promise { const network = this._getOption('staticNetwork'); @@ -831,6 +948,8 @@ export abstract class JsonRpcApiProvider extends AbstractProvi * it is overridden, then `super._start()` **MUST** be called. * * Calling it multiple times is safe and has no effect. + * + * @ignore */ _start(): void { if (this.#notReady == null || this.#notReady.resolve == null) { @@ -871,6 +990,9 @@ export abstract class JsonRpcApiProvider extends AbstractProvi /** * Resolves once the {@link JsonRpcApiProvider._start | **_start**} has been called. This can be used in sub-classes * to defer sending data until the connection has been established. + * + * @returns {Promise} A promise that resolves once the provider is ready. + * @ignore */ async _waitUntilReady(): Promise { if (this.#notReady == null) { @@ -885,8 +1007,8 @@ export abstract class JsonRpcApiProvider extends AbstractProvi * Sub-classes may override this to modify the behavior of subscription management. * * @param {Subscription} sub - The subscription to manage. - * * @returns {Subscriber} The subscriber that will manage the subscription. + * @ignore */ _getSubscriber(sub: Subscription): Subscriber { // Pending Filters aren't availble via polling @@ -909,6 +1031,8 @@ export abstract class JsonRpcApiProvider extends AbstractProvi /** * Returns true only if the {@link JsonRpcApiProvider._start | **_start**} has been called. + * + * @returns {boolean} True if the provider is ready. */ get ready(): boolean { return this.#notReady == null; @@ -919,8 +1043,9 @@ export abstract class JsonRpcApiProvider extends AbstractProvi * converted to Quantity values. * * @param {TransactionRequest} tx - The transaction to normalize. - * * @returns {JsonRpcTransactionRequest} The normalized transaction. + * @throws {Error} If the transaction is invalid. + * @ignore */ getRpcTransaction(tx: TransactionRequest): JsonRpcTransactionRequest { const result: JsonRpcTransactionRequest = {}; @@ -969,10 +1094,9 @@ export abstract class JsonRpcApiProvider extends AbstractProvi * Returns the request method and arguments required to perform `req`. * * @param {PerformActionRequest} req - The request to perform. - * * @returns {null | { method: string; args: any[] }} The method and arguments to use. - * @throws {Error} If the request is not supported. - * @throws {Error} If the request is invalid. + * @throws {Error} If the request is not supported or invalid. + * @ignore */ getRpcRequest(req: PerformActionRequest): null | { method: string; args: Array } { switch (req.method) { @@ -1116,8 +1240,8 @@ export abstract class JsonRpcApiProvider extends AbstractProvi * * @param {JsonRpcPayload} payload - The payload that was sent. * @param {JsonRpcError} _error - The error that was received. - * * @returns {Error} The coalesced error. + * @ignore */ getRpcError(payload: JsonRpcPayload, _error: JsonRpcError): Error { const { method } = payload; @@ -1230,7 +1354,6 @@ export abstract class JsonRpcApiProvider extends AbstractProvi * @param {string} method - The method to call. * @param {any[] | Record} params - The parameters to pass to the method. * @param {Shard} shard - The shard to send the request to. - * * @returns {Promise} A promise that resolves to the result of the method call. */ send(method: string, params: Array | Record, shard?: Shard): Promise { @@ -1258,6 +1381,13 @@ export abstract class JsonRpcApiProvider extends AbstractProvi return >promise; } + /** + * Returns a JsonRpcSigner for the given address. + * + * @param {number | string} [address] - The address or index of the account. + * @returns {Promise} A promise that resolves to the JsonRpcSigner. + * @throws {Error} If the account is invalid. + */ async getSigner(address?: number | string): Promise { if (address == null) { address = 0; @@ -1290,11 +1420,19 @@ export abstract class JsonRpcApiProvider extends AbstractProvi throw new Error('invalid account'); } + /** + * Returns a list of JsonRpcSigners for all accounts. + * + * @returns {Promise>} A promise that resolves to an array of JsonRpcSigners. + */ async listAccounts(): Promise> { const accounts: Array = await this.send('quai_accounts', []); return accounts.map((a) => new JsonRpcSigner(this, a)); } + /** + * Destroys the provider, stopping all processing and canceling all pending requests. + */ destroy(): void { // Stop processing requests if (this.#drainTimer) { @@ -1445,4 +1583,4 @@ function spelunkMessage(value: any): Array { const result: Array = []; _spelunkMessage(value, result); return result; -} +} \ No newline at end of file diff --git a/src/providers/provider-socket.ts b/src/providers/provider-socket.ts index 616418c2..daa85607 100644 --- a/src/providers/provider-socket.ts +++ b/src/providers/provider-socket.ts @@ -19,6 +19,12 @@ import type { Networkish } from './network.js'; import type { WebSocketLike } from './provider-websocket.js'; import { Shard } from '../constants/index.js'; +/** + * @property {string} method - The method name. + * @property {Object} params - The parameters. + * @property {any} params.result - The result. + * @property {string} params.subscription - The subscription ID. + */ type JsonRpcSubscription = { method: string; params: { @@ -40,6 +46,7 @@ export class SocketSubscriber implements Subscriber { /** * The filter. + * @type {Array} */ get filter(): Array { return JSON.parse(this.#filter); @@ -52,6 +59,8 @@ export class SocketSubscriber implements Subscriber { /** * Creates a new **SocketSubscriber** attached to `provider` listening to `filter`. + * @param {SocketProvider} provider - The socket provider. + * @param {Array} filter - The filter. */ constructor(provider: SocketProvider, filter: Array) { this.#provider = provider; @@ -61,6 +70,9 @@ export class SocketSubscriber implements Subscriber { this.#emitPromise = null; } + /** + * Start the subscriber. + */ start(): void { this.#filterId = this.#provider.send('quai_subscribe', this.filter).then((filterId) => { this.#provider._register(filterId, this); @@ -68,6 +80,9 @@ export class SocketSubscriber implements Subscriber { }); } + /** + * Stop the subscriber. + */ stop(): void { (>this.#filterId).then((filterId) => { this.#provider.send('quai_unsubscribe', [filterId]); @@ -75,8 +90,10 @@ export class SocketSubscriber implements Subscriber { this.#filterId = null; } - // @TODO: pause should trap the current blockNumber, unsub, and on resume use getLogs - // and resume + /** + * Pause the subscriber. + * @param {boolean} [dropWhilePaused] - Whether to drop logs while paused. + */ pause(dropWhilePaused?: boolean): void { assert( dropWhilePaused, @@ -87,11 +104,16 @@ export class SocketSubscriber implements Subscriber { this.#paused = !!dropWhilePaused; } + /** + * Resume the subscriber. + */ resume(): void { this.#paused = null; } /** + * Handle incoming messages. + * @param {any} message - The message to handle. * @ignore */ _handleMessage(message: any): void { @@ -117,10 +139,13 @@ export class SocketSubscriber implements Subscriber { /** * Sub-classes **must** override this to emit the events on the provider. + * @param {SocketProvider} provider - The socket provider. + * @param {any} message - The message to emit. + * @returns {Promise} + * @abstract */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars async _emit(provider: SocketProvider, message: any): Promise { - throw new Error('sub-classes must implemente this; _emit'); + throw new Error('sub-classes must implement this; _emit'); } } @@ -131,30 +156,46 @@ export class SocketSubscriber implements Subscriber { */ export class SocketBlockSubscriber extends SocketSubscriber { /** + * Creates a new **SocketBlockSubscriber**. + * @param {SocketProvider} provider - The socket provider. * @ignore */ constructor(provider: SocketProvider) { super(provider, ['newHeads']); } + /** + * Emit the block event. + * @param {SocketProvider} provider - The socket provider. + * @param {any} message - The message to emit. + * @returns {Promise} + */ async _emit(provider: SocketProvider, message: any): Promise { provider.emit('block', parseInt(message.number)); } } /** - * A **SocketPendingSubscriber** listens for pending transacitons and emits `"pending"` events. + * A **SocketPendingSubscriber** listens for pending transactions and emits `"pending"` events. * * @category Providers */ export class SocketPendingSubscriber extends SocketSubscriber { /** + * Creates a new **SocketPendingSubscriber**. + * @param {SocketProvider} provider - The socket provider. * @ignore */ constructor(provider: SocketProvider) { super(provider, ['newPendingTransactions']); } + /** + * Emit the pending event. + * @param {SocketProvider} provider - The socket provider. + * @param {any} message - The message to emit. + * @returns {Promise} + */ async _emit(provider: SocketProvider, message: any): Promise { provider.emit('pending', message); } @@ -170,12 +211,16 @@ export class SocketEventSubscriber extends SocketSubscriber { /** * The filter. + * @type {EventFilter} */ get logFilter(): EventFilter { return JSON.parse(this.#logFilter); } /** + * Creates a new **SocketEventSubscriber**. + * @param {SocketProvider} provider - The socket provider. + * @param {EventFilter} filter - The event filter. * @ignore */ constructor(provider: SocketProvider, filter: EventFilter) { @@ -183,6 +228,12 @@ export class SocketEventSubscriber extends SocketSubscriber { this.#logFilter = JSON.stringify(filter); } + /** + * Emit the event log. + * @param {SocketProvider} provider - The socket provider. + * @param {any} message - The message to emit. + * @returns {Promise} + */ async _emit(provider: SocketProvider, message: any): Promise { provider.emit(this.logFilter, provider._wrapLog(message, provider._network)); } @@ -208,6 +259,8 @@ export class SocketProvider extends JsonRpcApiProvider { * Creates a new **SocketProvider** connected to `network`. * * If unspecified, the network will be discovered. + * @param {Networkish} [network] - The network to connect to. + * @param {JsonRpcApiProviderOptions} [_options] - The options for the provider. */ constructor(network?: Networkish, _options?: JsonRpcApiProviderOptions) { // Copy the options @@ -237,6 +290,11 @@ export class SocketProvider extends JsonRpcApiProvider { this.#pending = new Map(); } + /** + * Get the subscriber for a given subscription. + * @param {Subscription} sub - The subscription. + * @returns {Subscriber} The subscriber. + */ _getSubscriber(sub: Subscription): Subscriber { switch (sub.type) { case 'close': @@ -258,8 +316,10 @@ export class SocketProvider extends JsonRpcApiProvider { } /** - * Register a new subscriber. This is used internalled by Subscribers and generally is unecessary unless extending + * Register a new subscriber. This is used internally by Subscribers and generally is unnecessary unless extending * capabilities. + * @param {number | string} filterId - The filter ID. + * @param {SocketSubscriber} subscriber - The subscriber. */ _register(filterId: number | string, subscriber: SocketSubscriber): void { this.#subs.set(filterId, subscriber); @@ -272,6 +332,12 @@ export class SocketProvider extends JsonRpcApiProvider { } } + /** + * Send a JSON-RPC payload. + * @param {JsonRpcPayload | Array} payload - The payload to send. + * @param {Shard} [shard] - The shard. + * @returns {Promise>} The result or error. + */ async _send( payload: JsonRpcPayload | Array, shard?: Shard, @@ -297,6 +363,7 @@ export class SocketProvider extends JsonRpcApiProvider { /** * Sub-classes **must** call this with messages received over their transport to be processed and dispatched. + * @param {string} message - The message to process. */ async _processMessage(message: string): Promise { const result = JSON.parse(message); @@ -343,8 +410,11 @@ export class SocketProvider extends JsonRpcApiProvider { /** * Sub-classes **must** override this to send `message` over their transport. + * @param {string} message - The message to send. + * @param {Shard} [shard] - The shard. + * @returns {Promise} + * @abstract */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars async _write(message: string, shard?: Shard): Promise { throw new Error('sub-classes must override this'); } diff --git a/src/providers/provider-websocket.ts b/src/providers/provider-websocket.ts index a9773e9f..b59d85b3 100644 --- a/src/providers/provider-websocket.ts +++ b/src/providers/provider-websocket.ts @@ -37,16 +37,26 @@ export type WebSocketCreator = () => WebSocketLike; * WebSockets are often preferred because they retain a live connection to a server, which permits more instant access * to events. * - * However, this incurs higher server infrasturture costs, so additional resources may be required to host your own + * However, this incurs higher server infrastructure costs, so additional resources may be required to host your own * WebSocket nodes and many third-party services charge additional fees for WebSocket endpoints. * * @category Providers + * @extends SocketProvider */ export class WebSocketProvider extends SocketProvider { #websockets: WebSocketLike[]; + /** + * A map to track the readiness of each shard. + * @type {Map} + */ readyMap: Map = new Map(); + /** + * Get the array of WebSocketLike objects. + * @throws {Error} If the websocket is closed. + * @returns {WebSocketLike[]} The array of WebSocketLike objects. + */ get websocket(): WebSocketLike[] { if (this.#websockets == null) { throw new Error('websocket closed'); @@ -54,6 +64,12 @@ export class WebSocketProvider extends SocketProvider { return this.#websockets; } + /** + * Create a new WebSocketProvider. + * @param {string | string[] | WebSocketLike | WebSocketCreator} url - The URL(s) or WebSocket object or creator. + * @param {Networkish} [network] - The network to connect to. + * @param {JsonRpcApiProviderOptions} [options] - The options for the JSON-RPC API provider. + */ constructor( url: string | string[] | WebSocketLike | WebSocketCreator, network?: Networkish, @@ -64,6 +80,12 @@ export class WebSocketProvider extends SocketProvider { this.initPromise = this.initUrlMap(typeof url === 'string' ? [url] : url); } + /** + * Initialize a WebSocket connection for a shard. + * @ignore + * @param {WebSocketLike} websocket - The WebSocket object. + * @param {Shard} shard - The shard identifier. + */ initWebSocket(websocket: WebSocketLike, shard: Shard): void { websocket.onopen = async () => { try { @@ -81,6 +103,12 @@ export class WebSocketProvider extends SocketProvider { }; } + /** + * Wait until the shard is ready. + * @param {Shard} shard - The shard identifier. + * @throws {Error} If the shard is not ready within the timeout period. + * @returns {Promise} A promise that resolves when the shard is ready. + */ async waitShardReady(shard: Shard): Promise { let count = 0; while (!this.readyMap.get(shard)) { @@ -92,6 +120,12 @@ export class WebSocketProvider extends SocketProvider { } } + /** + * Initialize the URL map with WebSocket connections. + * @ignore + * @param {U} urls - The URLs or WebSocket object or creator. + * @returns {Promise} A promise that resolves when the URL map is initialized. + */ async initUrlMap(urls: U) { const createWebSocket = (baseUrl: string, port: number): WebSocketLike => { return new _WebSocket(`${baseUrl}:${port}`) as WebSocketLike; @@ -141,6 +175,14 @@ export class WebSocketProvider extends SocketProvider { } } + /** + * Write a message to the WebSocket. + * @ignore + * @param {string} message - The message to send. + * @param {Shard} [shard] - The shard identifier. + * @throws {Error} If the WebSocket is closed or the shard is not found. + * @returns {Promise} A promise that resolves when the message is sent. + */ async _write(message: string, shard?: Shard): Promise { if (this.websocket.length < 1) { throw new Error('Websocket closed'); @@ -158,6 +200,10 @@ export class WebSocketProvider extends SocketProvider { websocket.send(message); } + /** + * Destroy the WebSocket connections and clean up resources. + * @returns {Promise} A promise that resolves when the WebSocket connections are closed. + */ async destroy(): Promise { this.#websockets.forEach((it) => it.close()); this.#websockets = []; diff --git a/src/providers/provider.ts b/src/providers/provider.ts index 1eb55ee3..2ae60132 100644 --- a/src/providers/provider.ts +++ b/src/providers/provider.ts @@ -54,8 +54,13 @@ import { QiTransactionLike } from '../transaction/qi-transaction.js'; import { QuaiTransactionLike } from '../transaction/quai-transaction.js'; import { toShard, toZone } from '../constants/index.js'; -// ----------------------- - +/** + * Get the value if it is not null or undefined. + * + * @ignore + * @param {undefined | null | T} value - The value to check. + * @returns {null | T} The value if not null or undefined, otherwise null. + */ function getValue(value: undefined | null | T): null | T { if (value == null) { return null; @@ -63,6 +68,13 @@ function getValue(value: undefined | null | T): null | T { return value; } +/** + * Convert a value to a JSON-friendly string. + * + * @ignore + * @param {null | bigint | string} value - The value to convert. + * @returns {null | string} The JSON-friendly string or null. + */ function toJson(value: null | bigint | string): null | string { if (value == null) { return null; @@ -70,8 +82,6 @@ function toJson(value: null | bigint | string): null | string { return value.toString(); } -// @TODO? implements Required - /** * A **FeeData** wraps all the fee-related values associated with the network. * @@ -96,7 +106,7 @@ export class FeeData { readonly maxFeePerGas!: null | bigint; /** - * The additional amout to pay per gas to encourage a validator to include the transaction. + * The additional amount to pay per gas to encourage a validator to include the transaction. * * The purpose of this is to compensate the validator for the adjusted risk for including a given transaction. * @@ -106,6 +116,10 @@ export class FeeData { /** * Creates a new FeeData for `gasPrice`, `maxFeePerGas` and `maxPriorityFeePerGas`. + * + * @param {null | bigint} [gasPrice] - The gas price. + * @param {null | bigint} [maxFeePerGas] - The maximum fee per gas. + * @param {null | bigint} [maxPriorityFeePerGas] - The maximum priority fee per gas. */ constructor(gasPrice?: null | bigint, maxFeePerGas?: null | bigint, maxPriorityFeePerGas?: null | bigint) { defineProperties(this, { @@ -117,6 +131,8 @@ export class FeeData { /** * Returns a JSON-friendly value. + * + * @returns {any} The JSON-friendly value. */ toJSON(): any { const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = this; @@ -129,6 +145,13 @@ export class FeeData { } } +/** + * Determines the address from a transaction request. + * + * @param {TransactionRequest} tx - The transaction request. + * @returns {AddressLike} The address from the transaction request. + * @throws {Error} If unable to determine the address. + */ export function addressFromTransactionRequest(tx: TransactionRequest): AddressLike { if ('from' in tx) { return tx.from; @@ -141,6 +164,7 @@ export function addressFromTransactionRequest(tx: TransactionRequest): AddressLi } throw new Error('Unable to determine address from transaction inputs, from or to field'); } + /** * A **TransactionRequest** is a transactions with potentially various properties not defined, or with less strict types * for its values. @@ -177,7 +201,7 @@ export interface QuaiTransactionRequest { nonce?: null | number; /** - * The maximum amount of gas to allow this transaction to consime. + * The maximum amount of gas to allow this transaction to consume. */ gasLimit?: null | BigNumberish; @@ -226,8 +250,6 @@ export interface QuaiTransactionRequest { */ customData?: any; - // Only meaningful when used for call - /** * When using `call` or `estimateGas`, this allows a specific block to be queried. Many backends do not support this * and when unsupported errors are silently squelched and `"latest"` is used. @@ -250,8 +272,14 @@ export interface QiTransactionRequest { */ chainId?: null | BigNumberish; + /** + * The inputs for the transaction. + */ inputs?: null | Array; + /** + * The outputs for the transaction. + */ outputs?: null | Array; } @@ -286,11 +314,10 @@ export interface QuaiPreparedTransactionRequest { /** * The nonce of the transaction, used to prevent replay attacks. */ - nonce?: number; /** - * The maximum amount of gas to allow this transaction to consime. + * The maximum amount of gas to allow this transaction to consume. */ gasLimit?: bigint; @@ -361,8 +388,14 @@ export interface QiPreparedTransactionRequest { */ chainId?: bigint; + /** + * The inputs for the transaction. + */ inputs?: null | Array; + /** + * The outputs for the transaction. + */ outputs?: null | Array; } @@ -371,7 +404,6 @@ export interface QiPreparedTransactionRequest { * * @category Providers * @param {TransactionRequest} req - The transaction request to copy. - * * @returns {PreparedTransactionRequest} The prepared transaction request. * @throws {Error} If the request is invalid. */ @@ -429,9 +461,6 @@ export function copyRequest(req: TransactionRequest): PreparedTransactionRequest return result; } -////////////////////// -// Block - /** * An Interface to indicate a {@link Block | **Block**} has been included in the blockchain. This asserts a Type Guard * that necessary properties are non-null. @@ -442,6 +471,11 @@ export function copyRequest(req: TransactionRequest): PreparedTransactionRequest */ export interface MinedBlock extends Block {} +/** + * Represents the body of a work object. + * + * @category Providers + */ export class WoBody implements WoBodyParams { readonly extTransactions!: Array; readonly header: WoBodyHeader; @@ -449,6 +483,12 @@ export class WoBody implements WoBodyParams { readonly manifest: Array; readonly transactions!: Array; readonly uncles!: Array; + + /** + * Creates a new WoBody instance. + * + * @param {WoBodyParams} params - The parameters for the WoBody. + */ constructor(params: WoBodyParams) { this.extTransactions = params.extTransactions; this.header = new WoBodyHeader(params.header); @@ -459,6 +499,11 @@ export class WoBody implements WoBodyParams { } } +/** + * Represents the header of a work object body. + * + * @category Providers + */ export class WoBodyHeader implements WoBodyHeaderParams { readonly baseFeePerGas!: null | bigint; readonly efficiencyScore: bigint; @@ -491,6 +536,12 @@ export class WoBodyHeader implements WoBodyHeaderParams { readonly transactionsRoot!: string; readonly uncledS: bigint; readonly utxoRoot!: string; + + /** + * Creates a new WoBodyHeader instance. + * + * @param {WoBodyHeaderParams} params - The parameters for the WoBodyHeader. + */ constructor(params: WoBodyHeaderParams) { this.baseFeePerGas = params.baseFeePerGas; this.efficiencyScore = params.efficiencyScore; @@ -526,6 +577,11 @@ export class WoBodyHeader implements WoBodyHeaderParams { } } +/** + * Represents the header of a work object. + * + * @category Providers + */ export class WoHeader implements WoHeaderParams { readonly difficulty!: string; readonly headerHash: string; @@ -536,6 +592,12 @@ export class WoHeader implements WoHeaderParams { readonly parentHash!: string; readonly time: string; readonly txHash: string; + + /** + * Creates a new WoHeader instance. + * + * @param {WoHeaderParams} params - The parameters for the WoHeader. + */ constructor(params: WoHeaderParams) { this.difficulty = params.difficulty; this.headerHash = params.headerHash; @@ -548,6 +610,7 @@ export class WoHeader implements WoHeaderParams { this.txHash = params.txHash; } } + /** * A **Block** represents the data associated with a full block on Ethereum. * @@ -573,6 +636,9 @@ export class Block implements BlockParams, Iterable { * Create a new **Block** object. * * This should generally not be necessary as the unless implementing a low-level library. + * + * @param {BlockParams} block - The block parameters. + * @param {Provider} provider - The provider. */ constructor(block: BlockParams, provider: Provider) { this.#transactions = block.transactions.map((tx) => { @@ -602,6 +668,8 @@ export class Block implements BlockParams, Iterable { /** * Returns the list of transaction hashes, in the order they were executed within the block. + * + * @returns {ReadonlyArray} The list of transaction hashes. */ get transactions(): ReadonlyArray { return this.#transactions.map((tx) => { @@ -612,6 +680,11 @@ export class Block implements BlockParams, Iterable { }); } + /** + * Returns the list of extended transaction hashes, in the order they were executed within the block. + * + * @returns {ReadonlyArray} The list of extended transaction hashes. + */ get extTransactions(): ReadonlyArray { return this.#extTransactions.map((tx) => { if (typeof tx === 'string') { @@ -626,6 +699,9 @@ export class Block implements BlockParams, Iterable { * * This is only available for blocks which prefetched transactions, by passing `true` to `prefetchTxs` into * {@link Provider.getBlock | **getBlock**}. + * + * @returns {Array} The list of prefetched transactions. + * @throws {Error} If the transactions were not prefetched. */ get prefetchedTransactions(): Array { const txs = this.#transactions.slice(); @@ -648,6 +724,15 @@ export class Block implements BlockParams, Iterable { return >txs; } + /** + * Returns the complete extended transactions, in the order they were executed within the block. + * + * This is only available for blocks which prefetched transactions, by passing `true` to `prefetchTxs` into + * {@link Provider.getBlock | **getBlock**}. + * + * @returns {Array} The list of prefetched extended transactions. + * @throws {Error} If the transactions were not prefetched. + */ get prefetchedExtTransactions(): Array { const txs = this.#extTransactions.slice(); @@ -671,6 +756,8 @@ export class Block implements BlockParams, Iterable { /** * Returns a JSON-friendly value. + * + * @returns {any} The JSON-friendly value. */ toJSON(): any { const { interlinkHashes, order, size, subManifest, totalEntropy, uncles, woBody, woHeader } = this; @@ -761,6 +848,8 @@ export class Block implements BlockParams, Iterable { /** * The number of transactions in this block. + * + * @returns {number} The number of transactions. */ get length(): number { return this.#transactions.length; @@ -769,6 +858,8 @@ export class Block implements BlockParams, Iterable { /** * The [Date](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) this block was * included at. + * + * @returns {null | Date} The date this block was included at, or null if the timestamp is not available. */ get date(): null | Date { const timestampHex = this.woHeader.time; @@ -785,6 +876,7 @@ export class Block implements BlockParams, Iterable { * @param {number | string} indexOrHash - The index or hash of the transaction. * * @returns {Promise} A promise resolving to the transaction. + * @throws {Error} If the transaction is not found. */ async getTransaction(indexOrHash: number | string): Promise { // Find the internal value by its index or hash @@ -820,6 +912,14 @@ export class Block implements BlockParams, Iterable { } } + /** + * Get the extended transaction at `index` within this block. + * + * @param {number | string} indexOrHash - The index or hash of the extended transaction. + * + * @returns {Promise} A promise resolving to the extended transaction. + * @throws {Error} If the extended transaction is not found. + */ async getExtTransaction(indexOrHash: number | string): Promise { // Find the internal value by its index or hash let tx: string | TransactionResponse | undefined = undefined; @@ -863,6 +963,7 @@ export class Block implements BlockParams, Iterable { * @param {number | string} indexOrHash - The index or hash of the transaction. * * @returns {TransactionResponse} The transaction. + * @throws {Error} If the transaction is not found. */ getPrefetchedTransaction(indexOrHash: number | string): TransactionResponse { const txs = this.prefetchedTransactions; @@ -2616,3 +2717,5 @@ export interface Provider extends ContractRunner, EventEmitterable; } + + diff --git a/src/providers/subscriber-connection.ts b/src/providers/subscriber-connection.ts index 481bce9c..e5c0583f 100644 --- a/src/providers/subscriber-connection.ts +++ b/src/providers/subscriber-connection.ts @@ -4,26 +4,43 @@ import type { Provider } from './provider.js'; /** * @category Providers - * @todo Write documentation for this interface. + * @interface + * @description Interface for Connection RPC Provider. */ export interface ConnectionRpcProvider extends Provider { - //send(method: string, params: Array): Promise; + /** + * Subscribe to a specific event. + * @param {Array} param - The parameters for the subscription. + * @param {function(any): void} processFunc - The function to process the result. + * @returns {number} The subscription ID. + */ _subscribe(param: Array, processFunc: (result: any) => void): number; + + /** + * Unsubscribe from a specific event. + * @param {number} filterId - The subscription ID to unsubscribe. + * @returns {void} + */ _unsubscribe(filterId: number): void; } /** * @category Providers - * @todo Write documentation for this class. + * @class + * @implements {Subscriber} + * @description Class for subscribing to block connections. */ export class BlockConnectionSubscriber implements Subscriber { #provider: ConnectionRpcProvider; #blockNumber: number; - #running: boolean; - #filterId: null | number; + /** + * @ignore + * @constructor + * @param {ConnectionRpcProvider} provider - The provider for the connection. + */ constructor(provider: ConnectionRpcProvider) { this.#provider = provider; this.#blockNumber = -2; @@ -31,6 +48,10 @@ export class BlockConnectionSubscriber implements Subscriber { this.#filterId = null; } + /** + * Start the block connection subscription. + * @returns {void} + */ start(): void { if (this.#running) { return; @@ -47,6 +68,10 @@ export class BlockConnectionSubscriber implements Subscriber { }); } + /** + * Stop the block connection subscription. + * @returns {void} + */ stop(): void { if (!this.#running) { return; @@ -59,6 +84,11 @@ export class BlockConnectionSubscriber implements Subscriber { } } + /** + * Pause the block connection subscription. + * @param {boolean} [dropWhilePaused=false] - Whether to drop blocks while paused. + * @returns {void} + */ pause(dropWhilePaused?: boolean): void { if (dropWhilePaused) { this.#blockNumber = -2; @@ -66,6 +96,10 @@ export class BlockConnectionSubscriber implements Subscriber { this.stop(); } + /** + * Resume the block connection subscription. + * @returns {void} + */ resume(): void { this.start(); } diff --git a/src/providers/subscriber-filterid.ts b/src/providers/subscriber-filterid.ts index e1504854..1f3ad7a7 100644 --- a/src/providers/subscriber-filterid.ts +++ b/src/providers/subscriber-filterid.ts @@ -7,6 +7,12 @@ import type { Network } from './network.js'; import type { EventFilter } from './provider.js'; import type { JsonRpcApiProvider } from './provider-jsonrpc.js'; +/** + * Deep copies an object. + * + * @param {any} obj - The object to copy. + * @returns {any} A deep copy of the object. + */ function copy(obj: any): any { return JSON.parse(JSON.stringify(obj)); } @@ -14,8 +20,8 @@ function copy(obj: any): any { /** * Some backends support subscribing to events using a Filter ID. * - * When subscribing with this technique, the node issues a unique //Filter ID//. At this point the node dedicates - * resources to the filter, so that periodic calls to follow up on the //Filter ID// will receive any events since the + * When subscribing with this technique, the node issues a unique **Filter ID**. At this point the node dedicates + * resources to the filter, so that periodic calls to follow up on the **Filter ID** will receive any events since the * last call. * * @category Providers @@ -33,9 +39,12 @@ export class FilterIdSubscriber implements Subscriber { #hault: boolean; /** - * Creates a new **FilterIdSubscriber** which will used {@link FilterIdSubscriber._subscribe | **_subscribe**} and + * @ignore + * Creates a new **FilterIdSubscriber** which will use {@link FilterIdSubscriber._subscribe | **_subscribe**} and * {@link FilterIdSubscriber._emitResults | **_emitResults**} to setup the subscription and provide the event to the * `provider`. + * + * @param {JsonRpcApiProvider} provider - The provider to use. */ constructor(provider: JsonRpcApiProvider) { this.#provider = provider; @@ -52,30 +61,45 @@ export class FilterIdSubscriber implements Subscriber { /** * Sub-classes **must** override this to begin the subscription. + * + * @param {JsonRpcApiProvider} provider - The provider to use. + * @returns {Promise} A promise that resolves to the subscription ID. + * @throws {Error} If the method is not overridden. */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars _subscribe(provider: JsonRpcApiProvider): Promise { throw new Error('subclasses must override this'); } /** - * Sub-classes **must** override this handle the events. + * Sub-classes **must** override this to handle the events. + * + * @param {AbstractProvider} provider - The provider to use. + * @param {Array} result - The results to handle. + * @returns {Promise} A promise that resolves when the results are handled. + * @throws {Error} If the method is not overridden. */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars _emitResults(provider: AbstractProvider, result: Array): Promise { throw new Error('subclasses must override this'); } /** - * Sub-classes **must** override this handle recovery on errors. + * Sub-classes **must** override this to handle recovery on errors. + * + * @param {AbstractProvider} provider - The provider to use. + * @returns {Subscriber} The recovered subscriber. + * @throws {Error} If the method is not overridden. */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars _recover(provider: AbstractProvider): Subscriber { throw new Error('subclasses must override this'); } - // TODO: `blockNumber` is not used, should it be removed? - // eslint-disable-next-line @typescript-eslint/no-unused-vars + /** + * Polls for new events. + * + * @ignore + * @param {number} blockNumber - The block number to poll from. + * @returns {Promise} A promise that resolves when polling is complete. + */ async #poll(blockNumber: number): Promise { try { // Subscribe if necessary @@ -107,7 +131,7 @@ export class FilterIdSubscriber implements Subscriber { } if ((this.#network as Network).chainId !== network.chainId) { - throw new Error('chaid changed'); + throw new Error('chain changed'); } if (this.#hault) { @@ -123,6 +147,11 @@ export class FilterIdSubscriber implements Subscriber { this.#provider.once('block', this.#poller); } + /** + * Tears down the subscription. + * + * @ignore + */ #teardown(): void { const filterIdPromise = this.#filterIdPromise; if (filterIdPromise) { @@ -133,6 +162,9 @@ export class FilterIdSubscriber implements Subscriber { } } + /** + * Starts the subscriber. + */ start(): void { if (this.#running) { return; @@ -142,6 +174,9 @@ export class FilterIdSubscriber implements Subscriber { this.#poll(-2); } + /** + * Stops the subscriber. + */ stop(): void { if (!this.#running) { return; @@ -153,6 +188,11 @@ export class FilterIdSubscriber implements Subscriber { this.#provider.off('block', this.#poller); } + /** + * Pauses the subscriber. + * + * @param {boolean} [dropWhilePaused] - Whether to drop the subscription while paused. + */ pause(dropWhilePaused?: boolean): void { if (dropWhilePaused) { this.#teardown(); @@ -160,6 +200,9 @@ export class FilterIdSubscriber implements Subscriber { this.#provider.off('block', this.#poller); } + /** + * Resumes the subscriber. + */ resume(): void { this.start(); } @@ -174,22 +217,45 @@ export class FilterIdEventSubscriber extends FilterIdSubscriber { #event: EventFilter; /** - * Creates a new **FilterIdEventSubscriber** attached to `provider` listening for `filter%%. + * @ignore + * Creates a new **FilterIdEventSubscriber** attached to `provider` listening for `filter`. + * + * @param {JsonRpcApiProvider} provider - The provider to use. + * @param {EventFilter} filter - The event filter to use. */ constructor(provider: JsonRpcApiProvider, filter: EventFilter) { super(provider); this.#event = copy(filter); } + /** + * Recovers the subscriber. + * + * @param {AbstractProvider} provider - The provider to use. + * @returns {Subscriber} The recovered subscriber. + */ _recover(provider: AbstractProvider): Subscriber { return new PollingEventSubscriber(provider, this.#event); } + /** + * Subscribes to the event filter. + * + * @param {JsonRpcApiProvider} provider - The provider to use. + * @returns {Promise} A promise that resolves to the subscription ID. + */ async _subscribe(provider: JsonRpcApiProvider): Promise { const filterId = await provider.send('quai_newFilter', [this.#event]); return filterId; } + /** + * Emits the results of the event filter. + * + * @param {JsonRpcApiProvider} provider - The provider to use. + * @param {Array} results - The results to emit. + * @returns {Promise} A promise that resolves when the results are emitted. + */ async _emitResults(provider: JsonRpcApiProvider, results: Array): Promise { for (const result of results) { provider.emit(this.#event, provider._wrapLog(result, provider._network)); @@ -203,10 +269,23 @@ export class FilterIdEventSubscriber extends FilterIdSubscriber { * @category Providers */ export class FilterIdPendingSubscriber extends FilterIdSubscriber { + /** + * Subscribes to the pending transactions filter. + * + * @param {JsonRpcApiProvider} provider - The provider to use. + * @returns {Promise} A promise that resolves to the subscription ID. + */ async _subscribe(provider: JsonRpcApiProvider): Promise { return await provider.send('quai_newPendingTransactionFilter', []); } + /** + * Emits the results of the pending transactions filter. + * + * @param {JsonRpcApiProvider} provider - The provider to use. + * @param {Array} results - The results to emit. + * @returns {Promise} A promise that resolves when the results are emitted. + */ async _emitResults(provider: JsonRpcApiProvider, results: Array): Promise { for (const result of results) { provider.emit('pending', result); diff --git a/src/providers/subscriber-polling.ts b/src/providers/subscriber-polling.ts index 9644964f..0673c822 100644 --- a/src/providers/subscriber-polling.ts +++ b/src/providers/subscriber-polling.ts @@ -3,6 +3,12 @@ import { assert, isHexString } from '../utils/index.js'; import type { AbstractProvider, Subscriber } from './abstract-provider.js'; import type { EventFilter, OrphanFilter, ProviderEvent } from './provider.js'; +/** + * Deep copies an object. + * + * @param {any} obj - The object to copy. + * @returns {any} The copied object. + */ function copy(obj: any): any { return JSON.parse(JSON.stringify(obj)); } @@ -10,6 +16,10 @@ function copy(obj: any): any { /** * Return the polling subscriber for common events. * + * @param {AbstractProvider} provider - The provider to attach the subscriber to. + * @param {ProviderEvent} event - The event to subscribe to. + * @returns {Subscriber} The polling subscriber. + * @throws {Error} If the event is unsupported. * @category Providers */ export function getPollingSubscriber(provider: AbstractProvider, event: ProviderEvent): Subscriber { @@ -34,7 +44,6 @@ export function getPollingSubscriber(provider: AbstractProvider, event: Provider export class PollingBlockSubscriber implements Subscriber { #provider: AbstractProvider; #poller: null | number; - #interval: number; // The most recent block we have scanned for events. The value -2 @@ -43,25 +52,39 @@ export class PollingBlockSubscriber implements Subscriber { /** * Create a new **PollingBlockSubscriber** attached to `provider`. + * @ignore */ constructor(provider: AbstractProvider) { this.#provider = provider; this.#poller = null; this.#interval = 4000; - this.#blockNumber = -2; } /** * The polling interval. + * + * @returns {number} The current polling interval. */ get pollingInterval(): number { return this.#interval; } + + /** + * Sets the polling interval. + * + * @param {number} value - The new polling interval. + */ set pollingInterval(value: number) { this.#interval = value; } + /** + * Polls for new blocks. + * + * @returns {Promise} A promise that resolves when polling is complete. + * @ignore + */ async #poll(): Promise { try { const blockNumber = await this.#provider.getBlockNumber(); @@ -99,6 +122,9 @@ export class PollingBlockSubscriber implements Subscriber { this.#poller = this.#provider._setTimeout(this.#poll.bind(this), this.#interval); } + /** + * Starts the polling process. + */ start(): void { if (this.#poller) { return; @@ -107,6 +133,9 @@ export class PollingBlockSubscriber implements Subscriber { this.#poll(); } + /** + * Stops the polling process. + */ stop(): void { if (!this.#poller) { return; @@ -115,6 +144,11 @@ export class PollingBlockSubscriber implements Subscriber { this.#poller = null; } + /** + * Pauses the polling process. + * + * @param {boolean} [dropWhilePaused] - Whether to drop the block number while paused. + */ pause(dropWhilePaused?: boolean): void { this.stop(); if (dropWhilePaused) { @@ -122,13 +156,16 @@ export class PollingBlockSubscriber implements Subscriber { } } + /** + * Resumes the polling process. + */ resume(): void { this.start(); } } /** - * An **OnBlockSubscriber** can be sub-classed, with a {@link OnBlockSubscriber._poll | **_poll**} implmentation which + * An **OnBlockSubscriber** can be sub-classed, with a {@link OnBlockSubscriber._poll | **_poll**} implementation which * will be called on every new block. * * @category Providers @@ -140,6 +177,7 @@ export class OnBlockSubscriber implements Subscriber { /** * Create a new **OnBlockSubscriber** attached to `provider`. + * @ignore */ constructor(provider: AbstractProvider) { this.#provider = provider; @@ -151,13 +189,19 @@ export class OnBlockSubscriber implements Subscriber { /** * Called on every new block. + * + * @param {number} blockNumber - The block number. + * @param {AbstractProvider} provider - The provider. + * @returns {Promise} A promise that resolves when the poll is complete. + * @throws {Error} If the method is not overridden by a subclass. */ - // TODO: implement this - // eslint-disable-next-line @typescript-eslint/no-unused-vars async _poll(blockNumber: number, provider: AbstractProvider): Promise { throw new Error('sub-classes must override this'); } + /** + * Starts the subscriber. + */ start(): void { if (this.#running) { return; @@ -168,6 +212,9 @@ export class OnBlockSubscriber implements Subscriber { this.#provider.on('block', this.#poll); } + /** + * Stops the subscriber. + */ stop(): void { if (!this.#running) { return; @@ -177,11 +224,18 @@ export class OnBlockSubscriber implements Subscriber { this.#provider.off('block', this.#poll); } - // TODO: `dropWhilePaused` is not used; remove? - // eslint-disable-next-line @typescript-eslint/no-unused-vars + /** + * Pauses the subscriber. + * + * @param {boolean} [dropWhilePaused] - Whether to drop the block number while paused. + */ pause(dropWhilePaused?: boolean): void { this.stop(); } + + /** + * Resumes the subscriber. + */ resume(): void { this.start(); } @@ -193,13 +247,23 @@ export class OnBlockSubscriber implements Subscriber { export class PollingOrphanSubscriber extends OnBlockSubscriber { #filter: OrphanFilter; + /** + * Create a new **PollingOrphanSubscriber** attached to `provider`, listening for `filter`. + * @ignore + */ constructor(provider: AbstractProvider, filter: OrphanFilter) { super(provider); this.#filter = copy(filter); } - // TODO: implement this - // eslint-disable-next-line @typescript-eslint/no-unused-vars + /** + * Polls for orphaned blocks. + * + * @param {number} blockNumber - The block number. + * @param {AbstractProvider} provider - The provider. + * @returns {Promise} A promise that resolves when the poll is complete. + * @throws {Error} If the method is not implemented. + */ async _poll(blockNumber: number, provider: AbstractProvider): Promise { throw new Error('@TODO'); console.log(this.#filter); @@ -216,12 +280,20 @@ export class PollingTransactionSubscriber extends OnBlockSubscriber { /** * Create a new **PollingTransactionSubscriber** attached to `provider`, listening for `hash`. + * @ignore */ constructor(provider: AbstractProvider, hash: string) { super(provider); this.#hash = hash; } + /** + * Polls for the transaction receipt. + * + * @param {number} blockNumber - The block number. + * @param {AbstractProvider} provider - The provider. + * @returns {Promise} A promise that resolves when the poll is complete. + */ async _poll(blockNumber: number, provider: AbstractProvider): Promise { const tx = await provider.getTransactionReceipt(this.#hash); if (tx) { @@ -239,15 +311,12 @@ export class PollingEventSubscriber implements Subscriber { #provider: AbstractProvider; #filter: EventFilter; #poller: (b: number) => void; - #running: boolean; - - // The most recent block we have scanned for events. The value -2 - // indicates we still need to fetch an initial block number #blockNumber: number; /** - * Create a new **PollingTransactionSubscriber** attached to `provider`, listening for `filter%%. + * Create a new **PollingEventSubscriber** attached to `provider`, listening for `filter`. + * @ignore */ constructor(provider: AbstractProvider, filter: EventFilter) { this.#provider = provider; @@ -257,6 +326,13 @@ export class PollingEventSubscriber implements Subscriber { this.#blockNumber = -2; } + /** + * Polls for logs based on the filter. + * + * @param {number} blockNumber - The block number. + * @returns {Promise} A promise that resolves when the poll is complete. + * @ignore + */ async #poll(blockNumber: number): Promise { // The initial block hasn't been determined yet if (this.#blockNumber === -2) { @@ -288,6 +364,9 @@ export class PollingEventSubscriber implements Subscriber { } } + /** + * Starts the subscriber. + */ start(): void { if (this.#running) { return; @@ -302,6 +381,9 @@ export class PollingEventSubscriber implements Subscriber { this.#provider.on('block', this.#poller); } + /** + * Stops the subscriber. + */ stop(): void { if (!this.#running) { return; @@ -311,6 +393,11 @@ export class PollingEventSubscriber implements Subscriber { this.#provider.off('block', this.#poller); } + /** + * Pauses the subscriber. + * + * @param {boolean} [dropWhilePaused] - Whether to drop the block number while paused. + */ pause(dropWhilePaused?: boolean): void { this.stop(); if (dropWhilePaused) { @@ -318,6 +405,9 @@ export class PollingEventSubscriber implements Subscriber { } } + /** + * Resumes the subscriber. + */ resume(): void { this.start(); } diff --git a/src/quais.ts b/src/quais.ts index 3be3811d..e2d010da 100644 --- a/src/quais.ts +++ b/src/quais.ts @@ -3,8 +3,6 @@ export { version } from './_version.js'; // APPLICATION BINARY INTERFACE export { - decodeBytes32String, - encodeBytes32String, AbiCoder, ConstructorFragment, ErrorFragment, @@ -32,11 +30,13 @@ export { recoverAddress, getCreateAddress, getCreate2Address, - isAddressable, - isAddress, resolveAddress, validateAddress, formatMixedCaseChecksumAddress, + isAddressable, + isAddress, + isQuaiAddress, + isQiAddress, } from './address/index.js'; //CONSTANTS @@ -102,7 +102,6 @@ export { FeeData, Log, TransactionReceipt, - TransactionResponse, AbstractProvider, JsonRpcApiProvider, JsonRpcProvider, @@ -122,16 +121,11 @@ export { export { AbstractSigner, VoidSigner } from './signers/index.js'; // TRANSACTION -export { - accessListify, - AbstractTransaction, - FewestCoinSelector, - QiTransaction, - QuaiTransaction, -} from './transaction/index.js'; +export { accessListify, FewestCoinSelector, QiTransaction, QuaiTransaction } from './transaction/index.js'; // UTILS export { + // data concat, dataLength, dataSlice, @@ -143,14 +137,8 @@ export { stripZerosLeft, zeroPadBytes, zeroPadValue, - defineProperties, - resolveProperties, - assert, - assertArgument, - assertArgumentCount, - assertNormalize, - assertPrivate, - makeError, + + // errors isCallException, isError, EventPayload, @@ -158,6 +146,8 @@ export { FetchResponse, FetchCancelSignal, FixedNumber, + + // numbers getBigInt, getNumber, getUint, @@ -169,40 +159,41 @@ export { fromTwos, toTwos, mask, + + // strings formatQuai, parseQuai, - formatEther, - parseEther, formatUnits, parseUnits, uuidV4, getTxType, getZoneForAddress, getAddressDetails, - isQiAddress, } from './utils/index.js'; export { + // bytes 32 + decodeBytes32, + encodeBytes32, + + // base 58 decodeBase58, encodeBase58, + + // base 64 decodeBase64, encodeBase64, - decodeProtoTransaction, - encodeProtoTransaction, - decodeProtoWorkObject, - encodeProtoWorkObject, + + // utf8 toUtf8Bytes, toUtf8CodePoints, toUtf8String, - Utf8ErrorFuncs, } from './encoding/index.js'; // WALLET export { Mnemonic, - BaseWallet, QuaiHDWallet, - HDNodeVoidWallet, QiHDWallet, Wallet, isKeystoreJson, @@ -260,8 +251,6 @@ export type { TypedDataDomain, TypedDataField } from './hash/index.js'; // PROVIDERS export type { Provider, - AbstractProviderOptions, - AbstractProviderPlugin, BlockParams, BlockTag, DebugEventBrowserProvider, @@ -289,6 +278,7 @@ export type { TopicFilter, TransactionReceiptParams, TransactionRequest, + TransactionResponse, TransactionResponseParams, WebSocketCreator, WebSocketLike, diff --git a/src/transaction/abstract-coinselector.ts b/src/transaction/abstract-coinselector.ts index 6988bc6b..bae8434d 100644 --- a/src/transaction/abstract-coinselector.ts +++ b/src/transaction/abstract-coinselector.ts @@ -1,10 +1,23 @@ import { UTXO, UTXOEntry, UTXOLike } from './utxo.js'; +/** + * Represents a target for spending. + * @typedef {Object} SpendTarget + * @property {string} address - The address to send to. + * @property {bigint} value - The amount to send. + */ export type SpendTarget = { address: string; value: bigint; }; +/** + * Represents the result of selected coins. + * @typedef {Object} SelectedCoinsResult + * @property {UTXO[]} inputs - The selected UTXOs. + * @property {UTXO[]} spendOutputs - The outputs for spending. + * @property {UTXO[]} changeOutputs - The outputs for change. + */ export type SelectedCoinsResult = { inputs: UTXO[]; spendOutputs: UTXO[]; @@ -16,19 +29,29 @@ export type SelectedCoinsResult = { * UTXOs for a spend and to properly handle spend and change outputs. * * This class is abstract and should not be used directly. Sub-classes should implement the - * {@link AbstractCoinSelector.performSelection | **performSelection** } method to provide the actual coin selection + * {@link AbstractCoinSelector#performSelection | **performSelection**} method to provide the actual coin selection * logic. * * @category Transaction + * @abstract */ export abstract class AbstractCoinSelector { #availableUXTOs: UTXO[]; #spendOutputs: UTXO[]; #changeOutputs: UTXO[]; + /** + * Gets the available UTXOs. + * @returns {UTXO[]} The available UTXOs. + */ get availableUXTOs(): UTXO[] { return this.#availableUXTOs; } + + /** + * Sets the available UTXOs. + * @param {UTXOLike[]} value - The UTXOs to set. + */ set availableUXTOs(value: UTXOLike[]) { this.#availableUXTOs = value.map((val) => { const utxo = UTXO.from(val); @@ -37,22 +60,41 @@ export abstract class AbstractCoinSelector { }); } + /** + * Gets the spend outputs. + * @returns {UTXO[]} The spend outputs. + */ get spendOutputs(): UTXO[] { return this.#spendOutputs; } + + /** + * Sets the spend outputs. + * @param {UTXOLike[]} value - The spend outputs to set. + */ set spendOutputs(value: UTXOLike[]) { this.#spendOutputs = value.map((utxo) => UTXO.from(utxo)); } + /** + * Gets the change outputs. + * @returns {UTXO[]} The change outputs. + */ get changeOutputs(): UTXO[] { return this.#changeOutputs; } + + /** + * Sets the change outputs. + * @param {UTXOLike[]} value - The change outputs to set. + */ set changeOutputs(value: UTXOLike[]) { this.#changeOutputs = value.map((utxo) => UTXO.from(utxo)); } /** * Constructs a new AbstractCoinSelector instance with an empty UTXO array. + * @param {UTXOEntry[]} [availableUXTOs=[]] - The initial available UTXOs. */ constructor(availableUXTOs: UTXOEntry[] = []) { this.#availableUXTOs = availableUXTOs.map((val: UTXOLike) => { @@ -70,8 +112,8 @@ export abstract class AbstractCoinSelector { * and change outputs. * * @param {SpendTarget} target - The target address and value to spend. - * * @returns {SelectedCoinsResult} The selected UTXOs and outputs. + * @abstract */ abstract performSelection(target: SpendTarget): SelectedCoinsResult; @@ -81,6 +123,7 @@ export abstract class AbstractCoinSelector { * * @param {UTXO} utxo - The UTXO to validate. * @throws {Error} If the UTXO is invalid. + * @protected */ protected _validateUTXO(utxo: UTXO): void { if (utxo.address == null) { diff --git a/src/transaction/abstract-transaction.ts b/src/transaction/abstract-transaction.ts index 9b749b45..4777d03d 100644 --- a/src/transaction/abstract-transaction.ts +++ b/src/transaction/abstract-transaction.ts @@ -25,136 +25,111 @@ export interface TransactionLike { /** * The signature for the transaction */ - signature?: null | SignatureLike; + /** + * The hash of the transaction. + */ hash?: null | string; } /** * @category Transaction * @todo Write documentation for this interface. - * - * @todo Write documentation for this interface. */ export interface ProtoTransaction { /** - * @todo Write documentation for this property. + * The type of the transaction. */ type: number; /** - * @todo Write documentation for this property. + * The recipient address. */ to?: Uint8Array | null; /** - * @todo Write documentation for this property. + * The nonce of the transaction. */ nonce?: number; /** - * @todo Write documentation for this property. + * The value of the transaction. */ value?: Uint8Array; /** - * @todo Write documentation for this property. + * The gas limit for the transaction. */ gas?: number; /** - * @todo Write documentation for this property. + * The data of the transaction. */ data?: Uint8Array; /** - * @todo Write documentation for this property. + * The chain ID of the transaction. */ chain_id: Uint8Array; /** - * @todo Write documentation for this property. + * The gas fee cap for the transaction. */ gas_fee_cap?: Uint8Array; /** - * @todo Write documentation for this property. + * The gas tip cap for the transaction. */ gas_tip_cap?: Uint8Array; /** - * @todo Write documentation for this property. + * The access list for the transaction. */ access_list?: ProtoAccessList; /** - * @todo Write documentation for this property. - */ - etx_gas_limit?: number; - - /** - * @todo Write documentation for this property. - */ - etx_gas_price?: Uint8Array; - - /** - * @todo Write documentation for this property. - */ - etx_gas_tip?: Uint8Array; - - /** - * @todo Write documentation for this property. - */ - etx_data?: Uint8Array; - - /** - * @todo Write documentation for this property. - */ - etx_access_list?: ProtoAccessList; - - /** - * @todo Write documentation for this property. + * The V component of the signature. */ v?: Uint8Array; /** - * @todo Write documentation for this property. + * The R component of the signature. */ r?: Uint8Array; /** - * @todo Write documentation for this property. + * The S component of the signature. */ s?: Uint8Array; /** - * @todo Write documentation for this property. + * The originating transaction hash. */ originating_tx_hash?: string; /** - * @todo Write documentation for this property. + * The external transaction index. */ etx_index?: number; /** - * @todo Write documentation for this property. + * The external transaction sender. */ etx_sender?: Uint8Array; /** - * @todo Write documentation for this property. + * The transaction inputs. */ tx_ins?: { tx_ins: Array }; /** - * @todo Write documentation for this property. + * The transaction outputs. */ tx_outs?: { tx_outs: Array }; /** - * @todo Write documentation for this property. + * The signature of the transaction. */ signature?: Uint8Array; } @@ -162,37 +137,53 @@ export interface ProtoTransaction { /** * @category Transaction * @todo Write documentation for this type. - * - * @todo If not used, replace with `ignore` */ export type ProtoTxOutput = { + /** + * The address of the output. + */ address: Uint8Array; + + /** + * The denomination of the output. + */ denomination: number; }; /** * @category Transaction * @todo Write documentation for this type. - * - * @todo If not used, replace with `ignore` */ export type ProtoTxInput = { + /** + * The previous out point. + */ previous_out_point: { + /** + * The hash of the previous out point. + */ hash: { value: Uint8Array; }; + /** + * The index of the previous out point. + */ index: number; }; + /** + * The public key. + */ pub_key: Uint8Array; }; /** * @category Transaction * @todo Write documentation for this interface. - * - * @todo Write documentation for this interface. */ export interface ProtoAccessList { + /** + * The access tuples. + */ access_tuples: Array; } @@ -201,27 +192,23 @@ export interface ProtoAccessList { * @todo Write documentation for this interface. */ export interface ProtoAccessTuple { + /** + * The address of the access tuple. + */ address: Uint8Array; + + /** + * The storage keys of the access tuple. + */ storage_key: Array; } type allowedSignatureTypes = Signature | string; /** - * A **Transaction** describes an operation to be executed on Ethereum by an Externally Owned Account (EOA). It includes - * who (the {@link ProtoTransaction.to | **to** } address), what (the {@link ProtoTransaction.data | **data** }) and how - * much (the {@link ProtoTransaction.value | **value** } in ether) the operation should entail. - * - * @category Transaction - * @example - * - * ```ts - * tx = new Transaction(); - * //_result: - * - * tx.data = '0x1234'; - * //_result: - * ``` + * An **AbstractTransaction** describes the common operations to be executed on Quai and Qi ledgers + * by an Externally Owned Account (EOA). This class must be subclassed by concrete implementations + * of transactions on each ledger. */ export abstract class AbstractTransaction implements TransactionLike { protected _type: number | null; @@ -293,6 +280,7 @@ export abstract class AbstractTransaction imple this._signature = (value == null ? null : Signature.from(value)) as S; } } + /** * Creates a new Transaction with default values. */ @@ -324,7 +312,6 @@ export abstract class AbstractTransaction imple from: string; signature: Signature; } { - //isSigned(): this is SignedTransaction { return this.signature != null; } @@ -370,7 +357,7 @@ export abstract class AbstractTransaction imple abstract inferTypes(): Array; /** - * Create a copy of this transaciton. + * Create a copy of this transaction. * * @returns {AbstractTransaction} The cloned transaction. */ @@ -386,14 +373,30 @@ export abstract class AbstractTransaction imple /** * Return a protobuf-friendly JSON object. * + * @param {boolean} includeSignature - Whether to include the signature in the protobuf. * @returns {ProtoTransaction} The protobuf-friendly JSON object. */ abstract toProtobuf(includeSignature: boolean): ProtoTransaction; + /** + * Get the origin zone of the transaction. + * + * @returns {Zone | undefined} The origin zone. + */ abstract get originZone(): Zone | undefined; + /** + * Get the destination zone of the transaction. + * + * @returns {Zone | undefined} The destination zone. + */ abstract get destZone(): Zone | undefined; + /** + * Check if the transaction is external. + * + * @returns {boolean} True if the transaction is external. + */ get isExternal(): boolean { return this.destZone !== undefined && this.originZone !== this.destZone; } diff --git a/src/transaction/accesslist.ts b/src/transaction/accesslist.ts index ff207955..3eba4f3c 100644 --- a/src/transaction/accesslist.ts +++ b/src/transaction/accesslist.ts @@ -4,6 +4,13 @@ import { assertArgument, isHexString } from '../utils/index.js'; import type { AccessList, AccessListish } from './index.js'; +/** + * Converts an address and storage keys into an access set. + * + * @param {string} addr - The address to validate and convert. + * @param {Array} storageKeys - The storage keys to validate and convert. + * @returns {{ address: string; storageKeys: Array }} The access set. + */ function accessSetify(addr: string, storageKeys: Array): { address: string; storageKeys: Array } { validateAddress(addr); return { @@ -16,11 +23,10 @@ function accessSetify(addr: string, storageKeys: Array): { address: stri } /** - * Returns a {@link AccessList | **AccessList** } from any quais-supported access-list structure. + * Returns an {@link AccessList | **AccessList**} from any quasi-supported access-list structure. * * @category Transaction * @param {AccessListish} value - The value to convert to an access list. - * * @returns {AccessList} The access list. * @throws {Error} If the value is not a valid access list. */ diff --git a/src/transaction/coinselector-fewest.ts b/src/transaction/coinselector-fewest.ts index 5e13d455..b5201638 100644 --- a/src/transaction/coinselector-fewest.ts +++ b/src/transaction/coinselector-fewest.ts @@ -82,7 +82,7 @@ export class FewestCoinSelector extends AbstractCoinSelector { throw new Error('Insufficient funds'); } - // // Check if any denominations can be removed from the input set and it still remain valid + // Check if any denominations can be removed from the input set and it still remain valid selectedUTXOs = this.sortUTXOsByDenomination(selectedUTXOs, 'asc'); let runningTotal = totalValue; @@ -139,6 +139,13 @@ export class FewestCoinSelector extends AbstractCoinSelector { }; } + /** + * Sorts UTXOs by their denomination. + * + * @param {UTXO[]} utxos - The UTXOs to sort. + * @param {'asc' | 'desc'} direction - The direction to sort ('asc' for ascending, 'desc' for descending). + * @returns {UTXO[]} The sorted UTXOs. + */ private sortUTXOsByDenomination(utxos: UTXO[], direction: 'asc' | 'desc'): UTXO[] { if (direction === 'asc') { return [...utxos].sort((a, b) => { @@ -152,12 +159,23 @@ export class FewestCoinSelector extends AbstractCoinSelector { }); } + /** + * Validates the target amount. + * + * @param {SpendTarget} target - The target amount to validate. + * @throws Will throw an error if the target amount is less than or equal to 0. + */ private validateTarget(target: SpendTarget) { if (target.value <= BigInt(0)) { throw new Error('Target amount must be greater than 0'); } } + /** + * Validates the available UTXOs. + * + * @throws Will throw an error if there are no available UTXOs. + */ private validateUTXOs() { if (this.availableUXTOs.length === 0) { throw new Error('No UTXOs available'); diff --git a/src/transaction/qi-transaction.ts b/src/transaction/qi-transaction.ts index 84606a15..c428e247 100644 --- a/src/transaction/qi-transaction.ts +++ b/src/transaction/qi-transaction.ts @@ -1,50 +1,73 @@ import { keccak256 } from '../crypto/index.js'; import { AbstractTransaction, TransactionLike, TxInput, TxOutput } from './index.js'; -import { assertArgument, getBytes, getZoneForAddress, hexlify, isQiAddress, toBigInt } from '../utils/index.js'; +import { assertArgument, getBytes, getZoneForAddress, hexlify, toBigInt } from '../utils/index.js'; import { decodeProtoTransaction } from '../encoding/index.js'; import { formatNumber } from '../providers/format.js'; -import { computeAddress } from '../address/index.js'; +import { computeAddress, isQiAddress } from '../address/index.js'; import { ProtoTransaction } from './abstract-transaction.js'; import { Zone } from '../constants/index.js'; /** + * Interface representing a QiTransaction. * @category Transaction - * @todo Write documentation for this interface. */ export interface QiTransactionLike extends TransactionLike { /** - * @todo Write documentation for this property. + * Transaction inputs. + * @type {TxInput[] | null} */ txInputs?: null | TxInput[]; /** - * @todo Write documentation for this property. + * Transaction outputs. + * @type {TxOutput[] | null} */ txOutputs?: null | TxOutput[]; } /** + * Class representing a QiTransaction. * @category Transaction - * @todo Write documentation for this class. - * - * @todo Write documentation for the properties of this class. + * @extends {AbstractTransaction} + * @implements {QiTransactionLike} */ export class QiTransaction extends AbstractTransaction implements QiTransactionLike { #txInputs?: null | TxInput[]; #txOutputs?: null | TxOutput[]; + /** + * Get transaction inputs. + * @returns {TxInput[]} The transaction inputs. + */ get txInputs(): TxInput[] { return (this.#txInputs ?? []).map((entry) => ({ ...entry })); } + + /** + * Set transaction inputs. + * @param {TxInput[] | null} value - The transaction inputs. + * @throws {Error} If the value is not an array. + */ set txInputs(value: TxInput[] | null) { if (!Array.isArray(value)) { throw new Error('txInputs must be an array'); } this.#txInputs = value.map((entry) => ({ ...entry })); } + + /** + * Get transaction outputs. + * @returns {TxOutput[]} The transaction outputs. + */ get txOutputs(): TxOutput[] { return (this.#txOutputs ?? []).map((output) => ({ ...output })); } + + /** + * Set transaction outputs. + * @param {TxOutput[] | null} value - The transaction outputs. + * @throws {Error} If the value is not an array. + */ set txOutputs(value: TxOutput[] | null) { if (!Array.isArray(value)) { throw new Error('txOutputs must be an array'); @@ -53,8 +76,10 @@ export class QiTransaction extends AbstractTransaction implements QiTran } /** - * The permuted hash of the transaction as specified by - * [QIP-0010](https://github.com/quai-network/qips/blob/master/qip-0010.md). + * Get the permuted hash of the transaction as specified by QIP-0010. + * @see {@link [QIP0010](https://github.com/quai-network/qips/blob/master/qip-0010.md)} + * @returns {string | null} The transaction hash. + * @throws {Error} If the transaction has no inputs or outputs, or if cross-zone & cross-ledger transactions are not supported. */ get hash(): null | string { if (this.signature == null) { @@ -94,7 +119,8 @@ export class QiTransaction extends AbstractTransaction implements QiTran } /** - * The zone of the sender address + * Get the zone of the sender address. + * @returns {Zone | undefined} The origin zone. */ get originZone(): Zone | undefined { const senderAddr = computeAddress(this.txInputs[0].pubkey || ''); @@ -104,7 +130,8 @@ export class QiTransaction extends AbstractTransaction implements QiTran } /** - * The zone of the recipient address + * Get the zone of the recipient address. + * @returns {Zone | undefined} The destination zone. */ get destZone(): Zone | undefined { const zone = getZoneForAddress(this.txOutputs[0].address); @@ -122,7 +149,6 @@ export class QiTransaction extends AbstractTransaction implements QiTran /** * Validates the explicit properties and returns a list of compatible transaction types. - * * @returns {number[]} The compatible transaction types. */ inferTypes(): Array { @@ -141,8 +167,7 @@ export class QiTransaction extends AbstractTransaction implements QiTran } /** - * Create a copy of this transaciton. - * + * Create a copy of this transaction. * @returns {QiTransaction} The cloned transaction. */ clone(): QiTransaction { @@ -151,7 +176,6 @@ export class QiTransaction extends AbstractTransaction implements QiTran /** * Return a JSON-friendly object. - * * @returns {QiTransactionLike} The JSON-friendly object. */ toJSON(): TransactionLike { @@ -174,7 +198,7 @@ export class QiTransaction extends AbstractTransaction implements QiTran /** * Return a protobuf-friendly JSON object. - * + * @param {boolean} [includeSignature=true] - Whether to include the signature. * @returns {ProtoTransaction} The protobuf-friendly JSON object. */ toProtobuf(includeSignature: boolean = true): ProtoTransaction { @@ -206,11 +230,10 @@ export class QiTransaction extends AbstractTransaction implements QiTran } /** - * Create a **Transaction** from a serialized transaction or a Transaction-like object. - * + * Create a Transaction from a serialized transaction or a Transaction-like object. * @param {string | QiTransactionLike} tx - The transaction to decode. - * * @returns {QiTransaction} The decoded transaction. + * @throws {Error} If the transaction is unsigned and defines a hash. */ static from(tx: string | QiTransactionLike): QiTransaction { if (typeof tx === 'string') { @@ -243,11 +266,8 @@ export class QiTransaction extends AbstractTransaction implements QiTran } /** - * Create a **Transaction** from a ProtoTransaction object. - * + * Create a Transaction from a ProtoTransaction object. * @param {ProtoTransaction} protoTx - The transaction to decode. - * @param {Uint8Array} [payload] - The serialized transaction. - * * @returns {QiTransaction} The decoded transaction. */ static fromProto(protoTx: ProtoTransaction): QiTransaction { diff --git a/src/transaction/quai-transaction.ts b/src/transaction/quai-transaction.ts index 77a69794..474971ec 100644 --- a/src/transaction/quai-transaction.ts +++ b/src/transaction/quai-transaction.ts @@ -10,13 +10,12 @@ import { getNumber, getZoneForAddress, hexlify, - isQiAddress, toBeArray, toBigInt, zeroPadValue, } from '../utils/index.js'; import { decodeProtoTransaction, encodeProtoTransaction } from '../encoding/index.js'; -import { getAddress, recoverAddress, validateAddress } from '../address/index.js'; +import { getAddress, recoverAddress, validateAddress, isQuaiAddress } from '../address/index.js'; import { formatNumber, handleNumber } from '../providers/format.js'; import { ProtoTransaction } from './abstract-transaction.js'; import { Zone } from '../constants/index.js'; @@ -35,6 +34,7 @@ export interface QuaiTransactionLike extends TransactionLike { * The sender. */ from?: string; + /** * The nonce. */ @@ -76,6 +76,12 @@ export interface QuaiTransactionLike extends TransactionLike { accessList?: null | AccessListish; } +/** + * Parses a signature from an array of fields. + * + * @param {Array} fields - The fields to parse. + * @returns {Signature} The parsed signature. + */ export function _parseSignature(fields: Array): Signature { let yParity: number; try { @@ -94,6 +100,7 @@ export function _parseSignature(fields: Array): Signature { } /** + * Represents a Quai transaction. * @category Transaction * @todo Write documentation for this class. */ @@ -111,6 +118,7 @@ export class QuaiTransaction extends AbstractTransaction implements Q /** * The `to` address for the transaction or `null` if the transaction is an `init` transaction. + * @type {null | string} */ get to(): null | string { return this.#to; @@ -123,6 +131,8 @@ export class QuaiTransaction extends AbstractTransaction implements Q /** * The permuted hash of the transaction as specified by * [QIP-0010](https://github.com/quai-network/qips/blob/master/qip-0010.md). + * @type {null | string} + * @throws {Error} If the transaction is not signed. */ get hash(): null | string { if (this.signature == null) return null; @@ -134,7 +144,7 @@ export class QuaiTransaction extends AbstractTransaction implements Q throw new Error('Missing from or to address'); } - const isSameLedger = isQiAddress(this.from) === isQiAddress(this.to); + const isSameLedger = isQuaiAddress(this.from) === isQuaiAddress(this.to); if (this.isExternal && !isSameLedger) { throw new Error('Cross-zone & cross-ledger transactions are not supported'); } @@ -156,6 +166,7 @@ export class QuaiTransaction extends AbstractTransaction implements Q /** * The zone of the sender address + * @type {Zone | undefined} */ get originZone(): Zone | undefined { const zone = this.from ? getZoneForAddress(this.from) : undefined; @@ -164,6 +175,7 @@ export class QuaiTransaction extends AbstractTransaction implements Q /** * The zone of the recipient address + * @type {Zone | undefined} */ get destZone(): Zone | undefined { const zone = this.to !== null ? getZoneForAddress(this.to || '') : undefined; @@ -172,6 +184,7 @@ export class QuaiTransaction extends AbstractTransaction implements Q /** * The transaction nonce. + * @type {number} */ get nonce(): number { return this.#nonce; @@ -182,6 +195,7 @@ export class QuaiTransaction extends AbstractTransaction implements Q /** * The gas limit. + * @type {bigint} */ get gasLimit(): bigint { return this.#gasLimit; @@ -194,6 +208,7 @@ export class QuaiTransaction extends AbstractTransaction implements Q * The gas price. * * On legacy networks this defines the fee that will be paid. On EIP-1559 networks, this should be `null`. + * @type {null | bigint} */ get gasPrice(): null | bigint { const value = this.#gasPrice; @@ -205,6 +220,7 @@ export class QuaiTransaction extends AbstractTransaction implements Q /** * The maximum priority fee per unit of gas to pay. On legacy networks this should be `null`. + * @type {null | bigint} */ get maxPriorityFeePerGas(): null | bigint { const value = this.#maxPriorityFeePerGas; @@ -219,6 +235,7 @@ export class QuaiTransaction extends AbstractTransaction implements Q /** * The maximum total fee per unit of gas to pay. On legacy networks this should be `null`. + * @type {null | bigint} */ get maxFeePerGas(): null | bigint { const value = this.#maxFeePerGas; @@ -233,6 +250,7 @@ export class QuaiTransaction extends AbstractTransaction implements Q /** * The transaction data. For `init` transactions this is the deployment code. + * @type {string} */ get data(): string { return this.#data; @@ -243,6 +261,7 @@ export class QuaiTransaction extends AbstractTransaction implements Q /** * The amount of ether to send in this transactions. + * @type {bigint} */ get value(): bigint { return this.#value; @@ -256,6 +275,7 @@ export class QuaiTransaction extends AbstractTransaction implements Q * * An access list permits discounted (but pre-paid) access to bytecode and state variable access within contract * execution. + * @type {null | AccessList} */ get accessList(): null | AccessList { const value = this.#accessList || null; @@ -270,6 +290,7 @@ export class QuaiTransaction extends AbstractTransaction implements Q /** * Creates a new Transaction with default values. + * @param {string} [from] - The sender address. */ constructor(from?: string) { super(); @@ -322,7 +343,7 @@ export class QuaiTransaction extends AbstractTransaction implements Q } /** - * Create a copy of this transaciton. + * Create a copy of this transaction. * * @returns {QuaiTransaction} The cloned transaction. */ @@ -364,6 +385,7 @@ export class QuaiTransaction extends AbstractTransaction implements Q /** * Return a protobuf-friendly JSON object. * + * @param {boolean} [includeSignature=true] - Whether to include the signature. * @returns {ProtoTransaction} The protobuf-friendly JSON object. */ toProtobuf(includeSignature: boolean = true): ProtoTransaction { @@ -393,7 +415,6 @@ export class QuaiTransaction extends AbstractTransaction implements Q * Create a **Transaction** from a serialized transaction or a Transaction-like object. * * @param {string | QuaiTransactionLike} tx - The transaction to decode. - * * @returns {QuaiTransaction} The decoded transaction. */ static from(tx: string | QuaiTransactionLike): QuaiTransaction { @@ -443,7 +464,7 @@ export class QuaiTransaction extends AbstractTransaction implements Q } if (tx.from != null) { - assertArgument(!isQiAddress(tx.from), 'from address must be a Quai address', 'tx.from', tx.from); + assertArgument(isQuaiAddress(tx.from), 'from address must be a Quai address', 'tx.from', tx.from); assertArgument( (result.from || '').toLowerCase() === (tx.from || '').toLowerCase(), 'from mismatch', @@ -459,8 +480,6 @@ export class QuaiTransaction extends AbstractTransaction implements Q * Create a **Transaction** from a ProtoTransaction object. * * @param {ProtoTransaction} protoTx - The transaction to decode. - * @param {Uint8Array} [payload] - The serialized transaction. - * * @returns {QuaiTransaction} The decoded transaction. */ static fromProto(protoTx: ProtoTransaction): QuaiTransaction { diff --git a/src/transaction/utxo.ts b/src/transaction/utxo.ts index 288feafc..6d0f4ab3 100644 --- a/src/transaction/utxo.ts +++ b/src/transaction/utxo.ts @@ -3,9 +3,9 @@ import { getBigInt } from '../utils/index.js'; import type { BigNumberish } from '../utils/index.js'; /** + * Represents an a spendable transaction outpoint. * @category Transaction * @todo Write documentation for this type. - * * @todo If not used, replace with `ignore` */ export type Outpoint = { @@ -15,10 +15,9 @@ export type Outpoint = { }; /** + * Represents a UTXO entry. * @category Transaction - * @todo Write documentation for this type. - * - * @todo If not used, replace with `ignore` + * @ignore */ export interface UTXOEntry { denomination: null | bigint; @@ -26,10 +25,9 @@ export interface UTXOEntry { } /** + * Represents a UTXO-like object. * @category Transaction - * @todo Write documentation for this type. - * - * @todo If not used, replace with `ignore` + * @ignore */ export interface UTXOLike extends UTXOEntry { txhash?: null | string; @@ -37,10 +35,8 @@ export interface UTXOLike extends UTXOEntry { } /** + * Represents a Qi transaction input. * @category Transaction - * @todo Write documentation for this type. - * - * @todo If not used, replace with `ignore` */ export type TxInput = { txhash: string; @@ -49,22 +45,17 @@ export type TxInput = { }; /** + * Represents a Qi transaction output. * @category Transaction - * @todo Write documentation for this type. - * - * @todo If not used, replace with `ignore` */ export type TxOutput = { address: string; denomination: number; }; - /** + * List of supported Qi denominations. * @category Transaction - * @todo Write documentation for this type. - * - * @todo If not used, replace with `ignore` */ export const denominations: bigint[] = [ BigInt(1), // 0.001 Qi @@ -88,10 +79,8 @@ export const denominations: bigint[] = [ /** * Checks if the provided denomination is valid. - * * @category Transaction * @param {bigint} denomination - The denomination to check. - * * @returns {boolean} True if the denomination is valid, false otherwise. */ function isValidDenomination(denomination: bigint): boolean { @@ -100,12 +89,11 @@ function isValidDenomination(denomination: bigint): boolean { /** * Handles conversion of string to bigint, specifically for transaction parameters. - * * @category Transaction * @param {string} value - The value to convert. * @param {string} param - The parameter name. - * * @returns {bigint} The converted value. + * @ignore */ function handleBigInt(value: string, param: string): bigint { if (value === '0x') { @@ -116,11 +104,10 @@ function handleBigInt(value: string, param: string): bigint { /** * Given a value, returns an array of supported denominations that sum to the value. - * * @category Transaction * @param {bigint} value - The value to denominate. - * * @returns {bigint[]} An array of denominations that sum to the value. + * @throws {Error} If the value is less than or equal to 0 or cannot be matched with available denominations. */ export function denominate(value: bigint): bigint[] { if (value <= BigInt(0)) { @@ -149,10 +136,9 @@ export function denominate(value: bigint): bigint[] { } /** + * Represents a UTXO (Unspent Transaction Output). * @category Transaction - * @todo Write documentation for this type. - * - * @todo If not used, replace with `ignore` + * @implements {UTXOLike} */ export class UTXO implements UTXOLike { #txhash: null | string; @@ -160,31 +146,69 @@ export class UTXO implements UTXOLike { #address: null | string; #denomination: null | bigint; + /** + * Gets the transaction hash. + * @returns {null | string} The transaction hash. + */ get txhash(): null | string { return this.#txhash; } + + /** + * Sets the transaction hash. + * @param {null | string} value - The transaction hash. + */ set txhash(value: null | string) { this.#txhash = value; } + /** + * Gets the index. + * @returns {null | number} The index. + */ get index(): null | number { return this.#index; } + + /** + * Sets the index. + * @param {null | number} value - The index. + */ set index(value: null | number) { this.#index = value; } + /** + * Gets the address. + * @returns {string} The address. + */ get address(): string { return this.#address || ''; } + + /** + * Sets the address. + * @param {string} value - The address. + * @throws {Error} If the address is invalid. + */ set address(value: string) { validateAddress(value); this.#address = value; } + /** + * Gets the denomination. + * @returns {null | bigint} The denomination. + */ get denomination(): null | bigint { return this.#denomination; } + + /** + * Sets the denomination. + * @param {null | BigNumberish} value - The denomination. + * @throws {Error} If the denomination value is invalid. + */ set denomination(value: null | BigNumberish) { if (value == null) { this.#denomination = null; @@ -211,7 +235,6 @@ export class UTXO implements UTXOLike { /** * Converts the UTXO instance to a JSON object. - * * @returns {any} A JSON representation of the UTXO instance. */ toJSON(): any { @@ -225,9 +248,7 @@ export class UTXO implements UTXOLike { /** * Creates a UTXO instance from a UTXOLike object. - * * @param {UTXOLike} utxo - The UTXOLike object to convert. - * * @returns {UTXO} The UTXO instance. */ static from(utxo: UTXOLike): UTXO { diff --git a/src/transaction/work-object.ts b/src/transaction/work-object.ts index e86beffd..7b8653ac 100644 --- a/src/transaction/work-object.ts +++ b/src/transaction/work-object.ts @@ -8,6 +8,7 @@ import { QuaiTransaction, QuaiTransactionLike } from './quai-transaction.js'; * Interface representing a WorkObject, which includes header, body, and transaction information. * * @category Transaction + * @ignore */ export interface WorkObjectLike { /** @@ -30,6 +31,7 @@ export interface WorkObjectLike { * Interface representing the header information of a WorkObject. * * @category Transaction + * @ignore */ export interface WorkObjectHeaderLike { /** @@ -72,6 +74,7 @@ export interface WorkObjectHeaderLike { * Interface representing the body information of a WorkObject. * * @category Transaction + * @ignore */ export interface WorkObjectBodyLike { /** @@ -104,6 +107,7 @@ export interface WorkObjectBodyLike { * Interface representing the header information within the body of a WorkObject. * * @category Transaction + * @ignore */ export interface HeaderLike { /** diff --git a/src/utils/data.ts b/src/utils/data.ts index 2d2841bb..5839fefc 100644 --- a/src/utils/data.ts +++ b/src/utils/data.ts @@ -26,6 +26,17 @@ export type HexString = string; */ export type BytesLike = DataHexString | Uint8Array; +/** + * Converts a BytesLike value to a Uint8Array. + * + * @ignore + * @category Utils + * @param {BytesLike} value - The value to convert. + * @param {string} [name] - The name of the value for error context. + * @param {boolean} [copy] - Whether to create a copy of the value. + * @returns {Uint8Array} The converted Uint8Array. + * @throws {Error} If the value is not a valid BytesLike. + */ function _getBytes(value: BytesLike, name?: string, copy?: boolean): Uint8Array { if (value instanceof Uint8Array) { if (copy) { @@ -53,7 +64,7 @@ function _getBytes(value: BytesLike, name?: string, copy?: boolean): Uint8Array * * @category Utils * @param {BytesLike} value - The value to convert to a Uint8Array. - * @param {string} name - The name of the value for error context. + * @param {string} [name] - The name of the value for error context. * * @returns {Uint8Array} The typed Uint8Array. */ @@ -78,8 +89,8 @@ export function getBytesCopy(value: BytesLike, name?: string): Uint8Array { /** * Returns true if `value` is a valid {@link HexString | **HexString**}. * - * If `length` is `true` or a //number//, it also checks that `value` is a valid - * {@link DataHexString | **DataHexString**} of `length` (if a //number//) bytes of data (e.g. `0x1234` is 2 bytes). + * If `length` is `true` or a number, it also checks that `value` is a valid + * {@link DataHexString | **DataHexString**} of `length` (if a number) bytes of data (e.g. `0x1234` is 2 bytes). * * @category Utils * @param {any} value - The value to check. @@ -174,6 +185,7 @@ export function dataLength(data: BytesLike): number { * @param {number} [end] - The end offset. * * @returns {string} The sliced data. + * @throws {Error} If the end offset is beyond the data bounds. */ export function dataSlice(data: BytesLike, start?: number, end?: number): string { const bytes = getBytes(data); @@ -203,6 +215,17 @@ export function stripZerosLeft(data: BytesLike): string { return '0x' + bytes; } +/** + * Pads the data to the specified length. + * + * @ignore + * @category Utils + * @param {BytesLike} data - The data to pad. + * @param {number} length - The length to pad to. + * @param {boolean} left - Whether to pad on the left. + * @returns {string} The padded data. + * @throws {Error} If the padding exceeds data length. + */ function zeroPad(data: BytesLike, length: number, left: boolean): string { const bytes = getBytes(data); assert(length >= bytes.length, 'padding exceeds data length', 'BUFFER_OVERRUN', { diff --git a/src/utils/index.ts b/src/utils/index.ts index 02938ded..fb9da349 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -50,11 +50,11 @@ export { export { resolveProperties, defineProperties } from './properties.js'; -export { formatQuai, parseQuai, formatEther, parseEther, formatUnits, parseUnits } from './units.js'; +export { formatQuai, parseQuai, formatUnits, parseUnits } from './units.js'; export { uuidV4 } from './uuid.js'; -export { getTxType, getZoneForAddress, getAddressDetails, isQiAddress } from './shards.js'; +export { getTxType, getZoneForAddress, getAddressDetails } from './shards.js'; ///////////////////////////// // Types diff --git a/src/utils/shards.ts b/src/utils/shards.ts index 7b5d42b4..fdf2510c 100644 --- a/src/utils/shards.ts +++ b/src/utils/shards.ts @@ -1,4 +1,5 @@ import { toZone, Zone } from '../constants/zones.js'; +import { isQiAddress } from '../quais.js'; /** * Retrieves the shard information for a given address based on its byte prefix. The function parses the address to * extract its byte prefix, then filters the ShardData to find a matching shard entry. If no matching shard is found, it @@ -40,42 +41,22 @@ export function getAddressDetails(address: string): { zone: Zone; isUTXO: boolea * Otherwise, it returns 0. * * @category Utils - * @param {string | null} from - The sender address, expected to start with "0x" followed by its hexadecimal - * representation. If null, the function returns 0. - * @param {string | null} to - The recipient address, expected to start with "0x" followed by its hexadecimal - * representation. If null, the function returns 0. + * @param {string | null} from - The sender address. If null, the function returns 0. + * @param {string | null} to - The recipient address. If null, the function returns 0. * * @returns {number} The transaction type based on the addresses. */ export function getTxType(from: string | null, to: string | null): number { if (from === null || to === null) return 0; - const fromUTXO = isQiAddress(from); - const toUTXO = isQiAddress(to); + const senderAddressIsQi = isQiAddress(from); + const recipientAddressIsQi = isQiAddress(to); switch (true) { - case fromUTXO && toUTXO: + case senderAddressIsQi && recipientAddressIsQi: return 2; - case fromUTXO && !toUTXO: + case senderAddressIsQi && !recipientAddressIsQi: return 2; default: return 0; } } - -/** - * Checks whether a given blockchain address is a UTXO address based on the 9th bit of the address. This function - * extracts the second byte of the address and checks its first bit to determine the UTXO status. - * - * @category Utils - * @param {string} address - The blockchain address to be analyzed, expected to start with "0x" followed by its - * hexadecimal representation. - * - * @returns {boolean} True if the address is a UTXO address, false otherwise. - */ -export function isQiAddress(address: string): boolean { - const secondByte = address.substring(4, 6); - const binaryString = parseInt(secondByte, 16).toString(2).padStart(8, '0'); - const isUTXO = binaryString[0] === '1'; - - return isUTXO; -} diff --git a/src/utils/units.ts b/src/utils/units.ts index 27acaad9..5d12c345 100644 --- a/src/utils/units.ts +++ b/src/utils/units.ts @@ -28,7 +28,7 @@ const names = ['wei', 'kwei', 'mwei', 'gwei', 'szabo', 'finney', 'ether']; * @category Utils * @param {BigNumberish} value - The value to convert. * @param {string | Numeric} [unit=18] - The unit to convert to. Default is `18` Default is `18` Default is `18` Default - * is `18` Default is `18` Default is `18` + * is `18` Default is `18` Default is `18` Default is `18` * * @returns {string} The converted value. * @throws {Error} If the unit is invalid. @@ -53,7 +53,7 @@ export function formatUnits(value: BigNumberish, unit?: string | Numeric): strin * @category Utils * @param {string} value - The value to convert. * @param {string | Numeric} [unit=18] - The unit to convert from. Default is `18` Default is `18` Default is `18` - * Default is `18` Default is `18` Default is `18` + * Default is `18` Default is `18` Default is `18` Default is `18` * * @returns {bigint} The converted value. * @throws {Error} If the unit is invalid. @@ -121,7 +121,3 @@ export function parseQuai(ether: string): bigint { export function parseQi(value: string): bigint { return parseUnits(value, 3); } - -// Aliases to maintain backwards compatibility. These will be removed in the future. -export const formatEther = formatQuai; -export const parseEther = parseQuai; diff --git a/src/wallet/base-wallet.ts b/src/wallet/base-wallet.ts index 376ebdfd..026a1684 100644 --- a/src/wallet/base-wallet.ts +++ b/src/wallet/base-wallet.ts @@ -11,10 +11,10 @@ import { QuaiTransaction, QuaiTransactionLike } from '../transaction/quai-transa import { keccak256 } from '../crypto/index.js'; /** - * The **BaseWallet** is a stream-lined implementation of a [Signer](../interfaces/Signer) that operates with a private + * The **BaseWallet** is a stream-lined implementation of a {@link AbstractSigner} that operates with a private * key. * - * It is preferred to use the [Wallet](../classes/Wallet) class, as it offers additional functionality and simplifies + * It is preferred to use the {@link Wallet} class, as it offers additional functionality and simplifies * loading a variety of JSON formats, Mnemonic Phrases, etc. * * This class may be of use for those attempting to implement a minimal Signer. @@ -24,15 +24,25 @@ import { keccak256 } from '../crypto/index.js'; export class BaseWallet extends AbstractSigner { /** * The wallet address. + * @type {string} + * @readonly */ readonly #address!: string; + /** + * The signing key used for signing payloads. + * @type {SigningKey} + * @readonly + */ readonly #signingKey: SigningKey; /** * Creates a new BaseWallet for `privateKey`, optionally connected to `provider`. * * If `provider` is not specified, only offline methods can be used. + * + * @param {SigningKey} privateKey - The private key for the wallet. + * @param {null | Provider} [provider] - The provider to connect to. */ constructor(privateKey: SigningKey, provider?: null | Provider) { super(provider); @@ -53,6 +63,8 @@ export class BaseWallet extends AbstractSigner { /** * The address of this wallet. + * @type {string} + * @readonly */ get address(): string { return this.#address; @@ -60,6 +72,8 @@ export class BaseWallet extends AbstractSigner { /** * The {@link SigningKey | **SigningKey**} used for signing payloads. + * @type {SigningKey} + * @readonly */ get signingKey(): SigningKey { return this.#signingKey; @@ -67,6 +81,8 @@ export class BaseWallet extends AbstractSigner { /** * The private key for this wallet. + * @type {string} + * @readonly */ get privateKey(): string { return this.signingKey.privateKey; @@ -74,14 +90,32 @@ export class BaseWallet extends AbstractSigner { // TODO: `_zone` is not used, should it be removed? // eslint-disable-next-line @typescript-eslint/no-unused-vars + /** + * Returns the address of this wallet. + * + * @param {string} [_zone] - The zone (optional). + * @returns {Promise} The wallet address. + */ async getAddress(_zone?: string): Promise { return this.#address; } + /** + * Connects the wallet to a provider. + * + * @param {null | Provider} provider - The provider to connect to. + * @returns {BaseWallet} The connected wallet. + */ connect(provider: null | Provider): BaseWallet { return new BaseWallet(this.#signingKey, provider); } + /** + * Signs a transaction. + * + * @param {QuaiTransactionRequest} tx - The transaction request. + * @returns {Promise} The signed transaction. + */ async signTransaction(tx: QuaiTransactionRequest): Promise { // Replace any Addressable with an address const { to, from } = await resolveProperties({ @@ -113,6 +147,13 @@ export class BaseWallet extends AbstractSigner { return btx.serialized; } + /** + * Signs a message. + * + * @param {string | Uint8Array} message - The message to sign. + * @returns {Promise} The signed message. + * @async + */ async signMessage(message: string | Uint8Array): Promise { return this.signMessageSync(message); } @@ -123,13 +164,21 @@ export class BaseWallet extends AbstractSigner { * Returns the signature for `message` signed with this wallet. * * @param {string | Uint8Array} message - The message to sign. - * * @returns {string} The serialized signature. */ signMessageSync(message: string | Uint8Array): string { return this.signingKey.sign(hashMessage(message)).serialized; } + /** + * Signs typed data. + * + * @param {TypedDataDomain} domain - The domain of the typed data. + * @param {Record>} types - The types of the typed data. + * @param {Record} value - The value of the typed data. + * @returns {Promise} The signed typed data. + * @async + */ async signTypedData( domain: TypedDataDomain, types: Record>, diff --git a/src/wallet/hdnodewallet.ts b/src/wallet/hdnodewallet.ts index c7e521e8..6fe2dceb 100644 --- a/src/wallet/hdnodewallet.ts +++ b/src/wallet/hdnodewallet.ts @@ -1,8 +1,3 @@ -/** - * Explain HD Wallets.. - * - * @_subsection: api/wallet:HD Wallets [hd-wallets] - */ import { computeHmac, randomBytes, ripemd160, SigningKey, sha256 } from "../crypto/index.js"; import { VoidSigner } from "../signers/index.js"; import { computeAddress } from "../address/index.js"; @@ -113,29 +108,32 @@ function derivePath>(node: T, path: string): T { /** * An **HDNodeWallet** is a [[Signer]] backed by the private key derived - * from an HD Node using the [[link-bip-32]] stantard. + * from an HD Node using the [[link-bip-32]] standard. * - * An HD Node forms a hierarchal structure with each HD Node having a + * An HD Node forms a hierarchical structure with each HD Node having a * private key and the ability to derive child HD Nodes, defined by * a path indicating the index of each child. */ export class HDNodeWallet extends BaseWallet { /** * The compressed public key. + * @type {string} */ readonly publicKey!: string; /** * The fingerprint. * - * A fingerprint allows quick qay to detect parent and child nodes, + * A fingerprint allows a quick way to detect parent and child nodes, * but developers should be prepared to deal with collisions as it * is only 4 bytes. + * @type {string} */ readonly fingerprint!: string; /** * The parent fingerprint. + * @type {string} */ readonly parentFingerprint!: string; @@ -143,39 +141,53 @@ export class HDNodeWallet extends BaseWallet { * The mnemonic used to create this HD Node, if available. * * Sources such as extended keys do not encode the mnemonic, in - * which case this will be ``null``. + * which case this will be `null`. + * @type {null | Mnemonic} */ readonly mnemonic!: null | Mnemonic; /** * The chaincode, which is effectively a public key used * to derive children. + * @type {string} */ readonly chainCode!: string; /** * The derivation path of this wallet. * - * Since extended keys do not provider full path details, this - * may be ``null``, if instantiated from a source that does not - * enocde it. + * Since extended keys do not provide full path details, this + * may be `null`, if instantiated from a source that does not + * encode it. + * @type {null | string} */ readonly path!: null | string; /** - * The child index of this wallet. Values over ``2 *\* 31`` indicate + * The child index of this wallet. Values over `2 ** 31` indicate * the node is hardened. + * @type {number} */ readonly index!: number; /** * The depth of this wallet, which is the number of components * in its path. + * @type {number} */ readonly depth!: number; /** * @private + * @param {any} guard + * @param {SigningKey} signingKey + * @param {string} parentFingerprint + * @param {string} chainCode + * @param {null | string} path + * @param {number} index + * @param {number} depth + * @param {null | Mnemonic} mnemonic + * @param {null | Provider} provider */ constructor(guard: any, signingKey: SigningKey, parentFingerprint: string, chainCode: string, path: null | string, index: number, depth: number, mnemonic: null | Mnemonic, provider: null | Provider) { super(signingKey, provider); @@ -192,11 +204,20 @@ export class HDNodeWallet extends BaseWallet { defineProperties(this, { mnemonic }); } + /** + * Connects the wallet to a provider. + * @param {null | Provider} provider + * @returns {HDNodeWallet} + */ connect(provider: null | Provider): HDNodeWallet { return new HDNodeWallet(_guard, this.signingKey, this.parentFingerprint, this.chainCode, this.path, this.index, this.depth, this.mnemonic, provider); } + /** + * @private + * @returns {KeystoreAccount} + */ #account(): KeystoreAccount { const account: KeystoreAccount = { address: this.address, privateKey: this.privateKey }; const m = this.mnemonic; @@ -213,24 +234,29 @@ export class HDNodeWallet extends BaseWallet { /** * Resolves to a [JSON Keystore Wallet](json-wallets) encrypted with - * %%password%%. + * `password`. * - * If %%progressCallback%% is specified, it will receive periodic - * updates as the encryption process progreses. + * If `progressCallback` is specified, it will receive periodic + * updates as the encryption process progresses. + * @param {Uint8Array | string} password + * @param {ProgressCallback} [progressCallback] + * @returns {Promise} */ async encrypt(password: Uint8Array | string, progressCallback?: ProgressCallback): Promise { return await encryptKeystoreJson(this.#account(), password, { progressCallback }); } /** - * Returns a [JSON Keystore Wallet](json-wallets) encryped with - * %%password%%. + * Returns a [JSON Keystore Wallet](json-wallets) encrypted with + * `password`. * * It is preferred to use the [async version](encrypt) instead, - * which allows a [[ProgressCallback]] to keep the user informed. + * which allows a `ProgressCallback` to keep the user informed. * * This method will block the event loop (freezing all UI) until * it is complete, which may be a non-trivial duration. + * @param {Uint8Array | string} password + * @returns {string} */ encryptSync(password: Uint8Array | string): string { return encryptKeystoreJsonSync(this.#account(), password); @@ -239,8 +265,9 @@ export class HDNodeWallet extends BaseWallet { /** * The extended key. * - * This key will begin with the prefix ``xpriv`` and can be used to + * This key will begin with the prefix `xpriv` and can be used to * reconstruct this HD Node to derive its children. + * @returns {string} */ get extendedKey(): string { // We only support the mainnet values for now, but if anyone needs @@ -269,6 +296,7 @@ export class HDNodeWallet extends BaseWallet { /** * Returns true if this wallet has a path, providing a Type Guard * that the path is non-null. + * @returns {boolean} */ hasPath(): this is { path: string } { return (this.path != null); } @@ -278,6 +306,7 @@ export class HDNodeWallet extends BaseWallet { * * A neutered node has no private key, but can be used to derive * child addresses and other public data about the HD Node. + * @returns {HDNodeVoidWallet} */ neuter(): HDNodeVoidWallet { return new HDNodeVoidWallet(_guard, this.address, this.publicKey, @@ -286,7 +315,9 @@ export class HDNodeWallet extends BaseWallet { } /** - * Return the child for %%index%%. + * Return the child for `index`. + * @param {Numeric} _index + * @returns {HDNodeWallet} */ deriveChild(_index: Numeric): HDNodeWallet { const index = getNumber(_index, "index"); @@ -308,12 +339,20 @@ export class HDNodeWallet extends BaseWallet { } /** - * Return the HDNode for %%path%% from this node. + * Return the HDNode for `path` from this node. + * @param {string} path + * @returns {HDNodeWallet} */ derivePath(path: string): HDNodeWallet { return derivePath(this, path); } + /** + * @private + * @param {BytesLike} _seed + * @param {null | Mnemonic} mnemonic + * @returns {HDNodeWallet} + */ static #fromSeed(_seed: BytesLike, mnemonic: null | Mnemonic): HDNodeWallet { assertArgument(isBytesLike(_seed), "invalid seed", "seed", "[REDACTED]"); @@ -328,11 +367,13 @@ export class HDNodeWallet extends BaseWallet { } /** - * Creates a new HD Node from %%extendedKey%%. + * Creates a new HD Node from `extendedKey`. * - * If the %%extendedKey%% will either have a prefix or ``xpub`` or - * ``xpriv``, returning a neutered HD Node ([[HDNodeVoidWallet]]) - * or full HD Node ([[HDNodeWallet) respectively. + * If the `extendedKey` will either have a prefix or `xpub` or + * `xpriv`, returning a neutered HD Node ([[HDNodeVoidWallet]]) + * or full HD Node ([[HDNodeWallet]]) respectively. + * @param {string} extendedKey + * @returns {HDNodeWallet | HDNodeVoidWallet} */ static fromExtendedKey(extendedKey: string): HDNodeWallet | HDNodeVoidWallet { const bytes = toBeArray(decodeBase58(extendedKey)); // @TODO: redact @@ -367,6 +408,10 @@ export class HDNodeWallet extends BaseWallet { /** * Creates a new random HDNode. + * @param {string} path + * @param {string} [password] + * @param {Wordlist} [wordlist] + * @returns {HDNodeWallet} */ static createRandom(path: string, password?: string, wordlist?: Wordlist): HDNodeWallet { if (password == null) { password = ""; } @@ -376,14 +421,22 @@ export class HDNodeWallet extends BaseWallet { } /** - * Create an HD Node from %%mnemonic%%. + * Create an HD Node from `mnemonic`. + * @param {Mnemonic} mnemonic + * @param {string} path + * @returns {HDNodeWallet} */ static fromMnemonic(mnemonic: Mnemonic, path: string): HDNodeWallet { return HDNodeWallet.#fromSeed(mnemonic.computeSeed(), mnemonic).derivePath(path); } /** - * Creates an HD Node from a mnemonic %%phrase%%. + * Creates an HD Node from a mnemonic `phrase`. + * @param {string} phrase + * @param {string} path + * @param {string} [password] + * @param {Wordlist} [wordlist] + * @returns {HDNodeWallet} */ static fromPhrase(phrase: string, path: string, password?: string, wordlist?: Wordlist): HDNodeWallet { if (password == null) { password = ""; } @@ -393,7 +446,9 @@ export class HDNodeWallet extends BaseWallet { } /** - * Creates an HD Node from a %%seed%%. + * Creates an HD Node from a `seed`. + * @param {BytesLike} seed + * @returns {HDNodeWallet} */ static fromSeed(seed: BytesLike): HDNodeWallet { return HDNodeWallet.#fromSeed(seed, null); @@ -404,59 +459,75 @@ export class HDNodeWallet extends BaseWallet { * A **HDNodeVoidWallet** cannot sign, but provides access to * the children nodes of a [[link-bip-32]] HD wallet addresses. * - * The can be created by using an extended ``xpub`` key to + * They can be created by using an extended `xpub` key to * [[HDNodeWallet_fromExtendedKey]] or by - * [nuetering](HDNodeWallet-neuter) a [[HDNodeWallet]]. + * [neutering](HDNodeWallet-neuter) a [[HDNodeWallet]]. */ export class HDNodeVoidWallet extends VoidSigner { /** * The compressed public key. + * @type {string} */ readonly publicKey!: string; /** * The fingerprint. * - * A fingerprint allows quick qay to detect parent and child nodes, + * A fingerprint allows a quick way to detect parent and child nodes, * but developers should be prepared to deal with collisions as it * is only 4 bytes. + * @type {string} */ readonly fingerprint!: string; /** * The parent node fingerprint. + * @type {string} */ readonly parentFingerprint!: string; /** * The chaincode, which is effectively a public key used * to derive children. + * @type {string} */ readonly chainCode!: string; /** * The derivation path of this wallet. * - * Since extended keys do not provider full path details, this - * may be ``null``, if instantiated from a source that does not - * enocde it. + * Since extended keys do not provide full path details, this + * may be `null`, if instantiated from a source that does not + * encode it. + * @type {null | string} */ readonly path!: null | string; /** - * The child index of this wallet. Values over ``2 *\* 31`` indicate + * The child index of this wallet. Values over `2 ** 31` indicate * the node is hardened. + * @type {number} */ readonly index!: number; /** * The depth of this wallet, which is the number of components * in its path. + * @type {number} */ readonly depth!: number; /** * @private + * @param {any} guard + * @param {string} address + * @param {string} publicKey + * @param {string} parentFingerprint + * @param {string} chainCode + * @param {null | string} path + * @param {number} index + * @param {number} depth + * @param {null | Provider} provider */ constructor(guard: any, address: string, publicKey: string, parentFingerprint: string, chainCode: string, path: null | string, index: number, depth: number, provider: null | Provider) { super(address, provider); @@ -470,6 +541,11 @@ export class HDNodeVoidWallet extends VoidSigner { }); } + /** + * Connects the wallet to a provider. + * @param {null | Provider} provider + * @returns {HDNodeVoidWallet} + */ connect(provider: null | Provider): HDNodeVoidWallet { return new HDNodeVoidWallet(_guard, this.address, this.publicKey, this.parentFingerprint, this.chainCode, this.path, this.index, this.depth, provider); @@ -478,8 +554,9 @@ export class HDNodeVoidWallet extends VoidSigner { /** * The extended key. * - * This key will begin with the prefix ``xpub`` and can be used to + * This key will begin with the prefix `xpub` and can be used to * reconstruct this neutered key to derive its children addresses. + * @returns {string} */ get extendedKey(): string { // We only support the mainnet values for now, but if anyone needs @@ -503,11 +580,14 @@ export class HDNodeVoidWallet extends VoidSigner { /** * Returns true if this wallet has a path, providing a Type Guard * that the path is non-null. + * @returns {boolean} */ hasPath(): this is { path: string } { return (this.path != null); } /** - * Return the child for %%index%%. + * Return the child for `index`. + * @param {Numeric} _index + * @returns {HDNodeVoidWallet} */ deriveChild(_index: Numeric): HDNodeVoidWallet { const index = getNumber(_index, "index"); @@ -531,37 +611,24 @@ export class HDNodeVoidWallet extends VoidSigner { } /** - * Return the signer for %%path%% from this node. + * Return the signer for `path` from this node. + * @param {string} path + * @returns {HDNodeVoidWallet} */ derivePath(path: string): HDNodeVoidWallet { return derivePath(this, path); } } -/* -export class HDNodeWalletManager { - #root: HDNodeWallet; - - constructor(phrase: string, password?: null | string, path?: null | string, locale?: null | Wordlist) { - if (password == null) { password = ""; } - if (path == null) { path = "m/44'/60'/0'/0"; } - if (locale == null) { locale = LangEn.wordlist(); } - this.#root = HDNodeWallet.fromPhrase(phrase, password, path, locale); - } - - getSigner(index?: number): HDNodeWallet { - return this.#root.deriveChild((index == null) ? 0: index); - } -} -*/ - /** - * Returns the [[link-bip-32]] path for the account at %%index%%. + * Returns the [[link-bip-32]] path for the account at `index`. * * This is the pattern used by wallets like Ledger. * * There is also an [alternate pattern](getIndexedAccountPath) used by * some software. + * @param {Numeric} _index + * @returns {string} */ export function getAccountPath(_index: Numeric): string { const index = getNumber(_index, "index"); @@ -571,16 +638,18 @@ export function getAccountPath(_index: Numeric): string { /** * Returns the path using an alternative pattern for deriving accounts, - * at %%index%%. + * at `index`. * - * This derivation path uses the //index// component rather than the - * //account// component to derive sequential accounts. + * This derivation path uses the `index` component rather than the + * `account` component to derive sequential accounts. * * This is the pattern used by wallets like MetaMask. + * @param {Numeric} _index + * @returns {string} */ export function getIndexedAccountPath(_index: Numeric): string { const index = getNumber(_index, "index"); assertArgument(index >= 0 && index < HardenedBit, "invalid account index", "index", index); - return `m/44'/60'/0'/0/${ index}`; + return `m/44'/60'/0'/0/${ index }`; } diff --git a/src/wallet/hdwallet.ts b/src/wallet/hdwallet.ts index cd36309b..9ef1f920 100644 --- a/src/wallet/hdwallet.ts +++ b/src/wallet/hdwallet.ts @@ -3,11 +3,15 @@ import { Mnemonic } from './mnemonic.js'; import { LangEn } from '../wordlists/lang-en.js'; import type { Wordlist } from '../wordlists/index.js'; import { randomBytes } from '../crypto/index.js'; -import { getZoneForAddress, isQiAddress } from '../utils/index.js'; +import { getZoneForAddress } from '../utils/index.js'; +import { isQiAddress } from '../address/index.js'; import { Zone } from '../constants/index.js'; import { TransactionRequest, Provider, TransactionResponse } from '../providers/index.js'; import { AllowedCoinType } from '../constants/index.js'; +/** + * Interface representing information about a neutered address. + */ export interface NeuteredAddressInfo { pubKey: string; address: string; @@ -17,6 +21,9 @@ export interface NeuteredAddressInfo { zone: Zone; } +/** + * Interface representing the serialized state of an HD wallet. + */ export interface SerializedHDWallet { version: number; phrase: string; @@ -24,9 +31,14 @@ export interface SerializedHDWallet { addresses: Array; } -// Constant to represent the maximum attempt to derive an address +/** + * Constant to represent the maximum attempt to derive an address. + */ const MAX_ADDRESS_DERIVATION_ATTEMPTS = 10000000; +/** + * Abstract class representing a Hierarchical Deterministic (HD) wallet. + */ export abstract class AbstractHDWallet { protected static _version: number = 1; @@ -35,23 +47,38 @@ export abstract class AbstractHDWallet { // Map of addresses to address info protected _addresses: Map = new Map(); - // Root node of the HD wallet + /** + * Root node of the HD wallet. + */ protected _root: HDNodeWallet; protected provider?: Provider; /** - * @private + * @param {HDNodeWallet} root - The root node of the HD wallet. + * @param {Provider} [provider] - The provider for the HD wallet. */ protected constructor(root: HDNodeWallet, provider?: Provider) { this._root = root; this.provider = provider; } + /** + * Returns the parent path for a given coin type. + * + * @param {number} coinType - The coin type. + * + * @returns {string} The parent path. + */ protected static parentPath(coinType: number): string { return `m/44'/${coinType}'`; } + /** + * Returns the coin type of the wallet. + * + * @returns {AllowedCoinType} The coin type. + */ protected coinType(): AllowedCoinType { return (this.constructor as typeof AbstractHDWallet)._coinType!; } @@ -103,11 +130,30 @@ export abstract class AbstractHDWallet { ); } + /** + * Adds an address to the wallet. + * + * @param {number} account - The account number. + * @param {number} addressIndex - The address index. + * @param {boolean} [isChange=false] - Whether the address is a change address. Default is `false` + * + * @returns {NeuteredAddressInfo} The added address info. + */ public addAddress(account: number, addressIndex: number, isChange: boolean = false): NeuteredAddressInfo { return this._addAddress(this._addresses, account, addressIndex, isChange); } - // helper method to add an address to the wallet address map + /** + * Helper method to add an address to the wallet address map. + * + * @param {Map} addressMap - The address map. + * @param {number} account - The account number. + * @param {number} addressIndex - The address index. + * @param {boolean} [isChange=false] - Whether the address is a change address. Default is `false` + * + * @returns {NeuteredAddressInfo} The added address info. + * @throws {Error} If the address for the index already exists. + */ protected _addAddress( addressMap: Map, account: number, @@ -167,6 +213,13 @@ export abstract class AbstractHDWallet { return this.createAndStoreAddressInfo(addressNode, accountIndex, zone, isChange, addressMap); } + /** + * Gets the address info for a given address. + * + * @param {string} address - The address. + * + * @returns {NeuteredAddressInfo | null} The address info or null if not found. + */ public getAddressInfo(address: string): NeuteredAddressInfo | null { const addressInfo = this._addresses.get(address); if (!addressInfo) { @@ -175,17 +228,39 @@ export abstract class AbstractHDWallet { return addressInfo; } + /** + * Gets the addresses for a given account. + * + * @param {number} account - The account number. + * + * @returns {NeuteredAddressInfo[]} The addresses for the account. + */ public getAddressesForAccount(account: number): NeuteredAddressInfo[] { const addresses = this._addresses.values(); return Array.from(addresses).filter((addressInfo) => addressInfo.account === account); } + /** + * Gets the addresses for a given zone. + * + * @param {Zone} zone - The zone. + * + * @returns {NeuteredAddressInfo[]} The addresses for the zone. + */ public getAddressesForZone(zone: Zone): NeuteredAddressInfo[] { this.validateZone(zone); const addresses = this._addresses.values(); return Array.from(addresses).filter((addressInfo) => addressInfo.zone === zone); } + /** + * Creates an instance of the HD wallet. + * + * @param {new (root: HDNodeWallet) => T} this - The constructor of the HD wallet. + * @param {Mnemonic} mnemonic - The mnemonic. + * + * @returns {T} The created instance. + */ protected static createInstance( this: new (root: HDNodeWallet) => T, mnemonic: Mnemonic, @@ -195,10 +270,27 @@ export abstract class AbstractHDWallet { return new this(root); } + /** + * Creates an HD wallet from a mnemonic. + * + * @param {new (root: HDNodeWallet) => T} this - The constructor of the HD wallet. + * @param {Mnemonic} mnemonic - The mnemonic. + * + * @returns {T} The created instance. + */ static fromMnemonic(this: new (root: HDNodeWallet) => T, mnemonic: Mnemonic): T { return (this as any).createInstance(mnemonic); } + /** + * Creates a random HD wallet. + * + * @param {new (root: HDNodeWallet) => T} this - The constructor of the HD wallet. + * @param {string} [password] - The password. + * @param {Wordlist} [wordlist] - The wordlist. + * + * @returns {T} The created instance. + */ static createRandom( this: new (root: HDNodeWallet) => T, password?: string, @@ -214,6 +306,16 @@ export abstract class AbstractHDWallet { return (this as any).createInstance(mnemonic); } + /** + * Creates an HD wallet from a phrase. + * + * @param {new (root: HDNodeWallet) => T} this - The constructor of the HD wallet. + * @param {string} phrase - The phrase. + * @param {string} [password] - The password. + * @param {Wordlist} [wordlist] - The wordlist. + * + * @returns {T} The created instance. + */ static fromPhrase( this: new (root: HDNodeWallet) => T, phrase: string, @@ -230,14 +332,39 @@ export abstract class AbstractHDWallet { return (this as any).createInstance(mnemonic); } + /** + * Abstract method to sign a transaction. + * + * @param {TransactionRequest} tx - The transaction request. + * + * @returns {Promise} A promise that resolves to the signed transaction. + */ abstract signTransaction(tx: TransactionRequest): Promise; + /** + * Abstract method to send a transaction. + * + * @param {TransactionRequest} tx - The transaction request. + * + * @returns {Promise} A promise that resolves to the transaction response. + */ abstract sendTransaction(tx: TransactionRequest): Promise; + /** + * Connects the wallet to a provider. + * + * @param {Provider} provider - The provider. + */ public connect(provider: Provider): void { this.provider = provider; } + /** + * Validates the zone. + * + * @param {Zone} zone - The zone. + * @throws {Error} If the zone is invalid. + */ protected validateZone(zone: Zone): void { if (!Object.values(Zone).includes(zone)) { throw new Error(`Invalid zone: ${zone}`); @@ -253,7 +380,7 @@ export abstract class AbstractHDWallet { * * @param {string} addr - The address for which to derive the HD node. * - * @returns {HDNodeWallet} - The derived HD node wallet corresponding to the given address. + * @returns {HDNodeWallet} The derived HD node wallet corresponding to the given address. * @throws {Error} If the given address is not known to the wallet. * @throws {Error} If the account associated with the address is not found. */ diff --git a/src/wallet/json-keystore.ts b/src/wallet/json-keystore.ts index 979ea7e9..abf50f01 100644 --- a/src/wallet/json-keystore.ts +++ b/src/wallet/json-keystore.ts @@ -74,6 +74,15 @@ export function isKeystoreJson(json: string): boolean { return false; } +/** + * Decrypts the given ciphertext using the provided key and data. + * + * @param {any} data - The data containing encryption parameters. + * @param {Uint8Array} key - The key to use for decryption. + * @param {Uint8Array} ciphertext - The ciphertext to decrypt. + * + * @returns {string} The decrypted data as a hex string. + */ function decrypt(data: any, key: Uint8Array, ciphertext: Uint8Array): string { const cipher = spelunk(data, 'crypto.cipher:string'); if (cipher === 'aes-128-ctr') { @@ -87,6 +96,14 @@ function decrypt(data: any, key: Uint8Array, ciphertext: Uint8Array): string { }); } +/** + * Retrieves the account details from the given data and key. + * + * @param {any} data - The data containing account information. + * @param {string} _key - The key to use for decryption. + * + * @returns {KeystoreAccount} The decrypted account details. + */ function getAccount(data: any, _key: string): KeystoreAccount { const key = getBytes(_key); const ciphertext = spelunk(data, 'crypto.ciphertext:data!'); @@ -152,6 +169,13 @@ type KdfParams = algorithm: 'sha256' | 'sha512'; }; +/** + * Retrieves the key derivation function parameters from the given data. + * + * @param {any} data - The data containing KDF parameters. + * + * @returns {KdfParams} The key derivation function parameters. + */ function getDecryptKdfParams(data: any): KdfParams { const kdf = spelunk(data, 'crypto.kdf:string'); if (kdf && typeof kdf === 'string') { @@ -222,6 +246,13 @@ export function decryptKeystoreJsonSync(json: string, _password: string | Uint8A return getAccount(data, key); } +/** + * Pauses execution for the specified duration. + * + * @param {number} duration - The duration to stall in milliseconds. + * + * @returns {Promise} A promise that resolves after the specified duration. + */ function stall(duration: number): Promise { return new Promise((resolve) => { setTimeout(() => { @@ -276,6 +307,13 @@ export async function decryptKeystoreJson( return getAccount(data, key); } +/** + * Retrieves the key derivation function parameters for encryption. + * + * @param {EncryptOptions} options - The encryption options. + * + * @returns {ScryptParams} The key derivation function parameters. + */ function getEncryptKdfParams(options: EncryptOptions): ScryptParams { // Check/generate the salt const salt = options.salt != null ? getBytes(options.salt, 'options.salt') : randomBytes(32); @@ -317,6 +355,16 @@ function getEncryptKdfParams(options: EncryptOptions): ScryptParams { return { name: 'scrypt', dkLen: 32, salt, N, r, p }; } +/** + * Encrypts the keystore with the given key, KDF parameters, account, and options. + * + * @param {Uint8Array} key - The key to use for encryption. + * @param {ScryptParams} kdf - The key derivation function parameters. + * @param {KeystoreAccount} account - The account to encrypt. + * @param {EncryptOptions} options - The encryption options. + * + * @returns {any} The encrypted keystore data. + */ function _encryptKeystore(key: Uint8Array, kdf: ScryptParams, account: KeystoreAccount, options: EncryptOptions): any { const privateKey = getBytes(account.privateKey, 'privateKey'); diff --git a/src/wallet/mnemonic.ts b/src/wallet/mnemonic.ts index 9dce66ea..92be6c08 100644 --- a/src/wallet/mnemonic.ts +++ b/src/wallet/mnemonic.ts @@ -13,16 +13,33 @@ import { LangEn } from '../wordlists/lang-en.js'; import type { BytesLike } from '../utils/index.js'; import type { Wordlist } from '../wordlists/index.js'; -// Returns a byte with the MSB bits set +/** + * Returns a byte with the MSB bits set. + * + * @param {number} bits - The number of bits to set. + * @returns {number} The byte with the MSB bits set. + */ function getUpperMask(bits: number): number { return (((1 << bits) - 1) << (8 - bits)) & 0xff; } -// Returns a byte with the LSB bits set +/** + * Returns a byte with the LSB bits set. + * + * @param {number} bits - The number of bits to set. + * @returns {number} The byte with the LSB bits set. + */ function getLowerMask(bits: number): number { return ((1 << bits) - 1) & 0xff; } +/** + * Converts a mnemonic phrase to entropy. + * + * @param {string} mnemonic - The mnemonic phrase. + * @param {Wordlist} [wordlist] - The wordlist for the mnemonic. + * @returns {string} The entropy. + */ function mnemonicToEntropy(mnemonic: string, wordlist?: null | Wordlist): string { assertNormalize('NFKD'); @@ -70,6 +87,13 @@ function mnemonicToEntropy(mnemonic: string, wordlist?: null | Wordlist): string return hexlify(entropy.slice(0, entropyBits / 8)); } +/** + * Converts entropy to a mnemonic phrase. + * + * @param {Uint8Array} entropy - The entropy. + * @param {Wordlist} [wordlist] - The wordlist for the mnemonic. + * @returns {string} The mnemonic phrase. + */ function entropyToMnemonic(entropy: Uint8Array, wordlist?: null | Wordlist): string { assertArgument( entropy.length % 4 === 0 && entropy.length >= 16 && entropy.length <= 32, @@ -149,7 +173,11 @@ export class Mnemonic { readonly entropy!: string; /** - * @private + * @param {any} guard - The guard object. + * @param {string} entropy - The entropy. + * @param {string} phrase - The mnemonic phrase. + * @param {string} [password] - The password for the mnemonic. + * @param {Wordlist} [wordlist] - The wordlist for the mnemonic. */ constructor(guard: any, entropy: string, phrase: string, password?: null | string, wordlist?: null | Wordlist) { if (password == null) { @@ -164,6 +192,8 @@ export class Mnemonic { /** * Returns the seed for the mnemonic. + * + * @returns {string} The seed. */ computeSeed(): string { const salt = toUtf8Bytes('mnemonic' + this.password, 'NFKD'); @@ -178,7 +208,6 @@ export class Mnemonic { * @param {string} phrase - The mnemonic phrase. * @param {string} [password] - The password for the mnemonic. * @param {Wordlist} [wordlist] - The wordlist for the mnemonic. - * * @returns {Mnemonic} The new Mnemonic object. */ static fromPhrase(phrase: string, password?: null | string, wordlist?: null | Wordlist): Mnemonic { @@ -202,7 +231,6 @@ export class Mnemonic { * @param {BytesLike} _entropy - The entropy for the mnemonic. * @param {string} [password] - The password for the mnemonic. * @param {Wordlist} [wordlist] - The wordlist for the mnemonic. - * * @returns {Mnemonic} The new Mnemonic object. */ static fromEntropy(_entropy: BytesLike, password?: null | string, wordlist?: null | Wordlist): Mnemonic { @@ -222,7 +250,6 @@ export class Mnemonic { * * @param {BytesLike} _entropy - The entropy for the mnemonic. * @param {Wordlist} [wordlist] - The wordlist for the mnemonic. - * * @returns {string} The mnemonic phrase. */ static entropyToPhrase(_entropy: BytesLike, wordlist?: null | Wordlist): string { @@ -235,7 +262,6 @@ export class Mnemonic { * * @param {string} phrase - The mnemonic phrase. * @param {Wordlist} [wordlist] - The wordlist for the mnemonic. - * * @returns {string} The entropy. */ static phraseToEntropy(phrase: string, wordlist?: null | Wordlist): string { @@ -250,7 +276,6 @@ export class Mnemonic { * * @param {string} phrase - The mnemonic phrase. * @param {Wordlist} [wordlist] - The wordlist for the mnemonic. - * * @returns {boolean} True if the phrase is valid. * @throws {Error} If the phrase is invalid. */ diff --git a/src/wallet/qi-hdwallet.ts b/src/wallet/qi-hdwallet.ts index 9564b1aa..cd66d775 100644 --- a/src/wallet/qi-hdwallet.ts +++ b/src/wallet/qi-hdwallet.ts @@ -13,13 +13,28 @@ import { getZoneForAddress } from '../utils/index.js'; import { AllowedCoinType, Zone } from '../constants/index.js'; import { Mnemonic } from './mnemonic.js'; -type OutpointInfo = { +/** + * @property {Outpoint} outpoint - The outpoint object. + * @property {string} address - The address associated with the outpoint. + * @property {Zone} zone - The zone of the outpoint. + * @property {number} [account] - The account number (optional). + * @interface OutpointInfo + */ +interface OutpointInfo { outpoint: Outpoint; address: string; zone: Zone; account?: number; -}; +} +/** + * @extends SerializedHDWallet + * @property {OutpointInfo[]} outpoints - Array of outpoint information. + * @property {NeuteredAddressInfo[]} changeAddresses - Array of change addresses. + * @property {NeuteredAddressInfo[]} gapAddresses - Array of gap addresses. + * @property {NeuteredAddressInfo[]} gapChangeAddresses - Array of gap change addresses. + * @interface SerializedQiHDWallet + */ interface SerializedQiHDWallet extends SerializedHDWallet { outpoints: OutpointInfo[]; changeAddresses: NeuteredAddressInfo[]; @@ -27,24 +42,89 @@ interface SerializedQiHDWallet extends SerializedHDWallet { gapChangeAddresses: NeuteredAddressInfo[]; } +/** + * The Qi HD wallet is a BIP44-compliant hierarchical deterministic wallet used for managing a set of addresses in the + * Qi ledger. This is wallet implementation is the primary way to interact with the Qi UTXO ledger on the Quai network. + * + * The Qi HD wallet supports: + * + * - Adding accounts to the wallet heierchy + * - Generating addresses for a specific account in any {@link Zone} + * - Signing and sending transactions for any address in the wallet + * - Serializing the wallet to JSON and deserializing it back to a wallet instance. + * + * @category Wallet + * @example + * + * ```ts + * import { QiHDWallet, Zone } from 'quais'; + * + * const wallet = new QiHDWallet(); + * const cyrpus1Address = wallet.getNextAddress(0, Zone.Cyrpus1); // get the first address in the Cyrpus1 zone + * await wallet.sendTransaction({ txInputs: [...], txOutputs: [...] }); // send a transaction + * const serializedWallet = wallet.serialize(); // serialize current (account/address) state of the wallet + * . + * . + * . + * const deserializedWallet = QiHDWallet.deserialize(serializedWallet); // create a new wallet instance from the serialized data + * ``` + */ export class QiHDWallet extends AbstractHDWallet { + /** + * @private + * @type {number} + */ protected static _version: number = 1; + /** + * @private + * @type {number} + */ protected static _GAP_LIMIT: number = 20; + /** + * @private + * @type {AllowedCoinType} + */ protected static _coinType: AllowedCoinType = 969; - // Map of change addresses to address info + /** + * Map of change addresses to address info. + * + * @private + * @type {Map} + */ protected _changeAddresses: Map = new Map(); - // Array of gap addresses + /** + * Array of gap addresses. + * + * @private + * @type {NeuteredAddressInfo[]} + */ protected _gapChangeAddresses: NeuteredAddressInfo[] = []; - // Array of gap change addresses + /** + * Array of gap change addresses. + * + * @private + * @type {NeuteredAddressInfo[]} + */ protected _gapAddresses: NeuteredAddressInfo[] = []; + /** + * Array of outpoint information. + * + * @private + * @type {OutpointInfo[]} + */ protected _outpoints: OutpointInfo[] = []; + /** + * @private + * @param {HDNodeWallet} root - The root HDNodeWallet. + * @param {Provider} [provider] - The provider (optional). + */ private constructor(root: HDNodeWallet, provider?: Provider) { super(root, provider); } @@ -61,18 +141,30 @@ export class QiHDWallet extends AbstractHDWallet { return this._getNextAddress(account, zone, true, this._changeAddresses); } + /** + * Imports an array of outpoints. + * + * @param {OutpointInfo[]} outpoints - The outpoints to import. + */ public importOutpoints(outpoints: OutpointInfo[]): void { this.validateOutpointInfo(outpoints); this._outpoints.push(...outpoints); } + /** + * Gets the outpoints for the specified zone. + * + * @param {Zone} zone - The zone. + * + * @returns {OutpointInfo[]} The outpoints for the zone. + */ public getOutpoints(zone: Zone): OutpointInfo[] { this.validateZone(zone); return this._outpoints.filter((outpoint) => outpoint.zone === zone); } /** - * Signs a Qi transaction and returns the serialized transaction + * Signs a Qi transaction and returns the serialized transaction. * * @param {QiTransactionRequest} tx - The transaction to sign. * @@ -98,6 +190,14 @@ export class QiHDWallet extends AbstractHDWallet { return txobj.serialized; } + /** + * Sends a Qi transaction. + * + * @param {QiTransactionRequest} tx - The transaction to send. + * + * @returns {Promise} The transaction response. + * @throws {Error} If the provider is not set or if the transaction has no inputs. + */ public async sendTransaction(tx: QiTransactionRequest): Promise { if (!this.provider) { throw new Error('Provider is not set'); @@ -122,15 +222,30 @@ export class QiHDWallet extends AbstractHDWallet { return await this.provider.broadcastTransaction(shard, signedTx); } - // createSchnorrSignature returns a schnorr signature for the given message and private key + /** + * Returns a schnorr signature for the given message and private key. + * + * @private + * @param {TxInput} input - The transaction input. + * @param {Uint8Array} hash - The hash of the message. + * + * @returns {string} The schnorr signature. + */ private createSchnorrSignature(input: TxInput, hash: Uint8Array): string { const privKey = this.getPrivateKeyForTxInput(input); const signature = schnorr.sign(hash, getBytes(privKey)); return hexlify(signature); } - // createMuSigSignature returns a MuSig signature for the given message - // and private keys corresponding to the input addresses + /** + * Returns a MuSig signature for the given message and private keys corresponding to the input addresses. + * + * @private + * @param {QiTransaction} tx - The Qi transaction. + * @param {Uint8Array} hash - The hash of the message. + * + * @returns {string} The MuSig signature. + */ private createMuSigSignature(tx: QiTransaction, hash: Uint8Array): string { const musig = MuSigFactory(musigCrypto); @@ -319,7 +434,15 @@ export class QiHDWallet extends AbstractHDWallet { return addressesCount; } - // getOutpointsByAddress queries the network node for the outpoints of the specified address + /** + * Queries the network node for the outpoints of the specified address. + * + * @private + * @param {string} address - The address to query. + * + * @returns {Promise} The outpoints for the address. + * @throws {Error} If the query fails. + */ private async getOutpointsByAddress(address: string): Promise { try { const outpointsMap = await this.provider!.getOutpointsByAddress(address); @@ -332,18 +455,39 @@ export class QiHDWallet extends AbstractHDWallet { } } + /** + * Gets the change addresses for the specified zone. + * + * @param {Zone} zone - The zone. + * + * @returns {NeuteredAddressInfo[]} The change addresses for the zone. + */ public getChangeAddressesForZone(zone: Zone): NeuteredAddressInfo[] { this.validateZone(zone); const changeAddresses = this._changeAddresses.values(); return Array.from(changeAddresses).filter((addressInfo) => addressInfo.zone === zone); } + /** + * Gets the gap addresses for the specified zone. + * + * @param {Zone} zone - The zone. + * + * @returns {NeuteredAddressInfo[]} The gap addresses for the zone. + */ public getGapAddressesForZone(zone: Zone): NeuteredAddressInfo[] { this.validateZone(zone); const gapAddresses = this._gapAddresses.filter((addressInfo) => addressInfo.zone === zone); return gapAddresses; } + /** + * Gets the gap change addresses for the specified zone. + * + * @param {Zone} zone - The zone. + * + * @returns {NeuteredAddressInfo[]} The gap change addresses for the zone. + */ public getGapChangeAddressesForZone(zone: Zone): NeuteredAddressInfo[] { this.validateZone(zone); const gapChangeAddresses = this._gapChangeAddresses.filter((addressInfo) => addressInfo.zone === zone); @@ -430,9 +574,8 @@ export class QiHDWallet extends AbstractHDWallet { } /** - * Validates an array of OutpointInfo objects. - * - * This method checks the validity of each OutpointInfo object by performing the following validations: + * Validates an array of OutpointInfo objects. This method checks the validity of each OutpointInfo object by + * performing the following validations: * * - Validates the zone using the `validateZone` method. * - Checks if the address exists in the wallet. diff --git a/src/wallet/quai-hdwallet.ts b/src/wallet/quai-hdwallet.ts index 75a22541..c30c2447 100644 --- a/src/wallet/quai-hdwallet.ts +++ b/src/wallet/quai-hdwallet.ts @@ -7,15 +7,69 @@ import { SerializedHDWallet } from './hdwallet.js'; import { Mnemonic } from './mnemonic.js'; import { TypedDataDomain, TypedDataField } from '../hash/index.js'; +/** + * The Quai HD wallet is a BIP44-compliant hierarchical deterministic wallet used for managing a set of addresses in the + * Quai ledger. This is the easiest way to manage the interaction of managing accounts and addresses on the Quai + * network, however, if your use case requires a single address Quai address, you can use the {@link Wallet} class. + * + * The Quai HD wallet supports: + * + * - Adding accounts to the wallet heierchy + * - Generating addresses for a specific account in any {@link Zone} + * - Signing and sending transactions for any address in the wallet + * - Signing and verifying EIP1193 typed data for any address in the wallet. + * - Serializing the wallet to JSON and deserializing it back to a wallet instance. + * + * @category Wallet + * @example + * + * ```ts + * import { QuaiHDWallet, Zone } from 'quais'; + * + * const wallet = new QuaiHDWallet(); + * const cyrpus1Address = wallet.getNextAddress(0, Zone.Cyrpus1); // get the first address in the Cyrpus1 zone + * await wallet.sendTransaction({ from: address, to: '0x...', value: 100 }); // send a transaction + * const serializedWallet = wallet.serialize(); // serialize current (account/address) state of the wallet + * . + * . + * . + * const deserializedWallet = QuaiHDWallet.deserialize(serializedWallet); // create a new wallet instance from the serialized data + * ``` + */ export class QuaiHDWallet extends AbstractHDWallet { + /** + * The version of the wallet. + * + * @type {number} + * @static + */ protected static _version: number = 1; + /** + * The coin type for the wallet. + * + * @type {AllowedCoinType} + * @static + */ protected static _coinType: AllowedCoinType = 994; + /** + * Create a QuaiHDWallet instance. + * + * @param {HDNodeWallet} root - The root HD node wallet. + * @param {Provider} [provider] - The provider. + */ private constructor(root: HDNodeWallet, provider?: Provider) { super(root, provider); } + /** + * Sign a transaction. + * + * @param {QuaiTransactionRequest} tx - The transaction request. + * + * @returns {Promise} A promise that resolves to the signed transaction. + */ public async signTransaction(tx: QuaiTransactionRequest): Promise { const from = await resolveAddress(tx.from); const fromNode = this._getHDNodeForAddress(from); @@ -23,6 +77,14 @@ export class QuaiHDWallet extends AbstractHDWallet { return signedTx; } + /** + * Send a transaction. + * + * @param {QuaiTransactionRequest} tx - The transaction request. + * + * @returns {Promise} A promise that resolves to the transaction response. + * @throws {Error} If the provider is not set. + */ public async sendTransaction(tx: QuaiTransactionRequest): Promise { if (!this.provider) { throw new Error('Provider is not set'); @@ -33,6 +95,14 @@ export class QuaiHDWallet extends AbstractHDWallet { return await fromNodeConnected.sendTransaction(tx); } + /** + * Sign a message. + * + * @param {string} address - The address. + * @param {string | Uint8Array} message - The message to sign. + * + * @returns {Promise} A promise that resolves to the signed message. + */ public async signMessage(address: string, message: string | Uint8Array): Promise { const addrNode = this._getHDNodeForAddress(address); return await addrNode.signMessage(message); @@ -41,12 +111,6 @@ export class QuaiHDWallet extends AbstractHDWallet { /** * Deserializes the given serialized HD wallet data into an instance of QuaiHDWallet. * - * This method performs the following steps: - * - * - Validates the serialized wallet data. - * - Creates a new wallet instance using the mnemonic phrase and derivation path. - * - Imports the addresses into the wallet instance. - * * @async * @param {SerializedHDWallet} serialized - The serialized wallet data to be deserialized. * diff --git a/src/wallet/utils.ts b/src/wallet/utils.ts index b78e0df8..b7118308 100644 --- a/src/wallet/utils.ts +++ b/src/wallet/utils.ts @@ -1,6 +1,7 @@ /** - * @ignore + * @module wallet/utils */ + import { getBytesCopy, assertArgument, @@ -13,6 +14,11 @@ import { import { computeHmac, sha256 } from '../crypto/index.js'; import { encodeBase58, toUtf8Bytes } from '../encoding/index.js'; +/** + * Converts a hex string to a Uint8Array. If the string does not start with '0x', it adds it. + * @param {string} hexString - The hex string to convert. + * @returns {Uint8Array} The resulting byte array. + */ export function looseArrayify(hexString: string): Uint8Array { if (typeof hexString === 'string' && !hexString.startsWith('0x')) { hexString = '0x' + hexString; @@ -20,6 +26,11 @@ export function looseArrayify(hexString: string): Uint8Array { return getBytesCopy(hexString); } +/** + * Converts a password to a Uint8Array. If the password is a string, it converts it to UTF-8 bytes. + * @param {string|Uint8Array} password - The password to convert. + * @returns {Uint8Array} The resulting byte array. + */ export function getPassword(password: string | Uint8Array): Uint8Array { if (typeof password === 'string') { return toUtf8Bytes(password, 'NFKC'); @@ -27,6 +38,12 @@ export function getPassword(password: string | Uint8Array): Uint8Array { return getBytesCopy(password); } +/** + * Traverses an object based on a path and returns the value at that path. + * @param {any} object - The object to traverse. + * @param {string} _path - The path to traverse. + * @returns {T} The value at the specified path. + */ export function spelunk(object: any, _path: string): T { const match = _path.match(/^([a-z0-9$_.-]*)(:([a-z]+))?(!)?$/i); assertArgument(match != null, 'invalid path', 'path', _path); @@ -99,15 +116,24 @@ export function spelunk(object: any, _path: string): T { // HDNODEWallet and UTXO Wallet util methods -// "Bitcoin seed" +/** "Bitcoin seed" */ export const MasterSecret = new Uint8Array([66, 105, 116, 99, 111, 105, 110, 32, 115, 101, 101, 100]); +/** Hardened bit constant */ export const HardenedBit = 0x80000000; +/** Constant N used in cryptographic operations */ export const N = BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'); +/** Hexadecimal characters */ export const Nibbles = '0123456789abcdef'; +/** + * Pads a value with leading zeros to a specified length. + * @param {string|number} value - The value to pad. + * @param {number} length - The desired length. + * @returns {string} The padded value. + */ export function zpad(value: string | number, length: number): string { // Determine if the value is hexadecimal const isHex = typeof value === 'string' && value.startsWith('0x'); @@ -130,6 +156,11 @@ export function zpad(value: string | number, length: number): string { return result; } +/** + * Encodes a value using Base58Check encoding. + * @param {BytesLike} _value - The value to encode. + * @returns {string} The Base58Check encoded string. + */ export function encodeBase58Check(_value: BytesLike): string { const value = getBytes(_value); const check = dataSlice(sha256(sha256(value)), 0, 4); @@ -137,6 +168,14 @@ export function encodeBase58Check(_value: BytesLike): string { return encodeBase58(bytes); } +/** + * Serializes an index, chain code, public key, and private key into a pair of derived keys. + * @param {number} index - The index to serialize. + * @param {string} chainCode - The chain code. + * @param {string} publicKey - The public key. + * @param {null|string} privateKey - The private key. + * @returns {{IL: Uint8Array, IR: Uint8Array}} The derived keys. + */ export function ser_I( index: number, chainCode: string, @@ -164,41 +203,4 @@ export function ser_I( const I = getBytes(computeHmac('sha512', chainCode, data)); return { IL: I.slice(0, 32), IR: I.slice(32) }; -} - -export type HDNodeLike = { - coinType?: number; - depth: number; - deriveChild: (i: number) => T; - // setCoinType?: () => void -}; - -export function derivePath>(node: T, path: string): T { - const components = path.split('/'); - - assertArgument(components.length > 0 && (components[0] === 'm' || node.depth > 0), 'invalid path', 'path', path); - - if (components[0] === 'm') { - components.shift(); - } - - let result: T = node; - for (let i = 0; i < components.length; i++) { - const component = components[i]; - - if (component.match(/^[0-9]+'$/)) { - const index = parseInt(component.substring(0, component.length - 1)); - assertArgument(index < HardenedBit, 'invalid path index', `path[${i}]`, component); - result = result.deriveChild(HardenedBit + index); - } else if (component.match(/^[0-9]+$/)) { - const index = parseInt(component); - assertArgument(index < HardenedBit, 'invalid path index', `path[${i}]`, component); - result = result.deriveChild(index); - } else { - assertArgument(false, 'invalid path component', `path[${i}]`, component); - } - } - // Extract the coin type from the path and set it on the node - // if (result.setCoinType) result.setCoinType(); - return result; } \ No newline at end of file diff --git a/src/wallet/wallet.ts b/src/wallet/wallet.ts index 77fc8577..58651dd4 100644 --- a/src/wallet/wallet.ts +++ b/src/wallet/wallet.ts @@ -18,13 +18,16 @@ import type { KeystoreAccount } from "./json-keystore.js"; * * This class is generally the main entry point for developers that wish to use a private key directly, as it can create * instances from a large variety of common sources, including raw private key, - * [BIP-39](https://en.bitcoin.it/wiki/BIP_0039) mnemonics and encrypte JSON wallets. + * [BIP-39](https://en.bitcoin.it/wiki/BIP_0039) mnemonics and encrypted JSON wallets. * * @category Wallet */ export class Wallet extends BaseWallet { /** * Create a new wallet for the private `key`, optionally connected to `provider`. + * + * @param {string | SigningKey} key - The private key. + * @param {null | Provider} [provider] - The provider to connect to. */ constructor(key: string | SigningKey, provider?: null | Provider) { if (typeof key === 'string' && !key.startsWith('0x')) { @@ -35,6 +38,12 @@ export class Wallet extends BaseWallet { super(signingKey, provider); } + /** + * Connects the wallet to a provider. + * + * @param {null | Provider} provider - The provider to connect to. + * @returns {Wallet} The connected wallet. + */ connect(provider: null | Provider): Wallet { return new Wallet(this.signingKey!, provider); } @@ -42,11 +51,10 @@ export class Wallet extends BaseWallet { /** * Resolves to a [JSON Keystore Wallet](json-wallets) encrypted with `password`. * - * If `progressCallback` is specified, it will receive periodic updates as the encryption process progreses. + * If `progressCallback` is specified, it will receive periodic updates as the encryption process progresses. * * @param {Uint8Array | string} password - The password to encrypt the wallet with. * @param {ProgressCallback} [progressCallback] - An optional callback to keep the user informed. - * * @returns {Promise} The encrypted JSON wallet. */ async encrypt(password: Uint8Array | string, progressCallback?: ProgressCallback): Promise { @@ -55,7 +63,7 @@ export class Wallet extends BaseWallet { } /** - * Returns a [JSON Keystore Wallet](json-wallets) encryped with `password`. + * Returns a [JSON Keystore Wallet](json-wallets) encrypted with `password`. * * It is preferred to use the [async version](encrypt) instead, which allows a * {@link ProgressCallback | **ProgressCallback**} to keep the user informed. @@ -64,7 +72,6 @@ export class Wallet extends BaseWallet { * duration. * * @param {Uint8Array | string} password - The password to encrypt the wallet with. - * * @returns {string} The encrypted JSON wallet. */ encryptSync(password: Uint8Array | string): string { @@ -72,6 +79,13 @@ export class Wallet extends BaseWallet { return encryptKeystoreJsonSync(account, password); } + /** + * Creates a wallet from a keystore account. + * + * @private + * @param {KeystoreAccount} account - The keystore account. + * @returns {Wallet} The wallet instance. + */ static #fromAccount(account: KeystoreAccount): Wallet { assertArgument(account, "invalid JSON wallet", "json", "[ REDACTED ]"); @@ -90,7 +104,6 @@ export class Wallet extends BaseWallet { * @param {string} json - The JSON data to decrypt. * @param {Uint8Array | string} password - The password to decrypt the JSON data. * @param {ProgressCallback} [progress] - An optional callback to keep the user informed. - * * @returns {Promise} The decrypted wallet. */ static async fromEncryptedJson(json: string, password: Uint8Array | string, progress?: ProgressCallback): Promise { @@ -100,7 +113,6 @@ export class Wallet extends BaseWallet { return Wallet.#fromAccount(account); } throw new Error("invalid JSON wallet"); - } /** @@ -111,7 +123,6 @@ export class Wallet extends BaseWallet { * * @param {string} json - The JSON data to decrypt. * @param {Uint8Array | string} password - The password to decrypt the JSON data. - * * @returns {QuaiHDWallet | Wallet} The decrypted wallet. */ static fromEncryptedJsonSync(json: string, password: Uint8Array | string): QuaiHDWallet | Wallet {