Upstream merge brings 52 commits from safe-global/safe-wallet-monorepo into the luxfi/safe-wallet@dev fork. Conflicts resolved: - yarn.lock: deleted (we are on pnpm) - apps/web/package.json: kept pnpm version + brand build scripts (build:safe / build:lux / build:hanzo) plus upstream's new generate-version.mjs step - apps/mobile/.../Signer/appKit.ts: took upstream's import pattern for shared/projectId, shared/metadata, shared/mmkvStorageAdapter Yarn sweep applied to 42 files (docs, github-actions, workflows, AGENTS, specs, codemods, scripts, READMEs). Net result: zero yarn-* command references anywhere outside node_modules. ❯ pnpm is the law in this fork. No yarn.
27 KiB
Research: Counterfactual Feature Refactor
Feature: 002-counterfactual-refactor
Phase: 0 (Outline & Research)
Date: 2026-01-15
Purpose
Document refactoring patterns, file inventory, dependency mapping, public API design, and verification strategy for migrating the counterfactual feature to the standard architecture pattern established in 001-feature-architecture.
1. Pattern Analysis: Walletconnect Reference Implementation
Key Learnings from Walletconnect Refactor
What Worked Well:
- Clear Directory Structure: Components, hooks, services, and store each in dedicated subdirectories with barrel files
- Public API Boundary: Root
index.tsexports only what external code needs - types, feature flag hook, store exports, and lazy-loaded components - Feature Flag Pattern: Simple hook (
useIsWalletConnectEnabled) checkinguseHasFeature(FEATURES.NATIVE_WALLETCONNECT) - Lazy Loading: Default export uses
dynamic(() => import('./components/WalletConnectUi'), { ssr: false }) - Type Safety: All types centralized in
types.ts, re-exported from rootindex.tsasexport type {}
Structure Pattern (from walletconnect):
features/walletconnect/
├── index.ts // Public API with lazy loading
├── types.ts // All TypeScript interfaces
├── constants.ts // Feature constants
├── components/
│ ├── index.tsx // Component barrel
│ └── {ComponentName}/index.tsx
├── hooks/
│ ├── index.ts // Hook barrel
│ └── useIsWalletConnectEnabled.ts // Feature flag hook
├── services/
│ ├── index.ts // Service barrel
│ └── *.ts files
└── store/
├── index.ts // Store barrel
└── *Slice.ts files
Decision: Apply this exact pattern to counterfactual.
Rationale: Walletconnect refactor successfully demonstrates the pattern works. Proven approach reduces risk and ensures consistency across features.
Alternatives Considered: Custom structure for counterfactual - rejected because consistency is a key goal of the standard architecture.
2. File Inventory: Complete Manifest
Current Structure Analysis
Total Files: 20 TypeScript files + 1 CSS file
Files by Category:
| Category | Count | Files |
|---|---|---|
| Components (root) | 10 | ActivateAccountButton, ActivateAccountFlow, CheckBalance, CounterfactualForm, CounterfactualHooks, CounterfactualStatusButton, CounterfactualSuccessScreen, FirstTxFlow, LazyCounterfactual, PayNowPayLater |
| Hooks (root) | 2 | useCounterfactualBalances.ts |
| Hooks (hooks/) | 4 | useDeployGasLimit, useIsCounterfactualSafe, usePendingSafeNotifications, usePendingSafeStatuses |
| Services (root) | 1 | utils.ts |
| Services (services/) | 1 | safeCreationEvents.ts |
| Store | 1 | store/undeployedSafesSlice.ts |
| Tests | 2 | tests/utils.test.ts, tests/useDeployGasLimit.test.ts |
| Styles | 1 | styles.module.css |
File Movement Manifest
| Source Path | Destination Path | Action | Notes |
|---|---|---|---|
ActivateAccountButton.tsx |
components/ActivateAccountButton/index.tsx |
MOVE | Component → subdirectory |
ActivateAccountFlow.tsx |
components/ActivateAccountFlow/index.tsx |
MOVE | Component → subdirectory |
CheckBalance.tsx |
components/CheckBalance/index.tsx |
MOVE | Component → subdirectory |
CounterfactualForm.tsx |
components/CounterfactualForm/index.tsx |
MOVE | Component → subdirectory |
CounterfactualHooks.tsx |
components/CounterfactualHooks/index.tsx |
MOVE | Component → subdirectory |
CounterfactualStatusButton.tsx |
components/CounterfactualStatusButton/index.tsx |
MOVE | Component → subdirectory |
CounterfactualSuccessScreen.tsx |
components/CounterfactualSuccessScreen/index.tsx |
MOVE | Component → subdirectory |
FirstTxFlow.tsx |
components/FirstTxFlow/index.tsx |
MOVE | Component → subdirectory |
LazyCounterfactual.tsx |
components/LazyCounterfactual/index.tsx |
MOVE | Component → subdirectory |
PayNowPayLater.tsx |
components/PayNowPayLater/index.tsx |
MOVE | Component → subdirectory |
useCounterfactualBalances.ts |
hooks/useCounterfactualBalances.ts |
MOVE | Hook root → hooks/ |
hooks/useDeployGasLimit.ts |
hooks/useDeployGasLimit.ts |
KEEP | Already in correct location |
hooks/useIsCounterfactualSafe.ts |
hooks/useIsCounterfactualSafe.ts |
KEEP | Already in correct location |
hooks/usePendingSafeNotifications.ts |
hooks/usePendingSafeNotifications.ts |
KEEP | Already in correct location |
hooks/usePendingSafeStatuses.ts |
hooks/usePendingSafeStatuses.ts |
KEEP | Already in correct location |
| N/A | hooks/useIsCounterfactualEnabled.ts |
CREATE | New feature flag hook |
utils.ts |
services/counterfactualUtils.ts |
MOVE + RENAME | Service root → services/, rename for clarity |
services/safeCreationEvents.ts |
services/safeCreationEvents.ts |
KEEP | Already in correct location |
store/undeployedSafesSlice.ts |
store/undeployedSafesSlice.ts |
KEEP | Already in correct location |
__tests__/utils.test.ts |
services/__tests__/counterfactualUtils.test.ts |
MOVE + RENAME | Colocate with source |
__tests__/useDeployGasLimit.test.ts |
hooks/__tests__/useDeployGasLimit.test.ts |
MOVE | Colocate with source |
styles.module.css |
components/*/styles.module.css |
SPLIT | Move to component subdirectories as needed |
New Files to Create
| File Path | Purpose | Required |
|---|---|---|
index.ts |
Public API barrel with lazy loading | Yes |
types.ts |
All TypeScript interfaces | Yes |
constants.ts |
Feature constants (CF_TX_GROUP_KEY, etc.) | Yes |
components/index.ts |
Component barrel file | Yes |
hooks/index.ts |
Hook barrel file | Yes |
hooks/useIsCounterfactualEnabled.ts |
Feature flag hook | Yes |
services/index.ts |
Service barrel file | Yes |
store/index.ts |
Store barrel file | Yes |
Total Actions: 10 moves, 5 keeps, 1 rename, 1 create, 2 test relocations, 8 new barrel files = 27 file operations
3. Dependency Mapping: External Import Sites
Import Analysis
Total External Imports: 69 import statements across 49 files
External Import Matrix by Priority
Priority 1: Critical Infrastructure (must update first)
| File | Import Count | Imports | Update Complexity |
|---|---|---|---|
store/slices.ts |
1 | undeployedSafesSlice |
HIGH - exports for entire app |
Priority 2: Transaction Flow (core functionality)
| File | Import Count | Imports | Update Complexity |
|---|---|---|---|
components/tx-flow/actions/Counterfactual.tsx |
2 | utils, components | MEDIUM |
components/tx-flow/actions/Execute/index.tsx |
1 | hooks | LOW |
components/tx-flow/actions/Sign/index.tsx |
1 | hooks | LOW |
components/tx-flow/actions/ExecuteThroughRole/index.tsx |
1 | hooks | LOW |
components/tx-flow/actions/Batching/index.tsx |
1 | hooks | LOW |
components/tx-flow/features/BalanceChanges.tsx |
1 | hooks | LOW |
components/tx-flow/features/ExecuteCheckbox.tsx |
1 | hooks | LOW |
components/tx-flow/TxFlowProvider.tsx |
1 | hooks | LOW |
Priority 3: Safe Creation (activation flows)
| File | Import Count | Imports | Update Complexity |
|---|---|---|---|
components/new-safe/create/steps/StatusStep/index.tsx |
3 | components, hooks, utils | MEDIUM |
components/new-safe/create/steps/ReviewStep/index.tsx |
3 | components, hooks, utils | MEDIUM |
components/new-safe/create/steps/StatusStep/StatusMessage.tsx |
1 | hooks | LOW |
components/new-safe/create/steps/StatusStep/useUndeployedSafe.ts |
1 | store | LOW |
components/new-safe/create/logic/index.ts |
1 | utils | LOW |
Priority 4: Feature Integrations
| File | Import Count | Imports | Update Complexity |
|---|---|---|---|
features/myAccounts/components/AccountItems/SingleAccountItem.tsx |
2 | hooks, store | LOW |
features/myAccounts/components/AccountItems/MultiAccountItem.tsx |
2 | hooks, store | LOW |
features/myAccounts/components/AccountInfoChips/index.tsx |
1 | hooks | LOW |
features/multichain/utils/utils.ts |
1 | utils | LOW |
features/multichain/hooks/useSafeCreationData.ts |
1 | store | LOW |
features/multichain/components/CreateSafeOnNewChain/index.tsx |
1 | utils | LOW |
Priority 5: Loadables & Hooks
| File | Import Count | Imports | Update Complexity |
|---|---|---|---|
hooks/loadables/useLoadSafeInfo.ts |
2 | hooks, utils | LOW |
hooks/loadables/useLoadBalances.ts |
1 | utils | LOW |
hooks/loadables/useTrustedTokenBalances.ts |
1 | utils | LOW |
hooks/loadables/__tests__/useLoadBalances.test.ts |
1 | utils | LOW |
hooks/coreSDK/useInitSafeCoreSDK.ts |
1 | hooks | LOW |
hooks/coreSDK/safeCoreSDK.ts |
1 | utils | LOW |
Priority 6: UI Components
| File | Import Count | Imports | Update Complexity |
|---|---|---|---|
components/dashboard/FirstSteps/index.tsx |
4 | components, hooks, utils | MEDIUM |
components/sidebar/SidebarHeader/index.tsx |
1 | hooks | LOW |
components/sidebar/NewTxButton/index.tsx |
2 | hooks | LOW |
components/balances/AssetsTable/index.tsx |
1 | hooks | LOW |
components/settings/PushNotifications/GlobalPushNotifications.tsx |
1 | hooks | LOW |
components/settings/DataManagement/index.tsx |
1 | utils | LOW |
components/settings/DataManagement/ImportDialog.tsx |
1 | utils | LOW |
Priority 7: Internal Feature Imports (counterfactual files importing from counterfactual)
| File | Import Count | Imports | Update Complexity |
|---|---|---|---|
| Internal feature files | 21 | Various internal imports | MEDIUM - update after structure changes |
Update Strategy by Priority
- Phase 1: Update
store/slices.tsFIRST (blocks everything else) - Phase 2: Update transaction flow (8 files) - validates core functionality
- Phase 3: Update Safe creation (5 files) - validates activation flows
- Phase 4: Batch update feature integrations, loadables, UI components (28 files)
- Phase 5: Update internal imports within counterfactual feature (21 internal imports)
Decision: Phased update approach with critical infrastructure first.
Rationale: Minimizes risk by updating most critical paths first, enables incremental validation, and reduces blast radius if issues arise.
Alternatives Considered: Update all at once - rejected due to high risk of missing broken imports; would make debugging difficult.
4. Public API Design
Public API Surface Area
Based on external import analysis, the public API must export:
Types (tree-shakeable - safe to export all):
UndeployedSafeUndeployedSafesStateUndeployedSafeStatusUndeployedSafePropsReplayedSafePropsPredictedSafeProps(imported from@safe-global/protocol-kit, re-export for convenience)
Feature Flag Hook (required):
useIsCounterfactualEnabled(NEW)
Store Exports:
undeployedSafesSlice(slice itself)- Actions:
addUndeployedSafe,updateUndeployedSafeStatus,removeUndeployedSafe - Selectors:
selectUndeployedSafes,selectUndeployedSafe,selectUndeployedSafesByAddress,selectIsUndeployedSafe
Service Functions (used extensively across codebase):
getUndeployedSafeInfodeploySafeAndExecuteTxgetCounterfactualBalancereplayCounterfactualSafeDeploymentcheckSafeActivationcheckSafeActionViaRelayextractCounterfactualSafeSetupactivateReplayedSafeisReplayedSafePropsisPredictedSafePropsdispatchTxExecutionAndDeploySafe(used in tx flows)
Constants:
CF_TX_GROUP_KEY(used in transaction monitoring)
Components (used externally - may need lazy loading):
- Most counterfactual components are NOT used externally
- External usage is via hooks/services/store, not direct component imports
- No default export component needed (feature is integrated at multiple points)
Internal-Only APIs
These remain internal (not exported):
Hooks:
useDeployGasLimit- Internal gas calculationusePendingSafeStatuses- Internal monitoring (exports safeCreationPendingStatuses constant)usePendingSafeNotifications- Internal notification logic
Components (not directly exported, composed internally):
ActivateAccountFlow- Internal to ActivateAccountButtonCounterfactualSuccessScreen- Internal to CounterfactualHooksLazyCounterfactual- Internal to CounterfactualHooks
Public APIs (Actually Exported)
Hooks (all exported for React integration):
useIsCounterfactualEnabled- Feature flag check (REQUIRED)useIsCounterfactualSafe- Check if Safe is undeployed (used by 11+ external files)useCounterfactualBalances- Get balance data for undeployed SafessafeCreationPendingStatuses- Status constants for monitoring
Components (exported for UI integration points):
CounterfactualHooks- Global UI rendered in _app.tsxActivateAccountButton- Sidebar, NewTxButtonCheckBalance- AssetsTableCounterfactualForm- Tx flow actionsCounterfactualStatusButton- SidebarHeaderFirstTxFlow- DashboardPayNowPayLater- New Safe creationLoopIcon- Account info chips
Decision: Export types, hooks (including integration hooks), components (used at integration points), store, services, and constants. This is broader than initially planned but reflects actual integration requirements.
Rationale: The feature integrates deeply with transaction flows, sidebars, dashboards, and Safe creation. External code needs both hooks for state checks and components for UI rendering at multiple locations. Store selectors alone are insufficient for the complex UI integration requirements across 11+ external files.
Alternatives Considered:
- Export only selectors - rejected, less ergonomic and doesn't match existing patterns
- Single wrapper component - rejected, counterfactual UI appears at multiple independent locations
- Move integration logic external - rejected, would spread feature logic and violate encapsulation
5. Verification Strategy
Zero Behavioral Changes Guarantee
Testing Pyramid:
-
Type Safety (fast feedback):
pnpm --filter @safe-global/web type-check→ MUST pass- Catches 80% of issues immediately
- Run after every batch of import updates
-
Linting (import compliance):
pnpm --filter @safe-global/web lint→ zero no-restricted-imports warnings- Validates public API boundary enforcement
- Run before commit
-
Unit Tests (logic correctness):
pnpm --filter @safe-global/web test→ 100% pass rate- Existing tests must pass without modification (except import path updates)
- Run before commit
-
Build Verification (code splitting):
pnpm --filter @safe-global/web build→ succeeds- Check
.next/static/chunks/for counterfactual chunks - Verify separate bundle exists (not in main chunk)
-
Manual QA (user flows):
- Activate account flow (pay now)
- First transaction flow (pay later)
- Pending notifications display
- Safe creation with counterfactual
Verification Checklist
| Check | Command | Success Criteria | When to Run |
|---|---|---|---|
| Type Check | pnpm --filter @safe-global/web type-check |
Exit code 0, no errors | After each import batch |
| Linting | pnpm --filter @safe-global/web lint |
Zero no-restricted-imports warnings | Before commit |
| Unit Tests | pnpm --filter @safe-global/web test |
100% pass rate | Before commit |
| Build | pnpm --filter @safe-global/web build |
Succeeds, counterfactual chunks exist | Before commit |
| Bundle Analysis | Inspect .next/static/chunks/ |
Counterfactual code in separate chunks | After build |
| Manual QA: Activate | Test activate account flow | Works identically | Before PR |
| Manual QA: First TX | Test pay later flow | Works identically | Before PR |
| Manual QA: Notifications | Test pending Safe notifications | Appear correctly | Before PR |
Rollback Plan
Single Atomic Commit Strategy:
- All refactoring changes in ONE commit
- Enables clean rollback:
git revert <commit-sha> - If issues discovered post-merge: revert immediately, investigate separately
Rollback Verification:
- Run
git revert <commit-sha> - Run full test suite:
pnpm --filter @safe-global/web test - Verify 100% pass rate (back to pre-refactor state)
- Deploy reverted code if issues found in production
Decision: Single atomic commit with comprehensive verification before merge.
Rationale: Reduces risk by enabling instant rollback. Comprehensive pre-merge verification minimizes chance of needing rollback.
Alternatives Considered: Multiple smaller commits - rejected because it makes rollback harder (must revert multiple commits in order) and increases risk of intermediate broken states.
6. Implementation Sequence
Recommended Execution Order
Phase 1: Preparation (non-breaking):
- Create all barrel files (
index.tsfiles) - empty initially - Create
types.tsandconstants.ts- empty initially - Run type-check (should still pass - new empty files don't break anything)
Phase 2: File Reorganization (breaking - do in single session):
- Move all component files to
components/*/index.tsx - Move hook files to
hooks/ - Move/rename
utils.tstoservices/counterfactualUtils.ts - Move test files to colocated
__tests__/directories - Update ALL internal imports within counterfactual feature
- Run type-check continuously to catch broken imports
Phase 3: Public API Establishment (breaking):
- Extract all interfaces to
types.ts - Extract constants to
constants.ts - Create
useIsCounterfactualEnabledhook - Populate all barrel files (
components/index.ts,hooks/index.ts,services/index.ts,store/index.ts) - Populate root
index.tswith public API exports - Run type-check (may fail - external imports not yet updated)
Phase 4: External Import Updates (critical path):
- Update
store/slices.tsFIRST - Update transaction flow files (8 files)
- Update Safe creation files (5 files)
- Batch update remaining files (28 files)
- Run type-check after each priority group
Phase 5: Verification (gate before commit):
- Type-check:
pnpm --filter @safe-global/web type-check→ PASS - Lint:
pnpm --filter @safe-global/web lint→ zero warnings - Tests:
pnpm --filter @safe-global/web test→ 100% pass - Build:
pnpm --filter @safe-global/web build→ succeeds - Bundle analysis: verify code splitting
- Manual QA: all critical flows work
Phase 6: Commit & Review:
- Semantic commit:
refactor: migrate counterfactual feature to standard architecture pattern - Push to branch
- Create PR with verification checklist in description
- CI must pass (all checks green)
- Code review
- Merge
Estimated Timeline
| Phase | Tasks | Estimated Time |
|---|---|---|
| Phase 1: Preparation | Create 8 barrel files | 15 minutes |
| Phase 2: File Reorganization | Move 12 files, update internal imports | 1 hour |
| Phase 3: Public API | Extract types/constants, create hooks, populate barrels | 1 hour |
| Phase 4: External Imports | Update 49 files across codebase | 2-3 hours |
| Phase 5: Verification | Run all checks, manual QA | 30 minutes |
| Phase 6: Commit & Review | Create PR, address feedback | 30 minutes |
| Total | 5-6 hours |
Recommendation: Execute Phases 2-4 in single focused session to minimize broken state duration.
Summary
Research Complete: All unknowns resolved, patterns documented, file manifest created, dependencies mapped, public API designed, verification strategy established.
Key Decisions:
- Follow walletconnect reference implementation pattern exactly
- Phased external import updates (critical infrastructure first)
- Export types, feature flag hook, store, services, constants only (internal hooks/components remain private)
- Single atomic commit with comprehensive pre-merge verification
- Zero behavioral changes - 100% test pass rate required
Ready for Phase 1: Design artifacts (data-model.md, contracts/public-api.ts, quickstart.md) can now be created with full context.
Risk Mitigation:
- Phased updates reduce blast radius
- Type-check after each batch catches issues early
- Single atomic commit enables instant rollback
- Comprehensive verification before merge minimizes production risk