Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .simplicity-dex.example/keypair.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<your_private_key>
1 change: 1 addition & 0 deletions .simplicity-dex.example/relays.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<your_preferred_relays>
23 changes: 7 additions & 16 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
24 changes: 0 additions & 24 deletions crates/global_utils/src/env_parser.rs

This file was deleted.

1 change: 0 additions & 1 deletion crates/global_utils/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
pub mod env_parser;
pub mod logger;
10 changes: 9 additions & 1 deletion crates/global_utils/src/logger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand Down
19 changes: 19 additions & 0 deletions crates/nostr_relay_connector/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 }
11 changes: 11 additions & 0 deletions crates/nostr_relay_connector/src/error.rs
Original file line number Diff line number Diff line change
@@ -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<T> = std::result::Result<T, RelayClientError>;
2 changes: 2 additions & 0 deletions crates/nostr_relay_connector/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod error;
pub mod relay_client;
113 changes: 113 additions & 0 deletions crates/nostr_relay_connector/src/relay_client.rs
Original file line number Diff line number Diff line change
@@ -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<Item = impl TryIntoUrl>,
keys: Option<impl IntoNostrSigner>,
client_config: ClientConfig,
) -> crate::error::Result<Self> {
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<Events> {
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<Arc<dyn NostrSigner>> {
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<RelayUrl, Relay> {
self.client.relays().await
}

#[instrument(skip_all, level = "debug", ret)]
pub async fn publish_event(&self, event: &Event) -> crate::error::Result<EventId> {
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<SubscribeAutoCloseOptions>,
) -> crate::error::Result<SubscriptionId> {
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<T: Debug>(output: Output<T>) -> crate::error::Result<T> {
tracing::debug!(output = ?output, "Handling Relay output");
Ok(output.val)
}
}
7 changes: 7 additions & 0 deletions crates/nostr_relay_processor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
15 changes: 15 additions & 0 deletions crates/nostr_relay_processor/src/error.rs
Original file line number Diff line number Diff line change
@@ -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<T> = std::result::Result<T, RelayProcessorError>;
2 changes: 2 additions & 0 deletions crates/nostr_relay_processor/src/handlers.rs
Original file line number Diff line number Diff line change
@@ -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;
22 changes: 22 additions & 0 deletions crates/nostr_relay_processor/src/handlers/get_events.rs
Original file line number Diff line number Diff line change
@@ -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<Events> {
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)
}
}
36 changes: 33 additions & 3 deletions crates/nostr_relay_processor/src/handlers/list_orders.rs
Original file line number Diff line number Diff line change
@@ -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<Events> {
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()
}
21 changes: 21 additions & 0 deletions crates/nostr_relay_processor/src/handlers/order_replies.rs
Original file line number Diff line number Diff line change
@@ -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<Events> {
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)
}
35 changes: 32 additions & 3 deletions crates/nostr_relay_processor/src/handlers/place_order.rs
Original file line number Diff line number Diff line change
@@ -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<EventId> {
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)
}
Loading