1use crate::reputation_engine::{
7 AttestationMetadata, AttestationRecord, CaptureLayer, ReputationEngine,
8};
9use reqwest::Client;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use tracing::{debug, error, info, warn};
13
14#[derive(Debug, Clone)]
16pub struct SupabaseConfig {
17 pub url: String,
19 pub service_role_key: String,
21}
22
23impl SupabaseConfig {
24 pub fn from_env() -> Result<Self, String> {
26 let url = std::env::var("SUPABASE_URL")
27 .map_err(|_| "Missing SUPABASE_URL environment variable")?;
28 let service_role_key = std::env::var("SUPABASE_SERVICE_ROLE_KEY")
29 .map_err(|_| "Missing SUPABASE_SERVICE_ROLE_KEY environment variable")?;
30
31 Ok(Self {
32 url,
33 service_role_key,
34 })
35 }
36}
37
38#[derive(Debug, Deserialize)]
40pub struct SupabaseAttestation {
41 pub id: String,
42 pub user_id: String,
43 pub layer_id: Option<i32>,
44 pub layer_group: Option<String>,
45 #[serde(rename = "type")]
46 pub attestation_type: String,
47 pub weight: i32,
48 pub is_positive: bool,
49 pub metadata_json: Option<serde_json::Value>,
50 pub verified: bool,
51 pub source: String,
52 pub source_tx: Option<String>,
53 pub created_at: String,
54 pub expires_at: Option<String>,
55 pub deleted_at: Option<String>,
56}
57
58#[derive(Debug, Deserialize)]
60pub struct SupabaseReputationScore {
61 pub composite: i32,
62 pub reliability: i32,
63 pub skill: i32,
64 pub social: i32,
65 pub tenure: i32,
66 pub infrastructure: i32,
67 pub tier: String,
68}
69
70pub struct SupabaseClient {
72 config: SupabaseConfig,
73 http: Client,
74}
75
76impl SupabaseClient {
77 pub fn new(config: SupabaseConfig) -> Self {
79 Self {
80 config,
81 http: Client::new(),
82 }
83 }
84
85 pub fn from_env() -> Result<Self, String> {
87 let config = SupabaseConfig::from_env()?;
88 Ok(Self::new(config))
89 }
90
91 pub async fn fetch_attestations(
93 &self,
94 user_id: &str,
95 ) -> Result<Vec<SupabaseAttestation>, String> {
96 let url = format!(
97 "{}/rest/v1/attestations?user_id=eq.{}&deleted_at=is.null&order=created_at.desc",
98 self.config.url, user_id
99 );
100
101 let response = self
102 .http
103 .get(&url)
104 .header("apikey", &self.config.service_role_key)
105 .header("Authorization", format!("Bearer {}", self.config.service_role_key))
106 .header("Content-Type", "application/json")
107 .send()
108 .await
109 .map_err(|e| format!("HTTP request failed: {}", e))?;
110
111 if !response.status().is_success() {
112 let status = response.status();
113 let body = response.text().await.unwrap_or_default();
114 return Err(format!("Supabase error {}: {}", status, body));
115 }
116
117 let attestations: Vec<SupabaseAttestation> = response
118 .json()
119 .await
120 .map_err(|e| format!("JSON parse error: {}", e))?;
121
122 debug!("Fetched {} attestations for user {}", attestations.len(), user_id);
123 Ok(attestations)
124 }
125
126 pub async fn fetch_reputation_score(
128 &self,
129 user_id: &str,
130 ) -> Result<Option<SupabaseReputationScore>, String> {
131 let url = format!(
132 "{}/rest/v1/rpc/calculate_reputation_score",
133 self.config.url
134 );
135
136 let body = serde_json::json!({
137 "p_user_id": user_id
138 });
139
140 let response = self
141 .http
142 .post(&url)
143 .header("apikey", &self.config.service_role_key)
144 .header("Authorization", format!("Bearer {}", self.config.service_role_key))
145 .header("Content-Type", "application/json")
146 .json(&body)
147 .send()
148 .await
149 .map_err(|e| format!("HTTP request failed: {}", e))?;
150
151 if !response.status().is_success() {
152 let status = response.status();
153 let body = response.text().await.unwrap_or_default();
154 return Err(format!("Supabase error {}: {}", status, body));
155 }
156
157 let scores: Vec<SupabaseReputationScore> = response
159 .json()
160 .await
161 .map_err(|e| format!("JSON parse error: {}", e))?;
162
163 Ok(scores.into_iter().next())
164 }
165
166 pub async fn add_attestation(
168 &self,
169 user_id: &str,
170 layer_id: Option<i32>,
171 attestation_type: &str,
172 weight: i32,
173 is_positive: bool,
174 metadata: Option<serde_json::Value>,
175 source: &str,
176 source_tx: Option<&str>,
177 verified: bool,
178 ) -> Result<String, String> {
179 let url = format!("{}/rest/v1/attestations", self.config.url);
180
181 let layer_group = layer_id.map(|id| match id {
183 1..=6 => "passive_utility",
184 7..=11 => "infrastructure",
185 12..=16 => "intelligence",
186 17..=22 => "aggressive_autopilot",
187 _ => "passive_utility",
188 });
189
190 let body = serde_json::json!({
191 "user_id": user_id,
192 "layer_id": layer_id,
193 "layer_group": layer_group,
194 "type": attestation_type,
195 "weight": weight,
196 "is_positive": is_positive,
197 "metadata_json": metadata.unwrap_or(serde_json::json!({})),
198 "source": source,
199 "source_tx": source_tx,
200 "verified": verified,
201 "verified_at": if verified { Some(chrono::Utc::now().to_rfc3339()) } else { None },
202 "verified_by": if verified { Some("system") } else { None },
203 });
204
205 let response = self
206 .http
207 .post(&url)
208 .header("apikey", &self.config.service_role_key)
209 .header("Authorization", format!("Bearer {}", self.config.service_role_key))
210 .header("Content-Type", "application/json")
211 .header("Prefer", "return=representation")
212 .json(&body)
213 .send()
214 .await
215 .map_err(|e| format!("HTTP request failed: {}", e))?;
216
217 if !response.status().is_success() {
218 let status = response.status();
219 let body = response.text().await.unwrap_or_default();
220 return Err(format!("Supabase error {}: {}", status, body));
221 }
222
223 let created: Vec<serde_json::Value> = response
224 .json()
225 .await
226 .map_err(|e| format!("JSON parse error: {}", e))?;
227
228 let id = created
229 .first()
230 .and_then(|v| v.get("id"))
231 .and_then(|v| v.as_str())
232 .unwrap_or("unknown")
233 .to_string();
234
235 info!("Created attestation {} for user {}", id, user_id);
236 Ok(id)
237 }
238}
239
240fn map_attestation_type_to_layer(attestation_type: &str, layer_id: Option<i32>) -> CaptureLayer {
242 if let Some(id) = layer_id {
244 return match id {
245 1 => CaptureLayer::Shopping,
246 2 => CaptureLayer::Referral,
247 3 => CaptureLayer::Attention,
248 4 => CaptureLayer::Data,
249 5 => CaptureLayer::Insurance,
250 6 => CaptureLayer::Compute,
251 7 => CaptureLayer::Network,
252 8 => CaptureLayer::Energy,
253 9 => CaptureLayer::DePINAggregator,
254 10 => CaptureLayer::InferenceArbitrage,
255 11 => CaptureLayer::StorageDePIN,
256 12 => CaptureLayer::Skill,
257 13 => CaptureLayer::CurationSignal,
258 14 => CaptureLayer::Social,
259 15 => CaptureLayer::KnowledgeAPI,
260 16 => CaptureLayer::PersonalModelLicensing,
261 17 => CaptureLayer::Liquidity,
262 18 => CaptureLayer::GovernanceProxy,
263 19 => CaptureLayer::InventoryArbitrage,
264 20 => CaptureLayer::SubAgentManager,
265 21 => CaptureLayer::ReputationCollateral,
266 22 => CaptureLayer::SwarmCoordinationFee,
267 _ => CaptureLayer::Shopping,
268 };
269 }
270
271 match attestation_type {
273 "vault_created" | "stack_initiated" | "stack_completed" | "yield_claimed"
275 | "yield_compounded" | "early_withdrawal" | "transaction_failed" => CaptureLayer::Shopping,
276
277 "certification_submitted" | "certification_verified" | "expertise_demonstrated"
279 | "api_contribution" => CaptureLayer::Skill,
280
281 "referral_given" | "referral_received" | "community_contribution" | "governance_vote" => {
283 CaptureLayer::Social
284 }
285
286 "daily_login" | "weekly_active" | "monthly_milestone" | "anniversary" => {
288 CaptureLayer::Shopping
289 }
290
291 "node_registered" | "bandwidth_contributed" | "compute_contributed"
293 | "storage_contributed" => CaptureLayer::Network,
294
295 _ => CaptureLayer::Shopping,
297 }
298}
299
300fn convert_to_attestation_record(supabase: &SupabaseAttestation) -> AttestationRecord {
302 let layer = map_attestation_type_to_layer(&supabase.attestation_type, supabase.layer_id);
303
304 let timestamp = chrono::DateTime::parse_from_rfc3339(&supabase.created_at)
306 .map(|dt| dt.timestamp() as u64)
307 .unwrap_or(0);
308
309 let metadata = supabase.metadata_json.as_ref().and_then(|json| {
311 let mut meta = AttestationMetadata::default();
312
313 if let Some(obj) = json.as_object() {
314 if let Some(days) = obj.get("durationDays").and_then(|v| v.as_i64()) {
315 meta.lock_duration_days = Some(days as u16);
316 }
317 if let Some(days) = obj.get("lock_duration_days").and_then(|v| v.as_i64()) {
318 meta.lock_duration_days = Some(days as u16);
319 }
320 if let Some(held) = obj.get("held_to_maturity").and_then(|v| v.as_bool()) {
321 meta.held_to_maturity = Some(held);
322 }
323 if let Some(acc) = obj.get("accuracy_percent").and_then(|v| v.as_i64()) {
324 meta.accuracy_percent = Some(acc as u8);
325 }
326 if let Some(uptime) = obj.get("uptime_percent").and_then(|v| v.as_i64()) {
327 meta.uptime_percent = Some(uptime as u8);
328 }
329 if let Some(tier) = obj.get("difficulty_tier").and_then(|v| v.as_i64()) {
331 meta.difficulty_tier = Some(tier as u8);
332 }
333 if let Some(mult) = obj.get("verification_multiplier").and_then(|v| v.as_f64()) {
334 meta.verification_multiplier = Some(mult as f32);
335 }
336 }
337
338 Some(meta)
339 });
340
341 let magnitude = (supabase.weight as u64) * 10_000; AttestationRecord {
345 layer,
346 timestamp,
347 positive: supabase.is_positive,
348 magnitude,
349 metadata,
350 }
351}
352
353impl ReputationEngine {
355 pub async fn load_from_supabase(&mut self, client: &SupabaseClient) -> Result<usize, String> {
357 let attestations = client.fetch_attestations(&self.user_pubkey).await?;
358 let count = attestations.len();
359
360 for supabase_att in &attestations {
361 let record = convert_to_attestation_record(supabase_att);
362 self.process_attestation(&record);
363 }
364
365 info!(
366 "Loaded {} attestations from Supabase for user {}",
367 count, self.user_pubkey
368 );
369 Ok(count)
370 }
371
372 pub async fn from_supabase(
374 user_pubkey: String,
375 client: &SupabaseClient,
376 ) -> Result<Self, String> {
377 let mut engine = Self::new(user_pubkey);
378 engine.load_from_supabase(client).await?;
379 Ok(engine)
380 }
381}
382
383#[cfg(test)]
388mod tests {
389 use super::*;
390
391 #[test]
392 fn test_layer_mapping() {
393 assert_eq!(
394 map_attestation_type_to_layer("stack_initiated", None),
395 CaptureLayer::Shopping
396 );
397 assert_eq!(
398 map_attestation_type_to_layer("certification_verified", None),
399 CaptureLayer::Skill
400 );
401 assert_eq!(
402 map_attestation_type_to_layer("referral_given", None),
403 CaptureLayer::Social
404 );
405 assert_eq!(
406 map_attestation_type_to_layer("node_registered", None),
407 CaptureLayer::Network
408 );
409 }
410
411 #[test]
412 fn test_layer_id_override() {
413 assert_eq!(
415 map_attestation_type_to_layer("stack_initiated", Some(12)),
416 CaptureLayer::Skill
417 );
418 }
419
420 #[test]
421 fn test_convert_attestation() {
422 let supabase = SupabaseAttestation {
423 id: "test-id".to_string(),
424 user_id: "test-user".to_string(),
425 layer_id: Some(1),
426 layer_group: Some("passive_utility".to_string()),
427 attestation_type: "stack_initiated".to_string(),
428 weight: 50,
429 is_positive: true,
430 metadata_json: Some(serde_json::json!({
431 "durationDays": 90,
432 "amount": 100000000
433 })),
434 verified: true,
435 source: "api".to_string(),
436 source_tx: None,
437 created_at: "2024-03-27T00:00:00Z".to_string(),
438 expires_at: None,
439 deleted_at: None,
440 };
441
442 let record = convert_to_attestation_record(&supabase);
443 assert_eq!(record.layer, CaptureLayer::Shopping);
444 assert!(record.positive);
445 assert!(record.metadata.is_some());
446 assert_eq!(record.metadata.unwrap().lock_duration_days, Some(90));
447 }
448}