webylib 0.3.6

Webcash HD wallet library — bearer e-cash with BIP32-style key derivation, SQLite storage, AES-256-GCM encryption, and full C FFI for cross-platform SDKs
Documentation
//! Wallet snapshot export/import for backup and recovery.

use std::collections::HashMap;
use serde::{Deserialize, Serialize};

use super::Wallet;
use crate::error::Result;

/// Complete wallet state snapshot for backup/restore.
#[derive(Serialize, Deserialize, Debug)]
pub struct WalletSnapshot {
    pub master_secret: String,
    pub unspent_outputs: Vec<UnspentOutputSnapshot>,
    pub spent_hashes: Vec<SpentHashSnapshot>,
    pub depths: HashMap<String, i64>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct UnspentOutputSnapshot {
    pub secret: String,
    pub amount: i64,
    pub created_at: String,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct SpentHashSnapshot {
    pub hash: String,
    pub spent_at: String,
}

/// Internal export format for encryption/backup (native only).
#[cfg(not(target_arch = "wasm32"))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct WalletExport {
    pub version: String,
    pub metadata: HashMap<String, String>,
    pub outputs: Vec<(String, i64, String, i32)>,
    pub spent_hashes: Vec<(Vec<u8>, String)>,
    pub exported_at: String,
}

impl Wallet {
    pub fn export_snapshot(&self) -> Result<WalletSnapshot> {
        let master_secret = self.store.get_meta("master_secret")?.unwrap_or_default();
        let unspent = self.store.get_unspent_full()?
            .into_iter()
            .map(|(secret, amount, created_at)| UnspentOutputSnapshot { secret, amount, created_at })
            .collect();

        let spent = self.store.get_spent_hashes_with_time()?
            .into_iter()
            .map(|(hash_blob, spent_at)| SpentHashSnapshot {
                hash: hex::encode(hash_blob),
                spent_at,
            })
            .collect();

        let depths = self.store.get_all_depths()?
            .into_iter()
            .map(|(k, v)| (k, v as i64))
            .collect();

        Ok(WalletSnapshot { master_secret, unspent_outputs: unspent, spent_hashes: spent, depths })
    }

    pub fn import_snapshot(&self, snapshot: &WalletSnapshot) -> Result<()> {
        self.store.atomic(&mut |store| {
            store.clear_all()?;
            store.set_meta("master_secret", &snapshot.master_secret)?;

            for (code, depth) in &snapshot.depths {
                store.set_depth(code, *depth as u64)?;
            }

            for item in &snapshot.unspent_outputs {
                let secret_hash = crate::crypto::sha256(item.secret.as_bytes());
                store.insert_output(&secret_hash, &item.secret, item.amount)?;
            }

            for item in &snapshot.spent_hashes {
                let hash_bytes = hex::decode(&item.hash)
                    .map_err(|_| crate::error::Error::wallet("Invalid hex in snapshot"))?;
                store.insert_spent_hash(&hash_bytes)?;
            }

            Ok(())
        })
    }

    #[cfg(not(target_arch = "wasm32"))]
    pub(crate) async fn export_wallet_data(&self) -> Result<Vec<u8>> {
        let metadata = self.store.get_all_meta()?;
        let outputs = self.store.get_all_outputs()?;
        let spent_hashes = self.store.get_spent_hashes_with_time()?;

        let wallet_export = WalletExport {
            version: "1.0".to_string(),
            metadata,
            outputs,
            spent_hashes,
            exported_at: chrono::Utc::now().to_rfc3339(),
        };

        serde_json::to_vec(&wallet_export)
            .map_err(|e| crate::error::Error::wallet(format!("Failed to serialize wallet data: {}", e)))
    }

    #[cfg(not(target_arch = "wasm32"))]
    pub(crate) async fn import_wallet_data(&self, data: &[u8]) -> Result<()> {
        let wallet_export: WalletExport = serde_json::from_slice(data)
            .map_err(|e| crate::error::Error::wallet(format!("Failed to deserialize wallet data: {}", e)))?;

        self.store.clear_all()?;

        for (key, value) in wallet_export.metadata {
            self.store.set_meta(&key, &value)?;
        }

        for (secret, amount, _created_at, _spent) in wallet_export.outputs {
            let secret_hash = crate::crypto::sha256(secret.as_bytes());
            self.store.insert_output(&secret_hash, &secret, amount)?;
        }

        for (hash, _spent_at) in wallet_export.spent_hashes {
            self.store.insert_spent_hash(&hash)?;
        }

        log::info!("Wallet data imported from encrypted backup");
        Ok(())
    }
}