@drift-labs/sdk 2.144.0-beta.2 → 2.144.0-beta.3

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.
@@ -0,0 +1,414 @@
1
+ import {
2
+ Connection,
3
+ PublicKey,
4
+ TransactionMessage,
5
+ AddressLookupTableAccount,
6
+ TransactionInstruction,
7
+ } from '@solana/web3.js';
8
+ import { BN } from '@coral-xyz/anchor';
9
+ import { decode } from '@msgpack/msgpack';
10
+
11
+ export enum SwapMode {
12
+ ExactIn = 'ExactIn',
13
+ ExactOut = 'ExactOut',
14
+ }
15
+
16
+ interface RoutePlanStep {
17
+ ammKey: Uint8Array;
18
+ label: string;
19
+ inputMint: Uint8Array;
20
+ outputMint: Uint8Array;
21
+ inAmount: number;
22
+ outAmount: number;
23
+ allocPpb: number;
24
+ feeMint?: Uint8Array;
25
+ feeAmount?: number;
26
+ contextSlot?: number;
27
+ }
28
+
29
+ interface PlatformFee {
30
+ amount: number;
31
+ fee_bps: number;
32
+ }
33
+
34
+ type Pubkey = Uint8Array;
35
+
36
+ interface AccountMeta {
37
+ p: Pubkey;
38
+ s: boolean;
39
+ w: boolean;
40
+ }
41
+
42
+ interface Instruction {
43
+ p: Pubkey;
44
+ a: AccountMeta[];
45
+ d: Uint8Array;
46
+ }
47
+
48
+ interface SwapRoute {
49
+ inAmount: number;
50
+ outAmount: number;
51
+ slippageBps: number;
52
+ platformFee?: PlatformFee;
53
+ steps: RoutePlanStep[];
54
+ instructions: Instruction[];
55
+ addressLookupTables: Pubkey[];
56
+ contextSlot?: number;
57
+ timeTaken?: number;
58
+ expiresAtMs?: number;
59
+ expiresAfterSlot?: number;
60
+ computeUnits?: number;
61
+ computeUnitsSafe?: number;
62
+ transaction?: Uint8Array;
63
+ referenceId?: string;
64
+ }
65
+
66
+ interface SwapQuotes {
67
+ id: string;
68
+ inputMint: Uint8Array;
69
+ outputMint: Uint8Array;
70
+ swapMode: SwapMode;
71
+ amount: number;
72
+ quotes: { [key: string]: SwapRoute };
73
+ }
74
+
75
+ export interface QuoteResponse {
76
+ inputMint: string;
77
+ inAmount: string;
78
+ outputMint: string;
79
+ outAmount: string;
80
+ swapMode: SwapMode;
81
+ slippageBps: number;
82
+ platformFee?: { amount?: string; feeBps?: number };
83
+ routePlan: Array<{ swapInfo: any; percent: number }>;
84
+ contextSlot?: number;
85
+ timeTaken?: number;
86
+ error?: string;
87
+ errorCode?: string;
88
+ }
89
+
90
+ const TITAN_API_URL = 'https://api.titan.exchange';
91
+
92
+ export class TitanClient {
93
+ authToken: string;
94
+ url: string;
95
+ connection: Connection;
96
+
97
+ constructor({
98
+ connection,
99
+ authToken,
100
+ url,
101
+ }: {
102
+ connection: Connection;
103
+ authToken: string;
104
+ url?: string;
105
+ }) {
106
+ this.connection = connection;
107
+ this.authToken = authToken;
108
+ this.url = url ?? TITAN_API_URL;
109
+ }
110
+
111
+ /**
112
+ * Get routes for a swap
113
+ */
114
+ public async getQuote({
115
+ inputMint,
116
+ outputMint,
117
+ amount,
118
+ userPublicKey,
119
+ maxAccounts = 50, // 50 is an estimated amount with buffer
120
+ slippageBps,
121
+ swapMode,
122
+ onlyDirectRoutes,
123
+ excludeDexes,
124
+ sizeConstraint,
125
+ accountsLimitWritable,
126
+ }: {
127
+ inputMint: PublicKey;
128
+ outputMint: PublicKey;
129
+ amount: BN;
130
+ userPublicKey: PublicKey;
131
+ maxAccounts?: number;
132
+ slippageBps?: number;
133
+ swapMode?: string;
134
+ onlyDirectRoutes?: boolean;
135
+ excludeDexes?: string[];
136
+ sizeConstraint?: number;
137
+ accountsLimitWritable?: number;
138
+ }): Promise<QuoteResponse> {
139
+ const params = new URLSearchParams({
140
+ inputMint: inputMint.toString(),
141
+ outputMint: outputMint.toString(),
142
+ amount: amount.toString(),
143
+ userPublicKey: userPublicKey.toString(),
144
+ ...(slippageBps && { slippageBps: slippageBps.toString() }),
145
+ ...(swapMode && {
146
+ swapMode:
147
+ swapMode === 'ExactOut' ? SwapMode.ExactOut : SwapMode.ExactIn,
148
+ }),
149
+ ...(onlyDirectRoutes && {
150
+ onlyDirectRoutes: onlyDirectRoutes.toString(),
151
+ }),
152
+ ...(maxAccounts && { accountsLimitTotal: maxAccounts.toString() }),
153
+ ...(excludeDexes && { excludeDexes: excludeDexes.join(',') }),
154
+ ...(sizeConstraint && { sizeConstraint: sizeConstraint.toString() }),
155
+ ...(accountsLimitWritable && {
156
+ accountsLimitWritable: accountsLimitWritable.toString(),
157
+ }),
158
+ });
159
+
160
+ const response = await fetch(
161
+ `${this.url}/api/v1/quote/swap?${params.toString()}`,
162
+ {
163
+ headers: {
164
+ Accept: 'application/vnd.msgpack',
165
+ 'Accept-Encoding': 'gzip, deflate, br',
166
+ Authorization: `Bearer ${this.authToken}`,
167
+ },
168
+ }
169
+ );
170
+
171
+ if (!response.ok) {
172
+ throw new Error(
173
+ `Titan API error: ${response.status} ${response.statusText}`
174
+ );
175
+ }
176
+
177
+ const buffer = await response.arrayBuffer();
178
+ const data = decode(buffer) as SwapQuotes;
179
+
180
+ const route =
181
+ data.quotes[
182
+ Object.keys(data.quotes).find((key) => key.toLowerCase() === 'titan') ||
183
+ ''
184
+ ];
185
+
186
+ if (!route) {
187
+ throw new Error('No routes available');
188
+ }
189
+
190
+ return {
191
+ inputMint: inputMint.toString(),
192
+ inAmount: amount.toString(),
193
+ outputMint: outputMint.toString(),
194
+ outAmount: route.outAmount.toString(),
195
+ swapMode: data.swapMode,
196
+ slippageBps: route.slippageBps,
197
+ platformFee: route.platformFee
198
+ ? {
199
+ amount: route.platformFee.amount.toString(),
200
+ feeBps: route.platformFee.fee_bps,
201
+ }
202
+ : undefined,
203
+ routePlan:
204
+ route.steps?.map((step: any) => ({
205
+ swapInfo: {
206
+ ammKey: new PublicKey(step.ammKey).toString(),
207
+ label: step.label,
208
+ inputMint: new PublicKey(step.inputMint).toString(),
209
+ outputMint: new PublicKey(step.outputMint).toString(),
210
+ inAmount: step.inAmount.toString(),
211
+ outAmount: step.outAmount.toString(),
212
+ feeAmount: step.feeAmount?.toString() || '0',
213
+ feeMint: step.feeMint ? new PublicKey(step.feeMint).toString() : '',
214
+ },
215
+ percent: 100,
216
+ })) || [],
217
+ contextSlot: route.contextSlot,
218
+ timeTaken: route.timeTaken,
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Get a swap transaction for quote
224
+ */
225
+ public async getSwap({
226
+ inputMint,
227
+ outputMint,
228
+ amount,
229
+ userPublicKey,
230
+ maxAccounts = 50, // 50 is an estimated amount with buffer
231
+ slippageBps,
232
+ swapMode,
233
+ onlyDirectRoutes,
234
+ excludeDexes,
235
+ sizeConstraint,
236
+ accountsLimitWritable,
237
+ }: {
238
+ inputMint: PublicKey;
239
+ outputMint: PublicKey;
240
+ amount: BN;
241
+ userPublicKey: PublicKey;
242
+ maxAccounts?: number;
243
+ slippageBps?: number;
244
+ swapMode?: SwapMode;
245
+ onlyDirectRoutes?: boolean;
246
+ excludeDexes?: string[];
247
+ sizeConstraint?: number;
248
+ accountsLimitWritable?: number;
249
+ }): Promise<{
250
+ transactionMessage: TransactionMessage;
251
+ lookupTables: AddressLookupTableAccount[];
252
+ }> {
253
+ const params = new URLSearchParams({
254
+ inputMint: inputMint.toString(),
255
+ outputMint: outputMint.toString(),
256
+ amount: amount.toString(),
257
+ userPublicKey: userPublicKey.toString(),
258
+ ...(slippageBps && { slippageBps: slippageBps.toString() }),
259
+ ...(swapMode && { swapMode: swapMode }),
260
+ ...(maxAccounts && { accountsLimitTotal: maxAccounts.toString() }),
261
+ ...(excludeDexes && { excludeDexes: excludeDexes.join(',') }),
262
+ ...(onlyDirectRoutes && {
263
+ onlyDirectRoutes: onlyDirectRoutes.toString(),
264
+ }),
265
+ ...(sizeConstraint && { sizeConstraint: sizeConstraint.toString() }),
266
+ ...(accountsLimitWritable && {
267
+ accountsLimitWritable: accountsLimitWritable.toString(),
268
+ }),
269
+ });
270
+
271
+ const response = await fetch(
272
+ `${this.url}/api/v1/quote/swap?${params.toString()}`,
273
+ {
274
+ headers: {
275
+ Accept: 'application/vnd.msgpack',
276
+ 'Accept-Encoding': 'gzip, deflate, br',
277
+ Authorization: `Bearer ${this.authToken}`,
278
+ },
279
+ }
280
+ );
281
+
282
+ if (!response.ok) {
283
+ if (response.status === 404) {
284
+ throw new Error('No routes available');
285
+ }
286
+ throw new Error(
287
+ `Titan API error: ${response.status} ${response.statusText}`
288
+ );
289
+ }
290
+
291
+ const buffer = await response.arrayBuffer();
292
+ const data = decode(buffer) as SwapQuotes;
293
+
294
+ const route =
295
+ data.quotes[
296
+ Object.keys(data.quotes).find((key) => key.toLowerCase() === 'titan') ||
297
+ ''
298
+ ];
299
+
300
+ if (!route) {
301
+ throw new Error('No routes available');
302
+ }
303
+
304
+ if (route.instructions && route.instructions.length > 0) {
305
+ try {
306
+ const { transactionMessage, lookupTables } =
307
+ await this.getTransactionMessageAndLookupTables(route, userPublicKey);
308
+ return { transactionMessage, lookupTables };
309
+ } catch (err) {
310
+ throw new Error(
311
+ 'Something went wrong with creating the Titan swap transaction. Please try again.'
312
+ );
313
+ }
314
+ }
315
+ throw new Error('No instructions provided in the route');
316
+ }
317
+
318
+ /**
319
+ * Get the titan instructions from transaction by filtering out instructions to compute budget and associated token programs
320
+ * @param transactionMessage the transaction message
321
+ * @param inputMint the input mint
322
+ * @param outputMint the output mint
323
+ */
324
+ public getTitanInstructions({
325
+ transactionMessage,
326
+ inputMint,
327
+ outputMint,
328
+ }: {
329
+ transactionMessage: TransactionMessage;
330
+ inputMint: PublicKey;
331
+ outputMint: PublicKey;
332
+ }): TransactionInstruction[] {
333
+ // Filter out common system instructions that can be handled by DriftClient
334
+ const filteredInstructions = transactionMessage.instructions.filter(
335
+ (instruction) => {
336
+ const programId = instruction.programId.toString();
337
+
338
+ // Filter out system programs
339
+ if (programId === 'ComputeBudget111111111111111111111111111111') {
340
+ return false;
341
+ }
342
+
343
+ if (programId === 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA') {
344
+ return false;
345
+ }
346
+
347
+ if (programId === '11111111111111111111111111111111') {
348
+ return false;
349
+ }
350
+
351
+ // Filter out Associated Token Account creation for input/output mints
352
+ if (programId === 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL') {
353
+ if (instruction.keys.length > 3) {
354
+ const mint = instruction.keys[3].pubkey;
355
+ if (mint.equals(inputMint) || mint.equals(outputMint)) {
356
+ return false;
357
+ }
358
+ }
359
+ }
360
+
361
+ return true;
362
+ }
363
+ );
364
+ return filteredInstructions;
365
+ }
366
+
367
+ private async getTransactionMessageAndLookupTables(
368
+ route: SwapRoute,
369
+ userPublicKey: PublicKey
370
+ ): Promise<{
371
+ transactionMessage: TransactionMessage;
372
+ lookupTables: AddressLookupTableAccount[];
373
+ }> {
374
+ const solanaInstructions: TransactionInstruction[] = route.instructions.map(
375
+ (instruction) => ({
376
+ programId: new PublicKey(instruction.p),
377
+ keys: instruction.a.map((meta) => ({
378
+ pubkey: new PublicKey(meta.p),
379
+ isSigner: meta.s,
380
+ isWritable: meta.w,
381
+ })),
382
+ data: Buffer.from(instruction.d),
383
+ })
384
+ );
385
+
386
+ // Get recent blockhash
387
+ const { blockhash } = await this.connection.getLatestBlockhash();
388
+
389
+ // Build address lookup tables if provided
390
+ const addressLookupTables: AddressLookupTableAccount[] = [];
391
+ if (route.addressLookupTables && route.addressLookupTables.length > 0) {
392
+ for (const altPubkey of route.addressLookupTables) {
393
+ try {
394
+ const altAccount = await this.connection.getAddressLookupTable(
395
+ new PublicKey(altPubkey)
396
+ );
397
+ if (altAccount.value) {
398
+ addressLookupTables.push(altAccount.value);
399
+ }
400
+ } catch (err) {
401
+ console.warn(`Failed to fetch address lookup table:`, err);
402
+ }
403
+ }
404
+ }
405
+
406
+ const transactionMessage = new TransactionMessage({
407
+ payerKey: userPublicKey,
408
+ recentBlockhash: blockhash,
409
+ instructions: solanaInstructions,
410
+ });
411
+
412
+ return { transactionMessage, lookupTables: addressLookupTables };
413
+ }
414
+ }