diff --git a/coordinator/migrations/2024-05-08-104311_nullable_contract_id_on_dlc_protocols/down.sql b/coordinator/migrations/2024-05-08-104311_nullable_contract_id_on_dlc_protocols/down.sql new file mode 100644 index 000000000..03115ba56 --- /dev/null +++ b/coordinator/migrations/2024-05-08-104311_nullable_contract_id_on_dlc_protocols/down.sql @@ -0,0 +1 @@ +ALTER TABLE dlc_protocols ALTER COLUMN contract_id SET NOT NULL; diff --git a/coordinator/migrations/2024-05-08-104311_nullable_contract_id_on_dlc_protocols/up.sql b/coordinator/migrations/2024-05-08-104311_nullable_contract_id_on_dlc_protocols/up.sql new file mode 100644 index 000000000..4aac069ec --- /dev/null +++ b/coordinator/migrations/2024-05-08-104311_nullable_contract_id_on_dlc_protocols/up.sql @@ -0,0 +1 @@ +ALTER TABLE dlc_protocols ALTER COLUMN contract_id DROP NOT NULL; diff --git a/coordinator/src/bin/coordinator.rs b/coordinator/src/bin/coordinator.rs index 21e8e0d0f..85427e513 100644 --- a/coordinator/src/bin/coordinator.rs +++ b/coordinator/src/bin/coordinator.rs @@ -285,8 +285,7 @@ async fn main() -> Result<()> { network, ); - node.spawn_shadow_dlc_channels_task(); - node.spawn_watch_closing_channels(); + node.spawn_watch_dlc_channel_events_task(); tokio::spawn({ let node = node.clone(); diff --git a/coordinator/src/db/dlc_channels.rs b/coordinator/src/db/dlc_channels.rs index c07a969d7..598224e30 100644 --- a/coordinator/src/db/dlc_channels.rs +++ b/coordinator/src/db/dlc_channels.rs @@ -1,4 +1,3 @@ -use crate::dlc_protocol::ProtocolId; use crate::node::channel; use crate::schema::dlc_channels; use crate::schema::sql_types::DlcChannelStateType; @@ -24,6 +23,7 @@ use std::any::TypeId; use std::str::FromStr; use time::OffsetDateTime; use uuid::Uuid; +use xxi_node::node::ProtocolId; #[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression)] #[diesel(sql_type = DlcChannelStateType)] @@ -191,7 +191,7 @@ pub(crate) fn set_channel_collab_closing( .execute(conn) } -pub(crate) fn set_channel_collab_closed( +pub(crate) fn set_channel_closed( conn: &mut PgConnection, channel_id: &DlcChannelId, close_txid: Txid, diff --git a/coordinator/src/db/dlc_protocols.rs b/coordinator/src/db/dlc_protocols.rs index 799f17fa7..316ae2ced 100644 --- a/coordinator/src/db/dlc_protocols.rs +++ b/coordinator/src/db/dlc_protocols.rs @@ -1,6 +1,5 @@ use crate::db; use crate::dlc_protocol; -use crate::dlc_protocol::ProtocolId; use crate::schema::dlc_protocols; use crate::schema::sql_types::ProtocolStateType; use crate::schema::sql_types::ProtocolTypeType; @@ -21,6 +20,7 @@ use std::any::TypeId; use std::str::FromStr; use time::OffsetDateTime; use uuid::Uuid; +use xxi_node::node::ProtocolId; #[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression, Eq, Hash)] #[diesel(sql_type = ProtocolStateType)] @@ -68,7 +68,7 @@ pub(crate) struct DlcProtocol { pub protocol_id: Uuid, pub previous_protocol_id: Option, pub channel_id: String, - pub contract_id: String, + pub contract_id: Option, pub protocol_state: DlcProtocolState, pub trader_pubkey: String, pub timestamp: OffsetDateTime, @@ -113,9 +113,14 @@ pub(crate) fn get_dlc_protocol( let protocol = dlc_protocol::DlcProtocol { id: dlc_protocol.protocol_id.into(), + previous_id: dlc_protocol + .previous_protocol_id + .map(|previous_id| previous_id.into()), timestamp: dlc_protocol.timestamp, channel_id: DlcChannelId::from_hex(&dlc_protocol.channel_id).expect("valid dlc channel id"), - contract_id: ContractId::from_hex(&dlc_protocol.contract_id).expect("valid contract id"), + contract_id: dlc_protocol + .contract_id + .map(|cid| ContractId::from_hex(cid).expect("valid contract id")), trader: PublicKey::from_str(&dlc_protocol.trader_pubkey).expect("valid public key"), protocol_state: dlc_protocol.protocol_state.into(), protocol_type, @@ -130,7 +135,7 @@ pub(crate) fn set_dlc_protocol_state_to_failed( ) -> QueryResult<()> { let affected_rows = diesel::update(dlc_protocols::table) .filter(dlc_protocols::protocol_id.eq(protocol_id.to_uuid())) - .set((dlc_protocols::protocol_state.eq(DlcProtocolState::Failed),)) + .set(dlc_protocols::protocol_state.eq(DlcProtocolState::Failed)) .execute(conn)?; if affected_rows == 0 { @@ -143,14 +148,14 @@ pub(crate) fn set_dlc_protocol_state_to_failed( pub(crate) fn set_dlc_protocol_state_to_success( conn: &mut PgConnection, protocol_id: ProtocolId, - contract_id: &ContractId, + contract_id: Option<&ContractId>, channel_id: &DlcChannelId, ) -> QueryResult<()> { let affected_rows = diesel::update(dlc_protocols::table) .filter(dlc_protocols::protocol_id.eq(protocol_id.to_uuid())) .set(( dlc_protocols::protocol_state.eq(DlcProtocolState::Success), - dlc_protocols::contract_id.eq(hex::encode(contract_id)), + dlc_protocols::contract_id.eq(contract_id.map(hex::encode)), dlc_protocols::channel_id.eq(hex::encode(channel_id)), )) .execute(conn)?; @@ -166,7 +171,7 @@ pub(crate) fn create( conn: &mut PgConnection, protocol_id: ProtocolId, previous_protocol_id: Option, - contract_id: &ContractId, + contract_id: Option<&ContractId>, channel_id: &DlcChannelId, protocol_type: dlc_protocol::DlcProtocolType, trader: &PublicKey, @@ -175,7 +180,7 @@ pub(crate) fn create( .values(&( dlc_protocols::protocol_id.eq(protocol_id.to_uuid()), dlc_protocols::previous_protocol_id.eq(previous_protocol_id.map(|ppid| ppid.to_uuid())), - dlc_protocols::contract_id.eq(hex::encode(contract_id)), + dlc_protocols::contract_id.eq(contract_id.map(hex::encode)), dlc_protocols::channel_id.eq(hex::encode(channel_id)), dlc_protocols::protocol_state.eq(DlcProtocolState::Pending), dlc_protocols::trader_pubkey.eq(trader.to_string()), diff --git a/coordinator/src/db/trade_params.rs b/coordinator/src/db/trade_params.rs index 0a3f06b82..6f0b7f7d8 100644 --- a/coordinator/src/db/trade_params.rs +++ b/coordinator/src/db/trade_params.rs @@ -1,5 +1,4 @@ use crate::dlc_protocol; -use crate::dlc_protocol::ProtocolId; use crate::orderbook::db::custom_types::Direction; use crate::schema::trade_params; use bitcoin::secp256k1::PublicKey; @@ -14,6 +13,7 @@ use diesel::RunQueryDsl; use std::str::FromStr; use uuid::Uuid; use xxi_node::commons; +use xxi_node::node::ProtocolId; #[derive(Queryable, Debug)] #[diesel(table_name = trade_params)] diff --git a/coordinator/src/dlc_protocol.rs b/coordinator/src/dlc_protocol.rs index 3ab3f5a33..791c541e9 100644 --- a/coordinator/src/dlc_protocol.rs +++ b/coordinator/src/dlc_protocol.rs @@ -14,99 +14,23 @@ use diesel::Connection; use diesel::PgConnection; use diesel::QueryResult; use dlc_manager::ContractId; -use dlc_manager::ReferenceId; use rust_decimal::prelude::FromPrimitive; use rust_decimal::prelude::ToPrimitive; use rust_decimal::Decimal; -use std::fmt::Display; -use std::fmt::Formatter; -use std::str::from_utf8; use time::OffsetDateTime; use tokio::sync::broadcast::Sender; -use uuid::Uuid; use xxi_node::cfd::calculate_pnl; use xxi_node::commons; use xxi_node::commons::Direction; use xxi_node::node::rust_dlc_manager::DlcChannelId; - -#[derive(Debug, Copy, Clone, PartialEq)] -pub struct ProtocolId(Uuid); - -impl ProtocolId { - pub fn new() -> Self { - ProtocolId(Uuid::new_v4()) - } - - pub fn to_uuid(&self) -> Uuid { - self.0 - } -} - -impl Default for ProtocolId { - fn default() -> Self { - Self::new() - } -} - -impl Display for ProtocolId { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.0.to_string().fmt(f) - } -} - -impl From for ReferenceId { - fn from(value: ProtocolId) -> Self { - let uuid = value.to_uuid(); - - // 16 bytes. - let uuid_bytes = uuid.as_bytes(); - - // 32-digit hex string. - let hex = hex::encode(uuid_bytes); - - // Derived `ReferenceId`: 32-bytes. - let hex_bytes = hex.as_bytes(); - - let mut array = [0u8; 32]; - array.copy_from_slice(hex_bytes); - - array - } -} - -impl TryFrom for ProtocolId { - type Error = anyhow::Error; - - fn try_from(value: ReferenceId) -> Result { - // 32-digit hex string. - let hex = from_utf8(&value)?; - - // 16 bytes. - let uuid_bytes = hex::decode(hex)?; - - let uuid = Uuid::from_slice(&uuid_bytes)?; - - Ok(ProtocolId(uuid)) - } -} - -impl From for ProtocolId { - fn from(value: Uuid) -> Self { - ProtocolId(value) - } -} - -impl From for Uuid { - fn from(value: ProtocolId) -> Self { - value.0 - } -} +use xxi_node::node::ProtocolId; pub struct DlcProtocol { pub id: ProtocolId, + pub previous_id: Option, pub timestamp: OffsetDateTime, pub channel_id: DlcChannelId, - pub contract_id: ContractId, + pub contract_id: Option, pub trader: PublicKey, pub protocol_state: DlcProtocolState, pub protocol_type: DlcProtocolType, @@ -247,7 +171,7 @@ impl DlcProtocolExecutor { &self, protocol_id: ProtocolId, previous_protocol_id: Option, - contract_id: &ContractId, + contract_id: Option<&ContractId>, channel_id: &DlcChannelId, protocol_type: DlcProtocolType, ) -> Result<()> { @@ -325,15 +249,14 @@ impl DlcProtocolExecutor { ) } DlcProtocolType::Settle { trade_params } => { - let settled_contract = &dlc_protocol.contract_id; - + let settled_contract = dlc_protocol.contract_id; self.finish_close_trade_dlc_protocol( conn, trade_params, protocol_id, // If the contract got settled, we do not get a new contract id, hence we // copy the contract id of the settled contract. - settled_contract, + settled_contract.as_ref(), channel_id, ) } @@ -349,7 +272,10 @@ impl DlcProtocolExecutor { channel_id, ) } - DlcProtocolType::Close { .. } | DlcProtocolType::ForceClose { .. } => { + DlcProtocolType::Close { .. } => { + self.finish_close_channel_dlc_protocol(conn, trader_id, protocol_id, channel_id) + } + DlcProtocolType::ForceClose { .. } => { debug_assert!(false, "Finishing unexpected dlc protocol types"); Ok(()) } @@ -395,7 +321,7 @@ impl DlcProtocolExecutor { conn: &mut PgConnection, trade_params: &TradeParams, protocol_id: ProtocolId, - settled_contract: &ContractId, + settled_contract: Option<&ContractId>, channel_id: &DlcChannelId, ) -> QueryResult<()> { db::dlc_protocols::set_dlc_protocol_state_to_success( @@ -501,7 +427,7 @@ impl DlcProtocolExecutor { db::dlc_protocols::set_dlc_protocol_state_to_success( conn, protocol_id, - contract_id, + Some(contract_id), channel_id, )?; @@ -547,7 +473,7 @@ impl DlcProtocolExecutor { db::dlc_protocols::set_dlc_protocol_state_to_success( conn, protocol_id, - contract_id, + Some(contract_id), channel_id, )?; @@ -594,13 +520,25 @@ impl DlcProtocolExecutor { db::dlc_protocols::set_dlc_protocol_state_to_success( conn, protocol_id, - contract_id, + Some(contract_id), channel_id, )?; db::positions::Position::set_position_to_open(conn, trader.to_string(), *contract_id)?; Ok(()) } + + /// Completes the collab close dlc protocol as successful + fn finish_close_channel_dlc_protocol( + &self, + conn: &mut PgConnection, + trader: &PublicKey, + protocol_id: ProtocolId, + channel_id: &DlcChannelId, + ) -> QueryResult<()> { + tracing::debug!(%trader, %protocol_id, "Finalizing channel close"); + db::dlc_protocols::set_dlc_protocol_state_to_success(conn, protocol_id, None, channel_id) + } } #[cfg(test)] diff --git a/coordinator/src/node.rs b/coordinator/src/node.rs index 2750902a9..fc22040aa 100644 --- a/coordinator/src/node.rs +++ b/coordinator/src/node.rs @@ -1,6 +1,6 @@ use crate::db; use crate::dlc_protocol; -use crate::dlc_protocol::ProtocolId; +use crate::dlc_protocol::DlcProtocolType; use crate::message::OrderbookMessage; use crate::node::storage::NodeStorage; use crate::position::models::PositionState; @@ -44,6 +44,7 @@ use xxi_node::message_handler::TenTenOneSignChannel; use xxi_node::node; use xxi_node::node::event::NodeEvent; use xxi_node::node::tentenone_message_name; +use xxi_node::node::ProtocolId; use xxi_node::node::RunningNode; pub mod channel; @@ -74,7 +75,7 @@ pub struct Node { _running: Arc, pub pool: Pool>, pub settings: Arc>, - tx_position_feed: Sender, + pub tx_position_feed: Sender, trade_notifier: mpsc::Sender, } @@ -224,8 +225,6 @@ impl Node { "Received message" ); - self.verify_collab_close_offer(&node_id, msg)?; - let inbound_msg = { let mut conn = self.pool.get()?; let serialized_inbound_message = SerializedDlcMessage::try_from(msg)?; @@ -239,6 +238,8 @@ impl Node { } }; + self.verify_collab_close_offer(&node_id, msg)?; + let resp = self .inner .process_tentenone_message(msg.clone(), node_id) @@ -364,7 +365,7 @@ impl Node { tracing::info!( channel_id = hex::encode(close_offer.channel_id), node_id = node_id.to_string(), - "Received an offer to collaboratively close a channel" + "Accepting offer to collaboratively close a channel" ); self.inner @@ -576,6 +577,23 @@ impl Node { _ => {} }; + let protocol_id = close_offer.reference_id.context("Missing reference id")?; + let protocol_id = ProtocolId::try_from(protocol_id)?; + + let previous_id = channel + .get_reference_id() + .map(ProtocolId::try_from) + .transpose()?; + + let protocol_executor = dlc_protocol::DlcProtocolExecutor::new(self.pool.clone()); + protocol_executor.start_dlc_protocol( + protocol_id, + previous_id, + None, + &channel.get_id(), + DlcProtocolType::Close { trader: *node_id }, + )?; + Ok(()) } } diff --git a/coordinator/src/node/channel.rs b/coordinator/src/node/channel.rs index 657233c66..75e653de2 100644 --- a/coordinator/src/node/channel.rs +++ b/coordinator/src/node/channel.rs @@ -1,12 +1,17 @@ use crate::db; +use crate::dlc_protocol; use crate::dlc_protocol::DlcProtocolType; -use crate::dlc_protocol::ProtocolId; use crate::node::Node; +use crate::position::models::PositionState; use anyhow::bail; +use anyhow::Context; use anyhow::Result; use bitcoin::secp256k1::PublicKey; use bitcoin::Amount; +use bitcoin::ScriptBuf; use bitcoin::Txid; +use bitcoin_old::Transaction; +use diesel::PgConnection; use dlc_manager::channel::signed_channel::SignedChannel; use dlc_manager::channel::signed_channel::SignedChannelState; use dlc_manager::channel::Channel; @@ -14,12 +19,18 @@ use dlc_manager::channel::ClosedChannel; use dlc_manager::channel::ClosedPunishedChannel; use dlc_manager::channel::ClosingChannel; use dlc_manager::channel::SettledClosingChannel; +use dlc_manager::contract::ClosedContract; +use dlc_manager::contract::Contract; +use dlc_manager::contract::PreClosedContract; use dlc_manager::DlcChannelId; +use dlc_manager::ReferenceId; +use rust_decimal::Decimal; use time::OffsetDateTime; use tokio::sync::broadcast::error::RecvError; use xxi_node::bitcoin_conversion::to_secp_pk_30; use xxi_node::bitcoin_conversion::to_txid_30; use xxi_node::node::event::NodeEvent; +use xxi_node::node::ProtocolId; use xxi_node::storage::DlcChannelEvent; pub enum DlcChannelState { @@ -50,7 +61,37 @@ pub struct DlcChannel { } impl Node { - pub fn spawn_shadow_dlc_channels_task(&self) { + pub async fn close_dlc_channel( + &self, + channel_id: DlcChannelId, + is_force_close: bool, + ) -> Result<()> { + let channel = self.inner.get_dlc_channel_by_id(&channel_id)?; + let previous_id = channel + .get_reference_id() + .map(ProtocolId::try_from) + .transpose()?; + + let protocol_id = self + .inner + .close_dlc_channel(channel_id, is_force_close) + .await?; + + let protocol_executor = dlc_protocol::DlcProtocolExecutor::new(self.pool.clone()); + protocol_executor.start_dlc_protocol( + protocol_id, + previous_id, + None, + &channel.get_id(), + DlcProtocolType::Close { + trader: to_secp_pk_30(channel.get_counter_party_id()), + }, + )?; + + Ok(()) + } + + pub fn spawn_watch_dlc_channel_events_task(&self) { let mut receiver = self.inner.event_handler.subscribe(); tokio::spawn({ @@ -187,29 +228,7 @@ impl Node { claim_transaction.map(|tx| to_txid_30(tx.txid())), )?; } - DlcChannelEvent::Closing(_) => { - let buffer_transaction = match channel { - Channel::Signed(SignedChannel { - state: - SignedChannelState::Closing { - buffer_transaction, .. - }, - .. - }) => buffer_transaction, - Channel::Closing(ClosingChannel { - buffer_transaction, .. - }) => buffer_transaction, - channel => { - bail!("DLC channel in unexpected state. dlc_channel = {channel:?}") - } - }; - - db::dlc_channels::set_channel_force_closing( - &mut conn, - &channel.get_id(), - to_txid_30(buffer_transaction.txid()), - )?; - } + DlcChannelEvent::Closing(_) => self.handle_closing_event(&mut conn, channel)?, DlcChannelEvent::ClosedPunished(_) => { let punish_txid = match channel { Channel::ClosedPunished(ClosedPunishedChannel { punish_txid, .. }) => { @@ -243,25 +262,11 @@ impl Node { to_txid_30(close_transaction.txid()), )?; } - DlcChannelEvent::Closed(_) - | DlcChannelEvent::CounterClosed(_) - | DlcChannelEvent::CollaborativelyClosed(_) => { - let close_txid = match channel { - Channel::Closed(ClosedChannel { closing_txid, .. }) => closing_txid, - Channel::CounterClosed(ClosedChannel { closing_txid, .. }) => closing_txid, - Channel::CollaborativelyClosed(ClosedChannel { closing_txid, .. }) => { - closing_txid - } - channel => { - bail!("DLC channel in unexpected state. dlc_channel = {channel:?}") - } - }; - - db::dlc_channels::set_channel_collab_closed( - &mut conn, - &channel.get_id(), - to_txid_30(*close_txid), - )?; + DlcChannelEvent::Closed(_) | DlcChannelEvent::CounterClosed(_) => { + self.handle_force_closed_event(&mut conn, channel, protocol_id)? + } + DlcChannelEvent::CollaborativelyClosed(_) => { + self.handle_collaboratively_closed_event(&mut conn, channel, protocol_id)? } DlcChannelEvent::FailedAccept(_) | DlcChannelEvent::FailedSign(_) => { let protocol_id = ProtocolId::try_from(protocol_id)?; @@ -285,4 +290,193 @@ impl Node { Ok(()) } + + fn handle_closing_event(&self, conn: &mut PgConnection, channel: &Channel) -> Result<()> { + // If a channel is set to closing it means the buffer transaction got broadcasted, + // which will only happen if the channel got force closed while the + // user had an open position. + let trader_id = channel.get_counter_party_id(); + + // we do not know the price yet, since we have to wait for the position to expire. + if db::positions::Position::set_open_position_to_closing( + conn, + &to_secp_pk_30(trader_id), + None, + )? > 0 + { + tracing::info!(%trader_id, "Set open position to closing after the dlc channel got force closed."); + } + + let buffer_transaction = match channel { + Channel::Signed(SignedChannel { + state: + SignedChannelState::Closing { + buffer_transaction, .. + }, + .. + }) => buffer_transaction, + Channel::Closing(ClosingChannel { + buffer_transaction, .. + }) => buffer_transaction, + channel => { + bail!("DLC channel in unexpected state. dlc_channel = {channel:?}") + } + }; + + db::dlc_channels::set_channel_force_closing( + conn, + &channel.get_id(), + to_txid_30(buffer_transaction.txid()), + )?; + + Ok(()) + } + + fn handle_force_closed_event( + &self, + conn: &mut PgConnection, + channel: &Channel, + reference_id: ReferenceId, + ) -> Result<()> { + let protocol_id = ProtocolId::try_from(reference_id)?; + let dlc_protocol = db::dlc_protocols::get_dlc_protocol(conn, protocol_id)?; + let contract_id = &dlc_protocol.contract_id.context("Missing contract id")?; + let trader_id = dlc_protocol.trader; + let contract = self + .inner + .get_contract_by_id(contract_id)? + .context("Missing contract")?; + + let position = db::positions::Position::get_position_by_trader( + conn, + trader_id, + /* the closing price doesn't matter here. */ + vec![PositionState::Closing { closing_price: 0.0 }], + )? + .with_context(|| { + format!("Couldn't find closing position for trader. trader_id = {trader_id}") + })?; + + let (closing_price, trader_realized_pnl_sat) = match contract { + Contract::PreClosed(PreClosedContract { + // We assume a closed contract does always have an attestation + attestations: Some(attestations), + signed_cet, + .. + }) + | Contract::Closed(ClosedContract { + // We assume a closed contract does always have an attestation + attestations: Some(attestations), + signed_cet: Some(signed_cet), + .. + }) => { + let trader_realized_pnl_sat = self.calculate_trader_realized_pnl_from_cet( + conn, + &dlc_protocol.channel_id, + signed_cet, + )?; + + let closing_price = Decimal::from_str_radix( + &attestations + .first() + .context("at least one attestation")? + .outcomes + .join(""), + 2, + )?; + + (closing_price, trader_realized_pnl_sat) + } + contract => { + bail!("Contract in unexpected state. Expected PreClosed or Closed Got: {:?}, trader_id = {trader_id}", contract) + } + }; + + tracing::debug!( + ?position, + %trader_id, + "Finalize closing position after force closure", + ); + + if db::positions::Position::set_position_to_closed_with_pnl( + conn, + position.id, + trader_realized_pnl_sat, + closing_price, + )? > 0 + { + tracing::info!(%trader_id, "Set closing position to closed after the dlc channel got force closed."); + } else { + tracing::warn!(%trader_id, "Failed to set closing position to closed after the dlc channel got force closed."); + } + + let close_txid = match channel { + Channel::Closed(ClosedChannel { closing_txid, .. }) => closing_txid, + Channel::CounterClosed(ClosedChannel { closing_txid, .. }) => closing_txid, + channel => { + bail!("DLC channel in unexpected state. dlc_channel = {channel:?}") + } + }; + + db::dlc_channels::set_channel_closed(conn, &channel.get_id(), to_txid_30(*close_txid))?; + Ok(()) + } + + fn handle_collaboratively_closed_event( + &self, + conn: &mut PgConnection, + channel: &Channel, + reference_id: ReferenceId, + ) -> Result<()> { + let protocol_executor = dlc_protocol::DlcProtocolExecutor::new(self.pool.clone()); + protocol_executor.finish_dlc_protocol( + ProtocolId::try_from(reference_id)?, + &to_secp_pk_30(channel.get_counter_party_id()), + None, + &channel.get_id(), + self.tx_position_feed.clone(), + )?; + + let close_txid = match channel { + Channel::CollaborativelyClosed(ClosedChannel { closing_txid, .. }) => closing_txid, + channel => { + bail!("DLC channel in unexpected state. dlc_channel = {channel:?}") + } + }; + + db::dlc_channels::set_channel_closed(conn, &channel.get_id(), to_txid_30(*close_txid))?; + + Ok(()) + } + + /// Calculates the trader realized pnl from the cet outputs which do not belong to us. + /// 1. Sum the trader payouts + /// 2. Subtract the trader reserve sats from the trader payout + fn calculate_trader_realized_pnl_from_cet( + &self, + conn: &mut PgConnection, + channel_id: &DlcChannelId, + signed_cet: Transaction, + ) -> Result { + let trader_payout: u64 = signed_cet + .output + .iter() + .filter(|output| { + !self + .inner + .is_mine(&ScriptBuf::from_bytes(output.script_pubkey.to_bytes())) + }) + .map(|output| output.value) + .sum(); + + let dlc_channel = + db::dlc_channels::get_dlc_channel(conn, channel_id)?.with_context(|| { + format!("Couldn't find dlc channel by channel id = {:?}", channel_id) + })?; + + let trader_realized_pnl_sat = + trader_payout as i64 - dlc_channel.trader_reserve_sats.to_sat() as i64; + + Ok(trader_realized_pnl_sat) + } } diff --git a/coordinator/src/node/rollover.rs b/coordinator/src/node/rollover.rs index 608f31c99..9d3746bbb 100644 --- a/coordinator/src/node/rollover.rs +++ b/coordinator/src/node/rollover.rs @@ -3,7 +3,6 @@ use crate::db; use crate::db::positions; use crate::dlc_protocol; use crate::dlc_protocol::DlcProtocolType; -use crate::dlc_protocol::ProtocolId; use crate::node::Node; use crate::notifications::Notification; use crate::notifications::NotificationKind; @@ -37,6 +36,7 @@ use xxi_node::bitcoin_conversion::to_xonly_pk_30; use xxi_node::commons; use xxi_node::commons::ContractSymbol; use xxi_node::node::event::NodeEvent; +use xxi_node::node::ProtocolId; #[derive(Debug, Clone)] struct Rollover { @@ -262,7 +262,7 @@ impl Node { protocol_executor.start_dlc_protocol( protocol_id, previous_id, - &contract_id, + Some(&contract_id), dlc_channel_id, DlcProtocolType::Rollover { trader: rollover.counterparty_pubkey, diff --git a/coordinator/src/position/mod.rs b/coordinator/src/position/mod.rs index ba96dc7b2..c446ac883 100644 --- a/coordinator/src/position/mod.rs +++ b/coordinator/src/position/mod.rs @@ -1,219 +1 @@ -use crate::db; -use crate::dlc_protocol::ProtocolId; -use crate::node::Node; -use crate::position::models::PositionState; -use anyhow::bail; -use anyhow::Context; -use anyhow::Result; -use bitcoin::ScriptBuf; -use bitcoin_old::Transaction; -use diesel::r2d2::ConnectionManager; -use diesel::r2d2::PooledConnection; -use diesel::PgConnection; -use dlc_manager::contract::ClosedContract; -use dlc_manager::contract::Contract; -use dlc_manager::contract::PreClosedContract; -use dlc_manager::DlcChannelId; -use rust_decimal::Decimal; -use tokio::sync::broadcast::error::RecvError; -use xxi_node::bitcoin_conversion::to_secp_pk_30; -use xxi_node::node::event::NodeEvent; -use xxi_node::storage::DlcChannelEvent; - pub mod models; - -impl Node { - pub fn spawn_watch_closing_channels(&self) { - let mut receiver = self.inner.event_handler.subscribe(); - - tokio::spawn({ - let node = self.clone(); - async move { - loop { - match receiver.recv().await { - Ok(NodeEvent::DlcChannelEvent { dlc_channel_event }) => { - if let Err(e) = node - .update_position_after_dlc_channel_event(dlc_channel_event) - .await - { - tracing::error!(?dlc_channel_event, "Failed to update position after dlc channel event. Error: {e:}") - } - } - Ok(NodeEvent::Connected { .. }) - | Ok(NodeEvent::SendDlcMessage { .. }) - | Ok(NodeEvent::StoreDlcMessage { .. }) - | Ok(NodeEvent::SendLastDlcMessage { .. }) => {} // ignored - Err(RecvError::Lagged(skipped)) => { - tracing::warn!("Skipped {skipped} messages"); - } - Err(RecvError::Closed) => { - tracing::error!("Lost connection to sender!"); - break; - } - } - } - } - }); - } - - /// Checks if the dlc channel got force closed and updates a potential open position. If the dlc - /// channel is closing the position will be set to `Closing`, if the dlc channel is closed or - /// counter closed the closing position will be set to closed with a closing price (from the - /// attestation and a trader realized pnl calculated from the cet payout and the last trader - /// reserve) - async fn update_position_after_dlc_channel_event( - &self, - dlc_channel_event: DlcChannelEvent, - ) -> Result<()> { - let mut conn = self.pool.get()?; - - let reference_id = dlc_channel_event.get_reference_id().with_context(|| format!("Can't process dlc channel event without reference id. dlc_channel_event = {dlc_channel_event:?}"))?; - let protocol_id = ProtocolId::try_from(reference_id)?; - - match dlc_channel_event { - // If a channel is set to closing it means the buffer transaction got broadcasted, which - // will only happen if the channel got force closed while the user had an open position. - DlcChannelEvent::Closing(_) => { - let channel = &self.inner.get_dlc_channel_by_reference_id(reference_id)?; - let trader_id = channel.get_counter_party_id(); - - // we do not know the price yet, since we have to wait for the position to expire. - if db::positions::Position::set_open_position_to_closing( - &mut conn, - &to_secp_pk_30(trader_id), - None, - )? > 0 - { - tracing::info!(%trader_id, "Set open position to closing after the dlc channel got force closed."); - } - } - // A dlc channel is set to `Closed` or `CounterClosed` if the CET got broadcasted. The - // underlying contract is either `PreClosed` or `Closed` depending on the CET - // confirmations. - DlcChannelEvent::Closed(_) | DlcChannelEvent::CounterClosed(_) => { - let dlc_protocol = db::dlc_protocols::get_dlc_protocol(&mut conn, protocol_id)?; - let trader_id = dlc_protocol.trader; - let contract = self - .inner - .get_contract_by_id(&dlc_protocol.contract_id)? - .context("Missing contract")?; - - let position = db::positions::Position::get_position_by_trader( - &mut conn, - trader_id, - /* the closing price doesn't matter here. */ - vec![PositionState::Closing { closing_price: 0.0 }], - )? - .with_context(|| { - format!("Couldn't find closing position for trader. trader_id = {trader_id}") - })?; - - let (closing_price, trader_realized_pnl_sat) = match contract { - Contract::PreClosed(PreClosedContract { - // We assume a closed contract does always have an attestation - attestations: Some(attestations), - signed_cet, - .. - }) - | Contract::Closed(ClosedContract { - // We assume a closed contract does always have an attestation - attestations: Some(attestations), - signed_cet: Some(signed_cet), - .. - }) => { - let trader_realized_pnl_sat = self.calculate_trader_realized_pnl_from_cet( - &mut conn, - &dlc_protocol.channel_id, - signed_cet, - )?; - - let closing_price = Decimal::from_str_radix( - &attestations - .first() - .context("at least one attestation")? - .outcomes - .join(""), - 2, - )?; - - (closing_price, trader_realized_pnl_sat) - } - contract => { - bail!("Contract in unexpected state. Expected PreClosed or Closed Got: {:?}, trader_id = {trader_id}", contract) - } - }; - - tracing::debug!( - ?position, - %trader_id, - "Finalize closing position after force closure", - ); - - if db::positions::Position::set_position_to_closed_with_pnl( - &mut conn, - position.id, - trader_realized_pnl_sat, - closing_price, - )? > 0 - { - tracing::info!(%trader_id, "Set closing position to closed after the dlc channel got force closed."); - } else { - tracing::warn!(%trader_id, "Failed to set closing position to closed after the dlc channel got force closed."); - } - } - DlcChannelEvent::Offered(_) - | DlcChannelEvent::Accepted(_) - | DlcChannelEvent::Established(_) - | DlcChannelEvent::SettledOffered(_) - | DlcChannelEvent::SettledReceived(_) - | DlcChannelEvent::SettledAccepted(_) - | DlcChannelEvent::SettledConfirmed(_) - | DlcChannelEvent::Settled(_) - | DlcChannelEvent::SettledClosing(_) - | DlcChannelEvent::RenewOffered(_) - | DlcChannelEvent::RenewAccepted(_) - | DlcChannelEvent::RenewConfirmed(_) - | DlcChannelEvent::RenewFinalized(_) - | DlcChannelEvent::CollaborativeCloseOffered(_) - | DlcChannelEvent::ClosedPunished(_) - | DlcChannelEvent::CollaborativelyClosed(_) - | DlcChannelEvent::FailedAccept(_) - | DlcChannelEvent::FailedSign(_) - | DlcChannelEvent::Cancelled(_) - | DlcChannelEvent::Deleted(_) => {} // ignored - } - - Ok(()) - } - - /// Calculates the trader realized pnl from the cet outputs which do not belong to us. - /// 1. Sum the trader payouts - /// 2. Subtract the trader reserve sats from the trader payout - fn calculate_trader_realized_pnl_from_cet( - &self, - conn: &mut PooledConnection>, - channel_id: &DlcChannelId, - signed_cet: Transaction, - ) -> Result { - let trader_payout: u64 = signed_cet - .output - .iter() - .filter(|output| { - !self - .inner - .is_mine(&ScriptBuf::from_bytes(output.script_pubkey.to_bytes())) - }) - .map(|output| output.value) - .sum(); - - let dlc_channel = - db::dlc_channels::get_dlc_channel(conn, channel_id)?.with_context(|| { - format!("Couldn't find dlc channel by channel id = {:?}", channel_id) - })?; - - let trader_realized_pnl_sat = - trader_payout as i64 - dlc_channel.trader_reserve_sats.to_sat() as i64; - - Ok(trader_realized_pnl_sat) - } -} diff --git a/coordinator/src/routes/admin.rs b/coordinator/src/routes/admin.rs index 4c1661313..68422b97b 100644 --- a/coordinator/src/routes/admin.rs +++ b/coordinator/src/routes/admin.rs @@ -1,6 +1,5 @@ use crate::collaborative_revert; use crate::db; -use crate::dlc_protocol::ProtocolId; use crate::parse_dlc_channel_id; use crate::referrals; use crate::routes::AppState; @@ -42,6 +41,7 @@ use xxi_node::bitcoin_conversion::to_secp_pk_30; use xxi_node::bitcoin_conversion::to_txid_30; use xxi_node::commons; use xxi_node::commons::CollaborativeRevertCoordinatorRequest; +use xxi_node::node::ProtocolId; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Balance { @@ -298,7 +298,6 @@ pub async fn close_channel( state .node - .inner .close_dlc_channel(channel_id, params.force.unwrap_or_default()) .await .map_err(|e| AppError::InternalServerError(format!("{e:#}")))?; diff --git a/coordinator/src/schema.rs b/coordinator/src/schema.rs index 4453c9803..e161360d2 100644 --- a/coordinator/src/schema.rs +++ b/coordinator/src/schema.rs @@ -203,7 +203,7 @@ diesel::table! { protocol_id -> Uuid, previous_protocol_id -> Nullable, channel_id -> Text, - contract_id -> Text, + contract_id -> Nullable, protocol_state -> ProtocolStateType, trader_pubkey -> Text, timestamp -> Timestamptz, diff --git a/coordinator/src/trade/mod.rs b/coordinator/src/trade/mod.rs index 2397a00b9..321052aae 100644 --- a/coordinator/src/trade/mod.rs +++ b/coordinator/src/trade/mod.rs @@ -3,7 +3,6 @@ use crate::db; use crate::decimal_from_f32; use crate::dlc_protocol; use crate::dlc_protocol::DlcProtocolType; -use crate::dlc_protocol::ProtocolId; use crate::message::OrderbookMessage; use crate::node::Node; use crate::orderbook::db::matches; @@ -52,6 +51,7 @@ use xxi_node::commons::TradeAndChannelParams; use xxi_node::commons::TradeParams; use xxi_node::node::event::NodeEvent; use xxi_node::node::signed_channel_state_name; +use xxi_node::node::ProtocolId; pub mod models; pub mod websocket; @@ -369,7 +369,7 @@ impl TradeExecutor { trade_params.filled_with.clone(), contract_input, trade_params.pubkey, - protocol_id.into(), + protocol_id, ) .await .context("Could not propose DLC channel")?; @@ -378,7 +378,7 @@ impl TradeExecutor { protocol_executor.start_dlc_protocol( protocol_id, None, - &temporary_contract_id, + Some(&temporary_contract_id), &temporary_channel_id, DlcProtocolType::open_channel(trade_params, protocol_id), )?; @@ -548,7 +548,7 @@ impl TradeExecutor { trade_params.filled_with.clone(), &dlc_channel_id, contract_input, - protocol_id.into(), + protocol_id, ) .await .context("Could not propose reopen DLC channel update")?; @@ -557,7 +557,7 @@ impl TradeExecutor { protocol_executor.start_dlc_protocol( protocol_id, previous_id, - &temporary_contract_id, + Some(&temporary_contract_id), &channel.get_id(), DlcProtocolType::open_position(trade_params, protocol_id), )?; @@ -721,7 +721,7 @@ impl TradeExecutor { trade_params.filled_with.clone(), &dlc_channel_id, contract_input, - protocol_id.into(), + protocol_id, ) .await .context("Could not propose resize DLC channel update")?; @@ -730,7 +730,7 @@ impl TradeExecutor { protocol_executor.start_dlc_protocol( protocol_id, previous_id, - &temporary_contract_id, + Some(&temporary_contract_id), &channel.get_id(), DlcProtocolType::resize_position(trade_params, protocol_id, realized_pnl), )?; @@ -878,7 +878,7 @@ impl TradeExecutor { trade_params.filled_with.clone(), &channel_id, settlement_amount_trader, - protocol_id.into(), + protocol_id, ) .await?; @@ -886,7 +886,7 @@ impl TradeExecutor { protocol_executor.start_dlc_protocol( protocol_id, previous_id, - &contract_id, + Some(&contract_id), &channel.get_id(), DlcProtocolType::settle(trade_params, protocol_id), )?; diff --git a/crates/xxi-node/src/node/dlc_channel.rs b/crates/xxi-node/src/node/dlc_channel.rs index ab15d95ca..87951c3d8 100644 --- a/crates/xxi-node/src/node/dlc_channel.rs +++ b/crates/xxi-node/src/node/dlc_channel.rs @@ -11,6 +11,7 @@ use crate::message_handler::TenTenOneSettleAccept; use crate::message_handler::TenTenOneSettleOffer; use crate::node::event::NodeEvent; use crate::node::Node; +use crate::node::ProtocolId; use crate::node::Storage as LnDlcStorage; use crate::on_chain_wallet::BdkStorage; use crate::storage::TenTenOneStorage; @@ -45,7 +46,7 @@ impl Result<(ContractId, DlcChannelId)> { tracing::info!( trader_id = %counterparty, @@ -89,7 +90,7 @@ impl Result<()> { + ) -> Result { let channel_id_hex = hex::encode(channel_id); tracing::info!( @@ -152,17 +153,22 @@ impl Result<()> { + fn force_close_dlc_channel( + &self, + channel: SignedChannel, + protocol_id: ProtocolId, + ) -> Result<()> { let channel_id = channel.channel_id; let channel_id_hex = hex::encode(channel_id); @@ -172,12 +178,16 @@ impl Result<()> { + async fn propose_dlc_channel_collaborative_close( + &self, + channel: SignedChannel, + protocol_id: ProtocolId, + ) -> Result<()> { let counterparty = channel.counter_party; match channel.state { @@ -196,7 +206,7 @@ impl Result<()> { let channel_id_hex = hex::encode(channel_id); @@ -250,7 +260,7 @@ impl Result { tracing::info!(channel_id = %hex::encode(dlc_channel_id), "Proposing a DLC channel reopen or resize"); spawn_blocking({ @@ -326,7 +336,7 @@ impl { @@ -425,3 +428,76 @@ impl Display for NodeInfo { format!("{scheme}://{}@{}", self.pubkey, self.address).fmt(f) } } + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct ProtocolId(Uuid); + +impl ProtocolId { + pub fn new() -> Self { + ProtocolId(Uuid::new_v4()) + } + + pub fn to_uuid(&self) -> Uuid { + self.0 + } +} + +impl Default for ProtocolId { + fn default() -> Self { + Self::new() + } +} + +impl Display for ProtocolId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.to_string().fmt(f) + } +} + +impl From for ReferenceId { + fn from(value: ProtocolId) -> Self { + let uuid = value.to_uuid(); + + // 16 bytes. + let uuid_bytes = uuid.as_bytes(); + + // 32-digit hex string. + let hex = hex::encode(uuid_bytes); + + // Derived `ReferenceId`: 32-bytes. + let hex_bytes = hex.as_bytes(); + + let mut array = [0u8; 32]; + array.copy_from_slice(hex_bytes); + + array + } +} + +impl TryFrom for ProtocolId { + type Error = anyhow::Error; + + fn try_from(value: ReferenceId) -> Result { + // 32-digit hex string. + let hex = from_utf8(&value)?; + + // 16 bytes. + let uuid_bytes = hex::decode(hex)?; + + let uuid = Uuid::from_slice(&uuid_bytes)?; + + Ok(ProtocolId(uuid)) + } +} + +impl From for ProtocolId { + fn from(value: Uuid) -> Self { + ProtocolId(value) + } +} + +impl From for Uuid { + fn from(value: ProtocolId) -> Self { + value.0 + } +} diff --git a/crates/xxi-node/src/tests/dlc_channel.rs b/crates/xxi-node/src/tests/dlc_channel.rs index 1101adb4b..3e4532170 100644 --- a/crates/xxi-node/src/tests/dlc_channel.rs +++ b/crates/xxi-node/src/tests/dlc_channel.rs @@ -3,6 +3,7 @@ use crate::node::dlc_channel::estimated_dlc_channel_fee_reserve; use crate::node::event::NodeEvent; use crate::node::InMemoryStore; use crate::node::Node; +use crate::node::ProtocolId; use crate::node::RunningNode; use crate::on_chain_wallet; use crate::storage::TenTenOneInMemoryStorage; @@ -11,7 +12,6 @@ use crate::tests::dummy_contract_input; use crate::tests::dummy_filled_with; use crate::tests::dummy_order; use crate::tests::init_tracing; -use crate::tests::new_reference_id; use crate::tests::wait_until; use bitcoin::Amount; use dlc_manager::channel::signed_channel::SignedChannel; @@ -43,7 +43,7 @@ async fn can_open_and_settle_offchain() { filled_with.clone(), &coordinator_signed_channel.channel_id, contract_input, - new_reference_id(), + ProtocolId::new(), ) .await .unwrap(); @@ -464,7 +464,7 @@ async fn open_channel_and_position_and_settle_position( filled_with.clone(), contract_input, app.info.pubkey, - new_reference_id(), + ProtocolId::new(), ) .await .unwrap(); @@ -590,7 +590,7 @@ async fn open_channel_and_position_and_settle_position( filled_with.clone(), &coordinator_signed_channel.channel_id, coordinator_dlc_collateral.to_sat() / 2, - new_reference_id(), + ProtocolId::new(), ) .await .unwrap(); diff --git a/crates/xxi-node/src/tests/mod.rs b/crates/xxi-node/src/tests/mod.rs index f815e78e9..5b3acfe0f 100644 --- a/crates/xxi-node/src/tests/mod.rs +++ b/crates/xxi-node/src/tests/mod.rs @@ -18,7 +18,6 @@ use anyhow::Result; use bitcoin::secp256k1::XOnlyPublicKey; use bitcoin::Amount; use bitcoin::Network; -use bitcoin_old::hashes::hex::ToHex; use dlc_manager::contract::contract_input::ContractInput; use dlc_manager::contract::contract_input::ContractInputInfo; use dlc_manager::contract::contract_input::OracleInput; @@ -30,7 +29,6 @@ use dlc_manager::payout_curve::PayoutPoint; use dlc_manager::payout_curve::PolynomialPayoutCurvePiece; use dlc_manager::payout_curve::RoundingInterval; use dlc_manager::payout_curve::RoundingIntervals; -use dlc_manager::ReferenceId; use futures::Future; use rand::distributions::Alphanumeric; use rand::thread_rng; @@ -48,7 +46,6 @@ use std::sync::Arc; use std::sync::Once; use std::time::Duration; use time::OffsetDateTime; -use uuid::Uuid; mod bitcoind; mod dlc_channel; @@ -437,19 +434,6 @@ fn dummy_contract_input( } } -pub fn new_reference_id() -> ReferenceId { - let uuid = Uuid::new_v4(); - let hex = uuid.as_simple().to_hex(); - let bytes = hex.as_bytes(); - - debug_assert!(bytes.len() == 32, "length must be exactly 32 bytes"); - - let mut array = [0u8; 32]; - array.copy_from_slice(bytes); - - array -} - pub fn dummy_order() -> commons::Order { commons::Order { id: Default::default(), diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 9feac5c96..c1dacdf7c 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -32,6 +32,13 @@ PODS: - GoogleUtilities/UserDefaults (~> 7.8) - nanopb (< 2.30910.0, >= 2.30908.0) - Flutter (1.0.0) + - flutter_inappwebview_ios (0.0.1): + - Flutter + - flutter_inappwebview_ios/Core (= 0.0.1) + - OrderedSet (~> 5.0) + - flutter_inappwebview_ios/Core (0.0.1): + - Flutter + - OrderedSet (~> 5.0) - flutter_local_notifications (0.0.1): - Flutter - flutter_native_splash (0.0.1): @@ -63,6 +70,7 @@ PODS: - nanopb/encode (= 2.30909.0) - nanopb/decode (2.30909.0) - nanopb/encode (2.30909.0) + - OrderedSet (5.0.0) - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): @@ -81,11 +89,14 @@ PODS: - Flutter - url_launcher_ios (0.0.1): - Flutter + - webview_flutter_wkwebview (0.0.1): + - Flutter DEPENDENCIES: - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - Flutter (from `Flutter`) + - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) @@ -95,6 +106,7 @@ DEPENDENCIES: - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - social_share (from `.symlinks/plugins/social_share/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) SPEC REPOS: trunk: @@ -107,6 +119,7 @@ SPEC REPOS: - GoogleUtilities - MTBBarcodeScanner - nanopb + - OrderedSet - PromisesObjC EXTERNAL SOURCES: @@ -116,6 +129,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/firebase_messaging/ios" Flutter: :path: Flutter + flutter_inappwebview_ios: + :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" flutter_local_notifications: :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_native_splash: @@ -134,6 +149,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/social_share/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" SPEC CHECKSUMS: Firebase: 07150e75d142fb9399f6777fa56a187b17f833a0 @@ -144,12 +161,14 @@ SPEC CHECKSUMS: FirebaseInstallations: cae95cab0f965ce05b805189de1d4c70b11c76fb FirebaseMessaging: bb2c4f6422a753038fe137d90ae7c1af57251316 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2 GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 + OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 @@ -158,6 +177,7 @@ SPEC CHECKSUMS: shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 social_share: 702a5e3842addd22db515aa9e1e00a4b80a0296d url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 + webview_flutter_wkwebview: be0f0d33777f1bfd0c9fdcb594786704dbf65f36 PODFILE CHECKSUM: cc1f88378b4bfcf93a6ce00d2c587857c6008d3b diff --git a/mobile/lib/common/background_task_change_notifier.dart b/mobile/lib/common/background_task_change_notifier.dart index 5aa7a3be6..fe6d7e941 100644 --- a/mobile/lib/common/background_task_change_notifier.dart +++ b/mobile/lib/common/background_task_change_notifier.dart @@ -7,7 +7,7 @@ import 'package:get_10101/logger/logger.dart'; class Stack { final _list = []; - void push(E value) => _list.add(value); + void push(E value) => _list.insert(0, value); E pop() => _list.removeLast(); @@ -64,6 +64,11 @@ class BackgroundTaskChangeNotifier extends ChangeNotifier implements Subscriber events.push(BackgroundTask(type: TaskType.liquidate, status: taskStatus, error: error)); notifyListeners(); } + + if (event.field0 is bridge.BackgroundTask_CloseChannel) { + events.push(BackgroundTask(type: TaskType.closeChannel, status: taskStatus, error: error)); + notifyListeners(); + } } } } diff --git a/mobile/lib/common/background_task_dialog_screen.dart b/mobile/lib/common/background_task_dialog_screen.dart index 1856701b8..184996d8e 100644 --- a/mobile/lib/common/background_task_dialog_screen.dart +++ b/mobile/lib/common/background_task_dialog_screen.dart @@ -3,7 +3,9 @@ import 'package:flutter/services.dart'; import 'package:get_10101/common/background_task_change_notifier.dart'; import 'package:get_10101/common/domain/background_task.dart'; import 'package:get_10101/common/task_status_dialog.dart'; +import 'package:get_10101/features/wallet/wallet_screen.dart'; import 'package:get_10101/logger/logger.dart'; +import 'package:go_router/go_router.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'dart:convert'; import 'package:get_10101/bridge_generated/bridge_definitions.dart' as bridge; @@ -62,7 +64,13 @@ class _BackgroundTaskDialogScreenState extends State pageBuilder: (context, _, __) { // watch task updates from within the dialog. try { - final task = context.watch().events.pop(); + final events = context.watch().events; + var task = events.pop(); + while (events.isNotEmpty) { + // we might have received very fast events. In that case we pop until we are at + // the latest event. + task = events.pop(); + } if (activeTask != null && task.type != activeTask!.type) { logger.w("Received another task event $task while $activeTask is still active!"); } @@ -158,6 +166,22 @@ class _BackgroundTaskDialogScreenState extends State TaskStatus.failed => const Text("Oops, something went wrong!") }, onClose: () => activeTask = null), + TaskType.closeChannel => TaskStatusDialog( + task: task!, + content: switch (task.status) { + TaskStatus.pending => const Text( + "Your channel is getting closed collaboratively.\n\nPlease do not close the app while the order is executed."), + TaskStatus.success => const Text( + "Your channel has been closed collaboratively.\n\nIf you don't see your funds as incoming on-chain transaction, try to run a full-sync (Settings > Wallet Settings)"), + TaskStatus.failed => const Text("Oops, something went wrong!") + }, + onClose: () { + activeTask = null; + // we need to delay routing a bit as we might still be processing the addPostFrameCallback. + Future.delayed(const Duration(milliseconds: 250), () { + GoRouter.of(context).go(WalletScreen.route); + }); + }), TaskType.unknown || null => null }; } diff --git a/mobile/lib/common/domain/background_task.dart b/mobile/lib/common/domain/background_task.dart index 37fad582e..50ab83a43 100644 --- a/mobile/lib/common/domain/background_task.dart +++ b/mobile/lib/common/domain/background_task.dart @@ -8,6 +8,7 @@ enum TaskType { collaborativeRevert, fullSync, recover, + closeChannel, unknown } @@ -62,39 +63,6 @@ class BackgroundTask { return bridge.BackgroundTask_Rollover(TaskStatus.apiDummy()); } - static BackgroundTask fromApi(bridge.BackgroundTask task) { - final taskType = getTaskType(task); - - final (taskStatus, error) = TaskStatus.fromApi(task.field0); - return BackgroundTask(type: taskType, status: taskStatus, error: error); - } - - static TaskType getTaskType(bridge.BackgroundTask task) { - if (task is bridge.BackgroundTask_RecoverDlc) { - return TaskType.recover; - } - if (task is bridge.BackgroundTask_Rollover) { - return TaskType.rollover; - } - if (task is bridge.BackgroundTask_CollabRevert) { - return TaskType.collaborativeRevert; - } - if (task is bridge.BackgroundTask_FullSync) { - return TaskType.fullSync; - } - if (task is bridge.BackgroundTask_AsyncTrade) { - return TaskType.asyncTrade; - } - if (task is bridge.BackgroundTask_Expire) { - return TaskType.expire; - } - if (task is bridge.BackgroundTask_Liquidate) { - return TaskType.liquidate; - } - - return TaskType.unknown; - } - @override String toString() { return "$type ($status)"; diff --git a/mobile/lib/common/settings/collab_close_screen.dart b/mobile/lib/common/settings/collab_close_screen.dart index 7db3c2dea..b70e820cc 100644 --- a/mobile/lib/common/settings/collab_close_screen.dart +++ b/mobile/lib/common/settings/collab_close_screen.dart @@ -4,7 +4,6 @@ import 'package:get_10101/common/dlc_channel_change_notifier.dart'; import 'package:get_10101/common/settings/settings_screen.dart'; import 'package:get_10101/common/snack_bar.dart'; import 'package:get_10101/features/trade/trade_screen.dart'; -import 'package:get_10101/features/wallet/wallet_screen.dart'; import 'package:go_router/go_router.dart'; import 'package:get_10101/ffi.dart' as rust; import 'package:provider/provider.dart'; @@ -106,10 +105,7 @@ class _CollabCloseScreenState extends State { ), onConfirmation: () async { final messenger = ScaffoldMessenger.of(context); - rust.api - .closeChannel() - .then((value) => GoRouter.of(context).go(WalletScreen.route)) - .catchError((e) { + rust.api.closeChannel().catchError((e) { showSnackBar( messenger, e.toString(), diff --git a/mobile/macos/Flutter/GeneratedPluginRegistrant.swift b/mobile/macos/Flutter/GeneratedPluginRegistrant.swift index e37187323..e21a78c9c 100644 --- a/mobile/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/mobile/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import firebase_core import firebase_messaging +import flutter_inappwebview_macos import flutter_local_notifications import package_info_plus import path_provider_foundation @@ -17,6 +18,7 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) + InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/mobile/native/src/api.rs b/mobile/native/src/api.rs index 8a64e8268..ab6323bcd 100644 --- a/mobile/native/src/api.rs +++ b/mobile/native/src/api.rs @@ -16,6 +16,9 @@ pub use crate::dlc_channel::SignedChannelState; use crate::emergency_kit; use crate::event; use crate::event::api::FlutterSubscriber; +use crate::event::BackgroundTask; +use crate::event::EventInternal; +use crate::event::TaskStatus; use crate::health; use crate::logger; use crate::max_quantity::max_quantity; @@ -41,6 +44,7 @@ use std::backtrace::Backtrace; use std::fmt; use std::path::Path; use std::path::PathBuf; +use std::time::Duration; use time::OffsetDateTime; use tokio::sync::broadcast; use tokio::sync::broadcast::channel; @@ -511,7 +515,26 @@ pub fn get_new_address() -> Result { #[tokio::main(flavor = "current_thread")] pub async fn close_channel() -> Result<()> { - dlc::close_channel(false).await + event::publish(&EventInternal::BackgroundNotification( + BackgroundTask::CloseChannel(TaskStatus::Pending), + )); + + let fail = |e: &anyhow::Error| { + event::publish(&EventInternal::BackgroundNotification( + BackgroundTask::CloseChannel(TaskStatus::Failed(format!("{e:#}"))), + )) + }; + + dlc::close_channel(false).await.inspect_err(fail)?; + // wait a bit so that the sync can find the the broadcasted transaction. + tokio::time::sleep(Duration::from_millis(500)).await; + dlc::refresh_wallet_info().await.inspect_err(fail)?; + + event::publish(&EventInternal::BackgroundNotification( + BackgroundTask::CloseChannel(TaskStatus::Success), + )); + + Ok(()) } #[tokio::main(flavor = "current_thread")] diff --git a/mobile/native/src/dlc/mod.rs b/mobile/native/src/dlc/mod.rs index 953530572..578669233 100644 --- a/mobile/native/src/dlc/mod.rs +++ b/mobile/native/src/dlc/mod.rs @@ -652,7 +652,9 @@ pub async fn close_channel(is_force_close: bool) -> Result<()> { node.inner .close_dlc_channel(channel_details.channel_id, is_force_close) - .await + .await?; + + Ok(()) } pub fn get_signed_dlc_channels() -> Result> { diff --git a/mobile/native/src/dlc/node.rs b/mobile/native/src/dlc/node.rs index e105c8525..2f7382cb9 100644 --- a/mobile/native/src/dlc/node.rs +++ b/mobile/native/src/dlc/node.rs @@ -413,6 +413,10 @@ impl Node { TenTenOneMessage::CollaborativeCloseOffer(TenTenOneCollaborativeCloseOffer { collaborative_close_offer: CollaborativeCloseOffer { channel_id, .. }, }) => { + event::publish(&EventInternal::BackgroundNotification( + BackgroundTask::CloseChannel(TaskStatus::Pending), + )); + let channel_id_hex_string = hex::encode(channel_id); tracing::info!( channel_id = channel_id_hex_string, @@ -422,7 +426,16 @@ impl Node { // TODO(bonomat): we should verify that the proposed amount is acceptable self.inner - .accept_dlc_channel_collaborative_close(&channel_id)?; + .accept_dlc_channel_collaborative_close(&channel_id) + .inspect_err(|e| { + event::publish(&EventInternal::BackgroundNotification( + BackgroundTask::CloseChannel(TaskStatus::Failed(format!("{e:#}"))), + )) + })?; + + event::publish(&EventInternal::BackgroundNotification( + BackgroundTask::CloseChannel(TaskStatus::Success), + )); } _ => (), } diff --git a/mobile/native/src/event/api.rs b/mobile/native/src/event/api.rs index 41d1c0982..e675cf798 100644 --- a/mobile/native/src/event/api.rs +++ b/mobile/native/src/event/api.rs @@ -50,6 +50,8 @@ pub enum BackgroundTask { CollabRevert(TaskStatus), /// The app is performing a full sync of the on-chain wallet. FullSync(TaskStatus), + /// The app is closing its dlc channel + CloseChannel(TaskStatus), } impl From for Event { @@ -150,6 +152,9 @@ impl From for BackgroundTask { BackgroundTask::CollabRevert(status.into()) } event::BackgroundTask::FullSync(status) => BackgroundTask::FullSync(status.into()), + event::BackgroundTask::CloseChannel(status) => { + BackgroundTask::CloseChannel(status.into()) + } } } } diff --git a/mobile/native/src/event/mod.rs b/mobile/native/src/event/mod.rs index a3ee38c45..183992712 100644 --- a/mobile/native/src/event/mod.rs +++ b/mobile/native/src/event/mod.rs @@ -50,6 +50,7 @@ pub enum BackgroundTask { CollabRevert(TaskStatus), RecoverDlc(TaskStatus), FullSync(TaskStatus), + CloseChannel(TaskStatus), } #[derive(Clone, Debug)]