diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index fd39d50f95..87681bf13c 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -157,11 +157,8 @@ import { isSpotPositionAvailable } from './math/spotPosition'; import { calculateMarketMaxAvailableInsurance } from './math/market'; import { fetchUserStatsAccount } from './accounts/fetch'; import { castNumberToSpotPrecision } from './math/spotMarket'; -import { - JupiterClient, - QuoteResponse, - SwapMode, -} from './jupiter/jupiterClient'; +import { JupiterClient, QuoteResponse } from './jupiter/jupiterClient'; +import { SwapMode } from './swap/UnifiedSwapClient'; import { getNonIdleUserFilter } from './memcmp'; import { UserStatsSubscriptionConfig } from './userStatsConfig'; import { getMarinadeDepositIx, getMarinadeFinanceProgram } from './marinade'; @@ -210,9 +207,11 @@ import { isBuilderOrderCompleted, } from './math/builder'; import { TitanClient, SwapMode as TitanSwapMode } from './titan/titanClient'; +import { UnifiedSwapClient } from './swap/UnifiedSwapClient'; /** - * Union type for swap clients (Titan and Jupiter) + * Union type for swap clients (Titan and Jupiter) - Legacy type + * @deprecated Use UnifiedSwapClient class instead */ export type SwapClient = TitanClient | JupiterClient; @@ -5772,7 +5771,7 @@ export class DriftClient { quote, onlyDirectRoutes = false, }: { - swapClient: SwapClient; + swapClient: UnifiedSwapClient | SwapClient; outMarketIndex: number; inMarketIndex: number; outAssociatedTokenAccount?: PublicKey; @@ -5793,7 +5792,23 @@ export class DriftClient { lookupTables: AddressLookupTableAccount[]; }; - if (swapClient instanceof TitanClient) { + // Use unified SwapClient if available + if (swapClient instanceof UnifiedSwapClient) { + res = await this.getSwapIxV2({ + swapClient, + outMarketIndex, + inMarketIndex, + outAssociatedTokenAccount, + inAssociatedTokenAccount, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + reduceOnly, + quote, + v6, + }); + } else if (swapClient instanceof TitanClient) { res = await this.getTitanSwapIx({ titanClient: swapClient, outMarketIndex, @@ -5823,7 +5838,7 @@ export class DriftClient { }); } else { throw new Error( - 'Invalid swap client type. Must be TitanClient or JupiterClient.' + 'Invalid swap client type. Must be SwapClient, TitanClient, or JupiterClient.' ); } @@ -6243,6 +6258,134 @@ export class DriftClient { return { beginSwapIx, endSwapIx }; } + public async getSwapIxV2({ + swapClient, + outMarketIndex, + inMarketIndex, + outAssociatedTokenAccount, + inAssociatedTokenAccount, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + reduceOnly, + quote, + v6, + }: { + swapClient: UnifiedSwapClient; + outMarketIndex: number; + inMarketIndex: number; + outAssociatedTokenAccount?: PublicKey; + inAssociatedTokenAccount?: PublicKey; + amount: BN; + slippageBps?: number; + swapMode?: SwapMode; + onlyDirectRoutes?: boolean; + reduceOnly?: SwapReduceOnly; + quote?: QuoteResponse; + v6?: { + quote?: QuoteResponse; + }; + }): Promise<{ + ixs: TransactionInstruction[]; + lookupTables: AddressLookupTableAccount[]; + }> { + // Get market accounts to determine mints + const outMarket = this.getSpotMarketAccount(outMarketIndex); + const inMarket = this.getSpotMarketAccount(inMarketIndex); + + const isExactOut = swapMode === 'ExactOut'; + const exactOutBufferedAmountIn = amount.muln(1001).divn(1000); // Add 10bp buffer + + const preInstructions: TransactionInstruction[] = []; + + // Handle token accounts if not provided + let finalOutAssociatedTokenAccount = outAssociatedTokenAccount; + let finalInAssociatedTokenAccount = inAssociatedTokenAccount; + + if (!finalOutAssociatedTokenAccount) { + const tokenProgram = this.getTokenProgramForSpotMarket(outMarket); + finalOutAssociatedTokenAccount = await this.getAssociatedTokenAccount( + outMarket.marketIndex, + false, + tokenProgram + ); + + const accountInfo = await this.connection.getAccountInfo( + finalOutAssociatedTokenAccount + ); + if (!accountInfo) { + preInstructions.push( + this.createAssociatedTokenAccountIdempotentInstruction( + finalOutAssociatedTokenAccount, + this.provider.wallet.publicKey, + this.provider.wallet.publicKey, + outMarket.mint, + tokenProgram + ) + ); + } + } + + if (!finalInAssociatedTokenAccount) { + const tokenProgram = this.getTokenProgramForSpotMarket(inMarket); + finalInAssociatedTokenAccount = await this.getAssociatedTokenAccount( + inMarket.marketIndex, + false, + tokenProgram + ); + + const accountInfo = await this.connection.getAccountInfo( + finalInAssociatedTokenAccount + ); + if (!accountInfo) { + preInstructions.push( + this.createAssociatedTokenAccountIdempotentInstruction( + finalInAssociatedTokenAccount, + this.provider.wallet.publicKey, + this.provider.wallet.publicKey, + inMarket.mint, + tokenProgram + ) + ); + } + } + + // Get drift swap instructions for begin and end + const { beginSwapIx, endSwapIx } = await this.getSwapIx({ + outMarketIndex, + inMarketIndex, + amountIn: isExactOut ? exactOutBufferedAmountIn : amount, + inTokenAccount: finalInAssociatedTokenAccount, + outTokenAccount: finalOutAssociatedTokenAccount, + reduceOnly, + }); + + // Get core swap instructions from SwapClient + const swapResult = await swapClient.getSwapInstructions({ + inputMint: inMarket.mint, + outputMint: outMarket.mint, + amount, + userPublicKey: this.provider.wallet.publicKey, + slippageBps, + swapMode, + onlyDirectRoutes, + quote: quote ?? v6?.quote, + }); + + const allInstructions = [ + ...preInstructions, + beginSwapIx, + ...swapResult.instructions, + endSwapIx, + ]; + + return { + ixs: allInstructions, + lookupTables: swapResult.lookupTables, + }; + } + public async stakeForMSOL({ amount }: { amount: BN }): Promise { const ixs = await this.getStakeForMSOLIx({ amount }); const tx = await this.buildTransaction(ixs); diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 5458898cfd..0919ebd346 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -53,7 +53,8 @@ export * from './events/webSocketLogProvider'; export * from './events/parse'; export * from './events/pollingLogProvider'; export * from './jupiter/jupiterClient'; -export { TitanClient } from './titan/titanClient'; +// Primary swap client interface - use this for all swap operations +export * from './swap/UnifiedSwapClient'; export * from './math/auction'; export * from './math/builder'; export * from './math/spotMarket'; diff --git a/sdk/src/jupiter/jupiterClient.ts b/sdk/src/jupiter/jupiterClient.ts index e943db5851..a2a2ba483b 100644 --- a/sdk/src/jupiter/jupiterClient.ts +++ b/sdk/src/jupiter/jupiterClient.ts @@ -8,8 +8,7 @@ import { } from '@solana/web3.js'; import fetch from 'node-fetch'; import { BN } from '@coral-xyz/anchor'; - -export type SwapMode = 'ExactIn' | 'ExactOut'; +import { SwapMode } from '../swap/UnifiedSwapClient'; export interface MarketInfo { id: string; diff --git a/sdk/src/swap/UnifiedSwapClient.ts b/sdk/src/swap/UnifiedSwapClient.ts new file mode 100644 index 0000000000..966f7e71bd --- /dev/null +++ b/sdk/src/swap/UnifiedSwapClient.ts @@ -0,0 +1,293 @@ +import { + Connection, + PublicKey, + TransactionMessage, + AddressLookupTableAccount, + VersionedTransaction, + TransactionInstruction, +} from '@solana/web3.js'; +import { BN } from '@coral-xyz/anchor'; +import { + JupiterClient, + QuoteResponse as JupiterQuoteResponse, +} from '../jupiter/jupiterClient'; +import { + TitanClient, + QuoteResponse as TitanQuoteResponse, + SwapMode as TitanSwapMode, +} from '../titan/titanClient'; + +export type SwapMode = 'ExactIn' | 'ExactOut'; +export type SwapClientType = 'jupiter' | 'titan'; + +export type UnifiedQuoteResponse = JupiterQuoteResponse | TitanQuoteResponse; + +export interface SwapQuoteParams { + inputMint: PublicKey; + outputMint: PublicKey; + amount: BN; + userPublicKey?: PublicKey; // Required for Titan, optional for Jupiter + maxAccounts?: number; + slippageBps?: number; + swapMode?: SwapMode; + onlyDirectRoutes?: boolean; + excludeDexes?: string[]; + sizeConstraint?: number; // Titan-specific + accountsLimitWritable?: number; // Titan-specific + autoSlippage?: boolean; // Jupiter-specific + maxAutoSlippageBps?: number; // Jupiter-specific + usdEstimate?: number; // Jupiter-specific +} + +export interface SwapTransactionParams { + quote: UnifiedQuoteResponse; + userPublicKey: PublicKey; + slippageBps?: number; +} + +export interface SwapTransactionResult { + transaction?: VersionedTransaction; // Jupiter returns this + transactionMessage?: TransactionMessage; // Titan returns this + lookupTables?: AddressLookupTableAccount[]; // Titan returns this +} + +export class UnifiedSwapClient { + private client: JupiterClient | TitanClient; + private clientType: SwapClientType; + + constructor({ + clientType, + connection, + authToken, + url, + }: { + clientType: SwapClientType; + connection: Connection; + authToken?: string; // Required for Titan, optional for Jupiter + url?: string; // Optional custom URL + }) { + this.clientType = clientType; + + if (clientType === 'jupiter') { + this.client = new JupiterClient({ + connection, + url, + }); + } else if (clientType === 'titan') { + if (!authToken) { + throw new Error('authToken is required for Titan client'); + } + this.client = new TitanClient({ + connection, + authToken, + url, + }); + } else { + throw new Error(`Unsupported client type: ${clientType}`); + } + } + + /** + * Get a swap quote from the underlying client + */ + public async getQuote( + params: SwapQuoteParams + ): Promise { + if (this.clientType === 'jupiter') { + const jupiterClient = this.client as JupiterClient; + const { + userPublicKey: _userPublicKey, // Not needed for Jupiter + sizeConstraint: _sizeConstraint, // Jupiter-specific params to exclude + accountsLimitWritable: _accountsLimitWritable, + ...jupiterParams + } = params; + + return await jupiterClient.getQuote(jupiterParams); + } else { + const titanClient = this.client as TitanClient; + const { + autoSlippage: _autoSlippage, // Titan-specific params to exclude + maxAutoSlippageBps: _maxAutoSlippageBps, + usdEstimate: _usdEstimate, + ...titanParams + } = params; + + if (!titanParams.userPublicKey) { + throw new Error('userPublicKey is required for Titan quotes'); + } + + // Cast to ensure TypeScript knows userPublicKey is defined + const titanParamsWithUser = { + ...titanParams, + userPublicKey: titanParams.userPublicKey, + swapMode: titanParams.swapMode as string, // Titan expects string + }; + + return await titanClient.getQuote(titanParamsWithUser); + } + } + + /** + * Get a swap transaction from the underlying client + */ + public async getSwap( + params: SwapTransactionParams + ): Promise { + if (this.clientType === 'jupiter') { + const jupiterClient = this.client as JupiterClient; + // Cast the quote to Jupiter's QuoteResponse type + const jupiterParams = { + ...params, + quote: params.quote as JupiterQuoteResponse, + }; + const transaction = await jupiterClient.getSwap(jupiterParams); + return { transaction }; + } else { + const titanClient = this.client as TitanClient; + const { quote, userPublicKey, slippageBps } = params; + + // For Titan, we need to reconstruct the parameters from the quote + const titanQuote = quote as TitanQuoteResponse; + const result = await titanClient.getSwap({ + inputMint: new PublicKey(titanQuote.inputMint), + outputMint: new PublicKey(titanQuote.outputMint), + amount: new BN(titanQuote.inAmount), + userPublicKey, + slippageBps: slippageBps || titanQuote.slippageBps, + swapMode: titanQuote.swapMode, + }); + + return { + transactionMessage: result.transactionMessage, + lookupTables: result.lookupTables, + }; + } + } + + /** + * Get swap instructions from the underlying client (Jupiter or Titan) + * This is the core swap logic without any context preparation + */ + public async getSwapInstructions({ + inputMint, + outputMint, + amount, + userPublicKey, + slippageBps, + swapMode = 'ExactIn', + onlyDirectRoutes = false, + quote, + sizeConstraint, + }: { + inputMint: PublicKey; + outputMint: PublicKey; + amount: BN; + userPublicKey: PublicKey; + slippageBps?: number; + swapMode?: SwapMode; + onlyDirectRoutes?: boolean; + quote?: UnifiedQuoteResponse; + sizeConstraint?: number; + }): Promise<{ + instructions: TransactionInstruction[]; + lookupTables: AddressLookupTableAccount[]; + }> { + const isExactOut = swapMode === 'ExactOut'; + let swapInstructions: TransactionInstruction[]; + let lookupTables: AddressLookupTableAccount[]; + + if (this.clientType === 'jupiter') { + const jupiterClient = this.client as JupiterClient; + + // Get quote if not provided + let finalQuote = quote as JupiterQuoteResponse; + if (!finalQuote) { + finalQuote = await jupiterClient.getQuote({ + inputMint, + outputMint, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + }); + } + + if (!finalQuote) { + throw new Error("Could not fetch Jupiter's quote. Please try again."); + } + + // Get swap transaction and extract instructions + const transaction = await jupiterClient.getSwap({ + quote: finalQuote, + userPublicKey, + slippageBps, + }); + + const { transactionMessage, lookupTables: jupiterLookupTables } = + await jupiterClient.getTransactionMessageAndLookupTables({ + transaction, + }); + + swapInstructions = jupiterClient.getJupiterInstructions({ + transactionMessage, + inputMint, + outputMint, + }); + + lookupTables = jupiterLookupTables; + } else { + const titanClient = this.client as TitanClient; + + // For Titan, get swap directly (it handles quote internally) + const { transactionMessage, lookupTables: titanLookupTables } = + await titanClient.getSwap({ + inputMint, + outputMint, + amount, + userPublicKey, + slippageBps, + swapMode: isExactOut ? TitanSwapMode.ExactOut : TitanSwapMode.ExactIn, + onlyDirectRoutes, + sizeConstraint: sizeConstraint || 1280 - 375, // MAX_TX_BYTE_SIZE - buffer for drift instructions + }); + + swapInstructions = titanClient.getTitanInstructions({ + transactionMessage, + inputMint, + outputMint, + }); + + lookupTables = titanLookupTables; + } + + return { instructions: swapInstructions, lookupTables }; + } + + /** + * Get the underlying client instance + */ + public getClient(): JupiterClient | TitanClient { + return this.client; + } + + /** + * Get the client type + */ + public getClientType(): SwapClientType { + return this.clientType; + } + + /** + * Check if this is a Jupiter client + */ + public isJupiter(): boolean { + return this.clientType === 'jupiter'; + } + + /** + * Check if this is a Titan client + */ + public isTitan(): boolean { + return this.clientType === 'titan'; + } +}