diff --git a/taptrade-cli-demo/coordinator/src/communication/mod.rs b/taptrade-cli-demo/coordinator/src/communication/mod.rs index 0962745..deae6c6 100755 --- a/taptrade-cli-demo/coordinator/src/communication/mod.rs +++ b/taptrade-cli-demo/coordinator/src/communication/mod.rs @@ -244,8 +244,12 @@ async fn poll_final_payout( error!("Database error fetching final payout: {e}"); Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()) } - _ => { - error!("Unknown error handling poll_final_payout()"); + Err(RequestError::CoordinatorError(e)) => { + error!("Coordinator error handling final payout: {e}"); + Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()) + } + e => { + error!("Unknown error handling poll_final_payout(): {:?}", e); Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()) } } @@ -274,8 +278,8 @@ async fn submit_payout_signature( // error!("Database error fetching final payout: {e}"); // Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()) // } - _ => { - error!("Unknown error handling submit_payout_signature()"); + e => { + error!("Unknown error handling submit_payout_signature(): {:?}", e); Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()) } } diff --git a/taptrade-cli-demo/coordinator/src/coordinator/coordinator_utils.rs b/taptrade-cli-demo/coordinator/src/coordinator/coordinator_utils.rs index 35217ef..2521559 100644 --- a/taptrade-cli-demo/coordinator/src/coordinator/coordinator_utils.rs +++ b/taptrade-cli-demo/coordinator/src/coordinator/coordinator_utils.rs @@ -66,7 +66,7 @@ impl KeyspendContext { wallet::aggregate_musig_pubkeys(maker_pk, taker_pk)?; let agg_nonce: MusigAggNonce = coordinator_utils::agg_hex_musig_nonces(maker_nonce, taker_nonce)?; - let keyspend_psbt = PartiallySignedTransaction::from_str(keyspend_psbt)?; + let keyspend_psbt = PartiallySignedTransaction::deserialize(&hex::decode(keyspend_psbt)?)?; let partial_maker_sig = PartialSignature::from_hex(maker_sig)?; let partial_taker_sig = PartialSignature::from_hex(taker_sig)?; diff --git a/taptrade-cli-demo/coordinator/src/coordinator/mod.rs b/taptrade-cli-demo/coordinator/src/coordinator/mod.rs index 8d7f07b..e46aaf7 100755 --- a/taptrade-cli-demo/coordinator/src/coordinator/mod.rs +++ b/taptrade-cli-demo/coordinator/src/coordinator/mod.rs @@ -13,11 +13,10 @@ pub async fn process_order( let wallet = &coordinator.coordinator_wallet; let database = &coordinator.coordinator_db; - let bond_address = wallet.get_new_address().await?; - let locking_amount_sat = offer.amount_satoshi * offer.bond_ratio as u64 / 100; + let locking_amount_sat = offer.amount_satoshi * u64::from(offer.bond_ratio) / 100; let bond_requirements = BondRequirementResponse { - bond_address, + bond_address: wallet.get_new_address().await?, locking_amount_sat, }; @@ -272,17 +271,36 @@ pub async fn handle_final_payout( .await .map_err(|e| RequestError::Database(e.to_string()))?; - let payout_keyspend_psbt_hex = coordinator - .coordinator_wallet - .assemble_keyspend_payout_psbt(&escrow_payout_data) + let payout_keyspend_psbt_hex = if let Some(payout_psbt_hex) = database + .fetch_keyspend_payout_psbt(&payload.offer_id_hex) .await - .map_err(|e| RequestError::CoordinatorError(e.to_string()))?; - - database - .insert_keyspend_payout_psbt(&payload.offer_id_hex, &payout_keyspend_psbt_hex) - .await - .map_err(|e| RequestError::Database(e.to_string()))?; + .map_err(|e| RequestError::Database(e.to_string()))? + { + payout_psbt_hex + } else { + if !database + .toggle_processing(&payload.offer_id_hex) + .await + .map_err(|e| RequestError::Database(e.to_string()))? + { + return Ok(PayoutProcessingResult::NotReady); + } + let payout_keyspend_psbt_hex = coordinator + .coordinator_wallet + .assemble_keyspend_payout_psbt(&escrow_payout_data) + .await + .map_err(|e| RequestError::CoordinatorError(e.to_string()))?; + database + .insert_keyspend_payout_psbt(&payload.offer_id_hex, &payout_keyspend_psbt_hex) + .await + .map_err(|e| RequestError::Database(e.to_string()))?; + database + .toggle_processing(&payload.offer_id_hex) + .await + .map_err(|e| RequestError::Database(e.to_string()))?; + payout_keyspend_psbt_hex + }; return Ok(PayoutProcessingResult::ReadyPSBT(PayoutResponse { payout_psbt_hex: payout_keyspend_psbt_hex, agg_musig_nonce_hex: escrow_payout_data.agg_musig_nonce.to_string(), @@ -327,7 +345,7 @@ pub async fn handle_payout_signature( coordinator: Arc, ) -> Result { let database = &coordinator.coordinator_db; - let _wallet = &coordinator.coordinator_wallet; + // let _wallet = &coordinator.coordinator_wallet; check_offer_and_confirmation(&payload.offer_id_hex, &payload.robohash_hex, database).await?; @@ -340,16 +358,16 @@ pub async fn handle_payout_signature( .await .map_err(|e| RequestError::Database(e.to_string()))?; - let keyspend_information = if let Some(keyspend_context) = database + let keyspend_information = match database .fetch_keyspend_payout_information(&payload.offer_id_hex) .await .map_err(|e| RequestError::Database(e.to_string()))? { - keyspend_context - } else { - return Ok(false); + Some(context) => context, + None => return Ok(false), }; - dbg!("Keyspend info: {}", keyspend_information); + + debug!("Keyspend info: {:?}", keyspend_information); warn!("Use musig2 validate partial sig to validate sigs before using to blame users providing wrong sigs"); Ok(true) diff --git a/taptrade-cli-demo/coordinator/src/database/mod.rs b/taptrade-cli-demo/coordinator/src/database/mod.rs index cf4b120..29eed09 100644 --- a/taptrade-cli-demo/coordinator/src/database/mod.rs +++ b/taptrade-cli-demo/coordinator/src/database/mod.rs @@ -1,6 +1,8 @@ #[cfg(test)] mod db_tests; +use axum::routing::trace; + use super::*; #[derive(Clone, Debug)] @@ -144,7 +146,8 @@ impl CoordinatorDB { escrow_amount_taker_sat INTEGER, escrow_fee_per_participant INTEGER, escrow_output_descriptor TEXT, - payout_transaction_psbt_hex TEXT + payout_transaction_psbt_hex TEXT, + processing INTEGER NOT NULL )", // escrow_psbt_is_confirmed will be set 1 once the escrow psbt is confirmed onchain ) .execute(&db_pool) @@ -374,8 +377,8 @@ impl CoordinatorDB { bond_ratio, offer_duration_ts, bond_address_maker, bond_address_taker, bond_amount_sat, bond_tx_hex_maker, bond_tx_hex_taker, payout_address_maker, payout_address_taker, taproot_xonly_pubkey_hex_maker, taproot_xonly_pubkey_hex_taker, musig_pub_nonce_hex_maker, musig_pubkey_compressed_hex_maker, musig_pub_nonce_hex_taker, musig_pubkey_compressed_hex_taker, escrow_psbt_hex, escrow_psbt_txid, escrow_output_descriptor, escrow_psbt_is_confirmed, escrow_ongoing, - escrow_taproot_pk_coordinator, escrow_amount_maker_sat, escrow_amount_taker_sat, escrow_fee_per_participant) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + escrow_taproot_pk_coordinator, escrow_amount_maker_sat, escrow_amount_taker_sat, escrow_fee_per_participant, processing) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ) .bind(public_offer.offer_id) .bind(public_offer.robohash_maker) @@ -406,6 +409,7 @@ impl CoordinatorDB { .bind(escrow_tx_data.escrow_amount_maker_sat as i64) .bind(escrow_tx_data.escrow_amount_taker_sat as i64) .bind(escrow_tx_data.escrow_fee_sat_per_participant as i64) + .bind(0) .execute(&*self.db_pool) .await?; @@ -912,24 +916,24 @@ impl CoordinatorDB { let is_already_there = match is_maker { true => { let status = sqlx::query( - "SELECT musig_partial_sig_maker FROM taken_offers WHERE offer_id = ?", + "SELECT musig_partial_sig_hex_maker FROM taken_offers WHERE offer_id = ?", ) .bind(offer_id_hex) .fetch_one(&*self.db_pool) .await?; status - .get::, _>("musig_partial_sig_maker") + .get::, _>("musig_partial_sig_hex_maker") .is_some() } false => { let status = sqlx::query( - "SELECT musig_partial_sig_taker FROM taken_offers WHERE offer_id = ?", + "SELECT musig_partial_sig_hex_taker FROM taken_offers WHERE offer_id = ?", ) .bind(offer_id_hex) .fetch_one(&*self.db_pool) .await?; status - .get::, _>("musig_partial_sig_taker") + .get::, _>("musig_partial_sig_hex_taker") .is_some() } }; @@ -939,9 +943,9 @@ impl CoordinatorDB { return Err(anyhow!("Partial sig already submitted")); } else { let query = if is_maker { - "UPDATE taken_offers SET musig_partial_sig_maker = ? WHERE offer_id = ?" + "UPDATE taken_offers SET musig_partial_sig_hex_maker = ? WHERE offer_id = ?" } else { - "UPDATE taken_offers SET musig_partial_sig_taker = ? WHERE offer_id = ?" + "UPDATE taken_offers SET musig_partial_sig_hex_taker = ? WHERE offer_id = ?" }; sqlx::query(query) .bind(partial_sig_hex) @@ -957,13 +961,13 @@ impl CoordinatorDB { offer_id_hex: &str, ) -> Result> { let row = sqlx::query( - "SELECT musig_partial_sig_maker, musig_partial_sig_taker, + "SELECT musig_partial_sig_hex_maker, musig_partial_sig_hex_taker, musig_pubkey_compressed_hex_maker, musig_pubkey_compressed_hex_taker, musig_pub_nonce_hex_maker, musig_pub_nonce_hex_taker, payout_transaction_psbt_hex FROM taken_offers WHERE offer_id = ?", ).bind(offer_id_hex).fetch_one(&*self.db_pool).await?; - let maker_sig: Option = row.try_get("musig_partial_sig_maker")?; - let taker_sig: Option = row.try_get("musig_partial_sig_taker")?; + let maker_sig: Option = row.try_get("musig_partial_sig_hex_maker")?; + let taker_sig: Option = row.try_get("musig_partial_sig_hex_taker")?; let maker_pubkey: String = row.try_get("musig_pubkey_compressed_hex_maker")?; let taker_pubkey: String = row.try_get("musig_pubkey_compressed_hex_taker")?; @@ -987,4 +991,35 @@ impl CoordinatorDB { Ok(None) } } + + pub async fn fetch_keyspend_payout_psbt(&self, offer_id_hex: &str) -> Result> { + let row = + sqlx::query("SELECT payout_transaction_psbt_hex FROM taken_offers WHERE offer_id = ?") + .bind(offer_id_hex) + .fetch_one(&*self.db_pool) + .await?; + + let payout_psbt: Option = row.try_get("payout_transaction_psbt_hex")?; + Ok(payout_psbt) + } + + pub async fn toggle_processing(&self, offer_id: &str) -> Result { + let result = sqlx::query( + r#" + UPDATE taken_offers + SET processing = CASE + WHEN processing = 0 THEN 1 + ELSE 0 + END + WHERE offer_id = ? + RETURNING processing + "#, + ) + .bind(offer_id) + .fetch_one(&*self.db_pool) + .await?; + + trace!("Toggled processing status for offer {}", offer_id); + Ok(result.get::(0) == 1) + } } diff --git a/taptrade-cli-demo/coordinator/src/wallet/escrow_psbt.rs b/taptrade-cli-demo/coordinator/src/wallet/escrow_psbt.rs index 3e60d55..0818fac 100644 --- a/taptrade-cli-demo/coordinator/src/wallet/escrow_psbt.rs +++ b/taptrade-cli-demo/coordinator/src/wallet/escrow_psbt.rs @@ -30,16 +30,16 @@ pub fn aggregate_musig_pubkeys( maker_musig_pubkey: &str, taker_musig_pubkey: &str, ) -> Result { - debug!( - "Aggregating musig pubkeys: {} and {}", - maker_musig_pubkey, taker_musig_pubkey - ); let pubkeys: [MuSig2PubKey; 2] = [ MuSig2PubKey::from_str(maker_musig_pubkey).context("Error parsing musig pk 1")?, MuSig2PubKey::from_str(taker_musig_pubkey).context("Error parsing musig pk 2")?, ]; let key_agg_ctx = KeyAggContext::new(pubkeys).context("Error aggregating musig pubkeys")?; + debug!( + "Aggregating musig pubkeys: {} and {} to {:?}", + maker_musig_pubkey, taker_musig_pubkey, key_agg_ctx + ); Ok(key_agg_ctx) } diff --git a/taptrade-cli-demo/coordinator/src/wallet/payout_tx.rs b/taptrade-cli-demo/coordinator/src/wallet/payout_tx.rs index 49a6703..6bc6bc2 100644 --- a/taptrade-cli-demo/coordinator/src/wallet/payout_tx.rs +++ b/taptrade-cli-demo/coordinator/src/wallet/payout_tx.rs @@ -1,8 +1,16 @@ +use bdk::FeeRate; + /// construction of the transaction spending the escrow output after a successfull trade as keyspend transaction use super::*; fn get_tx_fees_abs_sat(blockchain_backend: &RpcBlockchain) -> Result<(u64, u64)> { - let feerate = blockchain_backend.estimate_fee(6)?; + let feerate = match blockchain_backend.estimate_fee(6) { + Ok(feerate) => feerate, + Err(e) => { + error!("Failed to estimate fee: {}. Using fallback 40 sat/vb`", e); + FeeRate::from_sat_per_vb(40.0) + } + }; let keyspend_payout_tx_size_vb = 140; // ~, always 1 input, 2 outputs let tx_fee_abs = feerate.fee_vb(keyspend_payout_tx_size_vb); @@ -10,13 +18,6 @@ fn get_tx_fees_abs_sat(blockchain_backend: &RpcBlockchain) -> Result<(u64, u64)> Ok((tx_fee_abs, tx_fee_abs / 2)) } -// pub fn aggregate_partial_signatures( -// maker_sig_hex: &str, -// taker_sig_hex: &str, -// ) -> anyhow::Result { -// Ok(()) -// } - impl CoordinatorWallet { fn get_escrow_utxo( &self, diff --git a/taptrade-cli-demo/rpc_node/regtest/data/mine-blocks.sh b/taptrade-cli-demo/rpc_node/regtest/data/mine-blocks.sh index 73d2f3f..2e9ae2e 100644 --- a/taptrade-cli-demo/rpc_node/regtest/data/mine-blocks.sh +++ b/taptrade-cli-demo/rpc_node/regtest/data/mine-blocks.sh @@ -17,8 +17,8 @@ else bitcoin-cli -regtest -datadir="/home/bitcoin/.bitcoin" -rpcwallet="coordinator_wallet" -generate 101 fi -# Generate a block every 120 seconds +# Generate a block every 10 seconds while true; do bitcoin-cli -regtest -datadir="/home/bitcoin/.bitcoin" -rpcwallet="coordinator_wallet" -generate 1 - sleep 120 -done \ No newline at end of file + sleep 10 +done diff --git a/taptrade-cli-demo/trader/src/communication/api.rs b/taptrade-cli-demo/trader/src/communication/api.rs index aecc01f..6f09b95 100644 --- a/taptrade-cli-demo/trader/src/communication/api.rs +++ b/taptrade-cli-demo/trader/src/communication/api.rs @@ -121,3 +121,10 @@ pub struct TradeObligationsUnsatisfied { pub robohash_hex: String, pub offer_id_hex: String, } + +#[derive(Debug, Serialize)] +pub struct PayoutSignatureRequest { + pub partial_sig_hex: String, + pub offer_id_hex: String, + pub robohash_hex: String, +} diff --git a/taptrade-cli-demo/trader/src/communication/mod.rs b/taptrade-cli-demo/trader/src/communication/mod.rs index ad51b48..8b8a08d 100644 --- a/taptrade-cli-demo/trader/src/communication/mod.rs +++ b/taptrade-cli-demo/trader/src/communication/mod.rs @@ -292,10 +292,12 @@ impl IsOfferReadyRequest { } } let payout_response: PayoutResponse = res.json()?; - let final_psbt = PartiallySignedTransaction::from_str(&payout_response.payout_psbt_hex)?; + let final_psbt = PartiallySignedTransaction::deserialize(&hex::decode( + &payout_response.payout_psbt_hex, + )?)?; let agg_nonce = AggNonce::from_str(&payout_response.agg_musig_nonce_hex) .map_err(|e| anyhow!("Error parsing agg nonce: {}", e))?; - let agg_pubk_ctx = KeyAggContext::from_hex(&payout_response.agg_musig_nonce_hex) + let agg_pubk_ctx = KeyAggContext::from_hex(&payout_response.agg_musig_pubkey_ctx_hex) .map_err(|e| anyhow!("Error parsing agg pubkey ctx: {}", e))?; Ok((final_psbt, agg_nonce, agg_pubk_ctx)) } @@ -325,3 +327,29 @@ impl TradeObligationsUnsatisfied { Ok(()) } } + +impl PayoutSignatureRequest { + pub fn send(trader_config: &TraderSettings, signature: &str, offer_id_hex: &str) -> Result<()> { + let request = Self { + robohash_hex: trader_config.robosats_robohash_hex.clone(), + offer_id_hex: offer_id_hex.to_string(), + partial_sig_hex: signature.to_string(), + }; + + let client = reqwest::blocking::Client::new(); + let res = client + .post(format!( + "{}{}", + trader_config.coordinator_endpoint, "/submit-payout-signature" + )) + .json(&request) + .send()?; + if res.status() != 200 && res.status() != 202 { + return Err(anyhow!( + "Submitting trade obligations unsatisfied failed. Status: {}", + res.status() + )); + } + Ok(()) + } +} diff --git a/taptrade-cli-demo/trader/src/trading/mod.rs b/taptrade-cli-demo/trader/src/trading/mod.rs index 151c12d..6853774 100644 --- a/taptrade-cli-demo/trader/src/trading/mod.rs +++ b/taptrade-cli-demo/trader/src/trading/mod.rs @@ -22,6 +22,7 @@ use bdk::{ database::MemoryDatabase, wallet::AddressInfo, }; +use communication::api::PayoutSignatureRequest; use reqwest::header::ACCEPT_LANGUAGE; use std::{str::FromStr, thread, time::Duration}; @@ -54,16 +55,16 @@ pub fn run_maker(maker_config: &TraderSettings) -> Result<()> { let (payout_keyspend_psbt, agg_pub_nonce, agg_pubk_ctx) = IsOfferReadyRequest::poll_payout(maker_config, &offer)?; debug!("Payout PSBT received: {}", &payout_keyspend_psbt); - let signed_payout_psbt = wallet + let signature = wallet .validate_payout_psbt(&payout_keyspend_psbt)? - .sign_keyspend_payout_psbt( + .create_keyspend_payout_signature( payout_keyspend_psbt, agg_pubk_ctx, agg_pub_nonce, offer.used_musig_config, )?; // submit signed payout psbt back to coordinator - panic!("Payout to be implemented!"); + PayoutSignatureRequest::send(&maker_config, &signature, &offer.offer_id_hex)?; } else { error!("Trade failed. Initiating escrow mode."); TradeObligationsUnsatisfied::request_escrow(&offer.offer_id_hex, maker_config)?; @@ -96,14 +97,17 @@ pub fn run_taker(taker_config: &TraderSettings) -> Result<()> { IsOfferReadyRequest::poll_payout(taker_config, &accepted_offer)?; debug!("Received payout psbt: {}", &payout_keyspend_psbt); - let signed_payout_psbt = wallet + let signature = wallet .validate_payout_psbt(&payout_keyspend_psbt)? - .sign_keyspend_payout_psbt( + .create_keyspend_payout_signature( payout_keyspend_psbt, agg_pubk_ctx, agg_pub_nonce, accepted_offer.used_musig_config, )?; + + // submit partial signature back to coordinator + PayoutSignatureRequest::send(&taker_config, &signature, &accepted_offer.offer_id_hex)?; // here we need to handle if the other party is not cooperating } else { error!("Trade failed."); diff --git a/taptrade-cli-demo/trader/src/wallet/mod.rs b/taptrade-cli-demo/trader/src/wallet/mod.rs index b293a14..127c617 100644 --- a/taptrade-cli-demo/trader/src/wallet/mod.rs +++ b/taptrade-cli-demo/trader/src/wallet/mod.rs @@ -213,7 +213,7 @@ impl TradingWallet { /// creates a partial signature to spend the keyspend path of the escrow output /// which will be returned to the coordinator for aggregation - pub fn sign_keyspend_payout_psbt( + pub fn create_keyspend_payout_signature( &self, validated_payout_psbt: PartiallySignedTransaction, key_agg_context: KeyAggContext,