1use std::collections::HashMap;
2use std::fmt::Debug;
3use std::str::FromStr;
4use std::sync::Arc;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use aes_gcm::aead::{Aead, AeadCore, KeyInit};
8use aes_gcm::{Aes256Gcm, Key, Nonce};
9use async_trait::async_trait;
10use bitcoin::bip32::DerivationPath;
11use bitcoin::hashes::{sha256, Hash};
12use bitcoin::secp256k1::rand::rngs::OsRng;
13use cdk_common::auth::oidc::OidcClient;
14use cdk_common::common::ProofInfo;
15use cdk_common::database::wallet::Database;
16use cdk_common::database::{Error as DatabaseError, KVStoreDatabase};
17use cdk_common::mint_url::MintUrl;
18use cdk_common::nuts::{
19 CurrencyUnit, Id, KeySet, KeySetInfo, Keys, MintInfo, PublicKey, SpendingConditions, State,
20};
21use cdk_common::secret::Secret;
22use cdk_common::util::hex;
23use cdk_common::wallet::{
24 self, MintQuote, Transaction, TransactionDirection, TransactionId, WalletSaga,
25};
26use reqwest::{Client, StatusCode};
27use serde::{Deserialize, Serialize};
28use tokio::sync::RwLock;
29use url::Url;
30
31use crate::Error;
32
33#[rustfmt::skip]
34mod migrations {
35 include!(concat!(env!("OUT_DIR"), "/migrations_supabase.rs"));
36}
37
38pub(crate) fn get_schema_sql_inner() -> String {
43 migrations::MIGRATIONS
44 .iter()
45 .map(|(_, _, sql)| *sql)
46 .collect::<Vec<&str>>()
47 .join("\n\n")
48}
49
50fn url_encode(value: &str) -> String {
52 urlencoding::encode(value).into_owned()
53}
54
55fn decode_jwt_expiry(token: &str) -> Option<u64> {
57 let parts: Vec<&str> = token.split('.').collect();
58 if parts.len() < 2 {
59 return None;
60 }
61 let payload_part = parts[1];
62
63 use bitcoin::base64::engine::general_purpose;
64 use bitcoin::base64::Engine as _;
65
66 let decoded = general_purpose::URL_SAFE_NO_PAD.decode(payload_part).ok()?;
67
68 #[derive(Deserialize)]
69 struct Claims {
70 exp: Option<u64>,
71 }
72
73 let claims: Claims = serde_json::from_slice(&decoded).ok()?;
74 claims.exp
75}
76
77#[derive(Debug, Clone)]
81pub enum AuthProvider {
82 None,
84 SupabaseAuth,
88 Oidc(OidcClient),
90}
91
92#[derive(Debug, Deserialize)]
94struct SupabaseTokenResponse {
95 access_token: String,
96 refresh_token: Option<String>,
97 expires_in: Option<i64>,
98 #[serde(skip)]
99 _token_type: (),
100}
101
102#[derive(Debug, Clone)]
121pub struct SupabaseWalletDatabase {
122 url: Url,
123 api_key: String,
124 jwt_token: Arc<RwLock<Option<String>>>,
125 refresh_token: Arc<RwLock<Option<String>>>,
126 token_expiration: Arc<RwLock<Option<u64>>>,
127 auth_provider: Arc<RwLock<AuthProvider>>,
128 client: Client,
129 encryption_key: Arc<RwLock<Option<Key<Aes256Gcm>>>>,
130}
131
132impl SupabaseWalletDatabase {
133 pub async fn new(url: Url, api_key: String) -> Result<Self, Error> {
142 Ok(Self {
143 url,
144 api_key,
145 jwt_token: Arc::new(RwLock::new(None)),
146 refresh_token: Arc::new(RwLock::new(None)),
147 token_expiration: Arc::new(RwLock::new(None)),
148 auth_provider: Arc::new(RwLock::new(AuthProvider::None)),
149 client: Client::new(),
150 encryption_key: Arc::new(RwLock::new(None)),
151 })
152 }
153
154 pub async fn with_supabase_auth(url: Url, api_key: String) -> Result<Self, Error> {
164 Ok(Self {
165 url,
166 api_key,
167 jwt_token: Arc::new(RwLock::new(None)),
168 refresh_token: Arc::new(RwLock::new(None)),
169 token_expiration: Arc::new(RwLock::new(None)),
170 auth_provider: Arc::new(RwLock::new(AuthProvider::SupabaseAuth)),
171 client: Client::new(),
172 encryption_key: Arc::new(RwLock::new(None)),
173 })
174 }
175
176 pub async fn with_oidc(
186 url: Url,
187 api_key: String,
188 oidc_client: OidcClient,
189 ) -> Result<Self, Error> {
190 Ok(Self {
191 url,
192 api_key,
193 jwt_token: Arc::new(RwLock::new(None)),
194 refresh_token: Arc::new(RwLock::new(None)),
195 token_expiration: Arc::new(RwLock::new(None)),
196 auth_provider: Arc::new(RwLock::new(AuthProvider::Oidc(oidc_client))),
197 client: Client::new(),
198 encryption_key: Arc::new(RwLock::new(None)),
199 })
200 }
201
202 pub const REQUIRED_SCHEMA_VERSION: u32 = 5;
208
209 pub fn get_schema_sql() -> String {
217 get_schema_sql_inner()
218 }
219
220 pub async fn check_schema_compatibility(&self) -> Result<(), Error> {
241 let path = "rest/v1/schema_info?key=eq.schema_version&select=value";
242
243 let result = self.get_request(path).await;
244
245 match result {
246 Ok((status, text)) => {
247 if status == StatusCode::NOT_FOUND
248 || text.contains("relation")
249 || text.contains("does not exist")
250 {
251 return Err(Error::SchemaNotInitialized);
252 }
253
254 if !status.is_success() {
255 return Err(Error::SchemaNotInitialized);
258 }
259
260 let items: Vec<serde_json::Value> =
262 serde_json::from_str(&text).map_err(|_| Error::SchemaNotInitialized)?;
263
264 if items.is_empty() {
265 return Err(Error::SchemaNotInitialized);
266 }
267
268 let version_str = items[0]
269 .get("value")
270 .and_then(|v| v.as_str())
271 .ok_or(Error::SchemaNotInitialized)?;
272
273 let found_version: u32 = version_str
274 .parse()
275 .map_err(|_| Error::SchemaNotInitialized)?;
276
277 if found_version < Self::REQUIRED_SCHEMA_VERSION {
278 return Err(Error::SchemaMismatch {
279 required: Self::REQUIRED_SCHEMA_VERSION,
280 found: found_version,
281 });
282 }
283
284 tracing::info!(
285 schema_version = found_version,
286 required = Self::REQUIRED_SCHEMA_VERSION,
287 "Database schema compatibility check passed"
288 );
289
290 Ok(())
291 }
292 Err(_) => Err(Error::SchemaNotInitialized),
293 }
294 }
295
296 pub async fn set_jwt_token(&self, token: Option<String>) {
298 let mut jwt = self.jwt_token.write().await;
299 *jwt = token.clone();
300
301 let mut expiration = self.token_expiration.write().await;
302
303 if let Some(t) = token {
304 *expiration = decode_jwt_expiry(&t);
305 } else {
306 *expiration = None;
307 }
308 }
309
310 pub async fn set_refresh_token(&self, token: Option<String>) {
312 let mut refresh = self.refresh_token.write().await;
313 *refresh = token;
314 }
315
316 pub async fn set_encryption_password(&self, password: &str) {
318 let key = sha256::Hash::hash(password.as_bytes());
319
320 let mut encryption_key = self.encryption_key.write().await;
321 *encryption_key = Some(*Key::<Aes256Gcm>::from_slice(key.as_byte_array()));
322 }
323
324 async fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>, DatabaseError> {
325 let key_guard = self.encryption_key.read().await;
326 let key = key_guard
327 .as_ref()
328 .ok_or(DatabaseError::Internal("Encryption key not set".into()))?;
329 let cipher = Aes256Gcm::new(key);
330 let nonce = Aes256Gcm::generate_nonce(&mut OsRng); let ciphertext = cipher
332 .encrypt(&nonce, data)
333 .map_err(|_| DatabaseError::Internal("Encryption failed".into()))?;
334
335 let mut result = nonce.to_vec();
337 result.extend_from_slice(&ciphertext);
338 Ok(result)
339 }
340
341 async fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>, DatabaseError> {
342 let key_guard = self.encryption_key.read().await;
343 let key = key_guard
344 .as_ref()
345 .ok_or(DatabaseError::Internal("Encryption key not set".into()))?;
346 let cipher = Aes256Gcm::new(key);
347
348 if data.len() < 12 {
349 return Err(DatabaseError::Internal("Invalid ciphertext length".into()));
350 }
351
352 let nonce = Nonce::from_slice(&data[0..12]);
353 let ciphertext = &data[12..];
354
355 cipher
356 .decrypt(nonce, ciphertext)
357 .map_err(|_| DatabaseError::Internal("Decryption failed".into()))
358 }
359
360 async fn decrypt_proof_table(&self, p: &mut ProofTable) {
361 if let Ok(encrypted_bytes) = hex::decode(&p.secret) {
363 if let Ok(decrypted) = self.decrypt(&encrypted_bytes).await {
364 if let Ok(secret_str) = String::from_utf8(decrypted) {
365 p.secret = secret_str;
366 }
367 }
368 }
369
370 if let Ok(encrypted_c) = hex::decode(&p.c) {
372 if let Ok(decrypted_c) = self.decrypt(&encrypted_c).await {
373 p.c = hex::encode(decrypted_c);
374 }
375 }
376 }
377
378 pub async fn refresh_access_token(&self) -> Result<(), Error> {
385 let refresh_token = self.refresh_token.read().await.clone();
386 let refresh = refresh_token
387 .ok_or_else(|| Error::Supabase("No refresh token available".to_string()))?;
388
389 let auth_provider = self.auth_provider.read().await.clone();
390
391 match auth_provider {
392 AuthProvider::None => {
393 return Err(Error::Supabase(
394 "No authentication provider configured".to_string(),
395 ));
396 }
397 AuthProvider::SupabaseAuth => {
398 let auth_url = self
400 .url
401 .join("auth/v1/token?grant_type=refresh_token")
402 .map_err(|e| Error::Supabase(format!("Invalid auth URL: {}", e)))?;
403
404 let body = serde_json::json!({
405 "refresh_token": refresh
406 });
407
408 let response = self
409 .client
410 .post(auth_url)
411 .header("apikey", &self.api_key)
412 .header("Content-Type", "application/json")
413 .json(&body)
414 .send()
415 .await
416 .map_err(Error::Reqwest)?;
417
418 let status = response.status();
419 if !status.is_success() {
420 let text = response.text().await.unwrap_or_default();
421 return Err(Error::Supabase(format!(
422 "Supabase token refresh failed: HTTP {} - {}",
423 status, text
424 )));
425 }
426
427 let token_response: SupabaseTokenResponse =
428 response.json().await.map_err(Error::Reqwest)?;
429
430 self.set_jwt_token(Some(token_response.access_token)).await;
431
432 if let Some(new_refresh) = token_response.refresh_token {
433 self.set_refresh_token(Some(new_refresh)).await;
434 }
435
436 if let Some(expires_in) = token_response.expires_in {
437 let expiration = SystemTime::now()
438 .duration_since(UNIX_EPOCH)
439 .map_err(|e| Error::Supabase(format!("SystemTime error: {}", e)))?
440 .as_secs()
441 + expires_in as u64;
442 let mut exp = self.token_expiration.write().await;
443 *exp = Some(expiration);
444 }
445 }
446 AuthProvider::Oidc(oidc) => {
447 let client_id = oidc.client_id().ok_or_else(|| {
448 Error::Supabase("Client ID not set in OIDC client".to_string())
449 })?;
450
451 let response = oidc
452 .refresh_access_token(client_id, refresh)
453 .await
454 .map_err(|e| Error::Supabase(e.to_string()))?;
455
456 self.set_jwt_token(Some(response.access_token)).await;
457
458 if let Some(new_refresh) = response.refresh_token {
459 self.set_refresh_token(Some(new_refresh)).await;
460 }
461
462 if let Some(expires_in) = response.expires_in {
463 let expiration = SystemTime::now()
464 .duration_since(UNIX_EPOCH)
465 .map_err(|e| Error::Supabase(format!("SystemTime error: {}", e)))?
466 .as_secs()
467 + expires_in as u64;
468 let mut exp = self.token_expiration.write().await;
469 *exp = Some(expiration);
470 }
471 }
472 }
473
474 Ok(())
475 }
476
477 pub async fn signup(&self, email: &str, password: &str) -> Result<SupabaseAuthResponse, Error> {
479 let response = SupabaseAuth::signup(&self.url, &self.api_key, email, password).await?;
480
481 if !response.access_token.is_empty() {
483 self.set_jwt_token(Some(response.access_token.clone()))
484 .await;
485 }
486 if let Some(refresh) = &response.refresh_token {
487 self.set_refresh_token(Some(refresh.clone())).await;
488 }
489
490 Ok(response)
491 }
492
493 pub async fn signin(&self, email: &str, password: &str) -> Result<SupabaseAuthResponse, Error> {
495 let response = SupabaseAuth::signin(&self.url, &self.api_key, email, password).await?;
496
497 self.set_jwt_token(Some(response.access_token.clone()))
498 .await;
499 if let Some(refresh) = &response.refresh_token {
500 self.set_refresh_token(Some(refresh.clone())).await;
501 }
502 if let Some(expires_in) = response.expires_in {
503 let expiration = SystemTime::now()
504 .duration_since(UNIX_EPOCH)
505 .map_err(|e| Error::Supabase(format!("SystemTime error: {}", e)))?
506 .as_secs()
507 + expires_in as u64;
508 let mut exp = self.token_expiration.write().await;
509 *exp = Some(expiration);
510 }
511
512 Ok(response)
513 }
514
515 pub async fn get_jwt_token(&self) -> Option<String> {
517 self.jwt_token.read().await.clone()
518 }
519
520 pub async fn call_rpc(&self, function_name: &str, params_json: &str) -> Result<String, Error> {
522 let params: serde_json::Value = if params_json.trim().is_empty() {
525 serde_json::Value::Object(serde_json::Map::new())
526 } else {
527 serde_json::from_str(params_json).map_err(Error::Serde)?
528 };
529
530 let path = format!("rest/v1/rpc/{}", function_name);
531 let url = self.join_url(&path)?;
532 let auth_bearer = self.get_auth_bearer().await;
533
534 let res = self
535 .client
536 .post(url.clone())
537 .header("apikey", &self.api_key)
538 .header("Authorization", format!("Bearer {}", auth_bearer))
539 .header("Content-Type", "application/json")
540 .json(¶ms)
541 .send()
542 .await
543 .map_err(Error::Reqwest)?;
544
545 let status = res.status();
546 let text = res.text().await.map_err(Error::Reqwest)?;
547
548 if !status.is_success() {
549 return Err(Error::Supabase(format!(
550 "RPC '{}' failed: HTTP {} - {}",
551 function_name, status, text
552 )));
553 }
554
555 Ok(text)
556 }
557
558 async fn get_auth_bearer(&self) -> String {
562 let expiration = *self.token_expiration.read().await;
564 if let Some(exp) = expiration {
565 let now = SystemTime::now()
566 .duration_since(UNIX_EPOCH)
567 .expect("SystemTime should be after UNIX_EPOCH")
568 .as_secs();
569 if now + 60 > exp {
571 if let Err(e) = self.refresh_access_token().await {
572 tracing::warn!("Failed to refresh token: {}", e);
573 }
574 }
575 }
576
577 self.jwt_token
578 .read()
579 .await
580 .clone()
581 .unwrap_or_else(|| self.api_key.clone())
582 }
583
584 pub fn join_url(&self, path: &str) -> Result<Url, DatabaseError> {
586 self.url
587 .join(path)
588 .map_err(|e| DatabaseError::Internal(e.to_string()))
589 }
590
591 async fn get_request(&self, path: &str) -> Result<(StatusCode, String), Error> {
593 let url = self.join_url(path)?;
594 let auth_bearer = self.get_auth_bearer().await;
595
596 tracing::debug!(method = "GET", url = %url, "Supabase request");
597
598 let res = self
599 .client
600 .get(url.clone())
601 .header("apikey", &self.api_key)
602 .header("Authorization", format!("Bearer {}", auth_bearer))
603 .send()
604 .await
605 .map_err(Error::Reqwest)?;
606
607 let status = res.status();
608 let text = res.text().await.map_err(Error::Reqwest)?;
609
610 tracing::debug!(method = "GET", url = %url, status = %status, response_len = text.len(), "Supabase response");
611
612 Ok((status, text))
613 }
614
615 async fn post_request<T: Serialize + Debug>(
617 &self,
618 path: &str,
619 body: &T,
620 ) -> Result<(StatusCode, String), Error> {
621 let url = self.join_url(path)?;
622 let auth_bearer = self.get_auth_bearer().await;
623
624 tracing::debug!(method = "POST", url = %url, "Supabase request");
625
626 let res = self
627 .client
628 .post(url.clone())
629 .header("apikey", &self.api_key)
630 .header("Authorization", format!("Bearer {}", auth_bearer))
631 .header("Prefer", "resolution=merge-duplicates,missing=default")
632 .json(body)
633 .send()
634 .await
635 .map_err(Error::Reqwest)?;
636
637 let status = res.status();
638 let text = res.text().await.map_err(Error::Reqwest)?;
639
640 tracing::debug!(method = "POST", url = %url, status = %status, response_len = text.len(), "Supabase response");
641
642 Ok((status, text))
643 }
644
645 async fn patch_request<T: Serialize + Debug>(
647 &self,
648 path: &str,
649 body: &T,
650 ) -> Result<(StatusCode, String), Error> {
651 let url = self.join_url(path)?;
652 let auth_bearer = self.get_auth_bearer().await;
653
654 tracing::debug!(method = "PATCH", url = %url, "Supabase request");
655
656 let res = self
657 .client
658 .patch(url.clone())
659 .header("apikey", &self.api_key)
660 .header("Authorization", format!("Bearer {}", auth_bearer))
661 .json(body)
662 .send()
663 .await
664 .map_err(Error::Reqwest)?;
665
666 let status = res.status();
667 let text = res.text().await.map_err(Error::Reqwest)?;
668
669 tracing::debug!(method = "PATCH", url = %url, status = %status, response_len = text.len(), "Supabase response");
670
671 Ok((status, text))
672 }
673
674 async fn patch_request_returning<T: Serialize + Debug>(
678 &self,
679 path: &str,
680 body: &T,
681 ) -> Result<(StatusCode, String), Error> {
682 let url = self.join_url(path)?;
683 let auth_bearer = self.get_auth_bearer().await;
684
685 tracing::debug!(method = "PATCH", url = %url, "Supabase request (returning)");
686
687 let res = self
688 .client
689 .patch(url.clone())
690 .header("apikey", &self.api_key)
691 .header("Authorization", format!("Bearer {}", auth_bearer))
692 .header("Prefer", "return=representation")
693 .json(body)
694 .send()
695 .await
696 .map_err(Error::Reqwest)?;
697
698 let status = res.status();
699 let text = res.text().await.map_err(Error::Reqwest)?;
700
701 tracing::debug!(method = "PATCH", url = %url, status = %status, response_len = text.len(), "Supabase response (returning)");
702
703 Ok((status, text))
704 }
705
706 async fn delete_request(&self, path: &str) -> Result<(StatusCode, String), Error> {
708 let url = self.join_url(path)?;
709 let auth_bearer = self.get_auth_bearer().await;
710
711 tracing::debug!(method = "DELETE", url = %url, "Supabase request");
712
713 let res = self
714 .client
715 .delete(url.clone())
716 .header("apikey", &self.api_key)
717 .header("Authorization", format!("Bearer {}", auth_bearer))
718 .send()
719 .await
720 .map_err(Error::Reqwest)?;
721
722 let status = res.status();
723 let text = res.text().await.map_err(Error::Reqwest)?;
724
725 tracing::debug!(method = "DELETE", url = %url, status = %status, response_len = text.len(), "Supabase response");
726
727 Ok((status, text))
728 }
729
730 fn parse_response<T: serde::de::DeserializeOwned>(text: &str) -> Result<Option<Vec<T>>, Error> {
732 if text.trim().is_empty() || text.trim() == "[]" {
733 return Ok(None);
734 }
735 let items: Vec<T> = serde_json::from_str(text).map_err(Error::Serde)?;
736 if items.is_empty() {
737 Ok(None)
738 } else {
739 Ok(Some(items))
740 }
741 }
742}
743
744#[async_trait]
745impl KVStoreDatabase for SupabaseWalletDatabase {
746 type Err = DatabaseError;
747
748 async fn kv_read(
749 &self,
750 primary_namespace: &str,
751 secondary_namespace: &str,
752 key: &str,
753 ) -> Result<Option<Vec<u8>>, Self::Err> {
754 let path = format!(
755 "rest/v1/kv_store?primary_namespace=eq.{}&secondary_namespace=eq.{}&key=eq.{}",
756 url_encode(primary_namespace),
757 url_encode(secondary_namespace),
758 url_encode(key)
759 );
760
761 let (status, text) = self.get_request(&path).await?;
762
763 if status == StatusCode::NO_CONTENT || !status.is_success() {
764 if !status.is_success() && status != StatusCode::NO_CONTENT {
765 return Err(DatabaseError::Internal(format!(
766 "kv_read failed: HTTP {}",
767 status
768 )));
769 }
770 return Ok(None);
771 }
772
773 if let Some(items) = Self::parse_response::<KVStoreTable>(&text)? {
774 if let Some(item) = items.into_iter().next() {
775 let bytes = hex::decode(item.value)
776 .map_err(|_| DatabaseError::Internal("Invalid hex in kv_store".into()))?;
777
778 let decrypted = self.decrypt(&bytes).await?;
780 return Ok(Some(decrypted));
781 }
782 }
783 Ok(None)
784 }
785
786 async fn kv_list(
787 &self,
788 primary_namespace: &str,
789 secondary_namespace: &str,
790 ) -> Result<Vec<String>, Self::Err> {
791 let path = format!(
792 "rest/v1/kv_store?primary_namespace=eq.{}&secondary_namespace=eq.{}",
793 url_encode(primary_namespace),
794 url_encode(secondary_namespace)
795 );
796
797 let (status, text) = self.get_request(&path).await?;
798
799 if !status.is_success() {
800 return Err(DatabaseError::Internal(format!(
801 "kv_list failed: HTTP {}",
802 status
803 )));
804 }
805
806 if let Some(items) = Self::parse_response::<KVStoreTable>(&text)? {
807 Ok(items.into_iter().map(|i| i.key).collect())
808 } else {
809 Ok(Vec::new())
810 }
811 }
812}
813#[async_trait]
814impl Database<DatabaseError> for SupabaseWalletDatabase {
815 async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, DatabaseError> {
816 let path = format!(
817 "rest/v1/mint?mint_url=eq.{}",
818 url_encode(&mint_url.to_string())
819 );
820 let (status, text) = self.get_request(&path).await?;
821
822 if status == StatusCode::NOT_FOUND {
824 return Ok(None);
825 }
826 if !status.is_success() {
827 return Err(DatabaseError::Internal(format!(
828 "get_mint failed: HTTP {}",
829 status
830 )));
831 }
832
833 if let Some(mints) = Self::parse_response::<MintTable>(&text)? {
834 if let Some(mint) = mints.into_iter().next() {
835 return Ok(Some(mint.try_into()?));
836 }
837 }
838 Ok(None)
839 }
840
841 async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, DatabaseError> {
842 let (status, text) = self.get_request("rest/v1/mint").await?;
843
844 if !status.is_success() {
845 return Err(DatabaseError::Internal(format!(
846 "get_mints failed: HTTP {}",
847 status
848 )));
849 }
850
851 let mut map = HashMap::new();
852 if let Some(mints) = Self::parse_response::<MintTable>(&text)? {
853 for mint in mints {
854 map.insert(
855 MintUrl::from_str(&mint.mint_url)
856 .map_err(|e| DatabaseError::Internal(e.to_string()))?,
857 Some(mint.try_into()?),
858 );
859 }
860 }
861 Ok(map)
862 }
863
864 async fn get_mint_keysets(
865 &self,
866 mint_url: MintUrl,
867 ) -> Result<Option<Vec<KeySetInfo>>, DatabaseError> {
868 let path = format!(
869 "rest/v1/keyset?mint_url=eq.{}",
870 url_encode(&mint_url.to_string())
871 );
872 let (status, text) = self.get_request(&path).await?;
873
874 if !status.is_success() {
875 return Err(DatabaseError::Internal(format!(
876 "get_mint_keysets failed: HTTP {}",
877 status
878 )));
879 }
880
881 if let Some(keysets) = Self::parse_response::<KeySetTable>(&text)? {
882 let result: Result<Vec<KeySetInfo>, _> =
883 keysets.into_iter().map(|ks| ks.try_into()).collect();
884 Ok(Some(result?))
885 } else {
886 Ok(None)
887 }
888 }
889
890 async fn get_keyset_by_id(&self, keyset_id: &Id) -> Result<Option<KeySetInfo>, DatabaseError> {
891 let path = format!(
892 "rest/v1/keyset?id=eq.{}",
893 url_encode(&keyset_id.to_string())
894 );
895 let (status, text) = self.get_request(&path).await?;
896
897 if !status.is_success() {
898 return Err(DatabaseError::Internal(format!(
899 "get_keyset_by_id failed: HTTP {}",
900 status
901 )));
902 }
903
904 if let Some(items) = Self::parse_response::<KeySetTable>(&text)? {
905 if let Some(item) = items.into_iter().next() {
906 return Ok(Some(item.try_into()?));
907 }
908 }
909 Ok(None)
910 }
911
912 async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintQuote>, DatabaseError> {
913 let path = format!("rest/v1/mint_quote?id=eq.{}", url_encode(quote_id));
914 let (status, text) = self.get_request(&path).await?;
915
916 if !status.is_success() {
917 return Err(DatabaseError::Internal(format!(
918 "get_mint_quote failed: HTTP {}",
919 status
920 )));
921 }
922
923 if let Some(items) = Self::parse_response::<MintQuoteTable>(&text)? {
924 if let Some(item) = items.into_iter().next() {
925 return Ok(Some(item.try_into()?));
926 }
927 }
928 Ok(None)
929 }
930
931 async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, DatabaseError> {
932 let (status, text) = self.get_request("rest/v1/mint_quote").await?;
933
934 if !status.is_success() {
935 return Err(DatabaseError::Internal(format!(
936 "get_mint_quotes failed: HTTP {}",
937 status
938 )));
939 }
940
941 if let Some(quotes) = Self::parse_response::<MintQuoteTable>(&text)? {
942 quotes.into_iter().map(|q| q.try_into()).collect()
943 } else {
944 Ok(Vec::new())
945 }
946 }
947
948 async fn get_unissued_mint_quotes(&self) -> Result<Vec<MintQuote>, DatabaseError> {
949 let (status, text) = self
950 .get_request("rest/v1/mint_quote?amount_issued=eq.0")
951 .await?;
952
953 if !status.is_success() {
954 return Err(DatabaseError::Internal(format!(
955 "get_unissued_mint_quotes failed: HTTP {}",
956 status
957 )));
958 }
959
960 if let Some(quotes) = Self::parse_response::<MintQuoteTable>(&text)? {
961 quotes.into_iter().map(|q| q.try_into()).collect()
962 } else {
963 Ok(Vec::new())
964 }
965 }
966
967 async fn get_melt_quote(
968 &self,
969 quote_id: &str,
970 ) -> Result<Option<wallet::MeltQuote>, DatabaseError> {
971 let path = format!("rest/v1/melt_quote?id=eq.{}", url_encode(quote_id));
972 let (status, text) = self.get_request(&path).await?;
973
974 if !status.is_success() {
975 return Err(DatabaseError::Internal(format!(
976 "get_melt_quote failed: HTTP {}",
977 status
978 )));
979 }
980
981 if let Some(items) = Self::parse_response::<MeltQuoteTable>(&text)? {
982 if let Some(item) = items.into_iter().next() {
983 return Ok(Some(item.try_into()?));
984 }
985 }
986 Ok(None)
987 }
988
989 async fn get_melt_quotes(&self) -> Result<Vec<wallet::MeltQuote>, DatabaseError> {
990 let (status, text) = self.get_request("rest/v1/melt_quote").await?;
991
992 if !status.is_success() {
993 return Err(DatabaseError::Internal(format!(
994 "get_melt_quotes failed: HTTP {}",
995 status
996 )));
997 }
998
999 if let Some(quotes) = Self::parse_response::<MeltQuoteTable>(&text)? {
1000 quotes.into_iter().map(|q| q.try_into()).collect()
1001 } else {
1002 Ok(Vec::new())
1003 }
1004 }
1005
1006 async fn get_keys(&self, id: &Id) -> Result<Option<Keys>, DatabaseError> {
1007 let path = format!("rest/v1/key?id=eq.{}", url_encode(&id.to_string()));
1008 let (status, text) = self.get_request(&path).await?;
1009
1010 if !status.is_success() {
1011 return Err(DatabaseError::Internal(format!(
1012 "get_keys failed: HTTP {}",
1013 status
1014 )));
1015 }
1016
1017 if let Some(items) = Self::parse_response::<KeyTable>(&text)? {
1018 if let Some(item) = items.into_iter().next() {
1019 return Ok(Some(item.try_into()?));
1020 }
1021 }
1022 Ok(None)
1023 }
1024
1025 async fn get_proofs(
1026 &self,
1027 mint_url: Option<MintUrl>,
1028 unit: Option<CurrencyUnit>,
1029 state: Option<Vec<State>>,
1030 spending_conditions: Option<Vec<SpendingConditions>>,
1031 ) -> Result<Vec<ProofInfo>, DatabaseError> {
1032 let mut query = String::from("rest/v1/proof?select=*");
1033 if let Some(url) = mint_url {
1034 query.push_str(&format!("&mint_url=eq.{}", url_encode(&url.to_string())));
1035 }
1036 if let Some(u) = unit {
1037 query.push_str(&format!("&unit=eq.{}", url_encode(&u.to_string())));
1038 }
1039 if let Some(states) = state {
1040 let s_str: Vec<String> = states.iter().map(|s| s.to_string()).collect();
1041 query.push_str(&format!("&state=in.({})", s_str.join(",")));
1042 }
1043
1044 let (status, text) = self.get_request(&query).await?;
1045
1046 if !status.is_success() {
1047 return Err(DatabaseError::Internal(format!(
1048 "get_proofs failed: HTTP {}",
1049 status
1050 )));
1051 }
1052
1053 let mut result = Vec::new();
1054 if let Some(proofs) = Self::parse_response::<ProofTable>(&text)? {
1055 for mut p in proofs {
1056 self.decrypt_proof_table(&mut p).await;
1057
1058 result.push(p.try_into()?);
1059 }
1060 }
1061
1062 if let Some(conds) = spending_conditions {
1064 result.retain(|p: &ProofInfo| {
1065 if let Some(sc) = &p.spending_condition {
1066 conds.contains(sc)
1067 } else {
1068 false
1069 }
1070 });
1071 }
1072
1073 Ok(result)
1074 }
1075
1076 async fn get_proofs_by_ys(&self, ys: Vec<PublicKey>) -> Result<Vec<ProofInfo>, DatabaseError> {
1077 if ys.is_empty() {
1078 return Ok(Vec::new());
1079 }
1080
1081 let ys_str: Vec<String> = ys.iter().map(|y| hex::encode(y.to_bytes())).collect();
1082 let filter = format!("({})", ys_str.join(","));
1083 let path = format!("rest/v1/proof?y=in.{}", filter);
1084
1085 let (status, text) = self.get_request(&path).await?;
1086
1087 if !status.is_success() {
1088 return Err(DatabaseError::Internal(format!(
1089 "get_proofs_by_ys failed: HTTP {}",
1090 status
1091 )));
1092 }
1093
1094 if let Some(proofs) = Self::parse_response::<ProofTable>(&text)? {
1095 let mut result = Vec::new();
1096 for mut p in proofs {
1097 self.decrypt_proof_table(&mut p).await;
1098
1099 result.push(p.try_into()?);
1100 }
1101 Ok(result)
1102 } else {
1103 Ok(Vec::new())
1104 }
1105 }
1106
1107 async fn get_balance(
1108 &self,
1109 mint_url: Option<MintUrl>,
1110 unit: Option<CurrencyUnit>,
1111 state: Option<Vec<State>>,
1112 ) -> Result<u64, DatabaseError> {
1113 let proofs = self.get_proofs(mint_url, unit, state, None).await?;
1116 Ok(proofs.iter().map(|p| p.proof.amount.to_u64()).sum())
1117 }
1118
1119 async fn get_transaction(
1120 &self,
1121 transaction_id: TransactionId,
1122 ) -> Result<Option<Transaction>, DatabaseError> {
1123 let id_hex = transaction_id.to_string();
1124 let path = format!("rest/v1/transactions?id=eq.\\x{}", id_hex);
1125
1126 let (status, text) = self.get_request(&path).await?;
1127
1128 if status == StatusCode::NOT_FOUND {
1130 return Ok(None);
1131 }
1132 if !status.is_success() {
1133 return Err(DatabaseError::Internal(format!(
1134 "get_transaction failed: HTTP {}",
1135 status
1136 )));
1137 }
1138
1139 if let Some(txs) = Self::parse_response::<TransactionTable>(&text)? {
1140 if let Some(t) = txs.into_iter().next() {
1141 return Ok(Some(t.try_into()?));
1142 }
1143 }
1144 Ok(None)
1145 }
1146
1147 async fn list_transactions(
1148 &self,
1149 mint_url: Option<MintUrl>,
1150 direction: Option<TransactionDirection>,
1151 unit: Option<CurrencyUnit>,
1152 ) -> Result<Vec<Transaction>, DatabaseError> {
1153 let mut query = String::from("rest/v1/transactions?select=*");
1154 if let Some(url) = mint_url {
1155 query.push_str(&format!("&mint_url=eq.{}", url_encode(&url.to_string())));
1156 }
1157 if let Some(d) = direction {
1158 query.push_str(&format!("&direction=eq.{}", url_encode(&d.to_string())));
1159 }
1160 if let Some(u) = unit {
1161 query.push_str(&format!("&unit=eq.{}", url_encode(&u.to_string())));
1162 }
1163
1164 let (status, text) = self.get_request(&query).await?;
1165
1166 if !status.is_success() {
1167 return Err(DatabaseError::Internal(format!(
1168 "list_transactions failed: HTTP {}",
1169 status
1170 )));
1171 }
1172
1173 if let Some(txs) = Self::parse_response::<TransactionTable>(&text)? {
1174 txs.into_iter().map(|t| t.try_into()).collect()
1175 } else {
1176 Ok(Vec::new())
1177 }
1178 }
1179
1180 async fn update_proofs(
1181 &self,
1182 added: Vec<ProofInfo>,
1183 removed_ys: Vec<PublicKey>,
1184 ) -> Result<(), DatabaseError> {
1185 if added.is_empty() && removed_ys.is_empty() {
1187 return Ok(());
1188 }
1189
1190 let mut proofs_json: Vec<serde_json::Value> = Vec::with_capacity(added.len());
1194 for p in added {
1195 let mut table: ProofTable = p.try_into()?;
1196
1197 let secret_bytes = table.secret.as_bytes();
1199 let encrypted = self.encrypt(secret_bytes).await?;
1200 table.secret = hex::encode(encrypted);
1201
1202 if let Ok(c_bytes) = hex::decode(&table.c) {
1204 let encrypted_c = self.encrypt(&c_bytes).await?;
1205 table.c = hex::encode(encrypted_c);
1206 }
1207
1208 proofs_json.push(serde_json::to_value(&table).map_err(DatabaseError::from)?);
1209 }
1210
1211 let ys_json: Vec<String> = removed_ys
1213 .iter()
1214 .map(|y| hex::encode(y.to_bytes()))
1215 .collect();
1216
1217 let rpc_body = serde_json::json!({
1219 "p_proofs_to_add": proofs_json,
1220 "p_ys_to_remove": ys_json
1221 });
1222
1223 let url = self.join_url("rest/v1/rpc/update_proofs_atomic")?;
1224 let auth_bearer = self.get_auth_bearer().await;
1225
1226 tracing::debug!(
1227 method = "POST",
1228 url = %url,
1229 proofs_count = proofs_json.len(),
1230 remove_count = ys_json.len(),
1231 "Supabase atomic update_proofs RPC"
1232 );
1233
1234 let res = self
1235 .client
1236 .post(url.clone())
1237 .header("apikey", &self.api_key)
1238 .header("Authorization", format!("Bearer {}", auth_bearer))
1239 .header("Content-Type", "application/json")
1240 .json(&rpc_body)
1241 .send()
1242 .await
1243 .map_err(Error::Reqwest)?;
1244
1245 let status = res.status();
1246 let text = res.text().await.map_err(Error::Reqwest)?;
1247
1248 tracing::debug!(
1249 method = "POST",
1250 url = %url,
1251 status = %status,
1252 response_len = text.len(),
1253 "Supabase atomic update_proofs response"
1254 );
1255
1256 if status.is_success() {
1257 return Ok(());
1258 }
1259
1260 Err(DatabaseError::Internal(format!(
1261 "update_proofs_atomic RPC failed: HTTP {}. Ensure migrations have been run.",
1262 status
1263 )))
1264 }
1265
1266 async fn update_proofs_state(
1267 &self,
1268 ys: Vec<PublicKey>,
1269 state: State,
1270 ) -> Result<(), DatabaseError> {
1271 if ys.is_empty() {
1272 return Ok(());
1273 }
1274
1275 let ys_str: Vec<String> = ys.iter().map(|y| hex::encode(y.to_bytes())).collect();
1276 let filter = format!("({})", ys_str.join(","));
1277 let path = format!("rest/v1/proof?y=in.{}", filter);
1278
1279 let (status, response_text) = self
1280 .patch_request(&path, &serde_json::json!({ "state": state.to_string() }))
1281 .await?;
1282
1283 if !status.is_success() {
1284 return Err(DatabaseError::Internal(format!(
1285 "update_proofs_state failed: HTTP {} - {}",
1286 status, response_text
1287 )));
1288 }
1289
1290 Ok(())
1291 }
1292
1293 async fn add_transaction(&self, transaction: Transaction) -> Result<(), DatabaseError> {
1294 let item: TransactionTable = transaction.try_into()?;
1295 let (status, response_text) = self
1296 .post_request("rest/v1/transactions?on_conflict=id,wallet_id", &item)
1297 .await?;
1298
1299 if !status.is_success() {
1300 return Err(DatabaseError::Internal(format!(
1301 "add_transaction failed: HTTP {} - {}",
1302 status, response_text
1303 )));
1304 }
1305 Ok(())
1306 }
1307
1308 async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), DatabaseError> {
1309 let id_hex = transaction_id.to_string();
1310 let path = format!("rest/v1/transactions?id=eq.\\x{}", id_hex);
1311
1312 let (status, response_text) = self.delete_request(&path).await?;
1313
1314 if !status.is_success() {
1315 return Err(DatabaseError::Internal(format!(
1316 "remove_transaction failed: HTTP {} - {}",
1317 status, response_text
1318 )));
1319 }
1320 Ok(())
1321 }
1322
1323 async fn update_mint_url(
1324 &self,
1325 old_mint_url: MintUrl,
1326 new_mint_url: MintUrl,
1327 ) -> Result<(), DatabaseError> {
1328 let old_encoded = url_encode(&old_mint_url.to_string());
1329 let update_body = serde_json::json!({ "mint_url": new_mint_url.to_string() });
1330
1331 let path = format!("rest/v1/mint?mint_url=eq.{}", old_encoded);
1333 let (status, response_text) = self.patch_request(&path, &update_body).await?;
1334 if !status.is_success() {
1335 return Err(DatabaseError::Internal(format!(
1336 "update_mint_url (mint) failed: HTTP {} - {}",
1337 status, response_text
1338 )));
1339 }
1340
1341 let path = format!("rest/v1/keyset?mint_url=eq.{}", old_encoded);
1343 let (status, response_text) = self.patch_request(&path, &update_body).await?;
1344 if !status.is_success() {
1345 return Err(DatabaseError::Internal(format!(
1346 "update_mint_url (keyset) failed: HTTP {} - {}",
1347 status, response_text
1348 )));
1349 }
1350
1351 let path = format!("rest/v1/mint_quote?mint_url=eq.{}", old_encoded);
1353 let (status, response_text) = self.patch_request(&path, &update_body).await?;
1354 if !status.is_success() {
1355 return Err(DatabaseError::Internal(format!(
1356 "update_mint_url (mint_quote) failed: HTTP {} - {}",
1357 status, response_text
1358 )));
1359 }
1360
1361 let path = format!("rest/v1/proof?mint_url=eq.{}", old_encoded);
1363 let (status, response_text) = self.patch_request(&path, &update_body).await?;
1364 if !status.is_success() {
1365 return Err(DatabaseError::Internal(format!(
1366 "update_mint_url (proof) failed: HTTP {} - {}",
1367 status, response_text
1368 )));
1369 }
1370
1371 let path = format!("rest/v1/transactions?mint_url=eq.{}", old_encoded);
1373 let (status, response_text) = self.patch_request(&path, &update_body).await?;
1374 if !status.is_success() {
1375 return Err(DatabaseError::Internal(format!(
1376 "update_mint_url (transactions) failed: HTTP {} - {}",
1377 status, response_text
1378 )));
1379 }
1380
1381 Ok(())
1382 }
1383
1384 async fn increment_keyset_counter(
1385 &self,
1386 keyset_id: &Id,
1387 count: u32,
1388 ) -> Result<u32, DatabaseError> {
1389 let rpc_body = serde_json::json!({
1392 "p_keyset_id": keyset_id.to_string(),
1393 "p_increment": count as i32
1394 });
1395
1396 let url = self.join_url("rest/v1/rpc/increment_keyset_counter")?;
1397 let auth_bearer = self.get_auth_bearer().await;
1398
1399 tracing::debug!(method = "POST", url = %url, keyset_id = %keyset_id, increment = count, "Supabase RPC request");
1400
1401 let res = self
1402 .client
1403 .post(url.clone())
1404 .header("apikey", &self.api_key)
1405 .header("Authorization", format!("Bearer {}", auth_bearer))
1406 .header("Content-Type", "application/json")
1407 .header("Prefer", "return=representation")
1408 .json(&rpc_body)
1409 .send()
1410 .await
1411 .map_err(Error::Reqwest)?;
1412
1413 let status = res.status();
1414 let text = res.text().await.map_err(Error::Reqwest)?;
1415
1416 tracing::debug!(method = "POST", url = %url, status = %status, response_len = text.len(), "Supabase RPC response");
1417
1418 if status.is_success() {
1419 let new_counter: i32 = serde_json::from_str(&text).map_err(|e| {
1421 DatabaseError::Internal(format!("Failed to parse counter response: {}", e))
1422 })?;
1423 return Ok(new_counter as u32);
1424 }
1425
1426 Err(DatabaseError::Internal(format!(
1427 "increment_keyset_counter RPC failed: HTTP {}. Ensure migrations have been run.",
1428 status
1429 )))
1430 }
1431
1432 async fn add_mint(
1433 &self,
1434 mint_url: MintUrl,
1435 mint_info: Option<MintInfo>,
1436 ) -> Result<(), DatabaseError> {
1437 let info_table: MintTable = match mint_info {
1438 Some(info) => MintTable::from_info(mint_url.clone(), info)?,
1439 None => MintTable {
1440 mint_url: mint_url.to_string(),
1441 ..Default::default()
1442 },
1443 };
1444
1445 let (status, response_text) = self
1446 .post_request("rest/v1/mint?on_conflict=mint_url,wallet_id", &info_table)
1447 .await?;
1448
1449 if !status.is_success() {
1450 return Err(DatabaseError::Internal(format!(
1451 "add_mint failed: HTTP {} - {}",
1452 status, response_text
1453 )));
1454 }
1455 Ok(())
1456 }
1457
1458 async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), DatabaseError> {
1459 let path = format!(
1460 "rest/v1/mint?mint_url=eq.{}",
1461 url_encode(&mint_url.to_string())
1462 );
1463 let (status, response_text) = self.delete_request(&path).await?;
1464
1465 if !status.is_success() {
1466 return Err(DatabaseError::Internal(format!(
1467 "remove_mint failed: HTTP {} - {}",
1468 status, response_text
1469 )));
1470 }
1471 Ok(())
1472 }
1473
1474 async fn add_mint_keysets(
1475 &self,
1476 mint_url: MintUrl,
1477 keysets: Vec<KeySetInfo>,
1478 ) -> Result<(), DatabaseError> {
1479 if keysets.is_empty() {
1480 return Ok(());
1481 }
1482
1483 let items: Result<Vec<KeySetTable>, DatabaseError> = keysets
1484 .into_iter()
1485 .map(|k| KeySetTable::from_info(mint_url.clone(), k))
1486 .collect();
1487 let items = items?;
1488
1489 let (status, response_text) = self
1490 .post_request("rest/v1/keyset?on_conflict=id,wallet_id", &items)
1491 .await?;
1492
1493 if !status.is_success() {
1494 return Err(DatabaseError::Internal(format!(
1495 "add_mint_keysets failed: HTTP {} - {}",
1496 status, response_text
1497 )));
1498 }
1499 Ok(())
1500 }
1501
1502 async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), DatabaseError> {
1503 let expected_version = quote.version;
1504 let mut item: MintQuoteTable = quote.try_into()?;
1505 item.version = Some(expected_version.wrapping_add(1) as i32);
1506
1507 let path = format!(
1508 "rest/v1/mint_quote?id=eq.{}&version=eq.{}",
1509 url_encode(&item.id),
1510 expected_version
1511 );
1512
1513 let (status, response_text) = self.patch_request_returning(&path, &item).await?;
1517
1518 if status.is_success() {
1519 let updated: serde_json::Value =
1520 serde_json::from_str(&response_text).unwrap_or(serde_json::Value::Null);
1521 let row_count = updated.as_array().map(|a| a.len()).unwrap_or(0);
1522
1523 if row_count > 0 {
1524 return Ok(());
1526 }
1527
1528 let (status, response_text) = self
1530 .post_request("rest/v1/mint_quote?on_conflict=id,wallet_id", &item)
1531 .await?;
1532
1533 if status.is_success() {
1534 return Ok(());
1535 }
1536
1537 return Err(DatabaseError::Internal(format!(
1538 "add_mint_quote insert failed: HTTP {} - {}",
1539 status, response_text
1540 )));
1541 }
1542
1543 Err(DatabaseError::Internal(format!(
1544 "add_mint_quote failed: HTTP {} - {}",
1545 status, response_text
1546 )))
1547 }
1548
1549 async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), DatabaseError> {
1550 let path = format!("rest/v1/mint_quote?id=eq.{}", url_encode(quote_id));
1551 let (status, response_text) = self.delete_request(&path).await?;
1552
1553 if !status.is_success() {
1554 return Err(DatabaseError::Internal(format!(
1555 "remove_mint_quote failed: HTTP {} - {}",
1556 status, response_text
1557 )));
1558 }
1559 Ok(())
1560 }
1561
1562 async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), DatabaseError> {
1563 let expected_version = quote.version;
1564 let mut item: MeltQuoteTable = quote.try_into()?;
1565 item.version = Some(expected_version.wrapping_add(1) as i32);
1566
1567 let path = format!(
1568 "rest/v1/melt_quote?id=eq.{}&version=eq.{}",
1569 url_encode(&item.id),
1570 expected_version
1571 );
1572
1573 let (status, response_text) = self.patch_request_returning(&path, &item).await?;
1577
1578 if status.is_success() {
1579 let updated: serde_json::Value =
1580 serde_json::from_str(&response_text).unwrap_or(serde_json::Value::Null);
1581 let row_count = updated.as_array().map(|a| a.len()).unwrap_or(0);
1582
1583 if row_count > 0 {
1584 return Ok(());
1586 }
1587
1588 let (status, response_text) = self
1590 .post_request("rest/v1/melt_quote?on_conflict=id,wallet_id", &item)
1591 .await?;
1592
1593 if status.is_success() {
1594 return Ok(());
1595 }
1596
1597 return Err(DatabaseError::Internal(format!(
1598 "add_melt_quote insert failed: HTTP {} - {}",
1599 status, response_text
1600 )));
1601 }
1602
1603 Err(DatabaseError::Internal(format!(
1604 "add_melt_quote failed: HTTP {} - {}",
1605 status, response_text
1606 )))
1607 }
1608
1609 async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), DatabaseError> {
1610 let path = format!("rest/v1/melt_quote?id=eq.{}", url_encode(quote_id));
1611 let (status, response_text) = self.delete_request(&path).await?;
1612
1613 if !status.is_success() {
1614 return Err(DatabaseError::Internal(format!(
1615 "remove_melt_quote failed: HTTP {} - {}",
1616 status, response_text
1617 )));
1618 }
1619 Ok(())
1620 }
1621
1622 async fn add_keys(&self, keyset: KeySet) -> Result<(), DatabaseError> {
1623 keyset.verify_id().map_err(DatabaseError::from)?;
1624 let item = KeyTable::from_keyset(&keyset)?;
1625
1626 let (status, response_text) = self
1627 .post_request("rest/v1/key?on_conflict=id,wallet_id", &item)
1628 .await?;
1629
1630 if !status.is_success() {
1631 return Err(DatabaseError::Internal(format!(
1632 "add_keys failed: HTTP {} - {}",
1633 status, response_text
1634 )));
1635 }
1636 Ok(())
1637 }
1638
1639 async fn remove_keys(&self, id: &Id) -> Result<(), DatabaseError> {
1640 let path = format!("rest/v1/key?id=eq.{}", url_encode(&id.to_string()));
1641 let (status, response_text) = self.delete_request(&path).await?;
1642
1643 if !status.is_success() {
1644 return Err(DatabaseError::Internal(format!(
1645 "remove_keys failed: HTTP {} - {}",
1646 status, response_text
1647 )));
1648 }
1649 Ok(())
1650 }
1651
1652 async fn kv_write(
1653 &self,
1654 primary_namespace: &str,
1655 secondary_namespace: &str,
1656 key: &str,
1657 value: &[u8],
1658 ) -> Result<(), DatabaseError> {
1659 let encrypted = self.encrypt(value).await?;
1661
1662 let item = KVStoreTable {
1663 primary_namespace: primary_namespace.to_string(),
1664 secondary_namespace: secondary_namespace.to_string(),
1665 key: key.to_string(),
1666 value: hex::encode(encrypted),
1667 _extra: Default::default(),
1668 };
1669
1670 let (status, response_text) = self
1671 .post_request(
1672 "rest/v1/kv_store?on_conflict=primary_namespace,secondary_namespace,key,wallet_id",
1673 &item,
1674 )
1675 .await?;
1676
1677 if !status.is_success() {
1678 return Err(DatabaseError::Internal(format!(
1679 "kv_write failed: HTTP {} - {}",
1680 status, response_text
1681 )));
1682 }
1683 Ok(())
1684 }
1685
1686 async fn kv_remove(
1687 &self,
1688 primary_namespace: &str,
1689 secondary_namespace: &str,
1690 key: &str,
1691 ) -> Result<(), DatabaseError> {
1692 let path = format!(
1693 "rest/v1/kv_store?primary_namespace=eq.{}&secondary_namespace=eq.{}&key=eq.{}",
1694 url_encode(primary_namespace),
1695 url_encode(secondary_namespace),
1696 url_encode(key)
1697 );
1698 let (status, response_text) = self.delete_request(&path).await?;
1699
1700 if !status.is_success() {
1701 return Err(DatabaseError::Internal(format!(
1702 "kv_remove failed: HTTP {} - {}",
1703 status, response_text
1704 )));
1705 }
1706 Ok(())
1707 }
1708
1709 async fn kv_read(
1710 &self,
1711 primary_namespace: &str,
1712 secondary_namespace: &str,
1713 key: &str,
1714 ) -> Result<Option<Vec<u8>>, DatabaseError> {
1715 KVStoreDatabase::kv_read(self, primary_namespace, secondary_namespace, key).await
1717 }
1718
1719 async fn kv_list(
1720 &self,
1721 primary_namespace: &str,
1722 secondary_namespace: &str,
1723 ) -> Result<Vec<String>, DatabaseError> {
1724 KVStoreDatabase::kv_list(self, primary_namespace, secondary_namespace).await
1726 }
1727
1728 async fn add_saga(&self, saga: WalletSaga) -> Result<(), DatabaseError> {
1731 let saga_json = serde_json::to_string(&saga)
1732 .map_err(|e| DatabaseError::Internal(format!("Serialize saga: {e}")))?;
1733
1734 let item = SagaTable {
1735 id: saga.id.to_string(),
1736 data: saga_json,
1737 version: saga.version as i32,
1738 completed: false,
1739 created_at: saga.created_at as i64,
1740 updated_at: saga.updated_at as i64,
1741 _extra: Default::default(),
1742 };
1743
1744 let (status, response_text) = self
1745 .post_request("rest/v1/saga?on_conflict=id,wallet_id", &item)
1746 .await?;
1747
1748 if !status.is_success() {
1749 return Err(DatabaseError::Internal(format!(
1750 "add_saga failed: HTTP {} - {}",
1751 status, response_text
1752 )));
1753 }
1754 Ok(())
1755 }
1756
1757 async fn get_saga(&self, id: &uuid::Uuid) -> Result<Option<WalletSaga>, DatabaseError> {
1758 let path = format!("rest/v1/saga?id=eq.{}", url_encode(&id.to_string()));
1759 let (status, text) = self.get_request(&path).await?;
1760
1761 if !status.is_success() {
1762 return Err(DatabaseError::Internal(format!(
1763 "get_saga failed: HTTP {}",
1764 status
1765 )));
1766 }
1767
1768 if let Some(items) = Self::parse_response::<SagaTable>(&text)? {
1769 if let Some(item) = items.into_iter().next() {
1770 let saga: WalletSaga = serde_json::from_str(&item.data)
1771 .map_err(|e| DatabaseError::Internal(format!("Deserialize saga: {e}")))?;
1772 return Ok(Some(saga));
1773 }
1774 }
1775 Ok(None)
1776 }
1777
1778 async fn update_saga(&self, saga: WalletSaga) -> Result<bool, DatabaseError> {
1779 let expected_version = saga.version.saturating_sub(1);
1780 let saga_json = serde_json::to_string(&saga)
1781 .map_err(|e| DatabaseError::Internal(format!("Serialize saga: {e}")))?;
1782
1783 let item = SagaTable {
1784 id: saga.id.to_string(),
1785 data: saga_json,
1786 version: saga.version as i32,
1787 completed: false,
1788 created_at: saga.created_at as i64,
1789 updated_at: saga.updated_at as i64,
1790 _extra: Default::default(),
1791 };
1792
1793 let path = format!(
1795 "rest/v1/saga?id=eq.{}&version=eq.{}",
1796 url_encode(&saga.id.to_string()),
1797 expected_version
1798 );
1799
1800 let (status, response_text) = self.patch_request(&path, &item).await?;
1801
1802 if !status.is_success() {
1803 return Err(DatabaseError::Internal(format!(
1804 "update_saga failed: HTTP {} - {}",
1805 status, response_text
1806 )));
1807 }
1808
1809 let current = self.get_saga(&saga.id).await?;
1813 match current {
1814 Some(s) => Ok(s.version == saga.version),
1815 None => Ok(false),
1816 }
1817 }
1818
1819 async fn delete_saga(&self, id: &uuid::Uuid) -> Result<(), DatabaseError> {
1820 let path = format!("rest/v1/saga?id=eq.{}", url_encode(&id.to_string()));
1821 let (status, response_text) = self.delete_request(&path).await?;
1822
1823 if !status.is_success() {
1824 return Err(DatabaseError::Internal(format!(
1825 "delete_saga failed: HTTP {} - {}",
1826 status, response_text
1827 )));
1828 }
1829 Ok(())
1830 }
1831
1832 async fn get_incomplete_sagas(&self) -> Result<Vec<WalletSaga>, DatabaseError> {
1833 let path = "rest/v1/saga?completed=eq.false&order=created_at.asc";
1834 let (status, text) = self.get_request(path).await?;
1835
1836 if !status.is_success() {
1837 return Err(DatabaseError::Internal(format!(
1838 "get_incomplete_sagas failed: HTTP {}",
1839 status
1840 )));
1841 }
1842
1843 if let Some(items) = Self::parse_response::<SagaTable>(&text)? {
1844 let mut sagas = Vec::new();
1845 for item in items {
1846 let saga: WalletSaga = serde_json::from_str(&item.data)
1847 .map_err(|e| DatabaseError::Internal(format!("Deserialize saga: {e}")))?;
1848 sagas.push(saga);
1849 }
1850 Ok(sagas)
1851 } else {
1852 Ok(Vec::new())
1853 }
1854 }
1855
1856 async fn reserve_proofs(
1859 &self,
1860 ys: Vec<PublicKey>,
1861 operation_id: &uuid::Uuid,
1862 ) -> Result<(), DatabaseError> {
1863 let op_id_str = operation_id.to_string();
1864 for y in &ys {
1865 let y_hex = hex::encode(y.to_bytes());
1866
1867 let update = serde_json::json!({
1869 "state": State::Reserved.to_string(),
1870 "used_by_operation": op_id_str,
1871 });
1872
1873 let patch_path = format!(
1876 "rest/v1/proof?y=eq.{}&state=eq.{}",
1877 url_encode(&y_hex),
1878 url_encode(&State::Unspent.to_string())
1879 );
1880
1881 let (status, response_text) = self.patch_request(&patch_path, &update).await?;
1882
1883 if !status.is_success() {
1884 return Err(DatabaseError::Internal(format!(
1885 "reserve_proofs: update failed: HTTP {} - {}",
1886 status, response_text
1887 )));
1888 }
1889
1890 let reserved_proofs = self.get_reserved_proofs(operation_id).await?;
1894 if !reserved_proofs.iter().any(|p| p.y == *y) {
1895 return Err(DatabaseError::ProofNotUnspent);
1896 }
1897 }
1898 Ok(())
1899 }
1900
1901 async fn release_proofs(&self, operation_id: &uuid::Uuid) -> Result<(), DatabaseError> {
1902 let op_id_str = operation_id.to_string();
1903
1904 let update = serde_json::json!({
1906 "state": State::Unspent.to_string(),
1907 "used_by_operation": null,
1908 });
1909 let path = format!(
1910 "rest/v1/proof?used_by_operation=eq.{}",
1911 url_encode(&op_id_str)
1912 );
1913 let (status, response_text) = self.patch_request(&path, &update).await?;
1914
1915 if !status.is_success() {
1916 return Err(DatabaseError::Internal(format!(
1917 "release_proofs failed: HTTP {} - {}",
1918 status, response_text
1919 )));
1920 }
1921 Ok(())
1922 }
1923
1924 async fn get_reserved_proofs(
1925 &self,
1926 operation_id: &uuid::Uuid,
1927 ) -> Result<Vec<ProofInfo>, DatabaseError> {
1928 let op_id_str = operation_id.to_string();
1929 let path = format!(
1930 "rest/v1/proof?used_by_operation=eq.{}",
1931 url_encode(&op_id_str)
1932 );
1933 let (status, text) = self.get_request(&path).await?;
1934
1935 if !status.is_success() {
1936 return Err(DatabaseError::Internal(format!(
1937 "get_reserved_proofs failed: HTTP {}",
1938 status
1939 )));
1940 }
1941
1942 if let Some(items) = Self::parse_response::<ProofTable>(&text)? {
1943 let mut proofs = Vec::with_capacity(items.len());
1944 for mut p in items {
1945 self.decrypt_proof_table(&mut p).await;
1946 proofs.push(p.try_into()?);
1947 }
1948 Ok(proofs)
1949 } else {
1950 Ok(Vec::new())
1951 }
1952 }
1953
1954 async fn reserve_melt_quote(
1957 &self,
1958 quote_id: &str,
1959 operation_id: &uuid::Uuid,
1960 ) -> Result<(), DatabaseError> {
1961 let op_id_str = operation_id.to_string();
1962
1963 let update = serde_json::json!({
1964 "used_by_operation": op_id_str,
1965 });
1966
1967 let patch_path = format!(
1970 "rest/v1/melt_quote?id=eq.{}&used_by_operation=is.null",
1971 url_encode(quote_id)
1972 );
1973
1974 let (status, response_text) = self.patch_request(&patch_path, &update).await?;
1975
1976 if !status.is_success() {
1977 return Err(DatabaseError::Internal(format!(
1978 "reserve_melt_quote failed: HTTP {} - {}",
1979 status, response_text
1980 )));
1981 }
1982
1983 let quote = self.get_melt_quote(quote_id).await?;
1985 match quote {
1986 Some(q) => {
1987 if q.used_by_operation.as_deref() == Some(&op_id_str) {
1988 Ok(())
1989 } else {
1990 Err(DatabaseError::QuoteAlreadyInUse)
1992 }
1993 }
1994 None => Err(DatabaseError::UnknownQuote),
1995 }
1996 }
1997
1998 async fn release_melt_quote(&self, operation_id: &uuid::Uuid) -> Result<(), DatabaseError> {
1999 let op_id_str = operation_id.to_string();
2000
2001 let update = serde_json::json!({
2002 "used_by_operation": null,
2003 });
2004 let path = format!(
2005 "rest/v1/melt_quote?used_by_operation=eq.{}",
2006 url_encode(&op_id_str)
2007 );
2008 let (status, response_text) = self.patch_request(&path, &update).await?;
2009
2010 if !status.is_success() {
2011 return Err(DatabaseError::Internal(format!(
2012 "release_melt_quote failed: HTTP {} - {}",
2013 status, response_text
2014 )));
2015 }
2016 Ok(())
2017 }
2018
2019 async fn reserve_mint_quote(
2020 &self,
2021 quote_id: &str,
2022 operation_id: &uuid::Uuid,
2023 ) -> Result<(), DatabaseError> {
2024 let op_id_str = operation_id.to_string();
2025
2026 let update = serde_json::json!({
2027 "used_by_operation": op_id_str,
2028 });
2029
2030 let patch_path = format!(
2033 "rest/v1/mint_quote?id=eq.{}&used_by_operation=is.null",
2034 url_encode(quote_id)
2035 );
2036
2037 let (status, response_text) = self.patch_request(&patch_path, &update).await?;
2038
2039 if !status.is_success() {
2040 return Err(DatabaseError::Internal(format!(
2041 "reserve_mint_quote failed: HTTP {} - {}",
2042 status, response_text
2043 )));
2044 }
2045
2046 let quote = self.get_mint_quote(quote_id).await?;
2048 match quote {
2049 Some(q) => {
2050 if q.used_by_operation.as_deref() == Some(&op_id_str) {
2051 Ok(())
2052 } else {
2053 Err(DatabaseError::QuoteAlreadyInUse)
2055 }
2056 }
2057 None => Err(DatabaseError::UnknownQuote),
2058 }
2059 }
2060
2061 async fn release_mint_quote(&self, operation_id: &uuid::Uuid) -> Result<(), DatabaseError> {
2062 let op_id_str = operation_id.to_string();
2063
2064 let update = serde_json::json!({
2065 "used_by_operation": null,
2066 });
2067 let path = format!(
2068 "rest/v1/mint_quote?used_by_operation=eq.{}",
2069 url_encode(&op_id_str)
2070 );
2071 let (status, response_text) = self.patch_request(&path, &update).await?;
2072
2073 if !status.is_success() {
2074 return Err(DatabaseError::Internal(format!(
2075 "release_mint_quote failed: HTTP {} - {}",
2076 status, response_text
2077 )));
2078 }
2079 Ok(())
2080 }
2081
2082 async fn add_p2pk_key(
2083 &self,
2084 pubkey: &PublicKey,
2085 derivation_path: DerivationPath,
2086 derivation_index: u32,
2087 ) -> Result<(), DatabaseError> {
2088 let created_time = SystemTime::now()
2089 .duration_since(UNIX_EPOCH)
2090 .map_err(|e| DatabaseError::Internal(format!("SystemTime error: {}", e)))?
2091 .as_secs();
2092
2093 let item = P2PKSigningKeyTable {
2094 pubkey: hex::encode(pubkey.to_bytes()),
2095 derivation_index: derivation_index as i64,
2096 derivation_path: derivation_path.to_string(),
2097 created_time: created_time as i64,
2098 _extra: Default::default(),
2099 };
2100
2101 let (status, response_text) = self
2102 .post_request(
2103 "rest/v1/p2pk_signing_key?on_conflict=pubkey,wallet_id",
2104 &item,
2105 )
2106 .await?;
2107
2108 if !status.is_success() {
2109 return Err(DatabaseError::Internal(format!(
2110 "add_p2pk_key failed: HTTP {} - {}",
2111 status, response_text
2112 )));
2113 }
2114 Ok(())
2115 }
2116
2117 async fn get_p2pk_key(
2118 &self,
2119 pubkey: &PublicKey,
2120 ) -> Result<Option<wallet::P2PKSigningKey>, DatabaseError> {
2121 let path = format!(
2122 "rest/v1/p2pk_signing_key?pubkey=eq.{}",
2123 url_encode(&hex::encode(pubkey.to_bytes()))
2124 );
2125 let (status, text) = self.get_request(&path).await?;
2126
2127 if status == StatusCode::NOT_FOUND {
2128 return Ok(None);
2129 }
2130 if !status.is_success() {
2131 return Err(DatabaseError::Internal(format!(
2132 "get_p2pk_key failed: HTTP {}",
2133 status
2134 )));
2135 }
2136
2137 if let Some(rows) = Self::parse_response::<P2PKSigningKeyTable>(&text)? {
2138 if let Some(row) = rows.into_iter().next() {
2139 return Ok(Some(row.try_into()?));
2140 }
2141 }
2142 Ok(None)
2143 }
2144
2145 async fn list_p2pk_keys(&self) -> Result<Vec<wallet::P2PKSigningKey>, DatabaseError> {
2146 let path = "rest/v1/p2pk_signing_key?order=derivation_index.desc";
2147 let (status, text) = self.get_request(path).await?;
2148
2149 if !status.is_success() {
2150 return Err(DatabaseError::Internal(format!(
2151 "list_p2pk_keys failed: HTTP {}",
2152 status
2153 )));
2154 }
2155
2156 if let Some(rows) = Self::parse_response::<P2PKSigningKeyTable>(&text)? {
2157 rows.into_iter()
2158 .map(|row| row.try_into())
2159 .collect::<Result<Vec<_>, _>>()
2160 } else {
2161 Ok(Vec::new())
2162 }
2163 }
2164
2165 async fn latest_p2pk(&self) -> Result<Option<wallet::P2PKSigningKey>, DatabaseError> {
2166 let path = "rest/v1/p2pk_signing_key?order=derivation_index.desc&limit=1";
2167 let (status, text) = self.get_request(path).await?;
2168
2169 if !status.is_success() {
2170 return Err(DatabaseError::Internal(format!(
2171 "latest_p2pk failed: HTTP {}",
2172 status
2173 )));
2174 }
2175
2176 if let Some(rows) = Self::parse_response::<P2PKSigningKeyTable>(&text)? {
2177 if let Some(row) = rows.into_iter().next() {
2178 return Ok(Some(row.try_into()?));
2179 }
2180 }
2181 Ok(None)
2182 }
2183}
2184
2185#[derive(Debug, Serialize, Deserialize)]
2191struct KVStoreTable {
2192 primary_namespace: String,
2193 secondary_namespace: String,
2194 key: String,
2195 value: String, #[serde(default, skip_serializing, flatten)]
2198 _extra: serde_json::Map<String, serde_json::Value>,
2199}
2200
2201#[derive(Debug, Serialize, Deserialize, Default)]
2202struct MintTable {
2203 mint_url: String,
2204 name: Option<String>,
2205 pubkey: Option<String>,
2206 version: Option<String>,
2207 description: Option<String>,
2208 description_long: Option<String>,
2209 contact: Option<String>,
2210 nuts: Option<String>,
2211 icon_url: Option<String>,
2212 urls: Option<String>,
2213 motd: Option<String>,
2214 mint_time: Option<i64>,
2215 tos_url: Option<String>,
2216 #[serde(default, skip_serializing, flatten)]
2218 _extra: serde_json::Map<String, serde_json::Value>,
2219}
2220
2221impl MintTable {
2222 fn from_info(mint_url: MintUrl, info: MintInfo) -> Result<Self, DatabaseError> {
2223 Ok(Self {
2224 mint_url: mint_url.to_string(),
2225 name: info.name,
2226 pubkey: info.pubkey.map(|p| hex::encode(p.to_bytes())),
2227 version: info
2228 .version
2229 .map(|v| serde_json::to_string(&v))
2230 .transpose()?,
2231 description: info.description,
2232 description_long: info.description_long,
2233 contact: info
2234 .contact
2235 .map(|c| serde_json::to_string(&c))
2236 .transpose()?,
2237 nuts: Some(serde_json::to_string(&info.nuts)?),
2238 icon_url: info.icon_url,
2239 urls: info.urls.map(|u| serde_json::to_string(&u)).transpose()?,
2240 motd: info.motd,
2241 mint_time: info.time.map(|t| t as i64),
2242 tos_url: info.tos_url,
2243 _extra: Default::default(),
2244 })
2245 }
2246}
2247
2248impl TryInto<MintInfo> for MintTable {
2249 type Error = DatabaseError;
2250 fn try_into(self) -> Result<MintInfo, Self::Error> {
2251 fn parse_json_field<T: serde::de::DeserializeOwned>(
2253 field: Option<String>,
2254 ) -> Result<Option<T>, serde_json::Error> {
2255 match field {
2256 Some(s) if !s.trim().is_empty() => {
2257 let s = s.trim();
2258 match serde_json::from_str::<T>(s) {
2259 Ok(v) => Ok(Some(v)),
2260 Err(e) => {
2261 if !s.starts_with('{') && !s.starts_with('[') && !s.starts_with('"') {
2264 let quoted = format!("\"{}\"", s);
2265 if let Ok(v) = serde_json::from_str::<T>("ed) {
2266 return Ok(Some(v));
2267 }
2268 }
2269 Err(e)
2270 }
2271 }
2272 }
2273 _ => Ok(None),
2274 }
2275 }
2276
2277 Ok(MintInfo {
2278 name: self.name,
2279 pubkey: self
2280 .pubkey
2281 .map(|p| {
2282 PublicKey::from_hex(&p)
2283 .map_err(|_| DatabaseError::Internal("Invalid pubkey hex".into()))
2284 })
2285 .transpose()?,
2286 version: parse_json_field(self.version)?,
2287 description: self.description,
2288 description_long: self.description_long,
2289 contact: parse_json_field(self.contact)?,
2290 nuts: parse_json_field(self.nuts)?.unwrap_or_default(),
2291 icon_url: self.icon_url,
2292 urls: parse_json_field(self.urls)?,
2293 motd: self.motd,
2294 time: self.mint_time.map(|t| t as u64),
2295 tos_url: self.tos_url,
2296 })
2297 }
2298}
2299
2300#[derive(Debug, Serialize, Deserialize)]
2301struct KeySetTable {
2302 mint_url: String,
2303 id: String,
2304 unit: String,
2305 active: bool,
2306 input_fee_ppk: i64,
2307 final_expiry: Option<i64>,
2308 keyset_u32: Option<i64>,
2309 #[serde(default, skip_serializing, flatten)]
2311 _extra: serde_json::Map<String, serde_json::Value>,
2312}
2313
2314impl KeySetTable {
2315 fn from_info(mint_url: MintUrl, info: KeySetInfo) -> Result<Self, DatabaseError> {
2316 Ok(Self {
2317 mint_url: mint_url.to_string(),
2318 id: info.id.to_string(),
2319 unit: info.unit.to_string(),
2320 active: info.active,
2321 input_fee_ppk: info.input_fee_ppk as i64,
2322 final_expiry: info.final_expiry.map(|v| v as i64),
2323 keyset_u32: Some(u32::from(info.id) as i64),
2324 _extra: Default::default(),
2325 })
2326 }
2327}
2328
2329impl TryInto<KeySetInfo> for KeySetTable {
2330 type Error = DatabaseError;
2331 fn try_into(self) -> Result<KeySetInfo, Self::Error> {
2332 Ok(KeySetInfo {
2333 id: Id::from_str(&self.id).map_err(|_| DatabaseError::InvalidKeysetId)?,
2334 unit: CurrencyUnit::from_str(&self.unit)
2335 .map_err(|_| DatabaseError::Internal("Invalid unit".into()))?,
2336 active: self.active,
2337 input_fee_ppk: self.input_fee_ppk as u64,
2338 final_expiry: self.final_expiry.map(|v| v as u64),
2339 })
2340 }
2341}
2342
2343#[derive(Debug, Serialize, Deserialize)]
2344struct P2PKSigningKeyTable {
2345 pubkey: String,
2346 derivation_index: i64,
2347 derivation_path: String,
2348 created_time: i64,
2349 #[serde(default, skip_serializing, flatten)]
2351 _extra: serde_json::Map<String, serde_json::Value>,
2352}
2353
2354impl TryInto<wallet::P2PKSigningKey> for P2PKSigningKeyTable {
2355 type Error = DatabaseError;
2356 fn try_into(self) -> Result<wallet::P2PKSigningKey, Self::Error> {
2357 Ok(wallet::P2PKSigningKey {
2358 pubkey: PublicKey::from_hex(&self.pubkey)
2359 .map_err(|_| DatabaseError::Internal("Invalid pubkey hex".into()))?,
2360 derivation_path: DerivationPath::from_str(&self.derivation_path)
2361 .map_err(|_| DatabaseError::Internal("Invalid derivation path".into()))?,
2362 derivation_index: self.derivation_index as u32,
2363 created_time: self.created_time as u64,
2364 })
2365 }
2366}
2367
2368#[derive(Debug, Serialize, Deserialize)]
2369struct KeyTable {
2370 id: String,
2371 keys: String, keyset_u32: Option<i64>,
2373 #[serde(default, skip_serializing, flatten)]
2375 _extra: serde_json::Map<String, serde_json::Value>,
2376}
2377
2378impl KeyTable {
2379 fn from_keyset(keyset: &KeySet) -> Result<Self, DatabaseError> {
2380 Ok(Self {
2381 id: keyset.id.to_string(),
2382 keys: serde_json::to_string(&keyset.keys)?,
2383 keyset_u32: Some(u32::from(keyset.id) as i64),
2384 _extra: Default::default(),
2385 })
2386 }
2387}
2388
2389impl TryInto<Keys> for KeyTable {
2390 type Error = DatabaseError;
2391 fn try_into(self) -> Result<Keys, Self::Error> {
2392 Ok(serde_json::from_str(&self.keys)?)
2393 }
2394}
2395
2396#[derive(Debug, Serialize, Deserialize)]
2397struct MintQuoteTable {
2398 id: String,
2399 mint_url: String,
2400 amount: i64,
2401 unit: String,
2402 request: Option<String>,
2403 state: String,
2404 expiry: i64,
2405 secret_key: Option<String>,
2406 payment_method: String,
2407 amount_issued: i64,
2408 amount_paid: i64,
2409 #[serde(default)]
2410 used_by_operation: Option<String>,
2411 #[serde(default)]
2412 version: Option<i32>,
2413 #[serde(default, skip_serializing, flatten)]
2415 _extra: serde_json::Map<String, serde_json::Value>,
2416}
2417
2418impl TryInto<MintQuote> for MintQuoteTable {
2419 type Error = DatabaseError;
2420 fn try_into(self) -> Result<MintQuote, Self::Error> {
2421 Ok(MintQuote {
2422 id: self.id,
2423 mint_url: MintUrl::from_str(&self.mint_url)
2424 .map_err(|e| DatabaseError::Internal(e.to_string()))?,
2425 amount: Some(cdk_common::Amount::from(self.amount as u64)),
2426 unit: CurrencyUnit::from_str(&self.unit)
2427 .map_err(|_| DatabaseError::Internal("Invalid unit".into()))?,
2428 request: self
2429 .request
2430 .ok_or(DatabaseError::Internal("Missing request".into()))?,
2431 state: cdk_common::nuts::MintQuoteState::from_str(&self.state)
2432 .map_err(|_| DatabaseError::Internal("Invalid state".into()))?,
2433 expiry: self.expiry as u64,
2434 secret_key: self
2435 .secret_key
2436 .map(|k| cdk_common::nuts::SecretKey::from_str(&k))
2437 .transpose()
2438 .map_err(|_| DatabaseError::Internal("Invalid secret key".into()))?,
2439 payment_method: cdk_common::PaymentMethod::from_str(&self.payment_method)
2440 .map_err(|_| DatabaseError::Internal("Invalid payment method".into()))?,
2441 amount_issued: cdk_common::Amount::from(self.amount_issued as u64),
2442 amount_paid: cdk_common::Amount::from(self.amount_paid as u64),
2443 used_by_operation: self.used_by_operation,
2444 version: self.version.unwrap_or(0) as u32,
2445 })
2446 }
2447}
2448
2449impl TryFrom<MintQuote> for MintQuoteTable {
2450 type Error = DatabaseError;
2451 fn try_from(q: MintQuote) -> Result<Self, Self::Error> {
2452 Ok(Self {
2453 id: q.id,
2454 mint_url: q.mint_url.to_string(),
2455 amount: q.amount.map(|a| a.to_u64() as i64).unwrap_or(0),
2456 unit: q.unit.to_string(),
2457 request: Some(q.request),
2458 state: q.state.to_string(),
2459 expiry: q.expiry as i64,
2460 secret_key: q.secret_key.map(|k| k.to_string()),
2461 payment_method: q.payment_method.to_string(),
2462 amount_issued: q.amount_issued.to_u64() as i64,
2463 amount_paid: q.amount_paid.to_u64() as i64,
2464 used_by_operation: q.used_by_operation,
2465 version: Some(q.version as i32),
2466 _extra: Default::default(),
2467 })
2468 }
2469}
2470
2471#[derive(Debug, Serialize, Deserialize)]
2472struct MeltQuoteTable {
2473 id: String,
2474 unit: String,
2475 amount: i64,
2476 request: String,
2477 fee_reserve: i64,
2478 state: String,
2479 expiry: i64,
2480 payment_preimage: Option<String>,
2481 payment_method: String,
2482 #[serde(default)]
2483 mint_url: Option<String>,
2484 #[serde(default)]
2485 used_by_operation: Option<String>,
2486 #[serde(default)]
2487 version: Option<i32>,
2488 #[serde(default, skip_serializing, flatten)]
2490 _extra: serde_json::Map<String, serde_json::Value>,
2491}
2492
2493impl TryInto<wallet::MeltQuote> for MeltQuoteTable {
2494 type Error = DatabaseError;
2495 fn try_into(self) -> Result<wallet::MeltQuote, Self::Error> {
2496 Ok(wallet::MeltQuote {
2497 id: self.id,
2498 mint_url: self
2499 .mint_url
2500 .as_deref()
2501 .map(cdk_common::mint_url::MintUrl::from_str)
2502 .transpose()
2503 .map_err(|_| DatabaseError::Internal("Invalid mint URL".into()))?,
2504 unit: CurrencyUnit::from_str(&self.unit)
2505 .map_err(|_| DatabaseError::Internal("Invalid unit".into()))?,
2506 amount: cdk_common::Amount::from(self.amount as u64),
2507 request: self.request,
2508 fee_reserve: cdk_common::Amount::from(self.fee_reserve as u64),
2509 state: cdk_common::nuts::MeltQuoteState::from_str(&self.state)
2510 .map_err(|_| DatabaseError::Internal("Invalid state".into()))?,
2511 expiry: self.expiry as u64,
2512 payment_preimage: self.payment_preimage,
2513 payment_method: cdk_common::PaymentMethod::from_str(&self.payment_method)
2514 .map_err(|_| DatabaseError::Internal("Invalid payment method".into()))?,
2515 used_by_operation: self.used_by_operation,
2516 version: self.version.unwrap_or(0) as u32,
2517 })
2518 }
2519}
2520
2521impl TryFrom<wallet::MeltQuote> for MeltQuoteTable {
2522 type Error = DatabaseError;
2523 fn try_from(q: wallet::MeltQuote) -> Result<Self, Self::Error> {
2524 Ok(Self {
2525 id: q.id,
2526 mint_url: q.mint_url.map(|u| u.to_string()),
2527 unit: q.unit.to_string(),
2528 amount: q.amount.to_u64() as i64,
2529 request: q.request,
2530 fee_reserve: q.fee_reserve.to_u64() as i64,
2531 state: q.state.to_string(),
2532 expiry: q.expiry as i64,
2533 payment_preimage: q.payment_preimage,
2534 payment_method: q.payment_method.to_string(),
2535 used_by_operation: q.used_by_operation,
2536 version: Some(q.version as i32),
2537 _extra: Default::default(),
2538 })
2539 }
2540}
2541
2542#[derive(Debug, Serialize, Deserialize)]
2543struct ProofTable {
2544 y: String,
2545 mint_url: String,
2546 state: String,
2547 spending_condition: Option<String>,
2548 unit: String,
2549 amount: i64,
2550 keyset_id: String,
2551 secret: String,
2552 c: String,
2553 witness: Option<String>,
2554 dleq_e: Option<String>,
2555 dleq_s: Option<String>,
2556 dleq_r: Option<String>,
2557 #[serde(default)]
2558 used_by_operation: Option<String>,
2559 #[serde(default)]
2560 created_by_operation: Option<String>,
2561 #[serde(default)]
2562 p2pk_e: Option<String>,
2563 #[serde(default, skip_serializing, flatten)]
2565 _extra: serde_json::Map<String, serde_json::Value>,
2566}
2567
2568impl TryInto<ProofInfo> for ProofTable {
2569 type Error = DatabaseError;
2570 fn try_into(self) -> Result<ProofInfo, Self::Error> {
2571 let y = PublicKey::from_hex(&self.y)
2572 .map_err(|_| DatabaseError::Internal("Invalid y".into()))?;
2573 let c = PublicKey::from_hex(&self.c)
2574 .map_err(|_| DatabaseError::Internal("Invalid c".into()))?;
2575 Ok(ProofInfo {
2576 y,
2577 mint_url: MintUrl::from_str(&self.mint_url)
2578 .map_err(|e| DatabaseError::Internal(e.to_string()))?,
2579 state: cdk_common::nuts::State::from_str(&self.state)
2580 .map_err(|_| DatabaseError::Internal("Invalid state".into()))?,
2581 spending_condition: self
2582 .spending_condition
2583 .filter(|s| !s.trim().is_empty())
2584 .map(|s| serde_json::from_str(&s))
2585 .transpose()?,
2586 unit: CurrencyUnit::from_str(&self.unit)
2587 .map_err(|_| DatabaseError::Internal("Invalid unit".into()))?,
2588 proof: cdk_common::Proof {
2589 amount: cdk_common::Amount::from(self.amount as u64),
2590 keyset_id: Id::from_str(&self.keyset_id)
2591 .map_err(|_| DatabaseError::InvalidKeysetId)?,
2592 secret: Secret::from_str(&self.secret)
2593 .map_err(|_| DatabaseError::Internal("Invalid secret".into()))?,
2594 c,
2595 witness: self
2596 .witness
2597 .filter(|w| !w.trim().is_empty())
2598 .map(|w| serde_json::from_str(&w))
2599 .transpose()?,
2600 dleq: match (self.dleq_e, self.dleq_s, self.dleq_r) {
2601 (Some(e), Some(s), Some(r)) => Some(cdk_common::ProofDleq {
2602 e: cdk_common::SecretKey::from_hex(&e)
2603 .map_err(|_| DatabaseError::Internal("Invalid dleq_e".into()))?,
2604 s: cdk_common::SecretKey::from_hex(&s)
2605 .map_err(|_| DatabaseError::Internal("Invalid dleq_s".into()))?,
2606 r: cdk_common::SecretKey::from_hex(&r)
2607 .map_err(|_| DatabaseError::Internal("Invalid dleq_r".into()))?,
2608 }),
2609 _ => None,
2610 },
2611 p2pk_e: self
2612 .p2pk_e
2613 .map(|s| PublicKey::from_hex(&s))
2614 .transpose()
2615 .map_err(|_| DatabaseError::Internal("Invalid p2pk_e".into()))?,
2616 },
2617 used_by_operation: self
2618 .used_by_operation
2619 .map(|s| uuid::Uuid::parse_str(&s))
2620 .transpose()
2621 .map_err(|_| DatabaseError::Internal("Invalid used_by_operation uuid".into()))?,
2622 created_by_operation: self
2623 .created_by_operation
2624 .map(|s| uuid::Uuid::parse_str(&s))
2625 .transpose()
2626 .map_err(|_| DatabaseError::Internal("Invalid created_by_operation uuid".into()))?,
2627 })
2628 }
2629}
2630
2631impl TryFrom<ProofInfo> for ProofTable {
2632 type Error = DatabaseError;
2633 fn try_from(p: ProofInfo) -> Result<Self, Self::Error> {
2634 Ok(Self {
2635 y: hex::encode(p.y.to_bytes()),
2636 mint_url: p.mint_url.to_string(),
2637 state: p.state.to_string(),
2638 spending_condition: p
2639 .spending_condition
2640 .map(|s| serde_json::to_string(&s))
2641 .transpose()?,
2642 unit: p.unit.to_string(),
2643 amount: p.proof.amount.to_u64() as i64,
2644 keyset_id: p.proof.keyset_id.to_string(),
2645 secret: p.proof.secret.to_string(),
2646 c: hex::encode(p.proof.c.to_bytes()),
2647 witness: p
2648 .proof
2649 .witness
2650 .map(|w| serde_json::to_string(&w))
2651 .transpose()?,
2652 dleq_e: p
2653 .proof
2654 .dleq
2655 .as_ref()
2656 .map(|d| hex::encode(d.e.to_secret_bytes())),
2657 dleq_s: p
2658 .proof
2659 .dleq
2660 .as_ref()
2661 .map(|d| hex::encode(d.s.to_secret_bytes())),
2662 dleq_r: p
2663 .proof
2664 .dleq
2665 .as_ref()
2666 .map(|d| hex::encode(d.r.to_secret_bytes())),
2667 used_by_operation: p.used_by_operation.map(|u| u.to_string()),
2668 created_by_operation: p.created_by_operation.map(|u| u.to_string()),
2669 p2pk_e: p.proof.p2pk_e.map(|e| hex::encode(e.to_bytes())),
2670 _extra: Default::default(),
2671 })
2672 }
2673}
2674
2675#[derive(Debug, Serialize, Deserialize)]
2676struct TransactionTable {
2677 id: String,
2678 mint_url: String,
2679 direction: String,
2680 unit: String,
2681 amount: i64,
2682 fee: i64,
2683 ys: Option<Vec<String>>,
2684 timestamp: i64,
2685 memo: Option<String>,
2686 metadata: Option<String>,
2687 quote_id: Option<String>,
2688 payment_request: Option<String>,
2689 payment_proof: Option<String>,
2690 payment_method: Option<String>,
2691 #[serde(default)]
2692 saga_id: Option<String>,
2693 #[serde(default, skip_serializing, flatten)]
2695 _extra: serde_json::Map<String, serde_json::Value>,
2696}
2697
2698impl TryInto<Transaction> for TransactionTable {
2699 type Error = DatabaseError;
2700 fn try_into(self) -> Result<Transaction, Self::Error> {
2701 let id_bytes = hex::decode(&self.id)
2702 .map_err(|_| DatabaseError::Internal("Invalid transaction id hex".into()))?;
2703 let _id_arr: [u8; 32] = id_bytes
2704 .try_into()
2705 .map_err(|_| DatabaseError::Internal("Invalid transaction id len".into()))?;
2706
2707 let ys = match self.ys {
2708 Some(strs) => strs
2709 .into_iter()
2710 .map(|s| {
2711 PublicKey::from_hex(&s)
2712 .map_err(|_| DatabaseError::Internal("Invalid y hex".into()))
2713 })
2714 .collect::<Result<Vec<_>, _>>()?,
2715 None => vec![],
2716 };
2717
2718 Ok(Transaction {
2719 mint_url: MintUrl::from_str(&self.mint_url)
2720 .map_err(|e| DatabaseError::Internal(e.to_string()))?,
2721 direction: TransactionDirection::from_str(&self.direction)
2722 .map_err(|_| DatabaseError::Internal("Invalid direction".into()))?,
2723 unit: CurrencyUnit::from_str(&self.unit)
2724 .map_err(|_| DatabaseError::Internal("Invalid unit".into()))?,
2725 amount: cdk_common::Amount::from(self.amount as u64),
2726 fee: cdk_common::Amount::from(self.fee as u64),
2727 ys,
2728 timestamp: self.timestamp as u64,
2729 memo: self.memo,
2730 metadata: self
2731 .metadata
2732 .filter(|m| !m.trim().is_empty())
2733 .map(|m| serde_json::from_str(&m))
2734 .transpose()?
2735 .unwrap_or_default(),
2736 quote_id: self.quote_id,
2737 payment_request: self.payment_request,
2738 payment_proof: self.payment_proof,
2739 payment_method: self
2740 .payment_method
2741 .map(|p| cdk_common::PaymentMethod::from_str(&p))
2742 .transpose()
2743 .map_err(|_| DatabaseError::Internal("Invalid payment method".into()))?,
2744 saga_id: self
2745 .saga_id
2746 .map(|s| uuid::Uuid::parse_str(&s))
2747 .transpose()
2748 .map_err(|_| DatabaseError::Internal("Invalid saga_id uuid".into()))?,
2749 })
2750 }
2751}
2752
2753impl TryFrom<Transaction> for TransactionTable {
2754 type Error = DatabaseError;
2755 fn try_from(t: Transaction) -> Result<Self, Self::Error> {
2756 Ok(Self {
2757 id: t.id().to_string(),
2758 mint_url: t.mint_url.to_string(),
2759 direction: t.direction.to_string(),
2760 unit: t.unit.to_string(),
2761 amount: t.amount.to_u64() as i64,
2762 fee: t.fee.to_u64() as i64,
2763 ys: Some(t.ys.iter().map(|y| hex::encode(y.to_bytes())).collect()),
2764 timestamp: t.timestamp as i64,
2765 memo: t.memo,
2766 metadata: if t.metadata.is_empty() {
2767 None
2768 } else {
2769 Some(serde_json::to_string(&t.metadata)?)
2770 },
2771 quote_id: t.quote_id,
2772 payment_request: t.payment_request,
2773 payment_proof: t.payment_proof,
2774 payment_method: t.payment_method.map(|p| p.to_string()),
2775 saga_id: t.saga_id.map(|u| u.to_string()),
2776 _extra: Default::default(),
2777 })
2778 }
2779}
2780
2781#[derive(Debug, Serialize, Deserialize)]
2782struct SagaTable {
2783 id: String,
2784 data: String, version: i32,
2786 completed: bool,
2787 created_at: i64,
2788 updated_at: i64,
2789 #[serde(default, skip_serializing, flatten)]
2791 _extra: serde_json::Map<String, serde_json::Value>,
2792}
2793
2794#[derive(Debug, Deserialize, Serialize, Clone)]
2796pub struct SupabaseAuthResponse {
2797 pub access_token: String,
2799 pub token_type: String,
2801 pub expires_in: Option<i64>,
2803 pub refresh_token: Option<String>,
2805 pub user: serde_json::Value,
2807}
2808
2809#[derive(Debug)]
2811pub struct SupabaseAuth;
2812
2813impl SupabaseAuth {
2814 pub async fn signup(
2816 url: &Url,
2817 api_key: &str,
2818 email: &str,
2819 password: &str,
2820 ) -> Result<SupabaseAuthResponse, Error> {
2821 let auth_url = url
2822 .join("auth/v1/signup")
2823 .map_err(|e| Error::Supabase(format!("Invalid auth URL: {}", e)))?;
2824
2825 let client = Client::new();
2826 let body = serde_json::json!({
2827 "email": email,
2828 "password": password
2829 });
2830
2831 let response = client
2832 .post(auth_url)
2833 .header("apikey", api_key)
2834 .header("Content-Type", "application/json")
2835 .json(&body)
2836 .send()
2837 .await
2838 .map_err(Error::Reqwest)?;
2839
2840 let status = response.status();
2841 if !status.is_success() {
2842 let text = response.text().await.unwrap_or_default();
2843 return Err(Error::Supabase(format!(
2844 "Supabase signup failed: HTTP {} - {}",
2845 status, text
2846 )));
2847 }
2848
2849 response.json().await.map_err(Error::Reqwest)
2850 }
2851
2852 pub async fn signin(
2854 url: &Url,
2855 api_key: &str,
2856 email: &str,
2857 password: &str,
2858 ) -> Result<SupabaseAuthResponse, Error> {
2859 let auth_url = url
2860 .join("auth/v1/token?grant_type=password")
2861 .map_err(|e| Error::Supabase(format!("Invalid auth URL: {}", e)))?;
2862
2863 let client = Client::new();
2864 let body = serde_json::json!({
2865 "email": email,
2866 "password": password
2867 });
2868
2869 let response = client
2870 .post(auth_url)
2871 .header("apikey", api_key)
2872 .header("Content-Type", "application/json")
2873 .json(&body)
2874 .send()
2875 .await
2876 .map_err(Error::Reqwest)?;
2877
2878 let status = response.status();
2879 if !status.is_success() {
2880 let text = response.text().await.unwrap_or_default();
2881 return Err(Error::Supabase(format!(
2882 "Supabase signin failed: HTTP {} - {}",
2883 status, text
2884 )));
2885 }
2886
2887 response.json().await.map_err(Error::Reqwest)
2888 }
2889}
2890
2891#[cfg(test)]
2892mod tests {
2893 use super::*;
2894
2895 #[test]
2896 fn test_set_encryption_password_key_derivation() {
2897 let key = sha256::Hash::hash(b"password");
2899 assert_eq!(
2900 hex::encode(key.as_byte_array()),
2901 "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
2902 );
2903 }
2904}