{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sweep-wallet",
  "title": "Sweep Wallet",
  "author": "Satchmo <https://bigblocks.dev>",
  "description": "Sweep assets from a WIF private key into the connected wallet. Scans for funding UTXOs, ordinals, and BSV-21 tokens, previews found assets, and sweeps with progress.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "input",
    "label",
    "separator",
    "skeleton"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/sweep-wallet/index.tsx",
      "content": "\"use client\"\n\nimport { SweepWalletUi, type SweepWalletUiProps } from \"./sweep-wallet-ui\"\nimport {\n  useSweepWallet,\n  type ScanResult,\n  type SweepResult,\n  type UseSweepWalletOptions,\n  type UseSweepWalletReturn,\n} from \"./use-sweep-wallet\"\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport { SweepWalletUi, type SweepWalletUiProps } from \"./sweep-wallet-ui\"\nexport {\n  useSweepWallet,\n  type ScanResult,\n  type SweepResult,\n  type SweepStep,\n  type SweepFundingUtxo,\n  type SweepOrdinalUtxo,\n  type SweepTokenGroup,\n  type UseSweepWalletOptions,\n  type UseSweepWalletReturn,\n} from \"./use-sweep-wallet\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Props for the composed SweepWallet block */\nexport interface SweepWalletProps {\n  /** Scan a WIF for spendable assets */\n  onScan: (wif: string) => Promise<ScanResult>\n  /** Execute the sweep transaction */\n  onSweep: (wif: string, assets: ScanResult) => Promise<SweepResult>\n  /** Callback after a successful sweep */\n  onSuccess?: (result: SweepResult) => void\n  /** Callback on any error */\n  onError?: (error: Error) => void\n  /** Optional CSS class */\n  className?: string\n}\n\n// ---------------------------------------------------------------------------\n// Composed component\n// ---------------------------------------------------------------------------\n\n/**\n * Full sweep-wallet block: WIF input, scan, preview, sweep, and success/error states.\n *\n * Wires `useSweepWallet` (logic) into `SweepWalletUi` (presentation).\n * The caller provides `onScan` and `onSweep` callbacks that integrate with\n * `@1sat/actions` sweep utilities.\n *\n * @example\n * ```tsx\n * import { SweepWallet } from \"@/components/blocks/sweep-wallet\"\n *\n * <SweepWallet\n *   onScan={async (wif) => {\n *     const pk = PrivateKey.fromWif(wif)\n *     const address = pk.toPublicKey().toAddress()\n *     return await scanAddressUtxos(services, address)\n *   }}\n *   onSweep={async (wif, assets) => {\n *     const inputs = await prepareSweepInputs(ctx, assets.funding)\n *     return await sweepBsv.execute(ctx, { inputs, wif })\n *   }}\n * />\n * ```\n */\nexport function SweepWallet({\n  onScan,\n  onSweep,\n  onSuccess,\n  onError,\n  className,\n}: SweepWalletProps) {\n  const hook = useSweepWallet({ onScan, onSweep, onSuccess, onError })\n\n  return (\n    <SweepWalletUi\n      step={hook.step}\n      wifInput={hook.wifInput}\n      onWifInputChange={hook.setWifInput}\n      isWifValid={hook.isWifValid}\n      scanResult={hook.scanResult}\n      sweepResult={hook.sweepResult}\n      error={hook.error}\n      isLoading={hook.isLoading}\n      onScan={hook.handleScan}\n      onSweep={hook.handleSweep}\n      onReset={hook.handleReset}\n      className={className}\n    />\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/sweep-wallet/index.tsx"
    },
    {
      "path": "registry/new-york/blocks/sweep-wallet/sweep-wallet-ui.tsx",
      "content": "\"use client\"\n\nimport {\n  AlertCircle,\n  CheckCircle2,\n  ClipboardPaste,\n  Coins,\n  ImageIcon,\n  KeyRound,\n  Loader2,\n  RotateCcw,\n  Search,\n  Wallet,\n} from \"lucide-react\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { cn } from \"@/lib/utils\"\nimport type {\n  ScanResult,\n  SweepResult,\n  SweepStep,\n} from \"./use-sweep-wallet\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SweepWalletUiProps {\n  /** Current workflow step */\n  step: SweepStep\n  /** Current WIF input value */\n  wifInput: string\n  /** Update the WIF input */\n  onWifInputChange: (value: string) => void\n  /** Whether the WIF looks valid */\n  isWifValid: boolean\n  /** Scanned assets */\n  scanResult: ScanResult | null\n  /** Sweep result */\n  sweepResult: SweepResult | null\n  /** Error message */\n  error: string | null\n  /** Whether an async operation is in flight */\n  isLoading: boolean\n  /** Initiate scan */\n  onScan: () => void\n  /** Initiate sweep */\n  onSweep: () => void\n  /** Reset to input step */\n  onReset: () => void\n  /** Optional CSS class */\n  className?: string\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst SATS_PER_BSV = 100_000_000\n\nfunction formatSats(sats: number): string {\n  if (sats >= SATS_PER_BSV) {\n    return `${(sats / SATS_PER_BSV).toFixed(8)} BSV`\n  }\n  return `${sats.toLocaleString()} sats`\n}\n\n// ---------------------------------------------------------------------------\n// Sub-components\n// ---------------------------------------------------------------------------\n\nfunction ScanningState() {\n  return (\n    <div className=\"flex flex-col items-center gap-4 py-6\">\n      <Loader2 className=\"size-8 animate-spin text-muted-foreground\" />\n      <div className=\"flex flex-col items-center gap-1\">\n        <p className=\"text-sm font-medium\">Scanning for assets...</p>\n        <p className=\"text-xs text-muted-foreground\">\n          Checking funding UTXOs, ordinals, and tokens\n        </p>\n      </div>\n      <div className=\"flex w-full flex-col gap-2\">\n        <Skeleton className=\"h-4 w-full\" />\n        <Skeleton className=\"h-4 w-3/4\" />\n        <Skeleton className=\"h-4 w-1/2\" />\n      </div>\n    </div>\n  )\n}\n\nfunction AssetPreview({ scanResult }: { scanResult: ScanResult }) {\n  const { funding, ordinals, tokens, totalSats } = scanResult\n  const fundingCount = funding.length\n  const ordinalCount = ordinals.length\n  const tokenCount = tokens.length\n\n  return (\n    <div className=\"flex flex-col gap-3\">\n      <p className=\"text-sm font-medium\">Found Assets</p>\n\n      {/* Funding */}\n      {fundingCount > 0 && (\n        <div className=\"flex items-center justify-between rounded-md border bg-muted/50 px-3 py-2\">\n          <div className=\"flex items-center gap-2\">\n            <Wallet className=\"size-4 text-muted-foreground\" aria-hidden=\"true\" />\n            <span className=\"text-sm\">BSV</span>\n          </div>\n          <div className=\"flex items-center gap-2\">\n            <span className=\"text-sm font-medium\">{formatSats(totalSats)}</span>\n            <Badge variant=\"secondary\" className=\"text-xs\">\n              {fundingCount} UTXO{fundingCount !== 1 ? \"s\" : \"\"}\n            </Badge>\n          </div>\n        </div>\n      )}\n\n      {/* Ordinals */}\n      {ordinalCount > 0 && (\n        <div className=\"flex items-center justify-between rounded-md border bg-muted/50 px-3 py-2\">\n          <div className=\"flex items-center gap-2\">\n            <ImageIcon className=\"size-4 text-muted-foreground\" aria-hidden=\"true\" />\n            <span className=\"text-sm\">Ordinals</span>\n          </div>\n          <Badge variant=\"secondary\" className=\"text-xs\">\n            {ordinalCount} item{ordinalCount !== 1 ? \"s\" : \"\"}\n          </Badge>\n        </div>\n      )}\n\n      {/* Tokens */}\n      {tokens.map((token) => (\n        <div\n          key={token.tokenId}\n          className=\"flex items-center justify-between rounded-md border bg-muted/50 px-3 py-2\"\n        >\n          <div className=\"flex items-center gap-2\">\n            <Coins className=\"size-4 text-muted-foreground\" aria-hidden=\"true\" />\n            <span className=\"text-sm\">{token.symbol ?? \"Token\"}</span>\n          </div>\n          <span className=\"text-sm font-medium font-mono\">{token.amount}</span>\n        </div>\n      ))}\n\n      {fundingCount === 0 && ordinalCount === 0 && tokenCount === 0 && (\n        <p className=\"text-sm text-muted-foreground text-center py-4\">\n          No assets found\n        </p>\n      )}\n    </div>\n  )\n}\n\nfunction SweepingState() {\n  return (\n    <div className=\"flex flex-col items-center gap-4 py-6\">\n      <Loader2 className=\"size-8 animate-spin text-primary\" />\n      <div className=\"flex flex-col items-center gap-1\">\n        <p className=\"text-sm font-medium\">Sweeping assets...</p>\n        <p className=\"text-xs text-muted-foreground\">\n          Building and broadcasting transactions\n        </p>\n      </div>\n    </div>\n  )\n}\n\nfunction SuccessState({\n  sweepResult,\n  onReset,\n}: {\n  sweepResult: SweepResult\n  onReset: () => void\n}) {\n  return (\n    <div className=\"flex flex-col gap-4\">\n      <div className=\"flex items-start gap-3 rounded-md border border-primary/20 bg-primary/5 p-3\">\n        <CheckCircle2 className=\"mt-0.5 size-4 flex-shrink-0 text-primary\" />\n        <div className=\"min-w-0 flex flex-col gap-1\">\n          <p className=\"text-sm font-medium\">Sweep complete</p>\n          {sweepResult.txid && (\n            <Badge\n              variant=\"outline\"\n              className=\"max-w-full truncate text-xs font-mono\"\n            >\n              {sweepResult.txid}\n            </Badge>\n          )}\n        </div>\n      </div>\n      <Button variant=\"outline\" className=\"w-full\" onClick={onReset}>\n        <RotateCcw aria-hidden=\"true\" data-icon=\"inline-start\" />\n        Sweep Another Wallet\n      </Button>\n    </div>\n  )\n}\n\nfunction ErrorState({\n  error,\n  onReset,\n}: {\n  error: string\n  onReset: () => void\n}) {\n  return (\n    <div className=\"flex flex-col gap-4\">\n      <div className=\"flex items-start gap-3 rounded-md border border-destructive/20 bg-destructive/5 p-3\">\n        <AlertCircle className=\"mt-0.5 size-4 flex-shrink-0 text-destructive\" />\n        <div className=\"flex flex-col gap-1\">\n          <p className=\"text-sm font-medium\">Sweep failed</p>\n          <p className=\"text-xs text-muted-foreground\">{error}</p>\n        </div>\n      </div>\n      <Button variant=\"outline\" className=\"w-full\" onClick={onReset}>\n        <RotateCcw aria-hidden=\"true\" data-icon=\"inline-start\" />\n        Try Again\n      </Button>\n    </div>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// Main UI\n// ---------------------------------------------------------------------------\n\n/**\n * Pure presentation component for the sweep-wallet block.\n *\n * Renders a Card with WIF input, scan/preview, sweep progress, and\n * success/error states. Never imports SDK packages.\n */\nexport function SweepWalletUi({\n  step,\n  wifInput,\n  onWifInputChange,\n  isWifValid,\n  scanResult,\n  sweepResult,\n  error,\n  isLoading,\n  onScan,\n  onSweep,\n  onReset,\n  className,\n}: SweepWalletUiProps) {\n  const handlePaste = async () => {\n    if (typeof navigator !== \"undefined\" && navigator.clipboard) {\n      try {\n        const text = await navigator.clipboard.readText()\n        onWifInputChange(text.trim())\n      } catch {\n        // Clipboard permission denied -- user can paste manually\n      }\n    }\n  }\n\n  return (\n    <Card className={cn(\"w-full max-w-md\", className)}>\n      <CardHeader>\n        <CardTitle className=\"flex items-center gap-2\">\n          <KeyRound className=\"size-5\" aria-hidden=\"true\" />\n          Sweep Wallet\n        </CardTitle>\n        <CardDescription>\n          Import assets from a WIF private key into your connected wallet.\n        </CardDescription>\n      </CardHeader>\n\n      <CardContent className=\"flex flex-col gap-4\">\n        {/* WIF input -- visible in input & preview steps */}\n        {(step === \"input\" || step === \"preview\") && (\n          <div className=\"flex flex-col gap-2\">\n            <Label htmlFor=\"sweep-wif-input\">WIF Private Key</Label>\n            <div className=\"flex items-center gap-2\">\n              <Input\n                id=\"sweep-wif-input\"\n                type=\"password\"\n                placeholder=\"5HueCG... or K... or L...\"\n                value={wifInput}\n                onChange={(e) => onWifInputChange(e.target.value)}\n                disabled={isLoading || step === \"preview\"}\n                className=\"flex-1 font-mono text-sm\"\n                autoComplete=\"off\"\n                spellCheck={false}\n              />\n              {step === \"input\" && (\n                <Button\n                  type=\"button\"\n                  variant=\"outline\"\n                  size=\"icon\"\n                  className=\"flex-shrink-0\"\n                  onClick={() => { void handlePaste() }}\n                  disabled={isLoading}\n                  aria-label=\"Paste from clipboard\"\n                >\n                  <ClipboardPaste aria-hidden=\"true\" data-icon=\"inline-start\" />\n                </Button>\n              )}\n            </div>\n            {wifInput.length > 0 && !isWifValid && (\n              <p className=\"text-xs text-destructive\" role=\"alert\">\n                Invalid WIF format\n              </p>\n            )}\n          </div>\n        )}\n\n        {/* Scanning skeleton */}\n        {step === \"scanning\" && <ScanningState />}\n\n        {/* Asset preview */}\n        {step === \"preview\" && scanResult && (\n          <>\n            <Separator />\n            <AssetPreview scanResult={scanResult} />\n          </>\n        )}\n\n        {/* Sweeping progress */}\n        {step === \"sweeping\" && <SweepingState />}\n\n        {/* Success */}\n        {step === \"done\" && sweepResult && (\n          <SuccessState sweepResult={sweepResult} onReset={onReset} />\n        )}\n\n        {/* Error */}\n        {step === \"error\" && error && (\n          <ErrorState error={error} onReset={onReset} />\n        )}\n      </CardContent>\n\n      {/* Footer actions for input & preview steps */}\n      {(step === \"input\" || step === \"preview\") && (\n        <CardFooter>\n          {step === \"input\" && (\n            <Button\n              className=\"w-full\"\n              disabled={!isWifValid || isLoading}\n              onClick={onScan}\n              aria-busy={isLoading}\n            >\n              <Search aria-hidden=\"true\" data-icon=\"inline-start\" />\n              Scan for Assets\n            </Button>\n          )}\n          {step === \"preview\" && (\n            <div className=\"flex w-full flex-col gap-2\">\n              <Button\n                className=\"w-full\"\n                disabled={isLoading}\n                onClick={onSweep}\n                aria-busy={isLoading}\n              >\n                <Wallet aria-hidden=\"true\" data-icon=\"inline-start\" />\n                Sweep All\n              </Button>\n              <Button\n                variant=\"ghost\"\n                className=\"w-full\"\n                onClick={onReset}\n                disabled={isLoading}\n              >\n                Cancel\n              </Button>\n            </div>\n          )}\n        </CardFooter>\n      )}\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/sweep-wallet/sweep-wallet-ui.tsx"
    },
    {
      "path": "registry/new-york/blocks/sweep-wallet/use-sweep-wallet.ts",
      "content": "import { useCallback, useMemo, useState } from \"react\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** A funding UTXO found during scan */\nexport interface SweepFundingUtxo {\n  /** Outpoint in txid_vout format */\n  outpoint: string\n  /** Satoshis held by this output */\n  satoshis: number\n}\n\n/** An ordinal UTXO found during scan */\nexport interface SweepOrdinalUtxo {\n  /** Outpoint in txid_vout format */\n  outpoint: string\n}\n\n/** A group of BSV-21 token UTXOs */\nexport interface SweepTokenGroup {\n  /** Token ID in txid_vout format */\n  tokenId: string\n  /** Human-readable symbol (e.g. \"GOLD\") */\n  symbol?: string\n  /** Total token amount as string (bigint serialization) */\n  amount: string\n}\n\n/** Categorized assets discovered by scanning a WIF/address */\nexport interface ScanResult {\n  /** Standard funding UTXOs */\n  funding: SweepFundingUtxo[]\n  /** 1Sat ordinal UTXOs */\n  ordinals: SweepOrdinalUtxo[]\n  /** BSV-21 token groups */\n  tokens: SweepTokenGroup[]\n  /** Total satoshis across all funding UTXOs */\n  totalSats: number\n}\n\n/** Result from a sweep operation */\nexport interface SweepResult {\n  /** Transaction ID on success */\n  txid?: string\n  /** Error message on failure */\n  error?: string\n}\n\n/** Step in the sweep workflow */\nexport type SweepStep = \"input\" | \"scanning\" | \"preview\" | \"sweeping\" | \"done\" | \"error\"\n\n/** Options for the useSweepWallet hook */\nexport interface UseSweepWalletOptions {\n  /** Scan a WIF for spendable assets. Called with the raw WIF string. */\n  onScan: (wif: string) => Promise<ScanResult>\n  /** Execute the sweep transaction. Called with WIF and the scan result. */\n  onSweep: (wif: string, assets: ScanResult) => Promise<SweepResult>\n  /** Callback after a successful sweep */\n  onSuccess?: (result: SweepResult) => void\n  /** Callback on any error (scan or sweep) */\n  onError?: (error: Error) => void\n}\n\n/** Return value of the useSweepWallet hook */\nexport interface UseSweepWalletReturn {\n  /** Current step in the workflow */\n  step: SweepStep\n  /** Current WIF input value */\n  wifInput: string\n  /** Update the WIF input */\n  setWifInput: (value: string) => void\n  /** Whether the WIF input looks valid (base58check, 51-52 chars) */\n  isWifValid: boolean\n  /** Scanned assets (available after successful scan) */\n  scanResult: ScanResult | null\n  /** Sweep result (available after successful sweep) */\n  sweepResult: SweepResult | null\n  /** Current error message */\n  error: string | null\n  /** Whether any async operation is in flight */\n  isLoading: boolean\n  /** Initiate scanning */\n  handleScan: () => Promise<void>\n  /** Initiate sweeping */\n  handleSweep: () => Promise<void>\n  /** Reset back to input step */\n  handleReset: () => void\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** WIF is base58check, typically 51 chars (uncompressed) or 52 chars (compressed) */\nconst WIF_PATTERN = /^[5KL][1-9A-HJ-NP-Za-km-z]{50,51}$/\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\n/**\n * Manages the sweep-wallet workflow: WIF input -> scan -> preview -> sweep -> done.\n *\n * Pure logic hook. Does not import any UI or SDK packages. The caller provides\n * `onScan` and `onSweep` callbacks that wire into `@1sat/actions`.\n */\nexport function useSweepWallet({\n  onScan,\n  onSweep,\n  onSuccess,\n  onError,\n}: UseSweepWalletOptions): UseSweepWalletReturn {\n  const [step, setStep] = useState<SweepStep>(\"input\")\n  const [wifInput, setWifInput] = useState(\"\")\n  const [scanResult, setScanResult] = useState<ScanResult | null>(null)\n  const [sweepResult, setSweepResult] = useState<SweepResult | null>(null)\n  const [error, setError] = useState<string | null>(null)\n\n  const isWifValid = useMemo(() => WIF_PATTERN.test(wifInput.trim()), [wifInput])\n\n  const isLoading = step === \"scanning\" || step === \"sweeping\"\n\n  const handleScan = useCallback(async () => {\n    const trimmed = wifInput.trim()\n    if (!WIF_PATTERN.test(trimmed)) return\n\n    setStep(\"scanning\")\n    setError(null)\n    setScanResult(null)\n    setSweepResult(null)\n\n    try {\n      const result = await onScan(trimmed)\n      setScanResult(result)\n\n      const hasAssets =\n        result.funding.length > 0 ||\n        result.ordinals.length > 0 ||\n        result.tokens.length > 0\n\n      if (hasAssets) {\n        setStep(\"preview\")\n      } else {\n        setError(\"No assets found for this key\")\n        setStep(\"error\")\n      }\n    } catch (err) {\n      const e = err instanceof Error ? err : new Error(String(err))\n      setError(e.message)\n      setStep(\"error\")\n      onError?.(e)\n    }\n  }, [wifInput, onScan, onError])\n\n  const handleSweep = useCallback(async () => {\n    if (!scanResult) return\n    const trimmed = wifInput.trim()\n    if (!WIF_PATTERN.test(trimmed)) return\n\n    setStep(\"sweeping\")\n    setError(null)\n\n    try {\n      const result = await onSweep(trimmed, scanResult)\n\n      if (result.error) {\n        setError(result.error)\n        setStep(\"error\")\n        onError?.(new Error(result.error))\n      } else {\n        setSweepResult(result)\n        setStep(\"done\")\n        onSuccess?.(result)\n      }\n    } catch (err) {\n      const e = err instanceof Error ? err : new Error(String(err))\n      setError(e.message)\n      setStep(\"error\")\n      onError?.(e)\n    }\n  }, [wifInput, scanResult, onSweep, onSuccess, onError])\n\n  const handleReset = useCallback(() => {\n    setStep(\"input\")\n    setWifInput(\"\")\n    setScanResult(null)\n    setSweepResult(null)\n    setError(null)\n  }, [])\n\n  return {\n    step,\n    wifInput,\n    setWifInput,\n    isWifValid,\n    scanResult,\n    sweepResult,\n    error,\n    isLoading,\n    handleScan,\n    handleSweep,\n    handleReset,\n  }\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/sweep-wallet/use-sweep-wallet.ts"
    }
  ],
  "type": "registry:block"
}