autonomi/client/
mod.rs

1// Copyright 2024 MaidSafe.net limited.
2//
3// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
4// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
5// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
6// KIND, either express or implied. Please review the Licences for the specific language governing
7// permissions and limitations relating to use of the SAFE Network Software.
8
9// Optionally enable nightly `doc_cfg`. Allows items to be annotated, e.g.: "Available on crate feature X only".
10#![cfg_attr(docsrs, feature(doc_cfg))]
11
12/// The 4 basic Network data types.
13/// - Chunk
14/// - GraphEntry
15/// - Pointer
16/// - Scratchpad
17pub mod data_types;
18use ant_bootstrap::BootstrapConfig;
19pub use data_types::chunk;
20pub use data_types::graph;
21pub use data_types::pointer;
22pub use data_types::scratchpad;
23
24/// High-level types built on top of the basic Network data types.
25/// Includes data, files and personnal data vaults
26mod high_level;
27pub use high_level::data;
28pub use high_level::files;
29pub use high_level::register;
30pub use high_level::vault;
31
32pub mod analyze;
33pub mod config;
34pub mod key_derivation;
35pub mod payment;
36pub mod quote;
37
38#[cfg(feature = "external-signer")]
39#[cfg_attr(docsrs, doc(cfg(feature = "external-signer")))]
40pub mod external_signer;
41
42// private module with utility functions
43mod chunk_cache;
44mod data_map_restoration;
45mod network;
46mod put_error_state;
47
48use payment::Receipt;
49pub use put_error_state::ChunkBatchUploadState;
50use quote::PaymentMode;
51
52use ant_bootstrap::{bootstrap::Bootstrap, contacts_fetcher::ALPHANET_CONTACTS};
53pub use ant_evm::Amount;
54use ant_evm::EvmNetwork;
55use config::ClientConfig;
56use payment::PayError;
57use quote::CostError;
58use self_encryption::DataMap;
59use std::collections::HashSet;
60use tokio::sync::mpsc;
61
62/// Time before considering the connection timed out.
63pub const CONNECT_TIMEOUT_SECS: u64 = 10;
64
65const CLIENT_EVENT_CHANNEL_SIZE: usize = 100;
66
67// Amount of peers to confirm into our routing table before we consider the client ready.
68use crate::client::config::ClientOperatingStrategy;
69use crate::networking::{Multiaddr, Network, NetworkAddress, NetworkError, multiaddr_is_global};
70pub use ant_protocol::CLOSE_GROUP_SIZE;
71use ant_protocol::storage::RecordKind;
72
73/// Represents a client for the Autonomi network.
74///
75/// # Example
76///
77/// To start interacting with the network, use [`Client::init`].
78///
79/// ```no_run
80/// # use autonomi::client::Client;
81/// # #[tokio::main]
82/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
83/// let client = Client::init().await?;
84/// # Ok(())
85/// # }
86/// ```
87#[derive(Clone, Debug)]
88pub struct Client {
89    /// The Autonomi Network to use for the client.
90    pub(crate) network: Network,
91    /// Events sent by the client, can be enabled by calling [`Client::enable_client_events`].
92    pub(crate) client_event_sender: Option<mpsc::Sender<ClientEvent>>,
93    /// The EVM network to use for the client.
94    evm_network: EvmNetwork,
95    /// The configuration for operations on the client.
96    config: ClientOperatingStrategy,
97    /// Max times of total chunks to carry out retry on upload failure.
98    /// Default to be `0` to indicate not carry out retry.
99    retry_failed: u64,
100    /// Payment mode to use for uploads
101    payment_mode: PaymentMode,
102}
103
104/// Error returned by [`Client::init`].
105#[derive(Debug, thiserror::Error)]
106pub enum ConnectError {
107    /// Did not manage to populate the routing table with enough peers.
108    #[error("Failed to populate our routing table with enough peers in time")]
109    TimedOut,
110
111    /// Same as [`ConnectError::TimedOut`] but with a list of incompatible protocols.
112    #[error("Failed to populate our routing table due to incompatible protocol: {0:?}")]
113    TimedOutWithIncompatibleProtocol(HashSet<String>, String),
114
115    /// An error occurred while bootstrapping the client.
116    #[error("Failed to bootstrap the client: {0}")]
117    Bootstrap(#[from] ant_bootstrap::Error),
118
119    /// The routing table does not contain any known peers to bootstrap from.
120    #[error("No known peers available in the routing table to bootstrap the client")]
121    NoKnownPeers(#[from] libp2p::kad::NoKnownPeers),
122
123    /// An error occurred while initializing the EVM network.
124    #[error("Failed to initialize the EVM network: {0}")]
125    EvmNetworkError(String),
126}
127
128/// Errors that can occur during the put operation.
129#[derive(Debug, thiserror::Error)]
130pub enum PutError {
131    #[error("Failed to self-encrypt data.")]
132    SelfEncryption(#[from] crate::self_encryption::Error),
133    #[error("Error occurred during cost estimation: {0}")]
134    CostError(#[from] CostError),
135    #[error("Error occurred during payment: {0}")]
136    PayError(#[from] PayError),
137    #[error("Serialization error: {0}")]
138    Serialization(String),
139    #[error("A wallet error occurred: {0}")]
140    Wallet(#[from] ant_evm::EvmError),
141    #[error("The payment proof contains no payees.")]
142    PayeesMissing,
143    #[error("A network error occurred for {address}: {network_error}")]
144    Network {
145        address: Box<NetworkAddress>,
146        network_error: NetworkError,
147        /// if a payment was made, it will be returned here so it can be reused
148        payment: Option<Receipt>,
149    },
150    #[error("Batch upload: {0}")]
151    Batch(ChunkBatchUploadState),
152}
153
154/// Errors that can occur during the get operation.
155#[derive(Debug, thiserror::Error)]
156pub enum GetError {
157    #[error("Could not deserialize data map.")]
158    InvalidDataMap(rmp_serde::decode::Error),
159    #[error("Failed to decrypt data.")]
160    Decryption(crate::self_encryption::Error),
161    #[error("Failed to deserialize")]
162    Deserialization(#[from] rmp_serde::decode::Error),
163    #[error("General networking error: {0}")]
164    Network(#[from] NetworkError),
165    #[error("General protocol error: {0}")]
166    Protocol(#[from] ant_protocol::Error),
167    #[error("Record could not be found.")]
168    RecordNotFound,
169    // The RecordKind that was obtained did not match with the expected one
170    #[error("The RecordKind obtained from the Record did not match with the expected kind: {0}")]
171    RecordKindMismatch(RecordKind),
172    #[error("Configuration error: {0}")]
173    Configuration(String),
174    #[error("Unable to recogonize the so claimed DataMap: {0}")]
175    UnrecognizedDataMap(String),
176    /// When trying to download a file that is too large to be handled in memory
177    /// you can increase the [`crate::client::config::MAX_IN_MEMORY_DOWNLOAD_SIZE`] env var or use the streaming API.
178    #[error(
179        "DataMap points to a file too large to be handled in memory, you can increase the MAX_IN_MEMORY_DOWNLOAD_SIZE env var or use streaming to avoid this error."
180    )]
181    TooLargeForMemory(DataMap),
182}
183
184impl Client {
185    /// Initialize the client with default configuration.
186    ///
187    /// See [`Client::init_with_config`].
188    pub async fn init() -> Result<Self, ConnectError> {
189        Self::init_with_config(ClientConfig {
190            bootstrap_config: BootstrapConfig::new(false),
191            ..Default::default()
192        })
193        .await
194    }
195
196    /// Initialize a client that is configured to be local.
197    ///
198    /// See [`Client::init_with_config`].
199    pub async fn init_local() -> Result<Self, ConnectError> {
200        Self::init_with_config(ClientConfig {
201            evm_network: EvmNetwork::new(true)
202                .map_err(|e| ConnectError::EvmNetworkError(e.to_string()))?,
203            strategy: Default::default(),
204            network_id: None,
205            bootstrap_config: BootstrapConfig::new(true),
206        })
207        .await
208    }
209
210    /// Initialize a client that is configured to be connected to the the alpha network (Impossible Futures).
211    pub async fn init_alpha() -> Result<Self, ConnectError> {
212        let client_config = ClientConfig {
213            bootstrap_config: BootstrapConfig {
214                network_contacts_url: ALPHANET_CONTACTS.iter().map(|s| s.to_string()).collect(),
215                ..Default::default()
216            },
217            evm_network: EvmNetwork::ArbitrumSepoliaTest,
218            strategy: Default::default(),
219            network_id: Some(2),
220        };
221        Self::init_with_config(client_config).await
222    }
223
224    /// Initialize a client that bootstraps from a list of peers.
225    ///
226    /// If any of the provided peers is a global address, the client will not be local.
227    ///
228    /// ```no_run
229    /// # use autonomi::Client;
230    /// # #[tokio::main]
231    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
232    /// // Will set `local` to true.
233    /// let client = Client::init_with_peers(vec!["/ip4/127.0.0.1/udp/1234/quic-v1".parse()?]).await?;
234    /// # Ok(())
235    /// # }
236    /// ```
237    pub async fn init_with_peers(peers: Vec<Multiaddr>) -> Result<Self, ConnectError> {
238        // Any global address makes the client non-local
239        let local = !peers.iter().any(multiaddr_is_global);
240        let bootstrap_config = BootstrapConfig {
241            local,
242            initial_peers: peers.clone(),
243            ..Default::default()
244        };
245
246        Self::init_with_config(ClientConfig {
247            bootstrap_config,
248            evm_network: EvmNetwork::new(local).unwrap_or_default(),
249            strategy: Default::default(),
250            network_id: None,
251        })
252        .await
253    }
254
255    /// Initialize the client with the given configuration.
256    ///
257    /// This will block until [`CLOSE_GROUP_SIZE`] have been added to the routing table.
258    ///
259    /// See [`ClientConfig`].
260    ///
261    /// ```no_run
262    /// use autonomi::client::Client;
263    /// # #[tokio::main]
264    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
265    /// let client = Client::init_with_config(Default::default()).await?;
266    /// # Ok(())
267    /// # }
268    /// ```
269    pub async fn init_with_config(config: ClientConfig) -> Result<Self, ConnectError> {
270        if let Some(network_id) = config.network_id {
271            ant_protocol::version::set_network_id(network_id);
272        }
273
274        let bootstrap = Bootstrap::new(config.bootstrap_config.clone()).await?;
275        let network = Network::new(bootstrap)?;
276
277        // Wait for the network to be ready with enough peers
278        let connectivity_result = network.wait_for_connectivity().await;
279
280        // If the connection failed and we were using the bootstrap cache,
281        // retry once with the cache disabled to fall back to mainnet contacts
282        if connectivity_result.is_err() && !config.bootstrap_config.disable_cache_reading {
283            warn!(
284                "Initial connection failed with bootstrap cache enabled. Retrying with cache disabled to use mainnet contacts..."
285            );
286
287            // Create a new config with cache reading disabled
288            let retry_config = BootstrapConfig {
289                disable_cache_reading: true,
290                ..config.bootstrap_config.clone()
291            };
292
293            // Retry the bootstrap and connection with cache disabled
294            let bootstrap_retry = Bootstrap::new(retry_config).await?;
295            let network_retry = Network::new(bootstrap_retry)?;
296
297            // Wait for connectivity with the new bootstrap configuration
298            network_retry.wait_for_connectivity().await?;
299
300            info!(
301                "Successfully connected to the network using mainnet contacts after cache failure"
302            );
303
304            return Ok(Self {
305                network: network_retry,
306                client_event_sender: None,
307                evm_network: config.evm_network,
308                config: config.strategy,
309                retry_failed: 0,
310                payment_mode: PaymentMode::Standard,
311            });
312        }
313
314        // If the first attempt succeeded or cache was already disabled, return normally
315        connectivity_result?;
316
317        Ok(Self {
318            network,
319            client_event_sender: None,
320            evm_network: config.evm_network,
321            config: config.strategy,
322            retry_failed: 0,
323            payment_mode: PaymentMode::default(),
324        })
325    }
326
327    /// Set the `ClientOperatingStrategy` for the client.
328    pub fn with_strategy(mut self, strategy: ClientOperatingStrategy) -> Self {
329        self.config = strategy;
330        self
331    }
332
333    /// Set whether to retry failed uploads automatically.
334    pub fn with_retry_failed(mut self, retry_failed: u64) -> Self {
335        self.retry_failed = retry_failed;
336        self
337    }
338
339    /// Set the payment mode for uploads.
340    pub fn with_payment_mode(mut self, payment_mode: PaymentMode) -> Self {
341        self.payment_mode = payment_mode;
342        self
343    }
344
345    /// Receive events from the client.
346    pub fn enable_client_events(&mut self) -> mpsc::Receiver<ClientEvent> {
347        let (client_event_sender, client_event_receiver) =
348            tokio::sync::mpsc::channel(CLIENT_EVENT_CHANNEL_SIZE);
349        self.client_event_sender = Some(client_event_sender);
350        debug!("All events to the clients are enabled");
351
352        client_event_receiver
353    }
354
355    /// Get the evm network.
356    pub fn evm_network(&self) -> &EvmNetwork {
357        &self.evm_network
358    }
359}
360
361/// Events that can be sent by the client.
362#[derive(Debug, Clone)]
363pub enum ClientEvent {
364    UploadComplete(UploadSummary),
365}
366
367/// Summary of an upload operation.
368#[derive(Debug, Clone)]
369pub struct UploadSummary {
370    /// Records that were uploaded to the network
371    pub records_paid: usize,
372    /// Records that were already paid for so were not re-uploaded
373    pub records_already_paid: usize,
374    /// Total cost of the upload
375    pub tokens_spent: Amount,
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381    use ant_logging::LogBuilder;
382
383    #[tokio::test]
384    async fn test_init_fails() {
385        let _guard = LogBuilder::init_single_threaded_tokio_test();
386
387        let initial_peers = vec![
388            "/ip4/127.0.0.1/udp/1/quic-v1/p2p/12D3KooWRBhwfeP2Y4TCx1SM6s9rUoHhR5STiGwxBhgFRcw3UERE"
389                .parse()
390                .unwrap(),
391        ];
392        let bootstrap = Bootstrap::new(
393            BootstrapConfig::default()
394                .with_initial_peers(initial_peers)
395                .with_disable_cache_reading(true)
396                .with_disable_env_peers(true)
397                .with_local(true),
398        )
399        .await
400        .unwrap();
401        let network = Network::new(bootstrap).unwrap();
402
403        match network.wait_for_connectivity().await {
404            Err(ConnectError::TimedOut) => {} // This is the expected outcome
405            Ok(()) => panic!("Expected `ConnectError::TimedOut`, but got `Ok`"),
406            Err(err) => {
407                panic!("Expected `ConnectError::TimedOut`, but got `{err:?}`")
408            }
409        }
410    }
411}