import type { AnchorProvider } from "@coral-xyz/anchor"; import { SendTransactionError } from "@solana/web3.js"; import type { CreateVersionedTransactionArgs } from "./createVersionedTransaction.js"; import { createVersionedTransaction } from "./createVersionedTransaction.js"; import type { TransactionEnvelope } from "./transactionEnvelope.js"; /** * Sends and confirms a transaction using an AnchorProvider. * @param provider * @param txs * @param signers * @returns */ export const sendAndConfirmTransaction = async ( provider: AnchorProvider, tx: TransactionEnvelope, opts: Omit< CreateVersionedTransactionArgs, "tx" | "payerKey" | "connection" > = {}, ) => { const { transaction, latestBlockhash } = await createVersionedTransaction({ connection: provider.connection, tx, payerKey: provider.wallet.publicKey, ...opts, }); const signedTx = await provider.wallet.signTransaction(transaction); const hash = await provider.connection.sendTransaction(signedTx); await provider.connection.confirmTransaction( { signature: hash, ...latestBlockhash, }, "confirmed", ); return hash; }; export const sendAndConfirmTransactionWithRetry = async ( provider: AnchorProvider, tx: TransactionEnvelope, { retryNum = 0, timeoutMilliseconds = 180_000, ...opts }: Omit & { retryNum?: number; timeoutMilliseconds?: number; } = {}, ): Promise => { const connection = provider.connection; const vt = await createVersionedTransaction({ connection: provider.connection, tx, payerKey: provider.wallet.publicKey, ...opts, }); try { const hash = await Promise.race([ (async () => { const hash = await connection.sendTransaction(vt.transaction); await connection.confirmTransaction( { signature: hash, ...vt.latestBlockhash, }, "processed", ); return hash; })(), (async () => { await new Promise((resolve) => setTimeout(resolve, timeoutMilliseconds), ); throw Error("Timeout"); })(), ]); return hash; } catch (e) { if ( [ "Timeout", "failed to send transaction: Transaction simulation failed: Blockhash not found", ].includes((e as Error).message) && retryNum < 100 ) { console.log("Retrying...", retryNum, "-", (e as Error).message); await new Promise((resolve) => setTimeout(resolve, 1000)); return sendAndConfirmTransactionWithRetry(provider, tx, { ...opts, retryNum: retryNum + 1, timeoutMilliseconds, }); } else { console.log("TX failed"); console.log(btoa(String.fromCharCode(...vt.transaction.serialize()))); throw e; } } }; export async function retryTransaction( func: (attempt: number) => Promise, handleError: (err: Error) => boolean, retries = 100, ) { const exceptions: string[] = []; for (let i = 0; i < retries; i++) { try { return await func(i); } catch (e) { exceptions.push((e as Error).message); if ( [ "Timeout", "failed to send transaction: Transaction simulation failed: Blockhash not found", ].includes((e as Error).message) || ["TransactionExpiredBlockheightExceededError"].includes( (e as Error).name, ) ) { console.log(`Retry ${i.toLocaleString()} due to timeout`); continue; } else { if (handleError(e as Error)) { console.log(`Retry ${i.toLocaleString()} due to acceptable error`); continue; } //console.error('Failed to send transaction', e); throw e; } } } throw new Error( `Failed to execute transaction after ${retries.toLocaleString()} retries with errors: ${exceptions.join(", ")}`, ); } export function errorIsExceededCU(err: Error) { if (err instanceof SendTransactionError) { if ( err.logs?.find( (log) => log.includes("Computational budget exceeded") || log.includes("exceeded CUs meter"), ) ) { return true; } } return false; }