noosphere_core/data/
bundle.rs

1// We are removing this module, so not gonna bother documenting...
2#![allow(missing_docs)]
3
4use std::{collections::BTreeMap, str::FromStr};
5
6use anyhow::{anyhow, Result};
7use async_trait::async_trait;
8use cid::Cid;
9
10use futures::{pin_mut, StreamExt};
11use libipld_cbor::DagCborCodec;
12use libipld_core::raw::RawCodec;
13use noosphere_storage::{block_deserialize, block_serialize, BlockStore, UcanStore};
14use serde::{de::DeserializeOwned, Deserialize, Serialize};
15use ucan::{store::UcanJwtStore, Ucan};
16
17use crate::{
18    data::{
19        AuthorityIpld, BodyChunkIpld, ChangelogIpld, ContentIpld, ContentType, DelegationIpld,
20        DelegationsIpld, Header, IdentityIpld, MapOperation, MemoIpld, RevocationIpld,
21        RevocationsIpld, VersionedMapIpld, VersionedMapKey, VersionedMapValue,
22    },
23    view::{Sphere, Timeslice},
24};
25
26use super::{AddressBookIpld, IdentitiesIpld, Jwt, Link, LinkRecord};
27
28// TODO: This should maybe only collect CIDs, and then streaming-serialize to
29// a CAR (https://ipld.io/specs/transport/car/carv2/)
30#[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)]
31pub struct Bundle(BTreeMap<String, Vec<u8>>);
32
33impl Bundle {
34    pub fn len(&self) -> usize {
35        self.0.len()
36    }
37
38    pub fn is_empty(&self) -> bool {
39        self.len() == 0
40    }
41
42    pub fn contains(&self, cid: &Cid) -> bool {
43        self.0.contains_key(&cid.to_string())
44    }
45
46    pub async fn load_into<S: BlockStore>(&self, store: &mut S) -> Result<()> {
47        debug!("Loading {} blocks into store...", self.0.len());
48
49        // TODO: Parrallelize this
50        for (cid_string, block_bytes) in self.0.iter() {
51            let cid = Cid::from_str(cid_string)?;
52
53            store.put_block(&cid, block_bytes).await?;
54
55            match cid.codec() {
56                codec_id if codec_id == u64::from(DagCborCodec) => {
57                    store.put_links::<DagCborCodec>(&cid, block_bytes).await?;
58                }
59                codec_id if codec_id == u64::from(RawCodec) => {
60                    store.put_links::<RawCodec>(&cid, block_bytes).await?;
61                }
62                codec_id => warn!("Unrecognized codec {}; skipping...", codec_id),
63            }
64
65            // TODO: Verify CID is correct, maybe?
66        }
67
68        Ok(())
69    }
70
71    pub async fn from_timeslice<'a, S: BlockStore>(
72        timeslice: &Timeslice<'a, S>,
73        store: &S,
74    ) -> Result<Bundle> {
75        let stream = timeslice.stream();
76        let mut bundle = Bundle::default();
77
78        pin_mut!(stream);
79
80        while let Some(ancestor) = stream.next().await {
81            let (_, memo) = ancestor?;
82            memo.extend_bundle(&mut bundle, store).await?;
83        }
84
85        Ok(bundle)
86    }
87
88    pub fn add(&mut self, cid: Cid, bytes: Vec<u8>) -> bool {
89        let cid_string = cid.to_string();
90        match self.0.contains_key(&cid_string) {
91            true => false,
92            false => {
93                self.0.insert(cid_string, bytes);
94                true
95            }
96        }
97    }
98
99    pub fn merge(&mut self, mut other: Bundle) {
100        self.0.append(&mut other.0);
101    }
102
103    pub fn map(&self) -> &BTreeMap<String, Vec<u8>> {
104        &self.0
105    }
106
107    pub async fn extend<CanBundle: TryBundle, S: BlockStore>(
108        &mut self,
109        cid: &Cid,
110        store: &S,
111    ) -> Result<()> {
112        CanBundle::extend_bundle_with_cid(cid, self, store).await?;
113        Ok(())
114    }
115}
116
117#[cfg(not(target_arch = "wasm32"))]
118pub trait TryBundleSendSync: Send + Sync {}
119
120#[cfg(not(target_arch = "wasm32"))]
121impl<T> TryBundleSendSync for T where T: Send + Sync {}
122
123#[cfg(target_arch = "wasm32")]
124pub trait TryBundleSendSync {}
125
126#[cfg(target_arch = "wasm32")]
127impl<T> TryBundleSendSync for T {}
128
129#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
130#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
131pub trait TryBundle: TryBundleSendSync + Serialize + DeserializeOwned {
132    async fn extend_bundle<S: BlockStore>(&self, bundle: &mut Bundle, _store: &S) -> Result<()> {
133        let (self_cid, self_bytes) = block_serialize::<DagCborCodec, _>(self)?;
134        bundle.add(self_cid, self_bytes);
135        Ok(())
136    }
137
138    async fn extend_bundle_with_cid<S: BlockStore>(
139        cid: &Cid,
140        bundle: &mut Bundle,
141        store: &S,
142    ) -> Result<()> {
143        let item = store.load::<DagCborCodec, Self>(cid).await?;
144        item.extend_bundle(bundle, store).await?;
145
146        Ok(())
147    }
148
149    async fn bundle<S: BlockStore>(&self, store: &S) -> Result<Bundle> {
150        let mut bundle = Bundle::default();
151        self.extend_bundle(&mut bundle, store).await?;
152        Ok(bundle)
153    }
154
155    async fn bundle_with_cid<S: BlockStore>(cid: &Cid, store: &S) -> Result<Bundle> {
156        let mut bundle = Bundle::default();
157        Self::extend_bundle_with_cid(cid, &mut bundle, store).await?;
158        Ok(bundle)
159    }
160}
161
162#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
163#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
164impl TryBundle for BodyChunkIpld {
165    async fn extend_bundle_with_cid<S: BlockStore>(
166        cid: &Cid,
167        bundle: &mut Bundle,
168        store: &S,
169    ) -> Result<()> {
170        let mut next_cid = Some(*cid);
171
172        while let Some(cid) = next_cid {
173            trace!(?cid, "Bundling BodyChunkIpld...");
174
175            let bytes = store.require_block(&cid).await?;
176            let chunk = block_deserialize::<DagCborCodec, BodyChunkIpld>(&bytes)?;
177            bundle.add(cid, bytes);
178            next_cid = chunk.next;
179        }
180
181        Ok(())
182    }
183}
184
185#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
186#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
187impl<K, V> TryBundle for ChangelogIpld<MapOperation<K, V>>
188where
189    K: VersionedMapKey,
190    V: VersionedMapValue + TryBundle,
191{
192    async fn extend_bundle_with_cid<S: BlockStore>(
193        cid: &Cid,
194        bundle: &mut Bundle,
195        store: &S,
196    ) -> Result<()> {
197        let bytes = store.require_block(cid).await?;
198        let changelog = block_deserialize::<DagCborCodec, Self>(&bytes)?;
199
200        trace!(?cid, "Bundling ChangeLogIpld...");
201
202        bundle.add(*cid, bytes);
203
204        for op in changelog.changes {
205            if let MapOperation::Add { value, key } = op {
206                trace!("...added entry {key}");
207                value.extend_bundle(bundle, store).await?;
208            }
209        }
210
211        Ok(())
212    }
213}
214
215#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
216#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
217impl TryBundle for MemoIpld {
218    async fn extend_bundle<S: BlockStore>(&self, bundle: &mut Bundle, store: &S) -> Result<()> {
219        let (self_cid, self_bytes) = block_serialize::<DagCborCodec, _>(self)?;
220        trace!(cid = ?self_cid, "Bundling MemoIpld....");
221
222        bundle.add(self_cid, self_bytes);
223
224        match self.get_first_header(&Header::ContentType) {
225            Some(value) => {
226                match ContentType::from_str(&value)? {
227                    ContentType::Subtext
228                    | ContentType::Text
229                    | ContentType::Bytes
230                    | ContentType::Json
231                    | ContentType::Cbor => {
232                        bundle.extend::<BodyChunkIpld, _>(&self.body, store).await?;
233                    }
234                    ContentType::Sphere => {
235                        trace!("Bundling sphere revision {self_cid}...");
236
237                        let sphere = Sphere::at(&Link::from(self_cid), store);
238                        let mutation = sphere.derive_mutation().await?;
239                        let sphere_body = sphere.to_body().await?;
240
241                        let sphere_body_bytes = store.require_block(&self.body).await?;
242                        bundle.add(self.body, sphere_body_bytes);
243
244                        if !mutation.content().changes().is_empty() {
245                            trace!(cid = ?sphere_body.content, "Bundling content...");
246                            ContentIpld::extend_bundle_with_cid(
247                                &sphere_body.content,
248                                bundle,
249                                store,
250                            )
251                            .await?;
252                        }
253
254                        if !mutation.delegations().changes().is_empty()
255                            || !mutation.revocations().changes().is_empty()
256                        {
257                            trace!(cid = ?sphere_body.authority, "Bundling authority...");
258                            AuthorityIpld::extend_bundle_with_cid(
259                                &sphere_body.authority,
260                                bundle,
261                                store,
262                            )
263                            .await?;
264                        }
265
266                        if !mutation.identities().changes().is_empty() {
267                            trace!(cid = ?sphere_body.address_book, "Bundling address book...");
268                            AddressBookIpld::extend_bundle_with_cid(
269                                &sphere_body.address_book,
270                                bundle,
271                                store,
272                            )
273                            .await?;
274                        }
275                    }
276                    ContentType::Unknown(content_type) => {
277                        warn!("Unrecognized content type {:?}; attempting to bundle as body chunks...", content_type);
278                        // Fallback to body chunks....
279                        bundle.extend::<BodyChunkIpld, _>(&self.body, store).await?;
280                    }
281                }
282            }
283            None => {
284                warn!("No content type specified; only bundling a single block");
285                bundle.add(
286                    self.body,
287                    store
288                        .get_block(&self.body)
289                        .await?
290                        .ok_or_else(|| anyhow!("Unable to find block for {}", self.body))?,
291                );
292            }
293        };
294
295        Ok(())
296    }
297
298    async fn extend_bundle_with_cid<S: BlockStore>(
299        cid: &Cid,
300        bundle: &mut Bundle,
301        store: &S,
302    ) -> Result<()> {
303        store
304            .load::<DagCborCodec, MemoIpld>(cid)
305            .await?
306            .extend_bundle(bundle, store)
307            .await?;
308        Ok(())
309    }
310}
311
312#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
313#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
314impl<K, V> TryBundle for VersionedMapIpld<K, V>
315where
316    K: VersionedMapKey,
317    V: VersionedMapValue + TryBundle,
318{
319    async fn extend_bundle<S: BlockStore>(&self, bundle: &mut Bundle, store: &S) -> Result<()> {
320        trace!("Bundling versioned map...");
321        let (self_cid, self_bytes) = block_serialize::<DagCborCodec, _>(self)?;
322
323        ChangelogIpld::<MapOperation<K, V>>::extend_bundle_with_cid(&self.changelog, bundle, store)
324            .await?;
325
326        bundle.add(self_cid, self_bytes);
327
328        Ok(())
329    }
330
331    async fn extend_bundle_with_cid<S: BlockStore>(
332        cid: &Cid,
333        bundle: &mut Bundle,
334        store: &S,
335    ) -> Result<()> {
336        let map: Self = store.load::<DagCborCodec, _>(cid).await?;
337        map.extend_bundle(bundle, store).await?;
338        Ok(())
339    }
340}
341
342#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
343#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
344impl<T> TryBundle for Link<T>
345where
346    T: TryBundle + Clone,
347{
348    async fn extend_bundle<S: BlockStore>(&self, bundle: &mut Bundle, store: &S) -> Result<()> {
349        T::extend_bundle_with_cid(self, bundle, store).await
350    }
351}
352
353#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
354#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
355impl TryBundle for Jwt {
356    async fn extend_bundle_with_cid<S: BlockStore>(
357        cid: &Cid,
358        bundle: &mut Bundle,
359        store: &S,
360    ) -> Result<()> {
361        bundle.add(*cid, store.require_block(cid).await?);
362        Ok(())
363    }
364}
365
366#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
367#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
368impl TryBundle for LinkRecord {
369    async fn extend_bundle<S: BlockStore>(&self, bundle: &mut Bundle, store: &S) -> Result<()> {
370        let cid = self.to_cid(cid::multihash::Code::Blake3_256)?;
371        Self::extend_bundle_with_cid(&cid, bundle, store).await
372    }
373
374    async fn extend_bundle_with_cid<S: BlockStore>(
375        cid: &Cid,
376        bundle: &mut Bundle,
377        store: &S,
378    ) -> Result<()> {
379        trace!("Bundling LinkRecord...");
380
381        let mut remaining = vec![*cid];
382        let ucan_store = UcanStore(store.clone());
383
384        while let Some(cid) = remaining.pop() {
385            trace!("...with proof {cid}");
386
387            let jwt = ucan_store.require_token(&cid).await?;
388            let ucan = Ucan::try_from(jwt.as_str())?;
389
390            bundle.add(cid, store.require_block(&cid).await?);
391
392            if let Some(proofs) = ucan.proofs() {
393                for proof_string in proofs {
394                    remaining.push(Cid::try_from(proof_string.as_str())?)
395                }
396            }
397        }
398        Ok(())
399    }
400}
401
402#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
403#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
404impl TryBundle for IdentityIpld {
405    async fn extend_bundle<S: BlockStore>(&self, bundle: &mut Bundle, store: &S) -> Result<()> {
406        trace!("Bundling IdentityIpld...");
407        let (self_cid, self_bytes) = block_serialize::<DagCborCodec, _>(self)?;
408        bundle.add(self_cid, self_bytes);
409        if let Some(cid) = &self.link_record {
410            LinkRecord::extend_bundle_with_cid(cid, bundle, store).await?;
411        };
412        Ok(())
413    }
414}
415
416#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
417#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
418impl TryBundle for DelegationIpld {
419    async fn extend_bundle<S: BlockStore>(&self, bundle: &mut Bundle, store: &S) -> Result<()> {
420        let (self_cid, self_bytes) = block_serialize::<DagCborCodec, _>(self)?;
421        bundle.add(self_cid, self_bytes);
422        bundle.add(self.jwt, store.require_block(&self.jwt).await?);
423        Ok(())
424    }
425}
426
427#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
428#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
429impl TryBundle for RevocationIpld {}
430
431#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
432#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
433impl TryBundle for AuthorityIpld {
434    async fn extend_bundle_with_cid<S: BlockStore>(
435        cid: &Cid,
436        bundle: &mut Bundle,
437        store: &S,
438    ) -> Result<()> {
439        let self_bytes = store.require_block(cid).await?;
440        let authority_ipld = block_deserialize::<DagCborCodec, AuthorityIpld>(&self_bytes)?;
441
442        DelegationsIpld::extend_bundle_with_cid(&authority_ipld.delegations, bundle, store).await?;
443        RevocationsIpld::extend_bundle_with_cid(&authority_ipld.revocations, bundle, store).await?;
444
445        bundle.add(*cid, self_bytes);
446
447        Ok(())
448    }
449}
450
451#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
452#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
453impl TryBundle for AddressBookIpld {
454    async fn extend_bundle_with_cid<S: BlockStore>(
455        cid: &Cid,
456        bundle: &mut Bundle,
457        store: &S,
458    ) -> Result<()> {
459        let self_bytes = store.require_block(cid).await?;
460        let address_book_ipld = block_deserialize::<DagCborCodec, AddressBookIpld>(&self_bytes)?;
461
462        IdentitiesIpld::extend_bundle_with_cid(&address_book_ipld.identities, bundle, store)
463            .await?;
464
465        bundle.add(*cid, self_bytes);
466
467        Ok(())
468    }
469}
470
471#[cfg(test)]
472mod tests {
473    use crate::{helpers::make_valid_link_record, tracing::initialize_tracing};
474
475    use anyhow::Result;
476    use cid::Cid;
477    use libipld_cbor::DagCborCodec;
478    use libipld_core::{ipld::Ipld, raw::RawCodec};
479    use noosphere_storage::{block_serialize, BlockStore, MemoryStore, UcanStore};
480    use ucan::{builder::UcanBuilder, crypto::KeyMaterial};
481
482    #[cfg(target_arch = "wasm32")]
483    use wasm_bindgen_test::wasm_bindgen_test;
484
485    #[cfg(target_arch = "wasm32")]
486    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
487
488    use crate::{
489        authority::generate_ed25519_key,
490        data::{Bundle, ContentIpld, DelegationIpld, MemoIpld, TryBundle},
491        view::{Sphere, SphereMutation, Timeline},
492    };
493
494    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
495    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
496    async fn it_bundles_an_empty_sphere() {
497        let mut store = MemoryStore::default();
498        let owner_key = generate_ed25519_key();
499        let owner_did = owner_key.get_did().await.unwrap();
500
501        let (sphere, _, _) = Sphere::generate(&owner_did, &mut store).await.unwrap();
502        let bundle = MemoIpld::bundle_with_cid(sphere.cid(), &store)
503            .await
504            .unwrap();
505
506        assert!(bundle.contains(sphere.cid()));
507
508        let memo = sphere.to_memo().await.unwrap();
509
510        assert!(bundle.contains(&memo.body));
511    }
512
513    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
514    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
515    async fn it_bundles_a_delegation_with_its_associated_jwt() -> Result<()> {
516        let store = MemoryStore::default();
517        let key = generate_ed25519_key();
518        let did = key.get_did().await.unwrap();
519
520        let jwt = UcanBuilder::default()
521            .issued_by(&key)
522            .for_audience(&did)
523            .with_lifetime(100)
524            .with_nonce()
525            .build()?
526            .sign()
527            .await?
528            .encode()?;
529
530        let (jwt_cid, _) = block_serialize::<RawCodec, _>(Ipld::Bytes(jwt.as_bytes().to_vec()))?;
531
532        let delegation = DelegationIpld::register("foo", &jwt, &store).await?;
533
534        let (delegation_cid, _) = block_serialize::<DagCborCodec, _>(&delegation)?;
535
536        let bundle = delegation.bundle(&store).await?;
537
538        assert!(bundle.contains(&delegation_cid));
539        assert!(bundle.contains(&jwt_cid));
540
541        Ok(())
542    }
543
544    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
545    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
546    async fn it_bundles_a_link_record_with_its_associated_proofs() -> Result<()> {
547        initialize_tracing(None);
548
549        let store = MemoryStore::default();
550        let (_, link_record, link_record_link) =
551            make_valid_link_record(&mut UcanStore(store.clone())).await?;
552
553        let proof_cid = Cid::try_from(
554            link_record
555                .proofs()
556                .as_ref()
557                .unwrap()
558                .first()
559                .unwrap()
560                .as_str(),
561        )?;
562        let bundle = link_record.bundle(&store).await?;
563
564        assert!(bundle.contains(&link_record_link));
565        assert!(bundle.contains(&proof_cid));
566
567        Ok(())
568    }
569
570    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
571    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
572    async fn it_bundles_a_sphere_with_links() {
573        let mut store = MemoryStore::default();
574        let owner_key = generate_ed25519_key();
575        let owner_did = owner_key.get_did().await.unwrap();
576
577        let (sphere, ucan, _) = Sphere::generate(&owner_did, &mut store).await.unwrap();
578
579        let foo_key = String::from("foo");
580        let foo_memo = MemoIpld::for_body(&mut store, b"foo").await.unwrap();
581        let foo_cid = store.save::<DagCborCodec, _>(&foo_memo).await.unwrap();
582
583        let mut mutation = SphereMutation::new(&owner_did);
584        mutation.content_mut().set(&foo_key, &foo_cid.into());
585
586        let mut revision = sphere.apply_mutation(&mutation).await.unwrap();
587        let new_cid = revision.sign(&owner_key, Some(&ucan)).await.unwrap();
588
589        let bundle = MemoIpld::bundle_with_cid(&new_cid, &store).await.unwrap();
590
591        assert_eq!(bundle.map().keys().len(), 6);
592
593        let sphere = Sphere::at(&new_cid, &store);
594
595        assert!(bundle.contains(sphere.cid()));
596
597        let memo = sphere.to_memo().await.unwrap();
598
599        assert!(bundle.contains(&memo.body));
600
601        let sphere_ipld = sphere.to_body().await.unwrap();
602        let links_cid = sphere_ipld.content;
603
604        assert!(bundle.contains(&links_cid));
605
606        let links_ipld = store
607            .load::<DagCborCodec, ContentIpld>(&links_cid)
608            .await
609            .unwrap();
610
611        assert!(bundle.contains(&links_ipld.changelog));
612        assert!(bundle.contains(&foo_cid));
613    }
614
615    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
616    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
617    async fn it_bundles_memo_body_content() {
618        let mut store = MemoryStore::default();
619
620        let owner_key = generate_ed25519_key();
621        let owner_did = owner_key.get_did().await.unwrap();
622
623        let (sphere, authorization, _) = Sphere::generate(&owner_did, &mut store).await.unwrap();
624
625        let body_cid = store
626            .save::<RawCodec, _>(Ipld::Bytes(b"foobar".to_vec()))
627            .await
628            .unwrap();
629
630        let memo = MemoIpld {
631            parent: None,
632            headers: Vec::new(),
633            body: body_cid,
634        };
635        let memo_cid = store.save::<DagCborCodec, _>(memo).await.unwrap();
636        let key = "foo".to_string();
637
638        let mut mutation = SphereMutation::new(&owner_did);
639
640        mutation.content_mut().set(&key, &memo_cid.into());
641
642        let mut revision = sphere.apply_mutation(&mutation).await.unwrap();
643
644        let sphere_revision = revision
645            .sign(&owner_key, Some(&authorization))
646            .await
647            .unwrap();
648
649        let bundle = MemoIpld::bundle_with_cid(&sphere_revision, &store)
650            .await
651            .unwrap();
652
653        assert!(bundle.contains(&body_cid));
654    }
655
656    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
657    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
658    async fn it_only_bundles_the_revision_delta() {
659        let mut store = MemoryStore::default();
660        let owner_key = generate_ed25519_key();
661        let owner_did = owner_key.get_did().await.unwrap();
662
663        let (sphere, ucan, _) = Sphere::generate(&owner_did, &mut store).await.unwrap();
664
665        let foo_key = String::from("foo");
666        let foo_memo = MemoIpld::for_body(&mut store, b"foo").await.unwrap();
667        let foo_cid = store.save::<DagCborCodec, _>(&foo_memo).await.unwrap();
668        let mut first_mutation = SphereMutation::new(&owner_did);
669        first_mutation.content_mut().set(&foo_key, &foo_cid.into());
670
671        let mut revision = sphere.apply_mutation(&first_mutation).await.unwrap();
672        let new_cid = revision.sign(&owner_key, Some(&ucan)).await.unwrap();
673
674        let sphere = Sphere::at(&new_cid, &store);
675
676        let bar_key = String::from("bar");
677        let bar_memo = MemoIpld::for_body(&mut store, b"bar").await.unwrap();
678        let bar_cid = store.save::<DagCborCodec, _>(&bar_memo).await.unwrap();
679
680        let mut second_mutation = SphereMutation::new(&owner_did);
681        second_mutation.content_mut().set(&bar_key, &bar_cid.into());
682
683        let mut revision = sphere.apply_mutation(&second_mutation).await.unwrap();
684        let new_cid = revision.sign(&owner_key, Some(&ucan)).await.unwrap();
685
686        let bundle = MemoIpld::bundle_with_cid(&new_cid, &store).await.unwrap();
687
688        assert_eq!(bundle.map().keys().len(), 6);
689        assert!(!bundle.contains(&foo_cid));
690        assert!(bundle.contains(&bar_cid));
691    }
692
693    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
694    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
695    async fn it_bundles_all_revisions_in_a_timeslice() {
696        let mut store = MemoryStore::default();
697        let owner_key = generate_ed25519_key();
698        let owner_did = owner_key.get_did().await.unwrap();
699
700        let (sphere, ucan, _) = Sphere::generate(&owner_did, &mut store).await.unwrap();
701
702        let original_cid = *sphere.cid();
703
704        let foo_key = String::from("foo");
705        let foo_memo = MemoIpld::for_body(&mut store, b"foo").await.unwrap();
706        let foo_cid = store.save::<DagCborCodec, _>(&foo_memo).await.unwrap();
707        let mut first_mutation = SphereMutation::new(&owner_did);
708        first_mutation.content_mut().set(&foo_key, &foo_cid.into());
709
710        let mut revision = sphere.apply_mutation(&first_mutation).await.unwrap();
711        let second_cid = revision.sign(&owner_key, Some(&ucan)).await.unwrap();
712
713        let sphere = Sphere::at(&second_cid, &store);
714
715        let bar_key = String::from("bar");
716        let bar_memo = MemoIpld::for_body(&mut store, b"bar").await.unwrap();
717        let bar_cid = store.save::<DagCborCodec, _>(&bar_memo).await.unwrap();
718        let mut second_mutation = SphereMutation::new(&owner_did);
719
720        second_mutation.content_mut().set(&bar_key, &bar_cid.into());
721
722        let mut revision = sphere.apply_mutation(&second_mutation).await.unwrap();
723        let final_cid = revision.sign(&owner_key, Some(&ucan)).await.unwrap();
724
725        let timeline = Timeline::new(&store);
726
727        let bundle = Bundle::from_timeslice(&timeline.slice(&final_cid, Some(&second_cid)), &store)
728            .await
729            .unwrap();
730
731        assert_eq!(bundle.map().keys().len(), 12);
732
733        assert!(bundle.contains(&foo_cid));
734        assert!(bundle.contains(&bar_cid));
735        assert!(bundle.contains(&final_cid));
736        assert!(bundle.contains(&second_cid));
737        assert!(!bundle.contains(&original_cid));
738    }
739}