UNPKG

@drift-labs/sdk

Version:
509 lines (508 loc) • 24.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TxHandler = exports.COMPUTE_UNITS_DEFAULT = void 0; const web3_js_1 = require("@solana/web3.js"); const txParamProcessor_1 = require("./txParamProcessor"); const bs58_1 = __importDefault(require("bs58")); const computeUnits_1 = require("../util/computeUnits"); const cachedBlockhashFetcher_1 = require("./blockhashFetcher/cachedBlockhashFetcher"); const baseBlockhashFetcher_1 = require("./blockhashFetcher/baseBlockhashFetcher"); const utils_1 = require("./utils"); const config_1 = require("../config"); /** * Explanation for SIGNATURE_BLOCK_AND_EXPIRY: * * When the whileValidTxSender waits for confirmation of a given transaction, it needs the last available blockheight and blockhash used in the signature to do so. For pre-signed transactions, these values aren't attached to the transaction object by default. For a "scrappy" workaround which doesn't break backwards compatibility, the SIGNATURE_BLOCK_AND_EXPIRY property is simply attached to the transaction objects as they are created or signed in this handler despite a mismatch in the typescript types. If the values are attached to the transaction when they reach the whileValidTxSender, it can opt-in to use these values. */ const DEV_TRY_FORCE_TX_TIMEOUTS = process.env.DEV_TRY_FORCE_TX_TIMEOUTS === 'true' || false; exports.COMPUTE_UNITS_DEFAULT = 200000; const BLOCKHASH_FETCH_RETRY_COUNT = 3; const BLOCKHASH_FETCH_RETRY_SLEEP = 200; const RECENT_BLOCKHASH_STALE_TIME_MS = 2000; // Reuse blockhashes within this timeframe during bursts of tx contruction /** * This class is responsible for creating and signing transactions. */ class TxHandler { constructor(props) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u; this.blockHashToLastValidBlockHeightLookup = {}; this.returnBlockHeightsWithSignedTxCallbackData = false; this.blockhashCommitment = config_1.DEFAULT_CONFIRMATION_OPTS.commitment; this.getProps = (wallet, confirmationOpts) => [wallet !== null && wallet !== void 0 ? wallet : this.wallet, confirmationOpts !== null && confirmationOpts !== void 0 ? confirmationOpts : this.confirmationOptions]; this.connection = props.connection; this.wallet = props.wallet; this.confirmationOptions = props.confirmationOptions; this.blockhashCommitment = (_e = (_d = (_b = (_a = props.confirmationOptions) === null || _a === void 0 ? void 0 : _a.preflightCommitment) !== null && _b !== void 0 ? _b : (_c = props === null || props === void 0 ? void 0 : props.connection) === null || _c === void 0 ? void 0 : _c.commitment) !== null && _d !== void 0 ? _d : this.blockhashCommitment) !== null && _e !== void 0 ? _e : 'confirmed'; this.blockHashFetcher = ((_f = props === null || props === void 0 ? void 0 : props.config) === null || _f === void 0 ? void 0 : _f.blockhashCachingEnabled) ? new cachedBlockhashFetcher_1.CachedBlockhashFetcher(this.connection, this.blockhashCommitment, (_j = (_h = (_g = props === null || props === void 0 ? void 0 : props.config) === null || _g === void 0 ? void 0 : _g.blockhashCachingConfig) === null || _h === void 0 ? void 0 : _h.retryCount) !== null && _j !== void 0 ? _j : BLOCKHASH_FETCH_RETRY_COUNT, (_m = (_l = (_k = props === null || props === void 0 ? void 0 : props.config) === null || _k === void 0 ? void 0 : _k.blockhashCachingConfig) === null || _l === void 0 ? void 0 : _l.retrySleepTimeMs) !== null && _m !== void 0 ? _m : BLOCKHASH_FETCH_RETRY_SLEEP, (_q = (_p = (_o = props === null || props === void 0 ? void 0 : props.config) === null || _o === void 0 ? void 0 : _o.blockhashCachingConfig) === null || _p === void 0 ? void 0 : _p.staleCacheTimeMs) !== null && _q !== void 0 ? _q : RECENT_BLOCKHASH_STALE_TIME_MS) : new baseBlockhashFetcher_1.BaseBlockhashFetcher(this.connection, this.blockhashCommitment); // #Optionals this.returnBlockHeightsWithSignedTxCallbackData = (_s = (_r = props.opts) === null || _r === void 0 ? void 0 : _r.returnBlockHeightsWithSignedTxCallbackData) !== null && _s !== void 0 ? _s : false; this.onSignedCb = (_t = props.opts) === null || _t === void 0 ? void 0 : _t.onSignedCb; this.preSignedCb = (_u = props.opts) === null || _u === void 0 ? void 0 : _u.preSignedCb; } getWallet() { return this.wallet; } addHashAndExpiryToLookup(hashAndExpiry) { if (!this.returnBlockHeightsWithSignedTxCallbackData) return; this.blockHashToLastValidBlockHeightLookup[hashAndExpiry.blockhash] = hashAndExpiry.lastValidBlockHeight; } updateWallet(wallet) { this.wallet = wallet; } /** * Created this to prevent non-finalized blockhashes being used when building transactions. We want to always use finalized because otherwise it's easy to get the BlockHashNotFound error (RPC uses finalized to validate a transaction). Using an older blockhash when building transactions should never really be a problem right now. * * https://www.helius.dev/blog/how-to-deal-with-blockhash-errors-on-solana#why-do-blockhash-errors-occur * * @returns */ async getLatestBlockhashForTransaction() { return this.blockHashFetcher.getLatestBlockhash(); } /** * Applies recent blockhash and signs a given transaction * @param tx * @param additionalSigners * @param wallet * @param confirmationOpts * @param preSigned * @param recentBlockhash * @returns */ async prepareTx(tx, additionalSigners, wallet, confirmationOpts, preSigned, recentBlockhash) { if (preSigned) { return tx; } [wallet, confirmationOpts] = this.getProps(wallet, confirmationOpts); tx.feePayer = wallet.publicKey; recentBlockhash = recentBlockhash ? recentBlockhash : await this.getLatestBlockhashForTransaction(); tx.recentBlockhash = recentBlockhash.blockhash; this.addHashAndExpiryToLookup(recentBlockhash); const signedTx = await this.signTx(tx, additionalSigners); // @ts-ignore signedTx.SIGNATURE_BLOCK_AND_EXPIRY = recentBlockhash; return signedTx; } isVersionedTransaction(tx) { return (0, utils_1.isVersionedTransaction)(tx); } isLegacyTransaction(tx) { return !this.isVersionedTransaction(tx); } getTxSigFromSignedTx(signedTx) { if (this.isVersionedTransaction(signedTx)) { return bs58_1.default.encode(Buffer.from(signedTx.signatures[0])); } else { return bs58_1.default.encode(Buffer.from(signedTx.signature)); } } getBlockhashFromSignedTx(signedTx) { if (this.isVersionedTransaction(signedTx)) { return signedTx.message.recentBlockhash; } else { return signedTx.recentBlockhash; } } async signTx(tx, additionalSigners, wallet) { var _a; [wallet] = this.getProps(wallet); additionalSigners .filter((s) => s !== undefined) .forEach((kp) => { tx.partialSign(kp); }); (_a = this.preSignedCb) === null || _a === void 0 ? void 0 : _a.call(this); const signedTx = await wallet.signTransaction(tx); // Turn txSig Buffer into base58 string const txSig = this.getTxSigFromSignedTx(signedTx); this.handleSignedTxData([ { txSig, signedTx, blockHash: this.getBlockhashFromSignedTx(signedTx), }, ]); return signedTx; } async signVersionedTx(tx, additionalSigners, recentBlockhash, wallet) { var _a; [wallet] = this.getProps(wallet); if (recentBlockhash) { tx.message.recentBlockhash = recentBlockhash.blockhash; this.addHashAndExpiryToLookup(recentBlockhash); // @ts-ignore tx.SIGNATURE_BLOCK_AND_EXPIRY = recentBlockhash; } additionalSigners === null || additionalSigners === void 0 ? void 0 : additionalSigners.filter((s) => s !== undefined).forEach((kp) => { tx.sign([kp]); }); (_a = this.preSignedCb) === null || _a === void 0 ? void 0 : _a.call(this); //@ts-ignore const signedTx = (await wallet.signTransaction(tx)); // Turn txSig Buffer into base58 string const txSig = this.getTxSigFromSignedTx(signedTx); this.handleSignedTxData([ { txSig, signedTx, blockHash: this.getBlockhashFromSignedTx(signedTx), }, ]); return signedTx; } handleSignedTxData(txData) { if (!this.returnBlockHeightsWithSignedTxCallbackData) { if (this.onSignedCb) { this.onSignedCb(txData); } return; } const signedTxData = txData.map((tx) => { const lastValidBlockHeight = this.blockHashToLastValidBlockHeightLookup[tx.blockHash]; return { ...tx, lastValidBlockHeight, }; }); if (this.onSignedCb) { this.onSignedCb(signedTxData); } return signedTxData; } /** * Gets transaction params with extra processing applied, like using the simulated compute units or using a dynamically calculated compute unit price. * @param txBuildingProps * @returns */ async getProcessedTransactionParams(txBuildingProps) { var _a, _b; const baseTxParams = { computeUnits: (_a = txBuildingProps === null || txBuildingProps === void 0 ? void 0 : txBuildingProps.txParams) === null || _a === void 0 ? void 0 : _a.computeUnits, computeUnitsPrice: (_b = txBuildingProps === null || txBuildingProps === void 0 ? void 0 : txBuildingProps.txParams) === null || _b === void 0 ? void 0 : _b.computeUnitsPrice, }; const processedTxParams = await txParamProcessor_1.TransactionParamProcessor.process({ baseTxParams, txBuilder: (updatedTxParams) => { var _a; return this.buildTransaction({ ...txBuildingProps, txParams: (_a = updatedTxParams.txParams) !== null && _a !== void 0 ? _a : baseTxParams, forceVersionedTransaction: true, }); }, processConfig: { useSimulatedComputeUnits: txBuildingProps.txParams.useSimulatedComputeUnits, computeUnitsBufferMultiplier: txBuildingProps.txParams.computeUnitsBufferMultiplier, useSimulatedComputeUnitsForCUPriceCalculation: txBuildingProps.txParams .useSimulatedComputeUnitsForCUPriceCalculation, getCUPriceFromComputeUnits: txBuildingProps.txParams.getCUPriceFromComputeUnits, }, processParams: { connection: this.connection, simulatedTx: txBuildingProps.simulatedTx, }, }); return processedTxParams; } _generateVersionedTransaction(recentBlockhash, message) { this.addHashAndExpiryToLookup(recentBlockhash); return new web3_js_1.VersionedTransaction(message); } generateLegacyVersionedTransaction(recentBlockhash, ixs, wallet) { [wallet] = this.getProps(wallet); const message = new web3_js_1.TransactionMessage({ payerKey: wallet.publicKey, recentBlockhash: recentBlockhash.blockhash, instructions: ixs, }).compileToLegacyMessage(); const tx = this._generateVersionedTransaction(recentBlockhash, message); // @ts-ignore tx.SIGNATURE_BLOCK_AND_EXPIRY = recentBlockhash; return tx; } generateVersionedTransaction(recentBlockhash, ixs, lookupTableAccounts, wallet) { [wallet] = this.getProps(wallet); const message = new web3_js_1.TransactionMessage({ payerKey: wallet.publicKey, recentBlockhash: recentBlockhash.blockhash, instructions: ixs, }).compileToV0Message(lookupTableAccounts); const tx = this._generateVersionedTransaction(recentBlockhash, message); // @ts-ignore tx.SIGNATURE_BLOCK_AND_EXPIRY = recentBlockhash; return tx; } generateLegacyTransaction(ixs, recentBlockhash) { const tx = new web3_js_1.Transaction().add(...ixs); if (recentBlockhash) { tx.recentBlockhash = recentBlockhash.blockhash; } return tx; } /** * Accepts multiple instructions and builds a transaction for each. Prevents needing to spam RPC with requests for the same blockhash. * @param props * @returns */ async buildBulkTransactions(props) { var _a; const recentBlockhash = (_a = props === null || props === void 0 ? void 0 : props.recentBlockhash) !== null && _a !== void 0 ? _a : (await this.getLatestBlockhashForTransaction()); return await Promise.all(props.instructions.map((ix) => { if (!ix) return undefined; return this.buildTransaction({ ...props, instructions: ix, recentBlockhash, }); })); } /** * * @param instructions * @param txParams * @param txVersion * @param lookupTables * @param forceVersionedTransaction Return a VersionedTransaction instance even if the version of the transaction is Legacy * @returns */ async buildTransaction(props) { var _a; const { txVersion, txParams, connection: _connection, preFlightCommitment: _preFlightCommitment, fetchAllMarketLookupTableAccounts, forceVersionedTransaction, instructions, } = props; let { lookupTables } = props; const marketLookupTables = await fetchAllMarketLookupTableAccounts(); lookupTables = lookupTables ? [...lookupTables, ...marketLookupTables] : marketLookupTables; // # Collect and process Tx Params let baseTxParams = { computeUnits: txParams === null || txParams === void 0 ? void 0 : txParams.computeUnits, computeUnitsPrice: txParams === null || txParams === void 0 ? void 0 : txParams.computeUnitsPrice, }; const instructionsArray = Array.isArray(instructions) ? instructions : [instructions]; let instructionsToUse; let simulatedTx; // add optional ixs if there's room and it doesn't fail simulation (usually oracle cranks) if (props.optionalIxs && txVersion === 0) { [instructionsToUse, simulatedTx] = await this.simulateAndCalculateInstructions({ ...props, instructions: instructionsArray, txVersion, lookupTables, }, props.optionalIxs, txVersion === 0, lookupTables); } else { instructionsToUse = instructionsArray; } if (txParams === null || txParams === void 0 ? void 0 : txParams.useSimulatedComputeUnits) { const processedTxParams = await this.getProcessedTransactionParams({ ...props, instructions: instructionsToUse, simulatedTx: simulatedTx, }); baseTxParams = { ...baseTxParams, ...processedTxParams, }; } const { hasSetComputeUnitLimitIx, hasSetComputeUnitPriceIx } = (0, computeUnits_1.containsComputeUnitIxs)(instructionsToUse); // # Create Tx Instructions const allIx = []; const computeUnits = baseTxParams === null || baseTxParams === void 0 ? void 0 : baseTxParams.computeUnits; if (computeUnits > 0 && !hasSetComputeUnitLimitIx) { allIx.push(web3_js_1.ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits, })); } const computeUnitsPrice = baseTxParams === null || baseTxParams === void 0 ? void 0 : baseTxParams.computeUnitsPrice; if (DEV_TRY_FORCE_TX_TIMEOUTS) { allIx.push(web3_js_1.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 0, })); } else if (computeUnitsPrice > 0 && !hasSetComputeUnitPriceIx) { allIx.push(web3_js_1.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: computeUnitsPrice, })); } allIx.push(...instructionsToUse); const recentBlockhash = (_a = props === null || props === void 0 ? void 0 : props.recentBlockhash) !== null && _a !== void 0 ? _a : (await this.getLatestBlockhashForTransaction()); // # Create and return Transaction if (txVersion === 'legacy') { if (forceVersionedTransaction) { return this.generateLegacyVersionedTransaction(recentBlockhash, allIx); } else { return this.generateLegacyTransaction(allIx, recentBlockhash); } } else { return this.generateVersionedTransaction(recentBlockhash, allIx, lookupTables); } } wrapInTx(instruction, computeUnits = 600000, computeUnitsPrice = 0) { const tx = new web3_js_1.Transaction(); if (computeUnits != exports.COMPUTE_UNITS_DEFAULT) { tx.add(web3_js_1.ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits, })); } if (DEV_TRY_FORCE_TX_TIMEOUTS) { tx.add(web3_js_1.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 0, })); } else if (computeUnitsPrice != 0) { tx.add(web3_js_1.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: computeUnitsPrice, })); } return tx.add(instruction); } /** * Get a map of signed and prepared transactions from an array of legacy transactions * @param txsToSign * @param keys * @param wallet * @param commitment * @returns */ async getPreparedAndSignedLegacyTransactionMap(txsMap, wallet, commitment, recentBlockhash) { var _a, _b; recentBlockhash = recentBlockhash ? recentBlockhash : await this.getLatestBlockhashForTransaction(); this.addHashAndExpiryToLookup(recentBlockhash); for (const tx of Object.values(txsMap)) { if (!tx) continue; tx.recentBlockhash = recentBlockhash.blockhash; tx.feePayer = (_a = wallet === null || wallet === void 0 ? void 0 : wallet.publicKey) !== null && _a !== void 0 ? _a : (_b = this.wallet) === null || _b === void 0 ? void 0 : _b.publicKey; // @ts-ignore tx.SIGNATURE_BLOCK_AND_EXPIRY = recentBlockhash; } return this.getSignedTransactionMap(txsMap, wallet); } /** * Get a map of signed transactions from an array of transactions to sign. * @param txsToSign * @param keys * @param wallet * @returns */ async getSignedTransactionMap(txsToSignMap, wallet) { var _a; [wallet] = this.getProps(wallet); const txsToSignEntries = Object.entries(txsToSignMap); // Create a map of the same keys as the input map, but with the values set to undefined. We'll populate the filtered (non-undefined) values with signed transactions. const signedTxMap = txsToSignEntries.reduce((acc, [key]) => { acc[key] = undefined; return acc; }, {}); const filteredTxEntries = txsToSignEntries.filter(([_, tx]) => !!tx); // Extra handling for legacy transactions for (const [_key, tx] of filteredTxEntries) { if (this.isLegacyTransaction(tx)) { tx.feePayer = wallet.publicKey; } } (_a = this.preSignedCb) === null || _a === void 0 ? void 0 : _a.call(this); const signedFilteredTxs = await wallet.signAllTransactions(filteredTxEntries.map(([_, tx]) => tx)); signedFilteredTxs.forEach((signedTx, index) => { var _a; // @ts-ignore signedTx.SIGNATURE_BLOCK_AND_EXPIRY = // @ts-ignore (_a = filteredTxEntries[index][1]) === null || _a === void 0 ? void 0 : _a.SIGNATURE_BLOCK_AND_EXPIRY; }); const signedTxData = this.handleSignedTxData(signedFilteredTxs.map((signedTx) => { return { txSig: this.getTxSigFromSignedTx(signedTx), signedTx, blockHash: this.getBlockhashFromSignedTx(signedTx), }; })); filteredTxEntries.forEach(([key], index) => { const signedTx = signedFilteredTxs[index]; // @ts-ignore signedTxMap[key] = signedTx; }); return { signedTxMap, signedTxData }; } /** * Accepts multiple instructions and builds a transaction for each. Prevents needing to spam RPC with requests for the same blockhash. * @param props * @returns */ async buildTransactionsMap(props) { const builtTxs = await this.buildBulkTransactions({ ...props, instructions: Object.values(props.instructionsMap), }); return Object.keys(props.instructionsMap).reduce((acc, key, index) => { acc[key] = builtTxs[index]; return acc; }, {}); } /** * Builds and signs transactions from a given array of instructions for multiple transactions. * @param props * @returns */ async buildAndSignTransactionMap(props) { const builtTxs = await this.buildTransactionsMap(props); const preppedTransactions = await (props.txVersion === 'legacy' ? this.getPreparedAndSignedLegacyTransactionMap(builtTxs, props.wallet, props.preFlightCommitment) : this.getSignedTransactionMap(builtTxs, props.wallet)); return preppedTransactions; } async simulateAndCalculateInstructions(txBuildingProps, optionalInstructions = [], versionedTransaction = true, addressLookupTables = []) { var _a; const baseInstructions = Array.isArray(txBuildingProps.instructions) ? txBuildingProps.instructions : [txBuildingProps.instructions]; if (optionalInstructions.length === 0) { return [baseInstructions, undefined]; } let allInstructions = [...optionalInstructions, ...baseInstructions]; let txSize = (0, utils_1.getSizeOfTransaction)(allInstructions, versionedTransaction, addressLookupTables); while (txSize > utils_1.MAX_TX_BYTE_SIZE && allInstructions.length > baseInstructions.length) { allInstructions = allInstructions.slice(1); txSize = (0, utils_1.getSizeOfTransaction)(allInstructions, versionedTransaction, addressLookupTables); } const tx = await this.buildTransaction({ ...txBuildingProps, optionalIxs: undefined, instructions: allInstructions, }); const simulatedTx = await this.connection.simulateTransaction(tx); if ((_a = simulatedTx.value) === null || _a === void 0 ? void 0 : _a.err) { const tx = await this.buildTransaction({ ...txBuildingProps, optionalIxs: undefined, instructions: baseInstructions, }); const simulationWithoutOptionalIxs = await this.connection.simulateTransaction(tx); return [baseInstructions, simulationWithoutOptionalIxs.value]; } return [allInstructions, simulatedTx.value]; } } exports.TxHandler = TxHandler;