diff --git a/explorer/src/components/AccountsCard.tsx b/explorer/src/components/AccountsCard.tsx index 6d669994fb..7bdb67a6a1 100644 --- a/explorer/src/components/AccountsCard.tsx +++ b/explorer/src/components/AccountsCard.tsx @@ -7,7 +7,8 @@ import { Account, Status } from "../providers/accounts"; -import { assertUnreachable, displayAddress } from "../utils"; +import { assertUnreachable } from "../utils"; +import { displayAddress } from "../utils/tx"; import { useCluster } from "../providers/cluster"; import { PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js"; diff --git a/explorer/src/components/TransactionModal.tsx b/explorer/src/components/TransactionModal.tsx index 45fcfa7756..b870006e23 100644 --- a/explorer/src/components/TransactionModal.tsx +++ b/explorer/src/components/TransactionModal.tsx @@ -5,12 +5,13 @@ import { ActionType, Selected } from "../providers/transactions"; -import { displayAddress } from "../utils"; +import { displayAddress, decodeCreate, decodeTransfer } from "../utils/tx"; import { useBlocks } from "../providers/blocks"; import { LAMPORTS_PER_SOL, TransferParams, - CreateAccountParams + CreateAccountParams, + TransactionInstruction } from "@solana/web3.js"; function TransactionModal() { @@ -70,31 +71,27 @@ function TransactionDetails({ selected }: { selected: Selected }) { ); } - const details = block.transactions[selected.signature]; - if (!details) return renderError("Transaction not found"); + const transaction = block.transactions[selected.signature]; + if (!transaction) return renderError("Transaction not found"); - const { transfers, creates } = details; - if (transfers.length === 0 && creates.length === 0) - return renderError( - "Details for this transaction's instructions are not yet supported" - ); + if (transaction.instructions.length === 0) + return renderError("No instructions found"); + + const instructionDetails = transaction.instructions.map((ix, index) => { + const transfer = decodeTransfer(ix); + if (transfer) return ; + const create = decodeCreate(ix); + if (create) return ; + return ; + }); - let i = 0; return ( <> - {details.transfers.map(transfer => { + {instructionDetails.map((details, i) => { return (
- {i > 1 ?
: null} - -
- ); - })} - {details.creates.map(create => { - return ( -
- {i > 1 ?
: null} - + {i > 1 ?
: null} + {details}
); })} @@ -102,9 +99,16 @@ function TransactionDetails({ selected }: { selected: Selected }) { ); } -function TransferDetails({ transfer }: { transfer: TransferParams }) { +function TransferDetails({ + transfer, + index +}: { + transfer: TransferParams; + index: number; +}) { return (
+

{`Instruction #${index + 1} (Transfer)`}

{transfer.fromPubkey.toBase58()} @@ -120,9 +124,17 @@ function TransferDetails({ transfer }: { transfer: TransferParams }) { ); } -function CreateDetails({ create }: { create: CreateAccountParams }) { +function CreateDetails({ + create, + index +}: { + create: CreateAccountParams; + index: number; +}) { return (
+

{`Instruction #${index + + 1} (Create Account)`}

{create.fromPubkey.toBase58()} @@ -142,6 +154,31 @@ function CreateDetails({ create }: { create: CreateAccountParams }) { ); } +function InstructionDetails({ + ix, + index +}: { + ix: TransactionInstruction; + index: number; +}) { + return ( +
+

{`Instruction #${index + 1}`}

+
+ {ix.keys.map(({ pubkey }, keyIndex) => ( + + {pubkey.toBase58()} + + ))} + {ix.data.length} + + {displayAddress(ix.programId)} + +
+
+ ); +} + function ListGroupItem({ label, children @@ -150,7 +187,7 @@ function ListGroupItem({ children: React.ReactNode; }) { return ( -
+
{label}
diff --git a/explorer/src/providers/accounts.tsx b/explorer/src/providers/accounts.tsx index b02c486e0c..c070890ee7 100644 --- a/explorer/src/providers/accounts.tsx +++ b/explorer/src/providers/accounts.tsx @@ -1,6 +1,6 @@ import React from "react"; import { PublicKey, Connection } from "@solana/web3.js"; -import { findGetParameter, findPathSegment } from "../utils"; +import { findGetParameter, findPathSegment } from "../utils/url"; import { useCluster, ClusterStatus } from "./cluster"; export enum Status { diff --git a/explorer/src/providers/blocks.tsx b/explorer/src/providers/blocks.tsx index 6b2ca70efa..3462909b70 100644 --- a/explorer/src/providers/blocks.tsx +++ b/explorer/src/providers/blocks.tsx @@ -1,13 +1,6 @@ import React from "react"; import bs58 from "bs58"; -import { - Connection, - Transaction, - TransferParams, - SystemProgram, - SystemInstruction, - CreateAccountParams -} from "@solana/web3.js"; +import { Connection, Transaction } from "@solana/web3.js"; import { useCluster, ClusterStatus } from "./cluster"; import { useTransactions } from "./transactions"; @@ -17,13 +10,7 @@ export enum Status { Success } -export interface TransactionDetails { - transaction: Transaction; - transfers: Array; - creates: Array; -} - -type Transactions = { [signature: string]: TransactionDetails }; +type Transactions = { [signature: string]: Transaction }; export interface Block { status: Status; transactions?: Transactions; @@ -154,38 +141,6 @@ export function BlocksProvider({ children }: BlocksProviderProps) { ); } -function decodeTransfers(tx: Transaction) { - const transferInstructions = tx.instructions - .filter(ix => ix.programId.equals(SystemProgram.programId)) - .filter(ix => SystemInstruction.decodeInstructionType(ix) === "Transfer"); - - let transfers: TransferParams[] = []; - transferInstructions.forEach(ix => { - try { - transfers.push(SystemInstruction.decodeTransfer(ix)); - } catch (err) { - console.error(ix, err); - } - }); - return transfers; -} - -function decodeCreates(tx: Transaction) { - const createInstructions = tx.instructions - .filter(ix => ix.programId.equals(SystemProgram.programId)) - .filter(ix => SystemInstruction.decodeInstructionType(ix) === "Create"); - - let creates: CreateAccountParams[] = []; - createInstructions.forEach(ix => { - try { - creates.push(SystemInstruction.decodeCreateAccount(ix)); - } catch (err) { - console.error(ix, err); - } - }); - return creates; -} - async function fetchBlock(dispatch: Dispatch, slot: number, url: string) { dispatch({ type: ActionType.Update, @@ -201,11 +156,7 @@ async function fetchBlock(dispatch: Dispatch, slot: number, url: string) { const signature = transaction.signature; if (signature) { const sig = bs58.encode(signature); - transactions[sig] = { - transaction, - transfers: decodeTransfers(transaction), - creates: decodeCreates(transaction) - }; + transactions[sig] = transaction; } }); status = Status.Success; diff --git a/explorer/src/providers/cluster.tsx b/explorer/src/providers/cluster.tsx index e0d98566dc..caded21c76 100644 --- a/explorer/src/providers/cluster.tsx +++ b/explorer/src/providers/cluster.tsx @@ -1,6 +1,6 @@ import React from "react"; import { clusterApiUrl, Connection } from "@solana/web3.js"; -import { findGetParameter } from "../utils"; +import { findGetParameter } from "../utils/url"; export enum ClusterStatus { Connected, diff --git a/explorer/src/providers/transactions.tsx b/explorer/src/providers/transactions.tsx index 2474705526..e7a9f09d21 100644 --- a/explorer/src/providers/transactions.tsx +++ b/explorer/src/providers/transactions.tsx @@ -5,7 +5,7 @@ import { SystemProgram, Account } from "@solana/web3.js"; -import { findGetParameter, findPathSegment } from "../utils"; +import { findGetParameter, findPathSegment } from "../utils/url"; import { useCluster, ClusterStatus } from "../providers/cluster"; import base58 from "bs58"; import { diff --git a/explorer/src/scss/_solana.scss b/explorer/src/scss/_solana.scss index 53c3cd441d..2c1eb5f181 100644 --- a/explorer/src/scss/_solana.scss +++ b/explorer/src/scss/_solana.scss @@ -40,4 +40,17 @@ code { input.text-signature, input.text-address { padding: 0 0.75rem +} + +h4.ix-pill { + display: inline-block; + padding: 5px; + border-radius: $border-radius; + margin-bottom: 2rem; + margin-left: -5px; + background-color: theme-color-level(info, $badge-soft-bg-level); +} + +.list-group-flush .list-group-item.ix-item:first-child { + border-top-width: 1px; } \ No newline at end of file diff --git a/explorer/src/utils/index.ts b/explorer/src/utils/index.ts new file mode 100644 index 0000000000..dc288916c0 --- /dev/null +++ b/explorer/src/utils/index.ts @@ -0,0 +1,3 @@ +export function assertUnreachable(x: never): never { + throw new Error("Unreachable!"); +} diff --git a/explorer/src/utils.ts b/explorer/src/utils/tx.ts similarity index 66% rename from explorer/src/utils.ts rename to explorer/src/utils/tx.ts index 001270f7bf..cd99934ade 100644 --- a/explorer/src/utils.ts +++ b/explorer/src/utils/tx.ts @@ -4,49 +4,16 @@ import { StakeProgram, VOTE_PROGRAM_ID, BpfLoader, + TransferParams, + SystemInstruction, + CreateAccountParams, + TransactionInstruction, SYSVAR_CLOCK_PUBKEY, SYSVAR_RENT_PUBKEY, SYSVAR_REWARDS_PUBKEY, SYSVAR_STAKE_HISTORY_PUBKEY } from "@solana/web3.js"; -export function findGetParameter(parameterName: string): string | null { - let result = null, - tmp = []; - window.location.search - .substr(1) - .split("&") - .forEach(function(item) { - tmp = item.split("="); - if (tmp[0].toLowerCase() === parameterName.toLowerCase()) { - if (tmp.length === 2) { - result = decodeURIComponent(tmp[1]); - } else if (tmp.length === 1) { - result = ""; - } - } - }); - return result; -} - -export function findPathSegment(pathName: string): string | null { - const segments = window.location.pathname.substr(1).split("/"); - if (segments.length < 2) return null; - - // remove all but last two segments - segments.splice(0, segments.length - 2); - - if (segments[0] === pathName) { - return segments[1]; - } - - return null; -} - -export function assertUnreachable(x: never): never { - throw new Error("Unreachable!"); -} - const PROGRAM_IDS = { Budget1111111111111111111111111111111111111: "Budget", Config1111111111111111111111111111111111111: "Config", @@ -86,3 +53,31 @@ export function displayAddress(pubkey: PublicKey): string { address ); } + +export function decodeTransfer( + ix: TransactionInstruction +): TransferParams | null { + if (!ix.programId.equals(SystemProgram.programId)) return null; + if (SystemInstruction.decodeInstructionType(ix) !== "Transfer") return null; + + try { + return SystemInstruction.decodeTransfer(ix); + } catch (err) { + console.error(ix, err); + return null; + } +} + +export function decodeCreate( + ix: TransactionInstruction +): CreateAccountParams | null { + if (!ix.programId.equals(SystemProgram.programId)) return null; + if (SystemInstruction.decodeInstructionType(ix) !== "Create") return null; + + try { + return SystemInstruction.decodeCreateAccount(ix); + } catch (err) { + console.error(ix, err); + return null; + } +} diff --git a/explorer/src/utils/url.ts b/explorer/src/utils/url.ts new file mode 100644 index 0000000000..846b7571e3 --- /dev/null +++ b/explorer/src/utils/url.ts @@ -0,0 +1,32 @@ +export function findGetParameter(parameterName: string): string | null { + let result = null, + tmp = []; + window.location.search + .substr(1) + .split("&") + .forEach(function(item) { + tmp = item.split("="); + if (tmp[0].toLowerCase() === parameterName.toLowerCase()) { + if (tmp.length === 2) { + result = decodeURIComponent(tmp[1]); + } else if (tmp.length === 1) { + result = ""; + } + } + }); + return result; +} + +export function findPathSegment(pathName: string): string | null { + const segments = window.location.pathname.substr(1).split("/"); + if (segments.length < 2) return null; + + // remove all but last two segments + segments.splice(0, segments.length - 2); + + if (segments[0] === pathName) { + return segments[1]; + } + + return null; +}