explorer: Prettify program logs for transactions page (#21453)
This commit is contained in:
parent
4b67a6900d
commit
7aad6fa6a6
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -23,7 +23,7 @@ export function MemoDetailsCard({
|
|||
ix={ix}
|
||||
index={index}
|
||||
result={result}
|
||||
title="Memo"
|
||||
title="Memo Program: Memo"
|
||||
innerCards={innerCards}
|
||||
childIndex={childIndex}
|
||||
>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -24,7 +24,7 @@ export function TransferDetailsCard(props: {
|
|||
ix={ix}
|
||||
index={index}
|
||||
result={result}
|
||||
title="Transfer"
|
||||
title="System Program: Transfer"
|
||||
innerCards={innerCards}
|
||||
childIndex={childIndex}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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";
|
||||
}
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue