Explorar el Código

Merge pull request #543 from thesimplekid/managment_rpc

Managment rpc
thesimplekid hace 1 mes
padre
commit
c6558bdbb2
Se han modificado 46 ficheros con 1809 adiciones y 77 borrados
  1. 3 0
      .github/workflows/ci.yml
  2. 0 1
      .helix/languages.toml
  3. 2 0
      README.md
  4. 12 0
      crates/cashu/src/nuts/nut04.rs
  5. 12 0
      crates/cashu/src/nuts/nut05.rs
  6. 6 0
      crates/cashu/src/nuts/nut06.rs
  7. 1 1
      crates/cdk-axum/Cargo.toml
  8. 1 1
      crates/cdk-cli/Cargo.toml
  9. 9 2
      crates/cdk-integration-tests/src/init_fake_wallet.rs
  10. 10 3
      crates/cdk-integration-tests/src/init_pure_tests.rs
  11. 9 3
      crates/cdk-integration-tests/src/init_regtest.rs
  12. 7 1
      crates/cdk-integration-tests/tests/mint.rs
  13. 41 0
      crates/cdk-mint-rpc/Cargo.toml
  14. 93 0
      crates/cdk-mint-rpc/README.md
  15. 5 0
      crates/cdk-mint-rpc/build.rs
  16. 47 0
      crates/cdk-mint-rpc/generate_certs.sh
  17. 196 0
      crates/cdk-mint-rpc/src/bin/mint_rpc_cli.rs
  18. 27 0
      crates/cdk-mint-rpc/src/client.rs
  19. 5 0
      crates/cdk-mint-rpc/src/lib.rs
  20. 1 0
      crates/cdk-mint-rpc/src/mint_rpc_cli/mod.rs
  21. 25 0
      crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/mod.rs
  22. 40 0
      crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/rotate_next_keyset.rs
  23. 47 0
      crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_contact.rs
  24. 25 0
      crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_icon_url.rs
  25. 25 0
      crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_long_description.rs
  26. 25 0
      crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_motd.rs
  27. 25 0
      crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_name.rs
  28. 43 0
      crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut04.rs
  29. 32 0
      crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut04_quote.rs
  30. 40 0
      crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut05.rs
  31. 25 0
      crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_short_description.rs
  32. 28 0
      crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_ttl.rs
  33. 43 0
      crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_urls.rs
  34. 115 0
      crates/cdk-mint-rpc/src/proto/cdk-mint-rpc.proto
  35. 5 0
      crates/cdk-mint-rpc/src/proto/mod.rs
  36. 586 0
      crates/cdk-mint-rpc/src/proto/server.rs
  37. 9 7
      crates/cdk-mintd/Cargo.toml
  38. 8 1
      crates/cdk-mintd/example.config.toml
  39. 13 0
      crates/cdk-mintd/src/config.rs
  40. 48 0
      crates/cdk-mintd/src/env_vars.rs
  41. 53 5
      crates/cdk-mintd/src/main.rs
  42. 2 17
      crates/cdk/src/mint/builder.rs
  43. 4 2
      crates/cdk/src/mint/keysets.rs
  44. 32 32
      crates/cdk/src/mint/mint_nut04.rs
  45. 16 1
      crates/cdk/src/mint/mod.rs
  46. 8 0
      flake.nix

+ 3 - 0
.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

+ 0 - 1
.helix/languages.toml

@@ -1,2 +1 @@
 [language-server.rust-analyzer.config]
-cargo = { features = ["wallet", "mint", "swagger", "redis"] }

+ 2 - 0
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 

+ 12 - 0
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<MintMethodSettings> {
+        self.methods
+            .iter()
+            .position(|settings| &settings.method == method && &settings.unit == unit)
+            .map(|index| self.methods.remove(index))
+    }
 }

+ 12 - 0
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<MeltMethodSettings> {
+        self.methods
+            .iter()
+            .position(|settings| settings.method.eq(method) && settings.unit.eq(unit))
+            .map(|index| self.methods.remove(index))
+    }
 }
 
 /// Melt Settings

+ 6 - 0
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where

+ 1 - 1
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 = [

+ 1 - 1
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 }

+ 9 - 2
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?;

+ 10 - 3
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<Arc<Mint>> {
 
     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<Arc<Mint>> {
     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);

+ 9 - 3
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<Err = cdk_lightning::Error> + 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(())

+ 7 - 1
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?;
 

+ 41 - 0
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"

+ 93 - 0
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
+

+ 5 - 0
crates/cdk-mint-rpc/build.rs

@@ -0,0 +1,5 @@
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+    println!("cargo:rerun-if-changed=src/proto/cdk-mint-rpc.proto");
+    tonic_build::compile_protos("src/proto/cdk-mint-rpc.proto")?;
+    Ok(())
+}

+ 47 - 0
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)

+ 196 - 0
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<PathBuf>,
+
+    #[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(())
+}

+ 27 - 0
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<tonic::transport::Channel>,
+}
+
+impl MintRPCClient {
+    pub async fn new(url: String) -> Result<Self, Error> {
+        Ok(Self {
+            inner: CdkMintClient::connect(url).await?,
+        })
+    }
+}
+
+#[tonic::async_trait]
+impl CdkMint for MintRPCClient {}

+ 5 - 0
crates/cdk-mint-rpc/src/lib.rs

@@ -0,0 +1,5 @@
+pub mod proto;
+
+pub mod mint_rpc_cli;
+
+pub use proto::*;

+ 1 - 0
crates/cdk-mint-rpc/src/mint_rpc_cli/mod.rs

@@ -0,0 +1 @@
+pub mod subcommands;

+ 25 - 0
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};

+ 40 - 0
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<u8>,
+    #[arg(short, long)]
+    input_fee_ppk: Option<u64>,
+}
+
+pub async fn rotate_next_keyset(
+    client: &mut CdkMintClient<Channel>,
+    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(())
+}

+ 47 - 0
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<Channel>,
+    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<Channel>,
+    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(())
+}

+ 25 - 0
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<Channel>,
+    sub_command_args: &UpdateIconUrlCommand,
+) -> Result<()> {
+    let _response = client
+        .update_icon_url(Request::new(UpdateIconUrlRequest {
+            icon_url: sub_command_args.name.clone(),
+        }))
+        .await?;
+
+    Ok(())
+}

+ 25 - 0
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<Channel>,
+    sub_command_args: &UpdateLongDescriptionCommand,
+) -> Result<()> {
+    let _response = client
+        .update_long_description(Request::new(UpdateDescriptionRequest {
+            description: sub_command_args.description.clone(),
+        }))
+        .await?;
+
+    Ok(())
+}

+ 25 - 0
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<Channel>,
+    sub_command_args: &UpdateMotdCommand,
+) -> Result<()> {
+    let _response = client
+        .update_motd(Request::new(UpdateMotdRequest {
+            motd: sub_command_args.motd.clone(),
+        }))
+        .await?;
+
+    Ok(())
+}

+ 25 - 0
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<Channel>,
+    sub_command_args: &UpdateNameCommand,
+) -> Result<()> {
+    let _response = client
+        .update_name(Request::new(UpdateNameRequest {
+            name: sub_command_args.name.clone(),
+        }))
+        .await?;
+
+    Ok(())
+}

+ 43 - 0
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<u64>,
+    #[arg(long)]
+    max_amount: Option<u64>,
+    #[arg(long)]
+    disabled: Option<bool>,
+    #[arg(long)]
+    description: Option<bool>,
+}
+
+pub async fn update_nut04(
+    client: &mut CdkMintClient<Channel>,
+    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(())
+}

+ 32 - 0
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<Channel>,
+    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(())
+}

+ 40 - 0
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<u64>,
+    #[arg(long)]
+    max_amount: Option<u64>,
+    #[arg(long)]
+    disabled: Option<bool>,
+}
+
+pub async fn update_nut05(
+    client: &mut CdkMintClient<Channel>,
+    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(())
+}

+ 25 - 0
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<Channel>,
+    sub_command_args: &UpdateShortDescriptionCommand,
+) -> Result<()> {
+    let _response = client
+        .update_short_description(Request::new(UpdateDescriptionRequest {
+            description: sub_command_args.description.clone(),
+        }))
+        .await?;
+
+    Ok(())
+}

+ 28 - 0
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<u64>,
+    #[arg(long)]
+    melt_ttl: Option<u64>,
+}
+pub async fn update_quote_ttl(
+    client: &mut CdkMintClient<Channel>,
+    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(())
+}

+ 43 - 0
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<Channel>,
+    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<Channel>,
+    sub_command_args: &RemoveUrlCommand,
+) -> Result<()> {
+    let _response = client
+        .remove_url(Request::new(UpdateUrlRequest {
+            url: sub_command_args.url.clone(),
+        }))
+        .await?;
+
+    Ok(())
+}

+ 115 - 0
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;
+}

+ 5 - 0
crates/cdk-mint-rpc/src/proto/mod.rs

@@ -0,0 +1,5 @@
+tonic::include_proto!("cdk_mint_rpc");
+
+mod server;
+
+pub use server::MintRPCServer;

+ 586 - 0
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<Mint>,
+    shutdown: Arc<Notify>,
+    handle: Option<Arc<JoinHandle<Result<(), Error>>>>,
+}
+
+impl MintRPCServer {
+    pub fn new(addr: &str, port: u16, mint: Arc<Mint>) -> Result<Self, Error> {
+        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<PathBuf>) -> 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<GetInfoRequest>,
+    ) -> Result<Response<GetInfoResponse>, 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<UpdateMotdRequest>,
+    ) -> Result<Response<UpdateResponse>, 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<UpdateDescriptionRequest>,
+    ) -> Result<Response<UpdateResponse>, 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<UpdateDescriptionRequest>,
+    ) -> Result<Response<UpdateResponse>, 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<UpdateNameRequest>,
+    ) -> Result<Response<UpdateResponse>, 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<UpdateIconUrlRequest>,
+    ) -> Result<Response<UpdateResponse>, 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<UpdateUrlRequest>,
+    ) -> Result<Response<UpdateResponse>, 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<UpdateUrlRequest>,
+    ) -> Result<Response<UpdateResponse>, 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<UpdateContactRequest>,
+    ) -> Result<Response<UpdateResponse>, 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<UpdateContactRequest>,
+    ) -> Result<Response<UpdateResponse>, 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<UpdateNut04Request>,
+    ) -> Result<Response<UpdateResponse>, 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<UpdateNut05Request>,
+    ) -> Result<Response<UpdateResponse>, 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<UpdateQuoteTtlRequest>,
+    ) -> Result<Response<UpdateResponse>, 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<UpdateNut04QuoteRequest>,
+    ) -> Result<Response<UpdateNut04QuoteRequest>, 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(&quote_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(&quote_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<RotateNextKeysetRequest>,
+    ) -> Result<Response<RotateNextKeysetResponse>, 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,
+        }))
+    }
+}

+ 9 - 7
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"]

+ 8 - 1
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

+ 13 - 0
crates/cdk-mintd/src/config.rs

@@ -187,6 +187,8 @@ pub struct Settings {
     pub lnd: Option<Lnd>,
     pub fake_wallet: Option<FakeWallet>,
     pub database: Database,
+    #[cfg(feature = "management-rpc")]
+    pub mint_management_rpc: Option<MintManagementRpc>,
 }
 
 #[derive(Debug, Clone, Serialize, Deserialize, Default)]
@@ -209,6 +211,17 @@ pub struct MintInfo {
     pub contact_email: Option<String>,
 }
 
+#[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<String>,
+    pub port: Option<u16>,
+    pub tls_dir_path: Option<PathBuf>,
+}
+
 impl Settings {
     #[must_use]
     pub fn new<P>(config_file_name: Option<P>) -> Self

+ 48 - 0
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<Self> {
@@ -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::<u16>() {
+                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
+    }
+}

+ 53 - 5
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<cdk_mint_rpc::MintRPCServer> = 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");

+ 2 - 17
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<Arc<dyn MintDatabase<Err = database::Error> + Send + Sync>>,
     /// Ln backends for mint
     ln: Option<HashMap<LnKey, Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>>>,
     seed: Option<Vec<u8>>,
-    quote_ttl: Option<QuoteTTL>,
     supported_units: HashMap<CurrencyUnit, (u64, u8)>,
 }
 
@@ -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"))?,

+ 4 - 2
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)
     }
 

+ 32 - 32
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();
-
-                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));
-                }
+            self.pay_mint_quote(&mint_quote).await?;
+        }
+        Ok(())
+    }
 
-                tracing::debug!(
-                    "Marking quote {} paid by lookup id {}",
-                    mint_quote.id,
-                    request_lookup_id
-                );
+    /// 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();
 
-                self.localstore
-                    .update_mint_quote_state(&mint_quote.id, MintQuoteState::Paid)
-                    .await?;
-            } else {
-                tracing::debug!(
-                    "{} Quote already {} continuing",
+            if mint_quote.expiry < unix_time {
+                tracing::warn!(
+                    "Mint quote {} paid at {} expired at {}, leaving current state",
                     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(())
     }
 

+ 16 - 1
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<QuoteTTL, Error> {
+        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

+ 8 - 0
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;