explorer: Add transaction inspector (#17769)
* Add SolBalance component helper * Add transaction inspector * Feedback
This commit is contained in:
parent
699ca6e71a
commit
1c88189a1c
|
@ -16431,6 +16431,7 @@
|
|||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-5.0.0.tgz",
|
||||
"integrity": "sha512-opNgmlu83ZCF792U281Ry7tak9IbVC+AKnXGovcQ8LG8wFaJv6cLnRlc6DIHlmNxWEexB5bZxi9SZ9JyUuOYjw==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"async-foreach": "^0.1.3",
|
||||
"chalk": "^1.1.1",
|
||||
|
|
|
@ -8,6 +8,7 @@ import { ClusterStatusBanner } from "components/ClusterStatusButton";
|
|||
import { SearchBar } from "components/SearchBar";
|
||||
|
||||
import { AccountDetailsPage } from "pages/AccountDetailsPage";
|
||||
import { TransactionInspectorPage } from "pages/inspector/InspectorPage";
|
||||
import { ClusterStatsPage } from "pages/ClusterStatsPage";
|
||||
import { SupplyPage } from "pages/SupplyPage";
|
||||
import { TransactionDetailsPage } from "pages/TransactionDetailsPage";
|
||||
|
@ -37,6 +38,13 @@ function App() {
|
|||
return <Redirect to={{ ...location, pathname }} />;
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={["/tx/inspector", "/tx/:signature/inspect"]}
|
||||
render={({ match }) => (
|
||||
<TransactionInspectorPage signature={match.params.signature} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={"/tx/:signature"}
|
||||
|
|
|
@ -39,6 +39,11 @@ export function Navbar() {
|
|||
Supply
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink className="nav-link" to={clusterPath("/tx/inspector")}>
|
||||
Inspector
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from "react";
|
|||
import { useSupply, useFetchSupply, Status } from "providers/supply";
|
||||
import { LoadingCard } from "./common/LoadingCard";
|
||||
import { ErrorCard } from "./common/ErrorCard";
|
||||
import { lamportsToSolString } from "utils";
|
||||
import { SolBalance } from "utils";
|
||||
import { TableCardBody } from "./common/TableCardBody";
|
||||
|
||||
export function SupplyCard() {
|
||||
|
@ -33,21 +33,27 @@ export function SupplyCard() {
|
|||
<tr>
|
||||
<td className="w-100">Total Supply (SOL)</td>
|
||||
<td className="text-lg-right">
|
||||
{lamportsToSolString(supply.total, 0)}
|
||||
<SolBalance lamports={supply.total} maximumFractionDigits={0} />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td className="w-100">Circulating Supply (SOL)</td>
|
||||
<td className="text-lg-right">
|
||||
{lamportsToSolString(supply.circulating, 0)}
|
||||
<SolBalance
|
||||
lamports={supply.circulating}
|
||||
maximumFractionDigits={0}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td className="w-100">Non-Circulating Supply (SOL)</td>
|
||||
<td className="text-lg-right">
|
||||
{lamportsToSolString(supply.nonCirculating, 0)}
|
||||
<SolBalance
|
||||
lamports={supply.nonCirculating}
|
||||
maximumFractionDigits={0}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</TableCardBody>
|
||||
|
|
|
@ -5,7 +5,7 @@ import { AccountBalancePair } from "@solana/web3.js";
|
|||
import { useRichList, useFetchRichList, Status } from "providers/richList";
|
||||
import { LoadingCard } from "./common/LoadingCard";
|
||||
import { ErrorCard } from "./common/ErrorCard";
|
||||
import { lamportsToSolString } from "utils";
|
||||
import { SolBalance } from "utils";
|
||||
import { useQuery } from "utils/url";
|
||||
import { useSupply } from "providers/supply";
|
||||
import { Address } from "./common/Address";
|
||||
|
@ -133,7 +133,9 @@ const renderAccountRow = (
|
|||
<td>
|
||||
<Address pubkey={account.address} link />
|
||||
</td>
|
||||
<td className="text-right">{lamportsToSolString(account.lamports, 0)}</td>
|
||||
<td className="text-right">
|
||||
<SolBalance lamports={account.lamports} maximumFractionDigits={0} />
|
||||
</td>
|
||||
<td className="text-right">{`${(
|
||||
(100 * account.lamports) /
|
||||
supply
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
import { TableCardBody } from "components/common/TableCardBody";
|
||||
import { lamportsToSolString } from "utils";
|
||||
import { SolBalance } from "utils";
|
||||
import { displayTimestampUtc } from "utils/date";
|
||||
import { Account, useFetchAccountInfo } from "providers/accounts";
|
||||
import { Address } from "components/common/Address";
|
||||
|
@ -125,13 +125,13 @@ function OverviewCard({
|
|||
<tr>
|
||||
<td>Balance (SOL)</td>
|
||||
<td className="text-lg-right text-uppercase">
|
||||
{lamportsToSolString(account.lamports || 0)}
|
||||
<SolBalance lamports={account.lamports || 0} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rent Reserve (SOL)</td>
|
||||
<td className="text-lg-right">
|
||||
{lamportsToSolString(stakeAccount.meta.rentExemptReserve)}
|
||||
<SolBalance lamports={stakeAccount.meta.rentExemptReserve} />
|
||||
</td>
|
||||
</tr>
|
||||
{hideDelegation && (
|
||||
|
@ -190,7 +190,7 @@ function DelegationCard({
|
|||
<tr>
|
||||
<td>Delegated Stake (SOL)</td>
|
||||
<td className="text-lg-right">
|
||||
{lamportsToSolString(stake.delegation.stake)}
|
||||
<SolBalance lamports={stake.delegation.stake} />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
@ -199,14 +199,14 @@ function DelegationCard({
|
|||
<tr>
|
||||
<td>Active Stake (SOL)</td>
|
||||
<td className="text-lg-right">
|
||||
{lamportsToSolString(activation.active)}
|
||||
<SolBalance lamports={activation.active} />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Inactive Stake (SOL)</td>
|
||||
<td className="text-lg-right">
|
||||
{lamportsToSolString(activation.inactive)}
|
||||
<SolBalance lamports={activation.inactive} />
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { lamportsToSolString } from "utils";
|
||||
import { SolBalance } from "utils";
|
||||
import {
|
||||
SysvarAccount,
|
||||
StakeHistoryInfo,
|
||||
|
@ -57,13 +57,13 @@ const renderAccountRow = (entry: StakeHistoryEntry, index: number) => {
|
|||
<tr key={index}>
|
||||
<td className="w-1 text-monospace">{entry.epoch}</td>
|
||||
<td className="text-monospace">
|
||||
{lamportsToSolString(entry.stakeHistory.effective)}
|
||||
<SolBalance lamports={entry.stakeHistory.effective} />
|
||||
</td>
|
||||
<td className="text-monospace">
|
||||
{lamportsToSolString(entry.stakeHistory.activating)}
|
||||
<SolBalance lamports={entry.stakeHistory.activating} />
|
||||
</td>
|
||||
<td className="text-monospace">
|
||||
{lamportsToSolString(entry.stakeHistory.deactivating)}
|
||||
<SolBalance lamports={entry.stakeHistory.deactivating} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
Details,
|
||||
useFetchTransactionDetails,
|
||||
useTransactionDetailsCache,
|
||||
} from "providers/transactions/details";
|
||||
} from "providers/transactions/parsed";
|
||||
import { reportError } from "utils/sentry";
|
||||
import { intoTransactionInstruction, displayAddress } from "utils/tx";
|
||||
import {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
import { Account } from "providers/accounts";
|
||||
import { lamportsToSolString } from "utils";
|
||||
import { SolBalance } from "utils";
|
||||
import { TableCardBody } from "components/common/TableCardBody";
|
||||
import { Address } from "components/common/Address";
|
||||
import { addressLabel } from "utils/tx";
|
||||
|
@ -35,8 +35,8 @@ export function UnknownAccountCard({ account }: { account: Account }) {
|
|||
)}
|
||||
<tr>
|
||||
<td>Balance (SOL)</td>
|
||||
<td className="text-lg-right text-uppercase">
|
||||
{lamportsToSolString(lamports)}
|
||||
<td className="text-lg-right">
|
||||
<SolBalance lamports={lamports} />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
import { TableCardBody } from "components/common/TableCardBody";
|
||||
import { lamportsToSolString } from "utils";
|
||||
import { SolBalance } from "utils";
|
||||
import { Account, useFetchAccountInfo } from "providers/accounts";
|
||||
import { Address } from "components/common/Address";
|
||||
import {
|
||||
|
@ -98,7 +98,7 @@ export function UpgradeableProgramSection({
|
|||
<tr>
|
||||
<td>Balance (SOL)</td>
|
||||
<td className="text-lg-right text-uppercase">
|
||||
{lamportsToSolString(account.lamports || 0)}
|
||||
<SolBalance lamports={account.lamports || 0} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -169,7 +169,7 @@ export function UpgradeableProgramDataSection({
|
|||
<tr>
|
||||
<td>Balance (SOL)</td>
|
||||
<td className="text-lg-right text-uppercase">
|
||||
{lamportsToSolString(account.lamports || 0)}
|
||||
<SolBalance lamports={account.lamports || 0} />
|
||||
</td>
|
||||
</tr>
|
||||
{account.details?.space !== undefined && (
|
||||
|
@ -236,7 +236,7 @@ export function UpgradeableProgramBufferSection({
|
|||
<tr>
|
||||
<td>Balance (SOL)</td>
|
||||
<td className="text-lg-right text-uppercase">
|
||||
{lamportsToSolString(account.lamports || 0)}
|
||||
<SolBalance lamports={account.lamports || 0} />
|
||||
</td>
|
||||
</tr>
|
||||
{account.details?.space !== undefined && (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { lamportsToSolString } from "utils";
|
||||
import { SolBalance } from "utils";
|
||||
import { BlockResponse, PublicKey } from "@solana/web3.js";
|
||||
import { Address } from "components/common/Address";
|
||||
|
||||
|
@ -49,11 +49,15 @@ export function BlockRewardsCard({ block }: { block: BlockResponse }) {
|
|||
<Address pubkey={new PublicKey(reward.pubkey)} link />
|
||||
</td>
|
||||
<td>{reward.rewardType}</td>
|
||||
<td>{lamportsToSolString(reward.lamports)}</td>
|
||||
<td>
|
||||
{reward.postBalance
|
||||
? lamportsToSolString(reward.postBalance)
|
||||
: "-"}
|
||||
<SolBalance lamports={reward.lamports} />
|
||||
</td>
|
||||
<td>
|
||||
{reward.postBalance ? (
|
||||
<SolBalance lamports={reward.postBalance} />
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</td>
|
||||
<td>{percentChange ? percentChange + "%" : "-"}</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
import { Address } from "./Address";
|
||||
import { Account } from "providers/accounts";
|
||||
import { lamportsToSolString } from "utils";
|
||||
import { SolBalance } from "utils";
|
||||
|
||||
type AccountHeaderProps = {
|
||||
title: string;
|
||||
|
@ -41,7 +41,7 @@ export function AccountBalanceRow({ account }: AccountProps) {
|
|||
<tr>
|
||||
<td>Balance (SOL)</td>
|
||||
<td className="text-lg-right text-uppercase">
|
||||
{lamportsToSolString(lamports)}
|
||||
<SolBalance lamports={lamports} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
import { BigNumber } from "bignumber.js";
|
||||
import { lamportsToSolString } from "utils";
|
||||
import { SolBalance } from "utils";
|
||||
|
||||
export function BalanceDelta({
|
||||
delta,
|
||||
|
@ -12,7 +12,7 @@ export function BalanceDelta({
|
|||
let sols;
|
||||
|
||||
if (isSol) {
|
||||
sols = lamportsToSolString(delta.toNumber());
|
||||
sols = <SolBalance lamports={delta.toNumber()} />;
|
||||
}
|
||||
|
||||
if (delta.gt(0)) {
|
||||
|
|
|
@ -8,9 +8,9 @@ import { RawDetails } from "./RawDetails";
|
|||
import { RawParsedDetails } from "./RawParsedDetails";
|
||||
import { SignatureContext } from "../../pages/TransactionDetailsPage";
|
||||
import {
|
||||
useTransactionDetails,
|
||||
useFetchRawTransaction,
|
||||
} from "providers/transactions/details";
|
||||
useRawTransactionDetails,
|
||||
} from "providers/transactions/raw";
|
||||
import { Address } from "components/common/Address";
|
||||
|
||||
type InstructionProps = {
|
||||
|
@ -37,11 +37,11 @@ export function InstructionCard({
|
|||
const [resultClass] = ixResult(result, index);
|
||||
const [showRaw, setShowRaw] = React.useState(defaultRaw || false);
|
||||
const signature = useContext(SignatureContext);
|
||||
const details = useTransactionDetails(signature);
|
||||
const rawDetails = useRawTransactionDetails(signature);
|
||||
|
||||
let raw: TransactionInstruction | undefined = undefined;
|
||||
if (details && childIndex === undefined) {
|
||||
raw = details?.data?.raw?.instructions[index];
|
||||
if (rawDetails && childIndex === undefined) {
|
||||
raw = rawDetails?.data?.raw?.transaction.instructions[index];
|
||||
}
|
||||
|
||||
const fetchRaw = useFetchRawTransaction();
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
StakeProgram,
|
||||
ParsedInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import { lamportsToSolString } from "utils";
|
||||
import { SolBalance } from "utils";
|
||||
import { InstructionCard } from "../InstructionCard";
|
||||
import { Address } from "components/common/Address";
|
||||
import { SplitInfo } from "./types";
|
||||
|
@ -58,7 +58,9 @@ export function SplitDetailsCard(props: {
|
|||
|
||||
<tr>
|
||||
<td>Split Amount (SOL)</td>
|
||||
<td className="text-lg-right">{lamportsToSolString(info.lamports)}</td>
|
||||
<td className="text-lg-right">
|
||||
<SolBalance lamports={info.lamports} />
|
||||
</td>
|
||||
</tr>
|
||||
</InstructionCard>
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
StakeProgram,
|
||||
ParsedInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import { lamportsToSolString } from "utils";
|
||||
import { SolBalance } from "utils";
|
||||
import { InstructionCard } from "../InstructionCard";
|
||||
import { Address } from "components/common/Address";
|
||||
import { WithdrawInfo } from "./types";
|
||||
|
@ -58,7 +58,9 @@ export function WithdrawDetailsCard(props: {
|
|||
|
||||
<tr>
|
||||
<td>Withdraw Amount (SOL)</td>
|
||||
<td className="text-lg-right">{lamportsToSolString(info.lamports)}</td>
|
||||
<td className="text-lg-right">
|
||||
<SolBalance lamports={info.lamports} />
|
||||
</td>
|
||||
</tr>
|
||||
</InstructionCard>
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
SignatureResult,
|
||||
ParsedInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import { lamportsToSolString } from "utils";
|
||||
import { SolBalance } from "utils";
|
||||
import { InstructionCard } from "../InstructionCard";
|
||||
import { Address } from "components/common/Address";
|
||||
import { CreateAccountInfo } from "./types";
|
||||
|
@ -51,7 +51,9 @@ export function CreateDetailsCard(props: {
|
|||
|
||||
<tr>
|
||||
<td>Transfer Amount (SOL)</td>
|
||||
<td className="text-lg-right">{lamportsToSolString(info.lamports)}</td>
|
||||
<td className="text-lg-right">
|
||||
<SolBalance lamports={info.lamports} />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
SignatureResult,
|
||||
ParsedInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import { lamportsToSolString } from "utils";
|
||||
import { SolBalance } from "utils";
|
||||
import { InstructionCard } from "../InstructionCard";
|
||||
import { Copyable } from "components/common/Copyable";
|
||||
import { Address } from "components/common/Address";
|
||||
|
@ -68,7 +68,9 @@ export function CreateWithSeedDetailsCard(props: {
|
|||
|
||||
<tr>
|
||||
<td>Transfer Amount (SOL)</td>
|
||||
<td className="text-lg-right">{lamportsToSolString(info.lamports)}</td>
|
||||
<td className="text-lg-right">
|
||||
<SolBalance lamports={info.lamports} />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
SignatureResult,
|
||||
ParsedInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import { lamportsToSolString } from "utils";
|
||||
import { SolBalance } from "utils";
|
||||
import { InstructionCard } from "../InstructionCard";
|
||||
import { Address } from "components/common/Address";
|
||||
import { WithdrawNonceInfo } from "./types";
|
||||
|
@ -58,7 +58,9 @@ export function NonceWithdrawDetailsCard(props: {
|
|||
|
||||
<tr>
|
||||
<td>Withdraw Amount (SOL)</td>
|
||||
<td className="text-lg-right">{lamportsToSolString(info.lamports)}</td>
|
||||
<td className="text-lg-right">
|
||||
<SolBalance lamports={info.lamports} />
|
||||
</td>
|
||||
</tr>
|
||||
</InstructionCard>
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
SignatureResult,
|
||||
ParsedInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import { lamportsToSolString } from "utils";
|
||||
import { SolBalance } from "utils";
|
||||
import { InstructionCard } from "../InstructionCard";
|
||||
import { Address } from "components/common/Address";
|
||||
import { TransferInfo } from "./types";
|
||||
|
@ -51,7 +51,9 @@ export function TransferDetailsCard(props: {
|
|||
|
||||
<tr>
|
||||
<td>Transfer Amount (SOL)</td>
|
||||
<td className="text-lg-right">{lamportsToSolString(info.lamports)}</td>
|
||||
<td className="text-lg-right">
|
||||
<SolBalance lamports={info.lamports} />
|
||||
</td>
|
||||
</tr>
|
||||
</InstructionCard>
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
SignatureResult,
|
||||
ParsedInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import { lamportsToSolString } from "utils";
|
||||
import { SolBalance } from "utils";
|
||||
import { InstructionCard } from "../InstructionCard";
|
||||
import { Copyable } from "components/common/Copyable";
|
||||
import { Address } from "components/common/Address";
|
||||
|
@ -59,7 +59,9 @@ export function TransferWithSeedDetailsCard(props: {
|
|||
|
||||
<tr>
|
||||
<td>Transfer Amount (SOL)</td>
|
||||
<td className="text-lg-right">{lamportsToSolString(info.lamports)}</td>
|
||||
<td className="text-lg-right">
|
||||
<SolBalance lamports={info.lamports} />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
|
|
|
@ -29,7 +29,7 @@ import { isSerumInstruction } from "components/instruction/serum/types";
|
|||
import { isTokenLendingInstruction } from "components/instruction/token-lending/types";
|
||||
import { isTokenSwapInstruction } from "components/instruction/token-swap/types";
|
||||
import { isBonfidaBotInstruction } from "components/instruction/bonfida-bot/types";
|
||||
import { useFetchTransactionDetails } from "providers/transactions/details";
|
||||
import { useFetchTransactionDetails } from "providers/transactions/parsed";
|
||||
import {
|
||||
useTransactionDetails,
|
||||
useTransactionStatus,
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import bs58 from "bs58";
|
||||
import {
|
||||
useFetchTransactionStatus,
|
||||
useTransactionStatus,
|
||||
useTransactionDetails,
|
||||
} from "providers/transactions";
|
||||
import { useFetchTransactionDetails } from "providers/transactions/details";
|
||||
import { useFetchTransactionDetails } from "providers/transactions/parsed";
|
||||
import { useCluster, ClusterStatus } from "providers/cluster";
|
||||
import {
|
||||
TransactionSignature,
|
||||
SystemProgram,
|
||||
SystemInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import { lamportsToSolString } from "utils";
|
||||
import { SolBalance } from "utils";
|
||||
import { ErrorCard } from "components/common/ErrorCard";
|
||||
import { LoadingCard } from "components/common/LoadingCard";
|
||||
import { TableCardBody } from "components/common/TableCardBody";
|
||||
|
@ -28,6 +29,7 @@ import { BalanceDelta } from "components/common/BalanceDelta";
|
|||
import { TokenBalancesCard } from "components/transaction/TokenBalancesCard";
|
||||
import { InstructionsSection } from "components/transaction/InstructionsSection";
|
||||
import { ProgramLogSection } from "components/transaction/ProgramLogSection";
|
||||
import { clusterPath } from "utils/url";
|
||||
|
||||
const AUTO_REFRESH_INTERVAL = 2000;
|
||||
const ZERO_CONFIRMATION_BAILOUT = 5;
|
||||
|
@ -206,6 +208,13 @@ function StatusCard({
|
|||
<div className="card">
|
||||
<div className="card-header align-items-center">
|
||||
<h3 className="card-header-title">Overview</h3>
|
||||
<Link
|
||||
to={clusterPath(`/tx/${signature}/inspect`)}
|
||||
className="btn btn-white btn-sm mr-2"
|
||||
>
|
||||
<span className="fe fe-settings mr-2"></span>
|
||||
Inspect
|
||||
</Link>
|
||||
{autoRefresh === AutoRefresh.Active ? (
|
||||
<span className="spinner-grow spinner-grow-sm"></span>
|
||||
) : (
|
||||
|
@ -288,7 +297,9 @@ function StatusCard({
|
|||
{fee && (
|
||||
<tr>
|
||||
<td>Fee (SOL)</td>
|
||||
<td className="text-lg-right">{lamportsToSolString(fee)}</td>
|
||||
<td className="text-lg-right">
|
||||
<SolBalance lamports={fee} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</TableCardBody>
|
||||
|
@ -357,7 +368,9 @@ function AccountsCard({
|
|||
<td>
|
||||
<BalanceDelta delta={delta} isSol />
|
||||
</td>
|
||||
<td>{lamportsToSolString(post)}</td>
|
||||
<td>
|
||||
<SolBalance lamports={post} />
|
||||
</td>
|
||||
<td>
|
||||
{index === 0 && (
|
||||
<span className="badge badge-soft-info mr-1">Fee Payer</span>
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
import React from "react";
|
||||
import { Message, PublicKey } from "@solana/web3.js";
|
||||
import { TableCardBody } from "components/common/TableCardBody";
|
||||
import { AddressWithContext } from "./AddressWithContext";
|
||||
import { ErrorCard } from "components/common/ErrorCard";
|
||||
|
||||
export function AccountsCard({ message }: { message: Message }) {
|
||||
const [expanded, setExpanded] = React.useState(true);
|
||||
|
||||
const { validMessage, error } = React.useMemo(() => {
|
||||
const {
|
||||
numRequiredSignatures,
|
||||
numReadonlySignedAccounts,
|
||||
numReadonlyUnsignedAccounts,
|
||||
} = message.header;
|
||||
|
||||
if (numReadonlySignedAccounts >= numRequiredSignatures) {
|
||||
return { validMessage: undefined, error: "Invalid header" };
|
||||
} else if (numReadonlyUnsignedAccounts >= message.accountKeys.length) {
|
||||
return { validMessage: undefined, error: "Invalid header" };
|
||||
} else if (message.accountKeys.length === 0) {
|
||||
return { validMessage: undefined, error: "Message has no accounts" };
|
||||
}
|
||||
|
||||
return {
|
||||
validMessage: message,
|
||||
error: undefined,
|
||||
};
|
||||
}, [message]);
|
||||
|
||||
const accountRows = React.useMemo(() => {
|
||||
const message = validMessage;
|
||||
if (!message) return;
|
||||
return message.accountKeys.map((publicKey, accountIndex) => {
|
||||
const {
|
||||
numRequiredSignatures,
|
||||
numReadonlySignedAccounts,
|
||||
numReadonlyUnsignedAccounts,
|
||||
} = message.header;
|
||||
|
||||
let readOnly = false;
|
||||
let signer = false;
|
||||
if (accountIndex < numRequiredSignatures) {
|
||||
signer = true;
|
||||
if (accountIndex >= numRequiredSignatures - numReadonlySignedAccounts) {
|
||||
readOnly = true;
|
||||
}
|
||||
} else if (
|
||||
accountIndex >=
|
||||
message.accountKeys.length - numReadonlyUnsignedAccounts
|
||||
) {
|
||||
readOnly = true;
|
||||
}
|
||||
|
||||
const props = {
|
||||
accountIndex,
|
||||
publicKey,
|
||||
signer,
|
||||
readOnly,
|
||||
};
|
||||
|
||||
return <AccountRow key={accountIndex} {...props} />;
|
||||
});
|
||||
}, [validMessage]);
|
||||
|
||||
if (error) {
|
||||
return <ErrorCard text={`Unable to display accounts. ${error}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-header-title">
|
||||
{`Account List (${message.accountKeys.length})`}
|
||||
</h3>
|
||||
<button
|
||||
className={`btn btn-sm d-flex ${
|
||||
expanded ? "btn-black active" : "btn-white"
|
||||
}`}
|
||||
onClick={() => setExpanded((e) => !e)}
|
||||
>
|
||||
{expanded ? "Collapse" : "Expand"}
|
||||
</button>
|
||||
</div>
|
||||
{expanded && <TableCardBody>{accountRows}</TableCardBody>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AccountRow({
|
||||
accountIndex,
|
||||
publicKey,
|
||||
signer,
|
||||
readOnly,
|
||||
}: {
|
||||
accountIndex: number;
|
||||
publicKey: PublicKey;
|
||||
signer: boolean;
|
||||
readOnly: boolean;
|
||||
}) {
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<div className="d-flex align-items-start flex-column">
|
||||
Account #{accountIndex + 1}
|
||||
<span className="mt-1">
|
||||
{signer && (
|
||||
<span className="badge badge-soft-info mr-1">Signer</span>
|
||||
)}
|
||||
{!readOnly && (
|
||||
<span className="badge badge-soft-danger">Writable</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-lg-right">
|
||||
<AddressWithContext pubkey={publicKey} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
import React from "react";
|
||||
import { PublicKey, SystemProgram } from "@solana/web3.js";
|
||||
import { Address } from "components/common/Address";
|
||||
import {
|
||||
Account,
|
||||
useAccountInfo,
|
||||
useFetchAccountInfo,
|
||||
} from "providers/accounts";
|
||||
import { ClusterStatus, useCluster } from "providers/cluster";
|
||||
import { addressLabel } from "utils/tx";
|
||||
import { lamportsToSolString } from "utils";
|
||||
|
||||
type AccountValidator = (account: Account) => string | undefined;
|
||||
|
||||
export const createFeePayerValidator = (
|
||||
feeLamports: number
|
||||
): AccountValidator => {
|
||||
return (account: Account): string | undefined => {
|
||||
if (account.details === undefined) return "Account doesn't exist";
|
||||
if (!account.details.owner.equals(SystemProgram.programId))
|
||||
return "Only system-owned accounts can pay fees";
|
||||
// TODO: Actually nonce accounts can pay fees too
|
||||
if (account.details.space > 0)
|
||||
return "Only unallocated accounts can pay fees";
|
||||
if (account.lamports < feeLamports) {
|
||||
return "Insufficient funds for fees";
|
||||
}
|
||||
return;
|
||||
};
|
||||
};
|
||||
|
||||
export const programValidator = (account: Account): string | undefined => {
|
||||
if (account.details === undefined) return "Account doesn't exist";
|
||||
if (!account.details.executable)
|
||||
return "Only executable accounts can be invoked";
|
||||
return;
|
||||
};
|
||||
|
||||
export function AddressWithContext({
|
||||
pubkey,
|
||||
validator,
|
||||
}: {
|
||||
pubkey: PublicKey;
|
||||
validator?: AccountValidator;
|
||||
}) {
|
||||
return (
|
||||
<div className="d-flex align-items-end flex-column">
|
||||
<Address pubkey={pubkey} link />
|
||||
<AccountInfo pubkey={pubkey} validator={validator} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AccountInfo({
|
||||
pubkey,
|
||||
validator,
|
||||
}: {
|
||||
pubkey: PublicKey;
|
||||
validator?: AccountValidator;
|
||||
}) {
|
||||
const address = pubkey.toBase58();
|
||||
const fetchAccount = useFetchAccountInfo();
|
||||
const info = useAccountInfo(address);
|
||||
const { cluster, status } = useCluster();
|
||||
|
||||
// Fetch account on load
|
||||
React.useEffect(() => {
|
||||
if (!info && status === ClusterStatus.Connected && pubkey) {
|
||||
fetchAccount(pubkey);
|
||||
}
|
||||
}, [address, status]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
if (!info?.data)
|
||||
return (
|
||||
<span className="text-muted">
|
||||
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
||||
Loading
|
||||
</span>
|
||||
);
|
||||
|
||||
const errorMessage = validator && validator(info.data);
|
||||
if (errorMessage) return <span className="text-warning">{errorMessage}</span>;
|
||||
|
||||
if (info.data.details?.executable) {
|
||||
return <span className="text-muted">Executable Program</span>;
|
||||
}
|
||||
|
||||
const owner = info.data.details?.owner;
|
||||
const ownerAddress = owner?.toBase58();
|
||||
const ownerLabel = ownerAddress && addressLabel(ownerAddress, cluster);
|
||||
|
||||
return (
|
||||
<span className="text-muted">
|
||||
{ownerAddress
|
||||
? `Owned by ${
|
||||
ownerLabel || ownerAddress
|
||||
}. Balance is ${lamportsToSolString(info.data.lamports)} SOL`
|
||||
: "Account doesn't exist"}
|
||||
</span>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,275 @@
|
|||
import React from "react";
|
||||
import { Message, PACKET_DATA_SIZE } from "@solana/web3.js";
|
||||
|
||||
import { TableCardBody } from "components/common/TableCardBody";
|
||||
import { SolBalance } from "utils";
|
||||
import { useQuery } from "utils/url";
|
||||
import { useHistory, useLocation } from "react-router";
|
||||
import {
|
||||
useFetchRawTransaction,
|
||||
useRawTransactionDetails,
|
||||
} from "providers/transactions/raw";
|
||||
import { FetchStatus } from "providers/cache";
|
||||
import { LoadingCard } from "components/common/LoadingCard";
|
||||
import { ErrorCard } from "components/common/ErrorCard";
|
||||
import { TransactionSignatures } from "./SignaturesCard";
|
||||
import { AccountsCard } from "./AccountsCard";
|
||||
import {
|
||||
AddressWithContext,
|
||||
createFeePayerValidator,
|
||||
} from "./AddressWithContext";
|
||||
import { SimulatorCard } from "./SimulatorCard";
|
||||
import { RawInput } from "./RawInputCard";
|
||||
import { InstructionsSection } from "./InstructionsSection";
|
||||
|
||||
export type TransactionData = {
|
||||
rawMessage: Uint8Array;
|
||||
message: Message;
|
||||
signatures?: (string | null)[];
|
||||
};
|
||||
|
||||
export function TransactionInspectorPage({
|
||||
signature,
|
||||
}: {
|
||||
signature?: string;
|
||||
}) {
|
||||
const [transaction, setTransaction] = React.useState<TransactionData>();
|
||||
const query = useQuery();
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const [paramString, setParamString] = React.useState<string>();
|
||||
|
||||
// Sync message with url search params
|
||||
React.useEffect(() => {
|
||||
if (signature) return;
|
||||
if (transaction) {
|
||||
const base64 = btoa(
|
||||
String.fromCharCode.apply(null, [...transaction.rawMessage])
|
||||
);
|
||||
const newParam = encodeURIComponent(base64);
|
||||
if (query.get("message") === newParam) return;
|
||||
query.set("message", newParam);
|
||||
history.push({ ...location, search: query.toString() });
|
||||
}
|
||||
}, [query, transaction, signature, history, location]);
|
||||
|
||||
const reset = React.useCallback(() => {
|
||||
query.delete("message");
|
||||
history.push({ ...location, search: query.toString() });
|
||||
setTransaction(undefined);
|
||||
}, [query, location, history]);
|
||||
|
||||
// Decode the message url param whenever it changes
|
||||
React.useEffect(() => {
|
||||
if (transaction || signature) return;
|
||||
|
||||
let messageParam = query.get("message");
|
||||
if (messageParam !== null) {
|
||||
let messageString;
|
||||
try {
|
||||
messageString = decodeURIComponent(messageParam);
|
||||
} catch (err) {
|
||||
query.delete("message");
|
||||
history.push({ ...location, search: query.toString() });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const buffer = Uint8Array.from(atob(messageString), (c) =>
|
||||
c.charCodeAt(0)
|
||||
);
|
||||
|
||||
if (buffer.length < 36) {
|
||||
query.delete("message");
|
||||
history.push({ ...location, search: query.toString() });
|
||||
throw new Error("buffer is too short");
|
||||
}
|
||||
|
||||
const message = Message.from(buffer);
|
||||
setParamString(undefined);
|
||||
setTransaction({
|
||||
message,
|
||||
rawMessage: buffer,
|
||||
});
|
||||
} catch (err) {
|
||||
setParamString(messageString);
|
||||
}
|
||||
} else {
|
||||
setParamString(undefined);
|
||||
}
|
||||
}, [query, transaction, signature, history, location]);
|
||||
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<div className="header">
|
||||
<div className="header-body">
|
||||
<h2 className="header-title">Transaction Inspector</h2>
|
||||
</div>
|
||||
</div>
|
||||
{signature ? (
|
||||
<PermalinkView signature={signature} reset={reset} />
|
||||
) : transaction ? (
|
||||
<LoadedView transaction={transaction} onClear={reset} />
|
||||
) : (
|
||||
<RawInput value={paramString} setTransactionData={setTransaction} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PermalinkView({
|
||||
signature,
|
||||
}: {
|
||||
signature: string;
|
||||
reset: () => void;
|
||||
}) {
|
||||
const details = useRawTransactionDetails(signature);
|
||||
const fetchTransaction = useFetchRawTransaction();
|
||||
const refreshTransaction = () => fetchTransaction(signature);
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const transaction = details?.data?.raw;
|
||||
const reset = React.useCallback(() => {
|
||||
history.push({ ...location, pathname: "/tx/inspector" });
|
||||
}, [history, location]);
|
||||
|
||||
// Fetch details on load
|
||||
React.useEffect(() => {
|
||||
if (!details) fetchTransaction(signature);
|
||||
}, [signature, details, fetchTransaction]);
|
||||
|
||||
if (!details || details.status === FetchStatus.Fetching) {
|
||||
return <LoadingCard />;
|
||||
} else if (details.status === FetchStatus.FetchFailed) {
|
||||
return (
|
||||
<ErrorCard
|
||||
retry={refreshTransaction}
|
||||
text="Failed to fetch transaction"
|
||||
/>
|
||||
);
|
||||
} else if (!transaction) {
|
||||
return (
|
||||
<ErrorCard
|
||||
text="Transaction was not found"
|
||||
retry={reset}
|
||||
retryText="Reset"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { message, signatures } = transaction;
|
||||
const tx = { message, rawMessage: message.serialize(), signatures };
|
||||
|
||||
return <LoadedView transaction={tx} onClear={reset} />;
|
||||
}
|
||||
|
||||
function LoadedView({
|
||||
transaction,
|
||||
onClear,
|
||||
}: {
|
||||
transaction: TransactionData;
|
||||
onClear: () => void;
|
||||
}) {
|
||||
const { message, rawMessage, signatures } = transaction;
|
||||
|
||||
return (
|
||||
<>
|
||||
<OverviewCard message={message} raw={rawMessage} onClear={onClear} />
|
||||
<SimulatorCard message={message} />
|
||||
{signatures && (
|
||||
<TransactionSignatures
|
||||
message={message}
|
||||
signatures={signatures}
|
||||
rawMessage={rawMessage}
|
||||
/>
|
||||
)}
|
||||
<AccountsCard message={message} />
|
||||
<InstructionsSection message={message} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const DEFAULT_FEES = {
|
||||
lamportsPerSignature: 5000,
|
||||
};
|
||||
|
||||
function OverviewCard({
|
||||
message,
|
||||
raw,
|
||||
onClear,
|
||||
}: {
|
||||
message: Message;
|
||||
raw: Uint8Array;
|
||||
onClear: () => void;
|
||||
}) {
|
||||
const fee =
|
||||
message.header.numRequiredSignatures * DEFAULT_FEES.lamportsPerSignature;
|
||||
const feePayerValidator = createFeePayerValidator(fee);
|
||||
|
||||
const size = React.useMemo(() => {
|
||||
const sigBytes = 1 + 64 * message.header.numRequiredSignatures;
|
||||
return sigBytes + raw.length;
|
||||
}, [message, raw]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-header-title">Transaction Overview</h3>
|
||||
<button className="btn btn-sm d-flex btn-white" onClick={onClear}>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<TableCardBody>
|
||||
<tr>
|
||||
<td>Serialized Size</td>
|
||||
<td className="text-lg-right">
|
||||
<div className="d-flex align-items-end flex-column">
|
||||
{size} bytes
|
||||
<span
|
||||
className={
|
||||
size <= PACKET_DATA_SIZE ? "text-muted" : "text-warning"
|
||||
}
|
||||
>
|
||||
Max transaction size is {PACKET_DATA_SIZE} bytes
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Fees</td>
|
||||
<td className="text-lg-right">
|
||||
<div className="d-flex align-items-end flex-column">
|
||||
<SolBalance lamports={fee} />
|
||||
<span className="text-muted">
|
||||
{`Each signature costs ${DEFAULT_FEES.lamportsPerSignature} lamports`}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div className="d-flex align-items-start flex-column">
|
||||
Fee payer
|
||||
<span className="mt-1">
|
||||
<span className="badge badge-soft-info mr-2">Signer</span>
|
||||
<span className="badge badge-soft-danger mr-2">Writable</span>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
{message.accountKeys.length === 0 ? (
|
||||
"No Fee Payer"
|
||||
) : (
|
||||
<AddressWithContext
|
||||
pubkey={message.accountKeys[0]}
|
||||
validator={feePayerValidator}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</TableCardBody>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
import React from "react";
|
||||
import bs58 from "bs58";
|
||||
import { CompiledInstruction, Message } from "@solana/web3.js";
|
||||
import { TableCardBody } from "components/common/TableCardBody";
|
||||
import { AddressWithContext, programValidator } from "./AddressWithContext";
|
||||
import { useCluster } from "providers/cluster";
|
||||
import { programLabel } from "utils/tx";
|
||||
|
||||
export function InstructionsSection({ message }: { message: Message }) {
|
||||
return (
|
||||
<>
|
||||
{message.instructions.map((ix, index) => {
|
||||
return <InstructionCard key={index} {...{ message, ix, index }} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function InstructionCard({
|
||||
message,
|
||||
ix,
|
||||
index,
|
||||
}: {
|
||||
message: Message;
|
||||
ix: CompiledInstruction;
|
||||
index: number;
|
||||
}) {
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
const { cluster } = useCluster();
|
||||
const programId = message.accountKeys[ix.programIdIndex];
|
||||
const programName = programLabel(programId.toBase58(), cluster) || "Unknown";
|
||||
|
||||
let data: string = "No data";
|
||||
if (ix.data) {
|
||||
data = "";
|
||||
|
||||
const chunks = [];
|
||||
const hexString = bs58.decode(ix.data).toString("hex");
|
||||
for (let i = 0; i < hexString.length; i += 2) {
|
||||
chunks.push(hexString.slice(i, i + 2));
|
||||
}
|
||||
|
||||
data = chunks.join(" ");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card" id={`instruction-index-${index + 1}`} key={index}>
|
||||
<div className={`card-header${!expanded ? " border-bottom-none" : ""}`}>
|
||||
<h3 className="card-header-title mb-0 d-flex align-items-center">
|
||||
<span className={`badge badge-soft-info mr-2`}>#{index + 1}</span>
|
||||
{programName} Instruction
|
||||
</h3>
|
||||
|
||||
<button
|
||||
className={`btn btn-sm d-flex ${
|
||||
expanded ? "btn-black active" : "btn-white"
|
||||
}`}
|
||||
onClick={() => setExpanded((e) => !e)}
|
||||
>
|
||||
{expanded ? "Collapse" : "Expand"}
|
||||
</button>
|
||||
</div>
|
||||
{expanded && (
|
||||
<TableCardBody>
|
||||
<tr>
|
||||
<td>Program</td>
|
||||
<td className="text-lg-right">
|
||||
<AddressWithContext
|
||||
pubkey={message.accountKeys[ix.programIdIndex]}
|
||||
validator={programValidator}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{ix.accounts.map((accountIndex, index) => {
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<div className="d-flex align-items-start flex-column">
|
||||
Account #{index + 1}
|
||||
<span className="mt-1">
|
||||
{accountIndex < message.header.numRequiredSignatures && (
|
||||
<span className="badge badge-soft-info mr-2">
|
||||
Signer
|
||||
</span>
|
||||
)}
|
||||
{message.isAccountWritable(accountIndex) && (
|
||||
<span className="badge badge-soft-danger mr-2">
|
||||
Writable
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-lg-right">
|
||||
<AddressWithContext
|
||||
pubkey={message.accountKeys[accountIndex]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
<tr>
|
||||
<td>
|
||||
Instruction Data <span className="text-muted">(Hex)</span>
|
||||
</td>
|
||||
<td className="text-lg-right">
|
||||
<pre className="d-inline-block text-left mb-0 data-wrap">
|
||||
{data}
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</TableCardBody>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
import React from "react";
|
||||
import { Message } 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;
|
||||
signatures: string[];
|
||||
} | null {
|
||||
const SIGNATURE_LENGTH = 64;
|
||||
const signatures = [];
|
||||
try {
|
||||
const signaturesLen = bytes[0];
|
||||
bytes = bytes.slice(1);
|
||||
for (let i = 0; i < signaturesLen; i++) {
|
||||
const rawSignature = bytes.slice(0, SIGNATURE_LENGTH);
|
||||
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);
|
||||
return { message, signatures };
|
||||
}
|
||||
|
||||
const MIN_MESSAGE_LENGTH =
|
||||
3 + // header
|
||||
1 + // accounts length
|
||||
32 + // accounts, must have at least one address for fees
|
||||
32 + // recent blockhash
|
||||
1; // instructions length
|
||||
|
||||
const MIN_TRANSACTION_LENGTH =
|
||||
1 + // signatures length
|
||||
64 + // signatures, must have at least one for fees
|
||||
MIN_MESSAGE_LENGTH;
|
||||
|
||||
const MAX_TRANSACTION_SIGNATURES =
|
||||
Math.floor((1232 - MIN_TRANSACTION_LENGTH) / (64 + 32)) + 1;
|
||||
|
||||
export function RawInput({
|
||||
value,
|
||||
setTransactionData,
|
||||
}: {
|
||||
value?: string;
|
||||
setTransactionData: (param: TransactionData | undefined) => void;
|
||||
}) {
|
||||
const rawTransactionInput = React.useRef<HTMLTextAreaElement>(null);
|
||||
const [error, setError] = React.useState<string>();
|
||||
const [rows, setRows] = React.useState(3);
|
||||
const query = useQuery();
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
|
||||
const onInput = React.useCallback(() => {
|
||||
const base64 = rawTransactionInput.current?.value;
|
||||
if (base64) {
|
||||
// Clear url params when input is detected
|
||||
if (query.get("message")) {
|
||||
query.delete("message");
|
||||
history.push({ ...location, search: query.toString() });
|
||||
} else if (query.get("transaction")) {
|
||||
query.delete("transaction");
|
||||
history.push({ ...location, search: query.toString() });
|
||||
}
|
||||
|
||||
// Dynamically expand height based on input length
|
||||
setRows(Math.max(3, Math.min(10, Math.round(base64.length / 150))));
|
||||
|
||||
let buffer;
|
||||
try {
|
||||
buffer = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError("Input must be base64 encoded");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (buffer.length < MIN_MESSAGE_LENGTH) {
|
||||
throw new Error("Input is not long enough to be valid.");
|
||||
} else if (buffer[0] > MAX_TRANSACTION_SIGNATURES) {
|
||||
throw new Error(`Input starts with invalid byte: "${buffer[0]}"`);
|
||||
}
|
||||
|
||||
const tx = deserializeTransaction(buffer);
|
||||
if (tx) {
|
||||
const message = tx.message;
|
||||
const rawMessage = message.serialize();
|
||||
setTransactionData({
|
||||
rawMessage,
|
||||
message,
|
||||
signatures: tx.signatures,
|
||||
});
|
||||
} else {
|
||||
const message = Message.from(buffer);
|
||||
setTransactionData({
|
||||
rawMessage: buffer,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
setError(undefined);
|
||||
return;
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
}
|
||||
} else {
|
||||
setError(undefined);
|
||||
}
|
||||
}, [setTransactionData, history, query, location]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const input = rawTransactionInput.current;
|
||||
if (input && value) {
|
||||
input.value = value;
|
||||
onInput();
|
||||
}
|
||||
}, [value]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const placeholder = "Paste raw base64 encoded transaction message";
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-header-title">Encoded Transaction Message</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<textarea
|
||||
rows={rows}
|
||||
onInput={onInput}
|
||||
ref={rawTransactionInput}
|
||||
className="form-control form-control-flush form-control-auto text-monospace"
|
||||
placeholder={placeholder}
|
||||
></textarea>
|
||||
<div className="row align-items-center">
|
||||
<div className="col d-flex align-items-center">
|
||||
{error && (
|
||||
<>
|
||||
<span className="text-warning small mr-2">
|
||||
<i className="fe fe-alert-circle"></i>
|
||||
</span>
|
||||
|
||||
<span className="text-warning">{error}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-footer">
|
||||
<h3>Instructions</h3>
|
||||
<ul>
|
||||
<li className="mb-2">
|
||||
<strong>CLI: </strong>Use <code>--dump-transaction-message</code>{" "}
|
||||
flag
|
||||
</li>
|
||||
<li className="mb-2">
|
||||
<strong>Rust: </strong>Add <code>base64</code> crate dependency and{" "}
|
||||
<code>
|
||||
println!("{}", base64::encode(&transaction.message_data()));
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
<strong>JavaScript: </strong>Add{" "}
|
||||
<code>console.log(tx.serializeMessage().toString("base64"));</code>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
import React from "react";
|
||||
import bs58 from "bs58";
|
||||
import * as nacl from "tweetnacl";
|
||||
import { Message, PublicKey } from "@solana/web3.js";
|
||||
import { Signature } from "components/common/Signature";
|
||||
import { Address } from "components/common/Address";
|
||||
|
||||
export function TransactionSignatures({
|
||||
signatures,
|
||||
message,
|
||||
rawMessage,
|
||||
}: {
|
||||
signatures: (string | null)[];
|
||||
message: Message;
|
||||
rawMessage: Uint8Array;
|
||||
}) {
|
||||
const signatureRows = React.useMemo(() => {
|
||||
return signatures.map((signature, index) => {
|
||||
const publicKey = message.accountKeys[index];
|
||||
|
||||
let verified;
|
||||
if (signature) {
|
||||
const key = publicKey.toBytes();
|
||||
const rawSignature = bs58.decode(signature);
|
||||
verified = verifySignature({
|
||||
message: rawMessage,
|
||||
signature: rawSignature,
|
||||
key,
|
||||
});
|
||||
}
|
||||
|
||||
const props = {
|
||||
index,
|
||||
signature,
|
||||
signer: publicKey,
|
||||
verified,
|
||||
};
|
||||
|
||||
return <SignatureRow key={publicKey.toBase58()} {...props} />;
|
||||
});
|
||||
}, [signatures, message, rawMessage]);
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-header-title">Signatures</h3>
|
||||
</div>
|
||||
<div className="table-responsive mb-0">
|
||||
<table className="table table-sm table-nowrap card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-muted">#</th>
|
||||
<th className="text-muted">Signature</th>
|
||||
<th className="text-muted">Signer</th>
|
||||
<th className="text-muted">Validity</th>
|
||||
<th className="text-muted">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="list">{signatureRows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function verifySignature({
|
||||
message,
|
||||
signature,
|
||||
key,
|
||||
}: {
|
||||
message: Uint8Array;
|
||||
signature: Uint8Array;
|
||||
key: Uint8Array;
|
||||
}): boolean {
|
||||
return nacl.sign.detached.verify(message, signature, key);
|
||||
}
|
||||
|
||||
function SignatureRow({
|
||||
signature,
|
||||
signer,
|
||||
verified,
|
||||
index,
|
||||
}: {
|
||||
signature: string | null;
|
||||
signer: PublicKey;
|
||||
verified?: boolean;
|
||||
index: number;
|
||||
}) {
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<span className="badge badge-soft-info mr-1">{index + 1}</span>
|
||||
</td>
|
||||
<td>
|
||||
{signature ? (
|
||||
<Signature signature={signature} truncateChars={40} />
|
||||
) : (
|
||||
"Missing Signature"
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<Address pubkey={signer} link />
|
||||
</td>
|
||||
<td>
|
||||
{verified === undefined ? (
|
||||
"N/A"
|
||||
) : verified ? (
|
||||
<span className="badge badge-soft-success mr-1">Valid</span>
|
||||
) : (
|
||||
<span className="badge badge-soft-warning mr-1">Invalid</span>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{index === 0 && (
|
||||
<span className="badge badge-soft-info mr-1">Fee Payer</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,249 @@
|
|||
import React from "react";
|
||||
import bs58 from "bs58";
|
||||
import { Connection, Message, Transaction } from "@solana/web3.js";
|
||||
import { useCluster } from "providers/cluster";
|
||||
import { TableCardBody } from "components/common/TableCardBody";
|
||||
import { programLabel } from "utils/tx";
|
||||
|
||||
type LogMessage = {
|
||||
text: string;
|
||||
prefix: string;
|
||||
style: "muted" | "info" | "success" | "warning";
|
||||
};
|
||||
|
||||
type InstructionLogs = {
|
||||
logs: LogMessage[];
|
||||
failed: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_SIGNATURE = bs58.encode(Buffer.alloc(64).fill(0));
|
||||
|
||||
export function SimulatorCard({ message }: { message: Message }) {
|
||||
const { cluster } = useCluster();
|
||||
const { simulate, simulating, simulationLogs: logs } = useSimulator(message);
|
||||
if (simulating) {
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-header-title">Transaction Simulation</h3>
|
||||
</div>
|
||||
<div className="card-body text-center">
|
||||
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
||||
Simulating
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (!logs) {
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-header-title">Transaction Simulation</h3>
|
||||
<button className="btn btn-sm d-flex btn-white" onClick={simulate}>
|
||||
Simulate
|
||||
</button>
|
||||
</div>
|
||||
<div className="card-body text-muted">
|
||||
<ul>
|
||||
<li>
|
||||
Simulation is free and will run this transaction against the
|
||||
latest confirmed ledger state.
|
||||
</li>
|
||||
<li>
|
||||
No state changes will be persisted and all signature checks will
|
||||
be disabled.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-header-title">Transaction Simulation</h3>
|
||||
<button className="btn btn-sm d-flex btn-white" onClick={simulate}>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
<TableCardBody>
|
||||
{message.instructions.map((ix, index) => {
|
||||
const programId = message.accountKeys[ix.programIdIndex];
|
||||
const programName =
|
||||
programLabel(programId.toBase58(), cluster) || "Unknown";
|
||||
const programLogs: InstructionLogs | undefined = logs[index];
|
||||
|
||||
let badgeColor = "white";
|
||||
if (programLogs) {
|
||||
badgeColor = programLogs.failed ? "warning" : "success";
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<div className="d-flex align-items-center">
|
||||
<span className={`badge badge-soft-${badgeColor} mr-2`}>
|
||||
#{index + 1}
|
||||
</span>
|
||||
{programName} Instruction
|
||||
</div>
|
||||
{programLogs && (
|
||||
<div className="d-flex align-items-start flex-column text-monospace p-2 font-size-sm">
|
||||
{programLogs.logs.map((log, key) => {
|
||||
return (
|
||||
<span key={key}>
|
||||
<span className="text-muted">{log.prefix}</span>
|
||||
<span className={`text-${log.style}`}>
|
||||
{log.text}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</TableCardBody>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useSimulator(message: Message) {
|
||||
const { cluster, url } = useCluster();
|
||||
const [simulating, setSimulating] = React.useState(false);
|
||||
const [logs, setLogs] = React.useState<Array<InstructionLogs> | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
setLogs(null);
|
||||
setSimulating(false);
|
||||
}, [url]);
|
||||
|
||||
const onClick = React.useCallback(() => {
|
||||
if (simulating) return;
|
||||
setSimulating(true);
|
||||
|
||||
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);
|
||||
|
||||
let depth = 0;
|
||||
let instructionLogs: InstructionLogs[] = [];
|
||||
const prefixBuilder = (depth: number) => {
|
||||
const prefix = new Array(depth - 1).fill("\u00A0\u00A0").join("");
|
||||
return prefix + "> ";
|
||||
};
|
||||
|
||||
let instructionError;
|
||||
const responseLogs = resp.value.logs;
|
||||
if (!responseLogs) {
|
||||
if (resp.value.err) throw new Error(JSON.stringify(resp.value.err));
|
||||
throw new Error("No logs detected");
|
||||
} else if (resp.value.err) {
|
||||
const err = resp.value.err;
|
||||
if (err && typeof err !== "string") {
|
||||
let ixError = (err as any)["InstructionError"];
|
||||
const [index, message] = ixError;
|
||||
if (typeof message === "string") {
|
||||
instructionError = { index, message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
responseLogs.forEach((log) => {
|
||||
if (log.startsWith("Program log:")) {
|
||||
instructionLogs[instructionLogs.length - 1].logs.push({
|
||||
prefix: prefixBuilder(depth),
|
||||
text: log,
|
||||
style: "muted",
|
||||
});
|
||||
} else {
|
||||
const regex = /Program (\w*) invoke \[(\d)\]/g;
|
||||
const matches = [...log.matchAll(regex)];
|
||||
|
||||
if (matches.length > 0) {
|
||||
const programAddress = matches[0][1];
|
||||
const programName =
|
||||
programLabel(programAddress, cluster) ||
|
||||
`Unknown (${programAddress}) Program`;
|
||||
|
||||
if (depth === 0) {
|
||||
instructionLogs.push({
|
||||
logs: [],
|
||||
failed: false,
|
||||
});
|
||||
} else {
|
||||
instructionLogs[instructionLogs.length - 1].logs.push({
|
||||
prefix: prefixBuilder(depth),
|
||||
style: "info",
|
||||
text: `Invoking ${programName}`,
|
||||
});
|
||||
}
|
||||
|
||||
depth++;
|
||||
} else if (log.includes("success")) {
|
||||
instructionLogs[instructionLogs.length - 1].logs.push({
|
||||
prefix: prefixBuilder(depth),
|
||||
style: "success",
|
||||
text: `Program returned success`,
|
||||
});
|
||||
depth--;
|
||||
} else if (log.includes("failed")) {
|
||||
const instructionLog =
|
||||
instructionLogs[instructionLogs.length - 1];
|
||||
if (!instructionLog.failed) {
|
||||
instructionLog.failed = true;
|
||||
instructionLog.logs.push({
|
||||
prefix: prefixBuilder(depth),
|
||||
style: "warning",
|
||||
text: `Program returned error: ${log.slice(
|
||||
log.indexOf(": ") + 2
|
||||
)}`,
|
||||
});
|
||||
}
|
||||
depth--;
|
||||
} else {
|
||||
// system transactions don't start with "Program log:"
|
||||
instructionLogs[instructionLogs.length - 1].logs.push({
|
||||
prefix: prefixBuilder(depth),
|
||||
text: log,
|
||||
style: "muted",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
instructionError &&
|
||||
instructionError.index === instructionLogs.length - 1
|
||||
) {
|
||||
const failedIx = instructionLogs[instructionError.index];
|
||||
failedIx.failed = true;
|
||||
failedIx.logs.push({
|
||||
prefix: prefixBuilder(1),
|
||||
text: `Runtime error: ${instructionError.message}`,
|
||||
style: "warning",
|
||||
});
|
||||
}
|
||||
|
||||
setLogs(instructionLogs);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setLogs(null);
|
||||
} finally {
|
||||
setSimulating(false);
|
||||
}
|
||||
})();
|
||||
}, [cluster, url, message, simulating]);
|
||||
return { simulate: onClick, simulating, simulationLogs: logs };
|
||||
}
|
|
@ -76,7 +76,7 @@ export type ProgramData =
|
|||
export interface Details {
|
||||
executable: boolean;
|
||||
owner: PublicKey;
|
||||
space?: number;
|
||||
space: number;
|
||||
data?: ProgramData;
|
||||
}
|
||||
|
||||
|
@ -143,7 +143,7 @@ async function fetchAccountInfo(
|
|||
lamports = result.lamports;
|
||||
|
||||
// Only save data in memory if we can decode it
|
||||
let space;
|
||||
let space: number;
|
||||
if (!("parsed" in result.data)) {
|
||||
space = result.data.length;
|
||||
} else {
|
||||
|
|
|
@ -6,11 +6,12 @@ import {
|
|||
TransactionConfirmationStatus,
|
||||
} from "@solana/web3.js";
|
||||
import { useCluster, Cluster } from "../cluster";
|
||||
import { DetailsProvider } from "./details";
|
||||
import { DetailsProvider } from "./parsed";
|
||||
import { RawDetailsProvider } from "./raw";
|
||||
import * as Cache from "providers/cache";
|
||||
import { ActionType, FetchStatus } from "providers/cache";
|
||||
import { reportError } from "utils/sentry";
|
||||
export { useTransactionDetails } from "./details";
|
||||
export { useTransactionDetails } from "./parsed";
|
||||
|
||||
export type Confirmations = number | "max";
|
||||
|
||||
|
@ -48,7 +49,9 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {
|
|||
return (
|
||||
<StateContext.Provider value={state}>
|
||||
<DispatchContext.Provider value={dispatch}>
|
||||
<DetailsProvider>{children}</DetailsProvider>
|
||||
<RawDetailsProvider>
|
||||
<DetailsProvider>{children}</DetailsProvider>
|
||||
</RawDetailsProvider>
|
||||
</DispatchContext.Provider>
|
||||
</StateContext.Provider>
|
||||
);
|
||||
|
|
|
@ -3,7 +3,6 @@ import {
|
|||
Connection,
|
||||
TransactionSignature,
|
||||
ParsedConfirmedTransaction,
|
||||
Transaction,
|
||||
} from "@solana/web3.js";
|
||||
import { useCluster, Cluster } from "../cluster";
|
||||
import * as Cache from "providers/cache";
|
||||
|
@ -12,7 +11,6 @@ import { reportError } from "utils/sentry";
|
|||
|
||||
export interface Details {
|
||||
transaction?: ParsedConfirmedTransaction | null;
|
||||
raw?: Transaction | null;
|
||||
}
|
||||
|
||||
type State = Cache.State<Details>;
|
||||
|
@ -121,53 +119,3 @@ export function useTransactionDetailsCache(): TransactionDetailsCache {
|
|||
|
||||
return context.entries;
|
||||
}
|
||||
|
||||
async function fetchRawTransaction(
|
||||
dispatch: Dispatch,
|
||||
signature: TransactionSignature,
|
||||
cluster: Cluster,
|
||||
url: string
|
||||
) {
|
||||
let fetchStatus;
|
||||
try {
|
||||
const response = await new Connection(url).getTransaction(signature);
|
||||
fetchStatus = FetchStatus.Fetched;
|
||||
|
||||
let data: Details = { raw: null };
|
||||
if (response !== null) {
|
||||
const { message, signatures } = response.transaction;
|
||||
data = {
|
||||
raw: Transaction.populate(message, signatures),
|
||||
};
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ActionType.Update,
|
||||
status: fetchStatus,
|
||||
key: signature,
|
||||
data,
|
||||
url,
|
||||
});
|
||||
} catch (error) {
|
||||
if (cluster !== Cluster.Custom) {
|
||||
reportError(error, { url });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useFetchRawTransaction() {
|
||||
const dispatch = React.useContext(DispatchContext);
|
||||
if (!dispatch) {
|
||||
throw new Error(
|
||||
`useFetchRawTransaaction must be used within a TransactionsProvider`
|
||||
);
|
||||
}
|
||||
|
||||
const { cluster, url } = useCluster();
|
||||
return React.useCallback(
|
||||
(signature: TransactionSignature) => {
|
||||
url && fetchRawTransaction(dispatch, signature, cluster, url);
|
||||
},
|
||||
[dispatch, cluster, url]
|
||||
);
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
import React from "react";
|
||||
import {
|
||||
Connection,
|
||||
TransactionSignature,
|
||||
Transaction,
|
||||
Message,
|
||||
} from "@solana/web3.js";
|
||||
import { useCluster, Cluster } from "../cluster";
|
||||
import * as Cache from "providers/cache";
|
||||
import { ActionType, FetchStatus } from "providers/cache";
|
||||
import { reportError } from "utils/sentry";
|
||||
|
||||
export interface Details {
|
||||
raw?: {
|
||||
transaction: Transaction;
|
||||
message: Message;
|
||||
signatures: string[];
|
||||
} | null;
|
||||
}
|
||||
|
||||
type State = Cache.State<Details>;
|
||||
type Dispatch = Cache.Dispatch<Details>;
|
||||
|
||||
export const StateContext = React.createContext<State | undefined>(undefined);
|
||||
export const DispatchContext = React.createContext<Dispatch | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
type DetailsProviderProps = { children: React.ReactNode };
|
||||
export function RawDetailsProvider({ children }: DetailsProviderProps) {
|
||||
const { url } = useCluster();
|
||||
const [state, dispatch] = Cache.useReducer<Details>(url);
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch({ type: ActionType.Clear, url });
|
||||
}, [dispatch, url]);
|
||||
|
||||
return (
|
||||
<StateContext.Provider value={state}>
|
||||
<DispatchContext.Provider value={dispatch}>
|
||||
{children}
|
||||
</DispatchContext.Provider>
|
||||
</StateContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useRawTransactionDetails(
|
||||
signature: TransactionSignature
|
||||
): Cache.CacheEntry<Details> | undefined {
|
||||
const context = React.useContext(StateContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
`useRawTransactionDetails must be used within a TransactionsProvider`
|
||||
);
|
||||
}
|
||||
|
||||
return context.entries[signature];
|
||||
}
|
||||
|
||||
async function fetchRawTransaction(
|
||||
dispatch: Dispatch,
|
||||
signature: TransactionSignature,
|
||||
cluster: Cluster,
|
||||
url: string
|
||||
) {
|
||||
let fetchStatus;
|
||||
try {
|
||||
const response = await new Connection(url).getTransaction(signature);
|
||||
fetchStatus = FetchStatus.Fetched;
|
||||
|
||||
let data: Details = { raw: null };
|
||||
if (response !== null) {
|
||||
const { message, signatures } = response.transaction;
|
||||
data = {
|
||||
raw: {
|
||||
message,
|
||||
signatures,
|
||||
transaction: Transaction.populate(message, signatures),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ActionType.Update,
|
||||
status: fetchStatus,
|
||||
key: signature,
|
||||
data,
|
||||
url,
|
||||
});
|
||||
} catch (error) {
|
||||
if (cluster !== Cluster.Custom) {
|
||||
reportError(error, { url });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useFetchRawTransaction() {
|
||||
const dispatch = React.useContext(DispatchContext);
|
||||
if (!dispatch) {
|
||||
throw new Error(
|
||||
`useFetchRawTransaction must be used within a TransactionsProvider`
|
||||
);
|
||||
}
|
||||
|
||||
const { cluster, url } = useCluster();
|
||||
return React.useCallback(
|
||||
(signature: TransactionSignature) => {
|
||||
url && fetchRawTransaction(dispatch, signature, cluster, url);
|
||||
},
|
||||
[dispatch, cluster, url]
|
||||
);
|
||||
}
|
|
@ -91,6 +91,10 @@ ul.log-messages {
|
|||
bottom: 0;
|
||||
}
|
||||
|
||||
.border-bottom-none {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
.opacity-50 {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { ReactNode } from "react";
|
||||
import React from "react";
|
||||
import BN from "bn.js";
|
||||
import {
|
||||
HumanizeDuration,
|
||||
|
@ -51,15 +51,25 @@ export function lamportsToSol(lamports: number | BN): number {
|
|||
export function lamportsToSolString(
|
||||
lamports: number | BN,
|
||||
maximumFractionDigits: number = 9
|
||||
): ReactNode {
|
||||
): string {
|
||||
const sol = lamportsToSol(lamports);
|
||||
return new Intl.NumberFormat("en-US", { maximumFractionDigits }).format(sol);
|
||||
}
|
||||
|
||||
export function SolBalance({
|
||||
lamports,
|
||||
maximumFractionDigits = 9,
|
||||
}: {
|
||||
lamports: number | BN;
|
||||
maximumFractionDigits?: number;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<span>
|
||||
◎
|
||||
<span className="text-monospace">
|
||||
{new Intl.NumberFormat("en-US", { maximumFractionDigits }).format(sol)}
|
||||
{lamportsToSolString(lamports, maximumFractionDigits)}
|
||||
</span>
|
||||
</>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,20 +5,23 @@ export function useQuery() {
|
|||
return new URLSearchParams(useLocation().search);
|
||||
}
|
||||
|
||||
export const clusterPath = (pathname: string) => {
|
||||
export const clusterPath = (pathname: string, params?: URLSearchParams) => {
|
||||
return (location: Location) => ({
|
||||
...pickClusterParams(location),
|
||||
...pickClusterParams(location, params),
|
||||
pathname,
|
||||
});
|
||||
};
|
||||
|
||||
export function pickClusterParams(location: Location): Location {
|
||||
export function pickClusterParams(
|
||||
location: Location,
|
||||
newParams?: URLSearchParams
|
||||
): Location {
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
const cluster = urlParams.get("cluster");
|
||||
const customUrl = urlParams.get("customUrl");
|
||||
|
||||
// Pick the params we care about
|
||||
const newParams = new URLSearchParams();
|
||||
newParams = newParams || new URLSearchParams();
|
||||
if (cluster) newParams.set("cluster", cluster);
|
||||
if (customUrl) newParams.set("customUrl", customUrl);
|
||||
|
||||
|
@ -27,36 +30,3 @@ export function pickClusterParams(location: Location): Location {
|
|||
search: newParams.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
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