@drift-labs/sdk
Version:
SDK for Drift Protocol
509 lines (508 loc) • 24.2 kB
JavaScript
"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;