explorer: Prettify program logs for transactions page (#21453)

This commit is contained in:
Justin Starry 2021-11-27 19:11:58 -06:00 committed by GitHub
parent 4b67a6900d
commit 7aad6fa6a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 408 additions and 427 deletions

View File

@ -0,0 +1,68 @@
import React from "react";
import { Message, ParsedMessage } from "@solana/web3.js";
import { Cluster } from "providers/cluster";
import { TableCardBody } from "components/common/TableCardBody";
import { programLabel } from "utils/tx";
import { InstructionLogs } from "utils/program-logs";
export function ProgramLogsCardBody({
message,
logs,
cluster,
}: {
message: Message | ParsedMessage;
logs: InstructionLogs[];
cluster: Cluster;
}) {
return (
<TableCardBody>
{message.instructions.map((ix, index) => {
let programId;
if ("programIdIndex" in ix) {
const programAccount = message.accountKeys[ix.programIdIndex];
if ("pubkey" in programAccount) {
programId = programAccount.pubkey;
} else {
programId = programAccount;
}
} else {
programId = ix.programId;
}
const programName =
programLabel(programId.toBase58(), cluster) || "Unknown Program";
const programLogs: InstructionLogs | undefined = logs[index];
let badgeColor = "white";
if (programLogs) {
badgeColor = programLogs.failed ? "warning" : "success";
}
return (
<tr key={index}>
<td>
<div className="d-flex align-items-center">
<span className={`badge badge-soft-${badgeColor} mr-2`}>
#{index + 1}
</span>
{programName} Instruction
</div>
{programLogs && (
<div className="d-flex align-items-start flex-column text-monospace p-2 font-size-sm">
{programLogs.logs.map((log, key) => {
return (
<span key={key}>
<span className="text-muted">{log.prefix}</span>
<span className={`text-${log.style}`}>{log.text}</span>
</span>
);
})}
</div>
)}
</td>
</tr>
);
})}
</TableCardBody>
);
}

View File

@ -152,7 +152,7 @@ export function MangoDetailsCard(props: {
ix={ix}
index={index}
result={result}
title={`Mango: ${title || "Unknown"}`}
title={`Mango Program: ${title || "Unknown"}`}
innerCards={innerCards}
childIndex={childIndex}
defaultRaw

View File

@ -23,7 +23,7 @@ export function MemoDetailsCard({
ix={ix}
index={index}
result={result}
title="Memo"
title="Memo Program: Memo"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -92,7 +92,7 @@ export function SerumDetailsCard(props: {
ix={ix}
index={index}
result={result}
title={`Serum: ${title || "Unknown"}`}
title={`Serum Program: ${title || "Unknown"}`}
innerCards={innerCards}
childIndex={childIndex}
defaultRaw

View File

@ -5,6 +5,8 @@ import {
ParsedInstruction,
} from "@solana/web3.js";
import { InstructionCard } from "./InstructionCard";
import { programLabel } from "utils/tx";
import { useCluster } from "providers/cluster";
export function UnknownDetailsCard({
ix,
@ -19,12 +21,15 @@ export function UnknownDetailsCard({
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { cluster } = useCluster();
const programName =
programLabel(ix.programId.toBase58(), cluster) || "Unknown Program";
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title="Unknown"
title={`${programName}: Unknown Instruction`}
innerCards={innerCards}
childIndex={childIndex}
defaultRaw

View File

@ -19,7 +19,7 @@ export function CancelOrderDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Serum: Cancel Order"
title="Serum Program: Cancel Order"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -15,7 +15,7 @@ export function AddOracleDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Mango: AddOracle"
title="Mango Program: AddOracle"
innerCards={innerCards}
childIndex={childIndex}
></InstructionCard>

View File

@ -18,7 +18,7 @@ export function AddPerpMarketDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Mango: AddPerpMarket"
title="Mango Program: AddPerpMarket"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -17,7 +17,7 @@ export function AddSpotMarketDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Mango: AddSpotMarket"
title="Mango Program: AddSpotMarket"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -24,7 +24,7 @@ export function CancelPerpOrderDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Mango: CancelPerpOrder"
title="Mango Program: CancelPerpOrder"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -24,7 +24,7 @@ export function CancelSpotOrderDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Mango: CancelSpotOrder"
title="Mango Program: CancelSpotOrder"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -52,7 +52,7 @@ export function ChangePerpMarketParamsDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Mango: ChangePerpMarketParams"
title="Mango Program: ChangePerpMarketParams"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -23,7 +23,7 @@ export function ConsumeEventsDetailsCard(props: {
ix={ix}
index={index}
result={result}
title={"Mango: ConsumeEvents"}
title={"Mango Program: ConsumeEvents"}
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -27,7 +27,7 @@ export function GenericMngoAccountDetailsCard(props: {
ix={ix}
index={index}
result={result}
title={"Mango: " + title}
title={"Mango Program: " + title}
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -35,7 +35,7 @@ export function GenericPerpMngoDetailsCard(props: {
ix={ix}
index={index}
result={result}
title={"Mango: " + title}
title={"Mango Program: " + title}
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -35,7 +35,7 @@ export function GenericSpotMngoDetailsCard(props: {
ix={ix}
index={index}
result={result}
title={"Mango: " + title}
title={"Mango Program: " + title}
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -58,7 +58,7 @@ export function PlacePerpOrderDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Mango: PlacePerpOrder"
title="Mango Program: PlacePerpOrder"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -68,7 +68,7 @@ export function PlaceSpotOrderDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Mango: PlaceSpotOrder"
title="Mango Program: PlaceSpotOrder"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -19,7 +19,7 @@ export function CancelOrderByClientIdDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Serum: Cancel Order By Client Id"
title="Serum Program: Cancel Order By Client Id"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -19,7 +19,7 @@ export function CancelOrderDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Serum: Cancel Order"
title="Serum Program: Cancel Order"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -19,7 +19,7 @@ export function ConsumeEventsDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Serum: Consume Events"
title="Serum Program: Consume Events"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -19,7 +19,7 @@ export function InitializeMarketDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Serum: Initialize Market"
title="Serum Program: Initialize Market"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -19,7 +19,7 @@ export function MatchOrdersDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Serum: Match Orders"
title="Serum Program: Match Orders"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -19,7 +19,7 @@ export function NewOrderDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Serum: New Order"
title="Serum Program: New Order"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -19,7 +19,7 @@ export function SettleFundsDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Serum: Settle Funds"
title="Serum Program: Settle Funds"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -23,7 +23,7 @@ export function AuthorizeDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Stake Authorize"
title="Stake Program: Authorize"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -23,7 +23,7 @@ export function DeactivateDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Deactivate Stake"
title="Stake Program: Deactivate Stake"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -23,7 +23,7 @@ export function DelegateDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Delegate Stake"
title="Stake Program: Delegate Stake"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -26,7 +26,7 @@ export function InitializeDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Stake Initialize"
title="Stake Program: Initialize Stake"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -23,7 +23,7 @@ export function MergeDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Stake Merge"
title="Stake Program: Merge Stake"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -24,7 +24,7 @@ export function SplitDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Split Stake"
title="Stake Program: Split Stake"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -24,7 +24,7 @@ export function WithdrawDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Withdraw Stake"
title="System Program: Withdraw Stake"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -23,7 +23,7 @@ export function AllocateDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Allocate Account"
title="System Program: Allocate Account"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -24,7 +24,7 @@ export function AllocateWithSeedDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Allocate Account w/ Seed"
title="System Program: Allocate Account w/ Seed"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -23,7 +23,7 @@ export function AssignDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Assign Account"
title="System Program: Assign Account"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -24,7 +24,7 @@ export function AssignWithSeedDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Assign Account w/ Seed"
title="System Program: Assign Account w/ Seed"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -24,7 +24,7 @@ export function CreateDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Create Account"
title="System Program: Create Account"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -25,7 +25,7 @@ export function CreateWithSeedDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Create Account w/ Seed"
title="System Program: Create Account w/ Seed"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -23,7 +23,7 @@ export function NonceAdvanceDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Advance Nonce"
title="System Program: Advance Nonce"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -23,7 +23,7 @@ export function NonceAuthorizeDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Authorize Nonce"
title="System Program: Authorize Nonce"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -23,7 +23,7 @@ export function NonceInitializeDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Initialize Nonce"
title="System Program: Initialize Nonce"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -24,7 +24,7 @@ export function NonceWithdrawDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Withdraw Nonce"
title="System Program: Withdraw Nonce"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -24,7 +24,7 @@ export function TransferDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Transfer"
title="System Program: Transfer"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -25,7 +25,7 @@ export function TransferWithSeedDetailsCard(props: {
ix={ix}
index={index}
result={result}
title="Transfer w/ Seed"
title="System Program: Transfer w/ Seed"
innerCards={innerCards}
childIndex={childIndex}
>

View File

@ -40,7 +40,7 @@ export function TokenDetailsCard(props: DetailsProps) {
const parsed = create(props.ix.parsed, ParsedInfo);
const { type: rawType, info } = parsed;
const type = create(rawType, TokenInstructionType);
const title = `Token: ${IX_TITLES[type]}`;
const title = `Token Program: ${IX_TITLES[type]}`;
const created = create(info, IX_STRUCTS[type] as any);
return <TokenInstruction title={title} info={created} {...props} />;
} catch (err) {

View File

@ -1,227 +1,33 @@
import React from "react";
import { SignatureProps } from "pages/TransactionDetailsPage";
import { useTransactionDetails } from "providers/transactions";
import { TransactionError } from "@solana/web3.js";
const transactionErrorMessage: Map<string, string> = new Map([
["AccountInUse", "Account in use"],
["AccountLoadedTwice", "Account loaded twice"],
[
"AccountNotFound",
"Attempt to debit an account but found no record of a prior credit.",
],
["ProgramAccountNotFound", "Attempt to load a program that does not exist"],
["InsufficientFundsForFee", "Insufficient funds for fee"],
[
"InvalidAccountForFee",
"This account may not be used to pay transaction fees",
],
["AlreadyProcessed", "This transaction has already been processed"],
["BlockhashNotFound", "Blockhash not found"],
["CallChainTooDeep", "Loader call chain is too deep"],
[
"MissingSignatureForFee",
"Transaction requires a fee but has no signature present",
],
["InvalidAccountIndex", "Transaction contains an invalid account reference"],
["SignatureFailure", "Transaction did not pass signature verification"],
[
"InvalidProgramForExecution",
"This program may not be used for executing instructions",
],
[
"SanitizeFailure",
"Transaction failed to sanitize accounts offsets correctly",
],
[
"ClusterMaintenance",
"Transactions are currently disabled due to cluster maintenance",
],
[
"AccountBorrowOutstanding",
"Transaction processing left an account with an outstanding borrowed reference",
],
["InstructionError", "Error processing Instruction {0}: {1}"],
]);
const instructionErrorMessage: Map<string, string> = new Map([
["GenericError", "generic instruction error"],
["InvalidArgument", "invalid program argument"],
["InvalidInstructionData", "invalid instruction data"],
["InvalidAccountData", "invalid account data for instruction"],
["AccountDataTooSmall", "account data too small for instruction"],
["InsufficientFunds", "insufficient funds for instruction"],
["IncorrectProgramId", "incorrect program id for instruction"],
["MissingRequiredSignature", "missing required signature for instruction"],
[
"AccountAlreadyInitialized",
"instruction requires an uninitialized account",
],
["UninitializedAccount", "instruction requires an initialized account"],
[
"UnbalancedInstruction",
"sum of account balances before and after instruction do not match",
],
["ModifiedProgramId", "instruction modified the program id of an account"],
[
"ExternalAccountLamportSpend",
"instruction spent from the balance of an account it does not own",
],
[
"ExternalAccountDataModified",
"instruction modified data of an account it does not own",
],
[
"ReadonlyLamportChange",
"instruction changed the balance of a read-only account",
],
["ReadonlyDataModified", "instruction modified data of a read-only account"],
["DuplicateAccountIndex", "instruction contains duplicate accounts"],
["ExecutableModified", "instruction changed executable bit of an account"],
["RentEpochModified", "instruction modified rent epoch of an account"],
["NotEnoughAccountKeys", "insufficient account keys for instruction"],
["AccountDataSizeChanged", "non-system instruction changed account size"],
["AccountNotExecutable", "instruction expected an executable account"],
[
"AccountBorrowFailed",
"instruction tries to borrow reference for an account which is already borrowed",
],
[
"AccountBorrowOutstanding",
"instruction left account with an outstanding borrowed reference",
],
[
"DuplicateAccountOutOfSync",
"instruction modifications of multiply-passed account differ",
],
["Custom", "custom program error: {0}"],
["InvalidError", "program returned invalid error code"],
["ExecutableDataModified", "instruction changed executable accounts data"],
[
"ExecutableLamportChange",
"instruction changed the balance of a executable account",
],
["ExecutableAccountNotRentExempt", "executable accounts must be rent exempt"],
["UnsupportedProgramId", "Unsupported program id"],
["CallDepth", "Cross-program invocation call depth too deep"],
["MissingAccount", "An account required by the instruction is missing"],
[
"ReentrancyNotAllowed",
"Cross-program invocation reentrancy not allowed for this instruction",
],
[
"MaxSeedLengthExceeded",
"Length of the seed is too long for address generation",
],
["InvalidSeeds", "Provided seeds do not result in a valid address"],
["InvalidRealloc", "Failed to reallocate account data"],
["ComputationalBudgetExceeded", "Computational budget exceeded"],
[
"PrivilegeEscalation",
"Cross-program invocation with unauthorized signer or writable account",
],
[
"ProgramEnvironmentSetupFailure",
"Failed to create program execution environment",
],
["ProgramFailedToComplete", "Program failed to complete"],
["ProgramFailedToCompile", "Program failed to compile"],
["Immutable", "Account is immutable"],
["IncorrectAuthority", "Incorrect authority provided"],
["BorshIoError", "Failed to serialize or deserialize account data: {0}"],
[
"AccountNotRentExempt",
"An account does not have enough lamports to be rent-exempt",
],
["InvalidAccountOwner", "Invalid account owner"],
["ArithmeticOverflow", "Program arithmetic overflowed"],
["UnsupportedSysvar", "Unsupported sysvar"],
["IllegalOwner", "Provided owner is not allowed"],
]);
function getTransactionError(
error?: TransactionError | null
): string | undefined {
if (!error) {
return;
}
if (typeof error === "string") {
const message = transactionErrorMessage.get(error);
if (message) {
return message;
}
} else if ("InstructionError" in error) {
const out = transactionErrorMessage.get("InstructionError");
const innerError = error["InstructionError"];
const index = innerError[0];
const instructionError = innerError[1];
if (out) {
return out
.replace("{0}", index)
.replace("{1}", getInstructionError(instructionError));
}
}
return "Unknown transaction error";
}
function getInstructionError(error: TransactionError): string {
let out;
let value;
if (typeof error === "string") {
const message = instructionErrorMessage.get(error);
if (message) {
return message;
}
} else if ("Custom" in error) {
out = instructionErrorMessage.get("Custom");
value = error["Custom"][0];
} else if ("BorshIoError" in error) {
out = instructionErrorMessage.get("BorshIoError");
value = error["BorshIoError"][0];
}
if (out && value) {
return out.replace("{0}", value);
}
return "Unknown instruction error";
}
import { ProgramLogsCardBody } from "components/ProgramLogsCardBody";
import { prettyProgramLogs } from "utils/program-logs";
import { useCluster } from "providers/cluster";
export function ProgramLogSection({ signature }: SignatureProps) {
const { cluster } = useCluster();
const details = useTransactionDetails(signature);
const logMessages = details?.data?.transaction?.meta?.logMessages;
const transactionError = getTransactionError(
details?.data?.transaction?.meta?.err
);
const transaction = details?.data?.transaction;
if (!transaction) return null;
const message = transaction.transaction.message;
if ((!logMessages || logMessages.length < 1) && !transactionError) {
return null;
}
const logMessages = transaction.meta?.logMessages || null;
const err = transaction.meta?.err || null;
const prettyLogs = prettyProgramLogs(logMessages, err, cluster);
return (
<>
<div className="container">
<div className="header">
<div className="header-body">
<h3 className="card-header-title">Program Log</h3>
</div>
</div>
</div>
<div className="card">
<ul className="log-messages">
{logMessages &&
logMessages.map((message, key) => (
<li key={key}>{message.replace(/^Program log: /, "")}</li>
))}
{transactionError && (
<li className="mt-3">Transaction failed: {transactionError}</li>
)}
</ul>
<div className="card-header">
<h3 className="card-header-title">Program Logs</h3>
</div>
<ProgramLogsCardBody
message={message}
logs={prettyLogs}
cluster={cluster}
/>
</div>
</>
);

View File

@ -2,19 +2,8 @@ import React from "react";
import bs58 from "bs58";
import { Connection, Message, Transaction } from "@solana/web3.js";
import { useCluster } from "providers/cluster";
import { TableCardBody } from "components/common/TableCardBody";
import { programLabel } from "utils/tx";
type LogMessage = {
text: string;
prefix: string;
style: "muted" | "info" | "success" | "warning";
};
type InstructionLogs = {
logs: LogMessage[];
failed: boolean;
};
import { InstructionLogs, prettyProgramLogs } from "utils/program-logs";
import { ProgramLogsCardBody } from "components/ProgramLogsCardBody";
const DEFAULT_SIGNATURE = bs58.encode(Buffer.alloc(64).fill(0));
@ -78,46 +67,7 @@ export function SimulatorCard({ message }: { message: Message }) {
Retry
</button>
</div>
<TableCardBody>
{message.instructions.map((ix, index) => {
const programId = message.accountKeys[ix.programIdIndex];
const programName =
programLabel(programId.toBase58(), cluster) || "Unknown";
const programLogs: InstructionLogs | undefined = logs[index];
let badgeColor = "white";
if (programLogs) {
badgeColor = programLogs.failed ? "warning" : "success";
}
return (
<tr key={index}>
<td>
<div className="d-flex align-items-center">
<span className={`badge badge-soft-${badgeColor} mr-2`}>
#{index + 1}
</span>
{programName} Instruction
</div>
{programLogs && (
<div className="d-flex align-items-start flex-column text-monospace p-2 font-size-sm">
{programLogs.logs.map((log, key) => {
return (
<span key={key}>
<span className="text-muted">{log.prefix}</span>
<span className={`text-${log.style}`}>
{log.text}
</span>
</span>
);
})}
</div>
)}
</td>
</tr>
);
})}
</TableCardBody>
<ProgramLogsCardBody message={message} logs={logs} cluster={cluster} />
</div>
);
}
@ -152,124 +102,8 @@ function useSimulator(message: Message) {
// Simulate without signers to skip signer verification
const resp = await connection.simulateTransaction(tx);
let depth = 0;
let instructionLogs: InstructionLogs[] = [];
const prefixBuilder = (depth: number) => {
const prefix = new Array(depth - 1).fill("\u00A0\u00A0").join("");
return prefix + "> ";
};
let instructionError;
const responseLogs = resp.value.logs;
const responseErr = resp.value.err;
if (!responseLogs) {
if (resp.value.err) throw new Error(JSON.stringify(responseErr));
throw new Error("No logs detected");
} else if (responseErr) {
if (typeof responseErr !== "string") {
let ixError = (responseErr as any)["InstructionError"];
const [index, message] = ixError;
if (typeof message === "string") {
instructionError = { index, message };
}
} else {
throw new Error(responseErr);
}
}
responseLogs.forEach((log) => {
if (log.startsWith("Program log:")) {
instructionLogs[instructionLogs.length - 1].logs.push({
prefix: prefixBuilder(depth),
text: log,
style: "muted",
});
} else {
const regex = /Program (\w*) invoke \[(\d)\]/g;
const matches = [...log.matchAll(regex)];
if (matches.length > 0) {
const programAddress = matches[0][1];
const programName =
programLabel(programAddress, cluster) ||
`Unknown (${programAddress}) Program`;
if (depth === 0) {
instructionLogs.push({
logs: [],
failed: false,
});
} else {
instructionLogs[instructionLogs.length - 1].logs.push({
prefix: prefixBuilder(depth),
style: "info",
text: `Invoking ${programName}`,
});
}
depth++;
} else if (log.includes("success")) {
instructionLogs[instructionLogs.length - 1].logs.push({
prefix: prefixBuilder(depth),
style: "success",
text: `Program returned success`,
});
depth--;
} else if (log.includes("failed")) {
const instructionLog =
instructionLogs[instructionLogs.length - 1];
if (!instructionLog.failed) {
instructionLog.failed = true;
instructionLog.logs.push({
prefix: prefixBuilder(depth),
style: "warning",
text: `Program returned error: ${log.slice(
log.indexOf(": ") + 2
)}`,
});
}
depth--;
} else {
if (depth === 0) {
instructionLogs.push({
logs: [],
failed: false,
});
depth++;
}
// system transactions don't start with "Program log:"
instructionLogs[instructionLogs.length - 1].logs.push({
prefix: prefixBuilder(depth),
text: log,
style: "muted",
});
}
}
});
// If the instruction's simulation returned an error without any logs then add an empty log entry for Runtime error
// For example BpfUpgradableLoader fails without returning any logs for Upgrade instruction with buffer that doesn't exist
if (instructionError && instructionLogs.length === 0) {
instructionLogs.push({
logs: [],
failed: true,
});
}
if (
instructionError &&
instructionError.index === instructionLogs.length - 1
) {
const failedIx = instructionLogs[instructionError.index];
failedIx.failed = true;
failedIx.logs.push({
prefix: prefixBuilder(1),
text: `Runtime error: ${instructionError.message}`,
style: "warning",
});
}
setLogs(instructionLogs);
// Prettify logs
setLogs(prettyProgramLogs(resp.value.logs, resp.value.err, cluster));
} catch (err) {
console.error(err);
setLogs(null);

View File

@ -0,0 +1,144 @@
import { TransactionError } from "@solana/web3.js";
const instructionErrorMessage: Map<string, string> = new Map([
["GenericError", "generic instruction error"],
["InvalidArgument", "invalid program argument"],
["InvalidInstructionData", "invalid instruction data"],
["InvalidAccountData", "invalid account data for instruction"],
["AccountDataTooSmall", "account data too small for instruction"],
["InsufficientFunds", "insufficient funds for instruction"],
["IncorrectProgramId", "incorrect program id for instruction"],
["MissingRequiredSignature", "missing required signature for instruction"],
[
"AccountAlreadyInitialized",
"instruction requires an uninitialized account",
],
["UninitializedAccount", "instruction requires an initialized account"],
[
"UnbalancedInstruction",
"sum of account balances before and after instruction do not match",
],
["ModifiedProgramId", "instruction modified the program id of an account"],
[
"ExternalAccountLamportSpend",
"instruction spent from the balance of an account it does not own",
],
[
"ExternalAccountDataModified",
"instruction modified data of an account it does not own",
],
[
"ReadonlyLamportChange",
"instruction changed the balance of a read-only account",
],
["ReadonlyDataModified", "instruction modified data of a read-only account"],
["DuplicateAccountIndex", "instruction contains duplicate accounts"],
["ExecutableModified", "instruction changed executable bit of an account"],
["RentEpochModified", "instruction modified rent epoch of an account"],
["NotEnoughAccountKeys", "insufficient account keys for instruction"],
["AccountDataSizeChanged", "non-system instruction changed account size"],
["AccountNotExecutable", "instruction expected an executable account"],
[
"AccountBorrowFailed",
"instruction tries to borrow reference for an account which is already borrowed",
],
[
"AccountBorrowOutstanding",
"instruction left account with an outstanding borrowed reference",
],
[
"DuplicateAccountOutOfSync",
"instruction modifications of multiply-passed account differ",
],
["Custom", "custom program error: {0}"],
["InvalidError", "program returned invalid error code"],
["ExecutableDataModified", "instruction changed executable accounts data"],
[
"ExecutableLamportChange",
"instruction changed the balance of a executable account",
],
["ExecutableAccountNotRentExempt", "executable accounts must be rent exempt"],
["UnsupportedProgramId", "Unsupported program id"],
["CallDepth", "Cross-program invocation call depth too deep"],
["MissingAccount", "An account required by the instruction is missing"],
[
"ReentrancyNotAllowed",
"Cross-program invocation reentrancy not allowed for this instruction",
],
[
"MaxSeedLengthExceeded",
"Length of the seed is too long for address generation",
],
["InvalidSeeds", "Provided seeds do not result in a valid address"],
["InvalidRealloc", "Failed to reallocate account data"],
["ComputationalBudgetExceeded", "Computational budget exceeded"],
[
"PrivilegeEscalation",
"Cross-program invocation with unauthorized signer or writable account",
],
[
"ProgramEnvironmentSetupFailure",
"Failed to create program execution environment",
],
["ProgramFailedToComplete", "Program failed to complete"],
["ProgramFailedToCompile", "Program failed to compile"],
["Immutable", "Account is immutable"],
["IncorrectAuthority", "Incorrect authority provided"],
["BorshIoError", "Failed to serialize or deserialize account data: {0}"],
[
"AccountNotRentExempt",
"An account does not have enough lamports to be rent-exempt",
],
["InvalidAccountOwner", "Invalid account owner"],
["ArithmeticOverflow", "Program arithmetic overflowed"],
["UnsupportedSysvar", "Unsupported sysvar"],
["IllegalOwner", "Provided owner is not allowed"],
]);
export type ProgramError = {
index: number;
message: string;
};
export function getTransactionInstructionError(
error?: TransactionError | null
): ProgramError | undefined {
if (!error) {
return;
}
if (typeof error === "object" && "InstructionError" in error) {
const innerError = error["InstructionError"];
const index = innerError[0] as number;
const instructionError = innerError[1];
return {
index,
message: getInstructionError(instructionError),
};
}
}
function getInstructionError(error: any): string {
let out;
let value;
if (typeof error === "string") {
const message = instructionErrorMessage.get(error);
if (message) {
return message;
}
} else if ("Custom" in error) {
out = instructionErrorMessage.get("Custom");
value = error["Custom"];
} else if ("BorshIoError" in error) {
out = instructionErrorMessage.get("BorshIoError");
value = error["BorshIoError"];
}
if (out && value) {
return out.replace("{0}", value);
}
return "Unknown instruction error";
}

View File

@ -0,0 +1,124 @@
import { TransactionError } from "@solana/web3.js";
import { Cluster } from "providers/cluster";
import { programLabel } from "utils/tx";
import { getTransactionInstructionError } from "utils/program-err";
export type LogMessage = {
text: string;
prefix: string;
style: "muted" | "info" | "success" | "warning";
};
export type InstructionLogs = {
logs: LogMessage[];
failed: boolean;
};
export function prettyProgramLogs(
logs: string[] | null,
error: TransactionError | null,
cluster: Cluster
): InstructionLogs[] {
let depth = 0;
let prettyLogs: InstructionLogs[] = [];
const prefixBuilder = (depth: number) => {
const prefix = new Array(depth - 1).fill("\u00A0\u00A0").join("");
return prefix + "> ";
};
let prettyError;
if (!logs) {
if (error) throw new Error(JSON.stringify(error));
throw new Error("No logs detected");
} else if (error) {
prettyError = getTransactionInstructionError(error);
}
logs.forEach((log) => {
if (log.startsWith("Program log:")) {
prettyLogs[prettyLogs.length - 1].logs.push({
prefix: prefixBuilder(depth),
text: log,
style: "muted",
});
} else {
const regex = /Program (\w*) invoke \[(\d)\]/g;
const matches = [...log.matchAll(regex)];
if (matches.length > 0) {
const programAddress = matches[0][1];
const programName =
programLabel(programAddress, cluster) ||
`Unknown (${programAddress}) Program`;
if (depth === 0) {
prettyLogs.push({
logs: [],
failed: false,
});
} else {
prettyLogs[prettyLogs.length - 1].logs.push({
prefix: prefixBuilder(depth),
style: "info",
text: `Invoking ${programName}`,
});
}
depth++;
} else if (log.includes("success")) {
prettyLogs[prettyLogs.length - 1].logs.push({
prefix: prefixBuilder(depth),
style: "success",
text: `Program returned success`,
});
depth--;
} else if (log.includes("failed")) {
const instructionLog = prettyLogs[prettyLogs.length - 1];
if (!instructionLog.failed) {
instructionLog.failed = true;
instructionLog.logs.push({
prefix: prefixBuilder(depth),
style: "warning",
text: `Program returned error: ${log.slice(log.indexOf(": ") + 2)}`,
});
}
depth--;
} else {
if (depth === 0) {
prettyLogs.push({
logs: [],
failed: false,
});
depth++;
}
// system transactions don't start with "Program log:"
prettyLogs[prettyLogs.length - 1].logs.push({
prefix: prefixBuilder(depth),
text: log,
style: "muted",
});
}
}
});
// If the instruction's simulation returned an error without any logs then add an empty log entry for Runtime error
// For example BpfUpgradableLoader fails without returning any logs for Upgrade instruction with buffer that doesn't exist
if (prettyError && prettyLogs.length === 0) {
prettyLogs.push({
logs: [],
failed: true,
});
}
if (prettyError && prettyError.index === prettyLogs.length - 1) {
const failedIx = prettyLogs[prettyError.index];
failedIx.failed = true;
failedIx.logs.push({
prefix: prefixBuilder(1),
text: `Runtime error: ${prettyError.message}`,
style: "warning",
});
}
return prettyLogs;
}