@drift-labs/sdk
Version:
SDK for Drift Protocol
267 lines (230 loc) • 6.56 kB
text/typescript
import { ConfirmationStrategy, TxSigAndSlot } from './types';
import {
ConfirmOptions,
Connection,
SendTransactionError,
Signer,
Transaction,
VersionedTransaction,
} from '@solana/web3.js';
import { BaseTxSender } from './baseTxSender';
import bs58 from 'bs58';
import { TxHandler } from './txHandler';
import { IWallet } from '../types';
import { DEFAULT_CONFIRMATION_OPTS } from '../config';
const DEFAULT_RETRY = 2000;
type ResolveReference = {
resolve?: () => void;
};
export class WhileValidTxSender extends BaseTxSender {
connection: Connection;
wallet: IWallet;
opts: ConfirmOptions;
timeout: number;
retrySleep: number;
additionalConnections: Connection[];
timoutCount = 0;
untilValid = new Map<
string,
{ blockhash: string; lastValidBlockHeight: number }
>();
useBlockHeightOffset = true;
private async checkAndSetUseBlockHeightOffset() {
this.connection.getVersion().then((version) => {
const solanaCoreVersion = version['solana-core'];
if (!solanaCoreVersion) return;
const majorVersion = solanaCoreVersion.split('.')[0];
if (!majorVersion) return;
const parsedMajorVersion = parseInt(majorVersion);
if (isNaN(parsedMajorVersion)) return;
if (parsedMajorVersion >= 2) {
this.useBlockHeightOffset = false;
} else {
this.useBlockHeightOffset = true;
}
});
}
public constructor({
connection,
wallet,
opts = { ...DEFAULT_CONFIRMATION_OPTS, maxRetries: 0 },
retrySleep = DEFAULT_RETRY,
additionalConnections = new Array<Connection>(),
confirmationStrategy = ConfirmationStrategy.Combo,
additionalTxSenderCallbacks = [],
txHandler,
trackTxLandRate,
txLandRateLookbackWindowMinutes,
landRateToFeeFunc,
throwOnTimeoutError = true,
throwOnTransactionError = true,
}: {
connection: Connection;
wallet: IWallet;
opts?: ConfirmOptions;
retrySleep?: number;
additionalConnections?;
additionalTxSenderCallbacks?: ((base58EncodedTx: string) => void)[];
confirmationStrategy?: ConfirmationStrategy;
txHandler?: TxHandler;
trackTxLandRate?: boolean;
txLandRateLookbackWindowMinutes?: number;
landRateToFeeFunc?: (landRate: number) => number;
throwOnTimeoutError?: boolean;
throwOnTransactionError?: boolean;
}) {
super({
connection,
wallet,
opts,
additionalConnections,
additionalTxSenderCallbacks,
txHandler,
trackTxLandRate,
txLandRateLookbackWindowMinutes,
confirmationStrategy,
landRateToFeeFunc,
throwOnTimeoutError,
throwOnTransactionError,
});
this.retrySleep = retrySleep;
this.checkAndSetUseBlockHeightOffset();
}
async sleep(reference: ResolveReference): Promise<void> {
return new Promise((resolve) => {
reference.resolve = resolve;
setTimeout(resolve, this.retrySleep);
});
}
async prepareTx(
tx: Transaction,
additionalSigners: Array<Signer>,
opts: ConfirmOptions,
preSigned?: boolean
): Promise<Transaction> {
let latestBlockhash =
await this.txHandler.getLatestBlockhashForTransaction();
// handle tx
let signedTx = tx;
if (!preSigned) {
signedTx = await this.txHandler.prepareTx(
tx,
additionalSigners,
undefined,
opts,
false,
latestBlockhash
);
}
// See SIGNATURE_BLOCK_AND_EXPIRY explanation in txHandler.ts if this is confusing
// @ts-ignore
if (preSigned && tx.SIGNATURE_BLOCK_AND_EXPIRY) {
// @ts-ignore
latestBlockhash = tx.SIGNATURE_BLOCK_AND_EXPIRY;
}
// handle subclass-specific side effects
const txSig = bs58.encode(
signedTx?.signature || signedTx.signatures[0]?.signature
);
this.untilValid.set(txSig, latestBlockhash);
return signedTx;
}
async sendVersionedTransaction(
tx: VersionedTransaction,
additionalSigners?: Array<Signer>,
opts?: ConfirmOptions,
preSigned?: boolean
): Promise<TxSigAndSlot> {
let latestBlockhash =
await this.txHandler.getLatestBlockhashForTransaction();
let signedTx;
if (preSigned) {
signedTx = tx;
// See SIGNATURE_BLOCK_AND_EXPIRY explanation in txHandler.ts if this is confusing
// @ts-ignore
if (tx.SIGNATURE_BLOCK_AND_EXPIRY) {
// @ts-ignore
latestBlockhash = tx.SIGNATURE_BLOCK_AND_EXPIRY;
}
// @ts-ignore
} else if (this.wallet.payer) {
tx.message.recentBlockhash = latestBlockhash.blockhash;
// @ts-ignore
tx.sign((additionalSigners ?? []).concat(this.wallet.payer));
signedTx = tx;
} else {
tx.message.recentBlockhash = latestBlockhash.blockhash;
additionalSigners
?.filter((s): s is Signer => s !== undefined)
.forEach((kp) => {
tx.sign([kp]);
});
signedTx = await this.txHandler.signVersionedTx(
tx,
additionalSigners,
latestBlockhash
);
}
if (opts === undefined) {
opts = this.opts;
}
const txSig = bs58.encode(signedTx.signatures[0]);
this.untilValid.set(txSig, latestBlockhash);
return this.sendRawTransaction(signedTx.serialize(), opts);
}
async sendRawTransaction(
rawTransaction: Buffer | Uint8Array,
opts: ConfirmOptions
): Promise<TxSigAndSlot> {
const startTime = this.getTimestamp();
const txid = await this.connection.sendRawTransaction(rawTransaction, opts);
this.txSigCache?.set(txid, false);
this.sendToAdditionalConnections(rawTransaction, opts);
let done = false;
const resolveReference: ResolveReference = {
resolve: undefined,
};
const stopWaiting = () => {
done = true;
if (resolveReference.resolve) {
resolveReference.resolve();
}
};
(async () => {
while (!done && this.getTimestamp() - startTime < this.timeout) {
await this.sleep(resolveReference);
if (!done) {
this.connection
.sendRawTransaction(rawTransaction, opts)
.catch((e) => {
console.error(e);
stopWaiting();
});
this.sendToAdditionalConnections(rawTransaction, opts);
}
}
})();
let slot: number;
try {
const result = await this.confirmTransaction(txid, opts.commitment);
this.txSigCache?.set(txid, true);
await this.checkConfirmationResultForError(txid, result?.value);
if (result?.value?.err && this.throwOnTransactionError) {
// Fallback error handling if there's a problem reporting the error in checkConfirmationResultForError
throw new SendTransactionError({
action: 'send',
signature: txid,
transactionMessage: `Transaction Failed`,
});
}
slot = result?.context?.slot;
// eslint-disable-next-line no-useless-catch
} catch (e) {
throw e;
} finally {
stopWaiting();
this.untilValid.delete(txid);
}
return { txSig: txid, slot };
}
}