diff --git a/taptrade-cli-demo/coordinator/src/communication/api.rs b/taptrade-cli-demo/coordinator/src/communication/api.rs index 7912101..af63783 100644 --- a/taptrade-cli-demo/coordinator/src/communication/api.rs +++ b/taptrade-cli-demo/coordinator/src/communication/api.rs @@ -47,9 +47,23 @@ pub struct OffersRequest { pub struct PublicOffer { pub amount_sat: u64, pub offer_id_hex: String, + pub required_bond_amount_sat: u64, + pub bond_locking_address: String, } #[derive(Deserialize, Serialize, Debug)] pub struct PublicOffers { pub offers: Option>, // don't include offers var in return json if no offers are available } + +#[derive(Serialize, Debug, Deserialize)] +pub struct OfferTakenResponse { + pub trade_psbt_hex_to_sign: String, +} + +// request to receive the escrow psbt to sign for the specified offer to take it +#[derive(Debug, Serialize, Deserialize)] +pub struct OfferPsbtRequest { + pub offer: PublicOffer, + pub trade_data: BondSubmissionRequest, +} diff --git a/taptrade-cli-demo/coordinator/src/communication/mod.rs b/taptrade-cli-demo/coordinator/src/communication/mod.rs index fc81389..bfdac85 100755 --- a/taptrade-cli-demo/coordinator/src/communication/mod.rs +++ b/taptrade-cli-demo/coordinator/src/communication/mod.rs @@ -44,18 +44,22 @@ async fn submit_maker_bond( Extension(database): Extension, Extension(wallet): Extension, Json(payload): Json, -) -> Result, AppError> { +) -> Result { let bond_requirements = database.fetch_maker_request(&payload.robohash_hex).await?; - let offer_id_hex = generate_random_order_id(16); // 16 bytes random offer id, maybe a different system makes more sense later on? (uuid or increasing counter...) // validate bond (check amounts, valid inputs, correct addresses, valid signature, feerate) - wallet + if !wallet .validate_bond_tx_hex(&payload.signed_bond_hex, &bond_requirements) - .await?; - + .await? + { + return Ok(StatusCode::NOT_ACCEPTABLE.into_response()); + } + let offer_id_hex = generate_random_order_id(16); // 16 bytes random offer id, maybe a different system makes more sense later on? (uuid or increasing counter...) + // create address for taker bond + let new_taker_bond_address = wallet.get_new_address().await?; // insert bond into sql database and move offer to different table let bond_locked_until_timestamp = database - .move_offer_to_active(&payload, &offer_id_hex) + .move_offer_to_active(&payload, &offer_id_hex, new_taker_bond_address) .await?; // begin monitoring bond -> async loop monitoring bonds in sql table "active_maker_offers" -> see ../coordinator/monitoring.rs @@ -65,7 +69,8 @@ async fn submit_maker_bond( Ok(Json(OrderActivatedResponse { bond_locked_until_timestamp, offer_id_hex, - })) + }) + .into_response()) } async fn fetch_available_offers( @@ -77,11 +82,38 @@ async fn fetch_available_offers( Ok(Json(PublicOffers { offers })) } +async fn submit_taker_bond( + Extension(database): Extension, + Extension(wallet): Extension, + Json(payload): Json, +) -> Result { + let bond_requirements = database + .fetch_taker_bond_requirements(&payload.offer.offer_id_hex) + .await; + match bond_requirements { + Ok(bond_requirements) => { + if !wallet + .validate_bond_tx_hex(&payload.trade_data.signed_bond_hex, &bond_requirements) + .await? + { + dbg!("Taker Bond validation failed"); + return Ok(StatusCode::NOT_ACCEPTABLE.into_response()); + } + } + Err(_) => return Ok(StatusCode::NOT_FOUND.into_response()), + } + database.add_taker_info_and_move_table(&payload).await?; + Ok(Json(OfferTakenResponse { trade_psbt_hex }).into_response()) +} + pub async fn api_server(database: CoordinatorDB, wallet: CoordinatorWallet) -> Result<()> { let app = Router::new() .route("/create-offer", post(receive_order)) .route("/submit-maker-bond", post(submit_maker_bond)) .route("/fetch-available-offers", post(fetch_available_offers)) + .route("/submit-taker-bond", post(submit_taker_bond)) + // submit-taker-bond + // request-offer-status .layer(Extension(database)) .layer(Extension(wallet)); // add other routes here diff --git a/taptrade-cli-demo/coordinator/src/database/mod.rs b/taptrade-cli-demo/coordinator/src/database/mod.rs index 7e35a5b..b9fa711 100644 --- a/taptrade-cli-demo/coordinator/src/database/mod.rs +++ b/taptrade-cli-demo/coordinator/src/database/mod.rs @@ -10,14 +10,29 @@ pub struct CoordinatorDB { } // db structure of offers awaiting bond submission in table maker_requests -pub struct AwaitingBondOffer { - pub robohash_hex: String, - pub is_buy_order: bool, - pub amount_satoshi: u64, - pub bond_ratio: u8, - pub offer_duration_ts: u64, - pub bond_address: String, - pub bond_amount_sat: u64, +struct AwaitingBondOffer { + robohash_hex: String, + is_buy_order: bool, + amount_satoshi: u64, + bond_ratio: u8, + offer_duration_ts: u64, + bond_address: String, + bond_amount_sat: u64, +} + +struct AwaitinigTakerOffer { + offer_id: String, + robohash_maker: Vec, + is_buy_order: bool, + amount_sat: i64, + bond_ratio: i32, + offer_duration_ts: i64, + bond_address_maker: String, + bond_amount_sat: i64, + bond_tx_hex_maker: String, + payout_address_maker: String, + musig_pub_nonce_hex_maker: String, + musig_pubkey_hex_maker: String, } // is our implementation resistant against sql injections? @@ -70,6 +85,33 @@ impl CoordinatorDB { ) .execute(&db_pool) .await?; + + sqlx::query( + "CREATE TABLE IF NOT EXISTS taken_offers ( + offer_id TEXT PRIMARY KEY, + robohash_maker BLOB, + robohash_taker BLOB, + is_buy_order INTEGER, + amount_sat INTEGER NOT NULL, + bond_ratio INTEGER NOT NULL, + offer_duration_ts INTEGER NOT NULL, + bond_address_maker TEXT NOT NULL, + bond_address_taker TEXT NOT NULL, + bond_amount_sat INTEGER NOT NULL, + bond_tx_hex_maker TEXT NOT NULL, + bond_tx_hex_taker TEXT NOT NULL, + payout_address_maker TEXT NOT NULL, + payout_address_taker TEXT NOT NULL, + musig_pub_nonce_hex_maker TEXT NOT NULL, + musig_pubkey_hex_maker TEXT NOT NULL, + musig_pub_nonce_hex_taker TEXT NOT NULL, + musig_pubkey_hex_taker TEXT NOT NULL, + escrow_psbt_hex_maker TEXT, + escrow_psbt_hex_taker TEXT + )", + ) + .execute(&db_pool) + .await?; dbg!("Database initialized"); let shared_db_pool = Arc::new(db_pool); Ok(Self { @@ -120,7 +162,7 @@ impl CoordinatorDB { &self, robohash_hex: &str, ) -> Result { - let fetched_values = sqlx::query_as::<_, (String, bool, i64, u8, i64, String, i64)> ( + let fetched_values = sqlx::query_as::<_, (Vec, bool, i64, u8, i64, String, i64)> ( "SELECT robohash, is_buy_order, amount_sat, bond_ratio, offer_duration_ts, bond_address, bond_amount_sat FROM maker_requests WHERE = ?", ) .bind(hex::decode(robohash_hex)?) @@ -148,6 +190,7 @@ impl CoordinatorDB { &self, data: &BondSubmissionRequest, offer_id: &String, + taker_bond_address: String, ) -> Result { let remaining_offer_information = self .fetch_and_delete_offer_from_bond_table(&data.robohash_hex) @@ -156,7 +199,7 @@ impl CoordinatorDB { sqlx::query( "INSERT OR REPLACE INTO active_maker_offers (offer_id, robohash, is_buy_order, amount_sat, bond_ratio, offer_duration_ts, bond_address, bond_amount_sat, bond_tx_hex, payout_address, musig_pub_nonce_hex, musig_pubkey_hex) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ) .bind(offer_id) .bind(hex::decode(&data.robohash_hex)?) @@ -170,6 +213,7 @@ impl CoordinatorDB { .bind(data.payout_address.clone()) .bind(data.musig_pub_nonce_hex.clone()) .bind(data.musig_pubkey_hex.clone()) + .bind(taker_bond_address) .execute(&*self.db_pool) .await?; @@ -180,8 +224,8 @@ impl CoordinatorDB { &self, requested_offer: &OffersRequest, ) -> Result>> { - let fetched_offers = sqlx::query_as::<_, (String, i64)> ( - "SELECT offer_id, amount_sat FROM maker_requests WHERE is_buy_order = ? AND amount_sat BETWEEN ? AND ?", + let fetched_offers = sqlx::query_as::<_, (String, i64, i64, String)> ( + "SELECT offer_id, amount_sat, bond_amount_sat, taker_bond_address FROM active_maker_offers WHERE is_buy_order = ? AND amount_sat BETWEEN ? AND ?", ) .bind(requested_offer.buy_offers) .bind(requested_offer.amount_min_sat as i64) @@ -191,16 +235,114 @@ impl CoordinatorDB { let available_offers: Vec = fetched_offers .into_iter() - .map(|(offer_id_hex, amount_sat)| PublicOffer { - offer_id_hex, - amount_sat: amount_sat as u64, - }) + .map( + |(offer_id_hex, amount_sat, bond_amount_sat, bond_address_taker)| PublicOffer { + offer_id_hex, + amount_sat: amount_sat as u64, + required_bond_amount_sat: bond_amount_sat as u64, + bond_locking_address: bond_address_taker, + }, + ) .collect(); if available_offers.is_empty() { return Ok(None); } Ok(Some(available_offers)) } + + pub async fn fetch_taker_bond_requirements( + &self, + offer_id_hex: &String, + ) -> Result { + let taker_bond_requirements = sqlx::query( + "SELECT taker_bond_address, bond_amount_sat FROM active_maker_offers WHERE offer_id = ?", + ) + .bind(offer_id_hex) + .fetch_one(&*self.db_pool) + .await?; + + Ok(BondRequirementResponse { + bond_address: taker_bond_requirements.try_get("taker_bond_address")?, + locking_amount_sat: taker_bond_requirements.try_get::("bond_amount_sat")? + as u64, + }) + } + + pub async fn fetch_and_delete_offer_from_public_offers_table( + &self, + offer_id_hex: &str, + ) -> Result { + let fetched_values = sqlx::query_as::<_, (Vec, bool, i64, i32, i64, String, i64, String, String, String, String)> ( + "SELECT robohash, is_buy_order, amount_sat, bond_ratio, offer_duration_ts, bond_address, bond_amount_sat, bond_tx_hex, payout_address, + musig_pub_nonce_hex, musig_pubkey_hex FROM active_maker_offers WHERE = ?", + ) + .bind(offer_id_hex) + .fetch_one(&*self.db_pool) + .await?; + + // Delete the database entry. + sqlx::query("DELETE FROM active_maker_offers WHERE = ?") + .bind(offer_id_hex) + .execute(&*self.db_pool) + .await?; + + Ok(AwaitinigTakerOffer { + offer_id: offer_id_hex.to_string(), + robohash_maker: fetched_values.0, + is_buy_order: fetched_values.1, + amount_sat: fetched_values.2, + bond_ratio: fetched_values.3, + offer_duration_ts: fetched_values.4, + bond_address_maker: fetched_values.5, + bond_amount_sat: fetched_values.6, + bond_tx_hex_maker: fetched_values.7, + payout_address_maker: fetched_values.8, + musig_pub_nonce_hex_maker: fetched_values.9, + musig_pubkey_hex_maker: fetched_values.10, + }) + } + + pub async fn add_taker_info_and_move_table( + &self, + trade_and_taker_info: &OfferPsbtRequest, + ) -> Result<()> { + let public_offer = self + .fetch_and_delete_offer_from_public_offers_table( + &trade_and_taker_info.offer.offer_id_hex, + ) + .await?; + + sqlx::query( + "INSERT OR REPLACE INTO taken_offers (offer_id, robohash_maker, robohash_taker, is_buy_order, amount_sat, + 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, musig_pub_nonce_hex_maker, musig_pubkey_hex_maker + musig_pub_nonce_hex_taker, musig_pubkey_hex_taker, escrow_psbt_hex_maker, escrow_psbt_hex_taker) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ) + .bind(public_offer.offer_id) + .bind(public_offer.robohash_maker) + .bind(hex::decode(&trade_and_taker_info.trade_data.robohash_hex)?) + .bind(public_offer.is_buy_order) + .bind(public_offer.amount_sat) + .bind(public_offer.bond_ratio) + .bind(public_offer.offer_duration_ts) + .bind(public_offer.bond_address_maker) + .bind(trade_and_taker_info.offer.bond_locking_address.clone()) + .bind(public_offer.bond_amount_sat) + .bind(public_offer.bond_tx_hex_maker) + .bind(trade_and_taker_info.trade_data.signed_bond_hex.clone()) + .bind(public_offer.payout_address_maker) + .bind(trade_and_taker_info.trade_data.payout_address.clone()) + .bind(public_offer.musig_pub_nonce_hex_maker) + .bind(public_offer.musig_pubkey_hex_maker) + .bind(trade_and_taker_info.trade_data.musig_pub_nonce_hex.clone()) + .bind(trade_and_taker_info.trade_data.musig_pubkey_hex.clone()) + .bind(escrow psbt maker) + .bind(escrow psbt taker) + .execute(&*self.db_pool) + .await?; + Ok(()) + } } #[cfg(test)] diff --git a/taptrade-cli-demo/trader/src/communication/api.rs b/taptrade-cli-demo/trader/src/communication/api.rs index 3078a31..c2434cc 100644 --- a/taptrade-cli-demo/trader/src/communication/api.rs +++ b/taptrade-cli-demo/trader/src/communication/api.rs @@ -69,6 +69,8 @@ pub struct PublicOffers { pub struct PublicOffer { pub amount_sat: u64, pub offer_id_hex: String, + pub required_bond_amount_sat: u64, + pub bond_locking_address: String, } // request to receive the escrow psbt to sign for the specified offer to take it diff --git a/taptrade-cli-demo/trader/src/communication/taker_requests.rs b/taptrade-cli-demo/trader/src/communication/taker_requests.rs index 0e903ba..7660b03 100644 --- a/taptrade-cli-demo/trader/src/communication/taker_requests.rs +++ b/taptrade-cli-demo/trader/src/communication/taker_requests.rs @@ -42,21 +42,6 @@ impl PublicOffers { } } -impl PublicOffer { - pub fn request_bond(&self, taker_config: &TraderSettings) -> Result { - let client = reqwest::blocking::Client::new(); - let res = client - .post(format!( - "{}{}", - taker_config.coordinator_endpoint, "/request-taker-bond" - )) - .json(self) - .send()? - .json::()?; - Ok(res) - } -} - impl OfferPsbtRequest { pub fn taker_request( offer: &PublicOffer, diff --git a/taptrade-cli-demo/trader/src/trading/taker_utils.rs b/taptrade-cli-demo/trader/src/trading/taker_utils.rs index b267186..b058b4c 100644 --- a/taptrade-cli-demo/trader/src/trading/taker_utils.rs +++ b/taptrade-cli-demo/trader/src/trading/taker_utils.rs @@ -11,12 +11,14 @@ impl ActiveOffer { taker_config: &TraderSettings, offer: &PublicOffer, ) -> Result { - // fetching the bond requirements for the requested Offer (amount, locking address) - let bond_conditions: BondRequirementResponse = offer.request_bond(taker_config)?; + let bond_requirements = BondRequirementResponse { + bond_address: offer.bond_locking_address.clone(), + locking_amount_sat: offer.required_bond_amount_sat, + }; // assembly of the Bond transaction and generation of MuSig data and payout address let (bond, mut musig_data, payout_address) = - trading_wallet.trade_onchain_assembly(&bond_conditions, taker_config)?; + trading_wallet.trade_onchain_assembly(&bond_requirements, taker_config)?; // now we submit the signed bond transaction to the coordinator and receive the escrow PSBT we have to sign // in exchange