UNPKG

@drift-labs/sdk-browser

Version:
242 lines (241 loc) 10.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TitanClient = exports.SwapMode = void 0; const web3_js_1 = require("@solana/web3.js"); const msgpack_1 = require("@msgpack/msgpack"); var SwapMode; (function (SwapMode) { SwapMode["ExactIn"] = "ExactIn"; SwapMode["ExactOut"] = "ExactOut"; })(SwapMode || (exports.SwapMode = SwapMode = {})); const TITAN_API_URL = 'https://api.titan.exchange'; class TitanClient { constructor({ connection, authToken, url, proxyUrl, }) { this.connection = connection; this.authToken = authToken; this.url = url !== null && url !== void 0 ? url : TITAN_API_URL; this.proxyUrl = proxyUrl; } buildParams({ inputMint, outputMint, amount, userPublicKey, maxAccounts, slippageBps, swapMode, onlyDirectRoutes, excludeDexes, sizeConstraint, accountsLimitWritable, }) { // 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: normalizedSwapMode.toString() }), ...(maxAccounts && { accountsLimitTotal: maxAccounts.toString() }), ...(excludeDexes && { excludeDexes: excludeDexes.join(',') }), ...(onlyDirectRoutes && { onlyDirectRoutes: onlyDirectRoutes.toString(), }), ...(sizeConstraint && { sizeConstraint: sizeConstraint.toString() }), ...(accountsLimitWritable && { accountsLimitWritable: accountsLimitWritable.toString(), }), }); } /** * Get routes for a swap */ async getQuote({ inputMint, outputMint, amount, userPublicKey, maxAccounts = 50, // 50 is an estimated amount with buffer slippageBps, swapMode, onlyDirectRoutes, excludeDexes, sizeConstraint, accountsLimitWritable, }) { var _a; const params = this.buildParams({ inputMint, outputMint, amount, userPublicKey, maxAccounts, slippageBps, swapMode, onlyDirectRoutes, excludeDexes, sizeConstraint, accountsLimitWritable, }); let 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) { throw new Error(`Titan API error: ${response.status} ${response.statusText}`); } const buffer = await response.arrayBuffer(); const data = (0, msgpack_1.decode)(buffer); // Cache the quote data and parameters for later use in getSwap this.lastQuoteData = data; this.lastQuoteParams = params.toString(); // We are only querying for the best avaiable route so use that const route = data.quotes[Object.keys(data.quotes)[0]]; if (!route) { throw new Error('No routes available'); } return { inputMint: inputMint.toString(), inAmount: amount.toString(), outputMint: outputMint.toString(), outAmount: route.outAmount.toString(), swapMode: data.swapMode, slippageBps: route.slippageBps, platformFee: route.platformFee ? { amount: route.platformFee.amount.toString(), feeBps: route.platformFee.fee_bps, } : undefined, routePlan: ((_a = route.steps) === null || _a === void 0 ? void 0 : _a.map((step) => { var _a; return ({ swapInfo: { ammKey: new web3_js_1.PublicKey(step.ammKey).toString(), label: step.label, inputMint: new web3_js_1.PublicKey(step.inputMint).toString(), outputMint: new web3_js_1.PublicKey(step.outputMint).toString(), inAmount: step.inAmount.toString(), outAmount: step.outAmount.toString(), feeAmount: ((_a = step.feeAmount) === null || _a === void 0 ? void 0 : _a.toString()) || '0', feeMint: step.feeMint ? new web3_js_1.PublicKey(step.feeMint).toString() : '', }, percent: 100, }); })) || [], contextSlot: route.contextSlot, timeTaken: route.timeTaken, }; } /** * Get a swap transaction for quote */ async getSwap({ inputMint, outputMint, amount, userPublicKey, maxAccounts = 50, // 50 is an estimated amount with buffer slippageBps, swapMode, onlyDirectRoutes, excludeDexes, sizeConstraint, accountsLimitWritable, }) { const params = this.buildParams({ inputMint, outputMint, amount, userPublicKey, maxAccounts, slippageBps, swapMode, onlyDirectRoutes, excludeDexes, sizeConstraint, accountsLimitWritable, }); // 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.'); } // Reuse the cached quote data const data = this.lastQuoteData; // We are only querying for the best avaiable route so use that const route = data.quotes[Object.keys(data.quotes)[0]]; if (!route) { throw new Error('No routes available'); } if (route.instructions && route.instructions.length > 0) { try { const { transactionMessage, lookupTables } = await this.getTransactionMessageAndLookupTables(route, userPublicKey); return { transactionMessage, lookupTables }; } catch (err) { 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'); } /** * Get the titan instructions from transaction by filtering out instructions to compute budget and associated token programs * @param transactionMessage the transaction message * @param inputMint the input mint * @param outputMint the output mint */ getTitanInstructions({ transactionMessage, inputMint, outputMint, }) { // Filter out common system instructions that can be handled by DriftClient const filteredInstructions = transactionMessage.instructions.filter((instruction) => { const programId = instruction.programId.toString(); // Filter out system programs if (programId === 'ComputeBudget111111111111111111111111111111') { return false; } if (programId === 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA') { return false; } if (programId === '11111111111111111111111111111111') { return false; } // Filter out Associated Token Account creation for input/output mints if (programId === 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL') { if (instruction.keys.length > 3) { const mint = instruction.keys[3].pubkey; if (mint.equals(inputMint) || mint.equals(outputMint)) { return false; } } } return true; }); return filteredInstructions; } async getTransactionMessageAndLookupTables(route, userPublicKey) { const solanaInstructions = route.instructions.map((instruction) => ({ programId: new web3_js_1.PublicKey(instruction.p), keys: instruction.a.map((meta) => ({ pubkey: new web3_js_1.PublicKey(meta.p), isSigner: meta.s, isWritable: meta.w, })), data: Buffer.from(instruction.d), })); // Get recent blockhash const { blockhash } = await this.connection.getLatestBlockhash(); // Build address lookup tables if provided const addressLookupTables = []; if (route.addressLookupTables && route.addressLookupTables.length > 0) { for (const altPubkey of route.addressLookupTables) { try { const altAccount = await this.connection.getAddressLookupTable(new web3_js_1.PublicKey(altPubkey)); if (altAccount.value) { addressLookupTables.push(altAccount.value); } } catch (err) { console.warn(`Failed to fetch address lookup table:`, err); } } } const transactionMessage = new web3_js_1.TransactionMessage({ payerKey: userPublicKey, recentBlockhash: blockhash, instructions: solanaInstructions, }); return { transactionMessage, lookupTables: addressLookupTables }; } } exports.TitanClient = TitanClient;