From 6c2b2dbeae0ba2df505ccbd73ef503498d628a2e Mon Sep 17 00:00:00 2001 From: Govard Barkhatov Date: Wed, 3 Sep 2025 19:15:30 +0300 Subject: [PATCH 1/3] feat: use expansion visibility service --- .../services/useExpansionVisibilityService.ts | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 src/ui/common/hooks/services/useExpansionVisibilityService.ts diff --git a/src/ui/common/hooks/services/useExpansionVisibilityService.ts b/src/ui/common/hooks/services/useExpansionVisibilityService.ts new file mode 100644 index 000000000..dff05d6b3 --- /dev/null +++ b/src/ui/common/hooks/services/useExpansionVisibilityService.ts @@ -0,0 +1,144 @@ +import { useCallback, useMemo } from "react"; + +import { useDelegationStorage } from "@/ui/common/hooks/storage/useDelegationStorage"; +import { useStakingExpansionState } from "@/ui/common/state/StakingExpansionState"; +import { + DelegationV2, + DelegationV2StakingState, +} from "@/ui/common/types/delegationsV2"; +import { getExpansionsLocalStorageKey } from "@/ui/common/utils/local_storage/getExpansionsLocalStorageKey"; + +/** + * Hook providing expansion visibility services. + * Centralizes complex logic for determining which delegations should be visible + * in different UI parts (Activity tab vs Verified Expansion Modal). + */ +export function useExpansionVisibilityService( + publicKeyNoCoord: string | undefined, +) { + const { expansions } = useStakingExpansionState(); + + // Access expansion localStorage for broadcast status detection + const expansionStorageKey = publicKeyNoCoord + ? getExpansionsLocalStorageKey(publicKeyNoCoord) + : ""; + + // Get pending delegations from localStorage to check broadcast status + const { delegations: expansionStorageDelegations } = useDelegationStorage( + expansionStorageKey, + expansions, + ); + + /** + * Checks if a delegation is a broadcasted expansion. + * A broadcasted expansion has INTERMEDIATE_PENDING_VERIFICATION status in localStorage + * and is VERIFIED in the API data. + */ + const isBroadcastedExpansion = useCallback( + (delegation: DelegationV2): boolean => { + // Check if this delegation exists in localStorage with INTERMEDIATE_PENDING_VERIFICATION status + const storedDelegation = expansionStorageDelegations.find( + (stored) => stored.stakingTxHashHex === delegation.stakingTxHashHex, + ); + + return ( + storedDelegation?.state === + DelegationV2StakingState.INTERMEDIATE_PENDING_VERIFICATION + ); + }, + [expansionStorageDelegations], + ); + + /** + * Checks if an original staking transaction should be hidden. + * Original staking transactions are hidden when they have broadcasted expansions. + */ + const isOriginalTransactionHidden = useCallback( + (stakeHashHex: string, delegations: DelegationV2[]): boolean => { + // Find any expansion that references this original transaction + const relatedExpansions = delegations.filter( + (delegation) => + delegation.previousStakingTxHashHex === stakeHashHex && + isBroadcastedExpansion(delegation), + ); + + return relatedExpansions.length > 0; + }, + [isBroadcastedExpansion], + ); + + /** + * 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) + * 3. Exclude original transactions that have broadcasted expansions + * 4. Include all other regular transactions + */ + const getActivityTabDelegations = useCallback( + (delegations: DelegationV2[]): DelegationV2[] => { + return delegations.filter((delegation) => { + // Always exclude EXPANDED state delegations (existing logic) + if (delegation.state === DelegationV2StakingState.EXPANDED) { + return false; + } + + // Handle VERIFIED expansions + if ( + delegation.state === DelegationV2StakingState.VERIFIED && + delegation.previousStakingTxHashHex + ) { + // Only include if it's been broadcasted by the user + return isBroadcastedExpansion(delegation); + } + + // Check if this is an original transaction that should be hidden + if ( + isOriginalTransactionHidden(delegation.stakingTxHashHex, delegations) + ) { + return false; + } + + // Include all other transactions + return true; + }); + }, + [isBroadcastedExpansion, isOriginalTransactionHidden], + ); + + /** + * Returns delegations that should be visible in the Verified Expansion Modal. + * These are VERIFIED expansions with previousStakingTxHashHex that haven't been broadcasted yet. + */ + const getVerifiedExpansionModalDelegations = useCallback( + (delegations: DelegationV2[]): DelegationV2[] => { + return delegations.filter( + (delegation) => + delegation.state === DelegationV2StakingState.VERIFIED && + delegation.previousStakingTxHashHex && + !isBroadcastedExpansion(delegation), + ); + }, + [isBroadcastedExpansion], + ); + + /** + * Memoized results for performance optimization. + */ + const memoizedResults = useMemo( + () => ({ + getActivityTabDelegations, + getVerifiedExpansionModalDelegations, + isOriginalTransactionHidden, + isBroadcastedExpansion, + }), + [ + getActivityTabDelegations, + getVerifiedExpansionModalDelegations, + isOriginalTransactionHidden, + isBroadcastedExpansion, + ], + ); + + return memoizedResults; +} From a5fc6625c5beb40f8b6b45133db038d5cf6d9e23 Mon Sep 17 00:00:00 2001 From: Govard Barkhatov Date: Wed, 3 Sep 2025 19:33:31 +0300 Subject: [PATCH 2/3] chore: return usecallback --- .../services/useExpansionVisibilityService.ts | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/src/ui/common/hooks/services/useExpansionVisibilityService.ts b/src/ui/common/hooks/services/useExpansionVisibilityService.ts index dff05d6b3..55409d4fd 100644 --- a/src/ui/common/hooks/services/useExpansionVisibilityService.ts +++ b/src/ui/common/hooks/services/useExpansionVisibilityService.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from "react"; +import { useCallback } from "react"; import { useDelegationStorage } from "@/ui/common/hooks/storage/useDelegationStorage"; import { useStakingExpansionState } from "@/ui/common/state/StakingExpansionState"; @@ -122,23 +122,10 @@ export function useExpansionVisibilityService( [isBroadcastedExpansion], ); - /** - * Memoized results for performance optimization. - */ - const memoizedResults = useMemo( - () => ({ - getActivityTabDelegations, - getVerifiedExpansionModalDelegations, - isOriginalTransactionHidden, - isBroadcastedExpansion, - }), - [ - getActivityTabDelegations, - getVerifiedExpansionModalDelegations, - isOriginalTransactionHidden, - isBroadcastedExpansion, - ], - ); - - return memoizedResults; + return { + getActivityTabDelegations, + getVerifiedExpansionModalDelegations, + isOriginalTransactionHidden, + isBroadcastedExpansion, + }; } From 58fa499967d2366b2cb919f83b4b7a247df83a39 Mon Sep 17 00:00:00 2001 From: Govard Barkhatov Date: Thu, 4 Sep 2025 08:52:10 +0300 Subject: [PATCH 3/3] chore: resolve comments --- .../services/useExpansionVisibilityService.ts | 46 ++++++++++++++----- .../getExpansionsLocalStorageKey.ts | 7 ++- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/ui/common/hooks/services/useExpansionVisibilityService.ts b/src/ui/common/hooks/services/useExpansionVisibilityService.ts index 55409d4fd..ca508d4a9 100644 --- a/src/ui/common/hooks/services/useExpansionVisibilityService.ts +++ b/src/ui/common/hooks/services/useExpansionVisibilityService.ts @@ -19,26 +19,32 @@ export function useExpansionVisibilityService( const { expansions } = useStakingExpansionState(); // Access expansion localStorage for broadcast status detection - const expansionStorageKey = publicKeyNoCoord - ? getExpansionsLocalStorageKey(publicKeyNoCoord) - : ""; + const expansionStorageKey = getExpansionsLocalStorageKey(publicKeyNoCoord); // Get pending delegations from localStorage to check broadcast status const { delegations: expansionStorageDelegations } = useDelegationStorage( expansionStorageKey, - expansions, + expansions ?? [], ); /** * Checks if a delegation is a broadcasted expansion. - * A broadcasted expansion has INTERMEDIATE_PENDING_VERIFICATION status in localStorage - * and is VERIFIED in the API data. + * 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) */ const isBroadcastedExpansion = useCallback( (delegation: DelegationV2): boolean => { - // Check if this delegation exists in localStorage with INTERMEDIATE_PENDING_VERIFICATION status - const storedDelegation = expansionStorageDelegations.find( - (stored) => stored.stakingTxHashHex === delegation.stakingTxHashHex, + // First, verify this is actually VERIFIED in the API data + if (delegation.state !== DelegationV2StakingState.VERIFIED) { + return false; + } + + // Then check if this delegation exists in localStorage with INTERMEDIATE_PENDING_VERIFICATION status + const storedDelegation = (expansionStorageDelegations ?? []).find( + (stored) => + stored.stakingTxHashHex.toLowerCase() === + delegation.stakingTxHashHex.toLowerCase(), ); return ( @@ -77,6 +83,22 @@ export function useExpansionVisibilityService( */ const getActivityTabDelegations = useCallback( (delegations: DelegationV2[]): DelegationV2[] => { + // Performance optimization: Pre-compute hidden transaction IDs to avoid O^2 complexity + const hiddenOriginalTxIds = new Set(); + + // First pass: identify all original transactions that should be hidden + delegations.forEach((delegation) => { + if ( + delegation.previousStakingTxHashHex && + isBroadcastedExpansion(delegation) + ) { + hiddenOriginalTxIds.add( + delegation.previousStakingTxHashHex.toLowerCase(), + ); + } + }); + + // Second pass: filter delegations using O(1) Set lookups return delegations.filter((delegation) => { // Always exclude EXPANDED state delegations (existing logic) if (delegation.state === DelegationV2StakingState.EXPANDED) { @@ -92,9 +114,9 @@ export function useExpansionVisibilityService( return isBroadcastedExpansion(delegation); } - // Check if this is an original transaction that should be hidden + // Check if this is an original transaction that should be hidden (O(1) lookup) if ( - isOriginalTransactionHidden(delegation.stakingTxHashHex, delegations) + hiddenOriginalTxIds.has(delegation.stakingTxHashHex.toLowerCase()) ) { return false; } @@ -103,7 +125,7 @@ export function useExpansionVisibilityService( return true; }); }, - [isBroadcastedExpansion, isOriginalTransactionHidden], + [isBroadcastedExpansion], ); /** diff --git a/src/ui/common/utils/local_storage/getExpansionsLocalStorageKey.ts b/src/ui/common/utils/local_storage/getExpansionsLocalStorageKey.ts index e92f9e845..12a7e799c 100644 --- a/src/ui/common/utils/local_storage/getExpansionsLocalStorageKey.ts +++ b/src/ui/common/utils/local_storage/getExpansionsLocalStorageKey.ts @@ -2,7 +2,10 @@ import { network } from "@/ui/common/config/network/btc"; const EXPANSIONS_KEY = "bbn-staking-expansions"; +// Safe no-op key to prevent potential issues with empty string +const NO_KEY = "__NO_KEY__"; + // Get the local storage key for staking expansions -export const getExpansionsLocalStorageKey = (pk: string) => { - return pk ? `${EXPANSIONS_KEY}-${network}-${pk}` : ""; +export const getExpansionsLocalStorageKey = (pk: string | undefined) => { + return pk ? `${EXPANSIONS_KEY}-${network}-${pk}` : NO_KEY; };