Merge pull request #543 from thesimplekid/managment_rpc

Managment rpc
This commit is contained in:
thesimplekid
2025-02-06 12:40:45 +00:00
committed by GitHub
46 changed files with 1809 additions and 77 deletions

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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))
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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 = [

View File

@@ -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 }

View File

@@ -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?;

View File

@@ -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);

View File

@@ -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(())

View File

@@ -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?;

View File

@@ -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"

View File

@@ -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

View File

@@ -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(())
}

View File

@@ -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)

View File

@@ -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(())
}

View File

@@ -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 {}

View File

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

View File

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

View File

@@ -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};

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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;
}

View File

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

View File

@@ -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,
}))
}
}

View File

@@ -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"]

View File

@@ -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

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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");

View File

@@ -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"))?,

View File

@@ -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)
}

View File

@@ -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(())
}

View File

@@ -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

View File

@@ -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;