From f967790753da80864ddefcb0a21009cd9c5ae992 Mon Sep 17 00:00:00 2001 From: Ian Macalinao Date: Tue, 4 Jan 2022 21:21:55 -0500 Subject: [PATCH] feat: Improve transaction error logs (#443) * feat: allow hiding error logs * misc utils * adds formatInstructionLogs to solana-contrib * simulation error stringify cleanup --- packages/chai-solana/src/expectTXTable.ts | 6 ++-- .../chai-solana/src/printInstructionLogs.ts | 15 ++++++-- packages/chai-solana/src/utils.ts | 11 +++++- packages/solana-contrib/src/broadcaster.ts | 34 +++++++++++++++---- .../src/transaction/TransactionEnvelope.ts | 5 +-- .../src/transaction/parseTransactionLogs.ts | 18 ++++++++++ packages/solana-contrib/src/utils/index.ts | 1 + packages/solana-contrib/src/utils/misc.ts | 18 ++++++++++ .../solana-contrib/src/utils/printTXTable.ts | 7 ++-- .../simulateTransactionWithCommitment.ts | 6 +++- 10 files changed, 101 insertions(+), 20 deletions(-) create mode 100644 packages/solana-contrib/src/utils/misc.ts diff --git a/packages/chai-solana/src/expectTXTable.ts b/packages/chai-solana/src/expectTXTable.ts index d003d0842..5b6454b7b 100644 --- a/packages/chai-solana/src/expectTXTable.ts +++ b/packages/chai-solana/src/expectTXTable.ts @@ -1,7 +1,7 @@ import type { TransactionEnvelope } from "@saberhq/solana-contrib"; import { parseTransactionLogs, printTXTable } from "@saberhq/solana-contrib"; -import { formatInstructionLogs } from "./printInstructionLogs"; +import { formatInstructionLogsForConsole } from "./printInstructionLogs"; import { expectTX } from "./utils"; /** @@ -92,7 +92,7 @@ export const expectTXTable = ( ) { if (formatLogs) { const parsed = parseTransactionLogs(logs, simulation.value.err); - const fmt = formatInstructionLogs(parsed); + const fmt = formatInstructionLogsForConsole(parsed); console.log(fmt); } else { console.log(logs.join("\n")); @@ -120,7 +120,7 @@ export const expectTXTable = ( lastLine = curLine; } } - console.log(" ", simulation.value.err); + console.log(" ", JSON.stringify(simulation.value.err, null, 2)); } } }) diff --git a/packages/chai-solana/src/printInstructionLogs.ts b/packages/chai-solana/src/printInstructionLogs.ts index db9b988c8..269e64380 100644 --- a/packages/chai-solana/src/printInstructionLogs.ts +++ b/packages/chai-solana/src/printInstructionLogs.ts @@ -1,12 +1,13 @@ import type { InstructionLogs } from "@saberhq/solana-contrib"; -import { formatLogEntry } from "@saberhq/solana-contrib"; +import { formatLogEntry, parseTransactionLogs } from "@saberhq/solana-contrib"; +import type { SendTransactionError } from "@solana/web3.js"; import colors from "colors/safe"; /** * Formats instruction logs to be printed to the console. * @param logs */ -export const formatInstructionLogs = ( +export const formatInstructionLogsForConsole = ( logs: readonly InstructionLogs[] ): string => logs @@ -39,3 +40,13 @@ export const formatInstructionLogs = ( ].join("\n"); }) .join("\n"); + +export const printSendTransactionError = (err: SendTransactionError) => { + try { + const parsed = parseTransactionLogs(err.logs ?? null, err); + console.error(formatInstructionLogsForConsole(parsed)); + } catch (e) { + console.error(`Could not parse transaction error`, e); + console.error("SendTransactionError", err); + } +}; diff --git a/packages/chai-solana/src/utils.ts b/packages/chai-solana/src/utils.ts index a4a6b337c..24a512f89 100644 --- a/packages/chai-solana/src/utils.ts +++ b/packages/chai-solana/src/utils.ts @@ -6,8 +6,11 @@ import type { TransactionReceipt, } from "@saberhq/solana-contrib"; import { PendingTransaction } from "@saberhq/solana-contrib"; +import { SendTransactionError } from "@solana/web3.js"; import { assert, expect } from "chai"; +import { printSendTransactionError } from "./printInstructionLogs"; + export const expectTX = ( tx: | TransactionEnvelope @@ -44,7 +47,13 @@ export const expectTX = ( } else { return expect( tx - ?.send() + ?.send({ printLogs: false }) + .catch((err) => { + if (err instanceof SendTransactionError) { + printSendTransactionError(err); + } + throw err; + }) .then((res) => res.wait()) .then(handleReceipt), msg diff --git a/packages/solana-contrib/src/broadcaster.ts b/packages/solana-contrib/src/broadcaster.ts index 2c0363e23..cd1a00bde 100644 --- a/packages/solana-contrib/src/broadcaster.ts +++ b/packages/solana-contrib/src/broadcaster.ts @@ -9,9 +9,20 @@ import type { } from "@solana/web3.js"; import type { Broadcaster } from "."; -import { DEFAULT_PROVIDER_OPTIONS, PendingTransaction } from "."; +import { + DEFAULT_PROVIDER_OPTIONS, + PendingTransaction, + suppressConsoleErrorAsync, +} from "."; import { simulateTransactionWithCommitment } from "./utils/simulateTransactionWithCommitment"; +export interface BroadcastOptions extends ConfirmOptions { + /** + * Prints the transaction logs as emitted by @solana/web3.js. Defaults to true. + */ + printLogs?: boolean; +} + /** * Broadcasts transactions to a single connection. */ @@ -38,16 +49,27 @@ export class SingleConnectionBroadcaster implements Broadcaster { */ async broadcast( tx: Transaction, - opts: ConfirmOptions = this.opts + { printLogs = true, ...opts }: BroadcastOptions = this.opts ): Promise { if (tx.signatures.length === 0) { throw new Error("Transaction must be signed before broadcasting."); } const rawTx = tx.serialize(); - return new PendingTransaction( - this.sendConnection, - await this.sendConnection.sendRawTransaction(rawTx, opts) - ); + + if (printLogs) { + return new PendingTransaction( + this.sendConnection, + await this.sendConnection.sendRawTransaction(rawTx, opts) + ); + } + + return await suppressConsoleErrorAsync(async () => { + // hide the logs of TX errors if printLogs = false + return new PendingTransaction( + this.sendConnection, + await this.sendConnection.sendRawTransaction(rawTx, opts) + ); + }); } /** diff --git a/packages/solana-contrib/src/transaction/TransactionEnvelope.ts b/packages/solana-contrib/src/transaction/TransactionEnvelope.ts index 06679951d..89bd29a57 100644 --- a/packages/solana-contrib/src/transaction/TransactionEnvelope.ts +++ b/packages/solana-contrib/src/transaction/TransactionEnvelope.ts @@ -9,6 +9,7 @@ import type { } from "@solana/web3.js"; import { Transaction } from "@solana/web3.js"; +import type { BroadcastOptions } from ".."; import { printTXTable } from ".."; import type { Provider } from "../interfaces"; import type { PendingTransaction } from "./PendingTransaction"; @@ -142,7 +143,7 @@ export class TransactionEnvelope { * @param opts * @returns */ - async send(opts?: ConfirmOptions): Promise { + async send(opts?: BroadcastOptions): Promise { const signed = await this.provider.signer.sign( this.build(), this.signers, @@ -155,7 +156,7 @@ export class TransactionEnvelope { * Sends the transaction and waits for confirmation. * @param opts */ - async confirm(opts?: ConfirmOptions): Promise { + async confirm(opts?: BroadcastOptions): Promise { return (await this.send(opts)).wait(); } diff --git a/packages/solana-contrib/src/transaction/parseTransactionLogs.ts b/packages/solana-contrib/src/transaction/parseTransactionLogs.ts index 67ff509e7..66a68a085 100644 --- a/packages/solana-contrib/src/transaction/parseTransactionLogs.ts +++ b/packages/solana-contrib/src/transaction/parseTransactionLogs.ts @@ -206,3 +206,21 @@ export const formatLogEntry = ( const prefixString = prefix ? buildPrefix(entry.depth) : ""; return `${prefixString}${formatLogEntryString(entry)}`; }; + +/** + * Formats instruction logs. + * @param logs + */ +export const formatInstructionLogs = ( + logs: readonly InstructionLogs[] +): string => + logs + .map((log, i) => { + return [ + `=> Instruction #${i}: ${ + log.programAddress ? `Program ${log.programAddress}` : "System" + }`, + ...log.logs.map((entry) => formatLogEntry(entry, true)), + ].join("\n"); + }) + .join("\n"); diff --git a/packages/solana-contrib/src/utils/index.ts b/packages/solana-contrib/src/utils/index.ts index 1bb57eff5..da66eac03 100644 --- a/packages/solana-contrib/src/utils/index.ts +++ b/packages/solana-contrib/src/utils/index.ts @@ -1,4 +1,5 @@ export * from "./instructions"; +export * from "./misc"; export * from "./printAccountOwners"; export * from "./printTXTable"; export * from "./publicKey"; diff --git a/packages/solana-contrib/src/utils/misc.ts b/packages/solana-contrib/src/utils/misc.ts new file mode 100644 index 000000000..9437fe76d --- /dev/null +++ b/packages/solana-contrib/src/utils/misc.ts @@ -0,0 +1,18 @@ +/** + * Hide the console.error because @solana/web3.js often emits noisy errors as a + * side effect. There are use cases of estimateTransactionSize where we + * frequently build transactions that are likely too big. + */ +export const suppressConsoleErrorAsync = async ( + fn: () => Promise +): Promise => { + const oldConsoleError = console.error; + try { + const result = await fn(); + console.error = oldConsoleError; + return result; + } catch (e) { + console.error = oldConsoleError; + throw e; + } +}; diff --git a/packages/solana-contrib/src/utils/printTXTable.ts b/packages/solana-contrib/src/utils/printTXTable.ts index 7062b9976..721c72d0e 100644 --- a/packages/solana-contrib/src/utils/printTXTable.ts +++ b/packages/solana-contrib/src/utils/printTXTable.ts @@ -184,14 +184,11 @@ const instructionsSize = ( return estimateTransactionSize(instructionedTx); }; + let fakeSigner: Signer | undefined = undefined; const getFakeSigner = (): Signer => { if (!fakeSigner) { - const fakeSignerKp = Keypair.generate(); - fakeSigner = { - publicKey: fakeSignerKp.publicKey, - secretKey: fakeSignerKp.secretKey, - }; + fakeSigner = Keypair.generate(); } return fakeSigner; }; diff --git a/packages/solana-contrib/src/utils/simulateTransactionWithCommitment.ts b/packages/solana-contrib/src/utils/simulateTransactionWithCommitment.ts index 118c2dd4b..7fa994396 100644 --- a/packages/solana-contrib/src/utils/simulateTransactionWithCommitment.ts +++ b/packages/solana-contrib/src/utils/simulateTransactionWithCommitment.ts @@ -5,6 +5,7 @@ import type { SimulatedTransactionResponse, Transaction, } from "@solana/web3.js"; +import { SendTransactionError } from "@solana/web3.js"; /** * Copy of Connection.simulateTransaction that takes a commitment parameter. @@ -50,7 +51,10 @@ export async function simulateTransactionWithCommitment( config, ]); if (res.error) { - throw new Error("failed to simulate transaction: " + res.error.message); + throw new SendTransactionError( + "failed to simulate transaction: " + res.error.message, + res.result.value.logs ?? undefined + ); } return res.result; }