Parse token instructions in the explorer (#11443)

This commit is contained in:
Justin Starry
2020-08-07 18:09:17 +08:00
committed by GitHub
parent bf01dca13f
commit e6aa3a4e07
16 changed files with 619 additions and 249 deletions

View File

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

View File

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

View File

@ -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<string, TokenAccountInfo>();
const mappedTokens = new Map<string, TokenAccountData>();
for (const token of tokens) {
const mintAddress = token.mint.toBase58();
const tokenInfo = mappedTokens.get(mintAddress);

View File

@ -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 (
<div className="card">
@ -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 <ErrorCard retry={refreshDetails} text="Metadata Missing" />;
}
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 && (
<span className="badge badge-soft-info mr-1">Fee Payer</span>
)}
{!message.isAccountWritable(index) && (
{!account.writable && (
<span className="badge badge-soft-info mr-1">Readonly</span>
)}
{index < message.header.numRequiredSignatures && (
{account.signer && (
<span className="badge badge-soft-info mr-1">Signer</span>
)}
{message.instructions.find((ix) => ix.programIdIndex === index) && (
{message.instructions.find((ix) => ix.programId.equals(pubkey)) && (
<span className="badge badge-soft-info mr-1">Program</span>
)}
</td>
@ -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 <ErrorCard retry={refreshDetails} text="No instructions found" />;
}
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 (
<TokenDetailsCard
key={index}
tx={transaction}
ix={next}
result={result}
index={index}
/>
);
}
if (SystemProgram.programId.equals(ix.programId)) {
return <SystemDetailsCard key={index} {...props} />;
} else if (StakeProgram.programId.equals(ix.programId)) {
return <StakeDetailsCard key={index} {...props} />;
} else {
return <UnknownDetailsCard key={index} {...props} />;
const props = { ix: next, result, index };
return <UnknownDetailsCard key={index} {...props} />;
}
const ix = intoTransactionInstruction(transaction, index);
if (!ix) {
return (
<ErrorCard
key={index}
text="Could not display this instruction, please report"
/>
);
}
const props = { ix, result, index };
if (SystemProgram.programId.equals(ix.programId)) {
return <SystemDetailsCard key={index} {...props} />;
} else if (StakeProgram.programId.equals(ix.programId)) {
return <StakeDetailsCard key={index} {...props} />;
} else {
return <UnknownDetailsCard key={index} {...props} />;
}
}
});
);
return (
<>

View File

@ -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({
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<tbody className="list">
{showRaw ? <RawDetails ix={ix} /> : children}
{showRaw ? (
"parsed" in ix ? (
<RawParsedDetails ix={ix} />
) : (
<RawDetails ix={ix} />
)
) : (
children
)}
</tbody>
</table>
</div>

View File

@ -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 (
<>
<tr>
<td>Program</td>
<td className="text-lg-right">
<Address pubkey={ix.programId} alignRight link />
</td>
</tr>
<tr>
<td>Instruction Data (JSON)</td>
<td className="text-lg-right">
<pre className="d-inline-block text-left">
{JSON.stringify(ix.parsed, null, 2)}
</pre>
</td>
</tr>
</>
);
}

View File

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

View File

@ -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 <TokenInstruction title={title} info={coerced} {...props} />;
} catch (err) {
return <UnknownDetailsCard {...props} />;
}
}
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 = <Address pubkey={value} alignRight link />;
} else {
tag = <>{value}</>;
}
key = key.charAt(0).toUpperCase() + key.slice(1);
attributes.push(
<tr key={key}>
<td>{key}</td>
<td className="text-lg-right">{tag}</td>
</tr>
);
}
return (
<InstructionCard
ix={props.ix}
index={props.index}
result={props.result}
title={props.title}
>
{attributes}
</InstructionCard>
);
}

View File

@ -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<typeof TokenInstructionType>;
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<typeof ParsedInstructionInfo>;
export const ParsedInstructionInfo = object({
type: TokenInstructionType,
info: any(),
});

View File

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

View File

@ -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(),
});

View File

@ -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",
]
)
),
},
};

View File

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

View File

@ -3,7 +3,7 @@
// Use this to write your custom SCSS
//
code {
code, pre {
background-color: $black-dark;
color: $white;
}

View File

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

View File

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