Skip to main content

hashiverse_client_rust/
lib.rs

1//! # `hashiverse-client-rust` — friendly Rust client wrapper
2//!
3//! Hashiverse is your open-source decentralized X/Twitter replacement. This crate
4//! is the ergonomic Rust entry point: it picks sensible defaults for every
5//! pluggable trait in [`hashiverse_lib`] (sqlite client storage, on-disk key
6//! locker, DNSSEC-validated public bootstrap, native parallel PoW, real-time
7//! clock, HTTPS transport) so you can spin up a working client in a few lines.
8//!
9//! ```no_run
10//! # async fn demo() -> anyhow::Result<()> {
11//! let hashiverse = hashiverse_client_rust::HashiverseBuilder::new()
12//!     .build_with_keyphrase("correct horse battery staple")
13//!     .await?;
14//!
15//! hashiverse.submit_post("<p>hello, hashiverse</p>").await?;
16//! # Ok(()) }
17//! ```
18//!
19//! If you need to override a default, use the builder methods
20//! ([`HashiverseBuilder::data_dir`], [`HashiverseBuilder::passphrase`],
21//! [`HashiverseBuilder::bootstrap`]). If you need full control, build a
22//! [`hashiverse_lib::client::hashiverse_client::HashiverseClient`] directly
23//! against [`hashiverse_lib`] — this crate is opinionated by design and
24//! deliberately does not expose every knob.
25
26use std::path::PathBuf;
27use std::sync::Arc;
28
29use anyhow::Result;
30
31use hashiverse_lib::client::args::Args;
32use hashiverse_lib::client::client_storage::sqlite_client_storage::SqliteClientStorage;
33use hashiverse_lib::client::hashiverse_client::HashiverseClient;
34use hashiverse_lib::client::key_locker::disk_key_locker::{DiskKeyLocker, DiskKeyLockerManager};
35use hashiverse_lib::client::key_locker::key_locker::KeyLockerManager;
36use hashiverse_lib::tools::pow_generator::native_parallel_pow_generator::NativeParallelPowGenerator;
37use hashiverse_lib::tools::runtime_services::RuntimeServices;
38use hashiverse_lib::tools::time_provider::time_provider::RealTimeProvider;
39use hashiverse_lib::transport::bootstrap_provider::bootstrap_provider::BootstrapProvider;
40use hashiverse_lib::transport::bootstrap_provider::dnssec_bootstrap_provider::DnssecBootstrapProvider;
41use hashiverse_lib::transport::bootstrap_provider::manual_bootstrap_provider::ManualBootstrapProvider;
42use hashiverse_lib::transport::partial_https_transport::PartialHttpsTransportFactory;
43
44pub use hashiverse_lib;
45
46/// Builder for a [`Hashiverse`] client with sensible defaults for every
47/// pluggable trait. See the crate-level docs for an example.
48pub struct HashiverseBuilder {
49    data_dir: PathBuf,
50    passphrase: String,
51    bootstrap_addresses: Option<Vec<String>>,
52}
53
54impl Default for HashiverseBuilder {
55    fn default() -> Self {
56        Self {
57            data_dir: default_data_dir(),
58            passphrase: String::new(),
59            bootstrap_addresses: None,
60        }
61    }
62}
63
64impl HashiverseBuilder {
65    pub fn new() -> Self {
66        Self::default()
67    }
68
69    /// Directory for the sqlite client storage and the on-disk key locker.
70    /// Default: the platform data-dir (`dirs_next::data_dir()`) joined with
71    /// `"hashiverse"`. Tildes in the supplied path are expanded.
72    pub fn data_dir(mut self, dir: impl Into<PathBuf>) -> Self {
73        let dir = dir.into();
74        let expanded = shellexpand::tilde(&dir.to_string_lossy()).into_owned();
75        self.data_dir = PathBuf::from(expanded);
76        self
77    }
78
79    /// Passphrase used to encrypt the on-disk key locker. Empty by default.
80    pub fn passphrase(mut self, passphrase: impl Into<String>) -> Self {
81        self.passphrase = passphrase.into();
82        self
83    }
84
85    /// Override the bootstrap peer list with a hand-curated set of
86    /// `host:port` strings. Default: [`DnssecBootstrapProvider`], which
87    /// resolves the public seed list via DoH with DNSSEC validation.
88    pub fn bootstrap(mut self, addresses: Vec<String>) -> Self {
89        self.bootstrap_addresses = Some(addresses);
90        self
91    }
92
93    /// Build a client by deriving the identity from `key_phrase`. The derived
94    /// key is stored in the on-disk locker so a subsequent run can pick it up
95    /// via [`HashiverseBuilder::build_from_stored_key`].
96    pub async fn build_with_keyphrase(self, key_phrase: impl Into<String>) -> Result<Hashiverse> {
97        std::fs::create_dir_all(&self.data_dir)?;
98        let key_locker_manager = DiskKeyLockerManager::with_data_dir(self.data_dir.clone(), self.passphrase.clone())?;
99        let key_locker: Arc<DiskKeyLocker> = key_locker_manager.create(key_phrase.into()).await?;
100        self.assemble(key_locker_manager, key_locker).await
101    }
102
103    /// Build a client by loading an identity previously stored in the on-disk
104    /// key locker. `client_id_hex` selects which stored key to use.
105    pub async fn build_from_stored_key(self, client_id_hex: impl Into<String>) -> Result<Hashiverse> {
106        std::fs::create_dir_all(&self.data_dir)?;
107        let key_locker_manager = DiskKeyLockerManager::with_data_dir(self.data_dir.clone(), self.passphrase.clone())?;
108        let key_locker: Arc<DiskKeyLocker> = key_locker_manager.switch(client_id_hex.into()).await?;
109        self.assemble(key_locker_manager, key_locker).await
110    }
111
112    async fn assemble(
113        self,
114        key_locker_manager: Arc<DiskKeyLockerManager>,
115        key_locker: Arc<DiskKeyLocker>,
116    ) -> Result<Hashiverse> {
117        let client_storage_dir = self.data_dir.join("client_storage");
118        std::fs::create_dir_all(&client_storage_dir)?;
119
120        let time_provider = Arc::new(RealTimeProvider);
121        let bootstrap_provider: Arc<dyn BootstrapProvider> = match self.bootstrap_addresses {
122            Some(addresses) => ManualBootstrapProvider::new(addresses),
123            None => Arc::new(DnssecBootstrapProvider::new()),
124        };
125        let transport_factory = Arc::new(PartialHttpsTransportFactory::new(bootstrap_provider));
126        let pow_generator = Arc::new(NativeParallelPowGenerator::new());
127
128        let runtime_services = Arc::new(RuntimeServices {
129            time_provider,
130            transport_factory,
131            pow_generator,
132        });
133
134        let client_storage = SqliteClientStorage::new(client_storage_dir).await?;
135        let hashiverse_client = HashiverseClient::new(
136            runtime_services,
137            client_storage,
138            key_locker,
139            Args::default(),
140        )
141        .await?;
142
143        Ok(Hashiverse {
144            client: Arc::new(hashiverse_client),
145            _key_locker_manager: key_locker_manager,
146        })
147    }
148}
149
150/// A configured Hashiverse client. Deref into a
151/// [`hashiverse_lib::client::hashiverse_client::HashiverseClient`] for the full API.
152pub struct Hashiverse {
153    client: Arc<HashiverseClient>,
154    _key_locker_manager: Arc<DiskKeyLockerManager>,
155}
156
157impl Hashiverse {
158    /// Direct access to the underlying `Arc<HashiverseClient>`. Use this when
159    /// you need to share the client between tasks; for one-shot calls you can
160    /// rely on the `Deref` impl and call methods on `&Hashiverse` directly.
161    pub fn client(&self) -> &Arc<HashiverseClient> {
162        &self.client
163    }
164}
165
166impl std::ops::Deref for Hashiverse {
167    type Target = HashiverseClient;
168    fn deref(&self) -> &HashiverseClient {
169        &self.client
170    }
171}
172
173fn default_data_dir() -> PathBuf {
174    dirs_next::data_dir().unwrap_or_else(std::env::temp_dir).join("hashiverse")
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn builder_defaults_resolve_to_a_data_dir() {
183        let builder = HashiverseBuilder::new();
184        assert!(builder.data_dir.ends_with("hashiverse"), "default data dir should end in 'hashiverse': {:?}", builder.data_dir);
185        assert!(builder.passphrase.is_empty());
186        assert!(builder.bootstrap_addresses.is_none());
187    }
188
189    #[test]
190    fn data_dir_tilde_expansion() {
191        let builder = HashiverseBuilder::new().data_dir("~/hashiverse-test");
192        let expanded = builder.data_dir.to_string_lossy();
193        assert!(!expanded.starts_with("~"), "tilde should be expanded: {}", expanded);
194    }
195}