import React from "react"; import { PACKET_DATA_SIZE, VersionedMessage } from "@solana/web3.js"; import { TableCardBody } from "components/common/TableCardBody"; import { SolBalance } from "components/common/SolBalance"; 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 { AddressTableLookupsCard } from "./AddressTableLookupsCard"; import { AddressWithContext, createFeePayerValidator, } from "./AddressWithContext"; import { SimulatorCard } from "./SimulatorCard"; import { MIN_MESSAGE_LENGTH, RawInput } from "./RawInputCard"; import { InstructionsSection } from "./InstructionsSection"; import base58 from "bs58"; import { useFetchAccountInfo } from "providers/accounts"; export type TransactionData = { rawMessage: Uint8Array; message: VersionedMessage; signatures?: (string | null)[]; }; // Decode a url param and return the result. If decoding fails, return whether // the param should be deleted. function decodeParam(params: URLSearchParams, name: string): string | boolean { const param = params.get(name); if (param === null) return false; try { return decodeURIComponent(param); } catch (err) { return true; } } // Decode a signatures param and throw an error on failure function decodeSignatures(signaturesParam: string): (string | null)[] { let signatures; try { signatures = JSON.parse(signaturesParam); } catch (err) { throw new Error("Signatures param is not valid JSON"); } if (!Array.isArray(signatures)) { throw new Error("Signatures param is not a JSON array"); } const validSignatures: (string | null)[] = []; for (const signature of signatures) { if (signature === null) { validSignatures.push(signature); continue; } if (typeof signature !== "string") { throw new Error("Signature is not a string"); } try { base58.decode(signature); validSignatures.push(signature); } catch (err) { throw new Error("Signature is not valid base58"); } } return validSignatures; } // Decodes url params into transaction data if possible. If decoding fails, // URL params are returned as a string that will prefill the transaction // message input field for debugging. Returns a tuple of [result, shouldRefreshUrl] function decodeUrlParams( params: URLSearchParams ): [TransactionData | string, boolean] { const messageParam = decodeParam(params, "message"); const signaturesParam = decodeParam(params, "signatures"); let refreshUrl = false; if (signaturesParam === true) { params.delete("signatures"); refreshUrl = true; } if (typeof messageParam === "boolean") { if (messageParam) { params.delete("message"); params.delete("signatures"); refreshUrl = true; } return ["", refreshUrl]; } let signatures: (string | null)[] | undefined = undefined; if (typeof signaturesParam === "string") { try { signatures = decodeSignatures(signaturesParam); } catch (err) { params.delete("signatures"); refreshUrl = true; } } try { const buffer = Uint8Array.from(atob(messageParam), (c) => c.charCodeAt(0)); if (buffer.length < MIN_MESSAGE_LENGTH) { throw new Error("message buffer is too short"); } const message = VersionedMessage.deserialize(buffer); const data = { message, rawMessage: buffer, signatures, }; return [data, refreshUrl]; } catch (err) { params.delete("message"); refreshUrl = true; return [messageParam, true]; } } export function TransactionInspectorPage({ signature, }: { signature?: string; }) { const [transaction, setTransaction] = React.useState(); const query = useQuery(); const history = useHistory(); const location = useLocation(); const [paramString, setParamString] = React.useState(); // Sync message with url search params React.useEffect(() => { if (signature) return; if (transaction) { let shouldRefreshUrl = false; if (transaction.signatures !== undefined) { const signaturesParam = encodeURIComponent( JSON.stringify(transaction.signatures) ); if (query.get("signatures") !== signaturesParam) { shouldRefreshUrl = true; query.set("signatures", signaturesParam); } } const base64 = btoa( String.fromCharCode.apply(null, [...transaction.rawMessage]) ); const newParam = encodeURIComponent(base64); if (query.get("message") !== newParam) { shouldRefreshUrl = true; query.set("message", newParam); } if (shouldRefreshUrl) { 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; const [result, refreshUrl] = decodeUrlParams(query); if (refreshUrl) { history.push({ ...location, search: query.toString() }); } if (typeof result === "string") { setParamString(result); } else { setTransaction(result); } }, [query, transaction, signature, history, location]); return (

Transaction Inspector

{signature ? ( ) : transaction ? ( ) : ( )}
); } 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 ; } else if (details.status === FetchStatus.FetchFailed) { return ( ); } else if (!transaction) { return ( ); } const { message, signatures } = transaction; const tx = { message, rawMessage: message.serialize(), signatures }; return ; } function LoadedView({ transaction, onClear, }: { transaction: TransactionData; onClear: () => void; }) { const { message, rawMessage, signatures } = transaction; const fetchAccountInfo = useFetchAccountInfo(); React.useEffect(() => { for (let lookup of message.addressTableLookups) { fetchAccountInfo(lookup.accountKey, "parsed"); } }, [message, fetchAccountInfo]); return ( <> {signatures && ( )} ); } const DEFAULT_FEES = { lamportsPerSignature: 5000, }; function OverviewCard({ message, raw, onClear, }: { message: VersionedMessage; 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 ( <>

Transaction Overview

Serialized Size
{size} bytes Max transaction size is {PACKET_DATA_SIZE} bytes
Fees
{`Each signature costs ${DEFAULT_FEES.lamportsPerSignature} lamports`}
Fee payer Signer Writable
{message.staticAccountKeys.length === 0 ? ( "No Fee Payer" ) : ( )}
); }