ant_core/data/client/mod.rs
1//! Client operations for the Autonomi network.
2//!
3//! Provides high-level APIs for storing and retrieving data
4//! on the Autonomi decentralized network.
5
6pub mod batch;
7pub mod cache;
8pub mod chunk;
9pub mod data;
10pub mod file;
11pub mod merkle;
12pub mod payment;
13pub(crate) mod peer_cache;
14pub mod quote;
15
16use crate::data::client::cache::ChunkCache;
17use crate::data::error::{Error, Result};
18use crate::data::network::Network;
19use ant_protocol::evm::Wallet;
20use ant_protocol::transport::{MultiAddr, P2PNode, PeerId};
21use ant_protocol::{XorName, CLOSE_GROUP_SIZE};
22use std::sync::atomic::{AtomicU64, Ordering};
23use std::sync::Arc;
24use tracing::debug;
25
26/// Default timeout for lightweight network operations (quotes, DHT lookups) in seconds.
27const DEFAULT_QUOTE_TIMEOUT_SECS: u64 = 10;
28
29/// Default timeout for chunk store operations in seconds.
30///
31/// Chunk PUTs transfer multi-MB payloads to multiple peers. On residential
32/// connections with limited upload bandwidth, the default quote timeout (10 s)
33/// is far too short — a 4 MB chunk at 1 Mbps takes ~32 s just for the data
34/// transfer, before accounting for QUIC slow-start and NAT traversal overhead.
35const DEFAULT_STORE_TIMEOUT_SECS: u64 = 10;
36
37/// Default quote concurrency: high because quoting is pure network I/O
38/// (DHT lookups + small request/response messages) with no CPU-bound work.
39const DEFAULT_QUOTE_CONCURRENCY: usize = 32;
40
41/// Default store concurrency: moderate because each chunk PUT sends ~4MB
42/// to 7 close-group peers. At 8 concurrent stores, ~225MB of outbound
43/// traffic can be in flight. Users on fast connections can increase this
44/// with --store-concurrency; users on slow connections can decrease it.
45const DEFAULT_STORE_CONCURRENCY: usize = 8;
46
47/// Configuration for the Autonomi client.
48#[derive(Debug, Clone)]
49pub struct ClientConfig {
50 /// Timeout for lightweight network operations (quotes, DHT lookups) in seconds.
51 pub quote_timeout_secs: u64,
52 /// Timeout for chunk store (PUT) operations in seconds.
53 ///
54 /// This should be significantly longer than `quote_timeout_secs` because
55 /// each chunk PUT transfers ~4 MB to multiple peers.
56 pub store_timeout_secs: u64,
57 /// Number of closest peers to consider for routing.
58 pub close_group_size: usize,
59 /// Maximum number of chunks quoted or downloaded concurrently.
60 ///
61 /// Controls parallelism for quote collection and chunk retrieval.
62 /// These are pure network I/O operations (DHT lookups, small messages)
63 /// with negligible CPU cost, so a high default is safe.
64 pub quote_concurrency: usize,
65 /// Maximum number of chunks stored concurrently during uploads.
66 ///
67 /// Controls parallelism for chunk PUT operations. Lower than quote
68 /// concurrency because storing to NAT nodes requires hole-punch
69 /// connection establishment, which is stateful and time-sensitive.
70 /// Defaults to half the available CPU threads.
71 pub store_concurrency: usize,
72 /// Allow loopback (`127.0.0.1`) connections in the saorsa-transport
73 /// layer. Set to `true` only for devnet / local testing. Production
74 /// peers on the public Autonomi network reject the QUIC handshake
75 /// variant produced when this is `true`, so the default is `false`.
76 ///
77 /// This mirrors the `--allow-loopback` flag in `ant-cli`, which already
78 /// defaults to `false` and threads through to the same
79 /// `CoreNodeConfig::builder().local(...)` call.
80 pub allow_loopback: bool,
81 /// Bind a dual-stack IPv6 socket (`true`) or an IPv4-only socket
82 /// (`false`). Defaults to `true`, matching the CLI default.
83 ///
84 /// Set to `false` only when running on hosts without a working IPv6
85 /// stack, to avoid advertising unreachable v6 addresses to the DHT
86 /// (which causes slow connects and junk DHT address records). This
87 /// mirrors the `--ipv4-only` flag in `ant-cli`.
88 pub ipv6: bool,
89}
90
91impl Default for ClientConfig {
92 fn default() -> Self {
93 Self {
94 quote_timeout_secs: DEFAULT_QUOTE_TIMEOUT_SECS,
95 store_timeout_secs: DEFAULT_STORE_TIMEOUT_SECS,
96 close_group_size: CLOSE_GROUP_SIZE,
97 quote_concurrency: DEFAULT_QUOTE_CONCURRENCY,
98 store_concurrency: DEFAULT_STORE_CONCURRENCY,
99 allow_loopback: false,
100 ipv6: true,
101 }
102 }
103}
104
105/// Client for the Autonomi decentralized network.
106///
107/// Provides high-level APIs for storing and retrieving chunks
108/// and files on the network.
109pub struct Client {
110 config: ClientConfig,
111 network: Network,
112 wallet: Option<Arc<Wallet>>,
113 evm_network: Option<ant_protocol::evm::Network>,
114 chunk_cache: ChunkCache,
115 next_request_id: AtomicU64,
116}
117
118impl Client {
119 /// Create a client connected to the given P2P node.
120 #[must_use]
121 pub fn from_node(node: Arc<P2PNode>, config: ClientConfig) -> Self {
122 let network = Network::from_node(node);
123 Self {
124 config,
125 network,
126 wallet: None,
127 evm_network: None,
128 chunk_cache: ChunkCache::default(),
129 next_request_id: AtomicU64::new(1),
130 }
131 }
132
133 /// Create a client connected to bootstrap peers.
134 ///
135 /// Threads `config.allow_loopback` and `config.ipv6` through to
136 /// `Network::new`, which controls the saorsa-transport `local` and
137 /// `ipv6` flags on the underlying `CoreNodeConfig`. See
138 /// `ClientConfig::allow_loopback` and `ClientConfig::ipv6` for details.
139 ///
140 /// # Errors
141 ///
142 /// Returns an error if the P2P node cannot be created or bootstrapping fails.
143 pub async fn connect(
144 bootstrap_peers: &[std::net::SocketAddr],
145 config: ClientConfig,
146 ) -> Result<Self> {
147 debug!(
148 "Connecting to Autonomi network with {} bootstrap peers (allow_loopback={}, ipv6={})",
149 bootstrap_peers.len(),
150 config.allow_loopback,
151 config.ipv6,
152 );
153 let network = Network::new(bootstrap_peers, config.allow_loopback, config.ipv6).await?;
154 Ok(Self {
155 config,
156 network,
157 wallet: None,
158 evm_network: None,
159 chunk_cache: ChunkCache::default(),
160 next_request_id: AtomicU64::new(1),
161 })
162 }
163
164 /// Set the wallet for payment operations.
165 ///
166 /// Also populates the EVM network from the wallet so that
167 /// token approvals work without a separate `with_evm_network` call.
168 #[must_use]
169 pub fn with_wallet(mut self, wallet: Wallet) -> Self {
170 self.evm_network = Some(wallet.network().clone());
171 self.wallet = Some(Arc::new(wallet));
172 self
173 }
174
175 /// Set the EVM network without requiring a wallet.
176 ///
177 /// This enables token approval and contract interactions
178 /// for external-signer flows where the private key lives outside Rust.
179 #[must_use]
180 pub fn with_evm_network(mut self, network: ant_protocol::evm::Network) -> Self {
181 self.evm_network = Some(network);
182 self
183 }
184
185 /// Get the EVM network, falling back to the wallet's network if available.
186 ///
187 /// # Errors
188 ///
189 /// Returns an error if neither `with_evm_network` nor `with_wallet` was called.
190 pub(crate) fn require_evm_network(&self) -> Result<&ant_protocol::evm::Network> {
191 if let Some(ref net) = self.evm_network {
192 return Ok(net);
193 }
194 if let Some(ref wallet) = self.wallet {
195 return Ok(wallet.network());
196 }
197 Err(Error::Payment(
198 "EVM network not configured — call with_evm_network() or with_wallet() first"
199 .to_string(),
200 ))
201 }
202
203 /// Get the client configuration.
204 #[must_use]
205 pub fn config(&self) -> &ClientConfig {
206 &self.config
207 }
208
209 /// Get a mutable reference to the client configuration.
210 pub fn config_mut(&mut self) -> &mut ClientConfig {
211 &mut self.config
212 }
213
214 /// Get a reference to the network layer.
215 #[must_use]
216 pub fn network(&self) -> &Network {
217 &self.network
218 }
219
220 /// Get the wallet, if configured.
221 #[must_use]
222 pub fn wallet(&self) -> Option<&Arc<Wallet>> {
223 self.wallet.as_ref()
224 }
225
226 /// Get a reference to the chunk cache.
227 #[must_use]
228 pub fn chunk_cache(&self) -> &ChunkCache {
229 &self.chunk_cache
230 }
231
232 /// Get the next request ID for protocol messages.
233 pub(crate) fn next_request_id(&self) -> u64 {
234 self.next_request_id.fetch_add(1, Ordering::Relaxed)
235 }
236
237 /// Return all peers in the close group for a target address.
238 ///
239 /// Queries the DHT for the closest peers by XOR distance.
240 /// Returns each peer paired with its known network addresses.
241 pub(crate) async fn close_group_peers(
242 &self,
243 target: &XorName,
244 ) -> Result<Vec<(PeerId, Vec<MultiAddr>)>> {
245 let peers = self
246 .network()
247 .find_closest_peers(target, self.config().close_group_size)
248 .await?;
249
250 if peers.is_empty() {
251 return Err(Error::InsufficientPeers(
252 "DHT returned no peers for target address".to_string(),
253 ));
254 }
255 Ok(peers)
256 }
257}