import type { IndexedOutput } from '@1sat/types'
import { useState } from 'react'
import type { EnrichedOrdinal, TokenBalance } from '../lib/scanner'
import { SWEEP_BATCH_SIZE } from '../lib/sweeper'
import { formatSats, formatTokenAmount } from '../lib/utils'
import { Badge } from './ui/badge'
import { Button } from './ui/button'
import { Input } from './ui/input'
function isImageType(ct: string): boolean {
return ct.startsWith('image/') && ct !== 'image/svg+xml'
}
function OrdinalCard({
ordinal,
isSelected,
onToggle,
}: { ordinal: EnrichedOrdinal; isSelected: boolean; onToggle: () => void }) {
const ct = ordinal.contentType ?? ''
const isImage = isImageType(ct)
const subtype = ct.includes('/') ? ct.split('/')[1] : ct
return (
{!ordinal.contentUrl ? (
{'\u25C6'}
) : isImage ? (
) : (
)}
)
}
export function FundingSection({
funding,
totalBsv,
sweepAmount,
onSweepAmountChange,
onSweep,
onSend,
walletConnected,
}: {
funding: IndexedOutput[]
totalBsv: number
sweepAmount: number | null
onSweepAmountChange: (amount: number | null) => void
onSweep: () => void
onSend?: (destination: string) => void
walletConnected: boolean
}) {
const [address, setAddress] = useState('')
if (funding.length === 0) return null
const isMax = sweepAmount === null
const displayAmount = isMax ? formatSats(totalBsv) : formatSats(sweepAmount!)
return (
BSV Funding
{formatSats(totalBsv)} sats
{(totalBsv / 100_000_000).toFixed(8)} BSV
{funding.length} UTXO{funding.length !== 1 ? 's' : ''}
{
const val = e.target.value
onSweepAmountChange(
val === '' ? null : Math.max(0, Math.min(totalBsv, Number(val))),
)
}}
className="flex-1 font-mono"
/>
sats
onSweepAmountChange(null)}
disabled={isMax}
>
Max
{onSend && (
setAddress(e.target.value)}
className="font-mono text-xs"
/>
)}
{onSend && (
onSend(address.trim())}
>
Send {displayAmount} sats
)}
{isMax ? 'Sweep All' : `Sweep ${displayAmount} sats`}
)
}
export function OrdinalsSection({
ordinals,
selectedOrdinals,
onToggle,
onSelectPage,
onDeselectAll,
onSweep,
onSweepAll,
onSend,
onBurn,
walletConnected,
}: {
ordinals: EnrichedOrdinal[]
selectedOrdinals: Set
onToggle: (outpoint: string) => void
onSelectPage: (outpoints: string[]) => void
onDeselectAll: () => void
onSweep: () => void
onSweepAll: () => void
onSend?: (destination: string) => void
onBurn?: () => void
walletConnected: boolean
}) {
const [page, setPage] = useState(0)
const [address, setAddress] = useState('')
if (ordinals.length === 0) return null
const totalPages = Math.ceil(ordinals.length / SWEEP_BATCH_SIZE)
const safePage = Math.min(page, totalPages - 1)
const start = safePage * SWEEP_BATCH_SIZE
const pageItems = ordinals.slice(start, start + SWEEP_BATCH_SIZE)
const pageOutpoints = pageItems.map((o) => o.outpoint)
const selectedCount = selectedOrdinals.size
const batchCount = Math.ceil(ordinals.length / SWEEP_BATCH_SIZE)
return (
Ordinals
{ordinals.length} inscription{ordinals.length !== 1 ? 's' : ''}
{selectedCount > 0 && (
({selectedCount} selected)
)}
· {SWEEP_BATCH_SIZE}/page
onSelectPage(pageOutpoints)}
>
Select page
Deselect
{pageItems.map((ord) => (
onToggle(ord.outpoint)}
/>
))}
{totalPages > 1 && (
setPage(safePage - 1)}
disabled={safePage === 0}
>
Prev
Page {safePage + 1} of {totalPages}
setPage(safePage + 1)}
disabled={safePage >= totalPages - 1}
>
Next
)}
{selectedCount > 0 && onSend && (
setAddress(e.target.value)}
className="font-mono text-xs"
/>
)}
{selectedCount > 0 && onSend && (
onSend(address.trim())}
>
Send {selectedCount}
)}
{selectedCount > 0 && (
Sweep {selectedCount}
{selectedCount > SWEEP_BATCH_SIZE
? ` (${Math.ceil(selectedCount / SWEEP_BATCH_SIZE)} batches)`
: ''}
)}
0 ? 'outline' : 'default'}
size="sm"
className="flex-1 min-w-[6rem]"
onClick={onSweepAll}
disabled={!walletConnected}
title={
walletConnected ? undefined : 'Connect BRC-100 wallet to sweep'
}
>
Sweep all
{batchCount > 1 ? ` (${batchCount} batches)` : ''}
{selectedCount > 0 && onBurn && (
{
if (
window.confirm(
`Permanently burn ${selectedCount} ordinal${selectedCount !== 1 ? 's' : ''}? This cannot be undone.`,
)
)
onBurn()
}}
>
Burn
)}
)
}
function TokenRow({
tb,
onSweep,
walletConnected,
}: {
tb: TokenBalance
onSweep?: (tokenId: string) => void
walletConnected: boolean
}) {
return (
{
;(e.target as HTMLImageElement).style.display = 'none'
}}
/>
{tb.symbol || tb.tokenId.slice(0, 8) + '...'}
{tb.isActive ? (
active
) : (
inactive
)}
{formatTokenAmount(tb.totalAmount.toString(), tb.decimals)}{' '}
{tb.symbol || ''}
({tb.outputs.length} output{tb.outputs.length !== 1 ? 's' : ''})
{tb.isActive && onSweep && (
onSweep(tb.tokenId)}
disabled={!walletConnected}
title={
walletConnected ? undefined : 'Connect BRC-100 wallet to sweep'
}
>
Sweep to Wallet
)}
)
}
export function Bsv21Section({
tokens,
onSweep,
walletConnected,
}: {
tokens: TokenBalance[]
onSweep?: (tokenId: string) => void
walletConnected: boolean
}) {
if (tokens.length === 0) return null
const active = tokens.filter((t) => t.isActive)
const inactive = tokens.filter((t) => !t.isActive)
return (
BSV-21 Tokens
{active.map((tb) => (
))}
{inactive.length > 0 && active.length > 0 && (
Inactive overlays ({inactive.length}) — cannot be swept
)}
{inactive.map((tb) => (
))}
)
}
export function Bsv20Section({ tokens }: { tokens: IndexedOutput[] }) {
if (tokens.length === 0) return null
return (
BSV-20 Tokens
Cannot be swept automatically.
{tokens.slice(0, 10).map((o) => {
const tickEvent = o.events?.find((e) => e.startsWith('tick:'))
const tick = tickEvent ? tickEvent.slice(5) : 'Token'
return (
{tick}
)
})}
{tokens.length > 10 && (
+{tokens.length - 10} more
)}
)
}
export function LockedSection({ locked }: { locked: IndexedOutput[] }) {
if (locked.length === 0) return null
return (
Locked Outputs
{locked.length} locked output{locked.length !== 1 ? 's' : ''}. These are
in contracts and cannot be swept directly.
)
}
export function RunSection({ run }: { run: IndexedOutput[] }) {
if (run.length === 0) return null
const totalSats = run.reduce((sum, o) => sum + (o.satoshis ?? 0), 0)
return (
RUN Protocol Tokens
{run.length} output{run.length !== 1 ? 's' : ''} (
{totalSats.toLocaleString()} sats). These are RUN protocol token outputs
and cannot be swept as BSV.
)
}