diff --git a/coordinator/src/lib.rs b/coordinator/src/lib.rs index dbe976ecf..43e66e899 100644 --- a/coordinator/src/lib.rs +++ b/coordinator/src/lib.rs @@ -27,6 +27,7 @@ pub mod cli; pub mod db; pub mod dlc_handler; pub mod dlc_protocol; +mod emergency_kit; mod leaderboard; pub mod logger; pub mod message; diff --git a/mobile/lib/common/settings/emergency_kit_screen.dart b/mobile/lib/common/settings/emergency_kit_screen.dart index 4026d871b..e928e3700 100644 --- a/mobile/lib/common/settings/emergency_kit_screen.dart +++ b/mobile/lib/common/settings/emergency_kit_screen.dart @@ -67,159 +67,183 @@ class _EmergencyKitScreenState extends State { ) ], )), - const SizedBox(height: 30), - EmergencyKitButton( - icon: const Icon(FontAwesomeIcons.broom), - title: "Cleanup filling orders", - onPressed: () async { - final messenger = ScaffoldMessenger.of(context); - final orderChangeNotifier = context.read(); - final goRouter = GoRouter.of(context); + ])), + const SizedBox(height: 10), + Expanded( + child: SingleChildScrollView( + child: Container( + margin: const EdgeInsets.only(left: 10, right: 10, bottom: 30), + child: Column(children: [ + EmergencyKitButton( + icon: const Icon(FontAwesomeIcons.broom), + title: "Cleanup filling orders", + onPressed: () async { + final messenger = ScaffoldMessenger.of(context); + final orderChangeNotifier = context.read(); + final goRouter = GoRouter.of(context); - try { - await rust.api.setFillingOrdersToFailed(); - await orderChangeNotifier.initialize(); - showSnackBar(messenger, "Successfully set filling orders to failed"); - } catch (e) { - showSnackBar( - messenger, "Failed to set filling orders to failed. Error: $e"); - } + try { + await rust.api.setFillingOrdersToFailed(); + await orderChangeNotifier.initialize(); + showSnackBar(messenger, "Successfully set filling orders to failed"); + } catch (e) { + showSnackBar( + messenger, "Failed to set filling orders to failed. Error: $e"); + } - goRouter.pop(); - }), - const SizedBox(height: 30), - Row( - children: [ - Expanded( - child: TextField( - onChanged: (value) => _dlcChannelId = value, - decoration: InputDecoration( - border: const OutlineInputBorder(), - hintText: "Copy from Settings > Channel", - labelText: "Dlc Channel Id", - labelStyle: const TextStyle(color: Colors.black87), - filled: true, - fillColor: Colors.white, - errorStyle: TextStyle( - color: Colors.red[900], - ), + goRouter.pop(); + }), + const SizedBox(height: 30), + Row( + children: [ + Expanded( + child: TextField( + onChanged: (value) => _dlcChannelId = value, + decoration: InputDecoration( + border: const OutlineInputBorder(), + hintText: "Copy from Settings > Channel", + labelText: "Dlc Channel Id", + labelStyle: const TextStyle(color: Colors.black87), + filled: true, + fillColor: Colors.white, + errorStyle: TextStyle( + color: Colors.red[900], ), - )), - IconButton( - onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text("Are you sure?"), - content: const Text( - "Performing that action may break your app state and should only get executed after consulting with the 10101 Team."), - actions: [ - TextButton( - onPressed: () => GoRouter.of(context).pop(), - child: const Text('No'), - ), - TextButton( - onPressed: () async { - final messenger = ScaffoldMessenger.of(context); - final goRouter = GoRouter.of(context); + ), + )), + IconButton( + onPressed: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text("Are you sure?"), + content: const Text( + "Performing that action may break your app state and should only get executed after consulting with the 10101 Team."), + actions: [ + TextButton( + onPressed: () => GoRouter.of(context).pop(), + child: const Text('No'), + ), + TextButton( + onPressed: () async { + final messenger = ScaffoldMessenger.of(context); + final goRouter = GoRouter.of(context); - try { - await dlcChannelChangeNotifier - .deleteDlcChannel(_dlcChannelId ?? ""); - showSnackBar(messenger, - "Successfully deleted dlc channel with id $_dlcChannelId"); - } catch (error) { - showSnackBar(messenger, "$error"); - } - goRouter.pop(); - }, - child: const Text('Yes'), - ), - ]); - }); - }, - icon: const Icon( - Icons.delete, - size: 32, - )) - ], - ), - const SizedBox(height: 30), - EmergencyKitButton( - icon: const Icon(FontAwesomeIcons.broom), - title: "Delete position", - onPressed: () async { - final messenger = ScaffoldMessenger.of(context); - final goRouter = GoRouter.of(context); + try { + await dlcChannelChangeNotifier + .deleteDlcChannel(_dlcChannelId ?? ""); + showSnackBar(messenger, + "Successfully deleted dlc channel with id $_dlcChannelId"); + } catch (error) { + showSnackBar(messenger, "$error"); + } + goRouter.pop(); + }, + child: const Text('Yes'), + ), + ]); + }); + }, + icon: const Icon( + Icons.delete, + size: 32, + )) + ], + ), + const SizedBox(height: 30), + EmergencyKitButton( + icon: const Icon(FontAwesomeIcons.broom), + title: "Delete position", + onPressed: () async { + final messenger = ScaffoldMessenger.of(context); + final goRouter = GoRouter.of(context); - try { - await rust.api.deletePosition(); - showSnackBar(messenger, "Successfully deleted position"); - } catch (e) { - showSnackBar(messenger, "Failed to delete position. Error: $e"); - } + try { + await rust.api.deletePosition(); + showSnackBar(messenger, "Successfully deleted position"); + } catch (e) { + showSnackBar(messenger, "Failed to delete position. Error: $e"); + } - goRouter.pop(); - }), - const SizedBox(height: 30), - EmergencyKitButton( - icon: const Icon(FontAwesomeIcons.broom), - title: "Resend SettleFinalize Message", - onPressed: () async { - final messenger = ScaffoldMessenger.of(context); - final goRouter = GoRouter.of(context); + goRouter.pop(); + }), + const SizedBox(height: 30), + EmergencyKitButton( + icon: const Icon(FontAwesomeIcons.broom), + title: "Recreate position", + onPressed: () async { + final messenger = ScaffoldMessenger.of(context); + final goRouter = GoRouter.of(context); - try { - await rust.api.resendSettleFinalizeMessage(); - showSnackBar(messenger, "Successfully resend SettleFinalize message"); - } catch (e) { - showSnackBar( - messenger, "Failed to resend SettleFinalize message. Error: $e"); - } + try { + await rust.api.recreatePosition(); + showSnackBar(messenger, "Successfully recreated position"); + } catch (e) { + showSnackBar(messenger, "Failed to recreate position. Error: $e"); + } - goRouter.pop(); - }), - const SizedBox(height: 30), - EmergencyKitButton( - icon: const Icon(FontAwesomeIcons.backwardStep), - title: "Rollback channel state", - onPressed: () async { - final messenger = ScaffoldMessenger.of(context); - final orderChangeNotifier = context.read(); - final goRouter = GoRouter.of(context); + goRouter.pop(); + }), + const SizedBox(height: 30), + EmergencyKitButton( + icon: const Icon(FontAwesomeIcons.broom), + title: "Resend SettleFinalize Message", + onPressed: () async { + final messenger = ScaffoldMessenger.of(context); + final goRouter = GoRouter.of(context); - try { - await rust.api.rollBackChannelState(); - await orderChangeNotifier.initialize(); - showSnackBar(messenger, "Successfully rolled back channel state"); - } catch (e) { - showSnackBar(messenger, "Failed to rollback channel state. Error: $e"); - } + try { + await rust.api.resendSettleFinalizeMessage(); + showSnackBar(messenger, "Successfully resend SettleFinalize message"); + } catch (e) { + showSnackBar( + messenger, "Failed to resend SettleFinalize message. Error: $e"); + } - goRouter.pop(); - }), - const SizedBox(height: 30), - Visibility( - visible: config.network == "regtest", - child: EmergencyKitButton( - icon: const Icon(FontAwesomeIcons.broom), - title: "Reset answered poll cache", - onPressed: () { - final messenger = ScaffoldMessenger.of(context); - final goRouter = GoRouter.of(context); + goRouter.pop(); + }), + const SizedBox(height: 30), + EmergencyKitButton( + icon: const Icon(FontAwesomeIcons.backwardStep), + title: "Rollback channel state", + onPressed: () async { + final messenger = ScaffoldMessenger.of(context); + final orderChangeNotifier = context.read(); + final goRouter = GoRouter.of(context); - try { - rust.api.resetAllAnsweredPolls(); - showSnackBar(messenger, - "Successfully reset answered polls - You can now answer them again"); - } catch (e) { - showSnackBar(messenger, "Failed to reset answered polls: $e"); - } + try { + await rust.api.rollBackChannelState(); + await orderChangeNotifier.initialize(); + showSnackBar(messenger, "Successfully rolled back channel state"); + } catch (e) { + showSnackBar(messenger, "Failed to rollback channel state. Error: $e"); + } - goRouter.pop(); - })), - ])), + goRouter.pop(); + }), + const SizedBox(height: 30), + Visibility( + visible: config.network == "regtest", + child: EmergencyKitButton( + icon: const Icon(FontAwesomeIcons.broom), + title: "Reset answered poll cache", + onPressed: () { + final messenger = ScaffoldMessenger.of(context); + final goRouter = GoRouter.of(context); + + try { + rust.api.resetAllAnsweredPolls(); + showSnackBar(messenger, + "Successfully reset answered polls - You can now answer them again"); + } catch (e) { + showSnackBar(messenger, "Failed to reset answered polls: $e"); + } + + goRouter.pop(); + })), + ]), + ))) ], ), ), diff --git a/mobile/native/src/api.rs b/mobile/native/src/api.rs index c49011b44..3b17f5ef5 100644 --- a/mobile/native/src/api.rs +++ b/mobile/native/src/api.rs @@ -389,6 +389,10 @@ pub fn delete_position() -> Result<()> { emergency_kit::delete_position() } +pub fn recreate_position() -> Result<()> { + emergency_kit::recreate_position() +} + pub fn resend_settle_finalize_message() -> Result<()> { emergency_kit::resend_settle_finalize_message() } diff --git a/mobile/native/src/db/mod.rs b/mobile/native/src/db/mod.rs index c9184b7a2..7f7f5b665 100644 --- a/mobile/native/src/db/mod.rs +++ b/mobile/native/src/db/mod.rs @@ -231,6 +231,21 @@ pub fn maybe_get_open_orders() -> Result> { Ok(orders) } +pub fn get_last_failed_order() -> Result> { + let mut db = connection()?; + + let mut orders = Order::get_by_state(OrderState::Failed, &mut db)?; + + orders.sort_by(|a, b| b.creation_timestamp.cmp(&a.creation_timestamp)); + + let order = match orders.first() { + Some(order) => Some(order.clone().try_into()?), + None => None, + }; + + Ok(order) +} + /// Return an [`Order`] that is currently in [`OrderState::Filling`]. pub fn get_order_in_filling() -> Result> { let mut db = connection()?; diff --git a/mobile/native/src/emergency_kit.rs b/mobile/native/src/emergency_kit.rs index eda51e800..16a6ef020 100644 --- a/mobile/native/src/emergency_kit.rs +++ b/mobile/native/src/emergency_kit.rs @@ -1,3 +1,4 @@ +use crate::calculations::calculate_liquidation_price; use crate::config; use crate::db; use crate::db::connection; @@ -5,9 +6,15 @@ use crate::event; use crate::event::EventInternal; use crate::ln_dlc; use crate::state::get_node; +use crate::trade::position::Position; +use crate::trade::position::PositionState; +use anyhow::bail; use anyhow::ensure; +use anyhow::Context; use anyhow::Result; use bitcoin::secp256k1::SecretKey; +use dlc_manager::channel::signed_channel::SignedChannelState; +use dlc_manager::contract::Contract; use dlc_manager::DlcChannelId; use dlc_manager::Signer; use dlc_messages::channel::SettleFinalize; @@ -17,6 +24,7 @@ use hex::FromHex; use lightning::ln::chan_utils::build_commitment_secret; use ln_dlc_node::bitcoin_conversion::to_secp_sk_29; use ln_dlc_node::node::event::NodeEvent; +use time::OffsetDateTime; use trade::ContractSymbol; pub fn set_filling_orders_to_failed() -> Result<()> { @@ -44,6 +52,90 @@ pub fn delete_position() -> Result<()> { Ok(()) } +pub fn recreate_position() -> Result<()> { + tracing::warn!("Executing emergency kit! Recreating position!"); + let node = get_node(); + let counterparty = config::get_coordinator_info().pubkey; + let channel = node.inner.get_signed_channel_by_trader_id(counterparty)?; + + ensure!( + matches!(channel.state, SignedChannelState::Established { .. }), + "A position can only be recreated from an established signed channel state" + ); + + let positions = db::get_positions()?; + let position = positions.first(); + ensure!( + position.is_none(), + "Can't recreate a position if there is already a position" + ); + + let order = db::get_last_failed_order()?.context("Couldn't find last failed order!")?; + let average_entry_price = order.execution_price().context("Missing execution price")?; + + tracing::debug!("Creating position from established signed dlc channel and last failed order"); + + let contract_id = channel.get_contract_id().context("Missing contract id")?; + + let contract = node + .inner + .get_contract_by_id(&contract_id)? + .context("Missing contract")?; + + let (collateral, expiry) = match contract { + Contract::Signed(contract) | Contract::Confirmed(contract) => { + let trader_reserve = node + .inner + .get_dlc_channel_usable_balance(&channel.channel_id)?; + + let oracle_event = &contract + .accepted_contract + .offered_contract + .contract_info + .first() + .context("missing contract info")? + .oracle_announcements + .first() + .context("missing oracle info")? + .oracle_event; + + let expiry_timestamp = + OffsetDateTime::from_unix_timestamp(oracle_event.event_maturity_epoch as i64)?; + + ( + contract.accepted_contract.accept_params.collateral - trader_reserve.to_sat(), + expiry_timestamp, + ) + } + _ => { + bail!("Contract in unexpected state: {:?}", contract); + } + }; + + let liquidation_price = + calculate_liquidation_price(average_entry_price, order.leverage, order.direction); + + let position = Position { + leverage: order.leverage, + quantity: order.quantity, + contract_symbol: order.contract_symbol, + direction: order.direction, + average_entry_price, + liquidation_price, + position_state: PositionState::Open, + collateral, + expiry, + updated: OffsetDateTime::now_utc(), + created: OffsetDateTime::now_utc(), + stable: false, + }; + db::insert_position(position.clone())?; + + event::publish(&EventInternal::PositionUpdateNotification(position)); + + Ok(()) +} + pub fn resend_settle_finalize_message() -> Result<()> { tracing::warn!("Executing emergency kit! Resending settle finalize message"); let coordinator_pubkey = config::get_coordinator_info().pubkey;