@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/README.md +274 -0
- package/dist/esm/client/SwapClient.js +351 -0
- package/dist/esm/client/SwapClient.js.map +1 -0
- package/dist/esm/examples/usage.js +239 -0
- package/dist/esm/examples/usage.js.map +1 -0
- package/dist/esm/index.js +11 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/types/index.js +5 -0
- package/dist/esm/types/index.js.map +1 -0
- package/dist/esm/utils/index.js +60 -0
- package/dist/esm/utils/index.js.map +1 -0
- package/dist/types/client/SwapClient.d.ts +159 -0
- package/dist/types/client/SwapClient.d.ts.map +1 -0
- package/dist/types/examples/usage.d.ts +17 -0
- package/dist/types/examples/usage.d.ts.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +34 -0
- package/dist/types/types/index.d.ts.map +1 -0
- package/dist/types/utils/index.d.ts +27 -0
- package/dist/types/utils/index.d.ts.map +1 -0
- package/package.json +58 -0
- package/src/client/SwapClient.ts +490 -0
- package/src/examples/usage.ts +261 -0
- package/src/index.ts +12 -0
- package/src/types/index.ts +36 -0
- package/src/utils/index.ts +77 -0
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
|
+
}
|