bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
241 lines (240 loc) • 18.1 kB
JavaScript
"use client";
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { HD, Mnemonic, PrivateKey } from "@bsv/sdk";
import { ClipboardCopyIcon, ClipboardIcon, ExclamationTriangleIcon, PersonIcon, TrashIcon, UploadIcon, } from "@radix-ui/react-icons";
import { decryptBackup, } from "bitcoin-backup";
import { useState } from "react";
import { cn } from "../../lib/utils.js";
import { FileImport } from "../backup-recovery/FileImport.js";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "../ui/alert-dialog.js";
import { Alert, AlertDescription } from "../ui/alert.js";
import { Badge } from "../ui/badge.js";
import { Button } from "../ui/button.js";
import { Card, CardContent } from "../ui/card.js";
import { Input } from "../ui/input.js";
import { ScrollArea } from "../ui/scroll-area.js";
import { Separator } from "../ui/separator.js";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs.js";
import { Textarea } from "../ui/textarea.js";
export function KeyManager({ onKeySelect, onKeyExport, initialKeys = [], className = "", }) {
const [keys, setKeys] = useState(initialKeys);
const [selectedKey, setSelectedKey] = useState(null);
const [decryptPassword, setDecryptPassword] = useState("");
const [error, setError] = useState("");
const [copiedId, setCopiedId] = useState("");
const [deleteConfirmId, setDeleteConfirmId] = useState("");
const handleFileImport = async (file, result) => {
try {
setError("");
if (result.fileType === "encrypted" && decryptPassword) {
// Handle encrypted backup - need to read file content first
const content = await file.text();
const decryptedBackup = await decryptBackup(content, decryptPassword);
const backup = decryptedBackup;
let keyValue = "";
let label = "Imported Backup";
if ("derivedPrivateKey" in backup && backup.derivedPrivateKey) {
keyValue = String(backup.derivedPrivateKey);
label =
"description" in backup && backup.description
? String(backup.description)
: "Imported BAP Master Backup";
}
else if ("privateKey" in backup && backup.privateKey) {
keyValue = String(backup.privateKey);
label =
"description" in backup && backup.description
? String(backup.description)
: "Imported Private Key Backup";
}
else if ("wif" in backup && backup.wif) {
keyValue = String(backup.wif);
label =
"label" in backup && backup.label
? String(backup.label)
: "Imported WIF Backup";
}
else if ("ordPk" in backup && backup.ordPk) {
keyValue = String(backup.ordPk);
label =
"label" in backup && backup.label
? String(backup.label)
: "Imported OneSat Backup";
}
const newKey = {
id: Date.now().toString(),
label,
type: "wif",
value: keyValue,
metadata: backup,
createdAt: new Date(),
};
if (keyValue) {
const privKey = PrivateKey.fromWif(keyValue);
newKey.address = privKey.toAddress();
newKey.publicKey = privKey.toPublicKey().toString();
}
setKeys([...keys, newKey]);
}
else if (result.fileType === "unencrypted" && result.data) {
// Handle unencrypted backup from the data property
const backup = result.data;
let keyValue = "";
let label = "Imported Backup";
if (typeof backup.derivedPrivateKey === "string") {
keyValue = backup.derivedPrivateKey;
label =
typeof backup.description === "string"
? backup.description
: "Imported BAP Master Backup";
}
else if (typeof backup.wif === "string" &&
typeof backup.id === "string") {
keyValue = backup.wif;
label =
typeof backup.label === "string"
? backup.label
: "Imported BAP Member Backup";
}
else if (typeof backup.wif === "string") {
keyValue = backup.wif;
label = "Imported WIF Key";
}
else if (typeof backup.ordPk === "string") {
keyValue = backup.ordPk;
label = "Imported OneSat Backup";
}
if (keyValue) {
const newKey = {
id: Date.now().toString(),
label,
type: "wif",
value: keyValue,
metadata: backup,
createdAt: new Date(),
};
const privKey = PrivateKey.fromWif(keyValue);
newKey.address = privKey.toAddress();
newKey.publicKey = privKey.toPublicKey().toString();
setKeys([...keys, newKey]);
}
}
}
catch (err) {
setError(err instanceof Error ? err.message : "Failed to import key");
}
};
const handleManualAdd = (type, value, label) => {
try {
setError("");
const newKey = {
id: Date.now().toString(),
label,
type,
value,
createdAt: new Date(),
};
if (type === "wif") {
const privKey = PrivateKey.fromWif(value);
newKey.address = privKey.toAddress();
newKey.publicKey = privKey.toPublicKey().toString();
}
else if (type === "mnemonic") {
const mnemonic = Mnemonic.fromString(value);
const hd = HD.fromSeed(mnemonic.toSeed());
const privKey = PrivateKey.fromHex(hd.privKey?.toString());
newKey.address = privKey.toAddress();
newKey.publicKey = privKey.toPublicKey().toString();
newKey.metadata = { wif: value }; // Store as WIF backup type
}
setKeys([...keys, newKey]);
}
catch (err) {
setError(err instanceof Error ? err.message : "Failed to add key");
}
};
const handleDelete = (id) => {
setKeys(keys.filter((k) => k.id !== id));
if (selectedKey?.id === id) {
setSelectedKey(null);
}
setDeleteConfirmId("");
};
const copyValue = async (value, id) => {
try {
await navigator.clipboard.writeText(value);
setCopiedId(id);
setTimeout(() => setCopiedId(""), 2000);
}
catch (err) {
console.error("Failed to copy:", err);
}
};
const exportKey = (key) => {
const exportData = {
label: key.label,
type: key.type,
value: key.value,
address: key.address,
publicKey: key.publicKey,
metadata: key.metadata,
exportedAt: new Date().toISOString(),
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${key.label.replace(/\s+/g, "-").toLowerCase()}-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
onKeyExport?.(key);
};
const getKeyTypeColor = (type) => {
switch (type) {
case "wif":
return "blue";
case "mnemonic":
return "green";
case "hdkey":
return "purple";
case "encrypted":
return "amber";
default:
return "gray";
}
};
return (_jsxs("div", { className: cn("flex flex-col gap-4", className), children: [_jsx(Card, { children: _jsx(CardContent, { className: "p-6", children: _jsxs(Tabs, { defaultValue: "manage", children: [_jsxs(TabsList, { className: "grid w-full grid-cols-3", children: [_jsx(TabsTrigger, { value: "manage", children: "Manage Keys" }), _jsx(TabsTrigger, { value: "import", children: "Import" }), _jsx(TabsTrigger, { value: "add", children: "Add Manually" })] }), _jsxs("div", { className: "mt-6", children: [_jsx(TabsContent, { value: "manage", children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex justify-between items-center", children: [_jsx("p", { className: "text-sm font-medium", children: "Stored Keys" }), _jsxs(Badge, { variant: "secondary", children: [keys.length, " keys"] })] }), keys.length === 0 ? (_jsxs(Alert, { children: [_jsx(PersonIcon, { className: "h-4 w-4" }), _jsx(AlertDescription, { children: "No keys stored yet. Import or add keys to get started." })] })) : (_jsx(ScrollArea, { className: "h-96", children: _jsx("div", { className: "flex flex-col gap-2", children: keys.map((key) => (_jsx(Card, { className: cn("cursor-pointer p-3 transition-colors", selectedKey?.id === key.id
? "bg-accent"
: "hover:bg-accent/50"), onClick: () => {
setSelectedKey(key);
onKeySelect?.(key);
}, children: _jsxs("div", { className: "flex justify-between items-start gap-2", children: [_jsxs("div", { className: "flex-1 min-w-0 space-y-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Badge, { variant: "secondary", className: "text-xs", children: key.type }), _jsx("p", { className: "text-sm font-medium", children: key.label })] }), key.address && (_jsx("code", { className: "text-xs bg-muted px-1 py-0.5 rounded break-all", children: key.address })), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Created: ", key.createdAt.toLocaleString()] })] }), _jsxs("div", { className: "flex gap-1", children: [_jsx(Button, { size: "icon", variant: "ghost", className: cn("h-6 w-6", copiedId === `${key.id}-value` &&
"text-green-600"), onClick: (e) => {
e.stopPropagation();
copyValue(key.value, `${key.id}-value`);
}, children: copiedId === `${key.id}-value` ? (_jsx(ClipboardCopyIcon, { className: "h-3 w-3" })) : (_jsx(ClipboardIcon, { className: "h-3 w-3" })) }), _jsx(Button, { size: "icon", variant: "ghost", className: "h-6 w-6", onClick: (e) => {
e.stopPropagation();
exportKey(key);
}, children: _jsx(UploadIcon, { className: "h-3 w-3" }) }), _jsx(Button, { size: "icon", variant: "ghost", className: "h-6 w-6 text-destructive hover:text-destructive", onClick: (e) => {
e.stopPropagation();
setDeleteConfirmId(key.id);
}, children: _jsx(TrashIcon, { className: "h-3 w-3" }) })] })] }) }, key.id))) }) })), selectedKey && (_jsxs(_Fragment, { children: [_jsx(Separator, { className: "my-4" }), _jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("p", { className: "text-sm font-medium", children: "Key Details" }), _jsxs("dl", { className: "space-y-2", children: [_jsxs("div", { className: "flex gap-2", children: [_jsx("dt", { className: "text-sm text-muted-foreground min-w-[120px]", children: "Type" }), _jsx("dd", { children: _jsx(Badge, { variant: "secondary", children: selectedKey.type }) })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("dt", { className: "text-sm text-muted-foreground min-w-[120px]", children: "Label" }), _jsx("dd", { children: _jsx("p", { className: "text-sm", children: selectedKey.label }) })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("dt", { className: "text-sm text-muted-foreground min-w-[120px]", children: "Created" }), _jsx("dd", { children: _jsx("p", { className: "text-sm text-muted-foreground", children: selectedKey.createdAt.toLocaleString() }) })] }), selectedKey.address && (_jsxs("div", { className: "flex gap-2", children: [_jsx("dt", { className: "text-sm text-muted-foreground min-w-[120px]", children: "Address" }), _jsx("dd", { children: _jsx("code", { className: "text-xs bg-muted px-1 py-0.5 rounded break-all", children: selectedKey.address }) })] })), selectedKey.publicKey && (_jsxs("div", { className: "flex gap-2", children: [_jsx("dt", { className: "text-sm text-muted-foreground min-w-[120px]", children: "Public Key" }), _jsx("dd", { children: _jsx("code", { className: "text-xs bg-muted px-1 py-0.5 rounded break-all", children: selectedKey.publicKey }) })] })), selectedKey.metadata && (_jsxs("div", { className: "flex gap-2", children: [_jsx("dt", { className: "text-sm text-muted-foreground min-w-[120px]", children: "Metadata" }), _jsx("dd", { children: _jsx("code", { className: "text-xs bg-muted px-1 py-0.5 rounded whitespace-pre-wrap", children: JSON.stringify(selectedKey.metadata, null, 2) }) })] }))] })] })] }))] }) }), _jsx(TabsContent, { value: "import", children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Import keys from backup files, WIF strings, or encrypted backups." }), _jsx(Input, { type: "password", placeholder: "Password for encrypted backups (optional)", value: decryptPassword, onChange: (e) => setDecryptPassword(e.target.value) }), _jsx(FileImport, { onFileValidated: (file, result) => handleFileImport(file, result), accept: ".json,.txt,.bak" })] }) }), _jsx(TabsContent, { value: "add", children: _jsx(ManualKeyAdd, { onAdd: handleManualAdd }) })] })] }) }) }), error && (_jsxs(Alert, { variant: "destructive", className: "mt-3", children: [_jsx(ExclamationTriangleIcon, { className: "h-4 w-4" }), _jsx(AlertDescription, { children: error })] })), _jsx(AlertDialog, { open: !!deleteConfirmId, onOpenChange: () => setDeleteConfirmId(""), children: _jsxs(AlertDialogContent, { className: "max-w-md", children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Delete Key" }), _jsx(AlertDialogDescription, { children: "Are you sure you want to delete this key? This action cannot be undone." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { onClick: () => setDeleteConfirmId(""), children: "Cancel" }), _jsx(AlertDialogAction, { className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", onClick: () => handleDelete(deleteConfirmId), children: "Delete" })] })] }) })] }));
}
// Manual key add component
function ManualKeyAdd({ onAdd, }) {
const [type, setType] = useState("wif");
const [value, setValue] = useState("");
const [label, setLabel] = useState("");
const handleSubmit = () => {
if (value && label) {
onAdd(type, value, label);
setValue("");
setLabel("");
}
};
return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: "Key Type" }), _jsx(Tabs, { value: type, onValueChange: (v) => setType(v), children: _jsxs(TabsList, { className: "grid w-full grid-cols-2", children: [_jsx(TabsTrigger, { value: "wif", children: "WIF" }), _jsx(TabsTrigger, { value: "mnemonic", children: "Mnemonic" })] }) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: "Label" }), _jsx(Input, { placeholder: "My Bitcoin Key", value: label, onChange: (e) => setLabel(e.target.value) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: type === "wif" ? "Private Key (WIF)" : "Mnemonic Phrase (12 words)" }), _jsx(Textarea, { placeholder: type === "wif"
? "L1234567890..."
: "word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12", value: value, onChange: (e) => setValue(e.target.value), rows: type === "wif" ? 2 : 3 })] }), _jsx(Button, { onClick: handleSubmit, disabled: !value || !label, size: "sm", children: "Add Key" })] }));
}