Skip to main content

ant_core/data/client/
data.rs

1//! In-memory data operations using self-encryption.
2//!
3//! Upload and download raw byte data. Content is encrypted via
4//! convergent encryption and stored as content-addressed chunks.
5//! Use this when you already have data in memory (e.g., `Bytes`).
6//! For file-based streaming uploads that avoid loading the entire
7//! file into memory, see the `file` module.
8
9use crate::data::client::adaptive::{observe_op, rebucketed_ordered};
10use crate::data::client::batch::{PaymentIntent, PreparedChunk};
11use crate::data::client::classify_error;
12use crate::data::client::file::{ExternalPaymentInfo, PreparedUpload, Visibility};
13use crate::data::client::merkle::{chunk_contents_for_upload_addresses, PaymentMode};
14use crate::data::client::Client;
15use crate::data::error::{Error, Result};
16use ant_protocol::{compute_address, DATA_TYPE_CHUNK};
17use bytes::Bytes;
18use futures::stream::StreamExt;
19use self_encryption::{decrypt, encrypt, get_root_data_map, DataMap, EncryptedChunk};
20use std::num::NonZeroUsize;
21use tokio::runtime::{Handle, RuntimeFlavor};
22use tracing::{debug, info};
23use xor_name::XorName;
24
25/// Result of an in-memory data upload: the `DataMap` needed to retrieve the data.
26#[derive(Debug, Clone)]
27pub struct DataUploadResult {
28    /// The data map containing chunk metadata for reconstruction.
29    pub data_map: DataMap,
30    /// Number of chunks stored on the network.
31    pub chunks_stored: usize,
32    /// Which payment mode was actually used (not just requested).
33    pub payment_mode_used: PaymentMode,
34}
35
36impl Client {
37    /// Upload in-memory data to the network using self-encryption.
38    ///
39    /// The content is encrypted and split into chunks, each stored
40    /// as a content-addressed chunk on the network. Returns a `DataMap`
41    /// that can be used to retrieve and decrypt the data.
42    ///
43    /// # Errors
44    ///
45    /// Returns an error if encryption fails or any chunk cannot be stored.
46    pub async fn data_upload(&self, content: Bytes) -> Result<DataUploadResult> {
47        let content_len = content.len();
48        debug!("Encrypting data ({content_len} bytes)");
49
50        let (data_map, encrypted_chunks) = encrypt(content)
51            .map_err(|e| Error::Encryption(format!("Failed to encrypt data: {e}")))?;
52
53        info!("Data encrypted into {} chunks", encrypted_chunks.len());
54
55        let chunk_contents: Vec<Bytes> = encrypted_chunks
56            .into_iter()
57            .map(|chunk| chunk.content)
58            .collect();
59
60        let (addresses, _storage_cost, _gas_cost) =
61            self.batch_upload_chunks(chunk_contents).await?;
62        let chunks_stored = addresses.len();
63
64        info!("Data uploaded: {chunks_stored} chunks stored ({content_len} bytes original)");
65
66        Ok(DataUploadResult {
67            data_map,
68            chunks_stored,
69            payment_mode_used: PaymentMode::Single,
70        })
71    }
72
73    /// Upload in-memory data with a specific payment mode.
74    ///
75    /// When `mode` is `Auto` and the chunk count >= threshold, or when `mode`
76    /// is `Merkle`, this buffers all chunks and pays via a single merkle
77    /// batch transaction. Otherwise falls back to per-chunk payment.
78    ///
79    /// # Errors
80    ///
81    /// Returns an error if encryption fails or any chunk cannot be stored.
82    pub async fn data_upload_with_mode(
83        &self,
84        content: Bytes,
85        mode: PaymentMode,
86    ) -> Result<DataUploadResult> {
87        let content_len = content.len();
88        debug!("Encrypting data ({content_len} bytes) with mode {mode:?}");
89
90        let (data_map, encrypted_chunks) = encrypt(content)
91            .map_err(|e| Error::Encryption(format!("Failed to encrypt data: {e}")))?;
92
93        let chunk_count = encrypted_chunks.len();
94        info!("Data encrypted into {chunk_count} chunks");
95
96        let chunk_contents: Vec<Bytes> = encrypted_chunks
97            .into_iter()
98            .map(|chunk| chunk.content)
99            .collect();
100
101        if self.should_use_merkle(chunk_count, mode) {
102            // Merkle batch payment path
103            info!("Using merkle batch payment for {chunk_count} chunks");
104
105            let chunk_entries: Vec<([u8; 32], u64)> = chunk_contents
106                .iter()
107                .map(|chunk| {
108                    let size = u64::try_from(chunk.len())
109                        .map_err(|e| Error::InvalidData(format!("chunk size too large: {e}")))?;
110                    Ok((compute_address(chunk), size))
111                })
112                .collect::<Result<Vec<_>>>()?;
113            let merkle_plan = match self
114                .plan_merkle_upload(chunk_entries, DATA_TYPE_CHUNK, None)
115                .await
116            {
117                Ok(plan) => plan,
118                Err(Error::InsufficientPeers(ref msg)) if mode == PaymentMode::Auto => {
119                    info!("Merkle preflight needs more peers ({msg}), falling back to wave-batch");
120                    let (addresses, _sc, _gc) = self.batch_upload_chunks(chunk_contents).await?;
121                    return Ok(DataUploadResult {
122                        data_map,
123                        chunks_stored: addresses.len(),
124                        payment_mode_used: PaymentMode::Single,
125                    });
126                }
127                Err(e) => return Err(e),
128            };
129
130            if merkle_plan.to_upload.is_empty() {
131                info!("All {chunk_count} chunks already stored; skipping merkle payment");
132                return Ok(DataUploadResult {
133                    data_map,
134                    chunks_stored: chunk_count,
135                    payment_mode_used: PaymentMode::Merkle,
136                });
137            }
138
139            let chunk_contents =
140                chunk_contents_for_upload_addresses(chunk_contents, &merkle_plan.to_upload)?;
141
142            let remaining_chunks = merkle_plan.to_upload.len();
143            if !self.should_use_merkle(remaining_chunks, mode) {
144                info!(
145                    "{remaining_chunks} chunks need upload after merkle preflight; \
146                     using single-node payment"
147                );
148                let (addresses, _sc, _gc) = self.batch_upload_chunks(chunk_contents).await?;
149                return Ok(DataUploadResult {
150                    data_map,
151                    chunks_stored: merkle_plan.already_stored.len() + addresses.len(),
152                    payment_mode_used: PaymentMode::Single,
153                });
154            }
155
156            // Try merkle batch; in Auto mode, fall back to per-chunk on network issues
157            let batch_result = match self
158                .pay_for_merkle_batch(
159                    &merkle_plan.to_upload,
160                    DATA_TYPE_CHUNK,
161                    merkle_plan.to_upload_avg_size(),
162                )
163                .await
164            {
165                Ok(result) => result,
166                Err(Error::InsufficientPeers(ref msg)) if mode == PaymentMode::Auto => {
167                    info!("Merkle needs more peers ({msg}), falling back to wave-batch");
168                    let (addresses, _sc, _gc) = self.batch_upload_chunks(chunk_contents).await?;
169                    return Ok(DataUploadResult {
170                        data_map,
171                        chunks_stored: merkle_plan.already_stored.len() + addresses.len(),
172                        payment_mode_used: PaymentMode::Single,
173                    });
174                }
175                Err(e) => return Err(e),
176            };
177
178            let outcome = self
179                .merkle_upload_chunks(
180                    chunk_contents,
181                    merkle_plan.to_upload,
182                    &batch_result,
183                    None,
184                    merkle_plan.already_stored.len(),
185                    chunk_count,
186                )
187                .await?;
188            // Unlike `FileUploadResult`, `DataUploadResult` cannot express a
189            // partial store, and the returned `data_map` is unusable unless
190            // every chunk landed (download fails on any missing chunk). So a
191            // residual shortfall after retries is a hard failure here, not a
192            // success with a quietly broken data map.
193            if outcome.failed > 0 {
194                return Err(Error::InsufficientPeers(format!(
195                    "Data merkle upload incomplete: {} of {} chunk(s) short of quorum after retries",
196                    outcome.failed, chunk_count
197                )));
198            }
199
200            info!(
201                "Data uploaded via merkle: {} chunks stored ({content_len} bytes)",
202                outcome.stored
203            );
204            Ok(DataUploadResult {
205                data_map,
206                chunks_stored: outcome.stored,
207                payment_mode_used: PaymentMode::Merkle,
208            })
209        } else {
210            // Wave-based batch payment path (single EVM tx per wave).
211            let (addresses, _sc, _gc) = self.batch_upload_chunks(chunk_contents).await?;
212
213            info!(
214                "Data uploaded: {} chunks stored ({content_len} bytes original)",
215                addresses.len()
216            );
217            Ok(DataUploadResult {
218                data_map,
219                chunks_stored: addresses.len(),
220                payment_mode_used: PaymentMode::Single,
221            })
222        }
223    }
224
225    /// Phase 1 of external-signer data upload: encrypt and collect quotes.
226    ///
227    /// Equivalent to [`Client::data_prepare_upload_with_visibility`] with
228    /// [`Visibility::Private`] — see that method for details.
229    pub async fn data_prepare_upload(&self, content: Bytes) -> Result<PreparedUpload> {
230        self.data_prepare_upload_with_visibility(content, Visibility::Private)
231            .await
232    }
233
234    /// Phase 1 of external-signer data upload with explicit [`Visibility`] control.
235    ///
236    /// Encrypts in-memory data via self-encryption, then collects storage
237    /// quotes for each chunk without making any on-chain payment. Returns
238    /// a [`PreparedUpload`] containing the data map and a [`PaymentIntent`]
239    /// with the payment details for external signing.
240    ///
241    /// When `visibility` is [`Visibility::Public`], the serialized `DataMap`
242    /// is bundled into the payment batch as an additional chunk and its
243    /// address is recorded on the returned [`PreparedUpload`]. After
244    /// [`Client::finalize_upload`] succeeds, that address is surfaced via
245    /// [`crate::data::client::file::FileUploadResult::data_map_address`] so
246    /// the uploader can share a single address from which anyone can retrieve
247    /// the data.
248    ///
249    /// Wave-batch payment only — the in-memory data path does not currently
250    /// support merkle batching. Use [`Client::file_prepare_upload_with_visibility`]
251    /// for merkle-eligible public uploads.
252    ///
253    /// After the caller signs and submits the payment transaction, call
254    /// [`Client::finalize_upload`] with the tx hashes to complete storage.
255    ///
256    /// # Errors
257    ///
258    /// Returns an error if encryption fails, DataMap serialization fails
259    /// (public only), or quote collection fails.
260    pub async fn data_prepare_upload_with_visibility(
261        &self,
262        content: Bytes,
263        visibility: Visibility,
264    ) -> Result<PreparedUpload> {
265        let content_len = content.len();
266        debug!("Preparing data upload for external signing (visibility={visibility:?}, {content_len} bytes)");
267
268        let (data_map, encrypted_chunks) = encrypt(content)
269            .map_err(|e| Error::Encryption(format!("Failed to encrypt data: {e}")))?;
270
271        let mut chunk_contents: Vec<Bytes> = encrypted_chunks
272            .into_iter()
273            .map(|chunk| chunk.content)
274            .collect();
275
276        info!("Data encrypted into {} chunks", chunk_contents.len());
277
278        // For public uploads, bundle the serialized DataMap as an extra chunk
279        // in the same payment batch. This lets the external signer pay for
280        // the data chunks and the DataMap chunk in one flow, and lets the
281        // finalize step return the DataMap's chunk address as the shareable
282        // retrieval address.
283        let data_map_address = match visibility {
284            Visibility::Private => None,
285            Visibility::Public => {
286                let serialized = rmp_serde::to_vec(&data_map).map_err(|e| {
287                    Error::Serialization(format!("Failed to serialize DataMap: {e}"))
288                })?;
289                let bytes = Bytes::from(serialized);
290                let address = compute_address(&bytes);
291                info!(
292                    "Public upload: bundling DataMap chunk ({} bytes) at address {}",
293                    bytes.len(),
294                    hex::encode(address)
295                );
296                chunk_contents.push(bytes);
297                Some(address)
298            }
299        };
300
301        let chunk_count = chunk_contents.len();
302        let chunks_with_addr: Vec<(Bytes, [u8; 32])> = chunk_contents
303            .into_iter()
304            .map(|content| {
305                let address = compute_address(&content);
306                (content, address)
307            })
308            .collect();
309
310        let quote_limiter = self.controller().quote.clone();
311        let quote_concurrency = quote_limiter.current().min(chunk_count.max(1));
312        let results: Vec<([u8; 32], Result<Option<PreparedChunk>>)> =
313            futures::stream::iter(chunks_with_addr)
314                .map(|(content, address)| {
315                    let limiter = quote_limiter.clone();
316                    async move {
317                        let result = observe_op(
318                            &limiter,
319                            || async move { self.prepare_chunk_payment(content).await },
320                            classify_error,
321                        )
322                        .await;
323                        (address, result)
324                    }
325                })
326                .buffer_unordered(quote_concurrency)
327                .collect()
328                .await;
329
330        let mut prepared_chunks = Vec::with_capacity(results.len());
331        let mut already_stored_addresses = Vec::new();
332        for (address, result) in results {
333            match result? {
334                Some(prepared) => prepared_chunks.push(prepared),
335                None => already_stored_addresses.push(address),
336            }
337        }
338
339        if let Some(addr) = data_map_address {
340            if already_stored_addresses.contains(&addr) {
341                info!(
342                    "Public upload: DataMap chunk {} was already stored \
343                     on the network — address is retrievable without a \
344                     new payment",
345                    hex::encode(addr)
346                );
347            }
348        }
349
350        let payment_intent = PaymentIntent::from_prepared_chunks(&prepared_chunks);
351
352        info!(
353            "Data prepared for external signing: {} chunks, {} already stored, total {} atto ({content_len} bytes)",
354            prepared_chunks.len(),
355            already_stored_addresses.len(),
356            payment_intent.total_amount,
357        );
358
359        Ok(PreparedUpload {
360            data_map,
361            payment_info: ExternalPaymentInfo::WaveBatch {
362                prepared_chunks,
363                payment_intent,
364            },
365            data_map_address,
366            already_stored_addresses,
367            total_chunks: chunk_count,
368        })
369    }
370
371    /// Store a `DataMap` on the network as a public chunk.
372    ///
373    /// The serialized `DataMap` is stored as a regular content-addressed chunk.
374    /// Anyone who knows the returned address can retrieve and use the `DataMap`
375    /// to download the original data.
376    ///
377    /// # Errors
378    ///
379    /// Returns an error if serialization or the chunk store fails.
380    pub async fn data_map_store(&self, data_map: &DataMap) -> Result<[u8; 32]> {
381        let serialized = rmp_serde::to_vec(data_map)
382            .map_err(|e| Error::Serialization(format!("Failed to serialize DataMap: {e}")))?;
383
384        info!(
385            "Storing DataMap as public chunk ({} bytes serialized)",
386            serialized.len()
387        );
388
389        self.chunk_put(Bytes::from(serialized)).await
390    }
391
392    /// Fetch a `DataMap` from the network by its chunk address.
393    ///
394    /// Retrieves the chunk at `address` and deserializes it as a `DataMap`.
395    ///
396    /// # Errors
397    ///
398    /// Returns [`Error::NotFound`] if no chunk exists at `address`; other
399    /// errors if retrieval or deserialization fails.
400    pub async fn data_map_fetch(&self, address: &[u8; 32]) -> Result<DataMap> {
401        let chunk = self.chunk_get(address).await?.ok_or_else(|| {
402            Error::NotFound(format!(
403                "DataMap chunk not found at {}",
404                hex::encode(address)
405            ))
406        })?;
407
408        decode_data_map_chunk(&chunk.content)
409    }
410
411    /// Fetch a `DataMap` from the network by trying the requested number
412    /// of closest peers for the DataMap chunk.
413    ///
414    /// # Errors
415    ///
416    /// Returns [`Error::NotFound`] if no chunk exists at `address`; other
417    /// errors if retrieval or deserialization fails.
418    pub async fn data_map_fetch_from_closest_peers(
419        &self,
420        address: &[u8; 32],
421        peer_count: NonZeroUsize,
422    ) -> Result<DataMap> {
423        let chunk = self
424            .chunk_get_from_closest_peers(address, peer_count.get())
425            .await?
426            .ok_or_else(|| {
427                Error::NotFound(format!(
428                    "DataMap chunk not found at {}",
429                    hex::encode(address)
430                ))
431            })?;
432
433        decode_data_map_chunk(&chunk.content)
434    }
435
436    /// Download and decrypt data from the network using its `DataMap`.
437    ///
438    /// Retrieves all chunks referenced by the data map, then decrypts
439    /// and reassembles the original content. Fetches chunks concurrently;
440    /// the fan-out is sized by the adaptive controller's `fetch` channel
441    /// and ramps up under healthy conditions.
442    ///
443    /// Large uploads produce a *shrunk* (child) `DataMap` whose `infos()`
444    /// reference wrapper chunks rather than the root content chunks. Such a
445    /// map is resolved back to its root form before download, keeping this
446    /// primitive symmetric with `data_upload`.
447    ///
448    /// Resolving a shrunk map bridges self-encryption's synchronous fetcher
449    /// onto the async network via `block_in_place`, so it requires a
450    /// multi-threaded Tokio runtime; on a current-thread runtime it returns
451    /// [`Error::Config`] instead of panicking. Flat maps (the common case)
452    /// take neither path, so this requirement never applies to them.
453    ///
454    /// # Errors
455    ///
456    /// Returns an error if any chunk cannot be retrieved (a chunk absent from
457    /// every queried peer surfaces as [`Error::NotFound`]), if decryption
458    /// fails, or if a shrunk map must be resolved on a current-thread runtime.
459    pub async fn data_download(&self, data_map: &DataMap) -> Result<Bytes> {
460        let root_data_map = self.resolve_root_data_map(data_map).await?;
461
462        let chunk_infos = root_data_map.infos();
463        debug!("Downloading data ({} chunks)", chunk_infos.len());
464
465        // Extract owned addresses to avoid HRTB lifetime issue with
466        // stream::iter over references combined with async closures.
467        let addresses: Vec<[u8; 32]> = chunk_infos.iter().map(|info| info.dst_hash.0).collect();
468
469        // Rolling rebucketing: re-reads the controller's fetch cap as
470        // each slot frees, so a long download (e.g. 10 GB = ~2500
471        // chunks) sees adaptive growth/decay mid-flight without batch
472        // fences. Output is index-sorted so self_encryption decrypt
473        // sees DataMap-ordered chunks.
474        let fetch_limiter = self.controller().fetch.clone();
475        let encrypted_chunks: Vec<EncryptedChunk> = rebucketed_ordered(
476            &fetch_limiter,
477            addresses.into_iter().enumerate(),
478            |(idx, address)| {
479                async move {
480                    // chunk_get_observed feeds the adaptive fetch
481                    // limiter once per call via chunk_get_outcome
482                    // (Ok(None) -> Timeout is the load-shedding
483                    // signal for sustained close-group exhaustion).
484                    let chunk = self.chunk_get_observed(&address).await?.ok_or_else(|| {
485                        Error::NotFound(format!(
486                            "Missing chunk {} required for data reconstruction",
487                            hex::encode(address)
488                        ))
489                    })?;
490                    Ok::<_, Error>((
491                        idx,
492                        EncryptedChunk {
493                            content: chunk.content,
494                        },
495                    ))
496                }
497            },
498        )
499        .await?;
500
501        debug!(
502            "All {} chunks retrieved, decrypting",
503            encrypted_chunks.len()
504        );
505
506        let content = decrypt(&root_data_map, &encrypted_chunks)
507            .map_err(|e| Error::Encryption(format!("Failed to decrypt data: {e}")))?;
508
509        info!("Data downloaded and decrypted ({} bytes)", content.len());
510
511        Ok(content)
512    }
513
514    /// Resolve a possibly-shrunk `DataMap` to its root (flat) form.
515    ///
516    /// `data_upload` shrinks large maps via self-encryption's
517    /// `shrink_data_map`: the serialized map is recursively encrypted into
518    /// wrapper chunks (which are uploaded alongside the content chunks) and
519    /// the returned map has `is_child() == true`. Its `infos()` reference the
520    /// outermost wrapper chunks, *not* the root content chunks, so handing it
521    /// straight to `decrypt` fails with a missing-chunk error for a root-level
522    /// chunk that was never fetched.
523    ///
524    /// This fetches the wrapper chunks and unshrinks recursively (via
525    /// `self_encryption::get_root_data_map`) until it obtains the root map,
526    /// whose `infos()` reference the actual content chunks. A map that is not
527    /// a child is returned unchanged without any network access.
528    ///
529    /// Resolution bridges self-encryption's synchronous fetcher onto the async
530    /// network via `block_in_place`, which requires a multi-threaded Tokio
531    /// runtime. On a current-thread runtime this returns [`Error::Config`]
532    /// rather than letting `block_in_place` panic.
533    ///
534    /// # Errors
535    ///
536    /// Returns an error if called on a current-thread runtime, if a wrapper
537    /// chunk cannot be retrieved, or if the map cannot be unshrunk.
538    async fn resolve_root_data_map(&self, data_map: &DataMap) -> Result<DataMap> {
539        if !data_map.is_child() {
540            return Ok(data_map.clone());
541        }
542
543        debug!("DataMap is shrunk (child); resolving root data map");
544
545        // `get_root_data_map` drives a synchronous `FnMut` fetcher, so bridge
546        // to the async `chunk_get_observed` via `block_in_place` + `block_on`
547        // (the same pattern `file_download` uses). The observed variant feeds
548        // the adaptive fetch limiter, keeping the wrapper-chunk fetches
549        // consistent with the content-chunk fetches in `data_download`.
550        // `block_in_place` panics on a current-thread runtime, so reject that
551        // precondition with a clear error instead of letting it panic inside
552        // the fetcher.
553        let handle = Handle::current();
554        ensure_shrunk_resolution_runtime(handle.runtime_flavor())?;
555
556        // The self-encryption fetcher may only yield `self_encryption::Error`.
557        // Capture the underlying `ant-core` error out-of-band so a missing
558        // wrapper chunk surfaces as `Error::NotFound` (matching the
559        // content-chunk path) and a network failure keeps its `Timeout` /
560        // `Network` classification, instead of every resolution failure
561        // flattening to `Error::Encryption`.
562        let mut fetch_error: Option<Error> = None;
563        let resolve_result = tokio::task::block_in_place(|| {
564            let mut get_chunk =
565                |name: XorName| -> std::result::Result<Bytes, self_encryption::Error> {
566                    let address = name.0;
567                    handle.block_on(async {
568                        match self.chunk_get_observed(&address).await {
569                            Ok(Some(chunk)) => Ok(chunk.content),
570                            Ok(None) => Err(record_wrapper_fetch_error(
571                                &mut fetch_error,
572                                Error::NotFound(format!(
573                                    "Missing wrapper chunk {} required to resolve root DataMap",
574                                    hex::encode(address)
575                                )),
576                            )),
577                            Err(e) => Err(record_wrapper_fetch_error(&mut fetch_error, e)),
578                        }
579                    })
580                };
581            get_root_data_map(data_map.clone(), &mut get_chunk)
582        });
583
584        resolve_result.map_err(|e| {
585            fetch_error.take().unwrap_or_else(|| {
586                Error::Encryption(format!("Failed to resolve root data map: {e}"))
587            })
588        })
589    }
590}
591
592/// Stash the real `ant-core` error behind a self-encryption fetch failure and
593/// return the `self_encryption::Error` the fetcher is required to yield.
594///
595/// `get_root_data_map`'s fetcher may only return `self_encryption::Error`, which
596/// would otherwise flatten a missing chunk or a classified network error into a
597/// generic `Error::Encryption`. Recording the descriptive error in `slot` lets
598/// [`Client::resolve_root_data_map`] recover it and preserve the error taxonomy.
599fn record_wrapper_fetch_error(slot: &mut Option<Error>, error: Error) -> self_encryption::Error {
600    let message = error.to_string();
601    *slot = Some(error);
602    self_encryption::Error::Generic(message)
603}
604
605/// Reject resolving a shrunk `DataMap` on a current-thread Tokio runtime.
606///
607/// Resolution uses `block_in_place` to bridge self-encryption's synchronous
608/// chunk fetcher onto the async network, and `block_in_place` panics on a
609/// current-thread runtime. Mapping that precondition to [`Error::Config`]
610/// turns a hard panic into a recoverable error for library consumers that
611/// drive `data_download` from a current-thread runtime.
612fn ensure_shrunk_resolution_runtime(flavor: RuntimeFlavor) -> Result<()> {
613    if flavor == RuntimeFlavor::CurrentThread {
614        return Err(Error::Config(
615            "resolving a shrunk DataMap requires a multi-threaded Tokio runtime, \
616             but data_download was called on a current-thread runtime"
617                .to_string(),
618        ));
619    }
620    Ok(())
621}
622
623fn decode_data_map_chunk(content: &[u8]) -> Result<DataMap> {
624    rmp_serde::from_slice(content)
625        .map_err(|e| Error::Serialization(format!("Failed to deserialize DataMap: {e}")))
626}
627
628/// Compile-time assertions that Client method futures are Send.
629///
630/// These methods are called from axum handlers and tokio::spawn contexts
631/// that require Send + 'static. The async closures inside stream
632/// combinators must not capture references with concrete lifetimes
633/// (HRTB issue). If any of these checks fail, the stream closures
634/// need restructuring to use owned values instead of references.
635#[cfg(test)]
636mod send_assertions {
637    use super::*;
638
639    fn _assert_send<T: Send>(_: &T) {}
640
641    #[allow(
642        dead_code,
643        unreachable_code,
644        unused_variables,
645        clippy::diverging_sub_expression
646    )]
647    async fn _data_download_is_send(client: &Client) {
648        let dm: DataMap = todo!();
649        let fut = client.data_download(&dm);
650        _assert_send(&fut);
651    }
652
653    #[allow(dead_code, unreachable_code, clippy::diverging_sub_expression)]
654    async fn _data_upload_is_send(client: &Client) {
655        let fut = client.data_upload(Bytes::new());
656        _assert_send(&fut);
657    }
658
659    #[allow(dead_code, unreachable_code, clippy::diverging_sub_expression)]
660    async fn _data_upload_with_mode_is_send(client: &Client) {
661        let fut = client.data_upload_with_mode(Bytes::new(), PaymentMode::Auto);
662        _assert_send(&fut);
663    }
664
665    #[allow(dead_code, unreachable_code, clippy::diverging_sub_expression)]
666    async fn _data_prepare_upload_is_send(client: &Client) {
667        let fut = client.data_prepare_upload(Bytes::new());
668        _assert_send(&fut);
669    }
670
671    #[allow(dead_code, unreachable_code, clippy::diverging_sub_expression)]
672    async fn _data_prepare_upload_with_visibility_is_send(client: &Client) {
673        let fut = client.data_prepare_upload_with_visibility(Bytes::new(), Visibility::Public);
674        _assert_send(&fut);
675    }
676}
677
678#[cfg(test)]
679mod runtime_guard_tests {
680    use super::*;
681
682    #[test]
683    fn shrunk_resolution_rejects_current_thread_runtime() {
684        // A current-thread runtime cannot drive `block_in_place`, so the guard
685        // must surface a descriptive `Error::Config` rather than panicking.
686        assert!(matches!(
687            ensure_shrunk_resolution_runtime(RuntimeFlavor::CurrentThread),
688            Err(Error::Config(_))
689        ));
690    }
691
692    #[test]
693    fn shrunk_resolution_accepts_multi_thread_runtime() {
694        assert!(ensure_shrunk_resolution_runtime(RuntimeFlavor::MultiThread).is_ok());
695    }
696}