Show details for all transaction instructions
This commit is contained in:
parent
1c1c628b19
commit
8de34e1f42
|
@ -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";
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export function assertUnreachable(x: never): never {
|
||||
throw new Error("Unreachable!");
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue