diff --git a/neveko-core/src/message.rs b/neveko-core/src/message.rs index 3419bbc..0233e73 100644 --- a/neveko-core/src/message.rs +++ b/neveko-core/src/message.rs @@ -23,6 +23,7 @@ pub const EXPORT_MSIG: &str = "export"; pub const MAKE_MSIG: &str = "make"; pub const PREPARE_MSIG: &str = "prepare"; pub const SIGN_MSIG: &str = "sign"; +pub const TXSET_MSIG: &str = "txset"; pub const VALID_MSIG_MSG_LENGTH: usize = 4; #[derive(PartialEq)] @@ -129,14 +130,22 @@ fn parse_multisig_message(mid: String) -> MultisigMessageData { let decoded = String::from_utf8(bytes).unwrap_or(utils::empty_string()); let values = decoded.split(":"); let mut v: Vec = values.map(|s| String::from(s)).collect(); - if v.len() != VALID_MSIG_MSG_LENGTH { + let sub_type: String = v.remove(0); + let valid_length = if sub_type == TXSET_MSIG { + VALID_MSIG_MSG_LENGTH - 2 + } else { + VALID_MSIG_MSG_LENGTH - 1 + }; + if v.len() != valid_length { return Default::default(); } - let sub_type: String = v.remove(0); let orid: String = v.remove(0); let customer_info: String = v.remove(0); - let mediator_info: String = v.remove(0); - let info = format!("{}:{}", customer_info, mediator_info); + let mut info = String::from(&customer_info); + if sub_type != TXSET_MSIG { + let mediator_info: String = v.remove(0); + info = format!("{}:{}", customer_info, mediator_info); + } bytes = Vec::new(); debug!("zero decryption bytes: {:?}", bytes); MultisigMessageData { diff --git a/neveko-core/src/models.rs b/neveko-core/src/models.rs index 2c86cea..4425346 100644 --- a/neveko-core/src/models.rs +++ b/neveko-core/src/models.rs @@ -236,8 +236,8 @@ pub struct Product { pub image: Vec, pub in_stock: bool, pub name: String, - pub price: u64, - pub qty: u64, + pub price: u128, + pub qty: u128, } impl Default for Product { @@ -272,11 +272,11 @@ impl Product { Err(_) => false, }; let name = v.remove(0); - let price = match v.remove(0).parse::() { + let price = match v.remove(0).parse::() { Ok(p) => p, Err(_) => 0, }; - let qty = match v.remove(0).parse::() { + let qty = match v.remove(0).parse::() { Ok(q) => q, Err(_) => 0, }; @@ -331,7 +331,7 @@ pub struct Order { /// This is the final destination for the payment pub subaddress: String, pub status: String, - pub quantity: u64, + pub quantity: u128, pub vend_kex_1: String, pub vend_kex_2: String, pub vend_kex_3: String, @@ -445,7 +445,7 @@ impl Order { }; let subaddress = v.remove(0); let status = v.remove(0); - let quantity = match v.remove(0).parse::() { + let quantity = match v.remove(0).parse::() { Ok(d) => d, Err(_) => 0, }; diff --git a/neveko-core/src/monero.rs b/neveko-core/src/monero.rs index 9ab1f09..b84e54b 100644 --- a/neveko-core/src/monero.rs +++ b/neveko-core/src/monero.rs @@ -29,6 +29,7 @@ enum RpcFields { Close, CreateAddress, CreateWallet, + DescribeTransfer, ExchangeMultisigKeys, Export, GetTxProof, @@ -57,6 +58,7 @@ impl RpcFields { RpcFields::Close => String::from("close_wallet"), RpcFields::CreateAddress => String::from("create_address"), RpcFields::CreateWallet => String::from("create_wallet"), + RpcFields::DescribeTransfer => String::from("describe_transfer"), RpcFields::ExchangeMultisigKeys => String::from("exchange_multisig_keys"), RpcFields::Export => String::from("export_multisig_info"), RpcFields::GetTxProof => String::from("get_tx_proof"), @@ -940,6 +942,45 @@ pub async fn transfer(d: reqres::Destination) -> reqres::XmrRpcTransferResponse } } +/// Performs the xmr rpc 'describe_transfer' method +pub async fn describe_transfer(multisig_txset: &String) -> reqres::XmrRpcDescribeTransferResponse { + info!("executing {}", RpcFields::DescribeTransfer.value()); + let client = reqwest::Client::new(); + let host = get_rpc_host(); + let params: reqres::XmrRpcDescribeTransferParams = reqres::XmrRpcDescribeTransferParams { + multisig_txset: String::from(multisig_txset), + }; + let req = reqres::XmrRpcDescribeTransfrerRequest { + jsonrpc: RpcFields::JsonRpcVersion.value(), + id: RpcFields::Id.value(), + method: RpcFields::DescribeTransfer.value(), + params, + }; + let login: RpcLogin = get_rpc_creds(); + match client + .post(host) + .json(&req) + .send_with_digest_auth(&login.username, &login.credential) + .await + { + Ok(response) => { + let res = response + .json::() + .await; + debug!( + "{} response: {:?}", + RpcFields::DescribeTransfer.value(), + res + ); + match res { + Ok(res) => res, + _ => Default::default(), + } + } + Err(_) => Default::default(), + } +} + /// Performs the xmr rpc 'sweep_all' method pub async fn sweep_all(address: String) -> reqres::XmrRpcSweepAllResponse { info!("executing {}", RpcFields::SweepAll.value()); diff --git a/neveko-core/src/order.rs b/neveko-core/src/order.rs index c181c85..bf97130 100644 --- a/neveko-core/src/order.rs +++ b/neveko-core/src/order.rs @@ -1,10 +1,14 @@ use crate::{ contact, db, + gpg, + i2p, + message, models::*, monero, + product, reqres, - utils, product, gpg, i2p, + utils, }; use log::{ debug, @@ -13,13 +17,8 @@ use log::{ }; use rocket::serde::json::Json; -/* - TODOs(c2m): - - release tracking (locker code?) when txset is released, update to delivered -*/ - enum StatusType { - _Delivered, + Delivered, MultisigMissing, MulitsigComplete, Shipped, @@ -28,7 +27,7 @@ enum StatusType { impl StatusType { pub fn value(&self) -> String { match *self { - StatusType::_Delivered => String::from("Delivered"), + StatusType::Delivered => String::from("Delivered"), StatusType::MultisigMissing => String::from("MultisigMissing"), StatusType::MulitsigComplete => String::from("MulitsigComplete"), StatusType::Shipped => String::from("Shipped"), @@ -195,7 +194,7 @@ pub async fn secure_retrieval(orid: &String, signature: &String) -> Order { } /// Check for import multisig info, validate block time and that the -/// +/// /// order wallet has been funded properly. Update the order to multisig complete pub async fn validate_order_for_ship(orid: &String) -> bool { info!("validating order for shipment"); @@ -226,14 +225,15 @@ pub async fn validate_order_for_ship(orid: &String) -> bool { ready_to_ship } -/// Write encrypted delivery info to lmdb. Once the customer releases the signed txset -/// -/// they will have access to this information (tracking number, locker code, etc.) +/// Write encrypted delivery info to lmdb. Once the customer releases the signed +/// txset +/// +/// they will have access to this information (tracking number, locker code, +/// etc.) pub async fn upload_delivery_info(orid: &String, delivery_info: &Vec) { info!("uploading delivery info"); let name = i2p::get_destination(None); - let e_delivery_info: Vec = gpg::encrypt(name, &delivery_info) - .unwrap_or(Vec::new()); + let e_delivery_info: Vec = gpg::encrypt(name, &delivery_info).unwrap_or(Vec::new()); if e_delivery_info.is_empty() { error!("unable to encrypt delivery info"); } @@ -248,3 +248,42 @@ pub async fn upload_delivery_info(orid: &String, delivery_info: &Vec) { db::Interface::async_delete(&s.env, &s.handle, &m_order.orid).await; db::Interface::async_write(&s.env, &s.handle, &m_order.orid, &Order::to_db(&m_order)).await; } + +/// The vendor will first search for a encrypted multisig message in the form +/// +/// txset-{order id}-{.b32.i2p} +pub async fn finalize_order(orid: &String) -> reqres::FinalizeOrderResponse { + info!("finalizing order"); + let mut m_order: Order = find(orid); + let s = db::Interface::async_open().await; + let key = format!("{}-{}-{}", message::TXSET_MSIG, orid, &m_order.cid); + let txset = db::Interface::async_read(&s.env, &s.handle, &key).await; + // describe transer to check amount, address and unlock_time + let r_describe: reqres::XmrRpcDescribeTransferResponse = + monero::describe_transfer(&txset).await; + let m_product: Product = product::find(&m_order.pid); + let total: u128 = m_product.price * m_order.quantity; + let description: &reqres::TransferDescription = &r_describe.result.desc[0]; + let is_valid_payment: bool = description.amount_out + description.fee >= total + && description.unlock_time < monero::LockTimeLimit::Blocks.value(); + if !is_valid_payment { + return Default::default(); + } + let r_submit: reqres::XmrRpcSubmitMultisigResponse = + sign_and_submit_multisig(orid, &txset).await; + if r_submit.result.tx_hash_list.is_empty() { + return Default::default(); + } + // lookup delivery info + let delivery_key = format!("delivery-{}", orid); + let r_delivery_info: String = db::Interface::async_read(&s.env, &s.handle, &delivery_key).await; + let delivery_info: Vec = hex::decode(r_delivery_info).unwrap_or(Vec::new()); + // update the order + m_order.status = StatusType::Delivered.value(); + db::Interface::async_delete(&s.env, &s.handle, &m_order.orid).await; + db::Interface::async_write(&s.env, &s.handle, &m_order.orid, &Order::to_db(&m_order)).await; + reqres::FinalizeOrderResponse { + orid: String::from(orid), + delivery_info, + } +} diff --git a/neveko-core/src/reqres.rs b/neveko-core/src/reqres.rs index 5bbea6c..4a2b182 100644 --- a/neveko-core/src/reqres.rs +++ b/neveko-core/src/reqres.rs @@ -122,6 +122,11 @@ pub struct XmrRpcTransferParams { pub get_tx_key: bool, } +#[derive(Deserialize, Serialize, Debug)] +pub struct XmrRpcDescribeTransferParams { + pub multisig_txset: String, +} + #[derive(Deserialize, Serialize, Debug)] pub struct XmrRpcSweepAllParams { pub address: String, @@ -279,6 +284,14 @@ pub struct XmrRpcTransfrerRequest { pub params: XmrRpcTransferParams, } +#[derive(Deserialize, Serialize, Debug)] +pub struct XmrRpcDescribeTransfrerRequest { + pub jsonrpc: String, + pub id: String, + pub method: String, + pub params: XmrRpcDescribeTransferParams, +} + #[derive(Deserialize, Serialize, Debug)] pub struct XmrRpcSweepAllRequest { pub jsonrpc: String, @@ -459,6 +472,25 @@ pub struct XmrRpcTranferResult { pub unsigned_txset: String, } +#[derive(Deserialize, Serialize, Debug)] +pub struct TransferDescription { + pub amount_in: u128, + pub amount_out: u128, + pub recepients: Vec, + pub change_address: String, + pub change_amount: u128, + pub fee: u128, + pub ring_size: u64, + pub unlock_time: u64, + pub dummy_outputs: u64, + pub extra: String, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct XmrRpcDescribeTranferResult { + pub desc: Vec, +} + #[derive(Deserialize, Serialize, Debug)] pub struct KeyImageList { key_images: Vec, @@ -995,6 +1027,19 @@ impl Default for XmrRpcTransferResponse { } } +#[derive(Deserialize, Debug)] +pub struct XmrRpcDescribeTransferResponse { + pub result: XmrRpcDescribeTranferResult, +} + +impl Default for XmrRpcDescribeTransferResponse { + fn default() -> Self { + XmrRpcDescribeTransferResponse { + result: XmrRpcDescribeTranferResult { desc: Vec::new() }, + } + } +} + #[derive(Deserialize, Debug)] pub struct XmrRpcSweepAllResponse { pub result: XmrRpcSweepAllResult, @@ -1110,7 +1155,7 @@ pub struct OrderRequest { pub cid: String, pub pid: String, pub ship_address: Vec, - pub quantity: u64, + pub quantity: u128, } impl Default for OrderRequest { @@ -1162,3 +1207,21 @@ impl Default for SignAndSubmitRequest { } } } + +/// Response for the order finalization +#[derive(Serialize, Deserialize, Debug)] +#[serde(crate = "rocket::serde")] +pub struct FinalizeOrderResponse { + pub orid: String, + /// This is encrypted by the vendors NEVEKO gpg key + pub delivery_info: Vec, +} + +impl Default for FinalizeOrderResponse { + fn default() -> Self { + FinalizeOrderResponse { + orid: utils::empty_string(), + delivery_info: Vec::new(), + } + } +} diff --git a/src/controller.rs b/src/controller.rs index af1d4b1..ce16976 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -1,9 +1,10 @@ use rocket::{ + catch, get, http::Status, post, response::status::Custom, - serde::json::Json, catch, + serde::json::Json, }; use neveko_core::*; @@ -157,7 +158,31 @@ pub async fn rx_multisig_message( /// /// Protected: true #[post("/")] -pub async fn request_shipment(orid: String, _jwp: proof::PaymentProof) -> Custom> { +pub async fn request_shipment( + orid: String, + _jwp: proof::PaymentProof, +) -> Custom> { + let is_ready: bool = order::validate_order_for_ship(&orid).await; + if !is_ready { + return Custom(Status::BadRequest, Json(Default::default())); + } + Custom(Status::Ok, Json(Default::default())) +} + +/// Send tx_data_hex, which is the output from signing +/// +/// a multisig transaction from the order wallet to the +/// +/// vendor's subaddress. After that the vendor will submit the +/// +/// transaction and return the encrypted delivery information. +/// +/// Protected: true +#[post("/")] +pub async fn finalize_order( + orid: String, + _jwp: proof::PaymentProof, +) -> Custom> { let is_ready: bool = order::validate_order_for_ship(&orid).await; if !is_ready { return Custom(Status::BadRequest, Json(Default::default())); @@ -175,7 +200,6 @@ pub async fn create_dispute( Custom(Status::Ok, Json(m_dispute)) } - // Catchers //---------------------------------------------------------------- diff --git a/src/main.rs b/src/main.rs index 6980dfb..6302a4d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,11 +16,14 @@ async fn rocket() -> _ { env_logger::init(); utils::start_up().await; rocket::custom(&config) - .register("/", catchers![ - controller::internal_error, - controller::not_found, - controller::payment_required - ]) + .register( + "/", + catchers![ + controller::internal_error, + controller::not_found, + controller::payment_required + ], + ) .mount("/multisig/info", routes![controller::get_multisig_info]) .mount("/invoice", routes![controller::gen_invoice]) .mount("/message/rx", routes![controller::rx_message]) @@ -34,6 +37,12 @@ async fn rocket() -> _ { .mount("/xmr/rpc", routes![controller::get_version]) .mount( "/market", - routes![controller::create_order, controller::get_products, controller::create_dispute], + routes![ + controller::create_order, + controller::create_dispute, + controller::get_products, + controller::finalize_order, + controller::request_shipment, + ], ) }