import { getBase58Encoder } from '@solana/kit'; import { PublicKey, VersionedMessage } from '@solana/web3.js'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import React from 'react'; import { AlertCircle } from 'react-feather'; import { Button } from '@/app/components/shared/ui/button'; import { Logger } from '@/app/shared/lib/logger'; import { MIN_MESSAGE_LENGTH, parseTransactionBytes } from '@/app/shared/lib/parse-transaction-bytes'; import { bridgeV1MessageBytes, isV1MessageBytes } from '@/app/shared/lib/v1-message-bridge'; import { Card, CardBody, CardFooter, CardHeader, CardTitle } from '@/app/shared/ui/Card'; import { FormControl } from '@/app/shared/ui/FormControl'; import { TabsContent, TabsList, TabsTrigger } from '@/app/shared/ui/Tabs'; import type { InspectorData, TransactionData } from './InspectorPage'; const BASE58_ENCODER = getBase58Encoder(); export { MIN_MESSAGE_LENGTH }; function getTransactionDataFromUserSuppliedBytes(bytes: Uint8Array): TransactionData { const { messageBytes, signatures } = parseTransactionBytes(bytes); if (isV1MessageBytes(messageBytes)) { const { message, transactionConfig } = bridgeV1MessageBytes(messageBytes); return { message, rawMessage: messageBytes, transactionConfig, version: 1, ...(signatures ? { signatures } : undefined), }; } const message = VersionedMessage.deserialize(messageBytes); return { message, rawMessage: messageBytes, ...(signatures ? { signatures } : undefined), }; } function parseAccountAddresses(input: string): string[] { // Split by commas, newlines, or spaces and filter out empty strings return ( input // eslint-disable-next-line no-restricted-syntax -- split by whitespace and comma delimiters .split(/[\s,]+/) .map(addr => addr.trim()) .filter(addr => addr.length > 0) ); } type TabData = { id: string; label: string; content: React.ReactNode; }; function TabInstructions() { const [activeTab, setActiveTab] = React.useState('cli'); const tabs: TabData[] = [ { content: (
Use --dump-transaction-message flag
), id: 'cli', label: 'CLI', }, { content: (
Add base64 crate dependency and{' '} println!("{}", base64::encode(&transaction.message_data()));
), id: 'rust', label: 'Rust', }, { content: (
@solana/web3.js < 2.0.0
Legacy Transaction:
console.log(tx.serializeMessage().toString("base64"));
Versioned Transaction:
console.log(Buffer.from(tx.serialize()).toString("base64"));
@solana/web3.js >= 2.0.0
Legacy Transaction:
console.log(getBase64EncodedWireTransaction(tx));
), id: 'ts', label: 'TypeScript', }, { content: (
Add vault_transaction from{' '} https://app.squads.so/squads/<squad_id>/transactions/<vault_transaction>
), id: 'squads', label: 'Squads', }, ]; return (
{tabs.map(tab => ( setActiveTab(tab.id)} > {tab.label} ))}
{tabs.map(tab => ( {tab.content} ))}
); } export function RawInput({ value, setTransactionData, }: { value?: string; setTransactionData: (param: InspectorData | undefined) => void; }) { const rawInput = React.useRef(null); const [error, setError] = React.useState(); const [rows, setRows] = React.useState(3); const currentPathname = usePathname(); const currentSearchParams = useSearchParams(); const router = useRouter(); const onInput = React.useCallback(() => { const input = rawInput.current?.value; if (!input) { setError(undefined); return; } // Clear url params when input is detected if (currentSearchParams?.get('message')) { const nextQueryParams = new URLSearchParams(currentSearchParams?.toString()); nextQueryParams.delete('message'); const queryString = nextQueryParams.toString(); router.push(`${currentPathname}${queryString ? `?${queryString}` : ''}`); } else if (currentSearchParams?.get('transaction')) { const nextQueryParams = new URLSearchParams(currentSearchParams?.toString()); nextQueryParams.delete('transaction'); const queryString = nextQueryParams.toString(); router.push(`${currentPathname}${queryString ? `?${queryString}` : ''}`); } // Dynamically expand height based on input length setRows(Math.max(3, Math.min(10, Math.round(input.length / 150)))); let buffer; // First try to parse as an account address try { const accounts = parseAccountAddresses(input); if (accounts.length > 0) { if (accounts.length > 1) { setError('Please provide only one account address'); return; } // Necessary to validate the account address new PublicKey(accounts[0]); setTransactionData({ account: accounts[0] }); setError(undefined); return; } } catch (err) { if (err instanceof Error) setError(err.message); } try { // Try base58 decode, use result as Uint8Array buffer = new Uint8Array(BASE58_ENCODER.encode(input)); } catch (_err) { // If base58 fails, try base64 try { buffer = Uint8Array.from(atob(input), c => c.charCodeAt(0)); } catch (err) { Logger.error(err); setError('Input must be base58/base64 encoded or a valid account address'); return; } } try { if (buffer.length < MIN_MESSAGE_LENGTH) { throw new Error('Input is not long enough to be a valid transaction message.'); } const transactionData = getTransactionDataFromUserSuppliedBytes(buffer); setTransactionData(transactionData); setError(undefined); } catch (err) { if (err instanceof Error) setError(err.message); } }, [currentSearchParams, router, currentPathname, setTransactionData]); const clearInput = React.useCallback(() => { if (rawInput.current) { rawInput.current.value = ''; setError(undefined); setTransactionData(undefined); } // Clear URL params if they exist if (currentSearchParams?.get('message') || currentSearchParams?.get('transaction')) { const nextQueryParams = new URLSearchParams(currentSearchParams?.toString()); nextQueryParams.delete('message'); nextQueryParams.delete('transaction'); const queryString = nextQueryParams.toString(); router.push(`${currentPathname}${queryString ? `?${queryString}` : ''}`); } }, [currentSearchParams, router, currentPathname, setTransactionData]); React.useEffect(() => { const input = rawInput.current; if (input && value) { input.value = value; onInput(); } }, [value, onInput]); const placeholder = 'Paste a raw base58/base64 encoded transaction message or Squads vault transaction account'; return (
Inspector Input