diff --git a/taptrade-cli-demo/coordinator/src/communication/handler_errors.rs b/taptrade-cli-demo/coordinator/src/communication/handler_errors.rs index 7df1a8f..066303b 100644 --- a/taptrade-cli-demo/coordinator/src/communication/handler_errors.rs +++ b/taptrade-cli-demo/coordinator/src/communication/handler_errors.rs @@ -22,4 +22,6 @@ pub enum RequestError { Database(String), NotConfirmed, NotFound, + PsbtAlreadySubmitted, + PsbtInvalid(String), } diff --git a/taptrade-cli-demo/coordinator/src/communication/mod.rs b/taptrade-cli-demo/coordinator/src/communication/mod.rs index ee0f65f..0bac4d0 100755 --- a/taptrade-cli-demo/coordinator/src/communication/mod.rs +++ b/taptrade-cli-demo/coordinator/src/communication/mod.rs @@ -121,8 +121,27 @@ async fn submit_escrow_psbt( Extension(coordinator): Extension>, Json(payload): Json, ) -> Result { - panic!("implement") - + debug!("\n\nReceived signed escrow psbt: {:?}", payload); + match handle_signed_escrow_psbt(&payload, coordinator).await { + Ok(()) => Ok(StatusCode::OK.into_response()), + Err(RequestError::PsbtAlreadySubmitted) => { + Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()) + } + Err(RequestError::PsbtInvalid) => Ok(StatusCode::NOT_ACCEPTABLE.into_response()), + _ => Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()), + // Err(RequestError::NotFound) => { + // info!("Offer for escrow psbt not found"); + // Ok(StatusCode::NOT_FOUND.into_response()) + // } + // Err(RequestError::NotConfirmed) => { + // info!("Offer for escrow psbt not confirmed"); + // Ok(StatusCode::NOT_ACCEPTABLE.into_response()) + // } + // Err(RequestError::Database(e)) => { + // error!("Database error fetching escrow psbt: {e}"); + // Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()) + // } + } // check if psbt is correct, valid and signed // publish psbt if it is correct // return 200 if everything is correct diff --git a/taptrade-cli-demo/coordinator/src/coordinator/mod.rs b/taptrade-cli-demo/coordinator/src/coordinator/mod.rs index f34e936..a947c8e 100755 --- a/taptrade-cli-demo/coordinator/src/coordinator/mod.rs +++ b/taptrade-cli-demo/coordinator/src/coordinator/mod.rs @@ -4,8 +4,6 @@ pub mod create_taproot; pub mod mempool_monitoring; pub mod tx_confirmation_monitoring; -use axum::routing::trace; - use self::coordinator_utils::*; use super::*; @@ -224,6 +222,55 @@ pub async fn fetch_escrow_confirmation_status( } } +pub async fn handle_signed_escrow_psbt( + payload: &PsbtSubmissionRequest, + coordinator: Arc, +) -> Result<(), RequestError> { + let database = &coordinator.coordinator_db; + let wallet = &coordinator.coordinator_wallet; + + match database + .is_valid_robohash_in_table(&payload.robohash_hex, &payload.offer_id_hex) + .await + { + Ok(false) => return Err(RequestError::NotFound), + Ok(true) => (), + Err(e) => return Err(RequestError::Database(e.to_string())), + }; + + match wallet + .validate_escrow_init_psbt(&payload.signed_psbt_hex) + .await + { + Ok(()) => (), + Err(e) => return Err(RequestError::PsbtInvalid(e.to_string())), + }; + match database.insert_signed_escrow_psbt(payload).await { + Ok(false) => return Err(RequestError::PsbtAlreadySubmitted), + Ok(true) => (), + Err(e) => return Err(RequestError::Database(e.to_string())), + }; + + // check if both signed parts are there, if so, combine and broadcast + let (maker_psbt, taker_psbt) = match database + .fetch_both_signed_escrow_psbts(&payload.offer_id_hex) + .await + { + Ok(Some((maker_psbt, taker_psbt))) => (maker_psbt, taker_psbt), + Ok(None) => return Ok(()), + Err(e) => return Err(RequestError::Database(e.to_string())), + }; + + if let Err(e) = wallet + .combine_and_broadcast_escrow_psbt(&maker_psbt, &taker_psbt) + .await + { + return Err(RequestError::PsbtInvalid(e.to_string())); + } + + Ok(()) +} + pub async fn handle_obligation_confirmation( payload: &OfferTakenRequest, coordinator: Arc, diff --git a/taptrade-cli-demo/coordinator/src/database/mod.rs b/taptrade-cli-demo/coordinator/src/database/mod.rs index d02dc53..a14951e 100644 --- a/taptrade-cli-demo/coordinator/src/database/mod.rs +++ b/taptrade-cli-demo/coordinator/src/database/mod.rs @@ -3,6 +3,7 @@ mod db_tests; use anyhow::Context; use futures_util::StreamExt; +use serde::de::IntoDeserializer; use super::*; use bdk::bitcoin::address::Address; @@ -137,6 +138,8 @@ impl CoordinatorDB { musig_pubkey_compressed_hex_taker TEXT NOT NULL, escrow_psbt_hex TEXT NOT NULL, escrow_psbt_txid TEXT NOT NULL, + signed_escrow_psbt_hex_maker TEXT, + signed_escrow_psbt_hex_taker TEXT, escrow_psbt_is_confirmed INTEGER, maker_happy INTEGER, taker_happy INTEGER, @@ -611,22 +614,8 @@ impl CoordinatorDB { Ok(robohash.is_some()) } - pub async fn fetch_escrow_tx_confirmation_status(&self, offer_id: &str) -> Result { - let status = - sqlx::query("SELECT escrow_psbt_is_confirmed FROM taken_offers WHERE offer_id = ?") - .bind(offer_id) - .fetch_one(&*self.db_pool) - .await?; - Ok(status.get::("escrow_psbt_is_confirmed") == 1) - } - - pub async fn set_trader_happy_field( - &self, - offer_id: &str, - robohash: &str, - is_happy: bool, - ) -> Result<()> { - let robohash_bytes = hex::decode(robohash)?; + async fn is_maker_in_taken_offers(&self, offer_id: &str, robohash_hex: &str) -> Result { + let robohash_bytes = hex::decode(robohash_hex)?; // First, check if the robohash matches the maker or taker let row = sqlx::query( @@ -642,6 +631,100 @@ impl CoordinatorDB { if !is_maker && !is_taker { return Err(anyhow::anyhow!("Robohash does not match maker or taker")); } + Ok(is_maker) + } + + pub async fn insert_signed_escrow_psbt( + &self, + signed_escow_psbt_data: &PsbtSubmissionRequest, + ) -> Result { + // first check if the escrow psbt has already been submitted + let is_maker = self + .is_maker_in_taken_offers( + &signed_escow_psbt_data.offer_id_hex, + &signed_escow_psbt_data.robohash_hex, + ) + .await?; + + let is_already_there = match is_maker { + true => { + let status = sqlx::query( + "SELECT signed_escrow_psbt_hex_maker FROM taken_offers WHERE offer_id = ?", + ) + .bind(&signed_escow_psbt_data.offer_id_hex) + .fetch_one(&*self.db_pool) + .await?; + status + .get::, _>("signed_escrow_psbt_hex_maker") + .is_some() + } + false => { + let status = sqlx::query( + "SELECT signed_escrow_psbt_hex_taker FROM taken_offers WHERE offer_id = ?", + ) + .bind(&signed_escow_psbt_data.offer_id_hex) + .fetch_one(&*self.db_pool) + .await?; + status + .get::, _>("signed_escrow_psbt_hex_taker") + .is_some() + } + }; + + if is_already_there { + return Ok(false); + } else { + let query = if is_maker { + "UPDATE taken_offers SET signed_escrow_psbt_hex_maker = ? WHERE offer_id = ?" + } else { + "UPDATE taken_offers SET signed_escrow_psbt_hex_taker = ? WHERE offer_id = ?" + }; + + sqlx::query(query) + .bind(&signed_escow_psbt_data.signed_psbt_hex) + .bind(&signed_escow_psbt_data.offer_id_hex) + .execute(&*self.db_pool) + .await?; + Ok(true) + } + } + + pub async fn fetch_both_signed_escrow_psbts( + &self, + offer_id_hex: &str, + ) -> Result> { + let row = sqlx::query( + "SELECT signed_escrow_psbt_hex_maker, signed_escrow_psbt_hex_taker FROM taken_offers WHERE offer_id = ?", + ) + .bind(offer_id_hex) + .fetch_one(&*self.db_pool) + .await?; + + let maker_psbt: Option = row.try_get("signed_escrow_psbt_hex_maker")?; + let taker_psbt: Option = row.try_get("signed_escrow_psbt_hex_taker")?; + + Ok(match (maker_psbt, taker_psbt) { + (Some(maker), Some(taker)) => Some((maker, taker)), + _ => None, + }) + } + + pub async fn fetch_escrow_tx_confirmation_status(&self, offer_id: &str) -> Result { + let status = + sqlx::query("SELECT escrow_psbt_is_confirmed FROM taken_offers WHERE offer_id = ?") + .bind(offer_id) + .fetch_one(&*self.db_pool) + .await?; + Ok(status.get::("escrow_psbt_is_confirmed") == 1) + } + + pub async fn set_trader_happy_field( + &self, + offer_id: &str, + robohash: &str, + is_happy: bool, + ) -> Result<()> { + let is_maker = self.is_maker_in_taken_offers(offer_id, robohash).await?; let query = if is_maker { "UPDATE taken_offers SET maker_happy = ? WHERE offer_id = ?" diff --git a/taptrade-cli-demo/coordinator/src/wallet/escrow_psbt.rs b/taptrade-cli-demo/coordinator/src/wallet/escrow_psbt.rs index 9193d45..41acc52 100644 --- a/taptrade-cli-demo/coordinator/src/wallet/escrow_psbt.rs +++ b/taptrade-cli-demo/coordinator/src/wallet/escrow_psbt.rs @@ -3,7 +3,9 @@ use bdk::{ bitcoin::psbt::Input, descriptor::Descriptor, miniscript::{descriptor::TapTree, policy::Concrete, Tap}, + SignOptions, }; +use bitcoin::psbt::PartiallySignedTransaction; use musig2::{secp256k1::PublicKey as MuSig2PubKey, KeyAggContext}; #[derive(Debug)] @@ -221,4 +223,33 @@ impl CoordinatorWallet { escrow_fee_sat_per_participant, }) } + + pub async fn validate_escrow_init_psbt(&self, escrow_init_psbt: &str) -> Result<()> { + warn!("Implement escrow psbt validation. For now, returning Ok"); + Ok(()) + } + + pub async fn combine_and_broadcast_escrow_psbt( + &self, + signed_maker_psbt_hex: &str, + signed_taker_psbt_hex: &str, + ) -> Result<()> { + let mut maker_psbt = PartiallySignedTransaction::from_str(signed_maker_psbt_hex)?; + let taker_psbt = PartiallySignedTransaction::from_str(signed_taker_psbt_hex)?; + + maker_psbt.combine(&taker_psbt)?; + + let wallet = self.wallet.lock().await; + match wallet.finalize_psbt(&mut maker_psbt, SignOptions::default()) { + Ok(true) => { + let tx = maker_psbt.extract_tx(); + let tx_hex = tx.to_string(); + self.backend.broadcast(&tx)?; + info!("Escrow transaction broadcasted: {}", tx.txid()); + Ok(()) + } + Ok(false) => Err(anyhow!("Failed to finalize escrow psbt")), + Err(e) => Err(anyhow!("Error finalizing escrow psbt: {}", e)), + } + } }