From e088803e90028ef43b1c8aac41d16a55fbab27a9 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Fri, 8 Nov 2024 19:58:07 +0000 Subject: [PATCH] feat: nut19 signature on mint witness --- bindings/cdk-js/src/nuts/nut04.rs | 12 +- bindings/cdk-js/src/wallet.rs | 4 +- crates/cdk-cli/src/sub_commands/mint.rs | 6 +- crates/cdk-integration-tests/src/lib.rs | 6 +- .../tests/fake_wallet.rs | 111 +++++++++++++--- crates/cdk-integration-tests/tests/mint.rs | 2 + crates/cdk-integration-tests/tests/regtest.rs | 27 ++-- crates/cdk-redb/src/mint/migrations.rs | 1 + .../20241108093102_mint_mint_quote_pubkey.sql | 1 + crates/cdk-sqlite/src/mint/mod.rs | 11 +- ...0241108092756_wallet_mint_quote_pubkey.sql | 1 + crates/cdk-sqlite/src/wallet/mod.rs | 11 +- crates/cdk/examples/mint-token.rs | 4 +- crates/cdk/examples/p2pk.rs | 4 +- crates/cdk/examples/proof-selection.rs | 4 +- crates/cdk/examples/wallet.rs | 4 +- crates/cdk/src/error.rs | 20 ++- crates/cdk/src/mint/mint_nut04.rs | 9 ++ crates/cdk/src/mint/types.rs | 6 +- crates/cdk/src/nuts/mod.rs | 1 + crates/cdk/src/nuts/nut04.rs | 13 +- crates/cdk/src/nuts/nut19.rs | 122 ++++++++++++++++++ crates/cdk/src/wallet/mint.rs | 29 ++++- crates/cdk/src/wallet/multi_mint_wallet.rs | 8 +- crates/cdk/src/wallet/types.rs | 4 +- flake.lock | 12 +- flake.nix | 2 +- 27 files changed, 367 insertions(+), 68 deletions(-) create mode 100644 crates/cdk-sqlite/src/mint/migrations/20241108093102_mint_mint_quote_pubkey.sql create mode 100644 crates/cdk-sqlite/src/wallet/migrations/20241108092756_wallet_mint_quote_pubkey.sql create mode 100644 crates/cdk/src/nuts/nut19.rs diff --git a/bindings/cdk-js/src/nuts/nut04.rs b/bindings/cdk-js/src/nuts/nut04.rs index 6cbdb33ce..aa90310c3 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/bindings/cdk-js/src/wallet.rs b/bindings/cdk-js/src/wallet.rs index 8f4cf405c..50e8c9642 100644 --- a/bindings/cdk-js/src/wallet.rs +++ b/bindings/cdk-js/src/wallet.rs @@ -93,7 +93,7 @@ impl JsWallet { ) -> Result { let quote = self .inner - .mint_quote(amount.into(), description) + .mint_quote(amount.into(), description, None) .await .map_err(into_err)?; @@ -142,7 +142,7 @@ impl JsWallet { Ok(self .inner - .mint("e_id, target, conditions) + .mint("e_id, target, conditions, None) .await .map_err(into_err)? .into()) diff --git a/crates/cdk-cli/src/sub_commands/mint.rs b/crates/cdk-cli/src/sub_commands/mint.rs index 46ce6a27c..daf486e04 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 2e52b0345..d16c95d3d 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 cf0ea1bef..5e8c4b44f 100644 --- a/crates/cdk-integration-tests/tests/fake_wallet.rs +++ b/crates/cdk-integration-tests/tests/fake_wallet.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use std::time::Duration; -use anyhow::Result; +use anyhow::{bail, Result}; use bip39::Mnemonic; use cdk::amount::SplitTarget; use cdk::cdk_database::WalletMemoryDatabase; @@ -27,12 +27,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 { @@ -67,12 +67,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 { @@ -130,12 +130,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 { @@ -175,12 +175,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 { @@ -235,12 +235,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 { @@ -296,12 +296,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 { @@ -334,12 +334,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(); @@ -377,6 +377,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/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index 3c9f1258a..3082eb207 100644 --- a/crates/cdk-integration-tests/tests/mint.rs +++ b/crates/cdk-integration-tests/tests/mint.rs @@ -78,6 +78,7 @@ async fn mint_proofs( amount, unix_time() + 36000, request_lookup.to_string(), + None, ); mint.localstore.add_mint_quote(quote.clone()).await?; @@ -90,6 +91,7 @@ async fn mint_proofs( let mint_request = MintBolt11Request { quote: quote.id, outputs: premint.blinded_messages(), + witness: None, }; let after_mint = mint.process_mint_request(mint_request).await?; diff --git a/crates/cdk-integration-tests/tests/regtest.rs b/crates/cdk-integration-tests/tests/regtest.rs index 107fa49eb..737060b0a 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()); @@ -151,14 +151,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()); @@ -179,12 +179,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()); @@ -235,12 +235,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()); @@ -287,12 +287,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()); @@ -307,7 +307,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?; @@ -316,7 +316,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(); @@ -358,7 +358,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 { @@ -381,6 +381,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 90feaeffb..0ba894633 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 000000000..06501e14f --- /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 c72e5220a..7bec2ec79 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 000000000..06501e14f --- /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 0decfac3b..644ca1b20 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 195fb0ff7..ef2713364 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 6e51f781e..8868fc138 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 210b77319..95f67924d 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 93b6fa23e..dc35014c9 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 5ddfac64a..ed531c223 100644 --- a/crates/cdk/src/error.rs +++ b/crates/cdk/src/error.rs @@ -162,6 +162,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), @@ -176,7 +182,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 @@ -239,6 +245,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)] @@ -369,6 +378,11 @@ impl From for ErrorResponse { error: Some(err.to_string()), detail: None, }, + Error::NUT19(err) => ErrorResponse { + code: ErrorCode::WitnessMissingOrInvalid, + error: Some(err.to_string()), + detail: None, + }, _ => ErrorResponse { code: ErrorCode::Unknown(9999), error: Some(err.to_string()), @@ -439,6 +453,8 @@ pub enum ErrorCode { TransactionUnbalanced, /// Amount outside of allowed range AmountOutofLimitRange, + /// Witness missing or invalid + WitnessMissingOrInvalid, /// Unknown error code Unknown(u16), } @@ -463,6 +479,7 @@ impl ErrorCode { 20005 => Self::QuotePending, 20006 => Self::InvoiceAlreadyPaid, 20007 => Self::QuoteExpired, + 20008 => Self::WitnessMissingOrInvalid, _ => Self::Unknown(code), } } @@ -486,6 +503,7 @@ impl ErrorCode { Self::QuotePending => 20005, Self::InvoiceAlreadyPaid => 20006, Self::QuoteExpired => 20007, + Self::WitnessMissingOrInvalid => 20008, Self::Unknown(code) => *code, } } diff --git a/crates/cdk/src/mint/mint_nut04.rs b/crates/cdk/src/mint/mint_nut04.rs index 5631fd043..c1651e389 100644 --- a/crates/cdk/src/mint/mint_nut04.rs +++ b/crates/cdk/src/mint/mint_nut04.rs @@ -64,6 +64,7 @@ impl Mint { amount, unit, description, + pubkey, } = mint_quote_request; self.check_mint_request_acceptable(amount, &unit)?; @@ -104,6 +105,7 @@ impl Mint { amount, create_invoice_response.expiry.unwrap_or(0), create_invoice_response.request_lookup_id.clone(), + pubkey, ); tracing::debug!( @@ -146,6 +148,7 @@ impl Mint { request: quote.request, state, expiry: Some(quote.expiry), + pubkey: quote.pubkey, }) } @@ -277,6 +280,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_witness(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 44047fd9b..9baac35bb 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 eb1f81707..7f913f49a 100644 --- a/crates/cdk/src/nuts/mod.rs +++ b/crates/cdk/src/nuts/mod.rs @@ -21,6 +21,7 @@ pub mod nut15; #[cfg(feature = "mint")] 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 40a6f8d4b..0202c2d76 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 @@ -90,6 +94,9 @@ pub struct MintQuoteBolt11Response { pub state: MintQuoteState, /// Unix timestamp until the quote is valid pub expiry: Option, + /// NUT-19 Pubkey + #[serde(skip_serializing_if = "Option::is_none")] + pub pubkey: Option, } #[cfg(feature = "mint")] @@ -100,6 +107,7 @@ impl From for MintQuoteBolt11Response { request: mint_quote.request, state: mint_quote.state, expiry: Some(mint_quote.expiry), + pubkey: mint_quote.pubkey, } } } @@ -114,6 +122,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 000000000..7896089b8 --- /dev/null +++ b/crates/cdk/src/nuts/nut19.rs @@ -0,0 +1,122 @@ +//! 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 { + /// Witness not provided + #[error("Witness not provided")] + WitnessMissing, + /// Quote witness invalid signature + #[error("Quote witness invalid signature")] + InvalidWitness, + /// Nut01 error + #[error(transparent)] + NUT01(#[from] crate::nuts::nut01::Error), +} + +impl MintBolt11Request { + /// Constructs the message to be signed according to NUT-19 specification. + /// + /// The message is constructed by concatenating: + /// 1. The quote ID + /// 2. All blinded secrets (B_0 through B_n) + /// + /// Format: `quote_id || B_0 || B_1 || ... || B_n` + pub fn msg_to_sign(&self) -> String { + // Pre-calculate capacity to avoid reallocations + let capacity = self.quote.len() + (self.outputs.len() * 66); + let mut msg = String::with_capacity(capacity); + + msg.push_str(&self.quote); + for output in &self.outputs { + msg.push_str(&output.blinded_secret.to_hex()); + } + msg + } + + /// Sign [`MintBolt11Request`] + pub fn sign(&mut self, secret_key: SecretKey) -> Result<(), Error> { + let msg = self.msg_to_sign(); + + let signature: Signature = secret_key.sign(msg.as_bytes())?; + + self.witness = Some(signature.to_string()); + + Ok(()) + } + + /// Verify signature on [`MintBolt11Request`] + pub fn verify_witness(&self, pubkey: PublicKey) -> Result<(), Error> { + let witness = self.witness.as_ref().ok_or(Error::WitnessMissing)?; + + let signature = Signature::from_str(witness).map_err(|_| Error::InvalidWitness)?; + + let msg_to_sign = self.msg_to_sign(); + + pubkey.verify(msg_to_sign.as_bytes(), &signature)?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_msg_to_sign() { + let request: MintBolt11Request = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}],"witness":"cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"}"#).unwrap(); + + let expected_msg_to_sign = "9d745270-1405-46de-b5c5-e2762b4f5e000342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c31102be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b5302209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"; + + let request_msg_to_sign = request.msg_to_sign(); + + assert_eq!(expected_msg_to_sign, request_msg_to_sign); + } + + #[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":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}], "witness": "d4b386f21f7aa7172f0994ee6e4dd966539484247ea71c99b81b8e09b1bb2acbc0026a43c221fd773471dc30d6a32b04692e6837ddaccf0830a63128308e4ee0"}"#).unwrap(); + + assert!(request.verify_witness(pubkey).is_ok()); + } + + #[test] + fn test_mint_request_signature() { + let mut request: MintBolt11Request = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}]}"#).unwrap(); + + let secret = + SecretKey::from_hex("50d7fd7aa2b2fe4607f41f4ce6f8794fc184dd47b8cdfbe4b3d1249aa02d35aa") + .unwrap(); + + request.sign(secret.clone()).unwrap(); + + assert!(request.verify_witness(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":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}],"witness":"cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"}"#).unwrap(); + + // Signature is on a different quote id verification should fail + assert!(request.verify_witness(pubkey).is_err()); + } +} diff --git a/crates/cdk/src/wallet/mint.rs b/crates/cdk/src/wallet/mint.rs index 77b9bb706..295bd9abd 100644 --- a/crates/cdk/src/wallet/mint.rs +++ b/crates/cdk/src/wallet/mint.rs @@ -6,7 +6,7 @@ use crate::dhke::construct_proofs; use crate::nuts::nut00::ProofsMethods; use crate::nuts::{ nut12, MintBolt11Request, MintQuoteBolt11Request, MintQuoteBolt11Response, PreMintSecrets, - SpendingConditions, State, + PublicKey, SecretKey, SpendingConditions, State, }; use crate::types::ProofInfo; use crate::util::unix_time; @@ -35,7 +35,7 @@ impl Wallet { /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None)?; /// let amount = Amount::from(100); /// - /// let quote = wallet.mint_quote(amount, None).await?; + /// let quote = wallet.mint_quote(amount, None, None).await?; /// Ok(()) /// } /// ``` @@ -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()) { @@ -157,10 +161,12 @@ impl Wallet { /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap(); /// let amount = Amount::from(100); /// - /// let quote = wallet.mint_quote(amount, None).await?; + /// let quote = wallet.mint_quote(amount, None, None).await?; /// let quote_id = quote.id; /// // To be called after quote request is paid - /// let amount_minted = wallet.mint("e_id, SplitTarget::default(), None).await?; + /// let amount_minted = wallet + /// .mint("e_id, SplitTarget::default(), None, None) + /// .await?; /// /// Ok(()) /// } @@ -171,6 +177,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 +226,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 b0f048b2e..346265a70 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 309a4c1cf..af793c882 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 f01519d10..b0cde25b2 100644 --- a/flake.lock +++ b/flake.lock @@ -21,11 +21,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1726560853, - "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -139,11 +139,11 @@ ] }, "locked": { - "lastModified": 1731378398, - "narHash": "sha256-a0QWaiX8+AJ9/XBLGMDy6c90GD7HzpxKVdlFwCke5Pw=", + "lastModified": 1731637922, + "narHash": "sha256-6iuzRINXyPX4DfUQZIGafpJnzjFXjVRYMymB10/jFFY=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "0ae9fc2f2fe5361837d59c0bdebbda176427111e", + "rev": "db10c66da18e816030b884388545add8cf096647", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index aebb30d00..468552bd4 100644 --- a/flake.nix +++ b/flake.nix @@ -53,7 +53,7 @@ targets = [ "wasm32-unknown-unknown" ]; # wasm }; - # Nighly for creating lock files + # Nightly used for formatting nightly_toolchain = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default.override { extensions = [ "rustfmt" "clippy" "rust-analyzer" ]; });