diff --git a/README.md b/README.md index 8e3ab70..d5dc203 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,9 @@ NEVidebla-MESago (invisible message) * nevmes-auth - `internal` auth server * nevmes-contact - `internal` add contacts server * nevmes-gui - primary user interface +* nevmes-market - `internal` marketplace admin server * nevmes-message - `internal` message tx/read etc. server -* nevmes - `external` primary server for contact share, payment, message rx etc. +* nevmes - `external` primary server for contact share, payment, market, message rx etc. * [monerod](https://www.getmonero.org/downloads/#cli) - (not included) monero-wallet-rpc needs this * can be overriden with remote node * use the `--remote-node` flag diff --git a/nevmes-core/src/message.rs b/nevmes-core/src/message.rs index 6990385..76e8e46 100644 --- a/nevmes-core/src/message.rs +++ b/nevmes-core/src/message.rs @@ -6,7 +6,7 @@ use crate::{ i2p, models::*, reqres, - utils, + utils, monero, }; use log::{ debug, @@ -17,9 +17,25 @@ use reqwest::StatusCode; use rocket::serde::json::Json; use std::error::Error; +#[derive(PartialEq)] +pub enum MessageType { + Normal, + Multisig, +} + +struct MultisigMessageData { + info: String, + sub_type: String, + orid: String, +} + /// Create a new message -pub async fn create(m: Json, jwp: String) -> Message { - let f_mid: String = format!("m{}", utils::generate_rnd()); +pub async fn create(m: Json, jwp: String, m_type: MessageType) -> Message { + let rnd = utils::generate_rnd(); + let mut f_mid: String = format!("m{}", &rnd); + if m_type == MessageType::Multisig { + f_mid = format!("msig{}", &rnd); + } info!("creating message: {}", &f_mid); let created = chrono::offset::Utc::now().timestamp(); // get contact public gpg key and encrypt the message @@ -47,7 +63,7 @@ pub async fn create(m: Json, jwp: String) -> Message { debug!("writing message index {} for id: {}", msg_list, list_key); db::Interface::write(&s.env, &s.handle, &String::from(list_key), &msg_list); info!("attempting to send message"); - let send = send_message(&new_message, &jwp).await; + let send = send_message(&new_message, &jwp, m_type).await; send.unwrap(); new_message } @@ -88,6 +104,68 @@ pub async fn rx(m: Json) { db::Interface::write(&s.env, &s.handle, &String::from(list_key), &msg_list); } +/// Parse the multisig message type and info +fn parse_multisig_message(mid: String) -> MultisigMessageData { + let d: reqres::DecryptedMessageBody = decrypt_body(mid); + let mut bytes = hex::decode(d.body.into_bytes()).unwrap_or(Vec::new()); + 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(); + let sub_type: String = v.remove(0); + let orid: String = v.remove(0); + let info: String = v.remove(0); + bytes = Vec::new(); + debug!("zero decryption bytes: {:?}", bytes); + MultisigMessageData { info, sub_type, orid } +} + +/// Rx multisig message +/// +/// Upon multisig message receipt the message is automatically +/// +/// decrypted for convenience sake. The client must determine which +/// +/// .b32.i2p address belongs to the vendor / mediator. +/// +/// ### Example +/// +/// ```rust +/// // lookup prepare info for vendor +/// let s = db::Interface::open(); +/// let key = "prepare-o123-test.b32.i2p"; +/// db::Interface::read(&s.env, &s.handle, &key); +/// ``` +pub async fn rx_multisig(m: Json) { + // make sure the message isn't something strange + let is_valid = validate_message(&m); + if !is_valid { + return; + } + // don't allow messages from outside the contact list + let is_in_contact_list = contact::exists(&m.from); + if !is_in_contact_list { + return; + } + let f_mid: String = format!("m{}", utils::generate_rnd()); + let new_message = Message { + mid: String::from(&f_mid), + uid: String::from("rx"), + from: String::from(&m.from), + body: m.body.iter().cloned().collect(), + created: chrono::offset::Utc::now().timestamp(), + to: String::from(&m.to), + }; + debug!("insert multisig message: {:?}", &new_message); + let s = db::Interface::open(); + let k = &new_message.mid; + db::Interface::async_write(&s.env, &s.handle, k, &Message::to_db(&new_message)).await; + let data: MultisigMessageData = parse_multisig_message(new_message.mid); + debug!("writing multisig message type {} for order {}", &data.sub_type, &data.orid); + // lookup msig message data by {type}-{order id}-{contact .b32.i2p address} + let msig_key = format!("{}-{}-{}", &data.sub_type, &data.orid, &m.from); + db::Interface::write(&s.env, &s.handle, &msig_key, &data.info); +} + /// Message lookup pub fn find(mid: &String) -> Message { let s = db::Interface::open(); @@ -134,18 +212,21 @@ pub fn find_all() -> Vec { } /// Tx message -async fn send_message(out: &Message, jwp: &str) -> Result<(), Box> { +async fn send_message(out: &Message, jwp: &str, m_type: MessageType) -> Result<(), Box> { let host = utils::get_i2p_http_proxy(); let proxy = reqwest::Proxy::http(&host)?; let client = reqwest::Client::builder().proxy(proxy).build(); - + let mut url = format!("http://{}/message/rx", out.to); + if m_type == MessageType::Multisig { + url = format!("http://{}/message/rx/multisig", out.to) + } // check if the contact is online let is_online: bool = is_contact_online(&out.to, String::from(jwp)) .await .unwrap_or(false); if is_online { return match client? - .post(format!("http://{}/message/rx", out.to)) + .post(url) .header("proof", jwp) .json(&out) .send() @@ -314,7 +395,12 @@ pub async fn retry_fts() { let k = format!("{}-{}", "fts-jwp", &message.to); let jwp = db::Interface::read(&s.env, &s.handle, &k); if jwp != utils::empty_string() { - send_message(&message, &jwp).await.unwrap(); + let m_type = if message.mid.contains("misg") { + MessageType::Multisig + } else { + MessageType::Normal + }; + send_message(&message, &jwp, m_type).await.unwrap(); } else { error!("not jwp found for fts id: {}", &message.mid); } @@ -339,6 +425,24 @@ fn is_fts_clear(r: String) -> bool { v.len() >= 2 && v[v.len() - 1] == utils::empty_string() && v[0] == utils::empty_string() } +pub async fn send_prepare_info(orid:String, contact:String) { + let s = db::Interface::open(); + let prepare_info = monero::prepare_wallet().await; + let k = format!("{}-{}", "fts-jwp", &contact); + let jwp = db::Interface::read(&s.env, &s.handle, &k); + let body_str = format!("prepare:{}:{}", &orid, &prepare_info.result.multisig_info); + let message: Message = Message { + mid: utils::empty_string(), + uid: utils::empty_string(), + body: body_str.into_bytes(), + created: chrono::Utc::now().timestamp(), + from: utils::empty_string(), + to: String::from(&contact), + }; + let j_message: Json = utils::message_to_json(&message); + create(j_message, jwp, MessageType::Multisig).await; +} + // Tests //------------------------------------------------------------------------------- @@ -366,7 +470,7 @@ mod tests { let j_message = utils::message_to_json(&message); let jwp = String::from("test-jwp"); tokio::spawn(async move { - let test_message = create(j_message, jwp).await; + let test_message = create(j_message, jwp, MessageType::Normal).await; let expected: Message = Default::default(); assert_eq!(test_message.body, expected.body); cleanup(&test_message.mid).await; diff --git a/nevmes-core/src/models.rs b/nevmes-core/src/models.rs index 107efee..ac535be 100644 --- a/nevmes-core/src/models.rs +++ b/nevmes-core/src/models.rs @@ -315,17 +315,20 @@ pub struct Order { pub cust_msig_txset: String, pub date: i64, pub deliver_date: i64, + /// Transaction hash from vendor or customer signed txset pub hash: String, pub mediator_kex_1: String, pub mediator_kex_2: String, pub mediator_kex_3: String, pub mediator_msig_make: String, pub mediator_msig_prepare: String, + /// Address gpg key encrypted bytes + pub ship_address: Vec, pub ship_date: i64, /// This is the final destination for the payment pub subaddress: String, pub status: String, - pub quantity: i64, + pub quantity: u64, pub vend_kex_1: String, pub vend_kex_2: String, pub vend_kex_3: String, @@ -350,13 +353,14 @@ impl Default for Order { cust_msig_txset: utils::empty_string(), date: 0, deliver_date: 0, - ship_date: 0, hash: utils::empty_string(), mediator_kex_1: utils::empty_string(), mediator_kex_2: utils::empty_string(), mediator_kex_3: utils::empty_string(), mediator_msig_make: utils::empty_string(), mediator_msig_prepare: utils::empty_string(), + ship_address: Vec::new(), + ship_date: 0, subaddress: utils::empty_string(), status: utils::empty_string(), quantity: 0, @@ -372,8 +376,9 @@ impl Default for Order { impl Order { pub fn to_db(o: &Order) -> String { + let ship_address = hex::encode(&o.ship_address); format!( - "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}", + "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}", o.cid, o.pid, o.cust_kex_1, @@ -390,6 +395,7 @@ impl Order { o.mediator_kex_1, o.mediator_kex_2, o.mediator_kex_3, + ship_address, o.ship_date, o.subaddress, o.status, @@ -429,13 +435,14 @@ impl Order { let mediator_kex_1 = v.remove(0); let mediator_kex_2 = v.remove(0); let mediator_kex_3 = v.remove(0); + let ship_address = hex::decode(v.remove(0)).unwrap_or(Vec::new()); let ship_date = match v.remove(0).parse::() { Ok(d) => d, Err(_) => 0, }; 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, }; @@ -464,6 +471,7 @@ impl Order { mediator_kex_3, mediator_msig_make, mediator_msig_prepare, + ship_address, ship_date, subaddress, status, @@ -496,6 +504,7 @@ impl Order { mediator_kex_3: String::from(&o.mediator_kex_3), mediator_msig_make: String::from(&o.mediator_msig_make), mediator_msig_prepare: String::from(&o.mediator_msig_prepare), + ship_address: o.ship_address.iter().cloned().collect(), ship_date: o.ship_date, subaddress: String::from(&o.subaddress), status: String::from(&o.status), diff --git a/nevmes-core/src/reqres.rs b/nevmes-core/src/reqres.rs index 8c63a54..d55cecf 100644 --- a/nevmes-core/src/reqres.rs +++ b/nevmes-core/src/reqres.rs @@ -1080,3 +1080,24 @@ impl Default for ErrorResponse { } } } + +/// Handle intial information for request +#[derive(Serialize, Deserialize, Debug)] +#[serde(crate = "rocket::serde")] +pub struct OrderRequest { + pub cid: String, + pub pid: String, + pub ship_address: Vec, + pub quantity: u64, +} + +impl Default for OrderRequest { + fn default() -> Self { + OrderRequest { + cid: utils::empty_string(), + pid: utils::empty_string(), + ship_address: Vec::new(), + quantity: 0, + } + } +} \ No newline at end of file diff --git a/nevmes-gui/src/apps/address_book.rs b/nevmes-gui/src/apps/address_book.rs index 9d44bbf..8b32b14 100644 --- a/nevmes-gui/src/apps/address_book.rs +++ b/nevmes-gui/src/apps/address_book.rs @@ -778,7 +778,8 @@ fn send_message_req(tx: Sender, ctx: egui::Context, body: String, to: Stri }; let j_message = utils::message_to_json(&m); tokio::spawn(async move { - let result = message::create(j_message, jwp).await; + let m_type = message::MessageType::Normal; + let result = message::create(j_message, jwp, m_type).await; if result.mid != utils::empty_string() { log::info!("sent message: {}", result.mid); let _ = tx.send(true); diff --git a/nevmes-market/src/main.rs b/nevmes-market/src/main.rs index 9be8950..e959a5b 100755 --- a/nevmes-market/src/main.rs +++ b/nevmes-market/src/main.rs @@ -13,7 +13,7 @@ async fn rocket() -> _ { ..rocket::Config::debug_default() }; env_logger::init(); - log::info!("nevmes-auth is online"); + log::info!("nevmes-market is online"); rocket::custom(&config) .mount( "/dispute", diff --git a/nevmes-market/src/order.rs b/nevmes-market/src/order.rs index a61933d..3ed5265 100644 --- a/nevmes-market/src/order.rs +++ b/nevmes-market/src/order.rs @@ -1,109 +1,94 @@ -// use nevmes_core::*; -// use log::{debug, error, info}; -// use rocket::serde::json::Json; -// use crate::product; +use nevmes_core::*; +use log::{debug, error, info}; +use rocket::serde::json::Json; -// enum StatusType { -// Delivered, -// MultisigMissing, -// MulitsigComplete, -// Shipped, -// } +enum StatusType { + _Delivered, + MultisigMissing, + _MulitsigComplete, + _Shipped, +} -// impl StatusType { -// pub fn value(&self) -> String { -// match *self { -// StatusType::Delivered => String::from("Delivered"), -// StatusType::MultisigMissing => String::from("MultisigMissing"), -// StatusType::MulitsigComplete => String::from("MulitsigComplete"), -// StatusType::Shipped => String::from("Shipped"), -// } -// } -// } +impl StatusType { + pub fn value(&self) -> String { + match *self { + StatusType::_Delivered => String::from("Delivered"), + StatusType::MultisigMissing => String::from("MultisigMissing"), + StatusType::_MulitsigComplete => String::from("MulitsigComplete"), + StatusType::_Shipped => String::from("Shipped"), + } + } +} -// /// Create a skeleton for order -// pub fn create(cid: String, pid: String) -> models::Order { -// let ts = chrono::offset::Utc::now().timestamp(); -// let orid: String = format!("O{}", utils::generate_rnd()); -// let m_product: models::Product = product::find(&pid); -// let new_order = models::Order { -// orid, -// cid: String::from(&cid), -// pid: String::from(&pid), -// cust_kex_1: utils::empty_string(), -// cust_kex_2: utils::empty_string(), -// cust_kex_3: utils::empty_string(), -// cust_msig_make: utils::empty_string(), -// cust_msig_prepare: utils::empty_string(), -// cust_msig_txset: utils::empty_string(), -// date: 0, -// deliver_date: 0, -// hash: utils::empty_string(), -// mediator_kex_1: utils::empty_string(), -// mediator_kex_2: utils::empty_string(), -// mediator_kex_3: utils::empty_string(), -// mediator_msig_make: utils::empty_string(), -// mediator_msig_prepare: utils::empty_string(), -// ship_date: 0, -// subaddress: utils::empty_string(), -// status: utils::empty_string(), -// quantity: 0, -// vend_kex_1: utils::empty_string(), -// vend_kex_2: utils::empty_string(), -// vend_kex_3: utils::empty_string(), -// vend_msig_make: utils::empty_string(), -// vend_msig_prepare: utils::empty_string(), -// vend_msig_txset: utils::empty_string(), -// xmr_address: utils::empty_string(), -// }; -// debug!("insert order: {:?}", new_order); -// let m_wallet = monero::create_wallet(String::from(&orid), &utils::empty_string()).await; -// if !m_wallet { -// error!("error creating msig wallet for order {}", &orid); -// } -// debug!("insert order: {:?}", &new_order); -// let s = db::Interface::open(); -// let k = &new_order.orid; -// db::Interface::write(&s.env, &s.handle, k, &models::Order::to_db(&new_order)); -// // in order to retrieve all orders, write keys to with ol -// let list_key = format!("ol"); -// let r = db::Interface::read(&s.env, &s.handle, &String::from(&list_key)); -// if r == utils::empty_string() { -// debug!("creating order index"); -// } -// let order_list = [r, String::from(&orid)].join(","); -// debug!("writing order index {} for id: {}", order_list, list_key); -// db::Interface::write(&s.env, &s.handle, &String::from(list_key), &order_list); -// new_order -// } +/// Create a intial order +pub async fn create(j_order: Json) -> models::Order { + info!("creating order"); + let ts = chrono::offset::Utc::now().timestamp(); + let orid: String = format!("O{}", utils::generate_rnd()); + let r_subaddress = monero::create_address().await; + let subaddress = r_subaddress.result.address; + let new_order = models::Order { + orid: String::from(&orid), + cid: String::from(&j_order.cid), + pid: String::from(&j_order.pid), + date: ts, + ship_address: j_order.ship_address.iter().cloned().collect(), + subaddress, + status: StatusType::MultisigMissing.value(), + quantity: j_order.quantity, + ..Default::default() + }; + debug!("insert order: {:?}", new_order); + let m_wallet = monero::create_wallet(String::from(&orid), &utils::empty_string()).await; + if !m_wallet { + error!("error creating msig wallet for order {}", &orid); + return Default::default(); + } + debug!("insert order: {:?}", &new_order); + let s = db::Interface::open(); + let k = &new_order.orid; + db::Interface::write(&s.env, &s.handle, k, &models::Order::to_db(&new_order)); + // in order to retrieve all orders, write keys to with ol + let list_key = format!("ol"); + let r = db::Interface::read(&s.env, &s.handle, &String::from(&list_key)); + if r == utils::empty_string() { + debug!("creating order index"); + } + let order_list = [r, String::from(&orid)].join(","); + debug!("writing order index {} for id: {}", order_list, list_key); + db::Interface::write(&s.env, &s.handle, &String::from(list_key), &order_list); + new_order +} -// /// Lookup order -// pub fn find(oid: String) -> models::Order { -// let s = db::Interface::open(); -// let r = db::Interface::read(&s.env, &s.handle, &String::from(&oid)); -// if r == utils::empty_string() { -// error!("order not found"); -// return Default::default(); -// } -// models::Order::from_db(String::from(&oid), r) -// } +/// Lookup order +pub fn find(oid: String) -> models::Order { + info!("find order: {}", &oid); + let s = db::Interface::open(); + let r = db::Interface::read(&s.env, &s.handle, &String::from(&oid)); + if r == utils::empty_string() { + error!("order not found"); + return Default::default(); + } + models::Order::from_db(String::from(&oid), r) +} -// /// Lookup all orders for customer -// pub async fn find_all_customer_orders(cid: String) -> Vec { -// let i_s = db::Interface::open(); -// let i_list_key = format!("ol"); -// let i_r = db::Interface::read(&i_s.env, &i_s.handle, &String::from(i_list_key)); -// if i_r == utils::empty_string() { -// error!("order index not found"); -// } -// let i_v_oid = i_r.split(","); -// let i_v: Vec = i_v_oid.map(|s| String::from(s)).collect(); -// let mut orders: Vec = Vec::new(); -// for o in i_v { -// let order: models::Order = find(o); -// if order.orid != utils::empty_string() && order.cid == cid { -// orders.push(order); -// } -// } -// orders -// } +/// Lookup all orders for customer +pub async fn find_all_customer_orders(cid: String) -> Vec { + info!("lookup orders for customer: {}", &cid); + let i_s = db::Interface::open(); + let i_list_key = format!("ol"); + let i_r = db::Interface::read(&i_s.env, &i_s.handle, &String::from(i_list_key)); + if i_r == utils::empty_string() { + error!("order index not found"); + } + let i_v_oid = i_r.split(","); + let i_v: Vec = i_v_oid.map(|s| String::from(s)).collect(); + let mut orders: Vec = Vec::new(); + for o in i_v { + let order: models::Order = find(o); + if order.orid != utils::empty_string() && order.cid == cid { + orders.push(order); + } + } + orders +} diff --git a/nevmes-message/src/controller.rs b/nevmes-message/src/controller.rs index fcf480c..6fdd124 100644 --- a/nevmes-message/src/controller.rs +++ b/nevmes-message/src/controller.rs @@ -15,12 +15,18 @@ use nevmes_core::{ }; /// Send message -#[post("/", data = "")] +#[post("/", data = "")] pub async fn send_message( m_req: Json, + r_type: String, token: proof::PaymentProof, ) -> Custom> { - let res: Message = message::create(m_req, token.get_jwp()).await; + let m_type: message::MessageType = if r_type == "multisig" { + message::MessageType::Multisig + } else { + message::MessageType::Normal + }; + let res: Message = message::create(m_req, token.get_jwp(), m_type).await; Custom(Status::Ok, Json(res)) } diff --git a/src/controller.rs b/src/controller.rs index 5058f3c..48bc8b6 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -37,7 +37,7 @@ pub async fn get_i2p_status() -> Custom> { } /// Share your contact information -/// TODO(c2m): configurable option to only allow adding after JWP creation +/// /// Protected: false #[get("/")] pub async fn share_contact_info() -> Custom> { @@ -78,11 +78,66 @@ pub async fn gen_jwp(proof: Json) -> Custom> { // NEVMES Market APIs //----------------------------------------------- -/// Get all products by passing vendor address +/// Get all products /// -/// Protected: true +/// Protected: false #[get("/products")] pub async fn get_products(_jwp: proof::PaymentProof) -> Custom>> { let m_products: Vec = product::find_all(); Custom(Status::Ok, Json(m_products)) } + +/// Create order +/// +/// Protected: true +#[post("/order/create", data = "")] +pub async fn create_order( + r_order: Json, + _jwp: proof::PaymentProof) -> Custom> { + let m_order: models::Order = order::create(r_order).await; + Custom(Status::Created, Json(m_order)) +} + +/// Customer order retreival. Must send `signature` +/// +/// which is the order id signed by the wallet. +/// +/// Protected: true +#[get("/order/retrieve//<_signature>")] +pub async fn retrieve_order( + orid: String, + _signature: String, + _jwp: proof::PaymentProof) -> Custom> { + + // get customer address + + // send address, orid and signature to verify() + + let m_order: models::Order = order::find(orid); + Custom(Status::Created, Json(m_order)) +} + +/// Create order +/// +/// Protected: true +#[get("/multisig/prepare//")] +pub async fn get_prepare_multisig_info( + orid: String, + contact: String, + _jwp: proof::PaymentProof) -> Custom> { + // TODO(c2m): create a multisig message + message::send_prepare_info(orid, contact).await; + Custom(Status::Ok, Json(Default::default())) +} + +/// Recieve multisig messages here +/// +/// Protected: true +#[post("/", data = "")] +pub async fn rx_multisig_message( + _jwp: proof::PaymentProof, + message: Json, +) -> Custom> { + message::rx_multisig(message).await; + Custom(Status::Ok, Json(Default::default())) +} diff --git a/src/main.rs b/src/main.rs index dd30550..6fb5825 100644 --- a/src/main.rs +++ b/src/main.rs @@ -54,6 +54,7 @@ async fn rocket() -> _ { .register("/", catchers![internal_error, not_found, payment_required]) .mount("/invoice", routes![controller::gen_invoice]) .mount("/message/rx", routes![controller::rx_message]) + .mount("/message/rx/multisig", routes![controller::rx_multisig_message]) .mount("/prove", routes![controller::gen_jwp]) .mount("/share", routes![controller::share_contact_info]) .mount("/i2p", routes![controller::get_i2p_status])