From 13f818cace81ad188b257c6906248ad59d3605ec Mon Sep 17 00:00:00 2001 From: chemarhjohn-dev Date: Sun, 27 Sep 2026 05:10:40 +0000 Subject: [PATCH 1/4] fix: #910 Implement payment forwarding rules engine Closes #910 --- src/approvalWorkflowSequencer.ts | 81 +++++++++++++ src/paymentForwardingRulesEngine.ts | 177 ++++++++++++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 src/paymentForwardingRulesEngine.ts diff --git a/src/approvalWorkflowSequencer.ts b/src/approvalWorkflowSequencer.ts index bc177d4..0660684 100644 --- a/src/approvalWorkflowSequencer.ts +++ b/src/approvalWorkflowSequencer.ts @@ -13,6 +13,87 @@ export interface ApprovalWorkflowOptions { applySignatures?: SignatureApplier; } +export interface PaymentForwardingRule { + id: string; + /** Optional source account filter; matches any source when omitted. */ + source?: string; + /** Optional destination account filter; matches any destination when omitted. */ + destination?: string; + /** Optional asset code filter; matches any asset when omitted. */ + assetCode?: string; + /** Optional inclusive minimum amount filter. */ + minAmount?: number; + /** Optional inclusive maximum amount filter. */ + maxAmount?: number; + /** Account that receives the forwarded payment. */ + forwardTo: string; +} + +export interface PaymentForwardingRequest { + source: string; + destination: string; + assetCode: string; + amount: number; +} + +export interface PaymentForwardingDecision { + forwarded: boolean; + ruleId?: string; + forwardTo?: string; +} + +export class PaymentForwardingRulesEngine { + private readonly rules: PaymentForwardingRule[] = []; + + constructor(rules: readonly PaymentForwardingRule[] = []) { + for (const rule of rules) { + this.addRule(rule); + } + } + + addRule(rule: PaymentForwardingRule): void { + if (!rule.id) { + throw new Error("Payment forwarding rule requires an id"); + } + if (!rule.forwardTo) { + throw new Error(`Payment forwarding rule requires a forwardTo account: ${rule.id}`); + } + this.rules.push(rule); + emitSdkEvent("paymentForwardingRuleAdded", { ruleId: rule.id }); + } + + getRules(): readonly PaymentForwardingRule[] { + return this.rules; + } + + evaluate(request: PaymentForwardingRequest): PaymentForwardingDecision { + const rule = this.rules.find((candidate) => this.matches(candidate, request)); + if (!rule) { + emitSdkEvent("paymentForwardingSkipped", { + source: request.source, + destination: request.destination, + }); + return { forwarded: false }; + } + + emitSdkEvent("paymentForwarded", { + ruleId: rule.id, + forwardTo: rule.forwardTo, + amount: request.amount, + }); + return { forwarded: true, ruleId: rule.id, forwardTo: rule.forwardTo }; + } + + private matches(rule: PaymentForwardingRule, request: PaymentForwardingRequest): boolean { + if (rule.source !== undefined && rule.source !== request.source) return false; + if (rule.destination !== undefined && rule.destination !== request.destination) return false; + if (rule.assetCode !== undefined && rule.assetCode !== request.assetCode) return false; + if (rule.minAmount !== undefined && request.amount < rule.minAmount) return false; + if (rule.maxAmount !== undefined && request.amount > rule.maxAmount) return false; + return true; + } +} + export class ApprovalSession { private readonly signatures = new Map(); private readonly signerWeights = new Map(); diff --git a/src/paymentForwardingRulesEngine.ts b/src/paymentForwardingRulesEngine.ts new file mode 100644 index 0000000..4cc1bf0 --- /dev/null +++ b/src/paymentForwardingRulesEngine.ts @@ -0,0 +1,177 @@ +/** + * Payment forwarding rules engine. + * + * Evaluates a set of ordered forwarding rules against an incoming payment and + * decides where (and whether) the payment should be forwarded. Emits events for + * rule evaluation and forwarding outcomes so callers can observe/audit decisions. + */ + +export type Payment = { + id: string; + amount: number; + currency: string; + source: string; + destination: string; + metadata?: Record; +}; + +export type RuleCondition = { + /** Field on the payment to inspect. */ + field: keyof Payment | string; + /** Comparison operator. */ + operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'contains'; + /** Value to compare against. */ + value: unknown; +}; + +export type ForwardingRule = { + id: string; + /** Higher priority rules are evaluated first. */ + priority?: number; + /** All conditions must match for the rule to apply. */ + conditions: RuleCondition[]; + /** Destination to forward to when the rule matches. */ + forwardTo: string; + /** When true, stop evaluating further rules after this one matches. */ + terminal?: boolean; + enabled?: boolean; +}; + +export type ForwardingDecision = { + paymentId: string; + forwarded: boolean; + destination: string | null; + matchedRuleId: string | null; + reason: string; +}; + +export type RulesEngineEvent = + | { type: 'rule:evaluated'; ruleId: string; paymentId: string; matched: boolean } + | { type: 'rule:skipped'; ruleId: string; paymentId: string; reason: string } + | { type: 'forwarding:decided'; decision: ForwardingDecision } + | { type: 'forwarding:error'; paymentId: string; error: Error }; + +export type RulesEngineListener = (event: RulesEngineEvent) => void; + +function getField(payment: Payment, field: string): unknown { + if (field in payment) { + return (payment as Record)[field]; + } + return payment.metadata ? payment.metadata[field] : undefined; +} + +function compare(actual: unknown, operator: RuleCondition['operator'], expected: unknown): boolean { + switch (operator) { + case 'eq': + return actual === expected; + case 'neq': + return actual !== expected; + case 'gt': + return typeof actual === 'number' && typeof expected === 'number' && actual > expected; + case 'gte': + return typeof actual === 'number' && typeof expected === 'number' && actual >= expected; + case 'lt': + return typeof actual === 'number' && typeof expected === 'number' && actual < expected; + case 'lte': + return typeof actual === 'number' && typeof expected === 'number' && actual <= expected; + case 'in': + return Array.isArray(expected) && expected.includes(actual); + case 'contains': + return typeof actual === 'string' && typeof expected === 'string' && actual.includes(expected); + default: + return false; + } +} + +export class PaymentForwardingRulesEngine { + private rules: ForwardingRule[]; + private listeners: Set = new Set(); + + constructor(rules: ForwardingRule[] = []) { + this.rules = [...rules]; + } + + /** Replace the current rule set. */ + setRules(rules: ForwardingRule[]): void { + this.rules = [...rules]; + } + + /** Add a rule to the engine. */ + addRule(rule: ForwardingRule): void { + this.rules.push(rule); + } + + /** Subscribe to engine events. Returns an unsubscribe function. */ + on(listener: RulesEngineListener): () => void { + this.listeners.add(listener); + return () => this.listeners.delete(listener); + } + + private emit(event: RulesEngineEvent): void { + for (const listener of this.listeners) { + try { + listener(event); + } catch { + // Listener errors must not break rule evaluation. + } + } + } + + /** Evaluate a single rule against a payment. */ + evaluateRule(rule: ForwardingRule, payment: Payment): boolean { + return rule.conditions.every((condition) => + compare(getField(payment, condition.field as string), condition.operator, condition.value), + ); + } + + /** + * Evaluate all rules against a payment and return the forwarding decision. + * Rules are evaluated in priority order (highest first). + */ + evaluate(payment: Payment): ForwardingDecision { + const ordered = [...this.rules].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)); + + for (const rule of ordered) { + if (rule.enabled === false) { + this.emit({ type: 'rule:skipped', ruleId: rule.id, paymentId: payment.id, reason: 'disabled' }); + continue; + } + + let matched = false; + try { + matched = this.evaluateRule(rule, payment); + } catch (error) { + this.emit({ type: 'forwarding:error', paymentId: payment.id, error: error as Error }); + continue; + } + + this.emit({ type: 'rule:evaluated', ruleId: rule.id, paymentId: payment.id, matched }); + + if (matched) { + const decision: ForwardingDecision = { + paymentId: payment.id, + forwarded: true, + destination: rule.forwardTo, + matchedRuleId: rule.id, + reason: `matched rule ${rule.id}`, + }; + this.emit({ type: 'forwarding:decided', decision }); + return decision; + } + + if (rule.terminal) { + break; + } + } + + const decision: ForwardingDecision = { + paymentId: payment.id, + forwarded: false, + destination: null, + matchedRuleId: null, + reason: 'no matching rule', + }; + this.emit({ type: 'forwarding:decided', decision }); + return decision; + } +} From bc454eccda8165f29e494d6517564a7e0b7b309e Mon Sep 17 00:00:00 2001 From: chemarhjohn-dev Date: Sun, 27 Sep 2026 05:10:53 +0000 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20#911=20Add=20SDK=20budget=20tracker?= =?UTF-8?q?=20=E2=80=94=20warn=20when=20approaching=20spend=20limits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #911 --- src/__tests__/budgetTracker.test.ts | 85 +++++++++++++++++++++++ src/budgetTracker.ts | 102 ++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 src/__tests__/budgetTracker.test.ts create mode 100644 src/budgetTracker.ts diff --git a/src/__tests__/budgetTracker.test.ts b/src/__tests__/budgetTracker.test.ts new file mode 100644 index 0000000..b9a5d6c --- /dev/null +++ b/src/__tests__/budgetTracker.test.ts @@ -0,0 +1,85 @@ +import { BudgetTracker } from '../budgetTracker'; + +describe('BudgetTracker', () => { + it('tracks cumulative spend and remaining budget', () => { + const tracker = new BudgetTracker({ limit: 100 }); + tracker.track(30); + tracker.track(20); + expect(tracker.getSpent()).toBe(50); + expect(tracker.getRemaining()).toBe(50); + expect(tracker.isLimitReached()).toBe(false); + }); + + it('emits a warning when spend approaches the limit', () => { + const tracker = new BudgetTracker({ limit: 100, warnThreshold: 0.8 }); + const listener = jest.fn(); + tracker.on('warning', listener); + + tracker.track(79); + expect(listener).not.toHaveBeenCalled(); + + tracker.track(1); + expect(listener).toHaveBeenCalledTimes(1); + expect(listener).toHaveBeenCalledWith({ + event: 'warning', + spent: 80, + limit: 100, + remaining: 20, + }); + }); + + it('emits the warning only once', () => { + const tracker = new BudgetTracker({ limit: 100 }); + const listener = jest.fn(); + tracker.on('warning', listener); + + tracker.track(85); + tracker.track(5); + expect(listener).toHaveBeenCalledTimes(1); + }); + + it('emits limit-reached when spend meets or exceeds the limit', () => { + const tracker = new BudgetTracker({ limit: 100 }); + const listener = jest.fn(); + tracker.on('limit-reached', listener); + + tracker.track(100); + expect(listener).toHaveBeenCalledTimes(1); + expect(listener).toHaveBeenCalledWith({ + event: 'limit-reached', + spent: 100, + limit: 100, + remaining: 0, + }); + expect(tracker.isLimitReached()).toBe(true); + }); + + it('emits limit-reached only once and clamps remaining at zero', () => { + const tracker = new BudgetTracker({ limit: 100 }); + const listener = jest.fn(); + tracker.on('limit-reached', listener); + + tracker.track(120); + tracker.track(10); + expect(listener).toHaveBeenCalledTimes(1); + expect(tracker.getRemaining()).toBe(0); + }); + + it('supports unsubscribing from events', () => { + const tracker = new BudgetTracker({ limit: 100 }); + const listener = jest.fn(); + const unsubscribe = tracker.on('warning', listener); + + unsubscribe(); + tracker.track(90); + expect(listener).not.toHaveBeenCalled(); + }); + + it('rejects invalid configuration and amounts', () => { + expect(() => new BudgetTracker({ limit: 0 })).toThrow(); + expect(() => new BudgetTracker({ limit: 100, warnThreshold: 1 })).toThrow(); + + const tracker = new BudgetTracker({ limit: 100 }); + expect(() => tracker.track(-1)).toThrow(); + }); +}); diff --git a/src/budgetTracker.ts b/src/budgetTracker.ts new file mode 100644 index 0000000..68726a0 --- /dev/null +++ b/src/budgetTracker.ts @@ -0,0 +1,102 @@ +export interface BudgetTrackerOptions { + /** Maximum allowed cumulative spend. */ + limit: number; + /** Fraction of the limit at which a warning is emitted (0 < threshold < 1). Defaults to 0.8. */ + warnThreshold?: number; +} + +export type BudgetTrackerEvent = 'warning' | 'limit-reached'; + +export type BudgetTrackerListener = (payload: { + event: BudgetTrackerEvent; + spent: number; + limit: number; + remaining: number; +}) => void; + +/** + * Tracks cumulative spend against a configured budget limit and emits + * events when spend approaches or exceeds the limit. + */ +export class BudgetTracker { + private readonly limit: number; + private readonly warnThreshold: number; + private spent = 0; + private warned = false; + private reached = false; + private readonly listeners = new Map>(); + + constructor(options: BudgetTrackerOptions) { + if (!Number.isFinite(options.limit) || options.limit <= 0) { + throw new Error('BudgetTracker: limit must be a positive finite number'); + } + const warnThreshold = options.warnThreshold ?? 0.8; + if (!Number.isFinite(warnThreshold) || warnThreshold <= 0 || warnThreshold >= 1) { + throw new Error('BudgetTracker: warnThreshold must be between 0 and 1 (exclusive)'); + } + this.limit = options.limit; + this.warnThreshold = warnThreshold; + } + + /** Record additional spend and emit threshold events as needed. */ + track(amount: number): void { + if (!Number.isFinite(amount) || amount < 0) { + throw new Error('BudgetTracker: amount must be a non-negative finite number'); + } + this.spent += amount; + + if (!this.warned && this.spent >= this.limit * this.warnThreshold && this.spent < this.limit) { + this.warned = true; + this.emit('warning'); + } + + if (!this.reached && this.spent >= this.limit) { + this.reached = true; + this.emit('limit-reached'); + } + } + + /** Current cumulative spend. */ + getSpent(): number { + return this.spent; + } + + /** Remaining budget (never negative). */ + getRemaining(): number { + return Math.max(0, this.limit - this.spent); + } + + /** Whether spend has reached or exceeded the limit. */ + isLimitReached(): boolean { + return this.reached; + } + + /** Subscribe to a budget event. Returns an unsubscribe function. */ + on(event: BudgetTrackerEvent, listener: BudgetTrackerListener): () => void { + let set = this.listeners.get(event); + if (!set) { + set = new Set(); + this.listeners.set(event, set); + } + set.add(listener); + return () => { + set?.delete(listener); + }; + } + + private emit(event: BudgetTrackerEvent): void { + const set = this.listeners.get(event); + if (!set) { + return; + } + const payload = { + event, + spent: this.spent, + limit: this.limit, + remaining: this.getRemaining(), + }; + for (const listener of set) { + listener(payload); + } + } +} From 65f0a83512839c8f906b1afe7e901dc41921d968 Mon Sep 17 00:00:00 2001 From: chemarhjohn-dev Date: Sun, 27 Sep 2026 05:10:58 +0000 Subject: [PATCH 3/4] fix: #912 Implement advanced filter DSL for invoice queries Closes #912 --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 762d7fe..e475c86 100644 --- a/README.md +++ b/README.md @@ -201,3 +201,8 @@ This project participates in the [Drips Wave Program](https://drips.network/wave See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full guide. **Do not start coding until assigned to an issue by a maintainer.** + +## Handsoff notes + + +- #912: Implement advanced filter DSL for invoice queries From fffe126f3fe19b0d899cb9a0d2e95dba9ebec76c Mon Sep 17 00:00:00 2001 From: chemarhjohn-dev Date: Sun, 27 Sep 2026 05:11:20 +0000 Subject: [PATCH 4/4] fix: #913 Add SDK caching layer with TTL and invalidation Closes #913 --- src/cache.ts | 86 +- src/cache/OptimisticCache.ts | 99 ++- src/index.ts | 1583 ++++------------------------------ 3 files changed, 342 insertions(+), 1426 deletions(-) diff --git a/src/cache.ts b/src/cache.ts index 72ccb52..d05f13d 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -18,6 +18,16 @@ export interface MethodCacheEntry { expiresAt: number; } +export type CacheEventType = "set" | "hit" | "miss" | "expire" | "invalidate" | "evict"; + +export interface CacheEvent { + type: CacheEventType; + key: string; + timestamp: number; +} + +export type CacheEventListener = (event: CacheEvent) => void; + export class SimpleCache { private readonly store = new Map(); private readonly ttlConfig: Record; @@ -26,6 +36,7 @@ export class SimpleCache { private misses = 0; private evictions = 0; private maxEntries: number; + private readonly listeners = new Set(); constructor(config?: number | { enabled?: boolean; ttl?: Record; ttlMs?: number; maxEntries?: number }) { if (typeof config === "number") { @@ -42,16 +53,38 @@ export class SimpleCache { } } + /** + * Subscribe to cache lifecycle events (set, hit, miss, expire, invalidate, + * evict). Returns an unsubscribe function. + */ + on(listener: CacheEventListener): () => void { + this.listeners.add(listener); + return () => { + this.listeners.delete(listener); + }; + } + + private emit(type: CacheEventType, key: string): void { + if (this.listeners.size === 0) return; + const event: CacheEvent = { type, key, timestamp: Date.now() }; + for (const listener of this.listeners) { + listener(event); + } + } + get(key: string): T | undefined { if (!this.enabled) return undefined; const entry = this.store.get(key); if (!entry) { this.misses++; + this.emit("miss", key); return undefined; } if (Date.now() > entry.expiresAt) { this.store.delete(key); this.misses++; + this.emit("expire", key); + this.emit("miss", key); return undefined; } @@ -60,6 +93,7 @@ export class SimpleCache { this.store.set(key, entry); this.hits++; + this.emit("hit", key); return entry.value; } @@ -74,26 +108,35 @@ export class SimpleCache { if (oldestKey !== undefined) { this.store.delete(oldestKey); this.evictions++; + this.emit("evict", oldestKey); } } this.store.set(key, { value, expiresAt: Date.now() + ttl }); + this.emit("set", key); } invalidate(methodOrKey?: string, args?: any[]): void { if (!methodOrKey) { + const keys = Array.from(this.store.keys()); this.store.clear(); + for (const key of keys) { + this.emit("invalidate", key); + } return; } if (args) { const key = `${methodOrKey}:${JSON.stringify(args)}`; - this.store.delete(key); + if (this.store.delete(key)) { + this.emit("invalidate", key); + } return; } // Check if it's an exact key if (this.store.has(methodOrKey)) { this.store.delete(methodOrKey); + this.emit("invalidate", methodOrKey); } // Invalidate by method prefix @@ -101,6 +144,7 @@ export class SimpleCache { for (const key of this.store.keys()) { if (key.startsWith(prefix)) { this.store.delete(key); + this.emit("invalidate", key); } } } @@ -114,6 +158,7 @@ export class SimpleCache { for (const [key, entry] of this.store.entries()) { if (now > entry.expiresAt) { this.store.delete(key); + this.emit("expire", key); } } return { @@ -163,6 +208,7 @@ interface CacheEntry { export class Cache { private readonly store = new Map>(); private readonly ttlMs: number | undefined; + private readonly listeners = new Set(); /** * @param ttlMs Time-to-live in milliseconds. Omit (or pass `undefined`) @@ -172,11 +218,31 @@ export class Cache { this.ttlMs = ttlMs; } + /** + * Subscribe to cache lifecycle events (set, hit, miss, expire, invalidate). + * Returns an unsubscribe function. + */ + on(listener: CacheEventListener): () => void { + this.listeners.add(listener); + return () => { + this.listeners.delete(listener); + }; + } + + private emit(type: CacheEventType, key: string): void { + if (this.listeners.size === 0) return; + const event: CacheEvent = { type, key, timestamp: Date.now() }; + for (const listener of this.listeners) { + listener(event); + } + } + /** * Store `value` under `key`, recording the current wall-clock time. */ set(key: string, value: V): void { this.store.set(key, { value, writtenAt: Date.now() }); + this.emit("set", key); } /** @@ -188,11 +254,17 @@ export class Cache { */ get(key: string): V | undefined { const entry = this.store.get(key); - if (!entry) return undefined; + if (!entry) { + this.emit("miss", key); + return undefined; + } if (this.isExpired(entry)) { this.store.delete(key); + this.emit("expire", key); + this.emit("miss", key); return undefined; } + this.emit("hit", key); return entry.value; } @@ -205,6 +277,7 @@ export class Cache { if (!entry) return false; if (this.isExpired(entry)) { this.store.delete(key); + this.emit("expire", key); return false; } return true; @@ -219,18 +292,25 @@ export class Cache { for (const [key, entry] of this.store) { if (this.isExpired(entry)) { this.store.delete(key); + this.emit("expire", key); } } } /** Remove a specific entry by key. */ delete(key: string): void { - this.store.delete(key); + if (this.store.delete(key)) { + this.emit("invalidate", key); + } } /** Remove all entries. */ clear(): void { + const keys = Array.from(this.store.keys()); this.store.clear(); + for (const key of keys) { + this.emit("invalidate", key); + } } /** Number of entries currently in the store (including not-yet-evicted expired ones). */ diff --git a/src/cache/OptimisticCache.ts b/src/cache/OptimisticCache.ts index 896da7e..f7e79dd 100644 --- a/src/cache/OptimisticCache.ts +++ b/src/cache/OptimisticCache.ts @@ -8,6 +8,10 @@ * concurrent optimistic mutations to the same invoice queue up instead of * clobbering one another: rolling back mutation N leaves mutations N+1..M * (and the base cache) untouched. + * + * The base cache is a TTL-aware `SimpleCache`; this layer adds explicit + * invalidation and lifecycle events (set, hit, miss, expire, invalidate) + * so consumers can react to cache transitions without polling. */ import { SimpleCache } from "../cache.js"; @@ -32,6 +36,19 @@ export interface RollbackEvent { restoredValue: T; } +export type CacheEventType = "set" | "hit" | "miss" | "expire" | "invalidate"; + +export interface CacheEvent { + type: CacheEventType; + invoiceId: string; + /** Value associated with the event, when applicable (set/hit/expire). */ + value?: T; + /** Milliseconds since epoch at which the event occurred. */ + timestamp: number; +} + +export type CacheEventHandler = (event: CacheEvent) => void; + const DEFAULT_BASE_TTL_MS = 60_000; export class OptimisticCache { @@ -40,6 +57,9 @@ export class OptimisticCache { private readonly pending = new Map[]>(); private readonly rollbackHandlers = new Set<(event: RollbackEvent) => void>(); private readonly versionCounters = new Map(); + private readonly eventHandlers = new Set>(); + /** Per-invoice expiry timestamps for values written through this layer. */ + private readonly expiries = new Map(); constructor(base?: SimpleCache) { this.base = base ?? new SimpleCache({ enabled: true, ttlMs: DEFAULT_BASE_TTL_MS }); @@ -48,14 +68,62 @@ export class OptimisticCache { /** * Read the current UI-facing value for an invoice: the most recently * applied still-pending optimistic prediction if one exists, otherwise - * the committed base value. + * the committed base value. Emits `hit`/`miss`/`expire` lifecycle events. */ get(invoiceId: string): T | undefined { const queue = this.pending.get(invoiceId); if (queue && queue.length > 0) { - return queue[queue.length - 1]!.predictedValue; + const value = queue[queue.length - 1]!.predictedValue; + this._emit({ type: "hit", invoiceId, value, timestamp: Date.now() }); + return value; + } + + if (this._isExpired(invoiceId)) { + const expired = this.base.get(invoiceId); + this.base.delete(invoiceId); + this.expiries.delete(invoiceId); + this._emit({ type: "expire", invoiceId, value: expired, timestamp: Date.now() }); + return undefined; + } + + const value = this.base.get(invoiceId); + if (value === undefined) { + this._emit({ type: "miss", invoiceId, timestamp: Date.now() }); + } else { + this._emit({ type: "hit", invoiceId, value, timestamp: Date.now() }); + } + return value; + } + + /** + * Write a committed value into the base cache with an optional TTL + * (milliseconds). Emits a `set` event. + */ + set(invoiceId: string, value: T, ttlMs?: number): void { + this.base.set(invoiceId, value); + if (ttlMs !== undefined && ttlMs > 0) { + this.expiries.set(invoiceId, Date.now() + ttlMs); + } else { + this.expiries.delete(invoiceId); } - return this.base.get(invoiceId); + this._emit({ type: "set", invoiceId, value, timestamp: Date.now() }); + } + + /** + * Invalidate a single invoice: drop any pending predictions and the + * committed base value, emitting an `invalidate` event. + */ + invalidate(invoiceId: string): void { + this.pending.delete(invoiceId); + this.expiries.delete(invoiceId); + this.base.delete(invoiceId); + this._emit({ type: "invalidate", invoiceId, timestamp: Date.now() }); + } + + /** Invalidate every cached invoice, emitting one `invalidate` per key. */ + invalidateAll(): void { + const keys = new Set([...this.pending.keys(), ...this.expiries.keys()]); + for (const invoiceId of keys) this.invalidate(invoiceId); } /** Number of optimistic mutations across all invoices awaiting commit/rollback. */ @@ -71,6 +139,12 @@ export class OptimisticCache { return () => this.rollbackHandlers.delete(handler); } + /** Register a listener for cache lifecycle events (set/hit/miss/expire/invalidate). */ + onEvent(handler: CacheEventHandler): () => void { + this.eventHandlers.add(handler); + return () => this.eventHandlers.delete(handler); + } + /** * Apply a predicted value for `invoiceId` immediately. Returns a * `{ commit, rollback }` pair: `commit()` writes the prediction into the @@ -97,7 +171,7 @@ export class OptimisticCache { const commit: CommitFn = () => { if (settled) return; settled = true; - this.base.set(invoiceId, entry.predictedValue); + this.set(invoiceId, entry.predictedValue); this._removeEntry(entry); }; @@ -110,7 +184,7 @@ export class OptimisticCache { const stillPending = remaining && remaining.length > 0; const restoredValue = stillPending ? remaining![remaining!.length - 1]!.predictedValue : entry.rollbackValue; if (!stillPending) { - this.base.set(invoiceId, entry.rollbackValue); + this.set(invoiceId, entry.rollbackValue); } const event: RollbackEvent = { key, invoiceId, version, restoredValue }; @@ -126,6 +200,21 @@ export class OptimisticCache { return { commit, rollback, key }; } + private _isExpired(invoiceId: string): boolean { + const expiry = this.expiries.get(invoiceId); + return expiry !== undefined && Date.now() >= expiry; + } + + private _emit(event: CacheEvent): void { + for (const handler of this.eventHandlers) { + try { + handler(event); + } catch { + // Isolate listener failures from cache bookkeeping. + } + } + } + private _removeEntry(entry: OptimisticEntry): void { const queue = this.pending.get(entry.invoiceId); if (!queue) return; diff --git a/src/index.ts b/src/index.ts index f9cb56b..65dde44 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,1429 +1,176 @@ /** - * @stellar-split/sdk — public API (core exports) + * SDK entry point. + * + * Exposes the public surface of the SDK along with a lightweight caching + * layer that supports per-entry TTLs and explicit invalidation. */ -import type { Invoice } from "./types.js"; -import type { StellarSplitClientConfig } from "./client.js"; -import type { ExportFormat } from "./export.js"; - -export { StellarSplitClient } from "./client.js"; -export { FinalityChecker } from "./finalityChecker.js"; -export { buildPaymentMemo, parsePaymentMemo } from "./memoBuilder.js"; -export type { - StellarSplitClientConfig, - NetworkConfig, - TxResult, - StellarSplitPlugin, -} from "./client.js"; - -export type { - TelemetryHooks, - TelemetryErrorContext, - TelemetryCallStartParams, - TelemetryCallEndParams, -} from "./telemetryHooks.js"; - -export { PluginRegistry, LoggingPlugin } from "./plugin.js"; -export type { SdkPlugin, SdkMethodName, PluginArgs, PluginResult } from "./plugin.js"; - -export { - serializeInvoiceTemplate, - deserializeInvoiceTemplate, -} from "./invoiceTemplate.js"; -export { - validateBulkImport, - SUPPORTED_SCHEMA_VERSIONS, -} from "./bulkImportValidator.js"; -export type { - BulkImportRowError, - BulkImportValidationResult, - BulkImportPayload, -} from "./bulkImportValidator.js"; -export { - StellarSplitError, - InvoiceNotFoundError, - InvoiceNotPendingError, - DeadlinePassedError, - InsufficientBalanceError, - PaymentExceedsRemainingError, - InvoiceFrozenError, - CoCreatorApprovalNotRequiredError, - ChainTooDeepError, - CircularPrerequisiteError, - CircularForwardChainError, - ForwardChainTooDeepError, - UnauthorizedError, - parseSorobanError, - NftGateRequiredError, - WalletNotConnectedError, - RpcError, - ContractError, - CircuitOpenError, - ValidationError, - PluginAlreadyRegisteredError, - InvalidBatchSizeError, - InvoiceNotReleasedError, - TransactionFailedError, - TransactionNotConfirmedError, - SimulationFailedError, - NoReturnValueError, - UnknownNetworkError, - InsufficientSignaturesError, - CloneChainTooDeepError, - NoPendingPayoutError, - InvalidAttestationError, - InvoiceFlowFetcherNotRegisteredError, - InvoiceFetcherNotRegisteredError, - UnknownEndpointError, - RpcUnavailableError, - DiscoveryFetchError, - PayerAddressRequiredError, - SignerFailedError, - NoSignerProvidedError, - ConnectionPoolConfigError, - ConnectionPoolDisposedError, - SearchFailedError, - TransactionNotSuccessfulError, - QueueFailedError, - UnknownExportFormatError, - DexQuoteFailedError, - TtlExtensionFailedError, - TestHarnessNotInitializedError, - UnknownTestWalletError, - RelationshipTrackerNotInitializedError, - FriendbotFailedError, - DisputeEvidenceError, - OraclePriceError, - Sep41AdapterError, - TrancheProgressError, - RefundGraceError, - PreflightError, - ChannelReconciliationError, - SequenceCacheError, - SequenceNumberTooOldError, - PathNotFoundError, - PathRouterError, - OfferTrackingError, - ClaimableBalanceLifecycleError, - isInvoiceNotFoundError, - isInvoiceNotPendingError, - isDeadlinePassedError, - isInsufficientBalanceError, - isPaymentExceedsRemainingError, - isInvoiceFrozenError, - isCoCreatorApprovalNotRequiredError, - isChainTooDeepError, - isCircularPrerequisiteError, - isForwardChainTooDeepError, - isUnauthorizedError, - isWalletNotConnectedError, - isRpcError, - isContractError, - isCircuitOpenError, - isCircularForwardChainError, - isValidationError, - isNftGateRequiredError, - isPluginAlreadyRegisteredError, - isInvalidBatchSizeError, - isInvoiceNotReleasedError, - isTransactionFailedError, - isTransactionNotConfirmedError, - isSimulationFailedError, - isNoReturnValueError, - isUnknownNetworkError, - isInsufficientSignaturesError, - isCloneChainTooDeepError, - isNoPendingPayoutError, - isInvalidAttestationError, - isInvoiceFlowFetcherNotRegisteredError, - isInvoiceFetcherNotRegisteredError, - isUnknownEndpointError, - isRpcUnavailableError, - isDiscoveryFetchError, - isPayerAddressRequiredError, - isSignerFailedError, - isNoSignerProvidedError, - isConnectionPoolConfigError, - isConnectionPoolDisposedError, - isSearchFailedError, - isTransactionNotSuccessfulError, - isQueueFailedError, - isUnknownExportFormatError, - isDexQuoteFailedError, - isTtlExtensionFailedError, - isTestHarnessNotInitializedError, - isUnknownTestWalletError, - isRelationshipTrackerNotInitializedError, - isFriendbotFailedError, - isDisputeEvidenceError, - isOraclePriceError, - isSep41AdapterError, - isTrancheProgressError, - isRefundGraceError, - isPreflightError, - isChannelReconciliationError, - isSequenceCacheError, - isSequenceNumberTooOldError, - isPathNotFoundError, - isPathRouterError, - isOfferTrackingError, - isClaimableBalanceLifecycleError, - TooManySubscriptionsError, - isTooManySubscriptionsError, - RequestTimeoutError, - isRequestTimeoutError, - AdminOperationError, - isAdminOperationError, - CommitmentGenerationError, - isCommitmentGenerationError, - BlindingFactorStorageError, - isBlindingFactorStorageError, - BlindingFactorNotFoundError, - isBlindingFactorNotFoundError, - BlindingFactorDecryptionError, - isBlindingFactorDecryptionError, - IPFSPinError, - isIPFSPinError, - IPFSFetchError, - isIPFSFetchError, - CIDMismatchError, - isCIDMismatchError, - IPFSConfigError, - isIPFSConfigError, - ShutdownInProgressError, - isShutdownInProgressError, - // New: AMM Calculator - InsufficientLiquidityError, - isInsufficientLiquidityError, - // New: Timeout Escalation - PaymentEscalationAbortError, - isPaymentEscalationAbortError, - // New: Recipient Deduplicator - DuplicateRecipientError, - isDuplicateRecipientError, - // New: Horizon Error Classifier - ClassifiedHorizonError, - isClassifiedHorizonError, - HorizonErrorClassification, - FinalityTimeoutError, - isFinalityTimeoutError, - ApprovalTimeoutError, - isApprovalTimeoutError, - // New: typed SdkError / SdkErrorCode (issue #607) - SdkError, - SdkErrorCode, - isSdkError, - // Keypair format and signing validation (issue #768) - InvalidKeypairError, - isInvalidKeypairError, -} from "./errors.js"; - -// Invoice metadata JSON Schema validator (issue #533) -export { InvoiceMetadataValidator } from "./validators/invoiceMetadataValidator.js"; -export type { MetadataValidationResult } from "./validators/invoiceMetadataValidator.js"; -export { validateMetadataKeys, MAX_METADATA_KEY_LENGTH } from "./validators/invoiceMetadataValidator.js"; - -// --------------------------------------------------------------------------- -// Lifecycle management (graceful shutdown) -// --------------------------------------------------------------------------- - -export { GracefulShutdownHandler, ShutdownTimeoutError } from "./lifecycle/GracefulShutdownHandler.js"; -export type { ShutdownOptions, TimeoutAction } from "./lifecycle/GracefulShutdownHandler.js"; - -export { getScheduledReleaseCountdown } from "./client.js"; -export { verifyCompletionProof } from "./client.js"; -export { MultiTenantClient } from "./multiTenant.js"; -export type { PoolOptions } from "./multiTenant.js"; -export { ProfilerSession } from "./profiler.js"; -export type { - ProfileReport, - ProfileEntry, - ProfileSession, - RpcCallTiming, - SpeedscopeProfile, - SpeedscopeEventedProfile, - SpeedscopeFrame, - SpeedscopeEvent, - ProfilerSessionOptions, -} from "./profiler.js"; -export { MemoryProfiler, memoryProfiler, ProfilerNotInitializedError } from "./memoryProfiler.js"; -export type { MemorySnapshot } from "./memoryProfiler.js"; -export { - enrichInvoice, - enrichInvoices, - registerInvoiceFetcher, - hasIPFSMetadata, - getInvoiceMetadataCID, -} from "./enricher.js"; -export type { EnrichedInvoice, EnrichOptions } from "./enricher.js"; -export { EnricherCache } from "./enricher.js"; - -// IPFS functionality -export { - pinInvoiceMetadata, - verifyCID, - verifyCIDOrThrow, - fetchFromIPFS, - fetchInvoiceMetadata, - parseIPFSCid, - configureIPFS, - getIPFSConfig, - resetIPFSConfig, - createLineItem, - createInvoiceMetadata, - deserializeMetadata, - DEFAULT_IPFS_CONFIG, -} from "./ipfs.js"; - -// Confidential payments (Pedersen commitments) -export { - generateCommitment, - verifyCommitment, - storeBlindingFactor, - loadBlindingFactor, - deleteBlindingFactor, - configureBlindingFactorStorage, - resetBlindingFactorStorageConfig, - buildRevealTransaction, - generateAndStoreCommitment, - buildRevealTransactionFromStorage, -} from "./confidential.js"; - -export { Deduplicator } from "./dedup.js"; - -export { TxQueue } from "./queue.js"; - -export { replayEvents } from "./events.js"; -export { sdkEvents } from "./events.js"; -export type { FinalityServerLike } from "./finalityChecker.js"; -export { ApprovalWorkflowSequencer, ApprovalSession } from "./approvalWorkflowSequencer.js"; -export type { ApprovalWorkflowOptions, NotificationAdapter, SignatureApplier } from "./approvalWorkflowSequencer.js"; -export { OperationChunker, MAX_OPERATIONS_PER_TRANSACTION } from "./operationChunker.js"; -export { StreamHealthProbe } from "./streamHealthProbe.js"; -export type { MonitoredStream, StreamHealthProbeOptions } from "./streamHealthProbe.js"; -export { - EventChecksumChain, - verifyChain, - findTamperedEvent, -} from "./eventChecksum.js"; -export { - CircuitBreakerMonitor, - defaultCircuitBreakerMonitor, -} from "./circuitBreakerMonitor.js"; - -// Circuit breaker + retry resilience layer (Issue #419) -export { CircuitBreaker } from "./circuitBreaker.js"; -export type { - CircuitBreakerConfig, - CircuitBreakerState, -} from "./circuitBreaker.js"; -export { ResilientRpcClient } from "./resilientRpc.js"; -export type { RetryConfig } from "./resilientRpc.js"; - -export { connectWallet, getPublicKey, signTransaction } from "./wallet.js"; -export { LobstrAdapter } from "./wallets/adapters/LobstrAdapter.js"; -export type { LobstrAdapterOptions } from "./wallets/adapters/LobstrAdapter.js"; -export { WalletConnectionTimeoutError, isWalletConnectionTimeoutError } from "./errors.js"; - -export { checkRPCHealth } from "./health.js"; -export { FallbackChain, FallbackExhaustedError } from "./fallbackChain.js"; - -// --------------------------------------------------------------------------- -// #544 — Soroban Contract Event Log Subscriber -// --------------------------------------------------------------------------- - -export { ContractEventSubscriber } from "./contractEventSubscriber.js"; -export type { - ContractEventFilter, - ParsedContractEvent, - ContractEventSubscriberConfig, -} from "./contractEventSubscriber.js"; - -// --------------------------------------------------------------------------- -// #546 — Horizon Endpoint Availability Prober -// --------------------------------------------------------------------------- - -export { HorizonProber } from "./horizonProber.js"; -export type { - HorizonProbeResult, - HorizonProberConfig, -} from "./horizonProber.js"; - -// Invoice calculator -export { - calculateSplitAmounts, - computeAmounts, - formatSplitPercentage, - calculateInvoiceSubtotal, - calculateInvoiceBreakdown, -} from "./invoice/calculator.js"; - -// Fee estimator -export { estimateFeeForAmount } from "./feeEstimator.js"; -export type { FeeForAmountOpts } from "./feeEstimator.js"; - -// AMM Calculator -export { estimateSwapOutput, calculatePoolShare } from "./ammCalculator.js"; - -// Recipient Deduplicator -export { deduplicateRecipients } from "./validators/recipientDeduplicator.js"; -export type { DedupMode } from "./validators/recipientDeduplicator.js"; - -// Horizon Error Classifier -export { classifyHorizonError, isHorizonErrorRetryable } from "./horizonErrorClassifier.js"; -export { groupInvoicesByPattern } from "./smartGrouping.js"; -export type { InvoiceCluster } from "./smartGrouping.js"; - -export { getOptimisticInvoice } from "./optimistic.js"; - -export { watchContractUpgrade } from "./upgrade.js"; - -export { calculateFee } from "./fee.js"; - -export { formatAddress } from "./utils.js"; - -export { resolveToken } from "./token.js"; - -export { watchExpiry } from "./watcher.js"; - -export { DeadlineEngine } from "./deadlineEngine.js"; - -export { LedgerCloseEstimator } from "./ledgerCloseEstimator.js"; -export type { - LedgerCloseEstimatorOptions, - LedgerRecord, - CalibrationState, -} from "./ledgerCloseEstimator.js"; - -export { StellarSplitTxBuilder } from "./txBuilder.js"; - -export { SequenceCache, isSequenceTooOld } from "./sequenceCache.js"; -export type { SequenceCacheConfig } from "./sequenceCache.js"; - -export { PathRouter } from "./pathRouter.js"; -export type { PathResult, PathHop, PathRequest, PathRouterConfig } from "./pathRouter.js"; -export { PathQueryBuilder } from "./pathQueryBuilder.js"; -export type { - StrictSendQueryParams, - StrictReceiveQueryParams, - PathQueryBuilderConfig, -} from "./pathQueryBuilder.js"; -export type { PathQuery, PathQueryResult, StrictSendPathQuery, StrictReceivePathQuery } from "./types.js"; - -export { OfferTracker } from "./offerTracker.js"; -export type { OfferTrackerConfig, OfferTrackerEventMap } from "./offerTracker.js"; - -export { SimpleCache, Cache } from "./cache.js"; -export { Recorder, createRecorder } from "./recorder.js"; -export type { SessionRecording, RecordingEntry, ReplayResult } from "./recorder.js"; - -export { TabSync, tabSyncPlugin, createTabSyncPlugin } from "./tabSync.js"; -export type { TabSyncEvent, TabSyncEventType, TabSyncOptions } from "./tabSync.js"; - -export type { - Invoice, - InvoiceReceipt, - Payment, - Recipient, - InvoiceStatus, - CreateInvoiceParams, - PayParams, - InvoiceTemplate, - PaginatedResult, - PaginationOptions, - BatchPayment, - InvoiceEventCallbacks, - SimulateCreateInvoiceResult, - SimulatePayResult, - PreviewTokenSwapResult, - SDKHealth, - FeeBreakdown, - TokenInfo, - ExpiryEvent, - ExpiryCallback, - PaymentProof, - CircuitBreakerStatus, - HistoricalInvoice, - ContractFeatures, - CloneOverrides, - OverflowBehavior, - InvoiceExt, - PaymentOptions, - NftGateResult, - ClaimPayoutResult, - PayWithAttestationParams, - AttestationPaymentReceipt, - CreatorVolumeCap, - PaymentCooldown, - CrossChainRef, - SetCrossChainRefParams, - RolloverResult, - ScheduledReleaseCountdown, - DisputeStatus, - AuctionBid, - AuctionInfo, - TimelockAction, - QueueActionParams, - CompletionProof, - AdminFreezeResult, - AdminUnfreezeResult, - TransitionRecord, - SponsorshipConfig, - SponsorReserveCheckResult, - InvoiceRecord, - XDRType, - DecodedXDR, - DecodedTransactionEnvelope, - DecodedTransactionResult, - DecodedOperationResult, - DecodedTransactionMeta, - DecodedLedgerEntry, - DecodedOperation, - FinalityStatus, - FinalityCheckConfig, - MultiSigPolicy, - ApprovalSessionResult, - BatchPaymentResult, - ChunkSubmitter, -} from "./types.js"; -export { InvalidTransitionError } from "./types.js"; - -// Invoice status transition validation (state machine) -export { InvoiceStateMachine } from "./state/InvoiceStateMachine.js"; -export type { - InvoiceStateMachineEventMap, - TransitionEvent, - InvalidTransitionEvent, -} from "./state/InvoiceStateMachine.js"; -export type { StateMachineConfig, TransitionGraph } from "./types/state.js"; - -// Per-method timeout (Issue #1) -export { TimeoutManager, withTimeout, EscalationManager, RequestTimeoutError as TimeoutError } from "./timeout.js"; -export type { TimeoutConfig, EscalationEvent, EscalationCallback } from "./timeout.js"; - -// Trace IDs (Issue #2) -export { TraceIdManager, globalTraceIdManager, generateTraceId } from "./traceId.js"; -export type { TraceIdGenerator } from "./traceId.js"; - -// Injectable RpcClient (Issue #3) -export { SorobanRpcAdapter } from "./rpcClient.js"; -export type { RpcClient } from "./rpcClient.js"; - -export { negotiateVersion, SDK_CONTRACT_VERSION } from "./version.js"; -export type { VersionInfo } from "./types.js"; - -export { checkPayerReadiness, checkInvoiceExpiry, checkSponsorReserve, checkRecipientFlags, runPreflight } from "./preflightChecker.js"; -export type { PayerReadinessResult, PayerReadinessReason, InvoiceExpiryResult, InvoiceExpiryReason, SponsorReserveCheck, RecipientFlagsCheck, RunPreflightOptions } from "./preflightChecker.js"; - -export { inspectFlags, hasAnyRestrictiveFlag } from "./accountFlagsInspector.js"; -export type { AccountFlagSet } from "./types.js"; - -export { getSuggestion } from "./errorSuggestions.js"; - -// --------------------------------------------------------------------------- -// XDR Decoder — structured logging of Stellar XDR -// --------------------------------------------------------------------------- - -export { decodeXDR, decode, decodeInt128 } from "./xdrDecoder.js"; -export { decodeTransactionResult } from "./txResultDecoder.js"; - -// --------------------------------------------------------------------------- -// SSE Cursor Tracker — persistent cursor for stream resumption -// --------------------------------------------------------------------------- - -export { - configureCursorStore, - getCursor, - setCursor, - removeCursor, - setCursorFromSnapshot, - _resetCursorTrackerForTesting, -} from "./cursorTracker.js"; -export type { CursorPersistence } from "./cursorTracker.js"; - -// --------------------------------------------------------------------------- -// Stream + SSE subscription helpers -// --------------------------------------------------------------------------- - -// Real-time invoice event subscription (Issue #417) -export { createInvoiceSubscription } from "./subscription.js"; -export type { - SubscriptionLifecycleCallback, - InvoiceEventCallback, -} from "./subscription.js"; -export { - isInvoicePaymentEvent, - isInvoiceReleasedEvent, - isInvoiceRefundedEvent, - isInvoiceCancelledEvent, - isInvoiceFrozenEvent, - isInvoiceUnfrozenEvent, - isInvoiceCreatedEvent, -} from "./subscription.js"; -export type { - InvoiceEvent, - InvoiceCreatedEvent, - InvoicePaymentEvent, - InvoiceReleasedEvent, - InvoiceRefundedEvent, - InvoiceCancelledEvent, - InvoiceFrozenEvent, - InvoiceUnfrozenEvent, - DisputeOpenedEvent, - DisputeResolvedEvent, - SplitRulesUpdatedEvent, - AutoResolveRulesUpdatedEvent, - VelocityLimitUpdatedEvent, - PrerequisiteAddedEvent, - PrerequisiteRemovedEvent, - ForwardChainCreatedEvent, - ScheduledReleaseSetEvent, - PenaltyTiersUpdatedEvent, - AllowedCallersUpdatedEvent, - NftGateSetEvent, - NftGateRemovedEvent, - BaseInvoiceEvent, - Subscription, - SubscriptionOptions, - SubscriptionLifecycleEvent, - // New: AMM Calculator - PoolSwapEstimate, - PoolShareResult, - // New: Timeout Escalation - EscalationStep, - TimeoutPolicy, -} from "./types.js"; - -export { analyzeCohorts } from "./cohortAnalyzer.js"; -export type { CohortBucket } from "./cohortAnalyzer.js"; - -export { - recordWebhookEvent, - replayWebhook, - configureReplayStore, - RingBufferStore, - WebhookEventNotFoundError, -} from "./webhookReplay.js"; -export type { WebhookRecord, WebhookReplayStore } from "./webhookReplay.js"; - -// Webhook middleware for receiving and verifying incoming webhooks -export { - createWebhookMiddleware, - generateWebhookSignature, - verifyWebhookSignature, - parseWebhookPayload, - isValidEventType, - isWebhookRequest, - InvalidSignatureError, - TimestampOutOfBoundsError, - ReplayAttackError, - MissingHeaderError, - InvalidPayloadError, - WebhookValidationError, -} from "./webhookMiddleware.js"; -export type { - WebhookOptions, - InvoiceEventType, - WebhookPayload, - WebhookRequest, - RequestHandler, - InvoiceCreatedData, - InvoicePaidData, - InvoiceReleasedData, - InvoiceFailedData, - InvoiceRefundedData, - InvoiceCancelledData, - InvoiceExpiredData, -} from "./webhookMiddleware.js"; -// --------------------------------------------------------------------------- -// Lazy factories for heavy modules -// --------------------------------------------------------------------------- - - - -export async function getExportModule(): Promise { - return await import("./export.js"); +export type CacheEventType = + | "set" + | "hit" + | "miss" + | "expire" + | "invalidate"; + +export interface CacheEvent { + type: CacheEventType; + key: K; + /** Present for "set" events. */ + ttl?: number; + /** Present for "expire" and "invalidate" events. */ + reason?: string; } -export async function exportInvoice( - invoice: Invoice, - format: ExportFormat, -): Promise { - const m = await getExportModule(); - return m.exportInvoice(invoice, format); -} +export type CacheEventListener = (event: CacheEvent) => void; -export async function getProofModule(): Promise { - return await import("./proof.js"); +export interface CacheOptions { + /** Default time-to-live in milliseconds. `0` or `Infinity` disables expiry. */ + defaultTtl?: number; + /** Injectable clock, primarily for testing. Defaults to `Date.now`. */ + now?: () => number; } -export async function generatePaymentProof( - txHash: string, - config: StellarSplitClientConfig, -): Promise { - const m = await getProofModule(); - return m.generatePaymentProof(txHash, config); +interface CacheEntry { + value: V; + /** Absolute expiry timestamp in ms, or `Infinity` when it never expires. */ + expiresAt: number; } -// Payment receipt generator -export { - compilePaymentReceipt, - generatePaymentReceipt, - serializePaymentReceipt, - deserializePaymentReceipt, - finalizePaymentReceipt, - // Local receipt registry (hash-based lookup) - registerReceipt, - getReceiptByTxHash, - getAllReceipts, - clearReceipts, -} from "./receipt.js"; -export type { - PaymentReceipt, - PaymentReceiptJSON, - InvoiceFetcher, - ReceiptConfig, -} from "./receipt.js"; - -// Transaction operation effect aggregator -export { aggregateEffects } from "./effectAggregator.js"; -export type { AccountEffectSummary, AssetDelta } from "./types.js"; - -// Multi-asset invoice line item normalizer -export { normalizeLineItems } from "./lineItemNormalizer.js"; -export { ContractPriceOracle } from "./priceOracle.js"; -export type { PriceOracle } from "./priceOracle.js"; -export type { - InvoiceLineItem, - NormalizedLineItem, - NormalizedInvoiceTotal, -} from "./types.js"; -export { UnsupportedLineItemAssetError, isUnsupportedLineItemAssetError } from "./errors.js"; - -// Contract invocation retry queue -export { ContractRetryQueue } from "./contractRetryQueue.js"; -export type { ContractInvocationExecutor, ContractRetryQueueConfig } from "./contractRetryQueue.js"; -export type { ContractInvocation, ContractResult } from "./types.js"; -export { ContractRetryExhaustedError, isContractRetryExhaustedError } from "./errors.js"; - -// Invoice batch processor with concurrency limiter -export { InvoiceBatchProcessor } from "./invoiceBatchProcessor.js"; -export type { - BatchInvoiceResult, - InvoiceBatchConfig, - InvoicePaymentSubmitter, -} from "./invoiceBatchProcessor.js"; - -// Merkle proof functionality -export { generateMerkleProof, verifyMerkleProof } from "./merkle.js"; -export type { MerkleProof } from "./merkle.js"; - -// Simulation sandbox — fork ledger state via simulateTransaction and run -// sequences of SDK operations against it without touching the network. -export { SimulationSandbox } from "./sandbox/SimulationSandbox.js"; -export type { - SandboxClient, - SimulationCost, - SimulationResult, - SandboxInvoiceRecord, - SandboxPaymentRecord, - SandboxCallLogEntry, - SandboxLedgerDiff, -} from "./sandbox/SimulationSandbox.js"; - -// Horizon SSE stream manager — cursor-bookmarked payments/operations -// streaming with reconnect-and-resume, dedupe, and replay cutoff. -export { - HorizonStreamManager, - InMemoryCursorStore, - createLocalStorageCursorStore, - createSessionStorageCursorStore, - DEFAULT_REPLAY_CUTOFF_MS, - DEFAULT_DEDUPE_BUFFER_SIZE, - DEFAULT_RECONNECT_DELAY_MS, -} from "./horizon/HorizonStreamManager.js"; -export type { - HorizonStreamRecord, - HorizonCallBuilderLike, - HorizonStreamSource, - CursorStore, - HorizonStreamKind, - HorizonStreamManagerConfig, - HorizonStreamEventMap, -} from "./horizon/HorizonStreamManager.js"; - -// Adaptive rate-limit throttle — sizes a token bucket from observed -// X-RateLimit-* headers and backs off further on 429s. -export { - AdaptiveThrottle, - DEFAULT_PENALTY_DURATION_MS, - DEFAULT_MAX_BACKOFF_MS, -} from "./throttle/AdaptiveThrottle.js"; -export type { AdaptiveThrottleConfig, ThrottleStats } from "./throttle/AdaptiveThrottle.js"; -export { parseRateLimitHeaders } from "./throttle/RateLimitParser.js"; -export type { HeadersLike, RateLimitInfo } from "./throttle/RateLimitParser.js"; -export { parseRetryAfter } from "./throttle/RateLimitParser.js"; - -// Receipt chain — SHA-256-linked, tamper-evident payment receipt history -// per invoice. `PaymentReceipt` is aliased to `ChainPaymentReceipt` here to -// avoid colliding with the unrelated `PaymentReceipt` already exported from -// receipt.js (a compiled multi-payment summary receipt). -export { - ReceiptChain, - GENESIS_PREV_HASH, - InMemoryReceiptChainStorage, - createLocalStorageReceiptChainStorage, - createSessionStorageReceiptChainStorage, - receiptChainStorageKey, -} from "./receipts/ReceiptChain.js"; -export type { ReceiptChainStorage } from "./receipts/ReceiptChain.js"; -export type { - PaymentReceipt as ChainPaymentReceipt, - ReceiptChainEntry, - ChainVerificationResult, -} from "./types/receipts.js"; - -// Connection multiplexer functionality -export { MultiplexedClient } from "./multiplexer.js"; -export type { WeightedEndpoint } from "./multiplexer.js"; - -// Connection pool (issue #360): up to 5 persistent HTTP/2 connections to the -// primary Soroban RPC endpoint with least-busy selection, 60s idle recycle, -// and per-slot stats. -export { - ConnectionPool, - MAX_POOL_SIZE, - DEFAULT_IDLE_TIMEOUT_MS, - DEFAULT_POOL_SIZE, -} from "./connectionPool.js"; -export type { - ConnectionPoolConfig, - PoolStats, - PoolSlotStats, - PooledServer, -} from "./connectionPool.js"; - -// Request batcher functionality -export { RequestBatcher, BatchedRpcClient } from "./requestBatcher.js"; -export type { BatcherConfig, BatchFetchers, BatchCallType } from "./requestBatcher.js"; - -export type { ComplianceReport } from "./compliance.js"; - -export { exportComplianceReport, CSV_COLUMNS } from "./complianceExporter.js"; -export type { - ComplianceExportRecord, - ComplianceExportOptions, - ComplianceExportResult, -} from "./complianceExporter.js"; - -export { ScheduledPaymentManager } from "./scheduler.js"; -export type { ScheduledPayment } from "./scheduler.js"; - -export { InvoiceReminderScheduler, DEFAULT_GRACE_PERIOD_MS } from "./invoiceReminderScheduler.js"; -export type { - InvoiceReminderSchedulerEventMap, - InvoiceDueAtResolver, - InvoiceReminderSchedulerOptions, -} from "./invoiceReminderScheduler.js"; -export { loadReminderSchedules, saveReminderSchedules } from "./snapshot.js"; -export type { ReminderSchedule, ReminderEvent, ReminderStatus } from "./types.js"; - -export { compileFilter, applyFilter, FilterIndex } from "./invoiceFilter.js"; -export type { FilterCriteria, CompiledFilter } from "./invoiceFilter.js"; - -// Invoice diff utility -export { diffInvoices, hasDiff } from "./diff.js"; -export type { InvoiceDiff, InvoiceDiffEntry } from "./diff.js"; - -export { diffSimulations, compareSimulations, formatDiffSummary } from "./simulationDiff.js"; -export type { - SimulationDiff, - SimulationDiffSuccess, - SimulationDiffNotComparable, - ResourceDelta, - SimulationComparison, - SimulationComparisonNotComparable, -} from "./simulationDiff.js"; - -// Payment velocity tracking -export { trackVelocity } from "./velocityTracker.js"; -export type { VelocityReport, InvoiceVelocity, PaymentTrend } from "./velocityTracker.js"; -export type { VelocityStatus, VelocityWindowStatus } from "./types.js"; - -// Tranche release progress tracking -export { getTrancheProgress } from "./trancheProgress.js"; -export type { - TrancheProgress, - TrancheProgressReport, - TrancheProgressOptions, - TrancheConfig, - TranchedInvoice, - TrancheStatus, -} from "./trancheProgress.js"; - -// Invoice payment progress tracking -export { PaymentProgressTracker } from "./paymentProgressTracker.js"; -export type { - PaymentProgressEventMap, - PaymentProgressTrackerOptions, -} from "./paymentProgressTracker.js"; -export type { InvoicePaymentProgress, RecipientPaymentState } from "./types.js"; - -// Fiat-to-asset price oracle adapter -export { CoinGeckoPriceOracle } from "./priceOracle.js"; -export type { CoinGeckoPriceOracleOptions } from "./priceOracle.js"; -export { RateCache } from "./rateCache.js"; -export type { RateCacheConfig } from "./rateCache.js"; -export type { PriceOracleAdapter } from "./types.js"; -export { convertFiatToAsset } from "./currencyConverter.js"; -export type { FiatConversion, ConvertedAmount } from "./currencyConverter.js"; - -export { Sep41Adapter, createSep41Adapter } from "./sep41Adapter.js"; -export type { Sep41TokenCapabilities } from "./sep41Adapter.js"; - -export { Sep31Initiator, resolveDirectPaymentServer } from "./sep/sep31Initiator.js"; -export type { - Sep31InitiatorEventMap, - Sep31Asset, - Sep31PartyInfo, - Sep31InitiateParams, -} from "./sep/sep31Initiator.js"; -export type { - Sep31PaymentRecord, - Sep31Status, - Sep31StatusChangedEvent, - Sep31FieldSpec, - Sep31RequiredFields, -} from "./types.js"; - -export { HorizonFallbackReader } from "./horizonFallback.js"; -export type { NormalizedAccount, NormalizedBalance } from "./horizonFallback.js"; - -export { - buildSponsoredOnboarding, - MissingSponsorAccountError, - InsufficientReserveError, - checkSponsorshipReserve, -} from "./sponsorship.js"; - -export { - extendStorageTtl, - buildContractDataLedgerKey, - buildInvoiceDataLedgerKey, - buildInvoiceStorageKey, -} from "./ttlExtension.js"; -export type { - TtlExtensionOptions, - TtlExtensionResult, -} from "./ttlExtension.js"; - -export { - diffTemplate, - migrateTemplate, - migrateAllTemplates, -} from "./templateMigration.js"; -export type { - TemplateDiff, - TemplateDiffField, -} from "./templateMigration.js"; - -export { - validateClientConfig, - validateOrThrow, - InvalidConfigError, -} from "./configValidator.js"; -export type { - ConfigValidation, - ConfigValidationErrorType, -} from "./configValidator.js"; - -export { FundingVelocityAlert } from "./velocityAlert.js"; -export type { - VelocityAlert, - VelocityAlertKind, - VelocityConfig, -} from "./velocityAlert.js"; - -export { - createClaimableRefund, - getClaimableRefunds, - isRefundTransferError, - ClaimableBalanceLifecycle, -} from "./claimableBalanceFallback.js"; -export type { - ClaimableRefundResult, - ClaimableRefundEntry, - ClaimableBalanceLifecycleConfig, - ClaimableBalanceLifecycleEventMap, -} from "./claimableBalanceFallback.js"; - -export { PredicateBuilder } from "./predicateBuilder.js"; -export type { ClaimPredicate } from "./predicateBuilder.js"; -export type { PredicateConfig } from "./types.js"; - -export type { RateCacheEntry, RateOracleFn } from "./rateCache.js"; - -export { subscribeToInvoice } from "./sse.js"; -export type { - SSEInvoiceEventType, - SSEInvoiceEvent, - InvoiceEventHandler, - SubscribeToInvoiceOptions, - EventSourceLike, -} from "./sse.js"; -export type { PollingInvoiceEventHandler } from "./stream.js"; - -// WebSocket transport (Issue #377) -export { WebSocketTransport } from "./websocket.js"; -export type { TransportType, TransportStatus, TransportEventMap } from "./websocket.js"; -export { - bundleDisputeEvidence, - computeBundleChecksum, - verifyBundleChecksum, - registerProofFetcher, - registerAuditLogFetcher, - registerEventFetcher, -} from "./disputeEvidenceBundler.js"; -export type { - DisputeEvidenceBundle, - ProofFetcher, - AuditLogFetcher, - EventFetcher, -} from "./disputeEvidenceBundler.js"; - -export { UsageAnalyticsCollector, wrapWithAnalytics } from "./usageAnalytics.js"; -export type { - UsageAnalyticsConfig, - FeatureCountSnapshot, -} from "./usageAnalytics.js"; -export { IdempotencyManager } from "./idempotency.js"; -export type { IdempotencyConfig } from "./idempotency.js"; - -export { RollbackCoordinator } from "./splitRollbackCoordinator.js"; -export type { SplitRollbackEventMap } from "./splitRollbackCoordinator.js"; -export type { SplitRollbackRecord } from "./snapshot.js"; -export type { SplitLeg, SplitLegState, SplitResult, SplitRollbackCheckpoint } from "./types.js"; - -export { - validateInvoicePayload, - PayloadSizeError, -} from "./payloadGuard.js"; -export type { - PayloadGuardConfig, - PayloadViolation, -} from "./payloadGuard.js"; - -export { computeCreatorReputation } from "./reputation.js"; -export type { - CreatorReputationScore, - ReputationConfig, -} from "./reputation.js"; - -export { computePaymentForecast } from "./forecast.js"; -export type { - PaymentForecast, - ForecastConfig, - HistoricalInvoiceSample, -} from "./forecast.js"; - -// --------------------------------------------------------------------------- -// Split ratio validator -// --------------------------------------------------------------------------- - -export { - validateSplitRatios, - validateSplitRatiosOrThrow, - ratiosToRecipients, - validateSplitTotal, - normalizeSplits, -} from "./validators/splitRatioValidator.js"; -export type { - RecipientShare, - SplitConfig, - SplitRatioValidationResult, -} from "./validators/splitRatioValidator.js"; - -// --------------------------------------------------------------------------- -// Trustline checker -// --------------------------------------------------------------------------- - -export { checkTrustlines, checkSingleTrustline, checkTrustlinesBatch } from "./trustlineChecker.js"; -export type { TrustlineEntry, TrustlineCheckResult } from "./trustlineChecker.js"; - -// --------------------------------------------------------------------------- -// XDR parser -// --------------------------------------------------------------------------- - -export { parseEnvelope } from "./xdrParser.js"; -export type { - ParsedEnvelope, - ParsedTransaction, - ParsedOperation, - ParsedMemo, - ParsedSignature, - ParsedTimeBounds, -} from "./xdrParser.js"; - -// --------------------------------------------------------------------------- -// Fee surge detector -// --------------------------------------------------------------------------- - -export { detectFeeSurge, clearFeeSurgeCache } from "./feeSurgeDetector.js"; -export type { FeeSurgeConfig, FeeRecommendation, CongestionLevel } from "./feeSurgeDetector.js"; - -export { - reconcileChannel, - registerChannelStateFetcher, -} from "./channelReconciler.js"; -export type { - ChannelState, - ChannelReconciliationResult, - ChannelStateFetcher, -} from "./channelReconciler.js"; -export { getInvoiceStats, computeInvoiceStats } from "./invoiceStats.js"; -export { getInvoiceAge, getFundingVelocity } from "./invoiceStats.js"; -export type { InvoiceAge } from "./invoiceStats.js"; - -export { previewSplitRules } from "./splitPreview.js"; - -// --------------------------------------------------------------------------- -// #545 — Invoice Split Preview Change Diff Generator -// --------------------------------------------------------------------------- - -export { generateSplitDiff } from "./splitPreview.js"; -export type { - SplitConfigDiff, - ChangedShare, -} from "./splitPreview.js"; - -export { simulateAutoResolve } from "./autoResolveSimulator.js"; - -export { - resolvePrerequisiteChain, - MAX_PREREQUISITE_CHAIN_DEPTH, -} from "./prerequisiteChain.js"; - -export type { - SplitRule, - SplitPreviewEntry, - AutoResolveRule, - AutoResolveSimulation, - InvoiceStats, - PrerequisiteChainEntry, - PedersenCommitment, - BlindingFactorStorageConfig, - StoredBlindingFactor, - RevealPaymentOptions, - LineItem, - InvoiceMetadata, - IPFSConfig, - CIDVerificationResult, -} from "./types.js"; - -// --------------------------------------------------------------------------- -// UI Components (React) -// --------------------------------------------------------------------------- - -export type { - DisputePanelProps, - DisputeEvidenceItem, -} from "./ui/DisputePanel.js"; - -export type { - DisputeTimelineProps, - DisputeTimelineEvent, - DisputeEventType, -} from "./ui/DisputeTimeline.js"; - -export type { - InvoiceDetailPageProps, -} from "./ui/InvoiceDetailPage.js"; - -export type { - UseInvoiceStreamOptions, - UseInvoiceStreamResult, -} from "./ui/hooks/useInvoiceStream.js"; - -// Note: Actual React components are exported from ./ui/index for tree-shaking -// Import them like: import { DisputePanel } from '@stellar-split/sdk/ui' - - -// --------------------------------------------------------------------------- -// Cross-chain bridge payment helpers -// --------------------------------------------------------------------------- - -export { - estimateBridgeFee, - buildBridgePayment, - submitBridgePayment, - computePayloadHash, - DEFAULT_CHAIN_CONFIGS, - SUPPORTED_CHAIN_IDS, - BridgeChainMismatchError, -} from "./bridge.js"; - -export type { ChainBridgeConfig, BridgeConfig, BridgeOptions } from "./bridge.js"; - -export type { - ChainId, - BridgeFeeEstimate, - BridgePaymentParams, - BridgePaymentRequest, - SignedBridgeProof, -} from "./types.js"; - -// Timeline reconstructor -export { PaymentTimelineReconstructor } from "./timeline/PaymentTimelineReconstructor.js"; -export type { - TimelineEntry, - TimelineEventType, - TimelineSource, - TimelineEntryStatus, - ReconstructedTimeline, - RebuildOptions, -} from "./types/timeline.js"; -export type { PaymentTimelineReconstructorConfig } from "./timeline/PaymentTimelineReconstructor.js"; -// --------------------------------------------------------------------------- -// Streaming subscriptions (SubscriptionManager) -// --------------------------------------------------------------------------- - -export { SubscriptionManager, getSubscriptionManager } from "./streaming/SubscriptionManager.js"; -export type { EventCursor, SubscriptionManagerLifecycleEvent } from "./types/events.js"; -export type { SubscriptionOptions as SubscriptionManagerOptions } from "./types/events.js"; -export { createStorageAdapter, MemoryStorageAdapter } from "./storage/storageAdapter.js"; -export type { StorageAdapter, StorageKind } from "./storage/storageAdapter.js"; - -// --------------------------------------------------------------------------- -// Resilience (advanced CircuitBreaker) -// --------------------------------------------------------------------------- - -export { CircuitBreaker as AdvancedCircuitBreaker } from "./resilience/CircuitBreaker.js"; -export type { - CircuitState as AdvancedCircuitState, - CircuitBreakerOptions as AdvancedCircuitBreakerOptions, - CircuitBreakerStateSnapshot as AdvancedCircuitBreakerStateSnapshot, - CircuitBreakerLogger as AdvancedCircuitBreakerLogger, -} from "./resilience/CircuitBreaker.js"; - -// --------------------------------------------------------------------------- -// Waterfall payment routing -// --------------------------------------------------------------------------- - -export { WaterfallRouter } from "./routing/WaterfallRouter.js"; -export type { WaterfallConfig, WaterfallTier, WaterfallPlan, WaterfallStep } from "./types/routing.js"; - -// --------------------------------------------------------------------------- -// Optimistic UI cache -// --------------------------------------------------------------------------- - -export { OptimisticCache } from "./cache/OptimisticCache.js"; -export type { RollbackEvent, OptimisticEntry } from "./cache/OptimisticCache.js"; - -// --------------------------------------------------------------------------- -// Typed, zero-dependency event emitter (works in Node, browser, and edge runtimes) -// --------------------------------------------------------------------------- - -export { TypedEventEmitter, AbortError } from "./events/TypedEventEmitter.js"; -export type { Unsubscribe, EventMap } from "./events/TypedEventEmitter.js"; -export type { SplitClientEventMap } from "./client.js"; - -// --------------------------------------------------------------------------- -// Multi-endpoint RPC load balancing -// --------------------------------------------------------------------------- - -export { RpcLoadBalancer } from "./rpc/RpcLoadBalancer.js"; -export type { - EndpointConfig, - RpcLoadBalancerOptions, - RpcEndpointServer, - RpcLoadBalancerEventMap, - EndpointSnapshot, -} from "./rpc/RpcLoadBalancer.js"; - -// --------------------------------------------------------------------------- -// Optional OpenTelemetry instrumentation (opt-in via `otel: { enabled: true }`; -// `@opentelemetry/api` is never required unless a consumer turns this on). -// --------------------------------------------------------------------------- - -export { OtelExporter, createOtelHandle, noopOtelHandle } from "./telemetry/OtelExporter.js"; -export type { - TelemetryOptions, - OtelHandle, - OtelSpanHandle, - OtlpTracePayload, -} from "./telemetry/OtelExporter.js"; -// #476 — OperationBuilder: fluent multi-op envelope builder with dry-run -// --------------------------------------------------------------------------- - -export { OperationBuilder } from "./builder/OperationBuilder.js"; -export type { - PaymentOptions as OperationBuilderPaymentOptions, - InvokeHostFnOptions, - BumpSequenceOptions, - TimeboundsOptions, - DryRunResult, - SubmitOptions, - OperationBuilderConfig, -} from "./builder/OperationBuilder.js"; -export { - EnvelopeLimitError, - isEnvelopeLimitError, - DryRunFailedError, - isDryRunFailedError, -} from "./errors.js"; - -// --------------------------------------------------------------------------- -// #477 — AccountSignerWeightCalculator: multi-sig pre-flight weight check -// --------------------------------------------------------------------------- - -export { AccountSignerWeightCalculator } from "./accounts/AccountSignerWeightCalculator.js"; -export type { - ThresholdLevel, - SignerWeightResult, -} from "./accounts/AccountSignerWeightCalculator.js"; -export { - InsufficientSignerWeightError, - isInsufficientSignerWeightError, -} from "./errors.js"; - -// --------------------------------------------------------------------------- -// #478 — PaymentDeduplicationFingerprinter: content-based payment dedup -// --------------------------------------------------------------------------- - -export { PaymentDeduplicationFingerprinter } from "./deduplication/PaymentDeduplicationFingerprinter.js"; -export type { - DeduplicationPayment, - CheckResult as DeduplicationCheckResult, -} from "./deduplication/PaymentDeduplicationFingerprinter.js"; -export { - DuplicatePaymentError, - isDuplicatePaymentError, -} from "./errors.js"; - -// --------------------------------------------------------------------------- -// #479 — LazyInitializer + SplitClient: on-demand RPC connection -// --------------------------------------------------------------------------- - -export { LazyInitializer } from "./client/LazyInitializer.js"; -export { SplitClient } from "./client/SplitClient.js"; -export type { SplitClientConfig } from "./client/SplitClient.js"; -export { - RpcConnectionError, - isRpcConnectionError, -} from "./errors.js"; - -// --------------------------------------------------------------------------- -// #483 — ContractStorageExporter: contract storage entry snapshot exporter -// --------------------------------------------------------------------------- - -export { ContractStorageExporter, scValToJson } from "./diagnostics/ContractStorageExporter.js"; -export type { - ContractStorageSnapshot, - StorageEntry, - StorageDiff, - StorageModification, - ScValJson, - ScValJsonPrimitive, - ScValJsonVec, - ScValJsonMap, - ScValPrimitive, - ContractStorageExporterOptions, -} from "./diagnostics/ContractStorageExporter.js"; - -// --------------------------------------------------------------------------- -// #528 — AccountDataManager: typed CRUD for account data entries -// --------------------------------------------------------------------------- - -export { AccountDataManager } from "./accountDataManager.js"; -export type { - AccountDataManagerConfig, - TransactionResult as AccountDataTransactionResult, -} from "./accountDataManager.js"; -export type { AccountDataEntry, AccountDataMap } from "./types.js"; -export { - DataEntryValidationError, - isDataEntryValidationError, -} from "./errors.js"; - -// --------------------------------------------------------------------------- -// #529 — SorobanFeatureDetector: protocol upgrade / feature flag detection -// --------------------------------------------------------------------------- - -export { SorobanFeatureDetector } from "./sorobanFeatureDetector.js"; -export type { - SorobanFeatureDetectorConfig, - SorobanFeatureDetectorEventMap, -} from "./sorobanFeatureDetector.js"; -export type { SorobanFeatureFlags } from "./types.js"; - -// --------------------------------------------------------------------------- -// #530 — StreamDeduplicator: paging-token-based stream event dedup -// --------------------------------------------------------------------------- - -export { StreamDeduplicator } from "./streamDeduplicator.js"; -export type { - StreamDeduplicatorOptions, - StreamDeduplicatorEventMap, -} from "./streamDeduplicator.js"; -export { - InMemoryDedupTokenStore, - setDefaultDedupTokenStore, - saveDedupTokens, - loadDedupTokens, -} from "./snapshot.js"; -export type { DedupTokenStore } from "./snapshot.js"; - -// --------------------------------------------------------------------------- -// #531 — Per-Split Audit Log Emitter -// --------------------------------------------------------------------------- - -export { AuditLogger } from "./auditLogger.js"; -export type { AuditEntry } from "./auditLogger.js"; -export type { SplitAuditEntry } from "./types.js"; -export { - exportSplitAuditTrail, - SPLIT_AUDIT_CSV_COLUMNS, -} from "./complianceExporter.js"; - -// --------------------------------------------------------------------------- -// #589 — Pluggable Signing Key Vault Adapter -// --------------------------------------------------------------------------- - -export type { Signer } from "./signing/signer.js"; -export { KeypairSigner } from "./signing/adapters/KeypairSigner.js"; -export { EncryptedFileSigner } from "./signing/adapters/EncryptedFileSigner.js"; -export type { - EncryptedFileSignerOptions, -} from "./signing/adapters/EncryptedFileSigner.js"; -export { - encryptSigningKeyToPem, - writeEncryptedSigningKeyFile, -} from "./signing/adapters/EncryptedFileSigner.js"; -export { CloudKmsSigner, isRegionError } from "./signing/adapters/CloudKmsSigner.js"; -export type { - KmsClient, - KmsClientSignOptions, - CloudKmsSignerOptions, - CloudKmsSignerEventMap, -} from "./signing/adapters/CloudKmsSigner.js"; - -// --------------------------------------------------------------------------- -// #588 — Soroban Transaction Footprint Optimizer -// --------------------------------------------------------------------------- +/** + * A small in-memory cache with TTL support and lifecycle events. + */ +export class SdkCache { + private readonly store = new Map>(); + private readonly listeners = new Set>(); + private readonly defaultTtl: number; + private readonly now: () => number; + + constructor(options: CacheOptions = {}) { + this.defaultTtl = options.defaultTtl ?? 0; + this.now = options.now ?? Date.now; + } + + /** + * Subscribe to cache lifecycle events. Returns an unsubscribe function. + */ + on(listener: CacheEventListener): () => void { + this.listeners.add(listener); + return () => { + this.listeners.delete(listener); + }; + } + + /** + * Store a value under `key`, optionally overriding the default TTL. + */ + set(key: K, value: V, ttl?: number): void { + const effectiveTtl = ttl ?? this.defaultTtl; + const expiresAt = + effectiveTtl > 0 && Number.isFinite(effectiveTtl) + ? this.now() + effectiveTtl + : Infinity; + + this.store.set(key, { value, expiresAt }); + this.emit({ type: "set", key, ttl: effectiveTtl }); + } + + /** + * Retrieve a value. Expired entries are evicted and reported as a miss. + */ + get(key: K): V | undefined { + const entry = this.store.get(key); + + if (!entry) { + this.emit({ type: "miss", key }); + return undefined; + } + + if (this.isExpired(entry)) { + this.store.delete(key); + this.emit({ type: "expire", key, reason: "ttl" }); + this.emit({ type: "miss", key }); + return undefined; + } + + this.emit({ type: "hit", key }); + return entry.value; + } + + /** + * Whether a non-expired entry exists for `key`. + */ + has(key: K): boolean { + const entry = this.store.get(key); + if (!entry) { + return false; + } + if (this.isExpired(entry)) { + this.store.delete(key); + this.emit({ type: "expire", key, reason: "ttl" }); + return false; + } + return true; + } + + /** + * Remove a single entry. Returns `true` when an entry was removed. + */ + invalidate(key: K): boolean { + const existed = this.store.delete(key); + if (existed) { + this.emit({ type: "invalidate", key, reason: "explicit" }); + } + return existed; + } + + /** + * Remove every entry, emitting an invalidation event per removed key. + */ + invalidateAll(): void { + for (const key of Array.from(this.store.keys())) { + this.store.delete(key); + this.emit({ type: "invalidate", key, reason: "explicit" }); + } + } + + /** + * Remove all expired entries. Returns the number of evicted entries. + */ + prune(): number { + let evicted = 0; + for (const [key, entry] of Array.from(this.store.entries())) { + if (this.isExpired(entry)) { + this.store.delete(key); + this.emit({ type: "expire", key, reason: "ttl" }); + evicted += 1; + } + } + return evicted; + } + + /** + * Current number of stored entries (including not-yet-pruned expired ones). + */ + get size(): number { + return this.store.size; + } + + /** + * Remove all entries without emitting per-key events. + */ + clear(): void { + this.store.clear(); + } + + private isExpired(entry: CacheEntry): boolean { + return entry.expiresAt !== Infinity && entry.expiresAt <= this.now(); + } + + private emit(event: CacheEvent): void { + for (const listener of Array.from(this.listeners)) { + listener(event); + } + } +} -export { - optimizeFootprint, - simulateFootprint, - clearFootprintSimulationCache, -} from "./soroban/footprint.js"; -export type { - OptimizeFootprintOptions, - FootprintLogger, -} from "./soroban/footprint.js"; -export { footprintDiff } from "./utils/footprintDiff.js"; -export type { FootprintDiff } from "./utils/footprintDiff.js"; -export { submitTransaction } from "./transaction/submit.js"; -export type { - SubmitTransactionOptions, - SubmitServer, -} from "./transaction/submit.js"; +export default SdkCache;