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,
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";

View File

@ -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 <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 (
<>
{details.transfers.map(transfer => {
{instructionDetails.map((details, i) => {
return (
<div key={++i}>
{i > 1 ? <hr className="mb-4"></hr> : null}
<TransferDetails transfer={transfer} />
</div>
);
})}
{details.creates.map(create => {
return (
<div key={++i}>
{i > 1 ? <hr className="mb-4"></hr> : null}
<CreateDetails create={create} />
{i > 1 ? <hr className="mt-0 mb-0"></hr> : null}
{details}
</div>
);
})}
@ -102,9 +99,16 @@ function TransactionDetails({ selected }: { selected: Selected }) {
);
}
function TransferDetails({ transfer }: { transfer: TransferParams }) {
function TransferDetails({
transfer,
index
}: {
transfer: TransferParams;
index: number;
}) {
return (
<div className="card-body">
<h4 className="ix-pill">{`Instruction #${index + 1} (Transfer)`}</h4>
<div className="list-group list-group-flush my-n3">
<ListGroupItem label="From">
<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 (
<div className="card-body">
<h4 className="ix-pill">{`Instruction #${index +
1} (Create Account)`}</h4>
<div className="list-group list-group-flush my-n3">
<ListGroupItem label="From">
<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({
label,
children
@ -150,7 +187,7 @@ function ListGroupItem({
children: React.ReactNode;
}) {
return (
<div className="list-group-item">
<div className="list-group-item ix-item">
<div className="row align-items-center">
<div className="col">
<h5 className="mb-0">{label}</h5>

View File

@ -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 {

View File

@ -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<TransferParams>;
creates: Array<CreateAccountParams>;
}
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;

View File

@ -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,

View File

@ -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 {

View File

@ -41,3 +41,16 @@ 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;
}

View File

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

View File

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

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