import { useState, useEffect } from 'react'; import { getBalance, getUtxos, getTransaction, getRecentTransactions, formatToshis } from '../lib/api'; import type { Balance, UTXO, Transaction, RecentTransaction } from '../lib/api'; import { Stack, Divider } from './ui/Layout'; import { Terminal, Section, LabelValue, DataTable, DataRow, ActionBar, Message, } from './ui/Terminal'; type SearchResult = | { type: 'account'; did: string; handle?: string; balance: Balance; utxos: UTXO[] } | { type: 'transaction'; txId: string; transaction: Transaction } | null; const handleCache = new Map(); async function resolveHandle(input: string): Promise<{ did: string; handle?: string } | null> { // If it's already a DID, return it if (input.startsWith('did:')) { // Try to get handle for display try { const res = await fetch(`https://plc.directory/${input}`); if (res.ok) { const doc = await res.json(); const handle = doc.alsoKnownAs?.[0]?.replace('at://', ''); if (handle) handleCache.set(input, handle); return { did: input, handle }; } } catch {} return { did: input }; } // Remove @ prefix if present const handle = input.startsWith('@') ? input.slice(1) : input; // Check cache if (handleCache.has(handle)) { return { did: handleCache.get(handle)!, handle }; } // Resolve handle to DID try { const res = await fetch(`https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`); if (res.ok) { const data = await res.json(); handleCache.set(handle, data.did); return { did: data.did, handle }; } } catch {} return null; } async function resolveDid(did: string): Promise { if (handleCache.has(did)) return handleCache.get(did)!; try { // For did:web, extract the domain directly if (did.startsWith('did:web:')) { const domain = did.replace('did:web:', ''); handleCache.set(did, domain); return domain; } // For did:plc, use PLC directory if (did.startsWith('did:plc:')) { const res = await fetch(`https://plc.directory/${did}`); if (res.ok) { const doc = await res.json(); const handle = doc.alsoKnownAs?.[0]?.replace('at://', '') || did; handleCache.set(did, handle); return handle; } } } catch {} return did; } export default function Explorer() { const [query, setQuery] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [result, setResult] = useState(null); const [resolvedHandles, setResolvedHandles] = useState>(new Map()); const [recentTxs, setRecentTxs] = useState([]); const [loadingRecent, setLoadingRecent] = useState(true); // Load recent transactions on mount useEffect(() => { const loadRecent = async () => { try { const data = await getRecentTransactions(20); setRecentTxs(data.transactions); } catch (e) { console.error('Failed to load recent transactions:', e); } finally { setLoadingRecent(false); } }; loadRecent(); }, []); const search = async () => { if (!query.trim()) return; setLoading(true); setError(null); setResult(null); const input = query.trim(); try { // Check if it looks like a transaction ID (TID format: 13 chars, alphanumeric) const isTxId = /^[a-z0-9]{13}$/i.test(input); if (isTxId) { // Search for transaction const txResult = await getTransaction(input); // Resolve handles for display const dids = new Set(); txResult.transaction.outputs.forEach(o => dids.add(o.owner)); const trigger = txResult.transaction.trigger; if (trigger) { if ('followerDid' in trigger) { dids.add(trigger.followerDid); } else if ('senderDid' in trigger) { dids.add(trigger.senderDid); } } const handles = new Map(); await Promise.all( Array.from(dids).map(async did => { const handle = await resolveDid(did); handles.set(did, handle); }) ); setResolvedHandles(handles); setResult({ type: 'transaction', txId: input, transaction: txResult.transaction, }); } else { // Search for account const resolved = await resolveHandle(input); if (!resolved) { throw new Error('Could not resolve handle or DID'); } const [balanceResult, utxosResult] = await Promise.all([ getBalance(resolved.did), getUtxos(resolved.did), ]); setResult({ type: 'account', did: resolved.did, handle: resolved.handle, balance: balanceResult, utxos: utxosResult.utxos, }); } } catch (e) { setError(e instanceof Error ? e.message : 'Search failed'); } finally { setLoading(false); } }; const formatDid = (did: string) => { const handle = resolvedHandles.get(did); if (handle && handle !== did) { return `@${handle}`; } return did.length > 24 ? `${did.slice(0, 24)}...` : did; }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { search(); } }; return (
{' '} setQuery(e.target.value)} placeholder="@handle, did:plc:..., or txid" style={{ width: '300px' }} />
{error && ( <>
{error}
)} {/* Show recent transactions when no search result */} {!result && !error && ( <>
{loadingRecent ? (

Loading...

) : recentTxs.length === 0 ? (

No transactions yet

) : ( {recentTxs.map((tx) => ( {tx.txId}, ]} widths={['5rem', '10rem', '8rem', '1fr']} /> ))} )}
)} {result?.type === 'account' && ( <>
{result.handle && ( )} 40 ? `${result.did.slice(0, 40)}...` : result.did} />
{result.utxos.length > 0 && ( <>
{result.utxos.slice(0, 10).map((utxo, i) => ( {utxo.txId}, ]} widths={['2rem', '10rem', '1fr']} /> ))} {result.utxos.length > 10 && (

...and {result.utxos.length - 10} more

)}
)} )} {result?.type === 'transaction' && ( <>
{result.transaction.trigger && 'followerDid' in result.transaction.trigger && ( )} {result.transaction.trigger && 'senderDid' in result.transaction.trigger && ( )}
{result.transaction.outputs.map((output, i) => ( ))}
{result.transaction.inputs.length > 0 && ( <>
{result.transaction.inputs.map((input, i) => ( {input.txId}:{input.index}, ]} widths={['2rem', '1fr']} /> ))}
)} )} [WALLET] [WHITEPAPER]
); }