From efc0b82fb93002104b0fc3286dcd47f602086490 Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Tue, 19 May 2026 15:15:15 +0300 Subject: [PATCH 01/13] #115 Add documentation for classes --- src/InvalidJsonStructureError.ts | 4 ++ src/StateTransitionClient.ts | 17 ++++- src/api/AggregatorClient.ts | 28 ++++---- src/api/CertificationData.ts | 19 ++++++ src/api/CertificationRequest.ts | 3 + src/api/CertificationResponse.ts | 3 + src/api/IAggregatorClient.ts | 10 +-- src/api/InclusionCertificate.ts | 32 +++++++++ src/api/InclusionProof.ts | 3 + src/api/InclusionProofResponse.ts | 5 ++ src/api/StateId.ts | 35 ++++++++-- src/api/bft/InputRecord.ts | 3 + src/api/bft/RootTrustBase.ts | 6 ++ src/api/bft/ShardId.ts | 32 +++++++++ src/api/bft/ShardTreeCertificate.ts | 3 + src/api/bft/UnicityCertificate.ts | 16 +++++ src/api/bft/UnicitySeal.ts | 27 ++++++++ src/api/bft/UnicityTreeCertificate.ts | 3 + .../UnicityCertificateVerification.ts | 22 +++++++ .../UnicitySealHashMatchesWithRootHashRule.ts | 6 ++ ...itySealQuorumSignaturesVerificationRule.ts | 7 ++ src/crypto/ISignature.ts | 5 +- src/crypto/ISigningService.ts | 5 ++ src/crypto/MintSigningService.ts | 8 ++- src/crypto/hash/DataHash.ts | 28 ++++++++ src/crypto/hash/DataHasher.ts | 14 ++-- src/crypto/hash/DataHasherFactory.ts | 12 ++++ src/crypto/hash/HashAlgorithm.ts | 14 ++++ src/crypto/hash/IDataHasher.ts | 16 +++++ src/crypto/hash/IDataHasherFactory.ts | 11 +++- src/crypto/hash/NodeDataHasher.ts | 16 ++--- src/crypto/hash/SubtleCryptoDataHasher.ts | 11 +--- .../hash/UnsupportedHashAlgorithmError.ts | 4 ++ src/crypto/secp256k1/Signature.ts | 42 ++++++++++++ src/crypto/secp256k1/SigningService.ts | 61 +++++++++++++---- src/payment/IPaymentData.ts | 7 ++ src/payment/SplitAssetProof.ts | 16 +++++ src/payment/SplitMintJustification.ts | 26 ++++++++ src/payment/SplitMintJustificationVerifier.ts | 9 +++ src/payment/TokenSplit.ts | 35 ++++++++++ src/payment/asset/Asset.ts | 17 +++++ src/payment/asset/AssetId.ts | 25 +++++-- src/payment/asset/PaymentAssetCollection.ts | 33 ++++++++++ .../error/TokenAssetCountMismatchError.ts | 4 ++ src/payment/error/TokenAssetMissingError.ts | 4 ++ .../error/TokenAssetValueMismatchError.ts | 4 ++ src/predicate/EncodedPredicate.ts | 39 +++++++++++ src/predicate/IPredicate.ts | 14 ++++ src/predicate/IUnlockScript.ts | 8 +++ src/predicate/PredicateEngine.ts | 4 ++ src/predicate/builtin/BuiltInPredicateType.ts | 3 + src/predicate/builtin/BurnPredicate.ts | 35 ++++++++++ .../DefaultBuiltInPredicateVerifier.ts | 12 ++++ src/predicate/builtin/IBuiltInPredicate.ts | 7 ++ src/predicate/builtin/SignaturePredicate.ts | 44 +++++++++++++ .../builtin/SignaturePredicateUnlockScript.ts | 21 ++++++ src/predicate/builtin/UnicityIdPredicate.ts | 33 ++++++++++ .../builtin/UnicityIdPredicateUnlockScript.ts | 26 ++++++++ .../verification/IBuiltInPredicateVerifier.ts | 16 +++++ .../SignaturePredicateVerifier.ts | 8 +++ .../UnicityIdPredicateVerifier.ts | 11 ++++ .../verification/IPredicateVerifier.ts | 10 +++ .../verification/PredicateVerifierService.ts | 26 ++++++++ src/serialization/cbor/CborDeserializer.ts | 65 +++++++++++++++++++ src/serialization/cbor/CborError.ts | 4 ++ src/serialization/cbor/CborMap.ts | 16 ++++- src/serialization/cbor/CborMapEntry.ts | 10 +++ src/serialization/cbor/CborReader.ts | 39 +++++++++++ src/serialization/cbor/CborSerializer.ts | 61 +++++++++++++++++ src/serialization/cbor/ICborSerializable.ts | 6 ++ src/serialization/cbor/MajorType.ts | 5 ++ src/smt/LeafInBranchError.ts | 4 ++ src/smt/LeafOutOfBoundsError.ts | 4 ++ src/smt/PathVerificationResult.ts | 5 ++ src/smt/plain/FinalizedLeafBranch.ts | 12 ++++ src/smt/plain/FinalizedNodeBranch.ts | 9 +++ src/smt/plain/PendingLeafBranch.ts | 12 ++++ src/smt/plain/PendingNodeBranch.ts | 9 +++ src/smt/plain/SparseMerkleTreePath.ts | 17 +++++ src/smt/plain/SparseMerkleTreePathStep.ts | 20 ++++++ src/smt/radix/FinalizedLeafBranch.ts | 22 +++++++ src/smt/radix/FinalizedNodeBranch.ts | 16 +++++ src/smt/radix/PendingLeafBranch.ts | 18 +++++ src/smt/radix/PendingNodeBranch.ts | 12 ++++ src/smt/radix/SparseMerkleTree.ts | 7 ++ src/smt/radix/SparseMerkleTreeRootNode.ts | 6 ++ src/smt/sum/FinalizedLeafBranch.ts | 12 ++++ src/smt/sum/FinalizedNodeBranch.ts | 9 +++ src/smt/sum/PendingLeafBranch.ts | 12 ++++ src/smt/sum/PendingNodeBranch.ts | 9 +++ src/smt/sum/SparseMerkleSumTreePath.ts | 33 ++++++++++ src/smt/sum/SparseMerkleSumTreePathStep.ts | 36 ++++++++++ src/transaction/CertifiedMintTransaction.ts | 55 ++++++++++++++++ .../CertifiedTransferTransaction.ts | 47 ++++++++++++++ src/transaction/ITransaction.ts | 22 +++++++ src/transaction/MintTransaction.ts | 52 +++++++++++++++ src/transaction/MintTransactionState.ts | 4 +- src/transaction/Token.ts | 60 +++++++++++++++++ src/transaction/TokenId.ts | 37 +++++++++-- src/transaction/TokenType.ts | 27 ++++++-- src/transaction/TransferTransaction.ts | 49 ++++++++++++++ .../IMintJustificationVerifier.ts | 14 ++++ .../MintJustificationVerifierService.ts | 17 +++++ ...ertifiedMintTransactionVerificationRule.ts | 9 +++ ...fiedTransferTransactionVerificationRule.ts | 8 +++ ...nicityIdMintTransactionVerificationRule.ts | 12 ++-- .../rule/InclusionProofVerificationRule.ts | 9 +++ .../rule/ShardIdMatchesStateIdRule.ts | 11 ++++ .../CertifiedUnicityIdMintTransaction.ts | 58 +++++++++++++++++ src/unicity-id/UnicityId.ts | 23 +++++++ src/unicity-id/UnicityIdMintTransaction.ts | 49 ++++++++++++++ src/unicity-id/UnicityIdToken.ts | 41 ++++++++++++ src/util/BigintConverter.ts | 3 + src/util/BitString.ts | 35 +++++++--- src/util/HexConverter.ts | 18 +++-- src/util/InclusionProofUtils.ts | 17 +++++ src/util/TypedArrayUtils.ts | 16 +++++ src/verification/IVerificationContext.ts | 4 ++ src/verification/VerificationError.ts | 6 -- src/verification/VerificationResult.ts | 9 ++- 120 files changed, 2110 insertions(+), 120 deletions(-) diff --git a/src/InvalidJsonStructureError.ts b/src/InvalidJsonStructureError.ts index f5827a0..9c7d3d4 100644 --- a/src/InvalidJsonStructureError.ts +++ b/src/InvalidJsonStructureError.ts @@ -1,3 +1,7 @@ +/** + * Thrown when a value being deserialized from JSON does not match the + * expected shape (missing fields, wrong types, etc.). + */ export class InvalidJsonStructureError extends Error { public constructor() { super('Invalid JSON structure.'); diff --git a/src/StateTransitionClient.ts b/src/StateTransitionClient.ts index 3b6fcb9..02277c5 100644 --- a/src/StateTransitionClient.ts +++ b/src/StateTransitionClient.ts @@ -4,19 +4,30 @@ import { IAggregatorClient } from './api/IAggregatorClient.js'; import { InclusionProofResponse } from './api/InclusionProofResponse.js'; import { StateId } from './api/StateId.js'; +/** + * High-level facade over an {@link IAggregatorClient}. Provides the two + * operations a state-transition flow needs: fetching an inclusion proof for a + * known state ID and submitting a certification request. + */ export class StateTransitionClient { public constructor(private readonly client: IAggregatorClient) {} /** - * Retrieves the inclusion proof for a given transaction. + * Retrieve the inclusion proof for a given state id. * - * @param {StateId} stateId The state ID of inclusion proof to retrieve. - * @return inclusion proof response from the aggregator. + * @param {StateId} stateId State id whose inclusion proof to retrieve. + * @returns {Promise} Inclusion proof response from the aggregator. */ public getInclusionProof(stateId: StateId): Promise { return this.client.getInclusionProof(stateId); } + /** + * Submit a certification request derived from the given certification data. + * + * @param {CertificationData} certificationData Certification data to submit. + * @returns {Promise} Certification response from the aggregator. + */ public submitCertificationRequest(certificationData: CertificationData): Promise { return this.client.submitCertificationRequest(certificationData); } diff --git a/src/api/AggregatorClient.ts b/src/api/AggregatorClient.ts index 5739516..c1ebf22 100644 --- a/src/api/AggregatorClient.ts +++ b/src/api/AggregatorClient.ts @@ -26,7 +26,23 @@ export class AggregatorClient implements IAggregatorClient { this.transport = new JsonRpcHttpTransport(url); } - public async getBlockHeight(): Promise { + /** + * @inheritDoc + */ + public async getInclusionProof(stateId: StateId): Promise { + const data = { stateId: HexConverter.encode(stateId.data) }; + return InclusionProofResponse.fromCBOR( + HexConverter.decode((await this.transport.request('get_inclusion_proof.v2', data)) as string), + ); + } + + /** + * Query the aggregator's latest block number. + * + * @returns {Promise} Latest block number. + * @throws {Error} If the response does not contain a numeric `blockNumber`. + */ + public async getLatestBlockNumber(): Promise { const response = await this.transport.request('get_block_height', {}); if ( response && @@ -41,16 +57,6 @@ export class AggregatorClient implements IAggregatorClient { throw new Error('Invalid response format for block height'); } - /** - * @inheritDoc - */ - public async getInclusionProof(stateId: StateId): Promise { - const data = { stateId: HexConverter.encode(stateId.data) }; - return InclusionProofResponse.fromCBOR( - HexConverter.decode((await this.transport.request('get_inclusion_proof.v2', data)) as string), - ); - } - /** * @inheritDoc */ diff --git a/src/api/CertificationData.ts b/src/api/CertificationData.ts index c8692a8..d9f01f4 100644 --- a/src/api/CertificationData.ts +++ b/src/api/CertificationData.ts @@ -12,6 +12,9 @@ import { MintTransaction } from '../transaction/MintTransaction.js'; import { HexConverter } from '../util/HexConverter.js'; import { dedent } from '../util/StringUtils.js'; +/** + * Certification request data. + */ export class CertificationData { public static readonly CBOR_TAG = 39031n; private static readonly VERSION = 1n; @@ -39,6 +42,9 @@ export class CertificationData { return new Uint8Array(this._unlockScript); } + /** + * @returns {bigint} Wire-format version of this certification data. + */ public get version(): bigint { return CertificationData.VERSION; } @@ -69,6 +75,12 @@ export class CertificationData { ); } + /** + * Create CertificationData from a mint transaction. + * + * @param {MintTransaction} transaction Mint transaction to certify. + * @returns {Promise} Certification data. + */ public static async fromMintTransaction(transaction: MintTransaction): Promise { const signingService = await MintSigningService.create(transaction.tokenId); @@ -78,6 +90,13 @@ export class CertificationData { ); } + /** + * Create CertificationData from a transaction and its unlock script. + * + * @param {ITransaction} transaction Transaction to certify. + * @param {IUnlockScript} unlockScript Unlock script for the transaction. + * @returns {Promise} Certification data. + */ public static async fromTransaction( transaction: ITransaction, unlockScript: IUnlockScript, diff --git a/src/api/CertificationRequest.ts b/src/api/CertificationRequest.ts index 613301a..1edb124 100644 --- a/src/api/CertificationRequest.ts +++ b/src/api/CertificationRequest.ts @@ -19,6 +19,9 @@ export class CertificationRequest { public readonly certificationData: CertificationData, ) {} + /** + * @returns {bigint} Wire-format version of this request. + */ public get version(): bigint { return CertificationRequest.VERSION; } diff --git a/src/api/CertificationResponse.ts b/src/api/CertificationResponse.ts index 848694c..da59963 100644 --- a/src/api/CertificationResponse.ts +++ b/src/api/CertificationResponse.ts @@ -26,6 +26,9 @@ export enum CertificationStatus { INVALID_SHARD = 'INVALID_SHARD', } +/** + * JSON shape of a certification response. + */ export interface ICertificationResponseJson { readonly status: CertificationStatus; } diff --git a/src/api/IAggregatorClient.ts b/src/api/IAggregatorClient.ts index 85c197b..41f926e 100644 --- a/src/api/IAggregatorClient.ts +++ b/src/api/IAggregatorClient.ts @@ -8,18 +8,18 @@ import { StateId } from './StateId.js'; */ export interface IAggregatorClient { /** - * Retrieve an inclusion proof for the given request. + * Retrieve an inclusion proof for the given state id. * - * @param stateId State identifier to query - * @returns The inclusion proof returned by the aggregator + * @param {StateId} stateId State identifier to query. + * @returns {Promise} Inclusion proof response from the aggregator. */ getInclusionProof(stateId: StateId): Promise; /** * Submit a transaction commitment for inclusion in the ledger. * - * @param {CertificationData} certificationData The certification data to submit - * @returns Result status from the aggregator + * @param {CertificationData} certificationData Certification data to submit. + * @returns {Promise} Certification response from the aggregator. */ submitCertificationRequest(certificationData: CertificationData): Promise; } diff --git a/src/api/InclusionCertificate.ts b/src/api/InclusionCertificate.ts index cbe3297..4c6b683 100644 --- a/src/api/InclusionCertificate.ts +++ b/src/api/InclusionCertificate.ts @@ -10,6 +10,9 @@ import { HexConverter } from '../util/HexConverter.js'; import { dedent } from '../util/StringUtils.js'; import { areUint8ArraysEqual } from '../util/TypedArrayUtils.js'; +/** + * Inclusion certificate for a leaf in a sparse Merkle tree. + */ export class InclusionCertificate { private static readonly BITMAP_SIZE = 32; private static readonly MAX_DEPTH = 255; @@ -19,6 +22,14 @@ export class InclusionCertificate { private readonly siblings: DataHash[], ) {} + /** + * Create an InclusionCertificate from a sparse Merkle tree root and leaf key. + * + * @param {SparseMerkleTreeRootNode} root Root node of the tree. + * @param {Uint8Array} key Leaf key. + * @returns {InclusionCertificate} New certificate. + * @throws {Error} If the path cannot be constructed. + */ public static create(root: SparseMerkleTreeRootNode, key: Uint8Array): InclusionCertificate { let node: FinalizedBranch | SparseMerkleTreeRootNode | null = root; @@ -50,6 +61,13 @@ export class InclusionCertificate { throw new Error('Could not construct inclusion certificate: Invalid path'); } + /** + * Decode an InclusionCertificate from its byte encoding. + * + * @param {Uint8Array} bytes Encoded certificate. + * @returns {InclusionCertificate} Decoded certificate. + * @throws {Error} If the encoding is malformed. + */ public static decode(bytes: Uint8Array): InclusionCertificate { if (bytes.length < InclusionCertificate.BITMAP_SIZE) { throw new Error('Inclusion Certificate bitmap is invalid'); @@ -83,6 +101,9 @@ export class InclusionCertificate { return new InclusionCertificate(bytes.slice(0, InclusionCertificate.BITMAP_SIZE), siblings); } + /** + * @returns {Uint8Array} Encoded certificate bytes. + */ public encode(): Uint8Array { const bytes = new Uint8Array(this.bitmap.length + this.siblings.length * HashAlgorithm.SHA256.length); bytes.set(this.bitmap); @@ -96,6 +117,9 @@ export class InclusionCertificate { return bytes; } + /** + * @returns {string} String representation of the inclusion certificate. + */ public toString(): string { return dedent` Inclusion Certificate @@ -105,6 +129,14 @@ export class InclusionCertificate { ]`; } + /** + * Verify the certificate path against the expected root hash. + * + * @param {StateId} leafKey Leaf key. + * @param {DataHash} leafValue Leaf value hash. + * @param {DataHash} expectedRootHash Expected sparse Merkle tree root hash. + * @returns {Promise} True if the path is valid. + */ public async verify(leafKey: StateId, leafValue: DataHash, expectedRootHash: DataHash): Promise { const key = leafKey.data; const value = leafValue.data; diff --git a/src/api/InclusionProof.ts b/src/api/InclusionProof.ts index 9ed7a30..985edc0 100644 --- a/src/api/InclusionProof.ts +++ b/src/api/InclusionProof.ts @@ -25,6 +25,9 @@ export class InclusionProof { public readonly unicityCertificate: UnicityCertificate, ) {} + /** + * @returns {bigint} Wire-format version of this inclusion proof. + */ public get version(): bigint { return InclusionProof.VERSION; } diff --git a/src/api/InclusionProofResponse.ts b/src/api/InclusionProofResponse.ts index 3435f17..33ac174 100644 --- a/src/api/InclusionProofResponse.ts +++ b/src/api/InclusionProofResponse.ts @@ -33,6 +33,11 @@ export class InclusionProofResponse { ); } + /** + * Convert InclusionProofResponse to CBOR bytes. + * + * @returns {Uint8Array} CBOR bytes. + */ public toCBOR(): Uint8Array { return CborSerializer.encodeArray( CborSerializer.encodeUnsignedInteger(this.blockNumber), diff --git a/src/api/StateId.ts b/src/api/StateId.ts index 3c4ade1..402dfb5 100644 --- a/src/api/StateId.ts +++ b/src/api/StateId.ts @@ -14,22 +14,39 @@ import { HexConverter } from '../util/HexConverter.js'; export class StateId { private constructor(private readonly hash: DataHash) {} + /** + * @returns {Uint8Array} Underlying state-id hash bytes. + */ public get data(): Uint8Array { return this.hash.data; } + /** + * Create StateId from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {StateId} Decoded state id. + */ public static fromCBOR(bytes: Uint8Array): StateId { return new StateId(new DataHash(HashAlgorithm.SHA256, CborDeserializer.decodeByteString(bytes))); } /** - * Creates a StateId from CertificationData. - * @param certificationData certification data. + * Create StateId from certification data. + * + * @param {CertificationData} certificationData Certification data. + * @returns {Promise} Derived state id. */ public static fromCertificationData(certificationData: CertificationData): Promise { return StateId.create(certificationData.lockScript, certificationData.sourceStateHash); } + /** + * Create StateId from a transaction's lock script and source state hash. + * + * @param {ITransaction} transaction Transaction. + * @returns {Promise} Derived state id. + */ public static fromTransaction(transaction: ITransaction): Promise { return StateId.create(transaction.lockScript, transaction.sourceStateHash); } @@ -48,17 +65,27 @@ export class StateId { return new StateId(hash); } + /** + * Equality check against another state id. + * + * @param {StateId} id Other state id. + * @returns {boolean} True if the two state ids share the same hash. + */ public equals(id: StateId): boolean { return this.hash.equals(id.hash); } + /** + * Convert StateId to CBOR bytes. + * + * @returns {Uint8Array} CBOR bytes. + */ public toCBOR(): Uint8Array { return CborSerializer.encodeByteString(this.data); } /** - * Returns a string representation of the StateId. - * @returns The string representation. + * @returns {string} String representation of the state id. */ public toString(): string { return `StateId[${HexConverter.encode(this.data)}]`; diff --git a/src/api/bft/InputRecord.ts b/src/api/bft/InputRecord.ts index 0b5287c..bd7a510 100644 --- a/src/api/bft/InputRecord.ts +++ b/src/api/bft/InputRecord.ts @@ -64,6 +64,9 @@ export class InputRecord { return new Uint8Array(this._summaryValue); } + /** + * @returns {bigint} Wire-format version of this input record. + */ public get version(): bigint { return InputRecord.VERSION; } diff --git a/src/api/bft/RootTrustBase.ts b/src/api/bft/RootTrustBase.ts index 9bde77a..524b7b0 100644 --- a/src/api/bft/RootTrustBase.ts +++ b/src/api/bft/RootTrustBase.ts @@ -67,6 +67,9 @@ export class RootTrustBaseNodeInfo { } } +/** + * JSON shape of a {@link RootTrustBase}. + */ interface IRootTrustBaseJson { readonly changeRecordHash: string | null; readonly epoch: string; @@ -192,6 +195,9 @@ export class RootTrustBase { ); } + /** + * @returns {string} String representation of the trust base. + */ public toString(): string { return dedent` RootTrustBase: diff --git a/src/api/bft/ShardId.ts b/src/api/bft/ShardId.ts index 0541549..fdaac8f 100644 --- a/src/api/bft/ShardId.ts +++ b/src/api/bft/ShardId.ts @@ -1,15 +1,28 @@ import { CborError } from '../../serialization/cbor/CborError.js'; +/** + * Shard identifier. + */ export class ShardId { private constructor( private readonly _bits: Uint8Array, public readonly length: number, ) {} + /** + * @returns {Uint8Array} Copy of the bit-string bytes. + */ public get bits(): Uint8Array { return new Uint8Array(this._bits); } + /** + * Decode a ShardId from its byte encoding. + * + * @param {Uint8Array} data Encoded bytes. + * @returns {ShardId} Decoded shard id. + * @throws {CborError} If the encoding is invalid. + */ public static decode(data: Uint8Array): ShardId { let lastByte = data.at(-1); if (lastByte === undefined) { @@ -34,6 +47,9 @@ export class ShardId { throw new CborError('Invalid ShardId encoding: last byte doesnt contain end marker'); } + /** + * @returns {Uint8Array} Encoded shard id bytes. + */ public encode(): Uint8Array { const byteCount = Math.floor(this.length / 8); const bitCount = this.length % 8; @@ -48,6 +64,13 @@ export class ShardId { return result; } + /** + * Return the bit at the given index. + * + * @param {number} index Bit index, zero-based. + * @returns {number} The bit value (0 or 1). + * @throws {Error} If the index is out of bounds. + */ public getBit(index: number): number { if (index < 0 || index >= this.length) { throw new Error('ShardId bit index out of bounds'); @@ -55,6 +78,12 @@ export class ShardId { return (this._bits[Math.floor(index / 8)] >> (7 - (index % 8))) & 1; } + /** + * Check whether this shard id is a bit-prefix of the given data. + * + * @param {Uint8Array} data Bytes to test. + * @returns {boolean} True if this shard id is a prefix of `data`. + */ public isPrefixOf(data: Uint8Array): boolean { const fullBytes = Math.floor(this.length / 8); const remainingBits = this.length % 8; @@ -75,6 +104,9 @@ export class ShardId { return true; } + /** + * @returns {string} Binary string representation of the shard id. + */ public toString(): string { const fullBytes = Math.floor(this.length / 8); const remainingBits = this.length % 8; diff --git a/src/api/bft/ShardTreeCertificate.ts b/src/api/bft/ShardTreeCertificate.ts index de89161..5f4211d 100644 --- a/src/api/bft/ShardTreeCertificate.ts +++ b/src/api/bft/ShardTreeCertificate.ts @@ -33,6 +33,9 @@ export class ShardTreeCertificate { return this._siblingHashList.map((hash) => new Uint8Array(hash)); } + /** + * @returns {bigint} Wire-format version of this certificate. + */ public get version(): bigint { return ShardTreeCertificate.VERSION; } diff --git a/src/api/bft/UnicityCertificate.ts b/src/api/bft/UnicityCertificate.ts index 46e62a6..7fb036d 100644 --- a/src/api/bft/UnicityCertificate.ts +++ b/src/api/bft/UnicityCertificate.ts @@ -31,14 +31,23 @@ export class UnicityCertificate { this._shardConfigurationHash = new Uint8Array(_shardConfigurationHash); } + /** + * @returns {Uint8Array} Copy of the shard configuration hash. + */ public get shardConfigurationHash(): Uint8Array { return new Uint8Array(this._shardConfigurationHash); } + /** + * @returns {Uint8Array|null} Copy of the technical record hash, or `null` if absent. + */ public get technicalRecordHash(): Uint8Array | null { return this._technicalRecordHash ? new Uint8Array(this._technicalRecordHash) : null; } + /** + * @returns {bigint} Wire-format version of this certificate. + */ public get version(): bigint { return UnicityCertificate.VERSION; } @@ -112,6 +121,13 @@ export class UnicityCertificate { ); } + /** + * Create UnicityCertificate from JSON. + * + * @param {unknown} data Hex string of CBOR-encoded certificate. + * @returns {UnicityCertificate} Decoded certificate. + * @throws {InvalidJsonStructureError} If the input is not a string. + */ public static fromJSON(data: unknown): UnicityCertificate { if (!UnicityCertificate.isJSON(data)) { throw new InvalidJsonStructureError(); diff --git a/src/api/bft/UnicitySeal.ts b/src/api/bft/UnicitySeal.ts index 7f92177..f22763a 100644 --- a/src/api/bft/UnicitySeal.ts +++ b/src/api/bft/UnicitySeal.ts @@ -27,24 +27,48 @@ export class UnicitySeal { private readonly _signatures: Map | null, ) {} + /** + * @returns {Uint8Array} Copy of the seal hash bytes. + */ public get hash(): Uint8Array { return new Uint8Array(this._hash); } + /** + * @returns {Uint8Array|null} Copy of the previous-seal hash, or `null` if absent. + */ public get previousHash(): Uint8Array | null { return this._previousHash ? new Uint8Array(this._previousHash) : null; } + /** + * @returns {Map|null} Copy of the signer-to-signature map, or `null` if absent. + */ public get signatures(): Map | null { return this._signatures ? new Map(Array.from(this._signatures.entries()).map(([key, value]) => [key, new Uint8Array(value)])) : null; } + /** + * @returns {bigint} Wire-format version of this seal. + */ public get version(): bigint { return UnicitySeal.VERSION; } + /** + * Create a UnicitySeal and sign it with the given signing services. + * + * @param {bigint} networkId Network identifier. + * @param {bigint} rootChainRoundNumber Root-chain round number. + * @param {bigint} epoch Epoch number. + * @param {bigint} timestamp Timestamp. + * @param {Uint8Array|null} _previousHash Previous-seal hash, or `null` for the first seal. + * @param {Uint8Array} _hash Hash being sealed. + * @param {Map} signers Signing services keyed by signer id. + * @returns {Promise} Signed seal. + */ public static async create( networkId: bigint, rootChainRoundNumber: bigint, @@ -109,6 +133,9 @@ export class UnicitySeal { ); } + /** + * @returns {Promise} Hash of this seal, computed without the signatures. + */ public calculateHash(): Promise { return new DataHasher(HashAlgorithm.SHA256) .update( diff --git a/src/api/bft/UnicityTreeCertificate.ts b/src/api/bft/UnicityTreeCertificate.ts index fdca51f..0d9f28d 100644 --- a/src/api/bft/UnicityTreeCertificate.ts +++ b/src/api/bft/UnicityTreeCertificate.ts @@ -79,6 +79,9 @@ export class UnicityTreeCertificate { return this._steps.slice(); } + /** + * @returns {bigint} Wire-format version of this certificate. + */ public get version(): bigint { return UnicityTreeCertificate.VERSION; } diff --git a/src/api/bft/verification/UnicityCertificateVerification.ts b/src/api/bft/verification/UnicityCertificateVerification.ts index db18aab..0475494 100644 --- a/src/api/bft/verification/UnicityCertificateVerification.ts +++ b/src/api/bft/verification/UnicityCertificateVerification.ts @@ -5,15 +5,30 @@ import { VerificationStatus } from '../../../verification/VerificationStatus.js' import { InclusionProof } from '../../InclusionProof.js'; import { RootTrustBase } from '../RootTrustBase.js'; +/** + * Result of a {@link UnicityCertificateVerification} run. + */ class UnicityCertificateVerificationResult extends VerificationResult { private constructor(status: VerificationStatus, results: VerificationResult[]) { super('UnicityCertificateVerification', status, '', results); } + /** + * Build a failed verification result. + * + * @param {VerificationResult[]} results Child rule results. + * @returns {UnicityCertificateVerificationResult} Failed result. + */ public static fail(results: VerificationResult[]): UnicityCertificateVerificationResult { return new UnicityCertificateVerificationResult(VerificationStatus.FAIL, results); } + /** + * Build a successful verification result. + * + * @param {VerificationResult[]} results Child rule results. + * @returns {UnicityCertificateVerificationResult} Successful result. + */ public static ok(results: VerificationResult[]): UnicityCertificateVerificationResult { return new UnicityCertificateVerificationResult(VerificationStatus.OK, results); } @@ -23,6 +38,13 @@ class UnicityCertificateVerificationResult extends VerificationResult} Verification outcome. + */ public static async verify( trustBase: RootTrustBase, inclusionProof: InclusionProof, diff --git a/src/api/bft/verification/rule/UnicitySealHashMatchesWithRootHashRule.ts b/src/api/bft/verification/rule/UnicitySealHashMatchesWithRootHashRule.ts index 598bcb4..4cd3c29 100644 --- a/src/api/bft/verification/rule/UnicitySealHashMatchesWithRootHashRule.ts +++ b/src/api/bft/verification/rule/UnicitySealHashMatchesWithRootHashRule.ts @@ -12,6 +12,12 @@ import { UnicityCertificate } from '../../UnicityCertificate.js'; * Rule to verify that the UnicitySeal hash matches the root hash of the UnicityTreeCertificate. */ export class UnicitySealHashMatchesWithRootHashRule { + /** + * Verify the unicity seal hash matches the recomputed root hash. + * + * @param {UnicityCertificate} unicityCertificate Unicity certificate to verify. + * @returns {Promise>} Verification outcome. + */ public static async verify(unicityCertificate: UnicityCertificate): Promise> { const shardTreeCertificateRootHash = await UnicityCertificate.calculateShardTreeCertificateRootHash( unicityCertificate.inputRecord, diff --git a/src/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.ts b/src/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.ts index 39a0442..97af527 100644 --- a/src/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.ts +++ b/src/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.ts @@ -9,6 +9,13 @@ import { UnicitySeal } from '../../UnicitySeal.js'; * Rule to verify that the UnicitySeal contains valid quorum signatures. */ export class UnicitySealQuorumSignaturesVerificationRule { + /** + * Verify the unicity seal carries a quorum of valid root-node signatures. + * + * @param {RootTrustBase} trustBase Root trust base. + * @param {UnicitySeal} unicitySeal Seal to verify. + * @returns {Promise>} Verification outcome. + */ public static async verify( trustBase: RootTrustBase, unicitySeal: UnicitySeal, diff --git a/src/crypto/ISignature.ts b/src/crypto/ISignature.ts index dd4c640..b6a00e5 100644 --- a/src/crypto/ISignature.ts +++ b/src/crypto/ISignature.ts @@ -1,4 +1,7 @@ -// Convert signature to just bytes +/** + * Cryptographic signature with a named algorithm and a serializable byte form + * (CBOR and JSON). + */ export interface ISignature { readonly algorithm: string; readonly bytes: Uint8Array; diff --git a/src/crypto/ISigningService.ts b/src/crypto/ISigningService.ts index dafe973..10674b9 100644 --- a/src/crypto/ISigningService.ts +++ b/src/crypto/ISigningService.ts @@ -1,6 +1,11 @@ import type { DataHash } from './hash/DataHash.js'; import { ISignature } from './ISignature.js'; +/** + * Service that can sign a {@link DataHash} and verify signatures of type `T`. + * + * @typeParam T Signature type produced and consumed by this service. + */ export interface ISigningService { readonly algorithm: string; readonly publicKey: Uint8Array; diff --git a/src/crypto/MintSigningService.ts b/src/crypto/MintSigningService.ts index 8d4763e..f61de13 100644 --- a/src/crypto/MintSigningService.ts +++ b/src/crypto/MintSigningService.ts @@ -6,7 +6,9 @@ import { SigningService } from './secp256k1/SigningService.js'; import { CborSerializer } from '../serialization/cbor/CborSerializer.js'; /** - * Signing service for minting operations. + * Derives a deterministic {@link SigningService} for minting a token from the + * token's {@link TokenId} and a fixed universal minter secret. The resulting + * key is reproducible by anyone holding the same token id. */ export class MintSigningService { private static readonly MINTER_SECRET = HexConverter.decode('495f414d5f554e4956455253414c5f4d494e5445525f464f525f'); @@ -14,8 +16,8 @@ export class MintSigningService { /** * Create signing service for minting operations. * - * @param {TokenId} tokenId token identifier - * @return signing service + * @param {TokenId} tokenId Token identifier. + * @returns {Promise} Signing service derived from the token id. */ public static create(tokenId: TokenId): Promise { return new DataHasher(HashAlgorithm.SHA256) diff --git a/src/crypto/hash/DataHash.ts b/src/crypto/hash/DataHash.ts index 03c3860..85bb6ff 100644 --- a/src/crypto/hash/DataHash.ts +++ b/src/crypto/hash/DataHash.ts @@ -3,6 +3,11 @@ import { HashError } from './HashError.js'; import { HexConverter } from '../../util/HexConverter.js'; import { areUint8ArraysEqual } from '../../util/TypedArrayUtils.js'; +/** + * Immutable hash value paired with the {@link HashAlgorithm} that produced + * it. Carries both the raw `data` bytes and a 2-byte-prefixed `imprint` + * encoding suitable for storage and comparison across algorithms. + */ export class DataHash { private readonly _imprint: Uint8Array; @@ -20,6 +25,9 @@ export class DataHash { this._imprint.set(new Uint8Array(_data), 2); } + /** + * @returns {Uint8Array} Copy of the raw hash bytes. + */ public get data(): Uint8Array { return new Uint8Array(this._data); } @@ -28,11 +36,21 @@ export class DataHash { * Returns the imprint of the hash, which includes the algorithm identifier and the data. * The first two bytes represent the algorithm, followed by the data bytes. * NB! Do not use this for signing, use `data` instead. + * + * @returns {Uint8Array} Copy of the imprint bytes. */ public get imprint(): Uint8Array { return new Uint8Array(this._imprint); } + /** + * Reconstruct a DataHash from its imprint form (2-byte algorithm id + * followed by the hash bytes). + * + * @param {Uint8Array} imprint Imprint bytes; must be at least 3 bytes long. + * @returns {DataHash} Decoded hash. + * @throws {HashError} If the imprint is too short. + */ public static fromImprint(imprint: Uint8Array): DataHash { if (imprint.length < 3) { throw new HashError('Imprint must have 2 bytes of algorithm and at least 1 byte of data.'); @@ -42,10 +60,20 @@ export class DataHash { return new DataHash(HashAlgorithm.fromId(algorithm), imprint.subarray(2)); } + /** + * Equality check against another hash, comparing imprints so algorithm + * mismatches are detected. + * + * @param {DataHash} hash Other hash, or `undefined`. + * @returns {boolean} True if the hashes are equal. + */ public equals(hash?: DataHash): boolean { return areUint8ArraysEqual(this._imprint, hash?._imprint); } + /** + * @returns {string} `[algorithm]hex` representation of the hash. + */ public toString(): string { return `[${this.algorithm.toString()}]${HexConverter.encode(this._data)}`; } diff --git a/src/crypto/hash/DataHasher.ts b/src/crypto/hash/DataHasher.ts index 95819e0..14601f2 100644 --- a/src/crypto/hash/DataHasher.ts +++ b/src/crypto/hash/DataHasher.ts @@ -6,6 +6,9 @@ import { HashAlgorithm } from './HashAlgorithm.js'; import { IDataHasher } from './IDataHasher.js'; import { UnsupportedHashAlgorithmError } from './UnsupportedHashAlgorithmError.js'; +/** + * Internal streaming digest interface implemented by the noble hashers. + */ interface IMessageDigest { destroy(): void; @@ -28,10 +31,6 @@ export const Algorithm = { export class DataHasher implements IDataHasher { private _messageDigest: IMessageDigest; - /** - * Create DataHasher instance the hash algorithm - * @param {HashAlgorithm} algorithm - */ public constructor(public readonly algorithm: HashAlgorithm) { if (!Algorithm[algorithm.id]) { throw new UnsupportedHashAlgorithmError(algorithm); @@ -41,17 +40,14 @@ export class DataHasher implements IDataHasher { } /** - * Hashes the data and returns the DataHash - * @returns DataHash + * @inheritDoc */ public digest(): Promise { return Promise.resolve(new DataHash(this.algorithm, this._messageDigest.digest())); } /** - * Add data for hashing - * @param {Uint8Array} data byte array - * @returns {DataHasher} + * @inheritDoc */ public update(data: Uint8Array): this { this._messageDigest.update(data); diff --git a/src/crypto/hash/DataHasherFactory.ts b/src/crypto/hash/DataHasherFactory.ts index 32926dc..0e93fb7 100644 --- a/src/crypto/hash/DataHasherFactory.ts +++ b/src/crypto/hash/DataHasherFactory.ts @@ -2,12 +2,24 @@ import { HashAlgorithm } from './HashAlgorithm.js'; import { IDataHasher } from './IDataHasher.js'; import { IDataHasherFactory } from './IDataHasherFactory.js'; +/** + * Generic factory that produces fresh {@link IDataHasher} instances of type + * `T` for a fixed {@link HashAlgorithm}. Lets callers choose the hasher + * implementation (Node, Web Crypto, noble) without coupling to it. + * + * @typeParam T Concrete hasher type produced by this factory. + */ export class DataHasherFactory implements IDataHasherFactory { public constructor( public readonly algorithm: HashAlgorithm, private readonly _hasherConstructor: new (algorithm: HashAlgorithm) => T, ) {} + /** + * Create a new hasher instance configured with {@link algorithm}. + * + * @returns {T} Fresh hasher instance. + */ public create(): T { return new this._hasherConstructor(this.algorithm); } diff --git a/src/crypto/hash/HashAlgorithm.ts b/src/crypto/hash/HashAlgorithm.ts index 5532a53..bb0f9f1 100644 --- a/src/crypto/hash/HashAlgorithm.ts +++ b/src/crypto/hash/HashAlgorithm.ts @@ -1,5 +1,9 @@ import { HashError } from './HashError.js'; +/** + * Enumeration of supported hash algorithms. Each instance carries its + * protocol id, human-readable name, and output length in bytes. + */ export class HashAlgorithm { public static readonly RIPEMD160 = new HashAlgorithm(4, 'RIPEMD-160', 20); public static readonly SHA224 = new HashAlgorithm(1, 'SHA-224', 28); @@ -13,6 +17,13 @@ export class HashAlgorithm { public readonly length: number, ) {} + /** + * Look up the algorithm by its protocol id. + * + * @param {number} id Numeric algorithm id. + * @returns {HashAlgorithm} Matching algorithm. + * @throws {HashError} If no algorithm with the given id is registered. + */ public static fromId(id: number): HashAlgorithm { switch (id) { case HashAlgorithm.SHA256.id: @@ -30,6 +41,9 @@ export class HashAlgorithm { } } + /** + * @returns {string} Human-readable name of the algorithm (e.g. `SHA-256`). + */ public toString(): string { return this.name; } diff --git a/src/crypto/hash/IDataHasher.ts b/src/crypto/hash/IDataHasher.ts index 27e7e28..2e8c9ac 100644 --- a/src/crypto/hash/IDataHasher.ts +++ b/src/crypto/hash/IDataHasher.ts @@ -1,9 +1,25 @@ import { DataHash } from './DataHash.js'; import { HashAlgorithm } from './HashAlgorithm.js'; +/** + * Streaming hasher: feed bytes with {@link IDataHasher.update} and obtain the + * final {@link DataHash} with {@link IDataHasher.digest}. + */ export interface IDataHasher { readonly algorithm: HashAlgorithm; + /** + * Compute the final hash of all data fed so far. + * + * @returns {Promise} Final hash. + */ digest(): Promise; + + /** + * Feed more bytes into the hasher. + * + * @param {Uint8Array} data Bytes to add. + * @returns {this} This hasher for chaining. + */ update(data: Uint8Array): this; } diff --git a/src/crypto/hash/IDataHasherFactory.ts b/src/crypto/hash/IDataHasherFactory.ts index fdf5db4..5789a76 100644 --- a/src/crypto/hash/IDataHasherFactory.ts +++ b/src/crypto/hash/IDataHasherFactory.ts @@ -1,6 +1,12 @@ import { HashAlgorithm } from './HashAlgorithm.js'; import { IDataHasher } from './IDataHasher.js'; +/** + * Factory that produces fresh {@link IDataHasher} instances for a fixed + * {@link HashAlgorithm}. + * + * @typeParam T Concrete hasher type produced by this factory. + */ export interface IDataHasherFactory { /** * The hash algorithm used by the data hasher. @@ -8,8 +14,9 @@ export interface IDataHasherFactory { readonly algorithm: HashAlgorithm; /** - * Creates a new instance of the data hasher. - * @returns IDataHasher instance. + * Create a new hasher instance. + * + * @returns {T} Fresh hasher instance. */ create(): T; } diff --git a/src/crypto/hash/NodeDataHasher.ts b/src/crypto/hash/NodeDataHasher.ts index e0c9a3b..e618236 100644 --- a/src/crypto/hash/NodeDataHasher.ts +++ b/src/crypto/hash/NodeDataHasher.ts @@ -12,29 +12,27 @@ export const Algorithm = { [HashAlgorithm.SHA512.id]: 'SHA512', }; +/** + * {@link IDataHasher} implementation backed by Node's built-in `crypto` module. + * Suitable for server-side code; use {@link SubtleCryptoDataHasher} in the + * browser. + */ export class NodeDataHasher implements IDataHasher { private _hasher: Hash; - /** - * Create Node Hasher - * @param {string} algorithm - */ public constructor(public readonly algorithm: HashAlgorithm) { this._hasher = createHash(Algorithm[this.algorithm.id]); } /** - * Digest the final result - * @return {Promise} + * @inheritDoc */ public digest(): Promise { return Promise.resolve(new DataHash(this.algorithm, this._hasher.digest())); } /** - * Update the hasher content - * @param {Uint8Array} data byte array - * @return {IDataHasher} + * @inheritDoc */ public update(data: Uint8Array): this { this._hasher.update(data); diff --git a/src/crypto/hash/SubtleCryptoDataHasher.ts b/src/crypto/hash/SubtleCryptoDataHasher.ts index 00a2c87..20ee2c5 100644 --- a/src/crypto/hash/SubtleCryptoDataHasher.ts +++ b/src/crypto/hash/SubtleCryptoDataHasher.ts @@ -17,10 +17,6 @@ export const Algorithm = { export class SubtleCryptoDataHasher implements IDataHasher { private _data: Uint8Array; - /** - * Create DataHasher instance the hash algorithm - * @param {string} algorithm - */ public constructor(public readonly algorithm: HashAlgorithm) { if (!Algorithm[algorithm.id]) { throw new UnsupportedHashAlgorithmError(algorithm); @@ -30,8 +26,7 @@ export class SubtleCryptoDataHasher implements IDataHasher { } /** - * Create hashing Promise for getting result DataHash - * @returns Promise. + * @inheritDoc */ public async digest(): Promise { return new DataHash( @@ -41,9 +36,7 @@ export class SubtleCryptoDataHasher implements IDataHasher { } /** - * Add data for hashing - * @param {Uint8Array} data byte array - * @returns {SubtleCryptoDataHasher} + * @inheritDoc */ public update(data: Uint8Array): this { const previousData = this._data; diff --git a/src/crypto/hash/UnsupportedHashAlgorithmError.ts b/src/crypto/hash/UnsupportedHashAlgorithmError.ts index d60a2d7..da1a372 100644 --- a/src/crypto/hash/UnsupportedHashAlgorithmError.ts +++ b/src/crypto/hash/UnsupportedHashAlgorithmError.ts @@ -1,5 +1,9 @@ import { HashAlgorithm } from './HashAlgorithm.js'; +/** + * Thrown when a hasher implementation is asked to use a {@link HashAlgorithm} + * it does not support (e.g. Web Crypto cannot do RIPEMD-160). + */ export class UnsupportedHashAlgorithmError extends Error { public constructor(algorithm: HashAlgorithm) { super(`Unsupported hash algorithm: ${algorithm.name}`); diff --git a/src/crypto/secp256k1/Signature.ts b/src/crypto/secp256k1/Signature.ts index 6b9d73e..00f0ec6 100644 --- a/src/crypto/secp256k1/Signature.ts +++ b/src/crypto/secp256k1/Signature.ts @@ -3,6 +3,10 @@ import { CborSerializer } from '../../serialization/cbor/CborSerializer.js'; import { HexConverter } from '../../util/HexConverter.js'; import { ISignature } from '../ISignature.js'; +/** + * secp256k1 recoverable signature: 64 bytes of compact `(r, s)` plus a + * single recovery byte used to recover the signer's public key. + */ export class Signature implements ISignature { public readonly algorithm: string = 'secp256k1'; @@ -13,10 +17,20 @@ export class Signature implements ISignature { this._bytes = new Uint8Array(_bytes); } + /** + * @returns {Uint8Array} Copy of the 64-byte signature. + */ public get bytes(): Uint8Array { return new Uint8Array(this._bytes); } + /** + * Create Signature from its 65-byte encoding. + * + * @param {Uint8Array} bytes 64 signature bytes followed by recovery byte. + * @returns {Signature} Decoded signature. + * @throws {Error} If the input is not 65 bytes long. + */ public static decode(bytes: Uint8Array): Signature { if (bytes.length !== 65) { throw new Error('Signature must contain signature and recovery byte.'); @@ -25,26 +39,54 @@ export class Signature implements ISignature { return new Signature(bytes.slice(0, -1), bytes[bytes.length - 1]); } + /** + * Create Signature from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {Signature} Decoded signature. + */ public static fromCBOR(bytes: Uint8Array): Signature { return Signature.decode(CborDeserializer.decodeByteString(bytes)); } + /** + * Create Signature from JSON. + * + * @param {string} data Hex string of the encoded signature. + * @returns {Signature} Decoded signature. + */ public static fromJSON(data: string): Signature { return Signature.decode(HexConverter.decode(data)); } + /** + * @returns {Uint8Array} 65-byte concatenation of signature bytes and recovery byte. + */ public encode(): Uint8Array { return new Uint8Array([...this._bytes, this.recovery]); } + /** + * Convert Signature to CBOR bytes. + * + * @returns {Uint8Array} CBOR bytes. + */ public toCBOR(): Uint8Array { return CborSerializer.encodeByteString(this.encode()); } + /** + * Convert Signature to JSON. + * + * @returns {string} Hex string of the encoded signature. + */ public toJSON(): string { return HexConverter.encode(this.encode()); } + /** + * @returns {string} Hex string of the encoded signature. + */ public toString(): string { return `${HexConverter.encode(this.encode())}`; } diff --git a/src/crypto/secp256k1/SigningService.ts b/src/crypto/secp256k1/SigningService.ts index c53632d..63fd2db 100644 --- a/src/crypto/secp256k1/SigningService.ts +++ b/src/crypto/secp256k1/SigningService.ts @@ -5,44 +5,70 @@ import { Signature } from './Signature.js'; import { DataHash } from '../hash/DataHash.js'; /** - * Default signing service. + * Default secp256k1 signing service. Wraps a 32-byte private key and exposes + * the matching compressed public key, plus helpers for sign/verify and for + * recovering a public key from a signature. + * * @implements {ISigningService} */ export class SigningService implements ISigningService { private readonly _publicKey: Uint8Array; - /** - * Signing service constructor. - * @param {Uint8Array} privateKey private key bytes. - */ public constructor(private readonly privateKey: Uint8Array) { this.privateKey = new Uint8Array(privateKey); this._publicKey = secp256k1.getPublicKey(this.privateKey, true); } + /** + * @returns {string} Algorithm name (`secp256k1`). + */ public get algorithm(): string { return 'secp256k1'; } /** - * @see {ISigningService.publicKey} + * @returns {Uint8Array} Copy of the compressed public key. */ public get publicKey(): Uint8Array { return new Uint8Array(this._publicKey); } + /** + * Generate a signing service with a fresh random private key. + * + * @returns {SigningService} New signing service. + */ public static generate(): SigningService { return new SigningService(SigningService.generatePrivateKey()); } + /** + * Generate a fresh random secp256k1 private key. + * + * @returns {Uint8Array} 32-byte private key. + */ public static generatePrivateKey(): Uint8Array { return secp256k1.utils.randomSecretKey(); } + /** + * Check whether the given bytes form a valid compressed secp256k1 public key. + * + * @param {Uint8Array} publicKey Compressed public key bytes. + * @returns {boolean} True if valid. + */ public static isPublicKeyValid(publicKey: Uint8Array): boolean { return secp256k1.utils.isValidPublicKey(publicKey, true); } + /** + * Recover the public key from the signature's recovery byte and verify the + * signature against `hash`. + * + * @param {DataHash} hash Hash that was signed. + * @param {Signature} signature Recoverable signature. + * @returns {Promise} True if the signature verifies. + */ public static verifySignatureWithRecoveredPublicKey(hash: DataHash, signature: Signature): Promise { const publicKey = secp256k1.Signature.fromBytes( new Uint8Array([signature.recovery, ...signature.bytes]), @@ -54,17 +80,22 @@ export class SigningService implements ISigningService { } /** - * Verify secp256k1 signature hash. - * @param {Uint8Array} hash Hash. - * @param {Uint8Array} signature Signature. - * @param {Uint8Array} publicKey Public key. + * Verify secp256k1 signature against the given public key. + * + * @param {DataHash} hash Signed hash. + * @param {Uint8Array} signature Compact signature bytes. + * @param {Uint8Array} publicKey Compressed public key. + * @returns {Promise} True if the signature verifies. */ public static verifyWithPublicKey(hash: DataHash, signature: Uint8Array, publicKey: Uint8Array): Promise { return Promise.resolve(secp256k1.verify(signature, hash.data, publicKey, { format: 'compact', prehash: false })); } /** - * @see {ISigningService.sign} 32-byte hash. + * Sign a hash with this service's private key. + * + * @param {DataHash} hash Hash to sign. + * @returns {Promise} Recoverable signature. */ public sign(hash: DataHash): Promise { const signature = secp256k1.sign(hash.data, this.privateKey, { format: 'recovered', prehash: false }); @@ -72,9 +103,11 @@ export class SigningService implements ISigningService { } /** - * Verify secp256k1 signature hash. - * @param {Uint8Array} hash Hash. - * @param {Uint8Array} signature Signature. + * Verify a signature against this service's public key. + * + * @param {DataHash} hash Signed hash. + * @param {Signature} signature Recoverable signature. + * @returns {Promise} True if the signature verifies. */ public verify(hash: DataHash, signature: Signature): Promise { return SigningService.verifyWithPublicKey(hash, signature.bytes, this._publicKey); diff --git a/src/payment/IPaymentData.ts b/src/payment/IPaymentData.ts index 17d64ad..b8e67c6 100644 --- a/src/payment/IPaymentData.ts +++ b/src/payment/IPaymentData.ts @@ -1,7 +1,14 @@ import { PaymentAssetCollection } from './asset/PaymentAssetCollection.js'; +/** + * Payment payload carried by a transfer: a collection of assets plus their + * canonical encoding used in payment proofs. + */ export interface IPaymentData { readonly assets: PaymentAssetCollection; + /** + * @returns {Promise} Canonical encoded form of this payment data. + */ encode(): Promise; } diff --git a/src/payment/SplitAssetProof.ts b/src/payment/SplitAssetProof.ts index 890f210..5bb2170 100644 --- a/src/payment/SplitAssetProof.ts +++ b/src/payment/SplitAssetProof.ts @@ -4,6 +4,9 @@ import { CborSerializer } from '../serialization/cbor/CborSerializer.js'; import { SparseMerkleTreePath } from '../smt/plain/SparseMerkleTreePath.js'; import { SparseMerkleSumTreePath } from '../smt/sum/SparseMerkleSumTreePath.js'; +/** + * Inclusion proof for a single asset within a split payment. + */ export class SplitAssetProof { private constructor( public readonly assetId: AssetId, @@ -11,6 +14,14 @@ export class SplitAssetProof { public readonly assetTreePath: SparseMerkleSumTreePath, ) {} + /** + * Create a SplitAssetProof. + * + * @param {AssetId} assetId Asset id. + * @param {SparseMerkleTreePath} aggregationPath Aggregation tree path. + * @param {SparseMerkleSumTreePath} assetTreePath Asset tree path. + * @returns {SplitAssetProof} New proof. + */ public static create( assetId: AssetId, aggregationPath: SparseMerkleTreePath, @@ -35,6 +46,11 @@ export class SplitAssetProof { ); } + /** + * Convert SplitAssetProof to CBOR bytes. + * + * @returns {Uint8Array} CBOR bytes. + */ public toCBOR(): Uint8Array { return CborSerializer.encodeArray( this.assetId.toCBOR(), diff --git a/src/payment/SplitMintJustification.ts b/src/payment/SplitMintJustification.ts index 13e0591..2dcfc4d 100644 --- a/src/payment/SplitMintJustification.ts +++ b/src/payment/SplitMintJustification.ts @@ -4,6 +4,9 @@ import { CborError } from '../serialization/cbor/CborError.js'; import { CborSerializer } from '../serialization/cbor/CborSerializer.js'; import { Token } from '../transaction/Token.js'; +/** + * Mint justification proving a new token was minted as part of a split. + */ export class SplitMintJustification { public static readonly CBOR_TAG = 39044n; @@ -14,10 +17,21 @@ export class SplitMintJustification { this._proofs = _proofs.slice(); } + /** + * @returns {SplitAssetProof[]} Copy of the asset proofs. + */ public get proofs(): SplitAssetProof[] { return this._proofs.slice(); } + /** + * Create a SplitMintJustification. + * + * @param {Token} token Source token being split. + * @param {SplitAssetProof[]} proofs Asset proofs for the new token. + * @returns {SplitMintJustification} New justification. + * @throws {Error} If `proofs` is empty. + */ public static create(token: Token, proofs: SplitAssetProof[]): SplitMintJustification { if (proofs.length === 0) { throw new Error('proofs cannot be empty.'); @@ -26,6 +40,13 @@ export class SplitMintJustification { return new SplitMintJustification(token, proofs); } + /** + * Create SplitMintJustification from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {Promise} Decoded justification. + * @throws {CborError} On wrong tag. + */ public static async fromCBOR(bytes: Uint8Array): Promise { const tag = CborDeserializer.decodeTag(bytes); if (tag.tag !== SplitMintJustification.CBOR_TAG) { @@ -40,6 +61,11 @@ export class SplitMintJustification { ); } + /** + * Convert SplitMintJustification to CBOR bytes. + * + * @returns {Uint8Array} CBOR bytes. + */ public toCBOR(): Uint8Array { return CborSerializer.encodeTag( SplitMintJustification.CBOR_TAG, diff --git a/src/payment/SplitMintJustificationVerifier.ts b/src/payment/SplitMintJustificationVerifier.ts index 2093df8..8c4b048 100644 --- a/src/payment/SplitMintJustificationVerifier.ts +++ b/src/payment/SplitMintJustificationVerifier.ts @@ -12,6 +12,9 @@ import { areUint8ArraysEqual } from '../util/TypedArrayUtils.js'; import { VerificationResult } from '../verification/VerificationResult.js'; import { VerificationStatus } from '../verification/VerificationStatus.js'; +/** + * Verifier for {@link SplitMintJustification} mint justifications. + */ export class SplitMintJustificationVerifier implements IMintJustificationVerifier { public constructor( private readonly trustBase: RootTrustBase, @@ -19,10 +22,16 @@ export class SplitMintJustificationVerifier implements IMintJustificationVerifie private readonly decodePaymentData: (bytes: Uint8Array) => Promise, ) {} + /** + * @returns {bigint} CBOR tag this verifier handles. + */ public get tag(): bigint { return SplitMintJustification.CBOR_TAG; } + /** + * @inheritDoc + */ public async verify( transaction: CertifiedMintTransaction, mintJustificationVerifier: MintJustificationVerifierService, diff --git a/src/payment/TokenSplit.ts b/src/payment/TokenSplit.ts index 6012f76..f07a5f2 100644 --- a/src/payment/TokenSplit.ts +++ b/src/payment/TokenSplit.ts @@ -17,6 +17,9 @@ import { PaymentAssetCollection } from './asset/PaymentAssetCollection.js'; import { TokenAssetCountMismatchError } from './error/TokenAssetCountMismatchError.js'; import { BurnPredicate } from '../predicate/builtin/BurnPredicate.js'; +/** + * Per-token entry in a {@link ProofMap}: a token id and its asset proofs. + */ class ProofMapEntry { private constructor( public readonly tokenId: TokenId, @@ -25,18 +28,37 @@ class ProofMapEntry { this._proofs = _proofs.slice(); } + /** + * @returns {SplitAssetProof[]} Copy of the asset proofs. + */ public get proofs(): SplitAssetProof[] { return this._proofs.slice(); } + /** + * Create a ProofMapEntry. + * + * @param {TokenId} tokenId Token id. + * @param {SplitAssetProof[]} proofs Asset proofs. + * @returns {ProofMapEntry} New entry. + */ public static create(tokenId: TokenId, proofs: SplitAssetProof[]): ProofMapEntry { return new ProofMapEntry(tokenId, proofs); } } +/** + * Token-id-keyed map of asset proofs produced by a token split. + */ class ProofMap { private constructor(private readonly _proofs: Map) {} + /** + * Create a ProofMap from raw entries. + * + * @param {[TokenId, SplitAssetProof[]][]} data Token-id and proofs pairs. + * @returns {ProofMap} New map. + */ public static create(data: [TokenId, SplitAssetProof[]][]): ProofMap { return new ProofMap( new Map( @@ -45,15 +67,28 @@ class ProofMap { ); } + /** + * Look up the entry for a token id. + * + * @param {TokenId} id Token id. + * @returns {ProofMapEntry|null} Matching entry, or `null`. + */ public get(id: TokenId): ProofMapEntry | null { return this._proofs.get(HexConverter.encode(id.bytes)) ?? null; } + /** + * @returns {number} Number of entries in this map. + */ public size(): number { return this._proofs.size; } } +/** + * Result of splitting a token: a burn transaction for the original token plus + * per-token asset proofs for the new tokens. + */ interface ISplit { readonly burn: { readonly ownerPredicate: BurnPredicate; diff --git a/src/payment/asset/Asset.ts b/src/payment/asset/Asset.ts index d1cde29..6444f9c 100644 --- a/src/payment/asset/Asset.ts +++ b/src/payment/asset/Asset.ts @@ -3,22 +3,39 @@ import { CborDeserializer } from '../../serialization/cbor/CborDeserializer.js'; import { CborSerializer } from '../../serialization/cbor/CborSerializer.js'; import { BigintConverter } from '../../util/BigintConverter.js'; +/** + * Asset id paired with a non-negative value. + */ export class Asset { public constructor( public readonly id: AssetId, private readonly _value: bigint, ) {} + /** + * @returns {bigint} Value held by this asset. + */ public get value(): bigint { return this._value; } + /** + * Create Asset from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {Asset} Decoded asset. + */ public static fromCBOR(bytes: Uint8Array): Asset { const data = CborDeserializer.decodeArray(bytes, 2); return new Asset(AssetId.fromCBOR(data[0]), BigintConverter.decode(CborDeserializer.decodeByteString(data[1]))); } + /** + * Convert Asset to CBOR bytes. + * + * @returns {Uint8Array} CBOR bytes. + */ public toCBOR(): Uint8Array { return CborSerializer.encodeArray( this.id.toCBOR(), diff --git a/src/payment/asset/AssetId.ts b/src/payment/asset/AssetId.ts index e417875..bd37a9e 100644 --- a/src/payment/asset/AssetId.ts +++ b/src/payment/asset/AssetId.ts @@ -5,37 +5,48 @@ import { HexConverter } from '../../util/HexConverter.js'; /** Identifier for a asset. */ export class AssetId { - /** - * @param _bytes Raw byte representation - */ public constructor(private readonly _bytes: Uint8Array) { this._bytes = new Uint8Array(_bytes); } + /** + * @returns {Uint8Array} Copy of the asset id bytes. + */ public get bytes(): Uint8Array { return new Uint8Array(this._bytes); } /** - * Creates a AssetId from a byte array encoded in CBOR. - * @param bytes + * Create AssetId from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {AssetId} Decoded asset id. */ public static fromCBOR(bytes: Uint8Array): AssetId { return new AssetId(CborDeserializer.decodeByteString(bytes)); } /** - * Converts the AssetId to a bitstring representation. + * Convert the AssetId to a bit-string representation. + * + * @returns {BitString} Bit-string view of the asset id. */ public toBitString(): BitString { return BitString.fromBytes(this._bytes); } - /** CBOR serialization. */ + /** + * Convert AssetId to CBOR bytes. + * + * @returns {Uint8Array} CBOR bytes. + */ public toCBOR(): Uint8Array { return CborSerializer.encodeByteString(this._bytes); } + /** + * @returns {string} Hex string representation of the asset id. + */ public toString(): string { return HexConverter.encode(this._bytes); } diff --git a/src/payment/asset/PaymentAssetCollection.ts b/src/payment/asset/PaymentAssetCollection.ts index 30eb01b..85c536d 100644 --- a/src/payment/asset/PaymentAssetCollection.ts +++ b/src/payment/asset/PaymentAssetCollection.ts @@ -4,9 +4,19 @@ import { CborDeserializer } from '../../serialization/cbor/CborDeserializer.js'; import { CborSerializer } from '../../serialization/cbor/CborSerializer.js'; import { HexConverter } from '../../util/HexConverter.js'; +/** + * Asset-id-keyed collection of {@link Asset} values used in a payment. + */ export class PaymentAssetCollection { private constructor(private readonly _assets: Map) {} + /** + * Create a PaymentAssetCollection. + * + * @param {...Asset} data Assets to include. + * @returns {PaymentAssetCollection} New collection. + * @throws {Error} If any asset id appears more than once. + */ public static create(...data: Asset[]): PaymentAssetCollection { const assets = new Map(); for (const asset of data) { @@ -20,6 +30,12 @@ export class PaymentAssetCollection { return new PaymentAssetCollection(assets); } + /** + * Create PaymentAssetCollection from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {PaymentAssetCollection} Decoded collection. + */ public static fromCBOR(bytes: Uint8Array): PaymentAssetCollection { const data = CborDeserializer.decodeArray(bytes); @@ -31,18 +47,35 @@ export class PaymentAssetCollection { return PaymentAssetCollection.create(...assets); } + /** + * Look up the asset with the given id. + * + * @param {AssetId} id Asset id. + * @returns {Asset|null} Matching asset, or `null`. + */ public get(id: AssetId): Asset | null { return this._assets.get(HexConverter.encode(id.bytes)) ?? null; } + /** + * @returns {number} Number of assets in this collection. + */ public size(): number { return this._assets.size; } + /** + * @returns {Asset[]} Assets in insertion order. + */ public toArray(): Asset[] { return Array.from(this._assets.values()); } + /** + * Convert PaymentAssetCollection to CBOR bytes. + * + * @returns {Uint8Array} CBOR bytes. + */ public toCBOR(): Uint8Array { return CborSerializer.encodeArray(...this.toArray().map((asset) => asset.toCBOR())); } diff --git a/src/payment/error/TokenAssetCountMismatchError.ts b/src/payment/error/TokenAssetCountMismatchError.ts index 8bfd6b6..c112055 100644 --- a/src/payment/error/TokenAssetCountMismatchError.ts +++ b/src/payment/error/TokenAssetCountMismatchError.ts @@ -1,3 +1,7 @@ +/** + * Thrown when a token split has a different number of assets than the + * source token's payment data. + */ export class TokenAssetCountMismatchError extends Error { public constructor() { super('Token and split tokens asset counts differ.'); diff --git a/src/payment/error/TokenAssetMissingError.ts b/src/payment/error/TokenAssetMissingError.ts index 8f7cbae..09d642f 100644 --- a/src/payment/error/TokenAssetMissingError.ts +++ b/src/payment/error/TokenAssetMissingError.ts @@ -1,5 +1,9 @@ import { AssetId } from '../asset/AssetId.js'; +/** + * Thrown when a token split references an asset id that does not exist in + * the source token's payment data. + */ export class TokenAssetMissingError extends Error { public constructor(assetId: AssetId) { super(`Token did not contain asset ${assetId.toString()}.`); diff --git a/src/payment/error/TokenAssetValueMismatchError.ts b/src/payment/error/TokenAssetValueMismatchError.ts index 4979f0c..611c2cd 100644 --- a/src/payment/error/TokenAssetValueMismatchError.ts +++ b/src/payment/error/TokenAssetValueMismatchError.ts @@ -1,5 +1,9 @@ import { AssetId } from '../asset/AssetId.js'; +/** + * Thrown when a token split's asset sum tree root value does not match the + * source token's recorded value for that asset. + */ export class TokenAssetValueMismatchError extends Error { public constructor(assetId: AssetId, value: bigint, splitValue: bigint) { super(`Token contained ${value} ${assetId.toString()} assets, but tree has ${splitValue}`); diff --git a/src/predicate/EncodedPredicate.ts b/src/predicate/EncodedPredicate.ts index c371265..46d1945 100644 --- a/src/predicate/EncodedPredicate.ts +++ b/src/predicate/EncodedPredicate.ts @@ -7,6 +7,11 @@ import { HexConverter } from '../util/HexConverter.js'; import { dedent } from '../util/StringUtils.js'; import { areUint8ArraysEqual } from '../util/TypedArrayUtils.js'; +/** + * Wire-form predicate: holds the engine id and opaque code/parameter bytes + * without re-decoding them. Used wherever predicates are stored or + * transmitted as CBOR. + */ export class EncodedPredicate implements IPredicate { public static readonly CBOR_TAG = 39032n; @@ -19,6 +24,13 @@ export class EncodedPredicate implements IPredicate { this._params = new Uint8Array(_params); } + /** + * Null-safe structural equality on engine, code, and parameters. + * + * @param {EncodedPredicate|null|undefined} a First predicate. + * @param {EncodedPredicate|null|undefined} b Second predicate. + * @returns {boolean} True if both are nullish, or if engines and bytes match. + */ public static equals(a: EncodedPredicate | null | undefined, b: EncodedPredicate | null | undefined): boolean { if (a == null || b == null) { return a == null && b == null; @@ -34,6 +46,13 @@ export class EncodedPredicate implements IPredicate { ); } + /** + * Create EncodedPredicate from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {EncodedPredicate} Decoded predicate. + * @throws {CborError} On wrong tag or unknown engine. + */ public static fromCBOR(bytes: Uint8Array): EncodedPredicate { const tag = CborDeserializer.decodeTag(bytes); if (tag.tag !== EncodedPredicate.CBOR_TAG) { @@ -53,18 +72,35 @@ export class EncodedPredicate implements IPredicate { ); } + /** + * Wrap any {@link IPredicate} into its encoded form. + * + * @param {IPredicate} predicate Predicate to wrap. + * @returns {EncodedPredicate} Encoded copy of the predicate. + */ public static fromPredicate(predicate: IPredicate): EncodedPredicate { return new EncodedPredicate(predicate.engine, predicate.encodeCode(), predicate.encodeParameters()); } + /** + * @inheritDoc + */ public encodeCode(): Uint8Array { return this._code.slice(); } + /** + * @inheritDoc + */ public encodeParameters(): Uint8Array { return this._params.slice(); } + /** + * Convert EncodedPredicate to CBOR bytes. + * + * @returns {Uint8Array} Tagged CBOR encoding of this predicate. + */ public toCBOR(): Uint8Array { return CborSerializer.encodeTag( EncodedPredicate.CBOR_TAG, @@ -76,6 +112,9 @@ export class EncodedPredicate implements IPredicate { ); } + /** + * @returns {string} String representation of the predicate. + */ public toString(): string { return dedent` EncodedPredicate: diff --git a/src/predicate/IPredicate.ts b/src/predicate/IPredicate.ts index b38368d..39d6566 100644 --- a/src/predicate/IPredicate.ts +++ b/src/predicate/IPredicate.ts @@ -1,9 +1,23 @@ import { PredicateEngine } from './PredicateEngine.js'; +/** + * Spending condition attached to a token state. Predicates split into a + * `code` part and a `parameters` part . + */ export interface IPredicate { + /** + * Predicate engine that owns this predicate's verifier. + */ get engine(): PredicateEngine; + /** + * @returns Canonical encoding of the predicate code. + */ encodeCode(): Uint8Array; + + /** + * @returns Canonical encoding of the predicate parameters. + */ encodeParameters(): Uint8Array; toString(): string; diff --git a/src/predicate/IUnlockScript.ts b/src/predicate/IUnlockScript.ts index 414c7fd..f6433db 100644 --- a/src/predicate/IUnlockScript.ts +++ b/src/predicate/IUnlockScript.ts @@ -1,3 +1,11 @@ +/** + * Witness data presented when spending a predicate-locked state. + * Concrete unlock scripts encode the proof a predicate verifier needs + * (e.g. a signature for {@link SignaturePredicate}). + */ export interface IUnlockScript { + /** + * @returns Canonical byte encoding of this unlock script. + */ encode(): Uint8Array; } diff --git a/src/predicate/PredicateEngine.ts b/src/predicate/PredicateEngine.ts index 9aee85c..3e92098 100644 --- a/src/predicate/PredicateEngine.ts +++ b/src/predicate/PredicateEngine.ts @@ -1,3 +1,7 @@ +/** + * Identifies which predicate execution environment a predicate belongs to. + * Each engine has its own verifier registry. + */ export enum PredicateEngine { BUILT_IN = 1, } diff --git a/src/predicate/builtin/BuiltInPredicateType.ts b/src/predicate/builtin/BuiltInPredicateType.ts index 74089e9..23228ee 100644 --- a/src/predicate/builtin/BuiltInPredicateType.ts +++ b/src/predicate/builtin/BuiltInPredicateType.ts @@ -1,3 +1,6 @@ +/** + * Predicate type ids for the built-in predicate engine. + */ export enum BuiltInPredicateType { Signature = 0x01, Burn = 0x02, diff --git a/src/predicate/builtin/BurnPredicate.ts b/src/predicate/builtin/BurnPredicate.ts index f543770..db56de4 100644 --- a/src/predicate/builtin/BurnPredicate.ts +++ b/src/predicate/builtin/BurnPredicate.ts @@ -7,27 +7,53 @@ import { PredicateEngine } from '../PredicateEngine.js'; import { IBuiltInPredicate } from './IBuiltInPredicate.js'; import { EncodedPredicate } from '../EncodedPredicate.js'; +/** + * Built-in predicate that permanently locks a state. The `reason` payload + * records why the token was burned and is never spendable. + */ export class BurnPredicate implements IBuiltInPredicate { private constructor(private readonly _reason: Uint8Array) { this._reason = new Uint8Array(_reason); } + /** + * @returns {PredicateEngine} Built-in predicate engine. + */ public get engine(): PredicateEngine { return PredicateEngine.BUILT_IN; } + /** + * @returns {Uint8Array} Copy of the burn reason bytes. + */ public get reason(): Uint8Array { return new Uint8Array(this._reason); } + /** + * @returns {BuiltInPredicateType} Burn predicate type id. + */ public get type(): BuiltInPredicateType { return BuiltInPredicateType.Burn; } + /** + * Create a BurnPredicate with the given reason payload. + * + * @param {Uint8Array} reason Reason bytes recorded with the burn. + * @returns {BurnPredicate} New predicate. + */ public static create(reason: Uint8Array): BurnPredicate { return new BurnPredicate(reason); } + /** + * Decode a BurnPredicate from an EncodedPredicate. + * + * @param {EncodedPredicate} predicate Encoded predicate. + * @returns {BurnPredicate} Decoded predicate. + * @throws {Error} If the engine or type does not match. + */ public static fromPredicate(predicate: EncodedPredicate): BurnPredicate { if (predicate.engine !== PredicateEngine.BUILT_IN) { throw new Error(`Predicate engine must be ${PredicateEngine.BUILT_IN}.`); @@ -41,14 +67,23 @@ export class BurnPredicate implements IBuiltInPredicate { return new BurnPredicate(predicate.encodeParameters()); } + /** + * @inheritDoc + */ public encodeCode(): Uint8Array { return CborSerializer.encodeUnsignedInteger(this.type); } + /** + * @inheritDoc + */ public encodeParameters(): Uint8Array { return this.reason; } + /** + * @returns {string} String representation of the predicate. + */ public toString(): string { return dedent` BurnPredicate diff --git a/src/predicate/builtin/DefaultBuiltInPredicateVerifier.ts b/src/predicate/builtin/DefaultBuiltInPredicateVerifier.ts index 223ef90..2aafd17 100644 --- a/src/predicate/builtin/DefaultBuiltInPredicateVerifier.ts +++ b/src/predicate/builtin/DefaultBuiltInPredicateVerifier.ts @@ -8,6 +8,10 @@ import { IPredicateVerifier } from '../verification/IPredicateVerifier.js'; import { IBuiltInPredicateVerifier } from './verification/IBuiltInPredicateVerifier.js'; import { SignaturePredicateVerifier } from './verification/SignaturePredicateVerifier.js'; +/** + * Default {@link IPredicateVerifier} for the built-in engine. Uses + * {@link IBuiltInPredicateVerifier} by the predicate's type id to verify it. + */ export class DefaultBuiltInPredicateVerifier implements IPredicateVerifier { public readonly engine: PredicateEngine = PredicateEngine.BUILT_IN; @@ -27,10 +31,18 @@ export class DefaultBuiltInPredicateVerifier implements IPredicateVerifier { this.verifiers = result; } + /** + * Create a verifier preloaded with the default built-in predicate verifiers. + * + * @returns {DefaultBuiltInPredicateVerifier} New verifier. + */ public static create(): DefaultBuiltInPredicateVerifier { return new DefaultBuiltInPredicateVerifier([new SignaturePredicateVerifier()]); } + /** + * @inheritDoc + */ public verify( predicate: EncodedPredicate, sourceStateHash: DataHash, diff --git a/src/predicate/builtin/IBuiltInPredicate.ts b/src/predicate/builtin/IBuiltInPredicate.ts index e078c59..05e99cd 100644 --- a/src/predicate/builtin/IBuiltInPredicate.ts +++ b/src/predicate/builtin/IBuiltInPredicate.ts @@ -1,6 +1,13 @@ import { IPredicate } from '../IPredicate.js'; import { BuiltInPredicateType } from './BuiltInPredicateType.js'; +/** + * Predicate executed by the built-in engine, distinguished by its + * {@link BuiltInPredicateType}. + */ export interface IBuiltInPredicate extends IPredicate { + /** + * Built-in predicate type id. + */ get type(): BuiltInPredicateType; } diff --git a/src/predicate/builtin/SignaturePredicate.ts b/src/predicate/builtin/SignaturePredicate.ts index 584204a..d147b9d 100644 --- a/src/predicate/builtin/SignaturePredicate.ts +++ b/src/predicate/builtin/SignaturePredicate.ts @@ -8,23 +8,44 @@ import { BuiltInPredicateType } from './BuiltInPredicateType.js'; import { IBuiltInPredicate } from './IBuiltInPredicate.js'; import { EncodedPredicate } from '../EncodedPredicate.js'; +/** + * Built-in predicate that locks a state to a single secp256k1 public key. + * Spending requires a {@link SignaturePredicateUnlockScript} signed by the + * matching private key. + */ export class SignaturePredicate implements IBuiltInPredicate { private constructor(private readonly _publicKey: Uint8Array) { this._publicKey = new Uint8Array(_publicKey); } + /** + * @returns {PredicateEngine} Built-in predicate engine. + */ public get engine(): PredicateEngine { return PredicateEngine.BUILT_IN; } + /** + * @returns {Uint8Array} Copy of the compressed public key. + */ public get publicKey(): Uint8Array { return new Uint8Array(this._publicKey); } + /** + * @returns {BuiltInPredicateType} Signature predicate type id. + */ public get type(): BuiltInPredicateType { return BuiltInPredicateType.Signature; } + /** + * Create a SignaturePredicate from a compressed secp256k1 public key. + * + * @param {Uint8Array} publicKey Compressed public key bytes. + * @returns {SignaturePredicate} New predicate. + * @throws {Error} If `publicKey` is not a valid compressed point. + */ public static create(publicKey: Uint8Array): SignaturePredicate { if (!SigningService.isPublicKeyValid(publicKey)) { throw new Error('Invalid public key.'); @@ -33,6 +54,13 @@ export class SignaturePredicate implements IBuiltInPredicate { return new SignaturePredicate(publicKey); } + /** + * Decode a SignaturePredicate from an EncodedPredicate. + * + * @param {EncodedPredicate} predicate Encoded predicate. + * @returns {SignaturePredicate} Decoded predicate. + * @throws {Error} If the engine or type does not match. + */ public static fromPredicate(predicate: EncodedPredicate): SignaturePredicate { if (predicate.engine !== PredicateEngine.BUILT_IN) { throw new Error(`Predicate engine must be ${PredicateEngine.BUILT_IN}.`); @@ -46,17 +74,33 @@ export class SignaturePredicate implements IBuiltInPredicate { return new SignaturePredicate(predicate.encodeParameters()); } + /** + * Create a SignaturePredicate from the public key of a SigningService. + * + * @param {SigningService} signingService Service whose public key is used. + * @returns {SignaturePredicate} New predicate. + */ public static fromSigningService(signingService: SigningService): SignaturePredicate { return new SignaturePredicate(signingService.publicKey); } + + /** + * @inheritDoc + */ public encodeCode(): Uint8Array { return CborSerializer.encodeUnsignedInteger(this.type); } + /** + * @inheritDoc + */ public encodeParameters(): Uint8Array { return this.publicKey; } + /** + * @returns {string} String representation of the predicate. + */ public toString(): string { return dedent` SignaturePredicate diff --git a/src/predicate/builtin/SignaturePredicateUnlockScript.ts b/src/predicate/builtin/SignaturePredicateUnlockScript.ts index 067b781..83b4007 100644 --- a/src/predicate/builtin/SignaturePredicateUnlockScript.ts +++ b/src/predicate/builtin/SignaturePredicateUnlockScript.ts @@ -6,9 +6,21 @@ import { CborSerializer } from '../../serialization/cbor/CborSerializer.js'; import { ITransaction } from '../../transaction/ITransaction.js'; import { IUnlockScript } from '../IUnlockScript.js'; +/** + * Unlock script for {@link SignaturePredicate}: a secp256k1 signature over + * the (sourceStateHash, transactionHash) pair. + */ export class SignaturePredicateUnlockScript implements IUnlockScript { private constructor(public readonly signature: Signature) {} + /** + * Sign the transaction's source-state and transaction hashes to produce + * an unlock script. + * + * @param {ITransaction} transaction Transaction being unlocked. + * @param {SigningService} signingService Service holding the private key. + * @returns {Promise} Signed unlock script. + */ public static async create( transaction: ITransaction, signingService: SigningService, @@ -25,10 +37,19 @@ export class SignaturePredicateUnlockScript implements IUnlockScript { return new SignaturePredicateUnlockScript(await signingService.sign(hash)); } + /** + * Decode an unlock script from its signature encoding. + * + * @param {Uint8Array} bytes Encoded signature bytes. + * @returns {SignaturePredicateUnlockScript} Decoded unlock script. + */ public static decode(bytes: Uint8Array): SignaturePredicateUnlockScript { return new SignaturePredicateUnlockScript(Signature.decode(bytes)); } + /** + * @inheritDoc + */ public encode(): Uint8Array { return this.signature.encode(); } diff --git a/src/predicate/builtin/UnicityIdPredicate.ts b/src/predicate/builtin/UnicityIdPredicate.ts index 327e9c2..2023a99 100644 --- a/src/predicate/builtin/UnicityIdPredicate.ts +++ b/src/predicate/builtin/UnicityIdPredicate.ts @@ -7,21 +7,45 @@ import { IBuiltInPredicate } from './IBuiltInPredicate.js'; import { UnicityId } from '../../unicity-id/UnicityId.js'; import { EncodedPredicate } from '../EncodedPredicate.js'; +/** + * Built-in predicate that locks a state to a specific {@link UnicityId}. + * Spending requires presenting a matching unicity-id token in the unlock + * script. + */ export class UnicityIdPredicate implements IBuiltInPredicate { private constructor(public readonly unicityId: UnicityId) {} + /** + * @returns {PredicateEngine} Built-in predicate engine. + */ public get engine(): PredicateEngine { return PredicateEngine.BUILT_IN; } + /** + * @returns {BuiltInPredicateType} UnicityId predicate type id. + */ public get type(): BuiltInPredicateType { return BuiltInPredicateType.UnicityId; } + /** + * Create a UnicityIdPredicate for the given UnicityId. + * + * @param {UnicityId} unicityId Unicity id the state is locked to. + * @returns {UnicityIdPredicate} New predicate. + */ public static create(unicityId: UnicityId): UnicityIdPredicate { return new UnicityIdPredicate(unicityId); } + /** + * Decode a UnicityIdPredicate from an EncodedPredicate. + * + * @param {EncodedPredicate} predicate Encoded predicate. + * @returns {UnicityIdPredicate} Decoded predicate. + * @throws {Error} If the engine or type does not match. + */ public static fromPredicate(predicate: EncodedPredicate): UnicityIdPredicate { if (predicate.engine !== PredicateEngine.BUILT_IN) { throw new Error(`Predicate engine must be ${PredicateEngine.BUILT_IN}.`); @@ -35,14 +59,23 @@ export class UnicityIdPredicate implements IBuiltInPredicate { return new UnicityIdPredicate(UnicityId.fromCBOR(predicate.encodeParameters())); } + /** + * @inheritDoc + */ public encodeCode(): Uint8Array { return CborSerializer.encodeUnsignedInteger(this.type); } + /** + * @inheritDoc + */ public encodeParameters(): Uint8Array { return this.unicityId.toCBOR(); } + /** + * @returns {string} String representation of the predicate. + */ public toString(): string { return dedent` UnicityIdPredicate diff --git a/src/predicate/builtin/UnicityIdPredicateUnlockScript.ts b/src/predicate/builtin/UnicityIdPredicateUnlockScript.ts index 06854fb..9f59589 100644 --- a/src/predicate/builtin/UnicityIdPredicateUnlockScript.ts +++ b/src/predicate/builtin/UnicityIdPredicateUnlockScript.ts @@ -7,6 +7,10 @@ import { ITransaction } from '../../transaction/ITransaction.js'; import { UnicityIdToken } from '../../unicity-id/UnicityIdToken.js'; import { IUnlockScript } from '../IUnlockScript.js'; +/** + * Unlock script for {@link UnicityIdPredicate}: a signed unlock script + * bundled with the unicity-id token that authorizes the spend. + */ export class UnicityIdPredicateUnlockScript implements IUnlockScript { private constructor( private readonly _unlockScript: Uint8Array, @@ -15,10 +19,23 @@ export class UnicityIdPredicateUnlockScript implements IUnlockScript { this._unlockScript = new Uint8Array(_unlockScript); } + /** + * @returns {Uint8Array} Copy of the inner signature unlock script bytes. + */ public get unlockScript(): Uint8Array { return new Uint8Array(this._unlockScript); } + /** + * Build an unlock script that proves the spender owns `token`, the + * unicity-id token bound to the predicate. + * + * @param {UnicityIdToken} token Unicity-id token authorizing the spend. + * @param {ITransaction} transaction Transaction being unlocked. + * @param {SigningService} signingService Service holding the private key. + * @returns {Promise} Signed unlock script. + * @throws {Error} If the token's id does not match the predicate's UnicityId. + */ public static async create( token: UnicityIdToken, transaction: ITransaction, @@ -33,6 +50,12 @@ export class UnicityIdPredicateUnlockScript implements IUnlockScript { return new UnicityIdPredicateUnlockScript(unlockScript.encode(), token); } + /** + * Decode a UnicityIdPredicateUnlockScript from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {Promise} Decoded unlock script. + */ public static async decode(bytes: Uint8Array): Promise { const [unlockScriptBytes, tokenBytes] = CborDeserializer.decodeArray(bytes); return new UnicityIdPredicateUnlockScript( @@ -41,6 +64,9 @@ export class UnicityIdPredicateUnlockScript implements IUnlockScript { ); } + /** + * @inheritDoc + */ public encode(): Uint8Array { return CborSerializer.encodeArray(CborSerializer.encodeByteString(this.unlockScript), this.token.toCBOR()); } diff --git a/src/predicate/builtin/verification/IBuiltInPredicateVerifier.ts b/src/predicate/builtin/verification/IBuiltInPredicateVerifier.ts index 46e8873..e527e26 100644 --- a/src/predicate/builtin/verification/IBuiltInPredicateVerifier.ts +++ b/src/predicate/builtin/verification/IBuiltInPredicateVerifier.ts @@ -4,9 +4,25 @@ import { VerificationStatus } from '../../../verification/VerificationStatus.js' import { EncodedPredicate } from '../../EncodedPredicate.js'; import { BuiltInPredicateType } from '../BuiltInPredicateType.js'; +/** + * Verifier for a single {@link BuiltInPredicateType}. Plugged into + * {@link DefaultBuiltInPredicateVerifier} which dispatches by type id. + */ export interface IBuiltInPredicateVerifier { + /** + * Built-in predicate type this verifier handles. + */ get type(): BuiltInPredicateType; + /** + * Verify an unlock script against the predicate. + * + * @param {EncodedPredicate} predicate Predicate being unlocked. + * @param {DataHash} sourceStateHash Hash of the state being spent. + * @param {DataHash} transactionHash Hash of the spending transaction. + * @param {Uint8Array} unlockScript Witness bytes for the predicate. + * @returns {Promise>} Verification outcome. + */ verify( predicate: EncodedPredicate, sourceStateHash: DataHash, diff --git a/src/predicate/builtin/verification/SignaturePredicateVerifier.ts b/src/predicate/builtin/verification/SignaturePredicateVerifier.ts index 733a765..53562cc 100644 --- a/src/predicate/builtin/verification/SignaturePredicateVerifier.ts +++ b/src/predicate/builtin/verification/SignaturePredicateVerifier.ts @@ -11,9 +11,17 @@ import { SignaturePredicate } from '../SignaturePredicate.js'; import { IBuiltInPredicateVerifier } from './IBuiltInPredicateVerifier.js'; import { BuiltInPredicateType } from '../BuiltInPredicateType.js'; +/** + * Verifier for {@link SignaturePredicate}: recomputes the + * (sourceStateHash, transactionHash) digest and checks the secp256k1 + * signature in the unlock script against the predicate's public key. + */ export class SignaturePredicateVerifier implements IBuiltInPredicateVerifier { public readonly type = BuiltInPredicateType.Signature; + /** + * @inheritDoc + */ public async verify( encodedPredicate: EncodedPredicate, sourceStateHash: DataHash, diff --git a/src/predicate/builtin/verification/UnicityIdPredicateVerifier.ts b/src/predicate/builtin/verification/UnicityIdPredicateVerifier.ts index e93514a..97577fe 100644 --- a/src/predicate/builtin/verification/UnicityIdPredicateVerifier.ts +++ b/src/predicate/builtin/verification/UnicityIdPredicateVerifier.ts @@ -9,6 +9,11 @@ import { BuiltInPredicateType } from '../BuiltInPredicateType.js'; import { UnicityIdPredicate } from '../UnicityIdPredicate.js'; import { UnicityIdPredicateUnlockScript } from '../UnicityIdPredicateUnlockScript.js'; +/** + * Verifier for {@link UnicityIdPredicate}: confirms the unlock script + * carries a valid unicity-id token whose id matches the predicate, then + * recursively verifies the token's target predicate against the spend. + */ export class UnicityIdPredicateVerifier implements IBuiltInPredicateVerifier { public constructor( private readonly verifier: PredicateVerifierService, @@ -16,10 +21,16 @@ export class UnicityIdPredicateVerifier implements IBuiltInPredicateVerifier { private readonly issuerPublicKey: Uint8Array, ) {} + /** + * @returns {BuiltInPredicateType} UnicityId predicate type id. + */ public get type(): BuiltInPredicateType { return BuiltInPredicateType.UnicityId; } + /** + * @inheritDoc + */ public async verify( encodedPredicate: EncodedPredicate, sourceStateHash: DataHash, diff --git a/src/predicate/verification/IPredicateVerifier.ts b/src/predicate/verification/IPredicateVerifier.ts index a15b157..b021759 100644 --- a/src/predicate/verification/IPredicateVerifier.ts +++ b/src/predicate/verification/IPredicateVerifier.ts @@ -4,9 +4,19 @@ import { VerificationStatus } from '../../verification/VerificationStatus.js'; import { IPredicate } from '../IPredicate.js'; import { PredicateEngine } from '../PredicateEngine.js'; +/** + * Verifier for predicates of one {@link PredicateEngine}. Given a predicate + * and the unlock script for it, decides whether the spend is authorized. + */ export interface IPredicateVerifier { + /** + * Engine this verifier handles. + */ get engine(): PredicateEngine; + /** + * Verify an unlock script against the predicate. + */ verify( encodedPredicate: IPredicate, sourceStateHash: DataHash, diff --git a/src/predicate/verification/PredicateVerifierService.ts b/src/predicate/verification/PredicateVerifierService.ts index 16ae882..189bb0f 100644 --- a/src/predicate/verification/PredicateVerifierService.ts +++ b/src/predicate/verification/PredicateVerifierService.ts @@ -6,11 +6,20 @@ import { PredicateEngine } from '../PredicateEngine.js'; import { IPredicateVerifier } from './IPredicateVerifier.js'; import { EncodedPredicate } from '../EncodedPredicate.js'; +/** + * Registry that dispatches predicate verification to the right + * {@link IPredicateVerifier} based on the predicate's engine. + */ export class PredicateVerifierService { private readonly verifiers: Map = new Map(); private constructor() {} + /** + * Create verifier service with default verifiers. + * + * @returns {PredicateVerifierService} Service with the default verifiers registered. + */ public static create(): PredicateVerifierService { const verifier = new PredicateVerifierService(); verifier.addVerifier(DefaultBuiltInPredicateVerifier.create()); @@ -18,6 +27,13 @@ export class PredicateVerifierService { return verifier; } + /** + * Register a verifier for its declared engine. + * + * @param {IPredicateVerifier} verifier Verifier to register. + * @returns {PredicateVerifierService} This service for chaining. + * @throws {Error} If a verifier is already registered for the engine. + */ public addVerifier(verifier: IPredicateVerifier): this { if (this.verifiers.has(verifier.engine)) { throw new Error(`Found duplicate predicate verifier for engine ${verifier.engine}.`); @@ -28,6 +44,16 @@ export class PredicateVerifierService { return this; } + /** + * Verify given predicate with registered predicate verifiers. + * + * @param {EncodedPredicate} predicate Predicate being unlocked. + * @param {DataHash} sourceStateHash Hash of the state being spent. + * @param {DataHash} transactionHash Hash of the spending transaction. + * @param {Uint8Array} unlockScript Witness bytes for the predicate. + * @returns {Promise>} Verification outcome. + * @throws {Error} If no verifier is registered for the predicate's engine. + */ public verify( predicate: EncodedPredicate, sourceStateHash: DataHash, diff --git a/src/serialization/cbor/CborDeserializer.ts b/src/serialization/cbor/CborDeserializer.ts index dc82c74..fb08534 100644 --- a/src/serialization/cbor/CborDeserializer.ts +++ b/src/serialization/cbor/CborDeserializer.ts @@ -5,11 +5,24 @@ import { CborReader } from './CborReader.js'; import { MajorType } from './MajorType.js'; import { areUint8ArraysEqual } from '../../util/TypedArrayUtils.js'; +/** + * Static helpers that decode canonical CBOR bytes into TypeScript values. + * Every method consumes the entire input and asserts there are no trailing + * bytes. + */ export class CborDeserializer { private static readonly FALSE_CBOR = new Uint8Array([0xf4]); private static readonly NULL_CBOR = new Uint8Array([0xf6]); private static readonly TRUE_CBOR = new Uint8Array([0xf5]); + /** + * Decode a CBOR array. + * + * @param {Uint8Array} data CBOR bytes. + * @param {number|null} expectedLength If set, throws when the array has a different length. + * @returns {Uint8Array[]} CBOR bytes of each element. + * @throws {CborError} On wrong length or trailing bytes. + */ public static decodeArray(data: Uint8Array, expectedLength: number | null = null): Uint8Array[] { const reader = new CborReader(data); const length = reader.readLength(MajorType.ARRAY); @@ -31,6 +44,13 @@ export class CborDeserializer { return result; } + /** + * Decode a CBOR boolean. + * + * @param {Uint8Array} data CBOR bytes. + * @returns {boolean} Boolean value. + * @throws {CborError} If the input encodes anything other than `true` or `false`. + */ public static decodeBoolean(data: Uint8Array): boolean { const reader = new CborReader(data); const cbor = reader.readRawCbor(); @@ -47,6 +67,12 @@ export class CborDeserializer { throw new CborError('Type mismatch, expected boolean.'); } + /** + * Decode a CBOR byte string. + * + * @param {Uint8Array} data CBOR bytes. + * @returns {Uint8Array} Decoded payload bytes. + */ public static decodeByteString(data: Uint8Array): Uint8Array { const reader = new CborReader(data); const length = reader.readLength(MajorType.BYTE_STRING); @@ -59,6 +85,14 @@ export class CborDeserializer { return result; } + /** + * Decode a CBOR map, enforcing canonical key ordering and rejecting + * duplicate keys. + * + * @param {Uint8Array} data CBOR bytes. + * @returns {CborMapEntry[]} Decoded entries. + * @throws {CborError} On duplicate or out-of-order keys. + */ public static decodeMap(data: Uint8Array): CborMapEntry[] { const reader = new CborReader(data); const length = reader.readLength(MajorType.MAP); @@ -86,10 +120,23 @@ export class CborDeserializer { return result; } + /** + * Decode a CBOR negative integer. + * + * @returns {bigint} Decoded integer. + * @throws {CborError} Always; not yet implemented. + */ public static decodeNegativeInteger(): bigint { throw new CborError('Not implemented.'); } + /** + * Decode `data` with `decode`, or return `null` if the input is CBOR `null`. + * + * @param {Uint8Array} data CBOR bytes. + * @param {(data: Uint8Array) => T} decode Decoder for the inner value. + * @returns {T|null} Decoded value or `null`. + */ public static decodeNullable(data: Uint8Array, decode: (data: Uint8Array) => T): T | null { const reader = new CborReader(data); const cbor = reader.readRawCbor(); @@ -102,6 +149,12 @@ export class CborDeserializer { return decode(cbor); } + /** + * Decode a CBOR tag. + * + * @param {Uint8Array} data CBOR bytes. + * @returns {{data: Uint8Array, tag: bigint}} Tag number and CBOR bytes of the tagged value. + */ public static decodeTag(data: Uint8Array): { data: Uint8Array; tag: bigint } { const reader = new CborReader(data); const tag = reader.readLength(MajorType.TAG); @@ -111,6 +164,12 @@ export class CborDeserializer { return { data: cbor, tag }; } + /** + * Decode a CBOR text string. + * + * @param {Uint8Array} data CBOR bytes. + * @returns {string} UTF-8 decoded string. + */ public static decodeTextString(data: Uint8Array): string { const reader = new CborReader(data); const length = reader.readLength(MajorType.TEXT_STRING); @@ -123,6 +182,12 @@ export class CborDeserializer { return new TextDecoder().decode(result); } + /** + * Decode a CBOR unsigned integer. + * + * @param {Uint8Array} data CBOR bytes. + * @returns {bigint} Decoded integer. + */ public static decodeUnsignedInteger(data: Uint8Array): bigint { const reader = new CborReader(data); const result = reader.readLength(MajorType.UNSIGNED_INTEGER); diff --git a/src/serialization/cbor/CborError.ts b/src/serialization/cbor/CborError.ts index 2ce3dd0..2dd3ea6 100644 --- a/src/serialization/cbor/CborError.ts +++ b/src/serialization/cbor/CborError.ts @@ -1 +1,5 @@ +/** + * Thrown when CBOR encoding or decoding fails (malformed data, non-canonical + * encoding, type or length mismatches, etc.). + */ export class CborError extends Error {} diff --git a/src/serialization/cbor/CborMap.ts b/src/serialization/cbor/CborMap.ts index 828d4cc..c924458 100644 --- a/src/serialization/cbor/CborMap.ts +++ b/src/serialization/cbor/CborMap.ts @@ -2,6 +2,11 @@ import { CborError } from './CborError.js'; import { CborMapEntry } from './CborMapEntry.js'; import { areUint8ArraysEqual } from '../../util/TypedArrayUtils.js'; +/** + * Canonical CBOR map: entries are kept sorted by encoded key bytes and + * duplicate keys are rejected, matching the canonical ordering rules in + * RFC 8949 §4.2. + */ export class CborMap { private readonly _entries: CborMapEntry[]; @@ -18,14 +23,19 @@ export class CborMap { } /** - * Get CBOR element list. - * - * @return element list + * @returns {CborMapEntry[]} Copy of the entry list. */ public get entries(): CborMapEntry[] { return this._entries.slice(); } + /** + * Canonical CBOR map entry ordering. + * + * @param {CborMapEntry} a First entry. + * @param {CborMapEntry} b Second entry. + * @returns {number} Negative if `a` sorts before `b`, positive if after, zero if equal. + */ public static compareEntries(a: CborMapEntry, b: CborMapEntry): number { const length = Math.min(a.key.length, b.key.length); for (let i = 0; i < length; i++) { diff --git a/src/serialization/cbor/CborMapEntry.ts b/src/serialization/cbor/CborMapEntry.ts index 6143ba3..2577760 100644 --- a/src/serialization/cbor/CborMapEntry.ts +++ b/src/serialization/cbor/CborMapEntry.ts @@ -1,3 +1,7 @@ +/** + * Single key/value pair inside a {@link CborMap}. Both key and value are + * stored as already-encoded canonical CBOR byte slices. + */ export class CborMapEntry { public constructor( private readonly _key: Uint8Array, @@ -7,10 +11,16 @@ export class CborMapEntry { this._value = new Uint8Array(_value); } + /** + * @returns {Uint8Array} Copy of the encoded key bytes. + */ public get key(): Uint8Array { return new Uint8Array(this._key); } + /** + * @returns {Uint8Array} Copy of the encoded value bytes. + */ public get value(): Uint8Array { return new Uint8Array(this._value); } diff --git a/src/serialization/cbor/CborReader.ts b/src/serialization/cbor/CborReader.ts index 60d46e1..4ac7615 100644 --- a/src/serialization/cbor/CborReader.ts +++ b/src/serialization/cbor/CborReader.ts @@ -1,6 +1,11 @@ import { CborError } from './CborError.js'; import { MajorType } from './MajorType.js'; +/** + * Low-level cursor over a CBOR byte buffer. Tracks a read position and + * provides primitives used by {@link CborDeserializer} to parse canonical + * CBOR. Indefinite-length encodings are rejected. + */ export class CborReader { private static ADDITIONAL_INFORMATION_MASK = 0b00011111; private static MAJOR_TYPE_MASK = 0b11100000; @@ -9,6 +14,11 @@ export class CborReader { public constructor(private readonly data: Uint8Array) {} + /** + * Throw if the reader has not consumed every byte of the buffer. + * + * @throws {CborError} If unread bytes remain. + */ public assertExhausted(): void { if (this.position !== this.data.length) { throw new CborError( @@ -17,6 +27,13 @@ export class CborReader { } } + /** + * Read exactly `length` bytes and advance the cursor. + * + * @param {number} length Number of bytes to read. + * @returns {Uint8Array} Bytes read. + * @throws {CborError} If fewer than `length` bytes remain. + */ public read(length: number): Uint8Array { try { if (this.position + length > this.data.length) { @@ -29,6 +46,12 @@ export class CborReader { } } + /** + * Read a single byte and advance the cursor. + * + * @returns {number} Byte read. + * @throws {CborError} If no bytes remain. + */ public readByte(): number { if (this.position >= this.data.length) { throw new CborError('Premature end of data.'); @@ -37,6 +60,16 @@ export class CborReader { return this.data[this.position++]; } + /** + * Read the initial byte and any extended length bytes for the given major + * type, returning the encoded length/value. Enforces canonical + * minimum-byte encoding. + * + * @param {MajorType} majorType Expected major type. + * @returns {bigint} Decoded length/value. + * @throws {CborError} On major-type mismatch, indefinite-length encoding, + * reserved additional info, or non-canonical length. + */ public readLength(majorType: MajorType): bigint { const initialByte = this.readByte(); const parsedMajorType = (initialByte & CborReader.MAJOR_TYPE_MASK) as MajorType; @@ -79,6 +112,12 @@ export class CborReader { return t; } + /** + * Read a single complete CBOR data item (including nested arrays, maps, + * and tags) without decoding it. + * + * @returns {Uint8Array} CBOR bytes of the data item. + */ public readRawCbor(): Uint8Array { if (this.position >= this.data.length) { throw new CborError('Premature end of data.'); diff --git a/src/serialization/cbor/CborSerializer.ts b/src/serialization/cbor/CborSerializer.ts index df5630b..e528162 100644 --- a/src/serialization/cbor/CborSerializer.ts +++ b/src/serialization/cbor/CborSerializer.ts @@ -2,7 +2,17 @@ import { CborError } from './CborError.js'; import { CborMap } from './CborMap.js'; import { MajorType } from './MajorType.js'; +/** + * Static helpers that encode TypeScript values to canonical CBOR bytes + * (RFC 8949 §4.2). + */ export class CborSerializer { + /** + * Encode an array of already-encoded CBOR items as a CBOR array. + * + * @param {...Uint8Array} input Encoded CBOR items. + * @returns {Uint8Array} CBOR bytes. + */ public static encodeArray(...input: Uint8Array[]): Uint8Array { const data = new Uint8Array(input.reduce((result, value) => result + value.length, 0)); let length = 0; @@ -23,6 +33,12 @@ export class CborSerializer { ]); } + /** + * Encode a boolean as the CBOR simple values `true` (0xf5) or `false` (0xf4). + * + * @param {boolean} data Boolean value. + * @returns {Uint8Array} CBOR bytes. + */ public static encodeBoolean(data: boolean): Uint8Array { if (data) { return new Uint8Array([0xf5]); @@ -30,6 +46,12 @@ export class CborSerializer { return new Uint8Array([0xf4]); } + /** + * Encode raw bytes as a CBOR byte string. + * + * @param {Uint8Array} input Raw bytes. + * @returns {Uint8Array} CBOR bytes. + */ public static encodeByteString(input: Uint8Array): Uint8Array { if (input.length < 24) { return new Uint8Array([MajorType.BYTE_STRING | input.length, ...input]); @@ -43,6 +65,12 @@ export class CborSerializer { ]); } + /** + * Encode a {@link CborMap} as a canonical CBOR map. + * + * @param {CborMap} input Canonical CBOR map. + * @returns {Uint8Array} CBOR bytes. + */ public static encodeMap(input: CborMap): Uint8Array { const entries = input.entries; const dataLength = entries.reduce((result, entry) => result + entry.key.length + entry.value.length, 0); @@ -67,10 +95,23 @@ export class CborSerializer { ]); } + /** + * Encode the CBOR `null` simple value (0xf6). + * + * @returns {Uint8Array} CBOR bytes. + */ public static encodeNull(): Uint8Array { return new Uint8Array([0xf6]); } + /** + * Encode `data` with `encoder`, or return CBOR `null` if `data` is + * `null`/`undefined`. + * + * @param {T|null|undefined} data Value to encode. + * @param {(data: T) => Uint8Array} encoder Encoder for the inner value. + * @returns {Uint8Array} CBOR bytes. + */ public static encodeNullable(data: T | null | undefined, encoder: (data: T) => Uint8Array): Uint8Array { if (data == null) { return new Uint8Array([0xf6]); @@ -79,6 +120,13 @@ export class CborSerializer { return encoder(data); } + /** + * Wrap already-encoded CBOR bytes in a CBOR tag. + * + * @param {number|bigint} tag Tag number. + * @param {Uint8Array} input Encoded CBOR bytes to tag. + * @returns {Uint8Array} CBOR bytes. + */ public static encodeTag(tag: number | bigint, input: Uint8Array): Uint8Array { if (tag < 24) { return new Uint8Array([MajorType.TAG | Number(tag), ...input]); @@ -92,6 +140,12 @@ export class CborSerializer { ]); } + /** + * Encode a UTF-8 string as a CBOR text string. + * + * @param {string} input Text value. + * @returns {Uint8Array} CBOR bytes. + */ public static encodeTextString(input: string): Uint8Array { const bytes = new TextEncoder().encode(input); if (bytes.length < 24) { @@ -106,6 +160,13 @@ export class CborSerializer { ]); } + /** + * Encode a non-negative integer as a CBOR unsigned integer. + * + * @param {bigint|number} input Non-negative integer. + * @returns {Uint8Array} CBOR bytes. + * @throws {CborError} If `input` is negative. + */ public static encodeUnsignedInteger(input: bigint | number): Uint8Array { if (input < 0) { throw new CborError('Only unsigned numbers are allowed.'); diff --git a/src/serialization/cbor/ICborSerializable.ts b/src/serialization/cbor/ICborSerializable.ts index 5ef75ae..793b09e 100644 --- a/src/serialization/cbor/ICborSerializable.ts +++ b/src/serialization/cbor/ICborSerializable.ts @@ -1,3 +1,9 @@ +/** + * Marker for objects that can serialize themselves to canonical CBOR bytes. + */ export interface ICborSerializable { + /** + * @returns CBOR-encoded representation of this object. + */ toCBOR(): Uint8Array; } diff --git a/src/serialization/cbor/MajorType.ts b/src/serialization/cbor/MajorType.ts index 7f54e3e..51d48bd 100644 --- a/src/serialization/cbor/MajorType.ts +++ b/src/serialization/cbor/MajorType.ts @@ -1,3 +1,8 @@ +/** + * CBOR major type bits (RFC 8949 §3.1), packed into the high 3 bits of the + * initial byte. Used by the serializer/deserializer to tag and recognize + * values. + */ export enum MajorType { UNSIGNED_INTEGER = 0b00000000, NEGATIVE_INTEGER = 0b00100000, diff --git a/src/smt/LeafInBranchError.ts b/src/smt/LeafInBranchError.ts index 298f9b1..8fab9ed 100644 --- a/src/smt/LeafInBranchError.ts +++ b/src/smt/LeafInBranchError.ts @@ -1,3 +1,7 @@ +/** + * Thrown when a sparse Merkle tree insertion would place a leaf at a path + * that is already occupied by a branch node. + */ export class LeafInBranchError extends Error { public constructor() { super('Cannot add leaf inside branch.'); diff --git a/src/smt/LeafOutOfBoundsError.ts b/src/smt/LeafOutOfBoundsError.ts index 9aee374..a377577 100644 --- a/src/smt/LeafOutOfBoundsError.ts +++ b/src/smt/LeafOutOfBoundsError.ts @@ -1,3 +1,7 @@ +/** + * Thrown when a sparse Merkle tree operation would extend the tree past a + * leaf node, which is not allowed. + */ export class LeafOutOfBoundsError extends Error { public constructor() { super('Cannot extend tree through leaf.'); diff --git a/src/smt/PathVerificationResult.ts b/src/smt/PathVerificationResult.ts index 48012c6..0a0d65a 100644 --- a/src/smt/PathVerificationResult.ts +++ b/src/smt/PathVerificationResult.ts @@ -1,3 +1,8 @@ +/** + * Outcome of verifying a sparse Merkle tree path. The verification is + * successful only when the path is both well-formed and proves inclusion of + * the leaf. + */ export class PathVerificationResult { public readonly isSuccessful: boolean; diff --git a/src/smt/plain/FinalizedLeafBranch.ts b/src/smt/plain/FinalizedLeafBranch.ts index 37283af..95c2f40 100644 --- a/src/smt/plain/FinalizedLeafBranch.ts +++ b/src/smt/plain/FinalizedLeafBranch.ts @@ -2,6 +2,9 @@ import { DataHash } from '../../crypto/hash/DataHash.js'; import { HexConverter } from '../../util/HexConverter.js'; import { dedent } from '../../util/StringUtils.js'; +/** + * Finalized leaf in a plain sparse Merkle tree. + */ export class FinalizedLeafBranch { public constructor( public readonly path: bigint, @@ -11,14 +14,23 @@ export class FinalizedLeafBranch { this._data = new Uint8Array(_data); } + /** + * @returns {Uint8Array} Copy of the leaf data bytes. + */ public get data(): Uint8Array { return new Uint8Array(this._data); } + /** + * @returns {Promise} This branch (already finalized). + */ public finalize(): Promise { return Promise.resolve(this); } + /** + * @returns {string} String representation of the leaf. + */ public toString(): string { return dedent` Leaf[${this.path.toString(2)}] diff --git a/src/smt/plain/FinalizedNodeBranch.ts b/src/smt/plain/FinalizedNodeBranch.ts index 5d40453..cb1b784 100644 --- a/src/smt/plain/FinalizedNodeBranch.ts +++ b/src/smt/plain/FinalizedNodeBranch.ts @@ -2,6 +2,9 @@ import { FinalizedBranch } from './FinalizedBranch.js'; import { DataHash } from '../../crypto/hash/DataHash.js'; import { dedent } from '../../util/StringUtils.js'; +/** + * Finalized interior node in a plain sparse Merkle tree. + */ export class FinalizedNodeBranch { public constructor( public readonly path: bigint, @@ -10,10 +13,16 @@ export class FinalizedNodeBranch { public readonly hash: DataHash, ) {} + /** + * @returns {Promise} This branch (already finalized). + */ public finalize(): Promise { return Promise.resolve(this); } + /** + * @returns {string} String representation of the node. + */ public toString(): string { return dedent` Node[${this.path.toString(2)}] diff --git a/src/smt/plain/PendingLeafBranch.ts b/src/smt/plain/PendingLeafBranch.ts index d5f5f00..d072c1a 100644 --- a/src/smt/plain/PendingLeafBranch.ts +++ b/src/smt/plain/PendingLeafBranch.ts @@ -4,6 +4,9 @@ import { IDataHasherFactory } from '../../crypto/hash/IDataHasherFactory.js'; import { CborSerializer } from '../../serialization/cbor/CborSerializer.js'; import { BigintConverter } from '../../util/BigintConverter.js'; +/** + * Pending leaf in a plain sparse Merkle tree, awaiting hashing. + */ export class PendingLeafBranch { public constructor( public readonly path: bigint, @@ -12,10 +15,19 @@ export class PendingLeafBranch { this._data = new Uint8Array(_data); } + /** + * @returns {Uint8Array} Copy of the leaf data bytes. + */ public get data(): Uint8Array { return new Uint8Array(this._data); } + /** + * Hash this leaf to produce a finalized branch. + * + * @param {IDataHasherFactory} factory Hasher factory. + * @returns {Promise} Finalized leaf branch. + */ public async finalize(factory: IDataHasherFactory): Promise { const hash = await factory .create() diff --git a/src/smt/plain/PendingNodeBranch.ts b/src/smt/plain/PendingNodeBranch.ts index 6cde054..8768187 100644 --- a/src/smt/plain/PendingNodeBranch.ts +++ b/src/smt/plain/PendingNodeBranch.ts @@ -5,6 +5,9 @@ import { IDataHasherFactory } from '../../crypto/hash/IDataHasherFactory.js'; import { CborSerializer } from '../../serialization/cbor/CborSerializer.js'; import { BigintConverter } from '../../util/BigintConverter.js'; +/** + * Pending interior node in a plain sparse Merkle tree, awaiting hashing. + */ export class PendingNodeBranch { public constructor( public readonly path: bigint, @@ -12,6 +15,12 @@ export class PendingNodeBranch { public readonly right: PendingBranch, ) {} + /** + * Hash this node (after finalizing children). + * + * @param {IDataHasherFactory} factory Hasher factory. + * @returns {Promise} Finalized node branch. + */ public async finalize(factory: IDataHasherFactory): Promise { const [left, right] = await Promise.all([this.left.finalize(factory), this.right.finalize(factory)]); const hash = await factory diff --git a/src/smt/plain/SparseMerkleTreePath.ts b/src/smt/plain/SparseMerkleTreePath.ts index 8abc58f..a19e78a 100644 --- a/src/smt/plain/SparseMerkleTreePath.ts +++ b/src/smt/plain/SparseMerkleTreePath.ts @@ -9,12 +9,21 @@ import { CborSerializer } from '../../serialization/cbor/CborSerializer.js'; import { BigintConverter } from '../../util/BigintConverter.js'; import { dedent } from '../../util/StringUtils.js'; +/** + * Path through a plain sparse Merkle tree from a leaf to the root. + */ export class SparseMerkleTreePath { public constructor( public readonly root: DataHash, public readonly steps: ReadonlyArray, ) {} + /** + * Create SparseMerkleTreePath from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {SparseMerkleTreePath} Decoded path. + */ public static fromCBOR(bytes: Uint8Array): SparseMerkleTreePath { const data = CborDeserializer.decodeArray(bytes, 2); const steps = CborDeserializer.decodeArray(data[1]); @@ -25,6 +34,11 @@ export class SparseMerkleTreePath { ); } + /** + * Convert SparseMerkleTreePath to CBOR bytes. + * + * @returns {Uint8Array} CBOR bytes. + */ public toCBOR(): Uint8Array { return CborSerializer.encodeArray( CborSerializer.encodeByteString(this.root.imprint), @@ -32,6 +46,9 @@ export class SparseMerkleTreePath { ); } + /** + * @returns {string} String representation of the path. + */ public toString(): string { return dedent` Merkle Tree Path diff --git a/src/smt/plain/SparseMerkleTreePathStep.ts b/src/smt/plain/SparseMerkleTreePathStep.ts index 7904526..37ead40 100644 --- a/src/smt/plain/SparseMerkleTreePathStep.ts +++ b/src/smt/plain/SparseMerkleTreePathStep.ts @@ -4,6 +4,9 @@ import { BigintConverter } from '../../util/BigintConverter.js'; import { HexConverter } from '../../util/HexConverter.js'; import { dedent } from '../../util/StringUtils.js'; +/** + * Single step along a plain sparse Merkle tree path. + */ export class SparseMerkleTreePathStep { public constructor( public readonly path: bigint, @@ -14,10 +17,19 @@ export class SparseMerkleTreePathStep { } } + /** + * @returns {Uint8Array|null} Copy of the step data bytes, or `null`. + */ public get data(): Uint8Array | null { return this._data ? new Uint8Array(this._data) : null; } + /** + * Create SparseMerkleTreePathStep from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {SparseMerkleTreePathStep} Decoded step. + */ public static fromCBOR(bytes: Uint8Array): SparseMerkleTreePathStep { const data = CborDeserializer.decodeArray(bytes, 2); @@ -27,6 +39,11 @@ export class SparseMerkleTreePathStep { ); } + /** + * Convert SparseMerkleTreePathStep to CBOR bytes. + * + * @returns {Uint8Array} CBOR bytes. + */ public toCBOR(): Uint8Array { return CborSerializer.encodeArray( CborSerializer.encodeByteString(BigintConverter.encode(this.path)), @@ -34,6 +51,9 @@ export class SparseMerkleTreePathStep { ); } + /** + * @returns {string} String representation of the step. + */ public toString(): string { return dedent` Merkle Tree Path Step diff --git a/src/smt/radix/FinalizedLeafBranch.ts b/src/smt/radix/FinalizedLeafBranch.ts index d2def72..10143c4 100644 --- a/src/smt/radix/FinalizedLeafBranch.ts +++ b/src/smt/radix/FinalizedLeafBranch.ts @@ -5,6 +5,9 @@ import { IDataHasherFactory } from '../../crypto/hash/IDataHasherFactory.js'; import { HexConverter } from '../../util/HexConverter.js'; import { dedent } from '../../util/StringUtils.js'; +/** + * Finalized leaf in a radix sparse Merkle tree. + */ export class FinalizedLeafBranch { public constructor( public readonly path: bigint, @@ -13,14 +16,27 @@ export class FinalizedLeafBranch { public readonly hash: DataHash, ) {} + /** + * @returns {Uint8Array} Copy of the leaf data bytes. + */ public get data(): Uint8Array { return this._data.slice(); } + /** + * @returns {Uint8Array} Copy of the leaf key bytes. + */ public get key(): Uint8Array { return this._key.slice(); } + /** + * Hash a {@link PendingLeafBranch} into a finalized leaf. + * + * @param {IDataHasherFactory} factory Hasher factory. + * @param {PendingLeafBranch} leaf Pending leaf to finalize. + * @returns {Promise} Finalized leaf branch. + */ public static async fromPendingLeaf( factory: IDataHasherFactory, leaf: PendingLeafBranch, @@ -38,10 +54,16 @@ export class FinalizedLeafBranch { return new FinalizedLeafBranch(leaf.path, key, data, hash); } + /** + * @returns {Promise} This branch (already finalized). + */ public finalize(): Promise { return Promise.resolve(this); } + /** + * @returns {string} String representation of the leaf. + */ public toString(): string { return dedent` FinalizedLeaf[${this.path.toString(2)}] diff --git a/src/smt/radix/FinalizedNodeBranch.ts b/src/smt/radix/FinalizedNodeBranch.ts index b48f457..c2c1083 100644 --- a/src/smt/radix/FinalizedNodeBranch.ts +++ b/src/smt/radix/FinalizedNodeBranch.ts @@ -5,6 +5,9 @@ import { IDataHasher } from '../../crypto/hash/IDataHasher.js'; import { IDataHasherFactory } from '../../crypto/hash/IDataHasherFactory.js'; import { dedent } from '../../util/StringUtils.js'; +/** + * Finalized interior node in a radix sparse Merkle tree. + */ export class FinalizedNodeBranch { public constructor( public readonly path: bigint, @@ -14,6 +17,13 @@ export class FinalizedNodeBranch { public readonly hash: DataHash, ) {} + /** + * Hash a {@link PendingNodeBranch} into a finalized node. + * + * @param {IDataHasherFactory} factory Hasher factory. + * @param {PendingNodeBranch} node Pending node to finalize. + * @returns {Promise} Finalized node branch. + */ public static async create( factory: IDataHasherFactory, node: PendingNodeBranch, @@ -29,10 +39,16 @@ export class FinalizedNodeBranch { return new FinalizedNodeBranch(node.path, node.depth, left, right, hash); } + /** + * @returns {Promise} This branch (already finalized). + */ public finalize(): Promise { return Promise.resolve(this); } + /** + * @returns {string} String representation of the node. + */ public toString(): string { return dedent` Node[${this.path.toString(2)}] diff --git a/src/smt/radix/PendingLeafBranch.ts b/src/smt/radix/PendingLeafBranch.ts index a83f572..469b3b2 100644 --- a/src/smt/radix/PendingLeafBranch.ts +++ b/src/smt/radix/PendingLeafBranch.ts @@ -4,6 +4,9 @@ import { IDataHasherFactory } from '../../crypto/hash/IDataHasherFactory.js'; import { HexConverter } from '../../util/HexConverter.js'; import { dedent } from '../../util/StringUtils.js'; +/** + * Pending leaf in a radix sparse Merkle tree, awaiting hashing. + */ export class PendingLeafBranch { public constructor( public readonly path: bigint, @@ -14,18 +17,33 @@ export class PendingLeafBranch { this._data = new Uint8Array(_data); } + /** + * @returns {Uint8Array} Copy of the leaf data bytes. + */ public get data(): Uint8Array { return this._data.slice(); } + /** + * @returns {Uint8Array} Copy of the leaf key bytes. + */ public get key(): Uint8Array { return this._key.slice(); } + /** + * Hash this leaf to produce a finalized branch. + * + * @param {IDataHasherFactory} factory Hasher factory. + * @returns {Promise} Finalized leaf branch. + */ public finalize(factory: IDataHasherFactory): Promise { return FinalizedLeafBranch.fromPendingLeaf(factory, this); } + /** + * @returns {string} String representation of the leaf. + */ public toString(): string { return dedent` PendingLeaf[${this.path.toString(2)}] diff --git a/src/smt/radix/PendingNodeBranch.ts b/src/smt/radix/PendingNodeBranch.ts index c956de9..2a4be12 100644 --- a/src/smt/radix/PendingNodeBranch.ts +++ b/src/smt/radix/PendingNodeBranch.ts @@ -4,6 +4,9 @@ import { IDataHasher } from '../../crypto/hash/IDataHasher.js'; import { IDataHasherFactory } from '../../crypto/hash/IDataHasherFactory.js'; import { dedent } from '../../util/StringUtils.js'; +/** + * Pending interior node in a radix sparse Merkle tree, awaiting hashing. + */ export class PendingNodeBranch { public constructor( public readonly path: bigint, @@ -12,10 +15,19 @@ export class PendingNodeBranch { public readonly right: PendingBranch, ) {} + /** + * Hash this node (after finalizing children). + * + * @param {IDataHasherFactory} factory Hasher factory. + * @returns {Promise} Finalized node branch. + */ public finalize(factory: IDataHasherFactory): Promise { return FinalizedNodeBranch.create(factory, this); } + /** + * @returns {string} String representation of the node. + */ public toString(): string { return dedent` PendingNode[${this.path.toString(2)}] diff --git a/src/smt/radix/SparseMerkleTree.ts b/src/smt/radix/SparseMerkleTree.ts index 2668382..2ebd0a8 100644 --- a/src/smt/radix/SparseMerkleTree.ts +++ b/src/smt/radix/SparseMerkleTree.ts @@ -24,6 +24,13 @@ export class SparseMerkleTree { */ public constructor(public readonly factory: IDataHasherFactory) {} + /** + * Add a leaf to the tree. + * + * @param {Uint8Array} key Leaf key. + * @param {Uint8Array} data Leaf data bytes. + * @returns {Promise} Resolves when the leaf has been inserted. + */ public async addLeaf(key: Uint8Array, data: Uint8Array): Promise { // TODO: Add length check data = new Uint8Array(data); diff --git a/src/smt/radix/SparseMerkleTreeRootNode.ts b/src/smt/radix/SparseMerkleTreeRootNode.ts index d00849e..c6e1f8c 100644 --- a/src/smt/radix/SparseMerkleTreeRootNode.ts +++ b/src/smt/radix/SparseMerkleTreeRootNode.ts @@ -56,6 +56,12 @@ export class SparseMerkleTreeRootNode { return new SparseMerkleTreeRootNode(null, null, new DataHash(HashAlgorithm.SHA256, new Uint8Array(32))); } + /** + * Check whether a leaf with the given key is in the tree. + * + * @param {Uint8Array} key Leaf key. + * @returns {boolean} True if the key is present. + */ public has(key: Uint8Array): boolean { const keyPath = BitString.fromBytesReversedLSB(key).toBigInt(); // eslint-disable-next-line @typescript-eslint/no-this-alias diff --git a/src/smt/sum/FinalizedLeafBranch.ts b/src/smt/sum/FinalizedLeafBranch.ts index 0179fa0..d4f2641 100644 --- a/src/smt/sum/FinalizedLeafBranch.ts +++ b/src/smt/sum/FinalizedLeafBranch.ts @@ -2,6 +2,9 @@ import { DataHash } from '../../crypto/hash/DataHash.js'; import { HexConverter } from '../../util/HexConverter.js'; import { dedent } from '../../util/StringUtils.js'; +/** + * Finalized leaf in a sparse Merkle sum tree. + */ export class FinalizedLeafBranch { public constructor( public readonly path: bigint, @@ -12,14 +15,23 @@ export class FinalizedLeafBranch { this._data = new Uint8Array(_data); } + /** + * @returns {Uint8Array} Copy of the leaf data bytes. + */ public get data(): Uint8Array { return new Uint8Array(this._data); } + /** + * @returns {Promise} This branch (already finalized). + */ public finalize(): Promise { return Promise.resolve(this); } + /** + * @returns {string} String representation of the leaf. + */ public toString(): string { return dedent` Leaf[${this.path.toString(2)}] diff --git a/src/smt/sum/FinalizedNodeBranch.ts b/src/smt/sum/FinalizedNodeBranch.ts index 3a8836f..bd4905b 100644 --- a/src/smt/sum/FinalizedNodeBranch.ts +++ b/src/smt/sum/FinalizedNodeBranch.ts @@ -2,6 +2,9 @@ import { FinalizedBranch } from './FinalizedBranch.js'; import { DataHash } from '../../crypto/hash/DataHash.js'; import { dedent } from '../../util/StringUtils.js'; +/** + * Finalized interior node in a sparse Merkle sum tree. + */ export class FinalizedNodeBranch { public constructor( public readonly path: bigint, @@ -11,10 +14,16 @@ export class FinalizedNodeBranch { public readonly hash: DataHash, ) {} + /** + * @returns {Promise} This branch (already finalized). + */ public finalize(): Promise { return Promise.resolve(this); } + /** + * @returns {string} String representation of the node. + */ public toString(): string { return dedent` Node[${this.path.toString(2)}] diff --git a/src/smt/sum/PendingLeafBranch.ts b/src/smt/sum/PendingLeafBranch.ts index 6cf93c4..2607409 100644 --- a/src/smt/sum/PendingLeafBranch.ts +++ b/src/smt/sum/PendingLeafBranch.ts @@ -4,6 +4,9 @@ import { IDataHasherFactory } from '../../crypto/hash/IDataHasherFactory.js'; import { CborSerializer } from '../../serialization/cbor/CborSerializer.js'; import { BigintConverter } from '../../util/BigintConverter.js'; +/** + * Pending leaf in a sparse Merkle sum tree, awaiting hashing. + */ export class PendingLeafBranch { public constructor( public readonly path: bigint, @@ -13,10 +16,19 @@ export class PendingLeafBranch { this._data = new Uint8Array(_data); } + /** + * @returns {Uint8Array} Copy of the leaf data bytes. + */ public get data(): Uint8Array { return new Uint8Array(this._data); } + /** + * Hash this leaf to produce a finalized branch. + * + * @param {IDataHasherFactory} factory Hasher factory. + * @returns {Promise} Finalized leaf branch. + */ public async finalize(factory: IDataHasherFactory): Promise { const hash = await factory .create() diff --git a/src/smt/sum/PendingNodeBranch.ts b/src/smt/sum/PendingNodeBranch.ts index a324548..6f8b885 100644 --- a/src/smt/sum/PendingNodeBranch.ts +++ b/src/smt/sum/PendingNodeBranch.ts @@ -5,6 +5,9 @@ import { IDataHasherFactory } from '../../crypto/hash/IDataHasherFactory.js'; import { CborSerializer } from '../../serialization/cbor/CborSerializer.js'; import { BigintConverter } from '../../util/BigintConverter.js'; +/** + * Pending interior node in a sparse Merkle sum tree, awaiting hashing. + */ export class PendingNodeBranch { public constructor( public readonly path: bigint, @@ -12,6 +15,12 @@ export class PendingNodeBranch { public readonly right: PendingBranch, ) {} + /** + * Hash this node (after finalizing children) and sum their values. + * + * @param {IDataHasherFactory} factory Hasher factory. + * @returns {Promise} Finalized node branch. + */ public async finalize(factory: IDataHasherFactory): Promise { const [left, right] = await Promise.all([this.left.finalize(factory), this.right.finalize(factory)]); diff --git a/src/smt/sum/SparseMerkleSumTreePath.ts b/src/smt/sum/SparseMerkleSumTreePath.ts index 67a68bb..88318f6 100644 --- a/src/smt/sum/SparseMerkleSumTreePath.ts +++ b/src/smt/sum/SparseMerkleSumTreePath.ts @@ -11,17 +11,29 @@ import { HexConverter } from '../../util/HexConverter.js'; import { dedent } from '../../util/StringUtils.js'; import { PathVerificationResult } from '../PathVerificationResult.js'; +/** + * JSON shape of a {@link SparseMerkleSumTreePath}. + */ export interface ISparseMerkleSumTreePathJson { readonly root: string; readonly steps: ReadonlyArray; } +/** + * Path through a sparse Merkle sum tree from a leaf to the root. + */ export class SparseMerkleSumTreePath { public constructor( public readonly root: DataHash, public readonly steps: ReadonlyArray, ) {} + /** + * Create SparseMerkleSumTreePath from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {SparseMerkleSumTreePath} Decoded path. + */ public static fromCBOR(bytes: Uint8Array): SparseMerkleSumTreePath { const data = CborDeserializer.decodeArray(bytes, 2); const steps = CborDeserializer.decodeArray(data[1]); @@ -32,6 +44,13 @@ export class SparseMerkleSumTreePath { ); } + /** + * Create SparseMerkleSumTreePath from JSON. + * + * @param {unknown} data JSON object. + * @returns {SparseMerkleSumTreePath} Decoded path. + * @throws {InvalidJsonStructureError} If the JSON does not match the expected shape. + */ public static fromJSON(data: unknown): SparseMerkleSumTreePath { if (!SparseMerkleSumTreePath.isJSON(data)) { throw new InvalidJsonStructureError(); @@ -43,6 +62,12 @@ export class SparseMerkleSumTreePath { ); } + /** + * Type guard for the JSON shape of a path. + * + * @param {unknown} data Value to test. + * @returns {boolean} True if `data` matches {@link ISparseMerkleSumTreePathJson}. + */ public static isJSON(data: unknown): data is ISparseMerkleSumTreePathJson { return ( typeof data === 'object' && @@ -55,6 +80,11 @@ export class SparseMerkleSumTreePath { ); } + /** + * Convert SparseMerkleSumTreePath to CBOR bytes. + * + * @returns {Uint8Array} CBOR bytes. + */ public toCBOR(): Uint8Array { return CborSerializer.encodeArray( CborSerializer.encodeByteString(this.root.imprint), @@ -62,6 +92,9 @@ export class SparseMerkleSumTreePath { ); } + /** + * @returns {string} String representation of the path. + */ public toString(): string { return dedent` Merkle Tree Path diff --git a/src/smt/sum/SparseMerkleSumTreePathStep.ts b/src/smt/sum/SparseMerkleSumTreePathStep.ts index 1c741e7..6d77441 100644 --- a/src/smt/sum/SparseMerkleSumTreePathStep.ts +++ b/src/smt/sum/SparseMerkleSumTreePathStep.ts @@ -5,12 +5,18 @@ import { BigintConverter } from '../../util/BigintConverter.js'; import { HexConverter } from '../../util/HexConverter.js'; import { dedent } from '../../util/StringUtils.js'; +/** + * JSON shape of a {@link SparseMerkleSumTreePathStep}. + */ export interface ISparseMerkleSumTreePathStepJson { readonly data: string | null; readonly path: string; readonly value: string; } +/** + * Single step along a sparse Merkle sum tree path. + */ export class SparseMerkleSumTreePathStep { public constructor( public readonly path: bigint, @@ -26,10 +32,19 @@ export class SparseMerkleSumTreePathStep { } } + /** + * @returns {Uint8Array|null} Copy of the step data bytes, or `null`. + */ public get data(): Uint8Array | null { return this._data ? new Uint8Array(this._data) : null; } + /** + * Create SparseMerkleSumTreePathStep from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {SparseMerkleSumTreePathStep} Decoded step. + */ public static fromCBOR(bytes: Uint8Array): SparseMerkleSumTreePathStep { const data = CborDeserializer.decodeArray(bytes, 3); @@ -40,6 +55,13 @@ export class SparseMerkleSumTreePathStep { ); } + /** + * Create SparseMerkleSumTreePathStep from JSON. + * + * @param {unknown} data JSON object. + * @returns {SparseMerkleSumTreePathStep} Decoded step. + * @throws {InvalidJsonStructureError} If the JSON does not match the expected shape. + */ public static fromJSON(data: unknown): SparseMerkleSumTreePathStep { if (!SparseMerkleSumTreePathStep.isJSON(data)) { throw new InvalidJsonStructureError(); @@ -52,6 +74,12 @@ export class SparseMerkleSumTreePathStep { ); } + /** + * Type guard for the JSON shape of a step. + * + * @param {unknown} data Value to test. + * @returns {boolean} True if `data` matches {@link ISparseMerkleSumTreePathStepJson}. + */ public static isJSON(data: unknown): data is ISparseMerkleSumTreePathStepJson { return ( typeof data === 'object' && @@ -65,6 +93,11 @@ export class SparseMerkleSumTreePathStep { ); } + /** + * Convert SparseMerkleSumTreePathStep to CBOR bytes. + * + * @returns {Uint8Array} CBOR bytes. + */ public toCBOR(): Uint8Array { return CborSerializer.encodeArray( CborSerializer.encodeByteString(BigintConverter.encode(this.path)), @@ -73,6 +106,9 @@ export class SparseMerkleSumTreePathStep { ); } + /** + * @returns {string} String representation of the step. + */ public toString(): string { return dedent` Merkle Tree Path Step diff --git a/src/transaction/CertifiedMintTransaction.ts b/src/transaction/CertifiedMintTransaction.ts index b352517..e5aefd5 100644 --- a/src/transaction/CertifiedMintTransaction.ts +++ b/src/transaction/CertifiedMintTransaction.ts @@ -16,49 +16,92 @@ import { InclusionProofVerificationStatus, } from './verification/rule/InclusionProofVerificationRule.js'; +/** + * Mint transaction bundled with a verified inclusion proof. + */ export class CertifiedMintTransaction implements ITransaction { private constructor( private readonly transaction: MintTransaction, public readonly inclusionProof: InclusionProof, ) {} + /** + * @returns {Uint8Array|null} Data payload of the inner transaction. + */ public get data(): Uint8Array | null { return this.transaction.data; } + /** + * @returns {Uint8Array|null} Mint justification bytes of the inner transaction. + */ public get justification(): Uint8Array | null { return this.transaction.justification; } + /** + * @returns {EncodedPredicate} Lock script of the inner transaction. + */ public get lockScript(): EncodedPredicate { return this.transaction.lockScript; } + /** + * @returns {EncodedPredicate} Recipient predicate of the inner transaction. + */ public get recipient(): EncodedPredicate { return this.transaction.recipient; } + /** + * @returns {DataHash} Source state hash of the inner transaction. + */ public get sourceStateHash(): DataHash { return this.transaction.sourceStateHash; } + /** + * @returns {Uint8Array} State mask of the inner transaction. + */ public get stateMask(): Uint8Array { return this.transaction.stateMask; } + /** + * @returns {TokenId} Token id of the inner transaction. + */ public get tokenId(): TokenId { return this.transaction.tokenId; } + /** + * @returns {TokenType} Token type of the inner transaction. + */ public get tokenType(): TokenType { return this.transaction.tokenType; } + /** + * Create CertifiedMintTransaction from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {Promise} Decoded certified transaction. + */ public static async fromCBOR(bytes: Uint8Array): Promise { const data = CborDeserializer.decodeArray(bytes, 2); return new CertifiedMintTransaction(await MintTransaction.fromCBOR(data[0]), InclusionProof.fromCBOR(data[1])); } + /** + * Create CertifiedMintTransaction from mint transaction and inclusion proof. + * + * @param {RootTrustBase} trustBase Root trust base used to verify the inclusion certificate. + * @param {PredicateVerifierService} predicateVerifier Verifier for any embedded predicates. + * @param {MintTransaction} transaction Transaction to certify. + * @param {InclusionProof} inclusionProof Inclusion proof for the transaction. + * @returns {Promise} Verified certified transaction. + * @throws {VerificationError} If the inclusion proof does not verify. + */ public static async fromTransaction( trustBase: RootTrustBase, predicateVerifier: PredicateVerifierService, @@ -78,18 +121,30 @@ export class CertifiedMintTransaction implements ITransaction { return new CertifiedMintTransaction(transaction, inclusionProof); } + /** + * @inheritDoc + */ public calculateStateHash(): Promise { return this.transaction.calculateStateHash(); } + /** + * @inheritDoc + */ public calculateTransactionHash(): Promise { return this.transaction.calculateTransactionHash(); } + /** + * @inheritDoc + */ public toCBOR(): Uint8Array { return CborSerializer.encodeArray(this.transaction.toCBOR(), this.inclusionProof.toCBOR()); } + /** + * @returns {string} String representation of the certified transaction. + */ public toString(): string { return dedent` CertifiedMintTransaction diff --git a/src/transaction/CertifiedTransferTransaction.ts b/src/transaction/CertifiedTransferTransaction.ts index ed7799e..c675a3a 100644 --- a/src/transaction/CertifiedTransferTransaction.ts +++ b/src/transaction/CertifiedTransferTransaction.ts @@ -15,32 +15,57 @@ import { InclusionProofVerificationStatus, } from './verification/rule/InclusionProofVerificationRule.js'; +/** + * Transfer transaction bundled with a verified inclusion proof. + */ export class CertifiedTransferTransaction implements ITransaction { private constructor( private readonly transaction: TransferTransaction, public readonly inclusionProof: InclusionProof, ) {} + /** + * @returns {Uint8Array|null} Data payload of the inner transaction. + */ public get data(): Uint8Array | null { return this.transaction.data; } + /** + * @returns {EncodedPredicate} Lock script of the inner transaction. + */ public get lockScript(): EncodedPredicate { return this.transaction.lockScript; } + /** + * @returns {EncodedPredicate} Recipient predicate of the inner transaction. + */ public get recipient(): EncodedPredicate { return this.transaction.recipient; } + /** + * @returns {DataHash} Source state hash of the inner transaction. + */ public get sourceStateHash(): DataHash { return this.transaction.sourceStateHash; } + /** + * @returns {Uint8Array} State mask of the inner transaction. + */ public get stateMask(): Uint8Array { return this.transaction.stateMask; } + /** + * Create CertifiedTransferTransaction from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @param {Token} token Token providing context for the transfer transaction. + * @returns {Promise} Decoded certified transaction. + */ public static async fromCBOR(bytes: Uint8Array, token: Token): Promise { const data = CborDeserializer.decodeArray(bytes, 2); return new CertifiedTransferTransaction( @@ -49,6 +74,16 @@ export class CertifiedTransferTransaction implements ITransaction { ); } + /** + * Create CertifiedTransferTransaction from mint transaction and inclusion proof. + * + * @param {RootTrustBase} trustBase Root trust base used to verify the inclusion certificate. + * @param {PredicateVerifierService} predicateVerifier Verifier for any embedded predicates. + * @param {TransferTransaction} transaction Transaction to certify. + * @param {InclusionProof} inclusionProof Inclusion proof for the transaction. + * @returns {Promise} Verified certified transaction. + * @throws {VerificationError} If the inclusion proof does not verify. + */ public static async fromTransaction( trustBase: RootTrustBase, predicateVerifier: PredicateVerifierService, @@ -68,18 +103,30 @@ export class CertifiedTransferTransaction implements ITransaction { return new CertifiedTransferTransaction(transaction, inclusionProof); } + /** + * @inheritDoc + */ public calculateStateHash(): Promise { return this.transaction.calculateStateHash(); } + /** + * @inheritDoc + */ public calculateTransactionHash(): Promise { return this.transaction.calculateTransactionHash(); } + /** + * @inheritDoc + */ public toCBOR(): Uint8Array { return CborSerializer.encodeArray(this.transaction.toCBOR(), this.inclusionProof.toCBOR()); } + /** + * @returns {string} String representation of the certified transaction. + */ public toString(): string { return dedent` CertifiedTransferTransaction diff --git a/src/transaction/ITransaction.ts b/src/transaction/ITransaction.ts index 9d1ce91..60c3a01 100644 --- a/src/transaction/ITransaction.ts +++ b/src/transaction/ITransaction.ts @@ -1,6 +1,11 @@ import { DataHash } from '../crypto/hash/DataHash.js'; import { EncodedPredicate } from '../predicate/EncodedPredicate.js'; +/** + * Common shape for mint, transfer, and certified transactions. Captures the + * source state, lock script, recipient predicate, and any data payload, and + * exposes hashes used by verification rules and certification requests. + */ export interface ITransaction { readonly data: Uint8Array | null; @@ -12,9 +17,26 @@ export interface ITransaction { readonly stateMask: Uint8Array; + /** + * Compute the hash of the resulting state after applying this transaction. + * + * @returns {Promise} Target state hash. + */ calculateStateHash(): Promise; + + /** + * Compute the canonical hash of this transaction. + * + * @returns {Promise} Transaction hash. + */ calculateTransactionHash(): Promise; + /** + * Convert this transaction to CBOR bytes. + * + * @returns {Uint8Array} CBOR bytes. + */ toCBOR(): Uint8Array; + toString(): string; } diff --git a/src/transaction/MintTransaction.ts b/src/transaction/MintTransaction.ts index d1116ab..95c590a 100644 --- a/src/transaction/MintTransaction.ts +++ b/src/transaction/MintTransaction.ts @@ -19,6 +19,9 @@ import { CborSerializer } from '../serialization/cbor/CborSerializer.js'; import { HexConverter } from '../util/HexConverter.js'; import { dedent } from '../util/StringUtils.js'; +/** + * Token mint transaction. + */ export class MintTransaction implements ITransaction { public static readonly CBOR_TAG = 39041n; private static readonly VERSION = 1n; @@ -33,22 +36,44 @@ export class MintTransaction implements ITransaction { private readonly _data: Uint8Array | null, ) {} + /** + * @returns {Uint8Array|null} Copy of the data payload, or `null` if absent. + */ public get data(): Uint8Array | null { return this._data ? new Uint8Array(this._data) : null; } + /** + * @returns {Uint8Array|null} Copy of the mint justification bytes, or `null` if absent. + */ public get justification(): Uint8Array | null { return this._justification ? new Uint8Array(this._justification) : null; } + /** + * @returns {Uint8Array} State mask used when computing the resulting state hash. + */ public get stateMask(): Uint8Array { return new Uint8Array(this.tokenId.bytes); } + /** + * @returns {bigint} Wire-format version of this transaction. + */ public get version(): bigint { return MintTransaction.VERSION; } + /** + * Create a MintTransaction for a fresh token. + * + * @param {IPredicate} recipient Predicate that will lock the minted state. + * @param {TokenId} tokenId Token id being minted. + * @param {TokenType} tokenType Token type being minted. + * @param {Uint8Array|null} justification Optional mint justification bytes. + * @param {Uint8Array|null} data Optional data payload. + * @returns {Promise} New mint transaction. + */ public static async create( recipient: IPredicate, tokenId: TokenId, @@ -71,6 +96,13 @@ export class MintTransaction implements ITransaction { ); } + /** + * Create MintTransaction from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {Promise} Decoded transaction. + * @throws {CborError} On wrong tag or unsupported version. + */ public static fromCBOR(bytes: Uint8Array): Promise { const tag = CborDeserializer.decodeTag(bytes); if (tag.tag !== MintTransaction.CBOR_TAG) { @@ -92,6 +124,9 @@ export class MintTransaction implements ITransaction { ); } + /** + * @inheritDoc + */ public calculateStateHash(): Promise { return new DataHasher(HashAlgorithm.SHA256) .update( @@ -103,10 +138,16 @@ export class MintTransaction implements ITransaction { .digest(); } + /** + * @inheritDoc + */ public calculateTransactionHash(): Promise { return new DataHasher(HashAlgorithm.SHA256).update(this.toCBOR()).digest(); } + /** + * @inheritDoc + */ public toCBOR(): Uint8Array { return CborSerializer.encodeTag( MintTransaction.CBOR_TAG, @@ -121,6 +162,14 @@ export class MintTransaction implements ITransaction { ); } + /** + * Bundle this transaction with its inclusion proof into a CertifiedMintTransaction. + * + * @param {RootTrustBase} trustBase Root trust base used to verify the inclusion certificate. + * @param {PredicateVerifierService} predicateVerifier Verifier for any predicates. + * @param {InclusionProof} inclusionProof Inclusion proof for this transaction. + * @returns {Promise} Verified certified transaction. + */ public toCertifiedTransaction( trustBase: RootTrustBase, predicateVerifier: PredicateVerifierService, @@ -129,6 +178,9 @@ export class MintTransaction implements ITransaction { return CertifiedMintTransaction.fromTransaction(trustBase, predicateVerifier, this, inclusionProof); } + /** + * @returns {string} String representation of the transaction. + */ public toString(): string { return dedent` MintTransaction diff --git a/src/transaction/MintTransactionState.ts b/src/transaction/MintTransactionState.ts index fe3be58..336b32f 100644 --- a/src/transaction/MintTransactionState.ts +++ b/src/transaction/MintTransactionState.ts @@ -24,8 +24,8 @@ export class MintTransactionState extends DataHash { /** * Create token initial state from token id. * - * @param {TokenId} tokenId token id - * @return mint state + * @param {TokenId} tokenId Token id. + * @returns {Promise} Mint state. */ public static async create(tokenId: TokenId): Promise { return new MintTransactionState( diff --git a/src/transaction/Token.ts b/src/transaction/Token.ts index 309240f..801ea08 100644 --- a/src/transaction/Token.ts +++ b/src/transaction/Token.ts @@ -16,6 +16,9 @@ import { VerificationStatus } from '../verification/VerificationStatus.js'; import { CertifiedMintTransactionVerificationRule } from './verification/rule/CertifiedMintTransactionVerificationRule.js'; import { CertifiedTransferTransactionVerificationRule } from './verification/rule/CertifiedTransferTransactionVerificationRule.js'; +/** + * Token representation. + */ export class Token { public static readonly CBOR_TAG = 39040n; private static readonly VERSION = 1n; @@ -25,26 +28,48 @@ export class Token { private readonly _transactions: CertifiedTransferTransaction[] = [], ) {} + /** + * @returns {TokenId} Token id from the genesis transaction. + */ public get id(): TokenId { return this.genesis.tokenId; } + /** + * @returns {ITransaction} Latest transaction, or the genesis if there are no transfers. + */ public get latestTransaction(): ITransaction { return this._transactions.at(-1) ?? this.genesis; } + /** + * @returns {CertifiedTransferTransaction[]} Copy of the transfer history. + */ public get transactions(): CertifiedTransferTransaction[] { return this._transactions.slice(); } + /** + * @returns {TokenType} Token type from the genesis transaction. + */ public get type(): TokenType { return this.genesis.tokenType; } + /** + * @returns {bigint} Wire-format version of this token. + */ public get version(): bigint { return Token.VERSION; } + /** + * Create Token from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {Promise} Decoded token. + * @throws {CborError} On wrong tag or unsupported version. + */ public static async fromCBOR(bytes: Uint8Array): Promise { const tag = CborDeserializer.decodeTag(bytes); if (tag.tag !== Token.CBOR_TAG) { @@ -68,6 +93,16 @@ export class Token { return token; } + /** + * Create a Token from a verified genesis mint transaction. + * + * @param {RootTrustBase} trustBase Root trust base used to verify the inclusion certificate. + * @param {PredicateVerifierService} predicateVerifier Verifier for embedded predicates. + * @param {MintJustificationVerifierService} mintJustificationVerifier Verifier for the mint justification. + * @param {CertifiedMintTransaction} genesis Genesis mint transaction. + * @returns {Promise} New token. + * @throws {VerificationError} If the genesis does not verify. + */ public static async mint( trustBase: RootTrustBase, predicateVerifier: PredicateVerifierService, @@ -83,6 +118,11 @@ export class Token { return token; } + /** + * Convert Token to CBOR bytes. + * + * @returns {Uint8Array} CBOR bytes. + */ public toCBOR(): Uint8Array { return CborSerializer.encodeTag( Token.CBOR_TAG, @@ -94,6 +134,9 @@ export class Token { ); } + /** + * @returns {string} String representation of the token. + */ public toString(): string { return dedent` Token @@ -104,6 +147,15 @@ export class Token { ]`; } + /** + * Append token with certified transfer transaction. + * + * @param {RootTrustBase} trustBase Root trust base used to verify the inclusion certificate. + * @param {PredicateVerifierService} predicateVerifier Verifier for embedded predicates. + * @param {CertifiedTransferTransaction} transaction Transfer transaction to apply. + * @returns {Promise} Updated token including the new transaction. + * @throws {VerificationError} If the transfer transaction does not verify. + */ public async transfer( trustBase: RootTrustBase, predicateVerifier: PredicateVerifierService, @@ -120,6 +172,14 @@ export class Token { return new Token(this.genesis, transactions); } + /** + * Verify the genesis and every transfer in this token. + * + * @param {RootTrustBase} trustBase Root trust base used to verify inclusion certificates. + * @param {PredicateVerifierService} predicateVerifier Verifier for embedded predicates. + * @param {MintJustificationVerifierService} mintJustificationVerifier Verifier for the mint justification. + * @returns {Promise>} Aggregated verification result. + */ public async verify( trustBase: RootTrustBase, predicateVerifier: PredicateVerifierService, diff --git a/src/transaction/TokenId.ts b/src/transaction/TokenId.ts index 5201849..a86e834 100644 --- a/src/transaction/TokenId.ts +++ b/src/transaction/TokenId.ts @@ -8,25 +8,42 @@ import { areUint8ArraysEqual } from '../util/TypedArrayUtils.js'; * Globally unique identifier of a token. */ export class TokenId { - /** - * @param _bytes Byte representation of the identifier - */ public constructor(private readonly _bytes: Uint8Array) { this._bytes = new Uint8Array(_bytes); } + /** + * @returns {Uint8Array} Copy of the identifier bytes. + */ public get bytes(): Uint8Array { return new Uint8Array(this._bytes); } + /** + * Create TokenId from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {TokenId} Decoded token id. + */ public static fromCBOR(bytes: Uint8Array): TokenId { return new TokenId(CborDeserializer.decodeByteString(bytes)); } + /** + * Generate a fresh random TokenId. + * + * @returns {TokenId} New token id with random 32-byte payload. + */ public static generate(): TokenId { return new TokenId(crypto.getRandomValues(new Uint8Array(32))); } + /** + * Equality check against another value. + * + * @param {unknown} o Other value. + * @returns {boolean} True if `o` is a TokenId with the same bytes. + */ public equals(o: unknown): boolean { if (this === o) { return true; @@ -40,18 +57,26 @@ export class TokenId { } /** - * Converts the TokenId to a bitstring representation. + * Convert the TokenId to a bit-string representation. + * + * @returns {BitString} Bit-string view of the identifier bytes. */ public toBitString(): BitString { return BitString.fromBytes(this._bytes); } - /** CBOR serialisation. */ + /** + * Convert TokenId to CBOR bytes. + * + * @returns {Uint8Array} CBOR bytes. + */ public toCBOR(): Uint8Array { return CborSerializer.encodeByteString(this._bytes); } - /** Convert instance to readable string */ + /** + * @returns {string} Human-readable representation of the token id. + */ public toString(): string { return `TokenId[${HexConverter.encode(this._bytes)}]`; } diff --git a/src/transaction/TokenType.ts b/src/transaction/TokenType.ts index 9ea3e08..6a901ed 100644 --- a/src/transaction/TokenType.ts +++ b/src/transaction/TokenType.ts @@ -4,31 +4,48 @@ import { HexConverter } from '../util/HexConverter.js'; /** Unique identifier describing the type/category of a token. */ export class TokenType { - /** - * @param _bytes Byte representation of the token type - */ public constructor(private readonly _bytes: Uint8Array) { this._bytes = new Uint8Array(_bytes); } + /** + * @returns {Uint8Array} Copy of the token type bytes. + */ public get bytes(): Uint8Array { return new Uint8Array(this._bytes); } + /** + * Create TokenType from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {TokenType} Decoded token type. + */ public static fromCBOR(bytes: Uint8Array): TokenType { return new TokenType(CborDeserializer.decodeByteString(bytes)); } + /** + * Generate a fresh random TokenType. + * + * @returns {TokenType} New token type with random 32-byte payload. + */ public static generate(): TokenType { return new TokenType(crypto.getRandomValues(new Uint8Array(32))); } - /** CBOR serialization. */ + /** + * Convert TokenType to CBOR bytes. + * + * @returns {Uint8Array} CBOR bytes. + */ public toCBOR(): Uint8Array { return CborSerializer.encodeByteString(this._bytes); } - /** Convert instance to readable string */ + /** + * @returns {string} Human-readable representation of the token type. + */ public toString(): string { return `TokenType[${HexConverter.encode(this._bytes)}]`; } diff --git a/src/transaction/TransferTransaction.ts b/src/transaction/TransferTransaction.ts index 4919ca6..14e7385 100644 --- a/src/transaction/TransferTransaction.ts +++ b/src/transaction/TransferTransaction.ts @@ -15,6 +15,9 @@ import { CborSerializer } from '../serialization/cbor/CborSerializer.js'; import { HexConverter } from '../util/HexConverter.js'; import { dedent } from '../util/StringUtils.js'; +/** + * Token transfer transaction. + */ export class TransferTransaction implements ITransaction { public static readonly CBOR_TAG = 39045n; private static readonly VERSION = 1n; @@ -27,18 +30,36 @@ export class TransferTransaction implements ITransaction { private readonly _data: Uint8Array | null, ) {} + /** + * @returns {Uint8Array|null} Copy of the data payload, or `null` if absent. + */ public get data(): Uint8Array | null { return this._data ? new Uint8Array(this._data) : null; } + /** + * @returns {Uint8Array} Copy of the state mask. + */ public get stateMask(): Uint8Array { return new Uint8Array(this._stateMask); } + /** + * @returns {bigint} Wire-format version of this transaction. + */ public get version(): bigint { return TransferTransaction.VERSION; } + /** + * Create a TransferTransaction for the given token. + * + * @param {Token} token Token being transferred (last transaction is used as the source). + * @param {IPredicate} recipient Predicate that will lock the new state. + * @param {Uint8Array} stateMask State mask mixed into the new state hash. + * @param {Uint8Array|null} data Optional data payload. + * @returns {Promise} New transfer transaction. + */ public static async create( token: Token, recipient: IPredicate, @@ -58,6 +79,14 @@ export class TransferTransaction implements ITransaction { ); } + /** + * Create TransferTransaction from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @param {Token} token Token providing context for the transfer transaction. + * @returns {Promise} Decoded transaction. + * @throws {CborError} On wrong tag or unsupported version. + */ public static fromCBOR(bytes: Uint8Array, token: Token): Promise { const tag = CborDeserializer.decodeTag(bytes); if (tag.tag !== TransferTransaction.CBOR_TAG) { @@ -78,6 +107,9 @@ export class TransferTransaction implements ITransaction { ); } + /** + * @inheritDoc + */ public calculateStateHash(): Promise { return new DataHasher(HashAlgorithm.SHA256) .update( @@ -89,10 +121,16 @@ export class TransferTransaction implements ITransaction { .digest(); } + /** + * @inheritDoc + */ public calculateTransactionHash(): Promise { return new DataHasher(HashAlgorithm.SHA256).update(this.toCBOR()).digest(); } + /** + * @inheritDoc + */ public toCBOR(): Uint8Array { return CborSerializer.encodeTag( TransferTransaction.CBOR_TAG, @@ -105,6 +143,14 @@ export class TransferTransaction implements ITransaction { ); } + /** + * Bundle this transaction with its inclusion proof into a CertifiedTransferTransaction. + * + * @param {RootTrustBase} trustBase Root trust base used to verify the inclusion certificate. + * @param {PredicateVerifierService} predicateVerifier Verifier for any embedded predicates. + * @param {InclusionProof} inclusionProof Inclusion proof for this transaction. + * @returns {Promise} Verified certified transaction. + */ public toCertifiedTransaction( trustBase: RootTrustBase, predicateVerifier: PredicateVerifierService, @@ -113,6 +159,9 @@ export class TransferTransaction implements ITransaction { return CertifiedTransferTransaction.fromTransaction(trustBase, predicateVerifier, this, inclusionProof); } + /** + * @returns {string} String representation of the transaction. + */ public toString(): string { return dedent` TransferTransaction diff --git a/src/transaction/verification/IMintJustificationVerifier.ts b/src/transaction/verification/IMintJustificationVerifier.ts index 32bbce5..c5da1de 100644 --- a/src/transaction/verification/IMintJustificationVerifier.ts +++ b/src/transaction/verification/IMintJustificationVerifier.ts @@ -3,9 +3,23 @@ import { MintJustificationVerifierService } from './MintJustificationVerifierSer import { VerificationResult } from '../../verification/VerificationResult.js'; import { VerificationStatus } from '../../verification/VerificationStatus.js'; +/** + * Verifier for a single mint justification CBOR tag. Plugged into + * {@link MintJustificationVerifierService} which dispatches by tag. + */ export interface IMintJustificationVerifier { + /** + * CBOR tag this verifier handles. + */ get tag(): bigint; + /** + * Verify the justification embedded in `transaction`. + * + * @param {CertifiedMintTransaction} transaction Transaction whose justification to verify. + * @param {MintJustificationVerifierService} mintJustificationVerifierService Service available for recursive verification. + * @returns {Promise>} Verification outcome. + */ verify( transaction: CertifiedMintTransaction, mintJustificationVerifierService: MintJustificationVerifierService, diff --git a/src/transaction/verification/MintJustificationVerifierService.ts b/src/transaction/verification/MintJustificationVerifierService.ts index f0826a7..6a2b866 100644 --- a/src/transaction/verification/MintJustificationVerifierService.ts +++ b/src/transaction/verification/MintJustificationVerifierService.ts @@ -4,9 +4,20 @@ import { CborDeserializer } from '../../serialization/cbor/CborDeserializer.js'; import { VerificationResult } from '../../verification/VerificationResult.js'; import { VerificationStatus } from '../../verification/VerificationStatus.js'; +/** + * Registry that dispatches mint justification verification to the right + * {@link IMintJustificationVerifier} based on the justification's CBOR tag. + */ export class MintJustificationVerifierService { private readonly verifiers: Map = new Map(); + /** + * Register a verifier for its declared tag. + * + * @param {IMintJustificationVerifier} verifier Verifier to register. + * @returns {MintJustificationVerifierService} This service for chaining. + * @throws {Error} If a verifier is already registered for the tag. + */ public register(verifier: IMintJustificationVerifier): this { if (this.verifiers.has(verifier.tag)) { throw new Error(`Duplicate mint justification verifier for tag ${verifier.tag}.`); @@ -16,6 +27,12 @@ export class MintJustificationVerifierService { return this; } + /** + * Verify given mint justification with registered verifiers. + * + * @param {CertifiedMintTransaction} transaction Certified mint transaction whose justification to verify. + * @returns {Promise>} Verification outcome. + */ public async verify(transaction: CertifiedMintTransaction): Promise> { const bytes = transaction.justification; if (!bytes) { diff --git a/src/transaction/verification/rule/CertifiedMintTransactionVerificationRule.ts b/src/transaction/verification/rule/CertifiedMintTransactionVerificationRule.ts index 75a80b5..f0543c6 100644 --- a/src/transaction/verification/rule/CertifiedMintTransactionVerificationRule.ts +++ b/src/transaction/verification/rule/CertifiedMintTransactionVerificationRule.ts @@ -13,6 +13,15 @@ import { MintJustificationVerifierService } from '../MintJustificationVerifierSe * Genesis verification rule. */ export class CertifiedMintTransactionVerificationRule { + /** + * Verify a certified mint genesis. + * + * @param {RootTrustBase} trustBase Root trust base used to verify the inclusion proof. + * @param {PredicateVerifierService} predicateVerifier Predicate verifier service. + * @param {MintJustificationVerifierService} mintJustificationVerifier Verifier for the mint justification. + * @param {CertifiedMintTransaction} genesis Certified mint transaction to verify. + * @returns {Promise>} Verification outcome. + */ public static async verify( trustBase: RootTrustBase, predicateVerifier: PredicateVerifierService, diff --git a/src/transaction/verification/rule/CertifiedTransferTransactionVerificationRule.ts b/src/transaction/verification/rule/CertifiedTransferTransactionVerificationRule.ts index f1216db..a5e4615 100644 --- a/src/transaction/verification/rule/CertifiedTransferTransactionVerificationRule.ts +++ b/src/transaction/verification/rule/CertifiedTransferTransactionVerificationRule.ts @@ -9,6 +9,14 @@ import { CertifiedTransferTransaction } from '../../CertifiedTransferTransaction * Transfer transaction verification rule. */ export class CertifiedTransferTransactionVerificationRule { + /** + * Verify a certified transfer transaction. + * + * @param {RootTrustBase} trustBase Root trust base. + * @param {PredicateVerifierService} predicateVerifier Predicate verifier service. + * @param {CertifiedTransferTransaction} transaction Transfer transaction to verify. + * @returns {Promise>} Verification outcome. + */ public static async verify( trustBase: RootTrustBase, predicateVerifier: PredicateVerifierService, diff --git a/src/transaction/verification/rule/CertifiedUnicityIdMintTransactionVerificationRule.ts b/src/transaction/verification/rule/CertifiedUnicityIdMintTransactionVerificationRule.ts index 35d9b31..d561664 100644 --- a/src/transaction/verification/rule/CertifiedUnicityIdMintTransactionVerificationRule.ts +++ b/src/transaction/verification/rule/CertifiedUnicityIdMintTransactionVerificationRule.ts @@ -18,11 +18,13 @@ import { VerificationStatus } from '../../../verification/VerificationStatus.js' */ export class CertifiedUnicityIdMintTransactionVerificationRule { /** - * @param trustBase Root trust base used to verify the inclusion proof. - * @param predicateVerifier Predicate verifier service. - * @param genesis Certified unicity-id mint transaction to verify. - * @param issuerPublicKey Trusted issuer public key to pin the lock script - * against, or `null` to skip the issuer pin. + * Verify the inclusion proof and (optionally) the issuer of the genesis transaction. + * + * @param {RootTrustBase} trustBase Root trust base used to verify the inclusion proof. + * @param {PredicateVerifierService} predicateVerifier Predicate verifier service. + * @param {CertifiedUnicityIdMintTransaction} genesis Certified unicity-id mint transaction to verify. + * @param {Uint8Array|null} issuerPublicKey Trusted issuer public key to pin the lock script against, or `null` to skip the issuer pin. + * @returns {Promise>} Verification outcome. */ public static async verify( trustBase: RootTrustBase, diff --git a/src/transaction/verification/rule/InclusionProofVerificationRule.ts b/src/transaction/verification/rule/InclusionProofVerificationRule.ts index d97a089..e90bbd7 100644 --- a/src/transaction/verification/rule/InclusionProofVerificationRule.ts +++ b/src/transaction/verification/rule/InclusionProofVerificationRule.ts @@ -28,6 +28,15 @@ export enum InclusionProofVerificationStatus { * Genesis verification rule. */ export class InclusionProofVerificationRule { + /** + * Verify an inclusion proof for a transaction. + * + * @param {RootTrustBase} trustBase Root trust base. + * @param {PredicateVerifierService} predicateVerifierFactory Predicate verifier service. + * @param {InclusionProof} inclusionProof Inclusion proof to verify. + * @param {ITransaction} transaction Transaction the proof should attest to. + * @returns {Promise>} Verification outcome. + */ public static async verify( trustBase: RootTrustBase, predicateVerifierFactory: PredicateVerifierService, diff --git a/src/transaction/verification/rule/ShardIdMatchesStateIdRule.ts b/src/transaction/verification/rule/ShardIdMatchesStateIdRule.ts index 753446a..bfdbd7a 100644 --- a/src/transaction/verification/rule/ShardIdMatchesStateIdRule.ts +++ b/src/transaction/verification/rule/ShardIdMatchesStateIdRule.ts @@ -3,7 +3,18 @@ import { StateId } from '../../../api/StateId.js'; import { VerificationResult } from '../../../verification/VerificationResult.js'; import { VerificationStatus } from '../../../verification/VerificationStatus.js'; +/** + * Verifies that a state id falls under the shard identified by a shard tree + * certificate. + */ export class ShardIdMatchesStateIdRule { + /** + * Verify that `stateId` is prefixed by the certificate's shard id. + * + * @param {StateId} stateId State id under verification. + * @param {ShardTreeCertificate} shardTreeCertificate Shard tree certificate carrying the expected shard. + * @returns {VerificationResult} OK if the shard is empty or a prefix, FAIL otherwise. + */ public static verify( stateId: StateId, shardTreeCertificate: ShardTreeCertificate, diff --git a/src/unicity-id/CertifiedUnicityIdMintTransaction.ts b/src/unicity-id/CertifiedUnicityIdMintTransaction.ts index 721b473..fd71db2 100644 --- a/src/unicity-id/CertifiedUnicityIdMintTransaction.ts +++ b/src/unicity-id/CertifiedUnicityIdMintTransaction.ts @@ -17,48 +17,84 @@ import { } from '../transaction/verification/rule/InclusionProofVerificationRule.js'; import { dedent } from '../util/StringUtils.js'; +/** + * Unicity-id mint transaction bundled with a verified inclusion proof. + */ export class CertifiedUnicityIdMintTransaction implements ITransaction { public constructor( private readonly transaction: UnicityIdMintTransaction, public readonly inclusionProof: InclusionProof, ) {} + /** + * @returns {Uint8Array} Data payload of the inner transaction. + */ public get data(): Uint8Array { return this.transaction.data; } + /** + * @returns {EncodedPredicate} Lock script of the inner transaction. + */ public get lockScript(): EncodedPredicate { return this.transaction.lockScript; } + /** + * @returns {EncodedPredicate} Recipient predicate of the inner transaction. + */ public get recipient(): EncodedPredicate { return this.transaction.recipient; } + /** + * @returns {DataHash} Source state hash of the inner transaction. + */ public get sourceStateHash(): DataHash { return this.transaction.sourceStateHash; } + /** + * @returns {Uint8Array} State mask of the inner transaction. + */ public get stateMask(): Uint8Array { return this.transaction.stateMask; } + /** + * @returns {SignaturePredicate} Target predicate of the inner transaction. + */ public get targetPredicate(): SignaturePredicate { return this.transaction.targetPredicate; } + /** + * @returns {TokenId} Token id of the inner transaction. + */ public get tokenId(): TokenId { return this.transaction.tokenId; } + /** + * @returns {TokenType} Token type of the inner transaction. + */ public get tokenType(): TokenType { return this.transaction.tokenType; } + /** + * @returns {UnicityId} Unicity id of the inner transaction. + */ public get unicityId(): UnicityId { return this.transaction.unicityId; } + /** + * Create CertifiedUnicityIdMintTransaction from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {Promise} Decoded certified transaction. + */ public static async fromCBOR(bytes: Uint8Array): Promise { const data = CborDeserializer.decodeArray(bytes, 2); return new CertifiedUnicityIdMintTransaction( @@ -67,6 +103,16 @@ export class CertifiedUnicityIdMintTransaction implements ITransaction { ); } + /** + * Create CertifiedUnicityIdMintTransaction from unicity-id mint transaction and inclusion proof. + * + * @param {RootTrustBase} trustBase Root trust base. + * @param {PredicateVerifierService} predicateVerifier Predicate verifier service. + * @param {UnicityIdMintTransaction} transaction Transaction to certify. + * @param {InclusionProof} inclusionProof Inclusion proof for the transaction. + * @returns {Promise} Verified certified transaction. + * @throws {Error} If the inclusion proof does not verify. + */ public static async fromTransaction( trustBase: RootTrustBase, predicateVerifier: PredicateVerifierService, @@ -86,18 +132,30 @@ export class CertifiedUnicityIdMintTransaction implements ITransaction { return new CertifiedUnicityIdMintTransaction(transaction, inclusionProof); } + /** + * @inheritDoc + */ public calculateStateHash(): Promise { return this.transaction.calculateStateHash(); } + /** + * @inheritDoc + */ public calculateTransactionHash(): Promise { return this.transaction.calculateTransactionHash(); } + /** + * @inheritDoc + */ public toCBOR(): Uint8Array { return CborSerializer.encodeArray(this.transaction.toCBOR(), this.inclusionProof.toCBOR()); } + /** + * @returns {string} String representation of the certified transaction. + */ public toString(): string { return dedent` CertifiedUnicityIdMintTransaction diff --git a/src/unicity-id/UnicityId.ts b/src/unicity-id/UnicityId.ts index eeb63ee..86df226 100644 --- a/src/unicity-id/UnicityId.ts +++ b/src/unicity-id/UnicityId.ts @@ -4,12 +4,22 @@ import { CborDeserializer } from '../serialization/cbor/CborDeserializer.js'; import { CborSerializer } from '../serialization/cbor/CborSerializer.js'; import { TokenId } from '../transaction/TokenId.js'; +/** + * Human-readable identifier (`@domain/name`) people use to send tokens. + * The bound {@link TokenId} can be derived from it via {@link toTokenId}. + */ export class UnicityId { public constructor( public readonly name: string, public readonly domain: string | null = null, ) {} + /** + * Create UnicityId from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {UnicityId} Decoded unicity id. + */ public static fromCBOR(bytes: Uint8Array): UnicityId { const data = CborDeserializer.decodeArray(bytes, 2); return new UnicityId( @@ -18,6 +28,11 @@ export class UnicityId { ); } + /** + * Convert UnicityId to CBOR bytes. + * + * @returns {Uint8Array} CBOR bytes. + */ public toCBOR(): Uint8Array { return CborSerializer.encodeArray( CborSerializer.encodeTextString(this.name), @@ -25,10 +40,18 @@ export class UnicityId { ); } + /** + * @returns {string} `@domain/name` representation of the unicity id. + */ public toString(): string { return `@${this.domain ? `${this.domain}/` : ''}${this.name}`; } + /** + * Derive the {@link TokenId} bound to this unicity id. + * + * @returns {Promise} Derived token id. + */ public async toTokenId(): Promise { const hash = await new DataHasher(HashAlgorithm.SHA256) .update( diff --git a/src/unicity-id/UnicityIdMintTransaction.ts b/src/unicity-id/UnicityIdMintTransaction.ts index 9a2a4e0..cc4f194 100644 --- a/src/unicity-id/UnicityIdMintTransaction.ts +++ b/src/unicity-id/UnicityIdMintTransaction.ts @@ -18,6 +18,9 @@ import { TokenId } from '../transaction/TokenId.js'; import { TokenType } from '../transaction/TokenType.js'; import { dedent } from '../util/StringUtils.js'; +/** + * Mint transaction for a unicity-id token. + */ export class UnicityIdMintTransaction implements ITransaction { public static readonly CBOR_TAG = 39041n; private static readonly VERSION = 1n; @@ -32,18 +35,37 @@ export class UnicityIdMintTransaction implements ITransaction { public readonly unicityId: UnicityId, ) {} + /** + * @returns {Uint8Array} CBOR-encoded target predicate. + */ public get data(): Uint8Array { return EncodedPredicate.fromPredicate(this.targetPredicate).toCBOR(); } + /** + * @returns {Uint8Array} State mask used when computing the resulting state hash. + */ public get stateMask(): Uint8Array { return new Uint8Array(this.tokenId.bytes); } + /** + * @returns {bigint} Wire-format version of this transaction. + */ public get version(): bigint { return UnicityIdMintTransaction.VERSION; } + /** + * Create a UnicityIdMintTransaction. + * + * @param {SignaturePredicate} lockScript Issuer lock script. + * @param {IPredicate} recipient Predicate that will lock the minted state. + * @param {UnicityId} unicityId Unicity id being minted. + * @param {TokenType} tokenType Token type. + * @param {SignaturePredicate} targetPredicate Predicate the unicity id resolves to. + * @returns {Promise} New mint transaction. + */ public static async create( lockScript: SignaturePredicate, recipient: IPredicate, @@ -64,6 +86,13 @@ export class UnicityIdMintTransaction implements ITransaction { ); } + /** + * Create UnicityIdMintTransaction from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {Promise} Decoded transaction. + * @throws {CborError} On wrong tag or unsupported version. + */ public static fromCBOR(bytes: Uint8Array): Promise { const tag = CborDeserializer.decodeTag(bytes); if (tag.tag !== UnicityIdMintTransaction.CBOR_TAG) { @@ -85,6 +114,9 @@ export class UnicityIdMintTransaction implements ITransaction { ); } + /** + * @inheritDoc + */ public calculateStateHash(): Promise { return new DataHasher(HashAlgorithm.SHA256) .update( @@ -96,10 +128,16 @@ export class UnicityIdMintTransaction implements ITransaction { .digest(); } + /** + * @inheritDoc + */ public calculateTransactionHash(): Promise { return new DataHasher(HashAlgorithm.SHA256).update(this.toCBOR()).digest(); } + /** + * @inheritDoc + */ public toCBOR(): Uint8Array { return CborSerializer.encodeTag( UnicityIdMintTransaction.CBOR_TAG, @@ -114,6 +152,14 @@ export class UnicityIdMintTransaction implements ITransaction { ); } + /** + * Bundle this transaction with its inclusion proof. + * + * @param {RootTrustBase} trustBase Root trust base used to verify the inclusion certificate. + * @param {PredicateVerifierService} predicateVerifier Verifier for embedded predicates. + * @param {InclusionProof} inclusionProof Inclusion proof for this transaction. + * @returns {Promise} Verified certified transaction. + */ public toCertifiedTransaction( trustBase: RootTrustBase, predicateVerifier: PredicateVerifierService, @@ -122,6 +168,9 @@ export class UnicityIdMintTransaction implements ITransaction { return CertifiedUnicityIdMintTransaction.fromTransaction(trustBase, predicateVerifier, this, inclusionProof); } + /** + * @returns {string} String representation of the transaction. + */ public toString(): string { return dedent` UnicityIdMintTransaction diff --git a/src/unicity-id/UnicityIdToken.ts b/src/unicity-id/UnicityIdToken.ts index eef00b3..fe2d79f 100644 --- a/src/unicity-id/UnicityIdToken.ts +++ b/src/unicity-id/UnicityIdToken.ts @@ -11,23 +11,48 @@ import { VerificationError } from '../verification/VerificationError.js'; import { VerificationResult } from '../verification/VerificationResult.js'; import { VerificationStatus } from '../verification/VerificationStatus.js'; +/** + * Token whose id is derived from a {@link UnicityId}, used to authorize + * spends bound to that unicity id. + */ export class UnicityIdToken { private constructor(public readonly genesis: CertifiedUnicityIdMintTransaction) {} + /** + * @returns {TokenId} Token id. + */ public get id(): TokenId { return this.genesis.tokenId; } + /** + * @returns {TokenType} Token type. + */ public get type(): TokenType { return this.genesis.tokenType; } + /** + * Create UnicityIdToken from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {Promise} Decoded token. + */ public static async fromCBOR(bytes: Uint8Array): Promise { const data = CborDeserializer.decodeArray(bytes, 1); return new UnicityIdToken(await CertifiedUnicityIdMintTransaction.fromCBOR(data[0])); } + /** + * Create a UnicityIdToken from a verified genesis transaction. + * + * @param {RootTrustBase} trustBase Root trust base used to verify the inclusion certificate. + * @param {PredicateVerifierService} predicateVerifier Verifier for embedded predicates. + * @param {CertifiedUnicityIdMintTransaction} genesis Genesis mint transaction. + * @returns {Promise} New token. + * @throws {VerificationError} If the genesis does not verify. + */ public static async mint( trustBase: RootTrustBase, predicateVerifier: PredicateVerifierService, @@ -46,16 +71,32 @@ export class UnicityIdToken { return token; } + /** + * Convert UnicityIdToken to CBOR bytes. + * + * @returns {Uint8Array} CBOR bytes. + */ public toCBOR(): Uint8Array { return CborSerializer.encodeArray(this.genesis.toCBOR()); } + /** + * @returns {string} String representation of the token. + */ public toString(): string { return dedent` UnicityIdToken ${this.genesis.toString()}`; } + /** + * Verify this token's against the trust base and issuer key. + * + * @param {RootTrustBase} trustBase Root trust base. + * @param {PredicateVerifierService} predicateVerifier Verifier for embedded predicates. + * @param {Uint8Array} issuerPublicKey Expected issuer public key. + * @returns {Promise>} Verification outcome. + */ public async verify( trustBase: RootTrustBase, predicateVerifier: PredicateVerifierService, diff --git a/src/util/BigintConverter.ts b/src/util/BigintConverter.ts index bc8ddcc..fe8cabe 100644 --- a/src/util/BigintConverter.ts +++ b/src/util/BigintConverter.ts @@ -1,3 +1,6 @@ +/** + * Big-endian conversion between {@link bigint} values and byte arrays. + */ export class BigintConverter { /** * Convert bytes to unsigned long diff --git a/src/util/BitString.ts b/src/util/BitString.ts index 9958ea3..679e9d6 100644 --- a/src/util/BitString.ts +++ b/src/util/BitString.ts @@ -1,6 +1,11 @@ import { BigintConverter } from './BigintConverter.js'; import { HexConverter } from './HexConverter.js'; +/** + * Bit string backed by a {@link bigint} with a leading sentinel bit, so that + * leading zero bits are preserved across conversions. Provides byte-order and + * bit-order helpers used by sparse Merkle tree routing. + */ export class BitString { /** * Represents a bit string as a bigint. @@ -12,24 +17,33 @@ export class BitString { } /** - * Creates a BitString from raw bytes with no bit reordering. + * Create a BitString from raw bytes with no bit reordering. * Bigint bit 0 is the LSB of the last byte. + * + * @param {Uint8Array} data Input bytes. + * @returns {BitString} New bit string. */ public static fromBytes(data: Uint8Array): BitString { return new BitString(new Uint8Array(data)); } /** - * Creates a BitString for LSB-first tree routing with reversed byte order. + * Create a BitString for LSB-first tree routing with reversed byte order. * Bigint bit 0 = bit 0 (LSB) of data[0], matching getBitAtDepth LSB convention. + * + * @param {Uint8Array} data Input bytes. + * @returns {BitString} New bit string. */ public static fromBytesReversedLSB(data: Uint8Array): BitString { return new BitString(new Uint8Array(data).reverse()); } /** - * Creates a BitString for MSB-first tree routing with reversed byte order. + * Create a BitString for MSB-first tree routing with reversed byte order. * Bigint bit 0 = bit 7 (MSB) of data[0], matching getBitAtDepth MSB convention. + * + * @param {Uint8Array} data Input bytes. + * @returns {BitString} New bit string. */ public static fromBytesReversedMSB(data: Uint8Array): BitString { return new BitString( @@ -50,25 +64,28 @@ export class BitString { } /** - * Converts BitString to bigint by adding a leading byte 1 to input byte array. + * Convert BitString to bigint by adding a leading byte 1 to input byte array. * This is to ensure that the bigint will retain the leading zero bits. - * @returns {bigint} The bigint representation of the bit string + * + * @returns {bigint} Bigint representation of the bit string. */ public toBigInt(): bigint { return this.value; } /** - * Converts bit string to Uint8Array. - * @returns {Uint8Array} The Uint8Array representation of the bit string + * Convert bit string to Uint8Array. + * + * @returns {Uint8Array} Byte representation of the bit string. */ public toBytes(): Uint8Array { return BigintConverter.encode(this.value).slice(1); } /** - * Converts bit string to string. - * @returns {string} The string representation of the bit string + * Convert bit string to string. + * + * @returns {string} Binary string representation of the bit string. */ public toString(): string { return this.value.toString(2).slice(1); diff --git a/src/util/HexConverter.ts b/src/util/HexConverter.ts index 82ec9f5..35cc829 100644 --- a/src/util/HexConverter.ts +++ b/src/util/HexConverter.ts @@ -1,10 +1,15 @@ import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js'; +/** + * Conversion between hex strings and byte arrays. Accepts an optional + * `0x`/`0X` prefix on decode and produces lowercase, unprefixed hex on encode. + */ export class HexConverter { /** - * Convert hex string to bytes - * @param value hex string - * @returns {Uint8Array} byte array + * Convert hex string to bytes. + * + * @param {string} value Hex string, optionally prefixed with `0x` or `0X`. + * @returns {Uint8Array} Decoded byte array. */ public static decode(value: string): Uint8Array { if (value.startsWith('0x') || value.startsWith('0X')) { @@ -14,9 +19,10 @@ export class HexConverter { } /** - * Convert byte array to hex - * @param {Uint8Array} data byte array - * @returns string hex string + * Convert byte array to hex. + * + * @param {Uint8Array} data Byte array. + * @returns {string} Lowercase, unprefixed hex string. */ public static encode(data: Uint8Array): string { return bytesToHex(data); diff --git a/src/util/InclusionProofUtils.ts b/src/util/InclusionProofUtils.ts index 651a3e9..d59b8cd 100644 --- a/src/util/InclusionProofUtils.ts +++ b/src/util/InclusionProofUtils.ts @@ -10,6 +10,10 @@ import { InclusionProofVerificationStatus, } from '../transaction/verification/rule/InclusionProofVerificationRule.js'; +/** + * Thrown by {@link waitInclusionProof} when the polling sleep is aborted + * (typically because the caller's abort signal fired). + */ class SleepError extends Error { public constructor(message: string) { super(message); @@ -31,6 +35,19 @@ function sleep(ms: number, signal: AbortSignal): Promise { }); } +/** + * Poll the aggregator until a valid inclusion proof for the given transaction + * is available, or the abort signal fires. + * + * @param {StateTransitionClient} client Client used to fetch inclusion proofs. + * @param {RootTrustBase} trustBase Root trust base used to verify the inclusion certificate. + * @param {PredicateVerifierService} predicateVerifier Verifier used to check the transaction predicate. + * @param {ITransaction} transaction Transaction whose inclusion is being awaited. + * @param {AbortSignal} signal Abort signal that terminates polling. Defaults to a 10s timeout. + * @param {number} interval Delay between polls in milliseconds. Defaults to 1000. + * @returns {Promise} Verified inclusion proof. + * @throws {SleepError} If the abort signal fires while sleeping between polls. + */ export async function waitInclusionProof( client: StateTransitionClient, trustBase: RootTrustBase, diff --git a/src/util/TypedArrayUtils.ts b/src/util/TypedArrayUtils.ts index 3fe0153..6f81020 100644 --- a/src/util/TypedArrayUtils.ts +++ b/src/util/TypedArrayUtils.ts @@ -1,3 +1,11 @@ +/** + * Returns true if both arrays are the same reference, both nullish, or contain + * the same bytes in the same order. + * + * @param {Uint8Array|null|undefined} a First array. + * @param {Uint8Array|null|undefined} b Second array. + * @returns {boolean} True if the arrays are equal. + */ export function areUint8ArraysEqual(a: Uint8Array | null | undefined, b: Uint8Array | null | undefined): boolean { if (a === b) { return true; @@ -10,6 +18,14 @@ export function areUint8ArraysEqual(a: Uint8Array | null | undefined, b: Uint8Ar return compareUint8Arrays(a, b) === 0; } +/** + * Lexicographic comparison of two byte arrays. Arrays of different lengths + * are ordered by length. + * + * @param {Uint8Array} a First array. + * @param {Uint8Array} b Second array. + * @returns {number} Negative if `a` sorts before `b`, positive if after, zero if equal. + */ export function compareUint8Arrays(a: Uint8Array, b: Uint8Array): number { if (a.length !== b.length) { return a.length - b.length; diff --git a/src/verification/IVerificationContext.ts b/src/verification/IVerificationContext.ts index c63070a..57f66ef 100644 --- a/src/verification/IVerificationContext.ts +++ b/src/verification/IVerificationContext.ts @@ -1,2 +1,6 @@ +/** + * Marker interface for objects passed to verification rules. Rule + * implementations narrow the type to their own required fields. + */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface IVerificationContext {} diff --git a/src/verification/VerificationError.ts b/src/verification/VerificationError.ts index 11a6b10..30051d1 100644 --- a/src/verification/VerificationError.ts +++ b/src/verification/VerificationError.ts @@ -4,12 +4,6 @@ import { VerificationResult } from './VerificationResult.js'; * Exception thrown when a verification fails. */ export class VerificationError extends Error { - /** - * Create exception with message and verification result. - * - * @param {string} message message - * @param {VerificationResult} verificationResult verification result - */ public constructor( message: string, public readonly verificationResult: VerificationResult, diff --git a/src/verification/VerificationResult.ts b/src/verification/VerificationResult.ts index d9cd72e..deee623 100644 --- a/src/verification/VerificationResult.ts +++ b/src/verification/VerificationResult.ts @@ -1,7 +1,11 @@ import { dedent } from '../util/StringUtils.js'; /** - * Verification result implementation. + * Outcome of one verification rule. Carries the rule name, a status of any + * type `S`, an optional message, and child results from nested rules so that + * a full verification trace can be rendered. + * + * @typeParam S Status type produced by the rule. */ export class VerificationResult { public constructor( @@ -13,6 +17,9 @@ export class VerificationResult { this.results = results.slice(); } + /** + * @returns {string} String representation of the verification result. + */ public toString(): string { return dedent` VerificationResult[${this.rule}]: From aef6ffaa775db2d037685d08fc65db3659b0c0bc Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Fri, 22 May 2026 14:43:20 +0300 Subject: [PATCH 02/13] #115 Add network id and dervice token id from it --- src/api/CertificationResponse.ts | 2 - src/api/NetworkId.ts | 44 ++++++ src/payment/SplitMintJustificationVerifier.ts | 9 ++ src/payment/SplitToken.ts | 20 +++ src/payment/SplitTokenRequest.ts | 34 +++++ src/payment/TokenSplit.ts | 135 +++++------------- src/transaction/CertifiedMintTransaction.ts | 18 ++- src/transaction/MintTransaction.ts | 34 +++-- src/transaction/TokenId.ts | 18 +++ src/transaction/TokenSalt.ts | 85 +++++++++++ tests/e2e/ShardAggregatorClient.ts | 18 +-- tests/examples/mint/ExampleTest.ts | 5 +- tests/examples/split/ExampleTest.ts | 48 ++++--- tests/examples/transfer/ExampleTest.ts | 5 +- tests/functional/payment/SplitBuilderTest.ts | 49 +++---- tests/unit/api/CertificationDataTest.ts | 8 +- tests/unit/api/InclusionProofTest.ts | 8 +- tests/unit/api/StateIdTest.ts | 8 +- tests/utils/TokenUtils.ts | 15 +- 19 files changed, 383 insertions(+), 180 deletions(-) create mode 100644 src/api/NetworkId.ts create mode 100644 src/payment/SplitToken.ts create mode 100644 src/payment/SplitTokenRequest.ts create mode 100644 src/transaction/TokenSalt.ts diff --git a/src/api/CertificationResponse.ts b/src/api/CertificationResponse.ts index da59963..67fecea 100644 --- a/src/api/CertificationResponse.ts +++ b/src/api/CertificationResponse.ts @@ -6,8 +6,6 @@ import { InvalidJsonStructureError } from '../InvalidJsonStructureError.js'; export enum CertificationStatus { /** The certification request was accepted and stored. */ SUCCESS = 'SUCCESS', - /** State identifier exists. */ - STATE_ID_EXISTS = 'STATE_ID_EXISTS', /** State identifier did not match the payload. */ STATE_ID_MISMATCH = 'STATE_ID_MISMATCH', /** Signature verification failed. */ diff --git a/src/api/NetworkId.ts b/src/api/NetworkId.ts new file mode 100644 index 0000000..4fdfbc1 --- /dev/null +++ b/src/api/NetworkId.ts @@ -0,0 +1,44 @@ +/** + * Unicity network identifier (α). Used to scope token ids and other + * network-bound values so they cannot be replayed across networks. + * + * `0` is reserved as an uninitialized sentinel and is rejected by + * {@link NetworkId.fromId}. + */ +export class NetworkId { + public static readonly LOCAL = new NetworkId(3n, 'LOCAL'); + public static readonly MAINNET = new NetworkId(1n, 'MAINNET'); + public static readonly TESTNET = new NetworkId(2n, 'TESTNET'); + + private constructor( + public readonly id: bigint, + public readonly name: string, + ) {} + + /** + * Look up a NetworkId by its numeric identifier. + * + * @param {number|bigint} id Numeric network identifier. + * @returns {NetworkId} Matching network identifier. + * @throws {Error} If `id` is `0`, negative, or not registered. + */ + public static fromId(id: number | bigint): NetworkId { + switch (BigInt(id)) { + case NetworkId.MAINNET.id: + return NetworkId.MAINNET; + case NetworkId.TESTNET.id: + return NetworkId.TESTNET; + case NetworkId.LOCAL.id: + return NetworkId.LOCAL; + default: + throw new Error(`Unknown network identifier: ${id}.`); + } + } + + /** + * @returns {string} Human-readable name of the network. + */ + public toString(): string { + return this.name; + } +} diff --git a/src/payment/SplitMintJustificationVerifier.ts b/src/payment/SplitMintJustificationVerifier.ts index 8c4b048..a03f9c1 100644 --- a/src/payment/SplitMintJustificationVerifier.ts +++ b/src/payment/SplitMintJustificationVerifier.ts @@ -59,6 +59,15 @@ export class SplitMintJustificationVerifier implements IMintJustificationVerifie ); } + if (transaction.networkId.id !== justification.token.genesis.networkId.id) { + return new VerificationResult( + 'SplitMintJustificationVerifier', + VerificationStatus.FAIL, + `Network identifier mismatch: mint is on ${transaction.networkId.toString()}, source token is on ${justification.token.genesis.networkId.toString()}.`, + [], + ); + } + const tokenVerificationResult = await justification.token.verify( this.trustBase, this.predicateVerifier, diff --git a/src/payment/SplitToken.ts b/src/payment/SplitToken.ts new file mode 100644 index 0000000..07aeb28 --- /dev/null +++ b/src/payment/SplitToken.ts @@ -0,0 +1,20 @@ +import { PaymentAssetCollection } from './asset/PaymentAssetCollection.js'; +import { SplitAssetProof } from './SplitAssetProof.js'; +import { NetworkId } from '../api/NetworkId.js'; +import { IPredicate } from '../predicate/IPredicate.js'; +import { TokenSalt } from '../transaction/TokenSalt.js'; +import { TokenType } from '../transaction/TokenType.js'; + +/** + * Realized split output: all data needed to mint the new token. + */ +export class SplitToken { + public constructor( + public readonly networkId: NetworkId, + public readonly recipient: IPredicate, + public readonly tokenType: TokenType, + public readonly salt: TokenSalt, + public readonly assets: PaymentAssetCollection, + public readonly proofs: SplitAssetProof[], + ) {} +} diff --git a/src/payment/SplitTokenRequest.ts b/src/payment/SplitTokenRequest.ts new file mode 100644 index 0000000..a757d79 --- /dev/null +++ b/src/payment/SplitTokenRequest.ts @@ -0,0 +1,34 @@ +import { PaymentAssetCollection } from './asset/PaymentAssetCollection.js'; +import { IPredicate } from '../predicate/IPredicate.js'; +import { TokenSalt } from '../transaction/TokenSalt.js'; +import { TokenType } from '../transaction/TokenType.js'; + +/** + * Request to mint one new token as part of a token split. + */ +export class SplitTokenRequest { + private constructor( + public readonly recipient: IPredicate, + public readonly tokenType: TokenType, + public readonly assets: PaymentAssetCollection, + public readonly salt: TokenSalt, + ) {} + + /** + * Create a SplitTokenRequest. + * + * @param {IPredicate} recipient Predicate that will lock the new token. + * @param {PaymentAssetCollection} assets Assets the new token will receive. + * @param {TokenType} tokenType Token type for the new token; defaults to a random token type. + * @param {TokenSalt} salt Salt for the new token; defaults to a random 32-byte salt. + * @returns {SplitTokenRequest} New request. + */ + public static create( + recipient: IPredicate, + assets: PaymentAssetCollection, + tokenType: TokenType = TokenType.generate(), + salt: TokenSalt = TokenSalt.generate(), + ): SplitTokenRequest { + return new SplitTokenRequest(recipient, tokenType, assets, salt); + } +} diff --git a/src/payment/TokenSplit.ts b/src/payment/TokenSplit.ts index f07a5f2..2baabbf 100644 --- a/src/payment/TokenSplit.ts +++ b/src/payment/TokenSplit.ts @@ -6,6 +6,8 @@ import { HashAlgorithm } from '../crypto/hash/HashAlgorithm.js'; import { HexConverter } from '../util/HexConverter.js'; import { TokenAssetValueMismatchError } from './error/TokenAssetValueMismatchError.js'; import { SplitAssetProof } from './SplitAssetProof.js'; +import { SplitToken } from './SplitToken.js'; +import { SplitTokenRequest } from './SplitTokenRequest.js'; import { SparseMerkleTree } from '../smt/plain/SparseMerkleTree.js'; import { SparseMerkleSumTree } from '../smt/sum/SparseMerkleSumTree.js'; import { SparseMerkleSumTreeRootNode } from '../smt/sum/SparseMerkleSumTreeRootNode.js'; @@ -13,89 +15,18 @@ import { Token } from '../transaction/Token.js'; import { TokenId } from '../transaction/TokenId.js'; import { TransferTransaction } from '../transaction/TransferTransaction.js'; import { AssetId } from './asset/AssetId.js'; -import { PaymentAssetCollection } from './asset/PaymentAssetCollection.js'; import { TokenAssetCountMismatchError } from './error/TokenAssetCountMismatchError.js'; import { BurnPredicate } from '../predicate/builtin/BurnPredicate.js'; /** - * Per-token entry in a {@link ProofMap}: a token id and its asset proofs. + * Result of splitting a token. */ -class ProofMapEntry { - private constructor( - public readonly tokenId: TokenId, - private readonly _proofs: SplitAssetProof[], - ) { - this._proofs = _proofs.slice(); - } - - /** - * @returns {SplitAssetProof[]} Copy of the asset proofs. - */ - public get proofs(): SplitAssetProof[] { - return this._proofs.slice(); - } - - /** - * Create a ProofMapEntry. - * - * @param {TokenId} tokenId Token id. - * @param {SplitAssetProof[]} proofs Asset proofs. - * @returns {ProofMapEntry} New entry. - */ - public static create(tokenId: TokenId, proofs: SplitAssetProof[]): ProofMapEntry { - return new ProofMapEntry(tokenId, proofs); - } -} - -/** - * Token-id-keyed map of asset proofs produced by a token split. - */ -class ProofMap { - private constructor(private readonly _proofs: Map) {} - - /** - * Create a ProofMap from raw entries. - * - * @param {[TokenId, SplitAssetProof[]][]} data Token-id and proofs pairs. - * @returns {ProofMap} New map. - */ - public static create(data: [TokenId, SplitAssetProof[]][]): ProofMap { - return new ProofMap( - new Map( - data.map(([tokenId, proofs]) => [HexConverter.encode(tokenId.bytes), ProofMapEntry.create(tokenId, proofs)]), - ), - ); - } - - /** - * Look up the entry for a token id. - * - * @param {TokenId} id Token id. - * @returns {ProofMapEntry|null} Matching entry, or `null`. - */ - public get(id: TokenId): ProofMapEntry | null { - return this._proofs.get(HexConverter.encode(id.bytes)) ?? null; - } - - /** - * @returns {number} Number of entries in this map. - */ - public size(): number { - return this._proofs.size; - } -} - -/** - * Result of splitting a token: a burn transaction for the original token plus - * per-token asset proofs for the new tokens. - */ -interface ISplit { +export interface ISplit { readonly burn: { readonly ownerPredicate: BurnPredicate; readonly transaction: TransferTransaction; }; - - readonly proofs: ProofMap; + readonly tokens: SplitToken[]; } /** @@ -103,23 +34,28 @@ interface ISplit { */ export class TokenSplit { /** - * Split old token to new tokens. + * Split a token into new outputs. * - * @param token token to be used for split - * @param decodePaymentData - * @param splitTokens - * @return token split object for submitting info + * @param {Token} token Source token to split. + * @param {(bytes: Uint8Array) => Promise} decodePaymentData Decoder for the source token's payment data. + * @param {SplitTokenRequest[]} requests Per-output mint requests. + * @returns {Promise} Burn transaction and split tokens ready to mint. */ public static async split( token: Token, decodePaymentData: (bytes: Uint8Array) => Promise, - splitTokens: [TokenId, PaymentAssetCollection][], + requests: SplitTokenRequest[], ): Promise { const hasher = new DataHasherFactory(HashAlgorithm.SHA256, DataHasher); const trees = new Map(); + const networkId = token.genesis.networkId; + + const resolved: [SplitTokenRequest, TokenId][] = []; + for (const request of requests) { + const tokenId = await TokenId.fromSalt(networkId, request.salt); + resolved.push([request, tokenId]); - for (const [tokenId, assets] of splitTokens) { - for (const asset of assets.toArray()) { + for (const asset of request.assets.toArray()) { const key = HexConverter.encode(asset.id.bytes); let tree = trees.get(key)?.[1]; if (!tree) { @@ -131,7 +67,6 @@ export class TokenSplit { } } - // Parse this from user object const paymentDataBytes = token.genesis.data; const paymentData = paymentDataBytes ? await decodePaymentData(paymentDataBytes) : null; if (paymentData == null) { @@ -169,28 +104,32 @@ export class TokenSplit { crypto.getRandomValues(new Uint8Array(32)), ); - const proofs: [TokenId, SplitAssetProof[]][] = []; - for (const [tokenId, assets] of splitTokens) { - proofs.push([ - tokenId, - assets - .toArray() - .map((asset) => - SplitAssetProof.create( - asset.id, - aggregationRoot.getPath(asset.id.toBitString().toBigInt()), - assetTreeRoots.get(HexConverter.encode(asset.id.bytes))!.getPath(tokenId.toBitString().toBigInt()), + const tokens: SplitToken[] = resolved.map( + ([request, tokenId]) => + new SplitToken( + networkId, + request.recipient, + request.tokenType, + request.salt, + request.assets, + request.assets + .toArray() + .map((asset) => + SplitAssetProof.create( + asset.id, + aggregationRoot.getPath(asset.id.toBitString().toBigInt()), + assetTreeRoots.get(HexConverter.encode(asset.id.bytes))!.getPath(tokenId.toBitString().toBigInt()), + ), ), - ), - ]); - } + ), + ); return { burn: { ownerPredicate: burnPredicate, transaction: burnTransaction, }, - proofs: ProofMap.create(proofs), + tokens, }; } } diff --git a/src/transaction/CertifiedMintTransaction.ts b/src/transaction/CertifiedMintTransaction.ts index e5aefd5..bcf8393 100644 --- a/src/transaction/CertifiedMintTransaction.ts +++ b/src/transaction/CertifiedMintTransaction.ts @@ -1,9 +1,11 @@ import { ITransaction } from './ITransaction.js'; import { MintTransaction } from './MintTransaction.js'; import { TokenId } from './TokenId.js'; +import { TokenSalt } from './TokenSalt.js'; import { TokenType } from './TokenType.js'; import { RootTrustBase } from '../api/bft/RootTrustBase.js'; import { InclusionProof } from '../api/InclusionProof.js'; +import { NetworkId } from '../api/NetworkId.js'; import { DataHash } from '../crypto/hash/DataHash.js'; import { EncodedPredicate } from '../predicate/EncodedPredicate.js'; import { PredicateVerifierService } from '../predicate/verification/PredicateVerifierService.js'; @@ -46,6 +48,13 @@ export class CertifiedMintTransaction implements ITransaction { return this.transaction.lockScript; } + /** + * @returns {NetworkId} Network identifier of the inner transaction. + */ + public get networkId(): NetworkId { + return this.transaction.networkId; + } + /** * @returns {EncodedPredicate} Recipient predicate of the inner transaction. */ @@ -53,6 +62,13 @@ export class CertifiedMintTransaction implements ITransaction { return this.transaction.recipient; } + /** + * @returns {TokenSalt} Mint-transaction salt of the inner transaction. + */ + public get salt(): TokenSalt { + return this.transaction.salt; + } + /** * @returns {DataHash} Source state hash of the inner transaction. */ @@ -68,7 +84,7 @@ export class CertifiedMintTransaction implements ITransaction { } /** - * @returns {TokenId} Token id of the inner transaction. + * @returns {TokenId} Token id of the inner transaction (derived from networkId and salt). */ public get tokenId(): TokenId { return this.transaction.tokenId; diff --git a/src/transaction/MintTransaction.ts b/src/transaction/MintTransaction.ts index 95c590a..5d35abc 100644 --- a/src/transaction/MintTransaction.ts +++ b/src/transaction/MintTransaction.ts @@ -2,9 +2,11 @@ import { CertifiedMintTransaction } from './CertifiedMintTransaction.js'; import { ITransaction } from './ITransaction.js'; import { MintTransactionState } from './MintTransactionState.js'; import { TokenId } from './TokenId.js'; +import { TokenSalt } from './TokenSalt.js'; import { TokenType } from './TokenType.js'; import { RootTrustBase } from '../api/bft/RootTrustBase.js'; import { InclusionProof } from '../api/InclusionProof.js'; +import { NetworkId } from '../api/NetworkId.js'; import { DataHash } from '../crypto/hash/DataHash.js'; import { DataHasher } from '../crypto/hash/DataHasher.js'; import { HashAlgorithm } from '../crypto/hash/HashAlgorithm.js'; @@ -29,9 +31,11 @@ export class MintTransaction implements ITransaction { private constructor( public readonly sourceStateHash: MintTransactionState, public readonly lockScript: EncodedPredicate, + public readonly networkId: NetworkId, public readonly recipient: EncodedPredicate, - public readonly tokenId: TokenId, + public readonly salt: TokenSalt, public readonly tokenType: TokenType, + public readonly tokenId: TokenId, private readonly _justification: Uint8Array | null, private readonly _data: Uint8Array | null, ) {} @@ -67,30 +71,36 @@ export class MintTransaction implements ITransaction { /** * Create a MintTransaction for a fresh token. * + * @param {NetworkId} networkId Network identifier. * @param {IPredicate} recipient Predicate that will lock the minted state. - * @param {TokenId} tokenId Token id being minted. * @param {TokenType} tokenType Token type being minted. + * @param {TokenSalt} salt Mint-transaction salt; defaults to a random 32-byte salt. * @param {Uint8Array|null} justification Optional mint justification bytes. * @param {Uint8Array|null} data Optional data payload. * @returns {Promise} New mint transaction. */ public static async create( + networkId: NetworkId, recipient: IPredicate, - tokenId: TokenId, tokenType: TokenType, + salt: TokenSalt | null = null, justification: Uint8Array | null = null, data: Uint8Array | null = null, ): Promise { + salt = salt ?? TokenSalt.generate(); justification = justification ? new Uint8Array(justification) : null; data = data ? new Uint8Array(data) : null; + const tokenId = await TokenId.fromSalt(networkId, salt); const signingService = await MintSigningService.create(tokenId); return new MintTransaction( await MintTransactionState.create(tokenId), EncodedPredicate.fromPredicate(SignaturePredicate.fromSigningService(signingService)), + networkId, EncodedPredicate.fromPredicate(recipient), - tokenId, + salt, tokenType, + tokenId, justification, data, ); @@ -109,18 +119,19 @@ export class MintTransaction implements ITransaction { throw new CborError(`Invalid CBOR tag for MintTransaction: ${tag.tag}`); } - const data = CborDeserializer.decodeArray(tag.data, 6); + const data = CborDeserializer.decodeArray(tag.data, 7); const version = CborDeserializer.decodeUnsignedInteger(data[0]); if (version !== MintTransaction.VERSION) { throw new CborError(`Unsupported MintTransaction version: ${version}`); } return MintTransaction.create( - EncodedPredicate.fromCBOR(data[1]), - TokenId.fromCBOR(data[2]), - TokenType.fromCBOR(data[3]), - CborDeserializer.decodeNullable(data[4], CborDeserializer.decodeByteString), + NetworkId.fromId(CborDeserializer.decodeUnsignedInteger(data[1])), + EncodedPredicate.fromCBOR(data[2]), + TokenType.fromCBOR(data[4]), + TokenSalt.fromCBOR(data[3]), CborDeserializer.decodeNullable(data[5], CborDeserializer.decodeByteString), + CborDeserializer.decodeNullable(data[6], CborDeserializer.decodeByteString), ); } @@ -153,8 +164,9 @@ export class MintTransaction implements ITransaction { MintTransaction.CBOR_TAG, CborSerializer.encodeArray( CborSerializer.encodeUnsignedInteger(this.version), + CborSerializer.encodeUnsignedInteger(this.networkId.id), this.recipient.toCBOR(), - this.tokenId.toCBOR(), + this.salt.toCBOR(), this.tokenType.toCBOR(), CborSerializer.encodeNullable(this._justification, CborSerializer.encodeByteString), CborSerializer.encodeNullable(this._data, CborSerializer.encodeByteString), @@ -185,9 +197,11 @@ export class MintTransaction implements ITransaction { return dedent` MintTransaction Version: ${this.version.toString()} + Network ID: ${this.networkId.toString()} Lock Script: ${this.lockScript.toString()} Recipient: ${this.recipient.toString()} + Salt: ${this.salt.toString()} Token ID: ${this.tokenId.toString()} Token Type: ${this.tokenType.toString()} Mint Justification: ${this._justification ? HexConverter.encode(this._justification) : 'null'} diff --git a/src/transaction/TokenId.ts b/src/transaction/TokenId.ts index a86e834..0f7655a 100644 --- a/src/transaction/TokenId.ts +++ b/src/transaction/TokenId.ts @@ -1,3 +1,7 @@ +import { TokenSalt } from './TokenSalt.js'; +import { NetworkId } from '../api/NetworkId.js'; +import { DataHasher } from '../crypto/hash/DataHasher.js'; +import { HashAlgorithm } from '../crypto/hash/HashAlgorithm.js'; import { CborDeserializer } from '../serialization/cbor/CborDeserializer.js'; import { CborSerializer } from '../serialization/cbor/CborSerializer.js'; import { BitString } from '../util/BitString.js'; @@ -29,6 +33,20 @@ export class TokenId { return new TokenId(CborDeserializer.decodeByteString(bytes)); } + /** + * Derive a TokenId from a salt and network identifier. + * + * @param {NetworkId} networkId Network identifier. + * @param {TokenSalt} salt Mint-transaction salt. + * @returns {Promise} Derived token id. + */ + public static async fromSalt(networkId: NetworkId, salt: TokenSalt): Promise { + const hash = await new DataHasher(HashAlgorithm.SHA256) + .update(CborSerializer.encodeArray(salt.toCBOR(), CborSerializer.encodeUnsignedInteger(networkId.id))) + .digest(); + return new TokenId(hash.data); + } + /** * Generate a fresh random TokenId. * diff --git a/src/transaction/TokenSalt.ts b/src/transaction/TokenSalt.ts new file mode 100644 index 0000000..627f270 --- /dev/null +++ b/src/transaction/TokenSalt.ts @@ -0,0 +1,85 @@ +import { CborDeserializer } from '../serialization/cbor/CborDeserializer.js'; +import { CborSerializer } from '../serialization/cbor/CborSerializer.js'; +import { HexConverter } from '../util/HexConverter.js'; +import { areUint8ArraysEqual } from '../util/TypedArrayUtils.js'; + +/** + * 32-byte salt mixed with a network identifier to derive a {@link TokenId}. + */ +export class TokenSalt { + public static readonly LENGTH = 32; + + private constructor(private readonly _bytes: Uint8Array) {} + + /** + * Wrap an existing 32-byte salt. + * + * @param {Uint8Array} bytes Salt bytes; must be exactly 32 bytes. + * @returns {TokenSalt} New salt. + * @throws {Error} If `bytes` is not 32 bytes long. + */ + public static fromBytes(bytes: Uint8Array): TokenSalt { + if (bytes.length !== TokenSalt.LENGTH) { + throw new Error(`TokenSalt must be ${TokenSalt.LENGTH} bytes long, got ${bytes.length}.`); + } + return new TokenSalt(new Uint8Array(bytes)); + } + + /** + * Create TokenSalt from CBOR bytes. + * + * @param {Uint8Array} bytes CBOR bytes. + * @returns {TokenSalt} Decoded salt. + */ + public static fromCBOR(bytes: Uint8Array): TokenSalt { + return TokenSalt.fromBytes(CborDeserializer.decodeByteString(bytes)); + } + + /** + * Generate a fresh random 32-byte TokenSalt. + * + * @returns {TokenSalt} New random salt. + */ + public static generate(): TokenSalt { + return new TokenSalt(crypto.getRandomValues(new Uint8Array(TokenSalt.LENGTH))); + } + + /** + * Equality check against another value. + * + * @param {unknown} o Other value. + * @returns {boolean} True if `o` is a TokenSalt with the same bytes. + */ + public equals(o: unknown): boolean { + if (this === o) { + return true; + } + if (!(o instanceof TokenSalt)) { + return false; + } + return areUint8ArraysEqual(this._bytes, o._bytes); + } + + /** + * @returns {Uint8Array} Copy of the salt bytes. + */ + public toBytes(): Uint8Array { + return new Uint8Array(this._bytes); + } + + /** + * Convert TokenSalt to CBOR bytes. + * + * @returns {Uint8Array} CBOR bytes. + */ + public toCBOR(): Uint8Array { + return CborSerializer.encodeByteString(this._bytes); + } + + /** + * @returns {string} Hex representation of the salt. + */ + public toString(): string { + return `TokenSalt[${HexConverter.encode(this._bytes)}]`; + } +} diff --git a/tests/e2e/ShardAggregatorClient.ts b/tests/e2e/ShardAggregatorClient.ts index 4711509..db0b1ac 100644 --- a/tests/e2e/ShardAggregatorClient.ts +++ b/tests/e2e/ShardAggregatorClient.ts @@ -23,7 +23,15 @@ export class ShardAggregatorClient implements IAggregatorClient { .map(([shardId, url]) => [shardId, new JsonRpcHttpTransport(url)]); } - public async getBlockHeight(): Promise { + public async getInclusionProof(stateId: StateId): Promise { + const transport = this.getTransport(stateId); + const data = { stateId: HexConverter.encode(stateId.data) }; + return InclusionProofResponse.fromCBOR( + HexConverter.decode((await transport.request('get_inclusion_proof.v2', data)) as string), + ); + } + + public async getLatestBlockNumber(): Promise { const heights = await Promise.all( this.shards.map(async ([, transport]) => { const response = await transport.request('get_block_height', {}); @@ -43,14 +51,6 @@ export class ShardAggregatorClient implements IAggregatorClient { return heights.reduce((max, h) => (h > max ? h : max)); } - public async getInclusionProof(stateId: StateId): Promise { - const transport = this.getTransport(stateId); - const data = { stateId: HexConverter.encode(stateId.data) }; - return InclusionProofResponse.fromCBOR( - HexConverter.decode((await transport.request('get_inclusion_proof.v2', data)) as string), - ); - } - public async submitCertificationRequest(certificationData: CertificationData): Promise { const request = await CertificationRequest.create(certificationData); const transport = this.getTransport(request.stateId); diff --git a/tests/examples/mint/ExampleTest.ts b/tests/examples/mint/ExampleTest.ts index a4d4936..b4746f6 100644 --- a/tests/examples/mint/ExampleTest.ts +++ b/tests/examples/mint/ExampleTest.ts @@ -2,6 +2,7 @@ import config from './config.json' with { type: 'json' }; import { AggregatorClient } from '../../../src/api/AggregatorClient.js'; import { RootTrustBase } from '../../../src/api/bft/RootTrustBase.js'; import { CertificationData } from '../../../src/api/CertificationData.js'; +import { NetworkId } from '../../../src/api/NetworkId.js'; import { SigningService } from '../../../src/crypto/secp256k1/SigningService.js'; import { SignaturePredicate } from '../../../src/predicate/builtin/SignaturePredicate.js'; import { PredicateVerifierService } from '../../../src/predicate/verification/PredicateVerifierService.js'; @@ -9,7 +10,6 @@ import { CborSerializer } from '../../../src/serialization/cbor/CborSerializer.j import { StateTransitionClient } from '../../../src/StateTransitionClient.js'; import { MintTransaction } from '../../../src/transaction/MintTransaction.js'; import { Token } from '../../../src/transaction/Token.js'; -import { TokenId } from '../../../src/transaction/TokenId.js'; import { TokenType } from '../../../src/transaction/TokenType.js'; import { MintJustificationVerifierService } from '../../../src/transaction/verification/MintJustificationVerifierService.js'; import { HexConverter } from '../../../src/util/HexConverter.js'; @@ -30,10 +30,11 @@ it('Token minting', async () => { const ownerPredicate = SignaturePredicate.fromSigningService(ownerSigningService); const mintTransaction = await MintTransaction.create( + NetworkId.LOCAL, ownerPredicate, - TokenId.generate(), TokenType.generate(), null, + null, CborSerializer.encodeTextString('My custom data'), ); const certificationData = await CertificationData.fromMintTransaction(mintTransaction); diff --git a/tests/examples/split/ExampleTest.ts b/tests/examples/split/ExampleTest.ts index ecf00f9..a6ce63e 100644 --- a/tests/examples/split/ExampleTest.ts +++ b/tests/examples/split/ExampleTest.ts @@ -4,12 +4,14 @@ import { AggregatorClient } from '../../../src/api/AggregatorClient.js'; import { RootTrustBase } from '../../../src/api/bft/RootTrustBase.js'; import { CertificationData } from '../../../src/api/CertificationData.js'; import { CertificationStatus } from '../../../src/api/CertificationResponse.js'; +import { NetworkId } from '../../../src/api/NetworkId.js'; import { SigningService } from '../../../src/crypto/secp256k1/SigningService.js'; import { Asset } from '../../../src/payment/asset/Asset.js'; import { AssetId } from '../../../src/payment/asset/AssetId.js'; import { PaymentAssetCollection } from '../../../src/payment/asset/PaymentAssetCollection.js'; import { SplitMintJustification } from '../../../src/payment/SplitMintJustification.js'; import { SplitMintJustificationVerifier } from '../../../src/payment/SplitMintJustificationVerifier.js'; +import { SplitTokenRequest } from '../../../src/payment/SplitTokenRequest.js'; import { TokenSplit } from '../../../src/payment/TokenSplit.js'; import { SignaturePredicate } from '../../../src/predicate/builtin/SignaturePredicate.js'; import { SignaturePredicateUnlockScript } from '../../../src/predicate/builtin/SignaturePredicateUnlockScript.js'; @@ -17,7 +19,6 @@ import { PredicateVerifierService } from '../../../src/predicate/verification/Pr import { StateTransitionClient } from '../../../src/StateTransitionClient.js'; import { MintTransaction } from '../../../src/transaction/MintTransaction.js'; import { Token } from '../../../src/transaction/Token.js'; -import { TokenId } from '../../../src/transaction/TokenId.js'; import { TokenType } from '../../../src/transaction/TokenType.js'; import { MintJustificationVerifierService } from '../../../src/transaction/verification/MintJustificationVerifierService.js'; import { HexConverter } from '../../../src/util/HexConverter.js'; @@ -48,11 +49,13 @@ it('Token splitting', async () => { ]; const paymentData = new CustomPaymentData(PaymentAssetCollection.create(...assets), 'my other data'); + const networkId = NetworkId.LOCAL; const mintTransaction = await MintTransaction.create( + networkId, ownerPredicate, - TokenId.generate(), TokenType.generate(), null, + null, await paymentData.encode(), ); @@ -72,13 +75,22 @@ it('Token splitting', async () => { ), ); - const splitTokens: [TokenId, PaymentAssetCollection][] = [ - [TokenId.generate(), PaymentAssetCollection.create(new Asset(new AssetId(textEncoder.encode('EUR')), 150n))], - [TokenId.generate(), PaymentAssetCollection.create(new Asset(new AssetId(textEncoder.encode('EUR')), 150n))], - [TokenId.generate(), PaymentAssetCollection.create(new Asset(new AssetId(textEncoder.encode('USD')), 500n))], + const requests = [ + SplitTokenRequest.create( + ownerPredicate, + PaymentAssetCollection.create(new Asset(new AssetId(textEncoder.encode('EUR')), 150n)), + ), + SplitTokenRequest.create( + ownerPredicate, + PaymentAssetCollection.create(new Asset(new AssetId(textEncoder.encode('EUR')), 150n)), + ), + SplitTokenRequest.create( + ownerPredicate, + PaymentAssetCollection.create(new Asset(new AssetId(textEncoder.encode('USD')), 500n)), + ), ]; - const result = await TokenSplit.split(token, CustomPaymentData.decode, splitTokens); + const result = await TokenSplit.split(token, CustomPaymentData.decode, requests); response = await client.submitCertificationRequest( await CertificationData.fromTransaction( @@ -102,19 +114,15 @@ it('Token splitting', async () => { ); let i = 1; - for (const [tokenId, assets] of splitTokens) { - const entry = result.proofs.get(tokenId); - if (entry == null) { - throw new Error('Missing split reason proof for token.'); - } - - const splitPaymentData = new CustomPaymentData(assets, 'split token'); + for (const splitToken of result.tokens) { + const splitPaymentData = new CustomPaymentData(splitToken.assets, 'split token'); const mintTransaction = await MintTransaction.create( - ownerPredicate, - tokenId, - TokenType.generate(), - SplitMintJustification.create(burntToken, entry.proofs).toCBOR(), + splitToken.networkId, + splitToken.recipient, + splitToken.tokenType, + splitToken.salt, + SplitMintJustification.create(burntToken, splitToken.proofs).toCBOR(), await splitPaymentData.encode(), ); @@ -125,7 +133,7 @@ it('Token splitting', async () => { throw new Error(`Token certification failed: ${response.status}`); } - const splitToken = await Token.mint( + const mintedSplitToken = await Token.mint( trustBase, predicateVerifier, mintJustificationVerifier, @@ -136,6 +144,6 @@ it('Token splitting', async () => { ), ); - console.log(`Token[${i++}]: `, HexConverter.encode(splitToken.toCBOR()), '\n'); + console.log(`Token[${i++}]: `, HexConverter.encode(mintedSplitToken.toCBOR()), '\n'); } }, 30000); diff --git a/tests/examples/transfer/ExampleTest.ts b/tests/examples/transfer/ExampleTest.ts index 3bd97c7..c54f2f3 100644 --- a/tests/examples/transfer/ExampleTest.ts +++ b/tests/examples/transfer/ExampleTest.ts @@ -3,6 +3,7 @@ import { AggregatorClient } from '../../../src/api/AggregatorClient.js'; import { RootTrustBase } from '../../../src/api/bft/RootTrustBase.js'; import { CertificationData } from '../../../src/api/CertificationData.js'; import { CertificationStatus } from '../../../src/api/CertificationResponse.js'; +import { NetworkId } from '../../../src/api/NetworkId.js'; import { SigningService } from '../../../src/crypto/secp256k1/SigningService.js'; import { SignaturePredicate } from '../../../src/predicate/builtin/SignaturePredicate.js'; import { SignaturePredicateUnlockScript } from '../../../src/predicate/builtin/SignaturePredicateUnlockScript.js'; @@ -12,7 +13,6 @@ import { CborSerializer } from '../../../src/serialization/cbor/CborSerializer.j import { StateTransitionClient } from '../../../src/StateTransitionClient.js'; import { MintTransaction } from '../../../src/transaction/MintTransaction.js'; import { Token } from '../../../src/transaction/Token.js'; -import { TokenId } from '../../../src/transaction/TokenId.js'; import { TokenType } from '../../../src/transaction/TokenType.js'; import { TransferTransaction } from '../../../src/transaction/TransferTransaction.js'; import { MintJustificationVerifierService } from '../../../src/transaction/verification/MintJustificationVerifierService.js'; @@ -30,10 +30,11 @@ async function receiveToken(client: StateTransitionClient, trustBase: RootTrustB const ownerPredicate = SignaturePredicate.fromSigningService(ownerSigningService); const mintTransaction = await MintTransaction.create( + NetworkId.LOCAL, ownerPredicate, - TokenId.generate(), TokenType.generate(), null, + null, CborSerializer.encodeTextString('My custom data'), ); const certificationData = await CertificationData.fromMintTransaction(mintTransaction); diff --git a/tests/functional/payment/SplitBuilderTest.ts b/tests/functional/payment/SplitBuilderTest.ts index 5ea1cf3..3559946 100644 --- a/tests/functional/payment/SplitBuilderTest.ts +++ b/tests/functional/payment/SplitBuilderTest.ts @@ -1,6 +1,7 @@ import { TestPaymentData } from './TestPaymentData.js'; import { CertificationData } from '../../../src/api/CertificationData.js'; import { CertificationStatus } from '../../../src/api/CertificationResponse.js'; +import { NetworkId } from '../../../src/api/NetworkId.js'; import { SigningService } from '../../../src/crypto/secp256k1/SigningService.js'; import { Asset } from '../../../src/payment/asset/Asset.js'; import { AssetId } from '../../../src/payment/asset/AssetId.js'; @@ -10,6 +11,7 @@ import { TokenAssetMissingError } from '../../../src/payment/error/TokenAssetMis import { TokenAssetValueMismatchError } from '../../../src/payment/error/TokenAssetValueMismatchError.js'; import { SplitMintJustification } from '../../../src/payment/SplitMintJustification.js'; import { SplitMintJustificationVerifier } from '../../../src/payment/SplitMintJustificationVerifier.js'; +import { SplitTokenRequest } from '../../../src/payment/SplitTokenRequest.js'; import { TokenSplit } from '../../../src/payment/TokenSplit.js'; import { SignaturePredicate } from '../../../src/predicate/builtin/SignaturePredicate.js'; import { SignaturePredicateUnlockScript } from '../../../src/predicate/builtin/SignaturePredicateUnlockScript.js'; @@ -17,7 +19,6 @@ import { PredicateVerifierService } from '../../../src/predicate/verification/Pr import { StateTransitionClient } from '../../../src/StateTransitionClient.js'; import { MintTransaction } from '../../../src/transaction/MintTransaction.js'; import { Token } from '../../../src/transaction/Token.js'; -import { TokenId } from '../../../src/transaction/TokenId.js'; import { TokenType } from '../../../src/transaction/TokenType.js'; import { MintJustificationVerifierService } from '../../../src/transaction/verification/MintJustificationVerifierService.js'; import { waitInclusionProof } from '../../../src/util/InclusionProofUtils.js'; @@ -44,11 +45,13 @@ describe('SplitBuilder Functional Test', () => { ]; const paymentData = new TestPaymentData(PaymentAssetCollection.create(...assets)); + const networkId = NetworkId.LOCAL; const mintTransaction = await MintTransaction.create( + networkId, predicate, - TokenId.generate(), TokenType.generate(), null, + null, await paymentData.encode(), ); let certificationData = await CertificationData.fromMintTransaction(mintTransaction); @@ -68,32 +71,34 @@ describe('SplitBuilder Functional Test', () => { ); await expect( - TokenSplit.split(token, TestPaymentData.decode, [[TokenId.generate(), PaymentAssetCollection.create(assets[0])]]), + TokenSplit.split(token, TestPaymentData.decode, [ + SplitTokenRequest.create(predicate, PaymentAssetCollection.create(assets[0])), + ]), ).rejects.toThrow(TokenAssetCountMismatchError); await expect( TokenSplit.split(token, TestPaymentData.decode, [ - [ - TokenId.generate(), + SplitTokenRequest.create( + predicate, PaymentAssetCollection.create( assets[0], new Asset(new AssetId(crypto.getRandomValues(new Uint8Array(10))), 400n), ), - ], + ), ]), ).rejects.toThrow(TokenAssetMissingError); await expect( TokenSplit.split(token, TestPaymentData.decode, [ - [TokenId.generate(), PaymentAssetCollection.create(assets[0], new Asset(assets[1].id, 1500n))], + SplitTokenRequest.create(predicate, PaymentAssetCollection.create(assets[0], new Asset(assets[1].id, 1500n))), ]), ).rejects.toThrow(TokenAssetValueMismatchError); - const splitTokens: [TokenId, PaymentAssetCollection][] = [ - [TokenId.generate(), PaymentAssetCollection.create(assets[0])], - [TokenId.generate(), PaymentAssetCollection.create(assets[1])], + const requests = [ + SplitTokenRequest.create(predicate, PaymentAssetCollection.create(assets[0])), + SplitTokenRequest.create(predicate, PaymentAssetCollection.create(assets[1])), ]; - const result = await TokenSplit.split(token, TestPaymentData.decode, splitTokens); + const result = await TokenSplit.split(token, TestPaymentData.decode, requests); certificationData = await CertificationData.fromTransaction( result.burn.transaction, @@ -113,18 +118,14 @@ describe('SplitBuilder Functional Test', () => { ), ); - for (const [tokenId, assets] of splitTokens) { - const entry = result.proofs.get(tokenId); - if (entry == null) { - throw new Error('Missing split reason proof for token.'); - } - + for (const splitToken of result.tokens) { const mintTransaction = await MintTransaction.create( - predicate, - tokenId, - TokenType.generate(), - SplitMintJustification.create(token, entry.proofs).toCBOR(), - await new TestPaymentData(assets).encode(), + splitToken.networkId, + splitToken.recipient, + splitToken.tokenType, + splitToken.salt, + SplitMintJustification.create(token, splitToken.proofs).toCBOR(), + await new TestPaymentData(splitToken.assets).encode(), ); const certificationData = await CertificationData.fromMintTransaction(mintTransaction); @@ -132,7 +133,7 @@ describe('SplitBuilder Functional Test', () => { const response = await client.submitCertificationRequest(certificationData); expect(response.status).toEqual(CertificationStatus.SUCCESS); - const splitToken = await Token.mint( + const mintedSplitToken = await Token.mint( trustBase, predicateVerifier, mintJustificationVerifier, @@ -144,7 +145,7 @@ describe('SplitBuilder Functional Test', () => { ); await expect( - Token.fromCBOR(splitToken.toCBOR()) + Token.fromCBOR(mintedSplitToken.toCBOR()) .then((token) => token.verify(trustBase, predicateVerifier, mintJustificationVerifier)) .then((result) => result.status), ).resolves.toEqual(VerificationStatus.OK); diff --git a/tests/unit/api/CertificationDataTest.ts b/tests/unit/api/CertificationDataTest.ts index e4af203..4f1a8e4 100644 --- a/tests/unit/api/CertificationDataTest.ts +++ b/tests/unit/api/CertificationDataTest.ts @@ -1,8 +1,9 @@ import { CertificationData } from '../../../src/api/CertificationData.js'; +import { NetworkId } from '../../../src/api/NetworkId.js'; import { SignaturePredicate } from '../../../src/predicate/builtin/SignaturePredicate.js'; import { EncodedPredicate } from '../../../src/predicate/EncodedPredicate.js'; import { MintTransaction } from '../../../src/transaction/MintTransaction.js'; -import { TokenId } from '../../../src/transaction/TokenId.js'; +import { TokenSalt } from '../../../src/transaction/TokenSalt.js'; import { TokenType } from '../../../src/transaction/TokenType.js'; import { HexConverter } from '../../../src/util/HexConverter.js'; @@ -10,15 +11,16 @@ describe('CertificationData', () => { it('should encode and decode to exactly same object', async () => { const certificationData = await CertificationData.fromMintTransaction( await MintTransaction.create( + NetworkId.MAINNET, SignaturePredicate.create( HexConverter.decode('02ce9f22e51333c97a8fb1f807a229ece3a8765a16af5fc1a13e30834be3280026'), ), - new TokenId(new Uint8Array(32)), new TokenType(new Uint8Array(32)), + TokenSalt.fromBytes(new Uint8Array(32)), ), ); expect(HexConverter.encode(certificationData.toCBOR())).toStrictEqual( - 'd998778501d9987883014101582103b00b30dcd21feaa837132ccd4b7b9595f704c9714ac66eed085f52bc396f9050582080201eff2f0c27ea9c8433eb999b4fd0fa5bfb4fe47fa2690859f0c83651604e5820076034611bb432b9a4ac3ee573ed0f687ae22d15d36dad9502cfabe6c31e0357584104e4b1681cee95339004cce2cbee141a745ca06ec4629d4d8bb8dabf32242942420fe93f900a6609e8d0c7788e7bef492110eacc7910362b375a93112b7cb41900', + 'd998778501d9987883014101582103a19eef04b8856f50bf2d688b0d8804575115e53d2a7780da363628343f9635075820e4b183ff6b7a399983cee26e4feea85d517dede0142def5c838e593a9e6152415820df524cffc08a1dc30579a8a51f440a97b30630988084f8d12a4d8bd741c7791258419efb637f14dbdaada6e293e2182932d82265b04b1abf4f28bc4c285b32b5e2325140fe7f94bc9b705c568b4fcb7f9ea90cf0fadcacc1b4504275f81558aad1e700', ); const result = CertificationData.fromCBOR(certificationData.toCBOR()); diff --git a/tests/unit/api/InclusionProofTest.ts b/tests/unit/api/InclusionProofTest.ts index bd8e122..cf218d6 100644 --- a/tests/unit/api/InclusionProofTest.ts +++ b/tests/unit/api/InclusionProofTest.ts @@ -3,6 +3,7 @@ import { UnicityCertificate } from '../../../src/api/bft/UnicityCertificate.js'; import { CertificationData } from '../../../src/api/CertificationData.js'; import { InclusionCertificate } from '../../../src/api/InclusionCertificate.js'; import { InclusionProof } from '../../../src/api/InclusionProof.js'; +import { NetworkId } from '../../../src/api/NetworkId.js'; import { StateId } from '../../../src/api/StateId.js'; import { DataHash } from '../../../src/crypto/hash/DataHash.js'; import { DataHasherFactory } from '../../../src/crypto/hash/DataHasherFactory.js'; @@ -15,7 +16,7 @@ import { PredicateVerifierService } from '../../../src/predicate/verification/Pr import { CborSerializer } from '../../../src/serialization/cbor/CborSerializer.js'; import { SparseMerkleTree } from '../../../src/smt/radix/SparseMerkleTree.js'; import { MintTransaction } from '../../../src/transaction/MintTransaction.js'; -import { TokenId } from '../../../src/transaction/TokenId.js'; +import { TokenSalt } from '../../../src/transaction/TokenSalt.js'; import { TokenType } from '../../../src/transaction/TokenType.js'; import { InclusionProofVerificationRule, @@ -39,8 +40,8 @@ describe('InclusionProof', () => { beforeAll(async () => { transaction = await MintTransaction.create( + NetworkId.LOCAL, SignaturePredicate.fromSigningService(signingService), - TokenId.generate(), TokenType.generate(), ); const smt = new SparseMerkleTree(new DataHasherFactory(HashAlgorithm.SHA256, NodeDataHasher)); @@ -87,9 +88,10 @@ describe('InclusionProof', () => { predicateVerifier, new InclusionProof(certificationData, null, unicityCertificate), await MintTransaction.create( + transaction.networkId, transaction.lockScript, - TokenId.generate(), transaction.tokenType, + TokenSalt.generate(), null, transaction.data, ), diff --git a/tests/unit/api/StateIdTest.ts b/tests/unit/api/StateIdTest.ts index e30172c..6fa87c0 100644 --- a/tests/unit/api/StateIdTest.ts +++ b/tests/unit/api/StateIdTest.ts @@ -1,7 +1,8 @@ +import { NetworkId } from '../../../src/api/NetworkId.js'; import { StateId } from '../../../src/api/StateId.js'; import { SignaturePredicate } from '../../../src/predicate/builtin/SignaturePredicate.js'; import { MintTransaction } from '../../../src/transaction/MintTransaction.js'; -import { TokenId } from '../../../src/transaction/TokenId.js'; +import { TokenSalt } from '../../../src/transaction/TokenSalt.js'; import { TokenType } from '../../../src/transaction/TokenType.js'; import { HexConverter } from '../../../src/util/HexConverter.js'; @@ -9,16 +10,17 @@ describe('StateId', () => { it('should encode and decode to exactly same object', async () => { const stateId = await StateId.fromTransaction( await MintTransaction.create( + NetworkId.MAINNET, SignaturePredicate.create( HexConverter.decode('02ce9f22e51333c97a8fb1f807a229ece3a8765a16af5fc1a13e30834be3280026'), ), - new TokenId(new Uint8Array(32)), new TokenType(new Uint8Array(32)), + TokenSalt.fromBytes(new Uint8Array(32)), ), ); expect(HexConverter.encode(stateId.toCBOR())).toStrictEqual( - '5820b08cbe261c1441a4f9c5127accaa683c5da8c87290bb28cddbddfac0b5033958', + '5820ffb36b55de9bfaf48b766d1f4e041a6c5d35ba23b402ea2a56a6c7692cb8f81a', ); expect(StateId.fromCBOR(stateId.toCBOR())).toStrictEqual(stateId); }); diff --git a/tests/utils/TokenUtils.ts b/tests/utils/TokenUtils.ts index 16eba06..addfc15 100644 --- a/tests/utils/TokenUtils.ts +++ b/tests/utils/TokenUtils.ts @@ -1,6 +1,7 @@ import { RootTrustBase } from '../../src/api/bft/RootTrustBase.js'; import { CertificationData } from '../../src/api/CertificationData.js'; import { CertificationStatus } from '../../src/api/CertificationResponse.js'; +import { NetworkId } from '../../src/api/NetworkId.js'; import { SigningService } from '../../src/crypto/secp256k1/SigningService.js'; import { SignaturePredicateUnlockScript } from '../../src/predicate/builtin/SignaturePredicateUnlockScript.js'; import { IPredicate } from '../../src/predicate/IPredicate.js'; @@ -10,7 +11,7 @@ import { ICborSerializable } from '../../src/serialization/cbor/ICborSerializabl import { StateTransitionClient } from '../../src/StateTransitionClient.js'; import { MintTransaction } from '../../src/transaction/MintTransaction.js'; import { Token } from '../../src/transaction/Token.js'; -import { TokenId } from '../../src/transaction/TokenId.js'; +import { TokenSalt } from '../../src/transaction/TokenSalt.js'; import { TokenType } from '../../src/transaction/TokenType.js'; import { TransferTransaction } from '../../src/transaction/TransferTransaction.js'; import { MintJustificationVerifierService } from '../../src/transaction/verification/MintJustificationVerifierService.js'; @@ -23,12 +24,20 @@ export async function mintToken( predicateVerifier: PredicateVerifierService, mintJustificationVerifier: MintJustificationVerifierService, recipient: IPredicate, - tokenId: TokenId = TokenId.generate(), + networkId: NetworkId = NetworkId.LOCAL, tokenType: TokenType = TokenType.generate(), + salt: TokenSalt = TokenSalt.generate(), justification: ICborSerializable | null = null, data: Uint8Array | null = null, ): Promise { - const transaction = await MintTransaction.create(recipient, tokenId, tokenType, justification?.toCBOR(), data); + const transaction = await MintTransaction.create( + networkId, + recipient, + tokenType, + salt, + justification?.toCBOR(), + data, + ); const certificationData = await CertificationData.fromMintTransaction(transaction); From 1dc3ff2d4e8f0d6b0c28494961733d1eaf0d3a5c Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Fri, 22 May 2026 16:03:33 +0300 Subject: [PATCH 03/13] #116 Add verification for network id --- .../verification/UnicityCertificateVerification.ts | 8 ++++++++ .../rule/CertifiedMintTransactionVerificationRule.ts | 11 +++++++++++ tests/utils/RootTrustBaseFixture.ts | 3 ++- tests/utils/UnicityCertificateFixture.ts | 3 ++- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/api/bft/verification/UnicityCertificateVerification.ts b/src/api/bft/verification/UnicityCertificateVerification.ts index 0475494..55093d0 100644 --- a/src/api/bft/verification/UnicityCertificateVerification.ts +++ b/src/api/bft/verification/UnicityCertificateVerification.ts @@ -50,6 +50,14 @@ export class UnicityCertificateVerification { inclusionProof: InclusionProof, ): Promise { const results: VerificationResult[] = []; + + const sealNetworkId = inclusionProof.unicityCertificate.unicitySeal.networkId; + if (sealNetworkId !== BigInt(trustBase.networkId)) { + results.push(new VerificationResult('UnicitySealNetworkMatchesTrustBaseRule', VerificationStatus.FAIL)); + return UnicityCertificateVerificationResult.fail(results); + } + results.push(new VerificationResult('UnicitySealNetworkMatchesTrustBaseRule', VerificationStatus.OK)); + let result = await UnicitySealHashMatchesWithRootHashRule.verify(inclusionProof.unicityCertificate); results.push(result); if (result.status !== VerificationStatus.OK) { diff --git a/src/transaction/verification/rule/CertifiedMintTransactionVerificationRule.ts b/src/transaction/verification/rule/CertifiedMintTransactionVerificationRule.ts index f0543c6..0aa7f71 100644 --- a/src/transaction/verification/rule/CertifiedMintTransactionVerificationRule.ts +++ b/src/transaction/verification/rule/CertifiedMintTransactionVerificationRule.ts @@ -30,6 +30,17 @@ export class CertifiedMintTransactionVerificationRule { ): Promise> { const results: VerificationResult[] = []; + if (genesis.networkId.id !== BigInt(trustBase.networkId)) { + results.push(new VerificationResult('MintNetworkMatchesTrustBaseRule', VerificationStatus.FAIL)); + return new VerificationResult( + 'CertifiedMintTransactionVerificationRule', + VerificationStatus.FAIL, + 'Mint network does not match trust base.', + results, + ); + } + results.push(new VerificationResult('MintNetworkMatchesTrustBaseRule', VerificationStatus.OK)); + const signingService = await MintSigningService.create(genesis.tokenId); const expectedLockScript = EncodedPredicate.fromPredicate(SignaturePredicate.fromSigningService(signingService)); let result: VerificationResult = EncodedPredicate.equals( diff --git a/tests/utils/RootTrustBaseFixture.ts b/tests/utils/RootTrustBaseFixture.ts index b2adf7b..7753bda 100644 --- a/tests/utils/RootTrustBaseFixture.ts +++ b/tests/utils/RootTrustBaseFixture.ts @@ -1,4 +1,5 @@ import { RootTrustBase } from '../../src/api/bft/RootTrustBase.js'; +import { NetworkId } from '../../src/api/NetworkId.js'; import { HexConverter } from '../../src/util/HexConverter.js'; export function createRootTrustBase(publicKey: Uint8Array): RootTrustBase { @@ -6,7 +7,7 @@ export function createRootTrustBase(publicKey: Uint8Array): RootTrustBase { changeRecordHash: null, epoch: '0', epochStartRound: '0', - networkId: 0, + networkId: Number(NetworkId.LOCAL.id), previousEntryHash: null, quorumThreshold: '1', rootNodes: [ diff --git a/tests/utils/UnicityCertificateFixture.ts b/tests/utils/UnicityCertificateFixture.ts index d8f9315..fe2fd84 100644 --- a/tests/utils/UnicityCertificateFixture.ts +++ b/tests/utils/UnicityCertificateFixture.ts @@ -6,6 +6,7 @@ import { ShardTreeCertificate } from '../../src/api/bft/ShardTreeCertificate.js' import { UnicityCertificate } from '../../src/api/bft/UnicityCertificate.js'; import { UnicitySeal } from '../../src/api/bft/UnicitySeal.js'; import { UnicityTreeCertificate } from '../../src/api/bft/UnicityTreeCertificate.js'; +import { NetworkId } from '../../src/api/NetworkId.js'; import { DataHash } from '../../src/crypto/hash/DataHash.js'; import { DataHasher } from '../../src/crypto/hash/DataHasher.js'; import { HashAlgorithm } from '../../src/crypto/hash/HashAlgorithm.js'; @@ -42,7 +43,7 @@ export async function createUnicityCertificate( .digest(); const seal = await UnicitySeal.create( - 0n, + NetworkId.LOCAL.id, 0n, 0n, 0n, From 7b33bc2b96a1d14b8f99e2abb38b22320b188e48 Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Fri, 22 May 2026 16:19:20 +0300 Subject: [PATCH 04/13] #115 Add duplicate token id check to split --- src/payment/TokenSplit.ts | 13 +++++++++---- src/payment/error/DuplicateSplitTokenIdError.ts | 9 +++++++++ src/transaction/TokenId.ts | 9 --------- 3 files changed, 18 insertions(+), 13 deletions(-) create mode 100644 src/payment/error/DuplicateSplitTokenIdError.ts diff --git a/src/payment/TokenSplit.ts b/src/payment/TokenSplit.ts index 2baabbf..2bd9b24 100644 --- a/src/payment/TokenSplit.ts +++ b/src/payment/TokenSplit.ts @@ -1,3 +1,4 @@ +import { DuplicateSplitTokenIdError } from './error/DuplicateSplitTokenIdError.js'; import { TokenAssetMissingError } from './error/TokenAssetMissingError.js'; import { IPaymentData } from './IPaymentData.js'; import { DataHasher } from '../crypto/hash/DataHasher.js'; @@ -50,10 +51,14 @@ export class TokenSplit { const trees = new Map(); const networkId = token.genesis.networkId; - const resolved: [SplitTokenRequest, TokenId][] = []; + const requestsWithTokenId = new Map(); for (const request of requests) { const tokenId = await TokenId.fromSalt(networkId, request.salt); - resolved.push([request, tokenId]); + const tokenIdPath = tokenId.toBitString().toBigInt(); + if (requestsWithTokenId.has(tokenIdPath)) { + throw new DuplicateSplitTokenIdError(tokenId.toString()); + } + requestsWithTokenId.set(tokenIdPath, [request, tokenId]); for (const asset of request.assets.toArray()) { const key = HexConverter.encode(asset.id.bytes); @@ -63,7 +68,7 @@ export class TokenSplit { trees.set(key, [asset.id, tree]); } - await tree.addLeaf(tokenId.toBitString().toBigInt(), asset.id.bytes, asset.value); + await tree.addLeaf(tokenIdPath, asset.id.bytes, asset.value); } } @@ -104,7 +109,7 @@ export class TokenSplit { crypto.getRandomValues(new Uint8Array(32)), ); - const tokens: SplitToken[] = resolved.map( + const tokens: SplitToken[] = Array.from(requestsWithTokenId.values()).map( ([request, tokenId]) => new SplitToken( networkId, diff --git a/src/payment/error/DuplicateSplitTokenIdError.ts b/src/payment/error/DuplicateSplitTokenIdError.ts new file mode 100644 index 0000000..9c7aa01 --- /dev/null +++ b/src/payment/error/DuplicateSplitTokenIdError.ts @@ -0,0 +1,9 @@ +/** + * Thrown when two split requests derive the same token id, which would + * collide in the sum trees. + */ +export class DuplicateSplitTokenIdError extends Error { + public constructor(tokenId: string) { + super(`Duplicate token id across split requests: ${tokenId}.`); + } +} diff --git a/src/transaction/TokenId.ts b/src/transaction/TokenId.ts index 0f7655a..66a75b9 100644 --- a/src/transaction/TokenId.ts +++ b/src/transaction/TokenId.ts @@ -47,15 +47,6 @@ export class TokenId { return new TokenId(hash.data); } - /** - * Generate a fresh random TokenId. - * - * @returns {TokenId} New token id with random 32-byte payload. - */ - public static generate(): TokenId { - return new TokenId(crypto.getRandomValues(new Uint8Array(32))); - } - /** * Equality check against another value. * From c118c4678dabb39d05003014230a4e5ced82a46e Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Fri, 22 May 2026 17:05:35 +0300 Subject: [PATCH 05/13] #115 Make networkId a number, update readme --- README.md | 84 +++++++------------ src/api/NetworkId.ts | 14 ++-- ...ertifiedMintTransactionVerificationRule.ts | 2 +- tests/utils/RootTrustBaseFixture.ts | 2 +- tests/utils/UnicityCertificateFixture.ts | 2 +- 5 files changed, 42 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index cfb7a48..6ab1e74 100644 --- a/README.md +++ b/README.md @@ -23,16 +23,29 @@ npm install @unicitylabs/state-transition-sdk ## Quick Start -Note: for examples, see further down in the [Examples section](#examples) or browse around in the [tests folder](./tests) of this SDK. +End-to-end runnable examples live under [`tests/examples/`](./tests/examples): + +- Mint: [`tests/examples/mint/ExampleTest.ts`](./tests/examples/mint/ExampleTest.ts) +- Transfer: [`tests/examples/transfer/ExampleTest.ts`](./tests/examples/transfer/ExampleTest.ts) +- Split: [`tests/examples/split/ExampleTest.ts`](./tests/examples/split/ExampleTest.ts) + +Tokens are shipped between parties as CBOR — use `token.toCBOR()` on the sender side and `Token.fromCBOR(bytes)` on the receiver side. ## Core Components ### StateTransitionClient -The main SDK interface for token operations: +A thin client over the aggregator. As a consumer you'll typically: + +1. Build a `MintTransaction` or `TransferTransaction`. +2. Submit its `CertificationData` and wait for an inclusion proof. +3. Turn it into a certified transaction and apply it to a `Token` (`Token.mint` / `token.transfer`). +4. Call `token.verify(...)` on the receiving side. + +`StateTransitionClient` covers step 2 only: -- `submitCertificationRequest()` - Submit transaction to aggregator -- `getInclusionProof()` - Retrieve inclusion proof for a commitment +- `submitCertificationRequest()` - Submit a commitment to the aggregator +- `getInclusionProof()` - Retrieve an inclusion proof for a state id ### Transaction Flow @@ -61,32 +74,12 @@ I --> J[End] ### Token Structure -``` -Token { - genesis: CertifiedMintTransaction { - transaction: MintTransaction { - recipient: IPredicate, - tokenId: TokenId, - tokenType: TokenType, - justification: Uint8Array | null - data: Uint8Array | null - }, - inclusionProof: InclusionProof - }, - transactions: [ - CertifiedTransferTransaction { - transaction: TransferTransaction { - version: number, - recipient: IPredicate, - stateMask: Uint8Array, - data: Uint8Array | null - }, - inclusionProof: InclusionProof - }, - ... - ] -} -``` +A `Token` is a self-contained, CBOR-serializable record that bundles its genesis with an ordered transfer history: + +- `genesis`: a `CertifiedMintTransaction` (a `MintTransaction` plus its `InclusionProof`). The mint transaction carries `networkId`, `tokenId`, `tokenType`, `salt`, `recipient`, optional `justification`, and optional `data`. +- `transactions`: an ordered list of `CertifiedTransferTransaction` entries, each wrapping a `TransferTransaction` (recipient, state mask, optional data) with its `InclusionProof`. + +See [`src/transaction/Token.ts`](./src/transaction/Token.ts) for the authoritative shape. ### Privacy Model - **Commitment-based**: Only cryptographic commitments published on-chain @@ -110,29 +103,22 @@ npm run build ### Testing -Run unit and integration tests. -NB! Integration tests require docker to be installed. +Run the default suite (unit + functional tests): ```bash npm test ``` -Run unit tests only. +Run the example flows (requires a reachable aggregator; URL is read from each example's `config.json`): ```bash -npm run test:unit +npm run test:examples ``` -Run integration tests only. +Run the end-to-end suite (expects a local aggregator at `http://localhost:3000` — see [`tests/e2e/E2ETransitionFlowTest.ts`](./tests/e2e/E2ETransitionFlowTest.ts)): ```bash -npm run test:integration -``` - -Run end-to-end tests only. - -```bash -AGGREGATOR_URL='https://gateway-test.unicity.network' npm run test:e2e +npm run test:e2e ``` ### Linting @@ -150,18 +136,8 @@ npm run lint:fix ## Network Configuration - **Test Gateway**: `https://gateway-test.unicity.network` -- **Default Token Type**: Configurable via TokenType enum - -## Examples - -### Minting Tokens -`tests/examples/mint/ExampleTest.ts` - -### Token Transfer -`tests/examples/transfer/ExampleTest.ts` - -### Token Splitting -`tests/examples/split/ExampleTest.ts` +- **Network identifiers**: `NetworkId.MAINNET`, `NetworkId.TESTNET`, `NetworkId.LOCAL` +- **Token type**: caller-supplied; use `TokenType.generate()` or construct from explicit bytes ## Unicity Signature Standard diff --git a/src/api/NetworkId.ts b/src/api/NetworkId.ts index 4fdfbc1..4f9c35c 100644 --- a/src/api/NetworkId.ts +++ b/src/api/NetworkId.ts @@ -6,12 +6,12 @@ * {@link NetworkId.fromId}. */ export class NetworkId { - public static readonly LOCAL = new NetworkId(3n, 'LOCAL'); - public static readonly MAINNET = new NetworkId(1n, 'MAINNET'); - public static readonly TESTNET = new NetworkId(2n, 'TESTNET'); + public static readonly LOCAL = new NetworkId(3, 'LOCAL'); + public static readonly MAINNET = new NetworkId(1, 'MAINNET'); + public static readonly TESTNET = new NetworkId(2, 'TESTNET'); private constructor( - public readonly id: bigint, + public readonly id: number, public readonly name: string, ) {} @@ -23,7 +23,11 @@ export class NetworkId { * @throws {Error} If `id` is `0`, negative, or not registered. */ public static fromId(id: number | bigint): NetworkId { - switch (BigInt(id)) { + const value = BigInt(id); + if (value < 0n || value > 0xffffffffn) { + throw new Error(`Network identifier out of 32-bit unsigned range: ${id}.`); + } + switch (Number(value)) { case NetworkId.MAINNET.id: return NetworkId.MAINNET; case NetworkId.TESTNET.id: diff --git a/src/transaction/verification/rule/CertifiedMintTransactionVerificationRule.ts b/src/transaction/verification/rule/CertifiedMintTransactionVerificationRule.ts index 0aa7f71..3f23b68 100644 --- a/src/transaction/verification/rule/CertifiedMintTransactionVerificationRule.ts +++ b/src/transaction/verification/rule/CertifiedMintTransactionVerificationRule.ts @@ -30,7 +30,7 @@ export class CertifiedMintTransactionVerificationRule { ): Promise> { const results: VerificationResult[] = []; - if (genesis.networkId.id !== BigInt(trustBase.networkId)) { + if (genesis.networkId.id !== trustBase.networkId) { results.push(new VerificationResult('MintNetworkMatchesTrustBaseRule', VerificationStatus.FAIL)); return new VerificationResult( 'CertifiedMintTransactionVerificationRule', diff --git a/tests/utils/RootTrustBaseFixture.ts b/tests/utils/RootTrustBaseFixture.ts index 7753bda..1c11cc3 100644 --- a/tests/utils/RootTrustBaseFixture.ts +++ b/tests/utils/RootTrustBaseFixture.ts @@ -7,7 +7,7 @@ export function createRootTrustBase(publicKey: Uint8Array): RootTrustBase { changeRecordHash: null, epoch: '0', epochStartRound: '0', - networkId: Number(NetworkId.LOCAL.id), + networkId: NetworkId.LOCAL.id, previousEntryHash: null, quorumThreshold: '1', rootNodes: [ diff --git a/tests/utils/UnicityCertificateFixture.ts b/tests/utils/UnicityCertificateFixture.ts index fe2fd84..46e54fb 100644 --- a/tests/utils/UnicityCertificateFixture.ts +++ b/tests/utils/UnicityCertificateFixture.ts @@ -43,7 +43,7 @@ export async function createUnicityCertificate( .digest(); const seal = await UnicitySeal.create( - NetworkId.LOCAL.id, + BigInt(NetworkId.LOCAL.id), 0n, 0n, 0n, From 4439a570711049c4122f64d2cc9a27640acfdf87 Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Fri, 22 May 2026 17:15:05 +0300 Subject: [PATCH 06/13] #115 Update dependencies --- package-lock.json | 1122 ++++++++++++++++++++++++--------------------- package.json | 16 +- 2 files changed, 599 insertions(+), 539 deletions(-) diff --git a/package-lock.json b/package-lock.json index a8b8ebe..f72f66f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,23 +11,23 @@ "dependencies": { "@noble/curves": "2.2.0", "@noble/hashes": "2.2.0", - "uuid": "13.0.0" + "uuid": "14.0.0" }, "devDependencies": { - "@babel/preset-env": "7.29.2", + "@babel/preset-env": "7.29.5", "@babel/preset-typescript": "7.28.5", "@eslint/js": "9.39.4", "@types/jest": "30.0.0", - "@types/node": "^20.19.39", - "babel-jest": "30.3.0", + "@types/node": "20.19.41", + "babel-jest": "30.4.1", "eslint": "9.39.4", "eslint-config-prettier": "10.1.8", "eslint-plugin-import": "2.32.0", "eslint-plugin-prettier": "5.5.5", - "globals": "17.4.0", - "jest": "30.3.0", - "typescript": "6.0.2", - "typescript-eslint": "8.58.2" + "globals": "17.6.0", + "jest": "30.4.2", + "typescript": "6.0.3", + "typescript-eslint": "8.59.4" } }, "node_modules/@ampproject/remapping": { @@ -60,9 +60,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", "dev": true, "license": "MIT", "engines": { @@ -458,6 +458,23 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.3.tgz", + "integrity": "sha512-SRS46DFR4HqzUzCVgi90/xMoL+zeBDBvWdKYXSEzh79kXswNFEglUpMKxR04//dPqwYXWUBJ3mpUd933ru9Kmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", @@ -1200,9 +1217,9 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", - "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", + "version": "7.29.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz", + "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==", "dev": true, "license": "MIT", "dependencies": { @@ -1655,19 +1672,20 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.2.tgz", - "integrity": "sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==", + "version": "7.29.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.5.tgz", + "integrity": "sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.29.0", + "@babel/compat-data": "^7.29.3", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.3", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", @@ -1699,7 +1717,7 @@ "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.28.6", - "@babel/plugin-transform-modules-systemjs": "^7.29.0", + "@babel/plugin-transform-modules-systemjs": "^7.29.4", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", "@babel/plugin-transform-new-target": "^7.27.1", @@ -1829,29 +1847,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -2152,17 +2147,17 @@ } }, "node_modules/@jest/console": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz", - "integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.4.1.tgz", + "integrity": "sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.3.0", + "@jest/types": "30.4.1", "@types/node": "*", "chalk": "^4.1.2", - "jest-message-util": "30.3.0", - "jest-util": "30.3.0", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", "slash": "^3.0.0" }, "engines": { @@ -2170,38 +2165,39 @@ } }, "node_modules/@jest/core": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz", - "integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==", + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.4.2.tgz", + "integrity": "sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.3.0", - "@jest/pattern": "30.0.1", - "@jest/reporters": "30.3.0", - "@jest/test-result": "30.3.0", - "@jest/transform": "30.3.0", - "@jest/types": "30.3.0", + "@jest/console": "30.4.1", + "@jest/pattern": "30.4.0", + "@jest/reporters": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "ci-info": "^4.2.0", "exit-x": "^0.2.2", + "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-changed-files": "30.3.0", - "jest-config": "30.3.0", - "jest-haste-map": "30.3.0", - "jest-message-util": "30.3.0", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.3.0", - "jest-resolve-dependencies": "30.3.0", - "jest-runner": "30.3.0", - "jest-runtime": "30.3.0", - "jest-snapshot": "30.3.0", - "jest-util": "30.3.0", - "jest-validate": "30.3.0", - "jest-watcher": "30.3.0", - "pretty-format": "30.3.0", + "jest-changed-files": "30.4.1", + "jest-config": "30.4.2", + "jest-haste-map": "30.4.1", + "jest-message-util": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-resolve-dependencies": "30.4.2", + "jest-runner": "30.4.2", + "jest-runtime": "30.4.2", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "jest-watcher": "30.4.1", + "pretty-format": "30.4.1", "slash": "^3.0.0" }, "engines": { @@ -2217,9 +2213,9 @@ } }, "node_modules/@jest/diff-sequences": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", - "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", + "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", "dev": true, "license": "MIT", "engines": { @@ -2227,39 +2223,39 @@ } }, "node_modules/@jest/environment": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", - "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz", + "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==", "dev": true, "license": "MIT", "dependencies": { - "@jest/fake-timers": "30.3.0", - "@jest/types": "30.3.0", + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", "@types/node": "*", - "jest-mock": "30.3.0" + "jest-mock": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.3.0.tgz", - "integrity": "sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==", "dev": true, "license": "MIT", "dependencies": { - "expect": "30.3.0", - "jest-snapshot": "30.3.0" + "expect": "30.4.1", + "jest-snapshot": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", - "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz", + "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2270,18 +2266,18 @@ } }, "node_modules/@jest/fake-timers": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", - "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz", + "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.3.0", - "@sinonjs/fake-timers": "^15.0.0", + "@jest/types": "30.4.1", + "@sinonjs/fake-timers": "^15.4.0", "@types/node": "*", - "jest-message-util": "30.3.0", - "jest-mock": "30.3.0", - "jest-util": "30.3.0" + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -2298,47 +2294,47 @@ } }, "node_modules/@jest/globals": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz", - "integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.4.1.tgz", + "integrity": "sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.3.0", - "@jest/expect": "30.3.0", - "@jest/types": "30.3.0", - "jest-mock": "30.3.0" + "@jest/environment": "30.4.1", + "@jest/expect": "30.4.1", + "@jest/types": "30.4.1", + "jest-mock": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/pattern": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", - "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", + "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", - "jest-regex-util": "30.0.1" + "jest-regex-util": "30.4.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/reporters": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.3.0.tgz", - "integrity": "sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.4.1.tgz", + "integrity": "sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.3.0", - "@jest/test-result": "30.3.0", - "@jest/transform": "30.3.0", - "@jest/types": "30.3.0", + "@jest/console": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", "@jridgewell/trace-mapping": "^0.3.25", "@types/node": "*", "chalk": "^4.1.2", @@ -2351,9 +2347,9 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^5.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "30.3.0", - "jest-util": "30.3.0", - "jest-worker": "30.3.0", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "jest-worker": "30.4.1", "slash": "^3.0.0", "string-length": "^4.0.2", "v8-to-istanbul": "^9.0.1" @@ -2371,9 +2367,9 @@ } }, "node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2384,13 +2380,13 @@ } }, "node_modules/@jest/snapshot-utils": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz", - "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.4.1.tgz", + "integrity": "sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.3.0", + "@jest/types": "30.4.1", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "natural-compare": "^1.4.0" @@ -2415,14 +2411,14 @@ } }, "node_modules/@jest/test-result": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz", - "integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.4.1.tgz", + "integrity": "sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.3.0", - "@jest/types": "30.3.0", + "@jest/console": "30.4.1", + "@jest/types": "30.4.1", "@types/istanbul-lib-coverage": "^2.0.6", "collect-v8-coverage": "^1.0.2" }, @@ -2431,15 +2427,15 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz", - "integrity": "sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.4.1.tgz", + "integrity": "sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.3.0", + "@jest/test-result": "30.4.1", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.3.0", + "jest-haste-map": "30.4.1", "slash": "^3.0.0" }, "engines": { @@ -2447,23 +2443,23 @@ } }, "node_modules/@jest/transform": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz", - "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.4.1.tgz", + "integrity": "sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", - "@jest/types": "30.3.0", + "@jest/types": "30.4.1", "@jridgewell/trace-mapping": "^0.3.25", "babel-plugin-istanbul": "^7.0.1", "chalk": "^4.1.2", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.3.0", - "jest-regex-util": "30.0.1", - "jest-util": "30.3.0", + "jest-haste-map": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-util": "30.4.1", "pirates": "^4.0.7", "slash": "^3.0.0", "write-file-atomic": "^5.0.1" @@ -2473,14 +2469,14 @@ } }, "node_modules/@jest/types": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", - "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", @@ -2531,16 +2527,22 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, "node_modules/@noble/curves": { @@ -2619,9 +2621,9 @@ } }, "node_modules/@sinonjs/fake-timers": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.2.tgz", - "integrity": "sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==", + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2629,9 +2631,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "dev": true, "license": "MIT", "optional": true, @@ -2744,9 +2746,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", - "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2778,17 +2780,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz", - "integrity": "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz", + "integrity": "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.58.2", - "@typescript-eslint/type-utils": "8.58.2", - "@typescript-eslint/utils": "8.58.2", - "@typescript-eslint/visitor-keys": "8.58.2", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/type-utils": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -2801,7 +2803,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.58.2", + "@typescript-eslint/parser": "^8.59.4", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -2817,17 +2819,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz", - "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz", + "integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.58.2", - "@typescript-eslint/types": "8.58.2", - "@typescript-eslint/typescript-estree": "8.58.2", - "@typescript-eslint/visitor-keys": "8.58.2", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", "debug": "^4.4.3" }, "engines": { @@ -2843,14 +2845,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.2.tgz", - "integrity": "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz", + "integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.58.2", - "@typescript-eslint/types": "^8.58.2", + "@typescript-eslint/tsconfig-utils": "^8.59.4", + "@typescript-eslint/types": "^8.59.4", "debug": "^4.4.3" }, "engines": { @@ -2865,14 +2867,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz", - "integrity": "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz", + "integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.2", - "@typescript-eslint/visitor-keys": "8.58.2" + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2883,9 +2885,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz", - "integrity": "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz", + "integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==", "dev": true, "license": "MIT", "engines": { @@ -2900,15 +2902,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz", - "integrity": "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz", + "integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.2", - "@typescript-eslint/typescript-estree": "8.58.2", - "@typescript-eslint/utils": "8.58.2", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -2925,9 +2927,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz", - "integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz", + "integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==", "dev": true, "license": "MIT", "engines": { @@ -2939,16 +2941,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz", - "integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz", + "integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.58.2", - "@typescript-eslint/tsconfig-utils": "8.58.2", - "@typescript-eslint/types": "8.58.2", - "@typescript-eslint/visitor-keys": "8.58.2", + "@typescript-eslint/project-service": "8.59.4", + "@typescript-eslint/tsconfig-utils": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -2977,9 +2979,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -3006,9 +3008,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", "dev": true, "license": "ISC", "bin": { @@ -3019,16 +3021,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz", - "integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.4.tgz", + "integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.58.2", - "@typescript-eslint/types": "8.58.2", - "@typescript-eslint/typescript-estree": "8.58.2" + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3043,13 +3045,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", - "integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz", + "integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/types": "8.59.4", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -3074,16 +3076,16 @@ } }, "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", "dev": true, "license": "ISC" }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", "cpu": [ "arm" ], @@ -3095,9 +3097,9 @@ ] }, "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", "cpu": [ "arm64" ], @@ -3109,9 +3111,9 @@ ] }, "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", "cpu": [ "arm64" ], @@ -3123,9 +3125,9 @@ ] }, "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", "cpu": [ "x64" ], @@ -3137,9 +3139,9 @@ ] }, "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", "cpu": [ "x64" ], @@ -3151,9 +3153,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", "cpu": [ "arm" ], @@ -3165,9 +3167,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", "cpu": [ "arm" ], @@ -3179,9 +3181,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", "cpu": [ "arm64" ], @@ -3193,9 +3195,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", "cpu": [ "arm64" ], @@ -3206,10 +3208,38 @@ "linux" ] }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", "cpu": [ "ppc64" ], @@ -3221,9 +3251,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", "cpu": [ "riscv64" ], @@ -3235,9 +3265,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", "cpu": [ "riscv64" ], @@ -3249,9 +3279,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", "cpu": [ "s390x" ], @@ -3263,9 +3293,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", "cpu": [ "x64" ], @@ -3277,9 +3307,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", "cpu": [ "x64" ], @@ -3290,10 +3320,24 @@ "linux" ] }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", "cpu": [ "wasm32" ], @@ -3301,16 +3345,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", "cpu": [ "arm64" ], @@ -3322,9 +3368,9 @@ ] }, "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", "cpu": [ "ia32" ], @@ -3336,9 +3382,9 @@ ] }, "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", "cpu": [ "x64" ], @@ -3621,16 +3667,16 @@ } }, "node_modules/babel-jest": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz", - "integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz", + "integrity": "sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/transform": "30.3.0", + "@jest/transform": "30.4.1", "@types/babel__core": "^7.20.5", "babel-plugin-istanbul": "^7.0.1", - "babel-preset-jest": "30.3.0", + "babel-preset-jest": "30.4.0", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "slash": "^3.0.0" @@ -3663,9 +3709,9 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz", - "integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==", + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.4.0.tgz", + "integrity": "sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==", "dev": true, "license": "MIT", "dependencies": { @@ -3745,13 +3791,13 @@ } }, "node_modules/babel-preset-jest": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz", - "integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==", + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.4.0.tgz", + "integrity": "sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==", "dev": true, "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "30.3.0", + "babel-plugin-jest-hoist": "30.4.0", "babel-preset-current-node-syntax": "^1.2.0" }, "engines": { @@ -4937,18 +4983,18 @@ } }, "node_modules/expect": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", - "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.3.0", + "@jest/expect-utils": "30.4.1", "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.3.0", - "jest-message-util": "30.3.0", - "jest-mock": "30.3.0", - "jest-util": "30.3.0" + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -5316,9 +5362,9 @@ } }, "node_modules/globals": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", - "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", "dev": true, "license": "MIT", "engines": { @@ -6113,16 +6159,16 @@ } }, "node_modules/jest": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.3.0.tgz", - "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.4.2.tgz", + "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.3.0", - "@jest/types": "30.3.0", + "@jest/core": "30.4.2", + "@jest/types": "30.4.1", "import-local": "^3.2.0", - "jest-cli": "30.3.0" + "jest-cli": "30.4.2" }, "bin": { "jest": "bin/jest.js" @@ -6140,14 +6186,14 @@ } }, "node_modules/jest-changed-files": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.3.0.tgz", - "integrity": "sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.4.1.tgz", + "integrity": "sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==", "dev": true, "license": "MIT", "dependencies": { "execa": "^5.1.1", - "jest-util": "30.3.0", + "jest-util": "30.4.1", "p-limit": "^3.1.0" }, "engines": { @@ -6155,29 +6201,29 @@ } }, "node_modules/jest-circus": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.3.0.tgz", - "integrity": "sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==", + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.4.2.tgz", + "integrity": "sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.3.0", - "@jest/expect": "30.3.0", - "@jest/test-result": "30.3.0", - "@jest/types": "30.3.0", + "@jest/environment": "30.4.1", + "@jest/expect": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", "@types/node": "*", "chalk": "^4.1.2", "co": "^4.6.0", "dedent": "^1.6.0", "is-generator-fn": "^2.1.0", - "jest-each": "30.3.0", - "jest-matcher-utils": "30.3.0", - "jest-message-util": "30.3.0", - "jest-runtime": "30.3.0", - "jest-snapshot": "30.3.0", - "jest-util": "30.3.0", + "jest-each": "30.4.1", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-runtime": "30.4.2", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", "p-limit": "^3.1.0", - "pretty-format": "30.3.0", + "pretty-format": "30.4.1", "pure-rand": "^7.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" @@ -6187,21 +6233,21 @@ } }, "node_modules/jest-cli": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.3.0.tgz", - "integrity": "sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==", + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.4.2.tgz", + "integrity": "sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.3.0", - "@jest/test-result": "30.3.0", - "@jest/types": "30.3.0", + "@jest/core": "30.4.2", + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", "chalk": "^4.1.2", "exit-x": "^0.2.2", "import-local": "^3.2.0", - "jest-config": "30.3.0", - "jest-util": "30.3.0", - "jest-validate": "30.3.0", + "jest-config": "30.4.2", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", "yargs": "^17.7.2" }, "bin": { @@ -6220,33 +6266,33 @@ } }, "node_modules/jest-config": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.3.0.tgz", - "integrity": "sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==", + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.4.2.tgz", + "integrity": "sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", "@jest/get-type": "30.1.0", - "@jest/pattern": "30.0.1", - "@jest/test-sequencer": "30.3.0", - "@jest/types": "30.3.0", - "babel-jest": "30.3.0", + "@jest/pattern": "30.4.0", + "@jest/test-sequencer": "30.4.1", + "@jest/types": "30.4.1", + "babel-jest": "30.4.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "deepmerge": "^4.3.1", "glob": "^10.5.0", "graceful-fs": "^4.2.11", - "jest-circus": "30.3.0", - "jest-docblock": "30.2.0", - "jest-environment-node": "30.3.0", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.3.0", - "jest-runner": "30.3.0", - "jest-util": "30.3.0", - "jest-validate": "30.3.0", + "jest-circus": "30.4.2", + "jest-docblock": "30.4.0", + "jest-environment-node": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-runner": "30.4.2", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", "parse-json": "^5.2.0", - "pretty-format": "30.3.0", + "pretty-format": "30.4.1", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -6271,25 +6317,25 @@ } }, "node_modules/jest-diff": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", - "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", + "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.3.0", + "@jest/diff-sequences": "30.4.0", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.3.0" + "pretty-format": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-docblock": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", - "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.4.0.tgz", + "integrity": "sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==", "dev": true, "license": "MIT", "dependencies": { @@ -6300,56 +6346,56 @@ } }, "node_modules/jest-each": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.3.0.tgz", - "integrity": "sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.4.1.tgz", + "integrity": "sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "@jest/types": "30.3.0", + "@jest/types": "30.4.1", "chalk": "^4.1.2", - "jest-util": "30.3.0", - "pretty-format": "30.3.0" + "jest-util": "30.4.1", + "pretty-format": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-environment-node": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.3.0.tgz", - "integrity": "sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.4.1.tgz", + "integrity": "sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.3.0", - "@jest/fake-timers": "30.3.0", - "@jest/types": "30.3.0", + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", "@types/node": "*", - "jest-mock": "30.3.0", - "jest-util": "30.3.0", - "jest-validate": "30.3.0" + "jest-mock": "30.4.1", + "jest-util": "30.4.1", + "jest-validate": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-haste-map": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz", - "integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.4.1.tgz", + "integrity": "sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.3.0", + "@jest/types": "30.4.1", "@types/node": "*", "anymatch": "^3.1.3", "fb-watchman": "^2.0.2", "graceful-fs": "^4.2.11", - "jest-regex-util": "30.0.1", - "jest-util": "30.3.0", - "jest-worker": "30.3.0", + "jest-regex-util": "30.4.0", + "jest-util": "30.4.1", + "jest-worker": "30.4.1", "picomatch": "^4.0.3", "walker": "^1.0.8" }, @@ -6361,49 +6407,50 @@ } }, "node_modules/jest-leak-detector": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.3.0.tgz", - "integrity": "sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.4.1.tgz", + "integrity": "sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "pretty-format": "30.3.0" + "pretty-format": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", - "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", + "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "jest-diff": "30.3.0", - "pretty-format": "30.3.0" + "jest-diff": "30.4.1", + "pretty-format": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-message-util": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", - "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", + "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.3.0", + "@jest/types": "30.4.1", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", + "jest-util": "30.4.1", "picomatch": "^4.0.3", - "pretty-format": "30.3.0", + "pretty-format": "30.4.1", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, @@ -6412,15 +6459,15 @@ } }, "node_modules/jest-mock": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", - "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", + "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.3.0", + "@jest/types": "30.4.1", "@types/node": "*", - "jest-util": "30.3.0" + "jest-util": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -6445,9 +6492,9 @@ } }, "node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", + "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", "dev": true, "license": "MIT", "engines": { @@ -6455,18 +6502,18 @@ } }, "node_modules/jest-resolve": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.3.0.tgz", - "integrity": "sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.4.1.tgz", + "integrity": "sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.3.0", + "jest-haste-map": "30.4.1", "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.3.0", - "jest-validate": "30.3.0", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", "slash": "^3.0.0", "unrs-resolver": "^1.7.11" }, @@ -6475,46 +6522,46 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.3.0.tgz", - "integrity": "sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==", + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.4.2.tgz", + "integrity": "sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==", "dev": true, "license": "MIT", "dependencies": { - "jest-regex-util": "30.0.1", - "jest-snapshot": "30.3.0" + "jest-regex-util": "30.4.0", + "jest-snapshot": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runner": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.3.0.tgz", - "integrity": "sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==", + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.4.2.tgz", + "integrity": "sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.3.0", - "@jest/environment": "30.3.0", - "@jest/test-result": "30.3.0", - "@jest/transform": "30.3.0", - "@jest/types": "30.3.0", + "@jest/console": "30.4.1", + "@jest/environment": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", "@types/node": "*", "chalk": "^4.1.2", "emittery": "^0.13.1", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", - "jest-docblock": "30.2.0", - "jest-environment-node": "30.3.0", - "jest-haste-map": "30.3.0", - "jest-leak-detector": "30.3.0", - "jest-message-util": "30.3.0", - "jest-resolve": "30.3.0", - "jest-runtime": "30.3.0", - "jest-util": "30.3.0", - "jest-watcher": "30.3.0", - "jest-worker": "30.3.0", + "jest-docblock": "30.4.0", + "jest-environment-node": "30.4.1", + "jest-haste-map": "30.4.1", + "jest-leak-detector": "30.4.1", + "jest-message-util": "30.4.1", + "jest-resolve": "30.4.1", + "jest-runtime": "30.4.2", + "jest-util": "30.4.1", + "jest-watcher": "30.4.1", + "jest-worker": "30.4.1", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -6523,32 +6570,32 @@ } }, "node_modules/jest-runtime": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.3.0.tgz", - "integrity": "sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==", + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.4.2.tgz", + "integrity": "sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.3.0", - "@jest/fake-timers": "30.3.0", - "@jest/globals": "30.3.0", + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/globals": "30.4.1", "@jest/source-map": "30.0.1", - "@jest/test-result": "30.3.0", - "@jest/transform": "30.3.0", - "@jest/types": "30.3.0", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", "@types/node": "*", "chalk": "^4.1.2", "cjs-module-lexer": "^2.1.0", "collect-v8-coverage": "^1.0.2", "glob": "^10.5.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.3.0", - "jest-message-util": "30.3.0", - "jest-mock": "30.3.0", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.3.0", - "jest-snapshot": "30.3.0", - "jest-util": "30.3.0", + "jest-haste-map": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -6557,9 +6604,9 @@ } }, "node_modules/jest-snapshot": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz", - "integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.4.1.tgz", + "integrity": "sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==", "dev": true, "license": "MIT", "dependencies": { @@ -6568,20 +6615,20 @@ "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.3.0", + "@jest/expect-utils": "30.4.1", "@jest/get-type": "30.1.0", - "@jest/snapshot-utils": "30.3.0", - "@jest/transform": "30.3.0", - "@jest/types": "30.3.0", + "@jest/snapshot-utils": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", "babel-preset-current-node-syntax": "^1.2.0", "chalk": "^4.1.2", - "expect": "30.3.0", + "expect": "30.4.1", "graceful-fs": "^4.2.11", - "jest-diff": "30.3.0", - "jest-matcher-utils": "30.3.0", - "jest-message-util": "30.3.0", - "jest-util": "30.3.0", - "pretty-format": "30.3.0", + "jest-diff": "30.4.1", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "pretty-format": "30.4.1", "semver": "^7.7.2", "synckit": "^0.11.8" }, @@ -6590,9 +6637,9 @@ } }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", "dev": true, "license": "ISC", "bin": { @@ -6603,13 +6650,13 @@ } }, "node_modules/jest-util": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", - "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.3.0", + "@jest/types": "30.4.1", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", @@ -6621,18 +6668,18 @@ } }, "node_modules/jest-validate": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz", - "integrity": "sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.4.1.tgz", + "integrity": "sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "@jest/types": "30.3.0", + "@jest/types": "30.4.1", "camelcase": "^6.3.0", "chalk": "^4.1.2", "leven": "^3.1.0", - "pretty-format": "30.3.0" + "pretty-format": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -6652,19 +6699,19 @@ } }, "node_modules/jest-watcher": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.3.0.tgz", - "integrity": "sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.4.1.tgz", + "integrity": "sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.3.0", - "@jest/types": "30.3.0", + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "emittery": "^0.13.1", - "jest-util": "30.3.0", + "jest-util": "30.4.1", "string-length": "^4.0.2" }, "engines": { @@ -6672,15 +6719,15 @@ } }, "node_modules/jest-worker": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", - "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz", + "integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.3.0", + "jest-util": "30.4.1", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" }, @@ -6874,9 +6921,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", "dev": true, "license": "ISC", "bin": { @@ -7431,15 +7478,16 @@ } }, "node_modules/pretty-format": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", - "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.5", + "@jest/schemas": "30.4.1", "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -7485,13 +7533,22 @@ ], "license": "MIT" }, - "node_modules/react-is": { + "node_modules/react-is-18": { + "name": "react-is", "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, "license": "MIT" }, + "node_modules/react-is-19": { + "name": "react-is", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", + "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", + "dev": true, + "license": "MIT" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -8457,9 +8514,9 @@ } }, "node_modules/typescript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", - "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -8472,16 +8529,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.2.tgz", - "integrity": "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.4.tgz", + "integrity": "sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.58.2", - "@typescript-eslint/parser": "8.58.2", - "@typescript-eslint/typescript-estree": "8.58.2", - "@typescript-eslint/utils": "8.58.2" + "@typescript-eslint/eslint-plugin": "8.59.4", + "@typescript-eslint/parser": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8566,38 +8623,41 @@ } }, "node_modules/unrs-resolver": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { - "napi-postinstall": "^0.3.0" + "napi-postinstall": "^0.3.4" }, "funding": { "url": "https://opencollective.com/unrs-resolver" }, "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.11.1", - "@unrs/resolver-binding-android-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-x64": "1.11.1", - "@unrs/resolver-binding-freebsd-x64": "1.11.1", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", - "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-musl": "1.11.1", - "@unrs/resolver-binding-wasm32-wasi": "1.11.1", - "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", - "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", - "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" } }, "node_modules/update-browserslist-db": { @@ -8642,9 +8702,9 @@ } }, "node_modules/uuid": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" diff --git a/package.json b/package.json index cca6ac0..4f9f610 100644 --- a/package.json +++ b/package.json @@ -35,22 +35,22 @@ "dependencies": { "@noble/curves": "2.2.0", "@noble/hashes": "2.2.0", - "uuid": "13.0.0" + "uuid": "14.0.0" }, "devDependencies": { - "@babel/preset-env": "7.29.2", + "@babel/preset-env": "7.29.5", "@babel/preset-typescript": "7.28.5", "@eslint/js": "9.39.4", "@types/jest": "30.0.0", - "@types/node": "^20.19.39", - "babel-jest": "30.3.0", + "@types/node": "20.19.41", + "babel-jest": "30.4.1", "eslint": "9.39.4", "eslint-config-prettier": "10.1.8", "eslint-plugin-import": "2.32.0", "eslint-plugin-prettier": "5.5.5", - "globals": "17.4.0", - "jest": "30.3.0", - "typescript": "6.0.2", - "typescript-eslint": "8.58.2" + "globals": "17.6.0", + "jest": "30.4.2", + "typescript": "6.0.3", + "typescript-eslint": "8.59.4" } } From 0d514dd64f564a851139eb7fbb57bfb0f061aac3 Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Fri, 22 May 2026 17:49:27 +0300 Subject: [PATCH 07/13] #115 Replace rootTrustBase and UnicitySeal networkId type with NetworkId --- src/api/NetworkId.ts | 4 ++-- src/api/bft/RootTrustBase.ts | 7 ++++--- src/api/bft/UnicitySeal.ts | 11 ++++++----- .../verification/UnicityCertificateVerification.ts | 3 +-- src/serialization/cbor/CborReader.ts | 6 +++--- .../rule/CertifiedMintTransactionVerificationRule.ts | 2 +- tests/utils/UnicityCertificateFixture.ts | 2 +- 7 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/api/NetworkId.ts b/src/api/NetworkId.ts index 4f9c35c..cd5bae9 100644 --- a/src/api/NetworkId.ts +++ b/src/api/NetworkId.ts @@ -24,8 +24,8 @@ export class NetworkId { */ public static fromId(id: number | bigint): NetworkId { const value = BigInt(id); - if (value < 0n || value > 0xffffffffn) { - throw new Error(`Network identifier out of 32-bit unsigned range: ${id}.`); + if (value < 0n || value > 0xffffn) { + throw new Error(`Network identifier out of 16-bit unsigned range: ${id}.`); } switch (Number(value)) { case NetworkId.MAINNET.id: diff --git a/src/api/bft/RootTrustBase.ts b/src/api/bft/RootTrustBase.ts index 524b7b0..e542086 100644 --- a/src/api/bft/RootTrustBase.ts +++ b/src/api/bft/RootTrustBase.ts @@ -1,6 +1,7 @@ import { InvalidJsonStructureError } from '../../InvalidJsonStructureError.js'; import { HexConverter } from '../../util/HexConverter.js'; import { dedent } from '../../util/StringUtils.js'; +import { NetworkId } from '../NetworkId.js'; /** * JSON representation of a root trust base node information. @@ -89,7 +90,7 @@ interface IRootTrustBaseJson { export class RootTrustBase { public constructor( public readonly version: bigint, - public readonly networkId: number, + public readonly networkId: NetworkId, public readonly epoch: bigint, public readonly epochStartRound: bigint, public readonly _rootNodes: RootTrustBaseNodeInfo[], @@ -158,7 +159,7 @@ export class RootTrustBase { return new RootTrustBase( BigInt(input.version), - input.networkId, + NetworkId.fromId(input.networkId), BigInt(input.epoch), BigInt(input.epochStartRound), input.rootNodes.map((node) => RootTrustBaseNodeInfo.fromJSON(node)), @@ -202,7 +203,7 @@ export class RootTrustBase { return dedent` RootTrustBase: Version: ${this.version}, - NetworkId: ${this.networkId}, + NetworkId: ${this.networkId.toString()}, Epoch: ${this.epoch}, EpochStartRound: ${this.epochStartRound}, RootNodes: [${this.rootNodes.map((node) => node.nodeId).join(', ')}], diff --git a/src/api/bft/UnicitySeal.ts b/src/api/bft/UnicitySeal.ts index f22763a..cbb6e43 100644 --- a/src/api/bft/UnicitySeal.ts +++ b/src/api/bft/UnicitySeal.ts @@ -9,6 +9,7 @@ import { CborMapEntry } from '../../serialization/cbor/CborMapEntry.js'; import { CborSerializer } from '../../serialization/cbor/CborSerializer.js'; import { HexConverter } from '../../util/HexConverter.js'; import { dedent } from '../../util/StringUtils.js'; +import { NetworkId } from '../NetworkId.js'; /** * UnicitySeal represents a seal in the Unicity BFT system, containing metadata and signatures. @@ -18,7 +19,7 @@ export class UnicitySeal { private static readonly VERSION = 1n; private constructor( - public readonly networkId: bigint, + public readonly networkId: NetworkId, public readonly rootChainRoundNumber: bigint, public readonly epoch: bigint, public readonly timestamp: bigint, @@ -60,7 +61,7 @@ export class UnicitySeal { /** * Create a UnicitySeal and sign it with the given signing services. * - * @param {bigint} networkId Network identifier. + * @param {NetworkId} networkId Network identifier. * @param {bigint} rootChainRoundNumber Root-chain round number. * @param {bigint} epoch Epoch number. * @param {bigint} timestamp Timestamp. @@ -70,7 +71,7 @@ export class UnicitySeal { * @returns {Promise} Signed seal. */ public static async create( - networkId: bigint, + networkId: NetworkId, rootChainRoundNumber: bigint, epoch: bigint, timestamp: bigint, @@ -118,7 +119,7 @@ export class UnicitySeal { } return new UnicitySeal( - CborDeserializer.decodeUnsignedInteger(data[1]), + NetworkId.fromId(CborDeserializer.decodeUnsignedInteger(data[1])), CborDeserializer.decodeUnsignedInteger(data[2]), CborDeserializer.decodeUnsignedInteger(data[3]), CborDeserializer.decodeUnsignedInteger(data[4]), @@ -162,7 +163,7 @@ export class UnicitySeal { UnicitySeal.CBOR_TAG, CborSerializer.encodeArray( CborSerializer.encodeUnsignedInteger(this.version), - CborSerializer.encodeUnsignedInteger(this.networkId), + CborSerializer.encodeUnsignedInteger(this.networkId.id), CborSerializer.encodeUnsignedInteger(this.rootChainRoundNumber), CborSerializer.encodeUnsignedInteger(this.epoch), CborSerializer.encodeUnsignedInteger(this.timestamp), diff --git a/src/api/bft/verification/UnicityCertificateVerification.ts b/src/api/bft/verification/UnicityCertificateVerification.ts index 55093d0..bf6b60d 100644 --- a/src/api/bft/verification/UnicityCertificateVerification.ts +++ b/src/api/bft/verification/UnicityCertificateVerification.ts @@ -51,8 +51,7 @@ export class UnicityCertificateVerification { ): Promise { const results: VerificationResult[] = []; - const sealNetworkId = inclusionProof.unicityCertificate.unicitySeal.networkId; - if (sealNetworkId !== BigInt(trustBase.networkId)) { + if (inclusionProof.unicityCertificate.unicitySeal.networkId !== trustBase.networkId) { results.push(new VerificationResult('UnicitySealNetworkMatchesTrustBaseRule', VerificationStatus.FAIL)); return UnicityCertificateVerificationResult.fail(results); } diff --git a/src/serialization/cbor/CborReader.ts b/src/serialization/cbor/CborReader.ts index 4ac7615..768079e 100644 --- a/src/serialization/cbor/CborReader.ts +++ b/src/serialization/cbor/CborReader.ts @@ -72,7 +72,7 @@ export class CborReader { */ public readLength(majorType: MajorType): bigint { const initialByte = this.readByte(); - const parsedMajorType = (initialByte & CborReader.MAJOR_TYPE_MASK) as MajorType; + const parsedMajorType: MajorType = initialByte & CborReader.MAJOR_TYPE_MASK; if (parsedMajorType !== majorType) { throw new CborError(`Major type mismatch: expected ${majorType}, got ${parsedMajorType}.`); @@ -123,10 +123,10 @@ export class CborReader { throw new CborError('Premature end of data.'); } - const majorType = this.data[this.position] & CborReader.MAJOR_TYPE_MASK; + const majorType: MajorType = this.data[this.position] & CborReader.MAJOR_TYPE_MASK; const position = this.position; const length = this.readLength(majorType); - switch (majorType as MajorType) { + switch (majorType) { case MajorType.BYTE_STRING: case MajorType.TEXT_STRING: this.read(Number(length)); diff --git a/src/transaction/verification/rule/CertifiedMintTransactionVerificationRule.ts b/src/transaction/verification/rule/CertifiedMintTransactionVerificationRule.ts index 3f23b68..dde52a7 100644 --- a/src/transaction/verification/rule/CertifiedMintTransactionVerificationRule.ts +++ b/src/transaction/verification/rule/CertifiedMintTransactionVerificationRule.ts @@ -30,7 +30,7 @@ export class CertifiedMintTransactionVerificationRule { ): Promise> { const results: VerificationResult[] = []; - if (genesis.networkId.id !== trustBase.networkId) { + if (genesis.networkId !== trustBase.networkId) { results.push(new VerificationResult('MintNetworkMatchesTrustBaseRule', VerificationStatus.FAIL)); return new VerificationResult( 'CertifiedMintTransactionVerificationRule', diff --git a/tests/utils/UnicityCertificateFixture.ts b/tests/utils/UnicityCertificateFixture.ts index 46e54fb..f83a0f0 100644 --- a/tests/utils/UnicityCertificateFixture.ts +++ b/tests/utils/UnicityCertificateFixture.ts @@ -43,7 +43,7 @@ export async function createUnicityCertificate( .digest(); const seal = await UnicitySeal.create( - BigInt(NetworkId.LOCAL.id), + NetworkId.LOCAL, 0n, 0n, 0n, From bdf6ffccb627ac0b17e131829155b8b77a152d28 Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Fri, 22 May 2026 18:08:32 +0300 Subject: [PATCH 08/13] #115 updated package-lock.json --- package-lock.json | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index f72f66f..3a84409 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,7 +75,6 @@ "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1847,6 +1846,29 @@ "dev": true, "license": "MIT" }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -2824,7 +2846,6 @@ "integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.4", "@typescript-eslint/types": "8.59.4", @@ -3401,7 +3422,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3855,7 +3875,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4587,7 +4606,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4648,7 +4666,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -8519,7 +8536,6 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" From 2c39f21ca93ad9490be513b839d24d4b07a48449 Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Fri, 22 May 2026 19:24:19 +0300 Subject: [PATCH 09/13] #115 Change minting parameter ordering --- src/transaction/MintTransaction.ts | 11 +++++------ tests/examples/mint/ExampleTest.ts | 4 +--- tests/examples/split/ExampleTest.ts | 6 ++---- tests/examples/transfer/ExampleTest.ts | 4 +--- tests/functional/payment/SplitBuilderTest.ts | 12 ++---------- tests/unit/api/CertificationDataTest.ts | 1 + tests/unit/api/InclusionProofTest.ts | 12 ++---------- tests/unit/api/StateIdTest.ts | 1 + tests/utils/TokenUtils.ts | 4 ++-- 9 files changed, 17 insertions(+), 38 deletions(-) diff --git a/src/transaction/MintTransaction.ts b/src/transaction/MintTransaction.ts index 5d35abc..6aa82b2 100644 --- a/src/transaction/MintTransaction.ts +++ b/src/transaction/MintTransaction.ts @@ -73,21 +73,20 @@ export class MintTransaction implements ITransaction { * * @param {NetworkId} networkId Network identifier. * @param {IPredicate} recipient Predicate that will lock the minted state. + * @param {Uint8Array|null} data Optional data payload. * @param {TokenType} tokenType Token type being minted. * @param {TokenSalt} salt Mint-transaction salt; defaults to a random 32-byte salt. * @param {Uint8Array|null} justification Optional mint justification bytes. - * @param {Uint8Array|null} data Optional data payload. * @returns {Promise} New mint transaction. */ public static async create( networkId: NetworkId, recipient: IPredicate, - tokenType: TokenType, - salt: TokenSalt | null = null, - justification: Uint8Array | null = null, data: Uint8Array | null = null, + tokenType: TokenType = TokenType.generate(), + salt: TokenSalt = TokenSalt.generate(), + justification: Uint8Array | null = null, ): Promise { - salt = salt ?? TokenSalt.generate(); justification = justification ? new Uint8Array(justification) : null; data = data ? new Uint8Array(data) : null; @@ -128,10 +127,10 @@ export class MintTransaction implements ITransaction { return MintTransaction.create( NetworkId.fromId(CborDeserializer.decodeUnsignedInteger(data[1])), EncodedPredicate.fromCBOR(data[2]), + CborDeserializer.decodeNullable(data[6], CborDeserializer.decodeByteString), TokenType.fromCBOR(data[4]), TokenSalt.fromCBOR(data[3]), CborDeserializer.decodeNullable(data[5], CborDeserializer.decodeByteString), - CborDeserializer.decodeNullable(data[6], CborDeserializer.decodeByteString), ); } diff --git a/tests/examples/mint/ExampleTest.ts b/tests/examples/mint/ExampleTest.ts index b4746f6..821691c 100644 --- a/tests/examples/mint/ExampleTest.ts +++ b/tests/examples/mint/ExampleTest.ts @@ -32,10 +32,8 @@ it('Token minting', async () => { const mintTransaction = await MintTransaction.create( NetworkId.LOCAL, ownerPredicate, - TokenType.generate(), - null, - null, CborSerializer.encodeTextString('My custom data'), + TokenType.generate(), ); const certificationData = await CertificationData.fromMintTransaction(mintTransaction); diff --git a/tests/examples/split/ExampleTest.ts b/tests/examples/split/ExampleTest.ts index a6ce63e..3fb811b 100644 --- a/tests/examples/split/ExampleTest.ts +++ b/tests/examples/split/ExampleTest.ts @@ -53,10 +53,8 @@ it('Token splitting', async () => { const mintTransaction = await MintTransaction.create( networkId, ownerPredicate, - TokenType.generate(), - null, - null, await paymentData.encode(), + TokenType.generate(), ); let response = await client.submitCertificationRequest(await CertificationData.fromMintTransaction(mintTransaction)); @@ -120,10 +118,10 @@ it('Token splitting', async () => { const mintTransaction = await MintTransaction.create( splitToken.networkId, splitToken.recipient, + await splitPaymentData.encode(), splitToken.tokenType, splitToken.salt, SplitMintJustification.create(burntToken, splitToken.proofs).toCBOR(), - await splitPaymentData.encode(), ); const certificationData = await CertificationData.fromMintTransaction(mintTransaction); diff --git a/tests/examples/transfer/ExampleTest.ts b/tests/examples/transfer/ExampleTest.ts index c54f2f3..9cd58a0 100644 --- a/tests/examples/transfer/ExampleTest.ts +++ b/tests/examples/transfer/ExampleTest.ts @@ -32,10 +32,8 @@ async function receiveToken(client: StateTransitionClient, trustBase: RootTrustB const mintTransaction = await MintTransaction.create( NetworkId.LOCAL, ownerPredicate, - TokenType.generate(), - null, - null, CborSerializer.encodeTextString('My custom data'), + TokenType.generate(), ); const certificationData = await CertificationData.fromMintTransaction(mintTransaction); diff --git a/tests/functional/payment/SplitBuilderTest.ts b/tests/functional/payment/SplitBuilderTest.ts index 3559946..4d5ec59 100644 --- a/tests/functional/payment/SplitBuilderTest.ts +++ b/tests/functional/payment/SplitBuilderTest.ts @@ -19,7 +19,6 @@ import { PredicateVerifierService } from '../../../src/predicate/verification/Pr import { StateTransitionClient } from '../../../src/StateTransitionClient.js'; import { MintTransaction } from '../../../src/transaction/MintTransaction.js'; import { Token } from '../../../src/transaction/Token.js'; -import { TokenType } from '../../../src/transaction/TokenType.js'; import { MintJustificationVerifierService } from '../../../src/transaction/verification/MintJustificationVerifierService.js'; import { waitInclusionProof } from '../../../src/util/InclusionProofUtils.js'; import { VerificationStatus } from '../../../src/verification/VerificationStatus.js'; @@ -46,14 +45,7 @@ describe('SplitBuilder Functional Test', () => { const paymentData = new TestPaymentData(PaymentAssetCollection.create(...assets)); const networkId = NetworkId.LOCAL; - const mintTransaction = await MintTransaction.create( - networkId, - predicate, - TokenType.generate(), - null, - null, - await paymentData.encode(), - ); + const mintTransaction = await MintTransaction.create(networkId, predicate, await paymentData.encode()); let certificationData = await CertificationData.fromMintTransaction(mintTransaction); let response = await client.submitCertificationRequest(certificationData); @@ -122,10 +114,10 @@ describe('SplitBuilder Functional Test', () => { const mintTransaction = await MintTransaction.create( splitToken.networkId, splitToken.recipient, + await new TestPaymentData(splitToken.assets).encode(), splitToken.tokenType, splitToken.salt, SplitMintJustification.create(token, splitToken.proofs).toCBOR(), - await new TestPaymentData(splitToken.assets).encode(), ); const certificationData = await CertificationData.fromMintTransaction(mintTransaction); diff --git a/tests/unit/api/CertificationDataTest.ts b/tests/unit/api/CertificationDataTest.ts index 4f1a8e4..24733d3 100644 --- a/tests/unit/api/CertificationDataTest.ts +++ b/tests/unit/api/CertificationDataTest.ts @@ -15,6 +15,7 @@ describe('CertificationData', () => { SignaturePredicate.create( HexConverter.decode('02ce9f22e51333c97a8fb1f807a229ece3a8765a16af5fc1a13e30834be3280026'), ), + null, new TokenType(new Uint8Array(32)), TokenSalt.fromBytes(new Uint8Array(32)), ), diff --git a/tests/unit/api/InclusionProofTest.ts b/tests/unit/api/InclusionProofTest.ts index cf218d6..386925d 100644 --- a/tests/unit/api/InclusionProofTest.ts +++ b/tests/unit/api/InclusionProofTest.ts @@ -16,8 +16,6 @@ import { PredicateVerifierService } from '../../../src/predicate/verification/Pr import { CborSerializer } from '../../../src/serialization/cbor/CborSerializer.js'; import { SparseMerkleTree } from '../../../src/smt/radix/SparseMerkleTree.js'; import { MintTransaction } from '../../../src/transaction/MintTransaction.js'; -import { TokenSalt } from '../../../src/transaction/TokenSalt.js'; -import { TokenType } from '../../../src/transaction/TokenType.js'; import { InclusionProofVerificationRule, InclusionProofVerificationStatus, @@ -39,11 +37,7 @@ describe('InclusionProof', () => { let trustBase: RootTrustBase; beforeAll(async () => { - transaction = await MintTransaction.create( - NetworkId.LOCAL, - SignaturePredicate.fromSigningService(signingService), - TokenType.generate(), - ); + transaction = await MintTransaction.create(NetworkId.LOCAL, SignaturePredicate.fromSigningService(signingService)); const smt = new SparseMerkleTree(new DataHasherFactory(HashAlgorithm.SHA256, NodeDataHasher)); const stateId = await StateId.fromTransaction(transaction); certificationData = await CertificationData.fromMintTransaction(transaction); @@ -90,10 +84,8 @@ describe('InclusionProof', () => { await MintTransaction.create( transaction.networkId, transaction.lockScript, - transaction.tokenType, - TokenSalt.generate(), - null, transaction.data, + transaction.tokenType, ), ).then((result) => result.status), ).resolves.toEqual(InclusionProofVerificationStatus.INCLUSION_CERTIFICATE_MISSING); diff --git a/tests/unit/api/StateIdTest.ts b/tests/unit/api/StateIdTest.ts index 6fa87c0..b367bc4 100644 --- a/tests/unit/api/StateIdTest.ts +++ b/tests/unit/api/StateIdTest.ts @@ -14,6 +14,7 @@ describe('StateId', () => { SignaturePredicate.create( HexConverter.decode('02ce9f22e51333c97a8fb1f807a229ece3a8765a16af5fc1a13e30834be3280026'), ), + null, new TokenType(new Uint8Array(32)), TokenSalt.fromBytes(new Uint8Array(32)), ), diff --git a/tests/utils/TokenUtils.ts b/tests/utils/TokenUtils.ts index addfc15..ec933d1 100644 --- a/tests/utils/TokenUtils.ts +++ b/tests/utils/TokenUtils.ts @@ -24,19 +24,19 @@ export async function mintToken( predicateVerifier: PredicateVerifierService, mintJustificationVerifier: MintJustificationVerifierService, recipient: IPredicate, + data: Uint8Array | null = null, networkId: NetworkId = NetworkId.LOCAL, tokenType: TokenType = TokenType.generate(), salt: TokenSalt = TokenSalt.generate(), justification: ICborSerializable | null = null, - data: Uint8Array | null = null, ): Promise { const transaction = await MintTransaction.create( networkId, recipient, + data, tokenType, salt, justification?.toCBOR(), - data, ); const certificationData = await CertificationData.fromMintTransaction(transaction); From 2a5fdef59d079a319e06a437b0b9e4bac6addfa8 Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Thu, 28 May 2026 14:49:37 +0300 Subject: [PATCH 10/13] #115 Allow custom Network ID --- src/api/NetworkId.ts | 32 ++++++++++++------- .../UnicityCertificateVerification.ts | 2 +- src/payment/SplitMintJustificationVerifier.ts | 2 +- ...ertifiedMintTransactionVerificationRule.ts | 2 +- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/api/NetworkId.ts b/src/api/NetworkId.ts index cd5bae9..a3a2969 100644 --- a/src/api/NetworkId.ts +++ b/src/api/NetworkId.ts @@ -1,9 +1,6 @@ /** * Unicity network identifier (α). Used to scope token ids and other * network-bound values so they cannot be replayed across networks. - * - * `0` is reserved as an uninitialized sentinel and is rejected by - * {@link NetworkId.fromId}. */ export class NetworkId { public static readonly LOCAL = new NetworkId(3, 'LOCAL'); @@ -12,22 +9,25 @@ export class NetworkId { private constructor( public readonly id: number, - public readonly name: string, + private readonly name?: string, ) {} /** - * Look up a NetworkId by its numeric identifier. + * Resolve a NetworkId from its numeric identifier. Returns the registered + * singleton for known ids; constructs a new (unnamed) instance for any + * other value in the 16-bit unsigned range. * * @param {number|bigint} id Numeric network identifier. - * @returns {NetworkId} Matching network identifier. - * @throws {Error} If `id` is `0`, negative, or not registered. + * @returns {NetworkId} NetworkId for the given identifier. + * @throws {Error} If `id` is outside the 16-bit unsigned range. */ public static fromId(id: number | bigint): NetworkId { const value = BigInt(id); if (value < 0n || value > 0xffffn) { throw new Error(`Network identifier out of 16-bit unsigned range: ${id}.`); } - switch (Number(value)) { + const numeric = Number(value); + switch (numeric) { case NetworkId.MAINNET.id: return NetworkId.MAINNET; case NetworkId.TESTNET.id: @@ -35,14 +35,24 @@ export class NetworkId { case NetworkId.LOCAL.id: return NetworkId.LOCAL; default: - throw new Error(`Unknown network identifier: ${id}.`); + return new NetworkId(numeric); } } /** - * @returns {string} Human-readable name of the network. + * Equality check against another NetworkId. + * + * @param {NetworkId} other Other network identifier. + * @returns {boolean} True if both share the same numeric id. + */ + public equals(other: NetworkId): boolean { + return this.id === other.id; + } + + /** + * @returns {string} `NetworkId[]` for registered networks, `NetworkId[]` otherwise. */ public toString(): string { - return this.name; + return `NetworkId[${this.name ?? this.id}]`; } } diff --git a/src/api/bft/verification/UnicityCertificateVerification.ts b/src/api/bft/verification/UnicityCertificateVerification.ts index bf6b60d..950481e 100644 --- a/src/api/bft/verification/UnicityCertificateVerification.ts +++ b/src/api/bft/verification/UnicityCertificateVerification.ts @@ -51,7 +51,7 @@ export class UnicityCertificateVerification { ): Promise { const results: VerificationResult[] = []; - if (inclusionProof.unicityCertificate.unicitySeal.networkId !== trustBase.networkId) { + if (!inclusionProof.unicityCertificate.unicitySeal.networkId.equals(trustBase.networkId)) { results.push(new VerificationResult('UnicitySealNetworkMatchesTrustBaseRule', VerificationStatus.FAIL)); return UnicityCertificateVerificationResult.fail(results); } diff --git a/src/payment/SplitMintJustificationVerifier.ts b/src/payment/SplitMintJustificationVerifier.ts index a03f9c1..640682d 100644 --- a/src/payment/SplitMintJustificationVerifier.ts +++ b/src/payment/SplitMintJustificationVerifier.ts @@ -59,7 +59,7 @@ export class SplitMintJustificationVerifier implements IMintJustificationVerifie ); } - if (transaction.networkId.id !== justification.token.genesis.networkId.id) { + if (!transaction.networkId.equals(justification.token.genesis.networkId)) { return new VerificationResult( 'SplitMintJustificationVerifier', VerificationStatus.FAIL, diff --git a/src/transaction/verification/rule/CertifiedMintTransactionVerificationRule.ts b/src/transaction/verification/rule/CertifiedMintTransactionVerificationRule.ts index dde52a7..f2e3d70 100644 --- a/src/transaction/verification/rule/CertifiedMintTransactionVerificationRule.ts +++ b/src/transaction/verification/rule/CertifiedMintTransactionVerificationRule.ts @@ -30,7 +30,7 @@ export class CertifiedMintTransactionVerificationRule { ): Promise> { const results: VerificationResult[] = []; - if (genesis.networkId !== trustBase.networkId) { + if (!genesis.networkId.equals(trustBase.networkId)) { results.push(new VerificationResult('MintNetworkMatchesTrustBaseRule', VerificationStatus.FAIL)); return new VerificationResult( 'CertifiedMintTransactionVerificationRule', From 26389cae0a016f0bfe9f80fb8b41aec930ae1e39 Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Thu, 28 May 2026 17:56:55 +0300 Subject: [PATCH 11/13] #115 Do not allow 0 as network ID --- src/api/NetworkId.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/NetworkId.ts b/src/api/NetworkId.ts index a3a2969..7813b78 100644 --- a/src/api/NetworkId.ts +++ b/src/api/NetworkId.ts @@ -23,8 +23,8 @@ export class NetworkId { */ public static fromId(id: number | bigint): NetworkId { const value = BigInt(id); - if (value < 0n || value > 0xffffn) { - throw new Error(`Network identifier out of 16-bit unsigned range: ${id}.`); + if (value < 1n || value > 0xffffn) { + throw new Error(`Network identifier out of allowed 16-bit unsigned range: ${id}.`); } const numeric = Number(value); switch (numeric) { From 1dbc4a0ad465cb150fa5686459858bbf901c5e0b Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Thu, 4 Jun 2026 10:15:35 +0300 Subject: [PATCH 12/13] #115 Do not allow empty proofs in split mint justification --- src/payment/SplitMintJustification.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/payment/SplitMintJustification.ts b/src/payment/SplitMintJustification.ts index 2dcfc4d..f3683fe 100644 --- a/src/payment/SplitMintJustification.ts +++ b/src/payment/SplitMintJustification.ts @@ -55,7 +55,7 @@ export class SplitMintJustification { const data = CborDeserializer.decodeArray(tag.data, 2); - return new SplitMintJustification( + return SplitMintJustification.create( await Token.fromCBOR(data[0]), CborDeserializer.decodeArray(data[1]).map((proof) => SplitAssetProof.fromCBOR(proof)), ); From e6355789c339a8be065e0c7977795a7d0b85f9e9 Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Thu, 4 Jun 2026 10:43:56 +0300 Subject: [PATCH 13/13] #115 Fix typo in UnicitySealQuorumSignaturesVerificationRule --- .../rule/UnicitySealQuorumSignaturesVerificationRule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.ts b/src/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.ts index 97af527..6bc8037 100644 --- a/src/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.ts +++ b/src/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.ts @@ -74,6 +74,6 @@ export class UnicitySealQuorumSignaturesVerificationRule { ); } - return new VerificationResult(`SignatureVerificationRule[${nodeId}]}`, VerificationStatus.OK); + return new VerificationResult(`SignatureVerificationRule[${nodeId}]`, VerificationStatus.OK); } }