diff --git a/sdk/src/swap/UnifiedSwapClient.ts b/sdk/src/swap/UnifiedSwapClient.ts index cc40d33ad..13a05b563 100644 --- a/sdk/src/swap/UnifiedSwapClient.ts +++ b/sdk/src/swap/UnifiedSwapClient.ts @@ -143,6 +143,7 @@ export class UnifiedSwapClient { ...titanParams, userPublicKey: titanParams.userPublicKey, swapMode: titanParams.swapMode as string, // Titan expects string + sizeConstraint: titanParams.sizeConstraint || 1280 - 375, // Use same default as getSwapInstructions }; return await titanClient.getQuote(titanParamsWithUser); diff --git a/sdk/src/titan/titanClient.ts b/sdk/src/titan/titanClient.ts index ac8f2633f..cff88b343 100644 --- a/sdk/src/titan/titanClient.ts +++ b/sdk/src/titan/titanClient.ts @@ -94,6 +94,8 @@ export class TitanClient { url: string; connection: Connection; proxyUrl?: string; + private lastQuoteData?: SwapQuotes; + private lastQuoteParams?: string; constructor({ connection, @@ -112,15 +114,12 @@ export class TitanClient { this.proxyUrl = proxyUrl; } - /** - * Get routes for a swap - */ - public async getQuote({ + private buildParams({ inputMint, outputMint, amount, userPublicKey, - maxAccounts = 50, // 50 is an estimated amount with buffer + maxAccounts, slippageBps, swapMode, onlyDirectRoutes, @@ -134,32 +133,77 @@ export class TitanClient { userPublicKey: PublicKey; maxAccounts?: number; slippageBps?: number; - swapMode?: string; + swapMode?: string | SwapMode; onlyDirectRoutes?: boolean; excludeDexes?: string[]; sizeConstraint?: number; accountsLimitWritable?: number; - }): Promise { - const params = new URLSearchParams({ + }): URLSearchParams { + // Normalize swapMode to enum value + const normalizedSwapMode = swapMode === 'ExactOut' || swapMode === SwapMode.ExactOut + ? SwapMode.ExactOut + : SwapMode.ExactIn; + + return new URLSearchParams({ inputMint: inputMint.toString(), outputMint: outputMint.toString(), amount: amount.toString(), userPublicKey: userPublicKey.toString(), ...(slippageBps && { slippageBps: slippageBps.toString() }), - ...(swapMode && { - swapMode: - swapMode === 'ExactOut' ? SwapMode.ExactOut : SwapMode.ExactIn, - }), + ...(swapMode && { swapMode: normalizedSwapMode }), + ...(maxAccounts && { accountsLimitTotal: maxAccounts.toString() }), + ...(excludeDexes && { excludeDexes: excludeDexes.join(',') }), ...(onlyDirectRoutes && { onlyDirectRoutes: onlyDirectRoutes.toString(), }), - ...(maxAccounts && { accountsLimitTotal: maxAccounts.toString() }), - ...(excludeDexes && { excludeDexes: excludeDexes.join(',') }), ...(sizeConstraint && { sizeConstraint: sizeConstraint.toString() }), ...(accountsLimitWritable && { accountsLimitWritable: accountsLimitWritable.toString(), }), }); + } + + /** + * Get routes for a swap + */ + public async getQuote({ + inputMint, + outputMint, + amount, + userPublicKey, + maxAccounts = 50, // 50 is an estimated amount with buffer + slippageBps, + swapMode, + onlyDirectRoutes, + excludeDexes, + sizeConstraint, + accountsLimitWritable, + }: { + inputMint: PublicKey; + outputMint: PublicKey; + amount: BN; + userPublicKey: PublicKey; + maxAccounts?: number; + slippageBps?: number; + swapMode?: string; + onlyDirectRoutes?: boolean; + excludeDexes?: string[]; + sizeConstraint?: number; + accountsLimitWritable?: number; + }): Promise { + const params = this.buildParams({ + inputMint, + outputMint, + amount, + userPublicKey, + maxAccounts, + slippageBps, + swapMode, + onlyDirectRoutes, + excludeDexes, + sizeConstraint, + accountsLimitWritable, + }); let response: Response; @@ -195,6 +239,10 @@ export class TitanClient { const buffer = await response.arrayBuffer(); const data = decode(buffer) as SwapQuotes; + // Cache the quote data and parameters for later use in getSwap + this.lastQuoteData = data; + this.lastQuoteParams = params.toString(); + const route = data.quotes[ Object.keys(data.quotes).find((key) => key.toLowerCase() === 'titan') || @@ -268,61 +316,28 @@ export class TitanClient { transactionMessage: TransactionMessage; lookupTables: AddressLookupTableAccount[]; }> { - const params = new URLSearchParams({ - inputMint: inputMint.toString(), - outputMint: outputMint.toString(), - amount: amount.toString(), - userPublicKey: userPublicKey.toString(), - ...(slippageBps && { slippageBps: slippageBps.toString() }), - ...(swapMode && { swapMode: swapMode }), - ...(maxAccounts && { accountsLimitTotal: maxAccounts.toString() }), - ...(excludeDexes && { excludeDexes: excludeDexes.join(',') }), - ...(onlyDirectRoutes && { - onlyDirectRoutes: onlyDirectRoutes.toString(), - }), - ...(sizeConstraint && { sizeConstraint: sizeConstraint.toString() }), - ...(accountsLimitWritable && { - accountsLimitWritable: accountsLimitWritable.toString(), - }), + const params = this.buildParams({ + inputMint, + outputMint, + amount, + userPublicKey, + maxAccounts, + slippageBps, + swapMode, + onlyDirectRoutes, + excludeDexes, + sizeConstraint, + accountsLimitWritable, }); - let response: Response; - - if (this.proxyUrl) { - // Use proxy route - send parameters in request body - response = await fetch(this.proxyUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(Object.fromEntries(params.entries())), - }); - } else { - // Direct request to Titan API - response = await fetch( - `${this.url}/api/v1/quote/swap?${params.toString()}`, - { - headers: { - Accept: 'application/vnd.msgpack', - 'Accept-Encoding': 'gzip, deflate, br', - Authorization: `Bearer ${this.authToken}`, - }, - } - ); - } - - if (!response.ok) { - if (response.status === 404) { - throw new Error('No routes available'); - } - throw new Error( - `Titan API error: ${response.status} ${response.statusText}` - ); + // Check if we have cached quote data that matches the current parameters + if (!this.lastQuoteData || this.lastQuoteParams !== params.toString()) { + throw new Error('No matching quote data found. Please get a fresh quote before attempting to swap.'); } - const buffer = await response.arrayBuffer(); - const data = decode(buffer) as SwapQuotes; - + // Reuse the cached quote data + const data = this.lastQuoteData; + const route = data.quotes[ Object.keys(data.quotes).find((key) => key.toLowerCase() === 'titan') || @@ -332,7 +347,7 @@ export class TitanClient { if (!route) { throw new Error('No routes available'); } - + if (route.instructions && route.instructions.length > 0) { try { const { transactionMessage, lookupTables } = @@ -342,6 +357,10 @@ export class TitanClient { throw new Error( 'Something went wrong with creating the Titan swap transaction. Please try again.' ); + } finally { + // Clear cached quote data after use + this.lastQuoteData = undefined; + this.lastQuoteParams = undefined; } } throw new Error('No instructions provided in the route');