Skip to main content

nexus_sdk/walrus/
nexus_data.rs

1//! Walrus upload and digest-verified resolution for direct Nexus data.
2
3use {
4    crate::{
5        move_bindings::{
6            canonical_walrus_blob_id,
7            interface::graph::{InputPort, OutputPort},
8            protocol_limits::primitives::data::MAX_INLINE_DATA_BYTES,
9            sui_framework::vec_map::{Entry as VecMapEntry, VecMap},
10        },
11        types::{NexusData, NexusValue},
12        walrus::{StorageInfo, WalrusClient, WALRUS_MAX_EPOCHS},
13    },
14    futures_util::future::{join_all, try_join_all},
15    sha2::{Digest as _, Sha256},
16    thiserror::Error,
17};
18
19/// A successful Walrus fetch whose bytes do not match the committed SHA-256 digest.
20#[derive(Debug, Error, PartialEq, Eq)]
21#[error("Walrus content digest mismatch for blob ID '{blob_id}'")]
22pub struct WalrusContentDigestMismatch {
23    pub blob_id: String,
24}
25
26/// A Walrus resolution failure that distinguishes unavailable content from fetched bytes that fail integrity.
27///
28/// Unlike [`WalrusContentDigestMismatch`], this type retains the fully resolved value on an integrity failure so an on-chain Tool can receive the concrete bytes while independently checking the committed digest.
29#[derive(Debug, Error)]
30pub enum WalrusFetchError<T> {
31    #[error("Walrus content unavailable: {source}")]
32    Unavailable {
33        #[source]
34        source: anyhow::Error,
35    },
36    #[error("{mismatch}")]
37    DigestMismatch {
38        resolved: T,
39        #[source]
40        mismatch: WalrusContentDigestMismatch,
41    },
42}
43
44impl<T> WalrusFetchError<T> {
45    fn into_strict_error(self) -> anyhow::Error {
46        match self {
47            Self::Unavailable { source } => source,
48            Self::DigestMismatch { mismatch, .. } => mismatch.into(),
49        }
50    }
51}
52
53#[derive(Clone, Debug, Default, PartialEq, Eq)]
54pub struct StorageConf {
55    pub walrus_publisher_url: Option<String>,
56    pub walrus_aggregator_url: Option<String>,
57    pub walrus_save_for_epochs: Option<u8>,
58}
59
60impl StorageConf {
61    /// Validates every local setting required before a Walrus upload can start.
62    pub fn validate_walrus_upload(&self) -> anyhow::Result<()> {
63        for (name, value) in [
64            ("publisher", self.walrus_publisher_url.as_deref()),
65            ("aggregator", self.walrus_aggregator_url.as_deref()),
66        ] {
67            let value = value
68                .ok_or_else(|| anyhow::anyhow!("Walrus {name} URL is not set in storage config"))?;
69            let url = reqwest::Url::parse(value)
70                .map_err(|error| anyhow::anyhow!("Walrus {name} URL is invalid: {error}"))?;
71            anyhow::ensure!(
72                matches!(url.scheme(), "http" | "https"),
73                "Walrus {name} URL must use http or https"
74            );
75        }
76
77        let store_for_epochs = self.walrus_save_for_epochs.ok_or_else(|| {
78            anyhow::anyhow!("Walrus save for epochs is not set in storage config")
79        })?;
80        anyhow::ensure!(
81            store_for_epochs <= WALRUS_MAX_EPOCHS,
82            "Walrus save for epochs exceeds maximum allowed ({WALRUS_MAX_EPOCHS})"
83        );
84        Ok(())
85    }
86}
87
88impl WalrusClient {
89    /// Uploads arbitrary execution bytes through an operator configured publisher,
90    /// verifies the readback and returns the exact digest commitment for a tool port.
91    /// Wallet owners can use `nexus_walrus::WalrusStorage::upload` for native storage.
92    pub async fn upload_value(&self, bytes: Vec<u8>, epochs: u8) -> anyhow::Result<NexusValue> {
93        anyhow::ensure!(
94            bytes.len() <= crate::execution_limits::MAX_RESOLVED_DATA_BYTES,
95            "blob exceeds execution byte limit"
96        );
97        anyhow::ensure!(
98            (1..=WALRUS_MAX_EPOCHS).contains(&epochs),
99            "invalid storage duration"
100        );
101        let digest = Sha256::digest(&bytes).to_vec();
102        let response = self.upload_bytes(bytes.clone(), epochs, None).await?;
103        let blob_id = blob_id_from_storage_info(response)?;
104        let reference = NexusValue::walrus_data(blob_id.as_bytes(), digest)?;
105        let readback = self.read_file_bounded(&blob_id, bytes.len()).await?;
106        anyhow::ensure!(
107            readback == bytes,
108            "Walrus upload readback differs from the original bytes"
109        );
110        Ok(reference)
111    }
112}
113
114impl NexusData {
115    /// Resolves Walrus Data to transient Tool data after verifying its committed digest.
116    pub async fn fetch(self, conf: &StorageConf) -> anyhow::Result<NexusData> {
117        self.resolve(conf)
118            .await
119            .map_err(WalrusFetchError::into_strict_error)
120    }
121
122    /// Resolves every Walrus value while retaining concrete bytes on a digest mismatch.
123    pub async fn resolve(
124        self,
125        conf: &StorageConf,
126    ) -> Result<NexusData, WalrusFetchError<NexusData>> {
127        if !self.is_well_formed() {
128            return Err(WalrusFetchError::Unavailable {
129                source: anyhow::anyhow!("cannot resolve malformed NexusData"),
130            });
131        }
132        let has_walrus = self.has_walrus();
133        let client = if has_walrus {
134            Some(walrus_reader(conf).map_err(|source| WalrusFetchError::Unavailable { source })?)
135        } else {
136            None
137        };
138        let many = self.is_many();
139        let source_values = self
140            .into_values()
141            .map_err(|source| WalrusFetchError::Unavailable { source })?;
142        let mut resolved = source_values
143            .iter()
144            .map(|value| match value {
145                NexusValue::WalrusData { .. } => NexusValue::InlineData { bytes: Vec::new() },
146                value => value.clone(),
147            })
148            .collect::<Vec<_>>();
149        let mut mismatch = None;
150        for (index, value) in source_values.into_iter().enumerate() {
151            if let NexusValue::WalrusData {
152                blob_id,
153                content_digest,
154            } = value
155            {
156                let blob_id = blob_id_from_bytes(&blob_id)
157                    .map_err(|source| WalrusFetchError::Unavailable { source })?;
158                let max_bytes = resolved_blob_byte_limit(&resolved, index, many)
159                    .map_err(|source| WalrusFetchError::Unavailable { source })?;
160                let bytes = client
161                    .as_ref()
162                    .expect("Walrus client exists when a reference is present")
163                    .read_file_bounded(&blob_id, max_bytes)
164                    .await
165                    .map_err(|source| WalrusFetchError::Unavailable {
166                        source: source.into(),
167                    })?;
168                let actual_digest: [u8; 32] = Sha256::digest(&bytes).into();
169                if actual_digest.as_slice() != content_digest && mismatch.is_none() {
170                    mismatch = Some(WalrusContentDigestMismatch { blob_id });
171                }
172                resolved[index] = NexusValue::InlineData { bytes };
173            }
174        }
175        let resolved = NexusData::from_values(resolved, many)
176            .map_err(|source| WalrusFetchError::Unavailable { source })?;
177        match mismatch {
178            Some(mismatch) => Err(WalrusFetchError::DigestMismatch { resolved, mismatch }),
179            None => Ok(resolved),
180        }
181    }
182
183    /// Verifies existing Walrus references while leaving their committed form unchanged.
184    pub async fn commit(self, conf: &StorageConf) -> anyhow::Result<Self> {
185        anyhow::ensure!(self.is_well_formed(), "cannot commit malformed NexusData");
186        if self.has_walrus() {
187            self.clone().fetch(conf).await?;
188        }
189        Ok(self)
190    }
191
192    /// Uploads every inline Data value and returns digest-bound Walrus values.
193    pub async fn upload_data_fields(self, conf: &StorageConf) -> anyhow::Result<Self> {
194        anyhow::ensure!(self.is_well_formed(), "cannot upload malformed NexusData");
195        let (client, store_for_epochs) = walrus_writer(conf)?;
196        let many = self.is_many();
197        let mut uploaded = Vec::new();
198        for value in self.into_values()? {
199            match value {
200                NexusValue::InlineData { bytes } => {
201                    uploaded.push(client.upload_value(bytes, store_for_epochs).await?);
202                }
203                NexusValue::WalrusData { .. } => {
204                    anyhow::bail!("Data upload received an already remote value");
205                }
206                NexusValue::Object { .. } => {
207                    anyhow::bail!("Data upload cannot store Object values in Walrus");
208                }
209            }
210        }
211        NexusData::from_values(uploaded, many)
212    }
213}
214
215impl VecMap<InputPort, NexusData> {
216    pub async fn commit_all(self, storage_conf: &StorageConf) -> anyhow::Result<Self> {
217        commit_entries(self.contents, storage_conf)
218            .await
219            .map(|contents| Self { contents })
220    }
221
222    pub async fn fetch_all(
223        self,
224        storage_conf: &StorageConf,
225    ) -> anyhow::Result<VecMap<InputPort, NexusData>> {
226        resolve_entries(self.contents, storage_conf)
227            .await
228            .map(|contents| VecMap { contents })
229            .map_err(WalrusFetchError::into_strict_error)
230    }
231
232    /// Resolves every port while retaining concrete values on a digest mismatch.
233    pub async fn resolve_all(
234        self,
235        storage_conf: &StorageConf,
236    ) -> Result<VecMap<InputPort, NexusData>, WalrusFetchError<VecMap<InputPort, NexusData>>> {
237        match resolve_entries(self.contents, storage_conf).await {
238            Ok(contents) => Ok(VecMap { contents }),
239            Err(WalrusFetchError::Unavailable { source }) => {
240                Err(WalrusFetchError::Unavailable { source })
241            }
242            Err(WalrusFetchError::DigestMismatch { resolved, mismatch }) => {
243                Err(WalrusFetchError::DigestMismatch {
244                    resolved: VecMap { contents: resolved },
245                    mismatch,
246                })
247            }
248        }
249    }
250}
251
252impl VecMap<OutputPort, NexusData> {
253    pub async fn commit_all(self, storage_conf: &StorageConf) -> anyhow::Result<Self> {
254        commit_entries(self.contents, storage_conf)
255            .await
256            .map(|contents| Self { contents })
257    }
258
259    pub async fn fetch_all(
260        self,
261        storage_conf: &StorageConf,
262    ) -> anyhow::Result<VecMap<OutputPort, NexusData>> {
263        resolve_entries(self.contents, storage_conf)
264            .await
265            .map(|contents| VecMap { contents })
266            .map_err(WalrusFetchError::into_strict_error)
267    }
268
269    /// Resolves every port while retaining concrete values on a digest mismatch.
270    pub async fn resolve_all(
271        self,
272        storage_conf: &StorageConf,
273    ) -> Result<VecMap<OutputPort, NexusData>, WalrusFetchError<VecMap<OutputPort, NexusData>>>
274    {
275        match resolve_entries(self.contents, storage_conf).await {
276            Ok(contents) => Ok(VecMap { contents }),
277            Err(WalrusFetchError::Unavailable { source }) => {
278                Err(WalrusFetchError::Unavailable { source })
279            }
280            Err(WalrusFetchError::DigestMismatch { resolved, mismatch }) => {
281                Err(WalrusFetchError::DigestMismatch {
282                    resolved: VecMap { contents: resolved },
283                    mismatch,
284                })
285            }
286        }
287    }
288}
289
290async fn commit_entries<P>(
291    contents: Vec<VecMapEntry<P, NexusData>>,
292    storage_conf: &StorageConf,
293) -> anyhow::Result<Vec<VecMapEntry<P, NexusData>>> {
294    validate_entries(&contents)?;
295    try_join_all(contents.into_iter().map(|entry| async move {
296        entry
297            .value
298            .commit(storage_conf)
299            .await
300            .map(|value| VecMapEntry {
301                key: entry.key,
302                value,
303            })
304    }))
305    .await
306}
307
308async fn resolve_entries<P>(
309    contents: Vec<VecMapEntry<P, NexusData>>,
310    storage_conf: &StorageConf,
311) -> Result<Vec<VecMapEntry<P, NexusData>>, WalrusFetchError<Vec<VecMapEntry<P, NexusData>>>> {
312    validate_entries(&contents).map_err(|source| WalrusFetchError::Unavailable { source })?;
313    let fetched = join_all(
314        contents
315            .into_iter()
316            .map(|entry| async move { (entry.key, entry.value.resolve(storage_conf).await) }),
317    )
318    .await;
319    let mut resolved = Vec::with_capacity(fetched.len());
320    let mut mismatch = None;
321    for (key, value) in fetched {
322        match value {
323            Ok(value) => resolved.push(VecMapEntry { key, value }),
324            Err(WalrusFetchError::Unavailable { source }) => {
325                return Err(WalrusFetchError::Unavailable { source });
326            }
327            Err(WalrusFetchError::DigestMismatch {
328                resolved: value,
329                mismatch: current,
330            }) => {
331                resolved.push(VecMapEntry { key, value });
332                mismatch.get_or_insert(current);
333            }
334        }
335    }
336    match mismatch {
337        Some(mismatch) => Err(WalrusFetchError::DigestMismatch { resolved, mismatch }),
338        None => Ok(resolved),
339    }
340}
341
342fn validate_entries<P>(contents: &[VecMapEntry<P, NexusData>]) -> anyhow::Result<()> {
343    anyhow::ensure!(
344        contents.iter().all(|entry| entry.value.is_well_formed()),
345        "cannot process malformed NexusData map",
346    );
347    Ok(())
348}
349
350fn resolved_blob_byte_limit(
351    resolved: &[NexusValue],
352    index: usize,
353    many: bool,
354) -> anyhow::Result<usize> {
355    anyhow::ensure!(
356        index < resolved.len(),
357        "Walrus value index is out of bounds"
358    );
359    let mut lower = 0;
360    let mut upper = MAX_INLINE_DATA_BYTES as usize;
361    while lower < upper {
362        let candidate = lower + (upper - lower).div_ceil(2);
363        let mut values = resolved.to_vec();
364        values[index] = NexusValue::InlineData {
365            bytes: vec![0; candidate],
366        };
367        if NexusData::from_values(values, many).is_ok() {
368            lower = candidate;
369        } else {
370            upper = candidate - 1;
371        }
372    }
373    Ok(lower)
374}
375
376fn walrus_reader(conf: &StorageConf) -> anyhow::Result<WalrusClient> {
377    let aggregator_url = conf
378        .walrus_aggregator_url
379        .as_ref()
380        .ok_or_else(|| anyhow::anyhow!("Walrus aggregator URL is not set in storage config"))?;
381    Ok(WalrusClient::builder()
382        .with_aggregator_url(aggregator_url)
383        .build())
384}
385
386fn walrus_writer(conf: &StorageConf) -> anyhow::Result<(WalrusClient, u8)> {
387    conf.validate_walrus_upload()?;
388    let publisher_url = conf
389        .walrus_publisher_url
390        .as_ref()
391        .ok_or_else(|| anyhow::anyhow!("Walrus publisher URL is not set in storage config"))?;
392    let aggregator_url = conf
393        .walrus_aggregator_url
394        .as_ref()
395        .ok_or_else(|| anyhow::anyhow!("Walrus aggregator URL is not set in storage config"))?;
396    let store_for_epochs = conf
397        .walrus_save_for_epochs
398        .ok_or_else(|| anyhow::anyhow!("Walrus save for epochs is not set in storage config"))?;
399    if store_for_epochs > WALRUS_MAX_EPOCHS {
400        anyhow::bail!("Walrus save for epochs exceeds maximum allowed ({WALRUS_MAX_EPOCHS})");
401    }
402    Ok((
403        WalrusClient::builder()
404            .with_publisher_url(publisher_url)
405            .with_aggregator_url(aggregator_url)
406            .build(),
407        store_for_epochs,
408    ))
409}
410
411fn blob_id_from_bytes(bytes: &[u8]) -> anyhow::Result<String> {
412    canonical_walrus_blob_id(bytes).map(str::to_owned)
413}
414
415fn blob_id_from_storage_info(info: StorageInfo) -> anyhow::Result<String> {
416    info.newly_created
417        .map(|created| created.blob_object.blob_id)
418        .or_else(|| info.already_certified.map(|certified| certified.blob_id))
419        .ok_or_else(|| anyhow::anyhow!("Failed to store data on Walrus: no committed blob info"))
420}
421
422#[cfg(test)]
423mod tests {
424    use {
425        super::*,
426        crate::walrus::{AlreadyCertified, BlobObject, BlobStorage, NewlyCreated, SuiEvent},
427        mockito::{Server, ServerGuard},
428    };
429
430    const BLOB_ID: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
431
432    async fn setup_mock_server_and_conf() -> anyhow::Result<(ServerGuard, StorageConf)> {
433        let server = Server::new_async().await;
434        let server_url = server.url();
435        Ok((
436            server,
437            StorageConf {
438                walrus_publisher_url: Some(server_url.clone()),
439                walrus_aggregator_url: Some(server_url),
440                walrus_save_for_epochs: Some(2),
441            },
442        ))
443    }
444
445    #[tokio::test]
446    async fn typed_inline_commit_and_fetch_are_noops() {
447        let storage_conf = StorageConf::default();
448        let data = NexusData::inline_data(b"payload").unwrap();
449        let fetched = data
450            .clone()
451            .commit(&storage_conf)
452            .await
453            .unwrap()
454            .fetch(&storage_conf)
455            .await
456            .unwrap();
457
458        assert!(matches!(
459            fetched.values().unwrap().as_slice(),
460            [NexusValue::InlineData { bytes }] if bytes == b"payload"
461        ));
462    }
463
464    #[tokio::test]
465    async fn empty_many_is_rejected_before_walrus_configuration() {
466        let value = NexusData::Many { values: Vec::new() };
467
468        assert!(value
469            .clone()
470            .upload_data_fields(&StorageConf::default())
471            .await
472            .unwrap_err()
473            .to_string()
474            .contains("cannot upload malformed NexusData"));
475        assert!(value
476            .clone()
477            .commit(&StorageConf::default())
478            .await
479            .unwrap_err()
480            .to_string()
481            .contains("cannot commit malformed NexusData"));
482        assert!(value
483            .fetch(&StorageConf::default())
484            .await
485            .unwrap_err()
486            .to_string()
487            .contains("cannot resolve malformed NexusData"));
488    }
489
490    #[tokio::test]
491    async fn malformed_map_entry_precedes_all_walrus_requests() {
492        let (mut server, storage_conf) = setup_mock_server_and_conf().await.unwrap();
493        let get = server
494            .mock("GET", format!("/v1/blobs/{BLOB_ID}").as_str())
495            .expect(0)
496            .create_async()
497            .await;
498        let valid = NexusData::walrus_data(BLOB_ID.as_bytes(), vec![0; 32]).unwrap();
499        let malformed = NexusData::Many { values: Vec::new() };
500        let map = || VecMap {
501            contents: vec![
502                VecMapEntry {
503                    key: InputPort::new("valid"),
504                    value: valid.clone(),
505                },
506                VecMapEntry {
507                    key: InputPort::new("malformed"),
508                    value: malformed.clone(),
509                },
510            ],
511        };
512
513        assert!(map()
514            .commit_all(&storage_conf)
515            .await
516            .unwrap_err()
517            .to_string()
518            .contains("cannot process malformed NexusData map"));
519        assert!(map()
520            .resolve_all(&storage_conf)
521            .await
522            .unwrap_err()
523            .to_string()
524            .contains("cannot process malformed NexusData map"));
525        get.assert_async().await;
526    }
527
528    #[tokio::test]
529    async fn typed_upload_reads_back_and_commits_digest() {
530        let (mut server, storage_conf) = setup_mock_server_and_conf().await.unwrap();
531        let response = StorageInfo {
532            newly_created: Some(NewlyCreated {
533                blob_object: BlobObject {
534                    blob_id: BLOB_ID.to_string(),
535                    id: "raw_object_id".to_string(),
536                    storage: BlobStorage { end_epoch: 200 },
537                },
538            }),
539            already_certified: None,
540        };
541        let put = server
542            .mock("PUT", "/v1/blobs?epochs=2")
543            .with_status(200)
544            .with_header("content-type", "application/json")
545            .with_body(serde_json::to_string(&response).unwrap())
546            .create_async()
547            .await;
548        let get = server
549            .mock("GET", format!("/v1/blobs/{BLOB_ID}").as_str())
550            .with_status(200)
551            .with_body("payload")
552            .create_async()
553            .await;
554
555        let committed = NexusData::inline_data(b"payload")
556            .unwrap()
557            .upload_data_fields(&storage_conf)
558            .await
559            .unwrap();
560
561        assert!(matches!(
562            committed.values().unwrap().as_slice(),
563            [NexusValue::WalrusData { blob_id, .. }]
564                if blob_id == BLOB_ID.as_bytes()
565        ));
566        put.assert_async().await;
567        get.assert_async().await;
568    }
569
570    #[tokio::test]
571    async fn typed_fetch_rejects_digest_mismatch() {
572        let (mut server, storage_conf) = setup_mock_server_and_conf().await.unwrap();
573        let get = server
574            .mock("GET", format!("/v1/blobs/{BLOB_ID}").as_str())
575            .with_status(200)
576            .with_body("different")
577            .create_async()
578            .await;
579        let value = NexusData::walrus_data(BLOB_ID.as_bytes(), Sha256::digest(b"payload").to_vec())
580            .unwrap();
581
582        let error = value.fetch(&storage_conf).await.unwrap_err();
583
584        assert!(error
585            .downcast_ref::<WalrusContentDigestMismatch>()
586            .is_some());
587        get.assert_async().await;
588    }
589
590    #[tokio::test]
591    async fn typed_fetch_rejects_content_larger_than_inline_limit() {
592        let (mut server, storage_conf) = setup_mock_server_and_conf().await.unwrap();
593        let bytes = vec![b'x'; 61_441];
594        let get = server
595            .mock("GET", format!("/v1/blobs/{BLOB_ID}").as_str())
596            .with_status(200)
597            .with_body(bytes.clone())
598            .create_async()
599            .await;
600        let value =
601            NexusData::walrus_data(BLOB_ID.as_bytes(), Sha256::digest(&bytes).to_vec()).unwrap();
602
603        assert!(value.fetch(&storage_conf).await.is_err());
604        get.assert_async().await;
605    }
606
607    #[tokio::test]
608    async fn typed_resolve_preserves_fetched_bytes_for_digest_mismatch() {
609        let (mut server, storage_conf) = setup_mock_server_and_conf().await.unwrap();
610        let get = server
611            .mock("GET", format!("/v1/blobs/{BLOB_ID}").as_str())
612            .with_status(200)
613            .with_body("different")
614            .create_async()
615            .await;
616        let value = NexusData::walrus_data(BLOB_ID.as_bytes(), Sha256::digest(b"payload").to_vec())
617            .unwrap();
618
619        let WalrusFetchError::DigestMismatch { resolved, mismatch } =
620            value.resolve(&storage_conf).await.unwrap_err()
621        else {
622            panic!("successful fetch with unequal bytes must be an integrity mismatch");
623        };
624
625        assert_eq!(mismatch.blob_id, BLOB_ID);
626        assert!(matches!(
627            resolved.values().unwrap().as_slice(),
628            [NexusValue::InlineData { bytes }] if bytes == b"different"
629        ));
630        get.assert_async().await;
631    }
632
633    #[tokio::test]
634    async fn typed_resolve_classifies_unavailable_content_separately() {
635        let (mut server, storage_conf) = setup_mock_server_and_conf().await.unwrap();
636        let get = server
637            .mock("GET", format!("/v1/blobs/{BLOB_ID}").as_str())
638            .with_status(404)
639            .create_async()
640            .await;
641        let value = NexusData::walrus_data(BLOB_ID.as_bytes(), Sha256::digest(b"payload").to_vec())
642            .unwrap();
643
644        assert!(matches!(
645            value.resolve(&storage_conf).await.unwrap_err(),
646            WalrusFetchError::Unavailable { .. }
647        ));
648        get.assert_async().await;
649    }
650
651    #[tokio::test]
652    async fn typed_fetch_preserves_many_cardinality_and_order() {
653        let (mut server, storage_conf) = setup_mock_server_and_conf().await.unwrap();
654        let get = server
655            .mock("GET", format!("/v1/blobs/{BLOB_ID}").as_str())
656            .with_status(200)
657            .with_body("remote")
658            .create_async()
659            .await;
660        let value = NexusData::many(vec![
661            NexusValue::inline_data(b"inline").unwrap(),
662            NexusValue::walrus_data(BLOB_ID.as_bytes(), Sha256::digest(b"remote").to_vec())
663                .unwrap(),
664        ])
665        .unwrap();
666
667        let resolved = value.fetch(&storage_conf).await.unwrap();
668
669        assert!(matches!(
670            resolved,
671            NexusData::Many { values }
672                if matches!(
673                    values.as_slice(),
674                    [
675                        NexusValue::InlineData { bytes: first },
676                        NexusValue::InlineData { bytes: second },
677                    ] if first == b"inline" && second == b"remote"
678                )
679        ));
680        get.assert_async().await;
681    }
682
683    #[test]
684    fn resolved_blob_limit_accounts_for_full_many_bcs_size() {
685        let resolved = vec![
686            NexusValue::InlineData {
687                bytes: vec![0; 61_440],
688            },
689            NexusValue::InlineData { bytes: Vec::new() },
690        ];
691
692        let limit = resolved_blob_byte_limit(&resolved, 1, true).unwrap();
693        let mut at_limit = resolved.clone();
694        at_limit[1] = NexusValue::InlineData {
695            bytes: vec![0; limit],
696        };
697        let mut over_limit = resolved;
698        over_limit[1] = NexusValue::InlineData {
699            bytes: vec![0; limit + 1],
700        };
701
702        assert!(limit < MAX_INLINE_DATA_BYTES as usize);
703        assert!(NexusData::from_values(at_limit, true).is_ok());
704        assert!(NexusData::from_values(over_limit, true).is_err());
705    }
706
707    #[test]
708    fn certified_response_returns_existing_blob_id() {
709        let info = StorageInfo {
710            newly_created: None,
711            already_certified: Some(AlreadyCertified {
712                blob_id: "existing".to_string(),
713                event: SuiEvent {
714                    tx_digest: "0x1".to_string(),
715                },
716                end_epoch: 1,
717            }),
718        };
719
720        assert_eq!(blob_id_from_storage_info(info).unwrap(), "existing");
721    }
722}