Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Improve transaction error logs #443

Merged
merged 4 commits into from Jan 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions 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";

/**
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -120,7 +120,7 @@ export const expectTXTable = (
lastLine = curLine;
}
}
console.log(" ", simulation.value.err);
console.log(" ", JSON.stringify(simulation.value.err, null, 2));
}
}
})
Expand Down
15 changes: 13 additions & 2 deletions 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
Expand Down Expand Up @@ -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);
}
};
11 changes: 10 additions & 1 deletion packages/chai-solana/src/utils.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
34 changes: 28 additions & 6 deletions packages/solana-contrib/src/broadcaster.ts
Expand Up @@ -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.
*/
Expand All @@ -38,16 +49,27 @@ export class SingleConnectionBroadcaster implements Broadcaster {
*/
async broadcast(
tx: Transaction,
opts: ConfirmOptions = this.opts
{ printLogs = true, ...opts }: BroadcastOptions = this.opts
): Promise<PendingTransaction> {
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)
);
});
}

/**
Expand Down
Expand Up @@ -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";
Expand Down Expand Up @@ -142,7 +143,7 @@ export class TransactionEnvelope {
* @param opts
* @returns
*/
async send(opts?: ConfirmOptions): Promise<PendingTransaction> {
async send(opts?: BroadcastOptions): Promise<PendingTransaction> {
const signed = await this.provider.signer.sign(
this.build(),
this.signers,
Expand All @@ -155,7 +156,7 @@ export class TransactionEnvelope {
* Sends the transaction and waits for confirmation.
* @param opts
*/
async confirm(opts?: ConfirmOptions): Promise<TransactionReceipt> {
async confirm(opts?: BroadcastOptions): Promise<TransactionReceipt> {
return (await this.send(opts)).wait();
}

Expand Down
18 changes: 18 additions & 0 deletions packages/solana-contrib/src/transaction/parseTransactionLogs.ts
Expand Up @@ -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");
1 change: 1 addition & 0 deletions 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";
Expand Down
18 changes: 18 additions & 0 deletions 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 <T>(
fn: () => Promise<T>
): Promise<T> => {
const oldConsoleError = console.error;
try {
const result = await fn();
console.error = oldConsoleError;
return result;
} catch (e) {
console.error = oldConsoleError;
throw e;
}
};
7 changes: 2 additions & 5 deletions packages/solana-contrib/src/utils/printTXTable.ts
Expand Up @@ -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;
};
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}