UNPKG

bigblocks

Version:

Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React

18 lines 12.6 kB
{ "name": "bap-identity-hooks-usebapkeyrotation", "type": "registry:hook", "dependencies": [ "@bsv/sdk", "@tanstack/react-query" ], "devDependencies": [], "registryDependencies": [], "files": [ { "path": "components/bap-identity/hooks/useBapKeyRotation.ts", "type": "registry:hook", "content": "import { Utils } from \"@bsv/sdk\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport { useBitcoinAuth } from \"../../../hooks/useBitcoinAuth.js\";\nimport { broadcastTransaction } from \"../../../lib/broadcast.js\";\nimport { createStorageKeys } from \"../../../lib/storage-keys.js\";\nimport type { BapMasterBackup } from \"../../../lib/types.js\";\nimport {\n\tisBapMasterBackupLegacy,\n\tisMasterBackupType42,\n} from \"../../../lib/types.js\";\nimport type {\n\tAuthUserWithBAP,\n\tBapKeyRotationEvent,\n\tKeyRotationOptions,\n\tKeyRotationResult,\n\tUseBapKeyRotationOptions,\n} from \"../types/identity.js\";\n\nconst { toHex } = Utils;\n\nexport function useBapKeyRotation(options: UseBapKeyRotationOptions = {}) {\n\tconst { autoRotate = false, rotationInterval = 86400000 } = options; // 24 hours\n\tconst { user, actions } = useBitcoinAuth();\n\n\t// Get BAP instance from context\n\tconst bapInstance = (user as AuthUserWithBAP)?.bapInstance;\n\tconst [rotationHistory, setRotationHistory] = useState<BapKeyRotationEvent[]>(\n\t\t[],\n\t);\n\tconst [autoRotationTimer, setAutoRotationTimer] = useState<ReturnType<\n\t\ttypeof setTimeout\n\t> | null>(null);\n\tconst [currentBackup, setCurrentBackup] = useState<BapMasterBackup | null>(\n\t\tnull,\n\t);\n\n\t// Get configurable storage keys\n\tconst storageKeys = createStorageKeys();\n\n\t// Load current backup when user changes\n\tuseEffect(() => {\n\t\tif (user) {\n\t\t\tactions\n\t\t\t\t.getCurrentBackup()\n\t\t\t\t.then(setCurrentBackup)\n\t\t\t\t.catch(() => setCurrentBackup(null));\n\t\t} else {\n\t\t\tsetCurrentBackup(null);\n\t\t}\n\t}, [user, actions]);\n\n\t// Get current key info\n\tconst currentKeyInfo = useCallback(() => {\n\t\tif (!bapInstance) {\n\t\t\treturn {\n\t\t\t\tpath: \"\",\n\t\t\t\taddress: \"\",\n\t\t\t\tidKey: \"\",\n\t\t\t};\n\t\t}\n\n\t\tconst ids = bapInstance.listIds();\n\t\tif (!ids || ids.length === 0) {\n\t\t\treturn {\n\t\t\t\tpath: \"\",\n\t\t\t\taddress: \"\",\n\t\t\t\tidKey: \"\",\n\t\t\t};\n\t\t}\n\n\t\tconst currentId = ids[0]; // Typically using the first identity\n\t\tif (!currentId) {\n\t\t\treturn {\n\t\t\t\tpath: \"\",\n\t\t\t\taddress: \"\",\n\t\t\t\tidKey: \"\",\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tpath: currentId.currentPath || currentId.rootPath,\n\t\t\taddress: bapInstance.getAddress(\n\t\t\t\tcurrentId.rootPath,\n\t\t\t\tcurrentId.currentPath,\n\t\t\t),\n\t\t\tidKey: currentId.idKey,\n\t\t};\n\t}, [bapInstance]);\n\n\t// Load rotation history from storage\n\tuseEffect(() => {\n\t\tconst loadHistory = async () => {\n\t\t\tconst info = currentKeyInfo();\n\t\t\tif (!info.idKey) return;\n\n\t\t\t// Use configurable storage key\n\t\t\tconst storageKey = storageKeys.getBapRotationKey(info.idKey);\n\t\t\tconst stored = localStorage.getItem(storageKey);\n\n\t\t\tif (stored) {\n\t\t\t\ttry {\n\t\t\t\t\tconst history = JSON.parse(stored) as Array<\n\t\t\t\t\t\tOmit<BapKeyRotationEvent, \"timestamp\"> & { timestamp: string }\n\t\t\t\t\t>;\n\t\t\t\t\tsetRotationHistory(\n\t\t\t\t\t\thistory.map((event) => ({\n\t\t\t\t\t\t\t...event,\n\t\t\t\t\t\t\ttimestamp: new Date(event.timestamp),\n\t\t\t\t\t\t})),\n\t\t\t\t\t);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconsole.error(\"Failed to load rotation history:\", err);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tloadHistory();\n\t}, [currentKeyInfo, storageKeys]);\n\n\t// Key rotation mutation\n\tconst rotationMutation = useMutation<\n\t\tKeyRotationResult,\n\t\tError,\n\t\tKeyRotationOptions\n\t>({\n\t\tmutationFn: async (rotationOptions = {}) => {\n\t\t\tif (!bapInstance) {\n\t\t\t\tthrow new Error(\"No BAP instance available\");\n\t\t\t}\n\n\t\t\tconst ids = bapInstance.listIds();\n\t\t\tif (!ids || ids.length === 0) {\n\t\t\t\tthrow new Error(\"No identities found\");\n\t\t\t}\n\n\t\t\tconst currentId = ids[0];\n\t\t\tif (!currentId) {\n\t\t\t\tthrow new Error(\"No current identity found\");\n\t\t\t}\n\n\t\t\tconst oldPath = currentId.currentPath || currentId.rootPath;\n\t\t\tconst oldAddress = bapInstance.getAddress(currentId.rootPath, oldPath);\n\n\t\t\t// Perform key rotation\n\t\t\tconst rotationResult = bapInstance.newId(currentId.idKey);\n\t\t\tif (!rotationResult) {\n\t\t\t\tthrow new Error(\"Key rotation failed\");\n\t\t\t}\n\n\t\t\t// Get the updated identity\n\t\t\tconst updatedIds = bapInstance.listIds();\n\t\t\tconst updatedId = updatedIds.find((id) => id.idKey === currentId.idKey);\n\t\t\tif (!updatedId) {\n\t\t\t\tthrow new Error(\"Failed to find updated identity\");\n\t\t\t}\n\n\t\t\tconst newPath = updatedId.currentPath;\n\t\t\tconst newAddress = bapInstance.getAddress(updatedId.rootPath, newPath);\n\n\t\t\t// Get the identity instance to sign the transaction\n\t\t\tconst identity = bapInstance.getId(currentId.idKey);\n\t\t\tif (!identity) {\n\t\t\t\tthrow new Error(\"Failed to get identity instance\");\n\t\t\t}\n\n\t\t\t// Build the rotation transaction using BAP ID protocol\n\t\t\t// ID transactions follow the format: BAP | ID | identityKey | newAddress\n\t\t\t// The getIdTransaction method returns an array of arrays (op_return data)\n\t\t\tconst rotationTxData = identity.getIdTransaction(oldPath);\n\n\t\t\t// Build a proper transaction with the rotation data\n\t\t\t// Note: In a production implementation, this would require:\n\t\t\t// 1. Getting UTXOs for the identity from a UTXO provider\n\t\t\t// 2. Calculating proper fees\n\t\t\t// 3. Adding change outputs if needed\n\n\t\t\t// For now, we'll create a minimal transaction structure\n\t\t\t// In production, you'd use a UTXO provider and proper fee calculation\n\t\t\tlet txid: string;\n\n\t\t\tif (rotationOptions.customBroadcast) {\n\t\t\t\t// If custom broadcast is provided, let them handle the full transaction building\n\t\t\t\t// We'll pass the op_return data as a hex string\n\t\t\t\tconst txDataHex = rotationTxData\n\t\t\t\t\t.map((item) => {\n\t\t\t\t\t\tif (Array.isArray(item)) {\n\t\t\t\t\t\t\treturn toHex(item);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn item;\n\t\t\t\t\t})\n\t\t\t\t\t.join(\"\");\n\t\t\t\ttxid = await rotationOptions.customBroadcast(txDataHex);\n\t\t\t} else {\n\t\t\t\t// Use the broadcastTransaction utility to broadcast the rotation\n\t\t\t\ttry {\n\t\t\t\t\tconst txDataHex = rotationTxData\n\t\t\t\t\t\t.map((item) => {\n\t\t\t\t\t\t\tif (Array.isArray(item)) {\n\t\t\t\t\t\t\t\treturn toHex(item);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn item;\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.join(\"\");\n\n\t\t\t\t\tconst broadcastResult = await broadcastTransaction(txDataHex);\n\t\t\t\t\tif (!broadcastResult.success || !broadcastResult.txid) {\n\t\t\t\t\t\tthrow new Error(broadcastResult.error || \"Broadcast failed\");\n\t\t\t\t\t}\n\t\t\t\t\ttxid = broadcastResult.txid;\n\t\t\t\t} catch (broadcastError) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\"Failed to broadcast rotation transaction:\",\n\t\t\t\t\t\tbroadcastError,\n\t\t\t\t\t);\n\t\t\t\t\t// Fall back to simulated txid for development\n\t\t\t\t\tif (process.env.NODE_ENV === \"development\") {\n\t\t\t\t\t\ttxid = `rotation_${currentId.idKey}_${Date.now()}`;\n\t\t\t\t\t\tconsole.warn(\"Using simulated txid for development:\", txid);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t`Failed to broadcast rotation transaction: ${broadcastError}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Create rotation event\n\t\t\tconst rotationEvent: BapKeyRotationEvent = {\n\t\t\t\tid: txid,\n\t\t\t\ttimestamp: new Date(),\n\t\t\t\tfromPath: oldPath,\n\t\t\t\ttoPath: newPath,\n\t\t\t\tfromAddress: oldAddress,\n\t\t\t\ttoAddress: newAddress,\n\t\t\t\ttxid,\n\t\t\t\tstatus: \"pending\",\n\t\t\t};\n\n\t\t\t// Update history\n\t\t\tconst newHistory = [rotationEvent, ...rotationHistory];\n\t\t\tsetRotationHistory(newHistory);\n\n\t\t\t// Save to storage\n\t\t\tconst info = currentKeyInfo();\n\t\t\tif (info.idKey) {\n\t\t\t\tconst storageKey = storageKeys.getBapRotationKey(info.idKey);\n\t\t\t\tlocalStorage.setItem(storageKey, JSON.stringify(newHistory));\n\t\t\t}\n\n\t\t\t// Auto-backup if requested\n\t\t\tif (rotationOptions.autoBackup) {\n\t\t\t\ttry {\n\t\t\t\t\tawait actions.exportBackup();\n\t\t\t\t} catch (backupError) {\n\t\t\t\t\tconsole.error(\"Auto-backup after rotation failed:\", backupError);\n\t\t\t\t\t// Don't fail the rotation if backup fails\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: {\n\t\t\t\t\tnewPath,\n\t\t\t\t\tnewAddress,\n\t\t\t\t\ttxid,\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t});\n\n\t// Auto-rotation effect - must be after mutation definition\n\tuseEffect(() => {\n\t\tif (!autoRotate || !bapInstance) {\n\t\t\t// Clear any existing timer\n\t\t\tif (autoRotationTimer) {\n\t\t\t\tclearTimeout(autoRotationTimer);\n\t\t\t\tsetAutoRotationTimer(null);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Set up auto-rotation timer\n\t\tconst timer = setTimeout(() => {\n\t\t\trotationMutation.mutate({\n\t\t\t\tautoBackup: true, // Auto-backup when auto-rotating\n\t\t\t});\n\t\t}, rotationInterval);\n\n\t\tsetAutoRotationTimer(timer);\n\n\t\t// Cleanup timer on unmount or dependency changes\n\t\treturn () => {\n\t\t\tclearTimeout(timer);\n\t\t};\n\t}, [\n\t\tautoRotate,\n\t\trotationInterval,\n\t\tbapInstance,\n\t\tautoRotationTimer,\n\t\trotationMutation,\n\t]);\n\n\t// Cleanup timer on unmount\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\tif (autoRotationTimer) {\n\t\t\t\tclearTimeout(autoRotationTimer);\n\t\t\t}\n\t\t};\n\t}, [autoRotationTimer]);\n\n\t// Check if user has master key access\n\tconst canRotateKeys = useCallback(() => {\n\t\tif (!user || !bapInstance || !currentBackup) return false;\n\n\t\t// Support both legacy (xprv + mnemonic) and Type 42 (rootPk) formats\n\t\tconst hasLegacyMaster = isBapMasterBackupLegacy(currentBackup);\n\t\tconst hasType42Master = isMasterBackupType42(currentBackup);\n\t\tconst hasMasterAccess = hasLegacyMaster || hasType42Master;\n\n\t\tif (!hasMasterAccess) return false;\n\n\t\t// Check if BAP instance has identities\n\t\tconst ids = bapInstance.listIds();\n\t\treturn ids && ids.length > 0;\n\t}, [user, bapInstance, currentBackup]);\n\n\t// Get appropriate error message\n\tconst getMasterKeyError = useCallback(() => {\n\t\tif (!user) return \"Please sign in to use key rotation.\";\n\t\tif (!bapInstance)\n\t\t\treturn \"BAP instance not available. Please ensure you're signed in with a Bitcoin identity.\";\n\n\t\tif (!currentBackup)\n\t\t\treturn \"No backup found. Please sign in with a master backup.\";\n\n\t\t// Check for master key (supports both legacy and Type 42 formats)\n\t\tconst hasLegacyMaster = isBapMasterBackupLegacy(currentBackup);\n\t\tconst hasType42Master = isMasterBackupType42(currentBackup);\n\t\tconst hasMasterAccess = hasLegacyMaster || hasType42Master;\n\n\t\tif (!hasMasterAccess) {\n\t\t\t// Check if it's a member backup format (would have wif and id fields)\n\t\t\tif (\"wif\" in currentBackup && \"id\" in currentBackup) {\n\t\t\t\treturn \"You are signed in with a member backup. Key rotation requires a master backup (legacy BIP32 or Type 42).\";\n\t\t\t}\n\t\t\tif (\"wif\" in currentBackup) {\n\t\t\t\treturn \"You are signed in with a simple WIF backup. Key rotation requires a master backup with identity data.\";\n\t\t\t}\n\t\t\treturn \"Invalid backup type. Please sign in with a master backup (legacy BIP32 with xprv+mnemonic or Type 42 with rootPk).\";\n\t\t}\n\n\t\tconst ids = bapInstance.listIds();\n\t\tif (!ids || ids.length === 0) {\n\t\t\treturn \"No identities found. Please create an identity first.\";\n\t\t}\n\n\t\treturn null;\n\t}, [user, bapInstance, currentBackup]);\n\n\treturn {\n\t\t// React Query mutation interface\n\t\trotateKey: rotationMutation.mutate,\n\t\trotateKeyAsync: rotationMutation.mutateAsync,\n\n\t\t// State\n\t\tisLoading: rotationMutation.isPending,\n\t\tisError: rotationMutation.isError,\n\t\tisSuccess: rotationMutation.isSuccess,\n\t\terror: rotationMutation.error,\n\t\tdata: rotationMutation.data,\n\n\t\t// Additional state\n\t\trotationHistory,\n\t\tcurrentKeyInfo: currentKeyInfo(),\n\t\tcanRotateKeys: canRotateKeys(),\n\t\tmasterKeyError: getMasterKeyError(),\n\n\t\t// Reset\n\t\treset: rotationMutation.reset,\n\t};\n}\n", "target": "<%- config.aliases.hooks %>/usebapkeyrotation.ts" } ] }