use crate::backend::{AnchorBackend, AnchorMetadata, AnchorReceipt};
use crate::error::AnchorError;
use async_trait::async_trait;
use chrono::Utc;
use reqwest::header::{HeaderMap, HeaderValue};
use tracing::info;
use uuid::Uuid;
use vex_core::Hash;
#[derive(Debug, Clone)]
pub struct CloudAnchor {
pub endpoint: String,
pub api_key: String,
client: reqwest::Client,
}
impl CloudAnchor {
pub fn new(endpoint: impl Into<String>, api_key: impl Into<String>) -> Self {
Self {
endpoint: endpoint.into(),
api_key: api_key.into(),
client: reqwest::Client::new(),
}
}
pub fn production() -> Result<Self, AnchorError> {
let api_key = std::env::var("PROVN_API_KEY").map_err(|_| {
AnchorError::BackendUnavailable(
"Missing PROVN_API_KEY environment variable".to_string(),
)
})?;
Ok(Self::new(
"https://provncloud-production-2d26.up.railway.app/v1/ingest/capsule",
api_key,
))
}
}
#[async_trait]
impl AnchorBackend for CloudAnchor {
async fn anchor(
&self,
root: &Hash,
metadata: AnchorMetadata,
) -> Result<AnchorReceipt, AnchorError> {
let mut headers = HeaderMap::new();
headers.insert(
"x-api-key",
HeaderValue::from_str(&self.api_key).map_err(|_| {
AnchorError::BackendUnavailable("Invalid API key format".to_string())
})?,
);
headers.insert(
"Content-Type",
HeaderValue::from_static("application/octet-stream"),
);
info!("Sending VEP capsule to ProvnCloud lane: {}", root.to_hex());
let response = self
.client
.post(&self.endpoint)
.headers(headers)
.body(root.0.to_vec()) .send()
.await
.map_err(|e| AnchorError::Network(format!("Cloud ingestion failed: {}", e)))?;
let status = response.status();
if !status.is_success() {
let err_text = response.text().await.unwrap_or_default();
return Err(AnchorError::WriteFailed(format!(
"Cloud rejected capsule ({}): {}",
status, err_text
)));
}
Ok(AnchorReceipt {
backend: "ProvnCloud".to_string(),
root_hash: root.to_hex(),
anchor_id: format!("cloud-link-{}", &Uuid::new_v4().to_string()[..8]),
anchored_at: Utc::now(),
proof: Some("settlement-expressway".to_string()),
metadata,
})
}
async fn verify(&self, receipt: &AnchorReceipt) -> Result<bool, AnchorError> {
Ok(receipt.backend == "ProvnCloud")
}
fn name(&self) -> &str {
"ProvnCloud"
}
async fn is_healthy(&self) -> bool {
!self.api_key.is_empty()
}
}
impl CloudAnchor {
pub async fn submit_vep(&self, vep_data: Vec<u8>) -> Result<AnchorReceipt, AnchorError> {
let mut headers = HeaderMap::new();
headers.insert(
"x-api-key",
HeaderValue::from_str(&self.api_key).map_err(|_| {
AnchorError::BackendUnavailable("Invalid API key format".to_string())
})?,
);
headers.insert(
"Content-Type",
HeaderValue::from_static("application/octet-stream"),
);
info!(
"Transmitting forensic capsule to ProvnCloud Lane ({} bytes)",
vep_data.len()
);
let response = self
.client
.post(&self.endpoint)
.headers(headers)
.body(vep_data)
.send()
.await
.map_err(|e| AnchorError::Network(format!("Expressway ingestion failed: {}", e)))?;
let status = response.status();
if !status.is_success() {
let err_text = response.text().await.unwrap_or_default();
return Err(AnchorError::WriteFailed(format!(
"Cloud Capsule Lane rejected bundle ({}): {}",
status, err_text
)));
}
Ok(AnchorReceipt {
backend: "ProvnCloud".to_string(),
root_hash: "signed-binary-bundle".to_string(), anchor_id: format!("cloud-tx-{}", &Uuid::new_v4().to_string()[..8]),
anchored_at: Utc::now(),
proof: Some("settlement-expressway".to_string()),
metadata: AnchorMetadata::new("forensic-test", 1),
})
}
}