diff --git a/src/ui/common/hooks/services/useExpansionVisibilityService.ts b/src/ui/common/hooks/services/useExpansionVisibilityService.ts index ca508d4a9..2b823fd95 100644 --- a/src/ui/common/hooks/services/useExpansionVisibilityService.ts +++ b/src/ui/common/hooks/services/useExpansionVisibilityService.ts @@ -31,7 +31,7 @@ export function useExpansionVisibilityService( * Checks if a delegation is a broadcasted expansion. * A broadcasted expansion must meet both criteria: * 1. Has VERIFIED status in the API data (delegation.state) - * 2. Has INTERMEDIATE_PENDING_VERIFICATION status in localStorage (user broadcasted it) + * 2. Has INTERMEDIATE_PENDING_BTC_CONFIRMATION status in localStorage (user broadcasted it) */ const isBroadcastedExpansion = useCallback( (delegation: DelegationV2): boolean => { @@ -40,7 +40,7 @@ export function useExpansionVisibilityService( return false; } - // Then check if this delegation exists in localStorage with INTERMEDIATE_PENDING_VERIFICATION status + // Then check if this delegation exists in localStorage with INTERMEDIATE_PENDING_BTC_CONFIRMATION status const storedDelegation = (expansionStorageDelegations ?? []).find( (stored) => stored.stakingTxHashHex.toLowerCase() === @@ -49,7 +49,7 @@ export function useExpansionVisibilityService( return ( storedDelegation?.state === - DelegationV2StakingState.INTERMEDIATE_PENDING_VERIFICATION + DelegationV2StakingState.INTERMEDIATE_PENDING_BTC_CONFIRMATION ); }, [expansionStorageDelegations], @@ -77,7 +77,7 @@ export function useExpansionVisibilityService( * Returns delegations that should be visible in the Activity tab. * Applies the following rules: * 1. Exclude VERIFIED expansions that are not broadcasted (show in modal only) - * 2. Include VERIFIED expansions that are broadcasted (INTERMEDIATE_PENDING_VERIFICATION) + * 2. Include VERIFIED expansions that are broadcasted (INTERMEDIATE_PENDING_BTC_CONFIRMATION) * 3. Exclude original transactions that have broadcasted expansions * 4. Include all other regular transactions */ diff --git a/src/ui/common/hooks/services/useStakingExpansionService.ts b/src/ui/common/hooks/services/useStakingExpansionService.ts index 2760b0f47..6107cafdd 100644 --- a/src/ui/common/hooks/services/useStakingExpansionService.ts +++ b/src/ui/common/hooks/services/useStakingExpansionService.ts @@ -17,6 +17,7 @@ import { DelegationV2, } from "@/ui/common/types/delegationsV2"; import { retry } from "@/ui/common/utils"; +import { markExpansionAsBroadcasted } from "@/ui/common/utils/local_storage/expansionStorage"; import { getTxHex } from "@/ui/common/utils/mempool_api"; import { validateExpansionFormData } from "@/ui/common/utils/stakingExpansionValidation"; @@ -388,6 +389,12 @@ export function useStakingExpansionService() { DelegationState.INTERMEDIATE_PENDING_BTC_CONFIRMATION, ); + // Mark expansion as broadcasted in localStorage for visibility tracking + markExpansionAsBroadcasted( + delegation.stakingTxHashHex, + publicKeyNoCoord, + ); + // Navigate to success goToStep(StakingExpansionStep.FEEDBACK_SUCCESS); setProcessing(false); @@ -409,6 +416,7 @@ export function useStakingExpansionService() { reset, isUTXOsLoading, availableUTXOs, + publicKeyNoCoord, ], ); diff --git a/src/ui/common/utils/local_storage/expansionStorage.ts b/src/ui/common/utils/local_storage/expansionStorage.ts new file mode 100644 index 000000000..cc653a5f5 --- /dev/null +++ b/src/ui/common/utils/local_storage/expansionStorage.ts @@ -0,0 +1,174 @@ +import { + DelegationV2, + DelegationV2StakingState, +} from "@/ui/common/types/delegationsV2"; + +import { getExpansionsLocalStorageKey } from "./getExpansionsLocalStorageKey"; + +/** + * Helper function to remove an item from a localStorage record. + * This centralizes the localStorage record manipulation pattern used throughout the file. + * + * @param storageKey - The localStorage key + * @param itemId - The ID of the item to remove from the record + */ +function removeFromLocalStorageRecord( + storageKey: string, + itemId: string, +): void { + try { + const data = localStorage.getItem(storageKey); + if (data) { + const record = JSON.parse(data); + if (record[itemId]) { + delete record[itemId]; + localStorage.setItem(storageKey, JSON.stringify(record)); + } + } + } catch (error) { + console.error( + `Failed to remove item from localStorage key ${storageKey}:`, + error, + ); + } +} + +/** + * Mark an expansion as broadcasted by updating its status to INTERMEDIATE_PENDING_BTC_CONFIRMATION. + * This is called after a verified expansion is successfully signed and broadcasted to Bitcoin. + * + * @param expansionTxHashHex - The transaction hash of the expansion to mark as broadcasted + * @param publicKeyNoCoord - The public key of the wallet + */ +export function markExpansionAsBroadcasted( + expansionTxHashHex: string, + publicKeyNoCoord: string | undefined, +): void { + if (!publicKeyNoCoord) { + console.warn( + "Cannot mark expansion as broadcasted: no public key provided", + ); + return; + } + + const storageKey = getExpansionsLocalStorageKey(publicKeyNoCoord); + const statusesKey = `${storageKey}_statuses`; + + try { + // Get existing statuses from localStorage + const existingStatuses = localStorage.getItem(statusesKey); + const statuses = existingStatuses ? JSON.parse(existingStatuses) : {}; + + // Update the status for this expansion + statuses[expansionTxHashHex] = + DelegationV2StakingState.INTERMEDIATE_PENDING_BTC_CONFIRMATION; + + // Save back to localStorage + localStorage.setItem(statusesKey, JSON.stringify(statuses)); + } catch (error) { + console.error("Failed to mark expansion as broadcasted:", error); + } +} + +/** + * Get all broadcasted expansions from localStorage. + * Broadcasted expansions are those with INTERMEDIATE_PENDING_BTC_CONFIRMATION status. + * + * @param publicKeyNoCoord - The public key of the wallet + * @param expansions - The list of expansions to check against localStorage + * @returns Array of expansions that have been broadcasted + */ +export function getBroadcastedExpansions( + publicKeyNoCoord: string | undefined, + expansions: DelegationV2[], +): DelegationV2[] { + if (!publicKeyNoCoord || !expansions || expansions.length === 0) { + return []; + } + + const storageKey = getExpansionsLocalStorageKey(publicKeyNoCoord); + const statusesKey = `${storageKey}_statuses`; + + try { + // Get statuses from localStorage + const storedStatuses = localStorage.getItem(statusesKey); + if (!storedStatuses) { + return []; + } + + const statuses = JSON.parse(storedStatuses); + + // Filter expansions to only include those marked as broadcasted + return expansions.filter( + (expansion) => + statuses[expansion.stakingTxHashHex] === + DelegationV2StakingState.INTERMEDIATE_PENDING_BTC_CONFIRMATION, + ); + } catch (error) { + console.error("Failed to get broadcasted expansions:", error); + return []; + } +} + +/** + * Clean up a broadcasted expansion from localStorage when it becomes ACTIVE. + * This is called when an expansion transitions from broadcasted to confirmed on-chain. + * + * @param expansionTxHashHex - The transaction hash of the expansion to clean up + * @param publicKeyNoCoord - The public key of the wallet + */ +export function cleanupActiveExpansion( + expansionTxHashHex: string, + publicKeyNoCoord: string | undefined, +): void { + if (!publicKeyNoCoord) { + return; + } + + const storageKey = getExpansionsLocalStorageKey(publicKeyNoCoord); + const pendingKey = `${storageKey}_pending`; + const statusesKey = `${storageKey}_statuses`; + + // Remove from pending delegations + removeFromLocalStorageRecord(pendingKey, expansionTxHashHex); + + // Remove from statuses + removeFromLocalStorageRecord(statusesKey, expansionTxHashHex); +} + +/** + * Check if an expansion has been broadcasted. + * This is a convenience function that checks if the expansion has + * INTERMEDIATE_PENDING_BTC_CONFIRMATION status in localStorage. + * + * @param expansionTxHashHex - The transaction hash to check + * @param publicKeyNoCoord - The public key of the wallet + * @returns true if the expansion has been broadcasted, false otherwise + */ +export function isExpansionBroadcasted( + expansionTxHashHex: string, + publicKeyNoCoord: string | undefined, +): boolean { + if (!publicKeyNoCoord) { + return false; + } + + const storageKey = getExpansionsLocalStorageKey(publicKeyNoCoord); + const statusesKey = `${storageKey}_statuses`; + + try { + const statusesData = localStorage.getItem(statusesKey); + if (!statusesData) { + return false; + } + + const statuses = JSON.parse(statusesData); + return ( + statuses[expansionTxHashHex] === + DelegationV2StakingState.INTERMEDIATE_PENDING_BTC_CONFIRMATION + ); + } catch (error) { + console.error("Failed to check if expansion is broadcasted:", error); + return false; + } +}