Explorer: Display CPI details for transaction (#13801)

* explorer: show inner instructions on token history

* resolve warnings

* show inner instructions on transaction details page

* adjust contrast and add inner instructions row

* show inner instructions after slot 46915769

* token history show inner instructions after 46915769

* restrict early slots only on mainnet

* self nit naming

* self nit: better name for constant

* add signature-truncate class

* resolve incoming raw transaction on transactions details
This commit is contained in:
Josh
2020-12-03 12:23:28 -08:00
committed by GitHub
parent 661ca52135
commit d3a4140899
40 changed files with 563 additions and 170 deletions

View File

@ -3,6 +3,7 @@ import {
PublicKey,
ConfirmedSignatureInfo,
ParsedInstruction,
PartiallyDecodedInstruction,
} from "@solana/web3.js";
import { CacheEntry, FetchStatus } from "providers/cache";
import {
@ -40,6 +41,13 @@ import {
isSerumInstruction,
parseSerumInstructionTitle,
} from "components/instruction/serum/types";
import { INNER_INSTRUCTIONS_START_SLOT } from "pages/TransactionDetailsPage";
import { useCluster, Cluster } from "providers/cluster";
type InstructionType = {
name: string;
innerInstructions: (ParsedInstruction | PartiallyDecodedInstruction)[];
};
export function TokenHistoryCard({ pubkey }: { pubkey: PublicKey }) {
const address = pubkey.toBase58();
@ -263,6 +271,7 @@ const TokenTransactionRow = React.memo(
details: CacheEntry<Details> | undefined;
}) => {
const fetchDetails = useFetchTransactionDetails();
const { cluster } = useCluster();
// Fetch details on load
React.useEffect(() => {
@ -309,19 +318,45 @@ const TokenTransactionRow = React.memo(
</tr>
);
const tokenInstructionNames = instructions
.map((ix, index): string | undefined => {
let tokenInstructionNames: InstructionType[] = [];
if (details?.data?.transaction) {
const transaction = details.data.transaction;
tokenInstructionNames = instructions
.map((ix, index): InstructionType | undefined => {
let name = "Unknown";
const innerInstructions: (
| ParsedInstruction
| PartiallyDecodedInstruction
)[] = [];
if (
transaction.meta?.innerInstructions &&
(cluster !== Cluster.MainnetBeta ||
transaction.slot >= INNER_INSTRUCTIONS_START_SLOT)
) {
transaction.meta.innerInstructions.forEach((ix) => {
if (ix.index === index) {
ix.instructions.forEach((inner) => {
innerInstructions.push(inner);
});
}
});
}
let transactionInstruction;
if (details?.data?.transaction?.transaction) {
if (transaction?.transaction) {
transactionInstruction = intoTransactionInstruction(
details.data.transaction.transaction,
index
transaction.transaction,
ix
);
}
if ("parsed" in ix) {
if (ix.program === "spl-token") {
return instructionTypeName(ix, tx);
name = instructionTypeName(ix, tx);
} else {
return undefined;
}
@ -330,7 +365,7 @@ const TokenTransactionRow = React.memo(
isSerumInstruction(transactionInstruction)
) {
try {
return parseSerumInstructionTitle(transactionInstruction);
name = parseSerumInstructionTitle(transactionInstruction);
} catch (error) {
reportError(error, { signature: tx.signature });
return undefined;
@ -340,7 +375,7 @@ const TokenTransactionRow = React.memo(
isTokenSwapInstruction(transactionInstruction)
) {
try {
return parseTokenSwapInstructionTitle(transactionInstruction);
name = parseTokenSwapInstructionTitle(transactionInstruction);
} catch (error) {
reportError(error, { signature: tx.signature });
return undefined;
@ -351,16 +386,23 @@ const TokenTransactionRow = React.memo(
account.equals(TOKEN_PROGRAM_ID)
) >= 0
) {
return "Unknown (Inner)";
}
name = "Unknown (Inner)";
} else {
return undefined;
}
}
return {
name: name,
innerInstructions: innerInstructions,
};
})
.filter((name) => name !== undefined) as string[];
.filter((name) => name !== undefined) as InstructionType[];
}
return (
<>
{tokenInstructionNames.map((typeName, index) => {
{tokenInstructionNames.map((instructionType, index) => {
return (
<tr key={index}>
<td className="w-1">
@ -373,14 +415,16 @@ const TokenTransactionRow = React.memo(
</span>
</td>
<td>
<Address pubkey={mint} link truncate />
<td className="forced-truncate">
<Address pubkey={mint} link truncateUnknown />
</td>
<td>{typeName}</td>
<td>
<Signature signature={tx.signature} link />
<InstructionDetails instructionType={instructionType} tx={tx} />
</td>
<td className="forced-truncate">
<Signature signature={tx.signature} link truncate />
</td>
</tr>
);
@ -389,3 +433,48 @@ const TokenTransactionRow = React.memo(
);
}
);
function InstructionDetails({
instructionType,
tx,
}: {
instructionType: InstructionType;
tx: ConfirmedSignatureInfo;
}) {
const [expanded, setExpanded] = React.useState(false);
let instructionTypes = instructionType.innerInstructions
.map((ix) => {
if ("parsed" in ix && ix.program === "spl-token") {
return instructionTypeName(ix, tx);
}
return undefined;
})
.filter((type) => type !== undefined);
return (
<>
<p className="tree">
{instructionTypes.length > 0 && (
<span
onClick={(e) => {
e.preventDefault();
setExpanded(!expanded);
}}
className={`c-pointer fe mr-2 ${
expanded ? "fe-minus-square" : "fe-plus-square"
}`}
></span>
)}
{instructionType.name}
</p>
{expanded && (
<ul className="tree">
{instructionTypes.map((type, index) => {
return <li key={index}>{type}</li>;
})}
</ul>
)}
</>
);
}

View File

@ -12,9 +12,17 @@ type Props = {
link?: boolean;
raw?: boolean;
truncate?: boolean;
truncateUnknown?: boolean;
};
export function Address({ pubkey, alignRight, link, raw, truncate }: Props) {
export function Address({
pubkey,
alignRight,
link,
raw,
truncate,
truncateUnknown,
}: Props) {
const [state, setState] = useState<CopyState>("copy");
const address = pubkey.toBase58();
const { cluster } = useCluster();
@ -33,6 +41,10 @@ export function Address({ pubkey, alignRight, link, raw, truncate }: Props) {
<span className="fe fe-check-circle"></span>
);
if (truncateUnknown && address === displayAddress(address, cluster)) {
truncate = true;
}
const content = (
<>
<span className="c-pointer font-size-tiny mr-2">{copyIcon}</span>

View File

@ -8,9 +8,10 @@ type Props = {
signature: TransactionSignature;
alignRight?: boolean;
link?: boolean;
truncate?: boolean;
};
export function Signature({ signature, alignRight, link }: Props) {
export function Signature({ signature, alignRight, link, truncate }: Props) {
const [state, setState] = useState<CopyState>("copy");
const copyToClipboard = () => navigator.clipboard.writeText(signature);
@ -40,7 +41,10 @@ export function Signature({ signature, alignRight, link }: Props) {
{copyButton}
<span className="text-monospace">
{link ? (
<Link className="" to={clusterPath(`/tx/${signature}`)}>
<Link
className={truncate ? "text-truncate signature-truncate" : ""}
to={clusterPath(`/tx/${signature}`)}
>
{signature}
</Link>
) : (

View File

@ -20,6 +20,8 @@ type InstructionProps = {
index: number;
ix: TransactionInstruction | ParsedInstruction;
defaultRaw?: boolean;
innerCards?: JSX.Element[];
childIndex?: number;
};
export function InstructionCard({
@ -29,6 +31,8 @@ export function InstructionCard({
index,
ix,
defaultRaw,
innerCards,
childIndex,
}: InstructionProps) {
const [resultClass] = ixResult(result, index);
const [showRaw, setShowRaw] = React.useState(defaultRaw || false);
@ -55,6 +59,7 @@ export function InstructionCard({
<h3 className="card-header-title mb-0 d-flex align-items-center">
<span className={`badge badge-soft-${resultClass} mr-2`}>
#{index + 1}
{childIndex !== undefined ? `.${childIndex + 1}` : ""}
</span>
{title}
</h3>
@ -92,6 +97,14 @@ export function InstructionCard({
) : (
children
)}
{innerCards && innerCards.length > 0 && (
<tr>
<td colSpan={2}>
Inner Instructions
<div className="inner-cards">{innerCards}</div>
</td>
</tr>
)}
</tbody>
</table>
</div>

View File

@ -7,14 +7,25 @@ export function MemoDetailsCard({
ix,
index,
result,
innerCards,
childIndex,
}: {
ix: ParsedInstruction;
index: number;
result: SignatureResult;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const data = wrap(ix.parsed, 50);
return (
<InstructionCard ix={ix} index={index} result={result} title="Memo">
<InstructionCard
ix={ix}
index={index}
result={result}
title="Memo"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Data (UTF-8)</td>
<td className="text-lg-right">

View File

@ -29,8 +29,10 @@ export function SerumDetailsCard(props: {
index: number;
result: SignatureResult;
signature: string;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, signature } = props;
const { ix, index, result, signature, innerCards, childIndex } = props;
const { url } = useCluster();
@ -91,6 +93,8 @@ export function SerumDetailsCard(props: {
index={index}
result={result}
title={`Serum: ${title || "Unknown"}`}
innerCards={innerCards}
childIndex={childIndex}
defaultRaw
/>
);

View File

@ -10,11 +10,15 @@ export function TokenSwapDetailsCard({
index,
result,
signature,
innerCards,
childIndex,
}: {
ix: TransactionInstruction;
index: number;
result: SignatureResult;
signature: string;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { url } = useCluster();
@ -34,6 +38,8 @@ export function TokenSwapDetailsCard({
index={index}
result={result}
title={`Token Swap: ${title || "Unknown"}`}
innerCards={innerCards}
childIndex={childIndex}
defaultRaw
/>
);

View File

@ -10,10 +10,14 @@ export function UnknownDetailsCard({
ix,
index,
result,
innerCards,
childIndex,
}: {
ix: TransactionInstruction | ParsedInstruction;
index: number;
result: SignatureResult;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
return (
<InstructionCard
@ -21,6 +25,8 @@ export function UnknownDetailsCard({
index={index}
result={result}
title="Unknown"
innerCards={innerCards}
childIndex={childIndex}
defaultRaw
/>
);

View File

@ -19,6 +19,8 @@ type DetailsProps = {
ix: ParsedInstruction;
index: number;
result: SignatureResult;
innerCards?: JSX.Element[];
childIndex?: number;
};
export function BpfLoaderDetailsCard(props: DetailsProps) {
@ -50,10 +52,12 @@ type Props<T> = {
index: number;
result: SignatureResult;
info: T;
innerCards?: JSX.Element[];
childIndex?: number;
};
export function BpfLoaderWriteDetailsCard(props: Props<WriteInfo>) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
const bytes = wrap(info.bytes, 50);
return (
<InstructionCard
@ -61,6 +65,8 @@ export function BpfLoaderWriteDetailsCard(props: Props<WriteInfo>) {
index={index}
result={result}
title="BPF Loader 2: Write"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>
@ -94,7 +100,7 @@ export function BpfLoaderWriteDetailsCard(props: Props<WriteInfo>) {
}
export function BpfLoaderFinalizeDetailsCard(props: Props<FinalizeInfo>) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -102,6 +108,8 @@ export function BpfLoaderFinalizeDetailsCard(props: Props<FinalizeInfo>) {
index={index}
result={result}
title="BPF Loader 2: Finalize"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -9,8 +9,10 @@ export function CancelOrderByClientIdDetailsCard(props: {
index: number;
result: SignatureResult;
info: CancelOrderByClientId;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -18,6 +20,8 @@ export function CancelOrderByClientIdDetailsCard(props: {
index={index}
result={result}
title="Serum: Cancel Order By Client Id"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Market</td>

View File

@ -9,8 +9,10 @@ export function CancelOrderDetailsCard(props: {
index: number;
result: SignatureResult;
info: CancelOrder;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -18,6 +20,8 @@ export function CancelOrderDetailsCard(props: {
index={index}
result={result}
title="Serum: Cancel Order"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -9,8 +9,10 @@ export function ConsumeEventsDetailsCard(props: {
index: number;
result: SignatureResult;
info: ConsumeEvents;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -18,6 +20,8 @@ export function ConsumeEventsDetailsCard(props: {
index={index}
result={result}
title="Serum: Consume Events"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -9,8 +9,10 @@ export function InitializeMarketDetailsCard(props: {
index: number;
result: SignatureResult;
info: InitializeMarket;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -18,6 +20,8 @@ export function InitializeMarketDetailsCard(props: {
index={index}
result={result}
title="Serum: Initialize Market"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -9,8 +9,10 @@ export function MatchOrdersDetailsCard(props: {
index: number;
result: SignatureResult;
info: MatchOrders;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -18,6 +20,8 @@ export function MatchOrdersDetailsCard(props: {
index={index}
result={result}
title="Serum: Match Orders"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -9,8 +9,10 @@ export function NewOrderDetailsCard(props: {
index: number;
result: SignatureResult;
info: NewOrder;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -18,6 +20,8 @@ export function NewOrderDetailsCard(props: {
index={index}
result={result}
title="Serum: New Order"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -9,8 +9,10 @@ export function SettleFundsDetailsCard(props: {
index: number;
result: SignatureResult;
info: SettleFunds;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -18,6 +20,8 @@ export function SettleFundsDetailsCard(props: {
index={index}
result={result}
title="Serum: Settle Funds"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -13,8 +13,10 @@ export function AuthorizeDetailsCard(props: {
index: number;
result: SignatureResult;
info: AuthorizeInfo;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -22,6 +24,8 @@ export function AuthorizeDetailsCard(props: {
index={index}
result={result}
title="Stake Authorize"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -13,8 +13,10 @@ export function DeactivateDetailsCard(props: {
index: number;
result: SignatureResult;
info: DeactivateInfo;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -22,6 +24,8 @@ export function DeactivateDetailsCard(props: {
index={index}
result={result}
title="Deactivate Stake"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -13,8 +13,10 @@ export function DelegateDetailsCard(props: {
index: number;
result: SignatureResult;
info: DelegateInfo;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -22,6 +24,8 @@ export function DelegateDetailsCard(props: {
index={index}
result={result}
title="Delegate Stake"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -14,8 +14,10 @@ export function InitializeDetailsCard(props: {
index: number;
result: SignatureResult;
info: InitializeInfo;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -23,6 +25,8 @@ export function InitializeDetailsCard(props: {
index={index}
result={result}
title="Stake Initialize"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -14,11 +14,20 @@ export function SplitDetailsCard(props: {
index: number;
result: SignatureResult;
info: SplitInfo;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard ix={ix} index={index} result={result} title="Split Stake">
<InstructionCard
ix={ix}
index={index}
result={result}
title="Split Stake"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>
<td className="text-lg-right">

View File

@ -29,6 +29,8 @@ type DetailsProps = {
ix: ParsedInstruction;
result: SignatureResult;
index: number;
innerCards?: JSX.Element[];
childIndex?: number;
};
export function StakeDetailsCard(props: DetailsProps) {

View File

@ -14,8 +14,10 @@ export function WithdrawDetailsCard(props: {
index: number;
result: SignatureResult;
info: WithdrawInfo;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -23,6 +25,8 @@ export function WithdrawDetailsCard(props: {
index={index}
result={result}
title="Withdraw Stake"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -13,8 +13,10 @@ export function AllocateDetailsCard(props: {
index: number;
result: SignatureResult;
info: AllocateInfo;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -22,6 +24,8 @@ export function AllocateDetailsCard(props: {
index={index}
result={result}
title="Allocate Account"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -14,8 +14,10 @@ export function AllocateWithSeedDetailsCard(props: {
index: number;
result: SignatureResult;
info: AllocateWithSeedInfo;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -23,6 +25,8 @@ export function AllocateWithSeedDetailsCard(props: {
index={index}
result={result}
title="Allocate Account w/ Seed"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -13,8 +13,10 @@ export function AssignDetailsCard(props: {
index: number;
result: SignatureResult;
info: AssignInfo;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -22,6 +24,8 @@ export function AssignDetailsCard(props: {
index={index}
result={result}
title="Assign Account"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -14,8 +14,10 @@ export function AssignWithSeedDetailsCard(props: {
index: number;
result: SignatureResult;
info: AssignWithSeedInfo;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -23,6 +25,8 @@ export function AssignWithSeedDetailsCard(props: {
index={index}
result={result}
title="Assign Account w/ Seed"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -14,8 +14,10 @@ export function CreateDetailsCard(props: {
index: number;
result: SignatureResult;
info: CreateAccountInfo;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -23,6 +25,8 @@ export function CreateDetailsCard(props: {
index={index}
result={result}
title="Create Account"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -15,8 +15,10 @@ export function CreateWithSeedDetailsCard(props: {
index: number;
result: SignatureResult;
info: CreateAccountWithSeedInfo;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -24,6 +26,8 @@ export function CreateWithSeedDetailsCard(props: {
index={index}
result={result}
title="Create Account w/ Seed"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -13,8 +13,10 @@ export function NonceAdvanceDetailsCard(props: {
index: number;
result: SignatureResult;
info: AdvanceNonceInfo;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -22,6 +24,8 @@ export function NonceAdvanceDetailsCard(props: {
index={index}
result={result}
title="Advance Nonce"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -13,8 +13,10 @@ export function NonceAuthorizeDetailsCard(props: {
index: number;
result: SignatureResult;
info: AuthorizeNonceInfo;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -22,6 +24,8 @@ export function NonceAuthorizeDetailsCard(props: {
index={index}
result={result}
title="Authorize Nonce"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -13,8 +13,10 @@ export function NonceInitializeDetailsCard(props: {
index: number;
result: SignatureResult;
info: InitializeNonceInfo;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -22,6 +24,8 @@ export function NonceInitializeDetailsCard(props: {
index={index}
result={result}
title="Initialize Nonce"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -14,8 +14,10 @@ export function NonceWithdrawDetailsCard(props: {
index: number;
result: SignatureResult;
info: WithdrawNonceInfo;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
@ -23,6 +25,8 @@ export function NonceWithdrawDetailsCard(props: {
index={index}
result={result}
title="Withdraw Nonce"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>

View File

@ -39,6 +39,8 @@ type DetailsProps = {
ix: ParsedInstruction;
result: SignatureResult;
index: number;
innerCards?: JSX.Element[];
childIndex?: number;
};
export function SystemDetailsCard(props: DetailsProps) {

View File

@ -14,11 +14,20 @@ export function TransferDetailsCard(props: {
index: number;
result: SignatureResult;
info: TransferInfo;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info } = props;
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard ix={ix} index={index} result={result} title="Transfer">
<InstructionCard
ix={ix}
index={index}
result={result}
title="Transfer"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>
<td className="text-lg-right">

View File

@ -14,7 +14,7 @@ export const PROGRAM_IDS: string[] = [
const INSTRUCTION_LOOKUP: { [key: number]: string } = {
0: "Initialize Swap",
1: "Swap",
1: "Exchange",
2: "Deposit",
3: "Withdraw",
};

View File

@ -32,6 +32,8 @@ type DetailsProps = {
ix: ParsedInstruction;
result: SignatureResult;
index: number;
innerCards?: JSX.Element[];
childIndex?: number;
};
export function TokenDetailsCard(props: DetailsProps) {
@ -56,6 +58,8 @@ type InfoProps = {
result: SignatureResult;
index: number;
title: string;
innerCards?: JSX.Element[];
childIndex?: number;
};
function TokenInstruction(props: InfoProps) {
@ -195,6 +199,8 @@ function TokenInstruction(props: InfoProps) {
index={props.index}
result={props.result}
title={props.title}
innerCards={props.innerCards}
childIndex={props.childIndex}
>
{attributes}
</InstructionCard>

View File

@ -6,12 +6,19 @@ import {
useTransactionDetails,
} from "providers/transactions";
import { useFetchTransactionDetails } from "providers/transactions/details";
import { useCluster, ClusterStatus } from "providers/cluster";
import { useCluster, ClusterStatus, Cluster } from "providers/cluster";
import {
TransactionSignature,
SystemProgram,
SystemInstruction,
ParsedInstruction,
PartiallyDecodedInstruction,
SignatureResult,
ParsedTransaction,
ParsedInnerInstruction,
Transaction,
} from "@solana/web3.js";
import { PublicKey } from "@solana/web3.js";
import { lamportsToSolString } from "utils";
import { UnknownDetailsCard } from "components/instruction/UnknownDetailsCard";
import { SystemDetailsCard } from "components/instruction/system/SystemDetailsCard";
@ -36,6 +43,7 @@ import { MemoDetailsCard } from "components/instruction/MemoDetailsCard";
const AUTO_REFRESH_INTERVAL = 2000;
const ZERO_CONFIRMATION_BAILOUT = 5;
export const INNER_INSTRUCTIONS_START_SLOT = 46915769;
type SignatureProps = {
signature: TransactionSignature;
@ -194,7 +202,10 @@ function StatusCard({
const blockhash = transaction?.message.recentBlockhash;
const isNonce = (() => {
if (!transaction) return false;
const ix = intoTransactionInstruction(transaction, 0);
const ix = intoTransactionInstruction(
transaction,
transaction.message.instructions[0]
);
return (
ix &&
SystemProgram.programId.equals(ix.programId) &&
@ -399,6 +410,7 @@ function AccountsCard({
function InstructionsSection({ signature }: SignatureProps) {
const status = useTransactionStatus(signature);
const details = useTransactionDetails(signature);
const { cluster } = useCluster();
const fetchDetails = useFetchTransactionDetails();
const refreshDetails = () => fetchDetails(signature);
@ -407,95 +419,66 @@ function InstructionsSection({ signature }: SignatureProps) {
const raw = details.data.raw?.transaction;
const { transaction } = details.data.transaction;
const { meta } = details.data.transaction;
if (transaction.message.instructions.length === 0) {
return <ErrorCard retry={refreshDetails} text="No instructions found" />;
}
const innerInstructions: {
[index: number]: (ParsedInstruction | PartiallyDecodedInstruction)[];
} = {};
if (
meta?.innerInstructions &&
(cluster !== Cluster.MainnetBeta ||
details.data.transaction.slot >= INNER_INSTRUCTIONS_START_SLOT)
) {
meta.innerInstructions.forEach((parsed: ParsedInnerInstruction) => {
if (!innerInstructions[parsed.index]) {
innerInstructions[parsed.index] = [];
}
parsed.instructions.forEach((ix) => {
innerInstructions[parsed.index].push(ix);
});
});
}
const result = status.data.info.result;
const instructionDetails = transaction.message.instructions.map(
(next, index) => {
if ("parsed" in next) {
switch (next.program) {
case "spl-token":
return (
<TokenDetailsCard
key={index}
tx={transaction}
ix={next}
result={result}
index={index}
/>
);
case "bpf-loader":
return (
<BpfLoaderDetailsCard
key={index}
tx={transaction}
ix={next}
result={result}
index={index}
/>
);
case "system":
return (
<SystemDetailsCard
key={index}
tx={transaction}
ix={next}
result={result}
index={index}
/>
);
case "stake":
return (
<StakeDetailsCard
key={index}
tx={transaction}
ix={next}
result={result}
index={index}
/>
);
case "spl-memo":
return (
<MemoDetailsCard
key={index}
ix={next}
result={result}
index={index}
/>
);
default:
const props = {
ix: next,
result,
(instruction, index) => {
let innerCards: JSX.Element[] = [];
if (index in innerInstructions) {
innerInstructions[index].forEach((ix, childIndex) => {
if (typeof ix.programId === "string") {
ix.programId = new PublicKey(ix.programId);
}
let res = renderInstructionCard({
index,
raw: raw?.instructions[index],
};
return <UnknownDetailsCard key={index} {...props} />;
}
ix,
result,
signature,
tx: transaction,
childIndex,
raw,
});
innerCards.push(res);
});
}
const ix = intoTransactionInstruction(transaction, index);
if (!ix) {
return (
<ErrorCard
key={index}
text="Could not display this instruction, please report"
/>
);
}
const props = { ix, result, index, signature };
if (isSerumInstruction(ix)) {
return <SerumDetailsCard key={index} {...props} />;
} else if (isTokenSwapInstruction(ix)) {
return <TokenSwapDetailsCard key={index} {...props} />;
} else {
return <UnknownDetailsCard key={index} {...props} />;
}
return renderInstructionCard({
index,
ix: instruction,
result,
signature,
tx: transaction,
innerCards,
raw,
});
}
);
@ -540,3 +523,80 @@ function ProgramLogSection({ signature }: SignatureProps) {
</>
);
}
function renderInstructionCard({
ix,
tx,
result,
index,
signature,
innerCards,
childIndex,
raw,
}: {
ix: ParsedInstruction | PartiallyDecodedInstruction;
tx: ParsedTransaction;
result: SignatureResult;
index: number;
signature: TransactionSignature;
innerCards?: JSX.Element[];
childIndex?: number;
raw?: Transaction;
}) {
const key = `${index}-${childIndex}`;
if ("parsed" in ix) {
const props = {
tx,
ix,
result,
index,
innerCards,
childIndex,
key,
};
switch (ix.program) {
case "spl-token":
return <TokenDetailsCard {...props} />;
case "bpf-loader":
return <BpfLoaderDetailsCard {...props} />;
case "system":
return <SystemDetailsCard {...props} />;
case "stake":
return <StakeDetailsCard {...props} />;
case "spl-memo":
return <MemoDetailsCard {...props} />;
default:
return <UnknownDetailsCard {...props} />;
}
}
const transactionIx = intoTransactionInstruction(tx, ix);
if (!transactionIx) {
return (
<ErrorCard
key={key}
text="Could not display this instruction, please report"
/>
);
}
const props = {
ix: transactionIx,
result,
index,
signature,
innerCards,
childIndex,
};
if (isSerumInstruction(transactionIx)) {
return <SerumDetailsCard key={key} {...props} />;
} else if (isTokenSwapInstruction(transactionIx)) {
return <TokenSwapDetailsCard key={key} {...props} />;
} else {
return <UnknownDetailsCard key={key} {...props} />;
}
}

View File

@ -193,7 +193,19 @@ h4.slot-pill {
height: 24px;
}
.address-truncate {
.forced-truncate {
.address-truncate {
max-width: 180px;
display: inline-block;
vertical-align: bottom;
@include media-breakpoint-down(sm) {
max-width: 120px;
}
}
}
.address-truncate, .signature-truncate {
@include media-breakpoint-down(md) {
max-width: 180px;
display: inline-block;
@ -205,6 +217,57 @@ h4.slot-pill {
}
}
p.tree,
ul.tree,
ul.tree ul {
list-style: none;
margin: 0;
padding: 0;
}
p.tree span.c-pointer {
color: $primary-dark;
}
ul.tree ul {
margin-left: 1.0em;
}
ul.tree li {
margin-left: 0.35em;
border-left: thin solid #808080;
}
ul.tree li:last-child {
border-left: none;
}
ul.tree li:before {
width: 1.4em;
height: 0.6em;
margin-right: 0.1em;
vertical-align: top;
border-bottom: thin solid #808080;
content: "";
display: inline-block;
}
ul.tree li:last-child:before {
border-left: thin solid #808080;
}
div.inner-cards {
margin: 1.5rem;
.card {
background-color: $gray-700-dark;
}
.table td {
border-top: 1px solid $gray-800-dark;
}
}
// work around for https://github.com/JedWatson/react-select/issues/3857
.search-bar__input {
width: 100% !important;

View File

@ -12,6 +12,8 @@ import {
ParsedTransaction,
TransactionInstruction,
Transaction,
PartiallyDecodedInstruction,
ParsedInstruction,
} from "@solana/web3.js";
import { TokenRegistry } from "tokenRegistry";
import { Cluster } from "providers/cluster";
@ -79,10 +81,9 @@ export function displayAddress(address: string, cluster: Cluster): string {
export function intoTransactionInstruction(
tx: ParsedTransaction,
index: number
instruction: ParsedInstruction | PartiallyDecodedInstruction
): TransactionInstruction | undefined {
const message = tx.message;
const instruction = message.instructions[index];
if ("parsed" in instruction) return;
const keys = [];