Show details for all transaction instructions

This commit is contained in:
Justin Starry 2020-04-09 17:49:47 +08:00 committed by Michael Vines
parent 1c1c628b19
commit 8de34e1f42
10 changed files with 149 additions and 117 deletions

View File

@ -7,7 +7,8 @@ import {
Account, Account,
Status Status
} from "../providers/accounts"; } from "../providers/accounts";
import { assertUnreachable, displayAddress } from "../utils"; import { assertUnreachable } from "../utils";
import { displayAddress } from "../utils/tx";
import { useCluster } from "../providers/cluster"; import { useCluster } from "../providers/cluster";
import { PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js"; import { PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";

View File

@ -5,12 +5,13 @@ import {
ActionType, ActionType,
Selected Selected
} from "../providers/transactions"; } from "../providers/transactions";
import { displayAddress } from "../utils"; import { displayAddress, decodeCreate, decodeTransfer } from "../utils/tx";
import { useBlocks } from "../providers/blocks"; import { useBlocks } from "../providers/blocks";
import { import {
LAMPORTS_PER_SOL, LAMPORTS_PER_SOL,
TransferParams, TransferParams,
CreateAccountParams CreateAccountParams,
TransactionInstruction
} from "@solana/web3.js"; } from "@solana/web3.js";
function TransactionModal() { function TransactionModal() {
@ -70,31 +71,27 @@ function TransactionDetails({ selected }: { selected: Selected }) {
); );
} }
const details = block.transactions[selected.signature]; const transaction = block.transactions[selected.signature];
if (!details) return renderError("Transaction not found"); if (!transaction) return renderError("Transaction not found");
const { transfers, creates } = details; if (transaction.instructions.length === 0)
if (transfers.length === 0 && creates.length === 0) return renderError("No instructions found");
return renderError(
"Details for this transaction's instructions are not yet supported" const instructionDetails = transaction.instructions.map((ix, index) => {
); const transfer = decodeTransfer(ix);
if (transfer) return <TransferDetails transfer={transfer} index={index} />;
const create = decodeCreate(ix);
if (create) return <CreateDetails create={create} index={index} />;
return <InstructionDetails ix={ix} index={index} />;
});
let i = 0;
return ( return (
<> <>
{details.transfers.map(transfer => { {instructionDetails.map((details, i) => {
return ( return (
<div key={++i}> <div key={++i}>
{i > 1 ? <hr className="mb-4"></hr> : null} {i > 1 ? <hr className="mt-0 mb-0"></hr> : null}
<TransferDetails transfer={transfer} /> {details}
</div>
);
})}
{details.creates.map(create => {
return (
<div key={++i}>
{i > 1 ? <hr className="mb-4"></hr> : null}
<CreateDetails create={create} />
</div> </div>
); );
})} })}
@ -102,9 +99,16 @@ function TransactionDetails({ selected }: { selected: Selected }) {
); );
} }
function TransferDetails({ transfer }: { transfer: TransferParams }) { function TransferDetails({
transfer,
index
}: {
transfer: TransferParams;
index: number;
}) {
return ( return (
<div className="card-body"> <div className="card-body">
<h4 className="ix-pill">{`Instruction #${index + 1} (Transfer)`}</h4>
<div className="list-group list-group-flush my-n3"> <div className="list-group list-group-flush my-n3">
<ListGroupItem label="From"> <ListGroupItem label="From">
<code>{transfer.fromPubkey.toBase58()}</code> <code>{transfer.fromPubkey.toBase58()}</code>
@ -120,9 +124,17 @@ function TransferDetails({ transfer }: { transfer: TransferParams }) {
); );
} }
function CreateDetails({ create }: { create: CreateAccountParams }) { function CreateDetails({
create,
index
}: {
create: CreateAccountParams;
index: number;
}) {
return ( return (
<div className="card-body"> <div className="card-body">
<h4 className="ix-pill">{`Instruction #${index +
1} (Create Account)`}</h4>
<div className="list-group list-group-flush my-n3"> <div className="list-group list-group-flush my-n3">
<ListGroupItem label="From"> <ListGroupItem label="From">
<code>{create.fromPubkey.toBase58()}</code> <code>{create.fromPubkey.toBase58()}</code>
@ -142,6 +154,31 @@ function CreateDetails({ create }: { create: CreateAccountParams }) {
); );
} }
function InstructionDetails({
ix,
index
}: {
ix: TransactionInstruction;
index: number;
}) {
return (
<div className="card-body">
<h4 className="ix-pill">{`Instruction #${index + 1}`}</h4>
<div className="list-group list-group-flush my-n3">
{ix.keys.map(({ pubkey }, keyIndex) => (
<ListGroupItem key={keyIndex} label={`Address #${keyIndex + 1}`}>
<code>{pubkey.toBase58()}</code>
</ListGroupItem>
))}
<ListGroupItem label="Data (Bytes)">{ix.data.length}</ListGroupItem>
<ListGroupItem label="Program">
<code>{displayAddress(ix.programId)}</code>
</ListGroupItem>
</div>
</div>
);
}
function ListGroupItem({ function ListGroupItem({
label, label,
children children
@ -150,7 +187,7 @@ function ListGroupItem({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<div className="list-group-item"> <div className="list-group-item ix-item">
<div className="row align-items-center"> <div className="row align-items-center">
<div className="col"> <div className="col">
<h5 className="mb-0">{label}</h5> <h5 className="mb-0">{label}</h5>

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { PublicKey, Connection } from "@solana/web3.js"; import { PublicKey, Connection } from "@solana/web3.js";
import { findGetParameter, findPathSegment } from "../utils"; import { findGetParameter, findPathSegment } from "../utils/url";
import { useCluster, ClusterStatus } from "./cluster"; import { useCluster, ClusterStatus } from "./cluster";
export enum Status { export enum Status {

View File

@ -1,13 +1,6 @@
import React from "react"; import React from "react";
import bs58 from "bs58"; import bs58 from "bs58";
import { import { Connection, Transaction } from "@solana/web3.js";
Connection,
Transaction,
TransferParams,
SystemProgram,
SystemInstruction,
CreateAccountParams
} from "@solana/web3.js";
import { useCluster, ClusterStatus } from "./cluster"; import { useCluster, ClusterStatus } from "./cluster";
import { useTransactions } from "./transactions"; import { useTransactions } from "./transactions";
@ -17,13 +10,7 @@ export enum Status {
Success Success
} }
export interface TransactionDetails { type Transactions = { [signature: string]: Transaction };
transaction: Transaction;
transfers: Array<TransferParams>;
creates: Array<CreateAccountParams>;
}
type Transactions = { [signature: string]: TransactionDetails };
export interface Block { export interface Block {
status: Status; status: Status;
transactions?: Transactions; 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) { async function fetchBlock(dispatch: Dispatch, slot: number, url: string) {
dispatch({ dispatch({
type: ActionType.Update, type: ActionType.Update,
@ -201,11 +156,7 @@ async function fetchBlock(dispatch: Dispatch, slot: number, url: string) {
const signature = transaction.signature; const signature = transaction.signature;
if (signature) { if (signature) {
const sig = bs58.encode(signature); const sig = bs58.encode(signature);
transactions[sig] = { transactions[sig] = transaction;
transaction,
transfers: decodeTransfers(transaction),
creates: decodeCreates(transaction)
};
} }
}); });
status = Status.Success; status = Status.Success;

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { clusterApiUrl, Connection } from "@solana/web3.js"; import { clusterApiUrl, Connection } from "@solana/web3.js";
import { findGetParameter } from "../utils"; import { findGetParameter } from "../utils/url";
export enum ClusterStatus { export enum ClusterStatus {
Connected, Connected,

View File

@ -5,7 +5,7 @@ import {
SystemProgram, SystemProgram,
Account Account
} from "@solana/web3.js"; } from "@solana/web3.js";
import { findGetParameter, findPathSegment } from "../utils"; import { findGetParameter, findPathSegment } from "../utils/url";
import { useCluster, ClusterStatus } from "../providers/cluster"; import { useCluster, ClusterStatus } from "../providers/cluster";
import base58 from "bs58"; import base58 from "bs58";
import { import {

View File

@ -40,4 +40,17 @@ code {
input.text-signature, input.text-address { input.text-signature, input.text-address {
padding: 0 0.75rem 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;
} }

View File

@ -0,0 +1,3 @@
export function assertUnreachable(x: never): never {
throw new Error("Unreachable!");
}

View File

@ -4,49 +4,16 @@ import {
StakeProgram, StakeProgram,
VOTE_PROGRAM_ID, VOTE_PROGRAM_ID,
BpfLoader, BpfLoader,
TransferParams,
SystemInstruction,
CreateAccountParams,
TransactionInstruction,
SYSVAR_CLOCK_PUBKEY, SYSVAR_CLOCK_PUBKEY,
SYSVAR_RENT_PUBKEY, SYSVAR_RENT_PUBKEY,
SYSVAR_REWARDS_PUBKEY, SYSVAR_REWARDS_PUBKEY,
SYSVAR_STAKE_HISTORY_PUBKEY SYSVAR_STAKE_HISTORY_PUBKEY
} from "@solana/web3.js"; } 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 = { const PROGRAM_IDS = {
Budget1111111111111111111111111111111111111: "Budget", Budget1111111111111111111111111111111111111: "Budget",
Config1111111111111111111111111111111111111: "Config", Config1111111111111111111111111111111111111: "Config",
@ -86,3 +53,31 @@ export function displayAddress(pubkey: PublicKey): string {
address 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;
}
}

32
explorer/src/utils/url.ts Normal file
View File

@ -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;
}