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}