- Status: Accepted
- Date: 2026-07-26
- Amends: None.
- Supersedes: None.
- Amended by: ADR 0015.
- Superseded by: None.
The legacy btcwallet architecture tightly coupled lifecycle management, synchronization logic, and state tracking within a single Wallet struct. This monolithic design led to several issues:
- Race Conditions: Ambiguity between "Started" and "Syncing" states made it difficult to safely manage concurrent access.
- Blocking Operations: Long-running sync operations would block control-plane requests (like
StoporInfo). - Testing Difficulty: The tight coupling made it nearly impossible to unit test synchronization logic in isolation from the full wallet stack.
We need a robust, testable, and concurrent architecture to support modern features like multi-wallet management and targeted rescans.
We will adopt a Controller-Syncer-State pattern with an Orthogonal State Model.
-
Controller (
Controllerinterface /Walletstruct):- Role: The public API surface and lifecycle manager.
- Responsibility: Validates requests, manages the
Start/Stoplifecycle, and delegates long-running tasks. It never blocks on chain operations.
-
Syncer (
chainSyncerinterface /syncerstruct):- Role: The background worker.
- Responsibility: Executes the chain loop, communicates with the backend, and manages the database state for synchronization. It is isolated and testable.
-
State (
walletStatestruct):- Role: The source of truth for the wallet's status.
- Responsibility: Maintains state across three independent dimensions (Lifecycle, Sync, Auth) using atomic operations.
Instead of a single status enum, we track three separate dimensions:
- Lifecycle:
Stopped->Starting->Started->Stopping - Synchronization:
BackendSyncing->Syncing->Synced|Rescanning - Authentication:
Locked|Unlocked
- Concurrency Safety: State transitions are atomic and explicitly managed, eliminating race conditions.
- Responsiveness: The Controller remains responsive to user requests even while the Syncer is performing heavy I/O.
- Testability: The
Syncercan be tested with a mockChainandStorewithout instantiating a fullWallet. TheControllercan be tested with a mockSyncer. - Clarity: The separation of concerns makes the codebase easier to navigate and reason about.
- Complexity: Increases the number of distinct types and files.
- Indirection: Calls to sync functionality now go through a channel-based request mechanism rather than direct method calls.