Explorer: add Raw instruction data to parsed instructions (#13855)
This commit allows users to click the "raw" button on transaction instructions and fetch the raw hex or base64 representations of the instruction. Adds a fetch action to the click event of the "raw" button on the instruction UI. adds a fetchRawTransaction hook that is passed down to the instruction UI components. Adds addition `rawFetchTrigger` and `raw` props passed to the instruction card components.
This commit is contained in:
@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useContext } from "react";
|
||||||
import {
|
import {
|
||||||
TransactionInstruction,
|
TransactionInstruction,
|
||||||
SignatureResult,
|
SignatureResult,
|
||||||
@ -6,6 +6,11 @@ import {
|
|||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { RawDetails } from "./RawDetails";
|
import { RawDetails } from "./RawDetails";
|
||||||
import { RawParsedDetails } from "./RawParsedDetails";
|
import { RawParsedDetails } from "./RawParsedDetails";
|
||||||
|
import { SignatureContext } from "../../pages/TransactionDetailsPage";
|
||||||
|
import {
|
||||||
|
useTransactionDetails,
|
||||||
|
useFetchRawTransaction,
|
||||||
|
} from "providers/transactions/details";
|
||||||
|
|
||||||
type InstructionProps = {
|
type InstructionProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@ -26,6 +31,22 @@ export function InstructionCard({
|
|||||||
}: InstructionProps) {
|
}: InstructionProps) {
|
||||||
const [resultClass] = ixResult(result, index);
|
const [resultClass] = ixResult(result, index);
|
||||||
const [showRaw, setShowRaw] = React.useState(defaultRaw || false);
|
const [showRaw, setShowRaw] = React.useState(defaultRaw || false);
|
||||||
|
const signature = useContext(SignatureContext);
|
||||||
|
const details = useTransactionDetails(signature);
|
||||||
|
let raw: TransactionInstruction | undefined = undefined;
|
||||||
|
if (details) {
|
||||||
|
raw = details?.data?.raw?.transaction.instructions[index];
|
||||||
|
}
|
||||||
|
const fetchRaw = useFetchRawTransaction();
|
||||||
|
const fetchRawTrigger = () => fetchRaw(signature);
|
||||||
|
|
||||||
|
const rawClickHandler = () => {
|
||||||
|
if (!defaultRaw && !showRaw && !raw) {
|
||||||
|
fetchRawTrigger();
|
||||||
|
}
|
||||||
|
|
||||||
|
return setShowRaw((r) => !r);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
@ -42,7 +63,7 @@ export function InstructionCard({
|
|||||||
className={`btn btn-sm d-flex ${
|
className={`btn btn-sm d-flex ${
|
||||||
showRaw ? "btn-black active" : "btn-white"
|
showRaw ? "btn-black active" : "btn-white"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setShowRaw((r) => !r)}
|
onClick={rawClickHandler}
|
||||||
>
|
>
|
||||||
<span className="fe fe-code mr-1"></span>
|
<span className="fe fe-code mr-1"></span>
|
||||||
Raw
|
Raw
|
||||||
@ -53,7 +74,7 @@ export function InstructionCard({
|
|||||||
<tbody className="list">
|
<tbody className="list">
|
||||||
{showRaw ? (
|
{showRaw ? (
|
||||||
"parsed" in ix ? (
|
"parsed" in ix ? (
|
||||||
<RawParsedDetails ix={ix} />
|
<RawParsedDetails ix={ix} raw={raw} />
|
||||||
) : (
|
) : (
|
||||||
<RawDetails ix={ix} />
|
<RawDetails ix={ix} />
|
||||||
)
|
)
|
||||||
|
@ -1,8 +1,21 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { ParsedInstruction } from "@solana/web3.js";
|
import { ParsedInstruction, TransactionInstruction } from "@solana/web3.js";
|
||||||
import { Address } from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
import { wrap } from "utils";
|
||||||
|
|
||||||
|
type RawParsedDetailsProps = {
|
||||||
|
ix: ParsedInstruction;
|
||||||
|
raw?: TransactionInstruction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RawParsedDetails({ ix, raw }: RawParsedDetailsProps) {
|
||||||
|
let hex = null;
|
||||||
|
let b64 = null;
|
||||||
|
if (raw) {
|
||||||
|
hex = wrap(raw.data.toString("hex"), 50);
|
||||||
|
b64 = wrap(raw.data.toString("base64"), 50);
|
||||||
|
}
|
||||||
|
|
||||||
export function RawParsedDetails({ ix }: { ix: ParsedInstruction }) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<tr>
|
<tr>
|
||||||
@ -12,6 +25,24 @@ export function RawParsedDetails({ ix }: { ix: ParsedInstruction }) {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
{hex ? (
|
||||||
|
<tr>
|
||||||
|
<td>Instruction Data (Hex)</td>
|
||||||
|
<td className="text-lg-right">
|
||||||
|
<pre className="d-inline-block text-left mb-0">{hex}</pre>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{b64 ? (
|
||||||
|
<tr>
|
||||||
|
<td>Instruction Data (Base64)</td>
|
||||||
|
<td className="text-lg-right">
|
||||||
|
<pre className="d-inline-block text-left mb-0">{b64}</pre>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Instruction Data (JSON)</td>
|
<td>Instruction Data (JSON)</td>
|
||||||
<td className="text-lg-right">
|
<td className="text-lg-right">
|
||||||
|
@ -41,6 +41,8 @@ type SignatureProps = {
|
|||||||
signature: TransactionSignature;
|
signature: TransactionSignature;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SignatureContext = React.createContext("");
|
||||||
|
|
||||||
enum AutoRefresh {
|
enum AutoRefresh {
|
||||||
Active,
|
Active,
|
||||||
Inactive,
|
Inactive,
|
||||||
@ -107,7 +109,9 @@ export function TransactionDetailsPage({ signature: raw }: SignatureProps) {
|
|||||||
<>
|
<>
|
||||||
<StatusCard signature={signature} autoRefresh={autoRefresh} />
|
<StatusCard signature={signature} autoRefresh={autoRefresh} />
|
||||||
<AccountsCard signature={signature} autoRefresh={autoRefresh} />
|
<AccountsCard signature={signature} autoRefresh={autoRefresh} />
|
||||||
<InstructionsSection signature={signature} />
|
<SignatureContext.Provider value={signature}>
|
||||||
|
<InstructionsSection signature={signature} />
|
||||||
|
</SignatureContext.Provider>
|
||||||
<ProgramLogSection signature={signature} />
|
<ProgramLogSection signature={signature} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -400,6 +404,8 @@ function InstructionsSection({ signature }: SignatureProps) {
|
|||||||
|
|
||||||
if (!status?.data?.info || !details?.data?.transaction) return null;
|
if (!status?.data?.info || !details?.data?.transaction) return null;
|
||||||
|
|
||||||
|
const raw = details.data.raw?.transaction;
|
||||||
|
|
||||||
const { transaction } = details.data.transaction;
|
const { transaction } = details.data.transaction;
|
||||||
if (transaction.message.instructions.length === 0) {
|
if (transaction.message.instructions.length === 0) {
|
||||||
return <ErrorCard retry={refreshDetails} text="No instructions found" />;
|
return <ErrorCard retry={refreshDetails} text="No instructions found" />;
|
||||||
@ -460,7 +466,12 @@ function InstructionsSection({ signature }: SignatureProps) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
const props = { ix: next, result, index };
|
const props = {
|
||||||
|
ix: next,
|
||||||
|
result,
|
||||||
|
index,
|
||||||
|
raw: raw?.instructions[index],
|
||||||
|
};
|
||||||
return <UnknownDetailsCard key={index} {...props} />;
|
return <UnknownDetailsCard key={index} {...props} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
Connection,
|
Connection,
|
||||||
TransactionSignature,
|
TransactionSignature,
|
||||||
ParsedConfirmedTransaction,
|
ParsedConfirmedTransaction,
|
||||||
|
ConfirmedTransaction,
|
||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { useCluster, Cluster } from "../cluster";
|
import { useCluster, Cluster } from "../cluster";
|
||||||
import * as Cache from "providers/cache";
|
import * as Cache from "providers/cache";
|
||||||
@ -11,6 +12,7 @@ import { reportError } from "utils/sentry";
|
|||||||
|
|
||||||
export interface Details {
|
export interface Details {
|
||||||
transaction?: ParsedConfirmedTransaction | null;
|
transaction?: ParsedConfirmedTransaction | null;
|
||||||
|
raw?: ConfirmedTransaction | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = Cache.State<Details>;
|
type State = Cache.State<Details>;
|
||||||
@ -119,3 +121,56 @@ export function useTransactionDetailsCache(): TransactionDetailsCache {
|
|||||||
|
|
||||||
return context.entries;
|
return context.entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchRawTransaction(
|
||||||
|
dispatch: Dispatch,
|
||||||
|
signature: TransactionSignature,
|
||||||
|
cluster: Cluster,
|
||||||
|
url: string
|
||||||
|
) {
|
||||||
|
dispatch({
|
||||||
|
type: ActionType.Update,
|
||||||
|
status: FetchStatus.Fetching,
|
||||||
|
key: signature,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
|
||||||
|
let fetchStatus;
|
||||||
|
let transaction;
|
||||||
|
try {
|
||||||
|
transaction = await new Connection(url).getConfirmedTransaction(signature);
|
||||||
|
fetchStatus = FetchStatus.Fetched;
|
||||||
|
} catch (error) {
|
||||||
|
if (cluster !== Cluster.Custom) {
|
||||||
|
reportError(error, { url });
|
||||||
|
}
|
||||||
|
fetchStatus = FetchStatus.FetchFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ActionType.Update,
|
||||||
|
status: fetchStatus,
|
||||||
|
key: signature,
|
||||||
|
data: {
|
||||||
|
raw: transaction,
|
||||||
|
},
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFetchRawTransaction() {
|
||||||
|
const dispatch = React.useContext(DispatchContext);
|
||||||
|
if (!dispatch) {
|
||||||
|
throw new Error(
|
||||||
|
`useFetchRawTransaaction must be used within a TransactionsProvider`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { cluster, url } = useCluster();
|
||||||
|
return React.useCallback(
|
||||||
|
(signature: TransactionSignature) => {
|
||||||
|
url && fetchRawTransaction(dispatch, signature, cluster, url);
|
||||||
|
},
|
||||||
|
[dispatch, cluster, url]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user