@stabbleorg/mclmm-sdk 0.1.14 → 0.2.0

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 (117) hide show
  1. package/idl/stabble_clmm.json +0 -28
  2. package/lib/__tests__/fixtures/pool-states.d.ts +84 -0
  3. package/lib/__tests__/fixtures/pool-states.d.ts.map +1 -0
  4. package/lib/__tests__/fixtures/tick-arrays.d.ts +42 -0
  5. package/lib/__tests__/fixtures/tick-arrays.d.ts.map +1 -0
  6. package/lib/__tests__/helpers/integration-mocks.d.ts +82 -0
  7. package/lib/__tests__/helpers/integration-mocks.d.ts.map +1 -0
  8. package/lib/__tests__/integration/performance.test.d.ts +12 -0
  9. package/lib/__tests__/integration/performance.test.d.ts.map +1 -0
  10. package/lib/__tests__/integration/swap-flow.test.d.ts +12 -0
  11. package/lib/__tests__/integration/swap-flow.test.d.ts.map +1 -0
  12. package/lib/__tests__/setup.d.ts +19 -0
  13. package/lib/__tests__/setup.d.ts.map +1 -0
  14. package/lib/__tests__/unit/math-utils.test.d.ts +11 -0
  15. package/lib/__tests__/unit/math-utils.test.d.ts.map +1 -0
  16. package/lib/__tests__/unit/pool-data-manager.test.d.ts +16 -0
  17. package/lib/__tests__/unit/pool-data-manager.test.d.ts.map +1 -0
  18. package/lib/__tests__/unit/price-api-client.test.d.ts +2 -0
  19. package/lib/__tests__/unit/price-api-client.test.d.ts.map +1 -0
  20. package/lib/__tests__/unit/swap-manager.test.d.ts +14 -0
  21. package/lib/__tests__/unit/swap-manager.test.d.ts.map +1 -0
  22. package/lib/__tests__/unit/swap-math-engine.test.d.ts +14 -0
  23. package/lib/__tests__/unit/swap-math-engine.test.d.ts.map +1 -0
  24. package/lib/api/config.d.ts +6 -0
  25. package/lib/api/config.d.ts.map +1 -1
  26. package/lib/client.d.ts +2 -0
  27. package/lib/client.d.ts.map +1 -1
  28. package/lib/constants.d.ts +41 -1
  29. package/lib/constants.d.ts.map +1 -1
  30. package/lib/generated/accounts/ammConfig.d.ts +1 -1
  31. package/lib/generated/accounts/ammConfig.d.ts.map +1 -1
  32. package/lib/generated/accounts/observationState.d.ts +1 -1
  33. package/lib/generated/accounts/observationState.d.ts.map +1 -1
  34. package/lib/generated/accounts/operationState.d.ts +1 -1
  35. package/lib/generated/accounts/operationState.d.ts.map +1 -1
  36. package/lib/generated/accounts/personalPositionState.d.ts +1 -1
  37. package/lib/generated/accounts/personalPositionState.d.ts.map +1 -1
  38. package/lib/generated/accounts/poolState.d.ts +1 -1
  39. package/lib/generated/accounts/poolState.d.ts.map +1 -1
  40. package/lib/generated/accounts/protocolPositionState.d.ts +1 -1
  41. package/lib/generated/accounts/protocolPositionState.d.ts.map +1 -1
  42. package/lib/generated/accounts/supportMintAssociated.d.ts +1 -1
  43. package/lib/generated/accounts/supportMintAssociated.d.ts.map +1 -1
  44. package/lib/generated/accounts/tickArrayBitmapExtension.d.ts +1 -1
  45. package/lib/generated/accounts/tickArrayBitmapExtension.d.ts.map +1 -1
  46. package/lib/generated/accounts/tickArrayState.d.ts +1 -1
  47. package/lib/generated/accounts/tickArrayState.d.ts.map +1 -1
  48. package/lib/generated/instructions/closePosition.d.ts +1 -1
  49. package/lib/generated/instructions/closePosition.d.ts.map +1 -1
  50. package/lib/generated/instructions/closeProtocolPosition.d.ts +1 -1
  51. package/lib/generated/instructions/closeProtocolPosition.d.ts.map +1 -1
  52. package/lib/generated/instructions/collectFundFee.d.ts +1 -1
  53. package/lib/generated/instructions/collectFundFee.d.ts.map +1 -1
  54. package/lib/generated/instructions/collectProtocolFee.d.ts +1 -1
  55. package/lib/generated/instructions/collectProtocolFee.d.ts.map +1 -1
  56. package/lib/generated/instructions/collectRemainingRewards.d.ts +1 -1
  57. package/lib/generated/instructions/collectRemainingRewards.d.ts.map +1 -1
  58. package/lib/generated/instructions/createAmmConfig.d.ts +1 -1
  59. package/lib/generated/instructions/createAmmConfig.d.ts.map +1 -1
  60. package/lib/generated/instructions/createOperationAccount.d.ts +1 -1
  61. package/lib/generated/instructions/createOperationAccount.d.ts.map +1 -1
  62. package/lib/generated/instructions/createPool.d.ts +1 -1
  63. package/lib/generated/instructions/createPool.d.ts.map +1 -1
  64. package/lib/generated/instructions/createSupportMintAssociated.d.ts +1 -1
  65. package/lib/generated/instructions/createSupportMintAssociated.d.ts.map +1 -1
  66. package/lib/generated/instructions/decreaseLiquidityV2.d.ts +1 -1
  67. package/lib/generated/instructions/decreaseLiquidityV2.d.ts.map +1 -1
  68. package/lib/generated/instructions/increaseLiquidityV2.d.ts +1 -1
  69. package/lib/generated/instructions/increaseLiquidityV2.d.ts.map +1 -1
  70. package/lib/generated/instructions/initializeReward.d.ts +1 -1
  71. package/lib/generated/instructions/initializeReward.d.ts.map +1 -1
  72. package/lib/generated/instructions/openPositionWithToken22Nft.d.ts +1 -1
  73. package/lib/generated/instructions/openPositionWithToken22Nft.d.ts.map +1 -1
  74. package/lib/generated/instructions/setRewardParams.d.ts +1 -1
  75. package/lib/generated/instructions/setRewardParams.d.ts.map +1 -1
  76. package/lib/generated/instructions/swapRouterBaseIn.d.ts +1 -1
  77. package/lib/generated/instructions/swapRouterBaseIn.d.ts.map +1 -1
  78. package/lib/generated/instructions/swapV2.d.ts +1 -1
  79. package/lib/generated/instructions/swapV2.d.ts.map +1 -1
  80. package/lib/generated/instructions/transferRewardOwner.d.ts +1 -1
  81. package/lib/generated/instructions/transferRewardOwner.d.ts.map +1 -1
  82. package/lib/generated/instructions/updateAmmConfig.d.ts +1 -1
  83. package/lib/generated/instructions/updateAmmConfig.d.ts.map +1 -1
  84. package/lib/generated/instructions/updateOperationAccount.d.ts +1 -1
  85. package/lib/generated/instructions/updateOperationAccount.d.ts.map +1 -1
  86. package/lib/generated/instructions/updatePoolStatus.d.ts +1 -1
  87. package/lib/generated/instructions/updatePoolStatus.d.ts.map +1 -1
  88. package/lib/generated/instructions/updateRewardInfos.d.ts +1 -1
  89. package/lib/generated/instructions/updateRewardInfos.d.ts.map +1 -1
  90. package/lib/generated/programs/ammV3.d.ts.map +1 -1
  91. package/lib/generated/shared/index.d.ts.map +1 -1
  92. package/lib/generated/types/decreaseLiquidityEvent.d.ts +0 -2
  93. package/lib/generated/types/decreaseLiquidityEvent.d.ts.map +1 -1
  94. package/lib/generated/types/index.d.ts +0 -1
  95. package/lib/generated/types/index.d.ts.map +1 -1
  96. package/lib/index.d.ts +5 -0
  97. package/lib/index.d.ts.map +1 -1
  98. package/lib/index.js +6011 -3305
  99. package/lib/index.mjs +5519 -2825
  100. package/lib/managers/index.d.ts +7 -0
  101. package/lib/managers/index.d.ts.map +1 -0
  102. package/lib/managers/pool-data-manager.d.ts +132 -0
  103. package/lib/managers/pool-data-manager.d.ts.map +1 -0
  104. package/lib/managers/price-api-client.d.ts +295 -0
  105. package/lib/managers/price-api-client.d.ts.map +1 -0
  106. package/lib/position-manager.d.ts.map +1 -1
  107. package/lib/swap.d.ts +832 -2
  108. package/lib/swap.d.ts.map +1 -1
  109. package/lib/types.d.ts +23 -4
  110. package/lib/types.d.ts.map +1 -1
  111. package/lib/utils/index.d.ts +5 -2
  112. package/lib/utils/index.d.ts.map +1 -1
  113. package/lib/utils/math.d.ts +386 -0
  114. package/lib/utils/math.d.ts.map +1 -1
  115. package/lib/utils/tickQuery.d.ts +62 -1
  116. package/lib/utils/tickQuery.d.ts.map +1 -1
  117. package/package.json +13 -5
package/lib/swap.d.ts CHANGED
@@ -1,6 +1,836 @@
1
+ import type { Account, Address, Instruction, TransactionSigner } from "@solana/kit";
2
+ import { type PoolState, type TickArrayState, type AmmConfig } from "./generated";
3
+ import { ClmmSdkConfig, SwapParams, SwapQuote } from "./types";
4
+ import { type PriceApiConfig } from "./managers";
5
+ import BN from "bn.js";
6
+ import Decimal from "decimal.js";
7
+ /** Swap Math Engine class for performing mathematical operations related to swaps
8
+ *
9
+ * Why this exists as a separate class: Separates pure mathematical calculations from
10
+ * orchestration logic. This allows the math to be:
11
+ * 1. Tested in isolation without mocking RPC calls
12
+ * 2. Reused across different swap implementations
13
+ * 3. Easily audited for correctness without business logic noise
14
+ */
15
+ export interface DetailedSwapQuote extends SwapQuote {
16
+ crossedTicks?: number;
17
+ endPrice?: Decimal;
18
+ priceImpactBreakdown?: {
19
+ fees: number;
20
+ slippage: number;
21
+ };
22
+ }
23
+ export interface SwapCalculationParams {
24
+ pool: PoolState;
25
+ ammConfig: AmmConfig;
26
+ amountIn: BN;
27
+ zeroForOne: boolean;
28
+ slippageTolerance: number;
29
+ poolAddress: Address;
30
+ }
31
+ export interface AccurateSwapParams extends SwapCalculationParams {
32
+ tickArrayCache: {
33
+ [key: string]: Account<TickArrayState>;
34
+ };
35
+ }
36
+ export declare class SwapMathEngine {
37
+ /**
38
+ * Simple swap calculation using current pool state only
39
+ *
40
+ * Why "simple": This method trades accuracy for speed. Instead of fetching and
41
+ * traversing potentially dozens of tick arrays from RPC, it assumes liquidity
42
+ * is concentrated at the current price. This is accurate for small swaps where
43
+ * the price doesn't move much, but becomes less accurate for large swaps that
44
+ * would cross multiple ticks.
45
+ *
46
+ * When to use: For UI quote displays, small swaps (<1% of liquidity), or when
47
+ * you need sub-second response times. The caching layer makes this very fast
48
+ * for repeated quotes.
49
+ *
50
+ * When NOT to use: For building actual swap instructions. Use calculateAccurateSwap
51
+ * instead to ensure the on-chain execution matches your quote.
52
+ */
53
+ calculateSimpleSwap(params: SwapCalculationParams): Promise<SwapQuote>;
54
+ /**
55
+ * Calculate swap using full tick traversal
56
+ *
57
+ * Why this is necessary: In CLMM (Uniswap V3 style) pools, liquidity is not
58
+ * uniformly distributed. It's concentrated in specific price ranges (ticks).
59
+ * A large swap may cross multiple ticks, each with different liquidity depths.
60
+ *
61
+ * To get an accurate quote, we must:
62
+ * 1. Fetch all tick arrays the swap might touch
63
+ * 2. Simulate walking through each tick, consuming liquidity
64
+ * 3. Track price impact as we cross tick boundaries
65
+ *
66
+ * Why not always use this: It requires multiple RPC calls to fetch tick arrays,
67
+ * making it slower and more expensive than simple calculation. Use it when
68
+ * accuracy matters more than speed (e.g., before executing large swaps).
69
+ *
70
+ * Trade-off: 3-5 RPC calls for accuracy vs. 2 RPC calls for speed.
71
+ */
72
+ calculateAccurateSwap(params: AccurateSwapParams): Promise<DetailedSwapQuote>;
73
+ /**
74
+ * Calculates the square root price limit for a swap based on slippage tolerance
75
+ *
76
+ * Why sqrt price: CLMM pools store price as sqrt(price) * 2^64 for mathematical
77
+ * efficiency. Computing swaps in sqrt space avoids expensive square root operations.
78
+ *
79
+ * Why we need limits: On-chain swaps need a price boundary to prevent front-running.
80
+ * If a swap would move the price beyond this limit, it reverts, protecting users
81
+ * from MEV attacks and unexpected price movement.
82
+ *
83
+ * Why apply slippage here: Rather than using arbitrary limits (like ±1%), we use
84
+ * the user's actual slippage tolerance. This respects their risk preference while
85
+ * providing protection.
86
+ *
87
+ * Why the min/max clamping: Solana programs panic on overflow. We must ensure the
88
+ * calculated limit stays within valid price bounds to prevent transaction failures.
89
+ */
90
+ calculateSqrtPriceLimit(sqrtPriceX64: BN, zeroForOne: boolean, slippageTolerance: number): BN;
91
+ }
1
92
  /**
93
+ * Swap Manager implementation
94
+ *
95
+ * Why this wrapper class: SwapQuoteResult provides a clean interface over the raw
96
+ * quote data. Instead of forcing users to manually convert between base units and
97
+ * human-readable decimals, or calculate price impact percentages, we provide getters
98
+ * that handle these conversions automatically.
99
+ *
100
+ * Why getters instead of upfront calculation: Lazy evaluation. Not every consumer
101
+ * needs every metric (some just need amountOut, others need full impact analysis).
102
+ * Getters let consumers pay only for what they use.
103
+ */
104
+ export declare class SwapQuoteResult {
105
+ readonly quote: SwapQuote;
106
+ private readonly decIn;
107
+ private readonly decOut;
108
+ private readonly zeroForOne;
109
+ constructor(quote: SwapQuote, decIn: number, decOut: number, zeroForOne: boolean);
110
+ /**
111
+ * Gets the execution price (output per input)
112
+ *
113
+ * Why this conversion: Token amounts are stored in base units (e.g., lamports for SOL),
114
+ * but users think in decimal units (SOL, USDC). We convert both sides to human-readable
115
+ * before calculating the rate to avoid precision loss from integer division.
116
+ */
117
+ get executionPrice(): Decimal;
118
+ priceUnitsLabel(inputSymbol: string, outputSymbol: string): string;
119
+ get isZeroForOne(): boolean;
120
+ get priceImpactPercent(): number;
121
+ get minOutput(): BN;
122
+ get totalFee(): BN;
123
+ get feePercent(): number;
124
+ get hasAcceptableImpact(): boolean;
125
+ get hasHighImpact(): boolean;
126
+ get hasCriticalImpact(): boolean;
127
+ /**
128
+ * Estimates compute units needed for the swap transaction
129
+ *
130
+ * Why we estimate this: Solana transactions have compute budgets. Underestimating
131
+ * causes transaction failures. Overestimating wastes priority fees. We provide a
132
+ * reasonable estimate based on swap complexity.
133
+ *
134
+ * Why 50k base + 20k per hop: Empirically derived from on-chain program measurements.
135
+ * Simple swaps use ~50k CU. Multi-hop routing adds ~20k per additional pool.
136
+ *
137
+ * Why this matters: Users can use this to calculate priority fees before sending
138
+ * transactions, improving UX and reducing failed transactions.
139
+ */
140
+ get estimatedComputeUnits(): number;
141
+ toJSON(): SwapQuote;
142
+ }
143
+ export interface SwapSimulation {
144
+ quote: SwapQuote;
145
+ willSucceed: boolean;
146
+ errors: string[];
147
+ warnings: string[];
148
+ }
149
+ export interface DetailedPriceImpact {
150
+ totalImpact: number;
151
+ breakdown: {
152
+ fees: number;
153
+ slippage: number;
154
+ crossedTicks: number;
155
+ };
156
+ comparison: {
157
+ marketPrice: Decimal;
158
+ executionPrice: Decimal;
159
+ difference: number;
160
+ };
161
+ priceSnapshots: {
162
+ priceBefore: Decimal;
163
+ priceAfter: Decimal;
164
+ };
165
+ }
166
+ export interface SwapManagerConfig {
167
+ /**
168
+ * Price API configuration
169
+ *
170
+ * Configuration for REST API price fetching from team's infrastructure.
171
+ * This avoids exposing RPC URIs and handles rate limiting centrally.
172
+ *
173
+ * Configuration:
174
+ * - baseUrl
175
+ * - timeout: Optional request timeout (default: 5000ms)
176
+ *
177
+ * When enabled, provides:
178
+ * - Pre-swap price validation against market prices
179
+ * - Price staleness detection
180
+ * - Batch price fetching for multi-pool operations
181
+ * - Automatic retry with concurrency control
182
+ */
183
+ priceApiConfig?: PriceApiConfig;
184
+ /**
185
+ * Enable market price validation before swaps (requires priceApiConfig)
186
+ *
187
+ * When enabled, SwapManager will fetch current market prices from the REST API
188
+ * and compare them against on-chain pool state. Useful for:
189
+ * - Detecting stale pool data
190
+ * - Identifying potential MEV opportunities
191
+ * - Validating quote accuracy
192
+ *
193
+ * Default: false (to maintain backward compatibility)
194
+ */
195
+ enablePriceValidation?: boolean;
196
+ }
197
+ /**
198
+ * SwapManager - Orchestrates swap operations for CLMM pools
199
+ *
200
+ * Why this architecture: We separate concerns into layers:
201
+ * 1. SwapMathEngine: Pure calculations (testable, auditable)
202
+ * 2. PoolDataManager: Data fetching and caching (optimizes RPC calls)
203
+ * 3. PriceApiClient: REST API price fetching (team's infrastructure)
204
+ * 4. SwapManager: Orchestration (combines everything into simple APIs)
205
+ *
206
+ * This separation allows:
207
+ * - Testing math independently of RPC infrastructure
208
+ * - Reusing pool data across multiple quotes
209
+ * - Optional price validation via REST API
210
+ * - Swapping out implementations without changing interfaces
211
+ *
212
+ * Why caching is critical: RPC calls are slow (100-500ms) and rate-limited.
213
+ * Without caching, getting a quote for UI display would block for half a second.
214
+ * With caching, subsequent quotes for the same pool are instant (< 1ms).
215
+ *
216
+ * Why 2-second cache TTL: Per team spec, this balances freshness with RPC efficiency
217
+ * for low-traffic applications similar to app.stabble.org. More aggressive than
218
+ * traditional 5-10s caching but appropriate for current expected usage patterns.
219
+ *
220
+ * @example
221
+ * ```typescript
222
+ * // Basic usage (no price API)
223
+ * const swapManager = new SwapManager(config);
2
224
  *
3
- * NOTE: Work in progress
225
+ * // With REST API for price validation
226
+ * const swapManager = new SwapManager(config, {
227
+ * priceApiConfig: {
228
+ * baseUrl: 'https://mclmm-api.stabble.org', // or dev URL
229
+ * timeout: 5000
230
+ * },
231
+ * enablePriceValidation: true
232
+ * });
4
233
  *
5
- */
234
+ * const quote = await swapManager.getSwapQuote(poolAddress, {
235
+ * tokenIn: tokenAAddress,
236
+ * tokenOut: tokenBAddress,
237
+ * amountIn: new BN(1000000),
238
+ * slippageTolerance: 0.01
239
+ * });
240
+ * ```
241
+ */
242
+ export declare class SwapManager {
243
+ private readonly config;
244
+ private readonly managerConfig?;
245
+ private readonly poolDataManager;
246
+ private readonly mathEngine;
247
+ private readonly priceApiClient?;
248
+ private readonly quoteCache;
249
+ private readonly quoteCacheTTL;
250
+ private readonly maxCacheSize;
251
+ /**
252
+ * Creates a new SwapManager instance
253
+ *
254
+ * @param config - SDK configuration with RPC client and logging
255
+ * @param managerConfig - Optional swap manager configuration
256
+ * @param managerConfig.priceApiConfig - REST API config for price validation
257
+ * @param managerConfig.enablePriceValidation - Enable price validation before swaps
258
+ */
259
+ constructor(config: ClmmSdkConfig, managerConfig?: SwapManagerConfig | undefined);
260
+ private log;
261
+ /**
262
+ * Clears the quote cache
263
+ *
264
+ * Why you'd call this: After major market events (e.g., oracle updates, large trades),
265
+ * cached quotes may no longer reflect reality. Manual clearing lets you force fresh
266
+ * calculations without waiting for TTL expiry.
267
+ *
268
+ * Why not clear automatically more often: Caching exists because RPC calls are slow.
269
+ * Over-clearing defeats the purpose and makes the UI feel sluggish.
270
+ */
271
+ clearQuoteCache(): void;
272
+ /**
273
+ * Clears all caches (quotes and pool data)
274
+ *
275
+ * Removes all cached data including swap quotes and pool states.
276
+ * Use this for a complete cache reset.
277
+ */
278
+ clearAllCaches(): void;
279
+ /**
280
+ * Get comprehensive cache metrics for observability
281
+ *
282
+ * Provides detailed metrics about cache performance including:
283
+ * - Hit/miss rates for pool and config data
284
+ * - Current cache sizes
285
+ * - In-flight request counts
286
+ * - Error cache statistics
287
+ *
288
+ * Use this to monitor cache effectiveness and diagnose performance issues.
289
+ *
290
+ * @returns Detailed cache metrics
291
+ *
292
+ * @example
293
+ * ```typescript
294
+ * const metrics = swapManager.getCacheMetrics();
295
+ *
296
+ * console.log('Pool cache hit rate:', metrics.poolData.pool.hitRate);
297
+ * console.log('Config cache hit rate:', metrics.poolData.config.hitRate);
298
+ * console.log('Quote cache size:', metrics.quoteCache.size);
299
+ * console.log('In-flight requests:', metrics.poolData.pool.inFlight);
300
+ * ```
301
+ */
302
+ getCacheMetrics(): {
303
+ poolData: {
304
+ pool: {
305
+ hits: number;
306
+ misses: number;
307
+ errors: number;
308
+ hitRate: number;
309
+ cacheSize: number;
310
+ inFlight: number;
311
+ errorCacheSize: number;
312
+ };
313
+ config: {
314
+ hits: number;
315
+ misses: number;
316
+ errors: number;
317
+ hitRate: number;
318
+ cacheSize: number;
319
+ inFlight: number;
320
+ errorCacheSize: number;
321
+ };
322
+ };
323
+ quoteCache: {
324
+ size: number;
325
+ maxSize: number;
326
+ ttl: number;
327
+ };
328
+ };
329
+ /**
330
+ * Reset all cache metrics to zero
331
+ *
332
+ * Clears metric counters without clearing cached data.
333
+ * Useful for starting fresh metric collection after configuration changes
334
+ * or for periodic metric reporting.
335
+ */
336
+ resetCacheMetrics(): void;
337
+ getAtaAddresses(owner: Address, mints: Address[]): Promise<Address[]>;
338
+ /**
339
+ * Estimates compute units and calculates priority fees for a swap transaction
340
+ *
341
+ * Why this exists: Solana's fee market is complex. Users need to:
342
+ * 1. Set compute budget (too low = failure, too high = wasted CU)
343
+ * 2. Set priority fee (too low = slow confirmation, too high = wasted SOL)
344
+ *
345
+ * Why three priority levels: Different users have different urgency needs.
346
+ * - Low: Batch operations where speed doesn't matter (100 microLamports/CU)
347
+ * - Medium: Normal trades where reasonable speed is expected (1,000 microLamports/CU)
348
+ * - High: Time-sensitive arbitrage or MEV protection (10,000 microLamports/CU)
349
+ *
350
+ * Why these specific microLamport values: Derived from observing Solana fee markets.
351
+ * These values provide reasonable service levels without overpaying.
352
+ *
353
+ * Priority fee calculation: `totalFee = (computeUnits × microLamportsPerCU) / 1,000,000`
354
+ * This converts from microLamports (1/1,000,000 of a lamport) to lamports.
355
+ *
356
+ * @param quote - Swap quote result containing estimated compute units
357
+ * @param priorityLevel - Priority level for transaction confirmation (default: "medium")
358
+ * - "low": 100 microLamports/CU (~0.05-5 lamports for typical swaps)
359
+ * - "medium": 1,000 microLamports/CU (~0.5-50 lamports for typical swaps)
360
+ * - "high": 10,000 microLamports/CU (~5-500 lamports for typical swaps)
361
+ *
362
+ * @returns Object containing:
363
+ * - computeUnits: Estimated compute units needed (50k base + 20k per hop)
364
+ * - microLamportsPerCU: Price per compute unit in microLamports
365
+ * - totalPriorityFeeLamports: Total priority fee in lamports
366
+ *
367
+ * @example
368
+ * ```typescript
369
+ * const quote = await swapManager.getSwapQuote(poolAddress, {
370
+ * tokenIn: usdcAddress,
371
+ * tokenOut: solAddress,
372
+ * amountIn: new BN(1_000_000),
373
+ * });
374
+ *
375
+ * // Estimate with medium priority (default)
376
+ * const budget = swapManager.estimateComputeBudget(quote);
377
+ * console.log(`Compute units: ${budget.computeUnits}`);
378
+ * console.log(`Priority fee: ${budget.totalPriorityFeeLamports} lamports`);
379
+ *
380
+ * // High priority for time-sensitive trades
381
+ * const urgentBudget = swapManager.estimateComputeBudget(quote, "high");
382
+ *
383
+ * // Use in transaction
384
+ * import { ComputeBudgetProgram } from "@solana/web3.js";
385
+ *
386
+ * const tx = new Transaction()
387
+ * .add(
388
+ * ComputeBudgetProgram.setComputeUnitLimit({
389
+ * units: budget.computeUnits
390
+ * })
391
+ * )
392
+ * .add(
393
+ * ComputeBudgetProgram.setComputeUnitPrice({
394
+ * microLamports: budget.microLamportsPerCU
395
+ * })
396
+ * )
397
+ * .add(swapInstruction);
398
+ * ```
399
+ */
400
+ estimateComputeBudget(quote: SwapQuoteResult, priorityLevel?: "low" | "medium" | "high"): {
401
+ computeUnits: number;
402
+ microLamportsPerCU: number;
403
+ totalPriorityFeeLamports: number;
404
+ };
405
+ /**
406
+ * Determines if a cached quote is still valid
407
+ *
408
+ * Uses time-based TTL (2 seconds per team spec). This balances freshness
409
+ * (quotes aren't too stale) with performance (enough caching to avoid RPC spam)
410
+ * for low-traffic applications.
411
+ *
412
+ * Why 2 seconds: Team spec recommends 2-second polling/caching to match
413
+ * app.stabble.org traffic patterns. More aggressive than traditional 5-10s
414
+ * but appropriate for current expected usage.
415
+ */
416
+ private isCacheValid;
417
+ /**
418
+ * Evicts oldest entry when cache is full (LRU eviction)
419
+ *
420
+ * Why LRU (Least Recently Used): Simple and effective. The oldest entries are
421
+ * least likely to be requested again. More sophisticated strategies (LFU, ARC)
422
+ * add complexity for minimal benefit in this use case.
423
+ *
424
+ * Why 1000 max size: Balances memory usage with hit rate. Each entry is ~500 bytes,
425
+ * so 1000 entries = ~500KB. This is negligible memory-wise but large enough to
426
+ * cover most active pools in a session.
427
+ *
428
+ * Why we need a cap at all: Without limits, cache could grow unbounded if someone
429
+ * queries many different pools or amounts. This would cause memory leaks in
430
+ * long-running applications.
431
+ */
432
+ private evictOldestCacheEntry;
433
+ /**
434
+ * Gets a swap quote using simple calculation (no tick traversal)
435
+ *
436
+ * Why this is the default: For 90% of swaps (small amounts, liquid pools), simple
437
+ * calculation is accurate enough and 3-5x faster than accurate calculation. Users
438
+ * get instant feedback in UIs without sacrificing meaningful accuracy.
439
+ *
440
+ * Why caching matters here: UI components often request quotes repeatedly as users
441
+ * type amounts. Without caching, each keystroke would trigger RPC calls. With
442
+ * caching, only the first request hits RPC; subsequent requests are instant.
443
+ *
444
+ * Cache behavior:
445
+ * - Quotes are cached for 5 seconds by default (balances freshness with performance)
446
+ * - If price streaming is enabled, cache is invalidated immediately on price changes
447
+ * - Cache uses LRU eviction with a max size of 1000 entries (prevents memory leaks)
448
+ *
449
+ * @param poolAddress - Address of the CLMM pool
450
+ * @param params - Swap parameters including tokens and amount
451
+ * @param params.tokenIn - Address of the input token
452
+ * @param params.tokenOut - Address of the output token
453
+ * @param params.amountIn - Amount of input token to swap (in base units)
454
+ * @param params.slippageTolerance - Optional slippage tolerance (0-1, default: 0.01 = 1%)
455
+ * @param params.poolState - Optional: Pre-fetched pool state to avoid RPC call (optimization for frontends with API data)
456
+ * @param params.ammConfig - Optional: Pre-fetched AMM config to avoid RPC call
457
+ * @param options - Optional fetch configuration
458
+ * @param options.signal - AbortSignal to cancel the request if needed
459
+ * @param options.allowStale - Return stale data immediately and refresh in background
460
+ *
461
+ * @returns Swap quote result with execution price, impact, fees, and minimum output
462
+ *
463
+ * @throws {ClmmError} PRICE_SLIPPAGE if slippage tolerance is invalid
464
+ * @throws {ClmmError} SWAP_AMOUNT_CANNOT_BE_ZERO if amount is <= 0
465
+ * @throws {ClmmError} POOL_NOT_FOUND if pool doesn't contain the token pair
466
+ *
467
+ * @example
468
+ * ```typescript
469
+ * // Basic usage (SDK fetches pool state via RPC)
470
+ * const quote = await swapManager.getSwapQuote(poolAddress, {
471
+ * tokenIn: usdcAddress,
472
+ * tokenOut: solAddress,
473
+ * amountIn: new BN(1_000_000), // 1 USDC (6 decimals)
474
+ * slippageTolerance: 0.005 // 0.5%
475
+ * });
476
+ *
477
+ * // Optimized for frontend with API data (avoids redundant RPC calls)
478
+ * const poolData = await fetch('/api/pools/' + poolAddress).then(r => r.json());
479
+ * const quote = await swapManager.getSwapQuote(poolAddress, {
480
+ * tokenIn: usdcAddress,
481
+ * tokenOut: solAddress,
482
+ * amountIn: new BN(1_000_000),
483
+ * poolState: poolData.state, // Pass pre-fetched data
484
+ * ammConfig: poolData.ammConfig
485
+ * });
486
+ *
487
+ * // With abort signal for cancellable requests
488
+ * const controller = new AbortController();
489
+ * const quote = await swapManager.getSwapQuote(poolAddress, {
490
+ * tokenIn: usdcAddress,
491
+ * tokenOut: solAddress,
492
+ * amountIn: new BN(1_000_000),
493
+ * }, {
494
+ * signal: controller.signal
495
+ * });
496
+ *
497
+ * // With stale-while-revalidate for better UX
498
+ * const quote = await swapManager.getSwapQuote(poolAddress, {
499
+ * tokenIn: usdcAddress,
500
+ * tokenOut: solAddress,
501
+ * amountIn: new BN(1_000_000),
502
+ * }, {
503
+ * allowStale: true // Returns immediately, refreshes in background
504
+ * });
505
+ *
506
+ * console.log(`Price: ${quote.executionPrice}`);
507
+ * console.log(`Impact: ${quote.priceImpactPercent}%`);
508
+ * console.log(`Min output: ${quote.minimumAmountOut}`);
509
+ * ```
510
+ */
511
+ getSwapQuote(poolAddress: Address, params: Omit<SwapParams, "wallet"> & {
512
+ tokenIn: Address;
513
+ tokenOut: Address;
514
+ poolState?: PoolState;
515
+ ammConfig?: AmmConfig;
516
+ }, options?: {
517
+ signal?: AbortSignal;
518
+ allowStale?: boolean;
519
+ }): Promise<SwapQuoteResult>;
520
+ /**
521
+ * Gets a swap quote using accurate calculation (with tick array traversal)
522
+ *
523
+ * Why this exists alongside getSwapQuote: Simple calculation assumes liquidity is
524
+ * concentrated at current price. For small swaps this is fine. But large swaps cross
525
+ * many ticks, each with different liquidity. Without traversing ticks, we'd
526
+ * underestimate price impact and users would get less than quoted.
527
+ *
528
+ * When to use this over getSwapQuote:
529
+ * 1. Large swaps (>5% of pool liquidity) - price impact is non-linear
530
+ * 2. Before building swap instructions - ensures on-chain result matches quote
531
+ * 3. When users demand precision - some use cases can't tolerate estimation error
532
+ *
533
+ * Cost trade-off: 2-5 additional RPC calls to fetch tick arrays, adding 200-500ms
534
+ * latency. Worth it for accuracy when executing valuable swaps.
535
+ *
536
+ * The method:
537
+ * 1. Fetches required tick arrays based on swap size estimation
538
+ * 2. Simulates tick-by-tick swap execution
539
+ * 3. Returns detailed quote with crossed ticks and price impact breakdown
540
+ *
541
+ * @param poolAddress - Address of the CLMM pool
542
+ * @param params - Swap parameters including tokens and amount
543
+ * @param params.tokenIn - Address of the input token
544
+ * @param params.tokenOut - Address of the output token
545
+ * @param params.amountIn - Amount of input token to swap (in base units)
546
+ * @param params.slippageTolerance - Optional slippage tolerance (0-1, default: 0.01 = 1%)
547
+ * @param params.tickArrayCount - Optional override for number of tick arrays to fetch
548
+ * @param options - Optional fetch configuration
549
+ * @param options.signal - AbortSignal to cancel the request if needed
550
+ * @param options.allowStale - Return stale data immediately and refresh in background
551
+ *
552
+ * @returns Detailed swap quote with tick information and price breakdown
553
+ *
554
+ * @throws {ClmmError} PRICE_SLIPPAGE if slippage tolerance is invalid
555
+ * @throws {ClmmError} SWAP_AMOUNT_CANNOT_BE_ZERO if amount is <= 0
556
+ * @throws {ClmmError} POOL_NOT_FOUND if pool doesn't contain the token pair
557
+ *
558
+ * @example
559
+ * ```typescript
560
+ * const quote = await swapManager.getAccurateSwapQuote(poolAddress, {
561
+ * tokenIn: usdcAddress,
562
+ * tokenOut: solAddress,
563
+ * amountIn: new BN(10_000_000_000), // 10,000 USDC (large swap)
564
+ * slippageTolerance: 0.02 // 2%
565
+ * });
566
+ *
567
+ * console.log(`Crossed ${quote.crossedTicks} ticks`);
568
+ * console.log(`End price: ${quote.endPrice}`);
569
+ * console.log(`Price impact breakdown:`, quote.priceImpactBreakdown);
570
+ * ```
571
+ */
572
+ getAccurateSwapQuote(poolAddress: Address, params: Omit<SwapParams, "wallet"> & {
573
+ tokenIn: Address;
574
+ tokenOut: Address;
575
+ tickArrayCount?: number;
576
+ }, options?: {
577
+ signal?: AbortSignal;
578
+ allowStale?: boolean;
579
+ }): Promise<DetailedSwapQuote>;
580
+ private fetchTickArraysForSwap;
581
+ /**
582
+ * Estimates the number of tick arrays needed for a swap.
583
+ * Uses a conservative approach with safety buffers to prevent mid-swap failures.
584
+ *
585
+ * Why estimation is necessary: We need to know which tick arrays to fetch before
586
+ * we can calculate the accurate swap. But we can't know exactly how many ticks
587
+ * we'll cross without doing the calculation. Chicken-and-egg problem.
588
+ *
589
+ * Solution: Conservative estimation. We estimate based on swap size relative to
590
+ * liquidity, then add a safety buffer. Better to fetch extra arrays (slight
591
+ * performance cost) than to fetch too few and have the swap fail on-chain.
592
+ *
593
+ * Why the +3 buffer: In sparse liquidity situations, the swap might skip over
594
+ * initialized ticks faster than expected. The buffer ensures we have enough
595
+ * arrays even in worst-case scenarios. Orca and Uniswap use similar buffers.
596
+ *
597
+ * Why cap at 20: Prevents pathological cases (e.g., nearly empty pools) from
598
+ * fetching dozens of arrays, while still covering very large swaps or sparse
599
+ * liquidity scenarios. 20 arrays cover ~400 ticks, enough for most edge cases.
600
+ *
601
+ * @param pool - Current pool state
602
+ * @param amountIn - Amount being swapped
603
+ * @returns Number of tick arrays to fetch (includes safety buffer)
604
+ */
605
+ private estimateTickArrayCount;
606
+ private getRequiredTickArrays;
607
+ getBatchQuotes(poolAddress: Address, params: Omit<SwapParams, "wallet"> & {
608
+ amounts: BN[];
609
+ tokenIn: Address;
610
+ tokenOut: Address;
611
+ }, options?: {
612
+ signal?: AbortSignal;
613
+ allowStale?: boolean;
614
+ }): Promise<Map<string, SwapQuoteResult>>;
615
+ calculateOptimalSlippage(quote: SwapQuote, options?: {
616
+ riskTolerance?: "low" | "medium" | "high";
617
+ maxSlippage?: number;
618
+ }): number;
619
+ getDetailedPriceImpact(poolAddress: Address, params: Omit<SwapParams, "wallet">, options?: {
620
+ signal?: AbortSignal;
621
+ allowStale?: boolean;
622
+ }): Promise<DetailedPriceImpact>;
623
+ /**
624
+ * Simulates a swap to validate it will succeed and identify potential issues
625
+ *
626
+ * Why simulation matters: On-chain transactions cost money and can't be undone.
627
+ * Running a simulation first catches problems before they cost users SOL in
628
+ * transaction fees. This is especially important for swaps that might fail due to:
629
+ * - Excessive slippage (price moved since quote)
630
+ * - Insufficient output (amount too small after fees)
631
+ * - Math errors (overflow, underflow)
632
+ *
633
+ * Why we check price impact thresholds: Different impact levels warrant different
634
+ * user warnings:
635
+ * - < 5%: Normal, no warning
636
+ * - 5-15%: High impact, warn user they might get sandwich attacked
637
+ * - > 15%: Critical, likely to fail or result in terrible execution
638
+ *
639
+ * Why we validate output amount: Some swaps result in fractional output amounts
640
+ * that round to zero. Better to catch this in simulation than discover it after
641
+ * the transaction succeeds but user got nothing.
642
+ *
643
+ * Use this before building instructions to catch problems early and provide
644
+ * better user feedback.
645
+ *
646
+ * @param poolAddress - Address of the CLMM pool
647
+ * @param params - Swap parameters to simulate
648
+ * @param options - Optional fetch configuration
649
+ * @param options.signal - AbortSignal to cancel the request if needed
650
+ * @param options.allowStale - Return stale data immediately and refresh in background
651
+ *
652
+ * @returns Simulation result with success flag, quote, errors, and warnings
653
+ *
654
+ * @example
655
+ * ```typescript
656
+ * const simulation = await swapManager.simulateSwap(poolAddress, {
657
+ * tokenIn: usdcAddress,
658
+ * tokenOut: solAddress,
659
+ * amountIn: new BN(100_000_000_000), // Large amount
660
+ * slippageTolerance: 0.01
661
+ * });
662
+ *
663
+ * if (!simulation.willSucceed) {
664
+ * console.error('Swap will fail:', simulation.errors);
665
+ * return;
666
+ * }
667
+ *
668
+ * if (simulation.warnings.length > 0) {
669
+ * console.warn('Warnings:', simulation.warnings);
670
+ * // Show user confirmation dialog
671
+ * }
672
+ *
673
+ * // Proceed with swap
674
+ * const instruction = await swapManager.buildSwapInstruction(
675
+ * poolAddress,
676
+ * wallet,
677
+ * params,
678
+ * simulation.quote
679
+ * );
680
+ * ```
681
+ */
682
+ simulateSwap(poolAddress: Address, params: SwapParams, options?: {
683
+ signal?: AbortSignal;
684
+ allowStale?: boolean;
685
+ }): Promise<SwapSimulation>;
686
+ /**
687
+ * Builds a Solana instruction for executing a swap
688
+ *
689
+ * Why this is complex: A swap instruction needs many accounts and parameters:
690
+ * - Pool and config accounts (state to read)
691
+ * - Token vaults (where tokens are held)
692
+ * - User token accounts (where tokens come from/go to)
693
+ * - Observation account (for TWAP oracle)
694
+ * - Price limits (for MEV protection)
695
+ *
696
+ * This method handles all that complexity so users just provide tokens and amounts.
697
+ *
698
+ * Why we accept priorQuote: Common pattern is to show a quote to the user, get
699
+ * confirmation, then execute. If we re-ran simulation here, the price might have
700
+ * changed between display and execution. Accepting priorQuote lets users execute
701
+ * exactly what they confirmed, with slippage protection handling price movement.
702
+ *
703
+ * Why we simulate if no quote provided: Catch-all safety net. If someone builds
704
+ * an instruction without checking first, we prevent obviously broken swaps from
705
+ * being sent on-chain.
706
+ *
707
+ * The instruction is ready to be added to a transaction and sent to the network.
708
+ *
709
+ * @param poolAddress - Address of the CLMM pool
710
+ * @param payer - Transaction signer (wallet that will execute the swap)
711
+ * @param params - Swap parameters
712
+ * @param params.tokenIn - Input token address
713
+ * @param params.tokenOut - Output token address
714
+ * @param params.amountIn - Amount to swap (in base units)
715
+ * @param params.slippageTolerance - Optional slippage tolerance (0-1)
716
+ * @param params.sqrtPriceLimitX64 - Optional custom price limit (advanced)
717
+ * @param priorQuote - Optional pre-computed quote (skips simulation if provided)
718
+ *
719
+ * @returns Solana instruction ready to be executed
720
+ *
721
+ * @throws {ClmmError} SWAP_SIMULATION_FAILED if pre-execution simulation fails
722
+ *
723
+ * @example
724
+ * ```typescript
725
+ * // Get quote first (recommended for displaying to user)
726
+ * const quote = await swapManager.getAccurateSwapQuote(poolAddress, {
727
+ * tokenIn: usdcAddress,
728
+ * tokenOut: solAddress,
729
+ * amountIn: new BN(1_000_000),
730
+ * slippageTolerance: 0.01
731
+ * });
732
+ *
733
+ * // Build instruction using the quote
734
+ * const instruction = await swapManager.buildSwapInstruction(
735
+ * poolAddress,
736
+ * wallet,
737
+ * {
738
+ * tokenIn: usdcAddress,
739
+ * tokenOut: solAddress,
740
+ * amountIn: new BN(1_000_000),
741
+ * slippageTolerance: 0.01
742
+ * },
743
+ * quote // Reuse quote to avoid re-simulation
744
+ * );
745
+ *
746
+ * // Add to transaction and send
747
+ * const tx = new Transaction().add(instruction);
748
+ * ```
749
+ */
750
+ buildSwapInstruction(poolAddress: Address, payer: TransactionSigner, params: SwapParams & {
751
+ sqrtPriceLimitX64?: BN;
752
+ }, priorQuote?: SwapQuote, options?: {
753
+ signal?: AbortSignal;
754
+ }): Promise<Instruction>;
755
+ getCurrentPrice(poolAddress: Address, options?: {
756
+ signal?: AbortSignal;
757
+ allowStale?: boolean;
758
+ }): Promise<Decimal>;
759
+ /**
760
+ * Validates pool price against market price from REST API
761
+ *
762
+ * Why this matters: On-chain pool state can become stale between blocks or lag
763
+ * behind market prices due to arbitrage delays. Comparing against the REST API
764
+ * helps detect:
765
+ * - Stale pool data (price hasn't updated recently)
766
+ * - Arbitrage opportunities (on-chain price differs from market)
767
+ * - Potential MEV risks (large price discrepancies)
768
+ *
769
+ * This uses the enhanced PriceApiClient with:
770
+ * - Automatic retry on failures
771
+ * - Concurrency control
772
+ * - Abort signal support
773
+ * - Staleness detection
774
+ *
775
+ * @param poolAddress - Pool to validate
776
+ * @param opts - Optional configuration
777
+ * @param opts.signal - AbortSignal to cancel the request
778
+ * @param opts.maxDivergence - Maximum acceptable price divergence (0-1, default: 0.05 = 5%)
779
+ * @returns Validation result with price comparison
780
+ *
781
+ * @throws {Error} If priceApiConfig is not configured
782
+ *
783
+ * @example
784
+ * ```typescript
785
+ * const validation = await swapManager.validatePoolPrice(poolAddress, {
786
+ * maxDivergence: 0.02 // 2% tolerance
787
+ * });
788
+ *
789
+ * if (!validation.isValid) {
790
+ * console.warn('Pool price diverges from market:', validation);
791
+ * // Maybe increase slippage or wait for arbitrage
792
+ * }
793
+ * ```
794
+ */
795
+ validatePoolPrice(poolAddress: Address, opts?: {
796
+ signal?: AbortSignal;
797
+ maxDivergence?: number;
798
+ }): Promise<{
799
+ isValid: boolean;
800
+ onChainPrice: Decimal;
801
+ marketPrice?: Decimal;
802
+ divergence?: number;
803
+ divergencePercent?: number;
804
+ warning?: string;
805
+ }>;
806
+ /**
807
+ * Gets swap quote with optional market price validation
808
+ *
809
+ * Enhanced version of getSwapQuote that validates pool price against market
810
+ * prices when enablePriceValidation is enabled in config.
811
+ *
812
+ * @param poolAddress - Pool address
813
+ * @param params - Swap parameters
814
+ * @param opts - Optional configuration
815
+ * @param opts.signal - AbortSignal to cancel the request
816
+ * @param opts.skipValidation - Skip validation even if enabled (for performance)
817
+ * @returns Swap quote result with optional validation info
818
+ */
819
+ getValidatedSwapQuote(poolAddress: Address, params: Omit<SwapParams, "wallet"> & {
820
+ tokenIn: Address;
821
+ tokenOut: Address;
822
+ }, opts?: {
823
+ signal?: AbortSignal;
824
+ skipValidation?: boolean;
825
+ }): Promise<{
826
+ quote: SwapQuoteResult;
827
+ validation?: {
828
+ isValid: boolean;
829
+ onChainPrice: Decimal;
830
+ marketPrice?: Decimal;
831
+ divergencePercent?: number;
832
+ warning?: string;
833
+ };
834
+ }>;
835
+ }
6
836
  //# sourceMappingURL=swap.d.ts.map