From 7849d0158199b0b7f8b1f0b841dc4fe34aad5e1a Mon Sep 17 00:00:00 2001 From: Govard Barkhatov Date: Thu, 4 Sep 2025 09:45:01 +0300 Subject: [PATCH 1/2] feat: expansion local storage --- .../services/useExpansionVisibilityService.ts | 8 +- .../services/useStakingExpansionService.ts | 8 + .../utils/local_storage/expansionStorage.ts | 164 ++++++++++++++++++ 3 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 src/ui/common/utils/local_storage/expansionStorage.ts 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..d7649d87a --- /dev/null +++ b/src/ui/common/utils/local_storage/expansionStorage.ts @@ -0,0 +1,164 @@ +import { + DelegationV2, + DelegationV2StakingState, +} from "@/ui/common/types/delegationsV2"; + +import { getExpansionsLocalStorageKey } from "./getExpansionsLocalStorageKey"; + +/** + * 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`; + + try { + // Remove from pending delegations + const pendingData = localStorage.getItem(pendingKey); + if (pendingData) { + const pending = JSON.parse(pendingData); + if (pending[expansionTxHashHex]) { + delete pending[expansionTxHashHex]; + localStorage.setItem(pendingKey, JSON.stringify(pending)); + } + } + + // Remove from statuses + const statusesData = localStorage.getItem(statusesKey); + if (statusesData) { + const statuses = JSON.parse(statusesData); + if (statuses[expansionTxHashHex]) { + delete statuses[expansionTxHashHex]; + localStorage.setItem(statusesKey, JSON.stringify(statuses)); + } + } + } catch (error) { + console.error("Failed to cleanup active expansion:", error); + } +} + +/** + * 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; + } +} From fb20d917836b808af062be90f0c1b478070088ed Mon Sep 17 00:00:00 2001 From: Govard Barkhatov Date: Thu, 4 Sep 2025 10:04:47 +0300 Subject: [PATCH 2/2] chore: helper function --- .../utils/local_storage/expansionStorage.ts | 54 +++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/src/ui/common/utils/local_storage/expansionStorage.ts b/src/ui/common/utils/local_storage/expansionStorage.ts index d7649d87a..cc653a5f5 100644 --- a/src/ui/common/utils/local_storage/expansionStorage.ts +++ b/src/ui/common/utils/local_storage/expansionStorage.ts @@ -5,6 +5,34 @@ import { 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. @@ -101,29 +129,11 @@ export function cleanupActiveExpansion( const pendingKey = `${storageKey}_pending`; const statusesKey = `${storageKey}_statuses`; - try { - // Remove from pending delegations - const pendingData = localStorage.getItem(pendingKey); - if (pendingData) { - const pending = JSON.parse(pendingData); - if (pending[expansionTxHashHex]) { - delete pending[expansionTxHashHex]; - localStorage.setItem(pendingKey, JSON.stringify(pending)); - } - } + // Remove from pending delegations + removeFromLocalStorageRecord(pendingKey, expansionTxHashHex); - // Remove from statuses - const statusesData = localStorage.getItem(statusesKey); - if (statusesData) { - const statuses = JSON.parse(statusesData); - if (statuses[expansionTxHashHex]) { - delete statuses[expansionTxHashHex]; - localStorage.setItem(statusesKey, JSON.stringify(statuses)); - } - } - } catch (error) { - console.error("Failed to cleanup active expansion:", error); - } + // Remove from statuses + removeFromLocalStorageRecord(statusesKey, expansionTxHashHex); } /**