From e9af1016458b4436b7bc0000aa48afdcdf6ccb47 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Thu, 14 Sep 2023 13:19:29 +0200 Subject: [PATCH 1/5] chore: Ignore unrelated background notification tasks Before we got confusing log messages with "received unexpected event". --- .../lib/features/trade/async_order_change_notifier.dart | 7 +++++-- mobile/lib/features/trade/rollover_change_notifier.dart | 8 ++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/mobile/lib/features/trade/async_order_change_notifier.dart b/mobile/lib/features/trade/async_order_change_notifier.dart index 5cff390f4..eccb6930d 100644 --- a/mobile/lib/features/trade/async_order_change_notifier.dart +++ b/mobile/lib/features/trade/async_order_change_notifier.dart @@ -27,8 +27,11 @@ class AsyncOrderChangeNotifier extends ChangeNotifier implements Subscriber { @override void notify(bridge.Event event) { - if (event is bridge.Event_BackgroundNotification && - event.field0 is bridge.BackgroundTask_AsyncTrade) { + if (event is bridge.Event_BackgroundNotification) { + if (event.field0 is! bridge.BackgroundTask_AsyncTrade) { + // ignoring other kinds of background tasks + return; + } AsyncTrade asyncTrade = AsyncTrade.fromApi(event.field0 as bridge.BackgroundTask_AsyncTrade); FLog.debug(text: "Received a async trade event. Reason: ${asyncTrade.orderReason}"); showDialog( diff --git a/mobile/lib/features/trade/rollover_change_notifier.dart b/mobile/lib/features/trade/rollover_change_notifier.dart index 03cc748a2..30edcf00f 100644 --- a/mobile/lib/features/trade/rollover_change_notifier.dart +++ b/mobile/lib/features/trade/rollover_change_notifier.dart @@ -12,8 +12,12 @@ class RolloverChangeNotifier extends ChangeNotifier implements Subscriber { @override void notify(bridge.Event event) { - if (event is bridge.Event_BackgroundNotification && - event.field0 is bridge.BackgroundTask_Rollover) { + if (event is bridge.Event_BackgroundNotification) { + if (event.field0 is! bridge.BackgroundTask_Rollover) { + // ignoring other kinds of background tasks + return; + } + Rollover rollover = Rollover.fromApi(event.field0 as bridge.BackgroundTask_Rollover); FLog.debug(text: "Received a rollover event. Status: ${rollover.taskStatus}"); From 5d7222d0620a23f74ce69ed9e61bfde4c9542916 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Thu, 14 Sep 2023 13:21:47 +0200 Subject: [PATCH 2/5] feat: Sync dlc channel state with position This change will do the following things depending on the dlc channel state and position. - DLC Channel in state `Signed` but no position: Create position from `filling` order. - DLC Channel in state `OffChainClosed` and a position exists. Delete the position. - DLC Channel in state `CloseOffered` or `CloseAccepted`: Inform the UI that the dlc channel is recovering. - DLC Channel in state `Offered`, `Accepted` or `Finalized`: Inform the UI that the dlc channel is recovering. - DLC Channel in any other state but with position: Delete position the channel might have been force closed. --- CHANGELOG.md | 1 + Cargo.lock | 2 + crates/ln-dlc-node/src/node/dlc_channel.rs | 46 ++ crates/ln-dlc-node/src/node/mod.rs | 8 + mobile/lib/common/domain/background_task.dart | 14 + .../common/recover_dlc_change_notifier.dart | 55 ++ mobile/lib/main.dart | 6 + mobile/native/Cargo.toml | 4 + mobile/native/src/event/api.rs | 7 + mobile/native/src/event/mod.rs | 1 + mobile/native/src/ln_dlc/mod.rs | 5 + mobile/native/src/ln_dlc/node.rs | 73 ++- .../native/src/ln_dlc/sync_position_to_dlc.rs | 484 ++++++++++++++++++ mobile/native/src/trade/position/handler.rs | 2 +- 14 files changed, 664 insertions(+), 44 deletions(-) create mode 100644 mobile/lib/common/recover_dlc_change_notifier.dart create mode 100644 mobile/native/src/ln_dlc/sync_position_to_dlc.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 73cba68c5..326398fdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - feat: Allow exporting the seed phrase even if the Node is offline - Changed expiry to next Sunday 3 pm UTC - Automatically rollover if user opens app during rollover weekend +- Sync position with dlc channel state ## [1.2.6] - 2023-09-06 diff --git a/Cargo.lock b/Cargo.lock index 9f1b9f7e2..d5d8b4fd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2111,6 +2111,7 @@ dependencies = [ "coordinator-commons", "diesel", "diesel_migrations", + "dlc", "dlc-messages", "flutter_rust_bridge", "futures", @@ -2126,6 +2127,7 @@ dependencies = [ "parking_lot 0.12.1", "reqwest", "rust_decimal", + "secp256k1-zkp", "serde", "serde_json", "state", diff --git a/crates/ln-dlc-node/src/node/dlc_channel.rs b/crates/ln-dlc-node/src/node/dlc_channel.rs index 88fad91b7..4bc598c3b 100644 --- a/crates/ln-dlc-node/src/node/dlc_channel.rs +++ b/crates/ln-dlc-node/src/node/dlc_channel.rs @@ -23,6 +23,7 @@ use dlc_messages::OnChainMessage; use dlc_messages::SubChannelMessage; use lightning::ln::channelmanager::ChannelDetails; use std::sync::Arc; +use time::OffsetDateTime; use tokio::task::spawn_blocking; impl

Node

@@ -294,6 +295,51 @@ where Ok(()) } + /// Gets the collateral and expiry for a signed contract of that given channel_id. Will return + /// an error if the contract is not confirmed. + pub fn get_collateral_and_expiry_for_confirmed_contract( + &self, + channel_id: ChannelId, + ) -> Result<(u64, OffsetDateTime)> { + let storage = self.dlc_manager.get_store(); + let sub_channel = storage.get_sub_channel(channel_id)?.with_context(|| { + format!( + "Could not find sub channel by channel id {}", + channel_id.to_hex() + ) + })?; + let dlc_channel_id = sub_channel + .get_dlc_channel_id(0) + .context("Could not fetch dlc channel id")?; + + match self.get_contract_by_dlc_channel_id(dlc_channel_id)? { + Contract::Confirmed(contract) => { + let offered_contract = contract.accepted_contract.offered_contract; + let contract_info = offered_contract + .contract_info + .first() + .expect("contract info to exist on a signed contract"); + let oracle_announcement = contract_info + .oracle_announcements + .first() + .expect("oracle announcement to exist on signed contract"); + + let expiry_timestamp = OffsetDateTime::from_unix_timestamp( + oracle_announcement.oracle_event.event_maturity_epoch as i64, + )?; + + Ok(( + contract.accepted_contract.accept_params.collateral, + expiry_timestamp, + )) + } + _ => bail!( + "Confirmed contract not found for channel ID: {}", + hex::encode(channel_id) + ), + } + } + fn get_dlc_channel( &self, matcher: impl FnMut(&&SubChannel) -> bool, diff --git a/crates/ln-dlc-node/src/node/mod.rs b/crates/ln-dlc-node/src/node/mod.rs index e6fc18a4b..82a7f6ace 100644 --- a/crates/ln-dlc-node/src/node/mod.rs +++ b/crates/ln-dlc-node/src/node/mod.rs @@ -514,6 +514,14 @@ where } } } + + pub async fn sub_channel_manager_periodic_check(&self) -> Result<()> { + sub_channel_manager_periodic_check( + self.sub_channel_manager.clone(), + &self.dlc_message_handler, + ) + .await + } } async fn update_fee_rate_estimates( diff --git a/mobile/lib/common/domain/background_task.dart b/mobile/lib/common/domain/background_task.dart index e6c9afe90..11839e8f9 100644 --- a/mobile/lib/common/domain/background_task.dart +++ b/mobile/lib/common/domain/background_task.dart @@ -49,3 +49,17 @@ class Rollover { return bridge.BackgroundTask_Rollover(TaskStatus.apiDummy()); } } + +class RecoverDlc { + final TaskStatus taskStatus; + + RecoverDlc({required this.taskStatus}); + + static RecoverDlc fromApi(bridge.BackgroundTask_RecoverDlc recoverDlc) { + return RecoverDlc(taskStatus: TaskStatus.fromApi(recoverDlc.field0)); + } + + static bridge.BackgroundTask apiDummy() { + return bridge.BackgroundTask_RecoverDlc(TaskStatus.apiDummy()); + } +} diff --git a/mobile/lib/common/recover_dlc_change_notifier.dart b/mobile/lib/common/recover_dlc_change_notifier.dart new file mode 100644 index 000000000..52a8f8a8a --- /dev/null +++ b/mobile/lib/common/recover_dlc_change_notifier.dart @@ -0,0 +1,55 @@ +import 'package:f_logs/model/flog/flog.dart'; +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/features/trade/order_submission_status_dialog.dart'; +import 'package:provider/provider.dart'; + +class RecoverDlcChangeNotifier extends ChangeNotifier implements Subscriber { + late TaskStatus taskStatus; + + @override + void notify(bridge.Event event) { + if (event is bridge.Event_BackgroundNotification) { + if (event.field0 is! bridge.BackgroundTask_RecoverDlc) { + // ignoring other kinds of background tasks + return; + } + RecoverDlc recoverDlc = RecoverDlc.fromApi(event.field0 as bridge.BackgroundTask_RecoverDlc); + FLog.debug(text: "Received a recover dlc event. Status: ${recoverDlc.taskStatus}"); + + taskStatus = recoverDlc.taskStatus; + + if (taskStatus == TaskStatus.pending) { + // initialize dialog for the pending task + showDialog( + context: shellNavigatorKey.currentContext!, + builder: (context) { + TaskStatus status = context.watch().taskStatus; + + // todo(holzeis): Reusing the order submission status dialog is not nice, but it's actually suitable for any task execution that has pending, + // failed and success states. We may should consider renaming this dialog for its more generic purpose. + OrderSubmissionStatusDialogType type = OrderSubmissionStatusDialogType.pendingSubmit; + switch (status) { + case TaskStatus.pending: + type = OrderSubmissionStatusDialogType.successfulSubmit; + case TaskStatus.failed: + type = OrderSubmissionStatusDialogType.failedFill; + case TaskStatus.success: + type = OrderSubmissionStatusDialogType.filled; + } + + late Widget content = const Text("Recovering your dlc channel"); + + return OrderSubmissionStatusDialog(title: "Catching up!", type: type, content: content); + }, + ); + } else { + // notify dialog about changed task status + notifyListeners(); + } + } + } +} diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 670bda3fe..ecf05701c 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -15,6 +15,7 @@ import 'package:get_10101/common/color.dart'; import 'package:get_10101/common/domain/background_task.dart'; import 'package:get_10101/common/domain/service_status.dart'; import 'package:get_10101/common/global_keys.dart'; +import 'package:get_10101/common/recover_dlc_change_notifier.dart'; import 'package:get_10101/common/service_status_notifier.dart'; import 'package:get_10101/features/stable/stable_screen.dart'; import 'package:get_10101/features/trade/application/candlestick_service.dart'; @@ -90,6 +91,7 @@ void main() async { ChangeNotifierProvider(create: (context) => ChannelStatusNotifier()), ChangeNotifierProvider(create: (context) => AsyncOrderChangeNotifier(OrderService())), ChangeNotifierProvider(create: (context) => RolloverChangeNotifier()), + ChangeNotifierProvider(create: (context) => RecoverDlcChangeNotifier()), Provider(create: (context) => Environment.parse()), Provider(create: (context) => channelInfoService) ], child: const TenTenOneApp())); @@ -481,6 +483,7 @@ void subscribeToNotifiers(BuildContext context) { final stableValuesChangeNotifier = context.read(); final asyncOrderChangeNotifier = context.read(); final rolloverChangeNotifier = context.read(); + final recoverDlcChangeNotifier = context.read(); eventService.subscribe( orderChangeNotifier, bridge.Event.orderUpdateNotification(Order.apiDummy())); @@ -519,6 +522,9 @@ void subscribeToNotifiers(BuildContext context) { eventService.subscribe( rolloverChangeNotifier, bridge.Event.backgroundNotification(Rollover.apiDummy())); + eventService.subscribe( + recoverDlcChangeNotifier, bridge.Event.backgroundNotification(RecoverDlc.apiDummy())); + channelStatusNotifier.subscribe(eventService); eventService.subscribe( diff --git a/mobile/native/Cargo.toml b/mobile/native/Cargo.toml index 7069dfac0..1ce11da37 100644 --- a/mobile/native/Cargo.toml +++ b/mobile/native/Cargo.toml @@ -39,3 +39,7 @@ tracing = "0.1.37" tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "env-filter", "time", "json"] } trade = { path = "../../crates/trade" } uuid = { version = "1.3.0", features = ["v4", "fast-rng", "macro-diagnostics"] } + +[dev-dependencies] +dlc = { version = "0.4.0" } +secp256k1-zkp = { version = "0.7.0", features = ["bitcoin_hashes", "rand", "rand-std"] } diff --git a/mobile/native/src/event/api.rs b/mobile/native/src/event/api.rs index 6a4cf9a8e..208a7e59c 100644 --- a/mobile/native/src/event/api.rs +++ b/mobile/native/src/event/api.rs @@ -32,8 +32,14 @@ pub enum Event { #[frb] #[derive(Clone)] pub enum BackgroundTask { + /// The order book submitted an trade which was matched asynchronously while the app was + /// offline. AsyncTrade(OrderReason), + /// The order book submitted its intention to rollover the about to expire position. Rollover(TaskStatus), + /// The app was started with a dlc channel in an intermediate state. This task is in pending + /// until the dlc protocol reaches a final state. + RecoverDlc(TaskStatus), } impl From for Event { @@ -128,6 +134,7 @@ impl From for BackgroundTask { BackgroundTask::AsyncTrade(order_reason.into()) } event::BackgroundTask::Rollover(status) => BackgroundTask::Rollover(status.into()), + event::BackgroundTask::RecoverDlc(status) => BackgroundTask::RecoverDlc(status.into()), } } } diff --git a/mobile/native/src/event/mod.rs b/mobile/native/src/event/mod.rs index 1d30f9349..b18f9592e 100644 --- a/mobile/native/src/event/mod.rs +++ b/mobile/native/src/event/mod.rs @@ -47,6 +47,7 @@ pub enum EventInternal { pub enum BackgroundTask { AsyncTrade(OrderReason), Rollover(TaskStatus), + RecoverDlc(TaskStatus), } #[derive(Clone, Debug)] diff --git a/mobile/native/src/ln_dlc/mod.rs b/mobile/native/src/ln_dlc/mod.rs index 6970152c6..79d927694 100644 --- a/mobile/native/src/ln_dlc/mod.rs +++ b/mobile/native/src/ln_dlc/mod.rs @@ -66,6 +66,7 @@ use tokio::task::spawn_blocking; mod lightning_subscriber; mod node; +mod sync_position_to_dlc; pub mod channel_status; @@ -283,6 +284,10 @@ pub fn run(data_dir: String, seed_dir: String, runtime: &Runtime) -> Result<()> runtime.spawn(track_channel_status(node.clone())); + if let Err(e) = node.sync_position_with_dlc_channel_state().await { + tracing::error!("Failed to sync position with dlc channel state. Error: {e:#}"); + } + NODE.set(node); event::publish(&EventInternal::Init("10101 is ready.".to_string())); diff --git a/mobile/native/src/ln_dlc/node.rs b/mobile/native/src/ln_dlc/node.rs index 8ef2fe31a..c66e46106 100644 --- a/mobile/native/src/ln_dlc/node.rs +++ b/mobile/native/src/ln_dlc/node.rs @@ -6,12 +6,10 @@ use crate::event::TaskStatus; use crate::trade::order; use crate::trade::position; use crate::trade::position::PositionState; -use anyhow::bail; use anyhow::Context; use anyhow::Result; use bdk::bitcoin::secp256k1::PublicKey; use bdk::TransactionDetails; -use bitcoin::hashes::hex::ToHex; use dlc_messages::sub_channel::SubChannelCloseFinalize; use dlc_messages::sub_channel::SubChannelRevoke; use dlc_messages::ChannelMessage; @@ -29,8 +27,6 @@ use ln_dlc_node::channel::Channel; use ln_dlc_node::channel::FakeScid; use ln_dlc_node::node; use ln_dlc_node::node::dlc_message_name; -use ln_dlc_node::node::rust_dlc_manager::contract::Contract; -use ln_dlc_node::node::rust_dlc_manager::Storage; use ln_dlc_node::node::sub_channel_message_name; use ln_dlc_node::node::NodeInfo; use ln_dlc_node::node::PaymentDetails; @@ -245,44 +241,9 @@ impl Node { channel_id, .. })) = msg { - let storage = self.inner.dlc_manager.get_store(); - let sub_channel = storage.get_sub_channel(channel_id)?.with_context(|| { - format!( - "Could not find sub channel by channel id {}", - channel_id.to_hex() - ) - })?; - let dlc_channel_id = sub_channel - .get_dlc_channel_id(0) - .context("Could not fetch dlc channel id")?; - - let (accept_collateral, expiry_timestamp) = - match self.inner.get_contract_by_dlc_channel_id(dlc_channel_id)? { - Contract::Confirmed(contract) => { - let offered_contract = contract.accepted_contract.offered_contract; - let contract_info = offered_contract - .contract_info - .first() - .context("contract info to exist on a signed contract")?; - let oracle_announcement = contract_info - .oracle_announcements - .first() - .context("oracle announcement to exist on signed contract")?; - - let expiry_timestamp = OffsetDateTime::from_unix_timestamp( - oracle_announcement.oracle_event.event_maturity_epoch as i64, - )?; - - ( - contract.accepted_contract.accept_params.collateral, - expiry_timestamp, - ) - } - _ => bail!( - "Confirmed contract not found for channel ID: {}", - hex::encode(channel_id) - ), - }; + let (accept_collateral, expiry_timestamp) = self + .inner + .get_collateral_and_expiry_for_confirmed_contract(channel_id)?; let filled_order = order::handler::order_filled() .context("Cannot mark order as filled for confirmed DLC")?; @@ -294,6 +255,19 @@ impl Node { ) .context("Failed to update position after DLC creation")?; + // Sending always a recover dlc background notification success message here as we do + // not know if we might have reached this state after a restart. This event is only + // received by the UI at the moment indicating that the dialog can be closed. + // If the dialog is not open, this event would be simply ignored by the UI. + // + // fixme(holzeis): We should not require that event and align the UI handling with + // waiting for an order execution in the happy case with waiting for an + // order execution after an in between restart. For now it was the easiest + // to go parallel to that implementation so that we don't have to touch it. + event::publish(&EventInternal::BackgroundNotification( + BackgroundTask::RecoverDlc(TaskStatus::Success), + )); + if let Err(e) = self.pay_order_matching_fee(&channel_id) { tracing::error!("{e:#}"); } @@ -325,9 +299,22 @@ impl Node { })) = msg { let filled_order = order::handler::order_filled()?; - position::handler::update_position_after_dlc_closure(filled_order) + position::handler::update_position_after_dlc_closure(Some(filled_order)) .context("Failed to update position after DLC closure")?; + // Sending always a recover dlc background notification success message here as we do + // not know if we might have reached this state after a restart. This event is only + // received by the UI at the moment indicating that the dialog can be closed. + // If the dialog is not open, this event would be simply ignored by the UI. + // + // fixme(holzeis): We should not require that event and align the UI handling with + // waiting for an order execution in the happy case with waiting for an + // order execution after an in between restart. For now it was the easiest + // to go parallel to that implementation so that we don't have to touch it. + event::publish(&EventInternal::BackgroundNotification( + BackgroundTask::RecoverDlc(TaskStatus::Success), + )); + if let Err(e) = self.pay_order_matching_fee(&channel_id) { tracing::error!("{e:#}"); } diff --git a/mobile/native/src/ln_dlc/sync_position_to_dlc.rs b/mobile/native/src/ln_dlc/sync_position_to_dlc.rs new file mode 100644 index 000000000..18c79f822 --- /dev/null +++ b/mobile/native/src/ln_dlc/sync_position_to_dlc.rs @@ -0,0 +1,484 @@ +use crate::db; +use crate::event; +use crate::event::BackgroundTask; +use crate::event::EventInternal; +use crate::event::TaskStatus; +use crate::ln_dlc::node::Node; +use crate::trade::order; +use crate::trade::position; +use crate::trade::position::Position; +use anyhow::Result; +use ln_dlc_node::node::rust_dlc_manager::subchannel::SubChannel; +use ln_dlc_node::node::rust_dlc_manager::subchannel::SubChannelState; +use ln_dlc_node::node::rust_dlc_manager::ChannelId; +use ln_dlc_node::node::rust_dlc_manager::Storage; +use std::time::Duration; + +#[derive(PartialEq, Clone, Debug)] +enum SyncPositionToDlcAction { + ContinueSubchannelProtocol, + CreatePosition(ChannelId), + RemovePosition, +} + +impl Node { + /// Syncs the position with the dlc channel state. + /// + /// TODO(holzeis): With https://github.com/get10101/10101/issues/530 we should not require this logic anymore. + /// + /// - DLC Channel in state `Signed` but no position: Create position from `filling` order. + /// - DLC Channel in state `OffChainClosed` and a position exists. Delete the position. + /// - DLC Channel in state `CloseOffered` or `CloseAccepted`: Inform the UI that the dlc channel + /// is recovering. + /// - DLC Channel in state `Offered`, `Accepted` or `Finalized`: Inform the UI that the dlc + /// channel is recovering. + /// - DLC Channel in any other state but with position: Delete position the channel might have + /// been force closed. + pub async fn sync_position_with_dlc_channel_state(&self) -> Result<()> { + let channels = self.inner.channel_manager.list_channels(); + let channel_details = match channels.first() { + Some(channel_details) => channel_details, + None => return Ok(()), + }; + let dlc_channels = self.inner.dlc_manager.get_store().get_sub_channels()?; + let dlc_channel = dlc_channels + .iter() + .find(|dlc_channel| dlc_channel.channel_id == channel_details.channel_id); + + let positions = db::get_positions()?; + + match determine_sync_position_to_dlc_action(&positions.first(), &dlc_channel) { + Some(SyncPositionToDlcAction::ContinueSubchannelProtocol) => self.recover_dlc().await?, + Some(SyncPositionToDlcAction::CreatePosition(channel_id)) => { + match order::handler::order_filled() { + Ok(order) => { + let (accept_collateral, expiry_timestamp) = self + .inner + .get_collateral_and_expiry_for_confirmed_contract(channel_id)?; + + position::handler::update_position_after_dlc_creation( + order, + accept_collateral, + expiry_timestamp, + )?; + + tracing::info!("Successfully recovered position from order."); + } + Err(e) => { + tracing::error!("Could not recover position from order as no filling order was found! Error: {e:#}"); + } + } + } + Some(SyncPositionToDlcAction::RemovePosition) => { + let filled_order = match order::handler::order_filled() { + Ok(filled_order) => Some(filled_order), + Err(_) => None, + }; + + position::handler::update_position_after_dlc_closure(filled_order)?; + } + None => (), + } + + Ok(()) + } + + /// Sends a pending RecoverDlc background task notification to the UI, allowing the UI to show a + /// dialog with a spinner that the DLC protocol is still in progress. + /// Also triggers the `periodic_check` to process any actions that might have been created after + /// the channel reestablishment. + /// + /// fixme(holzeis): We currently use different events for show the recovery of a dlc and the + /// waiting for an order execution in the happy case (without an restart in between). Those + /// events and dialogs should be aligned. + async fn recover_dlc(&self) -> Result<()> { + tracing::warn!("It looks like the app was closed while the protocol was still executing."); + event::publish(&EventInternal::BackgroundNotification( + BackgroundTask::RecoverDlc(TaskStatus::Pending), + )); + + // fixme(holzeis): We are manually calling the periodic check here to speed up the + // processing of pending actions. + // Note, this might not speed up the process, as the coordinator might have to resend a + // message to continue the protocol. This should be fixed in `rust-dlc` and any + // pending actions should be processed immediately once the channel is ready instead + // of periodically checking if a pending action needs to be sent. + // Note, pending actions can only get created on channel reestablishment, hence we are + // waiting for arbitrary 5 seconds here to ensure that the channel is reestablished. + tokio::time::sleep(Duration::from_secs(5)).await; + if let Err(e) = self.inner.sub_channel_manager_periodic_check().await { + tracing::error!("Failed to process periodic check! Error: {e:#}"); + } + + Ok(()) + } +} + +/// Determines the action required in case the position and the dlc state get out of sync. +/// +/// Returns +/// - `Recover` if dlc is in an intermediate state. +/// - `CreatePosition` if the dlc is `Signed`, but no position exists. +/// - `RemovePosition` if the dlc is in any other state than `Signed` or an intermediate state and a +/// position exists. +/// - `None` otherwise. +fn determine_sync_position_to_dlc_action( + position: &Option<&Position>, + dlc_channel: &Option<&SubChannel>, +) -> Option { + match (position, dlc_channel) { + (Some(_), Some(dlc_channel)) => { + if matches!(dlc_channel.state, SubChannelState::Signed(_)) { + tracing::debug!("DLC channel and position are in sync"); + None + } else { + tracing::warn!(dlc_channel_state=?dlc_channel.state, "Found unexpected sub channel state"); + if matches!(dlc_channel.state, SubChannelState::OffChainClosed) { + tracing::warn!("Deleting position as dlc channel is already closed!"); + Some(SyncPositionToDlcAction::RemovePosition) + } else if matches!( + dlc_channel.state, + SubChannelState::CloseOffered(_) | SubChannelState::CloseAccepted(_) + ) { + Some(SyncPositionToDlcAction::ContinueSubchannelProtocol) + } else { + tracing::warn!( + "The DLC is in a state that can not be recovered. Removing position." + ); + // maybe a left over after a force-closure + Some(SyncPositionToDlcAction::RemovePosition) + } + } + } + (None, Some(dlc_channel)) => { + if matches!(dlc_channel.state, SubChannelState::OffChainClosed) { + tracing::debug!("DLC channel and position are in sync"); + None + } else { + tracing::warn!(dlc_channel_state=?dlc_channel.state, "Found unexpected sub channel state"); + if matches!(dlc_channel.state, SubChannelState::Signed(_)) { + tracing::warn!("Trying to recover position from order"); + Some(SyncPositionToDlcAction::CreatePosition( + dlc_channel.channel_id, + )) + } else if matches!( + dlc_channel.state, + SubChannelState::Offered(_) + | SubChannelState::Accepted(_) + | SubChannelState::Finalized(_) + ) { + Some(SyncPositionToDlcAction::ContinueSubchannelProtocol) + } else { + tracing::warn!("The DLC is in a state that can not be recovered."); + None + } + } + } + (Some(_), None) => { + tracing::warn!("Found position but without dlc channel. Removing position"); + Some(SyncPositionToDlcAction::RemovePosition) + } + _ => None, + } +} + +#[cfg(test)] +mod test { + use crate::ln_dlc::sync_position_to_dlc::determine_sync_position_to_dlc_action; + use crate::ln_dlc::sync_position_to_dlc::SyncPositionToDlcAction; + use crate::trade::position::Position; + use crate::trade::position::PositionState; + use bitcoin::secp256k1::ecdsa::Signature; + use bitcoin::secp256k1::PublicKey; + use bitcoin::PackedLockTime; + use bitcoin::Transaction; + use dlc::channel::sub_channel::SplitTx; + use lightning::chain::transaction::OutPoint; + use lightning::ln::chan_utils::CounterpartyCommitmentSecrets; + use ln_dlc_node::node::rust_dlc_manager::channel::party_points::PartyBasePoints; + use ln_dlc_node::node::rust_dlc_manager::subchannel::AcceptedSubChannel; + use ln_dlc_node::node::rust_dlc_manager::subchannel::CloseAcceptedSubChannel; + use ln_dlc_node::node::rust_dlc_manager::subchannel::CloseOfferedSubChannel; + use ln_dlc_node::node::rust_dlc_manager::subchannel::LnRollBackInfo; + use ln_dlc_node::node::rust_dlc_manager::subchannel::OfferedSubChannel; + use ln_dlc_node::node::rust_dlc_manager::subchannel::SignedSubChannel; + use ln_dlc_node::node::rust_dlc_manager::subchannel::SubChannel; + use ln_dlc_node::node::rust_dlc_manager::subchannel::SubChannelState; + use ln_dlc_node::node::rust_dlc_manager::ChannelId; + use secp256k1_zkp::EcdsaAdaptorSignature; + use std::str::FromStr; + use time::OffsetDateTime; + use trade::ContractSymbol; + use trade::Direction; + + #[test] + fn test_none_position_and_none_dlc_channel() { + let action = determine_sync_position_to_dlc_action(&None, &None); + assert_eq!(None, action); + } + + #[test] + fn test_some_position_and_none_dlc_channel() { + let action = determine_sync_position_to_dlc_action(&Some(&get_dummy_position()), &None); + assert_eq!(Some(SyncPositionToDlcAction::RemovePosition), action); + } + + #[test] + fn test_some_position_and_offchainclosed_dlc_channel() { + let action = determine_sync_position_to_dlc_action(&Some(&get_dummy_position()), &None); + assert_eq!(Some(SyncPositionToDlcAction::RemovePosition), action); + } + + #[test] + fn test_some_position_and_signed_dlc_channel() { + let action = determine_sync_position_to_dlc_action( + &Some(&get_dummy_position()), + &Some(&get_dummy_dlc_channel(SubChannelState::Signed( + get_dummy_signed_sub_channel(), + ))), + ); + assert_eq!(None, action); + } + + #[test] + fn test_some_position_and_closeoffered_dlc_channel() { + let action = determine_sync_position_to_dlc_action( + &Some(&get_dummy_position()), + &Some(&get_dummy_dlc_channel(SubChannelState::CloseOffered( + get_dummy_close_offered_sub_channel(), + ))), + ); + assert_eq!( + Some(SyncPositionToDlcAction::ContinueSubchannelProtocol), + action + ); + } + + #[test] + fn test_some_position_and_closeaccepted_dlc_channel() { + let action = determine_sync_position_to_dlc_action( + &Some(&get_dummy_position()), + &Some(&get_dummy_dlc_channel(SubChannelState::CloseAccepted( + get_dummy_close_accepted_sub_channel(), + ))), + ); + assert_eq!( + Some(SyncPositionToDlcAction::ContinueSubchannelProtocol), + action + ); + } + + #[test] + fn test_none_position_and_offchainclosed_dlc_channel() { + let action = determine_sync_position_to_dlc_action( + &None, + &Some(&get_dummy_dlc_channel(SubChannelState::OffChainClosed)), + ); + assert_eq!(None, action); + } + + #[test] + fn test_none_position_and_signed_dlc_channel() { + let action = determine_sync_position_to_dlc_action( + &None, + &Some(&get_dummy_dlc_channel(SubChannelState::Signed( + get_dummy_signed_sub_channel(), + ))), + ); + assert!(matches!( + action, + Some(SyncPositionToDlcAction::CreatePosition(_)) + )); + } + + #[test] + fn test_none_position_and_offered_dlc_channel() { + let action = determine_sync_position_to_dlc_action( + &None, + &Some(&get_dummy_dlc_channel(SubChannelState::Offered( + get_dummy_offered_sub_channel(), + ))), + ); + assert_eq!( + Some(SyncPositionToDlcAction::ContinueSubchannelProtocol), + action + ); + } + + #[test] + fn test_none_position_and_accepted_dlc_channel() { + let action = determine_sync_position_to_dlc_action( + &None, + &Some(&get_dummy_dlc_channel(SubChannelState::Accepted( + get_dummy_accepted_sub_channel(), + ))), + ); + assert_eq!( + Some(SyncPositionToDlcAction::ContinueSubchannelProtocol), + action + ); + } + + #[test] + fn test_none_position_and_finalized_dlc_channel() { + let action = determine_sync_position_to_dlc_action( + &None, + &Some(&get_dummy_dlc_channel(SubChannelState::Finalized( + get_dummy_signed_sub_channel(), + ))), + ); + assert_eq!( + Some(SyncPositionToDlcAction::ContinueSubchannelProtocol), + action + ); + } + + #[test] + fn test_none_position_and_other_dlc_channel_state() { + let action = determine_sync_position_to_dlc_action( + &None, + &Some(&get_dummy_dlc_channel(SubChannelState::OnChainClosed)), + ); + assert_eq!(None, action); + } + + #[test] + fn test_some_position_and_other_dlc_channel_state() { + let action = determine_sync_position_to_dlc_action( + &Some(&get_dummy_position()), + &Some(&get_dummy_dlc_channel(SubChannelState::OnChainClosed)), + ); + assert_eq!(Some(SyncPositionToDlcAction::RemovePosition), action); + } + + fn get_dummy_position() -> Position { + Position { + leverage: 0.0, + quantity: 0.0, + contract_symbol: ContractSymbol::BtcUsd, + direction: Direction::Long, + average_entry_price: 0.0, + liquidation_price: 0.0, + position_state: PositionState::Open, + collateral: 0, + expiry: OffsetDateTime::now_utc(), + updated: OffsetDateTime::now_utc(), + created: OffsetDateTime::now_utc(), + } + } + + fn get_dummy_dlc_channel(state: SubChannelState) -> SubChannel { + SubChannel { + channel_id: ChannelId::default(), + counter_party: get_dummy_pubkey(), + update_idx: 0, + state, + per_split_seed: None, + fee_rate_per_vb: 0, + own_base_points: PartyBasePoints { + own_basepoint: get_dummy_pubkey(), + revocation_basepoint: get_dummy_pubkey(), + publish_basepoint: get_dummy_pubkey(), + }, + counter_base_points: None, + fund_value_satoshis: 0, + original_funding_redeemscript: Default::default(), + is_offer: false, + own_fund_pk: get_dummy_pubkey(), + counter_fund_pk: get_dummy_pubkey(), + counter_party_secrets: CounterpartyCommitmentSecrets::new(), + } + } + + fn get_dummy_signed_sub_channel() -> SignedSubChannel { + SignedSubChannel { + own_per_split_point: get_dummy_pubkey(), + counter_per_split_point: get_dummy_pubkey(), + own_split_adaptor_signature: get_dummy_adaptor_signature(), + counter_split_adaptor_signature: get_dummy_adaptor_signature(), + split_tx: SplitTx { + transaction: get_dummy_tx(), + output_script: Default::default(), + }, + ln_glue_transaction: get_dummy_tx(), + counter_glue_signature: get_dummy_signature(), + ln_rollback: get_dummy_rollback_info(), + } + } + + fn get_dummy_close_offered_sub_channel() -> CloseOfferedSubChannel { + CloseOfferedSubChannel { + signed_subchannel: get_dummy_signed_sub_channel(), + offer_balance: 0, + accept_balance: 0, + is_offer: false, + } + } + + fn get_dummy_close_accepted_sub_channel() -> CloseAcceptedSubChannel { + CloseAcceptedSubChannel { + signed_subchannel: get_dummy_signed_sub_channel(), + own_balance: 0, + counter_balance: 0, + ln_rollback: get_dummy_rollback_info(), + commitment_transactions: vec![], + } + } + + fn get_dummy_offered_sub_channel() -> OfferedSubChannel { + OfferedSubChannel { + per_split_point: get_dummy_pubkey(), + } + } + + fn get_dummy_accepted_sub_channel() -> AcceptedSubChannel { + AcceptedSubChannel { + offer_per_split_point: get_dummy_pubkey(), + accept_per_split_point: get_dummy_pubkey(), + split_tx: SplitTx { + transaction: get_dummy_tx(), + output_script: Default::default(), + }, + ln_glue_transaction: get_dummy_tx(), + ln_rollback: get_dummy_rollback_info(), + commitment_transactions: vec![], + } + } + + fn get_dummy_pubkey() -> PublicKey { + PublicKey::from_str("02bd998ebd176715fe92b7467cf6b1df8023950a4dd911db4c94dfc89cc9f5a655") + .unwrap() + } + + fn get_dummy_tx() -> Transaction { + Transaction { + version: 1, + lock_time: PackedLockTime::ZERO, + input: vec![], + output: vec![], + } + } + + fn get_dummy_signature() -> Signature { + Signature::from_str( + "304402202f2545f818a5dac9311157d75065156b141e5a6437e817d1d75f9fab084e46940220757bb6f0916f83b2be28877a0d6b05c45463794e3c8c99f799b774443575910d", + ).unwrap() + } + + fn get_dummy_adaptor_signature() -> EcdsaAdaptorSignature { + "03424d14a5471c048ab87b3b83f6085d125d5864249ae4297a57c84e74710bb6730223f325042fce535d040fee52ec13231bf709ccd84233c6944b90317e62528b2527dff9d659a96db4c99f9750168308633c1867b70f3a18fb0f4539a1aecedcd1fc0148fc22f36b6303083ece3f872b18e35d368b3958efe5fb081f7716736ccb598d269aa3084d57e1855e1ea9a45efc10463bbf32ae378029f5763ceb40173f" + .parse() + .unwrap() + } + + fn get_dummy_rollback_info() -> LnRollBackInfo { + LnRollBackInfo { + channel_value_satoshis: 0, + value_to_self_msat: 0, + funding_outpoint: OutPoint { + txid: get_dummy_tx().txid(), + index: 0, + }, + } + } +} diff --git a/mobile/native/src/trade/position/handler.rs b/mobile/native/src/trade/position/handler.rs index f15f388b3..23a27fdea 100644 --- a/mobile/native/src/trade/position/handler.rs +++ b/mobile/native/src/trade/position/handler.rs @@ -209,7 +209,7 @@ pub fn update_position_after_dlc_creation( } /// Delete a position after closing a DLC channel. -pub fn update_position_after_dlc_closure(filled_order: Order) -> Result<()> { +pub fn update_position_after_dlc_closure(filled_order: Option) -> Result<()> { tracing::debug!(?filled_order, "Removing position after DLC channel closure"); if db::get_positions()?.is_empty() { From ec39bcf8ab19b02f6a8624a434cb7e2b80155372 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Fri, 15 Sep 2023 09:09:29 +0200 Subject: [PATCH 3/5] refactor: OrderSubmissionStatusDialog to TaskStatusDialog The `OrderSubmissionStatusDialog` is actually useful for any kind of task showing a pending, failed or success state. --- .../common/recover_dlc_change_notifier.dart | 18 +------- .../task_status_dialog.dart} | 44 +++++++------------ .../trade/async_order_change_notifier.dart | 14 +++--- .../trade/rollover_change_notifier.dart | 18 +------- mobile/lib/features/trade/trade_screen.dart | 41 +++++++---------- 5 files changed, 43 insertions(+), 92 deletions(-) rename mobile/lib/{features/trade/order_submission_status_dialog.dart => common/task_status_dialog.dart} (68%) diff --git a/mobile/lib/common/recover_dlc_change_notifier.dart b/mobile/lib/common/recover_dlc_change_notifier.dart index 52a8f8a8a..99a12bb33 100644 --- a/mobile/lib/common/recover_dlc_change_notifier.dart +++ b/mobile/lib/common/recover_dlc_change_notifier.dart @@ -4,7 +4,7 @@ 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/features/trade/order_submission_status_dialog.dart'; +import 'package:get_10101/common/task_status_dialog.dart'; import 'package:provider/provider.dart'; class RecoverDlcChangeNotifier extends ChangeNotifier implements Subscriber { @@ -28,22 +28,8 @@ class RecoverDlcChangeNotifier extends ChangeNotifier implements Subscriber { context: shellNavigatorKey.currentContext!, builder: (context) { TaskStatus status = context.watch().taskStatus; - - // todo(holzeis): Reusing the order submission status dialog is not nice, but it's actually suitable for any task execution that has pending, - // failed and success states. We may should consider renaming this dialog for its more generic purpose. - OrderSubmissionStatusDialogType type = OrderSubmissionStatusDialogType.pendingSubmit; - switch (status) { - case TaskStatus.pending: - type = OrderSubmissionStatusDialogType.successfulSubmit; - case TaskStatus.failed: - type = OrderSubmissionStatusDialogType.failedFill; - case TaskStatus.success: - type = OrderSubmissionStatusDialogType.filled; - } - late Widget content = const Text("Recovering your dlc channel"); - - return OrderSubmissionStatusDialog(title: "Catching up!", type: type, content: content); + return TaskStatusDialog(title: "Catching up!", status: status, content: content); }, ); } else { diff --git a/mobile/lib/features/trade/order_submission_status_dialog.dart b/mobile/lib/common/task_status_dialog.dart similarity index 68% rename from mobile/lib/features/trade/order_submission_status_dialog.dart rename to mobile/lib/common/task_status_dialog.dart index c6dd4ae63..c25febc73 100644 --- a/mobile/lib/features/trade/order_submission_status_dialog.dart +++ b/mobile/lib/common/task_status_dialog.dart @@ -1,37 +1,30 @@ import 'package:confetti/confetti.dart'; import 'package:flutter/material.dart'; +import 'package:get_10101/common/domain/background_task.dart'; import 'package:go_router/go_router.dart'; -enum OrderSubmissionStatusDialogType { - pendingSubmit, - successfulSubmit, - filled, - failedFill, - failedSubmit -} - -class OrderSubmissionStatusDialog extends StatefulWidget { +class TaskStatusDialog extends StatefulWidget { final String title; - final OrderSubmissionStatusDialogType type; + final TaskStatus status; final Widget content; final String buttonText; final EdgeInsets insetPadding; final String navigateToRoute; - const OrderSubmissionStatusDialog( + const TaskStatusDialog( {super.key, required this.title, - required this.type, + required this.status, required this.content, this.buttonText = "Close", this.insetPadding = const EdgeInsets.all(50), this.navigateToRoute = ""}); @override - State createState() => _OrderSubmissionStatusDialog(); + State createState() => _TaskStatusDialog(); } -class _OrderSubmissionStatusDialog extends State { +class _TaskStatusDialog extends State { late final ConfettiController _confettiController; @override @@ -48,8 +41,7 @@ class _OrderSubmissionStatusDialog extends State { @override Widget build(BuildContext context) { - bool isPending = widget.type == OrderSubmissionStatusDialogType.successfulSubmit || - widget.type == OrderSubmissionStatusDialogType.pendingSubmit; + bool isPending = widget.status == TaskStatus.pending; WidgetsBinding.instance.addPostFrameCallback((_) { _confettiController.play(); @@ -67,18 +59,16 @@ class _OrderSubmissionStatusDialog extends State { AlertDialog dialog = AlertDialog( icon: (() { - switch (widget.type) { - case OrderSubmissionStatusDialogType.pendingSubmit: - case OrderSubmissionStatusDialogType.successfulSubmit: + switch (widget.status) { + case TaskStatus.pending: return const Center( child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator())); - case OrderSubmissionStatusDialogType.failedFill: - case OrderSubmissionStatusDialogType.failedSubmit: + case TaskStatus.failed: return const Icon( Icons.cancel, color: Colors.red, ); - case OrderSubmissionStatusDialogType.filled: + case TaskStatus.success: return Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, @@ -101,14 +91,12 @@ class _OrderSubmissionStatusDialog extends State { } })(), title: Text("${widget.title} ${(() { - switch (widget.type) { - case OrderSubmissionStatusDialogType.pendingSubmit: - case OrderSubmissionStatusDialogType.successfulSubmit: + switch (widget.status) { + case TaskStatus.pending: return "Pending"; - case OrderSubmissionStatusDialogType.filled: + case TaskStatus.success: return "Success"; - case OrderSubmissionStatusDialogType.failedSubmit: - case OrderSubmissionStatusDialogType.failedFill: + case TaskStatus.failed: return "Failure"; } })()}"), diff --git a/mobile/lib/features/trade/async_order_change_notifier.dart b/mobile/lib/features/trade/async_order_change_notifier.dart index eccb6930d..16dd5e9a7 100644 --- a/mobile/lib/features/trade/async_order_change_notifier.dart +++ b/mobile/lib/features/trade/async_order_change_notifier.dart @@ -4,9 +4,9 @@ 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/features/trade/application/order_service.dart'; import 'package:get_10101/features/trade/domain/order.dart'; -import 'package:get_10101/features/trade/order_submission_status_dialog.dart'; import 'package:provider/provider.dart'; class AsyncOrderChangeNotifier extends ChangeNotifier implements Subscriber { @@ -39,16 +39,16 @@ class AsyncOrderChangeNotifier extends ChangeNotifier implements Subscriber { builder: (context) { Order? asyncOrder = context.watch().asyncOrder; - OrderSubmissionStatusDialogType type = OrderSubmissionStatusDialogType.pendingSubmit; + TaskStatus status = TaskStatus.pending; switch (asyncOrder?.state) { case OrderState.open: - type = OrderSubmissionStatusDialogType.successfulSubmit; + status = TaskStatus.pending; case OrderState.failed: - type = OrderSubmissionStatusDialogType.failedFill; + status = TaskStatus.failed; case OrderState.filled: - type = OrderSubmissionStatusDialogType.filled; + status = TaskStatus.success; case null: - type = OrderSubmissionStatusDialogType.pendingSubmit; + status = TaskStatus.pending; } late Widget content; @@ -60,7 +60,7 @@ class AsyncOrderChangeNotifier extends ChangeNotifier implements Subscriber { content = Container(); } - return OrderSubmissionStatusDialog(title: "Catching up!", type: type, content: content); + return TaskStatusDialog(title: "Catching up!", status: status, content: content); }, ); } else if (event is bridge.Event_OrderUpdateNotification) { diff --git a/mobile/lib/features/trade/rollover_change_notifier.dart b/mobile/lib/features/trade/rollover_change_notifier.dart index 30edcf00f..e72ca4623 100644 --- a/mobile/lib/features/trade/rollover_change_notifier.dart +++ b/mobile/lib/features/trade/rollover_change_notifier.dart @@ -4,7 +4,7 @@ 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/features/trade/order_submission_status_dialog.dart'; +import 'package:get_10101/common/task_status_dialog.dart'; import 'package:provider/provider.dart'; class RolloverChangeNotifier extends ChangeNotifier implements Subscriber { @@ -29,22 +29,8 @@ class RolloverChangeNotifier extends ChangeNotifier implements Subscriber { context: shellNavigatorKey.currentContext!, builder: (context) { TaskStatus status = context.watch().taskStatus; - - // todo(holzeis): Reusing the order submission status dialog is not nice, but it's actually suitable for any task execution that has pending, - // failed and success states. We may should consider renaming this dialog for its more generic purpose. - OrderSubmissionStatusDialogType type = OrderSubmissionStatusDialogType.pendingSubmit; - switch (status) { - case TaskStatus.pending: - type = OrderSubmissionStatusDialogType.successfulSubmit; - case TaskStatus.failed: - type = OrderSubmissionStatusDialogType.failedFill; - case TaskStatus.success: - type = OrderSubmissionStatusDialogType.filled; - } - late Widget content = const Text("Rolling over your position"); - - return OrderSubmissionStatusDialog(title: "Catching up!", type: type, content: content); + return TaskStatusDialog(title: "Catching up!", status: status, content: content); }, ); } else { diff --git a/mobile/lib/features/trade/trade_screen.dart b/mobile/lib/features/trade/trade_screen.dart index 6a2156798..a74713890 100644 --- a/mobile/lib/features/trade/trade_screen.dart +++ b/mobile/lib/features/trade/trade_screen.dart @@ -1,5 +1,10 @@ +import 'dart:io' show Platform; + +import 'package:candlesticks/candlesticks.dart'; import 'package:flutter/material.dart'; +import 'package:get_10101/common/domain/background_task.dart'; import 'package:get_10101/common/domain/model.dart'; +import 'package:get_10101/common/task_status_dialog.dart'; import 'package:get_10101/common/value_data_row.dart'; import 'package:get_10101/features/trade/candlestick_change_notifier.dart'; import 'package:get_10101/features/trade/contract_symbol_icon.dart'; @@ -13,19 +18,15 @@ import 'package:get_10101/features/trade/position_change_notifier.dart'; import 'package:get_10101/features/trade/position_list_item.dart'; import 'package:get_10101/features/trade/submit_order_change_notifier.dart'; import 'package:get_10101/features/trade/trade_bottom_sheet.dart'; -import 'package:candlesticks/candlesticks.dart'; import 'package:get_10101/features/trade/trade_bottom_sheet_confirmation.dart'; import 'package:get_10101/features/trade/trade_tabs.dart'; import 'package:get_10101/features/trade/trade_theme.dart'; import 'package:get_10101/features/trade/trade_value_change_notifier.dart'; +import 'package:get_10101/util/constants.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; -import 'package:get_10101/util/constants.dart'; import 'package:share_plus/share_plus.dart'; import 'package:social_share/social_share.dart'; -import 'dart:io' show Platform; - -import 'order_submission_status_dialog.dart'; class TradeScreen extends StatelessWidget { static const route = "/trade"; @@ -73,31 +74,21 @@ class TradeScreen extends StatelessWidget { switch (state) { case PendingOrderState.submitting: - return OrderSubmissionStatusDialog( - title: "Submit Order", - type: OrderSubmissionStatusDialogType.pendingSubmit, - content: body); + return TaskStatusDialog( + title: "Submit Order", status: TaskStatus.pending, content: body); case PendingOrderState.submittedSuccessfully: - return OrderSubmissionStatusDialog( - title: "Fill Order", - type: OrderSubmissionStatusDialogType.successfulSubmit, - content: body); + return TaskStatusDialog( + title: "Fill Order", status: TaskStatus.success, content: body); case PendingOrderState.submissionFailed: // TODO: This failure case has to be handled differently; are we planning to show orders that failed to submit in the order history? - return OrderSubmissionStatusDialog( - title: "Submit Order", - type: OrderSubmissionStatusDialogType.failedSubmit, - content: body); + return TaskStatusDialog( + title: "Submit Order", status: TaskStatus.failed, content: body); case PendingOrderState.orderFilled: - return OrderSubmissionStatusDialog( - title: "Fill Order", - type: OrderSubmissionStatusDialogType.filled, - content: body); + return TaskStatusDialog( + title: "Fill Order", status: TaskStatus.success, content: body); case PendingOrderState.orderFailed: - return OrderSubmissionStatusDialog( - title: "Fill Order", - type: OrderSubmissionStatusDialogType.failedFill, - content: body); + return TaskStatusDialog( + title: "Fill Order", status: TaskStatus.failed, content: body); } }, ); From 270f6c0c7f6b51a8396f052f8384840353ab9589 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Fri, 15 Sep 2023 09:19:48 +0200 Subject: [PATCH 4/5] chore: Set rollover and recover dialog to failed after timeout --- mobile/lib/common/recover_dlc_change_notifier.dart | 6 ++++++ mobile/lib/features/trade/rollover_change_notifier.dart | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/mobile/lib/common/recover_dlc_change_notifier.dart b/mobile/lib/common/recover_dlc_change_notifier.dart index 99a12bb33..425b6d315 100644 --- a/mobile/lib/common/recover_dlc_change_notifier.dart +++ b/mobile/lib/common/recover_dlc_change_notifier.dart @@ -32,6 +32,12 @@ class RecoverDlcChangeNotifier extends ChangeNotifier implements Subscriber { return TaskStatusDialog(title: "Catching up!", status: status, content: content); }, ); + + // setting the task status to failed after a timeout of 30 seconds. + Future.delayed(const Duration(seconds: 30), () { + taskStatus = TaskStatus.failed; + notifyListeners(); + }); } else { // notify dialog about changed task status notifyListeners(); diff --git a/mobile/lib/features/trade/rollover_change_notifier.dart b/mobile/lib/features/trade/rollover_change_notifier.dart index e72ca4623..d101df052 100644 --- a/mobile/lib/features/trade/rollover_change_notifier.dart +++ b/mobile/lib/features/trade/rollover_change_notifier.dart @@ -33,6 +33,12 @@ class RolloverChangeNotifier extends ChangeNotifier implements Subscriber { return TaskStatusDialog(title: "Catching up!", status: status, content: content); }, ); + + // setting the task status to failed after a timeout of 30 seconds. + Future.delayed(const Duration(seconds: 30), () { + taskStatus = TaskStatus.failed; + notifyListeners(); + }); } else { // notify dialog about changed task status notifyListeners(); From f33faa536954e8a3dffc783402266deda358bf9f Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Fri, 15 Sep 2023 10:12:10 +0200 Subject: [PATCH 5/5] chore: Capitalize todos and fixmes --- coordinator/src/node.rs | 2 +- coordinator/src/node/expired_positions.rs | 2 +- coordinator/src/orderbook/trading.rs | 8 ++++---- mobile/native/src/ln_dlc/node.rs | 6 +++--- mobile/native/src/ln_dlc/sync_position_to_dlc.rs | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/coordinator/src/node.rs b/coordinator/src/node.rs index 6fd82c905..a6f8e843e 100644 --- a/coordinator/src/node.rs +++ b/coordinator/src/node.rs @@ -494,7 +494,7 @@ impl Node { .map(Message::SubChannel), }; - // todo(holzeis): It would be nice if dlc messages are also propagated via events, so the + // TODO(holzeis): It would be nice if dlc messages are also propagated via events, so the // receiver can decide what events to process and we can skip this component specific logic // here. if let Message::Channel(ChannelMessage::RenewFinalize(r)) = &msg { diff --git a/coordinator/src/node/expired_positions.rs b/coordinator/src/node/expired_positions.rs index e669e1fbc..e575bb4a3 100644 --- a/coordinator/src/node/expired_positions.rs +++ b/coordinator/src/node/expired_positions.rs @@ -77,7 +77,7 @@ pub async fn close(node: Node, trading_sender: mpsc::Sender) -> let new_order = NewOrder { id: uuid::Uuid::new_v4(), contract_symbol: position.contract_symbol, - // todo(holzeis): we should not have to set the price for a market order. we propably + // TODO(holzeis): we should not have to set the price for a market order. we propably // need separate models for a limit and a market order. price: Decimal::ZERO, quantity: Decimal::try_from(position.quantity).expect("to fit into decimal"), diff --git a/coordinator/src/orderbook/trading.rs b/coordinator/src/orderbook/trading.rs index a54ce06a2..8d1f2cb35 100644 --- a/coordinator/src/orderbook/trading.rs +++ b/coordinator/src/orderbook/trading.rs @@ -121,7 +121,7 @@ pub fn start( } /// Processes a new limit and market order -/// todo(holzeis): The limit and market order models should be separated so we can process the +/// TODO(holzeis): The limit and market order models should be separated so we can process the /// models independently. /// /// @@ -144,7 +144,7 @@ async fn process_new_order( // before processing any match we set all expired limit orders to failed, to ensure the do // not get matched. - // todo(holzeis): orders should probably do not have an expiry, but should either be + // TODO(holzeis): orders should probably do not have an expiry, but should either be // replaced or deleted if not wanted anymore. orders::set_expired_limit_orders_to_failed(conn)?; @@ -176,7 +176,7 @@ async fn process_new_order( let matched_orders = match match_order(&order, opposite_direction_orders) { Ok(Some(matched_orders)) => matched_orders, Ok(None) => { - // todo(holzeis): Currently we still respond to the user immediately if there + // TODO(holzeis): Currently we still respond to the user immediately if there // has been a match or not, that's the reason why we also // have to set the order to failed here. But actually we // could keep the order until either expired or a @@ -220,7 +220,7 @@ async fn process_new_order( } Err(e) => { tracing::warn!(%trader_id, order_id, "{e:#}"); - // todo(holzeis): send push notification to user + // TODO(holzeis): send push notification to user if order.order_type == OrderType::Limit { // FIXME: The maker is currently not connected to the web socket so we diff --git a/mobile/native/src/ln_dlc/node.rs b/mobile/native/src/ln_dlc/node.rs index c66e46106..3d27cade8 100644 --- a/mobile/native/src/ln_dlc/node.rs +++ b/mobile/native/src/ln_dlc/node.rs @@ -200,7 +200,7 @@ impl Node { } }; - // todo(holzeis): It would be nice if dlc messages are also propagated via events, so the + // TODO(holzeis): It would be nice if dlc messages are also propagated via events, so the // receiver can decide what events to process and we can skip this component specific logic // here. if let Message::Channel(channel_message) = &msg { @@ -260,7 +260,7 @@ impl Node { // received by the UI at the moment indicating that the dialog can be closed. // If the dialog is not open, this event would be simply ignored by the UI. // - // fixme(holzeis): We should not require that event and align the UI handling with + // FIXME(holzeis): We should not require that event and align the UI handling with // waiting for an order execution in the happy case with waiting for an // order execution after an in between restart. For now it was the easiest // to go parallel to that implementation so that we don't have to touch it. @@ -307,7 +307,7 @@ impl Node { // received by the UI at the moment indicating that the dialog can be closed. // If the dialog is not open, this event would be simply ignored by the UI. // - // fixme(holzeis): We should not require that event and align the UI handling with + // FIXME(holzeis): We should not require that event and align the UI handling with // waiting for an order execution in the happy case with waiting for an // order execution after an in between restart. For now it was the easiest // to go parallel to that implementation so that we don't have to touch it. diff --git a/mobile/native/src/ln_dlc/sync_position_to_dlc.rs b/mobile/native/src/ln_dlc/sync_position_to_dlc.rs index 18c79f822..976a60db2 100644 --- a/mobile/native/src/ln_dlc/sync_position_to_dlc.rs +++ b/mobile/native/src/ln_dlc/sync_position_to_dlc.rs @@ -88,7 +88,7 @@ impl Node { /// Also triggers the `periodic_check` to process any actions that might have been created after /// the channel reestablishment. /// - /// fixme(holzeis): We currently use different events for show the recovery of a dlc and the + /// FIXME(holzeis): We currently use different events for show the recovery of a dlc and the /// waiting for an order execution in the happy case (without an restart in between). Those /// events and dialogs should be aligned. async fn recover_dlc(&self) -> Result<()> { @@ -97,7 +97,7 @@ impl Node { BackgroundTask::RecoverDlc(TaskStatus::Pending), )); - // fixme(holzeis): We are manually calling the periodic check here to speed up the + // HACK(holzeis): We are manually calling the periodic check here to speed up the // processing of pending actions. // Note, this might not speed up the process, as the coordinator might have to resend a // message to continue the protocol. This should be fixed in `rust-dlc` and any