@shogun-sdk/swap 0.1.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.
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@shogun-sdk/swap",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Shogun Network Swap utilities and helpers",
6
+ "author": "Shogun network",
7
+ "license": "ISC",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/shogun-network/shogun-sdk.git"
14
+ },
15
+ "main": "./dist/esm/index.js",
16
+ "types": "./dist/types/index.d.ts",
17
+ "typings": "./dist/types/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/types/index.d.ts",
21
+ "default": "./dist/esm/index.js"
22
+ },
23
+ "./package.json": "./package.json"
24
+ },
25
+ "sideEffects": false,
26
+ "files": [
27
+ "dist/**",
28
+ "!dist/**/*.tsbuildinfo",
29
+ "src/**/*.ts",
30
+ "!src/**/*.test.ts",
31
+ "!src/**/*.test-d.ts",
32
+ "README.md"
33
+ ],
34
+ "keywords": [
35
+ "shogun",
36
+ "swap",
37
+ "sdk",
38
+ "api",
39
+ "typescript"
40
+ ],
41
+ "devDependencies": {
42
+ "@types/node": "22.13.10",
43
+ "typescript": "5.8.2"
44
+ },
45
+ "dependencies": {
46
+ "viem": "2.31.0",
47
+ "@shogun-sdk/money-legos": "1.3.51",
48
+ "@shogun-sdk/intents-sdk": "1.2.4"
49
+ },
50
+ "scripts": {
51
+ "build": "pnpm clean && pnpm build:esm+types",
52
+ "build:esm+types": "tsc --project ./tsconfig.build.json --outDir ./dist/esm --declaration --declarationMap --declarationDir ./dist/types",
53
+ "clean": "rm -rf ./dist",
54
+ "check:types": "tsc --noEmit --project ./tsconfig.build.json",
55
+ "lint": "npx eslint --fix --ext .ts ./src",
56
+ "format": "prettier --write ./src"
57
+ }
58
+ }
@@ -0,0 +1,490 @@
1
+ /**
2
+ * SwapClient - Unified client for fetching quotes from multiple sources
3
+ *
4
+ * This client combines the QuoteProvider from intents-sdk and the OneShotClient
5
+ * from money-legos to provide a unified interface for quote fetching with fallback logic.
6
+ *
7
+ * Features:
8
+ * - Intelligent fallback from intents-sdk to one-shot API
9
+ * - Comprehensive error handling and retry logic
10
+ * - Support for all major chains and protocols
11
+ * - Built-in validation and type safety
12
+ * - Configurable timeouts and retry policies
13
+ */
14
+
15
+ import { QuoteProvider, type IntentsQuoteParams } from '@shogun-sdk/intents-sdk';
16
+ import { compareAddresses, fetchQuote, type QuoteParams as OneShotQuoteParams } from '@shogun-sdk/money-legos';
17
+ import { ChainID } from '@shogun-sdk/intents-sdk';
18
+
19
+ export interface SwapClientConfig {
20
+ /** API key for one-shot service */
21
+ apiKey: string;
22
+ /** API URL for one-shot service */
23
+ apiUrl: string;
24
+ /** Optional timeout for requests in milliseconds */
25
+ timeout?: number;
26
+ /** Maximum number of retry attempts for failed requests */
27
+ maxRetries?: number;
28
+ /** Delay between retry attempts in milliseconds */
29
+ retryDelay?: number;
30
+ /** Whether to enable debug logging */
31
+ debug?: boolean;
32
+ /** Custom headers to include with requests */
33
+ headers?: Record<string, string>;
34
+ /** Whether to prefer intents-sdk over one-shot API */
35
+ preferIntentsSdk?: boolean;
36
+ }
37
+
38
+ export interface UnifiedQuoteParams {
39
+ /** Source chain ID */
40
+ sourceChainId: ChainID;
41
+ /** Destination chain ID */
42
+ destChainId: ChainID;
43
+ /** Input token address */
44
+ tokenIn: string;
45
+ /** Output token address */
46
+ tokenOut: string;
47
+ /** Amount to swap (in smallest token units) */
48
+ amount: bigint;
49
+ /** Sender address */
50
+ senderAddress?: string;
51
+ /** Destination address */
52
+ destinationAddress?: string;
53
+ /** Slippage tolerance in basis points (optional) */
54
+ slippageBps?: number;
55
+ /** Affiliate wallet address (optional) */
56
+ affiliateWallet?: string;
57
+ /** Affiliate fee (optional) */
58
+ affiliateFee?: string;
59
+ /** Jito tip for Solana (optional) */
60
+ jitoTip?: number;
61
+ /** Gas refuel amount (optional) */
62
+ gasRefuel?: number;
63
+ /** Dynamic slippage flag (optional) */
64
+ dynamicSlippage?: boolean;
65
+ /** External call data (optional) */
66
+ externalCall?: string;
67
+ }
68
+
69
+ export interface UnifiedQuoteResponse {
70
+ /** Expected output amount */
71
+ amountOut: bigint;
72
+ /** Amount in USD */
73
+ amountInUsd: number;
74
+ /** Amount out in USD */
75
+ amountOutUsd: number;
76
+ /** Reduced amount out (with slippage) */
77
+ amountOutReduced?: bigint;
78
+ /** Reduced amount out in USD */
79
+ amountOutUsdReduced?: number;
80
+ /** Minimum stablecoin amount */
81
+ estimatedAmountInAsMinStablecoinAmount?: bigint;
82
+ /** Price impact percentage */
83
+ priceImpact: number;
84
+ /** Slippage percentage */
85
+ slippage: number;
86
+ /** Provider used for the quote */
87
+ provider: string;
88
+ /** Raw quote data */
89
+ rawQuote: any;
90
+ /** Source of the quote */
91
+ source: 'intents-sdk' | 'one-shot';
92
+ /** Error message if any */
93
+ error?: string;
94
+ }
95
+
96
+ export class SwapClient {
97
+ private readonly config: Required<SwapClientConfig>;
98
+
99
+ constructor(config: SwapClientConfig) {
100
+ this.config = {
101
+ timeout: 30000,
102
+ maxRetries: 3,
103
+ retryDelay: 1000,
104
+ debug: false,
105
+ headers: {},
106
+ preferIntentsSdk: true,
107
+ ...config,
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Validates the client configuration
113
+ */
114
+ private validateConfig(): void {
115
+ if (!this.config.apiKey) {
116
+ throw new Error('API key is required');
117
+ }
118
+ if (!this.config.apiUrl) {
119
+ throw new Error('API URL is required');
120
+ }
121
+ if (this.config.timeout <= 0) {
122
+ throw new Error('Timeout must be greater than 0');
123
+ }
124
+ if (this.config.maxRetries < 0) {
125
+ throw new Error('Max retries cannot be negative');
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Logs debug messages if debug mode is enabled
131
+ */
132
+ private debug(message: string, data?: any): void {
133
+ if (this.config.debug) {
134
+ console.log(`[SwapClient] ${message}`, data || '');
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Sleeps for the specified number of milliseconds
140
+ */
141
+ private sleep(ms: number): Promise<void> {
142
+ return new Promise(resolve => setTimeout(resolve, ms));
143
+ }
144
+
145
+ /**
146
+ * Validates quote parameters
147
+ */
148
+ private validateQuoteParams(params: UnifiedQuoteParams): void {
149
+ if (!params.sourceChainId || !params.destChainId) {
150
+ throw new Error('Source and destination chain IDs are required');
151
+ }
152
+ if (!params.tokenIn || !params.tokenOut) {
153
+ throw new Error('Token addresses are required');
154
+ }
155
+ if (params.amount <= 0n) {
156
+ throw new Error('Amount must be greater than 0');
157
+ }
158
+ if (compareAddresses(params.tokenIn, params.tokenOut) && params.sourceChainId === params.destChainId) {
159
+ throw new Error('Input and output tokens cannot be the same');
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Fetches a quote using the unified interface
165
+ * First tries intents-sdk QuoteProvider, then falls back to one-shot API
166
+ *
167
+ * @param params Quote parameters
168
+ * @param signal Optional AbortSignal for cancelling the request
169
+ * @returns Promise<UnifiedQuoteResponse>
170
+ */
171
+ public async getQuote(params: UnifiedQuoteParams, signal?: AbortSignal): Promise<UnifiedQuoteResponse> {
172
+ this.validateConfig();
173
+ this.validateQuoteParams(params);
174
+
175
+ this.debug('Fetching quote', { params });
176
+
177
+ // Determine quote source order based on configuration
178
+ const sources = this.config.preferIntentsSdk
179
+ ? ['intents-sdk', 'one-shot']
180
+ : ['one-shot', 'intents-sdk'];
181
+
182
+ for (const source of sources) {
183
+ for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
184
+ try {
185
+ this.debug(`Attempting quote from ${source} (attempt ${attempt + 1})`);
186
+
187
+ let quote: UnifiedQuoteResponse;
188
+ if (source === 'intents-sdk') {
189
+ quote = await this.getIntentsQuote(params, signal);
190
+ } else {
191
+ quote = await this.getOneShotQuote(params, signal);
192
+ }
193
+
194
+ if (quote && !quote.error && quote.amountOut > 0n) {
195
+ this.debug(`Successfully got quote from ${source}`, { amountOut: quote.amountOut.toString() });
196
+ return quote;
197
+ }
198
+
199
+ if (attempt < this.config.maxRetries) {
200
+ this.debug(`Quote attempt ${attempt + 1} failed, retrying in ${this.config.retryDelay}ms`);
201
+ await this.sleep(this.config.retryDelay);
202
+ }
203
+ } catch (error) {
204
+ this.debug(`Quote attempt ${attempt + 1} from ${source} failed`, error);
205
+
206
+ if (attempt < this.config.maxRetries) {
207
+ await this.sleep(this.config.retryDelay);
208
+ } else if (source === sources[sources.length - 1]) {
209
+ // Last source and last attempt
210
+ return {
211
+ amountOut: 0n,
212
+ amountInUsd: 0,
213
+ amountOutUsd: 0,
214
+ priceImpact: 0,
215
+ slippage: 0,
216
+ provider: 'none',
217
+ rawQuote: null,
218
+ source: source as 'intents-sdk' | 'one-shot',
219
+ error: error instanceof Error ? error.message : 'Unknown error occurred'
220
+ };
221
+ }
222
+ }
223
+ }
224
+ }
225
+
226
+ // This should never be reached, but just in case
227
+ return {
228
+ amountOut: 0n,
229
+ amountInUsd: 0,
230
+ amountOutUsd: 0,
231
+ priceImpact: 0,
232
+ slippage: 0,
233
+ provider: 'none',
234
+ rawQuote: null,
235
+ source: 'one-shot',
236
+ error: 'All quote sources failed'
237
+ };
238
+ }
239
+
240
+ /**
241
+ * Gets quote from intents-sdk QuoteProvider
242
+ */
243
+ private async getIntentsQuote(params: UnifiedQuoteParams, _signal?: AbortSignal): Promise<UnifiedQuoteResponse> {
244
+ const intentsParams: IntentsQuoteParams = {
245
+ sourceChainId: params.sourceChainId,
246
+ destChainId: params.destChainId,
247
+ amount: params.amount,
248
+ tokenIn: params.tokenIn,
249
+ tokenOut: params.tokenOut,
250
+ };
251
+
252
+ const quote = await QuoteProvider.getQuote(intentsParams);
253
+
254
+ return {
255
+ amountOut: quote.estimatedAmountOut,
256
+ amountInUsd: quote.amountInUsd,
257
+ amountOutUsd: quote.estimatedAmountOutUsd,
258
+ amountOutReduced: quote.estimatedAmountOutReduced,
259
+ amountOutUsdReduced: quote.estimatedAmountOutUsdReduced,
260
+ estimatedAmountInAsMinStablecoinAmount: quote.estimatedAmountInAsMinStablecoinAmount,
261
+ priceImpact: 0, // QuoteProvider doesn't provide price impact directly
262
+ slippage: 0, // QuoteProvider doesn't provide slippage directly
263
+ provider: 'intents-sdk',
264
+ rawQuote: quote,
265
+ source: 'intents-sdk'
266
+ };
267
+ }
268
+
269
+ /**
270
+ * Gets quote from one-shot API
271
+ */
272
+ private async getOneShotQuote(params: UnifiedQuoteParams, signal?: AbortSignal): Promise<UnifiedQuoteResponse> {
273
+ const oneShotParams: OneShotQuoteParams = {
274
+ srcChain: params.sourceChainId,
275
+ destChain: params.destChainId,
276
+ srcToken: params.tokenIn,
277
+ destToken: params.tokenOut,
278
+ amount: params.amount.toString(),
279
+ senderAddress: params.senderAddress || '',
280
+ affiliateWallet: params.affiliateWallet || '',
281
+ affiliateFee: params.affiliateFee || '0',
282
+ slippage: params.slippageBps ? params.slippageBps / 100 : 0.5, // Convert bps to percentage
283
+ destinationAddress: params.destinationAddress || '',
284
+ dstChainOrderAuthorityAddress: '',
285
+ jitoTip: params.jitoTip,
286
+ gasRefuel: params.gasRefuel,
287
+ dynamicSlippage: params.dynamicSlippage,
288
+ externalCall: params.externalCall,
289
+ };
290
+
291
+ const quote = await fetchQuote(
292
+ {
293
+ key: this.config.apiKey,
294
+ url: this.config.apiUrl,
295
+ },
296
+ oneShotParams,
297
+ signal || new AbortController().signal,
298
+ );
299
+
300
+ if (quote.error) {
301
+ throw new Error(quote.error);
302
+ }
303
+
304
+ return {
305
+ amountOut: BigInt(quote.outputAmount.value),
306
+ amountInUsd: 0, // One-shot doesn't provide USD values directly
307
+ amountOutUsd: 0,
308
+ priceImpact: 0, // One-shot doesn't provide price impact directly
309
+ slippage: oneShotParams.slippage,
310
+ provider: 'one-shot',
311
+ rawQuote: quote,
312
+ source: 'one-shot'
313
+ };
314
+ }
315
+
316
+ /**
317
+ * Gets a single-chain quote from intents-sdk
318
+ * Useful for same-chain swaps
319
+ */
320
+ public async getSingleChainQuote(
321
+ chainId: ChainID,
322
+ tokenIn: string,
323
+ tokenOut: string,
324
+ amount: bigint,
325
+ slippageBps?: number,
326
+ _signal?: AbortSignal
327
+ ): Promise<UnifiedQuoteResponse> {
328
+ this.validateConfig();
329
+
330
+ if (!chainId || !tokenIn || !tokenOut || amount <= 0n) {
331
+ throw new Error('Invalid parameters for single-chain quote');
332
+ }
333
+
334
+ this.debug('Fetching single-chain quote', { chainId, tokenIn, tokenOut, amount: amount.toString() });
335
+
336
+ try {
337
+ const quote = await QuoteProvider.getSingleChainQuote({
338
+ chainId,
339
+ amount,
340
+ tokenIn,
341
+ tokenOut,
342
+ slippageBps,
343
+ });
344
+
345
+ this.debug('Single-chain quote successful', { amountOut: quote.amountOut.toString() });
346
+
347
+ return {
348
+ amountOut: quote.amountOut,
349
+ amountInUsd: quote.amountInUsd,
350
+ amountOutUsd: quote.amountOutUsd,
351
+ priceImpact: quote.priceImpact,
352
+ slippage: quote.slippage,
353
+ provider: quote.provider,
354
+ rawQuote: quote.rawQuote,
355
+ source: 'intents-sdk'
356
+ };
357
+ } catch (error) {
358
+ this.debug('Single-chain quote failed', error);
359
+ return {
360
+ amountOut: 0n,
361
+ amountInUsd: 0,
362
+ amountOutUsd: 0,
363
+ priceImpact: 0,
364
+ slippage: 0,
365
+ provider: 'none',
366
+ rawQuote: null,
367
+ source: 'intents-sdk',
368
+ error: error instanceof Error ? error.message : 'Unknown error occurred'
369
+ };
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Gets multiple quotes for comparison
375
+ * Useful for finding the best route
376
+ */
377
+ public async getMultipleQuotes(
378
+ params: UnifiedQuoteParams[],
379
+ signal?: AbortSignal
380
+ ): Promise<UnifiedQuoteResponse[]> {
381
+ this.debug('Fetching multiple quotes', { count: params.length });
382
+
383
+ const promises = params.map(param => this.getQuote(param, signal));
384
+ const results = await Promise.allSettled(promises);
385
+
386
+ return results.map((result) => {
387
+ if (result.status === 'fulfilled') {
388
+ return result.value;
389
+ } else {
390
+ return {
391
+ amountOut: 0n,
392
+ amountInUsd: 0,
393
+ amountOutUsd: 0,
394
+ priceImpact: 0,
395
+ slippage: 0,
396
+ provider: 'none',
397
+ rawQuote: null,
398
+ source: 'one-shot',
399
+ error: result.reason instanceof Error ? result.reason.message : 'Unknown error occurred'
400
+ };
401
+ }
402
+ });
403
+ }
404
+
405
+ /**
406
+ * Gets the best quote from multiple options
407
+ */
408
+ public async getBestQuote(
409
+ params: UnifiedQuoteParams[],
410
+ signal?: AbortSignal
411
+ ): Promise<UnifiedQuoteResponse | null> {
412
+ const quotes = await this.getMultipleQuotes(params, signal);
413
+ const validQuotes = quotes.filter(quote => !quote.error && quote.amountOut > 0n);
414
+
415
+ if (validQuotes.length === 0) {
416
+ return null;
417
+ }
418
+
419
+ // Return the quote with the highest output amount
420
+ return validQuotes.reduce((best, current) =>
421
+ current.amountOut > best.amountOut ? current : best
422
+ );
423
+ }
424
+
425
+ /**
426
+ * Estimates gas costs for a swap (EVM chains only)
427
+ */
428
+ public async estimateGas(
429
+ chainId: ChainID,
430
+ tokenIn: string,
431
+ tokenOut: string,
432
+ amount: bigint,
433
+ _userAddress: string
434
+ ): Promise<{ gasEstimate: bigint; gasPrice: bigint } | null> {
435
+ this.debug('Estimating gas', { chainId, tokenIn, tokenOut, amount: amount.toString() });
436
+
437
+ try {
438
+ // This would need to be implemented based on your specific needs
439
+ // For now, return a placeholder
440
+ return {
441
+ gasEstimate: 200000n, // Typical swap gas limit
442
+ gasPrice: 20000000000n // 20 gwei
443
+ };
444
+ } catch (error) {
445
+ this.debug('Gas estimation failed', error);
446
+ return null;
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Validates if a token pair is supported for swapping
452
+ */
453
+ public async isTokenPairSupported(
454
+ chainId: ChainID,
455
+ tokenIn: string,
456
+ tokenOut: string
457
+ ): Promise<boolean> {
458
+ try {
459
+ // Try to get a minimal quote to check if the pair is supported
460
+ const quote = await this.getSingleChainQuote(
461
+ chainId,
462
+ tokenIn,
463
+ tokenOut,
464
+ BigInt('1'), // Minimal amount
465
+ 1000 // 10% slippage for validation
466
+ );
467
+
468
+ return !quote.error && quote.amountOut > 0n;
469
+ } catch (error) {
470
+ this.debug('Token pair validation failed', error);
471
+ return false;
472
+ }
473
+ }
474
+
475
+ /**
476
+ * Updates the client configuration
477
+ */
478
+ public updateConfig(newConfig: Partial<SwapClientConfig>): void {
479
+ Object.assign(this.config, newConfig);
480
+ this.validateConfig();
481
+ this.debug('Configuration updated', newConfig);
482
+ }
483
+
484
+ /**
485
+ * Gets the current configuration
486
+ */
487
+ public getConfig(): SwapClientConfig {
488
+ return { ...this.config };
489
+ }
490
+ }