@drift-labs/sdk-browser
Version:
SDK for Drift Protocol
293 lines (292 loc) • 12.8 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.BaseTxSender = void 0;
const types_1 = require("./types");
const assert_1 = __importDefault(require("assert"));
const bs58_1 = __importDefault(require("bs58"));
const txHandler_1 = require("./txHandler");
const node_cache_1 = __importDefault(require("node-cache"));
const config_1 = require("../config");
const txConstants_1 = require("../constants/txConstants");
const reportTransactionError_1 = require("./reportTransactionError");
const BASELINE_TX_LAND_RATE = 0.9;
const DEFAULT_TIMEOUT = 35000;
const DEFAULT_TX_LAND_RATE_LOOKBACK_WINDOW_MINUTES = 10;
class BaseTxSender {
constructor({ connection, wallet, opts = config_1.DEFAULT_CONFIRMATION_OPTS, timeout = DEFAULT_TIMEOUT, additionalConnections = new Array(), confirmationStrategy = types_1.ConfirmationStrategy.Combo, additionalTxSenderCallbacks, trackTxLandRate, txHandler, txLandRateLookbackWindowMinutes = DEFAULT_TX_LAND_RATE_LOOKBACK_WINDOW_MINUTES, landRateToFeeFunc, throwOnTimeoutError = true, throwOnTransactionError = true, }) {
this.timeoutCount = 0;
this.txLandRate = 0;
this.lastPriorityFeeSuggestion = 1;
this.connection = connection;
this.wallet = wallet;
this.opts = opts;
this.timeout = timeout;
this.additionalConnections = additionalConnections;
this.confirmationStrategy = confirmationStrategy;
this.additionalTxSenderCallbacks = additionalTxSenderCallbacks;
this.txHandler =
txHandler !== null && txHandler !== void 0 ? txHandler : new txHandler_1.TxHandler({
connection: this.connection,
wallet: this.wallet,
confirmationOptions: this.opts,
});
this.trackTxLandRate = trackTxLandRate;
this.lookbackWindowMinutes = txLandRateLookbackWindowMinutes * 60;
if (this.trackTxLandRate) {
this.txSigCache = new node_cache_1.default({
stdTTL: this.lookbackWindowMinutes,
checkperiod: 120,
});
}
this.landRateToFeeFunc =
landRateToFeeFunc !== null && landRateToFeeFunc !== void 0 ? landRateToFeeFunc : this.defaultLandRateToFeeFunc.bind(this);
this.throwOnTimeoutError = throwOnTimeoutError;
this.throwOnTransactionError = throwOnTransactionError;
}
async send(tx, additionalSigners, opts, preSigned) {
if (additionalSigners === undefined) {
additionalSigners = [];
}
if (opts === undefined) {
opts = this.opts;
}
const signedTx = await this.prepareTx(tx, additionalSigners, opts, preSigned);
return this.sendRawTransaction(signedTx.serialize(), opts);
}
async prepareTx(tx, additionalSigners, opts, preSigned) {
return this.txHandler.prepareTx(tx, additionalSigners, undefined, opts, preSigned);
}
async getVersionedTransaction(ixs, lookupTableAccounts, _additionalSigners, opts, blockhash) {
return this.txHandler.generateVersionedTransaction(blockhash !== null && blockhash !== void 0 ? blockhash : (await this.connection.getLatestBlockhash()), ixs, lookupTableAccounts, this.wallet);
}
async sendVersionedTransaction(tx, additionalSigners, opts, preSigned) {
let signedTx;
if (preSigned) {
signedTx = tx;
// @ts-ignore
}
else if (this.wallet.payer) {
// @ts-ignore
tx.sign((additionalSigners !== null && additionalSigners !== void 0 ? additionalSigners : []).concat(this.wallet.payer));
signedTx = tx;
}
else {
signedTx = await this.txHandler.signVersionedTx(tx, additionalSigners, undefined, this.wallet);
}
if (opts === undefined) {
opts = this.opts;
}
return this.sendRawTransaction(signedTx.serialize(), opts);
}
async sendRawTransaction(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
rawTransaction,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
opts) {
throw new Error('Must be implemented by subclass');
}
/* Simulate the tx and return a boolean for success value */
async simulateTransaction(tx) {
try {
const result = await this.connection.simulateTransaction(tx);
if (result.value.err != null) {
console.error('Error in transaction simulation: ', result.value.err);
return false;
}
return true;
}
catch (e) {
console.error('Error calling simulateTransaction: ', e);
return false;
}
}
async confirmTransactionWebSocket(signature, commitment) {
var _a, _b;
let decodedSignature;
try {
decodedSignature = bs58_1.default.decode(signature);
}
catch (err) {
throw new Error('signature must be base58 encoded: ' + signature);
}
(0, assert_1.default)(decodedSignature.length === 64, 'signature has invalid length');
const start = Date.now();
const subscriptionCommitment = commitment || this.opts.commitment;
const subscriptionIds = new Array();
const connections = [this.connection, ...this.additionalConnections];
let response = null;
const promises = connections.map((connection, i) => {
let subscriptionId;
const confirmPromise = new Promise((resolve, reject) => {
try {
subscriptionId = connection.onSignature(signature, (result, context) => {
subscriptionIds[i] = undefined;
response = {
context,
value: result,
};
resolve(null);
}, subscriptionCommitment);
}
catch (err) {
reject(err);
}
});
subscriptionIds.push(subscriptionId);
return confirmPromise;
});
try {
await this.promiseTimeout(promises, this.timeout);
}
finally {
for (const [i, subscriptionId] of subscriptionIds.entries()) {
if (subscriptionId) {
connections[i].removeSignatureListener(subscriptionId);
}
}
}
if (response === null) {
if (this.confirmationStrategy === types_1.ConfirmationStrategy.Combo) {
try {
const rpcResponse = await this.connection.getSignatureStatuses([
signature,
]);
if ((_b = (_a = rpcResponse === null || rpcResponse === void 0 ? void 0 : rpcResponse.value) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.confirmationStatus) {
response = {
context: rpcResponse.context,
value: { err: rpcResponse.value[0].err },
};
return response;
}
}
catch (error) {
// Ignore error to pass through to timeout error
}
}
this.timeoutCount += 1;
const duration = (Date.now() - start) / 1000;
if (this.throwOnTimeoutError) {
throw new types_1.TxSendError(`Transaction was not confirmed in ${duration.toFixed(2)} seconds. It is unknown if it succeeded or failed. Check signature ${signature} using the Solana Explorer or CLI tools.`, txConstants_1.NOT_CONFIRMED_ERROR_CODE);
}
}
return response;
}
async confirmTransactionPolling(signature, commitment = 'finalized') {
var _a;
let totalTime = 0;
let backoffTime = 400; // approx block time
const start = Date.now();
while (totalTime < this.timeout) {
await new Promise((resolve) => setTimeout(resolve, backoffTime));
const rpcResponse = await this.connection.getSignatureStatuses([
signature,
]);
const signatureResult = rpcResponse && ((_a = rpcResponse.value) === null || _a === void 0 ? void 0 : _a[0]);
if (rpcResponse &&
signatureResult &&
signatureResult.confirmationStatus === commitment) {
return { context: rpcResponse.context, value: { err: null } };
}
totalTime += backoffTime;
backoffTime = Math.min(backoffTime * 2, 5000);
}
// Transaction not confirmed within 30 seconds
this.timeoutCount += 1;
const duration = (Date.now() - start) / 1000;
if (this.throwOnTimeoutError) {
throw new types_1.TxSendError(`Transaction was not confirmed in ${duration.toFixed(2)} seconds. It is unknown if it succeeded or failed. Check signature ${signature} using the Solana Explorer or CLI tools.`, txConstants_1.NOT_CONFIRMED_ERROR_CODE);
}
}
async confirmTransaction(signature, commitment) {
if (this.confirmationStrategy === types_1.ConfirmationStrategy.WebSocket ||
this.confirmationStrategy === types_1.ConfirmationStrategy.Combo) {
return await this.confirmTransactionWebSocket(signature, commitment);
}
else if (this.confirmationStrategy === types_1.ConfirmationStrategy.Polling) {
return await this.confirmTransactionPolling(signature, commitment);
}
}
getTimestamp() {
return new Date().getTime();
}
promiseTimeout(promises, timeoutMs) {
let timeoutId;
const timeoutPromise = new Promise((resolve) => {
timeoutId = setTimeout(() => resolve(null), timeoutMs);
});
return Promise.race([...promises, timeoutPromise]).then((result) => {
clearTimeout(timeoutId);
return result;
});
}
sendToAdditionalConnections(rawTx, opts) {
var _a;
this.additionalConnections.map((connection) => {
connection.sendRawTransaction(rawTx, opts).catch((e) => {
console.error(
// @ts-ignore
`error sending tx to additional connection ${connection._rpcEndpoint}`);
console.error(e);
});
});
(_a = this.additionalTxSenderCallbacks) === null || _a === void 0 ? void 0 : _a.map((callback) => {
callback(bs58_1.default.encode(rawTx));
});
}
addAdditionalConnection(newConnection) {
const alreadyUsingConnection = this.additionalConnections.filter((connection) => {
// @ts-ignore
return connection._rpcEndpoint === newConnection.rpcEndpoint;
}).length > 0;
if (!alreadyUsingConnection) {
this.additionalConnections.push(newConnection);
}
}
getTimeoutCount() {
return this.timeoutCount;
}
async checkConfirmationResultForError(txSig, result) {
var _a;
if (result === null || result === void 0 ? void 0 : result.err) {
await (0, reportTransactionError_1.throwTransactionError)(txSig, this.connection, (_a = this.opts) === null || _a === void 0 ? void 0 : _a.commitment);
}
return;
}
getTxLandRate() {
if (!this.trackTxLandRate) {
return this.txLandRate;
}
const keys = this.txSigCache.keys();
const denominator = keys.length;
if (denominator === 0) {
return this.txLandRate;
}
let numerator = 0;
for (const key of keys) {
const value = this.txSigCache.get(key);
if (value) {
numerator += 1;
}
}
this.txLandRate = numerator / denominator;
return this.txLandRate;
}
defaultLandRateToFeeFunc(txLandRate) {
if (txLandRate >= BASELINE_TX_LAND_RATE ||
this.txSigCache.keys().length < 3) {
return 1;
}
const multiplier = 10 * Math.log10(1 + (BASELINE_TX_LAND_RATE - txLandRate) * 5);
return Math.min(multiplier, 10);
}
getSuggestedPriorityFeeMultiplier() {
if (!this.trackTxLandRate) {
return 1;
}
return this.landRateToFeeFunc(this.getTxLandRate());
}
}
exports.BaseTxSender = BaseTxSender;