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