306 lines
8.6 KiB
TypeScript
306 lines
8.6 KiB
TypeScript
import { AnchorProvider } from '@coral-xyz/anchor';
|
|
import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet';
|
|
import { u8 } from '@solana/buffer-layout';
|
|
import {
|
|
AddressLookupTableAccount,
|
|
Commitment,
|
|
ComputeBudgetProgram,
|
|
Connection,
|
|
Keypair,
|
|
MessageV0,
|
|
RpcResponseAndContext,
|
|
SignatureResult,
|
|
Signer,
|
|
TransactionConfirmationStatus,
|
|
TransactionError,
|
|
TransactionInstruction,
|
|
TransactionSignature,
|
|
VersionedTransaction,
|
|
} from '@solana/web3.js';
|
|
import { COMPUTE_BUDGET_PROGRAM_ID } from '../constants';
|
|
import { TxCallbackOptions } from '../client';
|
|
import { awaitTransactionSignatureConfirmation } from '@blockworks-foundation/mangolana/lib/transactions';
|
|
import { tryStringify } from '../utils';
|
|
|
|
export interface MangoSignatureStatus {
|
|
confirmations?: number | null;
|
|
confirmationStatus?: TransactionConfirmationStatus;
|
|
err?: TransactionError | null;
|
|
signature: TransactionSignature;
|
|
slot?: number;
|
|
}
|
|
|
|
export interface LatestBlockhash {
|
|
slot: number;
|
|
blockhash: string;
|
|
lastValidBlockHeight: number;
|
|
}
|
|
|
|
export interface LatestBlockhash {
|
|
slot: number;
|
|
blockhash: string;
|
|
lastValidBlockHeight: number;
|
|
}
|
|
|
|
export type SendTransactionOpts = Partial<{
|
|
preflightCommitment: Commitment;
|
|
latestBlockhash: Readonly<LatestBlockhash>;
|
|
prioritizationFee: number;
|
|
estimateFee: boolean;
|
|
additionalSigners: Keypair[];
|
|
postSendTxCallback: (callbackOpts: TxCallbackOptions) => void;
|
|
postTxConfirmationCallback: (callbackOpts: TxCallbackOptions) => void;
|
|
txConfirmationCommitment: Commitment;
|
|
confirmInBackground: boolean;
|
|
alts: AddressLookupTableAccount[];
|
|
multipleConnections: Connection[];
|
|
}>;
|
|
|
|
export async function sendTransaction(
|
|
provider: AnchorProvider,
|
|
ixs: TransactionInstruction[],
|
|
alts: AddressLookupTableAccount[],
|
|
opts: SendTransactionOpts = {},
|
|
): Promise<MangoSignatureStatus> {
|
|
const connection = provider.connection;
|
|
const latestBlockhash = await fetchLatestBlockHash(provider, opts);
|
|
|
|
const payer = (provider as AnchorProvider).wallet;
|
|
|
|
//
|
|
// setComputeUnitLimit, hard code to a higher minimum, this is needed so that we dont fail simple UI interactions
|
|
//
|
|
// https://github.com/solana-labs/solana-web3.js/blob/master/packages/library-legacy/src/programs/compute-budget.ts#L202
|
|
const computeUnitLimitIxFound = ixs.some(
|
|
(ix) =>
|
|
ix.programId.equals(COMPUTE_BUDGET_PROGRAM_ID) &&
|
|
u8().decode(ix.data.subarray(0, 1)) == 2,
|
|
);
|
|
|
|
if (!computeUnitLimitIxFound) {
|
|
const totalUserIntendedIxs = ixs.filter(
|
|
(ix) => !ix.programId.equals(COMPUTE_BUDGET_PROGRAM_ID),
|
|
).length;
|
|
const requestCu = Math.min(totalUserIntendedIxs * 300_000, 1_600_000);
|
|
ixs = [
|
|
ComputeBudgetProgram.setComputeUnitLimit({
|
|
units: requestCu,
|
|
}),
|
|
...ixs,
|
|
];
|
|
}
|
|
|
|
//
|
|
// setComputeUnitPrice
|
|
//
|
|
if (opts.prioritizationFee) {
|
|
ixs = [createComputeBudgetIx(opts.prioritizationFee), ...ixs];
|
|
}
|
|
|
|
const message = MessageV0.compile({
|
|
payerKey: (provider as AnchorProvider).wallet.publicKey,
|
|
instructions: ixs,
|
|
recentBlockhash: latestBlockhash.blockhash,
|
|
addressLookupTableAccounts: alts,
|
|
});
|
|
let vtx = new VersionedTransaction(message);
|
|
if (opts?.additionalSigners?.length) {
|
|
vtx.sign([...opts?.additionalSigners]);
|
|
}
|
|
|
|
if (
|
|
typeof payer.signTransaction === 'function' &&
|
|
!(payer instanceof NodeWallet || payer.constructor.name == 'NodeWallet')
|
|
) {
|
|
vtx = (await payer.signTransaction(
|
|
vtx as any,
|
|
)) as unknown as VersionedTransaction;
|
|
} else {
|
|
// Maybe this path is only correct for NodeWallet?
|
|
vtx.sign([(payer as any).payer as Signer]);
|
|
}
|
|
|
|
// if configured, send the transaction using multiple connections
|
|
let signature: string;
|
|
if (opts?.multipleConnections?.length ?? 0 > 0) {
|
|
const allConnections = [connection, ...opts.multipleConnections!];
|
|
signature = await Promise.any(
|
|
allConnections.map((c) => {
|
|
return c.sendRawTransaction(vtx.serialize(), {
|
|
skipPreflight: true, // mergedOpts.skipPreflight,
|
|
});
|
|
}),
|
|
);
|
|
} else {
|
|
signature = await connection.sendRawTransaction(vtx.serialize(), {
|
|
skipPreflight: true, // mergedOpts.skipPreflight,
|
|
});
|
|
}
|
|
|
|
if (opts.postSendTxCallback) {
|
|
try {
|
|
opts.postSendTxCallback({
|
|
txid: signature,
|
|
txSignatureBlockHash: latestBlockhash,
|
|
});
|
|
} catch (e) {
|
|
console.warn(`postSendTxCallback error ${e}`);
|
|
}
|
|
}
|
|
if (!opts.confirmInBackground) {
|
|
return await confirmTransaction(
|
|
connection,
|
|
opts,
|
|
latestBlockhash,
|
|
signature,
|
|
);
|
|
} else {
|
|
confirmTransaction(connection, opts, latestBlockhash, signature);
|
|
return { signature };
|
|
}
|
|
}
|
|
|
|
const confirmTransaction = async (
|
|
connection: Connection,
|
|
opts: Partial<SendTransactionOpts> = {},
|
|
latestBlockhash: Readonly<LatestBlockhash>,
|
|
signature: string,
|
|
): Promise<MangoSignatureStatus> => {
|
|
let status: RpcResponseAndContext<SignatureResult>;
|
|
const allConnections = [connection];
|
|
if (opts.multipleConnections && opts.multipleConnections.length) {
|
|
allConnections.push(...opts.multipleConnections);
|
|
}
|
|
const abortController = new AbortController();
|
|
try {
|
|
if (
|
|
latestBlockhash.blockhash != null &&
|
|
latestBlockhash.lastValidBlockHeight != null
|
|
) {
|
|
status = await Promise.any(
|
|
allConnections.map((c) =>
|
|
awaitTransactionSignatureConfirmation({
|
|
txid: signature,
|
|
confirmLevel: 'processed',
|
|
connection: c,
|
|
timeoutStrategy: {
|
|
block: latestBlockhash,
|
|
},
|
|
abortSignal: abortController.signal,
|
|
}),
|
|
),
|
|
);
|
|
} else {
|
|
status = await Promise.any(
|
|
allConnections.map((c) =>
|
|
awaitTransactionSignatureConfirmation({
|
|
txid: signature,
|
|
confirmLevel: 'processed',
|
|
connection: c,
|
|
timeoutStrategy: {
|
|
timeout: 90,
|
|
},
|
|
abortSignal: abortController.signal,
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
abortController.abort();
|
|
|
|
const signatureResult = status.value;
|
|
if (signatureResult.err) {
|
|
console.warn('Tx status: ', status);
|
|
throw new MangoError({
|
|
txid: signature,
|
|
message: `${JSON.stringify(status)}`,
|
|
});
|
|
}
|
|
if (opts.postTxConfirmationCallback) {
|
|
try {
|
|
opts.postTxConfirmationCallback({
|
|
txid: signature,
|
|
txSignatureBlockHash: latestBlockhash,
|
|
});
|
|
} catch (e) {
|
|
console.warn(`postTxConfirmationCallback error ${e}`);
|
|
}
|
|
}
|
|
return { signature, slot: status.context.slot, ...signatureResult };
|
|
} catch (e) {
|
|
abortController.abort();
|
|
if (e instanceof AggregateError) {
|
|
for (const individualError of e.errors) {
|
|
const stringifiedError = tryStringify(individualError);
|
|
throw new MangoError({
|
|
txid: signature,
|
|
message: `${
|
|
stringifiedError
|
|
? stringifiedError
|
|
: individualError
|
|
? individualError
|
|
: 'Unknown error'
|
|
}`,
|
|
});
|
|
}
|
|
}
|
|
if (isErrorWithSignatureResult(e)) {
|
|
const stringifiedError = tryStringify(e?.value?.err);
|
|
throw new MangoError({
|
|
txid: signature,
|
|
message: `${stringifiedError ? stringifiedError : e?.value?.err}`,
|
|
});
|
|
}
|
|
const stringifiedError = tryStringify(e);
|
|
throw new MangoError({
|
|
txid: signature,
|
|
message: `${stringifiedError ? stringifiedError : e}`,
|
|
});
|
|
}
|
|
};
|
|
|
|
export async function fetchLatestBlockHash(
|
|
provider: AnchorProvider,
|
|
opts: SendTransactionOpts = {},
|
|
): Promise<LatestBlockhash> {
|
|
if (opts.latestBlockhash) {
|
|
return opts.latestBlockhash;
|
|
}
|
|
const commitment =
|
|
opts.preflightCommitment ??
|
|
provider.opts.preflightCommitment ??
|
|
'finalized';
|
|
const blockhashRequest =
|
|
await provider.connection.getLatestBlockhashAndContext(commitment);
|
|
return {
|
|
slot: blockhashRequest.context.slot,
|
|
lastValidBlockHeight: blockhashRequest.value.lastValidBlockHeight,
|
|
blockhash: blockhashRequest.value.blockhash,
|
|
};
|
|
}
|
|
|
|
export const createComputeBudgetIx = (
|
|
microLamports: number,
|
|
): TransactionInstruction => {
|
|
const computeBudgetIx = ComputeBudgetProgram.setComputeUnitPrice({
|
|
microLamports,
|
|
});
|
|
return computeBudgetIx;
|
|
};
|
|
|
|
export class MangoError extends Error {
|
|
message: string;
|
|
txid: string;
|
|
|
|
constructor({ txid, message }) {
|
|
super();
|
|
this.message = message;
|
|
this.txid = txid;
|
|
}
|
|
}
|
|
|
|
function isErrorWithSignatureResult(
|
|
err: any,
|
|
): err is RpcResponseAndContext<SignatureResult> {
|
|
return err && typeof err.value !== 'undefined';
|
|
}
|