diff --git a/explorer/src/components/account/TokenHistoryCard.tsx b/explorer/src/components/account/TokenHistoryCard.tsx index f981595d91..7c3e937643 100644 --- a/explorer/src/components/account/TokenHistoryCard.tsx +++ b/explorer/src/components/account/TokenHistoryCard.tsx @@ -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
| undefined; }) => { const fetchDetails = useFetchTransactionDetails(); + const { cluster } = useCluster(); // Fetch details on load React.useEffect(() => { @@ -309,58 +318,91 @@ const TokenTransactionRow = React.memo( ); - const tokenInstructionNames = instructions - .map((ix, index): string | undefined => { - let transactionInstruction; - if (details?.data?.transaction?.transaction) { - transactionInstruction = intoTransactionInstruction( - details.data.transaction.transaction, - index - ); - } + 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 ("parsed" in ix) { - if (ix.program === "spl-token") { - return instructionTypeName(ix, tx); - } else { - return undefined; - } - } else if ( - transactionInstruction && - isSerumInstruction(transactionInstruction) - ) { - try { - return parseSerumInstructionTitle(transactionInstruction); - } catch (error) { - reportError(error, { signature: tx.signature }); - return undefined; - } - } else if ( - transactionInstruction && - isTokenSwapInstruction(transactionInstruction) - ) { - try { - return parseTokenSwapInstructionTitle(transactionInstruction); - } catch (error) { - reportError(error, { signature: tx.signature }); - return undefined; - } - } else { if ( - ix.accounts.findIndex((account) => - account.equals(TOKEN_PROGRAM_ID) - ) >= 0 + transaction.meta?.innerInstructions && + (cluster !== Cluster.MainnetBeta || + transaction.slot >= INNER_INSTRUCTIONS_START_SLOT) ) { - return "Unknown (Inner)"; + transaction.meta.innerInstructions.forEach((ix) => { + if (ix.index === index) { + ix.instructions.forEach((inner) => { + innerInstructions.push(inner); + }); + } + }); } - return undefined; - } - }) - .filter((name) => name !== undefined) as string[]; + + let transactionInstruction; + if (transaction?.transaction) { + transactionInstruction = intoTransactionInstruction( + transaction.transaction, + ix + ); + } + + if ("parsed" in ix) { + if (ix.program === "spl-token") { + name = instructionTypeName(ix, tx); + } else { + return undefined; + } + } else if ( + transactionInstruction && + isSerumInstruction(transactionInstruction) + ) { + try { + name = parseSerumInstructionTitle(transactionInstruction); + } catch (error) { + reportError(error, { signature: tx.signature }); + return undefined; + } + } else if ( + transactionInstruction && + isTokenSwapInstruction(transactionInstruction) + ) { + try { + name = parseTokenSwapInstructionTitle(transactionInstruction); + } catch (error) { + reportError(error, { signature: tx.signature }); + return undefined; + } + } else { + if ( + ix.accounts.findIndex((account) => + account.equals(TOKEN_PROGRAM_ID) + ) >= 0 + ) { + name = "Unknown (Inner)"; + } else { + return undefined; + } + } + + return { + name: name, + innerInstructions: innerInstructions, + }; + }) + .filter((name) => name !== undefined) as InstructionType[]; + } return ( <> - {tokenInstructionNames.map((typeName, index) => { + {tokenInstructionNames.map((instructionType, index) => { return ( @@ -373,14 +415,16 @@ const TokenTransactionRow = React.memo( - -
+ +
- {typeName} - - + + + + + ); @@ -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 ( + <> +

+ {instructionTypes.length > 0 && ( + { + e.preventDefault(); + setExpanded(!expanded); + }} + className={`c-pointer fe mr-2 ${ + expanded ? "fe-minus-square" : "fe-plus-square" + }`} + > + )} + {instructionType.name} +

+ {expanded && ( +
    + {instructionTypes.map((type, index) => { + return
  • {type}
  • ; + })} +
+ )} + + ); +} diff --git a/explorer/src/components/common/Address.tsx b/explorer/src/components/common/Address.tsx index 60c2d16bdf..d5cd799762 100644 --- a/explorer/src/components/common/Address.tsx +++ b/explorer/src/components/common/Address.tsx @@ -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("copy"); const address = pubkey.toBase58(); const { cluster } = useCluster(); @@ -33,6 +41,10 @@ export function Address({ pubkey, alignRight, link, raw, truncate }: Props) { ); + if (truncateUnknown && address === displayAddress(address, cluster)) { + truncate = true; + } + const content = ( <> {copyIcon} diff --git a/explorer/src/components/common/Signature.tsx b/explorer/src/components/common/Signature.tsx index 8578f25282..1e8dbe7bbb 100644 --- a/explorer/src/components/common/Signature.tsx +++ b/explorer/src/components/common/Signature.tsx @@ -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("copy"); const copyToClipboard = () => navigator.clipboard.writeText(signature); @@ -40,7 +41,10 @@ export function Signature({ signature, alignRight, link }: Props) { {copyButton} {link ? ( - + {signature} ) : ( diff --git a/explorer/src/components/instruction/InstructionCard.tsx b/explorer/src/components/instruction/InstructionCard.tsx index 6ccdb4ff1c..9bb078fc4b 100644 --- a/explorer/src/components/instruction/InstructionCard.tsx +++ b/explorer/src/components/instruction/InstructionCard.tsx @@ -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({

#{index + 1} + {childIndex !== undefined ? `.${childIndex + 1}` : ""} {title}

@@ -92,6 +97,14 @@ export function InstructionCard({ ) : ( children )} + {innerCards && innerCards.length > 0 && ( + + + Inner Instructions +
{innerCards}
+ + + )} diff --git a/explorer/src/components/instruction/MemoDetailsCard.tsx b/explorer/src/components/instruction/MemoDetailsCard.tsx index 54457a08f0..b44bf2d86b 100644 --- a/explorer/src/components/instruction/MemoDetailsCard.tsx +++ b/explorer/src/components/instruction/MemoDetailsCard.tsx @@ -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 ( - + Data (UTF-8) diff --git a/explorer/src/components/instruction/SerumDetailsCard.tsx b/explorer/src/components/instruction/SerumDetailsCard.tsx index cb5e2706b0..bc112c8bb2 100644 --- a/explorer/src/components/instruction/SerumDetailsCard.tsx +++ b/explorer/src/components/instruction/SerumDetailsCard.tsx @@ -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 /> ); diff --git a/explorer/src/components/instruction/TokenSwapDetailsCard.tsx b/explorer/src/components/instruction/TokenSwapDetailsCard.tsx index b4f7d85c8f..c25ef97c19 100644 --- a/explorer/src/components/instruction/TokenSwapDetailsCard.tsx +++ b/explorer/src/components/instruction/TokenSwapDetailsCard.tsx @@ -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 /> ); diff --git a/explorer/src/components/instruction/UnknownDetailsCard.tsx b/explorer/src/components/instruction/UnknownDetailsCard.tsx index 8117cbe6b4..1e37f47dec 100644 --- a/explorer/src/components/instruction/UnknownDetailsCard.tsx +++ b/explorer/src/components/instruction/UnknownDetailsCard.tsx @@ -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 ( ); diff --git a/explorer/src/components/instruction/bpf-loader/BpfLoaderDetailsCard.tsx b/explorer/src/components/instruction/bpf-loader/BpfLoaderDetailsCard.tsx index 47a19507b6..49a57bcd34 100644 --- a/explorer/src/components/instruction/bpf-loader/BpfLoaderDetailsCard.tsx +++ b/explorer/src/components/instruction/bpf-loader/BpfLoaderDetailsCard.tsx @@ -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 = { index: number; result: SignatureResult; info: T; + innerCards?: JSX.Element[]; + childIndex?: number; }; export function BpfLoaderWriteDetailsCard(props: Props) { - const { ix, index, result, info } = props; + const { ix, index, result, info, innerCards, childIndex } = props; const bytes = wrap(info.bytes, 50); return ( ) { index={index} result={result} title="BPF Loader 2: Write" + innerCards={innerCards} + childIndex={childIndex} > Program @@ -94,7 +100,7 @@ export function BpfLoaderWriteDetailsCard(props: Props) { } export function BpfLoaderFinalizeDetailsCard(props: Props) { - const { ix, index, result, info } = props; + const { ix, index, result, info, innerCards, childIndex } = props; return ( ) { index={index} result={result} title="BPF Loader 2: Finalize" + innerCards={innerCards} + childIndex={childIndex} > Program diff --git a/explorer/src/components/instruction/serum/CancelOrderByClientIdDetails.tsx b/explorer/src/components/instruction/serum/CancelOrderByClientIdDetails.tsx index de4d9f97f6..1bdce23f2c 100644 --- a/explorer/src/components/instruction/serum/CancelOrderByClientIdDetails.tsx +++ b/explorer/src/components/instruction/serum/CancelOrderByClientIdDetails.tsx @@ -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 ( Market diff --git a/explorer/src/components/instruction/serum/CancelOrderDetails.tsx b/explorer/src/components/instruction/serum/CancelOrderDetails.tsx index 33ac8372d7..64dcaf10d9 100644 --- a/explorer/src/components/instruction/serum/CancelOrderDetails.tsx +++ b/explorer/src/components/instruction/serum/CancelOrderDetails.tsx @@ -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 ( Program diff --git a/explorer/src/components/instruction/serum/ConsumeEventsDetails.tsx b/explorer/src/components/instruction/serum/ConsumeEventsDetails.tsx index c7831579e4..4a278b0b80 100644 --- a/explorer/src/components/instruction/serum/ConsumeEventsDetails.tsx +++ b/explorer/src/components/instruction/serum/ConsumeEventsDetails.tsx @@ -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 ( Program diff --git a/explorer/src/components/instruction/serum/InitializeMarketDetailsCard.tsx b/explorer/src/components/instruction/serum/InitializeMarketDetailsCard.tsx index b76d9b178c..b4949e34c2 100644 --- a/explorer/src/components/instruction/serum/InitializeMarketDetailsCard.tsx +++ b/explorer/src/components/instruction/serum/InitializeMarketDetailsCard.tsx @@ -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 ( Program diff --git a/explorer/src/components/instruction/serum/MatchOrdersDetailsCard.tsx b/explorer/src/components/instruction/serum/MatchOrdersDetailsCard.tsx index 1c83c91676..5457ccb1d9 100644 --- a/explorer/src/components/instruction/serum/MatchOrdersDetailsCard.tsx +++ b/explorer/src/components/instruction/serum/MatchOrdersDetailsCard.tsx @@ -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 ( Program diff --git a/explorer/src/components/instruction/serum/NewOrderDetailsCard.tsx b/explorer/src/components/instruction/serum/NewOrderDetailsCard.tsx index aa005aaec1..99bbe58a71 100644 --- a/explorer/src/components/instruction/serum/NewOrderDetailsCard.tsx +++ b/explorer/src/components/instruction/serum/NewOrderDetailsCard.tsx @@ -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 ( Program diff --git a/explorer/src/components/instruction/serum/SettleFundsDetailsCard.tsx b/explorer/src/components/instruction/serum/SettleFundsDetailsCard.tsx index a2c685425e..3cc3aea091 100644 --- a/explorer/src/components/instruction/serum/SettleFundsDetailsCard.tsx +++ b/explorer/src/components/instruction/serum/SettleFundsDetailsCard.tsx @@ -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 ( Program diff --git a/explorer/src/components/instruction/stake/AuthorizeDetailsCard.tsx b/explorer/src/components/instruction/stake/AuthorizeDetailsCard.tsx index baa99694c1..b390bb7852 100644 --- a/explorer/src/components/instruction/stake/AuthorizeDetailsCard.tsx +++ b/explorer/src/components/instruction/stake/AuthorizeDetailsCard.tsx @@ -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 ( Program diff --git a/explorer/src/components/instruction/stake/DeactivateDetailsCard.tsx b/explorer/src/components/instruction/stake/DeactivateDetailsCard.tsx index d53495e9fc..7577d4c03b 100644 --- a/explorer/src/components/instruction/stake/DeactivateDetailsCard.tsx +++ b/explorer/src/components/instruction/stake/DeactivateDetailsCard.tsx @@ -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 ( Program diff --git a/explorer/src/components/instruction/stake/DelegateDetailsCard.tsx b/explorer/src/components/instruction/stake/DelegateDetailsCard.tsx index d1a3043f13..502ea8d77d 100644 --- a/explorer/src/components/instruction/stake/DelegateDetailsCard.tsx +++ b/explorer/src/components/instruction/stake/DelegateDetailsCard.tsx @@ -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 ( Program diff --git a/explorer/src/components/instruction/stake/InitializeDetailsCard.tsx b/explorer/src/components/instruction/stake/InitializeDetailsCard.tsx index 695a77bb18..9c2f5a29e7 100644 --- a/explorer/src/components/instruction/stake/InitializeDetailsCard.tsx +++ b/explorer/src/components/instruction/stake/InitializeDetailsCard.tsx @@ -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 ( Program diff --git a/explorer/src/components/instruction/stake/SplitDetailsCard.tsx b/explorer/src/components/instruction/stake/SplitDetailsCard.tsx index f70db3dc63..e09e378249 100644 --- a/explorer/src/components/instruction/stake/SplitDetailsCard.tsx +++ b/explorer/src/components/instruction/stake/SplitDetailsCard.tsx @@ -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 ( - + Program diff --git a/explorer/src/components/instruction/stake/StakeDetailsCard.tsx b/explorer/src/components/instruction/stake/StakeDetailsCard.tsx index e5eeddd777..ec8e938aa8 100644 --- a/explorer/src/components/instruction/stake/StakeDetailsCard.tsx +++ b/explorer/src/components/instruction/stake/StakeDetailsCard.tsx @@ -29,6 +29,8 @@ type DetailsProps = { ix: ParsedInstruction; result: SignatureResult; index: number; + innerCards?: JSX.Element[]; + childIndex?: number; }; export function StakeDetailsCard(props: DetailsProps) { diff --git a/explorer/src/components/instruction/stake/WithdrawDetailsCard.tsx b/explorer/src/components/instruction/stake/WithdrawDetailsCard.tsx index 7d06054f0a..302b7ae7d0 100644 --- a/explorer/src/components/instruction/stake/WithdrawDetailsCard.tsx +++ b/explorer/src/components/instruction/stake/WithdrawDetailsCard.tsx @@ -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 ( Program diff --git a/explorer/src/components/instruction/system/AllocateDetailsCard.tsx b/explorer/src/components/instruction/system/AllocateDetailsCard.tsx index d7df2d8f1e..b40a636f48 100644 --- a/explorer/src/components/instruction/system/AllocateDetailsCard.tsx +++ b/explorer/src/components/instruction/system/AllocateDetailsCard.tsx @@ -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 ( Program diff --git a/explorer/src/components/instruction/system/AllocateWithSeedDetailsCard.tsx b/explorer/src/components/instruction/system/AllocateWithSeedDetailsCard.tsx index 69ac69e25f..cd7a114c44 100644 --- a/explorer/src/components/instruction/system/AllocateWithSeedDetailsCard.tsx +++ b/explorer/src/components/instruction/system/AllocateWithSeedDetailsCard.tsx @@ -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 ( Program diff --git a/explorer/src/components/instruction/system/AssignDetailsCard.tsx b/explorer/src/components/instruction/system/AssignDetailsCard.tsx index 6fb5d4f761..1353892eb1 100644 --- a/explorer/src/components/instruction/system/AssignDetailsCard.tsx +++ b/explorer/src/components/instruction/system/AssignDetailsCard.tsx @@ -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 ( Program diff --git a/explorer/src/components/instruction/system/AssignWithSeedDetailsCard.tsx b/explorer/src/components/instruction/system/AssignWithSeedDetailsCard.tsx index 6356dd748a..bf04d30475 100644 --- a/explorer/src/components/instruction/system/AssignWithSeedDetailsCard.tsx +++ b/explorer/src/components/instruction/system/AssignWithSeedDetailsCard.tsx @@ -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 ( Program diff --git a/explorer/src/components/instruction/system/CreateDetailsCard.tsx b/explorer/src/components/instruction/system/CreateDetailsCard.tsx index 7cfd9333be..c9f9a2d2d6 100644 --- a/explorer/src/components/instruction/system/CreateDetailsCard.tsx +++ b/explorer/src/components/instruction/system/CreateDetailsCard.tsx @@ -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 ( Program diff --git a/explorer/src/components/instruction/system/CreateWithSeedDetailsCard.tsx b/explorer/src/components/instruction/system/CreateWithSeedDetailsCard.tsx index c5d4588b77..84302f7773 100644 --- a/explorer/src/components/instruction/system/CreateWithSeedDetailsCard.tsx +++ b/explorer/src/components/instruction/system/CreateWithSeedDetailsCard.tsx @@ -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 ( Program diff --git a/explorer/src/components/instruction/system/NonceAdvanceDetailsCard.tsx b/explorer/src/components/instruction/system/NonceAdvanceDetailsCard.tsx index 1e94951261..833fec39a2 100644 --- a/explorer/src/components/instruction/system/NonceAdvanceDetailsCard.tsx +++ b/explorer/src/components/instruction/system/NonceAdvanceDetailsCard.tsx @@ -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 ( Program diff --git a/explorer/src/components/instruction/system/NonceAuthorizeDetailsCard.tsx b/explorer/src/components/instruction/system/NonceAuthorizeDetailsCard.tsx index fecb52009c..39b83b5ca0 100644 --- a/explorer/src/components/instruction/system/NonceAuthorizeDetailsCard.tsx +++ b/explorer/src/components/instruction/system/NonceAuthorizeDetailsCard.tsx @@ -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 ( Program diff --git a/explorer/src/components/instruction/system/NonceInitializeDetailsCard.tsx b/explorer/src/components/instruction/system/NonceInitializeDetailsCard.tsx index c5d2345a62..c421b604d5 100644 --- a/explorer/src/components/instruction/system/NonceInitializeDetailsCard.tsx +++ b/explorer/src/components/instruction/system/NonceInitializeDetailsCard.tsx @@ -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 ( Program diff --git a/explorer/src/components/instruction/system/NonceWithdrawDetailsCard.tsx b/explorer/src/components/instruction/system/NonceWithdrawDetailsCard.tsx index 176ca8b66e..98c50ffb69 100644 --- a/explorer/src/components/instruction/system/NonceWithdrawDetailsCard.tsx +++ b/explorer/src/components/instruction/system/NonceWithdrawDetailsCard.tsx @@ -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 ( Program diff --git a/explorer/src/components/instruction/system/SystemDetailsCard.tsx b/explorer/src/components/instruction/system/SystemDetailsCard.tsx index c39b7d1351..267f8a5ecc 100644 --- a/explorer/src/components/instruction/system/SystemDetailsCard.tsx +++ b/explorer/src/components/instruction/system/SystemDetailsCard.tsx @@ -39,6 +39,8 @@ type DetailsProps = { ix: ParsedInstruction; result: SignatureResult; index: number; + innerCards?: JSX.Element[]; + childIndex?: number; }; export function SystemDetailsCard(props: DetailsProps) { diff --git a/explorer/src/components/instruction/system/TransferDetailsCard.tsx b/explorer/src/components/instruction/system/TransferDetailsCard.tsx index 3b6da29cb8..f05a7e7d4f 100644 --- a/explorer/src/components/instruction/system/TransferDetailsCard.tsx +++ b/explorer/src/components/instruction/system/TransferDetailsCard.tsx @@ -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 ( - + Program diff --git a/explorer/src/components/instruction/token-swap/types.ts b/explorer/src/components/instruction/token-swap/types.ts index 9bb9e1ae62..d1638359af 100644 --- a/explorer/src/components/instruction/token-swap/types.ts +++ b/explorer/src/components/instruction/token-swap/types.ts @@ -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", }; diff --git a/explorer/src/components/instruction/token/TokenDetailsCard.tsx b/explorer/src/components/instruction/token/TokenDetailsCard.tsx index 61b28f63ca..503446933c 100644 --- a/explorer/src/components/instruction/token/TokenDetailsCard.tsx +++ b/explorer/src/components/instruction/token/TokenDetailsCard.tsx @@ -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} diff --git a/explorer/src/pages/TransactionDetailsPage.tsx b/explorer/src/pages/TransactionDetailsPage.tsx index d236df9448..78e7f7e90c 100644 --- a/explorer/src/pages/TransactionDetailsPage.tsx +++ b/explorer/src/pages/TransactionDetailsPage.tsx @@ -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 ; } + 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 ( - - ); - case "bpf-loader": - return ( - - ); - case "system": - return ( - - ); - case "stake": - return ( - - ); - case "spl-memo": - return ( - - ); - default: - const props = { - ix: next, - result, - index, - raw: raw?.instructions[index], - }; - return ; - } + (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, + ix, + result, + signature, + tx: transaction, + childIndex, + raw, + }); + + innerCards.push(res); + }); } - const ix = intoTransactionInstruction(transaction, index); - - if (!ix) { - return ( - - ); - } - - const props = { ix, result, index, signature }; - - if (isSerumInstruction(ix)) { - return ; - } else if (isTokenSwapInstruction(ix)) { - return ; - } else { - return ; - } + 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 ; + case "bpf-loader": + return ; + case "system": + return ; + case "stake": + return ; + case "spl-memo": + return ; + default: + return ; + } + } + + const transactionIx = intoTransactionInstruction(tx, ix); + + if (!transactionIx) { + return ( + + ); + } + + const props = { + ix: transactionIx, + result, + index, + signature, + innerCards, + childIndex, + }; + + if (isSerumInstruction(transactionIx)) { + return ; + } else if (isTokenSwapInstruction(transactionIx)) { + return ; + } else { + return ; + } +} diff --git a/explorer/src/scss/_solana.scss b/explorer/src/scss/_solana.scss index a81fd15afe..9183168f16 100644 --- a/explorer/src/scss/_solana.scss +++ b/explorer/src/scss/_solana.scss @@ -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; diff --git a/explorer/src/utils/tx.ts b/explorer/src/utils/tx.ts index 8c38bdf36f..30f2adcafd 100644 --- a/explorer/src/utils/tx.ts +++ b/explorer/src/utils/tx.ts @@ -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 = [];