UNPKG

bigblocks

Version:

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

231 lines (230 loc) 9.55 kB
import { useMutation } from "@tanstack/react-query"; import { BAP } from "bsv-bap"; import { useCallback, useEffect, useState } from "react"; import { useBitcoinAuth } from "../../../hooks/useBitcoinAuth.js"; import { isBapMasterBackupLegacy, isMasterBackupType42, } from "../../../lib/types.js"; import { DEFAULT_SOCIAL_API_URL } from "../../social/constants.js"; export function useBapProfileSync(options = {}) { const { autoSync = false, maxDiscoveryAttempts = 50, // Stop after 50 sequential empty results sigmaApiUrl = DEFAULT_SOCIAL_API_URL, } = options; const { user, actions } = useBitcoinAuth(); const [syncState, setSyncState] = useState({ isScanning: false, progress: 0, currentId: 0, foundCount: 0, error: null, }); // Fetch profile from SIGMA API const fetchProfile = useCallback(async (idKey) => { try { const response = await fetch(`${sigmaApiUrl}/identity/get`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ idKey }), }); if (!response.ok) { // 404 means profile doesn't exist, not an error if (response.status === 404) { return { result: null }; } throw new Error(`API error: ${response.status}`); } const data = await response.json(); return { result: data }; } catch (error) { console.warn(`Failed to fetch profile for ${idKey}:`, error); return { result: null }; } }, [sigmaApiUrl]); // Validate address against SIGMA API const validateByAddress = useCallback(async (address) => { try { const response = await fetch(`${sigmaApiUrl}/identity/validByAddress`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ address }), }); if (!response.ok) { return null; } const data = await response.json(); return data; } catch (error) { console.warn(`Failed to validate address ${address}:`, error); return null; } }, [sigmaApiUrl]); // Main profile synchronization mutation const syncMutation = useMutation({ mutationFn: async () => { if (!user) { throw new Error("No authenticated user"); } // Get current backup const currentBackup = await actions.getCurrentBackup(); if (!currentBackup) { throw new Error("No backup found for profile sync"); } setSyncState({ isScanning: true, progress: 0, currentId: 0, foundCount: 0, error: null, }); // Create BAP instance based on backup format let bap; if (isBapMasterBackupLegacy(currentBackup)) { bap = new BAP(currentBackup.xprv); } else if (isMasterBackupType42(currentBackup)) { bap = new BAP({ rootPk: currentBackup.rootPk }); } else { throw new Error("Unsupported backup format for profile sync"); } // Import existing IDs if available if (currentBackup.ids) { bap.importIds(currentBackup.ids); } const existingIds = bap.listIds(); const discoveredProfiles = []; // Step 1: Fetch profiles for existing known IDs setSyncState((prev) => ({ ...prev, progress: 10 })); for (const [index, idKey] of existingIds.entries()) { const profileResult = await fetchProfile(idKey); if (profileResult.result) { const identity = bap.getId(idKey); const address = identity?.getCurrentAddress() || ""; discoveredProfiles.push({ id: idKey, address, isPublished: true, // Map additional fields from SIGMA response (avoiding conflicts) ...(profileResult.result && typeof profileResult.result === "object" ? Object.fromEntries(Object.entries(profileResult.result).filter(([key]) => !["id", "address", "isPublished"].includes(key))) : {}), }); } setSyncState((prev) => ({ ...prev, progress: 10 + (index / existingIds.length) * 30, })); } // Step 2: Sequential discovery of new IDs setSyncState((prev) => ({ ...prev, progress: 40 })); let emptyResults = 0; let currentAttempt = 0; const maxAttempts = maxDiscoveryAttempts; while (emptyResults < 5 && currentAttempt < maxAttempts) { const nextId = bap.newId(); // Generate next sequential ID const nextIdKey = nextId.getIdentityKey(); setSyncState((prev) => ({ ...prev, currentId: currentAttempt + 1, progress: 40 + (currentAttempt / maxAttempts) * 50, })); try { const profileResult = await fetchProfile(nextIdKey); if (profileResult.result) { // Found a profile! Reset empty counter emptyResults = 0; const address = nextId.getCurrentAddress(); discoveredProfiles.push({ id: nextIdKey, address, isPublished: true, // Map additional fields from SIGMA response (avoiding conflicts) ...(profileResult.result && typeof profileResult.result === "object" ? Object.fromEntries(Object.entries(profileResult.result).filter(([key]) => !["id", "address", "isPublished"].includes(key))) : {}), }); setSyncState((prev) => ({ ...prev, foundCount: prev.foundCount + 1, })); } else { // No profile found, increment empty counter emptyResults++; } } catch (error) { console.warn(`Error checking ID ${nextIdKey}:`, error); emptyResults++; } currentAttempt++; // Small delay to avoid overwhelming the API await new Promise((resolve) => setTimeout(resolve, 100)); } // Step 3: Update backup with discovered IDs setSyncState((prev) => ({ ...prev, progress: 95 })); const finalIds = bap.exportIds(); let updatedBackup = null; // Only update if we found new IDs if (finalIds !== currentBackup.ids) { updatedBackup = { ...currentBackup, ids: finalIds, }; // Update the backup in session storage // Note: We're not persisting to localStorage here for security // The updated backup will be used in the current session console.log("Profile sync found new identities, backup updated in session"); } setSyncState((prev) => ({ ...prev, progress: 100, isScanning: false })); return { discoveredProfiles, updatedBackup, totalFound: discoveredProfiles.length, }; }, }); // Auto-sync after successful login useEffect(() => { if (autoSync && user && !syncMutation.isPending) { // Small delay to ensure user is fully authenticated const timer = setTimeout(() => { syncMutation.mutate(); }, 1000); return () => clearTimeout(timer); } return undefined; }, [autoSync, user, syncMutation]); // Manual sync function const syncProfiles = useCallback(() => { syncMutation.mutate(); }, [syncMutation]); // Reset sync state const resetSync = useCallback(() => { setSyncState({ isScanning: false, progress: 0, currentId: 0, foundCount: 0, error: null, }); syncMutation.reset(); }, [syncMutation]); return { // Sync operations syncProfiles, resetSync, // State isScanning: syncState.isScanning || syncMutation.isPending, progress: syncState.progress, currentId: syncState.currentId, foundCount: syncState.foundCount, error: syncMutation.error || syncState.error, // Results result: syncMutation.data, isSuccess: syncMutation.isSuccess, // Utility functions fetchProfile, validateByAddress, }; }