use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
use serde::{Deserialize, Serialize};
use super::store::{CnftStore, StoreError};
use super::types::{LeafRecord, TreeInfo};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TreeSnapshot {
pub format_version: u32,
pub tree_info: TreeInfo,
pub leaves: Vec<LeafRecord>,
pub last_signature: Option<String>,
}
pub const SNAPSHOT_FORMAT_VERSION: u32 = 1;
pub async fn dump_tree<S: CnftStore + ?Sized>(
store: &S,
tree: &[u8; 32],
) -> Result<Option<TreeSnapshot>, StoreError> {
let Some(tree_info) = store.get_tree(tree).await? else {
return Ok(None);
};
let leaves = store.list_leaves(tree).await?;
let last_signature = store.get_last_signature(tree).await?;
Ok(Some(TreeSnapshot {
format_version: SNAPSHOT_FORMAT_VERSION,
tree_info,
leaves,
last_signature,
}))
}
pub async fn load_tree<S: CnftStore + ?Sized>(
store: &S,
snapshot: TreeSnapshot,
) -> Result<LoadSummary, StoreError> {
if snapshot.format_version > SNAPSHOT_FORMAT_VERSION {
return Err(StoreError::UnknownTree {
tree: format!(
"snapshot format_version {} is newer than supported ({})",
snapshot.format_version, SNAPSHOT_FORMAT_VERSION
),
});
}
let tree = snapshot.tree_info.tree;
let leaf_count = snapshot.leaves.len();
store.put_tree(snapshot.tree_info).await?;
for leaf in snapshot.leaves {
store.put_leaf(leaf).await?;
}
if let Some(sig) = snapshot.last_signature {
store.set_last_signature(&tree, sig).await?;
}
Ok(LoadSummary {
tree,
leaf_count: leaf_count as u64,
})
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LoadSummary {
pub tree: [u8; 32],
pub leaf_count: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum SnapshotKind {
Tree,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotBlob {
pub kind: SnapshotKind,
pub format_version: u32,
pub data: String,
}
impl SnapshotBlob {
#[must_use]
pub fn from_tree(snapshot: &TreeSnapshot) -> Self {
let json = serde_json::to_vec(snapshot).unwrap_or_default();
Self {
kind: SnapshotKind::Tree,
format_version: snapshot.format_version,
data: B64.encode(json),
}
}
pub fn into_tree_snapshot(self) -> Result<TreeSnapshot, String> {
if self.kind != SnapshotKind::Tree {
return Err(format!("expected SnapshotKind::Tree, got {:?}", self.kind));
}
let bytes = B64
.decode(self.data.as_bytes())
.map_err(|e| format!("base64 decode: {e}"))?;
serde_json::from_slice(&bytes).map_err(|e| format!("snapshot deserialize: {e}"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cnft::store::MemoryCnftStore;
use crate::cnft::types::MintMetadata;
use tidepool_core::Creator;
const TREE: [u8; 32] = [0x11; 32];
fn stub_metadata() -> MintMetadata {
MintMetadata {
name: "Asset".into(),
symbol: "A".into(),
uri: "https://example.com/a.json".into(),
seller_fee_basis_points: 500,
primary_sale_happened: false,
is_mutable: true,
creators: vec![Creator {
address: [0x44; 32],
verified: true,
share: 100,
}],
collection: None,
data_hash_input: vec![0xaa; 16],
}
}
fn stub_leaf(index: u64) -> LeafRecord {
let i = u8::try_from(index).unwrap_or(0);
LeafRecord {
asset_id: [i + 1; 32],
tree: TREE,
nonce: index,
leaf_index: index,
mint_metadata: stub_metadata(),
owner: [i + 2; 32],
delegate: [i + 3; 32],
data_hash: [i + 4; 32],
creator_hash: [i + 5; 32],
leaf_hash: [i + 6; 32],
burned: false,
}
}
async fn seed_store() -> MemoryCnftStore {
let s = MemoryCnftStore::new();
s.put_tree(TreeInfo {
tree: TREE,
depth: 20,
max_buffer_size: 64,
num_minted: 3,
})
.await
.unwrap();
for i in 0..3 {
s.put_leaf(stub_leaf(i)).await.unwrap();
}
s.set_last_signature(&TREE, "CURSOR_SIG".into())
.await
.unwrap();
s
}
#[tokio::test]
async fn dump_captures_tree_leaves_and_cursor() {
let s = seed_store().await;
let snap = dump_tree(&s, &TREE).await.unwrap().expect("Some");
assert_eq!(snap.format_version, SNAPSHOT_FORMAT_VERSION);
assert_eq!(snap.tree_info.depth, 20);
assert_eq!(snap.tree_info.num_minted, 3);
assert_eq!(snap.leaves.len(), 3);
assert_eq!(snap.leaves[0].leaf_index, 0);
assert_eq!(snap.leaves[2].leaf_index, 2);
assert_eq!(snap.last_signature.as_deref(), Some("CURSOR_SIG"));
}
#[tokio::test]
async fn dump_unknown_tree_returns_none() {
let s = MemoryCnftStore::new();
assert!(dump_tree(&s, &[0x99; 32]).await.unwrap().is_none());
}
#[tokio::test]
async fn load_applies_snapshot_to_fresh_store() {
let src = seed_store().await;
let snap = dump_tree(&src, &TREE).await.unwrap().unwrap();
let dst = MemoryCnftStore::new();
let summary = load_tree(&dst, snap).await.unwrap();
assert_eq!(summary.leaf_count, 3);
assert_eq!(summary.tree, TREE);
let got_tree = dst.get_tree(&TREE).await.unwrap().expect("present");
assert_eq!(got_tree.num_minted, 3);
let got_leaves = dst.list_leaves(&TREE).await.unwrap();
assert_eq!(got_leaves.len(), 3);
assert_eq!(
dst.get_last_signature(&TREE).await.unwrap().as_deref(),
Some("CURSOR_SIG")
);
}
#[tokio::test]
async fn load_rejects_future_format_version() {
let dst = MemoryCnftStore::new();
let bad = TreeSnapshot {
format_version: SNAPSHOT_FORMAT_VERSION + 1,
tree_info: TreeInfo {
tree: TREE,
depth: 1,
max_buffer_size: 1,
num_minted: 0,
},
leaves: vec![],
last_signature: None,
};
assert!(load_tree(&dst, bad).await.is_err());
}
#[tokio::test]
async fn snapshot_blob_round_trip() {
let src = seed_store().await;
let snap = dump_tree(&src, &TREE).await.unwrap().unwrap();
let blob = SnapshotBlob::from_tree(&snap);
assert_eq!(blob.kind, SnapshotKind::Tree);
let decoded = blob.into_tree_snapshot().expect("decode");
assert_eq!(decoded, snap);
}
}