diff --git a/bdk_demo/lib/providers/wallet_providers.dart b/bdk_demo/lib/providers/wallet_providers.dart index 7038d9c..2e76097 100644 --- a/bdk_demo/lib/providers/wallet_providers.dart +++ b/bdk_demo/lib/providers/wallet_providers.dart @@ -1,8 +1,15 @@ +import 'package:bdk_demo/models/wallet_record.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import '../models/wallet_record.dart'; +import 'package:bdk_dart/bdk.dart'; import 'settings_providers.dart'; +typedef WalletDisposer = void Function(Wallet wallet); + +final walletDisposerProvider = Provider( + (ref) => + (wallet) => wallet.dispose(), +); + final activeWalletRecordProvider = NotifierProvider( ActiveWalletRecordNotifier.new, @@ -16,6 +23,43 @@ class ActiveWalletRecordNotifier extends Notifier { void clear() => state = null; } +final activeWalletProvider = NotifierProvider( + ActiveWalletNotifier.new, +); + +class ActiveWalletNotifier extends Notifier { + late WalletDisposer _walletDisposer; + Wallet? _currentWallet; + + void _disposeWallet(Wallet? wallet) { + if (wallet == null) return; + _walletDisposer(wallet); + } + + @override + Wallet? build() { + _walletDisposer = ref.read(walletDisposerProvider); + _currentWallet = null; + ref.onDispose(() => _disposeWallet(_currentWallet)); + return null; + } + + void set(Wallet wallet) { + if (identical(_currentWallet, wallet)) { + return; + } + _disposeWallet(_currentWallet); + _currentWallet = wallet; + state = wallet; + } + + void clear() { + _disposeWallet(_currentWallet); + _currentWallet = null; + state = null; + } +} + final walletRecordsProvider = NotifierProvider>( WalletRecordsNotifier.new, @@ -48,5 +92,4 @@ class WalletRecordsNotifier extends Notifier> { } } -// TODO: Add activeWalletProvider. // TODO: Add balanceProvider, syncStateProvider. diff --git a/bdk_demo/lib/services/storage_service.dart b/bdk_demo/lib/services/storage_service.dart index 0b30073..1b3b3e3 100644 --- a/bdk_demo/lib/services/storage_service.dart +++ b/bdk_demo/lib/services/storage_service.dart @@ -1,9 +1,8 @@ import 'dart:convert'; +import 'package:bdk_demo/models/wallet_record.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import '../models/wallet_record.dart'; - abstract final class _PrefKeys { static const introDone = 'intro_done'; static const darkTheme = 'dark_theme'; diff --git a/bdk_demo/lib/services/wallet_network_mapper.dart b/bdk_demo/lib/services/wallet_network_mapper.dart new file mode 100644 index 0000000..503dcca --- /dev/null +++ b/bdk_demo/lib/services/wallet_network_mapper.dart @@ -0,0 +1,10 @@ +import 'package:bdk_dart/bdk.dart'; +import 'package:bdk_demo/models/wallet_record.dart'; + +extension WalletNetworkX on WalletNetwork { + Network toBdkNetwork() => switch (this) { + WalletNetwork.signet => Network.signet, + WalletNetwork.testnet => Network.testnet, + WalletNetwork.regtest => Network.regtest, + }; +} diff --git a/bdk_demo/test/README.md b/bdk_demo/test/README.md new file mode 100644 index 0000000..875f34d --- /dev/null +++ b/bdk_demo/test/README.md @@ -0,0 +1,12 @@ +# Test Layout + +Tests are grouped by app layer to keep the suite easy to navigate as features grow. + +- `test/presentation/`: widget and UI behavior tests +- `test/providers/`: Riverpod notifier/provider state tests +- `test/services/`: service and mapping/unit logic tests +- `test/integration/` (future): multi-service or app-flow integration tests + +Naming convention: +- Use `*_test.dart` suffix. +- Prefer behavior-focused names (example: `app_shell_test.dart`). diff --git a/bdk_demo/test/widget_test.dart b/bdk_demo/test/presentation/app_shell_test.dart similarity index 100% rename from bdk_demo/test/widget_test.dart rename to bdk_demo/test/presentation/app_shell_test.dart diff --git a/bdk_demo/test/providers/active_wallet_notifier_test.dart b/bdk_demo/test/providers/active_wallet_notifier_test.dart new file mode 100644 index 0000000..ea9603a --- /dev/null +++ b/bdk_demo/test/providers/active_wallet_notifier_test.dart @@ -0,0 +1,91 @@ +import 'package:bdk_dart/bdk.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:bdk_demo/providers/wallet_providers.dart'; + +const _testExtendedPrivKey = + 'tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B'; + +Wallet _createTestWallet() { + final descriptor = Descriptor( + descriptor: 'wpkh($_testExtendedPrivKey/84h/1h/0h/0/*)', + network: Network.testnet, + ); + final changeDescriptor = Descriptor( + descriptor: 'wpkh($_testExtendedPrivKey/84h/1h/0h/1/*)', + network: Network.testnet, + ); + return Wallet( + descriptor: descriptor, + changeDescriptor: changeDescriptor, + network: Network.testnet, + persister: Persister.newInMemory(), + lookahead: 25, + ); +} + +void main() { + group('ActiveWalletNotifier', () { + test('set() with same wallet twice keeps state and avoids disposal', () { + var disposeCalls = 0; + final container = ProviderContainer( + overrides: [ + walletDisposerProvider.overrideWithValue((wallet) { + disposeCalls += 1; + wallet.dispose(); + }), + ], + ); + addTearDown(container.dispose); + + final notifier = container.read(activeWalletProvider.notifier); + final wallet = _createTestWallet(); + + notifier.set(wallet); + notifier.set(wallet); + + expect(disposeCalls, 0); + expect(identical(container.read(activeWalletProvider), wallet), isTrue); + }); + + test('disposes current wallet when provider is disposed', () { + var disposeCalls = 0; + final container = ProviderContainer( + overrides: [ + walletDisposerProvider.overrideWithValue((wallet) { + disposeCalls += 1; + wallet.dispose(); + }), + ], + ); + + final notifier = container.read(activeWalletProvider.notifier); + notifier.set(_createTestWallet()); + + container.dispose(); + + expect(disposeCalls, 1); + }); + + test('clear() disposes wallet and sets state to null', () { + var disposeCalls = 0; + final container = ProviderContainer( + overrides: [ + walletDisposerProvider.overrideWithValue((wallet) { + disposeCalls += 1; + wallet.dispose(); + }), + ], + ); + addTearDown(container.dispose); + + final notifier = container.read(activeWalletProvider.notifier); + notifier.set(_createTestWallet()); + + notifier.clear(); + + expect(disposeCalls, 1); + expect(container.read(activeWalletProvider), isNull); + }); + }); +} diff --git a/bdk_demo/test/services/wallet_network_mapper_test.dart b/bdk_demo/test/services/wallet_network_mapper_test.dart new file mode 100644 index 0000000..17f53d4 --- /dev/null +++ b/bdk_demo/test/services/wallet_network_mapper_test.dart @@ -0,0 +1,20 @@ +import 'package:bdk_dart/bdk.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:bdk_demo/models/wallet_record.dart'; +import 'package:bdk_demo/services/wallet_network_mapper.dart'; + +void main() { + group('WalletNetworkX.toBdkNetwork', () { + test('maps signet correctly', () { + expect(WalletNetwork.signet.toBdkNetwork(), Network.signet); + }); + + test('maps testnet correctly', () { + expect(WalletNetwork.testnet.toBdkNetwork(), Network.testnet); + }); + + test('maps regtest correctly', () { + expect(WalletNetwork.regtest.toBdkNetwork(), Network.regtest); + }); + }); +}