From e6aa3a4e0731faacef7957a7bc212e0f8bf3843e Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Fri, 7 Aug 2020 18:09:17 +0800 Subject: [PATCH] Parse token instructions in the explorer (#11443) --- explorer/package-lock.json | 29 +- explorer/package.json | 4 +- explorer/src/components/AccountDetails.tsx | 5 +- .../src/components/TransactionDetails.tsx | 83 +++-- .../instruction/InstructionCard.tsx | 19 +- .../instruction/RawParsedDetails.tsx | 25 ++ .../instruction/UnknownDetailsCard.tsx | 8 +- .../instruction/token/TokenDetailsCard.tsx | 88 +++++ .../src/components/instruction/token/types.ts | 129 +++++++ explorer/src/providers/accounts/tokens.tsx | 39 +- explorer/src/providers/stats/solanaBeach.tsx | 30 +- explorer/src/providers/transactions/cached.ts | 343 +++++++++--------- .../src/providers/transactions/details.tsx | 8 +- explorer/src/scss/_solana-dark-overrides.scss | 2 +- explorer/src/scss/_solana.scss | 2 +- explorer/src/utils/tx.ts | 54 +++ 16 files changed, 619 insertions(+), 249 deletions(-) create mode 100644 explorer/src/components/instruction/RawParsedDetails.tsx create mode 100644 explorer/src/components/instruction/token/TokenDetailsCard.tsx create mode 100644 explorer/src/components/instruction/token/types.ts diff --git a/explorer/package-lock.json b/explorer/package-lock.json index edc124143e..3d6d20d830 100644 --- a/explorer/package-lock.json +++ b/explorer/package-lock.json @@ -2334,9 +2334,9 @@ "integrity": "sha512-zLtOIToct1EBTbwldkMJsXC2eCsmWOOP7z6UG0M/sCgnPExtIjvVMCpPESvPnMbQzDZytXVy0nvMbUuK2gZs2A==" }, "@solana/web3.js": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.64.0.tgz", - "integrity": "sha512-DlNzAXgNdk7k4Pt6CfcaAutaiXJiog9hxswtzItf0q/0/Um8JvDI1YjnMONE3IKI/jyjmTaxhsQHWAQE42KofQ==", + "version": "0.66.0", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.66.0.tgz", + "integrity": "sha512-Uw7ooRWLqrq8I5U21mEryvvF/Eqqh4mq4K2W9Sxuz3boxkz7Ed7aAJVj5C5n1fbQr9I1cxxxgC+D5BHnogfS1A==", "requires": { "@babel/runtime": "^7.3.1", "bn.js": "^5.0.0", @@ -3032,9 +3032,9 @@ "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==" }, "@types/lodash": { - "version": "4.14.158", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.158.tgz", - "integrity": "sha512-InCEXJNTv/59yO4VSfuvNrZHt7eeNtWQEgnieIA+mIC+MOWM9arOWG2eQ8Vhk6NbOre6/BidiXhkZYeDY9U35w==" + "version": "4.14.159", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.159.tgz", + "integrity": "sha512-gF7A72f7WQN33DpqOWw9geApQPh4M3PxluMtaHxWHXEGSN12/WbcEk/eNSqWNQcQhF66VSZ06vCF94CrHwXJDg==" }, "@types/minimatch": { "version": "3.0.3", @@ -9055,9 +9055,9 @@ }, "dependencies": { "@types/node": { - "version": "12.12.53", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.53.tgz", - "integrity": "sha512-51MYTDTyCziHb70wtGNFRwB4l+5JNvdqzFSkbDvpbftEgVUBEE+T5f7pROhWMp/fxp07oNIEQZd5bbfAH22ohQ==" + "version": "12.12.54", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.54.tgz", + "integrity": "sha512-ge4xZ3vSBornVYlDnk7yZ0gK6ChHf/CHB7Gl1I0Jhah8DDnEQqBzgohYG4FX4p81TNirSETOiSyn+y1r9/IR6w==" } } }, @@ -14176,9 +14176,9 @@ } }, "rpc-websockets": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-5.2.4.tgz", - "integrity": "sha512-6jqeJK/18hPTsmeiN+K9O4miZiAmrIncgxfPXHwuGWs9BClA2zC3fOnTThRWo4blkrjH59oKKi0KMxSK+wdtNw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-5.3.1.tgz", + "integrity": "sha512-rIxEl1BbXRlIA9ON7EmY/2GUM7RLMy8zrUPTiLPFiYnYOz0I3PXfCmDDrge5vt4pW4oIcAXBDvgZuJ1jlY5+VA==", "requires": { "@babel/runtime": "^7.8.7", "assert-args": "^1.2.1", @@ -15386,9 +15386,8 @@ } }, "superstruct": { - "version": "0.10.12", - "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.10.12.tgz", - "integrity": "sha512-FiNhfegyytDI0QxrrEoeGknFM28SnoHqCBpkWewUm8jRNj74NVxLpiiePvkOo41Ze/aKMSHa/twWjNF81mKaQQ==" + "version": "github:solana-labs/superstruct#097ee6e2553ea609331bb3b2965a3b778d42015f", + "from": "github:solana-labs/superstruct" }, "supports-color": { "version": "5.5.0", diff --git a/explorer/package.json b/explorer/package.json index 10dc5955d9..6548e48c8b 100644 --- a/explorer/package.json +++ b/explorer/package.json @@ -4,7 +4,7 @@ "private": true, "dependencies": { "@react-hook/debounce": "^3.0.0", - "@solana/web3.js": "^0.64.0", + "@solana/web3.js": "^0.66.0", "@testing-library/jest-dom": "^5.11.2", "@testing-library/react": "^10.4.8", "@testing-library/user-event": "^12.1.0", @@ -30,7 +30,7 @@ "react-select": "^3.1.0", "socket.io-client": "^2.3.0", "solana-sdk-wasm": "file:wasm/pkg", - "superstruct": "^0.10.12", + "superstruct": "github:solana-labs/superstruct", "typescript": "^3.9.7", "wasm-loader": "^1.3.0" }, diff --git a/explorer/src/components/AccountDetails.tsx b/explorer/src/components/AccountDetails.tsx index 28776016d3..cb88583670 100644 --- a/explorer/src/components/AccountDetails.tsx +++ b/explorer/src/components/AccountDetails.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { PublicKey, StakeProgram, TokenAccountInfo } from "@solana/web3.js"; +import { PublicKey, StakeProgram } from "@solana/web3.js"; import { FetchStatus, useFetchAccountInfo, @@ -16,6 +16,7 @@ import { useFetchAccountHistory } from "providers/accounts/history"; import { useFetchAccountOwnedTokens, useAccountOwnedTokens, + TokenAccountData, } from "providers/accounts/tokens"; import { useCluster, ClusterStatus } from "providers/cluster"; import Address from "./common/Address"; @@ -158,7 +159,7 @@ function TokensCard({ pubkey }: { pubkey: PublicKey }) { ); } - const mappedTokens = new Map(); + const mappedTokens = new Map(); for (const token of tokens) { const mintAddress = token.mint.toBase58(); const tokenInfo = mappedTokens.get(mintAddress); diff --git a/explorer/src/components/TransactionDetails.tsx b/explorer/src/components/TransactionDetails.tsx index 4a8c50f43e..68cedf69a4 100644 --- a/explorer/src/components/TransactionDetails.tsx +++ b/explorer/src/components/TransactionDetails.tsx @@ -25,6 +25,8 @@ import InfoTooltip from "components/InfoTooltip"; import { isCached } from "providers/transactions/cached"; import Address from "./common/Address"; import Signature from "./common/Signature"; +import { intoTransactionInstruction } from "utils/tx"; +import { TokenDetailsCard } from "./instruction/token/TokenDetailsCard"; type Props = { signature: TransactionSignature }; export default function TransactionDetails({ signature }: Props) { @@ -91,12 +93,17 @@ function StatusCard({ signature }: Props) { }; const fee = details?.transaction?.meta?.fee; - const blockhash = details?.transaction?.transaction.recentBlockhash; - const ix = details?.transaction?.transaction.instructions[0]; - const isNonce = - ix && - SystemProgram.programId.equals(ix.programId) && - SystemInstruction.decodeInstructionType(ix) === "AdvanceNonceAccount"; + const transaction = details?.transaction?.transaction; + const blockhash = transaction?.message.recentBlockhash; + const isNonce = (() => { + if (!transaction) return false; + const ix = intoTransactionInstruction(transaction, 0); + return ( + ix && + SystemProgram.programId.equals(ix.programId) && + SystemInstruction.decodeInstructionType(ix) === "AdvanceNonceAccount" + ); + })(); return (
@@ -188,10 +195,7 @@ function AccountsCard({ signature }: Props) { const refreshStatus = () => fetchStatus(signature); const refreshDetails = () => fetchDetails(signature); const transaction = details?.transaction?.transaction; - const message = React.useMemo(() => { - return transaction?.compileMessage(); - }, [transaction]); - + const message = transaction?.message; const status = useTransactionStatus(signature); if (!status || !status.info) { @@ -219,10 +223,11 @@ function AccountsCard({ signature }: Props) { return ; } - const accountRows = message.accountKeys.map((pubkey, index) => { + const accountRows = message.accountKeys.map((account, index) => { const pre = meta.preBalances[index]; const post = meta.postBalances[index]; - const key = pubkey.toBase58(); + const pubkey = account.pubkey; + const key = account.pubkey.toBase58(); const renderChange = () => { const change = post - pre; if (change === 0) return ""; @@ -245,13 +250,13 @@ function AccountsCard({ signature }: Props) { {index === 0 && ( Fee Payer )} - {!message.isAccountWritable(index) && ( + {!account.writable && ( Readonly )} - {index < message.header.numRequiredSignatures && ( + {account.signer && ( Signer )} - {message.instructions.find((ix) => ix.programIdIndex === index) && ( + {message.instructions.find((ix) => ix.programId.equals(pubkey)) && ( Program )} @@ -290,22 +295,50 @@ function InstructionsSection({ signature }: Props) { if (!status || !status.info || !details || !details.transaction) return null; const { transaction } = details.transaction; - if (transaction.instructions.length === 0) { + if (transaction.message.instructions.length === 0) { return ; } const result = status.info.result; - const instructionDetails = transaction.instructions.map((ix, index) => { - const props = { ix, result, index }; + const instructionDetails = transaction.message.instructions.map( + (next, index) => { + if ("parsed" in next) { + if (next.program === "spl-token") { + return ( + + ); + } - if (SystemProgram.programId.equals(ix.programId)) { - return ; - } else if (StakeProgram.programId.equals(ix.programId)) { - return ; - } else { - return ; + const props = { ix: next, result, index }; + return ; + } + + const ix = intoTransactionInstruction(transaction, index); + if (!ix) { + return ( + + ); + } + + const props = { ix, result, index }; + if (SystemProgram.programId.equals(ix.programId)) { + return ; + } else if (StakeProgram.programId.equals(ix.programId)) { + return ; + } else { + return ; + } } - }); + ); return ( <> diff --git a/explorer/src/components/instruction/InstructionCard.tsx b/explorer/src/components/instruction/InstructionCard.tsx index 523b5743fe..7047633cb0 100644 --- a/explorer/src/components/instruction/InstructionCard.tsx +++ b/explorer/src/components/instruction/InstructionCard.tsx @@ -1,13 +1,18 @@ import React from "react"; -import { TransactionInstruction, SignatureResult } from "@solana/web3.js"; +import { + TransactionInstruction, + SignatureResult, + ParsedInstruction, +} from "@solana/web3.js"; import { RawDetails } from "./RawDetails"; +import { RawParsedDetails } from "./RawParsedDetails"; type InstructionProps = { title: string; children?: React.ReactNode; result: SignatureResult; index: number; - ix: TransactionInstruction; + ix: TransactionInstruction | ParsedInstruction; defaultRaw?: boolean; }; @@ -45,7 +50,15 @@ export function InstructionCard({
- {showRaw ? : children} + {showRaw ? ( + "parsed" in ix ? ( + + ) : ( + + ) + ) : ( + children + )}
diff --git a/explorer/src/components/instruction/RawParsedDetails.tsx b/explorer/src/components/instruction/RawParsedDetails.tsx new file mode 100644 index 0000000000..1e856dc87f --- /dev/null +++ b/explorer/src/components/instruction/RawParsedDetails.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { ParsedInstruction } from "@solana/web3.js"; +import Address from "components/common/Address"; + +export function RawParsedDetails({ ix }: { ix: ParsedInstruction }) { + return ( + <> + + Program + +
+ + + + + Instruction Data (JSON) + +
+            {JSON.stringify(ix.parsed, null, 2)}
+          
+ + + + ); +} diff --git a/explorer/src/components/instruction/UnknownDetailsCard.tsx b/explorer/src/components/instruction/UnknownDetailsCard.tsx index d50767c216..8117cbe6b4 100644 --- a/explorer/src/components/instruction/UnknownDetailsCard.tsx +++ b/explorer/src/components/instruction/UnknownDetailsCard.tsx @@ -1,5 +1,9 @@ import React from "react"; -import { TransactionInstruction, SignatureResult } from "@solana/web3.js"; +import { + TransactionInstruction, + SignatureResult, + ParsedInstruction, +} from "@solana/web3.js"; import { InstructionCard } from "./InstructionCard"; export function UnknownDetailsCard({ @@ -7,7 +11,7 @@ export function UnknownDetailsCard({ index, result, }: { - ix: TransactionInstruction; + ix: TransactionInstruction | ParsedInstruction; index: number; result: SignatureResult; }) { diff --git a/explorer/src/components/instruction/token/TokenDetailsCard.tsx b/explorer/src/components/instruction/token/TokenDetailsCard.tsx new file mode 100644 index 0000000000..c974e8fce7 --- /dev/null +++ b/explorer/src/components/instruction/token/TokenDetailsCard.tsx @@ -0,0 +1,88 @@ +import React from "react"; +import { coerce } from "superstruct"; +import { + SignatureResult, + ParsedTransaction, + PublicKey, + ParsedInstruction, +} from "@solana/web3.js"; + +import { UnknownDetailsCard } from "../UnknownDetailsCard"; +import { InstructionCard } from "../InstructionCard"; +import Address from "components/common/Address"; +import { ParsedInstructionInfo, IX_STRUCTS } from "./types"; + +const IX_TITLES = { + initializeMint: "Initialize Mint", + initializeAccount: "Initialize Account", + initializeMultisig: "Initialize Multisig", + transfer: "Transfer", + approve: "Approve", + revoke: "Revoke", + setOwner: "Set Owner", + mintTo: "Mint To", + burn: "Burn", + closeAccount: "Close Account", +}; + +type DetailsProps = { + tx: ParsedTransaction; + ix: ParsedInstruction; + result: SignatureResult; + index: number; +}; + +export function TokenDetailsCard(props: DetailsProps) { + try { + const parsed = coerce(props.ix.parsed, ParsedInstructionInfo); + const { type, info } = parsed; + const title = `Token: ${IX_TITLES[type]}`; + const coerced = coerce(info, IX_STRUCTS[type] as any); + return ; + } catch (err) { + return ; + } +} + +type InfoProps = { + ix: ParsedInstruction; + info: any; + result: SignatureResult; + index: number; + title: string; +}; + +function TokenInstruction(props: InfoProps) { + const attributes = []; + for (let key in props.info) { + const value = props.info[key]; + if (value === undefined) continue; + + let tag; + if (value instanceof PublicKey) { + tag =
; + } else { + tag = <>{value}; + } + + key = key.charAt(0).toUpperCase() + key.slice(1); + + attributes.push( + + {key} + {tag} + + ); + } + + return ( + + {attributes} + + ); +} diff --git a/explorer/src/components/instruction/token/types.ts b/explorer/src/components/instruction/token/types.ts new file mode 100644 index 0000000000..a16939cf60 --- /dev/null +++ b/explorer/src/components/instruction/token/types.ts @@ -0,0 +1,129 @@ +import { + enums, + object, + any, + StructType, + coercion, + struct, + number, + optional, + array, +} from "superstruct"; +import { PublicKey } from "@solana/web3.js"; + +const PubkeyValue = struct("Pubkey", (value) => value instanceof PublicKey); +const Pubkey = coercion(PubkeyValue, (value) => { + if (typeof value === "string") return new PublicKey(value); + throw new Error("invalid pubkey"); +}); + +const InitializeMint = object({ + mint: Pubkey, + amount: number(), + decimals: number(), + owner: optional(Pubkey), + account: optional(Pubkey), +}); + +const InitializeAccount = object({ + account: Pubkey, + mint: Pubkey, + owner: Pubkey, +}); + +const InitializeMultisig = object({ + multisig: Pubkey, + signers: array(Pubkey), + m: number(), +}); + +const Transfer = object({ + source: Pubkey, + destination: Pubkey, + amount: number(), + authority: optional(Pubkey), + multisigAuthority: optional(Pubkey), + signers: optional(array(Pubkey)), +}); + +const Approve = object({ + source: Pubkey, + delegate: Pubkey, + amount: number(), + owner: optional(Pubkey), + multisigOwner: optional(Pubkey), + signers: optional(array(Pubkey)), +}); + +const Revoke = object({ + source: Pubkey, + owner: optional(Pubkey), + multisigOwner: optional(Pubkey), + signers: optional(array(Pubkey)), +}); + +const SetOwner = object({ + owned: Pubkey, + newOwner: Pubkey, + owner: optional(Pubkey), + multisigOwner: optional(Pubkey), + signers: optional(array(Pubkey)), +}); + +const MintTo = object({ + mint: Pubkey, + account: Pubkey, + amount: number(), + owner: optional(Pubkey), + multisigOwner: optional(Pubkey), + signers: optional(array(Pubkey)), +}); + +const Burn = object({ + account: Pubkey, + amount: number(), + authority: optional(Pubkey), + multisigAuthority: optional(Pubkey), + signers: optional(array(Pubkey)), +}); + +const CloseAccount = object({ + account: Pubkey, + destination: Pubkey, + owner: optional(Pubkey), + multisigOwner: optional(Pubkey), + signers: optional(array(Pubkey)), +}); + +type TokenInstructionType = StructType; +const TokenInstructionType = enums([ + "initializeMint", + "initializeAccount", + "initializeMultisig", + "transfer", + "approve", + "revoke", + "setOwner", + "mintTo", + "burn", + "closeAccount", +]); + +export const IX_STRUCTS = { + initializeMint: InitializeMint, + initializeAccount: InitializeAccount, + initializeMultisig: InitializeMultisig, + transfer: Transfer, + approve: Approve, + revoke: Revoke, + setOwner: SetOwner, + mintTo: MintTo, + burn: Burn, + closeAccount: CloseAccount, +}; + +export type ParsedInstructionInfo = StructType; +export const ParsedInstructionInfo = object({ + type: TokenInstructionType, + info: any(), +}); diff --git a/explorer/src/providers/accounts/tokens.tsx b/explorer/src/providers/accounts/tokens.tsx index 5ee2b0d054..3704172092 100644 --- a/explorer/src/providers/accounts/tokens.tsx +++ b/explorer/src/providers/accounts/tokens.tsx @@ -1,17 +1,36 @@ import React from "react"; -import { Connection, PublicKey, TokenAccountInfo } from "@solana/web3.js"; +import { Connection, PublicKey } from "@solana/web3.js"; import { FetchStatus, useAccounts } from "./index"; import { useCluster, Cluster } from "../cluster"; +import { number, string, boolean, coerce, object, nullable } from "superstruct"; + +export type TokenAccountData = { + mint: PublicKey; + owner: PublicKey; + amount: number; + isInitialized: boolean; + isNative: boolean; +}; + +const TokenAccountInfo = object({ + mint: string(), + owner: string(), + amount: number(), + delegate: nullable(string()), + delegatedAmount: number(), + isInitialized: boolean(), + isNative: boolean(), +}); interface AccountTokens { status: FetchStatus; - tokens?: TokenAccountInfo[]; + tokens?: TokenAccountData[]; } interface Update { pubkey: PublicKey; status: FetchStatus; - tokens?: TokenAccountInfo[]; + tokens?: TokenAccountData[]; } type Action = Update | "clear"; @@ -98,8 +117,18 @@ async function fetchAccountTokens( const { value } = await new Connection( url, "recent" - ).getTokenAccountsByOwner(pubkey, { programId: TOKEN_PROGRAM_ID }); - tokens = value.map((accountInfo) => accountInfo.account.data); + ).getParsedTokenAccountsByOwner(pubkey, { programId: TOKEN_PROGRAM_ID }); + tokens = value.map((accountInfo) => { + const parsedInfo = accountInfo.account.data.parsed.info; + const info = coerce(parsedInfo, TokenAccountInfo); + return { + mint: new PublicKey(info.mint), + owner: new PublicKey(info.owner), + amount: info.amount, + isInitialized: info.isInitialized, + isNative: info.isNative, + }; + }); status = FetchStatus.Fetched; } catch (error) { status = FetchStatus.FetchFailed; diff --git a/explorer/src/providers/stats/solanaBeach.tsx b/explorer/src/providers/stats/solanaBeach.tsx index 56c473c63a..914512ab8c 100644 --- a/explorer/src/providers/stats/solanaBeach.tsx +++ b/explorer/src/providers/stats/solanaBeach.tsx @@ -1,49 +1,29 @@ import React from "react"; import io from "socket.io-client"; -import { object, number, is, StructType, any } from "superstruct"; +import { pick, number, is, StructType } from "superstruct"; import { useCluster, Cluster } from "providers/cluster"; -// TODO: use `partial` when it is fixed -// https://github.com/ianstormtaylor/superstruct/issues/405 -const DashboardInfo = object({ - activatedStake: number(), +const DashboardInfo = pick({ avgBlockTime_1h: number(), avgBlockTime_1min: number(), - circulatingSupply: number(), - dailyPriceChange: number(), - dailyVolume: number(), - delinquentStake: number(), - epochInfo: object({ - absoluteEpochStartSlot: number(), + epochInfo: pick({ absoluteSlot: number(), blockHeight: number(), epoch: number(), slotIndex: number(), slotsInEpoch: number(), }), - stakingYield: number(), - tokenPrice: number(), - totalDelegatedStake: number(), - totalSupply: number(), }); -// TODO: use `partial` when it is fixed -// https://github.com/ianstormtaylor/superstruct/issues/405 -const RootInfo = object({ - currentLeader: any(), - nextLeaders: any(), +const RootInfo = pick({ root: number(), - servedSlots: any(), }); export const PERF_UPDATE_SEC = 5; -// TODO: use `partial` when it is fixed -// https://github.com/ianstormtaylor/superstruct/issues/405 -const PerformanceInfo = object({ +const PerformanceInfo = pick({ avgTPS: number(), - perfHistory: any(), totalTransactionCount: number(), }); diff --git a/explorer/src/providers/transactions/cached.ts b/explorer/src/providers/transactions/cached.ts index a321530c2e..99f988c404 100644 --- a/explorer/src/providers/transactions/cached.ts +++ b/explorer/src/providers/transactions/cached.ts @@ -8,10 +8,11 @@ import { TransactionStatusInfo } from "./index"; import { Transaction, - ConfirmedTransaction, + ParsedConfirmedTransaction, Message, clusterApiUrl, } from "@solana/web3.js"; +import { intoParsedTransaction } from "utils/tx"; export const isCached = (url: string, signature: string): boolean => { return url === clusterApiUrl("mainnet-beta") && signature in CACHED_STATUSES; @@ -62,214 +63,228 @@ export const CACHED_STATUSES: { [key: string]: TransactionStatusInfo } = { }, }; -export const CACHED_DETAILS: { [key: string]: ConfirmedTransaction } = { +export const CACHED_DETAILS: { [key: string]: ParsedConfirmedTransaction } = { uQf4pS38FjRF294QFEXizhYkZFjSR9ZSBvvV6MV5b4VpdfRnK3PY9TWZ2qHMQKtte3XwKVLcWqsTF6wL9NEZMty: { meta: null, slot: 10440804, - transaction: Transaction.populate( - new Message({ - accountKeys: [ - "2ojv9BAiHUrvsm9gxDe7fJSzbNZSJcxZvf8dqmWGHG8S", - "4C6NCcLPUgGuBBkV2dJW96mrptMUCp3RG1ft9rqwjFi9", - "11111111111111111111111111111111", - ], - header: { - numReadonlySignedAccounts: 0, - numReadonlyUnsignedAccounts: 1, - numRequiredSignatures: 1, - }, - instructions: [ - { accounts: [0, 1], data: "3Bxs411UBrj8QXUb", programIdIndex: 2 }, - ], - recentBlockhash: "5Aw8MaMYdYtnfJyyrregWMWGgiMtWZ6GtRzeP6Ufo65Z", - }), - [ - "uQf4pS38FjRF294QFEXizhYkZFjSR9ZSBvvV6MV5b4VpdfRnK3PY9TWZ2qHMQKtte3XwKVLcWqsTF6wL9NEZMty", - ] + transaction: intoParsedTransaction( + Transaction.populate( + new Message({ + accountKeys: [ + "2ojv9BAiHUrvsm9gxDe7fJSzbNZSJcxZvf8dqmWGHG8S", + "4C6NCcLPUgGuBBkV2dJW96mrptMUCp3RG1ft9rqwjFi9", + "11111111111111111111111111111111", + ], + header: { + numReadonlySignedAccounts: 0, + numReadonlyUnsignedAccounts: 1, + numRequiredSignatures: 1, + }, + instructions: [ + { accounts: [0, 1], data: "3Bxs411UBrj8QXUb", programIdIndex: 2 }, + ], + recentBlockhash: "5Aw8MaMYdYtnfJyyrregWMWGgiMtWZ6GtRzeP6Ufo65Z", + }), + [ + "uQf4pS38FjRF294QFEXizhYkZFjSR9ZSBvvV6MV5b4VpdfRnK3PY9TWZ2qHMQKtte3XwKVLcWqsTF6wL9NEZMty", + ] + ) ), }, DYrfStEEzbV5sftX8LgUa54Nwnc5m5E1731cqBtiiC66TeXgKpfqZEQTuFY3vhHZ2K1BsaFM3X9FqisR28EtZr8: { meta: null, slot: 10451288, - transaction: Transaction.populate( - new Message({ - accountKeys: [ - "2ojv9BAiHUrvsm9gxDe7fJSzbNZSJcxZvf8dqmWGHG8S", - "4C6NCcLPUgGuBBkV2dJW96mrptMUCp3RG1ft9rqwjFi9", - "11111111111111111111111111111111", - ], - header: { - numReadonlySignedAccounts: 0, - numReadonlyUnsignedAccounts: 1, - numRequiredSignatures: 1, - }, - instructions: [ - { - accounts: [0, 1], - data: "3Bxs3zwYHuDo723R", - programIdIndex: 2, + transaction: intoParsedTransaction( + Transaction.populate( + new Message({ + accountKeys: [ + "2ojv9BAiHUrvsm9gxDe7fJSzbNZSJcxZvf8dqmWGHG8S", + "4C6NCcLPUgGuBBkV2dJW96mrptMUCp3RG1ft9rqwjFi9", + "11111111111111111111111111111111", + ], + header: { + numReadonlySignedAccounts: 0, + numReadonlyUnsignedAccounts: 1, + numRequiredSignatures: 1, }, - ], - recentBlockhash: "4hXYcBdfcadcjfWV17ZwMa4MXe8kbZHYHwr3GzfyqunL", - }), - [ - "DYrfStEEzbV5sftX8LgUa54Nwnc5m5E1731cqBtiiC66TeXgKpfqZEQTuFY3vhHZ2K1BsaFM3X9FqisR28EtZr8", - ] + instructions: [ + { + accounts: [0, 1], + data: "3Bxs3zwYHuDo723R", + programIdIndex: 2, + }, + ], + recentBlockhash: "4hXYcBdfcadcjfWV17ZwMa4MXe8kbZHYHwr3GzfyqunL", + }), + [ + "DYrfStEEzbV5sftX8LgUa54Nwnc5m5E1731cqBtiiC66TeXgKpfqZEQTuFY3vhHZ2K1BsaFM3X9FqisR28EtZr8", + ] + ) ), }, "3bLx2PLpkxCxJA5P7HVe8asFdSWXVAh1DrxfkqWE9bWvPRxXE2hqwj1vuSC858fUw3XAGQcHbJknhtNdxY2sehab": { meta: null, slot: 10516588, - transaction: Transaction.populate( - new Message({ - accountKeys: [ - "2ojv9BAiHUrvsm9gxDe7fJSzbNZSJcxZvf8dqmWGHG8S", - "4C6NCcLPUgGuBBkV2dJW96mrptMUCp3RG1ft9rqwjFi9", - "11111111111111111111111111111111", - ], - header: { - numReadonlySignedAccounts: 0, - numReadonlyUnsignedAccounts: 1, - numRequiredSignatures: 1, - }, - instructions: [ - { - accounts: [0, 1], - data: "3Bxs3zwYHuDo723R", - programIdIndex: 2, + transaction: intoParsedTransaction( + Transaction.populate( + new Message({ + accountKeys: [ + "2ojv9BAiHUrvsm9gxDe7fJSzbNZSJcxZvf8dqmWGHG8S", + "4C6NCcLPUgGuBBkV2dJW96mrptMUCp3RG1ft9rqwjFi9", + "11111111111111111111111111111111", + ], + header: { + numReadonlySignedAccounts: 0, + numReadonlyUnsignedAccounts: 1, + numRequiredSignatures: 1, }, - ], - recentBlockhash: "HSzTGt3PJMeQtFr94gEdeZqTRaBxgS8Wf1zq3MDdNT3L", - }), - [ - "3bLx2PLpkxCxJA5P7HVe8asFdSWXVAh1DrxfkqWE9bWvPRxXE2hqwj1vuSC858fUw3XAGQcHbJknhtNdxY2sehab", - ] + instructions: [ + { + accounts: [0, 1], + data: "3Bxs3zwYHuDo723R", + programIdIndex: 2, + }, + ], + recentBlockhash: "HSzTGt3PJMeQtFr94gEdeZqTRaBxgS8Wf1zq3MDdNT3L", + }), + [ + "3bLx2PLpkxCxJA5P7HVe8asFdSWXVAh1DrxfkqWE9bWvPRxXE2hqwj1vuSC858fUw3XAGQcHbJknhtNdxY2sehab", + ] + ) ), }, "3fE8xNgyxbwbvA5MX3wM87ahDDgCVEaaMMSa8UCWWNxojaRYBgrQyiKXLSxcryMWb7sEyVLBWyqUaRWnQCroSqjY": { meta: null, slot: 10575124, - transaction: Transaction.populate( - new Message({ - accountKeys: [ - "2ojv9BAiHUrvsm9gxDe7fJSzbNZSJcxZvf8dqmWGHG8S", - "4C6NCcLPUgGuBBkV2dJW96mrptMUCp3RG1ft9rqwjFi9", - "11111111111111111111111111111111", - ], - header: { - numReadonlySignedAccounts: 0, - numReadonlyUnsignedAccounts: 1, - numRequiredSignatures: 1, - }, - instructions: [ - { - accounts: [0, 1], - data: "3Bxs3zuKU6mRKSqD", - programIdIndex: 2, + transaction: intoParsedTransaction( + Transaction.populate( + new Message({ + accountKeys: [ + "2ojv9BAiHUrvsm9gxDe7fJSzbNZSJcxZvf8dqmWGHG8S", + "4C6NCcLPUgGuBBkV2dJW96mrptMUCp3RG1ft9rqwjFi9", + "11111111111111111111111111111111", + ], + header: { + numReadonlySignedAccounts: 0, + numReadonlyUnsignedAccounts: 1, + numRequiredSignatures: 1, }, - ], - recentBlockhash: "6f6TBMhUoypfR5HHnEqC6VoooKxEcNad5W3Sf63j9MSD", - }), - [ - "3fE8xNgyxbwbvA5MX3wM87ahDDgCVEaaMMSa8UCWWNxojaRYBgrQyiKXLSxcryMWb7sEyVLBWyqUaRWnQCroSqjY", - ] + instructions: [ + { + accounts: [0, 1], + data: "3Bxs3zuKU6mRKSqD", + programIdIndex: 2, + }, + ], + recentBlockhash: "6f6TBMhUoypfR5HHnEqC6VoooKxEcNad5W3Sf63j9MSD", + }), + [ + "3fE8xNgyxbwbvA5MX3wM87ahDDgCVEaaMMSa8UCWWNxojaRYBgrQyiKXLSxcryMWb7sEyVLBWyqUaRWnQCroSqjY", + ] + ) ), }, "5PWymGjKV7T1oqeqGn139EHFyjNM2dnNhHCUcfD2bmdj8cfF95HpY1uJ84W89c4sJQnmyZxXcYrcjumx2jHUvxZQ": { meta: null, slot: 12447825, - transaction: Transaction.populate( - new Message({ - accountKeys: [ - "HCV5dGFJXRrJ3jhDYA4DCeb9TEDTwGGYXtT3wHksu2Zr", - "4C6NCcLPUgGuBBkV2dJW96mrptMUCp3RG1ft9rqwjFi9", - "11111111111111111111111111111111", - ], - header: { - numReadonlySignedAccounts: 0, - numReadonlyUnsignedAccounts: 1, - numRequiredSignatures: 1, - }, - instructions: [ - { - accounts: [0, 1], - data: "3Bxs3zrfhSqZJTR1", - programIdIndex: 2, + transaction: intoParsedTransaction( + Transaction.populate( + new Message({ + accountKeys: [ + "HCV5dGFJXRrJ3jhDYA4DCeb9TEDTwGGYXtT3wHksu2Zr", + "4C6NCcLPUgGuBBkV2dJW96mrptMUCp3RG1ft9rqwjFi9", + "11111111111111111111111111111111", + ], + header: { + numReadonlySignedAccounts: 0, + numReadonlyUnsignedAccounts: 1, + numRequiredSignatures: 1, }, - ], - recentBlockhash: "3HJNFraT7XGAqMrQs83EKwDGB6LpHVwUMQKGaYMNY49E", - }), - [ - "5PWymGjKV7T1oqeqGn139EHFyjNM2dnNhHCUcfD2bmdj8cfF95HpY1uJ84W89c4sJQnmyZxXcYrcjumx2jHUvxZQ", - ] + instructions: [ + { + accounts: [0, 1], + data: "3Bxs3zrfhSqZJTR1", + programIdIndex: 2, + }, + ], + recentBlockhash: "3HJNFraT7XGAqMrQs83EKwDGB6LpHVwUMQKGaYMNY49E", + }), + [ + "5PWymGjKV7T1oqeqGn139EHFyjNM2dnNhHCUcfD2bmdj8cfF95HpY1uJ84W89c4sJQnmyZxXcYrcjumx2jHUvxZQ", + ] + ) ), }, "5K4KuqTTRNtzfpxWiwnkePzGfsa3tBEmpMy7vQFR3KWFAZNVY9tvoSaz1Yt5dKxcgsZPio2EsASVDGbQB1HvirGD": { meta: null, slot: 12450728, - transaction: Transaction.populate( - new Message({ - accountKeys: [ - "6yKHERk8rsbmJxvMpPuwPs1ct3hRiP7xaJF2tvnGU6nK", - "4C6NCcLPUgGuBBkV2dJW96mrptMUCp3RG1ft9rqwjFi9", - "3o6xgkJ9sTmDeQWyfj3sxwon18fXJB9PV5LDc8sfgR4a", - "11111111111111111111111111111111", - ], - header: { - numReadonlySignedAccounts: 0, - numReadonlyUnsignedAccounts: 1, - numRequiredSignatures: 2, - }, - instructions: [ - { - accounts: [1, 2], - data: "3Bxs3ztRCp3tH1yZ", - programIdIndex: 3, + transaction: intoParsedTransaction( + Transaction.populate( + new Message({ + accountKeys: [ + "6yKHERk8rsbmJxvMpPuwPs1ct3hRiP7xaJF2tvnGU6nK", + "4C6NCcLPUgGuBBkV2dJW96mrptMUCp3RG1ft9rqwjFi9", + "3o6xgkJ9sTmDeQWyfj3sxwon18fXJB9PV5LDc8sfgR4a", + "11111111111111111111111111111111", + ], + header: { + numReadonlySignedAccounts: 0, + numReadonlyUnsignedAccounts: 1, + numRequiredSignatures: 2, }, - ], - recentBlockhash: "8eXVUNRxrDgpsEuoTWyLay1LUh2djc3Y8cw2owXRN8cU", - }), - [ - "5K4KuqTTRNtzfpxWiwnkePzGfsa3tBEmpMy7vQFR3KWFAZNVY9tvoSaz1Yt5dKxcgsZPio2EsASVDGbQB1HvirGD", - "37tvpG1eAeEBizJPhJvmpC2BY8npwy6K1wrZdNwdRAfWSbkerY3ZwYAPMHbrzoq7tthvWC2qFU28niqLPxbukeXF", - ] + instructions: [ + { + accounts: [1, 2], + data: "3Bxs3ztRCp3tH1yZ", + programIdIndex: 3, + }, + ], + recentBlockhash: "8eXVUNRxrDgpsEuoTWyLay1LUh2djc3Y8cw2owXRN8cU", + }), + [ + "5K4KuqTTRNtzfpxWiwnkePzGfsa3tBEmpMy7vQFR3KWFAZNVY9tvoSaz1Yt5dKxcgsZPio2EsASVDGbQB1HvirGD", + "37tvpG1eAeEBizJPhJvmpC2BY8npwy6K1wrZdNwdRAfWSbkerY3ZwYAPMHbrzoq7tthvWC2qFU28niqLPxbukeXF", + ] + ) ), }, "45pGoC4Rr3fJ1TKrsiRkhHRbdUeX7633XAGVec6XzVdpRbzQgHhe6ZC6Uq164MPWtiqMg7wCkC6Wy3jy2BqsDEKf": { meta: null, slot: 12972684, - transaction: Transaction.populate( - new Message({ - accountKeys: [ - "6yKHERk8rsbmJxvMpPuwPs1ct3hRiP7xaJF2tvnGU6nK", - "3o6xgkJ9sTmDeQWyfj3sxwon18fXJB9PV5LDc8sfgR4a", - "1nc1nerator11111111111111111111111111111111", - "11111111111111111111111111111111", - ], - header: { - numReadonlySignedAccounts: 0, - numReadonlyUnsignedAccounts: 1, - numRequiredSignatures: 2, - }, - instructions: [ - { - accounts: [1, 2], - data: "3Bxs4NNAyLXRbuZZ", - programIdIndex: 3, + transaction: intoParsedTransaction( + Transaction.populate( + new Message({ + accountKeys: [ + "6yKHERk8rsbmJxvMpPuwPs1ct3hRiP7xaJF2tvnGU6nK", + "3o6xgkJ9sTmDeQWyfj3sxwon18fXJB9PV5LDc8sfgR4a", + "1nc1nerator11111111111111111111111111111111", + "11111111111111111111111111111111", + ], + header: { + numReadonlySignedAccounts: 0, + numReadonlyUnsignedAccounts: 1, + numRequiredSignatures: 2, }, - ], - recentBlockhash: "2xnatNUtSbeMRwi3k4vxPwXxeKFQYVuCNRg2rAgydWVP", - }), - [ - "45pGoC4Rr3fJ1TKrsiRkhHRbdUeX7633XAGVec6XzVdpRbzQgHhe6ZC6Uq164MPWtiqMg7wCkC6Wy3jy2BqsDEKf", - "2E7CDMTssxTYkdetCKVWQv9X2KNDPiuZrT2Y7647PhFEXuAWWxmHJb3ryCmP29ocQ1SNc7VyJjjm4X3jE8xWDmGY", - ] + instructions: [ + { + accounts: [1, 2], + data: "3Bxs4NNAyLXRbuZZ", + programIdIndex: 3, + }, + ], + recentBlockhash: "2xnatNUtSbeMRwi3k4vxPwXxeKFQYVuCNRg2rAgydWVP", + }), + [ + "45pGoC4Rr3fJ1TKrsiRkhHRbdUeX7633XAGVec6XzVdpRbzQgHhe6ZC6Uq164MPWtiqMg7wCkC6Wy3jy2BqsDEKf", + "2E7CDMTssxTYkdetCKVWQv9X2KNDPiuZrT2Y7647PhFEXuAWWxmHJb3ryCmP29ocQ1SNc7VyJjjm4X3jE8xWDmGY", + ] + ) ), }, }; diff --git a/explorer/src/providers/transactions/details.tsx b/explorer/src/providers/transactions/details.tsx index 7abce55053..0427d3fa77 100644 --- a/explorer/src/providers/transactions/details.tsx +++ b/explorer/src/providers/transactions/details.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Connection, TransactionSignature, - ConfirmedTransaction, + ParsedConfirmedTransaction, } from "@solana/web3.js"; import { useCluster } from "../cluster"; import { useTransactions, FetchStatus } from "./index"; @@ -10,7 +10,7 @@ import { CACHED_DETAILS, isCached } from "./cached"; export interface Details { fetchStatus: FetchStatus; - transaction: ConfirmedTransaction | null; + transaction: ParsedConfirmedTransaction | null; } type State = { [signature: string]: Details }; @@ -25,7 +25,7 @@ interface Update { type: ActionType.Update; signature: string; fetchStatus: FetchStatus; - transaction: ConfirmedTransaction | null; + transaction: ParsedConfirmedTransaction | null; } interface Add { @@ -134,7 +134,7 @@ async function fetchDetails( fetchStatus = FetchStatus.Fetched; } else { try { - transaction = await new Connection(url).getConfirmedTransaction( + transaction = await new Connection(url).getParsedConfirmedTransaction( signature ); fetchStatus = FetchStatus.Fetched; diff --git a/explorer/src/scss/_solana-dark-overrides.scss b/explorer/src/scss/_solana-dark-overrides.scss index e753c78c2e..f654c285b5 100644 --- a/explorer/src/scss/_solana-dark-overrides.scss +++ b/explorer/src/scss/_solana-dark-overrides.scss @@ -3,7 +3,7 @@ // Use this to write your custom SCSS // -code { +code, pre { background-color: $black-dark; color: $white; } diff --git a/explorer/src/scss/_solana.scss b/explorer/src/scss/_solana.scss index 9ff0cdaec3..60c7b4c88e 100644 --- a/explorer/src/scss/_solana.scss +++ b/explorer/src/scss/_solana.scss @@ -3,7 +3,7 @@ // Use this to write your custom SCSS // -code { +code, pre { padding: 0.33rem; border-radius: $border-radius; background-color: $gray-200; diff --git a/explorer/src/utils/tx.ts b/explorer/src/utils/tx.ts index ef21e902fd..5e5bd5c0f6 100644 --- a/explorer/src/utils/tx.ts +++ b/explorer/src/utils/tx.ts @@ -1,3 +1,4 @@ +import bs58 from "bs58"; import { SystemProgram, StakeProgram, @@ -7,6 +8,9 @@ import { SYSVAR_RENT_PUBKEY, SYSVAR_REWARDS_PUBKEY, SYSVAR_STAKE_HISTORY_PUBKEY, + ParsedTransaction, + TransactionInstruction, + Transaction, } from "@solana/web3.js"; const PROGRAM_IDS = { @@ -48,3 +52,53 @@ export function displayAddress(address: string): string { address ); } + +export function intoTransactionInstruction( + tx: ParsedTransaction, + index: number +): TransactionInstruction | undefined { + const message = tx.message; + const instruction = message.instructions[index]; + if ("parsed" in instruction) return; + + const keys = []; + for (const account of instruction.accounts) { + const accountKey = message.accountKeys.find(({ pubkey }) => + pubkey.equals(account) + ); + if (!accountKey) return; + keys.push({ + pubkey: accountKey.pubkey, + isSigner: accountKey.signer, + isWritable: accountKey.writable, + }); + } + + return new TransactionInstruction({ + data: bs58.decode(instruction.data), + keys: keys, + programId: instruction.programId, + }); +} + +export function intoParsedTransaction(tx: Transaction): ParsedTransaction { + const message = tx.compileMessage(); + return { + signatures: tx.signatures.map((value) => + bs58.encode(value.signature as any) + ), + message: { + accountKeys: message.accountKeys.map((key, index) => ({ + pubkey: key, + signer: tx.signatures.some(({ publicKey }) => publicKey.equals(key)), + writable: message.isAccountWritable(index), + })), + instructions: message.instructions.map((ix) => ({ + programId: message.accountKeys[ix.programIdIndex], + accounts: ix.accounts.map((index) => message.accountKeys[index]), + data: ix.data, + })), + recentBlockhash: message.recentBlockhash, + }, + }; +}