mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-19 21:55:03 +01:00
* WIP: Introduce a SignatoryManager service. The SignatoryManager manager provides an API to interact with keysets, private keys, and all key-related operations, offering segregation between the mint and the most sensible part of the mind: the private keys. Although the default signatory runs in memory, it is completely isolated from the rest of the system and can only be communicated through the interface offered by the signatory manager. Only messages can be sent from the mintd to the Signatory trait through the Signatory Manager. This pull request sets the foundation for eventually being able to run the Signatory and all the key-related operations in a separate service, possibly in a foreign service, to offload risks, as described in #476. The Signatory manager is concurrent and deferred any mechanism needed to handle concurrency to the Signatory trait. * Fixed missing default feature for signatory * Do not read keys from the DB * Removed KeysDatabase Trait from MintDatabase All Keys operations should be done through the signatory * Make sure signatory has all the keys in memory Drop also foreign constraints on sqlite * Fix race condition * Adding debug info to failing test * Add `sleep` in test * Fixed issue with active auth keyset * Fixed dependency * Move all keys and keysets to an ArcSwap. Since the keys and keysets exist in RAM, most wrapping functions are infallible and synchronous, improving performance and adding breaking API changes. The signatory will provide this information on the boot and update when the `rotate_keyset` is executed. Todo: Implement a subscription key to reload the keys when the GRPC server changes the keys. For the embedded mode, that makes no sense since there is a single way to rotate keys, and that bit is already covered. * Implementing https://github.com/cashubtc/nuts/pull/250 * Add CLI for cdk-signatory to spawn an external signatory Add to the pipeline the external signatory * Update tests * Apply suggestions from code review Co-authored-by: ok300 <106775972+ok300@users.noreply.github.com> Co-authored-by: thesimplekid <tsk@thesimplekid.com> * Minor change * Update proto buf to use the newest format * Rename binary * Add instrumentations * Add more comments * Use a single database for the signatory Store all keys, even auth keys, in a single database. Leave the MintAuthDatabse trait implementation for the CDK but not the signagtory This commit also moves the cli mod to its own file * Update dep * Add `test_mint_keyset_gen` test --------- Co-authored-by: ok300 <106775972+ok300@users.noreply.github.com> Co-authored-by: thesimplekid <tsk@thesimplekid.com>
412 lines
12 KiB
Rust
412 lines
12 KiB
Rust
//! Mint Builder
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
|
|
use anyhow::anyhow;
|
|
use bitcoin::bip32::DerivationPath;
|
|
use cdk_common::database::{self, MintDatabase, MintKeysDatabase};
|
|
use cdk_common::error::Error;
|
|
use cdk_common::nut04::MintMethodOptions;
|
|
use cdk_common::nut05::MeltMethodOptions;
|
|
use cdk_common::payment::Bolt11Settings;
|
|
use cdk_common::{nut21, nut22};
|
|
use cdk_signatory::signatory::Signatory;
|
|
|
|
use super::nut17::SupportedMethods;
|
|
use super::nut19::{self, CachedEndpoint};
|
|
#[cfg(feature = "auth")]
|
|
use super::MintAuthDatabase;
|
|
use super::Nuts;
|
|
use crate::amount::Amount;
|
|
#[cfg(feature = "auth")]
|
|
use crate::cdk_database;
|
|
use crate::cdk_payment::{self, MintPayment};
|
|
use crate::mint::Mint;
|
|
use crate::nuts::{
|
|
ContactInfo, CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings, MintVersion,
|
|
MppMethodSettings, PaymentMethod,
|
|
};
|
|
use crate::types::PaymentProcessorKey;
|
|
|
|
/// Cashu Mint
|
|
#[derive(Default)]
|
|
pub struct MintBuilder {
|
|
/// Mint Info
|
|
pub mint_info: MintInfo,
|
|
/// Mint Storage backend
|
|
pub localstore: Option<Arc<dyn MintDatabase<database::Error> + Send + Sync>>,
|
|
/// Database for the Signatory
|
|
keystore: Option<Arc<dyn MintKeysDatabase<Err = database::Error> + Send + Sync>>,
|
|
/// Mint Storage backend
|
|
#[cfg(feature = "auth")]
|
|
auth_localstore: Option<Arc<dyn MintAuthDatabase<Err = cdk_database::Error> + Send + Sync>>,
|
|
/// Ln backends for mint
|
|
ln: Option<
|
|
HashMap<PaymentProcessorKey, Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>>,
|
|
>,
|
|
seed: Option<Vec<u8>>,
|
|
supported_units: HashMap<CurrencyUnit, (u64, u8)>,
|
|
custom_paths: HashMap<CurrencyUnit, DerivationPath>,
|
|
// protected_endpoints: HashMap<ProtectedEndpoint, AuthRequired>,
|
|
openid_discovery: Option<String>,
|
|
signatory: Option<Arc<dyn Signatory + Sync + Send + 'static>>,
|
|
}
|
|
|
|
impl MintBuilder {
|
|
/// New mint builder
|
|
pub fn new() -> MintBuilder {
|
|
let mut builder = MintBuilder::default();
|
|
|
|
let nuts = Nuts::new()
|
|
.nut07(true)
|
|
.nut08(true)
|
|
.nut09(true)
|
|
.nut10(true)
|
|
.nut11(true)
|
|
.nut12(true)
|
|
.nut14(true)
|
|
.nut20(true);
|
|
|
|
builder.mint_info.nuts = nuts;
|
|
|
|
builder
|
|
}
|
|
|
|
/// Set signatory service
|
|
pub fn with_signatory(mut self, signatory: Arc<dyn Signatory + Sync + Send + 'static>) -> Self {
|
|
self.signatory = Some(signatory);
|
|
self
|
|
}
|
|
|
|
/// Set seed
|
|
pub fn with_seed(mut self, seed: Vec<u8>) -> Self {
|
|
self.seed = Some(seed);
|
|
self
|
|
}
|
|
|
|
/// Set localstore
|
|
pub fn with_localstore(
|
|
mut self,
|
|
localstore: Arc<dyn MintDatabase<database::Error> + Send + Sync>,
|
|
) -> MintBuilder {
|
|
self.localstore = Some(localstore);
|
|
self
|
|
}
|
|
|
|
/// Set keystore database
|
|
pub fn with_keystore(
|
|
mut self,
|
|
keystore: Arc<dyn MintKeysDatabase<Err = database::Error> + Send + Sync>,
|
|
) -> MintBuilder {
|
|
self.keystore = Some(keystore);
|
|
self
|
|
}
|
|
|
|
/// Set auth localstore
|
|
#[cfg(feature = "auth")]
|
|
pub fn with_auth_localstore(
|
|
mut self,
|
|
localstore: Arc<dyn MintAuthDatabase<Err = cdk_database::Error> + Send + Sync>,
|
|
) -> MintBuilder {
|
|
self.auth_localstore = Some(localstore);
|
|
self
|
|
}
|
|
|
|
/// Set Openid discovery url
|
|
pub fn with_openid_discovery(mut self, openid_discovery: String) -> Self {
|
|
self.openid_discovery = Some(openid_discovery);
|
|
self
|
|
}
|
|
|
|
/// Set name
|
|
pub fn with_name(mut self, name: String) -> Self {
|
|
self.mint_info.name = Some(name);
|
|
self
|
|
}
|
|
|
|
/// Set initial mint URLs
|
|
pub fn with_urls(mut self, urls: Vec<String>) -> Self {
|
|
self.mint_info.urls = Some(urls);
|
|
self
|
|
}
|
|
|
|
/// Set icon url
|
|
pub fn with_icon_url(mut self, icon_url: String) -> Self {
|
|
self.mint_info.icon_url = Some(icon_url);
|
|
self
|
|
}
|
|
|
|
/// Set icon url
|
|
pub fn with_motd(mut self, motd: String) -> Self {
|
|
self.mint_info.motd = Some(motd);
|
|
self
|
|
}
|
|
|
|
/// Set terms of service URL
|
|
pub fn with_tos_url(mut self, tos_url: String) -> Self {
|
|
self.mint_info.tos_url = Some(tos_url);
|
|
self
|
|
}
|
|
|
|
/// Set description
|
|
pub fn with_description(mut self, description: String) -> Self {
|
|
self.mint_info.description = Some(description);
|
|
self
|
|
}
|
|
|
|
/// Set long description
|
|
pub fn with_long_description(mut self, description: String) -> Self {
|
|
self.mint_info.description_long = Some(description);
|
|
self
|
|
}
|
|
|
|
/// Set version
|
|
pub fn with_version(mut self, version: MintVersion) -> Self {
|
|
self.mint_info.version = Some(version);
|
|
self
|
|
}
|
|
|
|
/// Set contact info
|
|
pub fn add_contact_info(mut self, contact_info: ContactInfo) -> Self {
|
|
let mut contacts = self.mint_info.contact.clone().unwrap_or_default();
|
|
contacts.push(contact_info);
|
|
self.mint_info.contact = Some(contacts);
|
|
self
|
|
}
|
|
|
|
/// Add ln backend
|
|
pub async fn add_ln_backend(
|
|
mut self,
|
|
unit: CurrencyUnit,
|
|
method: PaymentMethod,
|
|
limits: MintMeltLimits,
|
|
ln_backend: Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>,
|
|
) -> Result<Self, Error> {
|
|
let ln_key = PaymentProcessorKey {
|
|
unit: unit.clone(),
|
|
method: method.clone(),
|
|
};
|
|
|
|
tracing::debug!("Adding ln backed for {}, {}", unit, method);
|
|
tracing::debug!("with limits {:?}", limits);
|
|
|
|
let mut ln = self.ln.unwrap_or_default();
|
|
|
|
let settings = ln_backend.get_settings().await?;
|
|
|
|
let settings: Bolt11Settings = settings.try_into()?;
|
|
|
|
if settings.mpp {
|
|
let mpp_settings = MppMethodSettings {
|
|
method: method.clone(),
|
|
unit: unit.clone(),
|
|
};
|
|
|
|
let mut mpp = self.mint_info.nuts.nut15.clone();
|
|
|
|
mpp.methods.push(mpp_settings);
|
|
|
|
self.mint_info.nuts.nut15 = mpp;
|
|
}
|
|
|
|
if method == PaymentMethod::Bolt11 {
|
|
let mint_method_settings = MintMethodSettings {
|
|
method: method.clone(),
|
|
unit: unit.clone(),
|
|
min_amount: Some(limits.mint_min),
|
|
max_amount: Some(limits.mint_max),
|
|
options: Some(MintMethodOptions::Bolt11 {
|
|
description: settings.invoice_description,
|
|
}),
|
|
};
|
|
|
|
self.mint_info.nuts.nut04.methods.push(mint_method_settings);
|
|
self.mint_info.nuts.nut04.disabled = false;
|
|
|
|
let melt_method_settings = MeltMethodSettings {
|
|
method,
|
|
unit,
|
|
min_amount: Some(limits.melt_min),
|
|
max_amount: Some(limits.melt_max),
|
|
options: Some(MeltMethodOptions::Bolt11 {
|
|
amountless: settings.amountless,
|
|
}),
|
|
};
|
|
self.mint_info.nuts.nut05.methods.push(melt_method_settings);
|
|
self.mint_info.nuts.nut05.disabled = false;
|
|
}
|
|
|
|
ln.insert(ln_key.clone(), ln_backend);
|
|
|
|
let mut supported_units = self.supported_units.clone();
|
|
|
|
supported_units.insert(ln_key.unit, (0, 32));
|
|
self.supported_units = supported_units;
|
|
|
|
self.ln = Some(ln);
|
|
|
|
Ok(self)
|
|
}
|
|
|
|
/// Set pubkey
|
|
pub fn with_pubkey(mut self, pubkey: crate::nuts::PublicKey) -> Self {
|
|
self.mint_info.pubkey = Some(pubkey);
|
|
|
|
self
|
|
}
|
|
|
|
/// Support websockets
|
|
pub fn add_supported_websockets(mut self, supported_method: SupportedMethods) -> Self {
|
|
let mut supported_settings = self.mint_info.nuts.nut17.supported.clone();
|
|
|
|
if !supported_settings.contains(&supported_method) {
|
|
supported_settings.push(supported_method);
|
|
|
|
self.mint_info.nuts = self.mint_info.nuts.nut17(supported_settings);
|
|
}
|
|
|
|
self
|
|
}
|
|
|
|
/// Add support for NUT19
|
|
pub fn add_cache(mut self, ttl: Option<u64>, cached_endpoints: Vec<CachedEndpoint>) -> Self {
|
|
let nut19_settings = nut19::Settings {
|
|
ttl,
|
|
cached_endpoints,
|
|
};
|
|
|
|
self.mint_info.nuts.nut19 = nut19_settings;
|
|
|
|
self
|
|
}
|
|
|
|
/// Set custom derivation paths for mint units
|
|
pub fn add_custom_derivation_paths(
|
|
mut self,
|
|
custom_paths: HashMap<CurrencyUnit, DerivationPath>,
|
|
) -> Self {
|
|
self.custom_paths = custom_paths;
|
|
self
|
|
}
|
|
|
|
/// Set clear auth settings
|
|
pub fn set_clear_auth_settings(mut self, openid_discovery: String, client_id: String) -> Self {
|
|
let mut nuts = self.mint_info.nuts;
|
|
|
|
nuts.nut21 = Some(nut21::Settings::new(
|
|
openid_discovery.clone(),
|
|
client_id,
|
|
vec![],
|
|
));
|
|
|
|
self.openid_discovery = Some(openid_discovery);
|
|
|
|
self.mint_info.nuts = nuts;
|
|
|
|
self
|
|
}
|
|
|
|
/// Set blind auth settings
|
|
pub fn set_blind_auth_settings(mut self, bat_max_mint: u64) -> Self {
|
|
let mut nuts = self.mint_info.nuts;
|
|
|
|
nuts.nut22 = Some(nut22::Settings::new(bat_max_mint, vec![]));
|
|
|
|
self.mint_info.nuts = nuts;
|
|
|
|
self
|
|
}
|
|
|
|
/// Sets the input fee ppk for a given unit
|
|
///
|
|
/// The unit **MUST** already have been added with a ln backend
|
|
pub fn set_unit_fee(mut self, unit: &CurrencyUnit, input_fee_ppk: u64) -> Result<Self, Error> {
|
|
let (input_fee, _max_order) = self
|
|
.supported_units
|
|
.get_mut(unit)
|
|
.ok_or(Error::UnsupportedUnit)?;
|
|
|
|
*input_fee = input_fee_ppk;
|
|
|
|
Ok(self)
|
|
}
|
|
|
|
/// Build mint
|
|
pub async fn build(&self) -> anyhow::Result<Mint> {
|
|
let localstore = self
|
|
.localstore
|
|
.clone()
|
|
.ok_or(anyhow!("Localstore not set"))?;
|
|
let ln = self.ln.clone().ok_or(anyhow!("Ln backends not set"))?;
|
|
|
|
let signatory = if let Some(signatory) = self.signatory.as_ref() {
|
|
signatory.clone()
|
|
} else {
|
|
let seed = self.seed.as_ref().ok_or(anyhow!("Mint seed not set"))?;
|
|
let in_memory_signatory = cdk_signatory::db_signatory::DbSignatory::new(
|
|
self.keystore.clone().ok_or(anyhow!("keystore not set"))?,
|
|
seed,
|
|
self.supported_units.clone(),
|
|
HashMap::new(),
|
|
)
|
|
.await?;
|
|
|
|
Arc::new(cdk_signatory::embedded::Service::new(Arc::new(
|
|
in_memory_signatory,
|
|
)))
|
|
};
|
|
|
|
#[cfg(feature = "auth")]
|
|
if let Some(openid_discovery) = &self.openid_discovery {
|
|
let auth_localstore = self
|
|
.auth_localstore
|
|
.clone()
|
|
.ok_or(anyhow!("Auth localstore not set"))?;
|
|
|
|
return Ok(Mint::new_with_auth(
|
|
signatory,
|
|
localstore,
|
|
auth_localstore,
|
|
ln,
|
|
openid_discovery.clone(),
|
|
)
|
|
.await?);
|
|
}
|
|
|
|
#[cfg(not(feature = "auth"))]
|
|
if self.openid_discovery.is_some() {
|
|
return Err(anyhow!(
|
|
"OpenID discovery URL provided but auth feature is not enabled"
|
|
));
|
|
}
|
|
|
|
Ok(Mint::new(signatory, localstore, ln).await?)
|
|
}
|
|
}
|
|
|
|
/// Mint and Melt Limits
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub struct MintMeltLimits {
|
|
/// Min mint amount
|
|
pub mint_min: Amount,
|
|
/// Max mint amount
|
|
pub mint_max: Amount,
|
|
/// Min melt amount
|
|
pub melt_min: Amount,
|
|
/// Max melt amount
|
|
pub melt_max: Amount,
|
|
}
|
|
|
|
impl MintMeltLimits {
|
|
/// Create new [`MintMeltLimits`]. The `min` and `max` limits apply to both minting and melting.
|
|
pub fn new(min: u64, max: u64) -> Self {
|
|
Self {
|
|
mint_min: min.into(),
|
|
mint_max: max.into(),
|
|
melt_min: min.into(),
|
|
melt_max: max.into(),
|
|
}
|
|
}
|
|
}
|