From 976a64c25c729a9c5851d85514a09726309ac862 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 24 Feb 2021 12:38:53 -0800 Subject: [PATCH] explorer: introduce vote instruction card (#15521) * refactor: move instruction section to components * feat: votes instruction card * refactor: move program log section into separate component --- .../instruction/vote/VoteDetailsCard.tsx | 82 ++++++ .../src/components/instruction/vote/types.ts | 17 ++ .../transaction/InstructionsSection.tsx | 225 ++++++++++++++++ .../transaction/ProgramLogSection.tsx | 31 +++ explorer/src/pages/TransactionDetailsPage.tsx | 244 +----------------- 5 files changed, 361 insertions(+), 238 deletions(-) create mode 100644 explorer/src/components/instruction/vote/VoteDetailsCard.tsx create mode 100644 explorer/src/components/instruction/vote/types.ts create mode 100644 explorer/src/components/transaction/InstructionsSection.tsx create mode 100644 explorer/src/components/transaction/ProgramLogSection.tsx diff --git a/explorer/src/components/instruction/vote/VoteDetailsCard.tsx b/explorer/src/components/instruction/vote/VoteDetailsCard.tsx new file mode 100644 index 0000000000..cb0f49551f --- /dev/null +++ b/explorer/src/components/instruction/vote/VoteDetailsCard.tsx @@ -0,0 +1,82 @@ +import React from "react"; +import { ParsedInstruction, SignatureResult } from "@solana/web3.js"; +import { coerce } from "superstruct"; +import { ParsedInfo } from "validators"; +import { VoteInfo } from "./types"; +import { InstructionCard } from "../InstructionCard"; +import { Address } from "components/common/Address"; +import { displayTimestampUtc } from "utils/date"; + +export function VoteDetailsCard(props: { + ix: ParsedInstruction; + index: number; + result: SignatureResult; + innerCards?: JSX.Element[]; + childIndex?: number; +}) { + const { ix, index, result, innerCards, childIndex } = props; + const parsed = coerce(props.ix.parsed, ParsedInfo); + const info = coerce(parsed.info, VoteInfo); + + return ( + + + Vote Account + +
+ + + + + Vote Authority + +
+ + + + + Clock Sysvar + +
+ + + + + Slot Hashes Sysvar + +
+ + + + + Vote Hash + +
{info.vote.hash}
+ + + + + Timestamp + + {displayTimestampUtc(info.vote.timestamp)} + + + + + Slots + +
+            {info.vote.slots.join("\n")}
+          
+ + + + ); +} diff --git a/explorer/src/components/instruction/vote/types.ts b/explorer/src/components/instruction/vote/types.ts new file mode 100644 index 0000000000..6f6054c02a --- /dev/null +++ b/explorer/src/components/instruction/vote/types.ts @@ -0,0 +1,17 @@ +/* eslint-disable @typescript-eslint/no-redeclare */ + +import { array, number, pick, string, StructType } from "superstruct"; +import { Pubkey } from "validators/pubkey"; + +export type VoteInfo = StructType; +export const VoteInfo = pick({ + clockSysvar: Pubkey, + slotHashesSysvar: Pubkey, + voteAccount: Pubkey, + voteAuthority: Pubkey, + vote: pick({ + hash: string(), + slots: array(number()), + timestamp: number(), + }), +}); diff --git a/explorer/src/components/transaction/InstructionsSection.tsx b/explorer/src/components/transaction/InstructionsSection.tsx new file mode 100644 index 0000000000..ed2331e52d --- /dev/null +++ b/explorer/src/components/transaction/InstructionsSection.tsx @@ -0,0 +1,225 @@ +import React from "react"; + +import { ErrorCard } from "components/common/ErrorCard"; +import { + ParsedInnerInstruction, + ParsedInstruction, + ParsedTransaction, + PartiallyDecodedInstruction, + PublicKey, + SignatureResult, + Transaction, + TransactionSignature, +} from "@solana/web3.js"; +import { BpfLoaderDetailsCard } from "components/instruction/bpf-loader/BpfLoaderDetailsCard"; +import { MemoDetailsCard } from "components/instruction/MemoDetailsCard"; +import { SerumDetailsCard } from "components/instruction/SerumDetailsCard"; +import { StakeDetailsCard } from "components/instruction/stake/StakeDetailsCard"; +import { SystemDetailsCard } from "components/instruction/system/SystemDetailsCard"; +import { TokenDetailsCard } from "components/instruction/token/TokenDetailsCard"; +import { TokenLendingDetailsCard } from "components/instruction/TokenLendingDetailsCard"; +import { TokenSwapDetailsCard } from "components/instruction/TokenSwapDetailsCard"; +import { UnknownDetailsCard } from "components/instruction/UnknownDetailsCard"; +import { + SignatureProps, + INNER_INSTRUCTIONS_START_SLOT, +} from "pages/TransactionDetailsPage"; +import { intoTransactionInstruction } from "utils/tx"; +import { isSerumInstruction } from "components/instruction/serum/types"; +import { isTokenLendingInstruction } from "components/instruction/token-lending/types"; +import { isTokenSwapInstruction } from "components/instruction/token-swap/types"; +import { useFetchTransactionDetails } from "providers/transactions/details"; +import { + useTransactionDetails, + useTransactionStatus, +} from "providers/transactions"; +import { Cluster, useCluster } from "providers/cluster"; +import { VoteDetailsCard } from "components/instruction/vote/VoteDetailsCard"; + +export function InstructionsSection({ signature }: SignatureProps) { + const status = useTransactionStatus(signature); + const details = useTransactionDetails(signature); + const { cluster } = useCluster(); + const fetchDetails = useFetchTransactionDetails(); + const refreshDetails = () => fetchDetails(signature); + + if (!status?.data?.info || !details?.data?.transaction) return null; + + 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( + (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); + }); + } + + return renderInstructionCard({ + index, + ix: instruction, + result, + signature, + tx: transaction, + innerCards, + raw, + }); + } + ); + + return ( + <> +
+
+
+

Instruction(s)

+
+
+
+ {instructionDetails} + + ); +} + +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 ; + case "vote": + console.log(props); + return ; + default: + return ; + } + } + + // TODO: There is a bug in web3, where inner instructions + // aren't getting coerced. This is a temporary fix. + + if (typeof ix.programId === "string") { + ix.programId = new PublicKey(ix.programId); + } + + ix.accounts = ix.accounts.map((account) => { + if (typeof account === "string") { + return new PublicKey(account); + } + + return account; + }); + + // TODO: End hotfix + + 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 if (isTokenLendingInstruction(transactionIx)) { + return ; + } else { + return ; + } +} diff --git a/explorer/src/components/transaction/ProgramLogSection.tsx b/explorer/src/components/transaction/ProgramLogSection.tsx new file mode 100644 index 0000000000..eec1e81b4f --- /dev/null +++ b/explorer/src/components/transaction/ProgramLogSection.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { SignatureProps } from "pages/TransactionDetailsPage"; +import { useTransactionDetails } from "providers/transactions"; + +export function ProgramLogSection({ signature }: SignatureProps) { + const details = useTransactionDetails(signature); + const logMessages = details?.data?.transaction?.meta?.logMessages; + + if (!logMessages || logMessages.length < 1) { + return null; + } + + return ( + <> +
+
+
+

Program Log

+
+
+
+
+
    + {logMessages.map((message, key) => ( +
  • {message.replace(/^Program log: /, "")}
  • + ))} +
+
+ + ); +} diff --git a/explorer/src/pages/TransactionDetailsPage.tsx b/explorer/src/pages/TransactionDetailsPage.tsx index 60f885793f..be704e6832 100644 --- a/explorer/src/pages/TransactionDetailsPage.tsx +++ b/explorer/src/pages/TransactionDetailsPage.tsx @@ -6,24 +6,13 @@ import { useTransactionDetails, } from "providers/transactions"; import { useFetchTransactionDetails } from "providers/transactions/details"; -import { useCluster, ClusterStatus, Cluster } from "providers/cluster"; +import { useCluster, ClusterStatus } 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"; -import { StakeDetailsCard } from "components/instruction/stake/StakeDetailsCard"; -import { BpfLoaderDetailsCard } from "components/instruction/bpf-loader/BpfLoaderDetailsCard"; import { ErrorCard } from "components/common/ErrorCard"; import { LoadingCard } from "components/common/LoadingCard"; import { TableCardBody } from "components/common/TableCardBody"; @@ -32,19 +21,13 @@ import { InfoTooltip } from "components/common/InfoTooltip"; import { Address } from "components/common/Address"; import { Signature } from "components/common/Signature"; import { intoTransactionInstruction } from "utils/tx"; -import { TokenDetailsCard } from "components/instruction/token/TokenDetailsCard"; import { FetchStatus } from "providers/cache"; -import { SerumDetailsCard } from "components/instruction/SerumDetailsCard"; import { Slot } from "components/common/Slot"; -import { isTokenSwapInstruction } from "components/instruction/token-swap/types"; -import { isTokenLendingInstruction } from "components/instruction/token-lending/types"; -import { TokenSwapDetailsCard } from "components/instruction/TokenSwapDetailsCard"; -import { TokenLendingDetailsCard } from "components/instruction/TokenLendingDetailsCard"; -import { isSerumInstruction } from "components/instruction/serum/types"; -import { MemoDetailsCard } from "components/instruction/MemoDetailsCard"; import { BigNumber } from "bignumber.js"; import { BalanceDelta } from "components/common/BalanceDelta"; import { TokenBalancesCard } from "components/transaction/TokenBalancesCard"; +import { InstructionsSection } from "components/transaction/InstructionsSection"; +import { ProgramLogSection } from "components/transaction/ProgramLogSection"; const AUTO_REFRESH_INTERVAL = 2000; const ZERO_CONFIRMATION_BAILOUT = 5; @@ -119,15 +102,13 @@ export function TransactionDetailsPage({ signature: raw }: SignatureProps) { {signature === undefined ? ( ) : ( - <> + - - - + - + )} ); @@ -415,216 +396,3 @@ function AccountsCard({ ); } - -function InstructionsSection({ signature }: SignatureProps) { - const status = useTransactionStatus(signature); - const details = useTransactionDetails(signature); - const { cluster } = useCluster(); - const fetchDetails = useFetchTransactionDetails(); - const refreshDetails = () => fetchDetails(signature); - - if (!status?.data?.info || !details?.data?.transaction) return null; - - 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( - (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); - }); - } - - return renderInstructionCard({ - index, - ix: instruction, - result, - signature, - tx: transaction, - innerCards, - raw, - }); - } - ); - - return ( - <> -
-
-
-

Instruction(s)

-
-
-
- {instructionDetails} - - ); -} - -function ProgramLogSection({ signature }: SignatureProps) { - const details = useTransactionDetails(signature); - const logMessages = details?.data?.transaction?.meta?.logMessages; - - if (!logMessages || logMessages.length < 1) { - return null; - } - - return ( - <> -
-
-
-

Program Log

-
-
-
-
-
    - {logMessages.map((message, key) => ( -
  • {message.replace(/^Program log: /, "")}
  • - ))} -
-
- - ); -} - -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 ; - } - } - - // TODO: There is a bug in web3, where inner instructions - // aren't getting coerced. This is a temporary fix. - - if (typeof ix.programId === "string") { - ix.programId = new PublicKey(ix.programId); - } - - ix.accounts = ix.accounts.map((account) => { - if (typeof account === "string") { - return new PublicKey(account); - } - - return account; - }); - - // TODO: End hotfix - - 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 if (isTokenLendingInstruction(transactionIx)) { - return ; - } else { - return ; - } -}