bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
18 lines • 10.5 kB
JSON
{
"name": "bap-identity-hooks-usebapprofilesync",
"type": "registry:hook",
"dependencies": [
"@tanstack/react-query",
"bsv-bap"
],
"devDependencies": [],
"registryDependencies": [],
"files": [
{
"path": "components/bap-identity/hooks/useBapProfileSync.ts",
"type": "registry:hook",
"content": "import { useMutation } from \"@tanstack/react-query\";\nimport { BAP } from \"bsv-bap\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport { useBitcoinAuth } from \"../../../hooks/useBitcoinAuth.js\";\nimport type { BapMasterBackup, ProfileInfo } from \"../../../lib/types.js\";\nimport {\n\tisBapMasterBackupLegacy,\n\tisMasterBackupType42,\n} from \"../../../lib/types.js\";\nimport { DEFAULT_SOCIAL_API_URL } from \"../../social/constants.js\";\n\ninterface ProfileSyncOptions {\n\tautoSync?: boolean; // Auto-sync after login\n\tmaxDiscoveryAttempts?: number; // Max sequential IDs to test\n\tsigmaApiUrl?: string; // Custom SIGMA API URL (defaults to DEFAULT_SOCIAL_API_URL)\n}\n\nexport interface ProfileSyncResult {\n\tdiscoveredProfiles: ProfileInfo[];\n\tupdatedBackup: BapMasterBackup | null;\n\ttotalFound: number;\n}\n\ninterface ProfileSyncState {\n\tisScanning: boolean;\n\tprogress: number;\n\tcurrentId: number;\n\tfoundCount: number;\n\terror: Error | null;\n}\n\nexport interface SigmaIdentityResponse {\n\tidKey: string;\n\taddress: string;\n\tisPublished: boolean;\n\tname?: string;\n\tdisplayName?: string;\n\tavatar?: string;\n\tdescription?: string;\n\twebsite?: string;\n\temail?: string;\n\tcreatedAt?: string;\n\tupdatedAt?: string;\n\tverified?: boolean;\n\tpublicKey?: string;\n\tsocialLinks?: {\n\t\ttwitter?: string;\n\t\tgithub?: string;\n\t\tlinkedin?: string;\n\t};\n}\n\nexport interface IdentityData extends SigmaIdentityResponse {\n\t// Alias for backward compatibility during transition\n}\n\nexport function useBapProfileSync(options: ProfileSyncOptions = {}) {\n\tconst {\n\t\tautoSync = false,\n\t\tmaxDiscoveryAttempts = 50, // Stop after 50 sequential empty results\n\t\tsigmaApiUrl = DEFAULT_SOCIAL_API_URL,\n\t} = options;\n\n\tconst { user, actions } = useBitcoinAuth();\n\tconst [syncState, setSyncState] = useState<ProfileSyncState>({\n\t\tisScanning: false,\n\t\tprogress: 0,\n\t\tcurrentId: 0,\n\t\tfoundCount: 0,\n\t\terror: null,\n\t});\n\n\t// Fetch profile from SIGMA API\n\tconst fetchProfile = useCallback(\n\t\tasync (\n\t\t\tidKey: string,\n\t\t): Promise<{ result: SigmaIdentityResponse | null }> => {\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(`${sigmaApiUrl}/identity/get`, {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t\t\tbody: JSON.stringify({ idKey }),\n\t\t\t\t});\n\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\t// 404 means profile doesn't exist, not an error\n\t\t\t\t\tif (response.status === 404) {\n\t\t\t\t\t\treturn { result: null };\n\t\t\t\t\t}\n\t\t\t\t\tthrow new Error(`API error: ${response.status}`);\n\t\t\t\t}\n\n\t\t\t\tconst data = await response.json();\n\t\t\t\treturn { result: data };\n\t\t\t} catch (error) {\n\t\t\t\tconsole.warn(`Failed to fetch profile for ${idKey}:`, error);\n\t\t\t\treturn { result: null };\n\t\t\t}\n\t\t},\n\t\t[sigmaApiUrl],\n\t);\n\n\t// Validate address against SIGMA API\n\tconst validateByAddress = useCallback(\n\t\tasync (address: string): Promise<SigmaIdentityResponse | null> => {\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(`${sigmaApiUrl}/identity/validByAddress`, {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t\t\tbody: JSON.stringify({ address }),\n\t\t\t\t});\n\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst data = await response.json();\n\t\t\t\treturn data;\n\t\t\t} catch (error) {\n\t\t\t\tconsole.warn(`Failed to validate address ${address}:`, error);\n\t\t\t\treturn null;\n\t\t\t}\n\t\t},\n\t\t[sigmaApiUrl],\n\t);\n\n\t// Main profile synchronization mutation\n\tconst syncMutation = useMutation<ProfileSyncResult, Error, void>({\n\t\tmutationFn: async () => {\n\t\t\tif (!user) {\n\t\t\t\tthrow new Error(\"No authenticated user\");\n\t\t\t}\n\n\t\t\t// Get current backup\n\t\t\tconst currentBackup = await actions.getCurrentBackup();\n\t\t\tif (!currentBackup) {\n\t\t\t\tthrow new Error(\"No backup found for profile sync\");\n\t\t\t}\n\n\t\t\tsetSyncState({\n\t\t\t\tisScanning: true,\n\t\t\t\tprogress: 0,\n\t\t\t\tcurrentId: 0,\n\t\t\t\tfoundCount: 0,\n\t\t\t\terror: null,\n\t\t\t});\n\n\t\t\t// Create BAP instance based on backup format\n\t\t\tlet bap: BAP;\n\t\t\tif (isBapMasterBackupLegacy(currentBackup)) {\n\t\t\t\tbap = new BAP(currentBackup.xprv);\n\t\t\t} else if (isMasterBackupType42(currentBackup)) {\n\t\t\t\tbap = new BAP({ rootPk: currentBackup.rootPk });\n\t\t\t} else {\n\t\t\t\tthrow new Error(\"Unsupported backup format for profile sync\");\n\t\t\t}\n\n\t\t\t// Import existing IDs if available\n\t\t\tif (currentBackup.ids) {\n\t\t\t\tbap.importIds(currentBackup.ids);\n\t\t\t}\n\n\t\t\tconst existingIds = bap.listIds();\n\t\t\tconst discoveredProfiles: ProfileInfo[] = [];\n\n\t\t\t// Step 1: Fetch profiles for existing known IDs\n\t\t\tsetSyncState((prev) => ({ ...prev, progress: 10 }));\n\n\t\t\tfor (const [index, idKey] of existingIds.entries()) {\n\t\t\t\tconst profileResult = await fetchProfile(idKey);\n\t\t\t\tif (profileResult.result) {\n\t\t\t\t\tconst identity = bap.getId(idKey);\n\t\t\t\t\tconst address = identity?.getCurrentAddress() || \"\";\n\n\t\t\t\t\tdiscoveredProfiles.push({\n\t\t\t\t\t\tid: idKey,\n\t\t\t\t\t\taddress,\n\t\t\t\t\t\tisPublished: true,\n\t\t\t\t\t\t// Map additional fields from SIGMA response (avoiding conflicts)\n\t\t\t\t\t\t...(profileResult.result && typeof profileResult.result === \"object\"\n\t\t\t\t\t\t\t? Object.fromEntries(\n\t\t\t\t\t\t\t\t\tObject.entries(profileResult.result).filter(\n\t\t\t\t\t\t\t\t\t\t([key]) => ![\"id\", \"address\", \"isPublished\"].includes(key),\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tsetSyncState((prev) => ({\n\t\t\t\t\t...prev,\n\t\t\t\t\tprogress: 10 + (index / existingIds.length) * 30,\n\t\t\t\t}));\n\t\t\t}\n\n\t\t\t// Step 2: Sequential discovery of new IDs\n\t\t\tsetSyncState((prev) => ({ ...prev, progress: 40 }));\n\n\t\t\tlet emptyResults = 0;\n\t\t\tlet currentAttempt = 0;\n\t\t\tconst maxAttempts = maxDiscoveryAttempts;\n\n\t\t\twhile (emptyResults < 5 && currentAttempt < maxAttempts) {\n\t\t\t\tconst nextId = bap.newId(); // Generate next sequential ID\n\t\t\t\tconst nextIdKey = nextId.getIdentityKey();\n\n\t\t\t\tsetSyncState((prev) => ({\n\t\t\t\t\t...prev,\n\t\t\t\t\tcurrentId: currentAttempt + 1,\n\t\t\t\t\tprogress: 40 + (currentAttempt / maxAttempts) * 50,\n\t\t\t\t}));\n\n\t\t\t\ttry {\n\t\t\t\t\tconst profileResult = await fetchProfile(nextIdKey);\n\n\t\t\t\t\tif (profileResult.result) {\n\t\t\t\t\t\t// Found a profile! Reset empty counter\n\t\t\t\t\t\temptyResults = 0;\n\t\t\t\t\t\tconst address = nextId.getCurrentAddress();\n\n\t\t\t\t\t\tdiscoveredProfiles.push({\n\t\t\t\t\t\t\tid: nextIdKey,\n\t\t\t\t\t\t\taddress,\n\t\t\t\t\t\t\tisPublished: true,\n\t\t\t\t\t\t\t// Map additional fields from SIGMA response (avoiding conflicts)\n\t\t\t\t\t\t\t...(profileResult.result &&\n\t\t\t\t\t\t\ttypeof profileResult.result === \"object\"\n\t\t\t\t\t\t\t\t? Object.fromEntries(\n\t\t\t\t\t\t\t\t\t\tObject.entries(profileResult.result).filter(\n\t\t\t\t\t\t\t\t\t\t\t([key]) =>\n\t\t\t\t\t\t\t\t\t\t\t\t![\"id\", \"address\", \"isPublished\"].includes(key),\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tsetSyncState((prev) => ({\n\t\t\t\t\t\t\t...prev,\n\t\t\t\t\t\t\tfoundCount: prev.foundCount + 1,\n\t\t\t\t\t\t}));\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// No profile found, increment empty counter\n\t\t\t\t\t\temptyResults++;\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.warn(`Error checking ID ${nextIdKey}:`, error);\n\t\t\t\t\temptyResults++;\n\t\t\t\t}\n\n\t\t\t\tcurrentAttempt++;\n\n\t\t\t\t// Small delay to avoid overwhelming the API\n\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, 100));\n\t\t\t}\n\n\t\t\t// Step 3: Update backup with discovered IDs\n\t\t\tsetSyncState((prev) => ({ ...prev, progress: 95 }));\n\n\t\t\tconst finalIds = bap.exportIds();\n\t\t\tlet updatedBackup: BapMasterBackup | null = null;\n\n\t\t\t// Only update if we found new IDs\n\t\t\tif (finalIds !== currentBackup.ids) {\n\t\t\t\tupdatedBackup = {\n\t\t\t\t\t...currentBackup,\n\t\t\t\t\tids: finalIds,\n\t\t\t\t};\n\n\t\t\t\t// Update the backup in session storage\n\t\t\t\t// Note: We're not persisting to localStorage here for security\n\t\t\t\t// The updated backup will be used in the current session\n\t\t\t\tconsole.log(\n\t\t\t\t\t\"Profile sync found new identities, backup updated in session\",\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tsetSyncState((prev) => ({ ...prev, progress: 100, isScanning: false }));\n\n\t\t\treturn {\n\t\t\t\tdiscoveredProfiles,\n\t\t\t\tupdatedBackup,\n\t\t\t\ttotalFound: discoveredProfiles.length,\n\t\t\t};\n\t\t},\n\t});\n\n\t// Auto-sync after successful login\n\tuseEffect(() => {\n\t\tif (autoSync && user && !syncMutation.isPending) {\n\t\t\t// Small delay to ensure user is fully authenticated\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tsyncMutation.mutate();\n\t\t\t}, 1000);\n\n\t\t\treturn () => clearTimeout(timer);\n\t\t}\n\t\treturn undefined;\n\t}, [autoSync, user, syncMutation]);\n\n\t// Manual sync function\n\tconst syncProfiles = useCallback(() => {\n\t\tsyncMutation.mutate();\n\t}, [syncMutation]);\n\n\t// Reset sync state\n\tconst resetSync = useCallback(() => {\n\t\tsetSyncState({\n\t\t\tisScanning: false,\n\t\t\tprogress: 0,\n\t\t\tcurrentId: 0,\n\t\t\tfoundCount: 0,\n\t\t\terror: null,\n\t\t});\n\t\tsyncMutation.reset();\n\t}, [syncMutation]);\n\n\treturn {\n\t\t// Sync operations\n\t\tsyncProfiles,\n\t\tresetSync,\n\n\t\t// State\n\t\tisScanning: syncState.isScanning || syncMutation.isPending,\n\t\tprogress: syncState.progress,\n\t\tcurrentId: syncState.currentId,\n\t\tfoundCount: syncState.foundCount,\n\t\terror: syncMutation.error || syncState.error,\n\n\t\t// Results\n\t\tresult: syncMutation.data,\n\t\tisSuccess: syncMutation.isSuccess,\n\n\t\t// Utility functions\n\t\tfetchProfile,\n\t\tvalidateByAddress,\n\t};\n}\n",
"target": "<%- config.aliases.hooks %>/usebapprofilesync.ts"
}
]
}