@sip-protocol/sdk 0.7.2 → 0.7.4

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.
Files changed (262) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +267 -0
  3. package/dist/{TransportWebUSB-TQ7WZ4LE.mjs → TransportWebUSB-YQMAGJAJ.mjs} +12 -9
  4. package/dist/browser.d.mts +10 -4
  5. package/dist/browser.d.ts +10 -4
  6. package/dist/browser.js +48874 -18336
  7. package/dist/browser.mjs +674 -48
  8. package/dist/chunk-4GRJ5MAW.mjs +152 -0
  9. package/dist/chunk-5D7A3L3W.mjs +717 -0
  10. package/dist/chunk-64AYA5F5.mjs +7834 -0
  11. package/dist/chunk-GMDGB22A.mjs +379 -0
  12. package/dist/chunk-I534WKN7.mjs +328 -0
  13. package/dist/chunk-IBZVA5Y7.mjs +1003 -0
  14. package/dist/chunk-PRRZAWJE.mjs +223 -0
  15. package/dist/{chunk-UJCSKKID.mjs → chunk-XGB3TDIC.mjs} +13 -1
  16. package/dist/chunk-YWGJ77A2.mjs +33806 -0
  17. package/dist/{chunk-6WGN57S2.mjs → chunk-Z3K7W5S3.mjs} +48 -0
  18. package/dist/constants-LHAAUC2T.mjs +51 -0
  19. package/dist/dist-2OGQ7FED.mjs +3957 -0
  20. package/dist/dist-IFHPYLDX.mjs +254 -0
  21. package/dist/fulfillment_proof-ANHVPKTB.mjs +21 -0
  22. package/dist/funding_proof-ICFZ5LHY.mjs +21 -0
  23. package/dist/index-DXh2IGkz.d.ts +24681 -0
  24. package/dist/index-DeE1ZzA4.d.mts +24681 -0
  25. package/dist/index.d.mts +9 -3
  26. package/dist/index.d.ts +9 -3
  27. package/dist/index.js +48676 -17318
  28. package/dist/index.mjs +583 -19
  29. package/dist/interface-Bf7w1PLW.d.mts +679 -0
  30. package/dist/interface-Bf7w1PLW.d.ts +679 -0
  31. package/dist/{noir-DKfEzWy9.d.mts → noir-kzbLVTei.d.mts} +31 -21
  32. package/dist/{noir-DKfEzWy9.d.ts → noir-kzbLVTei.d.ts} +31 -21
  33. package/dist/proofs/halo2.d.mts +151 -0
  34. package/dist/proofs/halo2.d.ts +151 -0
  35. package/dist/proofs/halo2.js +350 -0
  36. package/dist/proofs/halo2.mjs +11 -0
  37. package/dist/proofs/kimchi.d.mts +160 -0
  38. package/dist/proofs/kimchi.d.ts +160 -0
  39. package/dist/proofs/kimchi.js +431 -0
  40. package/dist/proofs/kimchi.mjs +13 -0
  41. package/dist/proofs/noir.d.mts +1 -1
  42. package/dist/proofs/noir.d.ts +1 -1
  43. package/dist/proofs/noir.js +74 -18
  44. package/dist/proofs/noir.mjs +84 -24
  45. package/dist/solana-U3MEGU7W.mjs +280 -0
  46. package/dist/validity_proof-3POXLPNY.mjs +21 -0
  47. package/package.json +54 -21
  48. package/src/adapters/index.ts +41 -0
  49. package/src/adapters/jupiter.ts +571 -0
  50. package/src/adapters/near-intents.ts +135 -0
  51. package/src/advisor/advisor.ts +653 -0
  52. package/src/advisor/index.ts +54 -0
  53. package/src/advisor/tools.ts +303 -0
  54. package/src/advisor/types.ts +164 -0
  55. package/src/chains/ethereum/announcement.ts +536 -0
  56. package/src/chains/ethereum/bnb-optimizations.ts +474 -0
  57. package/src/chains/ethereum/commitment.ts +522 -0
  58. package/src/chains/ethereum/constants.ts +462 -0
  59. package/src/chains/ethereum/deployment.ts +596 -0
  60. package/src/chains/ethereum/gas-estimation.ts +538 -0
  61. package/src/chains/ethereum/index.ts +268 -0
  62. package/src/chains/ethereum/optimizations.ts +614 -0
  63. package/src/chains/ethereum/privacy-adapter.ts +855 -0
  64. package/src/chains/ethereum/registry.ts +584 -0
  65. package/src/chains/ethereum/rpc.ts +905 -0
  66. package/src/chains/ethereum/stealth.ts +491 -0
  67. package/src/chains/ethereum/token.ts +790 -0
  68. package/src/chains/ethereum/transfer.ts +637 -0
  69. package/src/chains/ethereum/types.ts +456 -0
  70. package/src/chains/ethereum/viewing-key.ts +455 -0
  71. package/src/chains/near/commitment.ts +608 -0
  72. package/src/chains/near/constants.ts +284 -0
  73. package/src/chains/near/function-call.ts +871 -0
  74. package/src/chains/near/history.ts +654 -0
  75. package/src/chains/near/implicit-account.ts +840 -0
  76. package/src/chains/near/index.ts +393 -0
  77. package/src/chains/near/native-transfer.ts +658 -0
  78. package/src/chains/near/nep141.ts +775 -0
  79. package/src/chains/near/privacy-adapter.ts +889 -0
  80. package/src/chains/near/resolver.ts +971 -0
  81. package/src/chains/near/rpc.ts +1016 -0
  82. package/src/chains/near/stealth.ts +419 -0
  83. package/src/chains/near/types.ts +317 -0
  84. package/src/chains/near/viewing-key.ts +876 -0
  85. package/src/chains/solana/anchor-transfer.ts +386 -0
  86. package/src/chains/solana/commitment.ts +577 -0
  87. package/src/chains/solana/constants.ts +126 -12
  88. package/src/chains/solana/ephemeral-keys.ts +543 -0
  89. package/src/chains/solana/index.ts +276 -1
  90. package/src/chains/solana/key-derivation.ts +418 -0
  91. package/src/chains/solana/kit-compat.ts +334 -0
  92. package/src/chains/solana/optimizations.ts +560 -0
  93. package/src/chains/solana/privacy-adapter.ts +605 -0
  94. package/src/chains/solana/providers/generic.ts +201 -0
  95. package/src/chains/solana/providers/helius-enhanced-types.ts +336 -0
  96. package/src/chains/solana/providers/helius-enhanced.ts +623 -0
  97. package/src/chains/solana/providers/helius.ts +402 -0
  98. package/src/chains/solana/providers/index.ts +85 -0
  99. package/src/chains/solana/providers/interface.ts +221 -0
  100. package/src/chains/solana/providers/quicknode.ts +409 -0
  101. package/src/chains/solana/providers/triton.ts +426 -0
  102. package/src/chains/solana/providers/webhook.ts +790 -0
  103. package/src/chains/solana/rpc-client.ts +1150 -0
  104. package/src/chains/solana/scan.ts +170 -73
  105. package/src/chains/solana/sol-transfer.ts +732 -0
  106. package/src/chains/solana/spl-transfer.ts +886 -0
  107. package/src/chains/solana/stealth-scanner.ts +703 -0
  108. package/src/chains/solana/sunspot-verifier.ts +453 -0
  109. package/src/chains/solana/transaction-builder.ts +755 -0
  110. package/src/chains/solana/transfer.ts +74 -5
  111. package/src/chains/solana/types.ts +77 -7
  112. package/src/chains/solana/utils.ts +110 -0
  113. package/src/chains/solana/viewing-key.ts +807 -0
  114. package/src/compliance/fireblocks.ts +921 -0
  115. package/src/compliance/index.ts +37 -0
  116. package/src/compliance/range-sas.ts +956 -0
  117. package/src/config/endpoints.ts +100 -0
  118. package/src/crypto.ts +11 -8
  119. package/src/errors.ts +82 -0
  120. package/src/evm/erc4337-relayer.ts +830 -0
  121. package/src/evm/index.ts +47 -0
  122. package/src/fees/calculator.ts +396 -0
  123. package/src/fees/index.ts +87 -0
  124. package/src/fees/near-contract.ts +429 -0
  125. package/src/fees/types.ts +268 -0
  126. package/src/index.ts +785 -1
  127. package/src/intent.ts +6 -3
  128. package/src/logger.ts +324 -0
  129. package/src/network/index.ts +80 -0
  130. package/src/network/proxy.ts +691 -0
  131. package/src/optimizations/index.ts +541 -0
  132. package/src/oracle/types.ts +1 -0
  133. package/src/privacy-backends/arcium-types.ts +727 -0
  134. package/src/privacy-backends/arcium.ts +719 -0
  135. package/src/privacy-backends/combined-privacy.ts +866 -0
  136. package/src/privacy-backends/cspl-token.ts +595 -0
  137. package/src/privacy-backends/cspl-types.ts +512 -0
  138. package/src/privacy-backends/cspl.ts +907 -0
  139. package/src/privacy-backends/health.ts +488 -0
  140. package/src/privacy-backends/inco-types.ts +323 -0
  141. package/src/privacy-backends/inco.ts +616 -0
  142. package/src/privacy-backends/index.ts +336 -0
  143. package/src/privacy-backends/interface.ts +906 -0
  144. package/src/privacy-backends/lru-cache.ts +343 -0
  145. package/src/privacy-backends/magicblock.ts +458 -0
  146. package/src/privacy-backends/mock.ts +258 -0
  147. package/src/privacy-backends/privacycash-types.ts +278 -0
  148. package/src/privacy-backends/privacycash.ts +456 -0
  149. package/src/privacy-backends/private-swap.ts +570 -0
  150. package/src/privacy-backends/rate-limiter.ts +683 -0
  151. package/src/privacy-backends/registry.ts +690 -0
  152. package/src/privacy-backends/router.ts +626 -0
  153. package/src/privacy-backends/shadowwire.ts +449 -0
  154. package/src/privacy-backends/sip-native.ts +256 -0
  155. package/src/privacy-logger.ts +191 -0
  156. package/src/production-safety.ts +373 -0
  157. package/src/proofs/aggregator.ts +1029 -0
  158. package/src/proofs/browser-composer.ts +1150 -0
  159. package/src/proofs/browser.ts +113 -25
  160. package/src/proofs/cache/index.ts +127 -0
  161. package/src/proofs/cache/interface.ts +545 -0
  162. package/src/proofs/cache/key-generator.ts +188 -0
  163. package/src/proofs/cache/lru-cache.ts +481 -0
  164. package/src/proofs/cache/multi-tier-cache.ts +575 -0
  165. package/src/proofs/cache/persistent-cache.ts +788 -0
  166. package/src/proofs/compliance-proof.ts +872 -0
  167. package/src/proofs/composer/base.ts +923 -0
  168. package/src/proofs/composer/index.ts +25 -0
  169. package/src/proofs/composer/interface.ts +518 -0
  170. package/src/proofs/composer/types.ts +383 -0
  171. package/src/proofs/converters/halo2.ts +452 -0
  172. package/src/proofs/converters/index.ts +208 -0
  173. package/src/proofs/converters/interface.ts +363 -0
  174. package/src/proofs/converters/kimchi.ts +462 -0
  175. package/src/proofs/converters/noir.ts +451 -0
  176. package/src/proofs/fallback.ts +888 -0
  177. package/src/proofs/halo2.ts +42 -0
  178. package/src/proofs/index.ts +471 -0
  179. package/src/proofs/interface.ts +13 -0
  180. package/src/proofs/kimchi.ts +42 -0
  181. package/src/proofs/lazy.ts +1004 -0
  182. package/src/proofs/mock.ts +25 -1
  183. package/src/proofs/noir.ts +111 -30
  184. package/src/proofs/orchestrator.ts +960 -0
  185. package/src/proofs/parallel/concurrency.ts +297 -0
  186. package/src/proofs/parallel/dependency-graph.ts +602 -0
  187. package/src/proofs/parallel/executor.ts +420 -0
  188. package/src/proofs/parallel/index.ts +131 -0
  189. package/src/proofs/parallel/interface.ts +685 -0
  190. package/src/proofs/parallel/worker-pool.ts +644 -0
  191. package/src/proofs/providers/halo2.ts +560 -0
  192. package/src/proofs/providers/index.ts +34 -0
  193. package/src/proofs/providers/kimchi.ts +641 -0
  194. package/src/proofs/validator.ts +881 -0
  195. package/src/proofs/verifier.ts +867 -0
  196. package/src/quantum/index.ts +112 -0
  197. package/src/quantum/winternitz-vault.ts +639 -0
  198. package/src/quantum/wots.ts +611 -0
  199. package/src/settlement/backends/direct-chain.ts +1 -0
  200. package/src/settlement/index.ts +9 -0
  201. package/src/settlement/router.ts +732 -46
  202. package/src/solana/index.ts +72 -0
  203. package/src/solana/jito-relayer.ts +687 -0
  204. package/src/solana/noir-verifier-types.ts +430 -0
  205. package/src/solana/noir-verifier.ts +816 -0
  206. package/src/stealth/address-derivation.ts +193 -0
  207. package/src/stealth/ed25519.ts +431 -0
  208. package/src/stealth/index.ts +233 -0
  209. package/src/stealth/meta-address.ts +221 -0
  210. package/src/stealth/secp256k1.ts +368 -0
  211. package/src/stealth/utils.ts +194 -0
  212. package/src/stealth.ts +50 -1504
  213. package/src/surveillance/algorithms/address-reuse.ts +143 -0
  214. package/src/surveillance/algorithms/cluster.ts +247 -0
  215. package/src/surveillance/algorithms/exchange.ts +295 -0
  216. package/src/surveillance/algorithms/temporal.ts +337 -0
  217. package/src/surveillance/analyzer.ts +442 -0
  218. package/src/surveillance/index.ts +64 -0
  219. package/src/surveillance/scoring.ts +372 -0
  220. package/src/surveillance/types.ts +264 -0
  221. package/src/sync/index.ts +106 -0
  222. package/src/sync/manager.ts +504 -0
  223. package/src/sync/mock-provider.ts +318 -0
  224. package/src/sync/oblivious.ts +625 -0
  225. package/src/tokens/index.ts +15 -0
  226. package/src/tokens/registry.ts +301 -0
  227. package/src/utils/deprecation.ts +94 -0
  228. package/src/utils/index.ts +9 -0
  229. package/src/wallet/ethereum/index.ts +68 -0
  230. package/src/wallet/ethereum/metamask-privacy.ts +420 -0
  231. package/src/wallet/ethereum/multi-wallet.ts +646 -0
  232. package/src/wallet/ethereum/privacy-adapter.ts +700 -0
  233. package/src/wallet/ethereum/types.ts +3 -1
  234. package/src/wallet/ethereum/walletconnect-adapter.ts +675 -0
  235. package/src/wallet/hardware/index.ts +10 -0
  236. package/src/wallet/hardware/ledger-privacy.ts +414 -0
  237. package/src/wallet/index.ts +71 -0
  238. package/src/wallet/near/adapter.ts +626 -0
  239. package/src/wallet/near/index.ts +86 -0
  240. package/src/wallet/near/meteor-wallet.ts +1153 -0
  241. package/src/wallet/near/my-near-wallet.ts +790 -0
  242. package/src/wallet/near/wallet-selector.ts +702 -0
  243. package/src/wallet/solana/adapter.ts +6 -4
  244. package/src/wallet/solana/index.ts +13 -0
  245. package/src/wallet/solana/privacy-adapter.ts +567 -0
  246. package/src/wallet/sui/types.ts +6 -4
  247. package/src/zcash/rpc-client.ts +13 -6
  248. package/dist/chunk-3INS3PR5.mjs +0 -884
  249. package/dist/chunk-3OVABDRH.mjs +0 -17096
  250. package/dist/chunk-DLDWZFYC.mjs +0 -1495
  251. package/dist/chunk-E6SZWREQ.mjs +0 -57
  252. package/dist/chunk-G33LB27A.mjs +0 -16166
  253. package/dist/chunk-HGU6HZRC.mjs +0 -231
  254. package/dist/chunk-L2K34JCU.mjs +0 -1496
  255. package/dist/chunk-SN4ZDTVW.mjs +0 -16166
  256. package/dist/constants-VOI7BSLK.mjs +0 -27
  257. package/dist/index-BYZbDjal.d.ts +0 -11390
  258. package/dist/index-CHB3KuOB.d.mts +0 -11859
  259. package/dist/index-CzWPI6Le.d.ts +0 -11859
  260. package/dist/index-xbWjohNq.d.mts +0 -11390
  261. package/dist/solana-5EMCTPTS.mjs +0 -46
  262. package/dist/solana-Q4NAVBTS.mjs +0 -46
@@ -2,6 +2,11 @@
2
2
  * Smart Router for Optimal Route Selection
3
3
  *
4
4
  * Queries all compatible backends and finds the best route based on preferences.
5
+ * Features:
6
+ * - Quote caching with configurable TTL
7
+ * - Per-backend timeouts to prevent slow backends from blocking
8
+ * - Error isolation using Promise.allSettled
9
+ * - Backend failure tracking and logging
5
10
  *
6
11
  * @module settlement/router
7
12
  */
@@ -13,7 +18,486 @@ import type {
13
18
  Quote,
14
19
  } from './interface'
15
20
  import type { SettlementRegistry } from './registry'
16
- import { ValidationError, NetworkError } from '../errors'
21
+ import { ValidationError, NetworkError, ErrorCode } from '../errors'
22
+ import { createLogger } from '../logger'
23
+
24
+ const log = createLogger('settlement/router')
25
+
26
+ // ─── Quote Cache ──────────────────────────────────────────────────────────────
27
+
28
+ interface CacheEntry<T> {
29
+ value: T
30
+ expiresAt: number
31
+ }
32
+
33
+ /**
34
+ * Simple TTL cache for quote results
35
+ *
36
+ * Uses Map with expiration timestamps. Expired entries are cleaned on get/set.
37
+ */
38
+ class QuoteCache {
39
+ private cache = new Map<string, CacheEntry<Quote[]>>()
40
+ private readonly defaultTtlMs: number
41
+ private readonly maxSize: number
42
+
43
+ constructor(options?: { ttlMs?: number; maxSize?: number }) {
44
+ this.defaultTtlMs = options?.ttlMs ?? 30_000 // 30 seconds default
45
+ this.maxSize = options?.maxSize ?? 1000
46
+ }
47
+
48
+ /**
49
+ * Generate cache key from quote params
50
+ */
51
+ private getKey(params: QuoteParams): string {
52
+ return [
53
+ params.fromChain,
54
+ params.fromToken,
55
+ params.toChain,
56
+ params.toToken,
57
+ params.amount.toString(),
58
+ params.privacyLevel,
59
+ ].join(':')
60
+ }
61
+
62
+ /**
63
+ * Get cached quotes if not expired
64
+ */
65
+ get(params: QuoteParams): Quote[] | undefined {
66
+ const key = this.getKey(params)
67
+ const entry = this.cache.get(key)
68
+
69
+ if (!entry) return undefined
70
+
71
+ if (Date.now() > entry.expiresAt) {
72
+ this.cache.delete(key)
73
+ return undefined
74
+ }
75
+
76
+ return entry.value
77
+ }
78
+
79
+ /**
80
+ * Cache quotes with TTL
81
+ */
82
+ set(params: QuoteParams, quotes: Quote[], ttlMs?: number): void {
83
+ // Enforce max size by removing oldest entries
84
+ if (this.cache.size >= this.maxSize) {
85
+ const firstKey = this.cache.keys().next().value
86
+ if (firstKey) this.cache.delete(firstKey)
87
+ }
88
+
89
+ const key = this.getKey(params)
90
+ this.cache.set(key, {
91
+ value: quotes,
92
+ expiresAt: Date.now() + (ttlMs ?? this.defaultTtlMs),
93
+ })
94
+ }
95
+
96
+ /**
97
+ * Clear all cached entries
98
+ */
99
+ clear(): void {
100
+ this.cache.clear()
101
+ }
102
+
103
+ /**
104
+ * Get cache statistics
105
+ */
106
+ stats(): { size: number; maxSize: number } {
107
+ return { size: this.cache.size, maxSize: this.maxSize }
108
+ }
109
+ }
110
+
111
+ // ─── Circuit Breaker ─────────────────────────────────────────────────────────
112
+
113
+ /**
114
+ * Circuit breaker states following the standard pattern
115
+ *
116
+ * @see https://martinfowler.com/bliki/CircuitBreaker.html
117
+ *
118
+ * ```
119
+ * CLOSED --[failures exceed threshold]--> OPEN
120
+ * ^ |
121
+ * | v
122
+ * +------[success]------- HALF_OPEN <--[timeout]--+
123
+ * ```
124
+ */
125
+ export type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN'
126
+
127
+ /**
128
+ * Circuit breaker status for a single backend
129
+ */
130
+ export interface CircuitBreakerStatus {
131
+ /** Current circuit state */
132
+ state: CircuitState
133
+ /** Number of consecutive failures */
134
+ consecutiveFailures: number
135
+ /** Total failure count (lifetime) */
136
+ totalFailures: number
137
+ /** Total success count (lifetime) */
138
+ totalSuccesses: number
139
+ /** Timestamp of last failure */
140
+ lastFailureAt: number | null
141
+ /** Timestamp of last success */
142
+ lastSuccessAt: number | null
143
+ /** Timestamp when circuit opened */
144
+ openedAt: number | null
145
+ /** Last error message */
146
+ lastError: string | null
147
+ }
148
+
149
+ /**
150
+ * Event callbacks for circuit breaker state changes
151
+ */
152
+ export interface CircuitBreakerEvents {
153
+ /** Called when circuit transitions to OPEN */
154
+ onOpen?: (backend: string, status: CircuitBreakerStatus) => void
155
+ /** Called when circuit transitions to HALF_OPEN */
156
+ onHalfOpen?: (backend: string, status: CircuitBreakerStatus) => void
157
+ /** Called when circuit transitions to CLOSED */
158
+ onClose?: (backend: string, status: CircuitBreakerStatus) => void
159
+ /** Called on any state change */
160
+ onStateChange?: (backend: string, from: CircuitState, to: CircuitState, status: CircuitBreakerStatus) => void
161
+ }
162
+
163
+ /**
164
+ * Circuit breaker options
165
+ */
166
+ export interface CircuitBreakerOptions {
167
+ /** Number of consecutive failures before opening (default: 3) */
168
+ failureThreshold?: number
169
+ /** Time in ms before trying to recover from OPEN (default: 30000) */
170
+ resetTimeMs?: number
171
+ /** Number of successes needed to close from HALF_OPEN (default: 1) */
172
+ successThreshold?: number
173
+ /** Event callbacks */
174
+ events?: CircuitBreakerEvents
175
+ }
176
+
177
+ /**
178
+ * Internal status tracking
179
+ */
180
+ interface InternalCircuitStatus extends CircuitBreakerStatus {
181
+ /** Number of successes in HALF_OPEN state */
182
+ halfOpenSuccesses: number
183
+ }
184
+
185
+ /**
186
+ * Circuit Breaker for Settlement Backends
187
+ *
188
+ * Implements the circuit breaker pattern to prevent cascading failures:
189
+ * - CLOSED: Normal operation, requests pass through
190
+ * - OPEN: Backend is failing, requests are rejected immediately
191
+ * - HALF_OPEN: Testing if backend recovered, limited requests allowed
192
+ *
193
+ * @example
194
+ * ```typescript
195
+ * const breaker = new CircuitBreaker({
196
+ * failureThreshold: 3,
197
+ * resetTimeMs: 30_000,
198
+ * events: {
199
+ * onOpen: (backend) => console.log(`Circuit OPEN for ${backend}`),
200
+ * onClose: (backend) => console.log(`Circuit CLOSED for ${backend}`),
201
+ * }
202
+ * })
203
+ *
204
+ * // Check before making request
205
+ * if (breaker.canRequest('near-intents')) {
206
+ * try {
207
+ * const result = await backend.getQuote(params)
208
+ * breaker.recordSuccess('near-intents')
209
+ * } catch (e) {
210
+ * breaker.recordFailure('near-intents', e.message)
211
+ * }
212
+ * }
213
+ * ```
214
+ */
215
+ export class CircuitBreaker {
216
+ private statuses = new Map<string, InternalCircuitStatus>()
217
+ private readonly failureThreshold: number
218
+ private readonly resetTimeMs: number
219
+ private readonly successThreshold: number
220
+ private readonly events: CircuitBreakerEvents
221
+
222
+ constructor(options?: CircuitBreakerOptions) {
223
+ this.failureThreshold = options?.failureThreshold ?? 3
224
+ this.resetTimeMs = options?.resetTimeMs ?? 30_000
225
+ this.successThreshold = options?.successThreshold ?? 1
226
+ this.events = options?.events ?? {}
227
+ }
228
+
229
+ /**
230
+ * Get or create status for a backend
231
+ */
232
+ private getStatus(backend: string): InternalCircuitStatus {
233
+ let status = this.statuses.get(backend)
234
+ if (!status) {
235
+ status = {
236
+ state: 'CLOSED',
237
+ consecutiveFailures: 0,
238
+ totalFailures: 0,
239
+ totalSuccesses: 0,
240
+ lastFailureAt: null,
241
+ lastSuccessAt: null,
242
+ openedAt: null,
243
+ lastError: null,
244
+ halfOpenSuccesses: 0,
245
+ }
246
+ this.statuses.set(backend, status)
247
+ }
248
+ return status
249
+ }
250
+
251
+ /**
252
+ * Transition circuit state with event emission
253
+ */
254
+ private transition(backend: string, status: InternalCircuitStatus, newState: CircuitState): void {
255
+ const oldState = status.state
256
+ if (oldState === newState) return
257
+
258
+ status.state = newState
259
+
260
+ // Log state transition
261
+ log.info(
262
+ { backend, from: oldState, to: newState, failures: status.consecutiveFailures },
263
+ `Circuit breaker state change: ${oldState} -> ${newState}`
264
+ )
265
+
266
+ // Emit events
267
+ this.events.onStateChange?.(backend, oldState, newState, status)
268
+
269
+ switch (newState) {
270
+ case 'OPEN':
271
+ status.openedAt = Date.now()
272
+ this.events.onOpen?.(backend, status)
273
+ break
274
+ case 'HALF_OPEN':
275
+ status.halfOpenSuccesses = 0
276
+ this.events.onHalfOpen?.(backend, status)
277
+ break
278
+ case 'CLOSED':
279
+ status.openedAt = null
280
+ status.consecutiveFailures = 0
281
+ this.events.onClose?.(backend, status)
282
+ break
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Check if a request can be made to this backend
288
+ *
289
+ * Returns true if circuit is CLOSED or HALF_OPEN (testing recovery)
290
+ */
291
+ canRequest(backend: string): boolean {
292
+ const status = this.getStatus(backend)
293
+
294
+ switch (status.state) {
295
+ case 'CLOSED':
296
+ return true
297
+
298
+ case 'OPEN': {
299
+ // Check if reset time has passed
300
+ const timeSinceOpen = status.openedAt ? Date.now() - status.openedAt : 0
301
+ if (timeSinceOpen >= this.resetTimeMs) {
302
+ // Transition to HALF_OPEN to test recovery
303
+ this.transition(backend, status, 'HALF_OPEN')
304
+ return true
305
+ }
306
+ return false
307
+ }
308
+
309
+ case 'HALF_OPEN':
310
+ return true
311
+
312
+ default:
313
+ return false
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Check if circuit is open (shorthand for !canRequest)
319
+ */
320
+ isOpen(backend: string): boolean {
321
+ return !this.canRequest(backend)
322
+ }
323
+
324
+ /**
325
+ * Record a successful request
326
+ */
327
+ recordSuccess(backend: string): void {
328
+ const status = this.getStatus(backend)
329
+ status.totalSuccesses++
330
+ status.lastSuccessAt = Date.now()
331
+ status.consecutiveFailures = 0
332
+ status.lastError = null
333
+
334
+ if (status.state === 'HALF_OPEN') {
335
+ status.halfOpenSuccesses++
336
+ if (status.halfOpenSuccesses >= this.successThreshold) {
337
+ // Recovery confirmed, close the circuit
338
+ this.transition(backend, status, 'CLOSED')
339
+ }
340
+ } else if (status.state !== 'CLOSED') {
341
+ // Any success in non-HALF_OPEN state closes the circuit
342
+ this.transition(backend, status, 'CLOSED')
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Record a failed request
348
+ */
349
+ recordFailure(backend: string, error?: string): void {
350
+ const status = this.getStatus(backend)
351
+ status.totalFailures++
352
+ status.consecutiveFailures++
353
+ status.lastFailureAt = Date.now()
354
+ status.lastError = error ?? 'Unknown error'
355
+
356
+ if (status.state === 'HALF_OPEN') {
357
+ // Failure in HALF_OPEN immediately opens the circuit
358
+ this.transition(backend, status, 'OPEN')
359
+ } else if (status.state === 'CLOSED') {
360
+ // Check if threshold exceeded
361
+ if (status.consecutiveFailures >= this.failureThreshold) {
362
+ this.transition(backend, status, 'OPEN')
363
+ }
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Get the current state of a backend's circuit
369
+ */
370
+ getState(backend: string): CircuitState {
371
+ return this.getStatusPublic(backend).state
372
+ }
373
+
374
+ /**
375
+ * Get detailed status for a backend (public API)
376
+ */
377
+ getStatusPublic(backend: string): CircuitBreakerStatus {
378
+ const status = this.statuses.get(backend)
379
+ if (!status) {
380
+ return {
381
+ state: 'CLOSED',
382
+ consecutiveFailures: 0,
383
+ totalFailures: 0,
384
+ totalSuccesses: 0,
385
+ lastFailureAt: null,
386
+ lastSuccessAt: null,
387
+ openedAt: null,
388
+ lastError: null,
389
+ }
390
+ }
391
+ // Return without internal fields
392
+ const { halfOpenSuccesses: _, ...publicStatus } = status
393
+ return publicStatus
394
+ }
395
+
396
+ /**
397
+ * Get all backend statuses for monitoring/health checks
398
+ */
399
+ getAllStatuses(): Map<string, CircuitBreakerStatus> {
400
+ const result = new Map<string, CircuitBreakerStatus>()
401
+ for (const [backend, status] of this.statuses) {
402
+ const { halfOpenSuccesses: _, ...publicStatus } = status
403
+ result.set(backend, publicStatus)
404
+ }
405
+ return result
406
+ }
407
+
408
+ /**
409
+ * Get health summary
410
+ */
411
+ getHealthSummary(): {
412
+ total: number
413
+ closed: number
414
+ open: number
415
+ halfOpen: number
416
+ backends: Array<{ name: string; state: CircuitState; failures: number }>
417
+ } {
418
+ const backends: Array<{ name: string; state: CircuitState; failures: number }> = []
419
+ let closed = 0
420
+ let open = 0
421
+ let halfOpen = 0
422
+
423
+ for (const [name, status] of this.statuses) {
424
+ backends.push({
425
+ name,
426
+ state: status.state,
427
+ failures: status.consecutiveFailures,
428
+ })
429
+
430
+ switch (status.state) {
431
+ case 'CLOSED':
432
+ closed++
433
+ break
434
+ case 'OPEN':
435
+ open++
436
+ break
437
+ case 'HALF_OPEN':
438
+ halfOpen++
439
+ break
440
+ }
441
+ }
442
+
443
+ return {
444
+ total: this.statuses.size,
445
+ closed,
446
+ open,
447
+ halfOpen,
448
+ backends,
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Reset a specific backend's circuit to CLOSED
454
+ */
455
+ reset(backend: string): void {
456
+ const status = this.getStatus(backend) as InternalCircuitStatus
457
+ status.consecutiveFailures = 0
458
+ status.lastError = null
459
+ this.transition(backend, status, 'CLOSED')
460
+ }
461
+
462
+ /**
463
+ * Reset all circuits to CLOSED
464
+ */
465
+ resetAll(): void {
466
+ for (const backend of this.statuses.keys()) {
467
+ this.reset(backend)
468
+ }
469
+ }
470
+ }
471
+
472
+ // Backwards-compatible type alias
473
+ type BackendStatus = CircuitBreakerStatus
474
+
475
+ /**
476
+ * @deprecated Use CircuitBreaker instead. This class is kept for backwards compatibility.
477
+ */
478
+ class BackendTracker {
479
+ private breaker: CircuitBreaker
480
+
481
+ constructor(options?: { failureThreshold?: number; resetTimeMs?: number }) {
482
+ this.breaker = new CircuitBreaker(options)
483
+ }
484
+
485
+ recordSuccess(backend: string): void {
486
+ this.breaker.recordSuccess(backend)
487
+ }
488
+
489
+ recordFailure(backend: string): void {
490
+ this.breaker.recordFailure(backend)
491
+ }
492
+
493
+ isCircuitOpen(backend: string): boolean {
494
+ return this.breaker.isOpen(backend)
495
+ }
496
+
497
+ getAllStatuses(): Map<string, BackendStatus> {
498
+ return this.breaker.getAllStatuses()
499
+ }
500
+ }
17
501
 
18
502
  /**
19
503
  * Route with quote information
@@ -73,6 +557,26 @@ export interface FindBestRouteParams {
73
557
  senderAddress?: string
74
558
  slippageTolerance?: number
75
559
  deadline?: number
560
+ /** Skip cache and fetch fresh quotes (default: false) */
561
+ skipCache?: boolean
562
+ }
563
+
564
+ /**
565
+ * Router configuration options
566
+ */
567
+ export interface SmartRouterOptions {
568
+ /** Quote cache TTL in milliseconds (default: 30000) */
569
+ cacheTtlMs?: number
570
+ /** Maximum cache size (default: 1000) */
571
+ cacheMaxSize?: number
572
+ /** Per-backend timeout in milliseconds (default: 5000) */
573
+ backendTimeoutMs?: number
574
+ /** Circuit breaker failure threshold (default: 3) */
575
+ circuitBreakerThreshold?: number
576
+ /** Circuit breaker reset time in milliseconds (default: 30000) */
577
+ circuitBreakerResetMs?: number
578
+ /** Log backend failures (default: uses structured logger, set to null to disable) */
579
+ onBackendFailure?: ((backend: string, error: string) => void) | null
76
580
  }
77
581
 
78
582
  /**
@@ -83,13 +587,23 @@ export interface FindBestRouteParams {
83
587
  * - Execution speed (estimated time)
84
588
  * - Privacy support (shielded vs transparent)
85
589
  *
590
+ * Features:
591
+ * - Quote caching with configurable TTL (default: 30s)
592
+ * - Per-backend timeouts (default: 5s)
593
+ * - Circuit breaker for failing backends
594
+ * - Error isolation with Promise.allSettled
595
+ *
86
596
  * @example
87
597
  * ```typescript
88
598
  * const registry = new SettlementRegistry()
89
599
  * registry.register(nearIntentsBackend)
90
600
  * registry.register(zcashBackend)
91
601
  *
92
- * const router = new SmartRouter(registry)
602
+ * const router = new SmartRouter(registry, {
603
+ * cacheTtlMs: 30_000,
604
+ * backendTimeoutMs: 5_000,
605
+ * })
606
+ *
93
607
  * const routes = await router.findBestRoute({
94
608
  * from: { chain: 'ethereum', token: 'USDC' },
95
609
  * to: { chain: 'solana', token: 'SOL' },
@@ -106,12 +620,57 @@ export interface FindBestRouteParams {
106
620
  * ```
107
621
  */
108
622
  export class SmartRouter {
109
- constructor(private registry: SettlementRegistry) {}
623
+ private readonly cache: QuoteCache
624
+ private readonly tracker: BackendTracker
625
+ private readonly backendTimeoutMs: number
626
+ private readonly onBackendFailure: ((backend: string, error: string) => void) | null
627
+
628
+ constructor(
629
+ private registry: SettlementRegistry,
630
+ options?: SmartRouterOptions
631
+ ) {
632
+ this.cache = new QuoteCache({
633
+ ttlMs: options?.cacheTtlMs,
634
+ maxSize: options?.cacheMaxSize,
635
+ })
636
+ this.tracker = new BackendTracker({
637
+ failureThreshold: options?.circuitBreakerThreshold,
638
+ resetTimeMs: options?.circuitBreakerResetMs,
639
+ })
640
+ this.backendTimeoutMs = options?.backendTimeoutMs ?? 5_000
641
+ this.onBackendFailure = options?.onBackendFailure === null
642
+ ? null
643
+ : options?.onBackendFailure ?? ((backend, error) => {
644
+ log.warn({ backend, error }, 'Backend failed')
645
+ })
646
+ }
647
+
648
+ /**
649
+ * Clear the quote cache
650
+ */
651
+ clearCache(): void {
652
+ this.cache.clear()
653
+ }
654
+
655
+ /**
656
+ * Get cache statistics
657
+ */
658
+ getCacheStats(): { size: number; maxSize: number } {
659
+ return this.cache.stats()
660
+ }
661
+
662
+ /**
663
+ * Get backend health statuses
664
+ */
665
+ getBackendStatuses(): Map<string, BackendStatus> {
666
+ return this.tracker.getAllStatuses()
667
+ }
110
668
 
111
669
  /**
112
670
  * Find best routes for a swap
113
671
  *
114
672
  * Queries all compatible backends in parallel and returns sorted routes.
673
+ * Uses caching, per-backend timeouts, and circuit breaker for reliability.
115
674
  *
116
675
  * @param params - Route finding parameters
117
676
  * @returns Sorted routes (best first)
@@ -129,6 +688,7 @@ export class SmartRouter {
129
688
  senderAddress,
130
689
  slippageTolerance,
131
690
  deadline,
691
+ skipCache = false,
132
692
  } = params
133
693
 
134
694
  // Validate amount
@@ -136,6 +696,34 @@ export class SmartRouter {
136
696
  throw new ValidationError('Amount must be greater than zero')
137
697
  }
138
698
 
699
+ // Build quote params
700
+ const quoteParams: QuoteParams = {
701
+ fromChain: from.chain,
702
+ toChain: to.chain,
703
+ fromToken: from.token,
704
+ toToken: to.token,
705
+ amount,
706
+ privacyLevel,
707
+ recipientMetaAddress,
708
+ senderAddress,
709
+ slippageTolerance,
710
+ deadline,
711
+ }
712
+
713
+ // Check cache first (unless skipCache is set)
714
+ if (!skipCache) {
715
+ const cachedQuotes = this.cache.get(quoteParams)
716
+ if (cachedQuotes && cachedQuotes.length > 0) {
717
+ // Reconstruct routes from cached quotes
718
+ const routes = this.reconstructRoutesFromCache(cachedQuotes, quoteParams)
719
+ if (routes.length > 0) {
720
+ this.rankRoutes(routes, { preferSpeed, preferLowFees })
721
+ routes.sort((a, b) => b.score - a.score)
722
+ return routes
723
+ }
724
+ }
725
+ }
726
+
139
727
  // Get all registered backends
140
728
  const allBackends = this.registry
141
729
  .list()
@@ -143,6 +731,11 @@ export class SmartRouter {
143
731
 
144
732
  // Filter backends that support this route and privacy level
145
733
  const compatibleBackends = allBackends.filter((backend) => {
734
+ // Skip backends with open circuits
735
+ if (this.tracker.isCircuitOpen(backend.name)) {
736
+ return false
737
+ }
738
+
146
739
  const { supportedSourceChains, supportedDestinationChains, supportedPrivacyLevels } =
147
740
  backend.capabilities
148
741
 
@@ -161,62 +754,65 @@ export class SmartRouter {
161
754
  )
162
755
  }
163
756
 
164
- // Build quote params
165
- const quoteParams: QuoteParams = {
166
- fromChain: from.chain,
167
- toChain: to.chain,
168
- fromToken: from.token,
169
- toToken: to.token,
170
- amount,
171
- privacyLevel,
172
- recipientMetaAddress,
173
- senderAddress,
174
- slippageTolerance,
175
- deadline,
176
- }
177
-
178
- // Query all compatible backends in parallel
757
+ // Query all compatible backends in parallel with timeouts
179
758
  const quotePromises = compatibleBackends.map(async (backend) => {
180
- try {
181
- const quote = await backend.getQuote(quoteParams)
182
- return {
759
+ return this.fetchQuoteWithTimeout(backend, quoteParams)
760
+ })
761
+
762
+ // Use Promise.allSettled for error isolation
763
+ const settledResults = await Promise.allSettled(quotePromises)
764
+
765
+ // Process results
766
+ const successfulRoutes: RouteWithQuote[] = []
767
+ const failures: Array<{ backend: string; error: string }> = []
768
+
769
+ settledResults.forEach((result, index) => {
770
+ const backend = compatibleBackends[index]
771
+
772
+ if (result.status === 'fulfilled' && result.value.success) {
773
+ const { quote } = result.value
774
+ this.tracker.recordSuccess(backend.name)
775
+ successfulRoutes.push({
183
776
  backend: backend.name,
184
777
  quote,
185
778
  backendInstance: backend,
186
- success: true,
779
+ score: 0,
780
+ })
781
+ } else {
782
+ let error: string
783
+ if (result.status === 'rejected') {
784
+ error = result.reason instanceof Error ? result.reason.message : 'Unknown error'
785
+ } else {
786
+ // result.value.success is false here
787
+ error = (result.value as { success: false; error: string }).error
187
788
  }
188
- } catch (error) {
189
- return {
190
- backend: backend.name,
191
- error: error instanceof Error ? error.message : 'Unknown error',
192
- success: false,
789
+
790
+ this.tracker.recordFailure(backend.name)
791
+ failures.push({ backend: backend.name, error })
792
+
793
+ // Log failure
794
+ if (this.onBackendFailure) {
795
+ this.onBackendFailure(backend.name, error)
193
796
  }
194
797
  }
195
798
  })
196
799
 
197
- const results = await Promise.all(quotePromises)
198
-
199
- // Filter successful quotes
200
- const successfulRoutes = results
201
- .filter((r): r is { backend: string; quote: Quote; backendInstance: SettlementBackend; success: true } => r.success)
202
- .map((r) => ({
203
- backend: r.backend,
204
- quote: r.quote,
205
- backendInstance: r.backendInstance,
206
- score: 0, // Will be calculated below
207
- }))
208
-
209
800
  if (successfulRoutes.length === 0) {
210
- const errors = results
211
- .filter((r): r is { backend: string; error: string; success: false } => !r.success)
212
- .map((r) => `${r.backend}: ${r.error}`)
801
+ const errorMessage = failures
802
+ .map((f) => `${f.backend}: ${f.error}`)
213
803
  .join(', ')
214
804
 
215
805
  throw new NetworkError(
216
- `All backends failed to provide quotes: ${errors}`
806
+ `All backends failed to provide quotes: ${errorMessage}`,
807
+ ErrorCode.NETWORK_FAILED,
808
+ { context: { failures } }
217
809
  )
218
810
  }
219
811
 
812
+ // Cache the successful quotes
813
+ const quotesToCache = successfulRoutes.map(r => r.quote)
814
+ this.cache.set(quoteParams, quotesToCache)
815
+
220
816
  // Calculate scores and rank
221
817
  this.rankRoutes(successfulRoutes, { preferSpeed, preferLowFees })
222
818
 
@@ -226,6 +822,89 @@ export class SmartRouter {
226
822
  return successfulRoutes
227
823
  }
228
824
 
825
+ /**
826
+ * Fetch quote from a backend with timeout
827
+ * @private
828
+ */
829
+ private async fetchQuoteWithTimeout(
830
+ backend: SettlementBackend,
831
+ params: QuoteParams
832
+ ): Promise<{ success: true; quote: Quote } | { success: false; error: string }> {
833
+ return new Promise((resolve) => {
834
+ let timeoutId: ReturnType<typeof setTimeout> | null = null
835
+ let resolved = false
836
+
837
+ // Set timeout
838
+ timeoutId = setTimeout(() => {
839
+ if (!resolved) {
840
+ resolved = true
841
+ resolve({
842
+ success: false,
843
+ error: `Timeout after ${this.backendTimeoutMs}ms`,
844
+ })
845
+ }
846
+ }, this.backendTimeoutMs)
847
+
848
+ // Fetch quote
849
+ backend.getQuote(params)
850
+ .then((quote) => {
851
+ if (!resolved) {
852
+ resolved = true
853
+ if (timeoutId) clearTimeout(timeoutId)
854
+ resolve({ success: true, quote })
855
+ }
856
+ })
857
+ .catch((error) => {
858
+ if (!resolved) {
859
+ resolved = true
860
+ if (timeoutId) clearTimeout(timeoutId)
861
+ resolve({
862
+ success: false,
863
+ error: error instanceof Error ? error.message : 'Unknown error',
864
+ })
865
+ }
866
+ })
867
+ })
868
+ }
869
+
870
+ /**
871
+ * Reconstruct RouteWithQuote from cached quotes
872
+ * @private
873
+ */
874
+ private reconstructRoutesFromCache(
875
+ quotes: Quote[],
876
+ params: QuoteParams
877
+ ): RouteWithQuote[] {
878
+ const routes: RouteWithQuote[] = []
879
+
880
+ for (const quote of quotes) {
881
+ // Try to find the backend instance
882
+ const backends = this.registry.list()
883
+ for (const name of backends) {
884
+ const backend = this.registry.get(name)
885
+ const { supportedSourceChains, supportedDestinationChains, supportedPrivacyLevels } =
886
+ backend.capabilities
887
+
888
+ const supportsRoute =
889
+ supportedSourceChains.includes(params.fromChain) &&
890
+ supportedDestinationChains.includes(params.toChain)
891
+ const supportsPrivacy = supportedPrivacyLevels.includes(params.privacyLevel)
892
+
893
+ if (supportsRoute && supportsPrivacy) {
894
+ routes.push({
895
+ backend: name,
896
+ quote,
897
+ backendInstance: backend,
898
+ score: 0,
899
+ })
900
+ break // Use first matching backend
901
+ }
902
+ }
903
+ }
904
+
905
+ return routes
906
+ }
907
+
229
908
  /**
230
909
  * Compare quotes from multiple routes side-by-side
231
910
  *
@@ -366,6 +1045,7 @@ export class SmartRouter {
366
1045
  * Create a new SmartRouter instance
367
1046
  *
368
1047
  * @param registry - Settlement registry with registered backends
1048
+ * @param options - Router configuration options
369
1049
  * @returns SmartRouter instance
370
1050
  *
371
1051
  * @example
@@ -374,10 +1054,16 @@ export class SmartRouter {
374
1054
  * registry.register(nearIntentsBackend)
375
1055
  * registry.register(zcashBackend)
376
1056
  *
377
- * const router = createSmartRouter(registry)
1057
+ * const router = createSmartRouter(registry, {
1058
+ * cacheTtlMs: 30_000,
1059
+ * backendTimeoutMs: 5_000,
1060
+ * })
378
1061
  * const routes = await router.findBestRoute({ ... })
379
1062
  * ```
380
1063
  */
381
- export function createSmartRouter(registry: SettlementRegistry): SmartRouter {
382
- return new SmartRouter(registry)
1064
+ export function createSmartRouter(
1065
+ registry: SettlementRegistry,
1066
+ options?: SmartRouterOptions
1067
+ ): SmartRouter {
1068
+ return new SmartRouter(registry, options)
383
1069
  }