@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.
- package/VERSION +1 -1
- package/lib/browser/accounts/grpcDriftClientAccountSubscriberV2.js +0 -12
- package/lib/browser/driftClient.d.ts +29 -8
- package/lib/browser/driftClient.js +97 -21
- package/lib/browser/titan/titanClient.d.ts +86 -0
- package/lib/browser/titan/titanClient.js +214 -0
- package/lib/node/accounts/grpcDriftClientAccountSubscriberV2.d.ts.map +1 -1
- package/lib/node/accounts/grpcDriftClientAccountSubscriberV2.js +0 -12
- package/lib/node/driftClient.d.ts +29 -8
- package/lib/node/driftClient.d.ts.map +1 -1
- package/lib/node/driftClient.js +97 -21
- package/lib/node/titan/titanClient.d.ts +87 -0
- package/lib/node/titan/titanClient.d.ts.map +1 -0
- package/lib/node/titan/titanClient.js +214 -0
- package/package.json +2 -1
- package/src/accounts/grpcDriftClientAccountSubscriberV2.ts +0 -15
- package/src/driftClient.ts +173 -22
- package/src/titan/titanClient.ts +414 -0
|
@@ -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
|
+
}
|