From 25559d5733562b95d1356170cd33aa4bdc970311 Mon Sep 17 00:00:00 2001 From: bartosz-lipinski <264380+bartosz-lipinski@users.noreply.github.com> Date: Thu, 22 Apr 2021 23:28:08 -0500 Subject: [PATCH] feat: add retry --- packages/common/src/contexts/connection.tsx | 224 ++++++++++++++++---- 1 file changed, 186 insertions(+), 38 deletions(-) diff --git a/packages/common/src/contexts/connection.tsx b/packages/common/src/contexts/connection.tsx index 5c8509c..6f5eb23 100644 --- a/packages/common/src/contexts/connection.tsx +++ b/packages/common/src/contexts/connection.tsx @@ -1,12 +1,15 @@ -import { useLocalStorageState } from '../utils/utils'; +import { sleep, useLocalStorageState } from '../utils/utils'; import { Account, BlockhashAndFeeCalculator, clusterApiUrl, Commitment, Connection, + RpcResponseAndContext, + SimulatedTransactionResponse, Transaction, TransactionInstruction, + TransactionSignature, } from '@solana/web3.js'; import React, { useContext, useEffect, useMemo, useState } from 'react'; import { notify } from '../utils/notifications'; @@ -364,43 +367,188 @@ export const sendTransaction = async ( 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 connection.confirmTransaction( - txid, - options && (options.commitment as any), - ); - const status = confirmation.value; - slot = confirmation.context.slot; - - if (status?.err) { - const errors = await getErrorForTransaction(connection, txid); - notify({ - message: 'Transaction failed...', - description: ( - <> - {errors.map(err => ( -
{err}
- ))} - - - ), - type: 'error', - }); - - throw new Error( - `Raw transaction ${txid} failed (${JSON.stringify(status)})`, - ); - } - } + 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, +}: { + signedTransaction: Transaction; + connection: Connection; + sendingMessage?: string; + sentMessage?: string; + successMessage?: string; + timeout?: number; +}): Promise<{ txid: string, slot: number }> { + const rawTransaction = signedTransaction.serialize(); + const startTime = getUnixTs(); + let slot = 0; + const txid: TransactionSignature = 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(300); + } + })(); + try { + slot = await awaitTransactionSignatureConfirmation(txid, timeout, connection); + } catch (err) { + if (err.timeout) { + throw new Error('Timed out awaiting confirmation on transaction'); + } + let simulateResult: SimulatedTransactionResponse | null = 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)); + } + throw new Error('Transaction failed'); + } finally { + done = true; + } + + console.log('Latency', txid, getUnixTs() - startTime); + return { txid, slot }; +} + +async function simulateTransaction( + connection: Connection, + transaction: Transaction, + commitment: Commitment, +): Promise> { + // @ts-ignore + transaction.recentBlockhash = await connection._recentBlockhash( + // @ts-ignore + connection._disableBlockhashCaching, + ); + + const signData = transaction.serializeMessage(); + // @ts-ignore + const wireTransaction = transaction._serialize(signData); + const encodedTransaction = wireTransaction.toString('base64'); + const config: any = { encoding: 'base64', commitment }; + const args = [encodedTransaction, config]; + + // @ts-ignore + 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: TransactionSignature, + timeout: number, + connection: Connection, +) { + let done = false; + let slot = 0; + let subId = 0; + await new Promise((resolve, reject) => { + (async () => { + setTimeout(() => { + if (done) { + return; + } + done = true; + console.log('Timed out for txid', txid); + reject({ timeout: true }); + }, timeout); + try { + subId = connection.onSignature( + txid, + (result) => { + console.log('WS confirmed', txid, result); + done = true; + if (result.err) { + reject(result.err); + } else { + resolve(result); + } + }, + 'recent', + ); + console.log('Set up WS connection', txid); + } catch (e) { + done = true; + console.log('WS error in setup', txid, e); + } + while (!done) { + // eslint-disable-next-line no-loop-func + (async () => { + try { + const signatureStatuses = await connection.getSignatureStatuses([ + txid, + ]); + slot = signatureStatuses && signatureStatuses.context.slot; + const result = signatureStatuses && signatureStatuses.value[0]; + if (!done) { + if (!result) { + console.log('REST null result for', txid, result); + } else if (result.err) { + console.log('REST error for', txid, result); + done = true; + reject(result.err); + } else if (!result.confirmations) { + console.log('REST no confirmations for', txid, result); + } else { + console.log('REST confirmation for', txid, result); + done = true; + resolve(result); + } + } + } catch (e) { + if (!done) { + console.log('REST connection error: txid', txid, e); + } + } + })(); + await sleep(500); + } + })(); + }).catch(_ => { + connection.removeSignatureListener(subId); + }).then(_ => { + connection.removeSignatureListener(subId); + }); + done = true; + return slot; +}