diff --git a/bindings/cdk-js/src/nuts/nut04.rs b/bindings/cdk-js/src/nuts/nut04.rs index 6cbdb33c..aa90310c 100644 --- a/bindings/cdk-js/src/nuts/nut04.rs +++ b/bindings/cdk-js/src/nuts/nut04.rs @@ -88,10 +88,18 @@ impl From for JsMintBolt11Request { impl JsMintBolt11Request { /// Try From Base 64 String #[wasm_bindgen(constructor)] - pub fn new(quote: String, outputs: JsValue) -> Result { + pub fn new( + quote: String, + outputs: JsValue, + witness: Option, + ) -> Result { let outputs = serde_wasm_bindgen::from_value(outputs).map_err(into_err)?; Ok(JsMintBolt11Request { - inner: MintBolt11Request { quote, outputs }, + inner: MintBolt11Request { + quote, + outputs, + witness, + }, }) } diff --git a/crates/cdk-cli/src/sub_commands/mint.rs b/crates/cdk-cli/src/sub_commands/mint.rs index 46ce6a27..daf486e0 100644 --- a/crates/cdk-cli/src/sub_commands/mint.rs +++ b/crates/cdk-cli/src/sub_commands/mint.rs @@ -52,7 +52,7 @@ pub async fn mint( }; let quote = wallet - .mint_quote(Amount::from(sub_command_args.amount), description) + .mint_quote(Amount::from(sub_command_args.amount), description, None) .await?; println!("Quote: {:#?}", quote); @@ -69,7 +69,9 @@ pub async fn mint( sleep(Duration::from_secs(2)).await; } - let receive_amount = wallet.mint("e.id, SplitTarget::default(), None).await?; + let receive_amount = wallet + .mint("e.id, SplitTarget::default(), None, None) + .await?; println!("Received {receive_amount} from mint {mint_url}"); diff --git a/crates/cdk-integration-tests/src/lib.rs b/crates/cdk-integration-tests/src/lib.rs index 2e52b034..d16c95d3 100644 --- a/crates/cdk-integration-tests/src/lib.rs +++ b/crates/cdk-integration-tests/src/lib.rs @@ -125,7 +125,7 @@ pub async fn wallet_mint( split_target: SplitTarget, description: Option, ) -> Result<()> { - let quote = wallet.mint_quote(amount, description).await?; + let quote = wallet.mint_quote(amount, description, None).await?; loop { let status = wallet.mint_quote_state("e.id).await?; @@ -138,7 +138,7 @@ pub async fn wallet_mint( sleep(Duration::from_secs(2)).await; } - let receive_amount = wallet.mint("e.id, split_target, None).await?; + let receive_amount = wallet.mint("e.id, split_target, None, None).await?; println!("Minted: {}", receive_amount); @@ -161,6 +161,7 @@ pub async fn mint_proofs( amount, unit: CurrencyUnit::Sat, description, + pubkey: None, }; let mint_quote = wallet_client @@ -187,6 +188,7 @@ pub async fn mint_proofs( let request = MintBolt11Request { quote: mint_quote.quote, outputs: premint_secrets.blinded_messages(), + witness: None, }; let mint_response = wallet_client.post_mint(mint_url.parse()?, request).await?; diff --git a/crates/cdk-integration-tests/tests/fake_wallet.rs b/crates/cdk-integration-tests/tests/fake_wallet.rs index 884e63d1..533fbf8f 100644 --- a/crates/cdk-integration-tests/tests/fake_wallet.rs +++ b/crates/cdk-integration-tests/tests/fake_wallet.rs @@ -1,12 +1,13 @@ use std::{sync::Arc, time::Duration}; -use anyhow::Result; +use anyhow::{bail, Result}; use bip39::Mnemonic; use cdk::{ amount::SplitTarget, cdk_database::WalletMemoryDatabase, nuts::{ - CurrencyUnit, MeltBolt11Request, MeltQuoteState, MintQuoteState, PreMintSecrets, State, + CurrencyUnit, MeltBolt11Request, MeltQuoteState, MintQuoteState, PreMintSecrets, SecretKey, + State, }, wallet::{ client::{HttpClient, HttpClientMethods}, @@ -30,12 +31,12 @@ async fn test_fake_tokens_pending() -> Result<()> { None, )?; - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; let fake_description = FakeInvoiceDescription { @@ -70,12 +71,12 @@ async fn test_fake_melt_payment_fail() -> Result<()> { None, )?; - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; let fake_description = FakeInvoiceDescription { @@ -133,12 +134,12 @@ async fn test_fake_melt_payment_fail_and_check() -> Result<()> { None, )?; - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; let fake_description = FakeInvoiceDescription { @@ -178,12 +179,12 @@ async fn test_fake_melt_payment_return_fail_status() -> Result<()> { None, )?; - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; let fake_description = FakeInvoiceDescription { @@ -238,12 +239,12 @@ async fn test_fake_melt_payment_error_unknown() -> Result<()> { None, )?; - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; let fake_description = FakeInvoiceDescription { @@ -299,12 +300,12 @@ async fn test_fake_melt_payment_err_paid() -> Result<()> { None, )?; - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; let fake_description = FakeInvoiceDescription { @@ -337,12 +338,12 @@ async fn test_fake_melt_change_in_quote() -> Result<()> { None, )?; - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; let fake_description = FakeInvoiceDescription::default(); @@ -380,6 +381,87 @@ async fn test_fake_melt_change_in_quote() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_fake_mint_with_witness() -> Result<()> { + let wallet = Wallet::new( + MINT_URL, + CurrencyUnit::Sat, + Arc::new(WalletMemoryDatabase::default()), + &Mnemonic::generate(12)?.to_seed_normalized(""), + None, + )?; + let secret = SecretKey::generate(); + let mint_quote = wallet + .mint_quote(100.into(), None, Some(secret.public_key())) + .await?; + + wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; + + let mint_amount = wallet + .mint(&mint_quote.id, SplitTarget::default(), None, Some(secret)) + .await?; + + assert!(mint_amount == 100.into()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_fake_mint_without_witness() -> Result<()> { + let wallet = Wallet::new( + MINT_URL, + CurrencyUnit::Sat, + Arc::new(WalletMemoryDatabase::default()), + &Mnemonic::generate(12)?.to_seed_normalized(""), + None, + )?; + + let secret = SecretKey::generate(); + let mint_quote = wallet + .mint_quote(100.into(), None, Some(secret.public_key())) + .await?; + + wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; + + let mint_amount = wallet + .mint(&mint_quote.id, SplitTarget::default(), None, None) + .await; + + match mint_amount { + Err(cdk::error::Error::SecretKeyNotProvided) => Ok(()), + _ => bail!("Wrong mint response for minting without witness"), + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_fake_mint_with_wrong_witness() -> Result<()> { + let wallet = Wallet::new( + MINT_URL, + CurrencyUnit::Sat, + Arc::new(WalletMemoryDatabase::default()), + &Mnemonic::generate(12)?.to_seed_normalized(""), + None, + )?; + let secret = SecretKey::generate(); + let mint_quote = wallet + .mint_quote(100.into(), None, Some(secret.public_key())) + .await?; + + wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; + let secret = SecretKey::generate(); + + let mint_amount = wallet + .mint(&mint_quote.id, SplitTarget::default(), None, Some(secret)) + .await; + + match mint_amount { + Err(cdk::error::Error::IncorrectSecretKey) => Ok(()), + _ => { + bail!("Wrong mint response for minting without witness") + } + } +} + // Keep polling the state of the mint quote id until it's paid async fn wait_for_mint_to_be_paid(wallet: &Wallet, mint_quote_id: &str) -> Result<()> { loop { diff --git a/crates/cdk-integration-tests/tests/regtest.rs b/crates/cdk-integration-tests/tests/regtest.rs index 2ba54595..374dfe14 100644 --- a/crates/cdk-integration-tests/tests/regtest.rs +++ b/crates/cdk-integration-tests/tests/regtest.rs @@ -73,12 +73,12 @@ async fn test_regtest_mint_melt_round_trip() -> Result<()> { .expect("Failed to connect"); let (mut write, mut reader) = ws_stream.split(); - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; lnd_client.pay_invoice(mint_quote.request).await?; let mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; assert!(mint_amount == 100.into()); @@ -139,14 +139,14 @@ async fn test_regtest_mint_melt() -> Result<()> { let mint_amount = Amount::from(100); - let mint_quote = wallet.mint_quote(mint_amount, None).await?; + let mint_quote = wallet.mint_quote(mint_amount, None, None).await?; assert_eq!(mint_quote.amount, mint_amount); lnd_client.pay_invoice(mint_quote.request).await?; let mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; assert!(mint_amount == 100.into()); @@ -167,12 +167,12 @@ async fn test_restore() -> Result<()> { None, )?; - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; lnd_client.pay_invoice(mint_quote.request).await?; let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; assert!(wallet.total_balance().await? == 100.into()); @@ -223,12 +223,12 @@ async fn test_pay_invoice_twice() -> Result<()> { None, )?; - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; lnd_client.pay_invoice(mint_quote.request).await?; let mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; assert_eq!(mint_amount, 100.into()); @@ -275,12 +275,12 @@ async fn test_internal_payment() -> Result<()> { None, )?; - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; lnd_client.pay_invoice(mint_quote.request).await?; let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; assert!(wallet.total_balance().await? == 100.into()); @@ -295,7 +295,7 @@ async fn test_internal_payment() -> Result<()> { None, )?; - let mint_quote = wallet_2.mint_quote(10.into(), None).await?; + let mint_quote = wallet_2.mint_quote(10.into(), None, None).await?; let melt = wallet.melt_quote(mint_quote.request.clone(), None).await?; @@ -304,7 +304,7 @@ async fn test_internal_payment() -> Result<()> { let _melted = wallet.melt(&melt.id).await.unwrap(); let _wallet_2_mint = wallet_2 - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await .unwrap(); @@ -346,7 +346,7 @@ async fn test_cached_mint() -> Result<()> { let mint_amount = Amount::from(100); - let quote = wallet.mint_quote(mint_amount, None).await?; + let quote = wallet.mint_quote(mint_amount, None, None).await?; lnd_client.pay_invoice(quote.request).await?; loop { @@ -369,6 +369,7 @@ async fn test_cached_mint() -> Result<()> { let request = MintBolt11Request { quote: quote.id, outputs: premint_secrets.blinded_messages(), + witness: None, }; let response = http_client diff --git a/crates/cdk-redb/src/mint/migrations.rs b/crates/cdk-redb/src/mint/migrations.rs index 90feaeff..0ba89463 100644 --- a/crates/cdk-redb/src/mint/migrations.rs +++ b/crates/cdk-redb/src/mint/migrations.rs @@ -59,6 +59,7 @@ impl From for MintQuote { state: quote.state, expiry: quote.expiry, request_lookup_id: Bolt11Invoice::from_str("e.request).unwrap().to_string(), + pubkey: None, } } } diff --git a/crates/cdk-sqlite/src/mint/migrations/20241108093102_mint_mint_quote_pubkey.sql b/crates/cdk-sqlite/src/mint/migrations/20241108093102_mint_mint_quote_pubkey.sql new file mode 100644 index 00000000..06501e14 --- /dev/null +++ b/crates/cdk-sqlite/src/mint/migrations/20241108093102_mint_mint_quote_pubkey.sql @@ -0,0 +1 @@ +ALTER TABLE mint_quote ADD pubkey TEXT; diff --git a/crates/cdk-sqlite/src/mint/mod.rs b/crates/cdk-sqlite/src/mint/mod.rs index c72e5220..7bec2ec7 100644 --- a/crates/cdk-sqlite/src/mint/mod.rs +++ b/crates/cdk-sqlite/src/mint/mod.rs @@ -205,8 +205,8 @@ WHERE active = 1 let res = sqlx::query( r#" INSERT OR REPLACE INTO mint_quote -(id, mint_url, amount, unit, request, state, expiry, request_lookup_id) -VALUES (?, ?, ?, ?, ?, ?, ?, ?); +(id, mint_url, amount, unit, request, state, expiry, request_lookup_id, pubkey) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); "#, ) .bind(quote.id.to_string()) @@ -217,6 +217,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?); .bind(quote.state.to_string()) .bind(quote.expiry as i64) .bind(quote.request_lookup_id) + .bind(quote.pubkey.map(|p| p.to_string())) .execute(&mut transaction) .await; @@ -1277,6 +1278,7 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result { let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?; let row_request_lookup_id: Option = row.try_get("request_lookup_id").map_err(Error::from)?; + let row_pubkey: Option = row.try_get("pubkey").map_err(Error::from)?; let request_lookup_id = match row_request_lookup_id { Some(id) => id, @@ -1286,6 +1288,10 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result { }, }; + let pubkey = row_pubkey + .map(|key| PublicKey::from_str(&key)) + .transpose()?; + Ok(MintQuote { id: row_id, mint_url: MintUrl::from_str(&row_mint_url)?, @@ -1295,6 +1301,7 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result { state: MintQuoteState::from_str(&row_state).map_err(Error::from)?, expiry: row_expiry as u64, request_lookup_id, + pubkey, }) } diff --git a/crates/cdk-sqlite/src/wallet/migrations/20241108092756_wallet_mint_quote_pubkey.sql b/crates/cdk-sqlite/src/wallet/migrations/20241108092756_wallet_mint_quote_pubkey.sql new file mode 100644 index 00000000..06501e14 --- /dev/null +++ b/crates/cdk-sqlite/src/wallet/migrations/20241108092756_wallet_mint_quote_pubkey.sql @@ -0,0 +1 @@ +ALTER TABLE mint_quote ADD pubkey TEXT; diff --git a/crates/cdk-sqlite/src/wallet/mod.rs b/crates/cdk-sqlite/src/wallet/mod.rs index 0decfac3..644ca1b2 100644 --- a/crates/cdk-sqlite/src/wallet/mod.rs +++ b/crates/cdk-sqlite/src/wallet/mod.rs @@ -342,8 +342,8 @@ WHERE id=? sqlx::query( r#" INSERT OR REPLACE INTO mint_quote -(id, mint_url, amount, unit, request, state, expiry) -VALUES (?, ?, ?, ?, ?, ?, ?); +(id, mint_url, amount, unit, request, state, expiry, pubkey) +VALUES (?, ?, ?, ?, ?, ?, ?, ?); "#, ) .bind(quote.id.to_string()) @@ -353,6 +353,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?); .bind(quote.request) .bind(quote.state.to_string()) .bind(quote.expiry as i64) + .bind(quote.pubkey.map(|p| p.to_string())) .execute(&self.pool) .await .map_err(Error::from)?; @@ -823,9 +824,14 @@ fn sqlite_row_to_mint_quote(row: &SqliteRow) -> Result { let row_request: String = row.try_get("request").map_err(Error::from)?; let row_state: String = row.try_get("state").map_err(Error::from)?; let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?; + let row_pubkey: Option = row.try_get("pubkey").map_err(Error::from)?; let state = MintQuoteState::from_str(&row_state)?; + let pubkey = row_pubkey + .map(|key| PublicKey::from_str(&key)) + .transpose()?; + Ok(MintQuote { id: row_id, mint_url: MintUrl::from_str(&row_mint_url)?, @@ -834,6 +840,7 @@ fn sqlite_row_to_mint_quote(row: &SqliteRow) -> Result { request: row_request, state, expiry: row_expiry as u64, + pubkey, }) } diff --git a/crates/cdk/examples/mint-token.rs b/crates/cdk/examples/mint-token.rs index 195fb0ff..ef271336 100644 --- a/crates/cdk/examples/mint-token.rs +++ b/crates/cdk/examples/mint-token.rs @@ -22,7 +22,7 @@ async fn main() -> Result<(), Error> { let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap(); - let quote = wallet.mint_quote(amount, None).await.unwrap(); + let quote = wallet.mint_quote(amount, None, None).await.unwrap(); println!("Quote: {:#?}", quote); @@ -39,7 +39,7 @@ async fn main() -> Result<(), Error> { } let receive_amount = wallet - .mint("e.id, SplitTarget::default(), None) + .mint("e.id, SplitTarget::default(), None, None) .await .unwrap(); diff --git a/crates/cdk/examples/p2pk.rs b/crates/cdk/examples/p2pk.rs index 6e51f781..8868fc13 100644 --- a/crates/cdk/examples/p2pk.rs +++ b/crates/cdk/examples/p2pk.rs @@ -22,7 +22,7 @@ async fn main() -> Result<(), Error> { let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap(); - let quote = wallet.mint_quote(amount, None).await.unwrap(); + let quote = wallet.mint_quote(amount, None, None).await.unwrap(); println!("Minting nuts ..."); @@ -39,7 +39,7 @@ async fn main() -> Result<(), Error> { } let _receive_amount = wallet - .mint("e.id, SplitTarget::default(), None) + .mint("e.id, SplitTarget::default(), None, None) .await .unwrap(); diff --git a/crates/cdk/examples/proof-selection.rs b/crates/cdk/examples/proof-selection.rs index 210b7731..95f67924 100644 --- a/crates/cdk/examples/proof-selection.rs +++ b/crates/cdk/examples/proof-selection.rs @@ -24,7 +24,7 @@ async fn main() { for amount in [64] { let amount = Amount::from(amount); - let quote = wallet.mint_quote(amount, None).await.unwrap(); + let quote = wallet.mint_quote(amount, None, None).await.unwrap(); println!("Pay request: {}", quote.request); @@ -41,7 +41,7 @@ async fn main() { } let receive_amount = wallet - .mint("e.id, SplitTarget::default(), None) + .mint("e.id, SplitTarget::default(), None, None) .await .unwrap(); diff --git a/crates/cdk/examples/wallet.rs b/crates/cdk/examples/wallet.rs index 93b6fa23..dc35014c 100644 --- a/crates/cdk/examples/wallet.rs +++ b/crates/cdk/examples/wallet.rs @@ -24,7 +24,7 @@ async fn main() { let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap(); - let quote = wallet.mint_quote(amount, None).await.unwrap(); + let quote = wallet.mint_quote(amount, None, None).await.unwrap(); println!("Pay request: {}", quote.request); @@ -41,7 +41,7 @@ async fn main() { } let receive_amount = wallet - .mint("e.id, SplitTarget::default(), None) + .mint("e.id, SplitTarget::default(), None, None) .await .unwrap(); diff --git a/crates/cdk/src/error.rs b/crates/cdk/src/error.rs index 1695a58c..85197d77 100644 --- a/crates/cdk/src/error.rs +++ b/crates/cdk/src/error.rs @@ -160,6 +160,12 @@ pub enum Error { /// Invoice Description not supported #[error("Invoice Description not supported")] InvoiceDescriptionUnsupported, + /// Secretkey to sign mint quote not provided + #[error("Secretkey to sign mint quote not provided")] + SecretKeyNotProvided, + /// Incorrect secret key provided + #[error("Incorrect secretkey provided")] + IncorrectSecretKey, /// Custom Error #[error("`{0}`")] Custom(String), @@ -174,7 +180,7 @@ pub enum Error { /// Parse int error #[error(transparent)] ParseInt(#[from] std::num::ParseIntError), - /// Parse Url Error + /// Parse 9rl Error #[error(transparent)] UrlParseError(#[from] url::ParseError), /// Utf8 parse error @@ -237,6 +243,9 @@ pub enum Error { /// NUT18 Error #[error(transparent)] NUT18(#[from] crate::nuts::nut18::Error), + /// NUT19 Error + #[error(transparent)] + NUT19(#[from] crate::nuts::nut19::Error), /// Database Error #[cfg(any(feature = "wallet", feature = "mint"))] #[error(transparent)] diff --git a/crates/cdk/src/mint/mint_nut04.rs b/crates/cdk/src/mint/mint_nut04.rs index fb0b6d4d..bc159865 100644 --- a/crates/cdk/src/mint/mint_nut04.rs +++ b/crates/cdk/src/mint/mint_nut04.rs @@ -62,6 +62,7 @@ impl Mint { amount, unit, description, + pubkey, } = mint_quote_request; self.check_mint_request_acceptable(amount, &unit)?; @@ -102,6 +103,7 @@ impl Mint { amount, create_invoice_response.expiry.unwrap_or(0), create_invoice_response.request_lookup_id.clone(), + pubkey, ); tracing::debug!( @@ -275,6 +277,12 @@ impl Mint { MintQuoteState::Paid => (), } + // If the there is a public key provoided in mint quote request + // verify the signature is provided for the mint request + if let Some(pubkey) = mint_quote.pubkey { + mint_request.verify(pubkey)?; + } + let blinded_messages: Vec = mint_request .outputs .iter() diff --git a/crates/cdk/src/mint/types.rs b/crates/cdk/src/mint/types.rs index 44047fd9..9baac35b 100644 --- a/crates/cdk/src/mint/types.rs +++ b/crates/cdk/src/mint/types.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; -use super::CurrencyUnit; +use super::{CurrencyUnit, PublicKey}; use crate::mint_url::MintUrl; use crate::nuts::{MeltQuoteState, MintQuoteState}; use crate::Amount; @@ -27,6 +27,8 @@ pub struct MintQuote { pub expiry: u64, /// Value used by ln backend to look up state of request pub request_lookup_id: String, + /// Pubkey + pub pubkey: Option, } impl MintQuote { @@ -38,6 +40,7 @@ impl MintQuote { amount: Amount, expiry: u64, request_lookup_id: String, + pubkey: Option, ) -> Self { let id = Uuid::new_v4(); @@ -50,6 +53,7 @@ impl MintQuote { state: MintQuoteState::Unpaid, expiry, request_lookup_id, + pubkey, } } } diff --git a/crates/cdk/src/nuts/mod.rs b/crates/cdk/src/nuts/mod.rs index 79bfb0d8..2ee8650b 100644 --- a/crates/cdk/src/nuts/mod.rs +++ b/crates/cdk/src/nuts/mod.rs @@ -20,6 +20,7 @@ pub mod nut14; pub mod nut15; pub mod nut17; pub mod nut18; +pub mod nut19; pub use nut00::{ BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, PreMint, PreMintSecrets, Proof, diff --git a/crates/cdk/src/nuts/nut04.rs b/crates/cdk/src/nuts/nut04.rs index 40a6f8d4..93df99dc 100644 --- a/crates/cdk/src/nuts/nut04.rs +++ b/crates/cdk/src/nuts/nut04.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod}; -use super::MintQuoteState; +use super::{MintQuoteState, PublicKey}; use crate::Amount; /// NUT04 Error @@ -32,7 +32,11 @@ pub struct MintQuoteBolt11Request { /// Unit wallet would like to pay with pub unit: CurrencyUnit, /// Memo to create the invoice with + #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, + /// NUT-19 Pubkey + #[serde(skip_serializing_if = "Option::is_none")] + pub pubkey: Option, } /// Possible states of a quote @@ -114,6 +118,9 @@ pub struct MintBolt11Request { /// Outputs #[cfg_attr(feature = "swagger", schema(max_items = 1_000))] pub outputs: Vec, + /// Signature + #[serde(skip_serializing_if = "Option::is_none")] + pub witness: Option, } impl MintBolt11Request { diff --git a/crates/cdk/src/nuts/nut19.rs b/crates/cdk/src/nuts/nut19.rs new file mode 100644 index 00000000..d84cec43 --- /dev/null +++ b/crates/cdk/src/nuts/nut19.rs @@ -0,0 +1,95 @@ +//! Mint Quote Signatures + +use std::str::FromStr; + +use bitcoin::secp256k1::schnorr::Signature; +use thiserror::Error; + +use super::{MintBolt11Request, PublicKey, SecretKey}; + +/// Nut19 Error +#[derive(Debug, Error)] +pub enum Error { + /// Quote witness not provided + #[error("Quote Witness not provided")] + QuoteWitnessMissing, + /// Quote witness invalid signature + #[error("Quote witness invalid signature")] + InvalidWitness, + /// Nut01 error + #[error(transparent)] + NUT01(#[from] crate::nuts::nut01::Error), +} + +impl MintBolt11Request { + /// Sign [`MintBolt11Request`] + pub fn sign(&mut self, secret_key: SecretKey) -> Result<(), Error> { + let msg = self.quote.as_bytes(); + + let signature: Signature = secret_key.sign(msg)?; + + self.witness = Some(signature.to_string()); + + Ok(()) + } + + /// Verify signature on [`MintBolt11Request`] + pub fn verify(&self, pubkey: PublicKey) -> Result<(), Error> { + let witness = self.witness.as_ref().ok_or(Error::QuoteWitnessMissing)?; + + let signature = Signature::from_str(&witness).map_err(|_| Error::InvalidWitness)?; + + pubkey.verify(self.quote.as_bytes(), &signature)?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_valid_signature() { + let pubkey = PublicKey::from_hex( + "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac", + ) + .unwrap(); + + let request: MintBolt11Request = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[],"witness":"d9be080b33179387e504bb6991ea41ae0dd715e28b01ce9f63d57198a095bccc776874914288e6989e97ac9d255ac667c205fa8d90a211184b417b4ffdd24092"}"#).unwrap(); + + assert!(request.verify(pubkey).is_ok()); + } + + #[test] + fn test_mint_request_signature() { + let id = "9d745270-1405-46de-b5c5-e2762b4f5e00"; + + let mut request = MintBolt11Request { + quote: id.to_string(), + outputs: vec![], + witness: None, + }; + + let secret = + SecretKey::from_hex("50d7fd7aa2b2fe4607f41f4ce6f8794fc184dd47b8cdfbe4b3d1249aa02d35aa") + .unwrap(); + + request.sign(secret.clone()).unwrap(); + + assert!(request.verify(secret.public_key()).is_ok()); + } + + #[test] + fn test_invalid_signature() { + let pubkey = PublicKey::from_hex( + "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac", + ) + .unwrap(); + + let request: MintBolt11Request = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[],"witness":"cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"}"#).unwrap(); + + // Signature is on a different quote id verification should fail + assert!(request.verify(pubkey).is_err()); + } +} diff --git a/crates/cdk/src/wallet/mint.rs b/crates/cdk/src/wallet/mint.rs index 743044f0..b2bd6eaa 100644 --- a/crates/cdk/src/wallet/mint.rs +++ b/crates/cdk/src/wallet/mint.rs @@ -2,7 +2,7 @@ use tracing::instrument; use super::MintQuote; use crate::nuts::nut00::ProofsMethods; -use crate::nuts::{MintBolt11Request, MintQuoteBolt11Request}; +use crate::nuts::{MintBolt11Request, MintQuoteBolt11Request, PublicKey, SecretKey}; use crate::{ amount::SplitTarget, dhke::construct_proofs, @@ -44,6 +44,7 @@ impl Wallet { &self, amount: Amount, description: Option, + pubkey: Option, ) -> Result { let mint_url = self.mint_url.clone(); let unit = self.unit.clone(); @@ -69,6 +70,7 @@ impl Wallet { amount, unit: unit.clone(), description, + pubkey, }; let quote_res = self @@ -84,6 +86,7 @@ impl Wallet { request: quote_res.request, state: quote_res.state, expiry: quote_res.expiry.unwrap_or(0), + pubkey, }; self.localstore.add_mint_quote(quote.clone()).await?; @@ -124,8 +127,9 @@ impl Wallet { let mint_quote_response = self.mint_quote_state(&mint_quote.id).await?; if mint_quote_response.state == MintQuoteState::Paid { + // TODO: Need to pass in keys here let amount = self - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; total_amount += amount; } else if mint_quote.expiry.le(&unix_time()) { @@ -171,6 +175,7 @@ impl Wallet { quote_id: &str, amount_split_target: SplitTarget, spending_conditions: Option, + secret_key: Option, ) -> Result { // Check that mint is in store of mints if self @@ -219,11 +224,21 @@ impl Wallet { )?, }; - let request = MintBolt11Request { + let mut request = MintBolt11Request { quote: quote_id.to_string(), outputs: premint_secrets.blinded_messages(), + witness: None, }; + if let Some(pubkey) = quote_info.pubkey { + let secret_key = secret_key.ok_or(Error::SecretKeyNotProvided)?; + if pubkey != secret_key.public_key() { + return Err(Error::IncorrectSecretKey); + } + + request.sign(secret_key)?; + } + let mint_res = self .client .post_mint(self.mint_url.clone(), request) diff --git a/crates/cdk/src/wallet/multi_mint_wallet.rs b/crates/cdk/src/wallet/multi_mint_wallet.rs index b0f048b2..346265a7 100644 --- a/crates/cdk/src/wallet/multi_mint_wallet.rs +++ b/crates/cdk/src/wallet/multi_mint_wallet.rs @@ -16,7 +16,7 @@ use super::types::SendKind; use super::Error; use crate::amount::SplitTarget; use crate::mint_url::MintUrl; -use crate::nuts::{CurrencyUnit, Proof, SecretKey, SpendingConditions, Token}; +use crate::nuts::{CurrencyUnit, Proof, PublicKey, SecretKey, SpendingConditions, Token}; use crate::types::Melted; use crate::wallet::types::MintQuote; use crate::{Amount, Wallet}; @@ -166,13 +166,14 @@ impl MultiMintWallet { wallet_key: &WalletKey, amount: Amount, description: Option, + pubkey: Option, ) -> Result { let wallet = self .get_wallet(wallet_key) .await .ok_or(Error::UnknownWallet(wallet_key.clone()))?; - wallet.mint_quote(amount, description).await + wallet.mint_quote(amount, description, pubkey).await } /// Check all mint quotes @@ -215,13 +216,14 @@ impl MultiMintWallet { wallet_key: &WalletKey, quote_id: &str, conditions: Option, + secret_key: Option, ) -> Result { let wallet = self .get_wallet(wallet_key) .await .ok_or(Error::UnknownWallet(wallet_key.clone()))?; wallet - .mint(quote_id, SplitTarget::default(), conditions) + .mint(quote_id, SplitTarget::default(), conditions, secret_key) .await } diff --git a/crates/cdk/src/wallet/types.rs b/crates/cdk/src/wallet/types.rs index 309a4c1c..af793c88 100644 --- a/crates/cdk/src/wallet/types.rs +++ b/crates/cdk/src/wallet/types.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::mint_url::MintUrl; -use crate::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState}; +use crate::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState, PublicKey}; use crate::Amount; /// Mint Quote Info @@ -23,6 +23,8 @@ pub struct MintQuote { pub state: MintQuoteState, /// Expiration time of quote pub expiry: u64, + /// Publickey [NUT-19] + pub pubkey: Option, } /// Melt Quote Info diff --git a/flake.lock b/flake.lock index 14dc9544..31b3e377 100644 --- a/flake.lock +++ b/flake.lock @@ -57,11 +57,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1730741070, - "narHash": "sha256-edm8WG19kWozJ/GqyYx2VjW99EdhjKwbY3ZwdlPAAlo=", + "lastModified": 1730883749, + "narHash": "sha256-mwrFF0vElHJP8X3pFCByJR365Q2463ATp2qGIrDUdlE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d063c1dd113c91ab27959ba540c0d9753409edf3", + "rev": "dba414932936fde69f0606b4f1d87c5bc0003ede", "type": "github" }, "original": {