use crate::storage::ipfs::{IPFSStorage, IPFSConfig};
use crate::{AnyaError, AnyaResult};
use bitcoin::{Network, Txid};
use lru::LruCache;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::num::NonZeroUsize;
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub type ContentId = String;
#[derive(Debug)]
pub struct DecentralizedStorage {
ipfs_storage: Arc<IPFSStorage>,
web5_adapter: Arc<Web5Adapter>,
bitcoin_client: Option<Arc<BitcoinAnchorService>>,
cache: Arc<Mutex<DecentralizedStorageCache>>,
user_did: String,
cache_config: CacheConfig,
metrics: Arc<Mutex<StorageMetrics>>,
}
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub hot_cache_ttl_secs: u64,
pub hot_cache_size: usize,
pub query_cache_ttl_secs: u64,
pub query_cache_size: usize,
pub metadata_cache_ttl_secs: u64,
pub metadata_cache_size: usize,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
hot_cache_ttl_secs: 3600, hot_cache_size: 1000, query_cache_ttl_secs: 300, query_cache_size: 500, metadata_cache_ttl_secs: 900, metadata_cache_size: 250, }
}
}
#[derive(Debug, Default)]
pub struct StorageMetrics {
pub cache_hits: usize,
pub cache_misses: usize,
pub anchor_attempts: usize,
pub anchor_failures: usize,
}
#[derive(Debug)]
pub struct DecentralizedStorageCache {
hot_cache: LruCache<ContentId, CachedData>,
query_cache: LruCache<String, CachedQuery>,
metadata_cache: LruCache<String, CachedMetadata>,
}
#[derive(Debug, Clone)]
struct CachedData {
data: Vec<u8>,
timestamp: SystemTime,
ttl: Duration,
}
#[derive(Debug, Clone)]
struct CachedQuery {
results: Vec<DWNRecord>,
timestamp: SystemTime,
ttl: Duration,
}
#[derive(Debug, Clone)]
struct CachedMetadata {
metadata: serde_json::Value,
timestamp: SystemTime,
ttl: Duration,
}
#[derive(Debug)]
pub struct BitcoinAnchorService {
network: Network,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RGBAsset {
pub id: String,
pub name: String,
pub description: Option<String>,
pub total_supply: u64,
pub precision: u8,
pub metadata: HashMap<String, String>,
pub contract_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RGBInvoice {
pub id: String,
pub asset_id: String,
pub amount: u64,
pub recipient_did: String,
pub created_at: u64,
pub expires_at: u64,
pub memo: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TransferStatus {
Pending,
Confirmed,
Failed,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssetTransfer {
pub id: String,
pub asset_id: String,
pub from_did: String,
pub to_did: String,
pub amount: u64,
pub status: TransferStatus,
pub created_at: u64,
pub confirmed_at: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BalanceRecord {
pub asset_id: String,
pub amount: u64,
pub last_updated: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransferRecord {
pub transfer: AssetTransfer,
pub status: TransferStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssetHistoryEntry {
pub asset_id: String,
pub event_type: String,
pub data: serde_json::Value,
pub timestamp: u64,
pub actor_did: String,
pub bitcoin_proof: Option<BitcoinProof>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BitcoinProof {
pub txid: String,
pub block_height: Option<u64>,
pub merkle_proof: Option<Vec<String>>,
pub confirmation_count: u32,
}
impl DecentralizedStorage {
pub async fn new(
ipfs_endpoint: &str,
web5_adapter: Arc<Web5Adapter>,
user_did: String,
network: Network,
cache_config: Option<CacheConfig>,
) -> AnyaResult<Self> {
let ipfs_config = IPFSConfig {
endpoint: ipfs_endpoint.to_string(),
enable_pinning: true,
max_file_size: 100 * 1024 * 1024, timeout: 30,
encrypt_sensitive: true,
};
let ipfs_storage = Arc::new(IPFSStorage::new(ipfs_config).await?);
let bitcoin_client = Some(Arc::new(BitcoinAnchorService::new(network)));
let cache_config = cache_config.unwrap_or_default();
let cache = Arc::new(Mutex::new(DecentralizedStorageCache::new(&cache_config)));
let metrics = Arc::new(Mutex::new(StorageMetrics::default()));
Ok(Self {
ipfs_storage,
web5_adapter,
bitcoin_client,
cache,
user_did,
cache_config,
metrics,
})
}
pub async fn asset_exists(&self, asset_id: &str) -> AnyaResult<bool> {
if let Ok(cache) = self.cache.lock() {
let cache_key = format!("asset_exists:{}", asset_id);
if let Some(cached) = cache.metadata_cache.peek(&cache_key) {
if cached.timestamp.elapsed().unwrap_or(Duration::MAX) < cached.ttl {
if let Ok(mut metrics) = self.metrics.lock() {
metrics.cache_hits += 1;
}
return Ok(cached.metadata.get("exists").unwrap_or(&serde_json::Value::Bool(false)).as_bool().unwrap_or(false));
}
}
}
if let Ok(mut metrics) = self.metrics.lock() {
metrics.cache_misses += 1;
}
let asset_records = self.web5_adapter.query_records("*", "anya/rgb/asset")
.map_err(|e| AnyaError::Storage(format!("DWN query error: {}", e)))?;
let exists = asset_records.iter().any(|record| {
record.metadata.get("asset_id") == Some(&asset_id.to_string())
});
if let Ok(mut cache) = self.cache.lock() {
let cache_key = format!("asset_exists:{}", asset_id);
let cached_metadata = CachedMetadata {
metadata: serde_json::json!({"exists": exists}),
timestamp: SystemTime::now(),
ttl: Duration::from_secs(900), };
cache.metadata_cache.put(cache_key, cached_metadata);
}
Ok(exists)
}
pub async fn store_asset(&self, asset: &RGBAsset) -> AnyaResult<String> {
let asset_data = serde_json::to_vec(asset)
.map_err(|e| AnyaError::Serialization(e.to_string()))?;
let ipfs_metadata = self.ipfs_storage
.store_content(&asset_data, Some(&format!("asset_{}.json", asset.id)))
.await?;
let content_id = ipfs_metadata.content_id.clone();
let index_record = DWNRecord {
id: format!("asset_index_{}", asset.id),
owner: self.user_did.clone(),
schema: "anya/rgb/asset".to_string(),
data: serde_json::json!({
"asset_id": asset.id,
"ipfs_hash": content_id,
"name": asset.name,
"total_supply": asset.total_supply,
"precision": asset.precision,
"metadata": ipfs_metadata
}),
metadata: {
let mut meta = HashMap::new();
meta.insert("asset_id".to_string(), asset.id.clone());
meta.insert("ipfs_hash".to_string(), content_id.clone());
meta.insert("created_at".to_string(), current_timestamp().to_string());
meta
},
attestations: Vec::new(),
};
let _index_id = self.web5_adapter.store_record(index_record)
.map_err(|e| AnyaError::Storage(format!("DWN index error: {}", e)))?;
if asset.total_supply > 1_000_000 { if let Some(anchor_service) = &self.bitcoin_client {
let asset_hash = self.compute_asset_hash(asset)?;
if let Ok(mut metrics) = self.metrics.lock() {
metrics.anchor_attempts += 1;
}
let anchor_result = anchor_service.anchor_data_hash(&asset_hash).await;
if anchor_result.is_err() {
if let Ok(mut metrics) = self.metrics.lock() {
metrics.anchor_failures += 1;
}
}
}
}
self.update_hot_cache(&content_id, &asset_data)?;
Ok(content_id)
}
pub async fn query_assets(&self, owner_did: &str) -> AnyaResult<Vec<RGBAsset>> {
let cache_key = format!("assets_query:{}", owner_did);
if let Ok(cache) = self.cache.lock() {
if let Some(cached) = cache.query_cache.peek(&cache_key) {
if cached.timestamp.elapsed().unwrap_or(Duration::MAX) < cached.ttl {
return self.records_to_assets(&cached.results).await;
}
}
}
let asset_records = self.web5_adapter.query_records(owner_did, "anya/rgb/asset")
.map_err(|e| AnyaError::Storage(format!("DWN query error: {}", e)))?;
if let Ok(mut cache) = self.cache.lock() {
let cached_query = CachedQuery {
results: asset_records.clone(),
timestamp: SystemTime::now(),
ttl: Duration::from_secs(300), };
cache.query_cache.put(cache_key, cached_query);
}
self.records_to_assets(&asset_records).await
}
pub async fn get_asset_balance(&self, asset_id: &str) -> AnyaResult<u64> {
let balance_records = self.web5_adapter.query_records(&self.user_did, "anya/rgb/balance")
.map_err(|e| AnyaError::Storage(format!("DWN balance query error: {}", e)))?;
for record in balance_records {
if let Some(record_asset_id) = record.metadata.get("asset_id") {
if record_asset_id == asset_id {
if let Some(amount_str) = record.metadata.get("amount") {
if let Ok(amount) = amount_str.parse::<u64>() {
return Ok(amount);
}
}
}
}
}
Ok(0) }
pub async fn store_invoice(&self, invoice: &RGBInvoice) -> AnyaResult<String> {
let invoice_data = serde_json::to_vec(invoice)
.map_err(|e| AnyaError::Serialization(e.to_string()))?;
let ipfs_metadata = self.ipfs_storage
.store_content(&invoice_data, Some(&format!("invoice_{}.json", invoice.id)))
.await?;
let content_id = ipfs_metadata.content_id.clone();
let invoice_record = DWNRecord {
id: format!("invoice_{}", invoice.id),
owner: self.user_did.clone(),
schema: "anya/rgb/invoice".to_string(),
data: serde_json::to_value(invoice)
.map_err(|e| AnyaError::Serialization(e.to_string()))?,
metadata: {
let mut meta = HashMap::new();
meta.insert("asset_id".to_string(), invoice.asset_id.clone());
meta.insert("amount".to_string(), invoice.amount.to_string());
meta.insert("created_at".to_string(), invoice.created_at.to_string());
meta.insert("ipfs_hash".to_string(), content_id.clone());
meta
},
attestations: Vec::new(),
};
self.web5_adapter.store_record(invoice_record)
.map_err(|e| AnyaError::Storage(format!("DWN invoice index error: {}", e)))
}
pub async fn store_transfer_and_update_balance(
&self,
transfer: &AssetTransfer,
) -> AnyaResult<String> {
let transfer_data = serde_json::to_vec(transfer)
.map_err(|e| AnyaError::Serialization(e.to_string()))?;
let ipfs_metadata = self.ipfs_storage
.store_content(&transfer_data, Some(&format!("transfer_{}.json", transfer.id)))
.await?;
let transfer_record = DWNRecord {
id: format!("transfer_{}", transfer.id),
owner: transfer.from_did.clone(),
schema: "anya/rgb/transfer".to_string(),
data: serde_json::to_value(transfer)
.map_err(|e| AnyaError::Serialization(e.to_string()))?,
metadata: {
let mut meta = HashMap::new();
meta.insert("asset_id".to_string(), transfer.asset_id.clone());
meta.insert("from_did".to_string(), transfer.from_did.clone());
meta.insert("to_did".to_string(), transfer.to_did.clone());
meta.insert("amount".to_string(), transfer.amount.to_string());
meta.insert("ipfs_hash".to_string(), ipfs_metadata.content_id.clone());
meta
},
attestations: Vec::new(),
};
self.web5_adapter.store_record(transfer_record)
.map_err(|e| AnyaError::Storage(format!("DWN transfer index error: {}", e)))?;
self.update_balance(&transfer.from_did, &transfer.asset_id, -(transfer.amount as i64)).await?;
self.update_balance(&transfer.to_did, &transfer.asset_id, transfer.amount as i64).await?;
self.update_hot_cache(&ipfs_metadata.content_id, &transfer_data)?;
Ok(ipfs_metadata.content_id)
}
pub async fn get_transfer_status(&self, transfer_id: &str) -> AnyaResult<TransferStatus> {
let transfer_records = self.web5_adapter.query_records("*", "anya/rgb/transfer")
.map_err(|e| AnyaError::Storage(format!("DWN transfer query error: {}", e)))?;
for record in transfer_records {
if record.id == format!("transfer_{}", transfer_id) {
if let Some(status_str) = record.metadata.get("status") {
return match status_str.as_str() {
"Pending" => Ok(TransferStatus::Pending),
"Confirmed" => Ok(TransferStatus::Confirmed),
"Failed" => Ok(TransferStatus::Failed),
"Cancelled" => Ok(TransferStatus::Cancelled),
_ => Ok(TransferStatus::Pending),
};
}
}
}
Ok(TransferStatus::Pending) }
pub async fn validate_transfer_with_anchoring(&self, transfer: &AssetTransfer) -> AnyaResult<bool> {
let sender_balance = self.get_asset_balance(&transfer.from_did).await?;
if sender_balance < transfer.amount {
return Ok(false);
}
if !self.asset_exists(&transfer.asset_id).await? {
return Ok(false);
}
if transfer.amount > 100_000 { if let Some(anchor_service) = &self.bitcoin_client {
if let Ok(mut metrics) = self.metrics.lock() {
metrics.anchor_attempts += 1;
}
let transfer_hash = self.compute_transfer_hash(transfer)?;
let anchor_result = anchor_service.anchor_data_hash(&transfer_hash).await;
if let Ok(anchor_txid) = anchor_result {
let anchor_record = DWNRecord {
id: format!("anchor_{}", transfer.id),
owner: self.user_did.clone(),
schema: "anya/bitcoin/anchor".to_string(),
data: serde_json::json!({
"transfer_id": transfer.id,
"anchor_txid": anchor_txid,
"timestamp": current_timestamp()
}),
metadata: HashMap::new(),
attestations: Vec::new(),
};
self.web5_adapter.store_record(anchor_record)
.map_err(|e| AnyaError::Storage(format!("Anchor record error: {}", e)))?;
} else {
if let Ok(mut metrics) = self.metrics.lock() {
metrics.anchor_failures += 1;
}
}
}
}
Ok(true)
}
pub async fn get_asset_history_with_proofs(&self, asset_id: &str) -> AnyaResult<Vec<AssetHistoryEntry>> {
let history_records = self.web5_adapter.query_records("*", "anya/rgb/history")
.map_err(|e| AnyaError::Storage(format!("DWN history query error: {}", e)))?;
let mut history_entries = Vec::new();
for record in history_records {
if let Some(record_asset_id) = record.metadata.get("asset_id") {
if record_asset_id == asset_id {
if let Ok(entry) = serde_json::from_value::<AssetHistoryEntry>(record.data.clone()) {
history_entries.push(entry);
}
}
}
}
history_entries.sort_by_key(|entry| entry.timestamp);
Ok(history_entries)
}
pub async fn get_asset_metadata(&self, asset_id: &str) -> AnyaResult<serde_json::Value> {
let asset_records = self.web5_adapter.query_records("*", "anya/rgb/asset")
.map_err(|e| AnyaError::Storage(format!("DWN asset query error: {}", e)))?;
for record in asset_records {
if let Some(record_asset_id) = record.metadata.get("asset_id") {
if record_asset_id == asset_id {
return Ok(record.data);
}
}
}
Err(AnyaError::Storage(format!("Asset metadata not found: {}", asset_id)))
}
async fn records_to_assets(&self, records: &[DWNRecord]) -> AnyaResult<Vec<RGBAsset>> {
let mut assets = Vec::new();
for record in records {
if let Some(ipfs_hash) = record.metadata.get("ipfs_hash") {
if let Ok(cache) = self.cache.lock() {
if let Some(cached) = cache.hot_cache.peek(ipfs_hash) {
if cached.timestamp.elapsed().unwrap_or(Duration::MAX) < cached.ttl {
let asset: RGBAsset = serde_json::from_slice(&cached.data)
.map_err(|e| AnyaError::Serialization(e.to_string()))?;
assets.push(asset);
continue;
}
}
}
let asset_data = self.ipfs_storage
.retrieve_content(ipfs_hash)
.await?;
let asset: RGBAsset = serde_json::from_slice(&asset_data)
.map_err(|e| AnyaError::Serialization(e.to_string()))?;
self.update_hot_cache(ipfs_hash, &asset_data)?;
assets.push(asset);
}
}
Ok(assets)
}
async fn update_balance(&self, did: &str, asset_id: &str, delta: i64) -> AnyaResult<()> {
let current_balance = if did == &self.user_did {
self.get_asset_balance(asset_id).await?
} else {
0
};
let new_balance = if delta < 0 {
current_balance.saturating_sub((-delta) as u64)
} else {
current_balance.saturating_add(delta as u64)
};
let balance_record = DWNRecord {
id: format!("balance_{}_{}", did, asset_id),
owner: did.to_string(),
schema: "anya/rgb/balance".to_string(),
data: serde_json::to_value(BalanceRecord {
asset_id: asset_id.to_string(),
amount: new_balance,
last_updated: current_timestamp(),
}).map_err(|e| AnyaError::Serialization(e.to_string()))?,
metadata: {
let mut meta = HashMap::new();
meta.insert("asset_id".to_string(), asset_id.to_string());
meta.insert("amount".to_string(), new_balance.to_string());
meta
},
attestations: Vec::new(),
};
self.web5_adapter.store_record(balance_record)
.map_err(|e| AnyaError::Storage(format!("Balance update error: {}", e)))?;
Ok(())
}
fn update_hot_cache(&self, content_id: &str, data: &[u8]) -> AnyaResult<()> {
if let Ok(mut cache) = self.cache.lock() {
let cached_data = CachedData {
data: data.to_vec(),
timestamp: SystemTime::now(),
ttl: Duration::from_secs(3600), };
cache.hot_cache.put(content_id.to_string(), cached_data);
}
Ok(())
}
fn compute_asset_hash(&self, asset: &RGBAsset) -> AnyaResult<Vec<u8>> {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
asset.id.hash(&mut hasher);
asset.total_supply.hash(&mut hasher);
Ok(hasher.finish().to_be_bytes().to_vec())
}
fn compute_transfer_hash(&self, transfer: &AssetTransfer) -> AnyaResult<Vec<u8>> {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
transfer.id.hash(&mut hasher);
transfer.asset_id.hash(&mut hasher);
transfer.amount.hash(&mut hasher);
Ok(hasher.finish().to_be_bytes().to_vec())
}
pub async fn get_storage_stats(&self) -> AnyaResult<serde_json::Value> {
let ipfs_stats = self.ipfs_storage.get_statistics().await?;
Ok(serde_json::json!({
"ipfs_stats": ipfs_stats,
"cache_size": {
"hot_cache": self.cache.lock().map(|c| c.hot_cache.len()).unwrap_or(0),
"query_cache": self.cache.lock().map(|c| c.query_cache.len()).unwrap_or(0),
"metadata_cache": self.cache.lock().map(|c| c.metadata_cache.len()).unwrap_or(0),
}
}))
}
pub async fn perform_gc(&self) -> AnyaResult<serde_json::Value> {
let ipfs_gc_result = self.ipfs_storage.perform_gc().await?;
if let Ok(mut cache) = self.cache.lock() {
let mut expired_keys = Vec::new();
for (key, cached_data) in cache.hot_cache.iter() {
if cached_data.timestamp.elapsed().unwrap_or(Duration::ZERO) > cached_data.ttl {
expired_keys.push(key.clone());
}
}
for key in &expired_keys {
cache.hot_cache.pop(key);
}
}
Ok(serde_json::json!({
"ipfs_gc": ipfs_gc_result,
"cache_cleanup": "completed"
}))
}
}
impl DecentralizedStorageCache {
fn new(config: &CacheConfig) -> Self {
Self {
hot_cache: LruCache::new(NonZeroUsize::new(config.hot_cache_size).unwrap()),
query_cache: LruCache::new(NonZeroUsize::new(config.query_cache_size).unwrap()),
metadata_cache: LruCache::new(NonZeroUsize::new(config.metadata_cache_size).unwrap()),
}
}
}
impl BitcoinAnchorService {
fn new(network: Network) -> Self {
Self { network }
}
async fn anchor_data_hash(&self, _hash: &[u8]) -> AnyaResult<String> {
Ok("mock_anchor_txid".to_string())
}
async fn get_transaction_proof(&self, txid: &str) -> AnyaResult<BitcoinProof> {
Ok(BitcoinProof {
txid: txid.to_string(),
block_height: Some(800000),
merkle_proof: None,
confirmation_count: 6,
})
}
}
fn current_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
use futures::TryStreamExt;
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_decentralized_storage_creation() {
let storage = DecentralizedStorage::new(
"http://127.0.0.1:5001",
"did:example:123".to_string(),
Network::Testnet,
).await;
assert!(storage.is_ok());
}
#[tokio::test]
async fn test_asset_storage_and_retrieval() {
let storage = DecentralizedStorage::new(
"http://127.0.0.1:5001",
"did:example:123".to_string(),
Network::Testnet,
).await.unwrap();
let asset = RGBAsset {
id: "test_asset".to_string(),
name: "Test Asset".to_string(),
description: Some("A test asset".to_string()),
total_supply: 1000000,
precision: 8,
metadata: HashMap::new(),
contract_id: "test_contract".to_string(),
};
let content_id = storage.store_asset(&asset).await.unwrap();
assert!(!content_id.is_empty());
let exists = storage.asset_exists(&asset.id).await.unwrap();
assert!(exists);
}
}