diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c45a5a2f..ee252779 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,12 +20,12 @@ jobs: - run: rustup update stable && rustup default stable # hashgraph-like-consensus's build.rs shells out to protoc via prost-build. - run: sudo apt-get update && sudo apt-get install -y protobuf-compiler - # chat-cli pulls in components' embedded_p2p_delivery feature, whose - # build.rs links liblogosdelivery (built via Nix or LOGOS_DELIVERY_LIB_DIR). - # The smoketest job builds and exercises it under Nix; here we keep the - # toolchain-only job fast by skipping it. - - run: cargo build --verbose --workspace --exclude chat-cli - - run: cargo test --verbose --workspace --exclude chat-cli + # chat-cli and the embedded-logos-delivery crate link liblogosdelivery + # (built via Nix or LOGOS_DELIVERY_LIB_DIR). The smoketest job builds and + # exercises them under Nix; here we keep the toolchain-only job fast by + # skipping them. + - run: cargo build --verbose --workspace --exclude chat-cli --exclude embedded-logos-delivery + - run: cargo test --verbose --workspace --exclude chat-cli --exclude embedded-logos-delivery clippy: name: Clippy diff --git a/Cargo.lock b/Cargo.lock index 9fcbbe77..b2d63115 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2072,6 +2072,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-logos-delivery" +version = "0.1.0" +dependencies = [ + "base64", + "crossbeam-channel", + "libchat", + "logos-generic-chat", + "serde", + "serde_json", + "thiserror", + "tracing", +] + [[package]] name = "enum-ordinalize" version = "4.3.2" @@ -3599,6 +3613,18 @@ dependencies = [ [[package]] name = "logos-chat" version = "0.1.0" +dependencies = [ + "components", + "crossbeam-channel", + "embedded-logos-delivery", + "libchat", + "logos-account", + "logos-generic-chat", +] + +[[package]] +name = "logos-generic-chat" +version = "0.1.0" dependencies = [ "chat-sqlite", "components", diff --git a/Cargo.toml b/Cargo.toml index c841526a..598388bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,10 @@ members = [ "core/shared-traits", "core/sqlite", "core/storage", - "crates/client", + "crates/logos-chat", + "crates/generic-chat", "extensions/components", + "extensions/embedded-logos-delivery", ] default-members = [ @@ -25,7 +27,7 @@ default-members = [ "core/shared-traits", "core/sqlite", "core/storage", - "crates/client", + "crates/generic-chat", ] [workspace.dependencies] @@ -34,8 +36,10 @@ logos-account = { path = "core/account" } chat-sqlite = { path = "core/sqlite" } components = { path = "extensions/components" } crypto = { path = "core/crypto" } +embedded-logos-delivery = { path = "extensions/embedded-logos-delivery" } libchat = { path = "core/conversations" } -logos-chat = { path = "crates/client" } +logos-chat = { path = "crates/logos-chat" } +logos-generic-chat = { path = "crates/generic-chat" } shared-traits = { path = "core/shared-traits" } storage = { path = "core/storage" } diff --git a/bin/chat-cli/Cargo.toml b/bin/chat-cli/Cargo.toml index bafef72e..71da08b6 100644 --- a/bin/chat-cli/Cargo.toml +++ b/bin/chat-cli/Cargo.toml @@ -11,7 +11,7 @@ path = "src/main.rs" # Workspace dependencies (sorted) crossbeam-channel = { workspace = true } logos-account = { workspace = true, features = ["dev"] } -logos-chat = { workspace = true, features = ["embedded-p2p-delivery"] } +logos-chat = { workspace = true } # External dependencies (sorted) anyhow = "1.0" diff --git a/bin/chat-cli/src/main.rs b/bin/chat-cli/src/main.rs index 3e8b43bc..cd069f96 100644 --- a/bin/chat-cli/src/main.rs +++ b/bin/chat-cli/src/main.rs @@ -8,11 +8,9 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; use clap::{Parser, ValueEnum}; use crossbeam_channel::Receiver; -use logos_account::TestLogosAccount; use logos_chat::{ - AccountDirectory, ChatClient, ChatClientBuilder, ChatStore, DelegateSigner, Event, - HttpRegistry, LogosChatClient, LogosConfig, NETWORK_PRESET, REGISTRY_ENDPOINT, - RegistrationService, StorageConfig, Transport, + AccountDirectory, ChatClient, ChatStore, Event, LogosConfig, P2pConfig, RegistrationService, + Transport, }; use app::ChatApp; @@ -78,24 +76,27 @@ fn main() -> Result<()> { let db_str = db_path(&cli)?; match cli.transport { - // logos-delivery is the transport baked into `LogosChatClient`, so the - // Logos client opens it from config rather than receiving one. TransportKind::LogosDelivery => { - let preset = cli.preset.as_deref().unwrap_or(NETWORK_PRESET); - println!("Starting logos-delivery node (preset={preset})..."); - println!("This may take a few seconds while connecting to the network."); - - let mut config = LogosConfig::new(db_str, "chat-cli"); + let mut p2p_config = P2pConfig::default(); if let Some(port) = cli.port { - config.set_tcp_port(port); + p2p_config.tcp_port = port; } if let Some(preset) = cli.preset.as_deref() { - config.set_preset(preset); + p2p_config.preset = preset.to_string(); } + + println!( + "Starting logos-delivery node (preset={})...", + p2p_config.preset + ); + println!("This may take a few seconds while connecting to the network."); + + let mut config = LogosConfig::new(db_str, "chat-cli"); if let Some(registry_url) = cli.registry_url.as_deref() { config.set_registry_url(registry_url); } - let (client, events) = LogosChatClient::open(config) + config.set_p2p_config(p2p_config); + let (client, events) = logos_chat::open(config) .map_err(|e| anyhow::anyhow!("{e:?}")) .context("failed to open chat client")?; @@ -104,32 +105,17 @@ fn main() -> Result<()> { } // The file transport is a local-only path: it reuses the Logos service // stack (delegate identity, HTTP registry, encrypted storage) but swaps - // the transport, so it builds a client directly instead of going through - // `LogosChatClient`. + // the transport in via `open_with_transport`. TransportKind::File => { let transport_dir = cli.data.join("transport"); let transport = transport::file::FileTransport::new(&transport_dir) .context("failed to create file transport")?; - let endpoint = cli.registry_url.as_deref().unwrap_or(REGISTRY_ENDPOINT); - // A fresh dev account endorsing a fresh delegate each launch, - // mirroring `LogosChatClient::open`. - let account = TestLogosAccount::new(); - let delegate = DelegateSigner::random(); - let mut registry = HttpRegistry::new(endpoint); - account - .add_delegate_signer(&mut registry, delegate.public_key()) - .map_err(|e| anyhow::anyhow!("{e:?}")) - .context("failed to publish the device bundle")?; - let (client, events) = ChatClientBuilder::new(account.address()) - .ident(delegate) - .transport(transport) - .registration(registry) - .storage_config(StorageConfig::Encrypted { - path: db_str, - key: "chat-cli".to_string(), - }) - .build() + let mut config = LogosConfig::new(db_str, "chat-cli"); + if let Some(registry_url) = cli.registry_url.as_deref() { + config.set_registry_url(registry_url); + } + let (client, events) = logos_chat::open_with_transport(config, transport) .map_err(|e| anyhow::anyhow!("{e:?}")) .context("failed to open chat client")?; diff --git a/crates/client/src/config.rs b/crates/client/src/config.rs deleted file mode 100644 index 1918e8b3..00000000 --- a/crates/client/src/config.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Baked-in configuration for the Logos service stack. - -/// The endpoint for the account and keypackage registration service. -pub const REGISTRY_ENDPOINT: &str = "https://devnet.chat-kc.logos.co"; - -/// The logos-delivery network preset the Logos client joins by default. -pub const NETWORK_PRESET: &str = "logos.dev"; - -/// Default TCP port for the embedded logos-delivery node. -pub const DEFAULT_TCP_PORT: u16 = 60000; diff --git a/crates/client/src/logos.rs b/crates/client/src/logos.rs deleted file mode 100644 index 4634a7b4..00000000 --- a/crates/client/src/logos.rs +++ /dev/null @@ -1,123 +0,0 @@ -//! The opinionated Logos client. -//! -//! `LogosChatClient` commits to the Logos service stack so independently built -//! clients share the same production services instead of each re-deriving them: -//! a delegate identity, the HTTP keypackage + account registry, encrypted -//! on-disk storage, and — unlike the generic [`ChatClientBuilder`] — the -//! logos-delivery transport itself. logos-delivery *is* the transport for the -//! Logos client, so the caller no longer supplies one; only per-client secrets -//! (the database path and key) and the network config are passed in. -//! -//! The logos-delivery transport carries a native dependency, so this whole -//! module is gated behind the `embedded-p2p-delivery` cargo feature. The registry -//! endpoint lives in [`crate::config`] instead, so it stays available to other -//! transports when the feature is off. - -use components::{EmbeddedP2pDeliveryService, HttpRegistry, P2pConfig}; -use crossbeam_channel::Receiver; -use libchat::{ChatStorage, StorageConfig}; -use logos_account::TestLogosAccount; - -use crate::ChatClientBuilder; -use crate::client::{ChatClient, Transport}; -use crate::config::{DEFAULT_TCP_PORT, NETWORK_PRESET, REGISTRY_ENDPOINT}; -use crate::delegate::DelegateSigner; -use crate::errors::ClientError; -use crate::event::Event; - -// logos-delivery already implements `DeliveryService`; teaching it the inbound -// half here (in the crate that owns `Transport`) makes it a full transport, so -// callers need no wrapper newtype. -impl Transport for EmbeddedP2pDeliveryService { - fn inbound(&mut self) -> Receiver> { - self.inbound_queue() - } -} - -/// Configuration for opening a [`LogosChatClient`]. -/// -/// `db_path` (a per-client location) and `db_key` (a secret) are required and -/// never baked into the library. The TCP port and the network config default to -/// the baked-in Logos values; override them with the setters (e.g. to point at a -/// local deployment). -pub struct LogosConfig { - db_path: String, - db_key: String, - tcp_port: u16, - preset: String, - registry_url: String, -} - -impl LogosConfig { - /// Config for the required per-client `db_path` and `db_key`. The TCP port, - /// network preset, and registry endpoint default to the baked-in Logos - /// values; override them with [`set_tcp_port`](Self::set_tcp_port), - /// [`set_preset`](Self::set_preset), and - /// [`set_registry_url`](Self::set_registry_url). - pub fn new(db_path: impl Into, db_key: impl Into) -> Self { - Self { - db_path: db_path.into(), - db_key: db_key.into(), - tcp_port: DEFAULT_TCP_PORT, - preset: NETWORK_PRESET.to_string(), - registry_url: REGISTRY_ENDPOINT.to_string(), - } - } - - /// Override the TCP port for the embedded logos-delivery node. - pub fn set_tcp_port(&mut self, tcp_port: u16) { - self.tcp_port = tcp_port; - } - - /// Override the logos-delivery network preset (defaults to the baked-in - /// [`NETWORK_PRESET`]). - pub fn set_preset(&mut self, preset: impl Into) { - self.preset = preset.into(); - } - - /// Override the registry endpoint (account + keypackage store; defaults to - /// the baked-in [`REGISTRY_ENDPOINT`]). - pub fn set_registry_url(&mut self, registry_url: impl Into) { - self.registry_url = registry_url.into(); - } -} - -/// A [`ChatClient`] wired to the Logos service stack: a [`DelegateSigner`] -/// identity acting for a fresh dev account, the HTTP keypackage + account -/// registry ([`HttpRegistry`], which is both the keypackage store and the -/// account → device directory), encrypted [`ChatStorage`], and the -/// logos-delivery transport. -pub type LogosChatClient = ChatClient; - -impl LogosChatClient { - /// Open a client on the Logos stack per `config`, starting a logos-delivery - /// node as its transport and persisting to the encrypted database. - pub fn open(config: LogosConfig) -> Result<(Self, Receiver), ClientError> { - let transport = EmbeddedP2pDeliveryService::start(P2pConfig { - preset: config.preset, - tcp_port: config.tcp_port, - ..Default::default() - }) - .map_err(|e| ClientError::Transport(e.to_string()))?; - - // A fresh account endorsing a fresh delegate each open: the account - // key is dropped after publishing the bundle, so devices cannot be - // added later. A caller-supplied, custody-holding account replaces - // this once the platform provides one. - let account = TestLogosAccount::new(); - let delegate = DelegateSigner::random(); - let mut registry = HttpRegistry::new(config.registry_url); - account - .add_delegate_signer(&mut registry, delegate.public_key()) - .map_err(|e| ClientError::BundlePublish(e.to_string()))?; - ChatClientBuilder::new(account.address()) - .ident(delegate) - .transport(transport) - .registration(registry) - .storage_config(StorageConfig::Encrypted { - path: config.db_path, - key: config.db_key, - }) - .build() - } -} diff --git a/crates/client/Cargo.toml b/crates/generic-chat/Cargo.toml similarity index 86% rename from crates/client/Cargo.toml rename to crates/generic-chat/Cargo.toml index 92b6a61a..beb802dc 100644 --- a/crates/client/Cargo.toml +++ b/crates/generic-chat/Cargo.toml @@ -1,14 +1,11 @@ [package] -name = "logos-chat" +name = "logos-generic-chat" version = "0.1.0" edition = "2024" [lib] crate-type = ["rlib"] -[features] -embedded-p2p-delivery = ["components/embedded_p2p_delivery"] - [dependencies] # Workspace dependencies (sorted) chat-sqlite = { workspace = true } diff --git a/crates/client/examples/message-exchange/README.md b/crates/generic-chat/examples/message-exchange/README.md similarity index 69% rename from crates/client/examples/message-exchange/README.md rename to crates/generic-chat/examples/message-exchange/README.md index cbb17889..203f7ddb 100644 --- a/crates/client/examples/message-exchange/README.md +++ b/crates/generic-chat/examples/message-exchange/README.md @@ -1,9 +1,9 @@ # message-exchange -An example Rust application built on top of [`crates/client`](../../). +An example Rust application built on top of [`crates/generic-chat`](../../). It demonstrates that creating a working chat client in pure Rust is trivial: depend on -`crates/client`, pick a `DeliveryService` implementation (here the in-memory +`crates/generic-chat`, pick a `DeliveryService` implementation (here the in-memory `InProcessDelivery` shipped with the crate), and wire up `ChatClient`. No boilerplate, no FFI. ## Running diff --git a/crates/client/examples/message-exchange/main.rs b/crates/generic-chat/examples/message-exchange/main.rs similarity index 95% rename from crates/client/examples/message-exchange/main.rs rename to crates/generic-chat/examples/message-exchange/main.rs index a51cc34e..025012f7 100644 --- a/crates/client/examples/message-exchange/main.rs +++ b/crates/generic-chat/examples/message-exchange/main.rs @@ -1,6 +1,6 @@ use components::EphemeralRegistry; use logos_account::TestLogosAccount; -use logos_chat::{ChatClientBuilder, Event, InProcessDelivery, MessageBus}; +use logos_generic_chat::{ChatClientBuilder, Event, InProcessDelivery, MessageBus}; use std::time::Duration; fn main() { diff --git a/crates/client/src/builder.rs b/crates/generic-chat/src/builder.rs similarity index 100% rename from crates/client/src/builder.rs rename to crates/generic-chat/src/builder.rs diff --git a/crates/client/src/client.rs b/crates/generic-chat/src/client.rs similarity index 100% rename from crates/client/src/client.rs rename to crates/generic-chat/src/client.rs diff --git a/crates/client/src/delegate.rs b/crates/generic-chat/src/delegate.rs similarity index 100% rename from crates/client/src/delegate.rs rename to crates/generic-chat/src/delegate.rs diff --git a/crates/client/src/delivery_in_process.rs b/crates/generic-chat/src/delivery_in_process.rs similarity index 100% rename from crates/client/src/delivery_in_process.rs rename to crates/generic-chat/src/delivery_in_process.rs diff --git a/crates/client/src/errors.rs b/crates/generic-chat/src/errors.rs similarity index 100% rename from crates/client/src/errors.rs rename to crates/generic-chat/src/errors.rs diff --git a/crates/client/src/event.rs b/crates/generic-chat/src/event.rs similarity index 100% rename from crates/client/src/event.rs rename to crates/generic-chat/src/event.rs diff --git a/crates/client/src/lib.rs b/crates/generic-chat/src/lib.rs similarity index 80% rename from crates/client/src/lib.rs rename to crates/generic-chat/src/lib.rs index b613995b..39ea635a 100644 --- a/crates/client/src/lib.rs +++ b/crates/generic-chat/src/lib.rs @@ -1,22 +1,16 @@ mod builder; mod client; -mod config; mod delegate; mod delivery_in_process; mod errors; mod event; -#[cfg(feature = "embedded-p2p-delivery")] -mod logos; pub use builder::{ChatClientBuilder, Unset}; pub use client::{ChatClient, Transport}; -pub use config::{DEFAULT_TCP_PORT, NETWORK_PRESET, REGISTRY_ENDPOINT}; pub use delegate::DelegateSigner; pub use delivery_in_process::{InProcessDelivery, MessageBus}; pub use errors::ClientError; pub use event::{Event, MessageSender}; -#[cfg(feature = "embedded-p2p-delivery")] -pub use logos::{LogosChatClient, LogosConfig}; // Re-export types callers need to interact with ChatClient. pub use libchat::{ diff --git a/crates/client/tests/saro_and_raya.rs b/crates/generic-chat/tests/saro_and_raya.rs similarity index 98% rename from crates/client/tests/saro_and_raya.rs rename to crates/generic-chat/tests/saro_and_raya.rs index ccc0916f..3ad15739 100644 --- a/crates/client/tests/saro_and_raya.rs +++ b/crates/generic-chat/tests/saro_and_raya.rs @@ -4,7 +4,7 @@ use components::EphemeralRegistry; use crossbeam_channel::{Receiver, Sender}; use crypto::Ed25519VerifyingKey; use logos_account::TestLogosAccount; -use logos_chat::{ +use logos_generic_chat::{ AddressedEnvelope, ChatClient, ChatClientBuilder, DelegateSigner, DeliveryService, Event, InProcessDelivery, MessageBus, Transport, }; @@ -30,7 +30,7 @@ fn create_test_client( ChatClient, Receiver, ), - logos_chat::ClientError, + logos_generic_chat::ClientError, > { let account = TestLogosAccount::new(); let delegate = DelegateSigner::random(); @@ -375,10 +375,16 @@ fn unpublished_account_address_is_an_error() { let err = saro .create_direct_conversation(&unpublished.address()) .expect_err("no bundle published for the account"); - assert!(matches!(err, logos_chat::ClientError::AccountResolution(_))); + assert!(matches!( + err, + logos_generic_chat::ClientError::AccountResolution(_) + )); let err = saro .create_direct_conversation("not-an-account-address") .expect_err("not an account key"); - assert!(matches!(err, logos_chat::ClientError::AccountResolution(_))); + assert!(matches!( + err, + logos_generic_chat::ClientError::AccountResolution(_) + )); } diff --git a/crates/logos-chat/Cargo.toml b/crates/logos-chat/Cargo.toml new file mode 100644 index 00000000..63a8af6b --- /dev/null +++ b/crates/logos-chat/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "logos-chat" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["rlib"] + +[dependencies] +# Workspace dependencies (sorted) +components = { workspace = true } +crossbeam-channel = { workspace = true } +embedded-logos-delivery = { workspace = true } +libchat = { workspace = true } +logos-account = { workspace = true, features = ["dev"] } +logos-generic-chat = { workspace = true } diff --git a/crates/logos-chat/src/lib.rs b/crates/logos-chat/src/lib.rs new file mode 100644 index 00000000..b9381e78 --- /dev/null +++ b/crates/logos-chat/src/lib.rs @@ -0,0 +1,12 @@ +mod logos; + +pub use logos::{LogosChatClient, LogosConfig, REGISTRY_ENDPOINT, open, open_with_transport}; +// Facade re-exports so callers need no direct dependency on the transport +// crate. +pub use embedded_logos_delivery::{ + DEFAULT_NETWORK_PRESET, DEFAULT_TCP_PORT, EmbeddedLogosDelivery, P2pConfig, +}; + +// Re-export the transport-generic client surface so callers depend on this +// crate alone. +pub use logos_generic_chat::*; diff --git a/crates/logos-chat/src/logos.rs b/crates/logos-chat/src/logos.rs new file mode 100644 index 00000000..b3e9d6f7 --- /dev/null +++ b/crates/logos-chat/src/logos.rs @@ -0,0 +1,117 @@ +//! The opinionated Logos client. +//! +//! [`open`] commits to the Logos service stack so independently built clients +//! share the same production services instead of each re-deriving them: a +//! delegate identity, the HTTP keypackage + account registry, and encrypted +//! on-disk storage. The stack is generic over the transport — any +//! [`Transport`] can be injected via [`open_with_transport`] — and the +//! concrete [`LogosChatClient`] commits to the embedded logos-delivery node, +//! whose native `liblogosdelivery` link keeps this crate outside the +//! workspace's default members. +//! +//! [`LogosChatClient`] points at `ChatClient`, which lives in +//! `logos-generic-chat`, so Rust's inherent-impl rule keeps the open +//! constructors off the alias; they are crate-level functions ([`open`], +//! [`open_with_transport`]) taking the all-inclusive [`LogosConfig`] instead. + +use components::HttpRegistry; +use crossbeam_channel::Receiver; +use embedded_logos_delivery::{EmbeddedLogosDelivery, P2pConfig}; +use libchat::{ChatStorage, StorageConfig}; +use logos_account::TestLogosAccount; + +use logos_generic_chat::{ + ChatClient, ChatClientBuilder, ClientError, DelegateSigner, Event, Transport, +}; + +/// The endpoint for the account and keypackage registration service. +pub const REGISTRY_ENDPOINT: &str = "https://devnet.chat-kc.logos.co"; + +/// Configuration for opening a Logos client. +/// +/// `db_path` (a per-client location) and `db_key` (a secret) are required and +/// never baked into the library. Everything else defaults: the registry +/// endpoint to the baked-in Logos value and the embedded node's p2p settings +/// to [`P2pConfig::default`]; override them with +/// [`set_registry_url`](Self::set_registry_url) and +/// [`set_p2p_config`](Self::set_p2p_config). +pub struct LogosConfig { + db_path: String, + db_key: String, + registry_url: String, + p2p_config: P2pConfig, +} + +impl LogosConfig { + /// Config for the required per-client `db_path` and `db_key`. The registry + /// endpoint defaults to the baked-in Logos value; override it with + /// [`set_registry_url`](Self::set_registry_url). + pub fn new(db_path: impl Into, db_key: impl Into) -> Self { + Self { + db_path: db_path.into(), + db_key: db_key.into(), + registry_url: REGISTRY_ENDPOINT.to_string(), + p2p_config: P2pConfig::default(), + } + } + + /// Override the registry endpoint (account + keypackage store; defaults to + /// the baked-in [`REGISTRY_ENDPOINT`]). + pub fn set_registry_url(&mut self, registry_url: impl Into) { + self.registry_url = registry_url.into(); + } + + /// Override the embedded node's p2p settings (defaults to + /// [`P2pConfig::default`]). Only [`open`] starts an embedded node, so + /// [`open_with_transport`] ignores this. + pub fn set_p2p_config(&mut self, p2p_config: P2pConfig) { + self.p2p_config = p2p_config; + } +} + +/// Open a client on the Logos stack per `config`, starting an embedded +/// logos-delivery node per its p2p settings as the transport. A convenience +/// over [`open_with_transport`] that commits to the [`LogosChatClient`] +/// transport. +pub fn open(config: LogosConfig) -> Result<(LogosChatClient, Receiver), ClientError> { + let transport = EmbeddedLogosDelivery::start(config.p2p_config.clone()) + .map_err(|e| ClientError::Transport(e.to_string()))?; + open_with_transport(config, transport) +} + +/// Open a client on the Logos stack per `config` with the injected transport, +/// persisting to the encrypted database. +#[allow(clippy::type_complexity)] +pub fn open_with_transport( + config: LogosConfig, + transport: T, +) -> Result<(ChatClient, Receiver), ClientError> { + // A fresh account endorsing a fresh delegate each open: the account + // key is dropped after publishing the bundle, so devices cannot be + // added later. A caller-supplied, custody-holding account replaces + // this once the platform provides one. + let account = TestLogosAccount::new(); + let delegate = DelegateSigner::random(); + let mut registry = HttpRegistry::new(config.registry_url); + account + .add_delegate_signer(&mut registry, delegate.public_key()) + .map_err(|e| ClientError::BundlePublish(e.to_string()))?; + ChatClientBuilder::new(account.address()) + .ident(delegate) + .transport(transport) + .registration(registry) + .storage_config(StorageConfig::Encrypted { + path: config.db_path, + key: config.db_key, + }) + .build() +} + +/// The Logos client: a [`ChatClient`] wired to the Logos service stack — a +/// [`DelegateSigner`] identity acting for a fresh dev account, the HTTP +/// keypackage + account registry ([`HttpRegistry`], which is both the +/// keypackage store and the account → device directory), and encrypted +/// [`ChatStorage`] — running an embedded logos-delivery node as its +/// transport. Open one with [`open`], or swap the transport via +/// [`open_with_transport`]. +pub type LogosChatClient = ChatClient; diff --git a/extensions/components/Cargo.toml b/extensions/components/Cargo.toml index 71092ca4..7e1a79f2 100644 --- a/extensions/components/Cargo.toml +++ b/extensions/components/Cargo.toml @@ -2,10 +2,6 @@ name = "components" version = "0.1.0" edition = "2024" -links = "logosdelivery" - -[features] -embedded_p2p_delivery = [] [dependencies] # Workspace dependencies (sorted) diff --git a/extensions/components/src/delivery.rs b/extensions/components/src/delivery.rs index 667d49cb..627ac550 100644 --- a/extensions/components/src/delivery.rs +++ b/extensions/components/src/delivery.rs @@ -1,9 +1,3 @@ mod local_broadcaster; pub use local_broadcaster::LocalBroadcaster; - -#[cfg(feature = "embedded_p2p_delivery")] -pub mod embedded_p2p_delivery; - -#[cfg(feature = "embedded_p2p_delivery")] -pub use embedded_p2p_delivery::{EmbeddedP2pDeliveryService, P2pConfig}; diff --git a/extensions/embedded-logos-delivery/Cargo.toml b/extensions/embedded-logos-delivery/Cargo.toml new file mode 100644 index 00000000..466f5fac --- /dev/null +++ b/extensions/embedded-logos-delivery/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "embedded-logos-delivery" +version = "0.1.0" +edition = "2024" +links = "logosdelivery" + +[dependencies] +# Workspace dependencies (sorted) +crossbeam-channel = { workspace = true } +libchat = { workspace = true } +logos-generic-chat = { workspace = true } + +# External dependencies (sorted) +base64 = "0.22" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "2" +tracing = "0.1" diff --git a/extensions/components/build.rs b/extensions/embedded-logos-delivery/build.rs similarity index 92% rename from extensions/components/build.rs rename to extensions/embedded-logos-delivery/build.rs index 5696f2ba..2773451d 100644 --- a/extensions/components/build.rs +++ b/extensions/embedded-logos-delivery/build.rs @@ -5,17 +5,12 @@ use std::process::Command; fn main() { println!("cargo:rerun-if-env-changed=LOGOS_DELIVERY_LIB_DIR"); - if std::env::var_os("CARGO_FEATURE_EMBEDDED_P2P_DELIVERY").is_none() { - return; - } - let Some(lib_dir) = locate_lib_dir() else { println!( - "cargo:warning=embedded_p2p_delivery feature is enabled but \ - liblogosdelivery could not be located; `cargo check`/`clippy` will \ - pass, but building or testing will fail at link. Enter the dev shell \ - with `nix develop` or set LOGOS_DELIVERY_LIB_DIR to the directory \ - containing the library." + "cargo:warning=liblogosdelivery could not be located; `cargo check`/\ + `clippy` will pass, but building or testing will fail at link. Enter \ + the dev shell with `nix develop` or set LOGOS_DELIVERY_LIB_DIR to \ + the directory containing the library." ); return; }; diff --git a/extensions/components/src/delivery/embedded_p2p_delivery.rs b/extensions/embedded-logos-delivery/src/lib.rs similarity index 87% rename from extensions/components/src/delivery/embedded_p2p_delivery.rs rename to extensions/embedded-logos-delivery/src/lib.rs index e2b855de..56cb8a5d 100644 --- a/extensions/components/src/delivery/embedded_p2p_delivery.rs +++ b/extensions/embedded-logos-delivery/src/lib.rs @@ -1,8 +1,12 @@ -//! logos-delivery backed [`client::DeliveryService`] implementation. +//! The embedded logos-delivery transport service. //! -//! `LogosDeliveryService` wraps an embedded logos-delivery node running on a -//! dedicated `std::thread`. All interaction is via synchronous `std::sync::mpsc` -//! channels. +//! [`EmbeddedLogosDelivery`] implements [`DeliveryService`] by wrapping an +//! embedded logos-delivery node running on a dedicated `std::thread`. All +//! interaction is via synchronous `std::sync::mpsc` channels. +//! +//! This crate links the native `liblogosdelivery` library, so it lives outside +//! the workspace's default members; depend on it (e.g. via the `logos-chat` +//! crate) only when shipping the embedded node. //! //! ## Content topic mapping //! @@ -51,6 +55,12 @@ type SubscriberList = Arc>>>>; // ── P2pConfig ─────────────────────────────────────────────────────────────────── +/// The logos-delivery network preset joined by default. +pub const DEFAULT_NETWORK_PRESET: &str = "logos.dev"; + +/// Default TCP port for the embedded logos-delivery node. +pub const DEFAULT_TCP_PORT: u16 = 60000; + #[derive(Debug, Clone)] pub struct P2pConfig { pub preset: String, @@ -61,8 +71,8 @@ pub struct P2pConfig { impl Default for P2pConfig { fn default() -> Self { Self { - preset: "logos.dev".into(), - tcp_port: 60000, + preset: DEFAULT_NETWORK_PRESET.into(), + tcp_port: DEFAULT_TCP_PORT, log_level: "ERROR".into(), } } @@ -115,21 +125,21 @@ impl WakuPayload { } } -// ── EmbeddedP2pDeliveryService ────────────────────────────────────────────────── +// ── EmbeddedLogosDelivery ────────────────────────────────────────────────── /// logos-delivery backed delivery service. Cheap to clone — all clones share /// the same background node. #[derive(Clone, Debug)] -pub struct EmbeddedP2pDeliveryService { +pub struct EmbeddedLogosDelivery { outbound: mpsc::SyncSender, #[allow(dead_code)] subscribers: SubscriberList, inbound_rx: Option>>, } -impl EmbeddedP2pDeliveryService { +impl EmbeddedLogosDelivery { /// Start the embedded logos-delivery node. The client drains inbound - /// payloads via [`Transport::inbound`]. + /// payloads via [`Self::inbound_queue`]. pub fn start(cfg: P2pConfig) -> Result { let (out_tx, out_rx) = mpsc::sync_channel::(256); let subscribers: SubscriberList = Arc::new(Mutex::new(Vec::new())); @@ -284,7 +294,7 @@ impl EmbeddedP2pDeliveryService { } } -impl DeliveryService for EmbeddedP2pDeliveryService { +impl DeliveryService for EmbeddedLogosDelivery { type Error = DeliveryError; fn publish(&mut self, envelope: AddressedEnvelope) -> Result<(), DeliveryError> { @@ -312,3 +322,13 @@ impl DeliveryService for EmbeddedP2pDeliveryService { Ok(()) } } + +// Teaching the service the inbound half makes it a full client transport, so +// callers need no wrapper newtype. The impl lives here (the crate owning the +// type) because the orphan rule bars it from the `logos-chat` crate, which +// owns neither the trait nor the type. +impl logos_generic_chat::Transport for EmbeddedLogosDelivery { + fn inbound(&mut self) -> Receiver> { + self.inbound_queue() + } +} diff --git a/extensions/components/src/delivery/embedded_p2p_delivery/sys.rs b/extensions/embedded-logos-delivery/src/sys.rs similarity index 100% rename from extensions/components/src/delivery/embedded_p2p_delivery/sys.rs rename to extensions/embedded-logos-delivery/src/sys.rs diff --git a/extensions/components/src/delivery/embedded_p2p_delivery/wrapper.rs b/extensions/embedded-logos-delivery/src/wrapper.rs similarity index 100% rename from extensions/components/src/delivery/embedded_p2p_delivery/wrapper.rs rename to extensions/embedded-logos-delivery/src/wrapper.rs