bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
311 lines (310 loc) • 12.9 kB
JavaScript
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,
};
}