Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sdk/src/swap/UnifiedSwapClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
151 changes: 85 additions & 66 deletions sdk/src/titan/titanClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ export class TitanClient {
url: string;
connection: Connection;
proxyUrl?: string;
private lastQuoteData?: SwapQuotes;
private lastQuoteParams?: string;

constructor({
connection,
Expand All @@ -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,
Expand All @@ -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<QuoteResponse> {
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<QuoteResponse> {
const params = this.buildParams({
inputMint,
outputMint,
amount,
userPublicKey,
maxAccounts,
slippageBps,
swapMode,
onlyDirectRoutes,
excludeDexes,
sizeConstraint,
accountsLimitWritable,
});

let response: Response;

Expand Down Expand Up @@ -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') ||
Expand Down Expand Up @@ -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') ||
Expand All @@ -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 } =
Expand All @@ -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');
Expand Down
Loading