diff --git a/Cargo.lock b/Cargo.lock index 7602ce9..bfe8644 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,6 +22,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.68" @@ -165,6 +174,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + [[package]] name = "cipher" version = "0.4.3" @@ -212,6 +236,34 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "comfy-table" +version = "6.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7b787b0dc42e8111badfdbe4c3059158ccb2db8780352fa1b01e8ccf45cc4d" +dependencies = [ + "crossterm", + "strum", + "strum_macros", + "unicode-width", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "cpufeatures" version = "0.2.5" @@ -221,6 +273,31 @@ dependencies = [ "libc", ] +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -231,6 +308,50 @@ dependencies = [ "typenum", ] +[[package]] +name = "cxx" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add3fc1717409d029b20c5b6903fc0c0b02fa6741d820054f4a2efa5e5816fd" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c87959ba14bc6fbc61df77c3fcfe180fc32b93538c4f1031dd802ccb5f2ff0" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69a3e162fde4e594ed2b07d0f83c6c67b745e7f28ce58c6df5e6b6bef99dfb59" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.10.6" @@ -311,6 +432,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.25" @@ -318,6 +454,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -326,6 +463,23 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" +[[package]] +name = "futures-executor" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" + [[package]] name = "futures-macro" version = "0.3.25" @@ -355,10 +509,13 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -382,7 +539,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -514,6 +671,30 @@ dependencies = [ "tokio-rustls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + [[package]] name = "idna" version = "0.2.1" @@ -604,6 +785,15 @@ version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + [[package]] name = "linux-raw-sys" version = "0.1.4" @@ -661,7 +851,7 @@ checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] @@ -670,13 +860,18 @@ name = "mostro-cli" version = "0.1.2" dependencies = [ "anyhow", + "chrono", "clap", + "comfy-table", + "futures", + "log", "nostr", "nostr-sdk", "pretty_env_logger", "serde", "serde_json", "tokio", + "uuid", ] [[package]] @@ -720,6 +915,25 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.15.0" @@ -1008,6 +1222,12 @@ dependencies = [ "base64", ] +[[package]] +name = "rustversion" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" + [[package]] name = "ryu" version = "1.0.12" @@ -1020,6 +1240,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "scratch" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" + [[package]] name = "sct" version = "0.7.0" @@ -1146,6 +1372,27 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" +[[package]] +name = "signal-hook" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -1252,6 +1499,25 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "1.0.107" @@ -1292,6 +1558,17 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "tokio" version = "1.23.0" @@ -1456,6 +1733,12 @@ dependencies = [ "smallvec 0.6.14", ] +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + [[package]] name = "untrusted" version = "0.7.1" @@ -1518,6 +1801,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 6362368..113913a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,9 @@ pretty_env_logger = "0.4.0" serde = "1.0.152" serde_json = "1.0.91" tokio = { version = "1.23.0", features = ["full"] } +comfy-table = "6.1.3" +chrono = "0.4.23" +log = "0.4.17" +pretty_env_logger = "0.4.0" +futures = "0.3" +uuid = {version = "1.2.2", features = ["v4","fast-rng","macro-diagnostics"]} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 201890d..4bca677 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,47 +1,56 @@ -use clap::Parser; +use clap::{Parser,Subcommand}; use nostr::util::nips::nip19::FromBech32; use nostr::util::time::timestamp; -use nostr::Keys; use nostr::{Kind, SubscriptionFilter}; -use nostr_sdk::{Client, RelayPoolNotifications, Result}; +use nostr_sdk::{RelayPoolNotifications, Result}; +use std::env::set_var; + pub mod types; +pub mod util; +use crate::util::{get_orders_list,print_orders_table}; /// cli arguments -#[derive(Parser, Debug)] -#[clap(author, version, about)] +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] /// Mostro P2P cli client -struct Arguments { - list: Option, +struct Cli { + #[command(subcommand)] + command: Option, + #[arg(short, long)] + verbose: bool, +} + +#[derive(Subcommand)] +enum Commands { + /// Requests open orders from mostro pubkey () + Listorders { + pubkey: String, + #[clap(default_value = "Pending")] + orderstatus: String, + }, } #[tokio::main] async fn main() -> Result<()> { pretty_env_logger::init(); // TODO: handle arguments - // let args = Arguments::parse(); + let cli = Cli::parse(); + //Init logger + if cli.verbose == true { + set_var("RUST_LOG", "info"); + } + pretty_env_logger::init(); + // mostro pubkey - let pubkey = "npub1m0str0n64lfulw5j6arrak75uvajj60kr024f5m6c4hsxtsnx4dqpd9ape"; + let pubkey = "npub1qqqq9uwdxa70fr858sx0zyzrt8ftwmhzt8zd9mv03put8xpgrphsc4xpqs"; let mostro_keys = nostr::key::XOnlyPublicKey::from_bech32(pubkey)?; - // Generate new keys - let my_keys: Keys = Client::generate_keys(); - // Create new client - let client = Client::new(&my_keys); - - // Add relays - // client.add_relay("wss://relay.damus.io", None).await?; - // client.add_relay("wss://nostr.fly.dev", None).await?; - client.add_relay("wss://nostr.zebedee.cloud", None).await?; - // client - // .add_relay("wss://relay.minds.com/nostr/v1/ws", None) - // .await?; - // client.add_relay("wss://nostr.fly.dev", None).await?; - // client.add_relay("wss://nostr.openchain.fr", None).await?; - - // Connect to relays and keep connection alive - client.connect().await?; + //Call function to connect to relays + let client = crate::util::connect_nostr().await?; + let my_keys = crate::util::get_keys()?; let subscription = SubscriptionFilter::new() .author(mostro_keys) @@ -49,13 +58,30 @@ async fn main() -> Result<()> { client.subscribe(vec![subscription]).await?; + match &cli.command { + Some(Commands::Listorders { pubkey , orderstatus}) => { + let mostro_key = nostr::key::XOnlyPublicKey::from_bech32(pubkey)?; + + println!("Requesting orders from mostro pubId - {}", mostro_key.clone()); + println!("You are searching {} orders", orderstatus.clone()); + + //Get orders from relays + let tableoforders = get_orders_list(mostro_key, orderstatus.to_owned(), &client).await?; + let table = print_orders_table(tableoforders)?; + println!("{}",table); + std::process::exit(0); + } + None => {} + } + + // Handle notifications loop { let mut notifications = client.notifications(); while let Ok(notification) = notifications.recv().await { if let RelayPoolNotifications::ReceivedEvent(event) = notification { if let Kind::Custom(kind) = event.kind { - if kind == 30000 { + if (30000..40000).contains(&kind) { let order = types::Order::from_json(&event.content)?; println!("Event id: {}", event.id); println!("Event kind: {}", kind); diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..5d09a20 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,205 @@ +use anyhow::{ Result, Error}; +use nostr::{Kind, SubscriptionFilter,ClientMessage, RelayMessage, Event}; +use nostr::key::XOnlyPublicKey; +use nostr_sdk::{Client, Relay, RelayPoolNotifications, RelayPool}; +use nostr::key::FromSkStr; +use comfy_table::*; +use comfy_table::presets::UTF8_FULL; +use crate::types::{Order}; +use chrono::NaiveDateTime; +use log::info; +use uuid::Uuid; +use tokio::time::timeout; +use std::env; +use std::time::Duration; + +pub fn get_keys() -> Result { + // nostr private key + let nsec1privkey = env::var("NSEC_PRIVKEY").expect("$NSEC_PRIVKEY is not set"); + let my_keys = nostr::key::Keys::from_sk_str(&nsec1privkey)?; + Ok(my_keys) +} + +pub async fn connect_nostr() -> Result { + let my_keys = crate::util::get_keys()?; + + // Create new client + let client = nostr_sdk::Client::new(&my_keys); + + // Add relays + // client.add_relay("wss://relay.grunch.dev", None).await?; + // client + // .add_relay("wss://relay.cryptocculture.com", None) + // .await?; + client.add_relay("wss://nostr.openchain.fr", None).await?; + client.add_relay("wss://relay.damus.io", None).await?; + client.add_relay("wss://nostr.fly.dev", None).await?; + client.add_relay("wss://nostr.zebedee.cloud", None).await?; + client.add_relay("wss://relay.nostr.ro", None).await?; + client.add_relay("wss://nostr-pub.wellorder.net", None).await?; + + // Connect to relays and keep connection alive + client.connect().await?; + + Ok(client) +} + +pub async fn get_events_of_mostro( + relay : &Relay, + filters: Vec, + client : &Client, +) -> Result, Error> { + let mut events: Vec = Vec::new(); + + let id = Uuid::new_v4(); + + // Subscribe + info!("Subscribing for all mostro orders to relay : {}",relay.url().to_string()); + let msg = ClientMessage::new_req(id.to_string(), filters.clone()); + + info!("Message sent : {:?}",msg); + + //Send msg to relay + relay.send_msg(msg.clone()).await?; + + // let mut notifications = rx.notifications(); + let mut notifications = client.notifications(); + + while let Ok(notification) = notifications.recv().await { + if let RelayPoolNotifications::ReceivedMessage(msg) = notification { + match msg { + RelayMessage::Event { + subscription_id, + event, + } => { + if subscription_id == id.to_string() { + events.push(event.as_ref().clone()); + } + } + RelayMessage::EndOfStoredEvents { subscription_id } => { + if subscription_id == id.to_string() { + break; + } + } + _ => (), + }; + } + } + + // Unsubscribe + relay.send_msg(ClientMessage::close(id.to_string())).await?; + + + Ok(events) +} + + +pub async fn get_orders_list(pubkey : XOnlyPublicKey , status : String, client : &Client) -> Result>{ + + let filters = SubscriptionFilter::new().author(pubkey).kind(Kind::Custom(30000)); + + info!("Request to mostro id : {:?} with event kind : {:?} ",filters.authors.as_ref().unwrap(), filters.kinds.as_ref().unwrap()); + + let relays = client.relays().await; + + //Collector of mostro orders on a specific relay + let mut mostro_req : Vec> = vec![]; + + //Extracted Orders List + let mut orderslist = Vec::::new(); + + // //Vector for single order id check - maybe multiple relay could send the same order id? Check unique one... + let mut idlist = Vec::::new(); + + for relay in relays.iter() { + info!("Requesting to relay : {}",relay.0.as_str()); + + let relrequest = get_events_of_mostro(&relay.1, vec![filters.clone()], &client); + + //Using a timeout of 5 seconds to avoid unresponsive relays to block the loop forever. + if let Ok(rx) = timeout(Duration::from_secs(5), relrequest).await { + match rx { + Ok(m)=> { + if m.is_empty(){ + info!("No order found on relay {}",relay.0.to_string()); + } + else{ + mostro_req.push(m) + } + }, + Err(_e) => println!("Error"), + } + } + else + { + println!("Timeout on request from {}",relay.0.to_string()); + }; + } + + //Scan events to extract all orders + for ordersrow in mostro_req.iter(){ + for ord in ordersrow{ + let order = Order::from_json(&ord.content)?; + info!("Found Order id : {:?}",order.id.unwrap()); + + //Just add selected status + if order.status.to_string() == status { + + //If order is yet present go on... + if idlist.contains(&order.id.unwrap()) { + info!("Found same id order {}", order.id.unwrap()); + continue; + }; + idlist.push(order.id.unwrap()); + orderslist.push(order); + } + } + } + + Ok(orderslist) +} + +pub fn print_orders_table(orderstable : Vec) -> Result{ + + let mut table = Table::new(); + table.load_preset(UTF8_FULL) + .set_content_arrangement(ContentArrangement::Dynamic) + .set_width(160) + .set_header(vec![ + Cell::new("Buy/Sell").add_attribute(Attribute::Bold), + Cell::new("Order Id").add_attribute(Attribute::Bold), + Cell::new("Status").add_attribute(Attribute::Bold), + Cell::new("Amount").add_attribute(Attribute::Bold), + Cell::new("Fiat Code").add_attribute(Attribute::Bold), + Cell::new("Fiat Amount").add_attribute(Attribute::Bold), + Cell::new("Payment method").add_attribute(Attribute::Bold), + Cell::new("Created").add_attribute(Attribute::Bold), + ]); + + //Table rows + let mut rows : Vec = Vec::new(); + + + //Iterate to create table of orders + for singleorder in orderstable.into_iter(){ + let date = NaiveDateTime::from_timestamp_opt(singleorder.created_at.unwrap() as i64,0 ); + + let r = Row::from(vec![ + Cell::new(singleorder.kind.to_string()), + Cell::new(singleorder.id.unwrap()), + Cell::new(singleorder.status.to_string()), + Cell::new(singleorder.amount.to_string()), + Cell::new(singleorder.fiat_code.to_string()), + Cell::new(singleorder.fiat_amount.to_string()), + Cell::new(singleorder.payment_method.to_string()), + Cell::new(date.unwrap()), + ]); + rows.push(r); + } + + table.add_rows(rows); + + //println!("{table}"); + + Ok(table.to_string()) +} \ No newline at end of file