@sip-protocol/sdk 0.7.3 → 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.
- package/LICENSE +21 -0
- package/README.md +267 -0
- package/dist/{TransportWebUSB-TQ7WZ4LE.mjs → TransportWebUSB-YQMAGJAJ.mjs} +12 -9
- package/dist/browser.d.mts +10 -4
- package/dist/browser.d.ts +10 -4
- package/dist/browser.js +47556 -19603
- package/dist/browser.mjs +628 -48
- package/dist/chunk-4GRJ5MAW.mjs +152 -0
- package/dist/chunk-5D7A3L3W.mjs +717 -0
- package/dist/chunk-64AYA5F5.mjs +7834 -0
- package/dist/chunk-GMDGB22A.mjs +379 -0
- package/dist/chunk-I534WKN7.mjs +328 -0
- package/dist/chunk-IBZVA5Y7.mjs +1003 -0
- package/dist/chunk-PRRZAWJE.mjs +223 -0
- package/dist/{chunk-UJCSKKID.mjs → chunk-XGB3TDIC.mjs} +13 -1
- package/dist/{chunk-3M3HNQCW.mjs → chunk-YWGJ77A2.mjs} +28656 -13103
- package/dist/{chunk-6WGN57S2.mjs → chunk-Z3K7W5S3.mjs} +48 -0
- package/dist/constants-LHAAUC2T.mjs +51 -0
- package/dist/dist-2OGQ7FED.mjs +3957 -0
- package/dist/dist-IFHPYLDX.mjs +254 -0
- package/dist/fulfillment_proof-ANHVPKTB.mjs +21 -0
- package/dist/funding_proof-ICFZ5LHY.mjs +21 -0
- package/dist/{index-DIBZHOOQ.d.ts → index-DXh2IGkz.d.ts} +21239 -10304
- package/dist/{index-8MQz13eJ.d.mts → index-DeE1ZzA4.d.mts} +21239 -10304
- package/dist/index.d.mts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +48396 -19623
- package/dist/index.mjs +537 -19
- package/dist/interface-Bf7w1PLW.d.mts +679 -0
- package/dist/interface-Bf7w1PLW.d.ts +679 -0
- package/dist/{noir-DKfEzWy9.d.mts → noir-kzbLVTei.d.mts} +31 -21
- package/dist/{noir-DKfEzWy9.d.ts → noir-kzbLVTei.d.ts} +31 -21
- package/dist/proofs/halo2.d.mts +151 -0
- package/dist/proofs/halo2.d.ts +151 -0
- package/dist/proofs/halo2.js +350 -0
- package/dist/proofs/halo2.mjs +11 -0
- package/dist/proofs/kimchi.d.mts +160 -0
- package/dist/proofs/kimchi.d.ts +160 -0
- package/dist/proofs/kimchi.js +431 -0
- package/dist/proofs/kimchi.mjs +13 -0
- package/dist/proofs/noir.d.mts +1 -1
- package/dist/proofs/noir.d.ts +1 -1
- package/dist/proofs/noir.js +74 -18
- package/dist/proofs/noir.mjs +84 -24
- package/dist/solana-U3MEGU7W.mjs +280 -0
- package/dist/validity_proof-3POXLPNY.mjs +21 -0
- package/package.json +54 -21
- package/src/adapters/index.ts +41 -0
- package/src/adapters/jupiter.ts +571 -0
- package/src/adapters/near-intents.ts +135 -0
- package/src/advisor/advisor.ts +653 -0
- package/src/advisor/index.ts +54 -0
- package/src/advisor/tools.ts +303 -0
- package/src/advisor/types.ts +164 -0
- package/src/chains/ethereum/announcement.ts +536 -0
- package/src/chains/ethereum/bnb-optimizations.ts +474 -0
- package/src/chains/ethereum/commitment.ts +522 -0
- package/src/chains/ethereum/constants.ts +462 -0
- package/src/chains/ethereum/deployment.ts +596 -0
- package/src/chains/ethereum/gas-estimation.ts +538 -0
- package/src/chains/ethereum/index.ts +268 -0
- package/src/chains/ethereum/optimizations.ts +614 -0
- package/src/chains/ethereum/privacy-adapter.ts +855 -0
- package/src/chains/ethereum/registry.ts +584 -0
- package/src/chains/ethereum/rpc.ts +905 -0
- package/src/chains/ethereum/stealth.ts +491 -0
- package/src/chains/ethereum/token.ts +790 -0
- package/src/chains/ethereum/transfer.ts +637 -0
- package/src/chains/ethereum/types.ts +456 -0
- package/src/chains/ethereum/viewing-key.ts +455 -0
- package/src/chains/near/commitment.ts +608 -0
- package/src/chains/near/constants.ts +284 -0
- package/src/chains/near/function-call.ts +871 -0
- package/src/chains/near/history.ts +654 -0
- package/src/chains/near/implicit-account.ts +840 -0
- package/src/chains/near/index.ts +393 -0
- package/src/chains/near/native-transfer.ts +658 -0
- package/src/chains/near/nep141.ts +775 -0
- package/src/chains/near/privacy-adapter.ts +889 -0
- package/src/chains/near/resolver.ts +971 -0
- package/src/chains/near/rpc.ts +1016 -0
- package/src/chains/near/stealth.ts +419 -0
- package/src/chains/near/types.ts +317 -0
- package/src/chains/near/viewing-key.ts +876 -0
- package/src/chains/solana/anchor-transfer.ts +386 -0
- package/src/chains/solana/commitment.ts +577 -0
- package/src/chains/solana/constants.ts +126 -12
- package/src/chains/solana/ephemeral-keys.ts +543 -0
- package/src/chains/solana/index.ts +252 -1
- package/src/chains/solana/key-derivation.ts +418 -0
- package/src/chains/solana/kit-compat.ts +334 -0
- package/src/chains/solana/optimizations.ts +560 -0
- package/src/chains/solana/privacy-adapter.ts +605 -0
- package/src/chains/solana/providers/generic.ts +47 -6
- package/src/chains/solana/providers/helius-enhanced-types.ts +336 -0
- package/src/chains/solana/providers/helius-enhanced.ts +623 -0
- package/src/chains/solana/providers/helius.ts +186 -33
- package/src/chains/solana/providers/index.ts +31 -0
- package/src/chains/solana/providers/interface.ts +61 -18
- package/src/chains/solana/providers/quicknode.ts +409 -0
- package/src/chains/solana/providers/triton.ts +426 -0
- package/src/chains/solana/providers/webhook.ts +338 -67
- package/src/chains/solana/rpc-client.ts +1150 -0
- package/src/chains/solana/scan.ts +83 -66
- package/src/chains/solana/sol-transfer.ts +732 -0
- package/src/chains/solana/spl-transfer.ts +886 -0
- package/src/chains/solana/stealth-scanner.ts +703 -0
- package/src/chains/solana/sunspot-verifier.ts +453 -0
- package/src/chains/solana/transaction-builder.ts +755 -0
- package/src/chains/solana/transfer.ts +74 -5
- package/src/chains/solana/types.ts +57 -6
- package/src/chains/solana/utils.ts +110 -0
- package/src/chains/solana/viewing-key.ts +807 -0
- package/src/compliance/fireblocks.ts +921 -0
- package/src/compliance/index.ts +23 -0
- package/src/compliance/range-sas.ts +398 -33
- package/src/config/endpoints.ts +100 -0
- package/src/crypto.ts +11 -8
- package/src/errors.ts +82 -0
- package/src/evm/erc4337-relayer.ts +830 -0
- package/src/evm/index.ts +47 -0
- package/src/fees/calculator.ts +396 -0
- package/src/fees/index.ts +87 -0
- package/src/fees/near-contract.ts +429 -0
- package/src/fees/types.ts +268 -0
- package/src/index.ts +686 -1
- package/src/intent.ts +6 -3
- package/src/logger.ts +324 -0
- package/src/network/index.ts +80 -0
- package/src/network/proxy.ts +691 -0
- package/src/optimizations/index.ts +541 -0
- package/src/oracle/types.ts +1 -0
- package/src/privacy-backends/arcium-types.ts +727 -0
- package/src/privacy-backends/arcium.ts +719 -0
- package/src/privacy-backends/combined-privacy.ts +866 -0
- package/src/privacy-backends/cspl-token.ts +595 -0
- package/src/privacy-backends/cspl-types.ts +512 -0
- package/src/privacy-backends/cspl.ts +907 -0
- package/src/privacy-backends/health.ts +488 -0
- package/src/privacy-backends/inco-types.ts +323 -0
- package/src/privacy-backends/inco.ts +616 -0
- package/src/privacy-backends/index.ts +254 -4
- package/src/privacy-backends/interface.ts +649 -6
- package/src/privacy-backends/lru-cache.ts +343 -0
- package/src/privacy-backends/magicblock.ts +458 -0
- package/src/privacy-backends/mock.ts +258 -0
- package/src/privacy-backends/privacycash.ts +13 -17
- package/src/privacy-backends/private-swap.ts +570 -0
- package/src/privacy-backends/rate-limiter.ts +683 -0
- package/src/privacy-backends/registry.ts +414 -2
- package/src/privacy-backends/router.ts +283 -3
- package/src/privacy-backends/shadowwire.ts +449 -0
- package/src/privacy-backends/sip-native.ts +3 -0
- package/src/privacy-logger.ts +191 -0
- package/src/production-safety.ts +373 -0
- package/src/proofs/aggregator.ts +1029 -0
- package/src/proofs/browser-composer.ts +1150 -0
- package/src/proofs/browser.ts +113 -25
- package/src/proofs/cache/index.ts +127 -0
- package/src/proofs/cache/interface.ts +545 -0
- package/src/proofs/cache/key-generator.ts +188 -0
- package/src/proofs/cache/lru-cache.ts +481 -0
- package/src/proofs/cache/multi-tier-cache.ts +575 -0
- package/src/proofs/cache/persistent-cache.ts +788 -0
- package/src/proofs/compliance-proof.ts +872 -0
- package/src/proofs/composer/base.ts +923 -0
- package/src/proofs/composer/index.ts +25 -0
- package/src/proofs/composer/interface.ts +518 -0
- package/src/proofs/composer/types.ts +383 -0
- package/src/proofs/converters/halo2.ts +452 -0
- package/src/proofs/converters/index.ts +208 -0
- package/src/proofs/converters/interface.ts +363 -0
- package/src/proofs/converters/kimchi.ts +462 -0
- package/src/proofs/converters/noir.ts +451 -0
- package/src/proofs/fallback.ts +888 -0
- package/src/proofs/halo2.ts +42 -0
- package/src/proofs/index.ts +471 -0
- package/src/proofs/interface.ts +13 -0
- package/src/proofs/kimchi.ts +42 -0
- package/src/proofs/lazy.ts +1004 -0
- package/src/proofs/mock.ts +25 -1
- package/src/proofs/noir.ts +110 -29
- package/src/proofs/orchestrator.ts +960 -0
- package/src/proofs/parallel/concurrency.ts +297 -0
- package/src/proofs/parallel/dependency-graph.ts +602 -0
- package/src/proofs/parallel/executor.ts +420 -0
- package/src/proofs/parallel/index.ts +131 -0
- package/src/proofs/parallel/interface.ts +685 -0
- package/src/proofs/parallel/worker-pool.ts +644 -0
- package/src/proofs/providers/halo2.ts +560 -0
- package/src/proofs/providers/index.ts +34 -0
- package/src/proofs/providers/kimchi.ts +641 -0
- package/src/proofs/validator.ts +881 -0
- package/src/proofs/verifier.ts +867 -0
- package/src/quantum/index.ts +112 -0
- package/src/quantum/winternitz-vault.ts +639 -0
- package/src/quantum/wots.ts +611 -0
- package/src/settlement/backends/direct-chain.ts +1 -0
- package/src/settlement/index.ts +9 -0
- package/src/settlement/router.ts +732 -46
- package/src/solana/index.ts +72 -0
- package/src/solana/jito-relayer.ts +687 -0
- package/src/solana/noir-verifier-types.ts +430 -0
- package/src/solana/noir-verifier.ts +816 -0
- package/src/stealth/address-derivation.ts +193 -0
- package/src/stealth/ed25519.ts +431 -0
- package/src/stealth/index.ts +233 -0
- package/src/stealth/meta-address.ts +221 -0
- package/src/stealth/secp256k1.ts +368 -0
- package/src/stealth/utils.ts +194 -0
- package/src/stealth.ts +50 -1504
- package/src/sync/index.ts +106 -0
- package/src/sync/manager.ts +504 -0
- package/src/sync/mock-provider.ts +318 -0
- package/src/sync/oblivious.ts +625 -0
- package/src/tokens/index.ts +15 -0
- package/src/tokens/registry.ts +301 -0
- package/src/utils/deprecation.ts +94 -0
- package/src/utils/index.ts +9 -0
- package/src/wallet/ethereum/index.ts +68 -0
- package/src/wallet/ethereum/metamask-privacy.ts +420 -0
- package/src/wallet/ethereum/multi-wallet.ts +646 -0
- package/src/wallet/ethereum/privacy-adapter.ts +700 -0
- package/src/wallet/ethereum/types.ts +3 -1
- package/src/wallet/ethereum/walletconnect-adapter.ts +675 -0
- package/src/wallet/hardware/index.ts +10 -0
- package/src/wallet/hardware/ledger-privacy.ts +414 -0
- package/src/wallet/index.ts +71 -0
- package/src/wallet/near/adapter.ts +626 -0
- package/src/wallet/near/index.ts +86 -0
- package/src/wallet/near/meteor-wallet.ts +1153 -0
- package/src/wallet/near/my-near-wallet.ts +790 -0
- package/src/wallet/near/wallet-selector.ts +702 -0
- package/src/wallet/solana/adapter.ts +6 -4
- package/src/wallet/solana/index.ts +13 -0
- package/src/wallet/solana/privacy-adapter.ts +567 -0
- package/src/wallet/sui/types.ts +6 -4
- package/src/zcash/rpc-client.ts +13 -6
- package/dist/chunk-2XIVXWHA.mjs +0 -1930
- package/dist/chunk-3INS3PR5.mjs +0 -884
- package/dist/chunk-3OVABDRH.mjs +0 -17096
- package/dist/chunk-7RFRWDCW.mjs +0 -1504
- package/dist/chunk-DLDWZFYC.mjs +0 -1495
- package/dist/chunk-E6SZWREQ.mjs +0 -57
- package/dist/chunk-F6F73W35.mjs +0 -16166
- package/dist/chunk-G33LB27A.mjs +0 -16166
- package/dist/chunk-HGU6HZRC.mjs +0 -231
- package/dist/chunk-L2K34JCU.mjs +0 -1496
- package/dist/chunk-OFDBEIEK.mjs +0 -16166
- package/dist/chunk-SF7YSLF5.mjs +0 -1515
- package/dist/chunk-SN4ZDTVW.mjs +0 -16166
- package/dist/chunk-WWUSGOXE.mjs +0 -17129
- package/dist/constants-VOI7BSLK.mjs +0 -27
- package/dist/index-B71aXVzk.d.ts +0 -13264
- package/dist/index-BYZbDjal.d.ts +0 -11390
- package/dist/index-CHB3KuOB.d.mts +0 -11859
- package/dist/index-CzWPI6Le.d.ts +0 -11859
- package/dist/index-pOIIuwfV.d.mts +0 -13264
- package/dist/index-xbWjohNq.d.mts +0 -11390
- package/dist/solana-4O4K45VU.mjs +0 -46
- package/dist/solana-5EMCTPTS.mjs +0 -46
- package/dist/solana-NDABAZ6P.mjs +0 -56
- package/dist/solana-Q4NAVBTS.mjs +0 -46
- package/dist/solana-ZYO63LY5.mjs +0 -46
package/src/settlement/router.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
211
|
-
.
|
|
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: ${
|
|
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(
|
|
382
|
-
|
|
1064
|
+
export function createSmartRouter(
|
|
1065
|
+
registry: SettlementRegistry,
|
|
1066
|
+
options?: SmartRouterOptions
|
|
1067
|
+
): SmartRouter {
|
|
1068
|
+
return new SmartRouter(registry, options)
|
|
383
1069
|
}
|