use crate::reputation_engine::{
AttestationMetadata, AttestationRecord, CaptureLayer, ReputationEngine,
};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tracing::{debug, error, info, warn};
#[derive(Debug, Clone)]
pub struct SupabaseConfig {
pub url: String,
pub service_role_key: String,
}
impl SupabaseConfig {
pub fn from_env() -> Result<Self, String> {
let url = std::env::var("SUPABASE_URL")
.map_err(|_| "Missing SUPABASE_URL environment variable")?;
let service_role_key = std::env::var("SUPABASE_SERVICE_ROLE_KEY")
.map_err(|_| "Missing SUPABASE_SERVICE_ROLE_KEY environment variable")?;
Ok(Self {
url,
service_role_key,
})
}
}
#[derive(Debug, Deserialize)]
pub struct SupabaseAttestation {
pub id: String,
pub user_id: String,
pub layer_id: Option<i32>,
pub layer_group: Option<String>,
#[serde(rename = "type")]
pub attestation_type: String,
pub weight: i32,
pub is_positive: bool,
pub metadata_json: Option<serde_json::Value>,
pub verified: bool,
pub source: String,
pub source_tx: Option<String>,
pub created_at: String,
pub expires_at: Option<String>,
pub deleted_at: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct SupabaseReputationScore {
pub composite: i32,
pub reliability: i32,
pub skill: i32,
pub social: i32,
pub tenure: i32,
pub infrastructure: i32,
pub tier: String,
}
pub struct SupabaseClient {
config: SupabaseConfig,
http: Client,
}
impl SupabaseClient {
pub fn new(config: SupabaseConfig) -> Self {
Self {
config,
http: Client::new(),
}
}
pub fn from_env() -> Result<Self, String> {
let config = SupabaseConfig::from_env()?;
Ok(Self::new(config))
}
pub async fn fetch_attestations(
&self,
user_id: &str,
) -> Result<Vec<SupabaseAttestation>, String> {
let url = format!(
"{}/rest/v1/attestations?user_id=eq.{}&deleted_at=is.null&order=created_at.desc",
self.config.url, user_id
);
let response = self
.http
.get(&url)
.header("apikey", &self.config.service_role_key)
.header("Authorization", format!("Bearer {}", self.config.service_role_key))
.header("Content-Type", "application/json")
.send()
.await
.map_err(|e| format!("HTTP request failed: {}", e))?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(format!("Supabase error {}: {}", status, body));
}
let attestations: Vec<SupabaseAttestation> = response
.json()
.await
.map_err(|e| format!("JSON parse error: {}", e))?;
debug!("Fetched {} attestations for user {}", attestations.len(), user_id);
Ok(attestations)
}
pub async fn fetch_reputation_score(
&self,
user_id: &str,
) -> Result<Option<SupabaseReputationScore>, String> {
let url = format!(
"{}/rest/v1/rpc/calculate_reputation_score",
self.config.url
);
let body = serde_json::json!({
"p_user_id": user_id
});
let response = self
.http
.post(&url)
.header("apikey", &self.config.service_role_key)
.header("Authorization", format!("Bearer {}", self.config.service_role_key))
.header("Content-Type", "application/json")
.json(&body)
.send()
.await
.map_err(|e| format!("HTTP request failed: {}", e))?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(format!("Supabase error {}: {}", status, body));
}
let scores: Vec<SupabaseReputationScore> = response
.json()
.await
.map_err(|e| format!("JSON parse error: {}", e))?;
Ok(scores.into_iter().next())
}
pub async fn add_attestation(
&self,
user_id: &str,
layer_id: Option<i32>,
attestation_type: &str,
weight: i32,
is_positive: bool,
metadata: Option<serde_json::Value>,
source: &str,
source_tx: Option<&str>,
verified: bool,
) -> Result<String, String> {
let url = format!("{}/rest/v1/attestations", self.config.url);
let layer_group = layer_id.map(|id| match id {
1..=6 => "passive_utility",
7..=11 => "infrastructure",
12..=16 => "intelligence",
17..=22 => "aggressive_autopilot",
_ => "passive_utility",
});
let body = serde_json::json!({
"user_id": user_id,
"layer_id": layer_id,
"layer_group": layer_group,
"type": attestation_type,
"weight": weight,
"is_positive": is_positive,
"metadata_json": metadata.unwrap_or(serde_json::json!({})),
"source": source,
"source_tx": source_tx,
"verified": verified,
"verified_at": if verified { Some(chrono::Utc::now().to_rfc3339()) } else { None },
"verified_by": if verified { Some("system") } else { None },
});
let response = self
.http
.post(&url)
.header("apikey", &self.config.service_role_key)
.header("Authorization", format!("Bearer {}", self.config.service_role_key))
.header("Content-Type", "application/json")
.header("Prefer", "return=representation")
.json(&body)
.send()
.await
.map_err(|e| format!("HTTP request failed: {}", e))?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(format!("Supabase error {}: {}", status, body));
}
let created: Vec<serde_json::Value> = response
.json()
.await
.map_err(|e| format!("JSON parse error: {}", e))?;
let id = created
.first()
.and_then(|v| v.get("id"))
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
info!("Created attestation {} for user {}", id, user_id);
Ok(id)
}
}
fn map_attestation_type_to_layer(attestation_type: &str, layer_id: Option<i32>) -> CaptureLayer {
if let Some(id) = layer_id {
return match id {
1 => CaptureLayer::Shopping,
2 => CaptureLayer::Referral,
3 => CaptureLayer::Attention,
4 => CaptureLayer::Data,
5 => CaptureLayer::Insurance,
6 => CaptureLayer::Compute,
7 => CaptureLayer::Network,
8 => CaptureLayer::Energy,
9 => CaptureLayer::DePINAggregator,
10 => CaptureLayer::InferenceArbitrage,
11 => CaptureLayer::StorageDePIN,
12 => CaptureLayer::Skill,
13 => CaptureLayer::CurationSignal,
14 => CaptureLayer::Social,
15 => CaptureLayer::KnowledgeAPI,
16 => CaptureLayer::PersonalModelLicensing,
17 => CaptureLayer::Liquidity,
18 => CaptureLayer::GovernanceProxy,
19 => CaptureLayer::InventoryArbitrage,
20 => CaptureLayer::SubAgentManager,
21 => CaptureLayer::ReputationCollateral,
22 => CaptureLayer::SwarmCoordinationFee,
_ => CaptureLayer::Shopping,
};
}
match attestation_type {
"vault_created" | "stack_initiated" | "stack_completed" | "yield_claimed"
| "yield_compounded" | "early_withdrawal" | "transaction_failed" => CaptureLayer::Shopping,
"certification_submitted" | "certification_verified" | "expertise_demonstrated"
| "api_contribution" => CaptureLayer::Skill,
"referral_given" | "referral_received" | "community_contribution" | "governance_vote" => {
CaptureLayer::Social
}
"daily_login" | "weekly_active" | "monthly_milestone" | "anniversary" => {
CaptureLayer::Shopping
}
"node_registered" | "bandwidth_contributed" | "compute_contributed"
| "storage_contributed" => CaptureLayer::Network,
_ => CaptureLayer::Shopping,
}
}
fn convert_to_attestation_record(supabase: &SupabaseAttestation) -> AttestationRecord {
let layer = map_attestation_type_to_layer(&supabase.attestation_type, supabase.layer_id);
let timestamp = chrono::DateTime::parse_from_rfc3339(&supabase.created_at)
.map(|dt| dt.timestamp() as u64)
.unwrap_or(0);
let metadata = supabase.metadata_json.as_ref().and_then(|json| {
let mut meta = AttestationMetadata::default();
if let Some(obj) = json.as_object() {
if let Some(days) = obj.get("durationDays").and_then(|v| v.as_i64()) {
meta.lock_duration_days = Some(days as u16);
}
if let Some(days) = obj.get("lock_duration_days").and_then(|v| v.as_i64()) {
meta.lock_duration_days = Some(days as u16);
}
if let Some(held) = obj.get("held_to_maturity").and_then(|v| v.as_bool()) {
meta.held_to_maturity = Some(held);
}
if let Some(acc) = obj.get("accuracy_percent").and_then(|v| v.as_i64()) {
meta.accuracy_percent = Some(acc as u8);
}
if let Some(uptime) = obj.get("uptime_percent").and_then(|v| v.as_i64()) {
meta.uptime_percent = Some(uptime as u8);
}
if let Some(tier) = obj.get("difficulty_tier").and_then(|v| v.as_i64()) {
meta.difficulty_tier = Some(tier as u8);
}
if let Some(mult) = obj.get("verification_multiplier").and_then(|v| v.as_f64()) {
meta.verification_multiplier = Some(mult as f32);
}
}
Some(meta)
});
let magnitude = (supabase.weight as u64) * 10_000;
AttestationRecord {
layer,
timestamp,
positive: supabase.is_positive,
magnitude,
metadata,
}
}
impl ReputationEngine {
pub async fn load_from_supabase(&mut self, client: &SupabaseClient) -> Result<usize, String> {
let attestations = client.fetch_attestations(&self.user_pubkey).await?;
let count = attestations.len();
for supabase_att in &attestations {
let record = convert_to_attestation_record(supabase_att);
self.process_attestation(&record);
}
info!(
"Loaded {} attestations from Supabase for user {}",
count, self.user_pubkey
);
Ok(count)
}
pub async fn from_supabase(
user_pubkey: String,
client: &SupabaseClient,
) -> Result<Self, String> {
let mut engine = Self::new(user_pubkey);
engine.load_from_supabase(client).await?;
Ok(engine)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_layer_mapping() {
assert_eq!(
map_attestation_type_to_layer("stack_initiated", None),
CaptureLayer::Shopping
);
assert_eq!(
map_attestation_type_to_layer("certification_verified", None),
CaptureLayer::Skill
);
assert_eq!(
map_attestation_type_to_layer("referral_given", None),
CaptureLayer::Social
);
assert_eq!(
map_attestation_type_to_layer("node_registered", None),
CaptureLayer::Network
);
}
#[test]
fn test_layer_id_override() {
assert_eq!(
map_attestation_type_to_layer("stack_initiated", Some(12)),
CaptureLayer::Skill
);
}
#[test]
fn test_convert_attestation() {
let supabase = SupabaseAttestation {
id: "test-id".to_string(),
user_id: "test-user".to_string(),
layer_id: Some(1),
layer_group: Some("passive_utility".to_string()),
attestation_type: "stack_initiated".to_string(),
weight: 50,
is_positive: true,
metadata_json: Some(serde_json::json!({
"durationDays": 90,
"amount": 100000000
})),
verified: true,
source: "api".to_string(),
source_tx: None,
created_at: "2024-03-27T00:00:00Z".to_string(),
expires_at: None,
deleted_at: None,
};
let record = convert_to_attestation_record(&supabase);
assert_eq!(record.layer, CaptureLayer::Shopping);
assert!(record.positive);
assert!(record.metadata.is_some());
assert_eq!(record.metadata.unwrap().lock_duration_days, Some(90));
}
}