diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0bd07f77..58f86b77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,6 +106,8 @@ jobs: --bin cdk-mintd --no-default-features --features swagger, --bin cdk-mintd --no-default-features --features redis, --bin cdk-mintd --no-default-features --features "redis swagger", + --bin cdk-mintd --no-default-features --features management-rpc, + --bin cdk-mint-cli, ] steps: - name: checkout @@ -213,6 +215,7 @@ jobs: -p cdk-phoenixd, -p cdk-fake-wallet, -p cdk-cln, + -p cdk-mint-rpc, ] steps: - name: checkout diff --git a/.helix/languages.toml b/.helix/languages.toml index c2a98dd6..ecb2763e 100644 --- a/.helix/languages.toml +++ b/.helix/languages.toml @@ -1,2 +1 @@ [language-server.rust-analyzer.config] -cargo = { features = ["wallet", "mint", "swagger", "redis"] } diff --git a/README.md b/README.md index c86606de..3850f259 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,11 @@ The project is split up into several crates in the `crates/` directory: * [**cdk-lnbits**](./crates/cdk-lnbits/): [LNbits](https://lnbits.com/) Lightning backend for mint. * [**cdk-phoenixd**](./crates/cdk-phoenixd/): Phoenixd Lightning backend for mint. * [**cdk-fake-wallet**](./crates/cdk-fake-wallet/): Fake Lightning backend for mint. To be used only for testing, quotes are automatically filled. + * [**cdk-mint-rpc**](./crates/cdk-mint-rpc/): Mint management gRPC server and cli. * Binaries: * [**cdk-cli**](./crates/cdk-cli/): Cashu wallet CLI. * [**cdk-mintd**](./crates/cdk-mintd/): Cashu Mint Binary. + * [**cdk-mint-cli**](./crates/cdk-mint-rpc/): Cashu Mint managemtn gRCP client cli. ## Development diff --git a/crates/cashu/src/nuts/nut04.rs b/crates/cashu/src/nuts/nut04.rs index cb6b3c2f..8bd5ad34 100644 --- a/crates/cashu/src/nuts/nut04.rs +++ b/crates/cashu/src/nuts/nut04.rs @@ -240,4 +240,16 @@ impl Settings { None } + + /// Remove [`MintMethodSettings`] for unit method pair + pub fn remove_settings( + &mut self, + unit: &CurrencyUnit, + method: &PaymentMethod, + ) -> Option { + self.methods + .iter() + .position(|settings| &settings.method == method && &settings.unit == unit) + .map(|index| self.methods.remove(index)) + } } diff --git a/crates/cashu/src/nuts/nut05.rs b/crates/cashu/src/nuts/nut05.rs index ec0a522d..4981dea4 100644 --- a/crates/cashu/src/nuts/nut05.rs +++ b/crates/cashu/src/nuts/nut05.rs @@ -399,6 +399,18 @@ impl Settings { None } + + /// Remove [`MeltMethodSettings`] for unit method pair + pub fn remove_settings( + &mut self, + unit: &CurrencyUnit, + method: &PaymentMethod, + ) -> Option { + self.methods + .iter() + .position(|settings| settings.method.eq(method) && settings.unit.eq(unit)) + .map(|index| self.methods.remove(index)) + } } /// Melt Settings diff --git a/crates/cashu/src/nuts/nut06.rs b/crates/cashu/src/nuts/nut06.rs index d8cce86b..48ddc7d9 100644 --- a/crates/cashu/src/nuts/nut06.rs +++ b/crates/cashu/src/nuts/nut06.rs @@ -26,6 +26,12 @@ impl MintVersion { } } +impl std::fmt::Display for MintVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}/{}", self.name, self.version) + } +} + impl Serialize for MintVersion { fn serialize(&self, serializer: S) -> Result where diff --git a/crates/cdk-axum/Cargo.toml b/crates/cdk-axum/Cargo.toml index cccf692d..bd3ea769 100644 --- a/crates/cdk-axum/Cargo.toml +++ b/crates/cdk-axum/Cargo.toml @@ -28,7 +28,7 @@ futures = { version = "0.3.28", default-features = false } moka = { version = "0.11.1", features = ["future"] } serde_json = "1" paste = "1.0.15" -serde = { version = "1.0.210", features = ["derive"] } +serde = { version = "1", features = ["derive"] } uuid = { version = "1", features = ["v4", "serde"] } sha2 = "0.10.8" redis = { version = "0.23.3", features = [ diff --git a/crates/cdk-cli/Cargo.toml b/crates/cdk-cli/Cargo.toml index 926411c8..82f0907d 100644 --- a/crates/cdk-cli/Cargo.toml +++ b/crates/cdk-cli/Cargo.toml @@ -17,7 +17,7 @@ bip39 = "2.0" cdk = { path = "../cdk", version = "0.6.0", default-features = false, features = ["wallet"]} cdk-redb = { path = "../cdk-redb", version = "0.6.0", default-features = false, features = ["wallet"] } cdk-sqlite = { path = "../cdk-sqlite", version = "0.6.0", default-features = false, features = ["wallet"] } -clap = { version = "4.4.8", features = ["derive", "env", "default"] } +clap = { version = "~4.0.32", features = ["derive"] } serde = { version = "1", default-features = false, features = ["derive"] } serde_json = "1" tokio = { version = "1", default-features = false } diff --git a/crates/cdk-integration-tests/src/init_fake_wallet.rs b/crates/cdk-integration-tests/src/init_fake_wallet.rs index 3f1f5e22..b6f53ac2 100644 --- a/crates/cdk-integration-tests/src/init_fake_wallet.rs +++ b/crates/cdk-integration-tests/src/init_fake_wallet.rs @@ -6,6 +6,7 @@ use bip39::Mnemonic; use cdk::cdk_database::{self, MintDatabase}; use cdk::mint::{FeeReserve, MintBuilder, MintMeltLimits}; use cdk::nuts::{CurrencyUnit, PaymentMethod}; +use cdk::types::QuoteTTL; use cdk_fake_wallet::FakeWallet; use tracing_subscriber::EnvFilter; @@ -37,7 +38,8 @@ where let mut mint_builder = MintBuilder::new(); - mint_builder = mint_builder.with_localstore(Arc::new(database)); + let localstore = Arc::new(database); + mint_builder = mint_builder.with_localstore(localstore.clone()); mint_builder = mint_builder.add_ln_backend( CurrencyUnit::Sat, @@ -65,9 +67,14 @@ where mint_builder = mint_builder .with_name("fake test mint".to_string()) .with_description("fake test mint".to_string()) - .with_quote_ttl(10000, 10000) .with_seed(mnemonic.to_seed_normalized("").to_vec()); + localstore + .set_mint_info(mint_builder.mint_info.clone()) + .await?; + let quote_ttl = QuoteTTL::new(10000, 10000); + localstore.set_quote_ttl(quote_ttl).await?; + let mint = mint_builder.build().await?; start_mint(addr, port, mint).await?; diff --git a/crates/cdk-integration-tests/src/init_pure_tests.rs b/crates/cdk-integration-tests/src/init_pure_tests.rs index 4f6b74e4..6bab9fbc 100644 --- a/crates/cdk-integration-tests/src/init_pure_tests.rs +++ b/crates/cdk-integration-tests/src/init_pure_tests.rs @@ -7,7 +7,7 @@ use async_trait::async_trait; use bip39::Mnemonic; use cdk::amount::SplitTarget; use cdk::cdk_database::mint_memory::MintMemoryDatabase; -use cdk::cdk_database::WalletMemoryDatabase; +use cdk::cdk_database::{MintDatabase, WalletMemoryDatabase}; use cdk::mint::{FeeReserve, MintBuilder, MintMeltLimits}; use cdk::nuts::nut00::ProofsMethods; use cdk::nuts::{ @@ -16,6 +16,7 @@ use cdk::nuts::{ MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, PaymentMethod, RestoreRequest, RestoreResponse, SwapRequest, SwapResponse, }; +use cdk::types::QuoteTTL; use cdk::util::unix_time; use cdk::wallet::client::MintConnector; use cdk::wallet::Wallet; @@ -146,7 +147,8 @@ pub async fn create_and_start_test_mint() -> anyhow::Result> { let database = MintMemoryDatabase::default(); - mint_builder = mint_builder.with_localstore(Arc::new(database)); + let localstore = Arc::new(database); + mint_builder = mint_builder.with_localstore(localstore.clone()); let fee_reserve = FeeReserve { min_fee_reserve: 1.into(), @@ -172,9 +174,14 @@ pub async fn create_and_start_test_mint() -> anyhow::Result> { mint_builder = mint_builder .with_name("pure test mint".to_string()) .with_description("pure test mint".to_string()) - .with_quote_ttl(10000, 10000) .with_seed(mnemonic.to_seed_normalized("").to_vec()); + localstore + .set_mint_info(mint_builder.mint_info.clone()) + .await?; + let quote_ttl = QuoteTTL::new(10000, 10000); + localstore.set_quote_ttl(quote_ttl).await?; + let mint = mint_builder.build().await?; let mint_arc = Arc::new(mint); diff --git a/crates/cdk-integration-tests/src/init_regtest.rs b/crates/cdk-integration-tests/src/init_regtest.rs index c9c6f4fd..4675b04b 100644 --- a/crates/cdk-integration-tests/src/init_regtest.rs +++ b/crates/cdk-integration-tests/src/init_regtest.rs @@ -8,6 +8,7 @@ use cdk::cdk_database::{self, MintDatabase}; use cdk::cdk_lightning::{self, MintLightning}; use cdk::mint::{FeeReserve, MintBuilder, MintMeltLimits}; use cdk::nuts::{CurrencyUnit, PaymentMethod}; +use cdk::types::QuoteTTL; use cdk_cln::Cln as CdkCln; use cdk_lnd::Lnd as CdkLnd; use ln_regtest_rs::bitcoin_client::BitcoinClient; @@ -155,8 +156,8 @@ where L: MintLightning + Send + Sync + 'static, { let mut mint_builder = MintBuilder::new(); - - mint_builder = mint_builder.with_localstore(Arc::new(database)); + let localstore = Arc::new(database); + mint_builder = mint_builder.with_localstore(localstore.clone()); mint_builder = mint_builder.add_ln_backend( CurrencyUnit::Sat, @@ -170,11 +171,16 @@ where mint_builder = mint_builder .with_name("regtest mint".to_string()) .with_description("regtest mint".to_string()) - .with_quote_ttl(10000, 10000) .with_seed(mnemonic.to_seed_normalized("").to_vec()); let mint = mint_builder.build().await?; + localstore + .set_mint_info(mint_builder.mint_info.clone()) + .await?; + let quote_ttl = QuoteTTL::new(10000, 10000); + localstore.set_quote_ttl(quote_ttl).await?; + start_mint(addr, port, mint).await?; Ok(()) diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index 265c031b..3b3c4c2b 100644 --- a/crates/cdk-integration-tests/tests/mint.rs +++ b/crates/cdk-integration-tests/tests/mint.rs @@ -17,6 +17,7 @@ use cdk::nuts::{ PreMintSecrets, ProofState, Proofs, SecretKey, SpendingConditions, State, SwapRequest, }; use cdk::subscription::{IndexableParams, Params}; +use cdk::types::QuoteTTL; use cdk::util::unix_time; use cdk::Mint; use cdk_fake_wallet::FakeWallet; @@ -465,11 +466,16 @@ async fn test_correct_keyset() -> Result<()> { mint_builder = mint_builder .with_name("regtest mint".to_string()) .with_description("regtest mint".to_string()) - .with_quote_ttl(10000, 1000) .with_seed(mnemonic.to_seed_normalized("").to_vec()); let mint = mint_builder.build().await?; + localstore + .set_mint_info(mint_builder.mint_info.clone()) + .await?; + let quote_ttl = QuoteTTL::new(10000, 10000); + localstore.set_quote_ttl(quote_ttl).await?; + mint.rotate_next_keyset(CurrencyUnit::Sat, 32, 0).await?; mint.rotate_next_keyset(CurrencyUnit::Sat, 32, 0).await?; diff --git a/crates/cdk-mint-rpc/Cargo.toml b/crates/cdk-mint-rpc/Cargo.toml new file mode 100644 index 00000000..f4fbe29b --- /dev/null +++ b/crates/cdk-mint-rpc/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "cdk-mint-rpc" +version = "0.6.0" +edition = "2021" +authors = ["CDK Developers"] +description = "CDK mintd mint managment RPC client and server" +license = "MIT" +homepage = "https://github.com/cashubtc/cdk" +repository = "https://github.com/cashubtc/cdk.git" +rust-version = "1.63.0" # MSRV + +[[bin]] +name = "cdk-mint-cli" +path = "src/bin/mint_rpc_cli.rs" + +[dependencies] +anyhow = "1" +cdk = { path = "../cdk", version = "0.6.0", default-features = false, features = [ + "mint", +] } +clap = { version = "~4.0.32", features = ["derive"] } +tonic = { version = "0.9", features = [ + "channel", + "tls", + "tls-webpki-roots", +] } +tracing = { version = "0.1", default-features = false, features = [ + "attributes", + "log", +] } +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +tokio = { version = "1", default-features = false } +serde_json = "1" +serde = { version = "1", features = ["derive"] } +thiserror = "1" +prost = "0.11.0" +home = "0.5.5" + + +[build-dependencies] +tonic-build = "0.9" diff --git a/crates/cdk-mint-rpc/README.md b/crates/cdk-mint-rpc/README.md new file mode 100644 index 00000000..f13710fa --- /dev/null +++ b/crates/cdk-mint-rpc/README.md @@ -0,0 +1,93 @@ + +# Cashu Mint Management RPC + +This crate is a grpc client and server to control and manage a cdk mint. This crate exposes a server complnate that can be imported as library compontant, see its usage in `cdk-mintd`. The client can be used as a cli by running `cargo r --bin cdk-mint-cli`. + +The server can be run with or without certificate authentication. For running with authentication follow the below steps to create certificates. + + +# gRPC TLS Certificate Generation Guide + +This guide explains how to generate the necessary TLS certificates for securing gRPC communication between client and server. + +## Overview + +The script generates the following certificates and keys: +- Certificate Authority (CA) certificate and key +- Server certificate and key +- Client certificate and key + +All certificates are generated in PEM format, which is commonly used in Unix/Linux systems. + +## Prerequisites + +- OpenSSL installed on your system +- Bash shell environment + +## Generated Files + +The script will create the following files: +- `ca.key` - Certificate Authority private key +- `ca.pem` - Certificate Authority certificate +- `server.key` - Server private key +- `server.pem` - Server certificate +- `client.key` - Client private key +- `client.pem` - Client certificate + +## Usage + +1. Save the script as `generate_certs.sh` +2. Make it executable: + ```bash + chmod +x generate_certs.sh + ``` +3. Run the script: + ```bash + ./generate_certs.sh + ``` + +## Certificate Details + +### Certificate Authority (CA) +- 4096-bit RSA key +- Valid for 365 days +- Used to sign both server and client certificates + +### Server Certificate +- 4096-bit RSA key +- Valid for 365 days +- Includes Subject Alternative Names (SAN): + - DNS: localhost + - DNS: my-server + - IP: 127.0.0.1 + +### Client Certificate +- 4096-bit RSA key +- Valid for 365 days +- Used for client authentication + + +## Verification + +The script includes verification steps to ensure the certificates are properly generated: +```bash +# Verify server certificate +openssl verify -CAfile ca.pem server.pem + +# Verify client certificate +openssl verify -CAfile ca.pem client.pem +``` + +## Security Notes + +1. Keep private keys (*.key files) secure and never share them +2. The CA certificate (ca.pem) needs to be distributed to both client and server +3. Server needs: + - server.key + - server.pem + - ca.pem +4. Client needs: + - client.key + - client.pem + - ca.pem + diff --git a/crates/cdk-mint-rpc/build.rs b/crates/cdk-mint-rpc/build.rs new file mode 100644 index 00000000..4516df19 --- /dev/null +++ b/crates/cdk-mint-rpc/build.rs @@ -0,0 +1,5 @@ +fn main() -> Result<(), Box> { + println!("cargo:rerun-if-changed=src/proto/cdk-mint-rpc.proto"); + tonic_build::compile_protos("src/proto/cdk-mint-rpc.proto")?; + Ok(()) +} diff --git a/crates/cdk-mint-rpc/generate_certs.sh b/crates/cdk-mint-rpc/generate_certs.sh new file mode 100755 index 00000000..6d9b453b --- /dev/null +++ b/crates/cdk-mint-rpc/generate_certs.sh @@ -0,0 +1,47 @@ +# Generate private key for Certificate Authority (CA) +openssl genrsa -out ca.key 4096 + +# Generate CA certificate +openssl req -new -x509 -days 365 -key ca.key -out ca.pem -subj "/C=US/ST=State/L=City/O=Organization/OU=Unit/CN=MyCA" + +# Generate private key for Server +openssl genrsa -out server.key 4096 + +# Generate Certificate Signing Request (CSR) for Server +openssl req -new -key server.key -out server.csr -subj "/C=US/ST=State/L=City/O=Organization/OU=Unit/CN=localhost" + +# Generate Server certificate +openssl x509 -req -days 365 -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out server.pem -extfile <(printf "subjectAltName=DNS:localhost,DNS:my-server,IP:127.0.0.1") + +# Generate private key for Client +openssl genrsa -out client.key 4096 + +# Generate CSR for Client +openssl req -new -key client.key -out client.csr -subj "/C=US/ST=State/L=City/O=Organization/OU=Unit/CN=client" + +# Generate Client certificate +openssl x509 -req -days 365 -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out client.pem + +# Verify the certificates +echo "Verifying Server Certificate:" +openssl verify -CAfile ca.pem server.pem + +echo "Verifying Client Certificate:" +openssl verify -CAfile ca.pem client.pem + +# Clean up CSR files (optional) +rm server.csr client.csr + +# Display certificate information +echo "Server Certificate Info:" +openssl x509 -in server.pem -text -noout | grep "Subject:\|Issuer:\|DNS:\|IP Address:" + +echo "Client Certificate Info:" +openssl x509 -in client.pem -text -noout | grep "Subject:\|Issuer:" + +# Final files you'll need: +# - ca.pem (Certificate Authority certificate) +# - server.key (Server private key) +# - server.pem (Server certificate) +# - client.key (Client private key) +# - client.pem (Client certificate) diff --git a/crates/cdk-mint-rpc/src/bin/mint_rpc_cli.rs b/crates/cdk-mint-rpc/src/bin/mint_rpc_cli.rs new file mode 100644 index 00000000..8f255790 --- /dev/null +++ b/crates/cdk-mint-rpc/src/bin/mint_rpc_cli.rs @@ -0,0 +1,196 @@ +use std::path::PathBuf; + +use anyhow::{anyhow, Result}; +use cdk_mint_rpc::cdk_mint_client::CdkMintClient; +use cdk_mint_rpc::mint_rpc_cli::subcommands; +use cdk_mint_rpc::GetInfoRequest; +use clap::{Parser, Subcommand}; +use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; +use tonic::Request; +use tracing::Level; +use tracing_subscriber::EnvFilter; + +const DEFAULT_WORK_DIR: &str = ".cdk-mint-rpc-cli"; + +#[derive(Parser)] +#[command(version, about, long_about = None)] +struct Cli { + /// Address of RPC server + #[arg(short, long, default_value = "http://127.0.0.1:8086")] + addr: String, + + /// Logging level + #[arg(short, long, default_value = "debug")] + log_level: Level, + + /// Path to working dir + #[arg(short, long)] + work_dir: Option, + + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Get info + GetInfo, + /// Update motd + UpdateMotd(subcommands::UpdateMotdCommand), + /// Update short description + UpdateShortDescription(subcommands::UpdateShortDescriptionCommand), + /// Update long description + UpdateLongDescription(subcommands::UpdateLongDescriptionCommand), + /// Update name + UpdateName(subcommands::UpdateNameCommand), + /// Update icon url + UpdateIconUrl(subcommands::UpdateIconUrlCommand), + /// Add Url + AddUrl(subcommands::AddUrlCommand), + /// Remove Url + RemoveUrl(subcommands::RemoveUrlCommand), + /// Add contact + AddContact(subcommands::AddContactCommand), + /// Remove contact + RemoveContact(subcommands::RemoveContactCommand), + /// Update nut04 + UpdateNut04(subcommands::UpdateNut04Command), + /// Update nut05 + UpdateNut05(subcommands::UpdateNut05Command), + /// Update quote ttl + UpdateQuoteTtl(subcommands::UpdateQuoteTtlCommand), + /// Update Nut04 quote + UpdateNut04QuoteState(subcommands::UpdateNut04QuoteCommand), + /// Rotate next keyset + RotateNextKeyset(subcommands::RotateNextKeysetCommand), +} + +#[tokio::main] +async fn main() -> Result<()> { + let args: Cli = Cli::parse(); + let default_filter = args.log_level; + + let sqlx_filter = "sqlx=warn,hyper_util=warn,reqwest=warn"; + + let env_filter = EnvFilter::new(format!("{},{}", default_filter, sqlx_filter)); + + // Parse input + tracing_subscriber::fmt().with_env_filter(env_filter).init(); + + let cli = Cli::parse(); + + let work_dir = match &args.work_dir { + Some(work_dir) => work_dir.clone(), + None => { + let home_dir = home::home_dir().ok_or(anyhow!("Could not find home dir"))?; + + home_dir.join(DEFAULT_WORK_DIR) + } + }; + + std::fs::create_dir_all(&work_dir)?; + tracing::debug!("Using work dir: {}", work_dir.display()); + + let channel = if work_dir.join("tls").is_dir() { + // TLS directory exists, configure TLS + let server_root_ca_cert = std::fs::read_to_string(work_dir.join("tls/ca.pem")).unwrap(); + let server_root_ca_cert = Certificate::from_pem(server_root_ca_cert); + let client_cert = std::fs::read_to_string(work_dir.join("tls/client.pem"))?; + let client_key = std::fs::read_to_string(work_dir.join("tls/client.key"))?; + let client_identity = Identity::from_pem(client_cert, client_key); + let tls = ClientTlsConfig::new() + .ca_certificate(server_root_ca_cert) + .identity(client_identity); + + Channel::from_shared(cli.addr.to_string())? + .tls_config(tls)? + .connect() + .await? + } else { + // No TLS directory, skip TLS configuration + Channel::from_shared(cli.addr.to_string())? + .connect() + .await? + }; + + let mut client = CdkMintClient::new(channel); + + match cli.command { + Commands::GetInfo => { + let response = client.get_info(Request::new(GetInfoRequest {})).await?; + let info = response.into_inner(); + println!( + "name: {}", + info.name.unwrap_or("None".to_string()) + ); + println!( + "version: {}", + info.version.unwrap_or("None".to_string()) + ); + println!( + "description: {}", + info.description.unwrap_or("None".to_string()) + ); + println!( + "long description: {}", + info.long_description.unwrap_or("None".to_string()) + ); + println!("motd: {}", info.motd.unwrap_or("None".to_string())); + println!("icon_url: {}", info.icon_url.unwrap_or("None".to_string())); + + for url in info.urls { + println!("mint_url: {}", url); + } + + for contact in info.contact { + println!("method: {}, info: {}", contact.method, contact.info); + } + println!("total issued: {} sat", info.total_issued); + println!("total redeemed: {} sat", info.total_redeemed); + } + Commands::UpdateMotd(sub_command_args) => { + subcommands::update_motd(&mut client, &sub_command_args).await?; + } + Commands::UpdateShortDescription(sub_command_args) => { + subcommands::update_short_description(&mut client, &sub_command_args).await?; + } + Commands::UpdateLongDescription(sub_command_args) => { + subcommands::update_long_description(&mut client, &sub_command_args).await?; + } + Commands::UpdateName(sub_command_args) => { + subcommands::update_name(&mut client, &sub_command_args).await?; + } + Commands::UpdateIconUrl(sub_command_args) => { + subcommands::update_icon_url(&mut client, &sub_command_args).await?; + } + Commands::AddUrl(sub_command_args) => { + subcommands::add_url(&mut client, &sub_command_args).await?; + } + Commands::RemoveUrl(sub_command_args) => { + subcommands::remove_url(&mut client, &sub_command_args).await?; + } + Commands::AddContact(sub_command_args) => { + subcommands::add_contact(&mut client, &sub_command_args).await?; + } + Commands::RemoveContact(sub_command_args) => { + subcommands::remove_contact(&mut client, &sub_command_args).await?; + } + Commands::UpdateNut04(sub_command_args) => { + subcommands::update_nut04(&mut client, &sub_command_args).await?; + } + Commands::UpdateNut05(sub_command_args) => { + subcommands::update_nut05(&mut client, &sub_command_args).await?; + } + Commands::UpdateQuoteTtl(sub_command_args) => { + subcommands::update_quote_ttl(&mut client, &sub_command_args).await?; + } + Commands::UpdateNut04QuoteState(sub_command_args) => { + subcommands::update_nut04_quote_state(&mut client, &sub_command_args).await?; + } + Commands::RotateNextKeyset(sub_command_args) => { + subcommands::rotate_next_keyset(&mut client, &sub_command_args).await?; + } + } + + Ok(()) +} diff --git a/crates/cdk-mint-rpc/src/client.rs b/crates/cdk-mint-rpc/src/client.rs new file mode 100644 index 00000000..54c9f428 --- /dev/null +++ b/crates/cdk-mint-rpc/src/client.rs @@ -0,0 +1,27 @@ +use thiserror::Error; + +use crate::cdk_mint_rpc::cdk_mint_client::CdkMintClient; +use crate::cdk_mint_rpc::cdk_mint_server::CdkMint; + +/// Error +#[derive(Debug, Error)] +pub enum Error { + /// Transport error + #[error(transparent)] + Transport(#[from] tonic::transport::Error), +} + +pub struct MintRPCClient { + inner: CdkMintClient, +} + +impl MintRPCClient { + pub async fn new(url: String) -> Result { + Ok(Self { + inner: CdkMintClient::connect(url).await?, + }) + } +} + +#[tonic::async_trait] +impl CdkMint for MintRPCClient {} diff --git a/crates/cdk-mint-rpc/src/lib.rs b/crates/cdk-mint-rpc/src/lib.rs new file mode 100644 index 00000000..2d47d22f --- /dev/null +++ b/crates/cdk-mint-rpc/src/lib.rs @@ -0,0 +1,5 @@ +pub mod proto; + +pub mod mint_rpc_cli; + +pub use proto::*; diff --git a/crates/cdk-mint-rpc/src/mint_rpc_cli/mod.rs b/crates/cdk-mint-rpc/src/mint_rpc_cli/mod.rs new file mode 100644 index 00000000..1255cb09 --- /dev/null +++ b/crates/cdk-mint-rpc/src/mint_rpc_cli/mod.rs @@ -0,0 +1 @@ +pub mod subcommands; diff --git a/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/mod.rs b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/mod.rs new file mode 100644 index 00000000..dbdee28b --- /dev/null +++ b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/mod.rs @@ -0,0 +1,25 @@ +mod rotate_next_keyset; +mod update_contact; +mod update_icon_url; +mod update_long_description; +mod update_motd; +mod update_name; +mod update_nut04; +mod update_nut04_quote; +mod update_nut05; +mod update_short_description; +mod update_ttl; +mod update_urls; + +pub use rotate_next_keyset::{rotate_next_keyset, RotateNextKeysetCommand}; +pub use update_contact::{add_contact, remove_contact, AddContactCommand, RemoveContactCommand}; +pub use update_icon_url::{update_icon_url, UpdateIconUrlCommand}; +pub use update_long_description::{update_long_description, UpdateLongDescriptionCommand}; +pub use update_motd::{update_motd, UpdateMotdCommand}; +pub use update_name::{update_name, UpdateNameCommand}; +pub use update_nut04::{update_nut04, UpdateNut04Command}; +pub use update_nut04_quote::{update_nut04_quote_state, UpdateNut04QuoteCommand}; +pub use update_nut05::{update_nut05, UpdateNut05Command}; +pub use update_short_description::{update_short_description, UpdateShortDescriptionCommand}; +pub use update_ttl::{update_quote_ttl, UpdateQuoteTtlCommand}; +pub use update_urls::{add_url, remove_url, AddUrlCommand, RemoveUrlCommand}; diff --git a/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/rotate_next_keyset.rs b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/rotate_next_keyset.rs new file mode 100644 index 00000000..55ab7149 --- /dev/null +++ b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/rotate_next_keyset.rs @@ -0,0 +1,40 @@ +use anyhow::Result; +use clap::Args; +use tonic::transport::Channel; +use tonic::Request; + +use crate::cdk_mint_client::CdkMintClient; +use crate::RotateNextKeysetRequest; + +#[derive(Args)] +pub struct RotateNextKeysetCommand { + #[arg(short, long)] + #[arg(default_value = "sat")] + unit: String, + #[arg(short, long)] + max_order: Option, + #[arg(short, long)] + input_fee_ppk: Option, +} + +pub async fn rotate_next_keyset( + client: &mut CdkMintClient, + sub_command_args: &RotateNextKeysetCommand, +) -> Result<()> { + let response = client + .rotate_next_keyset(Request::new(RotateNextKeysetRequest { + unit: sub_command_args.unit.clone(), + max_order: sub_command_args.max_order.map(|m| m.into()), + input_fee_ppk: sub_command_args.input_fee_ppk, + })) + .await?; + + let response = response.into_inner(); + + println!( + "Rotated to new keyset {} for unit {} with a max order of {} and fee of {}", + response.id, response.unit, response.max_order, response.input_fee_ppk + ); + + Ok(()) +} diff --git a/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_contact.rs b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_contact.rs new file mode 100644 index 00000000..5b1253ae --- /dev/null +++ b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_contact.rs @@ -0,0 +1,47 @@ +use anyhow::Result; +use clap::Args; +use tonic::transport::Channel; +use tonic::Request; + +use crate::cdk_mint_client::CdkMintClient; +use crate::UpdateContactRequest; + +#[derive(Args)] +pub struct AddContactCommand { + method: String, + info: String, +} + +pub async fn add_contact( + client: &mut CdkMintClient, + sub_command_args: &AddContactCommand, +) -> Result<()> { + let _response = client + .add_contact(Request::new(UpdateContactRequest { + method: sub_command_args.method.clone(), + info: sub_command_args.info.clone(), + })) + .await?; + + Ok(()) +} + +#[derive(Args)] +pub struct RemoveContactCommand { + method: String, + info: String, +} + +pub async fn remove_contact( + client: &mut CdkMintClient, + sub_command_args: &RemoveContactCommand, +) -> Result<()> { + let _response = client + .remove_contact(Request::new(UpdateContactRequest { + method: sub_command_args.method.clone(), + info: sub_command_args.info.clone(), + })) + .await?; + + Ok(()) +} diff --git a/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_icon_url.rs b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_icon_url.rs new file mode 100644 index 00000000..84ff26f9 --- /dev/null +++ b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_icon_url.rs @@ -0,0 +1,25 @@ +use anyhow::Result; +use clap::Args; +use tonic::transport::Channel; +use tonic::Request; + +use crate::cdk_mint_client::CdkMintClient; +use crate::UpdateIconUrlRequest; + +#[derive(Args)] +pub struct UpdateIconUrlCommand { + name: String, +} + +pub async fn update_icon_url( + client: &mut CdkMintClient, + sub_command_args: &UpdateIconUrlCommand, +) -> Result<()> { + let _response = client + .update_icon_url(Request::new(UpdateIconUrlRequest { + icon_url: sub_command_args.name.clone(), + })) + .await?; + + Ok(()) +} diff --git a/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_long_description.rs b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_long_description.rs new file mode 100644 index 00000000..ec08b6ff --- /dev/null +++ b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_long_description.rs @@ -0,0 +1,25 @@ +use anyhow::Result; +use clap::Args; +use tonic::transport::Channel; +use tonic::Request; + +use crate::cdk_mint_client::CdkMintClient; +use crate::UpdateDescriptionRequest; + +#[derive(Args)] +pub struct UpdateLongDescriptionCommand { + description: String, +} + +pub async fn update_long_description( + client: &mut CdkMintClient, + sub_command_args: &UpdateLongDescriptionCommand, +) -> Result<()> { + let _response = client + .update_long_description(Request::new(UpdateDescriptionRequest { + description: sub_command_args.description.clone(), + })) + .await?; + + Ok(()) +} diff --git a/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_motd.rs b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_motd.rs new file mode 100644 index 00000000..7966acd4 --- /dev/null +++ b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_motd.rs @@ -0,0 +1,25 @@ +use anyhow::Result; +use clap::Args; +use tonic::transport::Channel; +use tonic::Request; + +use crate::cdk_mint_client::CdkMintClient; +use crate::UpdateMotdRequest; + +#[derive(Args)] +pub struct UpdateMotdCommand { + motd: String, +} + +pub async fn update_motd( + client: &mut CdkMintClient, + sub_command_args: &UpdateMotdCommand, +) -> Result<()> { + let _response = client + .update_motd(Request::new(UpdateMotdRequest { + motd: sub_command_args.motd.clone(), + })) + .await?; + + Ok(()) +} diff --git a/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_name.rs b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_name.rs new file mode 100644 index 00000000..ce274292 --- /dev/null +++ b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_name.rs @@ -0,0 +1,25 @@ +use anyhow::Result; +use clap::Args; +use tonic::transport::Channel; +use tonic::Request; + +use crate::cdk_mint_client::CdkMintClient; +use crate::UpdateNameRequest; + +#[derive(Args)] +pub struct UpdateNameCommand { + name: String, +} + +pub async fn update_name( + client: &mut CdkMintClient, + sub_command_args: &UpdateNameCommand, +) -> Result<()> { + let _response = client + .update_name(Request::new(UpdateNameRequest { + name: sub_command_args.name.clone(), + })) + .await?; + + Ok(()) +} diff --git a/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut04.rs b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut04.rs new file mode 100644 index 00000000..f78f3691 --- /dev/null +++ b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut04.rs @@ -0,0 +1,43 @@ +use anyhow::Result; +use clap::Args; +use tonic::transport::Channel; +use tonic::Request; + +use crate::cdk_mint_client::CdkMintClient; +use crate::UpdateNut04Request; + +#[derive(Args)] +pub struct UpdateNut04Command { + #[arg(short, long)] + #[arg(default_value = "sat")] + unit: String, + #[arg(short, long)] + #[arg(default_value = "bolt11")] + method: String, + #[arg(long)] + min_amount: Option, + #[arg(long)] + max_amount: Option, + #[arg(long)] + disabled: Option, + #[arg(long)] + description: Option, +} + +pub async fn update_nut04( + client: &mut CdkMintClient, + sub_command_args: &UpdateNut04Command, +) -> Result<()> { + let _response = client + .update_nut04(Request::new(UpdateNut04Request { + method: sub_command_args.method.clone(), + unit: sub_command_args.unit.clone(), + disabled: sub_command_args.disabled, + min: sub_command_args.min_amount, + max: sub_command_args.max_amount, + description: sub_command_args.description, + })) + .await?; + + Ok(()) +} diff --git a/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut04_quote.rs b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut04_quote.rs new file mode 100644 index 00000000..b648b5d2 --- /dev/null +++ b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut04_quote.rs @@ -0,0 +1,32 @@ +use anyhow::Result; +use clap::Args; +use tonic::transport::Channel; +use tonic::Request; + +use crate::cdk_mint_client::CdkMintClient; +use crate::UpdateNut04QuoteRequest; + +#[derive(Args)] +pub struct UpdateNut04QuoteCommand { + quote_id: String, + #[arg(default_value = "PAID")] + state: String, +} + +pub async fn update_nut04_quote_state( + client: &mut CdkMintClient, + sub_command_args: &UpdateNut04QuoteCommand, +) -> Result<()> { + let response = client + .update_nut04_quote(Request::new(UpdateNut04QuoteRequest { + quote_id: sub_command_args.quote_id.clone(), + state: sub_command_args.state.clone(), + })) + .await?; + + let response = response.into_inner(); + + println!("Quote {} updated to {}", response.quote_id, response.state); + + Ok(()) +} diff --git a/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut05.rs b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut05.rs new file mode 100644 index 00000000..4088781e --- /dev/null +++ b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut05.rs @@ -0,0 +1,40 @@ +use anyhow::Result; +use clap::Args; +use tonic::transport::Channel; +use tonic::Request; + +use crate::cdk_mint_client::CdkMintClient; +use crate::UpdateNut05Request; + +#[derive(Args)] +pub struct UpdateNut05Command { + #[arg(short, long)] + #[arg(default_value = "sat")] + unit: String, + #[arg(short, long)] + #[arg(default_value = "bolt11")] + method: String, + #[arg(long)] + min_amount: Option, + #[arg(long)] + max_amount: Option, + #[arg(long)] + disabled: Option, +} + +pub async fn update_nut05( + client: &mut CdkMintClient, + sub_command_args: &UpdateNut05Command, +) -> Result<()> { + let _response = client + .update_nut05(Request::new(UpdateNut05Request { + method: sub_command_args.method.clone(), + unit: sub_command_args.unit.clone(), + disabled: sub_command_args.disabled, + min: sub_command_args.min_amount, + max: sub_command_args.max_amount, + })) + .await?; + + Ok(()) +} diff --git a/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_short_description.rs b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_short_description.rs new file mode 100644 index 00000000..a3e663e7 --- /dev/null +++ b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_short_description.rs @@ -0,0 +1,25 @@ +use anyhow::Result; +use clap::Args; +use tonic::transport::Channel; +use tonic::Request; + +use crate::cdk_mint_client::CdkMintClient; +use crate::UpdateDescriptionRequest; + +#[derive(Args)] +pub struct UpdateShortDescriptionCommand { + description: String, +} + +pub async fn update_short_description( + client: &mut CdkMintClient, + sub_command_args: &UpdateShortDescriptionCommand, +) -> Result<()> { + let _response = client + .update_short_description(Request::new(UpdateDescriptionRequest { + description: sub_command_args.description.clone(), + })) + .await?; + + Ok(()) +} diff --git a/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_ttl.rs b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_ttl.rs new file mode 100644 index 00000000..8cdcb5ab --- /dev/null +++ b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_ttl.rs @@ -0,0 +1,28 @@ +use anyhow::Result; +use clap::Args; +use tonic::transport::Channel; +use tonic::Request; + +use crate::cdk_mint_client::CdkMintClient; +use crate::UpdateQuoteTtlRequest; + +#[derive(Args)] +pub struct UpdateQuoteTtlCommand { + #[arg(long)] + mint_ttl: Option, + #[arg(long)] + melt_ttl: Option, +} +pub async fn update_quote_ttl( + client: &mut CdkMintClient, + sub_command_args: &UpdateQuoteTtlCommand, +) -> Result<()> { + let _response = client + .update_quote_ttl(Request::new(UpdateQuoteTtlRequest { + mint_ttl: sub_command_args.mint_ttl, + melt_ttl: sub_command_args.melt_ttl, + })) + .await?; + + Ok(()) +} diff --git a/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_urls.rs b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_urls.rs new file mode 100644 index 00000000..8079a0ac --- /dev/null +++ b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_urls.rs @@ -0,0 +1,43 @@ +use anyhow::Result; +use clap::Args; +use tonic::transport::Channel; +use tonic::Request; + +use crate::cdk_mint_client::CdkMintClient; +use crate::UpdateUrlRequest; + +#[derive(Args)] +pub struct AddUrlCommand { + url: String, +} + +pub async fn add_url( + client: &mut CdkMintClient, + sub_command_args: &AddUrlCommand, +) -> Result<()> { + let _response = client + .add_url(Request::new(UpdateUrlRequest { + url: sub_command_args.url.clone(), + })) + .await?; + + Ok(()) +} + +#[derive(Args)] +pub struct RemoveUrlCommand { + url: String, +} + +pub async fn remove_url( + client: &mut CdkMintClient, + sub_command_args: &RemoveUrlCommand, +) -> Result<()> { + let _response = client + .remove_url(Request::new(UpdateUrlRequest { + url: sub_command_args.url.clone(), + })) + .await?; + + Ok(()) +} diff --git a/crates/cdk-mint-rpc/src/proto/cdk-mint-rpc.proto b/crates/cdk-mint-rpc/src/proto/cdk-mint-rpc.proto new file mode 100644 index 00000000..7a57bd18 --- /dev/null +++ b/crates/cdk-mint-rpc/src/proto/cdk-mint-rpc.proto @@ -0,0 +1,115 @@ +syntax = "proto3"; + +package cdk_mint_rpc; + +service CdkMint { + rpc GetInfo(GetInfoRequest) returns (GetInfoResponse) {} + rpc UpdateMotd(UpdateMotdRequest) returns (UpdateResponse) {} + rpc UpdateShortDescription(UpdateDescriptionRequest) returns (UpdateResponse) {} + rpc UpdateLongDescription(UpdateDescriptionRequest) returns (UpdateResponse) {} + rpc UpdateIconUrl(UpdateIconUrlRequest) returns (UpdateResponse) {} + rpc UpdateName(UpdateNameRequest) returns (UpdateResponse) {} + rpc AddUrl(UpdateUrlRequest) returns (UpdateResponse) {} + rpc RemoveUrl(UpdateUrlRequest) returns (UpdateResponse) {} + rpc AddContact(UpdateContactRequest) returns (UpdateResponse) {} + rpc RemoveContact(UpdateContactRequest) returns (UpdateResponse) {} + rpc UpdateNut04(UpdateNut04Request) returns (UpdateResponse) {} + rpc UpdateNut05(UpdateNut05Request) returns (UpdateResponse) {} + rpc UpdateQuoteTtl(UpdateQuoteTtlRequest) returns (UpdateResponse) {} + rpc UpdateNut04Quote(UpdateNut04QuoteRequest) returns (UpdateNut04QuoteRequest) {} + rpc RotateNextKeyset(RotateNextKeysetRequest) returns (RotateNextKeysetResponse) {} +} + +message GetInfoRequest { +} + +message ContactInfo { + string method = 1; + string info = 2; +} + +message GetInfoResponse { + optional string name = 1; + optional string version = 2; + optional string description = 3; + optional string long_description = 4; + repeated ContactInfo contact = 5; + optional string motd = 6; + optional string icon_url = 7; + repeated string urls = 8; + uint64 total_issued = 9; + uint64 total_redeemed = 10; +} + +message UpdateResponse{ +} + +message UpdateMotdRequest { + string motd = 1; +} + +message UpdateDescriptionRequest { + string description = 1; +} + + +message UpdateIconUrlRequest { + string icon_url = 1; +} + +message UpdateNameRequest { + string name = 1; +} + + +message UpdateUrlRequest { + string url = 1; +} + +message UpdateContactRequest { + string method = 1; + string info = 2; +} + +message UpdateNut04Request { + string unit = 1; + string method = 2; + optional bool disabled = 3; + optional uint64 min = 4; + optional uint64 max = 5; + optional bool description = 6; +} + + +message UpdateNut05Request { + string unit = 1; + string method = 2; + optional bool disabled = 3; + optional uint64 min = 4; + optional uint64 max = 5; +} + +message UpdateQuoteTtlRequest { + optional uint64 mint_ttl = 1; + optional uint64 melt_ttl = 2; +} + + +message UpdateNut04QuoteRequest { + string quote_id = 1; + string state = 2; +} + +message RotateNextKeysetRequest { + string unit = 1; + optional uint32 max_order = 2; + optional uint64 input_fee_ppk = 3; +} + + +message RotateNextKeysetResponse { + string id = 1; + string unit = 2; + uint32 max_order = 3; + uint64 input_fee_ppk = 4; +} diff --git a/crates/cdk-mint-rpc/src/proto/mod.rs b/crates/cdk-mint-rpc/src/proto/mod.rs new file mode 100644 index 00000000..a9d78a58 --- /dev/null +++ b/crates/cdk-mint-rpc/src/proto/mod.rs @@ -0,0 +1,5 @@ +tonic::include_proto!("cdk_mint_rpc"); + +mod server; + +pub use server::MintRPCServer; diff --git a/crates/cdk-mint-rpc/src/proto/server.rs b/crates/cdk-mint-rpc/src/proto/server.rs new file mode 100644 index 00000000..67b2f512 --- /dev/null +++ b/crates/cdk-mint-rpc/src/proto/server.rs @@ -0,0 +1,586 @@ +use std::net::SocketAddr; +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::Arc; + +use cdk::mint::Mint; +use cdk::nuts::nut04::MintMethodSettings; +use cdk::nuts::nut05::MeltMethodSettings; +use cdk::nuts::{CurrencyUnit, MintQuoteState, PaymentMethod}; +use cdk::types::QuoteTTL; +use cdk::Amount; +use thiserror::Error; +use tokio::sync::Notify; +use tokio::task::JoinHandle; +use tokio::time::Duration; +use tonic::transport::{Certificate, Identity, Server, ServerTlsConfig}; +use tonic::{Request, Response, Status}; + +use crate::cdk_mint_server::{CdkMint, CdkMintServer}; +use crate::{ + ContactInfo, GetInfoRequest, GetInfoResponse, RotateNextKeysetRequest, + RotateNextKeysetResponse, UpdateContactRequest, UpdateDescriptionRequest, UpdateIconUrlRequest, + UpdateMotdRequest, UpdateNameRequest, UpdateNut04QuoteRequest, UpdateNut04Request, + UpdateNut05Request, UpdateQuoteTtlRequest, UpdateResponse, UpdateUrlRequest, +}; + +/// Error +#[derive(Debug, Error)] +pub enum Error { + /// Parse error + #[error(transparent)] + Parse(#[from] std::net::AddrParseError), + /// Transport error + #[error(transparent)] + Transport(#[from] tonic::transport::Error), + /// Io error + #[error(transparent)] + Io(#[from] std::io::Error), +} + +/// CDK Mint RPC Server +#[derive(Clone)] +pub struct MintRPCServer { + socket_addr: SocketAddr, + mint: Arc, + shutdown: Arc, + handle: Option>>>, +} + +impl MintRPCServer { + pub fn new(addr: &str, port: u16, mint: Arc) -> Result { + Ok(Self { + socket_addr: format!("{addr}:{port}").parse()?, + mint, + shutdown: Arc::new(Notify::new()), + handle: None, + }) + } + + pub async fn start(&mut self, tls_dir: Option) -> Result<(), Error> { + tracing::info!("Starting RPC server {}", self.socket_addr); + + let server = match tls_dir { + Some(tls_dir) => { + tracing::info!("TLS configuration found, starting secure server"); + let cert = std::fs::read_to_string(tls_dir.join("server.pem"))?; + let key = std::fs::read_to_string(tls_dir.join("server.key"))?; + let client_ca_cert = std::fs::read_to_string(tls_dir.join("ca.pem"))?; + let client_ca_cert = Certificate::from_pem(client_ca_cert); + let server_identity = Identity::from_pem(cert, key); + let tls_config = ServerTlsConfig::new() + .identity(server_identity) + .client_ca_root(client_ca_cert); + + Server::builder() + .tls_config(tls_config)? + .add_service(CdkMintServer::new(self.clone())) + } + None => { + tracing::warn!("No valid TLS configuration found, starting insecure server"); + Server::builder().add_service(CdkMintServer::new(self.clone())) + } + }; + + let shutdown = self.shutdown.clone(); + let addr = self.socket_addr; + + self.handle = Some(Arc::new(tokio::spawn(async move { + let server = server.serve_with_shutdown(addr, async { + shutdown.notified().await; + }); + + server.await?; + Ok(()) + }))); + + Ok(()) + } + + pub async fn stop(&self) -> Result<(), Error> { + self.shutdown.notify_one(); + if let Some(handle) = &self.handle { + while !handle.is_finished() { + tracing::info!("Waitning for mint rpc server to stop"); + tokio::time::sleep(Duration::from_millis(100)).await; + } + } + + tracing::info!("Mint rpc server stopped"); + Ok(()) + } +} + +impl Drop for MintRPCServer { + fn drop(&mut self) { + tracing::debug!("Dropping mint rpc server"); + self.shutdown.notify_one(); + } +} + +#[tonic::async_trait] +impl CdkMint for MintRPCServer { + async fn get_info( + &self, + _request: Request, + ) -> Result, Status> { + let info = self + .mint + .mint_info() + .await + .map_err(|err| Status::internal(err.to_string()))?; + + let total_issued = self + .mint + .total_issued() + .await + .map_err(|err| Status::internal(err.to_string()))?; + + let total_issued: Amount = Amount::try_sum(total_issued.values().cloned()) + .map_err(|_| Status::internal("Overflow".to_string()))?; + + let total_redeemed = self + .mint + .total_redeemed() + .await + .map_err(|err| Status::internal(err.to_string()))?; + + let total_redeemed: Amount = Amount::try_sum(total_redeemed.values().cloned()) + .map_err(|_| Status::internal("Overflow".to_string()))?; + + let contact = info + .contact + .unwrap_or_default() + .into_iter() + .map(|c| ContactInfo { + method: c.method, + info: c.info, + }) + .collect(); + + Ok(Response::new(GetInfoResponse { + name: info.name, + description: info.description, + long_description: info.description_long, + version: info.version.map(|v| v.to_string()), + contact, + motd: info.motd, + icon_url: info.icon_url, + urls: info.urls.unwrap_or_default(), + total_issued: total_issued.into(), + total_redeemed: total_redeemed.into(), + })) + } + + async fn update_motd( + &self, + request: Request, + ) -> Result, Status> { + let motd = request.into_inner().motd; + let mut info = self + .mint + .mint_info() + .await + .map_err(|err| Status::internal(err.to_string()))?; + info.motd = Some(motd); + + self.mint + .set_mint_info(info) + .await + .map_err(|err| Status::internal(err.to_string()))?; + + Ok(Response::new(UpdateResponse {})) + } + + async fn update_short_description( + &self, + request: Request, + ) -> Result, Status> { + let description = request.into_inner().description; + let mut info = self + .mint + .mint_info() + .await + .map_err(|err| Status::internal(err.to_string()))?; + + info.description = Some(description); + + self.mint + .set_mint_info(info) + .await + .map_err(|err| Status::internal(err.to_string()))?; + Ok(Response::new(UpdateResponse {})) + } + + async fn update_long_description( + &self, + request: Request, + ) -> Result, Status> { + let description = request.into_inner().description; + let mut info = self + .mint + .mint_info() + .await + .map_err(|err| Status::internal(err.to_string()))?; + + info.description = Some(description); + + self.mint + .set_mint_info(info) + .await + .map_err(|err| Status::internal(err.to_string()))?; + Ok(Response::new(UpdateResponse {})) + } + + async fn update_name( + &self, + request: Request, + ) -> Result, Status> { + let name = request.into_inner().name; + let mut info = self + .mint + .mint_info() + .await + .map_err(|err| Status::internal(err.to_string()))?; + + info.name = Some(name); + + self.mint + .set_mint_info(info) + .await + .map_err(|err| Status::internal(err.to_string()))?; + Ok(Response::new(UpdateResponse {})) + } + + async fn update_icon_url( + &self, + request: Request, + ) -> Result, Status> { + let icon_url = request.into_inner().icon_url; + + let mut info = self + .mint + .mint_info() + .await + .map_err(|err| Status::internal(err.to_string()))?; + + info.icon_url = Some(icon_url); + + self.mint + .set_mint_info(info) + .await + .map_err(|err| Status::internal(err.to_string()))?; + Ok(Response::new(UpdateResponse {})) + } + + async fn add_url( + &self, + request: Request, + ) -> Result, Status> { + let url = request.into_inner().url; + let mut info = self + .mint + .mint_info() + .await + .map_err(|err| Status::internal(err.to_string()))?; + let urls = info.urls; + urls.clone().unwrap_or_default().push(url); + + info.urls = urls; + + self.mint + .set_mint_info(info) + .await + .map_err(|err| Status::internal(err.to_string()))?; + Ok(Response::new(UpdateResponse {})) + } + + async fn remove_url( + &self, + request: Request, + ) -> Result, Status> { + let url = request.into_inner().url; + let mut info = self + .mint + .mint_info() + .await + .map_err(|err| Status::internal(err.to_string()))?; + let urls = info.urls; + urls.clone().unwrap_or_default().push(url); + + info.urls = urls; + + self.mint + .set_mint_info(info) + .await + .map_err(|err| Status::internal(err.to_string()))?; + Ok(Response::new(UpdateResponse {})) + } + + async fn add_contact( + &self, + request: Request, + ) -> Result, Status> { + let request_inner = request.into_inner(); + let mut info = self + .mint + .mint_info() + .await + .map_err(|err| Status::internal(err.to_string()))?; + + info.contact + .get_or_insert_with(Vec::new) + .push(cdk::nuts::ContactInfo::new( + request_inner.method, + request_inner.info, + )); + + self.mint + .set_mint_info(info) + .await + .map_err(|err| Status::internal(err.to_string()))?; + Ok(Response::new(UpdateResponse {})) + } + async fn remove_contact( + &self, + request: Request, + ) -> Result, Status> { + let request_inner = request.into_inner(); + let mut info = self + .mint + .mint_info() + .await + .map_err(|err| Status::internal(err.to_string()))?; + + if let Some(contact) = info.contact.as_mut() { + let contact_info = + cdk::nuts::ContactInfo::new(request_inner.method, request_inner.info); + contact.retain(|x| x != &contact_info); + + self.mint + .set_mint_info(info) + .await + .map_err(|err| Status::internal(err.to_string()))?; + } + Ok(Response::new(UpdateResponse {})) + } + + async fn update_nut04( + &self, + request: Request, + ) -> Result, Status> { + let mut info = self + .mint + .mint_info() + .await + .map_err(|err| Status::internal(err.to_string()))?; + + let mut nut04_settings = info.nuts.nut04.clone(); + + let request_inner = request.into_inner(); + + let unit = CurrencyUnit::from_str(&request_inner.unit) + .map_err(|_| Status::invalid_argument("Invalid unit".to_string()))?; + + let payment_method = PaymentMethod::from_str(&request_inner.method) + .map_err(|_| Status::invalid_argument("Invalid method".to_string()))?; + + let current_nut04_settings = nut04_settings.remove_settings(&unit, &payment_method); + + let mut methods = nut04_settings.methods.clone(); + + let updated_method_settings = MintMethodSettings { + method: payment_method, + unit, + min_amount: request_inner + .min + .map(Amount::from) + .or_else(|| current_nut04_settings.as_ref().and_then(|s| s.min_amount)), + max_amount: request_inner + .max + .map(Amount::from) + .or_else(|| current_nut04_settings.as_ref().and_then(|s| s.max_amount)), + description: request_inner.description.unwrap_or( + current_nut04_settings + .map(|c| c.description) + .unwrap_or_default(), + ), + }; + + methods.push(updated_method_settings); + + nut04_settings.methods = methods; + + if let Some(disabled) = request_inner.disabled { + nut04_settings.disabled = disabled; + } + + info.nuts.nut04 = nut04_settings; + + self.mint + .set_mint_info(info) + .await + .map_err(|err| Status::internal(err.to_string()))?; + + Ok(Response::new(UpdateResponse {})) + } + + async fn update_nut05( + &self, + request: Request, + ) -> Result, Status> { + let mut info = self + .mint + .mint_info() + .await + .map_err(|err| Status::internal(err.to_string()))?; + let mut nut05_settings = info.nuts.nut05.clone(); + + let request_inner = request.into_inner(); + + let unit = CurrencyUnit::from_str(&request_inner.unit) + .map_err(|_| Status::invalid_argument("Invalid unit".to_string()))?; + + let payment_method = PaymentMethod::from_str(&request_inner.method) + .map_err(|_| Status::invalid_argument("Invalid method".to_string()))?; + + let current_nut05_settings = nut05_settings.remove_settings(&unit, &payment_method); + + let mut methods = nut05_settings.methods; + + let updated_method_settings = MeltMethodSettings { + method: payment_method, + unit, + min_amount: request_inner + .min + .map(Amount::from) + .or_else(|| current_nut05_settings.as_ref().and_then(|s| s.min_amount)), + max_amount: request_inner + .max + .map(Amount::from) + .or_else(|| current_nut05_settings.as_ref().and_then(|s| s.max_amount)), + }; + + methods.push(updated_method_settings); + nut05_settings.methods = methods; + + if let Some(disabled) = request_inner.disabled { + nut05_settings.disabled = disabled; + } + + info.nuts.nut05 = nut05_settings; + + self.mint + .set_mint_info(info) + .await + .map_err(|err| Status::internal(err.to_string()))?; + + Ok(Response::new(UpdateResponse {})) + } + + async fn update_quote_ttl( + &self, + request: Request, + ) -> Result, Status> { + let current_ttl = self + .mint + .quote_ttl() + .await + .map_err(|err| Status::internal(err.to_string()))?; + + let request = request.into_inner(); + + let quote_ttl = QuoteTTL { + mint_ttl: request.mint_ttl.unwrap_or(current_ttl.mint_ttl), + melt_ttl: request.melt_ttl.unwrap_or(current_ttl.melt_ttl), + }; + + self.mint + .set_quote_ttl(quote_ttl) + .await + .map_err(|err| Status::internal(err.to_string()))?; + + Ok(Response::new(UpdateResponse {})) + } + + async fn update_nut04_quote( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + let quote_id = request + .quote_id + .parse() + .map_err(|_| Status::invalid_argument("Invalid quote id".to_string()))?; + + let state = MintQuoteState::from_str(&request.state) + .map_err(|_| Status::invalid_argument("Invalid quote state".to_string()))?; + + let mint_quote = self + .mint + .localstore + .get_mint_quote("e_id) + .await + .map_err(|_| Status::invalid_argument("Could not find quote".to_string()))? + .ok_or(Status::invalid_argument("Could not find quote".to_string()))?; + + match state { + MintQuoteState::Paid => { + self.mint + .pay_mint_quote(&mint_quote) + .await + .map_err(|_| Status::internal("Could not find quote".to_string()))?; + } + _ => { + let mut mint_quote = mint_quote; + + mint_quote.state = state; + + self.mint + .update_mint_quote(mint_quote) + .await + .map_err(|_| Status::internal("Could not update quote".to_string()))?; + } + } + + let mint_quote = self + .mint + .localstore + .get_mint_quote("e_id) + .await + .map_err(|_| Status::invalid_argument("Could not find quote".to_string()))? + .ok_or(Status::invalid_argument("Could not find quote".to_string()))?; + + Ok(Response::new(UpdateNut04QuoteRequest { + state: mint_quote.state.to_string(), + quote_id: mint_quote.id.to_string(), + })) + } + + async fn rotate_next_keyset( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let unit = CurrencyUnit::from_str(&request.unit) + .map_err(|_| Status::invalid_argument("Invalid unit".to_string()))?; + + let keyset_info = self + .mint + .rotate_next_keyset( + unit, + request.max_order.map(|a| a as u8).unwrap_or(32), + request.input_fee_ppk.unwrap_or(0), + ) + .await + .map_err(|_| Status::invalid_argument("Could not rotate keyset".to_string()))?; + + Ok(Response::new(RotateNextKeysetResponse { + id: keyset_info.id.to_string(), + unit: keyset_info.unit.to_string(), + max_order: keyset_info.max_order.into(), + input_fee_ppk: keyset_info.input_fee_ppk, + })) + } +} diff --git a/crates/cdk-mintd/Cargo.toml b/crates/cdk-mintd/Cargo.toml index 550d415a..aac504e6 100644 --- a/crates/cdk-mintd/Cargo.toml +++ b/crates/cdk-mintd/Cargo.toml @@ -6,9 +6,14 @@ authors = ["CDK Developers"] license = "MIT" homepage = "https://github.com/cashubtc/cdk" repository = "https://github.com/cashubtc/cdk.git" -rust-version = "1.63.0" # MSRV description = "CDK mint binary" +[features] +default = ["management-rpc"] +swagger = ["cdk-axum/swagger", "dep:utoipa", "dep:utoipa-swagger-ui"] +redis = ["cdk-axum/redis"] +management-rpc = ["cdk-mint-rpc"] + [dependencies] anyhow = "1" axum = "0.6.20" @@ -28,8 +33,9 @@ cdk-lnd = { path = "../cdk-lnd", version = "0.6.0", default-features = false } cdk-fake-wallet = { path = "../cdk-fake-wallet", version = "0.6.0", default-features = false } cdk-strike = { path = "../cdk-strike", version = "0.6.0" } cdk-axum = { path = "../cdk-axum", version = "0.6.0", default-features = false } +cdk-mint-rpc = { path = "../cdk-mint-rpc", version = "0.6.0", default-features = false, optional = true } config = { version = "0.13.3", features = ["toml"] } -clap = { version = "4.4.8", features = ["derive", "env", "default"] } +clap = { version = "~4.0.32", features = ["derive"] } tokio = { version = "1", default-features = false } tracing = { version = "0.1", default-features = false, features = [ "attributes", @@ -38,14 +44,10 @@ tracing = { version = "0.1", default-features = false, features = [ tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } futures = { version = "0.3.28", default-features = false } serde = { version = "1", default-features = false, features = ["derive"] } -bip39 = "2.0" +bip39 = { version = "2.0", features = ["rand"] } tower-http = { version = "0.4.4", features = ["cors", "compression-full"] } lightning-invoice = { version = "0.32.0", features = ["serde", "std"] } home = "0.5.5" url = "2.3" utoipa = { version = "4", optional = true } utoipa-swagger-ui = { version = "4", features = ["axum"], optional = true } - -[features] -swagger = ["cdk-axum/swagger", "dep:utoipa", "dep:utoipa-swagger-ui"] -redis = ["cdk-axum/redis"] diff --git a/crates/cdk-mintd/example.config.toml b/crates/cdk-mintd/example.config.toml index 8003901a..a982a02e 100644 --- a/crates/cdk-mintd/example.config.toml +++ b/crates/cdk-mintd/example.config.toml @@ -6,6 +6,12 @@ mnemonic = "" # input_fee_ppk = 0 # enable_swagger_ui = false +[mint_management_rpc] +enabled = true +# address = "127.0.0.1" +# port = 8086 + + [info.http_cache] # memory or redis backend = "memory" @@ -15,7 +21,8 @@ tti = 60 # key_prefix = "mintd" # connection_string = "redis://localhost" - +# NOTE: If [mint_management_rpc] is enabled these values will only be used on first start up. +# Further changes must be made through the rpc. [mint_info] # name = "cdk-mintd mutiney net mint" # Hex pubkey of mint diff --git a/crates/cdk-mintd/src/config.rs b/crates/cdk-mintd/src/config.rs index b14cfc79..a7ef3254 100644 --- a/crates/cdk-mintd/src/config.rs +++ b/crates/cdk-mintd/src/config.rs @@ -187,6 +187,8 @@ pub struct Settings { pub lnd: Option, pub fake_wallet: Option, pub database: Database, + #[cfg(feature = "management-rpc")] + pub mint_management_rpc: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] @@ -209,6 +211,17 @@ pub struct MintInfo { pub contact_email: Option, } +#[cfg(feature = "management-rpc")] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct MintManagementRpc { + /// When this is set to `true` the mint use the config file for the initial set up on first start. + /// Changes to the `[mint_info]` after this **MUST** be made via the RPC changes to the config file or env vars will be ignored. + pub enabled: bool, + pub address: Option, + pub port: Option, + pub tls_dir_path: Option, +} + impl Settings { #[must_use] pub fn new

(config_file_name: Option

) -> Self diff --git a/crates/cdk-mintd/src/env_vars.rs b/crates/cdk-mintd/src/env_vars.rs index 27b815b0..8c5e874f 100644 --- a/crates/cdk-mintd/src/env_vars.rs +++ b/crates/cdk-mintd/src/env_vars.rs @@ -5,6 +5,8 @@ use std::str::FromStr; use anyhow::{anyhow, bail, Result}; use cdk::nuts::CurrencyUnit; +#[cfg(feature = "management-rpc")] +use crate::config::MintManagementRpc; use crate::config::{ Cln, Database, DatabaseEngine, FakeWallet, Info, LNbits, Ln, LnBackend, Lnd, MintInfo, Phoenixd, Settings, Strike, @@ -70,6 +72,15 @@ pub const ENV_FAKE_WALLET_FEE_PERCENT: &str = "CDK_MINTD_FAKE_WALLET_FEE_PERCENT pub const ENV_FAKE_WALLET_RESERVE_FEE_MIN: &str = "CDK_MINTD_FAKE_WALLET_RESERVE_FEE_MIN"; pub const ENV_FAKE_WALLET_MIN_DELAY: &str = "CDK_MINTD_FAKE_WALLET_MIN_DELAY"; pub const ENV_FAKE_WALLET_MAX_DELAY: &str = "CDK_MINTD_FAKE_WALLET_MAX_DELAY"; +// Mint RPC Server +#[cfg(feature = "management-rpc")] +pub const ENV_MINT_MANAGEMENT_ENABLED: &str = "CDK_MINTD_MINT_MANAGEMENT_ENABLED"; +#[cfg(feature = "management-rpc")] +pub const ENV_MINT_MANAGEMENT_ADDRESS: &str = "CDK_MINTD_MANAGEMENT_ADDRESS"; +#[cfg(feature = "management-rpc")] +pub const ENV_MINT_MANAGEMENT_PORT: &str = "CDK_MINTD_MANAGEMENT_PORT"; +#[cfg(feature = "management-rpc")] +pub const ENV_MINT_MANAGEMENT_TLS_DIR_PATH: &str = "CDK_MINTD_MANAGEMENT_TLS_DIR_PATH"; impl Settings { pub fn from_env(&mut self) -> Result { @@ -82,6 +93,16 @@ impl Settings { self.mint_info = self.mint_info.clone().from_env(); self.ln = self.ln.clone().from_env(); + #[cfg(feature = "management-rpc")] + { + self.mint_management_rpc = Some( + self.mint_management_rpc + .clone() + .unwrap_or_default() + .from_env(), + ); + } + match self.ln.ln_backend { LnBackend::Cln => { self.cln = Some(self.cln.clone().unwrap_or_default().from_env()); @@ -430,3 +451,30 @@ impl FakeWallet { self } } + +#[cfg(feature = "management-rpc")] +impl MintManagementRpc { + pub fn from_env(mut self) -> Self { + if let Ok(enabled) = env::var(ENV_MINT_MANAGEMENT_ENABLED) { + if let Ok(enabled) = enabled.parse() { + self.enabled = enabled; + } + } + + if let Ok(address) = env::var(ENV_MINT_MANAGEMENT_ADDRESS) { + self.address = Some(address); + } + + if let Ok(port) = env::var(ENV_MINT_MANAGEMENT_PORT) { + if let Ok(port) = port.parse::() { + self.port = Some(port); + } + } + + if let Ok(tls_path) = env::var(ENV_MINT_MANAGEMENT_TLS_DIR_PATH) { + self.tls_dir_path = Some(tls_path.into()); + } + + self + } +} diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index f6d728e6..42377e14 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -24,6 +24,8 @@ use cdk::nuts::nut19::{CachedEndpoint, Method as NUT19Method, Path as NUT19Path} use cdk::nuts::{ContactInfo, CurrencyUnit, MintVersion, PaymentMethod}; use cdk::types::LnKey; use cdk_axum::cache::HttpCache; +#[cfg(feature = "management-rpc")] +use cdk_mint_rpc::MintRPCServer; use cdk_mintd::cli::CLIArgs; use cdk_mintd::config::{self, DatabaseEngine, LnBackend}; use cdk_mintd::env_vars::ENV_WORK_DIR; @@ -46,10 +48,11 @@ async fn main() -> anyhow::Result<()> { let sqlx_filter = "sqlx=warn"; let hyper_filter = "hyper=warn"; + let h2_filter = "h2=warn"; let env_filter = EnvFilter::new(format!( - "{},{},{}", - default_filter, sqlx_filter, hyper_filter + "{},{},{},{}", + default_filter, sqlx_filter, hyper_filter, h2_filter )); tracing_subscriber::fmt().with_env_filter(env_filter).init(); @@ -303,7 +306,6 @@ async fn main() -> anyhow::Result<()> { .with_name(settings.mint_info.name) .with_version(mint_version) .with_description(settings.mint_info.description) - .with_quote_ttl(10000, 10000) .with_seed(mnemonic.to_seed_normalized("").to_vec()); let cached_endpoints = vec![ @@ -358,12 +360,51 @@ async fn main() -> anyhow::Result<()> { } let shutdown = Arc::new(Notify::new()); - + let mint_clone = Arc::clone(&mint); tokio::spawn({ let shutdown = Arc::clone(&shutdown); - async move { mint.wait_for_paid_invoices(shutdown).await } + async move { mint_clone.wait_for_paid_invoices(shutdown).await } }); + #[cfg(feature = "management-rpc")] + let mut rpc_enabled = false; + #[cfg(not(feature = "management-rpc"))] + let rpc_enabled = false; + + #[cfg(feature = "management-rpc")] + let mut rpc_server: Option = None; + + #[cfg(feature = "management-rpc")] + { + if let Some(rpc_settings) = settings.mint_management_rpc { + if rpc_settings.enabled { + let addr = rpc_settings.address.unwrap_or("127.0.0.1".to_string()); + let port = rpc_settings.port.unwrap_or(8086); + let mut mint_rpc = MintRPCServer::new(&addr, port, mint.clone())?; + + let tls_dir = rpc_settings.tls_dir_path.unwrap_or(work_dir.join("tls")); + + mint_rpc.start(Some(tls_dir)).await?; + + rpc_server = Some(mint_rpc); + + rpc_enabled = true; + } + } + } + + if rpc_enabled { + if mint.mint_info().await.is_err() { + tracing::info!("Mint info not set on mint, setting."); + mint.set_mint_info(mint_builder.mint_info).await?; + } else { + tracing::info!("Mint info already set, not using config file settings."); + } + } else { + tracing::warn!("RPC not enabled, using mint info from config."); + mint.set_mint_info(mint_builder.mint_info).await?; + } + let axum_result = axum::Server::bind( &format!("{}:{}", listen_addr, listen_port) .as_str() @@ -374,6 +415,13 @@ async fn main() -> anyhow::Result<()> { shutdown.notify_waiters(); + #[cfg(feature = "management-rpc")] + { + if let Some(rpc_server) = rpc_server { + rpc_server.stop().await?; + } + } + match axum_result { Ok(_) => { tracing::info!("Axum server stopped with okay status"); diff --git a/crates/cdk/src/mint/builder.rs b/crates/cdk/src/mint/builder.rs index 5ab52bef..723cf290 100644 --- a/crates/cdk/src/mint/builder.rs +++ b/crates/cdk/src/mint/builder.rs @@ -16,19 +16,18 @@ use crate::nuts::{ ContactInfo, CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings, MintVersion, MppMethodSettings, PaymentMethod, }; -use crate::types::{LnKey, QuoteTTL}; +use crate::types::LnKey; /// Cashu Mint #[derive(Default)] pub struct MintBuilder { /// Mint Info - mint_info: MintInfo, + pub mint_info: MintInfo, /// Mint Storage backend localstore: Option + Send + Sync>>, /// Ln backends for mint ln: Option + Send + Sync>>>, seed: Option>, - quote_ttl: Option, supported_units: HashMap, } @@ -175,15 +174,6 @@ impl MintBuilder { self } - /// Set quote ttl - pub fn with_quote_ttl(mut self, mint_ttl: u64, melt_ttl: u64) -> Self { - let quote_ttl = QuoteTTL { mint_ttl, melt_ttl }; - - self.quote_ttl = Some(quote_ttl); - - self - } - /// Set pubkey pub fn with_pubkey(mut self, pubkey: crate::nuts::PublicKey) -> Self { self.mint_info.pubkey = Some(pubkey); @@ -222,11 +212,6 @@ impl MintBuilder { .localstore .clone() .ok_or(anyhow!("Localstore not set"))?; - localstore.set_mint_info(self.mint_info.clone()).await?; - - localstore - .set_quote_ttl(self.quote_ttl.ok_or(anyhow!("Quote ttl not set"))?) - .await?; Ok(Mint::new( self.seed.as_ref().ok_or(anyhow!("Mint seed not set"))?, diff --git a/crates/cdk/src/mint/keysets.rs b/crates/cdk/src/mint/keysets.rs index 7dd96bee..fe139755 100644 --- a/crates/cdk/src/mint/keysets.rs +++ b/crates/cdk/src/mint/keysets.rs @@ -89,7 +89,7 @@ impl Mint { /// Add current keyset to inactive keysets /// Generate new keyset - #[instrument(skip(self))] + #[instrument(skip(self, custom_paths))] pub async fn rotate_keyset( &self, unit: CurrencyUnit, @@ -115,11 +115,13 @@ impl Mint { ); let id = keyset_info.id; self.localstore.add_keyset_info(keyset_info.clone()).await?; - self.localstore.set_active_keyset(unit, id).await?; + self.localstore.set_active_keyset(unit.clone(), id).await?; let mut keysets = self.keysets.write().await; keysets.insert(id, keyset); + tracing::info!("Rotated to new keyset {} for {}", id, unit); + Ok(keyset_info) } diff --git a/crates/cdk/src/mint/mint_nut04.rs b/crates/cdk/src/mint/mint_nut04.rs index c5bbbf7a..51a4790c 100644 --- a/crates/cdk/src/mint/mint_nut04.rs +++ b/crates/cdk/src/mint/mint_nut04.rs @@ -216,45 +216,45 @@ impl Mint { .get_mint_quote_by_request_lookup_id(request_lookup_id) .await { - tracing::debug!( - "Received payment notification for mint quote {}", - mint_quote.id - ); - if mint_quote.state != MintQuoteState::Issued - && mint_quote.state != MintQuoteState::Paid - { - let unix_time = unix_time(); + self.pay_mint_quote(&mint_quote).await?; + } + Ok(()) + } - if mint_quote.expiry < unix_time { - tracing::warn!( - "Mint quote {} paid at {} expired at {}, leaving current state", - mint_quote.id, - mint_quote.expiry, - unix_time, - ); - return Err(Error::ExpiredQuote(mint_quote.expiry, unix_time)); - } + /// Mark mint quote as paid + #[instrument(skip_all)] + pub async fn pay_mint_quote(&self, mint_quote: &MintQuote) -> Result<(), Error> { + tracing::debug!( + "Received payment notification for mint quote {}", + mint_quote.id + ); + if mint_quote.state != MintQuoteState::Issued && mint_quote.state != MintQuoteState::Paid { + let unix_time = unix_time(); - tracing::debug!( - "Marking quote {} paid by lookup id {}", + if mint_quote.expiry < unix_time { + tracing::warn!( + "Mint quote {} paid at {} expired at {}, leaving current state", mint_quote.id, - request_lookup_id - ); - - self.localstore - .update_mint_quote_state(&mint_quote.id, MintQuoteState::Paid) - .await?; - } else { - tracing::debug!( - "{} Quote already {} continuing", - mint_quote.id, - mint_quote.state + mint_quote.expiry, + unix_time, ); + return Err(Error::ExpiredQuote(mint_quote.expiry, unix_time)); } - self.pubsub_manager - .mint_quote_bolt11_status(mint_quote, MintQuoteState::Paid); + self.localstore + .update_mint_quote_state(&mint_quote.id, MintQuoteState::Paid) + .await?; + } else { + tracing::debug!( + "{} Quote already {} continuing", + mint_quote.id, + mint_quote.state + ); } + + self.pubsub_manager + .mint_quote_bolt11_status(mint_quote.clone(), MintQuoteState::Paid); + Ok(()) } diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index 64417ba6..60357582 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv}; use bitcoin::secp256k1::{self, Secp256k1}; -use cdk_common::common::LnKey; +use cdk_common::common::{LnKey, QuoteTTL}; use cdk_common::database::{self, MintDatabase}; use cdk_common::mint::MintKeySetInfo; use futures::StreamExt; @@ -194,6 +194,21 @@ impl Mint { Ok(self.localstore.get_mint_info().await?) } + /// Set mint info + pub async fn set_mint_info(&self, mint_info: MintInfo) -> Result<(), Error> { + Ok(self.localstore.set_mint_info(mint_info).await?) + } + + /// Get quote ttl + pub async fn quote_ttl(&self) -> Result { + Ok(self.localstore.get_quote_ttl().await?) + } + + /// Set quote ttl + pub async fn set_quote_ttl(&self, quote_ttl: QuoteTTL) -> Result<(), Error> { + Ok(self.localstore.set_quote_ttl(quote_ttl).await?) + } + /// Wait for any invoice to be paid /// For each backend starts a task that waits for any invoice to be paid /// Once invoice is paid mint quote status is updated diff --git a/flake.nix b/flake.nix index 1bf2d631..a152cf44 100644 --- a/flake.nix +++ b/flake.nix @@ -244,6 +244,13 @@ cargo update -p async-compression --precise 0.4.3 cargo update -p zstd-sys --precise 2.0.8+zstd.1.5.5 + cargo update -p clap_lex --precise 0.3.0 + cargo update -p regex --precise 1.9.6 + cargo update -p petgraph --precise 0.6.2 + cargo update -p hashbrown@0.15.2 --precise 0.15.0 + cargo update -p async-stream --precise 0.3.5 + cargo update -p home --precise 0.5.5 + # For wasm32-unknown-unknown target cargo update -p bumpalo --precise 3.12.0 cargo update -p moka --precise 0.11.1 @@ -291,6 +298,7 @@ export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath [ pkgs.zlib ]}:$LD_LIBRARY_PATH + export RUST_SRC_PATH=${nightly_toolchain}/lib/rustlib/src/rust/library ''; buildInputs = buildInputs ++ [ nightly_toolchain ]; inherit nativeBuildInputs;