use crate::guardian::error::{GuardianError, Result};
use crate::p2p::network::{config::ClientConfig, types::*};
use iroh::{NodeId, SecretKey};
use rand_core::OsRng;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, info, warn};
#[derive(Clone)]
pub struct IrohClient {
backend: Arc<crate::p2p::network::core::IrohBackend>,
config: ClientConfig,
node_id: NodeId,
secret_key: SecretKey,
docs_client: Arc<RwLock<Option<crate::p2p::network::core::docs::WillowDocs>>>,
blobs_client: Arc<RwLock<Option<crate::p2p::network::core::blobs::BlobStore>>>,
}
impl IrohClient {
pub async fn new(config: ClientConfig) -> Result<Self> {
config
.validate()
.map_err(|e| GuardianError::Other(format!("Invalid configuration: {}", e)))?;
info!("Inicializando cliente Guardian DB");
let backend = Arc::new(crate::p2p::network::core::IrohBackend::new(&config).await?);
let node_info = backend.id().await?;
let node_id = node_info.id;
let secret_key = backend.secret_key().clone();
info!("NodeId: {}", node_id);
let client = Self {
backend,
config: config.clone(),
node_id,
secret_key,
docs_client: Arc::new(RwLock::new(None)),
blobs_client: Arc::new(RwLock::new(None)),
};
if config.data_store_path.is_some() {
match client.init_blobs().await {
Ok(_) => info!("✓ iroh-blobs inicializado com store compartilhado"),
Err(e) => {
warn!("Aviso: iroh-blobs não inicializado: {}", e);
debug!(" Use init_blobs() manualmente se precisar");
}
}
}
info!("✓ Cliente Guardian DB inicializado");
Ok(client)
}
pub async fn default() -> Result<Self> {
Self::new(ClientConfig::default()).await
}
pub async fn development() -> Result<Self> {
Self::new(ClientConfig::development()).await
}
pub async fn production() -> Result<Self> {
Self::new(ClientConfig::production()).await
}
pub async fn testing() -> Result<Self> {
Self::new(ClientConfig::testing()).await
}
pub async fn new_with_backend(
backend: Arc<crate::p2p::network::core::IrohBackend>,
) -> Result<Self> {
let config = ClientConfig::default();
let secret_key = SecretKey::generate(OsRng);
let node_id = secret_key.public();
Ok(Self {
backend,
config,
node_id,
secret_key,
docs_client: Arc::new(RwLock::new(None)),
blobs_client: Arc::new(RwLock::new(None)),
})
}
pub fn backend(&self) -> &Arc<crate::p2p::network::core::IrohBackend> {
&self.backend
}
pub async fn is_online(&self) -> bool {
self.backend.is_online().await
}
pub async fn add_bytes(&self, data: Vec<u8>) -> Result<AddResponse> {
struct BytesReader {
data: Vec<u8>,
pos: usize,
}
impl tokio::io::AsyncRead for BytesReader {
fn poll_read(
mut self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
let remaining = self.data.len() - self.pos;
let to_read = std::cmp::min(remaining, buf.remaining());
if to_read == 0 {
return std::task::Poll::Ready(Ok(()));
}
buf.put_slice(&self.data[self.pos..self.pos + to_read]);
self.pos += to_read;
std::task::Poll::Ready(Ok(()))
}
}
let reader = BytesReader { data, pos: 0 };
let pinned_data = Pin::new(Box::new(reader));
self.backend.add(pinned_data).await
}
pub async fn cat_bytes(&self, hash: &str) -> Result<Vec<u8>> {
let mut reader = self.backend.cat(hash).await?;
let mut data = Vec::new();
tokio::io::AsyncReadExt::read_to_end(&mut reader, &mut data).await?;
Ok(data)
}
pub fn get_channel_id(&self, other_peer: &NodeId) -> String {
let mut channel_id_peers = [self.node_id.to_string(), other_peer.to_string()];
channel_id_peers.sort();
format!(
"/iroh-pubsub-direct-channel/v1/{}",
channel_id_peers.join("/")
)
}
pub fn config(&self) -> &ClientConfig {
&self.config
}
pub fn node_id(&self) -> NodeId {
self.node_id
}
pub fn secret_key(&self) -> &SecretKey {
&self.secret_key
}
pub async fn id(&self) -> Result<NodeInfo> {
self.backend.id().await
}
pub async fn add_node_addr(&self, node_addr: iroh::NodeAddr) -> Result<()> {
let endpoint_arc = self.backend.get_endpoint().await?;
let endpoint_lock = endpoint_arc.read().await;
let endpoint = endpoint_lock
.as_ref()
.ok_or_else(|| GuardianError::Other("Endpoint não disponível".to_string()))?;
endpoint
.add_node_addr(node_addr)
.map_err(|e| GuardianError::Other(format!("Failed to add node addr: {}", e)))?;
Ok(())
}
pub async fn connect_gossip(&self, node_id: NodeId) -> Result<()> {
let endpoint_arc = self.backend.get_endpoint().await?;
let endpoint_lock = endpoint_arc.read().await;
let endpoint = endpoint_lock
.as_ref()
.ok_or_else(|| GuardianError::Other("Endpoint não disponível".to_string()))?;
let gossip_arc = self.backend.get_gossip().await?;
let gossip_lock = gossip_arc.read().await;
let gossip = gossip_lock
.as_ref()
.ok_or_else(|| GuardianError::Other("Gossip não inicializado".to_string()))?
.clone();
drop(gossip_lock);
let gossip_alpn = b"/iroh-gossip/1";
let connection = endpoint
.connect(node_id, gossip_alpn)
.await
.map_err(|e| GuardianError::Other(format!("Failed to connect via gossip: {}", e)))?;
gossip.handle_connection(connection).await.map_err(|e| {
GuardianError::Other(format!("Failed to register connection with gossip: {}", e))
})?;
Ok(())
}
pub async fn init_docs(&self) -> Result<()> {
let backend = self.backend.clone();
let mut client = crate::p2p::network::core::docs::WillowDocs::new(backend).await?;
client.init_default_author().await?;
let mut docs_guard = self.docs_client.write().await;
*docs_guard = Some(client);
info!("iroh-docs client inicializado com sucesso");
Ok(())
}
pub async fn docs_client(&self) -> Option<crate::p2p::network::core::docs::WillowDocs> {
let guard = self.docs_client.read().await;
(*guard).clone()
}
pub async fn has_docs_client(&self) -> bool {
let guard = self.docs_client.read().await;
guard.is_some()
}
pub async fn init_blobs(&self) -> Result<()> {
let store = self.backend.get_store_for_blobs().await?;
let client = crate::p2p::network::core::blobs::BlobStore::new(store);
let mut blobs_guard = self.blobs_client.write().await;
*blobs_guard = Some(client);
info!("iroh-blobs client inicializado com store compartilhado");
Ok(())
}
pub async fn blobs_client(&self) -> Option<crate::p2p::network::core::blobs::BlobStore> {
let guard = self.blobs_client.read().await;
(*guard).clone()
}
pub async fn has_blobs_client(&self) -> bool {
let guard = self.blobs_client.read().await;
guard.is_some()
}
pub async fn create_document_store(
&self,
identity: Arc<crate::log::identity::Identity>,
addr: Arc<dyn crate::address::Address>,
options: crate::traits::NewStoreOptions,
) -> Result<crate::stores::document_store::GuardianDBDocumentStore> {
crate::stores::document_store::GuardianDBDocumentStore::new(
Arc::new(self.clone()),
identity,
addr,
options,
)
.await
}
pub async fn shutdown(&self) -> Result<()> {
info!("Encerrando cliente Guardian DB");
info!("Cliente Guardian DB encerrado com sucesso");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_client_creation() {
let mut config = ClientConfig::development();
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
config.data_store_path = Some(format!("./tmp/test_creation_{}", timestamp).into());
let client = IrohClient::new(config).await;
assert!(client.is_ok());
}
#[tokio::test]
async fn test_client_online() {
let mut config = ClientConfig::development();
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
config.data_store_path = Some(format!("./tmp/test_online_{}", timestamp).into());
let client = IrohClient::new(config).await.unwrap();
assert!(client.is_online().await);
}
#[tokio::test]
async fn test_blobs_client_initialization() {
let mut config = ClientConfig::development();
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let data_path = format!("./tmp/test_blobs_{}", timestamp);
config.data_store_path = Some(data_path.into());
let client = IrohClient::new(config).await.unwrap();
assert!(
client.has_blobs_client().await,
"blobs_client deve ser inicializado automaticamente"
);
let blobs = client.blobs_client().await;
assert!(blobs.is_some(), "blobs_client() deve retornar Some");
}
#[tokio::test]
async fn test_blobs_client_manual_init() {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let mut config = ClientConfig::development();
let base_path = format!("./tmp/test_manual_blobs_{}", timestamp);
config.data_store_path = Some(base_path.clone().into());
let client = IrohClient::new(config).await.unwrap();
assert!(
client.has_blobs_client().await,
"blobs_client deve ser inicializado automaticamente quando data_store_path está presente"
);
let result = client.init_blobs().await;
assert!(result.is_ok(), "Re-inicialização deve ser permitida");
}
#[tokio::test]
async fn test_add_bytes_helper() {
let mut config = ClientConfig::development();
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
config.data_store_path = Some(format!("./tmp/test_add_bytes_{}", timestamp).into());
let client = IrohClient::new(config).await.unwrap();
let test_data = b"Hello, Guardian!".to_vec();
let response = client.add_bytes(test_data.clone()).await.unwrap();
assert!(!response.hash.is_empty());
assert_eq!(response.size_bytes().unwrap(), test_data.len());
let retrieved = client.cat_bytes(&response.hash).await.unwrap();
assert_eq!(retrieved, test_data);
}
#[tokio::test]
async fn test_backend_access() {
let mut config = ClientConfig::development();
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
config.data_store_path = Some(format!("./tmp/test_backend_{}", timestamp).into());
let client = IrohClient::new(config).await.unwrap();
let backend = client.backend();
let report = backend.generate_performance_report().await;
assert!(!report.is_empty());
}
#[tokio::test]
async fn test_node_info() {
let test_dir = format!("./tmp/iroh_test_info_{}", std::process::id());
let mut config = ClientConfig::development();
config.data_store_path = Some(test_dir.into());
let client = IrohClient::new(config).await.unwrap();
let info = client.id().await.unwrap();
assert_eq!(info.id, client.node_id());
}
#[tokio::test]
async fn test_get_channel_id() {
let client = IrohClient::development().await.unwrap();
let other_peer = SecretKey::generate(OsRng).public();
let channel_id = client.get_channel_id(&other_peer);
assert!(channel_id.starts_with("/iroh-pubsub-direct-channel/v1/"));
let channel_id2 = client.get_channel_id(&other_peer);
assert_eq!(channel_id, channel_id2);
}
#[tokio::test]
async fn test_error_handling() {
let mut config = ClientConfig::development();
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
config.data_store_path = Some(format!("./tmp/test_errors_{}", timestamp).into());
let client = IrohClient::new(config).await.unwrap();
let result = client.cat_bytes("invalid_hash").await;
assert!(result.is_err());
}
}