@gvnrdao/dh-sdk 0.0.293 → 0.0.294

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -185,19 +185,11 @@ export declare class BitcoinOperations {
185
185
  /**
186
186
  * Get balance cache statistics
187
187
  */
188
- getBalanceCacheStats(): {
189
- size: number;
190
- maxSize: number;
191
- ttlMs: number;
192
- } | null;
188
+ getBalanceCacheStats(): import("../cache/cache-manager.module").CacheStats | null;
193
189
  /**
194
190
  * Get address cache statistics
195
191
  */
196
- getAddressCacheStats(): {
197
- size: number;
198
- maxSize: number;
199
- ttlMs: number;
200
- } | null;
192
+ getAddressCacheStats(): import("../cache/cache-manager.module").CacheStats | null;
201
193
  /**
202
194
  * Get current network configuration
203
195
  */
@@ -1,74 +1,236 @@
1
1
  /**
2
- * Cache Manager Module
2
+ * Generic Cache Manager Module
3
3
  *
4
- * Provides a simple cache factory for SDK modules.
5
- * Each cache is an independent LRU cache with TTL support.
4
+ * Provides a type-safe, generic LRU cache with TTL support.
5
+ * Can be used for any data type (Bitcoin balances, addresses, query results, etc.)
6
+ *
7
+ * Features:
8
+ * - Generic type support
9
+ * - TTL-based expiration
10
+ * - LRU eviction policy
11
+ * - Hit/miss statistics
12
+ * - Memory-efficient
13
+ * - Thread-safe operations
6
14
  */
7
- export interface CacheConfig {
8
- maxSize: number;
9
- ttlMs: number;
15
+ import { Result } from '../../types/result';
16
+ import { SDKError } from '../../utils/error-handler';
17
+ /**
18
+ * Cache entry with metadata
19
+ */
20
+ interface CacheEntry<T> {
21
+ /** Cached value */
22
+ value: T;
23
+ /** Unix timestamp when entry was created (ms) */
24
+ timestamp: number;
25
+ /** Number of times this entry was accessed */
26
+ hits: number;
27
+ /** Unix timestamp of last access (ms) */
28
+ lastAccessed: number;
10
29
  }
30
+ /**
31
+ * Cache statistics
32
+ */
11
33
  export interface CacheStats {
34
+ /** Current number of entries in cache */
12
35
  size: number;
13
- maxSize: number;
14
- ttlMs: number;
36
+ /** Total cache hits */
37
+ hits: number;
38
+ /** Total cache misses */
39
+ misses: number;
40
+ /** Total evictions performed */
41
+ evictions: number;
42
+ /** Timestamp of oldest entry (ms) */
43
+ oldestEntry: number;
44
+ /** Timestamp of newest entry (ms) */
45
+ newestEntry: number;
46
+ /** Hit rate as percentage (0-100) */
47
+ hitRate: number;
15
48
  }
16
49
  /**
17
- * Simple LRU Cache with TTL support
50
+ * Cache configuration
51
+ */
52
+ export interface CacheConfig {
53
+ /** Maximum number of entries (default: 1000) */
54
+ maxSize?: number;
55
+ /** Time-to-live in milliseconds (default: 60000 = 1 minute) */
56
+ ttlMs?: number;
57
+ /** Enable debug logging (default: false) */
58
+ debug?: boolean;
59
+ /** Cache name for logging (default: 'Cache') */
60
+ name?: string;
61
+ }
62
+ /**
63
+ * Generic LRU Cache with TTL support
64
+ *
65
+ * @template K - Key type (usually string)
66
+ * @template V - Value type
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * // Create a cache for Bitcoin balances
71
+ * const balanceCache = new LRUCache<string, BitcoinBalance>({
72
+ * maxSize: 500,
73
+ * ttlMs: 60000,
74
+ * name: 'BitcoinBalance'
75
+ * });
76
+ *
77
+ * // Set value
78
+ * balanceCache.set('bc1q...', { balance: Satoshis(50000000n) });
79
+ *
80
+ * // Get value
81
+ * const balance = balanceCache.get('bc1q...');
82
+ * if (balance) {
83
+ * console.log('Cached balance:', balance.balance);
84
+ * }
85
+ * ```
18
86
  */
19
- export declare class Cache<T = any> {
87
+ export declare class LRUCache<K, V> {
20
88
  private cache;
89
+ /**
90
+ * Audit M-J: singleflight registry for `getOrCompute` / `getOrComputeResult`.
91
+ * Concurrent cache-miss callers for the same key share one inflight promise
92
+ * instead of each running `compute()` independently — important when the
93
+ * compute spends a paid LIT capacity credit or hits a rate-limited upstream.
94
+ */
95
+ private inflight;
96
+ private inflightResult;
21
97
  private readonly maxSize;
22
98
  private readonly ttlMs;
23
- constructor(config: CacheConfig);
99
+ private readonly debug;
100
+ private readonly name;
101
+ private stats;
102
+ constructor(config?: CacheConfig);
24
103
  /**
25
104
  * Get value from cache
105
+ *
106
+ * Returns null if:
107
+ * - Key not found
108
+ * - Entry has expired
109
+ *
110
+ * @param key - Cache key
111
+ * @returns Cached value or null
26
112
  */
27
- get(key: string): T | undefined;
113
+ get(key: K): V | null;
114
+ /**
115
+ * Get value from cache with Result wrapper
116
+ *
117
+ * Useful when you want to distinguish between "not found" and "expired"
118
+ */
119
+ getResult(key: K): Result<V, SDKError>;
28
120
  /**
29
121
  * Set value in cache
122
+ *
123
+ * If cache is full, evicts the least recently used entry
124
+ *
125
+ * @param key - Cache key
126
+ * @param value - Value to cache
127
+ * @param ttl - Optional custom TTL for this entry (ms)
30
128
  */
31
- set(key: string, value: T, ttl?: number): void;
129
+ set(key: K, value: V, ttl?: number): void;
32
130
  /**
33
- * Check if key exists in cache
131
+ * Set value in cache with Result wrapper
34
132
  */
35
- has(key: string): boolean;
133
+ setResult(key: K, value: V, ttl?: number): Result<void, SDKError>;
36
134
  /**
37
- * Delete key from cache
135
+ * Check if key exists in cache (without affecting stats)
38
136
  */
39
- delete(key: string): boolean;
137
+ has(key: K): boolean;
40
138
  /**
41
- * Clear all cache entries
139
+ * Delete specific key from cache
140
+ */
141
+ delete(key: K): boolean;
142
+ /**
143
+ * Clear entire cache
42
144
  */
43
145
  clear(): void;
44
146
  /**
45
- * Clean expired entries
147
+ * Get current cache size
46
148
  */
47
- cleanExpired(): number;
149
+ size(): number;
48
150
  /**
49
151
  * Get cache statistics
50
152
  */
51
- getStats(): {
52
- size: number;
53
- maxSize: number;
54
- ttlMs: number;
55
- };
153
+ getStats(): CacheStats;
154
+ /**
155
+ * Get hit rate percentage
156
+ */
157
+ getHitRate(): number;
158
+ /**
159
+ * Get all cached keys (for debugging)
160
+ */
161
+ getKeys(): K[];
162
+ /**
163
+ * Get all cached values (for debugging)
164
+ */
165
+ getValues(): V[];
166
+ /**
167
+ * Get all cache entries with metadata (for debugging)
168
+ */
169
+ getEntries(): Array<{
170
+ key: K;
171
+ value: V;
172
+ metadata: Omit<CacheEntry<V>, 'value'>;
173
+ }>;
174
+ /**
175
+ * Clean up expired entries
176
+ *
177
+ * Useful for periodic maintenance
178
+ *
179
+ * @returns Number of entries cleaned
180
+ */
181
+ cleanExpired(): number;
182
+ /**
183
+ * Check if cache entry is expired
184
+ */
185
+ private isExpired;
186
+ /**
187
+ * Evict least recently used entry
188
+ */
189
+ private evictLRU;
190
+ /**
191
+ * Get or compute value
192
+ *
193
+ * If key exists in cache, returns cached value.
194
+ * Otherwise, computes value using provided function and caches it.
195
+ *
196
+ * @param key - Cache key
197
+ * @param compute - Function to compute value if not in cache
198
+ * @param ttl - Optional custom TTL for this entry
199
+ * @returns Cached or computed value
200
+ */
201
+ getOrCompute(key: K, compute: () => Promise<V>, ttl?: number): Promise<V>;
202
+ /**
203
+ * Get or compute value with Result wrapper
204
+ */
205
+ getOrComputeResult(key: K, compute: () => Promise<Result<V, SDKError>>, ttl?: number): Promise<Result<V, SDKError>>;
56
206
  }
57
207
  /**
58
- * Cache Manager
59
- *
60
- * Factory for creating named caches with specific configurations
208
+ * Generic cache interface for type safety
209
+ */
210
+ export interface Cache<T> {
211
+ get(key: string): T | null | undefined;
212
+ set(key: string, value: T): void;
213
+ delete(key: string): boolean;
214
+ clear(): void;
215
+ has(key: string): boolean;
216
+ size(): number;
217
+ getStats(): CacheStats;
218
+ }
219
+ /**
220
+ * Cache Manager - Factory for creating specialized caches
61
221
  */
62
222
  export declare class CacheManager {
63
223
  private caches;
64
- private readonly debug;
65
- constructor(config?: {
66
- debug?: boolean;
67
- });
224
+ private readonly globalConfig;
225
+ constructor(globalConfig?: CacheConfig);
68
226
  /**
69
- * Get or create a cache instance
227
+ * Create or get a named cache
228
+ *
229
+ * @param name - Unique cache name
230
+ * @param config - Optional cache-specific configuration
231
+ * @returns LRU cache instance
70
232
  */
71
- getCache<K extends string = string, V = any>(name: K, config: CacheConfig): Cache<V>;
233
+ getCache<K, V>(name: string, config?: CacheConfig): LRUCache<K, V>;
72
234
  /**
73
235
  * Clear all caches
74
236
  */
@@ -80,13 +242,18 @@ export declare class CacheManager {
80
242
  /**
81
243
  * Get statistics for all caches
82
244
  */
83
- getAllStats(): Record<string, ReturnType<Cache["getStats"]>>;
245
+ getAllStats(): Record<string, CacheStats>;
246
+ /**
247
+ * Get list of all cache names
248
+ */
249
+ getCacheNames(): string[];
84
250
  /**
85
- * Destroy cache manager
251
+ * Delete a named cache
86
252
  */
87
- destroy(): void;
253
+ deleteCache(name: string): boolean;
88
254
  }
89
- export { Cache as LRUCache };
90
- export declare function createCacheManager(config?: {
91
- debug?: boolean;
92
- }): CacheManager;
255
+ /**
256
+ * Factory function to create a CacheManager instance
257
+ */
258
+ export declare function createCacheManager(config?: CacheConfig): CacheManager;
259
+ export {};
@@ -373,6 +373,25 @@ export declare class DiamondHandsSDK {
373
373
  * @returns Payment result with transaction details
374
374
  */
375
375
  makePayment(request: PartialPaymentRequest): Promise<PartialPaymentResult>;
376
+ /**
377
+ * Whether a failed payment attempt is a quantum-timing failure that a fresh
378
+ * re-sign can fix (safe to retry), versus a terminal failure.
379
+ *
380
+ * Retryable: pre-send simulation quantum errors, or an atomic mined revert
381
+ * (status 0) — no funds moved and the quantum was not recorded on-chain, so a
382
+ * fresh signature + resubmit is safe.
383
+ *
384
+ * NOT retryable: a confirmation timeout — the original tx may still be pending, so
385
+ * a resubmit could double-pay. It is surfaced (with its tx hash) instead.
386
+ */
387
+ private isRetryablePaymentQuantumFailure;
388
+ /**
389
+ * One payment attempt: user auth → Lit Action authorization → dead-zone gate →
390
+ * pre-send simulation → broadcast → confirmation. Always resolves to a
391
+ * PartialPaymentResult (never throws to the caller); the makePayment wrapper owns
392
+ * the write lock, the pause pre-check, and the bounded re-sign retry loop.
393
+ */
394
+ private _makePaymentAttempt;
376
395
  /**
377
396
  * Access the approved-withdrawal-address allowlist module (self-service
378
397
  * add/remove + reads). The connected wallet manages its OWN addresses; a
@@ -847,11 +866,7 @@ export declare class DiamondHandsSDK {
847
866
  /**
848
867
  * Get cache statistics
849
868
  */
850
- getCacheStats(): Record<string, {
851
- size: number;
852
- maxSize: number;
853
- ttlMs: number;
854
- }>;
869
+ getCacheStats(): Record<string, import("./cache/cache-manager.module").CacheStats>;
855
870
  /**
856
871
  * Get contract manager (for advanced usage)
857
872
  */
@@ -196,11 +196,7 @@ export declare class LoanQuery {
196
196
  /**
197
197
  * Get cache statistics
198
198
  */
199
- getCacheStats(): {
200
- size: number;
201
- maxSize: number;
202
- ttlMs: number;
203
- } | null;
199
+ getCacheStats(): import("../cache/cache-manager.module").CacheStats | null;
204
200
  }
205
201
  /**
206
202
  * Factory function to create a LoanQuery instance
@@ -128,11 +128,7 @@ export declare class PKPManager {
128
128
  /**
129
129
  * Get cache statistics
130
130
  */
131
- getCacheStats(): {
132
- size: number;
133
- maxSize: number;
134
- ttlMs: number;
135
- } | null;
131
+ getCacheStats(): import("../cache/cache-manager.module").CacheStats | null;
136
132
  }
137
133
  /**
138
134
  * Factory function to create a PKPManager instance
@@ -7,14 +7,40 @@
7
7
  * - SDK signs for NEXT quantum immediately (no waiting)
8
8
  * - 60-second quantum windows
9
9
  * - 3-quantum validation (past, current, next) = 180s total validity
10
- * - Dead zones: first 8s and last 8s of each quantum
11
10
  * - Users can operate once per 60 seconds per position
12
11
  *
12
+ * DEAD ZONE — the subtle part (see OperationAuthorizationRegistry._validateQuantum):
13
+ * The 3-quantum window widens *acceptance*, but it does NOT make dead zones
14
+ * irrelevant. A signature whose quantum is NOT the on-chain CURRENT quantum
15
+ * (i.e. a NEXT or PAST signature) is REJECTED with `DeadZoneViolation()` when the
16
+ * transaction is mined in the last DEAD_ZONE_SECONDS of the current quantum
17
+ * (`block.timestamp >= currentQuantum + 60 - 8`). A CURRENT-quantum signature is
18
+ * never dead-zoned. Because the SDK signs NEXT and submits immediately, a send that
19
+ * lands in that trailing window reverts — so the submission path MUST gate on it via
20
+ * `awaitSafeSubmissionWindow` (defer across the boundary, at which point the signed
21
+ * NEXT quantum becomes CURRENT and can never be dead-zoned).
22
+ *
13
23
  * Architecture:
14
24
  * - SDK uses this to calculate NEXT quantum immediately
25
+ * - SDK gates every on-chain submission through `awaitSafeSubmissionWindow`
15
26
  * - Tests use this to generate valid timestamps
16
27
  * - LIT Actions validate 3-quantum window with dead zone protection
17
28
  */
29
+ export declare const QUANTUM_WINDOW_SECONDS = 60;
30
+ export declare const DEAD_ZONE_SECONDS = 8;
31
+ /**
32
+ * Seconds of mainnet inclusion latency we insure against BEFORE the quantum
33
+ * boundary. ~1.3 of Ethereum's 12s slots. A NEXT/PAST-quantum send within
34
+ * `DEAD_ZONE_SECONDS + INCLUSION_LATENCY_BUDGET` of the boundary is deferred so it
35
+ * cannot be mined in the trailing dead zone `[boundary - 8, boundary)`.
36
+ */
37
+ export declare const INCLUSION_LATENCY_BUDGET = 16;
38
+ /**
39
+ * Seconds of client-clock-vs-chain skew guard we add AFTER the boundary when
40
+ * deferring. A CURRENT-quantum signature is never dead-zoned, so this only needs to
41
+ * cover modest clock skew — small on top of the boundary crossing.
42
+ */
43
+ export declare const POST_BOUNDARY_SKEW_MARGIN = 3;
18
44
  /**
19
45
  * Calculate a valid quantum timestamp for NEXT quantum (NO WAITING)
20
46
  *
@@ -73,3 +99,44 @@ export declare function waitUntilTimestamp(targetTimestamp: number): Promise<voi
73
99
  * @throws Error if timestamp is invalid
74
100
  */
75
101
  export declare function validateQuantumTiming(signedTimestamp: number, _bufferSeconds?: number): void;
102
+ /**
103
+ * Injectable clock/sleep seam for `awaitSafeSubmissionWindow` so unit tests can
104
+ * drive the decision deterministically. Defaults use the real clock and timer.
105
+ */
106
+ export interface SafeSubmissionOptions {
107
+ /** Returns the current time in whole seconds. Defaults to `Date.now()/1000`. */
108
+ now?: () => number;
109
+ /** Sleeps for `ms` milliseconds. Defaults to `setTimeout`. */
110
+ sleep?: (ms: number) => Promise<void>;
111
+ }
112
+ export interface SafeSubmissionResult {
113
+ /** Whether the send was deferred across the quantum boundary. */
114
+ waited: boolean;
115
+ /** Seconds slept (0 when not deferred). */
116
+ waitedSeconds: number;
117
+ }
118
+ /**
119
+ * Submission-time dead-zone gate.
120
+ *
121
+ * Call this IMMEDIATELY before broadcasting any on-chain operation that carries a
122
+ * quantum-signed authorization (makePayment / mintUCD / withdrawBTC / extendPosition).
123
+ *
124
+ * Mirrors the on-chain `_validateQuantum` predicate exactly (do NOT substitute
125
+ * `isInSafeWorkZone`/`isInDeadZone`, which disagree with enforcement at second 52):
126
+ * the contract reverts `DeadZoneViolation()` iff the signature's quantum is not the
127
+ * CURRENT quantum AND `block.timestamp >= currentQuantum + 60 - DEAD_ZONE_SECONDS`.
128
+ *
129
+ * Strategy: the SDK signs the NEXT quantum. If we are close enough to the trailing
130
+ * boundary that realistic inclusion latency could land the mine in
131
+ * `[boundary - 8, boundary)`, wait until just past the boundary. After the boundary
132
+ * the signed NEXT quantum IS the new CURRENT quantum, so the dead-zone branch can
133
+ * never be entered — regardless of inclusion latency.
134
+ *
135
+ * PAST-quantum signatures are NOT deferred: waiting forward would age them out of the
136
+ * acceptance window (`QuantumOutsideWindow`). They must be handled by re-signing
137
+ * upstream. In practice the SDK only ever signs CURRENT/NEXT.
138
+ *
139
+ * @param quantumTimestamp The quantum timestamp embedded in the LIT-signed payload.
140
+ * @returns whether it waited, and for how long.
141
+ */
142
+ export declare function awaitSafeSubmissionWindow(quantumTimestamp: number, opts?: SafeSubmissionOptions): Promise<SafeSubmissionResult>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gvnrdao/dh-sdk",
3
- "version": "0.0.293",
3
+ "version": "0.0.294",
4
4
  "description": "TypeScript SDK for Diamond Hands Protocol - Bitcoin-backed lending with LIT Protocol PKPs",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -82,7 +82,7 @@
82
82
  "sideEffects": false,
83
83
  "dependencies": {
84
84
  "@gvnrdao/dh-lit-actions": "^0.0.311",
85
- "@gvnrdao/dh-lit-ops": "^0.0.299",
85
+ "@gvnrdao/dh-lit-ops": "^0.0.301",
86
86
  "@noble/hashes": "^1.5.0",
87
87
  "axios": "^1.17.0",
88
88
  "bech32": "^2.0.0",