diff --git a/bdk_demo/lib/features/transactions/models/demo_tx_details.dart b/bdk_demo/lib/features/transactions/models/transaction_history_item.dart similarity index 89% rename from bdk_demo/lib/features/transactions/models/demo_tx_details.dart rename to bdk_demo/lib/features/transactions/models/transaction_history_item.dart index 4be76c1..34a9185 100644 --- a/bdk_demo/lib/features/transactions/models/demo_tx_details.dart +++ b/bdk_demo/lib/features/transactions/models/transaction_history_item.dart @@ -1,6 +1,6 @@ import 'package:bdk_demo/core/utils/formatters.dart'; -class DemoTxDetails { +class TransactionHistoryItem { final String txid; final int sent; final int received; @@ -8,7 +8,7 @@ class DemoTxDetails { final int? blockHeight; final DateTime? confirmationTime; - const DemoTxDetails({ + const TransactionHistoryItem({ required this.txid, required this.sent, required this.received, diff --git a/bdk_demo/lib/features/transactions/transaction_detail_page.dart b/bdk_demo/lib/features/transactions/transaction_detail_page.dart index a48f154..720fb76 100644 --- a/bdk_demo/lib/features/transactions/transaction_detail_page.dart +++ b/bdk_demo/lib/features/transactions/transaction_detail_page.dart @@ -2,7 +2,7 @@ import 'package:bdk_demo/core/theme/app_theme.dart'; import 'package:bdk_demo/core/utils/formatters.dart'; import 'package:bdk_demo/features/shared/widgets/secondary_app_bar.dart'; import 'package:bdk_demo/features/shared/widgets/wallet_ui_helpers.dart'; -import 'package:bdk_demo/features/transactions/models/demo_tx_details.dart'; +import 'package:bdk_demo/features/transactions/models/transaction_history_item.dart'; import 'package:bdk_demo/features/transactions/transactions_controller.dart'; import 'package:bdk_demo/models/currency_unit.dart'; import 'package:flutter/material.dart'; @@ -13,7 +13,7 @@ class TransactionDetailPage extends ConsumerWidget { const TransactionDetailPage({super.key, required this.txid}); - String _formatAmount(DemoTxDetails transaction) { + String _formatAmount(TransactionHistoryItem transaction) { final amount = transaction.netAmount; final prefix = amount >= 0 ? '+' : '-'; final value = Formatters.formatBalance(amount.abs(), CurrencyUnit.satoshi); @@ -37,14 +37,14 @@ class TransactionDetailPage extends ConsumerWidget { loading: () => const WalletStateCard( icon: Icons.hourglass_bottom, title: 'Loading transaction', - message: 'Preparing placeholder transaction details...', + message: 'Reading wallet transaction details...', showSpinner: true, centered: true, ), error: (_, __) => WalletStateCard( icon: Icons.error_outline, title: 'Transaction unavailable', - message: 'The demo could not load placeholder transaction details.', + message: 'The wallet transaction details could not be loaded.', accentColor: theme.colorScheme.error, centered: true, ), @@ -54,7 +54,7 @@ class TransactionDetailPage extends ConsumerWidget { icon: Icons.search_off, title: 'Transaction not found', message: - 'No placeholder transaction was found for this txid.\n\n$txid', + 'No wallet transaction was found for this txid.\n\n$txid', centered: true, ); } @@ -83,7 +83,7 @@ class TransactionDetailPage extends ConsumerWidget { ), const SizedBox(height: 8), Text( - 'Standalone transaction detail view for the selected placeholder transaction.', + 'Transaction detail for the selected wallet transaction.', style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurface.withAlpha(170), ), diff --git a/bdk_demo/lib/features/transactions/transaction_history_mapper.dart b/bdk_demo/lib/features/transactions/transaction_history_mapper.dart new file mode 100644 index 0000000..668c3e0 --- /dev/null +++ b/bdk_demo/lib/features/transactions/transaction_history_mapper.dart @@ -0,0 +1,52 @@ +import 'package:bdk_demo/features/transactions/models/transaction_history_item.dart'; + +sealed class TransactionHistoryPosition { + const TransactionHistoryPosition(); +} + +class ConfirmedTransactionPosition extends TransactionHistoryPosition { + final int blockHeight; + final int confirmationTime; + + const ConfirmedTransactionPosition({ + required this.blockHeight, + required this.confirmationTime, + }); +} + +class UnconfirmedTransactionPosition extends TransactionHistoryPosition { + final int? timestamp; + + const UnconfirmedTransactionPosition({this.timestamp}); +} + +class TransactionHistoryMapper { + const TransactionHistoryMapper._(); + + static TransactionHistoryItem fromWalletData({ + required String txid, + required int sent, + required int received, + required TransactionHistoryPosition position, + }) { + return switch (position) { + ConfirmedTransactionPosition() => TransactionHistoryItem( + txid: txid, + sent: sent, + received: received, + pending: false, + blockHeight: position.blockHeight, + confirmationTime: DateTime.fromMillisecondsSinceEpoch( + position.confirmationTime * 1000, + isUtc: true, + ), + ), + UnconfirmedTransactionPosition() => TransactionHistoryItem( + txid: txid, + sent: sent, + received: received, + pending: true, + ), + }; + } +} diff --git a/bdk_demo/lib/features/transactions/transactions_controller.dart b/bdk_demo/lib/features/transactions/transactions_controller.dart index 79aadb1..50e372d 100644 --- a/bdk_demo/lib/features/transactions/transactions_controller.dart +++ b/bdk_demo/lib/features/transactions/transactions_controller.dart @@ -1,12 +1,13 @@ -import 'package:bdk_demo/features/transactions/models/demo_tx_details.dart'; +import 'package:bdk_demo/features/transactions/models/transaction_history_item.dart'; import 'package:bdk_demo/features/transactions/transactions_repository.dart'; +import 'package:bdk_demo/providers/wallet_providers.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -enum TransactionsLoadState { idle, loading, success, error } +enum TransactionsLoadState { idle, loading, success, error, noWallet } class TransactionsState { final TransactionsLoadState status; - final List transactions; + final List transactions; final String statusMessage; final String? errorMessage; @@ -21,13 +22,12 @@ class TransactionsState { : this( status: TransactionsLoadState.idle, transactions: const [], - statusMessage: - 'Load the transaction demo to preview list and detail states.', + statusMessage: 'Load the active wallet transaction history.', ); TransactionsState copyWith({ TransactionsLoadState? status, - List? transactions, + List? transactions, String? statusMessage, String? errorMessage, }) { @@ -46,20 +46,42 @@ final transactionsControllerProvider = ); final transactionDetailsProvider = - FutureProvider.family((ref, txid) { + FutureProvider.family((ref, txid) { final repository = ref.read(transactionsRepositoryProvider); return repository.loadTransactionByTxid(txid); }); class TransactionsController extends Notifier { @override - TransactionsState build() => const TransactionsState.idle(); + TransactionsState build() { + final hasWallet = ref.watch(hasActiveWalletProvider); + if (!hasWallet) { + return const TransactionsState( + status: TransactionsLoadState.noWallet, + transactions: [], + statusMessage: + 'Create or load a wallet before viewing transaction history.', + ); + } + return const TransactionsState.idle(); + } Future loadTransactions() async { + final hasWallet = ref.read(hasActiveWalletProvider); + if (!hasWallet) { + state = const TransactionsState( + status: TransactionsLoadState.noWallet, + transactions: [], + statusMessage: + 'Create or load a wallet before viewing transaction history.', + ); + return; + } + state = state.copyWith( status: TransactionsLoadState.loading, transactions: const [], - statusMessage: 'Loading placeholder transactions...', + statusMessage: 'Loading transaction history...', errorMessage: null, ); @@ -72,15 +94,15 @@ class TransactionsController extends Notifier { status: TransactionsLoadState.success, transactions: transactions, statusMessage: transactions.isEmpty - ? 'Transaction demo loaded. No transactions yet.' - : 'Transaction demo loaded. Showing placeholder transaction rows.', + ? 'Transaction history loaded. No transactions yet.' + : 'Transaction history loaded.', errorMessage: null, ); } catch (error) { state = state.copyWith( status: TransactionsLoadState.error, transactions: const [], - statusMessage: 'The transaction demo could not be loaded.', + statusMessage: 'Transaction history could not be loaded.', errorMessage: _readableError(error), ); } diff --git a/bdk_demo/lib/features/transactions/transactions_list_page.dart b/bdk_demo/lib/features/transactions/transactions_list_page.dart index 63acc02..55c6143 100644 --- a/bdk_demo/lib/features/transactions/transactions_list_page.dart +++ b/bdk_demo/lib/features/transactions/transactions_list_page.dart @@ -2,9 +2,10 @@ import 'package:bdk_demo/core/theme/app_theme.dart'; import 'package:bdk_demo/core/utils/formatters.dart'; import 'package:bdk_demo/features/shared/widgets/secondary_app_bar.dart'; import 'package:bdk_demo/features/shared/widgets/wallet_ui_helpers.dart'; -import 'package:bdk_demo/features/transactions/models/demo_tx_details.dart'; +import 'package:bdk_demo/features/transactions/models/transaction_history_item.dart'; import 'package:bdk_demo/features/transactions/transactions_controller.dart'; import 'package:bdk_demo/models/currency_unit.dart'; +import 'package:bdk_demo/providers/wallet_providers.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -12,7 +13,10 @@ import 'package:go_router/go_router.dart'; class TransactionsListPage extends ConsumerWidget { const TransactionsListPage({super.key}); - void _openTransactionDetail(BuildContext context, DemoTxDetails transaction) { + void _openTransactionDetail( + BuildContext context, + TransactionHistoryItem transaction, + ) { context.pushNamed( 'transactionDetail', pathParameters: {'txid': transaction.txid}, @@ -23,10 +27,12 @@ class TransactionsListPage extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final state = ref.watch(transactionsControllerProvider); + final hasWallet = ref.watch(hasActiveWalletProvider); final isLoading = state.status == TransactionsLoadState.loading; + final canLoad = hasWallet && !isLoading; return Scaffold( - appBar: const SecondaryAppBar(title: 'Transactions Demo'), + appBar: const SecondaryAppBar(title: 'Transaction History'), body: SafeArea( child: ListView( padding: const EdgeInsets.all(24), @@ -51,25 +57,25 @@ class TransactionsListPage extends ConsumerWidget { ), const SizedBox(height: 16), Text( - 'Transactions Demo', + 'Transaction History', style: theme.textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 8), Text( - 'Preview placeholder transaction list and detail states in a standalone transactions feature. This demo does not sync a real wallet or query the blockchain.', + 'View transactions from the currently loaded wallet. Sync the wallet to refresh balance and history.', style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurface.withAlpha(180), ), ), const SizedBox(height: 20), FilledButton.icon( - onPressed: isLoading - ? null - : () => ref + onPressed: canLoad + ? () => ref .read(transactionsControllerProvider.notifier) - .loadTransactions(), + .loadTransactions() + : null, icon: isLoading ? SizedBox( width: 16, @@ -83,8 +89,8 @@ class TransactionsListPage extends ConsumerWidget { label: Text( state.status == TransactionsLoadState.success || state.status == TransactionsLoadState.error - ? 'Reload Transactions' - : 'Load Transactions Demo', + ? 'Reload Transaction History' + : 'Load Transaction History', ), ), ], @@ -94,7 +100,7 @@ class TransactionsListPage extends ConsumerWidget { const SizedBox(height: 24), const _SectionHeading( title: 'Transactions', - subtitle: 'Placeholder transaction list and detail navigation', + subtitle: 'Active wallet transaction list and detail navigation', ), const SizedBox(height: 12), _TransactionsBody(state: state, onTap: _openTransactionDetail), @@ -107,7 +113,8 @@ class TransactionsListPage extends ConsumerWidget { class _TransactionsBody extends StatelessWidget { final TransactionsState state; - final void Function(BuildContext context, DemoTxDetails transaction) onTap; + final void Function(BuildContext context, TransactionHistoryItem transaction) + onTap; const _TransactionsBody({required this.state, required this.onTap}); @@ -116,20 +123,25 @@ class _TransactionsBody extends StatelessWidget { final theme = Theme.of(context); return switch (state.status) { + TransactionsLoadState.noWallet => const WalletStateCard( + icon: Icons.account_balance_wallet_outlined, + title: 'No active wallet', + message: 'Create or load a wallet before viewing transaction history.', + ), TransactionsLoadState.idle => WalletStateCard( icon: Icons.info_outline, - title: 'Transactions not loaded yet', + title: 'Transaction history not loaded yet', message: state.statusMessage, ), TransactionsLoadState.loading => const WalletStateCard( icon: Icons.hourglass_bottom, - title: 'Loading placeholder transactions...', - message: 'Preparing scaffolded transaction rows.', + title: 'Loading transaction history...', + message: 'Reading wallet transactions.', showSpinner: true, ), TransactionsLoadState.error => WalletStateCard( icon: Icons.error_outline, - title: 'Transaction demo failed', + title: 'Transaction history failed', message: state.errorMessage ?? state.statusMessage, accentColor: theme.colorScheme.error, ), @@ -139,7 +151,7 @@ class _TransactionsBody extends StatelessWidget { icon: Icons.history_toggle_off, title: 'No transactions yet', message: - 'The transaction demo loaded successfully, but no placeholder transactions are configured yet.', + 'The active wallet has no transactions yet. Sync the wallet or receive funds to populate history.', ) : Card( child: Padding( @@ -199,7 +211,7 @@ class _SectionHeading extends StatelessWidget { } class _TransactionRow extends StatelessWidget { - final DemoTxDetails transaction; + final TransactionHistoryItem transaction; final VoidCallback onTap; const _TransactionRow({required this.transaction, required this.onTap}); diff --git a/bdk_demo/lib/features/transactions/transactions_repository.dart b/bdk_demo/lib/features/transactions/transactions_repository.dart index 7f579af..6efa1b2 100644 --- a/bdk_demo/lib/features/transactions/transactions_repository.dart +++ b/bdk_demo/lib/features/transactions/transactions_repository.dart @@ -1,53 +1,149 @@ -import 'package:bdk_demo/features/transactions/models/demo_tx_details.dart'; +import 'package:bdk_dart/bdk.dart' as bdk; +import 'package:bdk_demo/features/transactions/models/transaction_history_item.dart'; +import 'package:bdk_demo/features/transactions/transaction_history_mapper.dart'; +import 'package:bdk_demo/providers/wallet_providers.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; abstract interface class TransactionsRepository { - Future> loadTransactions(); - Future loadTransactionByTxid(String txid); + Future> loadTransactions(); + Future loadTransactionByTxid(String txid); } -final transactionsRepositoryProvider = Provider( - (ref) => DemoTransactionsRepository(), -); - -class DemoTransactionsRepository implements TransactionsRepository { - DemoTransactionsRepository({ - this.delay = const Duration(milliseconds: 150), - List? transactions, - }) : _transactions = transactions ?? _defaultTransactions; - - final Duration delay; - final List _transactions; - - static final _defaultTransactions = [ - DemoTxDetails( - txid: '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd', - sent: 0, - received: 42000, - pending: false, - blockHeight: 120, - confirmationTime: DateTime(2024, 1, 2, 3, 4), - ), - const DemoTxDetails( - txid: 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', - sent: 1600, - received: 0, - pending: true, - ), - ]; +final transactionsRepositoryProvider = Provider((ref) { + final wallet = ref.watch(activeWalletProvider); + return WalletTransactionsRepository( + source: wallet == null ? null : BdkWalletTransactionSource(wallet), + ); +}); + +abstract interface class TransactionHistorySource { + List transactions(); + + TransactionHistoryRecord? transactionByTxid(String txid); +} + +class TransactionHistoryRecord { + final String txid; + final int sent; + final int received; + final TransactionHistoryPosition position; + + const TransactionHistoryRecord({ + required this.txid, + required this.sent, + required this.received, + required this.position, + }); +} + +class WalletTransactionsRepository implements TransactionsRepository { + WalletTransactionsRepository({required TransactionHistorySource? source}) + : _source = source; + + final TransactionHistorySource? _source; + + @override + Future> loadTransactions() async { + final source = _source; + if (source == null) return const []; + + return source.transactions().map(_mapRecord).toList(growable: false); + } @override - Future> loadTransactions() async { - await Future.delayed(delay); - return List.unmodifiable(_transactions); + Future loadTransactionByTxid(String txid) async { + final source = _source; + if (source == null) return null; + + final record = source.transactionByTxid(txid); + return record == null ? null : _mapRecord(record); } + TransactionHistoryItem _mapRecord(TransactionHistoryRecord record) { + return TransactionHistoryMapper.fromWalletData( + txid: record.txid, + sent: record.sent, + received: record.received, + position: record.position, + ); + } +} + +class BdkWalletTransactionSource implements TransactionHistorySource { + BdkWalletTransactionSource(this._wallet); + + final bdk.Wallet _wallet; + @override - Future loadTransactionByTxid(String txid) async { - final transactions = await loadTransactions(); - for (final transaction in transactions) { - if (transaction.txid == txid) return transaction; + List transactions() { + return _wallet + .transactions() + .map(_recordFromCanonicalTx) + .toList(growable: false); + } + + @override + TransactionHistoryRecord? transactionByTxid(String txid) { + try { + final parsedTxid = bdk.Txid.fromString(hex: txid); + try { + final canonicalTx = _wallet.getTx(txid: parsedTxid); + if (canonicalTx != null) return _recordFromCanonicalTx(canonicalTx); + } finally { + parsedTxid.dispose(); + } + } catch (_) { + // If the txid cannot be parsed or fetched directly, fall back to the + // wallet transaction list so the detail page still behaves gracefully. } - return null; + + return _findTransactionByTxid(transactions(), txid); + } + + TransactionHistoryRecord _recordFromCanonicalTx(bdk.CanonicalTx canonicalTx) { + final transaction = canonicalTx.transaction; + final sentAndReceived = _wallet.sentAndReceived(tx: transaction); + final txid = transaction.computeTxid(); + final txidText = txid.toString(); + final sentSat = sentAndReceived.sent.toSat(); + final receivedSat = sentAndReceived.received.toSat(); + + txid.dispose(); + transaction.dispose(); + sentAndReceived.sent.dispose(); + sentAndReceived.received.dispose(); + + return TransactionHistoryRecord( + txid: txidText, + sent: sentSat, + received: receivedSat, + position: _positionFromBdk(canonicalTx.chainPosition), + ); + } + + TransactionHistoryPosition _positionFromBdk(bdk.ChainPosition position) { + if (position is bdk.ConfirmedChainPosition) { + final confirmation = position.confirmationBlockTime; + return ConfirmedTransactionPosition( + blockHeight: confirmation.blockId.height, + confirmationTime: confirmation.confirmationTime, + ); + } + + if (position is bdk.UnconfirmedChainPosition) { + return UnconfirmedTransactionPosition(timestamp: position.timestamp); + } + + throw StateError('Unsupported transaction chain position: $position'); + } +} + +TransactionHistoryRecord? _findTransactionByTxid( + List transactions, + String txid, +) { + for (final transaction in transactions) { + if (transaction.txid == txid) return transaction; } + return null; } diff --git a/bdk_demo/lib/providers/wallet_providers.dart b/bdk_demo/lib/providers/wallet_providers.dart index 1f5dfdf..6474c9d 100644 --- a/bdk_demo/lib/providers/wallet_providers.dart +++ b/bdk_demo/lib/providers/wallet_providers.dart @@ -32,6 +32,10 @@ final activeWalletProvider = NotifierProvider( ActiveWalletNotifier.new, ); +final hasActiveWalletProvider = Provider((ref) { + return ref.watch(activeWalletProvider) != null; +}); + class ActiveWalletNotifier extends Notifier { late WalletDisposer _walletDisposer; Wallet? _currentWallet; diff --git a/bdk_demo/test/features/transactions/transaction_history_mapper_test.dart b/bdk_demo/test/features/transactions/transaction_history_mapper_test.dart new file mode 100644 index 0000000..461321c --- /dev/null +++ b/bdk_demo/test/features/transactions/transaction_history_mapper_test.dart @@ -0,0 +1,51 @@ +import 'package:bdk_demo/features/transactions/transaction_history_mapper.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('TransactionHistoryMapper', () { + test('maps confirmed wallet transaction data', () { + final item = TransactionHistoryMapper.fromWalletData( + txid: '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd', + sent: 1200, + received: 42000, + position: const ConfirmedTransactionPosition( + blockHeight: 120, + confirmationTime: 1704164640, + ), + ); + + expect( + item.txid, + '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd', + ); + expect(item.sent, 1200); + expect(item.received, 42000); + expect(item.netAmount, 40800); + expect(item.pending, isFalse); + expect(item.blockHeight, 120); + expect( + item.confirmationTime, + DateTime.fromMillisecondsSinceEpoch(1704164640000, isUtc: true), + ); + expect(item.statusLabel, 'confirmed'); + }); + + test('maps unconfirmed wallet transaction data as pending', () { + final item = TransactionHistoryMapper.fromWalletData( + txid: + 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', + sent: 1600, + received: 0, + position: const UnconfirmedTransactionPosition(timestamp: 1704164640), + ); + + expect(item.sent, 1600); + expect(item.received, 0); + expect(item.netAmount, -1600); + expect(item.pending, isTrue); + expect(item.blockHeight, isNull); + expect(item.confirmationTime, isNull); + expect(item.statusLabel, 'pending'); + }); + }); +} diff --git a/bdk_demo/test/features/transactions/transactions_repository_test.dart b/bdk_demo/test/features/transactions/transactions_repository_test.dart new file mode 100644 index 0000000..08186aa --- /dev/null +++ b/bdk_demo/test/features/transactions/transactions_repository_test.dart @@ -0,0 +1,90 @@ +import 'package:bdk_demo/features/transactions/transaction_history_mapper.dart'; +import 'package:bdk_demo/features/transactions/transactions_repository.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class _FakeTransactionHistorySource implements TransactionHistorySource { + _FakeTransactionHistorySource(this.records); + + final List records; + + @override + List transactions() => records; + + @override + TransactionHistoryRecord? transactionByTxid(String txid) { + for (final transaction in records) { + if (transaction.txid == txid) return transaction; + } + return null; + } +} + +void main() { + group('WalletTransactionsRepository', () { + test('returns empty history when no active wallet is available', () async { + final repository = WalletTransactionsRepository(source: null); + + final transactions = await repository.loadTransactions(); + + expect(transactions, isEmpty); + }); + + test('maps wallet transaction records into history items', () async { + final repository = WalletTransactionsRepository( + source: _FakeTransactionHistorySource([ + const TransactionHistoryRecord( + txid: + '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd', + sent: 1200, + received: 42000, + position: ConfirmedTransactionPosition( + blockHeight: 120, + confirmationTime: 1704164640, + ), + ), + const TransactionHistoryRecord( + txid: + 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', + sent: 1600, + received: 0, + position: UnconfirmedTransactionPosition(), + ), + ]), + ); + + final transactions = await repository.loadTransactions(); + + expect(transactions, hasLength(2)); + expect(transactions.first.txid, startsWith('123456')); + expect(transactions.first.netAmount, 40800); + expect(transactions.first.pending, isFalse); + expect(transactions.first.blockHeight, 120); + expect(transactions.last.netAmount, -1600); + expect(transactions.last.pending, isTrue); + }); + + test('loads a transaction detail by txid from wallet records', () async { + final repository = WalletTransactionsRepository( + source: _FakeTransactionHistorySource([ + const TransactionHistoryRecord( + txid: + '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd', + sent: 0, + received: 42000, + position: ConfirmedTransactionPosition( + blockHeight: 120, + confirmationTime: 1704164640, + ), + ), + ]), + ); + + final transaction = await repository.loadTransactionByTxid( + '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd', + ); + + expect(transaction, isNotNull); + expect(transaction!.received, 42000); + }); + }); +} diff --git a/bdk_demo/test/helpers/fakes/fake_transactions_repository.dart b/bdk_demo/test/helpers/fakes/fake_transactions_repository.dart index 7a7d0ea..30da3ed 100644 --- a/bdk_demo/test/helpers/fakes/fake_transactions_repository.dart +++ b/bdk_demo/test/helpers/fakes/fake_transactions_repository.dart @@ -1,4 +1,4 @@ -import 'package:bdk_demo/features/transactions/models/demo_tx_details.dart'; +import 'package:bdk_demo/features/transactions/models/transaction_history_item.dart'; import 'package:bdk_demo/features/transactions/transactions_repository.dart'; class FakeTransactionsRepository implements TransactionsRepository { @@ -7,11 +7,11 @@ class FakeTransactionsRepository implements TransactionsRepository { this.throwOnLoad = false, }); - final List transactions; + final List transactions; final bool throwOnLoad; @override - Future> loadTransactions() async { + Future> loadTransactions() async { if (throwOnLoad) { throw Exception('forced transaction load failure'); } @@ -19,7 +19,7 @@ class FakeTransactionsRepository implements TransactionsRepository { } @override - Future loadTransactionByTxid(String txid) async { + Future loadTransactionByTxid(String txid) async { final items = await loadTransactions(); for (final transaction in items) { if (transaction.txid == txid) return transaction; diff --git a/bdk_demo/test/helpers/fixtures/placeholder_transactions.dart b/bdk_demo/test/helpers/fixtures/transaction_history_items.dart similarity index 63% rename from bdk_demo/test/helpers/fixtures/placeholder_transactions.dart rename to bdk_demo/test/helpers/fixtures/transaction_history_items.dart index bb7d8c1..79c0032 100644 --- a/bdk_demo/test/helpers/fixtures/placeholder_transactions.dart +++ b/bdk_demo/test/helpers/fixtures/transaction_history_items.dart @@ -1,7 +1,7 @@ -import 'package:bdk_demo/features/transactions/models/demo_tx_details.dart'; +import 'package:bdk_demo/features/transactions/models/transaction_history_item.dart'; -final placeholderTransactions = [ - DemoTxDetails( +final transactionHistoryItems = [ + TransactionHistoryItem( txid: '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd', sent: 0, received: 42000, @@ -9,7 +9,7 @@ final placeholderTransactions = [ blockHeight: 120, confirmationTime: DateTime(2024, 1, 2, 3, 4), ), - const DemoTxDetails( + const TransactionHistoryItem( txid: 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', sent: 1600, received: 0, diff --git a/bdk_demo/test/presentation/transactions/transaction_detail_page_test.dart b/bdk_demo/test/presentation/transactions/transaction_detail_page_test.dart index 3123a27..883f909 100644 --- a/bdk_demo/test/presentation/transactions/transaction_detail_page_test.dart +++ b/bdk_demo/test/presentation/transactions/transaction_detail_page_test.dart @@ -5,7 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../helpers/fakes/fake_transactions_repository.dart'; -import '../../helpers/fixtures/placeholder_transactions.dart'; +import '../../helpers/fixtures/transaction_history_items.dart'; Future _pumpDetailPage( WidgetTester tester, { @@ -31,9 +31,9 @@ void main() { await _pumpDetailPage( tester, repository: FakeTransactionsRepository( - transactions: placeholderTransactions, + transactions: transactionHistoryItems, ), - txid: placeholderTransactions.first.txid, + txid: transactionHistoryItems.first.txid, ); expect(find.text('Transaction Detail'), findsOneWidget); @@ -51,13 +51,13 @@ void main() { testWidgets('updates when the txid changes', (tester) async { final repository = FakeTransactionsRepository( - transactions: placeholderTransactions, + transactions: transactionHistoryItems, ); await _pumpDetailPage( tester, repository: repository, - txid: placeholderTransactions.first.txid, + txid: transactionHistoryItems.first.txid, ); expect( @@ -71,7 +71,7 @@ void main() { await _pumpDetailPage( tester, repository: repository, - txid: placeholderTransactions.last.txid, + txid: transactionHistoryItems.last.txid, ); expect( diff --git a/bdk_demo/test/presentation/transactions/transactions_list_page_test.dart b/bdk_demo/test/presentation/transactions/transactions_list_page_test.dart index f0940b2..3ceab16 100644 --- a/bdk_demo/test/presentation/transactions/transactions_list_page_test.dart +++ b/bdk_demo/test/presentation/transactions/transactions_list_page_test.dart @@ -1,17 +1,19 @@ import 'package:bdk_demo/features/transactions/transaction_detail_page.dart'; import 'package:bdk_demo/features/transactions/transactions_list_page.dart'; import 'package:bdk_demo/features/transactions/transactions_repository.dart'; +import 'package:bdk_demo/providers/wallet_providers.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import '../../helpers/fakes/fake_transactions_repository.dart'; -import '../../helpers/fixtures/placeholder_transactions.dart'; +import '../../helpers/fixtures/transaction_history_items.dart'; Future _pumpTransactionsFlow( WidgetTester tester, { required TransactionsRepository repository, + bool hasActiveWallet = true, }) async { final router = GoRouter( initialLocation: '/transactions', @@ -32,7 +34,10 @@ Future _pumpTransactionsFlow( await tester.pumpWidget( ProviderScope( - overrides: [transactionsRepositoryProvider.overrideWithValue(repository)], + overrides: [ + transactionsRepositoryProvider.overrideWithValue(repository), + hasActiveWalletProvider.overrideWithValue(hasActiveWallet), + ], child: MaterialApp.router(routerConfig: router), ), ); @@ -40,28 +45,28 @@ Future _pumpTransactionsFlow( } void main() { - testWidgets('shows intro before loading transactions', (tester) async { + testWidgets('shows intro before loading transaction history', (tester) async { await _pumpTransactionsFlow( tester, repository: FakeTransactionsRepository( - transactions: placeholderTransactions, + transactions: transactionHistoryItems, ), ); - expect(find.text('Transactions Demo'), findsNWidgets(2)); - expect(find.text('Load Transactions Demo'), findsOneWidget); - expect(find.text('Transactions not loaded yet'), findsOneWidget); + expect(find.text('Transaction History'), findsNWidgets(2)); + expect(find.text('Load Transaction History'), findsOneWidget); + expect(find.text('Transaction history not loaded yet'), findsOneWidget); }); - testWidgets('loads and renders placeholder transactions', (tester) async { + testWidgets('loads and renders wallet transactions', (tester) async { await _pumpTransactionsFlow( tester, repository: FakeTransactionsRepository( - transactions: placeholderTransactions, + transactions: transactionHistoryItems, ), ); - await tester.tap(find.text('Load Transactions Demo')); + await tester.tap(find.text('Load Transaction History')); await tester.pumpAndSettle(); expect(find.text('+42000 sat'), findsOneWidget); @@ -80,13 +85,13 @@ void main() { repository: FakeTransactionsRepository(transactions: const []), ); - await tester.tap(find.text('Load Transactions Demo')); + await tester.tap(find.text('Load Transaction History')); await tester.pumpAndSettle(); expect(find.text('No transactions yet'), findsOneWidget); expect( find.text( - 'The transaction demo loaded successfully, but no placeholder transactions are configured yet.', + 'The active wallet has no transactions yet. Sync the wallet or receive funds to populate history.', ), findsOneWidget, ); @@ -96,11 +101,11 @@ void main() { await _pumpTransactionsFlow( tester, repository: FakeTransactionsRepository( - transactions: placeholderTransactions, + transactions: transactionHistoryItems, ), ); - await tester.tap(find.text('Load Transactions Demo')); + await tester.tap(find.text('Load Transaction History')); await tester.pumpAndSettle(); await tester.tap(find.text('123456...abcd')); @@ -114,4 +119,54 @@ void main() { findsOneWidget, ); }); + + testWidgets( + 'no active wallet shows the no-wallet state and disables load button', + (tester) async { + await _pumpTransactionsFlow( + tester, + repository: FakeTransactionsRepository(transactions: const []), + hasActiveWallet: false, + ); + + expect(find.text('No active wallet'), findsOneWidget); + expect( + find.text( + 'Create or load a wallet before viewing transaction history.', + ), + findsOneWidget, + ); + + // Verify button is disabled + final buttonFinder = find.widgetWithText( + FilledButton, + 'Load Transaction History', + ); + expect(tester.widget(buttonFinder).onPressed, isNull); + }, + ); + + testWidgets( + 'active wallet with no transactions still shows the normal empty-history state after loading', + (tester) async { + await _pumpTransactionsFlow( + tester, + repository: FakeTransactionsRepository(transactions: const []), + hasActiveWallet: true, + ); + + expect(find.text('Transaction history not loaded yet'), findsOneWidget); + + await tester.tap(find.text('Load Transaction History')); + await tester.pumpAndSettle(); + + expect(find.text('No transactions yet'), findsOneWidget); + expect( + find.text( + 'The active wallet has no transactions yet. Sync the wallet or receive funds to populate history.', + ), + findsOneWidget, + ); + }, + ); }