import { Connection, Keypair, Transaction, sendAndConfirmTransaction, SendTransactionError, VersionedTransaction, TransactionMessage, } from "@solana/web3.js"; import { PoolType } from "../types.js"; import { createMarkExtremeInstruction } from "../instructions/markExtreme.js"; import { createSettleMarketInstruction } from "../instructions/settleMarket.js"; import { SOL_USD_PRICE_FEED_ACCOUNT } from "../constants.js"; import { PitProgram } from "../program.js"; /** * TransactionBuilder - Builds and sends transactions with retry logic * * Responsibilities: * - Builds mark_extreme transactions * - Builds settle_market transactions * - Implements exponential backoff retry (3 attempts: 1s, 2s, 4s) * - Handles transaction signing and confirmation */ export class TransactionBuilder { private connection: Connection; private signer: Keypair; private program: PitProgram; private verbose: boolean; constructor( connection: Connection, signer: Keypair, program: PitProgram, verbose: boolean = false ) { this.connection = connection; this.signer = signer; this.program = program; this.verbose = verbose; } /** * Build and send mark_extreme transaction * * @param weekNumber - Market week number * @param poolType - High or Low pool * @param newOutcome - New outcome value * @returns Transaction signature */ async markExtreme(weekNumber: number, poolType: PoolType, newOutcome: number): Promise { this.log( `Building mark_extreme: ${ poolType === PoolType.High ? "High" : "Low" } pool, outcome ${newOutcome}` ); try { // Use Pyth's sponsored push feed account for SOL/USD const ix = await createMarkExtremeInstruction(this.program, { weekNumber, poolType, newOutcome, keeper: this.signer.publicKey, priceUpdate: SOL_USD_PRICE_FEED_ACCOUNT, }); const blockhash = await this.connection.getLatestBlockhash(); const messageV0 = new TransactionMessage({ payerKey: this.signer.publicKey, recentBlockhash: blockhash.blockhash, instructions: [ix], }).compileToV0Message(); const tx = new VersionedTransaction(messageV0); const signature = await this.sendWithRetry(tx, blockhash); this.log(`mark_extreme success: ${signature}`); return signature; } catch (err) { this.log(`mark_extreme error: ${(err as Error).message}`); throw err; } } /** * Build and send settle_market transaction * * @param weekNumber - Market week number * @returns Transaction signature */ async settleMarket(weekNumber: number): Promise { this.log(`Building settle_market for week ${weekNumber}`); try { const ix = await createSettleMarketInstruction(this.program, { weekNumber, settler: this.signer.publicKey, }); const blockhash = await this.connection.getLatestBlockhash(); const messageV0 = new TransactionMessage({ payerKey: this.signer.publicKey, recentBlockhash: blockhash.blockhash, instructions: [ix], }).compileToV0Message(); const tx = new VersionedTransaction(messageV0); const signature = await this.sendWithRetry(tx, blockhash); this.log(`settle_market success: ${signature}`); return signature; } catch (err) { this.log(`settle_market error: ${(err as Error).message}`); throw err; } } /** * Send transaction with exponential backoff retry * * Retry strategy: * - Attempt 1: Immediate * - Attempt 2: After 1s delay * - Attempt 3: After 2s delay * - Attempt 4: After 4s delay (total 3 retries) * * @param tx - Transaction to send * @param blockhash - Recent blockhash info * @param maxRetries - Maximum number of retry attempts (default: 3) * @returns Transaction signature */ private async sendWithRetry( tx: Transaction | VersionedTransaction, blockhash: { blockhash: string; lastValidBlockHeight: number }, maxRetries: number = 3 ): Promise { let lastError: Error | null = null; for (let attempt = 0; attempt < maxRetries; attempt++) { try { let signature: string; if (tx instanceof VersionedTransaction) { tx.sign([this.signer]); signature = await this.connection.sendRawTransaction(tx.serialize()); await this.connection.confirmTransaction( { signature, blockhash: blockhash.blockhash, lastValidBlockHeight: blockhash.lastValidBlockHeight, }, "confirmed" ); } else { signature = await sendAndConfirmTransaction(this.connection, tx, [this.signer], { commitment: "confirmed", }); } return signature; } catch (err) { lastError = err as Error; // Handle blockhash expiration - get fresh blockhash and retry if (err instanceof SendTransactionError) { const errMsg = err.message.toLowerCase(); if (errMsg.includes("blockhash not found") || errMsg.includes("blockhash expired")) { // Get fresh blockhash for retry blockhash = await this.connection.getLatestBlockhash(); if (tx instanceof VersionedTransaction) { const message = TransactionMessage.decompile(tx.message); message.recentBlockhash = blockhash.blockhash; tx.message = message.compileToV0Message(); } else { tx.recentBlockhash = blockhash.blockhash; } } // Don't retry on program errors (invalid state, constraints, etc.) if (errMsg.includes("custom program error") || errMsg.includes("instruction error")) { throw err; } } if (attempt < maxRetries - 1) { // Exponential backoff: 1s, 2s, 4s const delay = 1000 * Math.pow(2, attempt); this.log( `Transaction failed (attempt ${attempt + 1}/${maxRetries}), retrying in ${delay}ms...` ); await new Promise((resolve) => setTimeout(resolve, delay)); } } } throw lastError ?? new Error("Transaction failed after max retries"); } private log(message: string): void { if (this.verbose) { console.log(`[TransactionBuilder] ${message}`); } } }