diff --git a/.simplicity-dex.example/keypair.txt b/.simplicity-dex.example/keypair.txt new file mode 100644 index 0000000..d130f25 --- /dev/null +++ b/.simplicity-dex.example/keypair.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.simplicity-dex.example/relays.txt b/.simplicity-dex.example/relays.txt new file mode 100644 index 0000000..6a6b8a4 --- /dev/null +++ b/.simplicity-dex.example/relays.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index ad79bba..e215b6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,27 +16,18 @@ readme = "README.md" [workspace.dependencies] anyhow = { version = "1.0.100" } clap = { version = "4.5.49", features = ["derive"] } -config = { version = "0.15.16", default-features = true } -dotenvy = { version = "0.15" } dirs = {version = "6.0.0"} +futures-util = { version = "0.3.31" } global_utils = { path = "./crates/global_utils" } -hex = { version = "0.4.3" } -itertools = { version = "0.14.0" } -reqwest = { version = "0.12.23", features = ["blocking", "json"] } -ring = { version = "0.17.14" } +nostr = { version = "0.43.1", features = ["std"] } +nostr-sdk = { version = "0.43.0" } +nostr_relay_connector = { path = "./crates/nostr_relay_connector"} +nostr_relay_processor = { path = "./crates/nostr_relay_processor"} serde = { version = "1.0.228", features = ["derive"] } serde_json = { version = "1.0.145" } -sha2 = { version = "0.10.9", features = ["compress"] } -simplicity-lang = { version = "0.5.0" } -simplicityhl = { version = "0.2.0", features = ["serde"] } -simplicityhl-core = { version = "0.1.1" } -state_change_types = { path = "./crates/state_change_types" } thiserror = { version = "2.0.17" } +tokio = { version = "1.48.0", features = ["macros", "test-util", "rt", "rt-multi-thread"] } tracing = { version = "0.1.41" } tracing-appender = { version = "0.2.3" } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } -nostr = { version = "0.43.1" } -tokio-tungstenite = { version = "0.28.0", features = ["native-tls"] } -futures-util = "0.3.31" -tokio = {version = "1.48.0", features = ["full"] } - +url = { version = "2.5.7" } diff --git a/crates/global_utils/src/env_parser.rs b/crates/global_utils/src/env_parser.rs deleted file mode 100644 index 05f944a..0000000 --- a/crates/global_utils/src/env_parser.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::env::VarError; -use thiserror::Error; -use tracing::instrument; - -#[derive(Debug, Error)] -pub enum EnvParserError { - #[error("Failed to parse env variable {missing_var_name}, err: {err}, check if it exists and is valid")] - ConfigEnvParseError { missing_var_name: String, err: VarError }, -} - -pub trait EnvParser { - const ENV_NAME: &'static str; - fn obtain_env_value() -> Result { - obtain_env_value(Self::ENV_NAME) - } -} - -#[instrument(level = "trace", skip(name), fields(name = name.as_ref()), ret)] -pub fn obtain_env_value(name: impl AsRef) -> Result { - std::env::var(name.as_ref()).map_err(|err| EnvParserError::ConfigEnvParseError { - missing_var_name: name.as_ref().to_string(), - err, - }) -} diff --git a/crates/global_utils/src/lib.rs b/crates/global_utils/src/lib.rs index 46eaf89..d991728 100644 --- a/crates/global_utils/src/lib.rs +++ b/crates/global_utils/src/lib.rs @@ -1,2 +1 @@ -pub mod env_parser; pub mod logger; diff --git a/crates/global_utils/src/logger.rs b/crates/global_utils/src/logger.rs index e9be7d9..cef6260 100644 --- a/crates/global_utils/src/logger.rs +++ b/crates/global_utils/src/logger.rs @@ -4,6 +4,9 @@ use tracing::{level_filters::LevelFilter, trace}; use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::{EnvFilter, Layer, fmt, layer::SubscriberExt, util::SubscriberInitExt}; +const ENV_VAR_NAME: &str = "DEX_LOG"; +const DEFAULT_LOG_DIRECTIVE: LevelFilter = LevelFilter::ERROR; + #[derive(Debug)] pub struct LoggerGuard { _std_out_guard: WorkerGuard, @@ -17,7 +20,12 @@ pub fn init_logger() -> LoggerGuard { .with_writer(std_out_writer) .with_target(false) .with_level(true) - .with_filter(EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new("TRACE"))); + .with_filter( + EnvFilter::builder() + .with_default_directive(DEFAULT_LOG_DIRECTIVE.into()) + .with_env_var(ENV_VAR_NAME) + .from_env_lossy(), + ); let std_err_layer = fmt::layer() .with_writer(std_err_writer) diff --git a/crates/nostr_relay_connector/Cargo.toml b/crates/nostr_relay_connector/Cargo.toml new file mode 100644 index 0000000..602275b --- /dev/null +++ b/crates/nostr_relay_connector/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "nostr_relay_connector" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +readme.workspace = true + +[dependencies] +tokio = { workspace = true, features = ["time"] } +futures-util = { workspace = true } +serde_json = { workspace = true } +anyhow = { workspace = true } +url = { workspace = true } +nostr = { workspace = true } +global_utils = { workspace = true } +nostr-sdk = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } \ No newline at end of file diff --git a/crates/nostr_relay_connector/src/error.rs b/crates/nostr_relay_connector/src/error.rs new file mode 100644 index 0000000..45f29ce --- /dev/null +++ b/crates/nostr_relay_connector/src/error.rs @@ -0,0 +1,11 @@ +#[derive(Debug, thiserror::Error)] +pub enum RelayClientError { + #[error("Failed to convert custom url to RelayURL, err: {err_msg}")] + FailedToConvertRelayUrl { err_msg: String }, + #[error("An error occurred in Nostr Client, err: {0}")] + NostrClientFailure(#[from] nostr_sdk::client::Error), + #[error("Relay Client requires for operation signature, add key to the Client")] + MissingSigner, +} + +pub type Result = std::result::Result; diff --git a/crates/nostr_relay_connector/src/lib.rs b/crates/nostr_relay_connector/src/lib.rs new file mode 100644 index 0000000..1d23e1d --- /dev/null +++ b/crates/nostr_relay_connector/src/lib.rs @@ -0,0 +1,2 @@ +pub mod error; +pub mod relay_client; diff --git a/crates/nostr_relay_connector/src/relay_client.rs b/crates/nostr_relay_connector/src/relay_client.rs new file mode 100644 index 0000000..f86f28e --- /dev/null +++ b/crates/nostr_relay_connector/src/relay_client.rs @@ -0,0 +1,113 @@ +use crate::error::RelayClientError; +use nostr::prelude::*; +use nostr_sdk::pool::Output; +use nostr_sdk::prelude::Events; +use nostr_sdk::{Client, Relay, SubscribeAutoCloseOptions}; +use std::collections::HashMap; +use std::fmt::Debug; +use std::sync::Arc; +use std::time::Duration; +use tracing::instrument; + +#[derive(Debug)] +pub struct RelayClient { + client: Client, + timeout: Duration, +} + +#[derive(Debug)] +pub struct ClientConfig { + pub timeout: Duration, +} + +impl RelayClient { + #[instrument(skip_all, level = "debug", err)] + pub async fn connect( + relay_urls: impl IntoIterator, + keys: Option, + client_config: ClientConfig, + ) -> crate::error::Result { + tracing::debug!(client_config = ?client_config, "Connecting to Nostr Relay Client(s)"); + + let client = match keys { + None => Client::default(), + Some(keys) => { + let client = Client::new(keys); + client.automatic_authentication(true); + client + } + }; + + for url in relay_urls { + let url = url + .try_into_url() + .map_err(|err| RelayClientError::FailedToConvertRelayUrl { + err_msg: format!("{:?}", err), + })?; + client.add_relay(url).await?; + } + + client.connect().await; + + Ok(Self { + client, + timeout: client_config.timeout, + }) + } + + #[instrument(skip_all, level = "debug", ret)] + pub async fn req_and_wait(&self, filter: Filter) -> crate::error::Result { + tracing::debug!(filter = ?filter, "Requesting events with filter"); + let events = self.client.fetch_combined_events(filter, self.timeout).await?; + Ok(events) + } + + #[instrument(skip_all, level = "debug", ret)] + pub async fn get_signer(&self) -> crate::error::Result> { + if !self.client.has_signer().await { + return Err(RelayClientError::MissingSigner); + } + Ok(self.client.signer().await?) + } + + #[instrument(skip_all, level = "debug", ret)] + pub async fn get_relays(&self) -> HashMap { + self.client.relays().await + } + + #[instrument(skip_all, level = "debug", ret)] + pub async fn publish_event(&self, event: &Event) -> crate::error::Result { + if !self.client.has_signer().await { + return Err(RelayClientError::MissingSigner); + } + let event_id = self.client.send_event(event).await?; + let event_id = Self::handle_relay_output(event_id)?; + Ok(event_id) + } + + #[instrument(skip(self), level = "debug")] + pub async fn subscribe( + &self, + filter: Filter, + opts: Option, + ) -> crate::error::Result { + Ok(self.client.subscribe(filter, opts).await?.val) + } + + #[instrument(skip(self), level = "debug")] + pub async fn unsubscribe(&self, subscription_id: &SubscriptionId) { + self.client.unsubscribe(subscription_id).await; + } + + #[instrument(skip_all, level = "debug", ret)] + pub async fn disconnect(&self) -> crate::error::Result<()> { + self.client.disconnect().await; + Ok(()) + } + + #[instrument(level = "debug")] + fn handle_relay_output(output: Output) -> crate::error::Result { + tracing::debug!(output = ?output, "Handling Relay output"); + Ok(output.val) + } +} diff --git a/crates/nostr_relay_processor/Cargo.toml b/crates/nostr_relay_processor/Cargo.toml index 4466b6f..6f17e63 100644 --- a/crates/nostr_relay_processor/Cargo.toml +++ b/crates/nostr_relay_processor/Cargo.toml @@ -8,3 +8,10 @@ readme.workspace = true [dependencies] anyhow = { workspace = true } +tokio = { workspace = true } +global_utils = { workspace = true } +nostr-sdk = { workspace = true } +nostr = { workspace = true } +nostr_relay_connector = { workspace = true } +tracing = { workspace = true } +thiserror = { workspace = true } diff --git a/crates/nostr_relay_processor/src/error.rs b/crates/nostr_relay_processor/src/error.rs new file mode 100644 index 0000000..0a824af --- /dev/null +++ b/crates/nostr_relay_processor/src/error.rs @@ -0,0 +1,15 @@ +use nostr::SignerError; +use nostr::filter::SingleLetterTagError; +use nostr_relay_connector::error::RelayClientError; + +#[derive(thiserror::Error, Debug)] +pub enum RelayProcessorError { + #[error(transparent)] + RelayClient(#[from] RelayClientError), + #[error("Signer error: {0}")] + Signer(#[from] SignerError), + #[error("Single letter error: {0}")] + SingleLetterTag(#[from] SingleLetterTagError), +} + +pub type Result = std::result::Result; diff --git a/crates/nostr_relay_processor/src/handlers.rs b/crates/nostr_relay_processor/src/handlers.rs index 26c0665..62df426 100644 --- a/crates/nostr_relay_processor/src/handlers.rs +++ b/crates/nostr_relay_processor/src/handlers.rs @@ -1,3 +1,5 @@ +pub(crate) mod get_events; pub(crate) mod list_orders; +pub(crate) mod order_replies; pub(crate) mod place_order; pub(crate) mod reply_order; diff --git a/crates/nostr_relay_processor/src/handlers/get_events.rs b/crates/nostr_relay_processor/src/handlers/get_events.rs new file mode 100644 index 0000000..a29bfdd --- /dev/null +++ b/crates/nostr_relay_processor/src/handlers/get_events.rs @@ -0,0 +1,22 @@ +pub mod ids { + use nostr::{EventId, Filter}; + use nostr_relay_connector::relay_client::RelayClient; + use nostr_sdk::prelude::Events; + use std::collections::{BTreeMap, BTreeSet}; + + pub async fn handle(client: &RelayClient, event_id: EventId) -> crate::error::Result { + let events = client + .req_and_wait(Filter { + ids: Some(BTreeSet::from([event_id])), + authors: None, + kinds: None, + search: None, + since: None, + until: None, + limit: None, + generic_tags: BTreeMap::default(), + }) + .await?; + Ok(events) + } +} diff --git a/crates/nostr_relay_processor/src/handlers/list_orders.rs b/crates/nostr_relay_processor/src/handlers/list_orders.rs index 26bad30..0a478cb 100644 --- a/crates/nostr_relay_processor/src/handlers/list_orders.rs +++ b/crates/nostr_relay_processor/src/handlers/list_orders.rs @@ -1,4 +1,34 @@ -#[warn(unused)] -pub fn handle() -> anyhow::Result<()> { - Ok(()) +use crate::types::{CustomKind, MakerOrderKind}; +use nostr::{Filter, Timestamp}; +use nostr_relay_connector::relay_client::RelayClient; +use nostr_sdk::prelude::Events; +use std::collections::{BTreeMap, BTreeSet}; + +pub async fn handle(client: &RelayClient) -> crate::error::Result { + let events = client + .req_and_wait(Filter { + ids: None, + authors: None, + kinds: Some(BTreeSet::from([MakerOrderKind::get_kind()])), + search: None, + since: None, + until: None, + limit: None, + generic_tags: BTreeMap::default(), + }) + .await?; + let events = filter_expired_events(events); + Ok(events) +} + +#[inline] +fn filter_expired_events(events_to_filter: Events) -> Events { + let time_now = Timestamp::now(); + events_to_filter + .into_iter() + .filter(|x| match x.tags.expiration() { + None => false, + Some(t) => t.as_u64() > time_now.as_u64(), + }) + .collect() } diff --git a/crates/nostr_relay_processor/src/handlers/order_replies.rs b/crates/nostr_relay_processor/src/handlers/order_replies.rs new file mode 100644 index 0000000..43ee191 --- /dev/null +++ b/crates/nostr_relay_processor/src/handlers/order_replies.rs @@ -0,0 +1,21 @@ +use crate::types::{CustomKind, TakerOrderKind}; +use nostr::{EventId, Filter, SingleLetterTag}; +use nostr_relay_connector::relay_client::RelayClient; +use nostr_sdk::prelude::Events; +use std::collections::{BTreeMap, BTreeSet}; + +pub async fn handle(client: &RelayClient, event_id: EventId) -> crate::error::Result { + let events = client + .req_and_wait(Filter { + ids: None, + authors: None, + kinds: Some(BTreeSet::from([TakerOrderKind::get_kind()])), + search: None, + since: None, + until: None, + limit: None, + generic_tags: BTreeMap::from([(SingleLetterTag::from_char('e')?, BTreeSet::from([event_id.to_string()]))]), + }) + .await?; + Ok(events) +} diff --git a/crates/nostr_relay_processor/src/handlers/place_order.rs b/crates/nostr_relay_processor/src/handlers/place_order.rs index e1b38d1..de4b1e9 100644 --- a/crates/nostr_relay_processor/src/handlers/place_order.rs +++ b/crates/nostr_relay_processor/src/handlers/place_order.rs @@ -1,4 +1,33 @@ -#[allow(unused)] -pub fn handle() -> anyhow::Result<()> { - Ok(()) +use crate::relay_processor::OrderPlaceEventTags; +use crate::types::{BLOCKSTREAM_MAKER_CONTENT, CustomKind, MAKER_EXPIRATION_TIME, MakerOrderKind}; +use nostr::{EventBuilder, EventId, Tag, TagKind, Timestamp}; +use nostr_relay_connector::relay_client::RelayClient; +use std::borrow::Cow; + +pub async fn handle(client: &RelayClient, tags: OrderPlaceEventTags) -> crate::error::Result { + let client_signer = client.get_signer().await?; + let client_pubkey = client_signer.get_public_key().await?; + + let timestamp_now = Timestamp::now(); + + let maker_order = EventBuilder::new(MakerOrderKind::get_kind(), BLOCKSTREAM_MAKER_CONTENT) + .tags([ + Tag::public_key(client_pubkey), + Tag::expiration(Timestamp::from(timestamp_now.as_u64() + MAKER_EXPIRATION_TIME)), + Tag::custom( + TagKind::Custom(Cow::from("compiler")), + [tags.compiler_name, tags.compiler_build_hash], + ), + Tag::custom(TagKind::Custom(Cow::from("asset_to_buy")), [tags.asset_to_buy]), + Tag::custom(TagKind::Custom(Cow::from("asset_to_sell")), [tags.asset_to_sell]), + Tag::custom(TagKind::Custom(Cow::from("price")), [tags.price.to_string()]), + ]) + .custom_created_at(timestamp_now); + + let text_note = maker_order.build(client_pubkey); + let signed_event = client_signer.sign_event(text_note).await?; + + let maker_order_event_id = client.publish_event(&signed_event).await?; + + Ok(maker_order_event_id) } diff --git a/crates/nostr_relay_processor/src/handlers/reply_order.rs b/crates/nostr_relay_processor/src/handlers/reply_order.rs index 26bad30..339816b 100644 --- a/crates/nostr_relay_processor/src/handlers/reply_order.rs +++ b/crates/nostr_relay_processor/src/handlers/reply_order.rs @@ -1,4 +1,33 @@ -#[warn(unused)] -pub fn handle() -> anyhow::Result<()> { - Ok(()) +use crate::relay_processor::OrderReplyEventTags; +use crate::types::{BLOCKSTREAM_TAKER_CONTENT, CustomKind, TakerOrderKind}; +use nostr::{EventBuilder, EventId, NostrSigner, PublicKey, Tag, TagKind, Timestamp}; +use nostr_relay_connector::relay_client::RelayClient; +use std::borrow::Cow; + +pub async fn handle( + client: &RelayClient, + maker_event_id: EventId, + maker_pubkey: PublicKey, + tags: OrderReplyEventTags, +) -> crate::error::Result { + let client_signer = client.get_signer().await?; + let client_pubkey = client_signer.get_public_key().await?; + + let timestamp_now = Timestamp::now(); + + let taker_response = EventBuilder::new(TakerOrderKind::get_kind(), BLOCKSTREAM_TAKER_CONTENT) + .tags([ + Tag::public_key(client_pubkey), + Tag::event(maker_event_id), + Tag::custom(TagKind::Custom(Cow::from("maker_pubkey")), [maker_pubkey]), + Tag::custom(TagKind::Custom(Cow::from("tx_id")), [tags.tx_id]), + ]) + .custom_created_at(timestamp_now); + + let reply_event = taker_response.build(client_pubkey); + let reply_event = client_signer.sign_event(reply_event).await?; + + let event_id = client.publish_event(&reply_event).await?; + + Ok(event_id) } diff --git a/crates/nostr_relay_processor/src/lib.rs b/crates/nostr_relay_processor/src/lib.rs index 232c395..096e506 100644 --- a/crates/nostr_relay_processor/src/lib.rs +++ b/crates/nostr_relay_processor/src/lib.rs @@ -1,2 +1,4 @@ -mod handlers; -mod relay_processor; +pub mod error; +pub mod handlers; +pub mod relay_processor; +pub mod types; diff --git a/crates/nostr_relay_processor/src/relay_processor.rs b/crates/nostr_relay_processor/src/relay_processor.rs index 8de376b..36fd341 100644 --- a/crates/nostr_relay_processor/src/relay_processor.rs +++ b/crates/nostr_relay_processor/src/relay_processor.rs @@ -1,29 +1,66 @@ use crate::handlers; +use nostr::prelude::IntoNostrSigner; +use nostr::{EventId, PublicKey, TryIntoUrl}; +use nostr_relay_connector::relay_client::{ClientConfig, RelayClient}; +use nostr_sdk::prelude::Events; -#[allow(unused)] -pub struct RelayProcessor {} +pub struct RelayProcessor { + relay_client: RelayClient, +} + +#[derive(Debug, Default, Clone)] +pub struct OrderPlaceEventTags { + pub asset_to_sell: String, + pub asset_to_buy: String, + pub price: u64, + pub expiry: u64, + pub compiler_name: String, + pub compiler_build_hash: String, +} + +#[derive(Debug, Default, Clone)] +pub struct OrderReplyEventTags { + pub tx_id: String, +} impl RelayProcessor { - #[allow(unused)] - pub fn new() -> Self { - Self {} + pub async fn try_from_config( + relay_urls: impl IntoIterator, + keys: Option, + client_config: ClientConfig, + ) -> crate::error::Result { + Ok(RelayProcessor { + relay_client: RelayClient::connect(relay_urls, keys, client_config).await?, + }) + } + + pub async fn place_order(&self, tags: OrderPlaceEventTags) -> crate::error::Result { + let event_id = handlers::place_order::handle(&self.relay_client, tags).await?; + Ok(event_id) + } + + pub async fn list_orders(&self) -> crate::error::Result { + let events = handlers::list_orders::handle(&self.relay_client).await?; + Ok(events) } - #[allow(unused)] - pub fn place_order() -> anyhow::Result<()> { - handlers::place_order::handle()?; - Ok(()) + pub async fn reply_order( + &self, + maker_event_id: EventId, + maker_pubkey: PublicKey, + tags: OrderReplyEventTags, + ) -> crate::error::Result { + let event_id = handlers::reply_order::handle(&self.relay_client, maker_event_id, maker_pubkey, tags).await?; + Ok(event_id) } - #[allow(unused)] - pub fn list_order() -> anyhow::Result<()> { - handlers::list_orders::handle()?; - Ok(()) + pub async fn get_order_replies(&self, event_id: EventId) -> crate::error::Result { + let events = handlers::order_replies::handle(&self.relay_client, event_id).await?; + Ok(events) } - #[allow(unused)] - pub fn reply_order() -> anyhow::Result<()> { - handlers::reply_order::handle()?; - Ok(()) + pub async fn get_events_by_id(&self, event_id: EventId) -> crate::error::Result { + let events = handlers::get_events::ids::handle(&self.relay_client, event_id).await?; + Ok(events) } } diff --git a/crates/nostr_relay_processor/src/types.rs b/crates/nostr_relay_processor/src/types.rs new file mode 100644 index 0000000..81b807b --- /dev/null +++ b/crates/nostr_relay_processor/src/types.rs @@ -0,0 +1,27 @@ +use nostr::Kind; + +pub trait CustomKind { + const ORDER_KIND_NUMBER: u16; + fn get_kind() -> Kind { + Kind::from(Self::ORDER_KIND_NUMBER) + } + fn get_u16() -> u16 { + Self::ORDER_KIND_NUMBER + } +} + +pub const POW_DIFFICULTY: u8 = 1; +pub const BLOCKSTREAM_MAKER_CONTENT: &str = "Liquid order [Maker]!"; +pub const BLOCKSTREAM_TAKER_CONTENT: &str = "Liquid order [Taker]!"; +pub const MAKER_EXPIRATION_TIME: u64 = 60; + +pub struct MakerOrderKind; +pub struct TakerOrderKind; + +impl CustomKind for MakerOrderKind { + const ORDER_KIND_NUMBER: u16 = 9901; +} + +impl CustomKind for TakerOrderKind { + const ORDER_KIND_NUMBER: u16 = 9902; +} diff --git a/crates/nostr_relay_processor/tests/test_order_placing.rs b/crates/nostr_relay_processor/tests/test_order_placing.rs new file mode 100644 index 0000000..20a6020 --- /dev/null +++ b/crates/nostr_relay_processor/tests/test_order_placing.rs @@ -0,0 +1,88 @@ +mod utils; + +mod tests { + use crate::utils::{DEFAULT_CLIENT_TIMEOUT, DEFAULT_RELAY_LIST, TEST_LOGGER}; + use nostr::{EventId, Keys, ToBech32}; + use nostr_relay_connector::relay_client::ClientConfig; + use nostr_relay_processor::relay_processor::{OrderPlaceEventTags, OrderReplyEventTags, RelayProcessor}; + use nostr_relay_processor::types::{CustomKind, MakerOrderKind, TakerOrderKind}; + use std::time::Duration; + use tracing::{info, instrument}; + + #[instrument] + #[tokio::test] + async fn test_wss_metadata() -> anyhow::Result<()> { + let _guard = &*TEST_LOGGER; + let key_maker = Keys::generate(); + info!( + "=== Maker pubkey: {}, privatekey: {}", + key_maker.public_key.to_bech32()?, + key_maker.secret_key().to_bech32()? + ); + let relay_processor_maker = RelayProcessor::try_from_config( + DEFAULT_RELAY_LIST, + Some(key_maker.clone()), + ClientConfig { + timeout: Duration::from_secs(DEFAULT_CLIENT_TIMEOUT), + }, + ) + .await?; + + let placed_order_event_id = relay_processor_maker + .place_order(OrderPlaceEventTags::default()) + .await?; + info!("=== placed order event id: {}", placed_order_event_id); + let order = relay_processor_maker.get_events_by_id(placed_order_event_id).await?; + info!("=== placed order: {:#?}", order); + assert_eq!(order.len(), 1); + assert_eq!(order.first().unwrap().kind, MakerOrderKind::get_kind()); + + let key_taker = Keys::generate(); + let relay_processor_taker = RelayProcessor::try_from_config( + DEFAULT_RELAY_LIST, + Some(key_taker.clone()), + ClientConfig { + timeout: Duration::from_secs(DEFAULT_CLIENT_TIMEOUT), + }, + ) + .await?; + info!( + "=== Taker pubkey: {}, privatekey: {}", + key_taker.public_key.to_bech32()?, + key_taker.secret_key().to_bech32()? + ); + let reply_event_id = relay_processor_taker + .reply_order( + placed_order_event_id, + key_maker.public_key, + OrderReplyEventTags::default(), + ) + .await?; + info!("=== order reply event id: {}", reply_event_id); + + let order_replies = relay_processor_maker.get_order_replies(placed_order_event_id).await?; + info!( + "=== order replies, amount: {}, orders: {:#?}", + order_replies.len(), + order_replies + ); + assert_eq!(order_replies.len(), 1); + assert_eq!(order_replies.first().unwrap().kind, TakerOrderKind::get_kind()); + + let orders_listed = relay_processor_maker.list_orders().await?; + info!( + "=== orders listed, amount: {}, orders: {:#?}", + orders_listed.len(), + orders_listed + ); + assert!( + orders_listed + .iter() + .map(|x| x.id) + .collect::>() + .contains(&placed_order_event_id) + ); + + Ok(()) + } +} diff --git a/crates/nostr_relay_processor/tests/utils.rs b/crates/nostr_relay_processor/tests/utils.rs new file mode 100644 index 0000000..aa9c9f6 --- /dev/null +++ b/crates/nostr_relay_processor/tests/utils.rs @@ -0,0 +1,6 @@ +use global_utils::logger::{LoggerGuard, init_logger}; +use std::sync::LazyLock; + +pub static TEST_LOGGER: LazyLock = LazyLock::new(init_logger); +pub const DEFAULT_RELAY_LIST: [&str; 1] = ["wss://relay.damus.io"]; +pub const DEFAULT_CLIENT_TIMEOUT: u64 = 10; diff --git a/nostr_options_cli/Cargo.toml b/nostr_options_cli/Cargo.toml index 791f153..ef76bce 100644 --- a/nostr_options_cli/Cargo.toml +++ b/nostr_options_cli/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "nostr_options_cli" +name = "simplicity-dex" version = "0.1.0" edition = "2024" [dependencies] +anyhow = { workspace = true } nostr = { workspace = true } global_utils = { workspace = true } -tokio-tungstenite = { workspace = true } futures-util = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } @@ -14,4 +14,6 @@ clap = { workspace = true } dirs = { workspace = true } tracing = { workspace = true } thiserror = { workspace = true } +nostr_relay_connector = { workspace = true } +nostr_relay_processor = { workspace = true } diff --git a/nostr_options_cli/src/cli.rs b/nostr_options_cli/src/cli.rs new file mode 100644 index 0000000..60d85b0 --- /dev/null +++ b/nostr_options_cli/src/cli.rs @@ -0,0 +1,165 @@ +use crate::utils::{ + DEFAULT_CLIENT_TIMEOUT_SECS, check_file_existence, default_key_path, default_relays_path, get_valid_key_from_file, + get_valid_urls_from_file, write_into_stdout, +}; +use clap::{Parser, Subcommand}; +use nostr::{EventId, PublicKey}; +use nostr_relay_connector::relay_client::ClientConfig; +use nostr_relay_processor::relay_processor::{OrderPlaceEventTags, OrderReplyEventTags, RelayProcessor}; +use std::path::PathBuf; +use std::time::Duration; +use tracing::instrument; + +#[derive(Parser)] +pub struct Cli { + #[arg( + short = 'k', + long, + help = "Specify private key for posting authorized events on Nostr Relay", + value_parser = check_file_existence + )] + key_path: Option, + #[arg( + short = 'r', + long, help = "Specify file with list of relays to use", + value_parser = check_file_existence + )] + relays_path: Option, + #[command(subcommand)] + command: Command, +} + +#[derive(Debug, Subcommand)] +enum Command { + #[command(about = "Commands collection for the maker role")] + Maker { + #[command(subcommand)] + action: MakerCommands, + }, + #[command(about = "Commands collection for the taker role")] + Taker { + #[command(subcommand)] + action: TakerCommands, + }, + #[command(about = "Get replies for a specific order by its ID [no authentication required]")] + GetOrderReplies { + #[arg(short = 'i', long)] + event_id: EventId, + }, + #[command(about = "List available orders from relays [no authentication required]")] + ListOrders, + #[command(about = "Get events by its ID [no authentication required]")] + GetEventsById { + #[arg(short = 'i', long)] + event_id: EventId, + }, +} + +#[derive(Debug, Subcommand)] +enum MakerCommands { + #[command(about = "Create order as Maker on Relays specified [authentication required]")] + CreateOrder { + #[arg(short = 's', long, default_value = "")] + asset_to_sell: String, + #[arg(short = 'b', long, default_value = "")] + asset_to_buy: String, + #[arg(short = 'p', long, default_value = "0")] + price: u64, + #[arg(short = 'e', long, default_value = "0")] + expiry: u64, + #[arg(short = 'c', long, default_value = "")] + compiler_name: String, + #[arg(short = 's', long, default_value = "")] + compiler_build_hash: String, + }, +} + +#[derive(Debug, Subcommand)] +enum TakerCommands { + #[command(about = "Reply order as Taker on Relays specified [authentication required]")] + ReplyOrder { + #[arg(short = 'i', long)] + maker_event_id: EventId, + #[arg(short = 'p', long, help = " Pubkey in bech32 or hex format")] + maker_pubkey: PublicKey, + #[arg(short = 't', long, help = "Txid from funding transaction step", required = false)] + tx_id: String, + }, +} + +impl Cli { + #[instrument(skip(self))] + pub async fn process(self) -> crate::error::Result<()> { + let keys = { + match get_valid_key_from_file(&self.key_path.unwrap_or(default_key_path())) { + Ok(keys) => Some(keys), + Err(err) => { + tracing::warn!("Failed to parse key, {err}"); + None + } + } + }; + let relays_urls = get_valid_urls_from_file(&self.relays_path.unwrap_or(default_relays_path()))?; + let relay_processor = RelayProcessor::try_from_config( + relays_urls, + keys, + ClientConfig { + timeout: Duration::from_secs(DEFAULT_CLIENT_TIMEOUT_SECS), + }, + ) + .await?; + + let msg = { + match self.command { + Command::Maker { action } => match action { + MakerCommands::CreateOrder { + asset_to_sell, + asset_to_buy, + price, + expiry, + compiler_name, + compiler_build_hash, + } => { + let res = relay_processor + .place_order(OrderPlaceEventTags { + asset_to_sell, + asset_to_buy, + price, + expiry, + compiler_name, + compiler_build_hash, + }) + .await?; + format!("Creating order result: {res:#?}") + } + }, + Command::Taker { action } => match action { + TakerCommands::ReplyOrder { + maker_event_id, + maker_pubkey, + tx_id, + } => { + let res = relay_processor + .reply_order(maker_event_id, maker_pubkey, OrderReplyEventTags { tx_id }) + .await?; + format!("Replying order result: {res:#?}") + } + }, + Command::GetOrderReplies { event_id } => { + let res = relay_processor.get_order_replies(event_id).await?; + format!("Order '{event_id}' replies: {res:#?}") + } + Command::ListOrders => { + let res = relay_processor.list_orders().await?; + format!("List of available orders: {res:#?}") + } + Command::GetEventsById { event_id } => { + let res = relay_processor.get_events_by_id(event_id).await?; + format!("List of available events: {res:#?}") + } + } + }; + write_into_stdout(msg)?; + Ok(()) + } +} diff --git a/nostr_options_cli/src/cli_processor.rs b/nostr_options_cli/src/cli_processor.rs deleted file mode 100644 index 53bb4f6..0000000 --- a/nostr_options_cli/src/cli_processor.rs +++ /dev/null @@ -1,124 +0,0 @@ -use clap::{Parser, Subcommand}; -use std::path::PathBuf; -use tracing::instrument; - -use crate::utils::{default_key_path, default_relays_path, write_into_stdout}; - -#[derive(Parser)] -pub struct Cli { - #[arg(short = 'k', long)] - key_path: Option, - #[command(subcommand)] - command: Command, -} - -#[derive(Debug, Subcommand)] -enum Command { - Maker { - #[command(subcommand)] - action: MakerCommand, - }, - - Taker { - #[command(subcommand)] - action: TakerCommand, - }, -} - -#[derive(Debug, Subcommand)] -enum MakerCommand { - CreateOrder { - #[arg(short = 'm', long)] - message: String, - - #[arg(short = 'r', long)] - relays_path: Option, - }, - - GetOrderReply { - #[arg(short = 'i', long)] - id: String, - - #[arg(short = 'r', long)] - relays_path: Option, - }, -} - -#[derive(Debug, Subcommand)] -enum TakerCommand { - ListOrders { - #[arg(short = 'r', long)] - relays_path: Option, - }, - - ReplyOrder { - #[arg(short = 'i', long)] - id: String, - }, -} - -impl Cli { - #[instrument(skip(self))] - pub fn process(self) -> crate::error::Result<()> { - let msg = { - match self.command { - Command::Maker { action } => match action { - MakerCommand::CreateOrder { message, relays_path } => { - let key_path = self.key_path.unwrap_or(default_key_path()); - let relays_path = relays_path.unwrap_or(default_relays_path()); - format!( - "Maker: Create Order\n message: {}\n key_path: {}\n relays_path: {}", - message, - key_path.display(), - relays_path.display() - ) - - // TODO: - //processor.create_order(message, key_path, relays_path).await?; - } - - MakerCommand::GetOrderReply { id, relays_path } => { - let key_path = self.key_path.unwrap_or(default_key_path()); - let relays_path = relays_path.unwrap_or(default_relays_path()); - format!( - "Maker: Get Order Reply\n id: {}\n key_path: {}\n relays_path: {}", - id, - key_path.display(), - relays_path.display() - ) - - // TODO: - //processor.get_order_reply(id, key_path, relays_path).await?; - } - }, - - Command::Taker { action } => match action { - TakerCommand::ListOrders { relays_path } => { - let key_path = self.key_path.unwrap_or(default_key_path()); - let relays_path = relays_path.unwrap_or(default_relays_path()); - format!( - "Taker: List Orders\n key_path: {}\n relays_path: {}", - key_path.to_string_lossy(), - relays_path.to_string_lossy() - ) - - // let key = ... - // let relays: Vec = ... - // TODO: - //processor.list_orders(key_path, relays_path).await?; - } - - TakerCommand::ReplyOrder { id } => { - let key_path = self.key_path.unwrap_or(default_key_path()); - format!("Taker: Reply Order\n id: {}\n key_path: {}", id, key_path.display()) - - // TODO - //processor.reply_order(id, key_path).await?; - } - }, - } - }; - write_into_stdout(msg)?; - Ok(()) - } -} diff --git a/nostr_options_cli/src/error.rs b/nostr_options_cli/src/error.rs index 9188719..5c2bdeb 100644 --- a/nostr_options_cli/src/error.rs +++ b/nostr_options_cli/src/error.rs @@ -1,7 +1,17 @@ +use crate::utils::FileError; +use nostr_relay_connector::error::RelayClientError; +use nostr_relay_processor::error::RelayProcessorError; + pub type Result = core::result::Result; #[derive(thiserror::Error, Debug)] pub enum CliError { #[error("Occcurred error with io, err: {0}")] Io(#[from] std::io::Error), + #[error(transparent)] + File(#[from] FileError), + #[error(transparent)] + RelayClient(#[from] RelayClientError), + #[error(transparent)] + RelayProcessor(#[from] RelayProcessorError), } diff --git a/nostr_options_cli/src/lib.rs b/nostr_options_cli/src/lib.rs index d42eeff..fe25d5b 100644 --- a/nostr_options_cli/src/lib.rs +++ b/nostr_options_cli/src/lib.rs @@ -1,3 +1,3 @@ -pub mod cli_processor; +pub mod cli; pub mod error; mod utils; diff --git a/nostr_options_cli/src/main.rs b/nostr_options_cli/src/main.rs index 478e789..f06cba9 100644 --- a/nostr_options_cli/src/main.rs +++ b/nostr_options_cli/src/main.rs @@ -1,14 +1,13 @@ use clap::Parser; use global_utils::logger::init_logger; -use nostr::prelude::*; -use nostr_options_cli::cli_processor::Cli; +use simplicity_dex::cli::Cli; +use tracing::instrument; #[tokio::main] -async fn main() -> Result<()> { +#[instrument] +async fn main() -> anyhow::Result<()> { let _logger_guard = init_logger(); - let cli = Cli::parse(); - cli.process()?; - + cli.process().await?; Ok(()) } diff --git a/nostr_options_cli/src/utils.rs b/nostr_options_cli/src/utils.rs index af75e36..16da6f8 100644 --- a/nostr_options_cli/src/utils.rs +++ b/nostr_options_cli/src/utils.rs @@ -1,7 +1,12 @@ +use nostr::{Keys, RelayUrl}; +use std::collections::HashSet; +use std::io::BufRead; +use std::str::FromStr; use std::{io::Write, path::PathBuf}; -const DEFAULT_RELAYS_FILEPATH: &str = ".default_relays_path.txt"; -const DEFAULT_KEY_PATH: &str = ".default_keypair_path.txt"; +const DEFAULT_RELAYS_FILEPATH: &str = ".simplicity-dex/relays.txt"; +const DEFAULT_KEY_PATH: &str = ".simplicity-dex/keypair.txt"; +pub const DEFAULT_CLIENT_TIMEOUT_SECS: u64 = 10; pub fn write_into_stdout + std::fmt::Debug>(text: T) -> std::io::Result { let mut output = text.as_ref().to_string(); @@ -20,3 +25,56 @@ pub fn default_relays_path() -> PathBuf { .unwrap_or_else(|| PathBuf::from(".")) .join(DEFAULT_RELAYS_FILEPATH) } +#[derive(Debug, thiserror::Error)] +pub enum FileError { + #[error("Unable to parse url: {1}, error: {0}")] + UrlParseError(nostr::types::url::Error, String), + #[error("Got error on reading/writing to file: {1}, error: {0}")] + ProblemWithFile(std::io::Error, PathBuf), + #[error("Incorrect path to the file, please check validity of the path (err: path is not a file), got path: {0}")] + IncorrectPathToFile(PathBuf), + #[error("File is empty, got path: {0}")] + EmptyFile(PathBuf), + #[error("File is empty, got path: {0}")] + KeyParseError(nostr::key::Error, String), +} + +pub fn check_file_existence(path: &str) -> Result { + let path = PathBuf::from(path); + + if path.is_file() { + Ok(path) + } else { + Err(FileError::IncorrectPathToFile(path.clone()).to_string()) + } +} + +pub fn get_valid_urls_from_file(filepath: &PathBuf) -> Result, FileError> { + let file = std::fs::File::open(filepath).map_err(|x| FileError::ProblemWithFile(x, filepath.clone()))?; + let reader = std::io::BufReader::new(file); + let mut set = HashSet::new(); + for x in reader.lines() { + let line = x.map_err(|x| FileError::ProblemWithFile(x, filepath.clone()))?; + match RelayUrl::parse(&line) { + Ok(url) => { + set.insert(url); + } + Err(e) => { + return Err(FileError::UrlParseError(e, line)); + } + } + } + Ok(set.into_iter().collect::>()) +} + +pub fn get_valid_key_from_file(filepath: &PathBuf) -> Result { + let file = std::fs::File::open(filepath).map_err(|x| FileError::ProblemWithFile(x, filepath.clone()))?; + let reader = std::io::BufReader::new(file); + let key = reader + .lines() + .next() + .ok_or_else(|| FileError::EmptyFile(filepath.clone()))? + .map_err(|x| FileError::ProblemWithFile(x, filepath.clone()))?; + let key = Keys::from_str(&key).map_err(|e| FileError::KeyParseError(e, key))?; + Ok(key) +}