From 46fcde19bdb88e4c927450fca18229b688344ad2 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Thu, 19 Feb 2026 17:04:04 +0100 Subject: [PATCH 01/13] feat: Replace default birthday in block height with Date Hardcode a default `DateTime` for all network, 1st June of 2025 for now. --- lib/constants.dart | 8 ++------ lib/extensions/network.dart | 14 -------------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/lib/constants.dart b/lib/constants.dart index 312e4cc7..8fb9c427 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -7,12 +7,8 @@ const String defaultTestnet = "https://silentpayments.dev/blindbit/testnet"; const String defaultSignet = "https://silentpayments.dev/blindbit/signet"; const String defaultRegtest = "https://silentpayments.dev/blindbit/regtest"; -// Default birthdays used in case we can't get the block height from blindbit -// These values are pretty arbitrary, they can be updated for newer heights later -const int defaultMainnetBirthday = 900000; -const int defaultTestnetBirthday = 2900000; -const int defaultSignetBirthday = 200000; -const int defaultRegtestBirthday = 80000; +// Default birthdays +final DateTime defaultBirthday = DateTime.utc(2025, 6, 1); // default dust limit. this is used in syncing, as well as sending // for syncing, amounts < dust limit will be ignored diff --git a/lib/extensions/network.dart b/lib/extensions/network.dart index 360818ee..c943ef00 100644 --- a/lib/extensions/network.dart +++ b/lib/extensions/network.dart @@ -48,18 +48,4 @@ extension NetworkExtension on ApiNetwork { return Bitcoin.blue; } } - - int get defaultBirthday { - switch (this) { - case ApiNetwork.mainnet: - return defaultMainnetBirthday; - case ApiNetwork.testnet3: - case ApiNetwork.testnet4: - return defaultTestnetBirthday; - case ApiNetwork.signet: - return defaultSignetBirthday; - case ApiNetwork.regtest: - return defaultRegtestBirthday; - } - } } From 0d405449fa840874ccf2ed1c8cdd80dab2615f61 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Thu, 19 Feb 2026 17:09:51 +0100 Subject: [PATCH 02/13] feat: new mempool api methods for blocks * `getBlockForHash`: needed to retrieve the timestamp of a block * `getBlockHashForHeight`: retrieve the block hash from its height * `getBlockFromTimestamp`: used to get the height of the closest block from a timestamp --- .../mempool_block_timestamp_response.dart | 20 ++++++++ lib/data/models/mempool_blocks.dart | 49 +++++++++++++++++++ lib/repositories/mempool_api_repository.dart | 43 ++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 lib/data/models/mempool_block_timestamp_response.dart create mode 100644 lib/data/models/mempool_blocks.dart diff --git a/lib/data/models/mempool_block_timestamp_response.dart b/lib/data/models/mempool_block_timestamp_response.dart new file mode 100644 index 00000000..c0d08693 --- /dev/null +++ b/lib/data/models/mempool_block_timestamp_response.dart @@ -0,0 +1,20 @@ +/// Response from mempool.space `/v1/mining/blocks/timestamp/{timestamp}` API. +class MempoolBlockTimestampResponse { + final int height; + final String hash; + final String timestamp; + + const MempoolBlockTimestampResponse({ + required this.height, + required this.hash, + required this.timestamp, + }); + + factory MempoolBlockTimestampResponse.fromJson(Map json) { + return MempoolBlockTimestampResponse( + height: json['height'] as int, + hash: json['hash'] as String, + timestamp: json['timestamp'] as String, + ); + } +} diff --git a/lib/data/models/mempool_blocks.dart b/lib/data/models/mempool_blocks.dart new file mode 100644 index 00000000..eb070410 --- /dev/null +++ b/lib/data/models/mempool_blocks.dart @@ -0,0 +1,49 @@ +class MempoolGetBlockResponse { + final String id; + final int height; + final int version; + final int timestamp; + final int txCount; + final int size; + final int weight; + final String merkleRoot; + final String previousblockhash; + final int mediantime; + final int nonce; + final int bits; + final double difficulty; + + const MempoolGetBlockResponse({ + required this.id, + required this.height, + required this.version, + required this.timestamp, + required this.txCount, + required this.size, + required this.weight, + required this.merkleRoot, + required this.previousblockhash, + required this.mediantime, + required this.nonce, + required this.bits, + required this.difficulty, + }); + + factory MempoolGetBlockResponse.fromJson(Map json) { + return MempoolGetBlockResponse( + id: json['id'] as String, + height: json['height'] as int, + version: json['version'] as int, + timestamp: json['timestamp'] as int, + txCount: json['tx_count'] as int, + size: json['size'] as int, + weight: json['weight'] as int, + merkleRoot: json['merkle_root'] as String, + previousblockhash: json['previousblockhash'] as String, + mediantime: json['mediantime'] as int, + nonce: json['nonce'] as int, + bits: json['bits'] as int, + difficulty: (json['difficulty'] as num).toDouble(), + ); + } +} diff --git a/lib/repositories/mempool_api_repository.dart b/lib/repositories/mempool_api_repository.dart index 8a21a9d6..ebb21515 100644 --- a/lib/repositories/mempool_api_repository.dart +++ b/lib/repositories/mempool_api_repository.dart @@ -1,5 +1,7 @@ import 'dart:convert'; +import 'package:danawallet/data/models/mempool_block_timestamp_response.dart'; +import 'package:danawallet/data/models/mempool_blocks.dart'; import 'package:danawallet/data/models/mempool_prices_response.dart'; import 'package:danawallet/generated/rust/api/structs/network.dart'; import 'package:danawallet/services/fee_api_converter.dart'; @@ -61,4 +63,45 @@ class MempoolApiRepository { throw Exception("Unexpected status code: ${response.statusCode}"); } } + + Future getBlockForHash(String blockHash) async { + final response = await http.get(Uri.parse('$baseUrl/block/$blockHash')); + if (response.statusCode == 200) { + return MempoolGetBlockResponse.fromJson(jsonDecode(response.body)); + } else { + throw Exception('Unexpected status code: ${response.statusCode}'); + } + } + + Future getBlockHashForHeight(int height) async { + // First get the block hash from height + final hashResponse = await http.get(Uri.parse('$baseUrl/block-height/$height')); + if (hashResponse.statusCode != 200) { + throw Exception('Unexpected status code: ${hashResponse.statusCode}'); + } + + // Then get the full block data using the hash + return hashResponse.body.trim(); + } + + /// Converts a Unix timestamp to block info using mempool API. + /// Returns the block closest to the given timestamp. + Future getBlockFromTimestamp(int timestamp) async { + final response = await http.get( + Uri.parse('$baseUrl/v1/mining/blocks/timestamp/$timestamp'), + ); + + if (response.statusCode != 200) { + throw Exception( + 'Failed to get block from timestamp: ${response.statusCode}', + ); + } + + try { + final json = jsonDecode(response.body) as Map; + return MempoolBlockTimestampResponse.fromJson(json); + } catch (e) { + throw Exception('Failed to parse block timestamp response: $e'); + } + } } From 76c8b769ab7c55acd0a0fdb3ccd23043ed461bd6 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Thu, 19 Feb 2026 17:10:11 +0100 Subject: [PATCH 03/13] feat: Add `getBlockHeightFromDate` in `chain_state` --- lib/states/chain_state.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/states/chain_state.dart b/lib/states/chain_state.dart index a2b7a315..54b08b70 100644 --- a/lib/states/chain_state.dart +++ b/lib/states/chain_state.dart @@ -1,4 +1,5 @@ import 'package:danawallet/data/models/recommended_fee_model.dart'; +import 'package:danawallet/extensions/date_time.dart'; import 'package:danawallet/extensions/network.dart'; import 'package:danawallet/generated/rust/api/chain.dart'; import 'package:danawallet/generated/rust/api/structs/network.dart'; @@ -148,4 +149,10 @@ class ChainState extends ChangeNotifier { return response; } } + + Future getBlockHeightFromDate(DateTime date) async { + final mempoolApiRepository = MempoolApiRepository(network: network); + final block = await mempoolApiRepository.getBlockFromTimestamp(date.toSeconds()); + return block.height; + } } From fd7afb6870c6fd2bc2f49e1af60d1f01caaa9d2c Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 20 Feb 2026 08:43:20 +0100 Subject: [PATCH 04/13] feat: Add extensions for `DateTime` and `int` Add `toSeconds()` method to `DateTime` and a `toDate()` method to `int` --- lib/extensions/date_time.dart | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 lib/extensions/date_time.dart diff --git a/lib/extensions/date_time.dart b/lib/extensions/date_time.dart new file mode 100644 index 00000000..bb048a55 --- /dev/null +++ b/lib/extensions/date_time.dart @@ -0,0 +1,23 @@ +extension DateTimeExtension on DateTime { + int toSeconds() => millisecondsSinceEpoch ~/ 1000; +} + +extension TimestampExtension on int { + DateTime toDate() { + const max32BitUnsigned = 4294967295; // 2^32 - 1 + + if (this < 0) { + throw ArgumentError( + 'Timestamp cannot be negative: $this', + ); + } + + if (this > max32BitUnsigned) { + throw ArgumentError( + 'Timestamp exceeds 32-bit unsigned int range: $this. Maximum value is $max32BitUnsigned', + ); + } + + return DateTime.fromMillisecondsSinceEpoch(this * 1000, isUtc: true); + } +} From 0c08ce9ce956de91f83041d53903c0462273ce25 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 20 Feb 2026 10:20:11 +0100 Subject: [PATCH 05/13] feat: store wallet birthday as timestamp instead of block height Birthday becomes a DateTime (stored as unix timestamp) instead of a block height integer. Includes migration logic for existing wallets that still have a block height stored. --- lib/repositories/wallet_repository.dart | 17 ++++--- lib/screens/onboarding/overview.dart | 3 +- .../onboarding/recovery/seed_phrase.dart | 4 +- lib/states/wallet_state.dart | 44 ++++++++++++++----- 4 files changed, 49 insertions(+), 19 deletions(-) diff --git a/lib/repositories/wallet_repository.dart b/lib/repositories/wallet_repository.dart index 8f35fcc9..96c6e4a1 100644 --- a/lib/repositories/wallet_repository.dart +++ b/lib/repositories/wallet_repository.dart @@ -1,4 +1,5 @@ import 'package:danawallet/data/models/bip353_address.dart'; +import 'package:danawallet/extensions/date_time.dart'; import 'package:danawallet/generated/rust/api/backup.dart'; import 'package:danawallet/generated/rust/api/history.dart'; import 'package:danawallet/generated/rust/api/outputs.dart'; @@ -48,7 +49,7 @@ class WalletRepository { } Future setupWallet( - WalletSetupResult walletSetup, ApiNetwork network, int birthday) async { + WalletSetupResult walletSetup, ApiNetwork network, DateTime birthday) async { if ((await secureStorage.readAll()).isNotEmpty) { throw Exception('Previous wallet not properly deleted'); } @@ -61,7 +62,7 @@ class WalletRepository { // insert new values await secureStorage.write(key: _keyScanSk, value: scanKey.encode()); await secureStorage.write(key: _keySpendKey, value: spendKey.encode()); - await nonSecureStorage.setInt(_keyBirthday, birthday); + await nonSecureStorage.setInt(_keyBirthday, birthday.toSeconds()); await nonSecureStorage.setString(_keyNetwork, network.name); if (seedPhrase != null) { @@ -133,9 +134,13 @@ class WalletRepository { return TxHistory.decode(encodedHistory: encodedHistory!); } - Future readBirthday() async { - final birthday = await nonSecureStorage.getInt(_keyBirthday); - return birthday!; + Future saveBirthday(DateTime birthday) async { + await nonSecureStorage.setInt(_keyBirthday, birthday.toSeconds()); + } + + Future readBirthday() async { + final timestamp = await nonSecureStorage.getInt(_keyBirthday); + return timestamp!.toDate(); } Future saveLastScan(int lastScan) async { @@ -185,7 +190,7 @@ class WalletRepository { return WalletBackup( wallet: wallet!, - birthday: birthday, + birthday: birthday.toSeconds(), lastScan: lastScan, txHistory: history, ownedOutputs: outputs, diff --git a/lib/screens/onboarding/overview.dart b/lib/screens/onboarding/overview.dart index 4e06dfaa..1f8d8d3c 100644 --- a/lib/screens/onboarding/overview.dart +++ b/lib/screens/onboarding/overview.dart @@ -122,8 +122,7 @@ class _OverviewScreenState extends State { // we *must* be connected to get the wallet birthday if (connected) { chainState.startSyncService(walletState, scanProgress, false); - final chainTip = chainState.tip; - await walletState.createNewWallet(network, chainTip); + await walletState.createNewWallet(network); // initialize contacts state with the user's payment code contactsState.initialize(walletState.receivePaymentCode, null); diff --git a/lib/screens/onboarding/recovery/seed_phrase.dart b/lib/screens/onboarding/recovery/seed_phrase.dart index 0dfccf15..67aeb176 100644 --- a/lib/screens/onboarding/recovery/seed_phrase.dart +++ b/lib/screens/onboarding/recovery/seed_phrase.dart @@ -1,6 +1,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:bitcoin_ui/bitcoin_ui.dart'; import 'package:danawallet/data/enums/warning_type.dart'; +import 'package:danawallet/extensions/date_time.dart'; import 'package:danawallet/extensions/network.dart'; import 'package:danawallet/generated/rust/api/structs/network.dart'; import 'package:danawallet/global_functions.dart'; @@ -51,7 +52,8 @@ class SeedPhraseScreenState extends State { final blindbitUrl = widget.network.defaultBlindbitUrl; - await walletState.restoreWallet(widget.network, mnemonic); + final defaultBirthday = widget.network.defaultBirthday; + await walletState.restoreWallet(widget.network, mnemonic, defaultBirthday.toDate()); chainState.initialize(widget.network); // we can safely ignore the result of connecting, since we use the default birthday diff --git a/lib/states/wallet_state.dart b/lib/states/wallet_state.dart index 436e1505..8ba7040f 100644 --- a/lib/states/wallet_state.dart +++ b/lib/states/wallet_state.dart @@ -1,6 +1,8 @@ import 'dart:async'; +import 'package:danawallet/constants.dart'; import 'package:danawallet/data/models/bip353_address.dart'; import 'package:danawallet/data/models/recipient_form_filled.dart'; +import 'package:danawallet/extensions/date_time.dart'; import 'package:danawallet/extensions/network.dart'; import 'package:danawallet/generated/rust/api/history.dart'; import 'package:danawallet/generated/rust/api/outputs.dart'; @@ -26,7 +28,7 @@ class WalletState extends ChangeNotifier { late ApiNetwork network; late String receivePaymentCode; late String changePaymentCode; - late int birthday; + DateTime? birthday; // variables that change late ApiAmount amount; @@ -79,7 +81,7 @@ class WalletState extends ChangeNotifier { // since the wallet data is present, the following items must also be present network = await walletRepository.readNetwork(); - birthday = await walletRepository.readBirthday(); + await setBirthday(); danaAddress = await walletRepository.readDanaAddress(); // we calculate these based on our wallet data (scan key, spend key, network) @@ -102,10 +104,7 @@ class WalletState extends ChangeNotifier { await walletRepository.reset(); } - Future restoreWallet(ApiNetwork network, String mnemonic) async { - // set birthday to default for current network - final birthday = network.defaultBirthday; - + Future restoreWallet(ApiNetwork network, String mnemonic, DateTime birthday) async { final args = WalletSetupArgs( setupType: WalletSetupType.mnemonic(mnemonic), network: network); final setupResult = SpWallet.setupWallet(setupArgs: args); @@ -120,19 +119,19 @@ class WalletState extends ChangeNotifier { await _updateWalletState(); } - Future createNewWallet(ApiNetwork network, int currentTip) async { - final birthday = currentTip; + Future createNewWallet(ApiNetwork network) async { + final now = DateTime.now().toUtc(); final args = WalletSetupArgs( setupType: const WalletSetupType.newWallet(), network: network); final setupResult = SpWallet.setupWallet(setupArgs: args); final wallet = - await walletRepository.setupWallet(setupResult, network, birthday); + await walletRepository.setupWallet(setupResult, network, now); // fill current state variables receivePaymentCode = wallet.getReceivingAddress(); changePaymentCode = wallet.getChangeAddress(); - this.birthday = birthday; + birthday = now; this.network = network; await _updateWalletState(); } @@ -150,6 +149,31 @@ class WalletState extends ChangeNotifier { return await walletRepository.readSeedPhrase(); } + // Older wallets may have birthday as a block height, we apply the same check than core + Future setBirthday() async { + final storedBirthday = await walletRepository.readBirthday(); + if (storedBirthday.isAfter(defaultBirthday)) { + // This is a timestamp, we can use it directly + birthday = storedBirthday; + } else { + // This is a block height, we need to convert it to a timestamp + // That unfortunately requires a network call that may fail + try { + final mempoolApi = MempoolApiRepository(network: network); + final block = await mempoolApi.getBlockForHash(await mempoolApi.getBlockHashForHeight(storedBirthday.toSeconds())); + final newBirthday = block.timestamp.toDate(); + Logger().i("Resolved block height $storedBirthday to date $newBirthday"); + // Update the birthday value to an epoch time + await walletRepository.saveBirthday(newBirthday); + birthday = newBirthday; + } catch (e) { + Logger().e("Error resolving block height $storedBirthday to timestamp: $e"); + birthday = null; + } + } + + } + Future resetToScanHeight(int height) async { lastScan = height; From 28da5cdbd0c168e2ae381c31b1ebf6f709f26e65 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 20 Feb 2026 10:20:54 +0100 Subject: [PATCH 06/13] feat: make lastScan nullable with lazy resolution from birthday --- lib/repositories/wallet_repository.dart | 16 ++++++----- .../network/network_settings_screen.dart | 5 +++- lib/screens/spend/amount_selection.dart | 5 +++- lib/services/synchronization_service.dart | 27 +++++++++++++++++-- lib/states/scan_progress_notifier.dart | 8 +++--- lib/states/wallet_state.dart | 2 +- 6 files changed, 49 insertions(+), 14 deletions(-) diff --git a/lib/repositories/wallet_repository.dart b/lib/repositories/wallet_repository.dart index 96c6e4a1..05c4d209 100644 --- a/lib/repositories/wallet_repository.dart +++ b/lib/repositories/wallet_repository.dart @@ -70,7 +70,7 @@ class WalletRepository { } // set default values for new wallet - await saveLastScan(birthday); + await saveLastScan(null); // We shouldn't have left over value from a previous wallet but anyway await saveHistory(TxHistory.empty()); await saveOwnedOutputs(OwnedOutputs.empty()); @@ -143,13 +143,17 @@ class WalletRepository { return timestamp!.toDate(); } - Future saveLastScan(int lastScan) async { - await nonSecureStorage.setInt(_keyLastScan, lastScan); + Future saveLastScan(int? lastScan) async { + if (lastScan != null) { + await nonSecureStorage.setInt(_keyLastScan, lastScan); + } else { + await nonSecureStorage.remove(_keyLastScan); + } } - Future readLastScan() async { + Future readLastScan() async { final lastScan = await nonSecureStorage.getInt(_keyLastScan); - return lastScan!; + return lastScan; } Future saveOwnedOutputs(OwnedOutputs ownedOutputs) async { @@ -191,7 +195,7 @@ class WalletRepository { return WalletBackup( wallet: wallet!, birthday: birthday.toSeconds(), - lastScan: lastScan, + lastScan: lastScan!, txHistory: history, ownedOutputs: outputs, seedPhrase: seedPhrase, diff --git a/lib/screens/settings/network/network_settings_screen.dart b/lib/screens/settings/network/network_settings_screen.dart index 06964b65..c6880f97 100644 --- a/lib/screens/settings/network/network_settings_screen.dart +++ b/lib/screens/settings/network/network_settings_screen.dart @@ -41,6 +41,7 @@ class NetworkSettingsScreen extends StatelessWidget { Future _onSetLastScan(BuildContext context) async { final walletState = Provider.of(context, listen: false); final homeState = Provider.of(context, listen: false); + final chainState = Provider.of(context, listen: false); TextEditingController controller = TextEditingController(); final scanHeight = await showInputAlertDialog( @@ -53,7 +54,9 @@ class NetworkSettingsScreen extends StatelessWidget { homeState.showMainScreen(); } else if (scanHeight is bool && scanHeight) { final birthday = walletState.birthday; - await walletState.resetToScanHeight(birthday); + // TODO probably better and simpler to set lastScan to null and let the synchronization service set it to the birthday height + final height = await chainState.getBlockHeightFromDate(birthday!); + await walletState.resetToScanHeight(height); homeState.showMainScreen(); } } diff --git a/lib/screens/spend/amount_selection.dart b/lib/screens/spend/amount_selection.dart index 8537c82c..4f437d90 100644 --- a/lib/screens/spend/amount_selection.dart +++ b/lib/screens/spend/amount_selection.dart @@ -74,7 +74,10 @@ class AmountSelectionScreenState extends State { final chainState = Provider.of(context, listen: false); final availableBalance = walletState.amount; - final blocksToScan = chainState.tip - walletState.lastScan; + int blocksToScan = 0; + if (walletState.lastScan != null) { + blocksToScan = chainState.tip - walletState.lastScan!; + } String recipientName = form.recipient!.displayName; TextStyle recipientTextStyle = BitcoinTextStyle.body4(Bitcoin.neutral7); diff --git a/lib/services/synchronization_service.dart b/lib/services/synchronization_service.dart index d6517481..3f50127d 100644 --- a/lib/services/synchronization_service.dart +++ b/lib/services/synchronization_service.dart @@ -82,14 +82,37 @@ class SynchronizationService { } Future _performSynchronizationTask() async { - if (walletState.lastScan < chainState.tip) { + if (walletState.lastScan == null) { + // This means we just setup the wallet, or we didn't have network so far. Try to set last scan to block height of birthday + Logger().d("Setting last scan to block height of birthday"); + // In case we're migrating an older wallet and we didn't have network, birthday is null and needs to be resolved first + if (walletState.birthday == null) { + Logger().d("Birthday is null, trying to resolve it"); + await walletState.setBirthday(); + } + if (walletState.birthday != null) { + try { + final height = await chainState.getBlockHeightFromDate(walletState.birthday!); + walletState.lastScan = height; + } catch (e) { + Logger().e("Error getting last scan: $e"); + // We wait and try again later + return; + } + } else { + Logger().e("Birthday is still null, cannot set last scan"); + return; + } + } + + if (walletState.lastScan! < chainState.tip) { if (!scanProgress.scanning) { Logger().i("Starting sync"); await scanProgress.scan(walletState); } } - if (chainState.tip < walletState.lastScan) { + if (chainState.tip < walletState.lastScan!) { // not sure what we should do here, that's really bad Logger().e('Current height is less than wallet last scan'); } diff --git a/lib/states/scan_progress_notifier.dart b/lib/states/scan_progress_notifier.dart index 966d2a55..51cec822 100644 --- a/lib/states/scan_progress_notifier.dart +++ b/lib/states/scan_progress_notifier.dart @@ -69,17 +69,19 @@ class ScanProgressNotifier extends ChangeNotifier { walletState.network.defaultBlindbitUrl; final dustLimit = await settings.getDustLimit() ?? defaultDustLimit; - final lastScan = walletState.lastScan; + if (walletState.lastScan == null) { + throw Exception("Last scan is null"); + } final ownedOutPoints = walletState.ownedOutputs.getUnconfirmedSpentOutpoints(); - activate(walletState.lastScan); + activate(walletState.lastScan!); await wallet.scanToTip( blindbitUrl: blindbitUrl, dustLimit: BigInt.from(dustLimit), ownedOutpoints: ownedOutPoints, - lastScan: lastScan); + lastScan: walletState.lastScan!); } catch (e) { deactivate(); rethrow; diff --git a/lib/states/wallet_state.dart b/lib/states/wallet_state.dart index 8ba7040f..6cb31312 100644 --- a/lib/states/wallet_state.dart +++ b/lib/states/wallet_state.dart @@ -33,7 +33,7 @@ class WalletState extends ChangeNotifier { // variables that change late ApiAmount amount; late ApiAmount unconfirmedChange; - late int lastScan; + late int? lastScan; late TxHistory txHistory; late OwnedOutputs ownedOutputs; From d3ed65161f69ee2bc908ec3eda3909f541f715fe Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 20 Feb 2026 15:48:36 +0100 Subject: [PATCH 07/13] feat: Ask user for birthday on wallet restoration * add a ticker box on Seed Phrase Recovery screen * if ticked open birthday picker * if user does'nt provice a birthday set network default --- .../recovery/birthday_picker_screen.dart | 73 +++++++++++++++++++ .../onboarding/recovery/seed_phrase.dart | 63 ++++++++++++++-- 2 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 lib/screens/onboarding/recovery/birthday_picker_screen.dart diff --git a/lib/screens/onboarding/recovery/birthday_picker_screen.dart b/lib/screens/onboarding/recovery/birthday_picker_screen.dart new file mode 100644 index 00000000..45cab23f --- /dev/null +++ b/lib/screens/onboarding/recovery/birthday_picker_screen.dart @@ -0,0 +1,73 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:bitcoin_ui/bitcoin_ui.dart'; +import 'package:danawallet/widgets/buttons/footer/footer_button.dart'; +import 'package:danawallet/widgets/skeletons/screen_skeleton.dart'; +import 'package:flutter/material.dart'; + +/// Screen for selecting the wallet creation date (birthday). +/// Returns the selected [DateTime] when user confirms, or null if user goes back. +class BirthdayPickerScreen extends StatefulWidget { + const BirthdayPickerScreen({super.key}); + + @override + State createState() => _BirthdayPickerScreenState(); +} + +class _BirthdayPickerScreenState extends State { + late DateTime _selectedDate; + + @override + void initState() { + super.initState(); + // Use UTC to avoid timezone issues with calendar dates + final now = DateTime.now().toUtc(); + _selectedDate = DateTime.utc(now.year, now.month, now.day); + } + + void _onConfirm() { + Navigator.of(context).pop(_selectedDate); + } + + @override + Widget build(BuildContext context) { + final subtitle = AutoSizeText( + "Choose the date when your wallet was created. This will make restoration faster.", + style: BitcoinTextStyle.body3(Bitcoin.neutral7).copyWith( + fontFamily: 'Inter', + ), + textAlign: TextAlign.center, + maxLines: 3, + ); + + final body = Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: subtitle, + ), + Expanded( + child: SingleChildScrollView( + child: CalendarDatePicker( + initialDate: _selectedDate, + firstDate: DateTime.utc(2009, 1, 3), // Bitcoin genesis + lastDate: DateTime.now().toUtc(), + currentDate: DateTime.now().toUtc(), + onDateChanged: (date) { + setState(() { + _selectedDate = date; + }); + }, + ), + ), + ), + ], + ); + + return ScreenSkeleton( + title: "Select wallet birthday", + showBackButton: true, + body: body, + footer: FooterButton(title: "Continue", onPressed: _onConfirm), + ); + } +} diff --git a/lib/screens/onboarding/recovery/seed_phrase.dart b/lib/screens/onboarding/recovery/seed_phrase.dart index 67aeb176..50afabd4 100644 --- a/lib/screens/onboarding/recovery/seed_phrase.dart +++ b/lib/screens/onboarding/recovery/seed_phrase.dart @@ -1,10 +1,11 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:bitcoin_ui/bitcoin_ui.dart'; +import 'package:danawallet/constants.dart'; import 'package:danawallet/data/enums/warning_type.dart'; -import 'package:danawallet/extensions/date_time.dart'; import 'package:danawallet/extensions/network.dart'; import 'package:danawallet/generated/rust/api/structs/network.dart'; import 'package:danawallet/global_functions.dart'; +import 'package:danawallet/screens/onboarding/recovery/birthday_picker_screen.dart'; import 'package:danawallet/screens/onboarding/register_dana_address.dart'; import 'package:danawallet/states/chain_state.dart'; import 'package:danawallet/states/contacts_state.dart'; @@ -40,6 +41,7 @@ class SeedPhraseScreenState extends State { late List controllers; late List focusNodes; late MnemonicInputPillBox pills; + bool _knowsBirthday = false; Future onRestore(BuildContext context) async { try { @@ -50,14 +52,44 @@ class SeedPhraseScreenState extends State { final scanProgress = Provider.of(context, listen: false); - final blindbitUrl = widget.network.defaultBlindbitUrl; + // Get birthday: navigate to picker if user knows it, else use default + DateTime birthday = defaultBirthday; + if (_knowsBirthday) { + final pickedDate = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const BirthdayPickerScreen(), + ), + ); + if (!context.mounted) { + return; // Context lost, abort restore + } + if (pickedDate == null) { + return; // User pressed back, stay on seed phrase screen + } + // pickedDate is already in UTC from BirthdayPickerScreen + // Use 1am UTC to avoid edge issues at midnight + birthday = DateTime.utc( + pickedDate.year, pickedDate.month, pickedDate.day, 1); + } - final defaultBirthday = widget.network.defaultBirthday; - await walletState.restoreWallet(widget.network, mnemonic, defaultBirthday.toDate()); + await walletState.restoreWallet(widget.network, mnemonic, birthday); chainState.initialize(widget.network); - // we can safely ignore the result of connecting, since we use the default birthday - await chainState.connect(blindbitUrl); + + // Try to connect, but continue even if it fails (offline mode) + final connected = await chainState.connect(widget.network.defaultBlindbitUrl); + if (!connected) { + // Connection failed, but continue anyway - sync will happen when network is available + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Unable to connect to network. Wallet will sync when connection is restored.'), + duration: Duration(seconds: 3), + ), + ); + } + } chainState.startSyncService(walletState, scanProgress, true); @@ -164,6 +196,25 @@ class SeedPhraseScreenState extends State { ], ), Expanded(child: pills), + Padding( + padding: EdgeInsets.symmetric(vertical: Adaptive.h(1.5)), + child: CheckboxListTile( + value: _knowsBirthday, + onChanged: (value) { + setState(() { + _knowsBirthday = value ?? false; + }); + }, + title: Text( + "I know when my wallet was created (birthday)", + style: BitcoinTextStyle.body3(Bitcoin.neutral7).copyWith( + fontFamily: 'Inter', + ), + ), + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + ), + ), footer, ], )), From 5c6062c6e912c2163cf14a6c773efdf0504320ea Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 20 Feb 2026 15:49:16 +0100 Subject: [PATCH 08/13] dependencies: Add `intl` package for Date formatting --- pubspec.lock | 8 ++++++++ pubspec.yaml | 1 + 2 files changed, 9 insertions(+) diff --git a/pubspec.lock b/pubspec.lock index 5c5828b4..ceeb6f4c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -474,6 +474,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" io: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6befc819..7f056d04 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,6 +47,7 @@ dependencies: path: ^1.9.1 collection: ^1.19.1 package_info_plus: ^9.0.0 + intl: ^0.19.0 dependency_overrides: # Pin url_launcher_android to avoid AGP 8.9.1 requirement (androidx.browser:1.9.0, androidx.core:1.17.0) From 8a3b4378f4a4d918edb25116691b54f53ab135b5 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 20 Feb 2026 16:04:37 +0100 Subject: [PATCH 09/13] feat: Display wallet birthday as human readable date on seed phrase screen --- .../recovery/view_mnemonic_screen.dart | 27 +++++++++++++++++-- .../wallet/wallet_settings_screen.dart | 2 +- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/lib/screens/recovery/view_mnemonic_screen.dart b/lib/screens/recovery/view_mnemonic_screen.dart index 1f1cb8dc..7c477fd8 100644 --- a/lib/screens/recovery/view_mnemonic_screen.dart +++ b/lib/screens/recovery/view_mnemonic_screen.dart @@ -4,26 +4,29 @@ import 'package:danawallet/global_functions.dart'; import 'package:danawallet/widgets/buttons/footer/footer_button.dart'; import 'package:danawallet/widgets/pills/mnemonic_pill_box.dart'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import 'package:sizer/sizer.dart'; class ViewMnemonicScreen extends StatelessWidget { final String mnemonic; + final DateTime? birthday; const ViewMnemonicScreen({ super.key, required this.mnemonic, + this.birthday, }); @override Widget build(BuildContext context) { final title = AutoSizeText( - "This is your recovery phrase", + "This is your wallet backup phrase", style: BitcoinTextStyle.title2(Colors.black) .copyWith(height: 1.8, fontFamily: 'Inter'), maxLines: 1, ); final text = AutoSizeText( - "Make sure to write it down as shown here, including both numbers and words.", + "You can recover this wallet by using this backup phrase.", style: BitcoinTextStyle.body3(Bitcoin.neutral7).copyWith( fontFamily: 'Inter', ), @@ -31,6 +34,20 @@ class ViewMnemonicScreen extends StatelessWidget { maxLines: 3, ); + Widget? birthdayText; + if (birthday != null) { + final locale = Localizations.localeOf(context); + final birthdayDateString = DateFormat('d MMM yyyy', locale.toString()).format(birthday!); + birthdayText = AutoSizeText( + "Wallet birthday: $birthdayDateString", + style: BitcoinTextStyle.body3(Bitcoin.neutral1Dark).copyWith( + fontFamily: 'Inter', + ), + textAlign: TextAlign.center, + maxLines: 1, + ); + } + final pills = MnemonicPillBox(mnemonic: mnemonic); final footer = FooterButton( title: "I wrote it down", onPressed: () => goBack(context)); @@ -57,6 +74,12 @@ class ViewMnemonicScreen extends StatelessWidget { vertical: Adaptive.h(3), horizontal: Adaptive.w(2)), child: text), + if (birthdayText != null) + Padding( + padding: EdgeInsets.symmetric( + vertical: Adaptive.h(1), + horizontal: Adaptive.w(2)), + child: birthdayText), ], ), Expanded(child: pills), diff --git a/lib/screens/settings/wallet/wallet_settings_screen.dart b/lib/screens/settings/wallet/wallet_settings_screen.dart index dcf77b17..bc490010 100644 --- a/lib/screens/settings/wallet/wallet_settings_screen.dart +++ b/lib/screens/settings/wallet/wallet_settings_screen.dart @@ -107,7 +107,7 @@ class WalletSettingsScreen extends StatelessWidget { if (context.mounted) { if (mnemonic != null) { - goToScreen(context, ViewMnemonicScreen(mnemonic: mnemonic)); + goToScreen(context, ViewMnemonicScreen(mnemonic: mnemonic, birthday: wallet.birthday)); } else { showAlertDialog("Seed phrase unknown", "Seed phrase unknown! Did you import from keys?"); From 2c55de4446a836dd13bc543b8f1d78028fbbc962 Mon Sep 17 00:00:00 2001 From: cygnet Date: Sat, 28 Feb 2026 04:18:28 +0100 Subject: [PATCH 10/13] feat: add a minimum allowed birthday Minimum allowed birthday can be used for two purposes: - In date picker screen, to set a sensible minimum - During wallet recovery, we parse Datetimes before this timestamp as a legacy birthday (block height) --- lib/constants.dart | 6 +++++- lib/screens/onboarding/recovery/birthday_picker_screen.dart | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/constants.dart b/lib/constants.dart index 8fb9c427..6522838e 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -7,9 +7,13 @@ const String defaultTestnet = "https://silentpayments.dev/blindbit/testnet"; const String defaultSignet = "https://silentpayments.dev/blindbit/signet"; const String defaultRegtest = "https://silentpayments.dev/blindbit/regtest"; -// Default birthdays +// Default birthday, this value is based on the first Dana release final DateTime defaultBirthday = DateTime.utc(2025, 6, 1); +// minimum birthday allowed during recovery. This value is set to the moment BIP352 got merged, +// see: https://github.com/bitcoin/bips/pull/1458 +final DateTime minimumAllowedBirthday = DateTime.utc(2024, 5, 8); + // default dust limit. this is used in syncing, as well as sending // for syncing, amounts < dust limit will be ignored // for sending, the user needs to send a minimum of >= dust diff --git a/lib/screens/onboarding/recovery/birthday_picker_screen.dart b/lib/screens/onboarding/recovery/birthday_picker_screen.dart index 45cab23f..a7cd47a1 100644 --- a/lib/screens/onboarding/recovery/birthday_picker_screen.dart +++ b/lib/screens/onboarding/recovery/birthday_picker_screen.dart @@ -1,5 +1,6 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:bitcoin_ui/bitcoin_ui.dart'; +import 'package:danawallet/constants.dart'; import 'package:danawallet/widgets/buttons/footer/footer_button.dart'; import 'package:danawallet/widgets/skeletons/screen_skeleton.dart'; import 'package:flutter/material.dart'; @@ -49,7 +50,7 @@ class _BirthdayPickerScreenState extends State { child: SingleChildScrollView( child: CalendarDatePicker( initialDate: _selectedDate, - firstDate: DateTime.utc(2009, 1, 3), // Bitcoin genesis + firstDate: minimumAllowedBirthday, lastDate: DateTime.now().toUtc(), currentDate: DateTime.now().toUtc(), onDateChanged: (date) { From b6a19b5bbfc0272295a7325fa94633b74faab00f Mon Sep 17 00:00:00 2001 From: cygnet Date: Sat, 28 Feb 2026 04:22:01 +0100 Subject: [PATCH 11/13] refactor(walletState): make birthday non-nullable Make birthday variable in WalletState non-nullable. In case we can't get the birthday variable, use a default birthday. This only happens when wallets upgrade from legacy birthday format (using block height) and there's a connection error. --- lib/states/wallet_state.dart | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/lib/states/wallet_state.dart b/lib/states/wallet_state.dart index 6cb31312..e6571ddf 100644 --- a/lib/states/wallet_state.dart +++ b/lib/states/wallet_state.dart @@ -28,7 +28,7 @@ class WalletState extends ChangeNotifier { late ApiNetwork network; late String receivePaymentCode; late String changePaymentCode; - DateTime? birthday; + late DateTime birthday; // variables that change late ApiAmount amount; @@ -81,7 +81,7 @@ class WalletState extends ChangeNotifier { // since the wallet data is present, the following items must also be present network = await walletRepository.readNetwork(); - await setBirthday(); + birthday = await _getBirthday(); danaAddress = await walletRepository.readDanaAddress(); // we calculate these based on our wallet data (scan key, spend key, network) @@ -149,26 +149,32 @@ class WalletState extends ChangeNotifier { return await walletRepository.readSeedPhrase(); } - // Older wallets may have birthday as a block height, we apply the same check than core - Future setBirthday() async { + Future _getBirthday() async { final storedBirthday = await walletRepository.readBirthday(); - if (storedBirthday.isAfter(defaultBirthday)) { + if (storedBirthday.isAfter(minimumAllowedBirthday)) { // This is a timestamp, we can use it directly - birthday = storedBirthday; + return storedBirthday; } else { - // This is a block height, we need to convert it to a timestamp - // That unfortunately requires a network call that may fail + // if the birthday is older than the minimum allowed birthday, + // this value must be from an earlier version where we stored the birthday as a block height. + // to fix this, we convert the stored birthday back to an integer, + // and fetch the date from that block + final blockHeight = storedBirthday.toSeconds(); try { final mempoolApi = MempoolApiRepository(network: network); - final block = await mempoolApi.getBlockForHash(await mempoolApi.getBlockHashForHeight(storedBirthday.toSeconds())); + final block = await mempoolApi.getBlockForHash( + await mempoolApi.getBlockHashForHeight(blockHeight)); final newBirthday = block.timestamp.toDate(); - Logger().i("Resolved block height $storedBirthday to date $newBirthday"); - // Update the birthday value to an epoch time + Logger().i("Resolved block height $blockHeight to date $newBirthday"); + // store converted birthday in persistent storage before returning await walletRepository.saveBirthday(newBirthday); - birthday = newBirthday; + return newBirthday; } catch (e) { - Logger().e("Error resolving block height $storedBirthday to timestamp: $e"); - birthday = null; + Logger() + .w("Error resolving block height $storedBirthday to timestamp: $e"); + // if this process fails, just use the default birthday. + // this value isn't crucial to enough to fail over + return defaultBirthday; } } From df68083bdcbbb517438afa61d0b9f8baf298794d Mon Sep 17 00:00:00 2001 From: cygnet Date: Sat, 28 Feb 2026 04:27:31 +0100 Subject: [PATCH 12/13] refactor: set lastScan to current block height on wallet creation When creating a new wallet, set lastScan to current block height. Before, this would be lazily initialized by the chain synchronization service, but I think setting it immediately on wallet creation is preferable. --- lib/repositories/wallet_repository.dart | 6 ++--- lib/screens/onboarding/overview.dart | 8 ++++--- lib/services/synchronization_service.dart | 29 ++++++++++------------- lib/states/wallet_state.dart | 19 +++++++++------ 4 files changed, 32 insertions(+), 30 deletions(-) diff --git a/lib/repositories/wallet_repository.dart b/lib/repositories/wallet_repository.dart index 05c4d209..4d04a7d5 100644 --- a/lib/repositories/wallet_repository.dart +++ b/lib/repositories/wallet_repository.dart @@ -48,8 +48,8 @@ class WalletRepository { }); } - Future setupWallet( - WalletSetupResult walletSetup, ApiNetwork network, DateTime birthday) async { + Future setupWallet(WalletSetupResult walletSetup, + ApiNetwork network, DateTime birthday, int? lastScan) async { if ((await secureStorage.readAll()).isNotEmpty) { throw Exception('Previous wallet not properly deleted'); } @@ -70,7 +70,7 @@ class WalletRepository { } // set default values for new wallet - await saveLastScan(null); // We shouldn't have left over value from a previous wallet but anyway + await saveLastScan(lastScan); await saveHistory(TxHistory.empty()); await saveOwnedOutputs(OwnedOutputs.empty()); diff --git a/lib/screens/onboarding/overview.dart b/lib/screens/onboarding/overview.dart index 1f8d8d3c..d0c13f45 100644 --- a/lib/screens/onboarding/overview.dart +++ b/lib/screens/onboarding/overview.dart @@ -119,11 +119,13 @@ class _OverviewScreenState extends State { chainState.initialize(network); final connected = await chainState.connect(blindbitUrl); - // we *must* be connected to get the wallet birthday + // we *must* be connected to get the current block height if (connected) { - chainState.startSyncService(walletState, scanProgress, false); - await walletState.createNewWallet(network); + final currentTip = chainState.tip; + await walletState.createNewWallet(network, currentTip); + // start chain sync service only *after* we created the wallet + chainState.startSyncService(walletState, scanProgress, false); // initialize contacts state with the user's payment code contactsState.initialize(walletState.receivePaymentCode, null); if (network == ApiNetwork.regtest && context.mounted) { diff --git a/lib/services/synchronization_service.dart b/lib/services/synchronization_service.dart index 3f50127d..58c6bd16 100644 --- a/lib/services/synchronization_service.dart +++ b/lib/services/synchronization_service.dart @@ -83,24 +83,12 @@ class SynchronizationService { Future _performSynchronizationTask() async { if (walletState.lastScan == null) { - // This means we just setup the wallet, or we didn't have network so far. Try to set last scan to block height of birthday + // if we just recovered a wallet, we haven't set the lastScan variable yet. Logger().d("Setting last scan to block height of birthday"); - // In case we're migrating an older wallet and we didn't have network, birthday is null and needs to be resolved first - if (walletState.birthday == null) { - Logger().d("Birthday is null, trying to resolve it"); - await walletState.setBirthday(); - } - if (walletState.birthday != null) { - try { - final height = await chainState.getBlockHeightFromDate(walletState.birthday!); - walletState.lastScan = height; - } catch (e) { - Logger().e("Error getting last scan: $e"); - // We wait and try again later - return; - } - } else { - Logger().e("Birthday is still null, cannot set last scan"); + try { + await _initializeLastScan(); + } catch (e) { + Logger().e("Error initializing last scan: $e"); return; } } @@ -118,6 +106,13 @@ class SynchronizationService { } } + Future _initializeLastScan() async { + final blockHeight = + await chainState.getBlockHeightFromDate(walletState.birthday); + + walletState.lastScan = blockHeight; + } + void stopSyncTimer() { Logger().i("Stopping sync service"); _timer?.cancel(); diff --git a/lib/states/wallet_state.dart b/lib/states/wallet_state.dart index e6571ddf..6dd12d44 100644 --- a/lib/states/wallet_state.dart +++ b/lib/states/wallet_state.dart @@ -104,35 +104,41 @@ class WalletState extends ChangeNotifier { await walletRepository.reset(); } - Future restoreWallet(ApiNetwork network, String mnemonic, DateTime birthday) async { + Future restoreWallet( + ApiNetwork network, String mnemonic, DateTime birthday) async { final args = WalletSetupArgs( setupType: WalletSetupType.mnemonic(mnemonic), network: network); final setupResult = SpWallet.setupWallet(setupArgs: args); - final wallet = - await walletRepository.setupWallet(setupResult, network, birthday); + final wallet = await walletRepository.setupWallet( + setupResult, network, birthday, null); // fill current state variables receivePaymentCode = wallet.getReceivingAddress(); changePaymentCode = wallet.getChangeAddress(); this.birthday = birthday; this.network = network; + + // lastScan will be initialized by chainState synchronization service + lastScan = null; + await _updateWalletState(); } - Future createNewWallet(ApiNetwork network) async { + Future createNewWallet(ApiNetwork network, int? currentTip) async { final now = DateTime.now().toUtc(); final args = WalletSetupArgs( setupType: const WalletSetupType.newWallet(), network: network); final setupResult = SpWallet.setupWallet(setupArgs: args); - final wallet = - await walletRepository.setupWallet(setupResult, network, now); + final wallet = await walletRepository.setupWallet( + setupResult, network, now, currentTip); // fill current state variables receivePaymentCode = wallet.getReceivingAddress(); changePaymentCode = wallet.getChangeAddress(); birthday = now; this.network = network; + lastScan = currentTip; await _updateWalletState(); } @@ -177,7 +183,6 @@ class WalletState extends ChangeNotifier { return defaultBirthday; } } - } Future resetToScanHeight(int height) async { From 45ca77b4c66e2f61bfa544b7c7438811a4d31b1d Mon Sep 17 00:00:00 2001 From: Sosthene Date: Sat, 28 Feb 2026 14:29:46 +0100 Subject: [PATCH 13/13] fix: add `_isLoading` bool to display a spinner on `seed_phrase` while loading --- .../onboarding/recovery/seed_phrase.dart | 114 ++++++++---------- 1 file changed, 52 insertions(+), 62 deletions(-) diff --git a/lib/screens/onboarding/recovery/seed_phrase.dart b/lib/screens/onboarding/recovery/seed_phrase.dart index 50afabd4..78192bf9 100644 --- a/lib/screens/onboarding/recovery/seed_phrase.dart +++ b/lib/screens/onboarding/recovery/seed_phrase.dart @@ -11,10 +11,11 @@ import 'package:danawallet/states/chain_state.dart'; import 'package:danawallet/states/contacts_state.dart'; import 'package:danawallet/states/scan_progress_notifier.dart'; import 'package:danawallet/states/wallet_state.dart'; -import 'package:danawallet/widgets/back_button.dart'; import 'package:danawallet/widgets/buttons/footer/footer_button.dart'; +import 'package:danawallet/widgets/loading_widget.dart'; import 'package:danawallet/widgets/pills/mnemonic_input_pill_box.dart'; import 'package:danawallet/widgets/pin_guard.dart'; +import 'package:danawallet/widgets/skeletons/screen_skeleton.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:sizer/sizer.dart'; @@ -42,6 +43,7 @@ class SeedPhraseScreenState extends State { late List focusNodes; late MnemonicInputPillBox pills; bool _knowsBirthday = false; + bool _isLoading = false; Future onRestore(BuildContext context) async { try { @@ -73,6 +75,10 @@ class SeedPhraseScreenState extends State { pickedDate.year, pickedDate.month, pickedDate.day, 1); } + setState(() { + _isLoading = true; + }); + await walletState.restoreWallet(widget.network, mnemonic, birthday); chainState.initialize(widget.network); @@ -111,6 +117,9 @@ class SeedPhraseScreenState extends State { } } catch (e) { if (context.mounted) { + setState(() { + _isLoading = false; + }); displayError("Restore failed", e); } } @@ -146,14 +155,11 @@ class SeedPhraseScreenState extends State { @override Widget build(BuildContext context) { - final title = AutoSizeText( - "Enter your recovery phrase", - style: BitcoinTextStyle.title2(Colors.black) - .copyWith(height: 1.8, fontFamily: 'Inter'), - maxLines: 1, - ); + if (_isLoading) { + return const LoadingWidget(); + } - final text = AutoSizeText( + final subtitle = AutoSizeText( "Enter your recovery phrase. Don't enter a recovery phrase that wasn't generated by Dana!", style: BitcoinTextStyle.body3(Bitcoin.neutral7).copyWith( fontFamily: 'Inter', @@ -162,62 +168,46 @@ class SeedPhraseScreenState extends State { maxLines: 3, ); + final body = Column( + children: [ + Padding( + padding: EdgeInsets.symmetric( + vertical: Adaptive.h(3), + horizontal: Adaptive.w(2), + ), + child: subtitle, + ), + Expanded(child: pills), + Padding( + padding: EdgeInsets.symmetric(vertical: Adaptive.h(1.5)), + child: CheckboxListTile( + value: _knowsBirthday, + onChanged: (value) { + setState(() { + _knowsBirthday = value ?? false; + }); + }, + title: Text( + "I know when my wallet was created (birthday)", + style: BitcoinTextStyle.body3(Bitcoin.neutral7).copyWith( + fontFamily: 'Inter', + ), + ), + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + ), + ), + ], + ); + final footer = FooterButton(title: "Import", onPressed: () => onRestore(context)); - // footer padding, reduced when keyboard is open - final keyboardOpen = MediaQuery.of(context).viewInsets.bottom > 0; - final bottomPaddingPercentage = keyboardOpen ? 1 : 5; - - return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: false, - title: const BackButtonWidget(), - ), - body: SafeArea( - child: Padding( - padding: EdgeInsets.fromLTRB( - Adaptive.w(5), // Responsive left padding - 0, - Adaptive.w(5), // Responsive right padding - Adaptive.h( - bottomPaddingPercentage), // Responsive bottom padding - ), - child: Column( - children: [ - Column( - children: [ - title, - Padding( - padding: EdgeInsets.symmetric( - vertical: Adaptive.h(3), - horizontal: Adaptive.w(2)), - child: text), - ], - ), - Expanded(child: pills), - Padding( - padding: EdgeInsets.symmetric(vertical: Adaptive.h(1.5)), - child: CheckboxListTile( - value: _knowsBirthday, - onChanged: (value) { - setState(() { - _knowsBirthday = value ?? false; - }); - }, - title: Text( - "I know when my wallet was created (birthday)", - style: BitcoinTextStyle.body3(Bitcoin.neutral7).copyWith( - fontFamily: 'Inter', - ), - ), - controlAffinity: ListTileControlAffinity.leading, - contentPadding: EdgeInsets.zero, - ), - ), - footer, - ], - )), - )); + return ScreenSkeleton( + title: "Enter your recovery phrase", + showBackButton: true, + body: body, + footer: footer, + ); } }