@drift-labs/sdk-browser
Version:
SDK for Drift Protocol
316 lines (282 loc) • 8.38 kB
text/typescript
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';
/**
* Unified quote response interface that combines properties from both Jupiter and Titan
* This provides a consistent interface while allowing for provider-specific fields
*/
export interface UnifiedQuoteResponse {
// Core properties available in both providers
inputMint: string;
inAmount: string;
outputMint: string;
outAmount: string;
swapMode: SwapMode;
slippageBps: number;
routePlan: Array<{ swapInfo: any; percent: number }>;
// Optional properties that may not be available in all providers
otherAmountThreshold?: string; // Jupiter has this, Titan doesn't
priceImpactPct?: string; // Jupiter provides this, Titan doesn't (we calculate it)
platformFee?: { amount?: string; feeBps?: number }; // Format varies between providers
contextSlot?: number;
timeTaken?: number;
error?: string;
errorCode?: string;
}
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<UnifiedQuoteResponse> {
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<SwapTransactionResult> {
if (this.clientType === 'jupiter') {
const jupiterClient = this.client as JupiterClient;
// Cast the quote to Jupiter's specific 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 swap 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';
}
}