diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37149ba2..8ed515c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -144,7 +144,6 @@ jobs: -p cdk --no-default-features, -p cdk --no-default-features --features wallet, -p cdk --no-default-features --features mint, - -p cdk --no-default-features --features "mint swagger", -p cdk-axum, -p cdk-strike, -p cdk-lnbits, diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 00000000..956ba5ed --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,152 @@ +# Development Guide + +This guide will help you set up your development environment for working with the CDK repository. + +## Prerequisites + +Before you begin, ensure you have: +- Git installed on your system +- GitHub account +- Basic familiarity with command line operations + +## Initial Setup + +### 1. Fork and Clone the Repository + +1. Navigate to the CDK repository on GitHub +2. Click the "Fork" button in the top-right corner +3. Clone your forked repository: +```bash +git clone https://github.com/YOUR-USERNAME/cdk.git +cd cdk +``` + +### 2. Install Nix + + + +CDK uses [Nix](https://nixos.org/explore.html) for building, CI, and managing dev environment. +Note: only `Nix` (the language & package manager) and not the NixOS (the Linux distribution) is needed. +Nix can be installed on any Linux distribution and macOS. + +While it is technically possible to not use Nix, it is highly recommended as +it ensures consistent and reproducible environment for all developers. + +### Install Nix + +You have 2 options to install nix: + +* **RECOMMENDED:** The [Determinate Nix Installer](https://github.com/DeterminateSystems/nix-installer) +* [The official installer](https://nixos.org/download.html) + +Example: + +``` +> nix --version +nix (Nix) 2.9.1 +``` + +The exact version might be different. + +### Enable nix flakes + +If you installed Nix using the "determinate installer" you can skip this step. If you used the "official installer", edit either `~/.config/nix/nix.conf` or `/etc/nix/nix.conf` and add: + +``` +experimental-features = nix-command flakes +``` + +If the Nix installation is in multi-user mode, don’t forget to restart the nix-daemon. + +## Use Nix Shell + +```sh + nix develop -c $SHELL +``` + +## Common Development Tasks + +### Building the Project +```sh +just build +``` + +### Running Unit Tests +```bash +just test +``` + +### Running Integration Tests +```bash +just itest REDB/SQLITE/MEMEORY +``` + +### Running Format +```bash +just format +``` + + +### Running Clippy +```bash +just clippy +``` + +### Running final check before commit +```sh +just final-check +``` + + +## Best Practices + +1. **Branch Management** + - Create feature branches from `main` + - Use descriptive branch names: `feature/new-feature` or `fix/bug-description` + +2. **Commit Messages** + - Follow conventional commits format + - Begin with type: `feat:`, `fix:`, `docs:`, `chore:`, etc. + - Provide clear, concise descriptions + +3. **Testing** + - Write tests for new features + - Ensure all tests pass before submitting PR + - Include integration tests where applicable + +## Troubleshooting + +### Common Issues + +1. **Development Shell Issues** + - Clean Nix store: `nix-collect-garbage -d` + - Remove and recreate development shell + +### Getting Help + +- Open an issue on GitHub +- Check existing issues for similar problems +- Include relevant error messages and system information +- Reach out in Discord [Invite link](https://discord.gg/tUxMKd5YjN) + +## Contributing + +1. Create a feature branch +2. Make your changes +3. Run tests and formatting +4. Submit a pull request +5. Wait for review and address feedback + +## Additional Resources + +- [Nix Documentation](https://nixos.org/manual/nix/stable/) +- [Contributing Guidelines](CONTRIBUTING.md) + +## License + +Refer to the LICENSE file in the repository for terms of use and distribution. diff --git a/README.md b/README.md index 174184b2..47eb1b77 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,6 @@ The project is split up into several crates in the `crates/` directory: | [16][16] | Animated QR codes | :x: | | [17][17] | WebSocket subscriptions | :construction: | -MSRV ## Bindings @@ -75,6 +74,9 @@ All contributions welcome. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, shall be licensed as above, without any additional terms or conditions. +Please see the [development guide](DEVELOPMENT.md). + + [00]: https://github.com/cashubtc/nuts/blob/main/00.md [01]: https://github.com/cashubtc/nuts/blob/main/01.md [02]: https://github.com/cashubtc/nuts/blob/main/02.md diff --git a/bindings/cdk-js/src/types/melt_quote.rs b/bindings/cdk-js/src/types/melt_quote.rs index d6e6f325..c00ac8b7 100644 --- a/bindings/cdk-js/src/types/melt_quote.rs +++ b/bindings/cdk-js/src/types/melt_quote.rs @@ -33,7 +33,7 @@ impl JsMeltQuote { #[wasm_bindgen(getter)] pub fn unit(&self) -> JsCurrencyUnit { - self.inner.unit.into() + self.inner.unit.clone().into() } #[wasm_bindgen(getter)] diff --git a/bindings/cdk-js/src/types/mint_quote.rs b/bindings/cdk-js/src/types/mint_quote.rs index fed36a92..ebc7f0d7 100644 --- a/bindings/cdk-js/src/types/mint_quote.rs +++ b/bindings/cdk-js/src/types/mint_quote.rs @@ -33,7 +33,7 @@ impl JsMintQuote { #[wasm_bindgen(getter)] pub fn unit(&self) -> JsCurrencyUnit { - self.inner.unit.into() + self.inner.unit.clone().into() } #[wasm_bindgen(getter)] diff --git a/crates/cdk-cli/src/sub_commands/mint_info.rs b/crates/cdk-cli/src/sub_commands/mint_info.rs index bcf9f5ec..739d9934 100644 --- a/crates/cdk-cli/src/sub_commands/mint_info.rs +++ b/crates/cdk-cli/src/sub_commands/mint_info.rs @@ -1,5 +1,6 @@ use anyhow::Result; use cdk::mint_url::MintUrl; +use cdk::wallet::client::HttpClientMethods; use cdk::HttpClient; use clap::Args; use url::Url; diff --git a/crates/cdk-cli/src/sub_commands/pay_request.rs b/crates/cdk-cli/src/sub_commands/pay_request.rs index 1498f980..308df1d0 100644 --- a/crates/cdk-cli/src/sub_commands/pay_request.rs +++ b/crates/cdk-cli/src/sub_commands/pay_request.rs @@ -21,7 +21,7 @@ pub async fn pay_request( ) -> Result<()> { let payment_request = &sub_command_args.payment_request; - let unit = payment_request.unit; + let unit = &payment_request.unit; let amount = match payment_request.amount { Some(amount) => amount, @@ -56,7 +56,7 @@ pub async fn pay_request( } if let Some(unit) = unit { - if wallet.unit != unit { + if &wallet.unit != unit { continue; } } @@ -97,7 +97,7 @@ pub async fn pay_request( id: payment_request.payment_id.clone(), memo: None, mint: matching_wallet.mint_url.clone(), - unit: matching_wallet.unit, + unit: matching_wallet.unit.clone(), proofs, }; diff --git a/crates/cdk-cln/src/lib.rs b/crates/cdk-cln/src/lib.rs index 1e8f609c..49bdc5c4 100644 --- a/crates/cdk-cln/src/lib.rs +++ b/crates/cdk-cln/src/lib.rs @@ -81,8 +81,8 @@ impl MintLightning for Cln { Settings { mpp: true, unit: CurrencyUnit::Msat, - mint_settings: self.mint_settings, - melt_settings: self.melt_settings, + mint_settings: self.mint_settings.clone(), + melt_settings: self.melt_settings.clone(), invoice_description: true, } } diff --git a/crates/cdk-fake-wallet/src/lib.rs b/crates/cdk-fake-wallet/src/lib.rs index e0fba072..6c66466d 100644 --- a/crates/cdk-fake-wallet/src/lib.rs +++ b/crates/cdk-fake-wallet/src/lib.rs @@ -112,8 +112,8 @@ impl MintLightning for FakeWallet { Settings { mpp: true, unit: CurrencyUnit::Msat, - mint_settings: self.mint_settings, - melt_settings: self.melt_settings, + mint_settings: self.mint_settings.clone(), + melt_settings: self.melt_settings.clone(), invoice_description: true, } } diff --git a/crates/cdk-integration-tests/src/init_regtest.rs b/crates/cdk-integration-tests/src/init_regtest.rs index f9664c18..51abb263 100644 --- a/crates/cdk-integration-tests/src/init_regtest.rs +++ b/crates/cdk-integration-tests/src/init_regtest.rs @@ -180,6 +180,7 @@ where Arc::new(database), ln_backends, supported_units, + HashMap::new(), ) .await?; diff --git a/crates/cdk-integration-tests/src/lib.rs b/crates/cdk-integration-tests/src/lib.rs index eacc3b3a..31e45766 100644 --- a/crates/cdk-integration-tests/src/lib.rs +++ b/crates/cdk-integration-tests/src/lib.rs @@ -11,11 +11,11 @@ use cdk::cdk_lightning::MintLightning; use cdk::dhke::construct_proofs; use cdk::mint::FeeReserve; use cdk::nuts::{ - CurrencyUnit, Id, KeySet, MeltMethodSettings, MintInfo, MintMethodSettings, MintQuoteState, - Nuts, PaymentMethod, PreMintSecrets, Proofs, State, + CurrencyUnit, Id, KeySet, MeltMethodSettings, MintBolt11Request, MintInfo, MintMethodSettings, + MintQuoteBolt11Request, MintQuoteState, Nuts, PaymentMethod, PreMintSecrets, Proofs, State, }; use cdk::types::{LnKey, QuoteTTL}; -use cdk::wallet::client::HttpClient; +use cdk::wallet::client::{HttpClient, HttpClientMethods}; use cdk::{Mint, Wallet}; use cdk_fake_wallet::FakeWallet; use init_regtest::{get_mint_addr, get_mint_port, get_mint_url}; @@ -82,6 +82,7 @@ pub async fn start_mint( Arc::new(MintMemoryDatabase::default()), ln_backends.clone(), supported_units, + HashMap::new(), ) .await?; let cache_time_to_live = 3600; @@ -158,8 +159,14 @@ pub async fn mint_proofs( let wallet_client = HttpClient::new(); + let request = MintQuoteBolt11Request { + amount, + unit: CurrencyUnit::Sat, + description, + }; + let mint_quote = wallet_client - .post_mint_quote(mint_url.parse()?, 1.into(), CurrencyUnit::Sat, description) + .post_mint_quote(mint_url.parse()?, request) .await?; println!("Please pay: {}", mint_quote.request); @@ -179,13 +186,12 @@ pub async fn mint_proofs( let premint_secrets = PreMintSecrets::random(keyset_id, amount, &SplitTarget::default())?; - let mint_response = wallet_client - .post_mint( - mint_url.parse()?, - &mint_quote.quote, - premint_secrets.clone(), - ) - .await?; + let request = MintBolt11Request { + quote: mint_quote.quote, + outputs: premint_secrets.blinded_messages(), + }; + + let mint_response = wallet_client.post_mint(mint_url.parse()?, request).await?; let pre_swap_proofs = construct_proofs( mint_response.signatures, diff --git a/crates/cdk-integration-tests/tests/fake_wallet.rs b/crates/cdk-integration-tests/tests/fake_wallet.rs index 9b6f3ddb..e1eee402 100644 --- a/crates/cdk-integration-tests/tests/fake_wallet.rs +++ b/crates/cdk-integration-tests/tests/fake_wallet.rs @@ -5,8 +5,11 @@ use bip39::Mnemonic; use cdk::{ amount::SplitTarget, cdk_database::WalletMemoryDatabase, - nuts::{CurrencyUnit, MeltQuoteState, PreMintSecrets, State}, - wallet::{client::HttpClient, Wallet}, + nuts::{CurrencyUnit, MeltBolt11Request, MeltQuoteState, PreMintSecrets, State}, + wallet::{ + client::{HttpClient, HttpClientMethods}, + Wallet, + }, }; use cdk_fake_wallet::{create_fake_invoice, FakeInvoiceDescription}; use cdk_integration_tests::attempt_to_swap_pending; @@ -354,28 +357,23 @@ async fn test_fake_melt_change_in_quote() -> Result<()> { let client = HttpClient::new(); - let melt_response = client - .post_melt( - MINT_URL.parse()?, - melt_quote.id.clone(), - proofs.clone(), - Some(premint_secrets.blinded_messages()), - ) - .await?; + let melt_request = MeltBolt11Request { + quote: melt_quote.id.clone(), + inputs: proofs.clone(), + outputs: Some(premint_secrets.blinded_messages()), + }; + + let melt_response = client.post_melt(MINT_URL.parse()?, melt_request).await?; assert!(melt_response.change.is_some()); let check = wallet.melt_quote_status(&melt_quote.id).await?; + let mut melt_change = melt_response.change.unwrap(); + melt_change.sort_by(|a, b| a.amount.cmp(&b.amount)); + + let mut check = check.change.unwrap(); + check.sort_by(|a, b| a.amount.cmp(&b.amount)); - assert_eq!( - melt_response - .change - .unwrap() - .sort_by(|a, b| a.amount.cmp(&b.amount)), - check - .change - .unwrap() - .sort_by(|a, b| a.amount.cmp(&b.amount)) - ); + assert_eq!(melt_change, check); Ok(()) } diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index c5cfbc58..b75f0378 100644 --- a/crates/cdk-integration-tests/tests/mint.rs +++ b/crates/cdk-integration-tests/tests/mint.rs @@ -52,6 +52,7 @@ async fn new_mint(fee: u64) -> Mint { Arc::new(MintMemoryDatabase::default()), HashMap::new(), supported_units, + HashMap::new(), ) .await .unwrap() @@ -327,7 +328,8 @@ async fn test_swap_unbalanced() -> Result<()> { async fn test_swap_overpay_underpay_fee() -> Result<()> { let mint = new_mint(1).await; - mint.rotate_keyset(CurrencyUnit::Sat, 1, 32, 1).await?; + mint.rotate_keyset(CurrencyUnit::Sat, 1, 32, 1, HashMap::new()) + .await?; let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; let keyset_id = Id::from(&keys); diff --git a/crates/cdk-integration-tests/tests/regtest.rs b/crates/cdk-integration-tests/tests/regtest.rs index 900e93bf..2ba54595 100644 --- a/crates/cdk-integration-tests/tests/regtest.rs +++ b/crates/cdk-integration-tests/tests/regtest.rs @@ -6,9 +6,13 @@ use cdk::{ amount::{Amount, SplitTarget}, cdk_database::WalletMemoryDatabase, nuts::{ - CurrencyUnit, MeltQuoteState, MintQuoteState, NotificationPayload, PreMintSecrets, State, + CurrencyUnit, MeltQuoteState, MintBolt11Request, MintQuoteState, NotificationPayload, + PreMintSecrets, State, + }, + wallet::{ + client::{HttpClient, HttpClientMethods}, + Wallet, }, - wallet::{client::HttpClient, Wallet}, }; use cdk_integration_tests::init_regtest::{ get_mint_url, get_mint_ws_url, init_cln_client, init_lnd_client, @@ -362,15 +366,16 @@ async fn test_cached_mint() -> Result<()> { let premint_secrets = PreMintSecrets::random(active_keyset_id, 31.into(), &SplitTarget::default()).unwrap(); + let request = MintBolt11Request { + quote: quote.id, + outputs: premint_secrets.blinded_messages(), + }; + let response = http_client - .post_mint( - get_mint_url().as_str().parse()?, - "e.id, - premint_secrets.clone(), - ) + .post_mint(get_mint_url().as_str().parse()?, request.clone()) .await?; let response1 = http_client - .post_mint(get_mint_url().as_str().parse()?, "e.id, premint_secrets) + .post_mint(get_mint_url().as_str().parse()?, request) .await?; assert!(response == response1); diff --git a/crates/cdk-lnbits/src/lib.rs b/crates/cdk-lnbits/src/lib.rs index 64e7bef7..21103d12 100644 --- a/crates/cdk-lnbits/src/lib.rs +++ b/crates/cdk-lnbits/src/lib.rs @@ -80,8 +80,8 @@ impl MintLightning for LNbits { Settings { mpp: false, unit: CurrencyUnit::Sat, - mint_settings: self.mint_settings, - melt_settings: self.melt_settings, + mint_settings: self.mint_settings.clone(), + melt_settings: self.melt_settings.clone(), invoice_description: true, } } diff --git a/crates/cdk-lnd/src/lib.rs b/crates/cdk-lnd/src/lib.rs index c5ecbeb8..b2319004 100644 --- a/crates/cdk-lnd/src/lib.rs +++ b/crates/cdk-lnd/src/lib.rs @@ -88,8 +88,8 @@ impl MintLightning for Lnd { Settings { mpp: true, unit: CurrencyUnit::Msat, - mint_settings: self.mint_settings, - melt_settings: self.melt_settings, + mint_settings: self.mint_settings.clone(), + melt_settings: self.melt_settings.clone(), invoice_description: true, } } diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index 695f1d4c..17bd6379 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -188,7 +188,7 @@ async fn main() -> anyhow::Result<()> { api_key.clone(), MintMethodSettings::default(), MeltMethodSettings::default(), - unit, + unit.clone(), Arc::new(Mutex::new(Some(receiver))), webhook_url.to_string(), ) @@ -199,7 +199,7 @@ async fn main() -> anyhow::Result<()> { .await?; routers.push(router); - let ln_key = LnKey::new(unit, PaymentMethod::Bolt11); + let ln_key = LnKey::new(unit.clone(), PaymentMethod::Bolt11); ln_backends.insert(ln_key, Arc::new(strike)); @@ -237,7 +237,7 @@ async fn main() -> anyhow::Result<()> { let unit = CurrencyUnit::Sat; - let ln_key = LnKey::new(unit, PaymentMethod::Bolt11); + let ln_key = LnKey::new(unit.clone(), PaymentMethod::Bolt11); ln_backends.insert(ln_key, Arc::new(lnbits)); @@ -326,7 +326,7 @@ async fn main() -> anyhow::Result<()> { let units = settings.fake_wallet.unwrap_or_default().supported_units; for unit in units { - let ln_key = LnKey::new(unit, PaymentMethod::Bolt11); + let ln_key = LnKey::new(unit.clone(), PaymentMethod::Bolt11); let wallet = Arc::new(FakeWallet::new( fee_reserve.clone(), @@ -361,13 +361,13 @@ async fn main() -> anyhow::Result<()> { let m = MppMethodSettings { method: key.method, - unit: key.unit, + unit: key.unit.clone(), mpp: settings.mpp, }; let n4 = MintMethodSettings { method: key.method, - unit: key.unit, + unit: key.unit.clone(), min_amount: settings.mint_settings.min_amount, max_amount: settings.mint_settings.max_amount, description: settings.invoice_description, @@ -375,7 +375,7 @@ async fn main() -> anyhow::Result<()> { let n5 = MeltMethodSettings { method: key.method, - unit: key.unit, + unit: key.unit.clone(), min_amount: settings.melt_settings.min_amount, max_amount: settings.melt_settings.max_amount, }; @@ -438,6 +438,7 @@ async fn main() -> anyhow::Result<()> { localstore, ln_backends.clone(), supported_units, + HashMap::new(), ) .await?; diff --git a/crates/cdk-phoenixd/src/lib.rs b/crates/cdk-phoenixd/src/lib.rs index 4aecfac6..16197f65 100644 --- a/crates/cdk-phoenixd/src/lib.rs +++ b/crates/cdk-phoenixd/src/lib.rs @@ -86,8 +86,8 @@ impl MintLightning for Phoenixd { Settings { mpp: false, unit: CurrencyUnit::Sat, - mint_settings: self.mint_settings, - melt_settings: self.melt_settings, + mint_settings: self.mint_settings.clone(), + melt_settings: self.melt_settings.clone(), invoice_description: true, } } diff --git a/crates/cdk-strike/src/lib.rs b/crates/cdk-strike/src/lib.rs index 76eb0f25..7273112e 100644 --- a/crates/cdk-strike/src/lib.rs +++ b/crates/cdk-strike/src/lib.rs @@ -77,9 +77,9 @@ impl MintLightning for Strike { fn get_settings(&self) -> Settings { Settings { mpp: false, - unit: self.unit, - mint_settings: self.mint_settings, - melt_settings: self.melt_settings, + unit: self.unit.clone(), + mint_settings: self.mint_settings.clone(), + melt_settings: self.melt_settings.clone(), invoice_description: true, } } @@ -288,7 +288,7 @@ impl MintLightning for Strike { payment_preimage: None, status: state, total_spent: from_strike_amount(invoice.total_amount, &self.unit)?.into(), - unit: self.unit, + unit: self.unit.clone(), } } Err(err) => match err { @@ -297,7 +297,7 @@ impl MintLightning for Strike { payment_preimage: None, status: MeltQuoteState::Unknown, total_spent: Amount::ZERO, - unit: self.unit, + unit: self.unit.clone(), }, _ => { return Err(Error::from(err).into()); diff --git a/crates/cdk/Cargo.toml b/crates/cdk/Cargo.toml index e89311a5..81673861 100644 --- a/crates/cdk/Cargo.toml +++ b/crates/cdk/Cargo.toml @@ -13,6 +13,7 @@ license = "MIT" [features] default = ["mint", "wallet"] mint = ["dep:futures"] +# We do not commit to a MSRV with swagger enabled swagger = ["mint", "dep:utoipa"] wallet = ["dep:reqwest"] bench = [] diff --git a/crates/cdk/src/mint/keysets.rs b/crates/cdk/src/mint/keysets.rs index 3c9642be..66445608 100644 --- a/crates/cdk/src/mint/keysets.rs +++ b/crates/cdk/src/mint/keysets.rs @@ -1,5 +1,6 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; +use bitcoin::bip32::DerivationPath; use tracing::instrument; use crate::Error; @@ -89,14 +90,20 @@ impl Mint { derivation_path_index: u32, max_order: u8, input_fee_ppk: u64, + custom_paths: HashMap, ) -> Result<(), Error> { - let derivation_path = derivation_path_from_unit(unit, derivation_path_index); + let derivation_path = match custom_paths.get(&unit) { + Some(path) => path.clone(), + None => derivation_path_from_unit(unit.clone(), derivation_path_index) + .ok_or(Error::UnsupportedUnit)?, + }; + let (keyset, keyset_info) = create_new_keyset( &self.secp_ctx, self.xpriv, derivation_path, Some(derivation_path_index), - unit, + unit.clone(), max_order, input_fee_ppk, ); diff --git a/crates/cdk/src/mint/melt.rs b/crates/cdk/src/mint/melt.rs index a803868d..1613fc3c 100644 --- a/crates/cdk/src/mint/melt.rs +++ b/crates/cdk/src/mint/melt.rs @@ -74,11 +74,11 @@ impl Mint { } }; - self.check_melt_request_acceptable(amount, *unit, PaymentMethod::Bolt11)?; + self.check_melt_request_acceptable(amount, unit.clone(), PaymentMethod::Bolt11)?; let ln = self .ln - .get(&LnKey::new(*unit, PaymentMethod::Bolt11)) + .get(&LnKey::new(unit.clone(), PaymentMethod::Bolt11)) .ok_or_else(|| { tracing::info!("Could not get ln backend for {}, bolt11 ", unit); @@ -97,7 +97,7 @@ impl Mint { let quote = MeltQuote::new( request.to_string(), - *unit, + unit.clone(), payment_quote.amount, payment_quote.fee, unix_time() + self.quote_ttl.melt_ttl, @@ -457,7 +457,10 @@ impl Mint { } _ => None, }; - let ln = match self.ln.get(&LnKey::new(quote.unit, PaymentMethod::Bolt11)) { + let ln = match self + .ln + .get(&LnKey::new(quote.unit.clone(), PaymentMethod::Bolt11)) + { Some(ln) => ln, None => { tracing::info!("Could not get ln backend for {}, bolt11 ", quote.unit); diff --git a/crates/cdk/src/mint/mint_nut04.rs b/crates/cdk/src/mint/mint_nut04.rs index 92d032e5..9d78473d 100644 --- a/crates/cdk/src/mint/mint_nut04.rs +++ b/crates/cdk/src/mint/mint_nut04.rs @@ -12,7 +12,7 @@ impl Mint { fn check_mint_request_acceptable( &self, amount: Amount, - unit: CurrencyUnit, + unit: &CurrencyUnit, ) -> Result<(), Error> { let nut04 = &self.mint_info.nuts.nut04; @@ -20,7 +20,7 @@ impl Mint { return Err(Error::MintingDisabled); } - match nut04.get_settings(&unit, &PaymentMethod::Bolt11) { + match nut04.get_settings(unit, &PaymentMethod::Bolt11) { Some(settings) => { if settings .max_amount @@ -64,11 +64,11 @@ impl Mint { description, } = mint_quote_request; - self.check_mint_request_acceptable(amount, unit)?; + self.check_mint_request_acceptable(amount, &unit)?; let ln = self .ln - .get(&LnKey::new(unit, PaymentMethod::Bolt11)) + .get(&LnKey::new(unit.clone(), PaymentMethod::Bolt11)) .ok_or_else(|| { tracing::info!("Bolt11 mint request for unsupported unit"); @@ -98,7 +98,7 @@ impl Mint { let quote = MintQuote::new( self.mint_url.clone(), create_invoice_response.request.to_string(), - unit, + unit.clone(), amount, create_invoice_response.expiry.unwrap_or(0), create_invoice_response.request_lookup_id.clone(), diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index 29a0b16f..d81d9b65 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -56,6 +56,7 @@ pub struct Mint { impl Mint { /// Create new [`Mint`] + #[allow(clippy::too_many_arguments)] pub async fn new( mint_url: &str, seed: &[u8], @@ -65,6 +66,7 @@ impl Mint { ln: HashMap + Send + Sync>>, // Hashmap where the key is the unit and value is (input fee ppk, max_order) supported_units: HashMap, + custom_paths: HashMap, ) -> Result { let secp_ctx = Secp256k1::new(); let xpriv = Xpriv::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted"); @@ -85,7 +87,7 @@ impl Mint { let keysets_by_unit: HashMap> = keysets_infos.iter().fold(HashMap::new(), |mut acc, ks| { - acc.entry(ks.unit).or_default().push(ks.clone()); + acc.entry(ks.unit.clone()).or_default().push(ks.clone()); acc }); @@ -114,7 +116,7 @@ impl Mint { &secp_ctx, xpriv, highest_index_keyset.max_order, - highest_index_keyset.unit, + highest_index_keyset.unit.clone(), highest_index_keyset.derivation_path.clone(), ); active_keysets.insert(id, keyset); @@ -127,37 +129,46 @@ impl Mint { highest_index_keyset.derivation_path_index.unwrap_or(0) + 1 }; - let derivation_path = derivation_path_from_unit(unit, derivation_path_index); + let derivation_path = match custom_paths.get(&unit) { + Some(path) => path.clone(), + None => derivation_path_from_unit(unit.clone(), derivation_path_index) + .ok_or(Error::UnsupportedUnit)?, + }; let (keyset, keyset_info) = create_new_keyset( &secp_ctx, xpriv, derivation_path, Some(derivation_path_index), - unit, + unit.clone(), *max_order, *input_fee_ppk, ); let id = keyset_info.id; localstore.add_keyset_info(keyset_info).await?; - localstore.set_active_keyset(unit, id).await?; + localstore.set_active_keyset(unit.clone(), id).await?; active_keysets.insert(id, keyset); - active_keyset_units.push(unit); + active_keyset_units.push(unit.clone()); } } } for (unit, (fee, max_order)) in supported_units { if !active_keyset_units.contains(&unit) { - let derivation_path = derivation_path_from_unit(unit, 0); + let derivation_path = match custom_paths.get(&unit) { + Some(path) => path.clone(), + None => { + derivation_path_from_unit(unit.clone(), 0).ok_or(Error::UnsupportedUnit)? + } + }; let (keyset, keyset_info) = create_new_keyset( &secp_ctx, xpriv, derivation_path, Some(0), - unit, + unit.clone(), max_order, fee, ); @@ -197,7 +208,7 @@ impl Mint { let mint = Arc::clone(&mint_arc); let ln = Arc::clone(ln); let shutdown = Arc::clone(&shutdown); - let key = *key; + let key = key.clone(); join_set.spawn(async move { if !ln.is_wait_invoice_active() { loop { @@ -441,7 +452,8 @@ impl Mint { Ok(RestoreResponse { outputs, - signatures, + signatures: signatures.clone(), + promises: Some(signatures), }) } @@ -562,7 +574,7 @@ fn create_new_keyset( ); let keyset_info = MintKeySetInfo { id: keyset.id, - unit: keyset.unit, + unit: keyset.unit.clone(), active: true, valid_from: unix_time(), valid_to: None, @@ -574,12 +586,17 @@ fn create_new_keyset( (keyset, keyset_info) } -fn derivation_path_from_unit(unit: CurrencyUnit, index: u32) -> DerivationPath { - DerivationPath::from(vec![ +fn derivation_path_from_unit(unit: CurrencyUnit, index: u32) -> Option { + let unit_index = match unit.derivation_index() { + Some(index) => index, + None => return None, + }; + + Some(DerivationPath::from(vec![ ChildNumber::from_hardened_idx(0).expect("0 is a valid index"), - ChildNumber::from_hardened_idx(unit.derivation_index()).expect("0 is a valid index"), + ChildNumber::from_hardened_idx(unit_index).expect("0 is a valid index"), ChildNumber::from_hardened_idx(index).expect("0 is a valid index"), - ]) + ])) } #[cfg(test)] @@ -601,7 +618,7 @@ mod tests { seed, 2, CurrencyUnit::Sat, - derivation_path_from_unit(CurrencyUnit::Sat, 0), + derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(), ); assert_eq!(keyset.unit, CurrencyUnit::Sat); @@ -645,7 +662,7 @@ mod tests { xpriv, 2, CurrencyUnit::Sat, - derivation_path_from_unit(CurrencyUnit::Sat, 0), + derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(), ); assert_eq!(keyset.unit, CurrencyUnit::Sat); @@ -725,6 +742,7 @@ mod tests { localstore, HashMap::new(), config.supported_units, + HashMap::new(), ) .await } @@ -780,7 +798,8 @@ mod tests { assert!(keysets.keysets.is_empty()); // generate the first keyset and set it to active - mint.rotate_keyset(CurrencyUnit::default(), 0, 1, 1).await?; + mint.rotate_keyset(CurrencyUnit::default(), 0, 1, 1, HashMap::new()) + .await?; let keysets = mint.keysets().await.unwrap(); assert!(keysets.keysets.len().eq(&1)); @@ -788,7 +807,8 @@ mod tests { let first_keyset_id = keysets.keysets[0].id; // set the first keyset to inactive and generate a new keyset - mint.rotate_keyset(CurrencyUnit::default(), 1, 1, 1).await?; + mint.rotate_keyset(CurrencyUnit::default(), 1, 1, 1, HashMap::new()) + .await?; let keysets = mint.keysets().await.unwrap(); diff --git a/crates/cdk/src/nuts/nut00/mod.rs b/crates/cdk/src/nuts/nut00/mod.rs index d6c3dffd..929b0c05 100644 --- a/crates/cdk/src/nuts/nut00/mod.rs +++ b/crates/cdk/src/nuts/nut00/mod.rs @@ -361,7 +361,7 @@ where /// Currency Unit #[non_exhaustive] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub enum CurrencyUnit { /// Sat @@ -373,17 +373,20 @@ pub enum CurrencyUnit { Usd, /// Euro Eur, + /// Custom currency unit + Custom(String), } #[cfg(feature = "mint")] impl CurrencyUnit { /// Derivation index mint will use for unit - pub fn derivation_index(&self) -> u32 { + pub fn derivation_index(&self) -> Option { match self { - Self::Sat => 0, - Self::Msat => 1, - Self::Usd => 2, - Self::Eur => 3, + Self::Sat => Some(0), + Self::Msat => Some(1), + Self::Usd => Some(2), + Self::Eur => Some(3), + _ => None, } } } @@ -391,12 +394,13 @@ impl CurrencyUnit { impl FromStr for CurrencyUnit { type Err = Error; fn from_str(value: &str) -> Result { - match value { - "sat" => Ok(Self::Sat), - "msat" => Ok(Self::Msat), - "usd" => Ok(Self::Usd), - "eur" => Ok(Self::Eur), - _ => Err(Error::UnsupportedUnit), + let value = &value.to_uppercase(); + match value.as_str() { + "SAT" => Ok(Self::Sat), + "MSAT" => Ok(Self::Msat), + "USD" => Ok(Self::Usd), + "EUR" => Ok(Self::Eur), + c => Ok(Self::Custom(c.to_string())), } } } @@ -404,15 +408,16 @@ impl FromStr for CurrencyUnit { impl fmt::Display for CurrencyUnit { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { - CurrencyUnit::Sat => "sat", - CurrencyUnit::Msat => "msat", - CurrencyUnit::Usd => "usd", - CurrencyUnit::Eur => "eur", + CurrencyUnit::Sat => "SAT", + CurrencyUnit::Msat => "MSAT", + CurrencyUnit::Usd => "USD", + CurrencyUnit::Eur => "EUR", + CurrencyUnit::Custom(unit) => unit, }; if let Some(width) = f.width() { - write!(f, "{:width$}", s, width = width) + write!(f, "{:width$}", s.to_lowercase(), width = width) } else { - write!(f, "{}", s) + write!(f, "{}", s.to_lowercase()) } } } diff --git a/crates/cdk/src/nuts/nut00/token.rs b/crates/cdk/src/nuts/nut00/token.rs index 96e15302..dccd695b 100644 --- a/crates/cdk/src/nuts/nut00/token.rs +++ b/crates/cdk/src/nuts/nut00/token.rs @@ -92,8 +92,8 @@ impl Token { /// Unit pub fn unit(&self) -> Option { match self { - Self::TokenV3(token) => *token.unit(), - Self::TokenV4(token) => Some(token.unit()), + Self::TokenV3(token) => token.unit().clone(), + Self::TokenV4(token) => Some(token.unit().clone()), } } @@ -326,8 +326,8 @@ impl TokenV4 { /// Unit #[inline] - pub fn unit(&self) -> CurrencyUnit { - self.unit + pub fn unit(&self) -> &CurrencyUnit { + &self.unit } } @@ -525,7 +525,7 @@ mod tests { token.token[0].proofs[0].clone().keyset_id, Id::from_str("009a1f293253e41e").unwrap() ); - assert_eq!(token.unit.unwrap(), CurrencyUnit::Sat); + assert_eq!(token.unit.clone().unwrap(), CurrencyUnit::Sat); let encoded = &token.to_string(); diff --git a/crates/cdk/src/nuts/nut03.rs b/crates/cdk/src/nuts/nut03.rs index 535c89a9..cee5f063 100644 --- a/crates/cdk/src/nuts/nut03.rs +++ b/crates/cdk/src/nuts/nut03.rs @@ -32,11 +32,11 @@ pub struct PreSwap { pub fee: Amount, } -/// Split Request [NUT-06] +/// Swap Request [NUT-03] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct SwapRequest { - /// Proofs that are to be spent in `Split` + /// Proofs that are to be spent in a `Swap` #[cfg_attr(feature = "swagger", schema(value_type = Vec))] pub inputs: Proofs, /// Blinded Messages for Mint to sign diff --git a/crates/cdk/src/nuts/nut04.rs b/crates/cdk/src/nuts/nut04.rs index 207a7a9a..40a6f8d4 100644 --- a/crates/cdk/src/nuts/nut04.rs +++ b/crates/cdk/src/nuts/nut04.rs @@ -137,7 +137,7 @@ pub struct MintBolt11Response { } /// Mint Method Settings -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct MintMethodSettings { /// Payment Method e.g. bolt11 @@ -179,7 +179,7 @@ impl Settings { ) -> Option { for method_settings in self.methods.iter() { if method_settings.method.eq(method) && method_settings.unit.eq(unit) { - return Some(*method_settings); + return Some(method_settings.clone()); } } diff --git a/crates/cdk/src/nuts/nut05.rs b/crates/cdk/src/nuts/nut05.rs index 23cb4738..ff99a38e 100644 --- a/crates/cdk/src/nuts/nut05.rs +++ b/crates/cdk/src/nuts/nut05.rs @@ -252,7 +252,7 @@ impl MeltBolt11Request { } /// Melt Method Settings -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct MeltMethodSettings { /// Payment Method e.g. bolt11 @@ -281,7 +281,7 @@ impl Settings { ) -> Option { for method_settings in self.methods.iter() { if method_settings.method.eq(method) && method_settings.unit.eq(unit) { - return Some(*method_settings); + return Some(method_settings.clone()); } } diff --git a/crates/cdk/src/nuts/nut09.rs b/crates/cdk/src/nuts/nut09.rs index abcd8f85..5f04045f 100644 --- a/crates/cdk/src/nuts/nut09.rs +++ b/crates/cdk/src/nuts/nut09.rs @@ -22,6 +22,9 @@ pub struct RestoreResponse { pub outputs: Vec, /// Signatures pub signatures: Vec, + /// Promises + // Temp compatibility with cashu-ts + pub promises: Option>, } mod test { diff --git a/crates/cdk/src/nuts/nut18.rs b/crates/cdk/src/nuts/nut18.rs index d165b148..cb19735e 100644 --- a/crates/cdk/src/nuts/nut18.rs +++ b/crates/cdk/src/nuts/nut18.rs @@ -154,7 +154,7 @@ mod tests { assert_eq!(&req.payment_id.unwrap(), "b7a90176"); assert_eq!(req.amount.unwrap(), 10.into()); - assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat); + assert_eq!(req.unit.clone().unwrap(), CurrencyUnit::Sat); assert_eq!( req.mints.unwrap(), vec![MintUrl::from_str("https://nofees.testnut.cashu.space")?] @@ -190,7 +190,7 @@ mod tests { assert_eq!(&req.payment_id.unwrap(), "b7a90176"); assert_eq!(req.amount.unwrap(), 10.into()); - assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat); + assert_eq!(req.unit.clone().unwrap(), CurrencyUnit::Sat); assert_eq!( req.mints.unwrap(), vec![MintUrl::from_str("https://nofees.testnut.cashu.space")?] diff --git a/crates/cdk/src/types.rs b/crates/cdk/src/types.rs index 213a8e7e..e3d91eca 100644 --- a/crates/cdk/src/types.rs +++ b/crates/cdk/src/types.rs @@ -141,7 +141,7 @@ impl ProofInfo { /// Key used in hashmap of ln backends to identify what unit and payment method /// it is for -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct LnKey { /// Unit of Payment backend pub unit: CurrencyUnit, diff --git a/crates/cdk/src/wallet/balance.rs b/crates/cdk/src/wallet/balance.rs index 6d127646..46c814ac 100644 --- a/crates/cdk/src/wallet/balance.rs +++ b/crates/cdk/src/wallet/balance.rs @@ -19,7 +19,7 @@ impl Wallet { // TODO If only the proofs for this wallet's unit are retrieved, why build a map with key = unit? let balances = proofs.iter().fold(HashMap::new(), |mut acc, proof| { - *acc.entry(self.unit).or_insert(Amount::ZERO) += proof.amount; + *acc.entry(self.unit.clone()).or_insert(Amount::ZERO) += proof.amount; acc }); @@ -33,7 +33,7 @@ impl Wallet { // TODO If only the proofs for this wallet's unit are retrieved, why build a map with key = unit? let balances = proofs.iter().fold(HashMap::new(), |mut acc, proof| { - *acc.entry(self.unit).or_insert(Amount::ZERO) += proof.amount; + *acc.entry(self.unit.clone()).or_insert(Amount::ZERO) += proof.amount; acc }); diff --git a/crates/cdk/src/wallet/client.rs b/crates/cdk/src/wallet/client.rs index d5b413ce..4296e6c2 100644 --- a/crates/cdk/src/wallet/client.rs +++ b/crates/cdk/src/wallet/client.rs @@ -1,5 +1,8 @@ //! Wallet client +use std::fmt::Debug; + +use async_trait::async_trait; use reqwest::Client; use serde_json::Value; use tracing::instrument; @@ -8,15 +11,12 @@ use url::Url; use super::Error; use crate::error::ErrorResponse; use crate::mint_url::MintUrl; -use crate::nuts::nut15::Mpp; use crate::nuts::{ - BlindedMessage, CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysResponse, - KeysetResponse, MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, - MintBolt11Request, MintBolt11Response, MintInfo, MintQuoteBolt11Request, - MintQuoteBolt11Response, PreMintSecrets, Proof, PublicKey, RestoreRequest, RestoreResponse, - SwapRequest, SwapResponse, + CheckStateRequest, CheckStateResponse, Id, KeySet, KeysResponse, KeysetResponse, + MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request, + MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, RestoreRequest, + RestoreResponse, SwapRequest, SwapResponse, }; -use crate::{Amount, Bolt11Invoice}; /// Http Client #[derive(Debug, Clone)] @@ -67,10 +67,14 @@ impl HttpClient { Ok(Self { inner: client }) } +} +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl HttpClientMethods for HttpClient { /// Get Active Mint Keys [NUT-01] #[instrument(skip(self), fields(mint_url = %mint_url))] - pub async fn get_mint_keys(&self, mint_url: MintUrl) -> Result, Error> { + async fn get_mint_keys(&self, mint_url: MintUrl) -> Result, Error> { let url = mint_url.join_paths(&["v1", "keys"])?; let keys = self.inner.get(url).send().await?.json::().await?; @@ -82,7 +86,7 @@ impl HttpClient { /// Get Keyset Keys [NUT-01] #[instrument(skip(self), fields(mint_url = %mint_url))] - pub async fn get_mint_keyset(&self, mint_url: MintUrl, keyset_id: Id) -> Result { + async fn get_mint_keyset(&self, mint_url: MintUrl, keyset_id: Id) -> Result { let url = mint_url.join_paths(&["v1", "keys", &keyset_id.to_string()])?; let keys = self.inner.get(url).send().await?.json::().await?; @@ -94,7 +98,7 @@ impl HttpClient { /// Get Keysets [NUT-02] #[instrument(skip(self), fields(mint_url = %mint_url))] - pub async fn get_mint_keysets(&self, mint_url: MintUrl) -> Result { + async fn get_mint_keysets(&self, mint_url: MintUrl) -> Result { let url = mint_url.join_paths(&["v1", "keysets"])?; let res = self.inner.get(url).send().await?.json::().await?; @@ -106,21 +110,13 @@ impl HttpClient { /// Mint Quote [NUT-04] #[instrument(skip(self), fields(mint_url = %mint_url))] - pub async fn post_mint_quote( + async fn post_mint_quote( &self, mint_url: MintUrl, - amount: Amount, - unit: CurrencyUnit, - description: Option, + request: MintQuoteBolt11Request, ) -> Result { let url = mint_url.join_paths(&["v1", "mint", "quote", "bolt11"])?; - let request = MintQuoteBolt11Request { - amount, - unit, - description, - }; - let res = self .inner .post(url) @@ -141,7 +137,7 @@ impl HttpClient { /// Mint Quote status #[instrument(skip(self), fields(mint_url = %mint_url))] - pub async fn get_mint_quote_status( + async fn get_mint_quote_status( &self, mint_url: MintUrl, quote_id: &str, @@ -160,20 +156,14 @@ impl HttpClient { } /// Mint Tokens [NUT-04] - #[instrument(skip(self, quote, premint_secrets), fields(mint_url = %mint_url))] - pub async fn post_mint( + #[instrument(skip(self, request), fields(mint_url = %mint_url))] + async fn post_mint( &self, mint_url: MintUrl, - quote: &str, - premint_secrets: PreMintSecrets, + request: MintBolt11Request, ) -> Result { let url = mint_url.join_paths(&["v1", "mint", "bolt11"])?; - let request = MintBolt11Request { - quote: quote.to_string(), - outputs: premint_secrets.blinded_messages(), - }; - let res = self .inner .post(url) @@ -191,23 +181,13 @@ impl HttpClient { /// Melt Quote [NUT-05] #[instrument(skip(self, request), fields(mint_url = %mint_url))] - pub async fn post_melt_quote( + async fn post_melt_quote( &self, mint_url: MintUrl, - unit: CurrencyUnit, - request: Bolt11Invoice, - mpp_amount: Option, + request: MeltQuoteBolt11Request, ) -> Result { let url = mint_url.join_paths(&["v1", "melt", "quote", "bolt11"])?; - let options = mpp_amount.map(|amount| Mpp { amount }); - - let request = MeltQuoteBolt11Request { - request, - unit, - options, - }; - let res = self .inner .post(url) @@ -225,7 +205,7 @@ impl HttpClient { /// Melt Quote Status #[instrument(skip(self), fields(mint_url = %mint_url))] - pub async fn get_melt_quote_status( + async fn get_melt_quote_status( &self, mint_url: MintUrl, quote_id: &str, @@ -242,22 +222,14 @@ impl HttpClient { /// Melt [NUT-05] /// [Nut-08] Lightning fee return if outputs defined - #[instrument(skip(self, quote, inputs, outputs), fields(mint_url = %mint_url))] - pub async fn post_melt( + #[instrument(skip(self, request), fields(mint_url = %mint_url))] + async fn post_melt( &self, mint_url: MintUrl, - quote: String, - inputs: Vec, - outputs: Option>, + request: MeltBolt11Request, ) -> Result { let url = mint_url.join_paths(&["v1", "melt", "bolt11"])?; - let request = MeltBolt11Request { - quote, - inputs, - outputs, - }; - let res = self .inner .post(url) @@ -278,9 +250,9 @@ impl HttpClient { } } - /// Split Token [NUT-06] + /// Swap Token [NUT-03] #[instrument(skip(self, swap_request), fields(mint_url = %mint_url))] - pub async fn post_swap( + async fn post_swap( &self, mint_url: MintUrl, swap_request: SwapRequest, @@ -304,7 +276,7 @@ impl HttpClient { /// Get Mint Info [NUT-06] #[instrument(skip(self), fields(mint_url = %mint_url))] - pub async fn get_mint_info(&self, mint_url: MintUrl) -> Result { + async fn get_mint_info(&self, mint_url: MintUrl) -> Result { let url = mint_url.join_paths(&["v1", "info"])?; let res = self.inner.get(url).send().await?.json::().await?; @@ -319,14 +291,13 @@ impl HttpClient { } /// Spendable check [NUT-07] - #[instrument(skip(self), fields(mint_url = %mint_url))] - pub async fn post_check_state( + #[instrument(skip(self, request), fields(mint_url = %mint_url))] + async fn post_check_state( &self, mint_url: MintUrl, - ys: Vec, + request: CheckStateRequest, ) -> Result { let url = mint_url.join_paths(&["v1", "checkstate"])?; - let request = CheckStateRequest { ys }; let res = self .inner @@ -345,7 +316,7 @@ impl HttpClient { /// Restore request [NUT-13] #[instrument(skip(self, request), fields(mint_url = %mint_url))] - pub async fn post_restore( + async fn post_restore( &self, mint_url: MintUrl, request: RestoreRequest, @@ -367,3 +338,84 @@ impl HttpClient { } } } + +/// Http Client Methods +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait HttpClientMethods: Debug { + /// Get Active Mint Keys [NUT-01] + async fn get_mint_keys(&self, mint_url: MintUrl) -> Result, Error>; + + /// Get Keyset Keys [NUT-01] + async fn get_mint_keyset(&self, mint_url: MintUrl, keyset_id: Id) -> Result; + + /// Get Keysets [NUT-02] + async fn get_mint_keysets(&self, mint_url: MintUrl) -> Result; + + /// Mint Quote [NUT-04] + async fn post_mint_quote( + &self, + mint_url: MintUrl, + request: MintQuoteBolt11Request, + ) -> Result; + + /// Mint Quote status + async fn get_mint_quote_status( + &self, + mint_url: MintUrl, + quote_id: &str, + ) -> Result; + + /// Mint Tokens [NUT-04] + async fn post_mint( + &self, + mint_url: MintUrl, + request: MintBolt11Request, + ) -> Result; + + /// Melt Quote [NUT-05] + async fn post_melt_quote( + &self, + mint_url: MintUrl, + request: MeltQuoteBolt11Request, + ) -> Result; + + /// Melt Quote Status + async fn get_melt_quote_status( + &self, + mint_url: MintUrl, + quote_id: &str, + ) -> Result; + + /// Melt [NUT-05] + /// [Nut-08] Lightning fee return if outputs defined + async fn post_melt( + &self, + mint_url: MintUrl, + request: MeltBolt11Request, + ) -> Result; + + /// Split Token [NUT-06] + async fn post_swap( + &self, + mint_url: MintUrl, + request: SwapRequest, + ) -> Result; + + /// Get Mint Info [NUT-06] + async fn get_mint_info(&self, mint_url: MintUrl) -> Result; + + /// Spendable check [NUT-07] + async fn post_check_state( + &self, + mint_url: MintUrl, + request: CheckStateRequest, + ) -> Result; + + /// Restore request [NUT-13] + async fn post_restore( + &self, + mint_url: MintUrl, + request: RestoreRequest, + ) -> Result; +} diff --git a/crates/cdk/src/wallet/melt.rs b/crates/cdk/src/wallet/melt.rs index c31f2ff4..4948ecd3 100644 --- a/crates/cdk/src/wallet/melt.rs +++ b/crates/cdk/src/wallet/melt.rs @@ -4,6 +4,7 @@ use lightning_invoice::Bolt11Invoice; use tracing::instrument; use crate::nuts::nut00::ProofsMethods; +use crate::nuts::{MeltBolt11Request, MeltQuoteBolt11Request, Mpp}; use crate::{ dhke::construct_proofs, nuts::{CurrencyUnit, MeltQuoteBolt11Response, PreMintSecrets, Proofs, State}, @@ -57,9 +58,17 @@ impl Wallet { _ => return Err(Error::UnitUnsupported), }; + let options = mpp.map(|amount| Mpp { amount }); + + let quote_request = MeltQuoteBolt11Request { + request: Bolt11Invoice::from_str(&request)?, + unit: self.unit.clone(), + options, + }; + let quote_res = self .client - .post_melt_quote(self.mint_url.clone(), self.unit, invoice, mpp) + .post_melt_quote(self.mint_url.clone(), quote_request) .await?; if quote_res.amount != amount { @@ -70,7 +79,7 @@ impl Wallet { id: quote_res.quote, amount, request, - unit: self.unit, + unit: self.unit.clone(), fee_reserve: quote_res.fee_reserve, state: quote_res.state, expiry: quote_res.expiry, @@ -146,15 +155,13 @@ impl Wallet { proofs_total - quote_info.amount, )?; - let melt_response = self - .client - .post_melt( - self.mint_url.clone(), - quote_id.to_string(), - proofs.clone(), - Some(premint_secrets.blinded_messages()), - ) - .await; + let request = MeltBolt11Request { + quote: quote_id.to_string(), + inputs: proofs.clone(), + outputs: Some(premint_secrets.blinded_messages()), + }; + + let melt_response = self.client.post_melt(self.mint_url.clone(), request).await; let melt_response = match melt_response { Ok(melt_response) => melt_response, @@ -226,7 +233,7 @@ impl Wallet { proof, self.mint_url.clone(), State::Unspent, - quote_info.unit, + quote_info.unit.clone(), ) }) .collect::, _>>()? diff --git a/crates/cdk/src/wallet/mint.rs b/crates/cdk/src/wallet/mint.rs index b429daaf..743044f0 100644 --- a/crates/cdk/src/wallet/mint.rs +++ b/crates/cdk/src/wallet/mint.rs @@ -2,6 +2,7 @@ use tracing::instrument; use super::MintQuote; use crate::nuts::nut00::ProofsMethods; +use crate::nuts::{MintBolt11Request, MintQuoteBolt11Request}; use crate::{ amount::SplitTarget, dhke::construct_proofs, @@ -45,7 +46,7 @@ impl Wallet { description: Option, ) -> Result { let mint_url = self.mint_url.clone(); - let unit = self.unit; + let unit = self.unit.clone(); // If we have a description, we check that the mint supports it. if description.is_some() { @@ -64,16 +65,22 @@ impl Wallet { } } + let request = MintQuoteBolt11Request { + amount, + unit: unit.clone(), + description, + }; + let quote_res = self .client - .post_mint_quote(mint_url.clone(), amount, unit, description) + .post_mint_quote(mint_url.clone(), request) .await?; let quote = MintQuote { mint_url, id: quote_res.quote.clone(), amount, - unit, + unit: unit.clone(), request: quote_res.request, state: quote_res.state, expiry: quote_res.expiry.unwrap_or(0), @@ -212,9 +219,14 @@ impl Wallet { )?, }; + let request = MintBolt11Request { + quote: quote_id.to_string(), + outputs: premint_secrets.blinded_messages(), + }; + let mint_res = self .client - .post_mint(self.mint_url.clone(), quote_id, premint_secrets.clone()) + .post_mint(self.mint_url.clone(), request) .await?; let keys = self.get_keyset_keys(active_keyset_id).await?; @@ -257,7 +269,7 @@ impl Wallet { proof, self.mint_url.clone(), State::Unspent, - quote_info.unit, + quote_info.unit.clone(), ) }) .collect::, _>>()?; diff --git a/crates/cdk/src/wallet/mod.rs b/crates/cdk/src/wallet/mod.rs index 85045eaa..7b8de868 100644 --- a/crates/cdk/src/wallet/mod.rs +++ b/crates/cdk/src/wallet/mod.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use bitcoin::bip32::Xpriv; use bitcoin::Network; +use client::HttpClientMethods; use tracing::instrument; use crate::amount::SplitTarget; @@ -55,7 +56,7 @@ pub struct Wallet { /// The targeted amount of proofs to have at each size pub target_proof_count: usize, xpriv: Xpriv, - client: HttpClient, + client: Arc, } impl Wallet { @@ -88,7 +89,7 @@ impl Wallet { Ok(Self { mint_url: MintUrl::from_str(mint_url)?, unit, - client: HttpClient::new(), + client: Arc::new(HttpClient::new()), localstore, xpriv, target_proof_count: target_proof_count.unwrap_or(3), @@ -96,8 +97,8 @@ impl Wallet { } /// Change HTTP client - pub fn set_client(&mut self, client: HttpClient) { - self.client = client; + pub fn set_client(&mut self, client: C) { + self.client = Arc::new(client); } /// Fee required for proof set @@ -329,7 +330,12 @@ impl Wallet { let unspent_proofs = unspent_proofs .into_iter() .map(|proof| { - ProofInfo::new(proof, self.mint_url.clone(), State::Unspent, keyset.unit) + ProofInfo::new( + proof, + self.mint_url.clone(), + State::Unspent, + keyset.unit.clone(), + ) }) .collect::, _>>()?; diff --git a/crates/cdk/src/wallet/multi_mint_wallet.rs b/crates/cdk/src/wallet/multi_mint_wallet.rs index 8d26e9e4..b0f048b2 100644 --- a/crates/cdk/src/wallet/multi_mint_wallet.rs +++ b/crates/cdk/src/wallet/multi_mint_wallet.rs @@ -55,7 +55,7 @@ impl MultiMintWallet { wallets: Arc::new(Mutex::new( wallets .into_iter() - .map(|w| (WalletKey::new(w.mint_url.clone(), w.unit), w)) + .map(|w| (WalletKey::new(w.mint_url.clone(), w.unit.clone()), w)) .collect(), )), } @@ -64,7 +64,7 @@ impl MultiMintWallet { /// Add wallet to MultiMintWallet #[instrument(skip(self, wallet))] pub async fn add_wallet(&self, wallet: Wallet) { - let wallet_key = WalletKey::new(wallet.mint_url.clone(), wallet.unit); + let wallet_key = WalletKey::new(wallet.mint_url.clone(), wallet.unit.clone()); let mut wallets = self.wallets.lock().await; @@ -126,7 +126,7 @@ impl MultiMintWallet { for (WalletKey { mint_url, unit: u }, wallet) in self.wallets.lock().await.iter() { let wallet_proofs = wallet.get_unspent_proofs().await?; - mint_proofs.insert(mint_url.clone(), (wallet_proofs, *u)); + mint_proofs.insert(mint_url.clone(), (wallet_proofs, u.clone())); } Ok(mint_proofs) } @@ -198,7 +198,7 @@ impl MultiMintWallet { let amount = wallet.check_all_mint_quotes().await?; amount_minted - .entry(wallet.unit) + .entry(wallet.unit.clone()) .and_modify(|b| *b += amount) .or_insert(amount); } @@ -246,7 +246,7 @@ impl MultiMintWallet { let mint_url = token_data.mint_url()?; // Check that all mints in tokes have wallets - let wallet_key = WalletKey::new(mint_url.clone(), unit); + let wallet_key = WalletKey::new(mint_url.clone(), unit.clone()); if !self.has(&wallet_key).await { return Err(Error::UnknownWallet(wallet_key.clone())); } diff --git a/crates/cdk/src/wallet/proofs.rs b/crates/cdk/src/wallet/proofs.rs index 12fed0ef..fd67015a 100644 --- a/crates/cdk/src/wallet/proofs.rs +++ b/crates/cdk/src/wallet/proofs.rs @@ -3,6 +3,7 @@ use std::collections::HashSet; use tracing::instrument; use crate::nuts::nut00::ProofsMethods; +use crate::nuts::CheckStateRequest; use crate::{ amount::SplitTarget, nuts::{Proof, ProofState, Proofs, PublicKey, SpendingConditions, State}, @@ -40,7 +41,7 @@ impl Wallet { .localstore .get_proofs( Some(self.mint_url.clone()), - Some(self.unit), + Some(self.unit.clone()), state, spending_conditions, ) @@ -65,7 +66,7 @@ impl Wallet { let spendable = self .client - .post_check_state(self.mint_url.clone(), proof_ys) + .post_check_state(self.mint_url.clone(), CheckStateRequest { ys: proof_ys }) .await? .states; @@ -86,7 +87,10 @@ impl Wallet { pub async fn check_proofs_spent(&self, proofs: Proofs) -> Result, Error> { let spendable = self .client - .post_check_state(self.mint_url.clone(), proofs.ys()?) + .post_check_state( + self.mint_url.clone(), + CheckStateRequest { ys: proofs.ys()? }, + ) .await?; let spent_ys: Vec<_> = spendable .states @@ -111,7 +115,7 @@ impl Wallet { .localstore .get_proofs( Some(self.mint_url.clone()), - Some(self.unit), + Some(self.unit.clone()), Some(vec![State::Pending, State::Reserved]), None, ) diff --git a/crates/cdk/src/wallet/receive.rs b/crates/cdk/src/wallet/receive.rs index d5999232..2a594b1f 100644 --- a/crates/cdk/src/wallet/receive.rs +++ b/crates/cdk/src/wallet/receive.rs @@ -111,7 +111,7 @@ impl Wallet { let proofs_info = proofs .clone() .into_iter() - .map(|p| ProofInfo::new(p, self.mint_url.clone(), State::Pending, self.unit)) + .map(|p| ProofInfo::new(p, self.mint_url.clone(), State::Pending, self.unit.clone())) .collect::, _>>()?; self.localstore .update_proofs(proofs_info.clone(), vec![]) @@ -150,7 +150,7 @@ impl Wallet { let recv_proof_infos = recv_proofs .into_iter() - .map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Unspent, self.unit)) + .map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Unspent, self.unit.clone())) .collect::, _>>()?; self.localstore .update_proofs( diff --git a/crates/cdk/src/wallet/send.rs b/crates/cdk/src/wallet/send.rs index 7885db5c..0c6e7b79 100644 --- a/crates/cdk/src/wallet/send.rs +++ b/crates/cdk/src/wallet/send.rs @@ -16,7 +16,12 @@ impl Wallet { let ys = proofs.ys()?; self.localstore.reserve_proofs(ys).await?; - Ok(Token::new(self.mint_url.clone(), proofs, memo, self.unit)) + Ok(Token::new( + self.mint_url.clone(), + proofs, + memo, + self.unit.clone(), + )) } /// Send diff --git a/crates/cdk/src/wallet/swap.rs b/crates/cdk/src/wallet/swap.rs index 043d37c5..650ceac9 100644 --- a/crates/cdk/src/wallet/swap.rs +++ b/crates/cdk/src/wallet/swap.rs @@ -111,7 +111,9 @@ impl Wallet { let send_proofs_info = proofs_to_send .clone() .into_iter() - .map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Reserved, *unit)) + .map(|proof| { + ProofInfo::new(proof, mint_url.clone(), State::Reserved, unit.clone()) + }) .collect::, _>>()?; added_proofs = send_proofs_info; @@ -126,7 +128,7 @@ impl Wallet { let keep_proofs = change_proofs .into_iter() - .map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Unspent, *unit)) + .map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Unspent, unit.clone())) .collect::, _>>()?; added_proofs.extend(keep_proofs); @@ -154,7 +156,7 @@ impl Wallet { .localstore .get_proofs( Some(self.mint_url.clone()), - Some(self.unit), + Some(self.unit.clone()), Some(vec![State::Unspent]), None, ) diff --git a/flake.lock b/flake.lock index d8d8de91..14dc9544 100644 --- a/flake.lock +++ b/flake.lock @@ -57,11 +57,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1730137625, - "narHash": "sha256-9z8oOgFZiaguj+bbi3k4QhAD6JabWrnv7fscC/mt0KE=", + "lastModified": 1730741070, + "narHash": "sha256-edm8WG19kWozJ/GqyYx2VjW99EdhjKwbY3ZwdlPAAlo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "64b80bfb316b57cdb8919a9110ef63393d74382a", + "rev": "d063c1dd113c91ab27959ba540c0d9753409edf3", "type": "github" }, "original": { @@ -139,11 +139,11 @@ ] }, "locked": { - "lastModified": 1730341826, - "narHash": "sha256-RFaeY7EWzXOmAL2IQEACbnrEza3TgD5UQApHR4hGHhY=", + "lastModified": 1730687492, + "narHash": "sha256-xQVadjquBA/tFxDt5A55LJ1D1AvkVWsnrKC2o+pr8F4=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "815d1b3ee71716fc91a7bd149801e1f04d45fbc5", + "rev": "41814763a2c597755b0755dbe3e721367a5e420f", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index b4078557..dc67d02e 100644 --- a/flake.nix +++ b/flake.nix @@ -37,7 +37,7 @@ # Toolchains # latest stable - stable_toolchain = pkgs.rust-bin.stable.latest.default.override { + stable_toolchain = pkgs.rust-bin.stable."1.82.0".default.override { targets = [ "wasm32-unknown-unknown" ]; # wasm }; @@ -63,7 +63,7 @@ pkg-config curl just - protobuf3_20 + protobuf nixpkgs-fmt rust-analyzer typos @@ -139,6 +139,7 @@ cargo update -p bumpalo --precise 3.12.0 cargo update -p moka --precise 0.11.1 cargo update -p triomphe --precise 0.1.11 + cargo update -p url --precise 2.5.2 "; buildInputs = buildInputs ++ WASMInputs ++ [ msrv_toolchain ]; inherit nativeBuildInputs; @@ -160,6 +161,7 @@ cargo update -p tokio-stream --precise 0.1.15 cargo update -p serde_with --precise 3.1.0 cargo update -p reqwest --precise 0.12.4 + cargo update -p url --precise 2.5.2 "; buildInputs = buildInputs ++ WASMInputs ++ [ db_msrv_toolchain ]; inherit nativeBuildInputs; diff --git a/justfile b/justfile index 552d46e6..4e36977e 100644 --- a/justfile +++ b/justfile @@ -45,7 +45,7 @@ test: build if [ ! -f Cargo.toml ]; then cd {{invocation_directory()}} fi - cargo test + cargo test --lib # run `cargo clippy` on everything clippy *ARGS="--locked --offline --workspace --all-targets": diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..7f4575e0 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel="1.82.0" +components = ["rustfmt", "clippy", "rust-analyzer"] +