UNPKG

bigblocks

Version:

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

311 lines (310 loc) 12.9 kB
import { Utils } from "@bsv/sdk"; import { useMutation } from "@tanstack/react-query"; import { useCallback, useEffect, useState } from "react"; import { useBitcoinAuth } from "../../../hooks/useBitcoinAuth.js"; import { broadcastTransaction } from "../../../lib/broadcast.js"; import { createStorageKeys } from "../../../lib/storage-keys.js"; import { isBapMasterBackupLegacy, isMasterBackupType42, } from "../../../lib/types.js"; const { toHex } = Utils; export function useBapKeyRotation(options = {}) { const { autoRotate = false, rotationInterval = 86400000 } = options; // 24 hours const { user, actions } = useBitcoinAuth(); // Get BAP instance from context const bapInstance = user?.bapInstance; const [rotationHistory, setRotationHistory] = useState([]); const [autoRotationTimer, setAutoRotationTimer] = useState(null); const [currentBackup, setCurrentBackup] = useState(null); // Get configurable storage keys const storageKeys = createStorageKeys(); // Load current backup when user changes useEffect(() => { if (user) { actions .getCurrentBackup() .then(setCurrentBackup) .catch(() => setCurrentBackup(null)); } else { setCurrentBackup(null); } }, [user, actions]); // Get current key info const currentKeyInfo = useCallback(() => { if (!bapInstance) { return { path: "", address: "", idKey: "", }; } const ids = bapInstance.listIds(); if (!ids || ids.length === 0) { return { path: "", address: "", idKey: "", }; } const currentId = ids[0]; // Typically using the first identity if (!currentId) { return { path: "", address: "", idKey: "", }; } return { path: currentId.currentPath || currentId.rootPath, address: bapInstance.getAddress(currentId.rootPath, currentId.currentPath), idKey: currentId.idKey, }; }, [bapInstance]); // Load rotation history from storage useEffect(() => { const loadHistory = async () => { const info = currentKeyInfo(); if (!info.idKey) return; // Use configurable storage key const storageKey = storageKeys.getBapRotationKey(info.idKey); const stored = localStorage.getItem(storageKey); if (stored) { try { const history = JSON.parse(stored); setRotationHistory(history.map((event) => ({ ...event, timestamp: new Date(event.timestamp), }))); } catch (err) { console.error("Failed to load rotation history:", err); } } }; loadHistory(); }, [currentKeyInfo, storageKeys]); // Key rotation mutation const rotationMutation = useMutation({ mutationFn: async (rotationOptions = {}) => { if (!bapInstance) { throw new Error("No BAP instance available"); } const ids = bapInstance.listIds(); if (!ids || ids.length === 0) { throw new Error("No identities found"); } const currentId = ids[0]; if (!currentId) { throw new Error("No current identity found"); } const oldPath = currentId.currentPath || currentId.rootPath; const oldAddress = bapInstance.getAddress(currentId.rootPath, oldPath); // Perform key rotation const rotationResult = bapInstance.newId(currentId.idKey); if (!rotationResult) { throw new Error("Key rotation failed"); } // Get the updated identity const updatedIds = bapInstance.listIds(); const updatedId = updatedIds.find((id) => id.idKey === currentId.idKey); if (!updatedId) { throw new Error("Failed to find updated identity"); } const newPath = updatedId.currentPath; const newAddress = bapInstance.getAddress(updatedId.rootPath, newPath); // Get the identity instance to sign the transaction const identity = bapInstance.getId(currentId.idKey); if (!identity) { throw new Error("Failed to get identity instance"); } // Build the rotation transaction using BAP ID protocol // ID transactions follow the format: BAP | ID | identityKey | newAddress // The getIdTransaction method returns an array of arrays (op_return data) const rotationTxData = identity.getIdTransaction(oldPath); // Build a proper transaction with the rotation data // Note: In a production implementation, this would require: // 1. Getting UTXOs for the identity from a UTXO provider // 2. Calculating proper fees // 3. Adding change outputs if needed // For now, we'll create a minimal transaction structure // In production, you'd use a UTXO provider and proper fee calculation let txid; if (rotationOptions.customBroadcast) { // If custom broadcast is provided, let them handle the full transaction building // We'll pass the op_return data as a hex string const txDataHex = rotationTxData .map((item) => { if (Array.isArray(item)) { return toHex(item); } return item; }) .join(""); txid = await rotationOptions.customBroadcast(txDataHex); } else { // Use the broadcastTransaction utility to broadcast the rotation try { const txDataHex = rotationTxData .map((item) => { if (Array.isArray(item)) { return toHex(item); } return item; }) .join(""); const broadcastResult = await broadcastTransaction(txDataHex); if (!broadcastResult.success || !broadcastResult.txid) { throw new Error(broadcastResult.error || "Broadcast failed"); } txid = broadcastResult.txid; } catch (broadcastError) { console.error("Failed to broadcast rotation transaction:", broadcastError); // Fall back to simulated txid for development if (process.env.NODE_ENV === "development") { txid = `rotation_${currentId.idKey}_${Date.now()}`; console.warn("Using simulated txid for development:", txid); } else { throw new Error(`Failed to broadcast rotation transaction: ${broadcastError}`); } } } // Create rotation event const rotationEvent = { id: txid, timestamp: new Date(), fromPath: oldPath, toPath: newPath, fromAddress: oldAddress, toAddress: newAddress, txid, status: "pending", }; // Update history const newHistory = [rotationEvent, ...rotationHistory]; setRotationHistory(newHistory); // Save to storage const info = currentKeyInfo(); if (info.idKey) { const storageKey = storageKeys.getBapRotationKey(info.idKey); localStorage.setItem(storageKey, JSON.stringify(newHistory)); } // Auto-backup if requested if (rotationOptions.autoBackup) { try { await actions.exportBackup(); } catch (backupError) { console.error("Auto-backup after rotation failed:", backupError); // Don't fail the rotation if backup fails } } return { success: true, data: { newPath, newAddress, txid, }, }; }, }); // Auto-rotation effect - must be after mutation definition useEffect(() => { if (!autoRotate || !bapInstance) { // Clear any existing timer if (autoRotationTimer) { clearTimeout(autoRotationTimer); setAutoRotationTimer(null); } return; } // Set up auto-rotation timer const timer = setTimeout(() => { rotationMutation.mutate({ autoBackup: true, // Auto-backup when auto-rotating }); }, rotationInterval); setAutoRotationTimer(timer); // Cleanup timer on unmount or dependency changes return () => { clearTimeout(timer); }; }, [ autoRotate, rotationInterval, bapInstance, autoRotationTimer, rotationMutation, ]); // Cleanup timer on unmount useEffect(() => { return () => { if (autoRotationTimer) { clearTimeout(autoRotationTimer); } }; }, [autoRotationTimer]); // Check if user has master key access const canRotateKeys = useCallback(() => { if (!user || !bapInstance || !currentBackup) return false; // Support both legacy (xprv + mnemonic) and Type 42 (rootPk) formats const hasLegacyMaster = isBapMasterBackupLegacy(currentBackup); const hasType42Master = isMasterBackupType42(currentBackup); const hasMasterAccess = hasLegacyMaster || hasType42Master; if (!hasMasterAccess) return false; // Check if BAP instance has identities const ids = bapInstance.listIds(); return ids && ids.length > 0; }, [user, bapInstance, currentBackup]); // Get appropriate error message const getMasterKeyError = useCallback(() => { if (!user) return "Please sign in to use key rotation."; if (!bapInstance) return "BAP instance not available. Please ensure you're signed in with a Bitcoin identity."; if (!currentBackup) return "No backup found. Please sign in with a master backup."; // Check for master key (supports both legacy and Type 42 formats) const hasLegacyMaster = isBapMasterBackupLegacy(currentBackup); const hasType42Master = isMasterBackupType42(currentBackup); const hasMasterAccess = hasLegacyMaster || hasType42Master; if (!hasMasterAccess) { // Check if it's a member backup format (would have wif and id fields) if ("wif" in currentBackup && "id" in currentBackup) { return "You are signed in with a member backup. Key rotation requires a master backup (legacy BIP32 or Type 42)."; } if ("wif" in currentBackup) { return "You are signed in with a simple WIF backup. Key rotation requires a master backup with identity data."; } return "Invalid backup type. Please sign in with a master backup (legacy BIP32 with xprv+mnemonic or Type 42 with rootPk)."; } const ids = bapInstance.listIds(); if (!ids || ids.length === 0) { return "No identities found. Please create an identity first."; } return null; }, [user, bapInstance, currentBackup]); return { // React Query mutation interface rotateKey: rotationMutation.mutate, rotateKeyAsync: rotationMutation.mutateAsync, // State isLoading: rotationMutation.isPending, isError: rotationMutation.isError, isSuccess: rotationMutation.isSuccess, error: rotationMutation.error, data: rotationMutation.data, // Additional state rotationHistory, currentKeyInfo: currentKeyInfo(), canRotateKeys: canRotateKeys(), masterKeyError: getMasterKeyError(), // Reset reset: rotationMutation.reset, }; }