diff --git a/crates/commons/src/lib.rs b/crates/commons/src/lib.rs index 21fef1226..3f80c13fe 100644 --- a/crates/commons/src/lib.rs +++ b/crates/commons/src/lib.rs @@ -15,6 +15,7 @@ mod rollover; mod signature; mod trade; +pub use crate::trade::*; pub use backup::*; pub use collab_revert::*; pub use liquidity_option::*; @@ -27,7 +28,6 @@ pub use price::Price; pub use price::Prices; pub use rollover::*; pub use signature::*; -pub use trade::*; pub const AUTH_SIGN_MESSAGE: &[u8; 19] = b"Hello it's me Mario"; diff --git a/mobile/lib/common/domain/background_task.dart b/mobile/lib/common/domain/background_task.dart index 0540c7708..802a7f47b 100644 --- a/mobile/lib/common/domain/background_task.dart +++ b/mobile/lib/common/domain/background_task.dart @@ -77,3 +77,17 @@ class CollabRevert { return bridge.BackgroundTask_CollabRevert(TaskStatus.apiDummy()); } } + +class FullSync { + final TaskStatus taskStatus; + + FullSync({required this.taskStatus}); + + static FullSync fromApi(bridge.BackgroundTask_FullSync fullSync) { + return FullSync(taskStatus: TaskStatus.fromApi(fullSync.field0)); + } + + static bridge.BackgroundTask apiDummy() { + return bridge.BackgroundTask_FullSync(TaskStatus.apiDummy()); + } +} diff --git a/mobile/lib/common/full_sync_change_notifier.dart b/mobile/lib/common/full_sync_change_notifier.dart new file mode 100644 index 000000000..61872e04c --- /dev/null +++ b/mobile/lib/common/full_sync_change_notifier.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:get_10101/bridge_generated/bridge_definitions.dart' as bridge; +import 'package:get_10101/common/application/event_service.dart'; +import 'package:get_10101/common/domain/background_task.dart'; +import 'package:get_10101/common/global_keys.dart'; +import 'package:get_10101/common/task_status_dialog.dart'; +import 'package:get_10101/logger/logger.dart'; +import 'package:provider/provider.dart'; + +class FullSyncChangeNotifier extends ChangeNotifier implements Subscriber { + late TaskStatus taskStatus; + + @override + void notify(bridge.Event event) async { + if (event is bridge.Event_BackgroundNotification) { + if (event.field0 is! bridge.BackgroundTask_FullSync) { + // ignoring other kinds of background tasks + return; + } + FullSync fullSync = FullSync.fromApi(event.field0 as bridge.BackgroundTask_FullSync); + logger.d("Received a full sync event. Status: ${fullSync.taskStatus}"); + + taskStatus = fullSync.taskStatus; + + if (taskStatus == TaskStatus.pending) { + while (shellNavigatorKey.currentContext == null) { + await Future.delayed(const Duration(milliseconds: 100)); // Adjust delay as needed + } + + // initialize dialog for the pending task + showDialog( + context: shellNavigatorKey.currentContext!, + builder: (context) { + TaskStatus status = context.watch().taskStatus; + + Widget content; + switch (status) { + case TaskStatus.pending: + content = const Text("Waiting for on-chain sync to complete"); + case TaskStatus.success: + content = const Text( + "Full on-chain sync completed. If your balance is still incomplete, go to Wallet Settings to trigger further syncs."); + case TaskStatus.failed: + content = const Text( + "Full on-chain sync failed. You can keep trying by shutting down the app and restarting."); + } + + return TaskStatusDialog(title: "Full wallet sync", status: status, content: content); + }, + ); + } else { + // notify dialog about changed task status + notifyListeners(); + } + } + } +} diff --git a/mobile/lib/common/init_service.dart b/mobile/lib/common/init_service.dart index dccf15bf9..a3ba277a2 100644 --- a/mobile/lib/common/init_service.dart +++ b/mobile/lib/common/init_service.dart @@ -3,6 +3,7 @@ import 'package:get_10101/common/application/lsp_change_notifier.dart'; import 'package:get_10101/common/dlc_channel_change_notifier.dart'; import 'package:get_10101/common/dlc_channel_service.dart'; import 'package:get_10101/common/domain/lsp_config.dart'; +import 'package:get_10101/common/full_sync_change_notifier.dart'; import 'package:get_10101/features/brag/github_service.dart'; import 'package:get_10101/features/trade/candlestick_change_notifier.dart'; import 'package:get_10101/features/trade/order_change_notifier.dart'; @@ -66,6 +67,7 @@ List createProviders() { ChangeNotifierProvider(create: (context) => CollabRevertChangeNotifier()), ChangeNotifierProvider(create: (context) => LspChangeNotifier(channelInfoService)), ChangeNotifierProvider(create: (context) => PollChangeNotifier(pollService)), + ChangeNotifierProvider(create: (context) => FullSyncChangeNotifier()), Provider(create: (context) => config), Provider(create: (context) => channelInfoService), Provider(create: (context) => pollService), @@ -93,6 +95,7 @@ void subscribeToNotifiers(BuildContext context) { final recoverDlcChangeNotifier = context.read(); final collabRevertChangeNotifier = context.read(); final lspConfigChangeNotifier = context.read(); + final fullSyncChangeNotifier = context.read(); eventService.subscribe( orderChangeNotifier, bridge.Event.orderUpdateNotification(Order.apiDummy())); @@ -136,6 +139,9 @@ void subscribeToNotifiers(BuildContext context) { eventService.subscribe(lspConfigChangeNotifier, bridge.Event.authenticated(LspConfig.apiDummy())); + eventService.subscribe( + fullSyncChangeNotifier, bridge.Event.backgroundNotification(FullSync.apiDummy())); + eventService.subscribe( AnonSubscriber((event) => logger.i(event.field0)), const bridge.Event.log("")); } diff --git a/mobile/lib/common/settings/wallet_settings.dart b/mobile/lib/common/settings/wallet_settings.dart index 159d8b6a3..6bdc946d7 100644 --- a/mobile/lib/common/settings/wallet_settings.dart +++ b/mobile/lib/common/settings/wallet_settings.dart @@ -66,69 +66,76 @@ class _WalletSettingsState extends State { height: 20, ), Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "The amount of addresses to sync for (at least). Once you confirm, a full wallet sync will be performed. The higher the gap is, the longer the sync will take. Hence, we recommend syncing incrementally.", - style: TextStyle(fontSize: 18), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), - child: Stack( - alignment: Alignment.centerRight, - children: [ - TextFormField( - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly - ], - keyboardType: TextInputType.number, - controller: lookAheadController, - decoration: const InputDecoration( - border: UnderlineInputBorder(), - labelText: 'Wallet Gap', + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Full sync \n", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const Text( + "Select the stop gap and confirm to perform a full wallet sync.\n\nThe stop gap determines how many consecutive unused addresses the wallet will have to find to stop syncing. The bigger the gap, the longer the sync will take. Hence, we recommend syncing incrementally.\n\nFor example: use a stop gap of 5, wait for the sync to complete and check your balance; if your balance still seems incorrect, come back here and use a stop gap of 10, etc.", + style: TextStyle(fontSize: 18), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), + child: Stack( + alignment: Alignment.centerRight, + children: [ + TextFormField( + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly + ], + keyboardType: TextInputType.number, + controller: lookAheadController, + decoration: const InputDecoration( + border: UnderlineInputBorder(), + labelText: 'Stop Gap', + ), ), - ), - Visibility( - visible: !syncing, - replacement: const CircularProgressIndicator(), - child: IconButton( - icon: const Icon( - Icons.check, - color: Colors.green, - ), - onPressed: () async { - final messenger = ScaffoldMessenger.of(context); - try { - var gap = lookAheadController.value.text; - var gapAsNumber = int.parse(gap); + Visibility( + visible: !syncing, + replacement: const CircularProgressIndicator(), + child: IconButton( + icon: const Icon( + Icons.check, + color: Colors.green, + ), + onPressed: () async { + final messenger = ScaffoldMessenger.of(context); + try { + var gap = lookAheadController.value.text; + var gapAsNumber = int.parse(gap); - setState(() { - syncing = true; - }); + setState(() { + syncing = true; + }); - await rust.api.fullSync(stopGap: gapAsNumber); - showSnackBar(messenger, "Successfully synced for new gap."); + await rust.api.fullSync(stopGap: gapAsNumber); + showSnackBar(messenger, "Successfully synced for new gap."); - setState(() { - syncing = false; - }); - } catch (exception) { - logger.e("Failed to complete full sync $exception"); - showSnackBar( - messenger, "Error when running full sync $exception"); - } finally { - setState(() { - syncing = false; - }); - } - }, - )) - ], - ), - ) - ], + setState(() { + syncing = false; + }); + } catch (exception) { + logger.e("Failed to complete full sync $exception"); + showSnackBar( + messenger, "Error when running full sync $exception"); + } finally { + setState(() { + syncing = false; + }); + } + }, + )) + ], + ), + ) + ], + ), ), ), ], diff --git a/mobile/native/src/event/api.rs b/mobile/native/src/event/api.rs index 723e5a1cd..dfa518972 100644 --- a/mobile/native/src/event/api.rs +++ b/mobile/native/src/event/api.rs @@ -42,6 +42,8 @@ pub enum BackgroundTask { RecoverDlc(TaskStatus), /// The coordinator wants to collaboratively close a ln channel with a stuck position. CollabRevert(TaskStatus), + /// The app is performing a full sync of the on-chain wallet. + FullSync(TaskStatus), } impl From for Event { @@ -141,6 +143,7 @@ impl From for BackgroundTask { event::BackgroundTask::CollabRevert(status) => { BackgroundTask::CollabRevert(status.into()) } + event::BackgroundTask::FullSync(status) => BackgroundTask::FullSync(status.into()), } } } diff --git a/mobile/native/src/event/mod.rs b/mobile/native/src/event/mod.rs index afd745d1f..a6dcb1272 100644 --- a/mobile/native/src/event/mod.rs +++ b/mobile/native/src/event/mod.rs @@ -47,6 +47,7 @@ pub enum BackgroundTask { Rollover(TaskStatus), CollabRevert(TaskStatus), RecoverDlc(TaskStatus), + FullSync(TaskStatus), } #[derive(Clone, Debug)] diff --git a/mobile/native/src/ln_dlc/mod.rs b/mobile/native/src/ln_dlc/mod.rs index 01ae2ace5..3b7fa0f5e 100644 --- a/mobile/native/src/ln_dlc/mod.rs +++ b/mobile/native/src/ln_dlc/mod.rs @@ -100,6 +100,9 @@ const UPDATE_WALLET_HISTORY_INTERVAL: Duration = Duration::from_secs(5); const CHECK_OPEN_ORDERS_INTERVAL: Duration = Duration::from_secs(60); const ON_CHAIN_SYNC_INTERVAL: Duration = Duration::from_secs(300); +/// The name of the BDK wallet database file. +const WALLET_DB_FILE_NAME: &str = "bdk-wallet"; + /// The prefix to the [`bdk_file_store`] database file where BDK persists /// [`bdk::wallet::ChangeSet`]s. /// @@ -310,7 +313,7 @@ pub fn run(seed_dir: String, runtime: &Runtime) -> Result<()> { let node_event_handler = Arc::new(NodeEventHandler::new()); let wallet_storage = { - let wallet_dir = Path::new(&config::get_data_dir()).join("wallet"); + let wallet_dir = Path::new(&config::get_data_dir()).join(WALLET_DB_FILE_NAME); bdk_file_store::Store::open_or_create_new(WALLET_DB_PREFIX.as_bytes(), wallet_dir)? }; @@ -426,14 +429,67 @@ pub fn run(seed_dir: String, runtime: &Runtime) -> Result<()> { } }); - state::set_node(node); + state::set_node(node.clone()); event::publish(&EventInternal::Init("10101 is ready.".to_string())); + tokio::spawn(full_sync_on_wallet_db_migration()); + Ok(()) }) } +pub async fn full_sync_on_wallet_db_migration() { + let node = state::get_node(); + + let old_wallet_db_path = Path::new(&config::get_data_dir()).join("wallet"); + if old_wallet_db_path.exists() { + event::publish(&EventInternal::BackgroundNotification( + event::BackgroundTask::FullSync(event::TaskStatus::Pending), + )); + + let stop_gap = 20; + tracing::info!( + %stop_gap, + "Old wallet DB detected. Attempting to populate new wallet with full sync" + ); + + match node.inner.full_sync(stop_gap).await { + Ok(_) => { + tracing::info!("Full sync successful"); + + // Spawn into the blocking thread pool of the dedicated backend runtime to avoid + // blocking the UI thread. + if let Ok(runtime) = state::get_or_create_tokio_runtime() { + runtime + .spawn_blocking(move || { + if let Err(e) = keep_wallet_balance_and_history_up_to_date(&node) { + tracing::error!("Failed to keep wallet history up to date: {e:#}"); + } + }) + .await + .expect("task to complete"); + } + + event::publish(&EventInternal::BackgroundNotification( + event::BackgroundTask::FullSync(event::TaskStatus::Success), + )); + + if let Err(e) = std::fs::remove_file(old_wallet_db_path) { + tracing::info!("Failed to delete old wallet DB file: {e:#}"); + } + } + Err(e) => { + tracing::error!("Full sync failed: {e:#}"); + + event::publish(&EventInternal::BackgroundNotification( + event::BackgroundTask::FullSync(event::TaskStatus::Failed), + )); + } + }; + } +} + pub fn init_new_mnemonic(target_seed_file: &Path) -> Result<()> { let seed = Bip39Seed::initialize(target_seed_file)?; state::set_seed(seed);