Explorer: Support displaying and inspecting versioned transactions (#27825)

* Explorer: Bump @solana/web3.js to v1.63.1

* Explorer: Support displaying and inspecting versioned transactions
This commit is contained in:
Justin Starry 2022-09-24 15:10:14 +08:00 committed by GitHub
parent 7810387b00
commit b93392cfca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 545 additions and 141 deletions

View File

@ -19,7 +19,7 @@
"@sentry/react": "^7.6.0",
"@solana/buffer-layout": "^3.0.0",
"@solana/spl-token-registry": "^0.2.3736",
"@solana/web3.js": "^1.62.0",
"@solana/web3.js": "^1.63.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.2.3",
@ -5152,9 +5152,9 @@
}
},
"node_modules/@solana/web3.js": {
"version": "1.62.0",
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.62.0.tgz",
"integrity": "sha512-rHnqJR5ECooUp8egurP9Qi1SKI1Q3pbF2ZkaHbEmFsSjBsyEe+Qqxa5h+7ueylqApYyk0zawnxz83y4kdrlNIA==",
"version": "1.63.1",
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.63.1.tgz",
"integrity": "sha512-wgEdGVK5FTS2zENxbcGSvKpGZ0jDS6BUdGu8Gn6ns0CzgJkK83u4ip3THSnBPEQ5i/jrqukg998BwV1H67+qiQ==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@noble/ed25519": "^1.7.0",
@ -31100,9 +31100,9 @@
}
},
"@solana/web3.js": {
"version": "1.62.0",
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.62.0.tgz",
"integrity": "sha512-rHnqJR5ECooUp8egurP9Qi1SKI1Q3pbF2ZkaHbEmFsSjBsyEe+Qqxa5h+7ueylqApYyk0zawnxz83y4kdrlNIA==",
"version": "1.63.1",
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.63.1.tgz",
"integrity": "sha512-wgEdGVK5FTS2zENxbcGSvKpGZ0jDS6BUdGu8Gn6ns0CzgJkK83u4ip3THSnBPEQ5i/jrqukg998BwV1H67+qiQ==",
"requires": {
"@babel/runtime": "^7.12.5",
"@noble/ed25519": "^1.7.0",

View File

@ -14,7 +14,7 @@
"@sentry/react": "^7.6.0",
"@solana/buffer-layout": "^3.0.0",
"@solana/spl-token-registry": "^0.2.3736",
"@solana/web3.js": "^1.62.0",
"@solana/web3.js": "^1.63.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.2.3",

View File

@ -1,4 +1,4 @@
import { Message, ParsedMessage } from "@solana/web3.js";
import { ParsedMessage, PublicKey, VersionedMessage } from "@solana/web3.js";
import { Cluster } from "providers/cluster";
import { TableCardBody } from "components/common/TableCardBody";
import { InstructionLogs } from "utils/program-logs";
@ -21,27 +21,24 @@ export function ProgramLogsCardBody({
cluster,
url,
}: {
message: Message | ParsedMessage;
message: VersionedMessage | ParsedMessage;
logs: InstructionLogs[];
cluster: Cluster;
url: string;
}) {
let logIndex = 0;
let instructionProgramIds: PublicKey[];
if ("compiledInstructions" in message) {
instructionProgramIds = message.compiledInstructions.map((ix) => {
return message.staticAccountKeys[ix.programIdIndex];
});
} else {
instructionProgramIds = message.instructions.map((ix) => ix.programId);
}
return (
<TableCardBody>
{message.instructions.map((ix, index) => {
let programId;
if ("programIdIndex" in ix) {
const programAccount = message.accountKeys[ix.programIdIndex];
if ("pubkey" in programAccount) {
programId = programAccount.pubkey;
} else {
programId = programAccount;
}
} else {
programId = ix.programId;
}
{instructionProgramIds.map((programId, index) => {
const programAddress = programId.toBase58();
let programLogs: InstructionLogs | undefined = logs[logIndex];
if (programLogs?.invokedProgram === programAddress) {

View File

@ -1,5 +1,5 @@
import React from "react";
import { BlockResponse, PublicKey } from "@solana/web3.js";
import { PublicKey, VersionedBlockResponse } from "@solana/web3.js";
import { Address } from "components/common/Address";
import { Link } from "react-router-dom";
import { clusterPath } from "utils/url";
@ -15,7 +15,7 @@ export function BlockAccountsCard({
block,
blockSlot,
}: {
block: BlockResponse;
block: VersionedBlockResponse;
blockSlot: number;
}) {
const [numDisplayed, setNumDisplayed] = React.useState(10);
@ -26,9 +26,12 @@ export function BlockAccountsCard({
block.transactions.forEach((tx) => {
const message = tx.transaction.message;
const txSet = new Map<string, boolean>();
message.instructions.forEach((ix) => {
ix.accounts.forEach((index) => {
const address = message.accountKeys[index].toBase58();
const accountKeys = message.getAccountKeys({
accountKeysFromLookups: tx.meta?.loadedAddresses,
});
message.compiledInstructions.forEach((ix) => {
ix.accountKeyIndexes.forEach((index) => {
const address = accountKeys.get(index)!.toBase58();
txSet.set(address, message.isAccountWritable(index));
});
});

View File

@ -2,11 +2,11 @@ import React from "react";
import { Link, useHistory, useLocation } from "react-router-dom";
import { Location } from "history";
import {
BlockResponse,
ConfirmedTransactionMeta,
TransactionSignature,
PublicKey,
VOTE_PROGRAM_ID,
VersionedBlockResponse,
} from "@solana/web3.js";
import { ErrorCard } from "components/common/ErrorCard";
import { Signature } from "components/common/Signature";
@ -51,7 +51,7 @@ type TransactionWithInvocations = {
logTruncated: boolean;
};
export function BlockHistoryCard({ block }: { block: BlockResponse }) {
export function BlockHistoryCard({ block }: { block: VersionedBlockResponse }) {
const [numDisplayed, setNumDisplayed] = React.useState(PAGE_SIZE);
const [showDropdown, setDropdown] = React.useState(false);
const query = useQuery();
@ -72,7 +72,7 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) {
signature = tx.transaction.signatures[0];
}
let programIndexes = tx.transaction.message.instructions
let programIndexes = tx.transaction.message.compiledInstructions
.map((ix) => ix.programIdIndex)
.concat(
tx.meta?.innerInstructions?.flatMap((ix) => {
@ -87,8 +87,11 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) {
});
const invocations = new Map<string, number>();
const accountKeys = tx.transaction.message.getAccountKeys({
accountKeysFromLookups: tx.meta?.loadedAddresses,
});
for (const [i, count] of indexMap.entries()) {
const programId = tx.transaction.message.accountKeys[i].toBase58();
const programId = accountKeys.get(i)!.toBase58();
invocations.set(programId, count);
const programTransactionCount = invokedPrograms.get(programId) || 0;
invokedPrograms.set(programId, programTransactionCount + 1);
@ -143,8 +146,15 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) {
if (accountFilter === null) {
return true;
}
const tx = block.transactions[index].transaction;
return tx.message.accountKeys.find((key) => key.equals(accountFilter));
const tx = block.transactions[index];
const accountKeys = tx.transaction.message.getAccountKeys({
accountKeysFromLookups: tx.meta?.loadedAddresses,
});
return accountKeys
.keySegments()
.flat()
.find((key) => key.equals(accountFilter));
});
const showComputeUnits = filteredTxs.every(

View File

@ -7,7 +7,7 @@ import { Slot } from "components/common/Slot";
import { ClusterStatus, useCluster } from "providers/cluster";
import { BlockHistoryCard } from "./BlockHistoryCard";
import { BlockRewardsCard } from "./BlockRewardsCard";
import { BlockResponse } from "@solana/web3.js";
import { VersionedBlockResponse } from "@solana/web3.js";
import { NavLink } from "react-router-dom";
import { clusterPath } from "utils/url";
import { BlockProgramsCard } from "./BlockProgramsCard";
@ -211,7 +211,7 @@ function MoreSection({
tab,
}: {
slot: number;
block: BlockResponse;
block: VersionedBlockResponse;
tab?: string;
}) {
return (

View File

@ -1,9 +1,13 @@
import React from "react";
import { BlockResponse, PublicKey } from "@solana/web3.js";
import { PublicKey, VersionedBlockResponse } from "@solana/web3.js";
import { Address } from "components/common/Address";
import { TableCardBody } from "components/common/TableCardBody";
export function BlockProgramsCard({ block }: { block: BlockResponse }) {
export function BlockProgramsCard({
block,
}: {
block: VersionedBlockResponse;
}) {
const totalTransactions = block.transactions.length;
const txSuccesses = new Map<string, number>();
const txFrequency = new Map<string, number>();
@ -12,18 +16,23 @@ export function BlockProgramsCard({ block }: { block: BlockResponse }) {
let totalInstructions = 0;
block.transactions.forEach((tx) => {
const message = tx.transaction.message;
totalInstructions += message.instructions.length;
totalInstructions += message.compiledInstructions.length;
const programUsed = new Set<string>();
const accountKeys = tx.transaction.message.getAccountKeys({
accountKeysFromLookups: tx.meta?.loadedAddresses,
});
const trackProgram = (index: number) => {
if (index >= message.accountKeys.length) return;
const programId = message.accountKeys[index];
if (index >= accountKeys.length) return;
const programId = accountKeys.get(index)!;
const programAddress = programId.toBase58();
programUsed.add(programAddress);
const frequency = ixFrequency.get(programAddress);
ixFrequency.set(programAddress, frequency ? frequency + 1 : 1);
};
message.instructions.forEach((ix) => trackProgram(ix.programIdIndex));
message.compiledInstructions.forEach((ix) =>
trackProgram(ix.programIdIndex)
);
tx.meta?.innerInstructions?.forEach((inner) => {
totalInstructions += inner.instructions.length;
inner.instructions.forEach((innerIx) =>

View File

@ -1,11 +1,11 @@
import React from "react";
import { SolBalance } from "utils";
import { BlockResponse, PublicKey } from "@solana/web3.js";
import { PublicKey, VersionedBlockResponse } from "@solana/web3.js";
import { Address } from "components/common/Address";
const PAGE_SIZE = 10;
export function BlockRewardsCard({ block }: { block: BlockResponse }) {
export function BlockRewardsCard({ block }: { block: VersionedBlockResponse }) {
const [rewardsDisplayed, setRewardsDisplayed] = React.useState(PAGE_SIZE);
if (!block.rewards || block.rewards.length < 1) {

View File

@ -197,6 +197,7 @@ function StatusCard({
const fee = transactionWithMeta?.meta?.fee;
const transaction = transactionWithMeta?.transaction;
const blockhash = transaction?.message.recentBlockhash;
const version = transactionWithMeta?.version;
const isNonce = (() => {
if (!transaction || transaction.message.instructions.length < 1) {
return false;
@ -330,6 +331,13 @@ function StatusCard({
</td>
</tr>
)}
{version !== undefined && (
<tr>
<td>Transaction Version</td>
<td className="text-lg-end text-uppercase">{version}</td>
</tr>
)}
</TableCardBody>
</div>
);
@ -414,14 +422,19 @@ function AccountsCard({ signature }: SignatureProps) {
{index === 0 && (
<span className="badge bg-info-soft me-1">Fee Payer</span>
)}
{account.writable && (
<span className="badge bg-info-soft me-1">Writable</span>
)}
{account.signer && (
<span className="badge bg-info-soft me-1">Signer</span>
)}
{account.writable && (
<span className="badge bg-danger-soft me-1">Writable</span>
)}
{message.instructions.find((ix) => ix.programId.equals(pubkey)) && (
<span className="badge bg-info-soft me-1">Program</span>
<span className="badge bg-warning-soft me-1">Program</span>
)}
{account.source === "lookupTable" && (
<span className="badge bg-gray-soft me-1">
Address Table Lookup
</span>
)}
</td>
</tr>

View File

@ -1,10 +1,13 @@
import React from "react";
import { Message, PublicKey } from "@solana/web3.js";
import { PublicKey, VersionedMessage } from "@solana/web3.js";
import { TableCardBody } from "components/common/TableCardBody";
import { AddressWithContext } from "./AddressWithContext";
import {
AddressFromLookupTableWithContext,
AddressWithContext,
} from "./AddressWithContext";
import { ErrorCard } from "components/common/ErrorCard";
export function AccountsCard({ message }: { message: Message }) {
export function AccountsCard({ message }: { message: VersionedMessage }) {
const [expanded, setExpanded] = React.useState(true);
const { validMessage, error } = React.useMemo(() => {
@ -16,9 +19,11 @@ export function AccountsCard({ message }: { message: Message }) {
if (numReadonlySignedAccounts >= numRequiredSignatures) {
return { validMessage: undefined, error: "Invalid header" };
} else if (numReadonlyUnsignedAccounts >= message.accountKeys.length) {
} else if (
numReadonlyUnsignedAccounts >= message.staticAccountKeys.length
) {
return { validMessage: undefined, error: "Invalid header" };
} else if (message.accountKeys.length === 0) {
} else if (message.staticAccountKeys.length === 0) {
return { validMessage: undefined, error: "Message has no accounts" };
}
@ -28,39 +33,86 @@ export function AccountsCard({ message }: { message: Message }) {
};
}, [message]);
const accountRows = React.useMemo(() => {
const { accountRows, numAccounts } = React.useMemo(() => {
const message = validMessage;
if (!message) return;
return message.accountKeys.map((publicKey, accountIndex) => {
const {
numRequiredSignatures,
numReadonlySignedAccounts,
numReadonlyUnsignedAccounts,
} = message.header;
if (!message) return { accountRows: undefined, numAccounts: 0 };
const staticAccountRows = message.staticAccountKeys.map(
(publicKey, accountIndex) => {
const {
numRequiredSignatures,
numReadonlySignedAccounts,
numReadonlyUnsignedAccounts,
} = message.header;
let readOnly = false;
let signer = false;
if (accountIndex < numRequiredSignatures) {
signer = true;
if (accountIndex >= numRequiredSignatures - numReadonlySignedAccounts) {
let readOnly = false;
let signer = false;
if (accountIndex < numRequiredSignatures) {
signer = true;
if (
accountIndex >=
numRequiredSignatures - numReadonlySignedAccounts
) {
readOnly = true;
}
} else if (
accountIndex >=
message.staticAccountKeys.length - numReadonlyUnsignedAccounts
) {
readOnly = true;
}
} else if (
accountIndex >=
message.accountKeys.length - numReadonlyUnsignedAccounts
) {
readOnly = true;
const props = {
accountIndex,
publicKey,
signer,
readOnly,
};
return <AccountRow key={accountIndex} {...props} />;
}
);
const props = {
accountIndex,
publicKey,
signer,
readOnly,
};
let accountIndex = message.staticAccountKeys.length;
const writableLookupTableRows = message.addressTableLookups.flatMap(
(lookup) => {
return lookup.writableIndexes.map((lookupTableIndex) => {
const props = {
accountIndex,
lookupTableKey: lookup.accountKey,
lookupTableIndex,
readOnly: false,
};
return <AccountRow key={accountIndex} {...props} />;
});
accountIndex += 1;
return <AccountFromLookupTableRow key={accountIndex} {...props} />;
});
}
);
const readonlyLookupTableRows = message.addressTableLookups.flatMap(
(lookup) => {
return lookup.readonlyIndexes.map((lookupTableIndex) => {
const props = {
accountIndex,
lookupTableKey: lookup.accountKey,
lookupTableIndex,
readOnly: true,
};
accountIndex += 1;
return <AccountFromLookupTableRow key={accountIndex} {...props} />;
});
}
);
return {
accountRows: [
...staticAccountRows,
...writableLookupTableRows,
...readonlyLookupTableRows,
],
numAccounts: accountIndex,
};
}, [validMessage]);
if (error) {
@ -70,9 +122,7 @@ export function AccountsCard({ message }: { message: Message }) {
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title">
{`Account List (${message.accountKeys.length})`}
</h3>
<h3 className="card-header-title">{`Account List (${numAccounts})`}</h3>
<button
className={`btn btn-sm d-flex ${
expanded ? "btn-black active" : "btn-white"
@ -87,6 +137,40 @@ export function AccountsCard({ message }: { message: Message }) {
);
}
function AccountFromLookupTableRow({
accountIndex,
lookupTableKey,
lookupTableIndex,
readOnly,
}: {
accountIndex: number;
lookupTableKey: PublicKey;
lookupTableIndex: number;
readOnly: boolean;
}) {
return (
<tr>
<td>
<div className="d-flex align-items-start flex-column">
Account #{accountIndex + 1}
<span className="mt-1">
{!readOnly && (
<span className="badge bg-danger-soft me-1">Writable</span>
)}
<span className="badge bg-gray-soft">Address Table Lookup</span>
</span>
</div>
</td>
<td className="text-lg-end">
<AddressFromLookupTableWithContext
lookupTableKey={lookupTableKey}
lookupTableIndex={lookupTableIndex}
/>
</td>
</tr>
);
}
function AccountRow({
accountIndex,
publicKey,

View File

@ -0,0 +1,122 @@
import React from "react";
import { PublicKey, VersionedMessage } from "@solana/web3.js";
import { Address } from "components/common/Address";
import { useAddressLookupTable, useFetchAccountInfo } from "providers/accounts";
export function AddressTableLookupsCard({
message,
}: {
message: VersionedMessage;
}) {
const [expanded, setExpanded] = React.useState(true);
const lookupRows = React.useMemo(() => {
let key = 0;
return message.addressTableLookups.flatMap((lookup) => {
const indexes = [
...lookup.writableIndexes.map((index) => ({ index, readOnly: false })),
...lookup.readonlyIndexes.map((index) => ({ index, readOnly: true })),
];
indexes.sort((a, b) => (a.index < b.index ? -1 : 1));
return indexes.map(({ index, readOnly }) => {
const props = {
lookupTableKey: lookup.accountKey,
lookupTableIndex: index,
readOnly,
};
return <LookupRow key={key++} {...props} />;
});
});
}, [message]);
if (message.version === "legacy") return null;
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title">Address Table Lookup(s)</h3>
<button
className={`btn btn-sm d-flex ${
expanded ? "btn-black active" : "btn-white"
}`}
onClick={() => setExpanded((e) => !e)}
>
{expanded ? "Collapse" : "Expand"}
</button>
</div>
{expanded && (
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="text-muted">Address Lookup Table Address</th>
<th className="text-muted">Table Index</th>
<th className="text-muted">Resolved Address</th>
<th className="text-muted">Details</th>
</tr>
</thead>
{lookupRows.length > 0 ? (
<tbody className="list">{lookupRows}</tbody>
) : (
<div className="card-footer">
<div className="text-muted text-center">No entries found</div>
</div>
)}
</table>
</div>
)}
</div>
);
}
function LookupRow({
lookupTableKey,
lookupTableIndex,
readOnly,
}: {
lookupTableKey: PublicKey;
lookupTableIndex: number;
readOnly: boolean;
}) {
const lookupTable = useAddressLookupTable(lookupTableKey.toBase58());
const fetchAccountInfo = useFetchAccountInfo();
React.useEffect(() => {
if (!lookupTable) fetchAccountInfo(lookupTableKey);
}, [lookupTableKey, lookupTable, fetchAccountInfo]);
let resolvedKeyComponent;
if (!lookupTable) {
resolvedKeyComponent = (
<span className="text-muted">
<span className="spinner-grow spinner-grow-sm me-2"></span>
Loading
</span>
);
} else if (typeof lookupTable === "string") {
resolvedKeyComponent = (
<span className="text-muted">Invalid Lookup Table</span>
);
} else if (lookupTableIndex < lookupTable.state.addresses.length) {
const resolvedKey = lookupTable.state.addresses[lookupTableIndex];
resolvedKeyComponent = <Address pubkey={resolvedKey} link />;
} else {
resolvedKeyComponent = (
<span className="text-muted">Invalid Lookup Table Index</span>
);
}
return (
<tr>
<td className="text-lg-end">
<Address pubkey={lookupTableKey} link />
</td>
<td className="text-lg-end">{lookupTableIndex}</td>
<td className="text-lg-end">{resolvedKeyComponent}</td>
<td>
{!readOnly && <span className="badge bg-info-soft me-1">Writable</span>}
</td>
</tr>
);
}

View File

@ -4,6 +4,7 @@ import { Address } from "components/common/Address";
import {
Account,
useAccountInfo,
useAddressLookupTable,
useFetchAccountInfo,
} from "providers/accounts";
import { ClusterStatus, useCluster } from "providers/cluster";
@ -36,6 +37,43 @@ export const programValidator = (account: Account): string | undefined => {
return;
};
export function AddressFromLookupTableWithContext({
lookupTableKey,
lookupTableIndex,
}: {
lookupTableKey: PublicKey;
lookupTableIndex: number;
}) {
const lookupTable = useAddressLookupTable(lookupTableKey.toBase58());
const fetchAccountInfo = useFetchAccountInfo();
React.useEffect(() => {
if (!lookupTable) fetchAccountInfo(lookupTableKey);
}, [lookupTableKey, lookupTable, fetchAccountInfo]);
let pubkey;
if (!lookupTable) {
return (
<span className="text-muted">
<span className="spinner-grow spinner-grow-sm me-2"></span>
Loading
</span>
);
} else if (typeof lookupTable === "string") {
return <div>Invalid Lookup Table</div>;
} else if (lookupTableIndex < lookupTable.state.addresses.length) {
pubkey = lookupTable.state.addresses[lookupTableIndex];
} else {
return <div>Invalid Lookup Table Index</div>;
}
return (
<div className="d-flex align-items-end flex-column">
<Address pubkey={pubkey} link />
<AccountInfo pubkey={pubkey} />
</div>
);
}
export function AddressWithContext({
pubkey,
validator,

View File

@ -1,5 +1,5 @@
import React from "react";
import { Message, PACKET_DATA_SIZE } from "@solana/web3.js";
import { PACKET_DATA_SIZE, VersionedMessage } from "@solana/web3.js";
import { TableCardBody } from "components/common/TableCardBody";
import { SolBalance } from "utils";
@ -14,6 +14,7 @@ import { LoadingCard } from "components/common/LoadingCard";
import { ErrorCard } from "components/common/ErrorCard";
import { TransactionSignatures } from "./SignaturesCard";
import { AccountsCard } from "./AccountsCard";
import { AddressTableLookupsCard } from "./AddressTableLookupsCard";
import {
AddressWithContext,
createFeePayerValidator,
@ -25,7 +26,7 @@ import base58 from "bs58";
export type TransactionData = {
rawMessage: Uint8Array;
message: Message;
message: VersionedMessage;
signatures?: (string | null)[];
};
@ -117,7 +118,7 @@ function decodeUrlParams(
throw new Error("message buffer is too short");
}
const message = Message.from(buffer);
const message = VersionedMessage.deserialize(buffer);
const data = {
message,
rawMessage: buffer,
@ -280,6 +281,7 @@ function LoadedView({
/>
)}
<AccountsCard message={message} />
<AddressTableLookupsCard message={message} />
<InstructionsSection message={message} />
</>
);
@ -294,7 +296,7 @@ function OverviewCard({
raw,
onClear,
}: {
message: Message;
message: VersionedMessage;
raw: Uint8Array;
onClear: () => void;
}) {
@ -354,11 +356,11 @@ function OverviewCard({
</div>
</td>
<td className="text-end">
{message.accountKeys.length === 0 ? (
{message.staticAccountKeys.length === 0 ? (
"No Fee Payer"
) : (
<AddressWithContext
pubkey={message.accountKeys[0]}
pubkey={message.staticAccountKeys[0]}
validator={feePayerValidator}
/>
)}

View File

@ -1,18 +1,25 @@
import React from "react";
import bs58 from "bs58";
import { CompiledInstruction, Message } from "@solana/web3.js";
import { MessageCompiledInstruction, VersionedMessage } from "@solana/web3.js";
import { TableCardBody } from "components/common/TableCardBody";
import { AddressWithContext, programValidator } from "./AddressWithContext";
import {
AddressFromLookupTableWithContext,
AddressWithContext,
programValidator,
} from "./AddressWithContext";
import { useCluster } from "providers/cluster";
import { getProgramName } from "utils/tx";
import { HexData } from "components/common/HexData";
import getInstructionCardScrollAnchorId from "utils/get-instruction-card-scroll-anchor-id";
import { useScrollAnchor } from "providers/scroll-anchor";
export function InstructionsSection({ message }: { message: Message }) {
export function InstructionsSection({
message,
}: {
message: VersionedMessage;
}) {
return (
<>
{message.instructions.map((ix, index) => {
{message.compiledInstructions.map((ix, index) => {
return <InstructionCard key={index} {...{ message, ix, index }} />;
})}
</>
@ -24,17 +31,31 @@ function InstructionCard({
ix,
index,
}: {
message: Message;
ix: CompiledInstruction;
message: VersionedMessage;
ix: MessageCompiledInstruction;
index: number;
}) {
const [expanded, setExpanded] = React.useState(false);
const { cluster } = useCluster();
const programId = message.accountKeys[ix.programIdIndex];
const programId = message.staticAccountKeys[ix.programIdIndex];
const programName = getProgramName(programId.toBase58(), cluster);
const scrollAnchorRef = useScrollAnchor(
getInstructionCardScrollAnchorId([index + 1])
);
const lookupsForAccountKeyIndex = [
...message.addressTableLookups.flatMap((lookup) =>
lookup.writableIndexes.map((index) => ({
lookupTableKey: lookup.accountKey,
lookupTableIndex: index,
}))
),
...message.addressTableLookups.flatMap((lookup) =>
lookup.readonlyIndexes.map((index) => ({
lookupTableKey: lookup.accountKey,
lookupTableIndex: index,
}))
),
];
return (
<div className="card" key={index} ref={scrollAnchorRef}>
<div className={`card-header${!expanded ? " border-bottom-none" : ""}`}>
@ -58,12 +79,19 @@ function InstructionCard({
<td>Program</td>
<td className="text-lg-end">
<AddressWithContext
pubkey={message.accountKeys[ix.programIdIndex]}
pubkey={message.staticAccountKeys[ix.programIdIndex]}
validator={programValidator}
/>
</td>
</tr>
{ix.accounts.map((accountIndex, index) => {
{ix.accountKeyIndexes.map((accountIndex, index) => {
let lookup;
if (accountIndex >= message.staticAccountKeys.length) {
const lookupIndex =
accountIndex - message.staticAccountKeys.length;
lookup = lookupsForAccountKeyIndex[lookupIndex];
}
return (
<tr key={index}>
<td>
@ -82,9 +110,16 @@ function InstructionCard({
</div>
</td>
<td className="text-lg-end">
<AddressWithContext
pubkey={message.accountKeys[accountIndex]}
/>
{lookup === undefined ? (
<AddressWithContext
pubkey={message.staticAccountKeys[accountIndex]}
/>
) : (
<AddressFromLookupTableWithContext
lookupTableKey={lookup.lookupTableKey}
lookupTableIndex={lookup.lookupTableIndex}
/>
)}
</td>
</tr>
);
@ -94,7 +129,7 @@ function InstructionCard({
Instruction Data <span className="text-muted">(Hex)</span>
</td>
<td className="text-lg-end">
<HexData raw={bs58.decode(ix.data)} />
<HexData raw={Buffer.from(ix.data)} />
</td>
</tr>
</TableCardBody>

View File

@ -1,12 +1,12 @@
import React from "react";
import { Message } from "@solana/web3.js";
import { VersionedMessage } from "@solana/web3.js";
import type { TransactionData } from "./InspectorPage";
import { useQuery } from "utils/url";
import { useHistory, useLocation } from "react-router";
import base58 from "bs58";
function deserializeTransaction(bytes: Uint8Array): {
message: Message;
message: VersionedMessage;
signatures: string[];
} | null {
const SIGNATURE_LENGTH = 64;
@ -19,17 +19,12 @@ function deserializeTransaction(bytes: Uint8Array): {
bytes = bytes.slice(SIGNATURE_LENGTH);
signatures.push(base58.encode(rawSignature));
}
const requiredSignatures = bytes[0];
if (requiredSignatures !== signaturesLen) {
throw new Error("Signature length mismatch");
}
} catch (err) {
// Errors above indicate that the bytes do not encode a transaction.
return null;
}
const message = Message.from(bytes);
const message = VersionedMessage.deserialize(bytes);
return { message, signatures };
}
@ -103,7 +98,7 @@ export function RawInput({
signatures: tx.signatures,
});
} else {
const message = Message.from(buffer);
const message = VersionedMessage.deserialize(buffer);
setTransactionData({
rawMessage: buffer,
message,

View File

@ -1,7 +1,7 @@
import React from "react";
import bs58 from "bs58";
import * as nacl from "tweetnacl";
import { Message, PublicKey } from "@solana/web3.js";
import { PublicKey, VersionedMessage } from "@solana/web3.js";
import { Signature } from "components/common/Signature";
import { Address } from "components/common/Address";
@ -11,12 +11,12 @@ export function TransactionSignatures({
rawMessage,
}: {
signatures: (string | null)[];
message: Message;
message: VersionedMessage;
rawMessage: Uint8Array;
}) {
const signatureRows = React.useMemo(() => {
return signatures.map((signature, index) => {
const publicKey = message.accountKeys[index];
const publicKey = message.staticAccountKeys[index];
let verified;
if (signature) {

View File

@ -1,13 +1,14 @@
import React from "react";
import bs58 from "bs58";
import { Connection, Message, Transaction } from "@solana/web3.js";
import {
Connection,
VersionedMessage,
VersionedTransaction,
} from "@solana/web3.js";
import { useCluster } from "providers/cluster";
import { InstructionLogs, parseProgramLogs } from "utils/program-logs";
import { ProgramLogsCardBody } from "components/ProgramLogsCardBody";
const DEFAULT_SIGNATURE = bs58.encode(Buffer.alloc(64).fill(0));
export function SimulatorCard({ message }: { message: Message }) {
export function SimulatorCard({ message }: { message: VersionedMessage }) {
const { cluster, url } = useCluster();
const {
simulate,
@ -77,7 +78,7 @@ export function SimulatorCard({ message }: { message: Message }) {
);
}
function useSimulator(message: Message) {
function useSimulator(message: VersionedMessage) {
const { cluster, url } = useCluster();
const [simulating, setSimulating] = React.useState(false);
const [logs, setLogs] = React.useState<Array<InstructionLogs> | null>(null);
@ -97,15 +98,10 @@ function useSimulator(message: Message) {
const connection = new Connection(url, "confirmed");
(async () => {
try {
const tx = Transaction.populate(
message,
new Array(message.header.numRequiredSignatures).fill(
DEFAULT_SIGNATURE
)
);
// Simulate without signers to skip signer verification
const resp = await connection.simulateTransaction(tx);
const resp = await connection.simulateTransaction(
new VersionedTransaction(message)
);
if (resp.value.logs === null) {
throw new Error("Expected to receive logs from simulation");
}

View File

@ -109,7 +109,9 @@ async function fetchParsedTransactions(
0,
MAX_TRANSACTION_BATCH_SIZE
);
const fetched = await connection.getParsedTransactions(signatures);
const fetched = await connection.getParsedTransactions(signatures, {
maxSupportedTransactionVersion: 0,
});
fetched.forEach(
(
transactionWithMeta: ParsedTransactionWithMeta | null,

View File

@ -1,6 +1,12 @@
import React from "react";
import { pubkeyToString } from "utils";
import { PublicKey, Connection, StakeActivationData } from "@solana/web3.js";
import {
PublicKey,
Connection,
StakeActivationData,
AddressLookupTableAccount,
AddressLookupTableProgram,
} from "@solana/web3.js";
import { useCluster, Cluster } from "../cluster";
import { HistoryProvider } from "./history";
import { TokensProvider } from "./tokens";
@ -19,6 +25,7 @@ import { VoteAccount } from "validators/accounts/vote";
import { NonceAccount } from "validators/accounts/nonce";
import { SysvarAccount } from "validators/accounts/sysvar";
import { ConfigAccount } from "validators/accounts/config";
import { ParsedAddressLookupTableAccount } from "validators/accounts/address-lookup-table";
import { FlaggedAccountsProvider } from "./flagged-accounts";
import {
ProgramDataAccount,
@ -76,6 +83,11 @@ export type ConfigProgramData = {
parsed: ConfigAccount;
};
export type AddressLookupTableProgramData = {
program: "address-lookup-table";
parsed: ParsedAddressLookupTableAccount;
};
export type ProgramData =
| UpgradeableLoaderAccountData
| StakeProgramData
@ -83,7 +95,8 @@ export type ProgramData =
| VoteProgramData
| NonceProgramData
| SysvarProgramData
| ConfigProgramData;
| ConfigProgramData
| AddressLookupTableProgramData;
export interface Details {
executable: boolean;
@ -238,6 +251,17 @@ async function fetchAccountInfo(
};
break;
case "address-lookup-table": {
const parsed = create(info, ParsedAddressLookupTableAccount);
data = {
program: result.data.program,
parsed,
};
break;
}
case "spl-token":
const parsed = create(info, TokenAccount);
let nftData;
@ -428,6 +452,40 @@ export function useTokenAccountInfo(
}
}
export function useAddressLookupTable(
address: string | undefined
): AddressLookupTableAccount | undefined | string {
const accountInfo = useAccountInfo(address);
if (address === undefined) return;
if (accountInfo?.data?.details === undefined) return;
if (accountInfo.data.lamports === 0) return "Lookup Table Not Found";
const { data, rawData } = accountInfo.data.details;
const key = new PublicKey(address);
if (data && data.program === "address-lookup-table") {
if (data.parsed.type === "lookupTable") {
return new AddressLookupTableAccount({
key,
state: data.parsed.info,
});
} else if (data.parsed.type === "uninitialized") {
return "Lookup Table Uninitialized";
}
} else if (
rawData &&
accountInfo.data.details.owner.equals(AddressLookupTableProgram.programId)
) {
try {
return new AddressLookupTableAccount({
key,
state: AddressLookupTableAccount.deserialize(rawData),
});
} catch {}
}
return "Invalid Lookup Table";
}
export function useFetchAccountInfo() {
const dispatch = React.useContext(DispatchContext);
if (!dispatch) {

View File

@ -1,7 +1,7 @@
import React from "react";
import * as Sentry from "@sentry/react";
import * as Cache from "providers/cache";
import { Connection, BlockResponse, PublicKey } from "@solana/web3.js";
import { Connection, PublicKey, VersionedBlockResponse } from "@solana/web3.js";
import { useCluster, Cluster } from "./cluster";
export enum FetchStatus {
@ -16,7 +16,7 @@ export enum ActionType {
}
type Block = {
block?: BlockResponse;
block?: VersionedBlockResponse;
blockLeader?: PublicKey;
childSlot?: number;
childLeader?: PublicKey;
@ -76,7 +76,9 @@ export async function fetchBlock(
try {
const connection = new Connection(url, "confirmed");
const block = await connection.getBlock(slot);
const block = await connection.getBlock(slot, {
maxSupportedTransactionVersion: 0,
});
if (block === null) {
data = {};
status = FetchStatus.Fetched;

View File

@ -57,7 +57,7 @@ async function fetchDetails(
try {
transactionWithMeta = await new Connection(url).getParsedTransaction(
signature,
"confirmed"
{ commitment: "confirmed", maxSupportedTransactionVersion: 0 }
);
fetchStatus = FetchStatus.Fetched;
} catch (error) {

View File

@ -2,8 +2,9 @@ import React from "react";
import {
Connection,
TransactionSignature,
Transaction,
Message,
TransactionMessage,
DecompileArgs,
VersionedMessage,
} from "@solana/web3.js";
import { useCluster, Cluster } from "../cluster";
import * as Cache from "providers/cache";
@ -12,8 +13,8 @@ import { reportError } from "utils/sentry";
export interface Details {
raw?: {
transaction: Transaction;
message: Message;
transaction: TransactionMessage;
message: VersionedMessage;
signatures: string[];
} | null;
}
@ -66,17 +67,22 @@ async function fetchRawTransaction(
) {
let fetchStatus;
try {
const response = await new Connection(url).getTransaction(signature);
const response = await new Connection(url).getTransaction(signature, {
maxSupportedTransactionVersion: 0,
});
fetchStatus = FetchStatus.Fetched;
let data: Details = { raw: null };
if (response !== null) {
const { message, signatures } = response.transaction;
const accountKeysFromLookups = response.meta?.loadedAddresses;
const decompileArgs: DecompileArgs | undefined =
accountKeysFromLookups && { accountKeysFromLookups };
data = {
raw: {
message,
signatures,
transaction: Transaction.populate(message, signatures),
transaction: TransactionMessage.decompile(message, decompileArgs),
},
};
}

View File

@ -0,0 +1,32 @@
/* eslint-disable @typescript-eslint/no-redeclare */
import { Infer, number, enums, type, array, optional } from "superstruct";
import { PublicKeyFromString } from "validators/pubkey";
import { BigIntFromString, NumberFromString } from "validators/number";
export type AddressLookupTableAccountType = Infer<
typeof AddressLookupTableAccountType
>;
export const AddressLookupTableAccountType = enums([
"uninitialized",
"lookupTable",
]);
export type AddressLookupTableAccountInfo = Infer<
typeof AddressLookupTableAccountInfo
>;
export const AddressLookupTableAccountInfo = type({
deactivationSlot: BigIntFromString,
lastExtendedSlot: NumberFromString,
lastExtendedSlotStartIndex: number(),
authority: optional(PublicKeyFromString),
addresses: array(PublicKeyFromString),
});
export type ParsedAddressLookupTableAccount = Infer<
typeof ParsedAddressLookupTableAccount
>;
export const ParsedAddressLookupTableAccount = type({
type: AddressLookupTableAccountType,
info: AddressLookupTableAccountInfo,
});