import { Keypair , Commitment , Connection , RpcResponseAndContext , SignatureStatus , SimulatedTransactionResponse , Transaction , TransactionInstruction , TransactionSignature , Blockhash , FeeCalculator , } from '@solana/web3.js' ; import { WalletNotConnectedError } from '@solana/wallet-adapter-base' ; interface BlockhashAndFeeCalculator { blockhash : Blockhash ; feeCalculator : FeeCalculator ; } export const getErrorForTransaction = async ( connection , txid ) => { await connection . confirmTransaction ( txid , 'max' ) ; const tx = await connection . getParsedConfirmedTransaction ( txid ) ; const errors = [ ] ; if ( tx ?. meta && tx . meta . logMessages ) { tx . meta . logMessages . forEach ( log => { const regex = /Error: (.*)/gm ; let m ; while ( ( m = regex . exec ( log ) ) !== null ) { if ( m . index === regex . lastIndex ) { regex . lastIndex ++ ; } if ( m . length > 1 ) { errors . push ( m [ 1 ] ) ; } } } ) ; } return errors ; } ; export enum SequenceType { Sequential , Parallel , StopOnFailure } export async function sendTransactionsWithManualRetry ( connection , wallet , instructions , signers ) { let stopPoint = 0 ; let tries = 0 ; let lastInstructionsLength = null ; let toRemoveSigners = { } ; instructions = instructions . filter ( ( instr , i ) => { if ( instr . length > 0 ) { return true ; } else { toRemoveSigners [ i ] = true ; return false ; } } ) ; let ids = [ ] ; let filteredSigners = signers . filter ( ( _ , i ) => ! toRemoveSigners [ i ] ) ; while ( stopPoint < instructions . length && tries < 3 ) { instructions = instructions . slice ( stopPoint , instructions . length ) ; filteredSigners = filteredSigners . slice ( stopPoint , filteredSigners . length ) ; if ( instructions . length === lastInstructionsLength ) tries = tries + 1 ; else tries = 0 ; try { if ( instructions . length === 1 ) { const id = await sendTransactionWithRetry ( connection , wallet , instructions [ 0 ] , filteredSigners [ 0 ] , 'single' ) ; ids . push ( id . txid ) ; stopPoint = 1 ; } else { const { txs } = await sendTransactions ( connection , wallet , instructions , filteredSigners , SequenceType . StopOnFailure , 'single' ) ; ids = ids . concat ( txs . map ( t => t . txid ) ) ; } } catch ( e ) { console . error ( e ) ; } console . log ( 'Died on ' , stopPoint , 'retrying from instruction' , instructions [ stopPoint ] , 'instructions length is' , instructions . length ) ; lastInstructionsLength = instructions . length ; } return ids ; } export const sendTransactions = async ( connection , wallet , instructionSet , signersSet , sequenceType = SequenceType . Parallel , commitment = 'singleGossip' , successCallback = ( txid , ind ) => { } , failCallback = ( txid , ind ) => false , block ? ) : Promise < { number : number ; txs : { txid : string ; slot : number ; } [ ] ; } > => { if ( ! wallet . publicKey ) throw new WalletNotConnectedError ( ) ; const unsignedTxns = [ ] ; if ( ! block ) { block = await connection . getRecentBlockhash ( commitment ) ; } for ( let i = 0 ; i < instructionSet . length ; i ++ ) { const instructions = instructionSet [ i ] ; const signers = signersSet [ i ] ; if ( instructions . length === 0 ) { continue ; } let transaction = new Transaction ( ) ; instructions . forEach ( instruction => transaction . add ( instruction ) ) ; transaction . recentBlockhash = block . blockhash ; transaction . setSigners ( wallet . publicKey , ... signers . map ( s => s . publicKey ) ) ; if ( signers . length > 0 ) { transaction . partialSign ( ... signers ) ; } unsignedTxns . push ( transaction ) ; } const signedTxns = await wallet . signAllTransactions ( unsignedTxns ) ; const pendingTxns = [ ] ; let breakEarlyObject = { breakEarly : false , i : 0 } ; console . log ( 'Signed txns length' , signedTxns . length , 'vs handed in length' , instructionSet . length ) ; for ( let i = 0 ; i < signedTxns . length ; i ++ ) { const signedTxnPromise = sendSignedTransaction ( { connection , signedTransaction : signedTxns [ i ] , } ) ; signedTxnPromise . then ( ( { txid , slot } ) => { successCallback ( txid , i ) ; } ) . catch ( reason => { failCallback ( signedTxns [ i ] , i ) ; if ( sequenceType === SequenceType . StopOnFailure ) { breakEarlyObject . breakEarly = true ; breakEarlyObject . i = i ; } } ) ; if ( sequenceType !== SequenceType . Parallel ) { try { await signedTxnPromise ; } catch ( e ) { console . log ( 'Caught failure' , e ) ; if ( breakEarlyObject . breakEarly ) { console . log ( 'Died on ' , breakEarlyObject . i ) ; return { number : breakEarlyObject . i , txs : await Promise . all ( pendingTxns ) , } ; } } } else { pendingTxns . push ( signedTxnPromise ) ; } } if ( sequenceType !== SequenceType . Parallel ) { await Promise . all ( pendingTxns ) ; } return { number : signedTxns . length , txs : await Promise . all ( pendingTxns ) } ; } ; export const sendTransaction = async ( connection , wallet , instructions , signers , awaitConfirmation = true , commitment = 'singleGossip' , includesFeePayer = false , block ? ) => { if ( ! wallet . publicKey ) throw new WalletNotConnectedError ( ) ; let transaction = new Transaction ( ) ; instructions . forEach ( instruction => transaction . add ( instruction ) ) ; transaction . recentBlockhash = ( block || ( await connection . getRecentBlockhash ( commitment ) ) ) . blockhash ; if ( includesFeePayer ) { transaction . setSigners ( ... signers . map ( s => s . publicKey ) ) ; } else { transaction . setSigners ( wallet . publicKey , ... signers . map ( s => s . publicKey ) ) ; } if ( signers . length > 0 ) { transaction . partialSign ( ... signers ) ; } if ( ! includesFeePayer ) { transaction = await wallet . signTransaction ( transaction ) ; } const rawTransaction = transaction . serialize ( ) ; let options = { skipPreflight : true , commitment , } ; const txid = await connection . sendRawTransaction ( rawTransaction , options ) ; let slot = 0 ; if ( awaitConfirmation ) { const confirmation = await awaitTransactionSignatureConfirmation ( txid , DEFAULT_TIMEOUT , connection , commitment ) ; if ( ! confirmation ) throw new Error ( 'Timed out awaiting confirmation on transaction' ) ; slot = confirmation ?. slot || 0 ; if ( confirmation ?. err ) { const errors = await getErrorForTransaction ( connection , txid ) ; console . log ( errors ) ; throw new Error ( `Raw transaction ${ txid } failed` ) ; } } return { txid , slot } ; } ; export const sendTransactionWithRetry = async ( connection , wallet , instructions , signers , commitment = 'singleGossip' , includesFeePayer = false , block ? , beforeSend ? ) => { if ( ! wallet . publicKey ) throw new WalletNotConnectedError ( ) ; let transaction = new Transaction ( ) ; instructions . forEach ( instruction => transaction . add ( instruction ) ) ; transaction . recentBlockhash = ( block || ( await connection . getRecentBlockhash ( commitment ) ) ) . blockhash ; if ( includesFeePayer ) { transaction . setSigners ( ... signers . map ( s => s . publicKey ) ) ; } else { transaction . setSigners ( wallet . publicKey , ... signers . map ( s => s . publicKey ) ) ; } if ( signers . length > 0 ) { transaction . partialSign ( ... signers ) ; } if ( ! includesFeePayer ) { transaction = await wallet . signTransaction ( transaction ) ; } if ( beforeSend ) { beforeSend ( ) ; } const { txid , slot } = await sendSignedTransaction ( { connection , signedTransaction : transaction , } ) ; return { txid , slot } ; } ; export const getUnixTs = ( ) => { return new Date ( ) . getTime ( ) / 1000 ; } ; const DEFAULT_TIMEOUT = 15000 ; export async function sendSignedTransaction ( { signedTransaction , connection , timeout = DEFAULT_TIMEOUT , } ) { const rawTransaction = signedTransaction . serialize ( ) ; const startTime = getUnixTs ( ) ; let slot = 0 ; const txid = await connection . sendRawTransaction ( rawTransaction , { skipPreflight : true , } ) ; console . log ( 'Started awaiting confirmation for' , txid ) ; let done = false ; ( async ( ) => { while ( ! done && getUnixTs ( ) - startTime < timeout ) { connection . sendRawTransaction ( rawTransaction , { skipPreflight : true , } ) ; await sleep ( 500 ) ; } } ) ( ) ; try { const confirmation = await awaitTransactionSignatureConfirmation ( txid , timeout , connection , 'recent' , true ) ; if ( ! confirmation ) throw new Error ( 'Timed out awaiting confirmation on transaction' ) ; if ( confirmation . err ) { console . error ( confirmation . err ) ; throw new Error ( 'Transaction failed: Custom instruction error' ) ; } slot = confirmation ?. slot || 0 ; } catch ( err ) { console . error ( 'Timeout Error caught' , err ) ; if ( err . timeout ) { throw new Error ( 'Timed out awaiting confirmation on transaction' ) ; } let simulateResult = null ; try { simulateResult = ( await simulateTransaction ( connection , signedTransaction , 'single' ) ) . value ; } catch ( e ) { } if ( simulateResult && simulateResult . err ) { if ( simulateResult . logs ) { for ( let i = simulateResult . logs . length - 1 ; i >= 0 ; -- i ) { const line = simulateResult . logs [ i ] ; if ( line . startsWith ( 'Program log: ' ) ) { throw new Error ( 'Transaction failed: ' + line . slice ( 'Program log: ' . length ) ) ; } } } throw new Error ( JSON . stringify ( simulateResult . err ) ) ; } } finally { done = true ; } console . log ( 'Latency' , txid , getUnixTs ( ) - startTime ) ; return { txid , slot } ; } async function simulateTransaction ( connection , transaction , commitment ) { transaction . recentBlockhash = await connection . _recentBlockhash ( connection . _disableBlockhashCaching ) ; const signData = transaction . serializeMessage ( ) ; const wireTransaction = transaction . _serialize ( signData ) ; const encodedTransaction = wireTransaction . toString ( 'base64' ) ; const config = { encoding : 'base64' , commitment } ; const args = [ encodedTransaction , config ] ; const res = await connection . _rpcRequest ( 'simulateTransaction' , args ) ; if ( res . error ) { throw new Error ( 'failed to simulate transaction: ' + res . error . message ) ; } return res . result ; } async function awaitTransactionSignatureConfirmation ( txid , timeout , connection , commitment = 'recent' , queryStatus = false ) { let done = false ; let status = { slot : 0 , confirmations : 0 , err : null , } ; let subId = 0 ; status = await new Promise ( async ( resolve , reject ) => { setTimeout ( ( ) => { if ( done ) { return ; } done = true ; console . log ( 'Rejecting for timeout...' ) ; reject ( { timeout : true } ) ; } , timeout ) ; try { subId = connection . onSignature ( txid , ( result , context ) => { done = true ; status = { err : result . err , slot : context . slot , confirmations : 0 , } ; if ( result . err ) { console . log ( 'Rejected via websocket' , result . err ) ; reject ( status ) ; } else { console . log ( 'Resolved via websocket' , result ) ; resolve ( status ) ; } } , commitment ) ; } catch ( e ) { done = true ; console . error ( 'WS error in setup' , txid , e ) ; } while ( ! done && queryStatus ) { ( async ( ) => { try { const signatureStatuses = await connection . getSignatureStatuses ( [ txid , ] ) ; status = signatureStatuses && signatureStatuses . value [ 0 ] ; if ( ! done ) { if ( ! status ) { console . log ( 'REST null result for' , txid , status ) ; } else if ( status . err ) { console . log ( 'REST error for' , txid , status ) ; done = true ; reject ( status . err ) ; } else if ( ! status . confirmations ) { console . log ( 'REST no confirmations for' , txid , status ) ; } else { console . log ( 'REST confirmation for' , txid , status ) ; done = true ; resolve ( status ) ; } } } catch ( e ) { if ( ! done ) { console . log ( 'REST connection error: txid' , txid , e ) ; } } } ) ( ) ; await sleep ( 2000 ) ; } } ) ; if ( connection . _signatureSubscriptions [ subId ] ) connection . removeSignatureListener ( subId ) ; done = true ; console . log ( 'Returning status' , status ) ; return status ; } export function sleep ( ms ) { return new Promise ( resolve => setTimeout ( resolve , ms ) ) ; }