bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
17 lines • 23.8 kB
JSON
{
"name": "developer-tools-keymanager",
"type": "registry:component",
"dependencies": [
"@bsv/sdk"
],
"devDependencies": [],
"registryDependencies": [],
"files": [
{
"path": "components/developer-tools/KeyManager.tsx",
"type": "registry:component",
"content": "\"use client\";\n\nimport { HD, Mnemonic, PrivateKey } from \"@bsv/sdk\";\nimport {\n\tClipboardCopyIcon,\n\tClipboardIcon,\n\tExclamationTriangleIcon,\n\tPersonIcon,\n\tTrashIcon,\n\tUploadIcon,\n} from \"@radix-ui/react-icons\";\nimport {\n\ttype BapMasterBackup,\n\ttype BapMemberBackup,\n\tdecryptBackup,\n} from \"bitcoin-backup\";\nimport type React from \"react\";\nimport { useState } from \"react\";\nimport { SPACING } from \"../../lib/layout-constants.js\";\nimport { cn } from \"../../lib/utils.js\";\nimport { FileImport } from \"../backup-recovery/FileImport.js\";\nimport {\n\tAlertDialog,\n\tAlertDialogAction,\n\tAlertDialogCancel,\n\tAlertDialogContent,\n\tAlertDialogDescription,\n\tAlertDialogFooter,\n\tAlertDialogHeader,\n\tAlertDialogTitle,\n\tAlertDialogTrigger,\n} from \"../ui/alert-dialog.js\";\nimport { Alert, AlertDescription } from \"../ui/alert.js\";\nimport { Badge } from \"../ui/badge.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"../ui/card.js\";\nimport { Input } from \"../ui/input.js\";\nimport { ScrollArea } from \"../ui/scroll-area.js\";\nimport { Separator } from \"../ui/separator.js\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"../ui/tabs.js\";\nimport { Textarea } from \"../ui/textarea.js\";\n\nexport interface StoredKey {\n\tid: string;\n\tlabel: string;\n\ttype: \"wif\" | \"mnemonic\" | \"hdkey\" | \"encrypted\";\n\tvalue: string;\n\taddress?: string;\n\tpublicKey?: string;\n\tmetadata?:\n\t\t| BapMasterBackup\n\t\t| BapMemberBackup\n\t\t| { ordPk: string; payPk: string; identityPk: string }\n\t\t| { wif: string };\n\tcreatedAt: Date;\n}\n\nexport interface KeyManagerProps {\n\tonKeySelect?: (key: StoredKey) => void;\n\tonKeyExport?: (key: StoredKey) => void;\n\tinitialKeys?: StoredKey[];\n\tclassName?: string;\n}\n\nexport function KeyManager({\n\tonKeySelect,\n\tonKeyExport,\n\tinitialKeys = [],\n\tclassName = \"\",\n}: KeyManagerProps) {\n\tconst [keys, setKeys] = useState<StoredKey[]>(initialKeys);\n\tconst [selectedKey, setSelectedKey] = useState<StoredKey | null>(null);\n\tconst [decryptPassword, setDecryptPassword] = useState(\"\");\n\tconst [error, setError] = useState(\"\");\n\tconst [copiedId, setCopiedId] = useState<string>(\"\");\n\tconst [deleteConfirmId, setDeleteConfirmId] = useState<string>(\"\");\n\n\tconst handleFileImport = async (\n\t\tfile: File,\n\t\tresult: {\n\t\t\tisValid: boolean;\n\t\t\tfileType: \"encrypted\" | \"unencrypted\" | \"unknown\";\n\t\t\tformat: \"json\" | \"text\" | \"binary\" | \"unknown\";\n\t\t\terror?: string;\n\t\t\tdata?: unknown;\n\t\t},\n\t) => {\n\t\ttry {\n\t\t\tsetError(\"\");\n\n\t\t\tif (result.fileType === \"encrypted\" && decryptPassword) {\n\t\t\t\t// Handle encrypted backup - need to read file content first\n\t\t\t\tconst content = await file.text();\n\t\t\t\tconst decryptedBackup = await decryptBackup(content, decryptPassword);\n\t\t\t\tconst backup = decryptedBackup;\n\n\t\t\t\tlet keyValue = \"\";\n\t\t\t\tlet label = \"Imported Backup\";\n\n\t\t\t\tif (\"derivedPrivateKey\" in backup && backup.derivedPrivateKey) {\n\t\t\t\t\tkeyValue = String(backup.derivedPrivateKey);\n\t\t\t\t\tlabel =\n\t\t\t\t\t\t\"description\" in backup && backup.description\n\t\t\t\t\t\t\t? String(backup.description)\n\t\t\t\t\t\t\t: \"Imported BAP Master Backup\";\n\t\t\t\t} else if (\"privateKey\" in backup && backup.privateKey) {\n\t\t\t\t\tkeyValue = String(backup.privateKey);\n\t\t\t\t\tlabel =\n\t\t\t\t\t\t\"description\" in backup && backup.description\n\t\t\t\t\t\t\t? String(backup.description)\n\t\t\t\t\t\t\t: \"Imported Private Key Backup\";\n\t\t\t\t} else if (\"wif\" in backup && backup.wif) {\n\t\t\t\t\tkeyValue = String(backup.wif);\n\t\t\t\t\tlabel =\n\t\t\t\t\t\t\"label\" in backup && backup.label\n\t\t\t\t\t\t\t? String(backup.label)\n\t\t\t\t\t\t\t: \"Imported WIF Backup\";\n\t\t\t\t} else if (\"ordPk\" in backup && backup.ordPk) {\n\t\t\t\t\tkeyValue = String(backup.ordPk);\n\t\t\t\t\tlabel =\n\t\t\t\t\t\t\"label\" in backup && backup.label\n\t\t\t\t\t\t\t? String(backup.label)\n\t\t\t\t\t\t\t: \"Imported OneSat Backup\";\n\t\t\t\t}\n\n\t\t\t\tconst newKey: StoredKey = {\n\t\t\t\t\tid: Date.now().toString(),\n\t\t\t\t\tlabel,\n\t\t\t\t\ttype: \"wif\",\n\t\t\t\t\tvalue: keyValue,\n\t\t\t\t\tmetadata: backup as\n\t\t\t\t\t\t| BapMasterBackup\n\t\t\t\t\t\t| BapMemberBackup\n\t\t\t\t\t\t| { ordPk: string; payPk: string; identityPk: string }\n\t\t\t\t\t\t| { wif: string },\n\t\t\t\t\tcreatedAt: new Date(),\n\t\t\t\t};\n\n\t\t\t\tif (keyValue) {\n\t\t\t\t\tconst privKey = PrivateKey.fromWif(keyValue);\n\t\t\t\t\tnewKey.address = privKey.toAddress();\n\t\t\t\t\tnewKey.publicKey = privKey.toPublicKey().toString();\n\t\t\t\t}\n\n\t\t\t\tsetKeys([...keys, newKey]);\n\t\t\t} else if (result.fileType === \"unencrypted\" && result.data) {\n\t\t\t\t// Handle unencrypted backup from the data property\n\t\t\t\tconst backup = result.data as Record<string, unknown>;\n\n\t\t\t\tlet keyValue = \"\";\n\t\t\t\tlet label = \"Imported Backup\";\n\n\t\t\t\tif (typeof backup.derivedPrivateKey === \"string\") {\n\t\t\t\t\tkeyValue = backup.derivedPrivateKey;\n\t\t\t\t\tlabel =\n\t\t\t\t\t\ttypeof backup.description === \"string\"\n\t\t\t\t\t\t\t? backup.description\n\t\t\t\t\t\t\t: \"Imported BAP Master Backup\";\n\t\t\t\t} else if (\n\t\t\t\t\ttypeof backup.wif === \"string\" &&\n\t\t\t\t\ttypeof backup.id === \"string\"\n\t\t\t\t) {\n\t\t\t\t\tkeyValue = backup.wif;\n\t\t\t\t\tlabel =\n\t\t\t\t\t\ttypeof backup.label === \"string\"\n\t\t\t\t\t\t\t? backup.label\n\t\t\t\t\t\t\t: \"Imported BAP Member Backup\";\n\t\t\t\t} else if (typeof backup.wif === \"string\") {\n\t\t\t\t\tkeyValue = backup.wif;\n\t\t\t\t\tlabel = \"Imported WIF Key\";\n\t\t\t\t} else if (typeof backup.ordPk === \"string\") {\n\t\t\t\t\tkeyValue = backup.ordPk;\n\t\t\t\t\tlabel = \"Imported OneSat Backup\";\n\t\t\t\t}\n\n\t\t\t\tif (keyValue) {\n\t\t\t\t\tconst newKey: StoredKey = {\n\t\t\t\t\t\tid: Date.now().toString(),\n\t\t\t\t\t\tlabel,\n\t\t\t\t\t\ttype: \"wif\",\n\t\t\t\t\t\tvalue: keyValue,\n\t\t\t\t\t\tmetadata: backup as\n\t\t\t\t\t\t\t| BapMasterBackup\n\t\t\t\t\t\t\t| BapMemberBackup\n\t\t\t\t\t\t\t| { ordPk: string; payPk: string; identityPk: string }\n\t\t\t\t\t\t\t| { wif: string },\n\t\t\t\t\t\tcreatedAt: new Date(),\n\t\t\t\t\t};\n\n\t\t\t\t\tconst privKey = PrivateKey.fromWif(keyValue);\n\t\t\t\t\tnewKey.address = privKey.toAddress();\n\t\t\t\t\tnewKey.publicKey = privKey.toPublicKey().toString();\n\n\t\t\t\t\tsetKeys([...keys, newKey]);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tsetError(err instanceof Error ? err.message : \"Failed to import key\");\n\t\t}\n\t};\n\n\tconst handleManualAdd = (\n\t\ttype: \"wif\" | \"mnemonic\",\n\t\tvalue: string,\n\t\tlabel: string,\n\t) => {\n\t\ttry {\n\t\t\tsetError(\"\");\n\n\t\t\tconst newKey: StoredKey = {\n\t\t\t\tid: Date.now().toString(),\n\t\t\t\tlabel,\n\t\t\t\ttype,\n\t\t\t\tvalue,\n\t\t\t\tcreatedAt: new Date(),\n\t\t\t};\n\n\t\t\tif (type === \"wif\") {\n\t\t\t\tconst privKey = PrivateKey.fromWif(value);\n\t\t\t\tnewKey.address = privKey.toAddress();\n\t\t\t\tnewKey.publicKey = privKey.toPublicKey().toString();\n\t\t\t} else if (type === \"mnemonic\") {\n\t\t\t\tconst mnemonic = Mnemonic.fromString(value);\n\t\t\t\tconst hd = HD.fromSeed(mnemonic.toSeed());\n\t\t\t\tconst privKey = PrivateKey.fromHex(hd.privKey?.toString());\n\t\t\t\tnewKey.address = privKey.toAddress();\n\t\t\t\tnewKey.publicKey = privKey.toPublicKey().toString();\n\t\t\t\tnewKey.metadata = { wif: value }; // Store as WIF backup type\n\t\t\t}\n\n\t\t\tsetKeys([...keys, newKey]);\n\t\t} catch (err) {\n\t\t\tsetError(err instanceof Error ? err.message : \"Failed to add key\");\n\t\t}\n\t};\n\n\tconst handleDelete = (id: string) => {\n\t\tsetKeys(keys.filter((k) => k.id !== id));\n\t\tif (selectedKey?.id === id) {\n\t\t\tsetSelectedKey(null);\n\t\t}\n\t\tsetDeleteConfirmId(\"\");\n\t};\n\n\tconst copyValue = async (value: string, id: string) => {\n\t\ttry {\n\t\t\tawait navigator.clipboard.writeText(value);\n\t\t\tsetCopiedId(id);\n\t\t\tsetTimeout(() => setCopiedId(\"\"), 2000);\n\t\t} catch (err) {\n\t\t\tconsole.error(\"Failed to copy:\", err);\n\t\t}\n\t};\n\n\tconst exportKey = (key: StoredKey) => {\n\t\tconst exportData = {\n\t\t\tlabel: key.label,\n\t\t\ttype: key.type,\n\t\t\tvalue: key.value,\n\t\t\taddress: key.address,\n\t\t\tpublicKey: key.publicKey,\n\t\t\tmetadata: key.metadata,\n\t\t\texportedAt: new Date().toISOString(),\n\t\t};\n\n\t\tconst blob = new Blob([JSON.stringify(exportData, null, 2)], {\n\t\t\ttype: \"application/json\",\n\t\t});\n\t\tconst url = URL.createObjectURL(blob);\n\t\tconst a = document.createElement(\"a\");\n\t\ta.href = url;\n\t\ta.download = `${key.label.replace(/\\s+/g, \"-\").toLowerCase()}-${Date.now()}.json`;\n\t\ta.click();\n\t\tURL.revokeObjectURL(url);\n\n\t\tonKeyExport?.(key);\n\t};\n\n\tconst getKeyTypeColor = (type: StoredKey[\"type\"]) => {\n\t\tswitch (type) {\n\t\t\tcase \"wif\":\n\t\t\t\treturn \"blue\";\n\t\t\tcase \"mnemonic\":\n\t\t\t\treturn \"green\";\n\t\t\tcase \"hdkey\":\n\t\t\t\treturn \"purple\";\n\t\t\tcase \"encrypted\":\n\t\t\t\treturn \"amber\";\n\t\t\tdefault:\n\t\t\t\treturn \"gray\";\n\t\t}\n\t};\n\n\treturn (\n\t\t<div className={cn(\"flex flex-col gap-4\", className)}>\n\t\t\t<Card>\n\t\t\t\t<CardContent className=\"p-6\">\n\t\t\t\t\t<Tabs defaultValue=\"manage\">\n\t\t\t\t\t\t<TabsList className=\"grid w-full grid-cols-3\">\n\t\t\t\t\t\t\t<TabsTrigger value=\"manage\">Manage Keys</TabsTrigger>\n\t\t\t\t\t\t\t<TabsTrigger value=\"import\">Import</TabsTrigger>\n\t\t\t\t\t\t\t<TabsTrigger value=\"add\">Add Manually</TabsTrigger>\n\t\t\t\t\t\t</TabsList>\n\n\t\t\t\t\t\t<div className=\"mt-6\">\n\t\t\t\t\t\t\t<TabsContent value=\"manage\">\n\t\t\t\t\t\t\t\t<div className=\"flex flex-col gap-4\">\n\t\t\t\t\t\t\t\t\t<div className=\"flex justify-between items-center\">\n\t\t\t\t\t\t\t\t\t\t<p className=\"text-sm font-medium\">Stored Keys</p>\n\t\t\t\t\t\t\t\t\t\t<Badge variant=\"secondary\">{keys.length} keys</Badge>\n\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t{keys.length === 0 ? (\n\t\t\t\t\t\t\t\t\t\t<Alert>\n\t\t\t\t\t\t\t\t\t\t\t<PersonIcon className=\"h-4 w-4\" />\n\t\t\t\t\t\t\t\t\t\t\t<AlertDescription>\n\t\t\t\t\t\t\t\t\t\t\t\tNo keys stored yet. Import or add keys to get started.\n\t\t\t\t\t\t\t\t\t\t\t</AlertDescription>\n\t\t\t\t\t\t\t\t\t\t</Alert>\n\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t<ScrollArea className=\"h-96\">\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex flex-col gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t{keys.map((key) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Card\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tkey={key.id}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"cursor-pointer p-3 transition-colors\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tselectedKey?.id === key.id\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? \"bg-accent\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: \"hover:bg-accent/50\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetSelectedKey(key);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tonKeySelect?.(key);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex justify-between items-start gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex-1 min-w-0 space-y-1\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Badge\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"text-xs\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{key.type}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-sm font-medium\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{key.label}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{key.address && (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<code className=\"text-xs bg-muted px-1 py-0.5 rounded break-all\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{key.address}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</code>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tCreated: {key.createdAt.toLocaleString()}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex gap-1\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"h-6 w-6\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tcopiedId === `${key.id}-value` &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"text-green-600\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\te.stopPropagation();\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tcopyValue(key.value, `${key.id}-value`);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{copiedId === `${key.id}-value` ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<ClipboardCopyIcon className=\"h-3 w-3\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<ClipboardIcon className=\"h-3 w-3\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"h-6 w-6\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\te.stopPropagation();\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\texportKey(key);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<UploadIcon className=\"h-3 w-3\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"h-6 w-6 text-destructive hover:text-destructive\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\te.stopPropagation();\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetDeleteConfirmId(key.id);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<TrashIcon className=\"h-3 w-3\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</ScrollArea>\n\t\t\t\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t\t\t\t{selectedKey && (\n\t\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t\t<Separator className=\"my-4\" />\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex flex-col gap-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-sm font-medium\">Key Details</p>\n\t\t\t\t\t\t\t\t\t\t\t\t<dl className=\"space-y-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dt className=\"text-sm text-muted-foreground min-w-[120px]\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tType\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</dt>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Badge variant=\"secondary\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{selectedKey.type}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dt className=\"text-sm text-muted-foreground min-w-[120px]\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tLabel\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</dt>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-sm\">{selectedKey.label}</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dt className=\"text-sm text-muted-foreground min-w-[120px]\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tCreated\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</dt>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{selectedKey.createdAt.toLocaleString()}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t\t\t\t\t{selectedKey.address && (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dt className=\"text-sm text-muted-foreground min-w-[120px]\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tAddress\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</dt>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<code className=\"text-xs bg-muted px-1 py-0.5 rounded break-all\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{selectedKey.address}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</code>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t\t\t\t\t\t\t\t{selectedKey.publicKey && (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dt className=\"text-sm text-muted-foreground min-w-[120px]\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tPublic Key\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</dt>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<code className=\"text-xs bg-muted px-1 py-0.5 rounded break-all\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{selectedKey.publicKey}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</code>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t\t\t\t\t\t\t\t{selectedKey.metadata && (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dt className=\"text-sm text-muted-foreground min-w-[120px]\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tMetadata\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</dt>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<code className=\"text-xs bg-muted px-1 py-0.5 rounded whitespace-pre-wrap\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{JSON.stringify(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tselectedKey.metadata,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tnull,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t2,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</code>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t</dl>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</TabsContent>\n\n\t\t\t\t\t\t\t<TabsContent value=\"import\">\n\t\t\t\t\t\t\t\t<div className=\"flex flex-col gap-4\">\n\t\t\t\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\tImport keys from backup files, WIF strings, or encrypted\n\t\t\t\t\t\t\t\t\t\tbackups.\n\t\t\t\t\t\t\t\t\t</p>\n\n\t\t\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\t\t\ttype=\"password\"\n\t\t\t\t\t\t\t\t\t\tplaceholder=\"Password for encrypted backups (optional)\"\n\t\t\t\t\t\t\t\t\t\tvalue={decryptPassword}\n\t\t\t\t\t\t\t\t\t\tonChange={(e: React.ChangeEvent<HTMLInputElement>) =>\n\t\t\t\t\t\t\t\t\t\t\tsetDecryptPassword(e.target.value)\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t/>\n\n\t\t\t\t\t\t\t\t\t<FileImport\n\t\t\t\t\t\t\t\t\t\tonFileValidated={(file, result) =>\n\t\t\t\t\t\t\t\t\t\t\thandleFileImport(file, result)\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\taccept=\".json,.txt,.bak\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</TabsContent>\n\n\t\t\t\t\t\t\t<TabsContent value=\"add\">\n\t\t\t\t\t\t\t\t<ManualKeyAdd onAdd={handleManualAdd} />\n\t\t\t\t\t\t\t</TabsContent>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Tabs>\n\t\t\t\t</CardContent>\n\t\t\t</Card>\n\n\t\t\t{error && (\n\t\t\t\t<Alert variant=\"destructive\" className=\"mt-3\">\n\t\t\t\t\t<ExclamationTriangleIcon className=\"h-4 w-4\" />\n\t\t\t\t\t<AlertDescription>{error}</AlertDescription>\n\t\t\t\t</Alert>\n\t\t\t)}\n\n\t\t\t<AlertDialog\n\t\t\t\topen={!!deleteConfirmId}\n\t\t\t\tonOpenChange={() => setDeleteConfirmId(\"\")}\n\t\t\t>\n\t\t\t\t<AlertDialogContent className=\"max-w-md\">\n\t\t\t\t\t<AlertDialogHeader>\n\t\t\t\t\t\t<AlertDialogTitle>Delete Key</AlertDialogTitle>\n\t\t\t\t\t\t<AlertDialogDescription>\n\t\t\t\t\t\t\tAre you sure you want to delete this key? This action cannot be\n\t\t\t\t\t\t\tundone.\n\t\t\t\t\t\t</AlertDialogDescription>\n\t\t\t\t\t</AlertDialogHeader>\n\t\t\t\t\t<AlertDialogFooter>\n\t\t\t\t\t\t<AlertDialogCancel onClick={() => setDeleteConfirmId(\"\")}>\n\t\t\t\t\t\t\tCancel\n\t\t\t\t\t\t</AlertDialogCancel>\n\t\t\t\t\t\t<AlertDialogAction\n\t\t\t\t\t\t\tclassName=\"bg-destructive text-destructive-foreground hover:bg-destructive/90\"\n\t\t\t\t\t\t\tonClick={() => handleDelete(deleteConfirmId)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tDelete\n\t\t\t\t\t\t</AlertDialogAction>\n\t\t\t\t\t</AlertDialogFooter>\n\t\t\t\t</AlertDialogContent>\n\t\t\t</AlertDialog>\n\t\t</div>\n\t);\n}\n\n// Manual key add component\nfunction ManualKeyAdd({\n\tonAdd,\n}: {\n\tonAdd: (type: \"wif\" | \"mnemonic\", value: string, label: string) => void;\n}) {\n\tconst [type, setType] = useState<\"wif\" | \"mnemonic\">(\"wif\");\n\tconst [value, setValue] = useState(\"\");\n\tconst [label, setLabel] = useState(\"\");\n\n\tconst handleSubmit = () => {\n\t\tif (value && label) {\n\t\t\tonAdd(type, value, label);\n\t\t\tsetValue(\"\");\n\t\t\tsetLabel(\"\");\n\t\t}\n\t};\n\n\treturn (\n\t\t<div className=\"flex flex-col gap-3\">\n\t\t\t<div className=\"grid grid-cols-2 gap-3\">\n\t\t\t\t<div className=\"flex flex-col gap-2\">\n\t\t\t\t\t<span className=\"text-sm text-muted-foreground\">Key Type</span>\n\t\t\t\t\t<Tabs\n\t\t\t\t\t\tvalue={type}\n\t\t\t\t\t\tonValueChange={(v) => setType(v as \"wif\" | \"mnemonic\")}\n\t\t\t\t\t>\n\t\t\t\t\t\t<TabsList className=\"grid w-full grid-cols-2\">\n\t\t\t\t\t\t\t<TabsTrigger value=\"wif\">WIF</TabsTrigger>\n\t\t\t\t\t\t\t<TabsTrigger value=\"mnemonic\">Mnemonic</TabsTrigger>\n\t\t\t\t\t\t</TabsList>\n\t\t\t\t\t</Tabs>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"flex flex-col gap-2\">\n\t\t\t\t\t<span className=\"text-sm text-muted-foreground\">Label</span>\n\t\t\t\t\t<Input\n\t\t\t\t\t\tplaceholder=\"My Bitcoin Key\"\n\t\t\t\t\t\tvalue={label}\n\t\t\t\t\t\tonChange={(e) => setLabel(e.target.value)}\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<div className=\"flex flex-col gap-2\">\n\t\t\t\t<span className=\"text-sm text-muted-foreground\">\n\t\t\t\t\t{type === \"wif\" ? \"Private Key (WIF)\" : \"Mnemonic Phrase (12 words)\"}\n\t\t\t\t</span>\n\t\t\t\t<Textarea\n\t\t\t\t\tplaceholder={\n\t\t\t\t\t\ttype === \"wif\"\n\t\t\t\t\t\t\t? \"L1234567890...\"\n\t\t\t\t\t\t\t: \"word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12\"\n\t\t\t\t\t}\n\t\t\t\t\tvalue={value}\n\t\t\t\t\tonChange={(e) => setValue(e.target.value)}\n\t\t\t\t\trows={type === \"wif\" ? 2 : 3}\n\t\t\t\t/>\n\t\t\t</div>\n\n\t\t\t<Button onClick={handleSubmit} disabled={!value || !label} size=\"sm\">\n\t\t\t\tAdd Key\n\t\t\t</Button>\n\t\t</div>\n\t);\n}\n",
"target": "<%- config.aliases.components %>/keymanager.tsx"
}
]
}