import { Provider } from "@project-serum/anchor"; import { SendTxRequest } from "@project-serum/anchor/dist/provider"; import { sleep } from "@project-serum/common"; import { Connection as ConnectionSolanaWeb3, sendAndConfirmRawTransaction, Transaction, TransactionSignature, } from "@solana/web3.js"; import { Construction, RunConstructionOpts, TxRequestsTyped } from "../index"; import { getAllTxRequests } from "./txs"; const RETRY_ON_TIMEOUT_DEFAULT = 1; const PARALLEL_SETUP_DEFAULT = false; /** * @param txFailed - the tx on which it failed and the instruction idx */ export type SendAllError = { err: any; txGroup: number; txFailed: { txIdx: number; instrIdx: number }; txInner?: number; }; /** * Similar to `send`, but for an array of transactions and signers. * * @params initialActionIdxOffset - the initial offset within the order to start running actions from */ export const sendAllTypedWithError = async ( provider: Provider, reqs: TxRequestsTyped[], construction: Construction, opts: RunConstructionOpts, initialActionOrderOffset?: number ): Promise> => { const { onTxDone, parallelSetup: _pSetup } = opts; const parallelSetup = _pSetup ?? PARALLEL_SETUP_DEFAULT; const blockhash = await provider.connection.getRecentBlockhash( opts.commitment.preflightCommitment ); const allTxRequests = getAllTxRequests(reqs); let txs = allTxRequests.map((r) => { let tx = r.tx.tx; let signers = r.tx.signers; if (signers === undefined) { signers = []; } tx.feePayer = provider.wallet.publicKey; tx.recentBlockhash = blockhash.blockhash; signers .filter((s) => s !== undefined) .forEach((kp) => { tx.partialSign(kp); }); return tx; }); const signedTxs = await provider.wallet.signAllTransactions(txs); let totalDone = 0; const totalNumbTxs = signedTxs.length; const onOneTxDone = () => { const txDone = allTxRequests[totalDone]; let actionNames: string[]; if (txDone.type === "action calls") { const offset = initialActionOrderOffset ?? 0; const actionOrder = txDone.reqInnerIdx + offset; const actionIdxs = construction.actionOrders[actionOrder]; actionNames = actionIdxs.map( (idx) => construction.actions[idx].actionName ); } totalDone += 1; if (onTxDone) onTxDone(totalDone, totalNumbTxs, actionNames); }; if (onTxDone) onTxDone(0, totalNumbTxs); const sigs: string[] = []; let numbSent = 0; for (let i = 0; i < reqs.length; i++) { const numbTxInGroup = reqs[i].txs.flat().length; const sendFN = parallelSetup && reqs[i].type === "token" ? sendSignedInParallel : sendSignedInSequential; const rets = await sendFN( provider, [reqs[i]], signedTxs.slice(numbSent, numbSent + numbTxInGroup), opts, i, onOneTxDone ); sigs.push(...rets); numbSent += numbTxInGroup; } return sigs; }; const sendSignedInParallel = async ( provider: Provider, reqs: TxRequestsTyped[], signedTxs: Transaction[], opts: RunConstructionOpts, txGroupOffset: number, onTxDone: () => void ): Promise => sendSigned(provider, reqs, signedTxs, opts, txGroupOffset, true, onTxDone); const sendSignedInSequential = async ( provider: Provider, reqs: TxRequestsTyped[], signedTxs: Transaction[], opts: RunConstructionOpts, txGroupOffset: number, onTxDone: () => void ): Promise => sendSigned(provider, reqs, signedTxs, opts, txGroupOffset, false, onTxDone); const getInstrIndexFromSkipPreflight = (e: string) => { const rx = /"InstructionError":\[([0-9]+),/g; const arr = rx.exec(e); if (arr && arr.length >= 2) return parseInt(arr[1]); return undefined; }; const getInstrIndexFromPreflight = (e: string) => { const rx = /Instruction ([0-9]+)/g; const arr = rx.exec(e); if (arr && arr.length >= 2) return parseInt(arr[1]); return undefined; }; const sendAndConfirmRawTransactionWrapper = async ( connection: ConnectionSolanaWeb3, tx: Transaction, opts?: RunConstructionOpts ): Promise => { const customConn = new ConnectionSolanaWeb3( //@ts-ignore connection._rpcEndpoint, { commitment: connection.commitment, confirmTransactionInitialTimeout: opts.customTxTimeoutMs, } ); try { const retryAmount = opts?.retryTxOnTimeout ?? RETRY_ON_TIMEOUT_DEFAULT; for (let i = 0; i < retryAmount; i++) { try { const txRet = await sendAndConfirmRawTransaction( customConn, tx.serialize(), opts?.commitment ); // Ensure that enough propagation occurs if (opts?.propagationSleepMs) await sleep(opts?.propagationSleepMs); return txRet; } catch (e) { if ( e.message && e.message.includes("Transaction was not confirmed in") ) { // Check signature ${signature} using the Solana const txSigStr = e.message .split("Check signature ")[1] .split(" using the")[0]; console.info("Rechecking the tx sig string of", txSigStr); // First query the chain if try { const txRet = await connection.getSignatureStatus(txSigStr, { searchTransactionHistory: true, }); if (txRet?.value) { // TODO: wait here for the right status. See https://github.com/Lev-Stambler/malloc-solana2/issues/72 if (!txRet.value?.err) { return txSigStr; } else { throw { errMsg: txRet.value.err }; } } } catch (e) { if (i === retryAmount - 1) throw e; if (e?.errMsg) throw e.errMsg; } // Exponentially back off if a tx is dropped const sleepMs = Math.floor(1000 * Math.pow(2, i)); console.error( e, `Could not confirm transaction in time, retrying round ${i} after sleeping ${sleepMs} ms` ); await sleep(sleepMs); } else { throw e; } } } } catch (e) { console.error(e); throw e; } throw "Transaction could not be confirmed"; }; const sendSigned = async ( provider: Provider, reqs: TxRequestsTyped[], signedTxs: Transaction[], opts: RunConstructionOpts, txGroupOffset: number, parallel: boolean, onTxDone: () => void ): Promise => { const sigs: string[] = []; const sigProms: Promise[] = []; let k = 0; // Go over the outer requests for (let reqGroupIdx = 0; reqGroupIdx < reqs.length; reqGroupIdx++) { // Go over every tx request within an outer request for ( let txReqIdx = 0; txReqIdx < reqs[reqGroupIdx].txs.length; txReqIdx++ ) { // If the inner request is an array of requests, loop over if (Array.isArray(reqs[reqGroupIdx].txs[txReqIdx])) for ( let innerTxReqIdx = 0; innerTxReqIdx < (reqs[reqGroupIdx].txs[txReqIdx] as SendTxRequest[]).length; innerTxReqIdx++ ) { const sendFn = async () => { try { const tx = signedTxs[k]; sigs.push( await sendAndConfirmRawTransactionWrapper( provider.connection, tx, opts ) ); onTxDone(); } catch (e) { // Hmmmm... action calls are wrong // try to extract the instruction index let instrIdx = 0; // set to 0 as default if (e && opts?.commitment?.skipPreflight !== true) { instrIdx = getInstrIndexFromPreflight(e.toString()); } else if (e) { instrIdx = getInstrIndexFromSkipPreflight(e.toString()); } if (!instrIdx) { console.error( "Unknown error instruction, assuming the first instruction", e ); instrIdx = 0; } throw { err: e, txGroup: reqGroupIdx + txGroupOffset, txFailed: { txIdx: txReqIdx, instrIdx: instrIdx, }, txInner: innerTxReqIdx, } as SendAllError; } k++; }; if (!parallel) await sendFn(); else sigProms.push(sendFn()); } else { const sendFn = async () => { try { const tx = signedTxs[k]; sigs.push( await sendAndConfirmRawTransactionWrapper( provider.connection, tx, opts ) ); onTxDone(); } catch (e) { let instrIdx = 0; // set to 0 as default if (e && opts?.commitment?.skipPreflight !== true) { instrIdx = getInstrIndexFromPreflight(e.toString()); } else if (e) { instrIdx = getInstrIndexFromSkipPreflight(e.toString()); } if (!instrIdx) { console.error( "Unknown error instruction, assuming the first instruction", e ); instrIdx = 0; } throw { err: e, txGroup: reqGroupIdx + txGroupOffset, txFailed: { txIdx: txReqIdx, instrIdx }, } as SendAllError; } k++; console.info( `Done with sending and confirming the ${ k === 0 ? "1st" : k === 1 ? "2nd" : k === 2 ? "3rd" : `${k + 1}th` } sequential tx` ); }; if (!parallel) await sendFn(); else sigProms.push(sendFn()); } } } if (parallel) await Promise.all(sigProms); console.info( `Done with sending and confirming the ${sigs.length} ${ parallel ? "parallel" : "sequential" } tx` ); return sigs; };