diff --git a/Changelog.md b/Changelog.md index 53797c7..123d687 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,15 @@ ## [Unreleased] +### Added +- `Percentage` branded type: compile-time guarantee that a number is validated to [0, 100] (`src/types/escrow.ts`) +- `asPercentage()` runtime guard: validates and casts a number to `Percentage`, throws `RangeError` on NaN, Infinity, or out-of-range values (`src/types/escrow.ts`) +- `Distribution` type: `recipient: string`, `percentage: Percentage` (`src/types/escrow.ts`) +- `ReleaseParams` type: `escrowAccountId: string`, `distribution: Distribution[]` (`src/types/escrow.ts`) +- `ReleasedPayment` type: `recipient: string`, `amount: string` (`src/types/escrow.ts`) +- `ReleaseResult` type: `successful`, `txHash`, `ledger`, `payments: ReleasedPayment[]` (`src/types/escrow.ts`) +- Unit tests for all escrow release types in `tests/unit/types/escrow.test.ts` + ## [0.1.0] - 2026-03-23 ### Added diff --git a/package-lock.json b/package-lock.json index 8dea29a..0bf8009 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "jest": "^29.0.0", "msw": "^2.0.0", "prettier": "^3.0.0", + "rimraf": "^6.1.3", "ts-jest": "^29.0.0", "typescript": "^5.0.0" } @@ -3149,6 +3150,23 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/flatted": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", @@ -4716,6 +4734,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4938,6 +4966,13 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5007,6 +5042,33 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -5022,9 +5084,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -5366,17 +5428,38 @@ } }, "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^7.1.3" + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" }, "bin": { - "rimraf": "bin.js" + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -5797,9 +5880,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index da407d1..02113e5 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "scripts": { "build": "tsc", "dev": "tsc --watch", - "prebuild": "rm -rf dist", + "prebuild": "rimraf dist", "test": "jest --testPathPattern=tests/unit --passWithNoTests", "test:cov": "jest --testPathPattern=tests/unit --coverage --passWithNoTests", "test:watch": "jest --testPathPattern=tests/unit --watch", @@ -39,6 +39,7 @@ "jest": "^29.0.0", "msw": "^2.0.0", "prettier": "^3.0.0", + "rimraf": "^6.1.3", "ts-jest": "^29.0.0", "typescript": "^5.0.0" }, diff --git a/src/escrow/index.ts b/src/escrow/index.ts index 5972c06..3dbf7c1 100644 --- a/src/escrow/index.ts +++ b/src/escrow/index.ts @@ -1 +1,8 @@ -// escrow module — to be implemented +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function createEscrowAccount(..._args: unknown[]): unknown { return undefined; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function lockCustodyFunds(..._args: unknown[]): unknown { return undefined; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function anchorTrustHash(..._args: unknown[]): unknown { return undefined; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function verifyEventHash(..._args: unknown[]): unknown { return undefined; } diff --git a/src/index.ts b/src/index.ts index 8250371..bd6e43a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,33 @@ export const SDK_VERSION = '0.1.0'; -// Exports will be added here as each module is implemented + +// 1. Main class +export { StellarSDK } from './sdk'; +export { StellarSDK as default } from './sdk'; + +// 2. Error classes +export { + SdkError, + ValidationError, + AccountNotFoundError, + EscrowNotFoundError, + InsufficientBalanceError, + HorizonSubmitError, + TransactionTimeoutError, + MonitorTimeoutError, + FriendbotError, + ConditionMismatchError, +} from './utils/errors'; + +// 3. Escrow types (canonical source for Signer + Thresholds) +export type { CreateEscrowParams, Signer, Thresholds, EscrowAccount } from './types/escrow'; +export { EscrowStatus } from './types/escrow'; + +// 4. Network types (Signer + Thresholds excluded to avoid conflict) +export type { SDKConfig, KeypairResult, AccountInfo, BalanceInfo } from './types/network'; + +// 5. Transaction types +export type { SubmitResult, TransactionStatus } from './types/transaction'; + +// 6. Standalone functions +export { createEscrowAccount, lockCustodyFunds, anchorTrustHash, verifyEventHash } from './escrow'; +export { buildMultisigTransaction } from './transactions'; diff --git a/src/sdk.ts b/src/sdk.ts new file mode 100644 index 0000000..0e9dd06 --- /dev/null +++ b/src/sdk.ts @@ -0,0 +1 @@ +export class StellarSDK {} diff --git a/src/transactions/index.ts b/src/transactions/index.ts index f9261cf..04d4874 100644 --- a/src/transactions/index.ts +++ b/src/transactions/index.ts @@ -1 +1,2 @@ -// transactions module — to be implemented +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function buildMultisigTransaction(..._args: unknown[]): unknown { return undefined; } diff --git a/src/types/escrow.ts b/src/types/escrow.ts index fc46830..aa99437 100644 --- a/src/types/escrow.ts +++ b/src/types/escrow.ts @@ -1,3 +1,7 @@ +import { Signer, Thresholds } from './network'; + +export { Signer, Thresholds }; + export interface CreateEscrowParams { adopterPublicKey: string; ownerPublicKey: string; @@ -7,17 +11,6 @@ export interface CreateEscrowParams { metadata?: { adoptionId: string; petId: string }; } -export interface Signer { - publicKey: string; - weight: number; -} - -export interface Thresholds { - low: number; - medium: number; - high: number; -} - export interface EscrowAccount { accountId: string; transactionHash: string; @@ -34,3 +27,56 @@ export enum EscrowStatus { SETTLED = 'SETTLED', NOT_FOUND = 'NOT_FOUND', } + +// --------------------------------------------------------------------------- +// Branded type: Percentage +// Ensures Distribution.percentage is constrained to 0-100 at the type level. +// Use `asPercentage()` to create a validated value at runtime. +// --------------------------------------------------------------------------- + +/** A number branded to signal it has been validated as 0 ≤ n ≤ 100. */ +export type Percentage = number & { readonly __brand: 'Percentage' }; + +/** + * Validates and casts a plain number to a `Percentage` branded type. + * Rejects NaN, Infinity, -Infinity, and any value outside [0, 100]. + * @throws {RangeError} if value is not a finite number in [0, 100]. + */ +export function asPercentage(value: number): Percentage { + if (!Number.isFinite(value) || value < 0 || value > 100) { + throw new RangeError( + `Percentage must be between 0 and 100, got ${value}`, + ); + } + return value as Percentage; +} + +// --------------------------------------------------------------------------- +// Escrow release / settlement types (Issue #34) +// --------------------------------------------------------------------------- + +/** A single recipient and their share of the escrow release. */ +export interface Distribution { + recipient: string; + percentage: Percentage; +} + +/** Parameters required to trigger an escrow release settlement. */ +export interface ReleaseParams { + escrowAccountId: string; + distribution: Distribution[]; +} + +/** Recorded payment made to a recipient during settlement. */ +export interface ReleasedPayment { + recipient: string; + amount: string; +} + +/** Result returned after an escrow release transaction is submitted. */ +export interface ReleaseResult { + successful: boolean; + txHash: string; + ledger: number; + payments: ReleasedPayment[]; +} diff --git a/src/types/network.ts b/src/types/network.ts index 35b4e28..b7e8703 100644 --- a/src/types/network.ts +++ b/src/types/network.ts @@ -36,4 +36,30 @@ export interface BalanceInfo { accountId: string; balance: string; lastModifiedLedger: number; -} \ No newline at end of file +} + +export interface AnchorParams { + hash: string; + eventType: string; + metadata?: Record; +} + +export interface AnchorResult { + txHash: string; + ledger: number; + verified: boolean; + timestamp: Date; +} + +export interface VerifyParams { + expectedHash: string; + transactionHash: string; +} + +export interface VerifyResult { + isValid: boolean; + timestamp?: Date; + ledger?: number; + confirmations?: number; + reason?: string; +} diff --git a/tests/unit/sdk-exports.test.ts b/tests/unit/sdk-exports.test.ts new file mode 100644 index 0000000..16c9602 --- /dev/null +++ b/tests/unit/sdk-exports.test.ts @@ -0,0 +1,111 @@ +import defaultExport, { + StellarSDK, + SdkError, + ValidationError, + AccountNotFoundError, + EscrowNotFoundError, + InsufficientBalanceError, + HorizonSubmitError, + TransactionTimeoutError, + MonitorTimeoutError, + FriendbotError, + ConditionMismatchError, + EscrowStatus, +} from '../../src/index'; + +// Requirements 1.3 +describe('StellarSDK named and default export identity', () => { + it('default export and named StellarSDK export are the same reference', () => { + expect(defaultExport).toBe(StellarSDK); + }); +}); + +// Requirements 2.1–2.10 +describe('Error class exports are instantiable from the entry point', () => { + it('SdkError is exported and instantiable', () => { + const err = new SdkError('msg', 'CODE'); + expect(err).toBeInstanceOf(SdkError); + expect(err).toBeInstanceOf(Error); + }); + + it('ValidationError is exported and instantiable', () => { + const err = new ValidationError('field', 'msg'); + expect(err).toBeInstanceOf(ValidationError); + expect(err).toBeInstanceOf(SdkError); + }); + + it('AccountNotFoundError is exported and instantiable', () => { + const err = new AccountNotFoundError('GTEST'); + expect(err).toBeInstanceOf(AccountNotFoundError); + expect(err).toBeInstanceOf(SdkError); + }); + + it('EscrowNotFoundError is exported and instantiable', () => { + const err = new EscrowNotFoundError('GTEST'); + expect(err).toBeInstanceOf(EscrowNotFoundError); + expect(err).toBeInstanceOf(SdkError); + }); + + it('InsufficientBalanceError is exported and instantiable', () => { + const err = new InsufficientBalanceError('10', '5'); + expect(err).toBeInstanceOf(InsufficientBalanceError); + expect(err).toBeInstanceOf(SdkError); + }); + + it('HorizonSubmitError is exported and instantiable', () => { + const err = new HorizonSubmitError('tx_bad_seq'); + expect(err).toBeInstanceOf(HorizonSubmitError); + expect(err).toBeInstanceOf(SdkError); + }); + + it('TransactionTimeoutError is exported and instantiable', () => { + const err = new TransactionTimeoutError('hash123'); + expect(err).toBeInstanceOf(TransactionTimeoutError); + expect(err).toBeInstanceOf(SdkError); + }); + + it('MonitorTimeoutError is exported and instantiable', () => { + const err = new MonitorTimeoutError('hash123', 3); + expect(err).toBeInstanceOf(MonitorTimeoutError); + expect(err).toBeInstanceOf(SdkError); + }); + + it('FriendbotError is exported and instantiable', () => { + const err = new FriendbotError('GTEST', 400); + expect(err).toBeInstanceOf(FriendbotError); + expect(err).toBeInstanceOf(SdkError); + }); + + it('ConditionMismatchError is exported and instantiable', () => { + const err = new ConditionMismatchError('stored', 'computed'); + expect(err).toBeInstanceOf(ConditionMismatchError); + expect(err).toBeInstanceOf(SdkError); + }); +}); + +// Requirements 4.2 +describe('EscrowStatus enum values resolve to their expected string literals', () => { + it('EscrowStatus.CREATED === "CREATED"', () => { + expect(EscrowStatus.CREATED).toBe('CREATED'); + }); + + it('EscrowStatus.FUNDED === "FUNDED"', () => { + expect(EscrowStatus.FUNDED).toBe('FUNDED'); + }); + + it('EscrowStatus.DISPUTED === "DISPUTED"', () => { + expect(EscrowStatus.DISPUTED).toBe('DISPUTED'); + }); + + it('EscrowStatus.SETTLING === "SETTLING"', () => { + expect(EscrowStatus.SETTLING).toBe('SETTLING'); + }); + + it('EscrowStatus.SETTLED === "SETTLED"', () => { + expect(EscrowStatus.SETTLED).toBe('SETTLED'); + }); + + it('EscrowStatus.NOT_FOUND === "NOT_FOUND"', () => { + expect(EscrowStatus.NOT_FOUND).toBe('NOT_FOUND'); + }); +}); diff --git a/tests/unit/types/escrow.test.ts b/tests/unit/types/escrow.test.ts new file mode 100644 index 0000000..c99ccd3 --- /dev/null +++ b/tests/unit/types/escrow.test.ts @@ -0,0 +1,152 @@ +import { + asPercentage, + Distribution, + ReleaseParams, + ReleaseResult, +} from '../../../src/types/escrow'; + + +// --------------------------------------------------------------------------- +// asPercentage — branded runtime validator +// --------------------------------------------------------------------------- + +describe('asPercentage', () => { + describe('happy path', () => { + it('accepts 0', () => { + expect(asPercentage(0)).toBe(0); + }); + + it('accepts 100', () => { + expect(asPercentage(100)).toBe(100); + }); + + it('accepts a midpoint value (50)', () => { + expect(asPercentage(50)).toBe(50); + }); + + it('accepts fractional values within range (33.33)', () => { + expect(asPercentage(33.33)).toBeCloseTo(33.33); + }); + }); + + describe('validation errors', () => { + it('throws RangeError for value below 0', () => { + expect(() => asPercentage(-1)).toThrow(RangeError); + expect(() => asPercentage(-1)).toThrow( + 'Percentage must be between 0 and 100, got -1', + ); + }); + + it('throws RangeError for value above 100', () => { + expect(() => asPercentage(101)).toThrow(RangeError); + expect(() => asPercentage(101)).toThrow( + 'Percentage must be between 0 and 100, got 101', + ); + }); + + it('throws for NaN', () => { + expect(() => asPercentage(NaN)).toThrow(RangeError); + }); + + it('throws for Infinity', () => { + expect(() => asPercentage(Infinity)).toThrow(RangeError); + }); + + it('throws for -Infinity', () => { + expect(() => asPercentage(-Infinity)).toThrow(RangeError); + }); + }); + + describe('edge cases', () => { + it('accepts exactly 0 (lower boundary)', () => { + expect(() => asPercentage(0)).not.toThrow(); + }); + + it('accepts exactly 100 (upper boundary)', () => { + expect(() => asPercentage(100)).not.toThrow(); + }); + + it('rejects 100.0001 (just above upper boundary)', () => { + expect(() => asPercentage(100.0001)).toThrow(RangeError); + }); + + it('rejects -0.0001 (just below lower boundary)', () => { + expect(() => asPercentage(-0.0001)).toThrow(RangeError); + }); + }); +}); + +// --------------------------------------------------------------------------- +// Distribution — structural conformance +// --------------------------------------------------------------------------- + +describe('Distribution', () => { + it('conforms to the expected shape', () => { + const dist: Distribution = { + recipient: 'GABCDE...1234', + percentage: asPercentage(50), + }; + expect(dist.recipient).toBe('GABCDE...1234'); + expect(dist.percentage).toBe(50); + }); +}); + +// --------------------------------------------------------------------------- +// ReleaseParams — structural conformance +// --------------------------------------------------------------------------- + +describe('ReleaseParams', () => { + it('holds escrowAccountId and a distribution array', () => { + const params: ReleaseParams = { + escrowAccountId: 'GESCROW...5678', + distribution: [ + { recipient: 'GADOPTER...', percentage: asPercentage(70) }, + { recipient: 'GOWNER...', percentage: asPercentage(30) }, + ], + }; + expect(params.distribution).toHaveLength(2); + expect(params.distribution[0].percentage).toBe(70); + expect(params.distribution[1].percentage).toBe(30); + }); + + it('allows an empty distribution array', () => { + const params: ReleaseParams = { + escrowAccountId: 'GESCROW...5678', + distribution: [], + }; + expect(params.distribution).toHaveLength(0); + }); +}); + +// --------------------------------------------------------------------------- +// ReleaseResult — structural conformance +// --------------------------------------------------------------------------- + +describe('ReleaseResult', () => { + it('conforms to the expected shape on success', () => { + const result: ReleaseResult = { + successful: true, + txHash: 'abc123txhash', + ledger: 55000, + payments: [ + { recipient: 'GADOPTER...', amount: '70.0000000' }, + { recipient: 'GOWNER...', amount: '30.0000000' }, + ], + }; + expect(result.successful).toBe(true); + expect(result.txHash).toBe('abc123txhash'); + expect(result.ledger).toBe(55000); + expect(result.payments).toHaveLength(2); + }); + + it('conforms to the expected shape on failure', () => { + const result: ReleaseResult = { + successful: false, + txHash: '', + ledger: 0, + payments: [], + }; + expect(result.successful).toBe(false); + expect(result.payments).toHaveLength(0); + }); +}); diff --git a/tests/unit/types/network.test.ts b/tests/unit/types/network.test.ts new file mode 100644 index 0000000..00d96bf --- /dev/null +++ b/tests/unit/types/network.test.ts @@ -0,0 +1,91 @@ +import * as networkTypes from '../../../src/types/network'; +import type { + AnchorParams, + AnchorResult, + VerifyParams, + VerifyResult, +} from '../../../src/types/network'; + +// Feature: anchor-verify-types, Property 1: All four interfaces are exported from the module +describe('module exports', () => { + it('exports AnchorParams, AnchorResult, VerifyParams, and VerifyResult', () => { + // TypeScript interfaces are erased at runtime, so we verify via type-level usage below. + // This test confirms the module itself is importable and the names are in scope. + expect(networkTypes).toBeDefined(); + }); +}); + +// Feature: anchor-verify-types, Property 2: AnchorParams accepts any conforming object +describe('AnchorParams', () => { + it('accepts an object with hash and eventType', () => { + const params: AnchorParams = { hash: 'abc123', eventType: 'TRUST_HASH' }; + expect(params.hash).toBe('abc123'); + expect(params.eventType).toBe('TRUST_HASH'); + }); + + it('accepts an object with hash, eventType, and optional metadata', () => { + const params: AnchorParams = { + hash: 'abc123', + eventType: 'TRUST_HASH', + metadata: { source: 'test', count: 1 }, + }; + expect(params.metadata).toEqual({ source: 'test', count: 1 }); + }); +}); + +// Feature: anchor-verify-types, Property 3: AnchorResult accepts any conforming object +describe('AnchorResult', () => { + it('accepts an object with txHash, ledger, verified, and timestamp', () => { + const ts = new Date('2024-01-01T00:00:00Z'); + const result: AnchorResult = { + txHash: 'tx_abc', + ledger: 42, + verified: true, + timestamp: ts, + }; + expect(result.txHash).toBe('tx_abc'); + expect(result.ledger).toBe(42); + expect(result.verified).toBe(true); + expect(result.timestamp).toBe(ts); + }); +}); + +// Feature: anchor-verify-types, Property 4: VerifyParams accepts any conforming object +describe('VerifyParams', () => { + it('accepts an object with expectedHash and transactionHash', () => { + const params: VerifyParams = { + expectedHash: 'hash_expected', + transactionHash: 'tx_hash', + }; + expect(params.expectedHash).toBe('hash_expected'); + expect(params.transactionHash).toBe('tx_hash'); + }); +}); + +// Feature: anchor-verify-types, Property 5: VerifyResult accepts minimal and fully-populated objects +describe('VerifyResult', () => { + it('accepts a minimal object with only isValid', () => { + const result: VerifyResult = { isValid: true }; + expect(result.isValid).toBe(true); + expect(result.timestamp).toBeUndefined(); + expect(result.ledger).toBeUndefined(); + expect(result.confirmations).toBeUndefined(); + expect(result.reason).toBeUndefined(); + }); + + it('accepts a fully-populated object with all optional fields', () => { + const ts = new Date('2024-06-01T12:00:00Z'); + const result: VerifyResult = { + isValid: false, + timestamp: ts, + ledger: 100, + confirmations: 5, + reason: 'hash mismatch', + }; + expect(result.isValid).toBe(false); + expect(result.timestamp).toBe(ts); + expect(result.ledger).toBe(100); + expect(result.confirmations).toBe(5); + expect(result.reason).toBe('hash mismatch'); + }); +});