diff --git a/crates/cdk-common/src/mint.rs b/crates/cdk-common/src/mint.rs index 4bc01051..f38ed1da 100644 --- a/crates/cdk-common/src/mint.rs +++ b/crates/cdk-common/src/mint.rs @@ -1,6 +1,7 @@ //! Mint types use bitcoin::bip32::DerivationPath; +use cashu::util::unix_time; use cashu::{MeltQuoteBolt11Response, MintQuoteBolt11Response}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -27,6 +28,13 @@ pub struct MintQuote { pub request_lookup_id: String, /// Pubkey pub pubkey: Option, + /// Unix time quote was created + #[serde(default)] + pub created_time: u64, + /// Unix time quote was paid + pub paid_time: Option, + /// Unix time quote was issued + pub issued_time: Option, } impl MintQuote { @@ -50,6 +58,9 @@ impl MintQuote { expiry, request_lookup_id, pubkey, + created_time: unix_time(), + paid_time: None, + issued_time: None, } } } @@ -79,6 +90,11 @@ pub struct MeltQuote { /// /// Used for an amountless invoice pub msat_to_pay: Option, + /// Unix time quote was created + #[serde(default)] + pub created_time: u64, + /// Unix time quote was paid + pub paid_time: Option, } impl MeltQuote { @@ -105,6 +121,8 @@ impl MeltQuote { payment_preimage: None, request_lookup_id, msat_to_pay, + created_time: unix_time(), + paid_time: None, } } } diff --git a/crates/cdk-payment-processor/src/proto/mod.rs b/crates/cdk-payment-processor/src/proto/mod.rs index b8389720..88e9aeba 100644 --- a/crates/cdk-payment-processor/src/proto/mod.rs +++ b/crates/cdk-payment-processor/src/proto/mod.rs @@ -176,6 +176,8 @@ impl From for MeltQuote { payment_preimage: value.payment_preimage, request_lookup_id: value.request_lookup_id, msat_to_pay: value.msat_to_pay.map(|a| a.into()), + created_time: value.created_time, + paid_time: value.paid_time, } } } @@ -198,6 +200,8 @@ impl TryFrom for cdk_common::mint::MeltQuote { payment_preimage: value.payment_preimage, request_lookup_id: value.request_lookup_id, msat_to_pay: value.msat_to_pay.map(|a| a.into()), + created_time: value.created_time, + paid_time: value.paid_time, }) } } diff --git a/crates/cdk-payment-processor/src/proto/payment_processor.proto b/crates/cdk-payment-processor/src/proto/payment_processor.proto index 3ac427d7..bbb46282 100644 --- a/crates/cdk-payment-processor/src/proto/payment_processor.proto +++ b/crates/cdk-payment-processor/src/proto/payment_processor.proto @@ -81,6 +81,8 @@ message MeltQuote { optional string payment_preimage = 8; string request_lookup_id = 9; optional uint64 msat_to_pay = 10; + uint64 created_time = 11; + optional uint64 paid_time = 12; } message MakePaymentRequest { diff --git a/crates/cdk-redb/src/mint/migrations.rs b/crates/cdk-redb/src/mint/migrations.rs index b7025588..c7a18cea 100644 --- a/crates/cdk-redb/src/mint/migrations.rs +++ b/crates/cdk-redb/src/mint/migrations.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use cdk_common::mint::MintQuote; use cdk_common::mint_url::MintUrl; +use cdk_common::util::unix_time; use cdk_common::{Amount, CurrencyUnit, MintQuoteState, Proof, State}; use lightning_invoice::Bolt11Invoice; use redb::{ @@ -209,6 +210,9 @@ impl From for MintQuote { expiry: quote.expiry, request_lookup_id: Bolt11Invoice::from_str("e.request).unwrap().to_string(), pubkey: None, + created_time: unix_time(), + paid_time: None, + issued_time: None, } } } diff --git a/crates/cdk-redb/src/mint/mod.rs b/crates/cdk-redb/src/mint/mod.rs index 895dbf50..6b563909 100644 --- a/crates/cdk-redb/src/mint/mod.rs +++ b/crates/cdk-redb/src/mint/mod.rs @@ -15,6 +15,7 @@ use cdk_common::database::{ use cdk_common::dhke::hash_to_curve; use cdk_common::mint::{self, MintKeySetInfo, MintQuote}; use cdk_common::nut00::ProofsMethods; +use cdk_common::util::unix_time; use cdk_common::{ BlindSignature, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintInfo, MintQuoteState, Proof, Proofs, PublicKey, State, @@ -40,10 +41,14 @@ const MINT_QUOTES_TABLE: TableDefinition<[u8; 16], &str> = TableDefinition::new( const MELT_QUOTES_TABLE: TableDefinition<[u8; 16], &str> = TableDefinition::new("melt_quotes"); const PROOFS_TABLE: TableDefinition<[u8; 33], &str> = TableDefinition::new("proofs"); const PROOFS_STATE_TABLE: TableDefinition<[u8; 33], &str> = TableDefinition::new("proofs_state"); +const PROOF_CREATED_TIME: TableDefinition<[u8; 33], u64> = + TableDefinition::new("proof_created_time"); const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config"); // Key is hex blinded_message B_ value is blinded_signature const BLINDED_SIGNATURES: TableDefinition<[u8; 33], &str> = TableDefinition::new("blinded_signatures"); +const BLIND_SIGNATURE_CREATED_TIME: TableDefinition<[u8; 33], u64> = + TableDefinition::new("blind_signature_created_time"); const QUOTE_PROOFS_TABLE: MultimapTableDefinition<[u8; 16], [u8; 33]> = MultimapTableDefinition::new("quote_proofs"); const QUOTE_SIGNATURES_TABLE: MultimapTableDefinition<[u8; 16], [u8; 33]> = @@ -149,7 +154,9 @@ impl MintRedbDatabase { let _ = write_txn.open_table(MELT_QUOTES_TABLE)?; let _ = write_txn.open_table(PROOFS_TABLE)?; let _ = write_txn.open_table(PROOFS_STATE_TABLE)?; + let _ = write_txn.open_table(PROOF_CREATED_TIME)?; let _ = write_txn.open_table(BLINDED_SIGNATURES)?; + let _ = write_txn.open_table(BLIND_SIGNATURE_CREATED_TIME)?; let _ = write_txn.open_multimap_table(QUOTE_PROOFS_TABLE)?; let _ = write_txn.open_multimap_table(QUOTE_SIGNATURES_TABLE)?; @@ -583,9 +590,16 @@ impl MintProofsDatabase for MintRedbDatabase { { let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?; + let mut time_table = write_txn + .open_table(PROOF_CREATED_TIME) + .map_err(Error::from)?; let mut quote_proofs_table = write_txn .open_multimap_table(QUOTE_PROOFS_TABLE) .map_err(Error::from)?; + + // Get current timestamp in seconds + let current_time = unix_time(); + for proof in proofs { let y: PublicKey = hash_to_curve(&proof.secret.to_bytes()).map_err(Error::from)?; let y = y.to_bytes(); @@ -596,6 +610,9 @@ impl MintProofsDatabase for MintRedbDatabase { serde_json::to_string(&proof).map_err(Error::from)?.as_str(), ) .map_err(Error::from)?; + + // Store creation time + time_table.insert(y, current_time).map_err(Error::from)?; } if let Some(quote_id) = "e_id { @@ -644,9 +661,13 @@ impl MintProofsDatabase for MintRedbDatabase { { let mut proofs_table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?; + let mut time_table = write_txn + .open_table(PROOF_CREATED_TIME) + .map_err(Error::from)?; for y in ys { proofs_table.remove(&y.to_bytes()).map_err(Error::from)?; + time_table.remove(&y.to_bytes()).map_err(Error::from)?; } } @@ -817,10 +838,16 @@ impl MintSignaturesDatabase for MintRedbDatabase { let mut table = write_txn .open_table(BLINDED_SIGNATURES) .map_err(Error::from)?; + let mut time_table = write_txn + .open_table(BLIND_SIGNATURE_CREATED_TIME) + .map_err(Error::from)?; let mut quote_sigs_table = write_txn .open_multimap_table(QUOTE_SIGNATURES_TABLE) .map_err(Error::from)?; + // Get current timestamp in seconds + let current_time = unix_time(); + for (blinded_message, blind_signature) in blinded_messages.iter().zip(blind_signatures) { let blind_sig = serde_json::to_string(&blind_signature).map_err(Error::from)?; @@ -828,6 +855,11 @@ impl MintSignaturesDatabase for MintRedbDatabase { .insert(blinded_message.to_bytes(), blind_sig.as_str()) .map_err(Error::from)?; + // Store creation time + time_table + .insert(blinded_message.to_bytes(), current_time) + .map_err(Error::from)?; + if let Some(quote_id) = "e_id { quote_sigs_table .insert(quote_id.as_bytes(), blinded_message.to_bytes()) diff --git a/crates/cdk-sqlite/src/mint/migrations/20250406091754_mint_time_of_quotes.sql b/crates/cdk-sqlite/src/mint/migrations/20250406091754_mint_time_of_quotes.sql new file mode 100644 index 00000000..2488f075 --- /dev/null +++ b/crates/cdk-sqlite/src/mint/migrations/20250406091754_mint_time_of_quotes.sql @@ -0,0 +1,8 @@ +-- Add timestamp columns to mint_quote table +ALTER TABLE mint_quote ADD COLUMN created_time INTEGER NOT NULL DEFAULT 0; +ALTER TABLE mint_quote ADD COLUMN paid_time INTEGER; +ALTER TABLE mint_quote ADD COLUMN issued_time INTEGER; + +-- Add timestamp columns to melt_quote table +ALTER TABLE melt_quote ADD COLUMN created_time INTEGER NOT NULL DEFAULT 0; +ALTER TABLE melt_quote ADD COLUMN paid_time INTEGER; diff --git a/crates/cdk-sqlite/src/mint/migrations/20250406093755_mint_created_time_signature.sql b/crates/cdk-sqlite/src/mint/migrations/20250406093755_mint_created_time_signature.sql new file mode 100644 index 00000000..7351d748 --- /dev/null +++ b/crates/cdk-sqlite/src/mint/migrations/20250406093755_mint_created_time_signature.sql @@ -0,0 +1,4 @@ +-- Add created_time column to blind_signature table +ALTER TABLE blind_signature ADD COLUMN created_time INTEGER NOT NULL DEFAULT 0; +-- Add created_time column to proof table +ALTER TABLE proof ADD COLUMN created_time INTEGER NOT NULL DEFAULT 0; diff --git a/crates/cdk-sqlite/src/mint/mod.rs b/crates/cdk-sqlite/src/mint/mod.rs index 7a8fbc3f..6732b7ed 100644 --- a/crates/cdk-sqlite/src/mint/mod.rs +++ b/crates/cdk-sqlite/src/mint/mod.rs @@ -15,6 +15,7 @@ use cdk_common::mint::{self, MintKeySetInfo, MintQuote}; use cdk_common::nut00::ProofsMethods; use cdk_common::nut05::QuoteState; use cdk_common::secret::Secret; +use cdk_common::util::unix_time; use cdk_common::{ Amount, BlindSignature, BlindSignatureDleq, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintInfo, MintQuoteState, PaymentMethod, Proof, Proofs, PublicKey, SecretKey, @@ -373,22 +374,28 @@ impl MintQuotesDatabase for MintSqliteDatabase { let res = sqlx::query( r#" INSERT INTO mint_quote -(id, amount, unit, request, state, expiry, request_lookup_id, pubkey) -VALUES (?, ?, ?, ?, ?, ?, ?, ?) +(id, amount, unit, request, state, expiry, request_lookup_id, pubkey, created_time, paid_time, issued_time) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET amount = excluded.amount, unit = excluded.unit, request = excluded.request, state = excluded.state, expiry = excluded.expiry, - request_lookup_id = excluded.request_lookup_id + request_lookup_id = excluded.request_lookup_id, + created_time = excluded.created_time, + paid_time = excluded.paid_time, + issued_time = excluded.issued_time ON CONFLICT(request_lookup_id) DO UPDATE SET amount = excluded.amount, unit = excluded.unit, request = excluded.request, state = excluded.state, expiry = excluded.expiry, - id = excluded.id + id = excluded.id, + created_time = excluded.created_time, + paid_time = excluded.paid_time, + issued_time = excluded.issued_time "#, ) .bind(quote.id.to_string()) @@ -399,6 +406,9 @@ ON CONFLICT(request_lookup_id) DO UPDATE SET .bind(quote.expiry as i64) .bind(quote.request_lookup_id) .bind(quote.pubkey.map(|p| p.to_string())) + .bind(quote.created_time as i64) + .bind(quote.paid_time.map(|t| t as i64)) + .bind(quote.issued_time.map(|t| t as i64)) .execute(&mut *transaction) .await; @@ -554,15 +564,43 @@ WHERE id=?; } }; - let update = sqlx::query( - r#" - UPDATE mint_quote SET state = ? WHERE id = ? - "#, - ) - .bind(state.to_string()) - .bind(quote_id.as_hyphenated()) - .execute(&mut *transaction) - .await; + let update_query = match state { + MintQuoteState::Paid => { + r#"UPDATE mint_quote SET state = ?, paid_time = ? WHERE id = ?"# + } + MintQuoteState::Issued => { + r#"UPDATE mint_quote SET state = ?, issued_time = ? WHERE id = ?"# + } + _ => r#"UPDATE mint_quote SET state = ? WHERE id = ?"#, + }; + + let current_time = unix_time(); + + let update = match state { + MintQuoteState::Paid => { + sqlx::query(update_query) + .bind(state.to_string()) + .bind(current_time as i64) + .bind(quote_id.as_hyphenated()) + .execute(&mut *transaction) + .await + } + MintQuoteState::Issued => { + sqlx::query(update_query) + .bind(state.to_string()) + .bind(current_time as i64) + .bind(quote_id.as_hyphenated()) + .execute(&mut *transaction) + .await + } + _ => { + sqlx::query(update_query) + .bind(state.to_string()) + .bind(quote_id.as_hyphenated()) + .execute(&mut *transaction) + .await + } + }; match update { Ok(_) => { @@ -684,8 +722,8 @@ WHERE id=? let res = sqlx::query( r#" INSERT INTO melt_quote -(id, unit, amount, request, fee_reserve, state, expiry, payment_preimage, request_lookup_id, msat_to_pay) -VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +(id, unit, amount, request, fee_reserve, state, expiry, payment_preimage, request_lookup_id, msat_to_pay, created_time, paid_time) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET unit = excluded.unit, amount = excluded.amount, @@ -695,7 +733,9 @@ ON CONFLICT(id) DO UPDATE SET expiry = excluded.expiry, payment_preimage = excluded.payment_preimage, request_lookup_id = excluded.request_lookup_id, - msat_to_pay = excluded.msat_to_pay + msat_to_pay = excluded.msat_to_pay, + created_time = excluded.created_time, + paid_time = excluded.paid_time ON CONFLICT(request_lookup_id) DO UPDATE SET unit = excluded.unit, amount = excluded.amount, @@ -704,7 +744,9 @@ ON CONFLICT(request_lookup_id) DO UPDATE SET state = excluded.state, expiry = excluded.expiry, payment_preimage = excluded.payment_preimage, - id = excluded.id; + id = excluded.id, + created_time = excluded.created_time, + paid_time = excluded.paid_time; "#, ) .bind(quote.id.to_string()) @@ -717,6 +759,8 @@ ON CONFLICT(request_lookup_id) DO UPDATE SET .bind(quote.payment_preimage) .bind(quote.request_lookup_id) .bind(quote.msat_to_pay.map(|a| u64::from(a) as i64)) + .bind(quote.created_time as i64) + .bind(quote.paid_time.map(|t| t as i64)) .execute(&mut *transaction) .await; @@ -831,15 +875,28 @@ WHERE id=?; } }; - let rec = sqlx::query( - r#" - UPDATE melt_quote SET state = ? WHERE id = ? - "#, - ) - .bind(state.to_string()) - .bind(quote_id.as_hyphenated()) - .execute(&mut *transaction) - .await; + let update_query = if state == MeltQuoteState::Paid { + r#"UPDATE melt_quote SET state = ?, paid_time = ? WHERE id = ?"# + } else { + r#"UPDATE melt_quote SET state = ? WHERE id = ?"# + }; + + let current_time = unix_time(); + + let rec = if state == MeltQuoteState::Paid { + sqlx::query(update_query) + .bind(state.to_string()) + .bind(current_time as i64) + .bind(quote_id.as_hyphenated()) + .execute(&mut *transaction) + .await + } else { + sqlx::query(update_query) + .bind(state.to_string()) + .bind(quote_id.as_hyphenated()) + .execute(&mut *transaction) + .await + }; match rec { Ok(_) => { @@ -978,12 +1035,14 @@ impl MintProofsDatabase for MintSqliteDatabase { async fn add_proofs(&self, proofs: Proofs, quote_id: Option) -> Result<(), Self::Err> { let mut transaction = self.pool.begin().await.map_err(Error::from)?; + let current_time = unix_time(); + for proof in proofs { let result = sqlx::query( r#" INSERT OR IGNORE INTO proof -(y, amount, keyset_id, secret, c, witness, state, quote_id) -VALUES (?, ?, ?, ?, ?, ?, ?, ?); +(y, amount, keyset_id, secret, c, witness, state, quote_id, created_time) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); "#, ) .bind(proof.y()?.to_bytes().to_vec()) @@ -994,6 +1053,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?); .bind(proof.witness.map(|w| serde_json::to_string(&w).unwrap())) .bind("UNSPENT") .bind(quote_id.map(|q| q.hyphenated())) + .bind(current_time as i64) .execute(&mut *transaction) .await; @@ -1292,12 +1352,14 @@ impl MintSignaturesDatabase for MintSqliteDatabase { quote_id: Option, ) -> Result<(), Self::Err> { let mut transaction = self.pool.begin().await.map_err(Error::from)?; + let current_time = unix_time(); + for (message, signature) in blinded_messages.iter().zip(blinded_signatures) { let res = sqlx::query( r#" INSERT INTO blind_signature -(y, amount, keyset_id, c, quote_id, dleq_e, dleq_s) -VALUES (?, ?, ?, ?, ?, ?, ?); +(y, amount, keyset_id, c, quote_id, dleq_e, dleq_s, created_time) +VALUES (?, ?, ?, ?, ?, ?, ?, ?); "#, ) .bind(message.to_bytes().to_vec()) @@ -1307,6 +1369,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?); .bind(quote_id.map(|q| q.hyphenated())) .bind(signature.dleq.as_ref().map(|dleq| dleq.e.to_secret_hex())) .bind(signature.dleq.as_ref().map(|dleq| dleq.s.to_secret_hex())) + .bind(current_time as i64) .execute(&mut *transaction) .await; @@ -1624,6 +1687,10 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result { row.try_get("request_lookup_id").map_err(Error::from)?; let row_pubkey: Option = row.try_get("pubkey").map_err(Error::from)?; + let row_created_time: i64 = row.try_get("created_time").map_err(Error::from)?; + let row_paid_time: Option = row.try_get("paid_time").map_err(Error::from)?; + let row_issued_time: Option = row.try_get("issued_time").map_err(Error::from)?; + let request_lookup_id = match row_request_lookup_id { Some(id) => id, None => match Bolt11Invoice::from_str(&row_request) { @@ -1645,6 +1712,9 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result { expiry: row_expiry as u64, request_lookup_id, pubkey, + created_time: row_created_time as u64, + paid_time: row_paid_time.map(|p| p as u64), + issued_time: row_issued_time.map(|p| p as u64), }) } @@ -1664,6 +1734,9 @@ fn sqlite_row_to_melt_quote(row: SqliteRow) -> Result { let row_msat_to_pay: Option = row.try_get("msat_to_pay").map_err(Error::from)?; + let row_created_time: i64 = row.try_get("created_time").map_err(Error::from)?; + let row_paid_time: Option = row.try_get("paid_time").map_err(Error::from)?; + Ok(mint::MeltQuote { id: row_id.into_uuid(), amount: Amount::from(row_amount as u64), @@ -1675,6 +1748,8 @@ fn sqlite_row_to_melt_quote(row: SqliteRow) -> Result { payment_preimage: row_preimage, request_lookup_id, msat_to_pay: row_msat_to_pay.map(|a| Amount::from(a as u64)), + created_time: row_created_time as u64, + paid_time: row_paid_time.map(|p| p as u64), }) }