diff --git a/neveko-core/src/order.rs b/neveko-core/src/order.rs index 30bbc3e..1f46a9f 100644 --- a/neveko-core/src/order.rs +++ b/neveko-core/src/order.rs @@ -9,8 +9,7 @@ use crate::{ monero, order, product, - reqres, - utils, + utils, reqres, }; use log::{ debug, @@ -421,12 +420,120 @@ pub async fn upload_delivery_info( /// upon a `vendor_update_success: true` response pub async fn finalize_order(orid: &String) -> reqres::FinalizeOrderResponse { info!("finalizing order: {}", orid); - + // verify recipient and unlock time + let mut m_order: Order = order::find(orid); + if m_order.vend_msig_txset == utils::empty_string() { + error!("txset missing"); + return Default::default(); + } + // get draft payment txset + let wallet_password = utils::empty_string(); + monero::open_wallet(orid, &wallet_password).await; + monero::refresh().await; + let address: String = String::from(&m_order.subaddress); + let m_describe = monero::describe_transfer(&m_order.vend_msig_txset).await; + let check_destination: reqres::Destination = reqres::Destination { + address, + ..Default::default() + }; + let valid = m_describe.result.desc[0].recepients.contains(&check_destination) + && m_describe.result.desc[0].unlock_time < monero::LockTimeLimit::Blocks.value(); + if !valid { + monero::close_wallet(orid, &wallet_password).await; + error!("invalid txset"); + return Default::default(); + } + // verify order wallet has been swept clean + let balance = monero::get_balance().await; + if balance.result.unlocked_balance != 0 { + monero::close_wallet(orid, &wallet_password).await; + error!("order wallet not swept"); + return Default::default(); + } + m_order.status = order::StatusType::Delivered.value(); + order::modify(Json(m_order)); reqres::FinalizeOrderResponse { + vendor_update_success: true, ..Default::default() } } +/// Executes POST /order/finalize/{orid} +/// +/// finalizing the order on the vendor side. +/// +/// Customer needs to verify the response and update their lmdb. +/// +/// see `finalize_order` +pub async fn transmit_finalize_request( + contact: &String, + jwp: &String, + orid: &String, +) -> Result> { + info!("executing transmit_cancel_request"); + let host = utils::get_i2p_http_proxy(); + let proxy = reqwest::Proxy::http(&host)?; + let client = reqwest::Client::builder().proxy(proxy).build(); + match client? + .post(format!( + "http://{}/market/order/finalize/{}", contact, orid + )) + .header("proof", jwp) + .send() + .await + { + Ok(response) => { + let res = response.json::().await; + debug!("finalize order response: {:?}", res); + match res { + Ok(r) => Ok(r), + _ => Ok(Default::default()), + } + } + Err(e) => { + error!("failed to finalize order due to: {:?}", e); + Ok(Default::default()) + } + } +} + +/// A post-decomposition trigger for the finalize request so that the logic +/// +/// can be executed from the gui. +pub async fn trigger_finalize_request(contact: &String, jwp: &String, orid: &String) -> reqres::FinalizeOrderResponse { + info!("executing trigger_finalize_request"); + let finalize = transmit_finalize_request(contact, jwp, orid).await; + // cache finalize order request to db + if finalize.is_err() { + log::error!("failed to trigger cancel request"); + return Default::default(); + } + let unwrap: reqres::FinalizeOrderResponse = finalize.unwrap(); + let mut m_order: Order = order::find(&orid); + m_order.status = order::StatusType::Delivered.value(); + backup(&m_order); + unwrap +} + +/// Decomposition trigger for `finalize_order()` +pub async fn d_trigger_finalize_request(contact: &String, orid: &String) -> reqres::FinalizeOrderResponse { + // ugh, sorry seems we need to get jwp for vendor from fts cache + // get jwp from db + let s = db::Interface::async_open().await; + let k = format!("{}-{}", crate::FTS_JWP_DB_KEY, &contact); + let jwp = db::Interface::async_read(&s.env, &s.handle, &k).await; + info!("executing d_trigger_finalize_request"); + // request finalize if the order status is shipped + let order: Order = order::find(&orid); + if order.status != order::StatusType::Shipped.value() { + let trigger = trigger_finalize_request(contact, &jwp, orid).await; + if trigger.vendor_update_success { + return trigger; + } + } + Default::default() +} + /// Send order request to vendor and start multisig flow pub async fn transmit_order_request( contact: String, @@ -638,7 +745,7 @@ pub async fn transmit_cancel_request( } } -/// Decomposition trigger for the shipping request +/// Decomposition trigger for the cancel request pub async fn d_trigger_cancel_request(contact: &String, orid: &String) -> Order { // ugh, sorry seems we need to get jwp for vendor from fts cache // get jwp from db diff --git a/neveko-core/src/reqres.rs b/neveko-core/src/reqres.rs index ecc7f2a..6cbef47 100644 --- a/neveko-core/src/reqres.rs +++ b/neveko-core/src/reqres.rs @@ -106,7 +106,7 @@ pub struct XmrRpcGetTxByIdParams { pub txid: String, } -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, PartialEq)] pub struct Destination { pub address: String, pub amount: u128, diff --git a/neveko-gui/src/apps/market.rs b/neveko-gui/src/apps/market.rs index 6536135..f200b5b 100644 --- a/neveko-gui/src/apps/market.rs +++ b/neveko-gui/src/apps/market.rs @@ -111,8 +111,8 @@ pub struct MarketApp { _refresh_on_delete_product_rx: Receiver, submit_order_tx: Sender, submit_order_rx: Receiver, - _submit_txset_tx: Sender, - _submit_txset_rx: Receiver, + submit_txset_tx: Sender, + submit_txset_rx: Receiver, cancel_request_tx: Sender, cancel_request_rx: Receiver, // ship_request_tx: Sender, @@ -144,7 +144,7 @@ impl Default for MarketApp { let (our_make_info_tx, our_make_info_rx) = std::sync::mpsc::channel(); let (order_xmr_address_tx, order_xmr_address_rx) = std::sync::mpsc::channel(); let (order_funded_tx, order_funded_rx) = std::sync::mpsc::channel(); - let (_submit_txset_tx, _submit_txset_rx) = std::sync::mpsc::channel(); + let (submit_txset_tx, submit_txset_rx) = std::sync::mpsc::channel(); let contents = std::fs::read("./assets/qr.png").unwrap_or(Vec::new()); MarketApp { contact_info_rx, @@ -217,8 +217,8 @@ impl Default for MarketApp { // ship_request_tx, submit_order_rx, submit_order_tx, - _submit_txset_rx, - _submit_txset_tx, + submit_txset_rx, + submit_txset_tx, vendor_status: Default::default(), vendors: Vec::new(), } @@ -332,6 +332,13 @@ impl eframe::App for MarketApp { self.is_loading = false; } + if let Ok(finalized) = self.submit_txset_rx.try_recv() { + if !finalized { + log::error!("failure to finalize shipment please contact vendor") + } + self.is_loading = false; + } + // Vendor status window //----------------------------------------------------------------------------------- let mut is_showing_vendor_status = self.is_showing_vendor_status; @@ -649,8 +656,7 @@ impl eframe::App for MarketApp { }); } if self.msig.completed_export && !self.msig.completed_shipping_request { - // idk if manual shipping request will be necessary with the new nasr logic, - // let's see + // idk if manual shipping request will be necessary with the new nasr logic ui.horizontal(|ui| { ui.label( RichText::new("Delivery Pending") @@ -698,19 +704,18 @@ impl eframe::App for MarketApp { ui.horizontal(|ui| { ui.label("Release Payment: \t"); if ui.button("Submit Txset").clicked() { + self.is_loading = true; + let vendor_prefix = String::from(crate::GUI_OVL_DB_KEY); + let vendor = + utils::search_gui_db(vendor_prefix, self.m_order.orid.clone()); // async trigger for signing and submitted the txset - // do something release_txset( - utils::empty_string(), - utils::empty_string(), + vendor, + self.m_order.orid.clone(), ctx.clone(), - utils::empty_string(), - self._submit_txset_tx.clone(), + self.submit_txset_tx.clone(), ); } - if ui.button("Check").clicked() { - // the multisig wallet should have a zero balance if the sweep succeeded - } }); } // ui.horizontal(|ui| { @@ -2296,10 +2301,9 @@ fn validate_msig_step( } fn release_txset( - _contact: String, + contact: String, orid: String, ctx: egui::Context, - _jwp: String, tx: Sender, ) { tokio::spawn(async move { @@ -2311,12 +2315,9 @@ fn release_txset( let _ = tx.send(false); return; } - // TODO(c2m): we need to build an API that tells the vendor - // to verify the txset was submitted successfully - // return boolean. - + let finalize = order::d_trigger_finalize_request(&contact, &orid).await; // update order to delivered if success - let _ = tx.send(true); + let _ = tx.send(finalize.vendor_update_success); ctx.request_repaint(); todo!() }); diff --git a/src/controller.rs b/src/controller.rs index 79fa4c4..422427c 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -12,6 +12,7 @@ use rocket::{ use neveko_core::*; // JSON APIs exposed over i2p +// Take care not to put any admin APIs inside of here /// Get payment API version /// @@ -236,14 +237,28 @@ pub async fn cancel_order( if m_order.cid == utils::empty_string() { return Custom(Status::BadRequest, Json(Default::default())); } - Custom(Status::Created, Json(m_order)) + Custom(Status::Ok, Json(m_order)) } -/// Create a dispute (customer) +/// Customer finalize order logic. Vendor updates order +/// +/// to `Delivered` status. +/// +/// Protected: true +#[post("/order/finalize/")] +pub async fn finalize_order(orid: String, _jwp: proof::PaymentProof) -> Custom> { + let finalize = order::finalize_order(&orid).await; + if !finalize.vendor_update_success { + return Custom(Status::BadRequest, Json(Default::default())); + } + Custom(Status::Ok, Json(finalize)) +} + +/// Create a dispute #[post("/create", data = "")] pub async fn create_dispute( dispute: Json, - _token: auth::BearerToken, + _jwp: proof::PaymentProof, ) -> Custom> { let m_dispute: models::Dispute = dispute::create(dispute); Custom(Status::Ok, Json(m_dispute)) diff --git a/src/main.rs b/src/main.rs index 77646e4..e111173 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,6 +50,7 @@ async fn rocket() -> _ { controller::request_shipment, controller::retrieve_order, controller::trigger_nasr, + controller::finalize_order, ], ) }