mirror of
https://github.com/aljazceru/cdk.git
synced 2026-02-11 08:06:59 +01:00
feat: strike multi unit
fix: mint create new keysets for units fix: use amount from melt quote fix: melt quote correct payment unit
This commit is contained in:
@@ -51,6 +51,8 @@
|
||||
### Fixed
|
||||
- cdk(mint): `SIG_ALL` is not allowed in `melt` ([thesimplekid]).
|
||||
- cdk(mint): On `swap` verify correct number of sigs on outputs when `SigAll` ([thesimplekid]).
|
||||
- cdk(mint): Use amount in payment_quote response from ln backend ([thesimplekid]).
|
||||
- cdk(mint): Create new keysets for added supported units ([thesimplekid]).
|
||||
|
||||
### Removed
|
||||
- cdk(wallet): Remove unused argument `SplitTarget` on `melt` ([thesimplekid]).
|
||||
|
||||
@@ -141,19 +141,6 @@ pub async fn get_melt_bolt11_quote(
|
||||
into_response(Error::UnsupportedUnit)
|
||||
})?;
|
||||
|
||||
let invoice_amount_msat = payload
|
||||
.request
|
||||
.amount_milli_satoshis()
|
||||
.ok_or(Error::InvoiceAmountUndefined)
|
||||
.map_err(into_response)?;
|
||||
|
||||
// Convert amount to quote unit
|
||||
let amount =
|
||||
to_unit(invoice_amount_msat, &CurrencyUnit::Msat, &payload.unit).map_err(|err| {
|
||||
tracing::error!("Backed does not support unit: {}", err);
|
||||
into_response(Error::UnsupportedUnit)
|
||||
})?;
|
||||
|
||||
let payment_quote = ln.get_payment_quote(&payload).await.map_err(|err| {
|
||||
tracing::error!(
|
||||
"Could not get payment quote for mint quote, {} bolt11, {}",
|
||||
@@ -169,7 +156,7 @@ pub async fn get_melt_bolt11_quote(
|
||||
.new_melt_quote(
|
||||
payload.request.to_string(),
|
||||
payload.unit,
|
||||
amount.into(),
|
||||
payment_quote.amount.into(),
|
||||
payment_quote.fee.into(),
|
||||
unix_time() + state.quote_ttl,
|
||||
payment_quote.request_lookup_id,
|
||||
|
||||
@@ -28,10 +28,11 @@ mnemonic = ""
|
||||
# Required ln backend `cln`, `strike`, `fakewallet`
|
||||
ln_backend = "cln"
|
||||
|
||||
# CLN
|
||||
# [cln]
|
||||
# Required if using cln backend path to rpc
|
||||
# cln_path = ""
|
||||
|
||||
# Strike
|
||||
# Required if using strike backed
|
||||
# strike_api_key=""
|
||||
# [strike]
|
||||
# api_key=""
|
||||
# Optional default sats
|
||||
# supported_units=[""]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use cdk::nuts::PublicKey;
|
||||
use cdk::nuts::{CurrencyUnit, PublicKey};
|
||||
use cdk::Amount;
|
||||
use config::{Config, ConfigError, File};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -29,14 +29,22 @@ pub enum LnBackend {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Ln {
|
||||
pub ln_backend: LnBackend,
|
||||
pub cln_path: Option<PathBuf>,
|
||||
pub strike_api_key: Option<String>,
|
||||
pub greenlight_invite_code: Option<String>,
|
||||
pub invoice_description: Option<String>,
|
||||
pub fee_percent: f32,
|
||||
pub reserve_fee_min: Amount,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Strike {
|
||||
pub api_key: String,
|
||||
pub supported_units: Option<Vec<CurrencyUnit>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Cln {
|
||||
pub rpc_path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DatabaseEngine {
|
||||
@@ -55,6 +63,8 @@ pub struct Settings {
|
||||
pub info: Info,
|
||||
pub mint_info: MintInfo,
|
||||
pub ln: Ln,
|
||||
pub cln: Option<Cln>,
|
||||
pub strike: Option<Strike>,
|
||||
pub database: Database,
|
||||
}
|
||||
|
||||
@@ -114,11 +124,9 @@ impl Settings {
|
||||
let settings: Settings = config.try_deserialize()?;
|
||||
|
||||
match settings.ln.ln_backend {
|
||||
LnBackend::Cln => assert!(settings.ln.cln_path.is_some()),
|
||||
//LnBackend::Greenlight => (),
|
||||
//LnBackend::Ldk => (),
|
||||
LnBackend::Cln => assert!(settings.cln.is_some()),
|
||||
LnBackend::FakeWallet => (),
|
||||
LnBackend::Strike => assert!(settings.ln.strike_api_key.is_some()),
|
||||
LnBackend::Strike => assert!(settings.strike.is_some()),
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
|
||||
@@ -119,81 +119,97 @@ async fn main() -> anyhow::Result<()> {
|
||||
min_fee_reserve: absolute_ln_fee_reserve,
|
||||
percent_fee_reserve: relative_ln_fee,
|
||||
};
|
||||
let (ln, ln_router): (
|
||||
|
||||
let mut ln_backends: HashMap<
|
||||
LnKey,
|
||||
Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>,
|
||||
Option<Router>,
|
||||
) = match settings.ln.ln_backend {
|
||||
> = HashMap::new();
|
||||
|
||||
let mut supported_units = HashMap::new();
|
||||
let input_fee_ppk = settings.info.input_fee_ppk.unwrap_or(0);
|
||||
|
||||
let ln_routers: Vec<Router> = match settings.ln.ln_backend {
|
||||
LnBackend::Cln => {
|
||||
let cln_socket = expand_path(
|
||||
settings
|
||||
.ln
|
||||
.cln_path
|
||||
.clone()
|
||||
.ok_or(anyhow!("cln socket not defined"))?
|
||||
.cln
|
||||
.expect("Config checked at load that cln is some")
|
||||
.rpc_path
|
||||
.to_str()
|
||||
.ok_or(anyhow!("cln socket not defined"))?,
|
||||
)
|
||||
.ok_or(anyhow!("cln socket not defined"))?;
|
||||
let cln = Arc::new(
|
||||
Cln::new(
|
||||
cln_socket,
|
||||
fee_reserve,
|
||||
MintMeltSettings::default(),
|
||||
MintMeltSettings::default(),
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
||||
(
|
||||
Arc::new(
|
||||
Cln::new(
|
||||
cln_socket,
|
||||
fee_reserve,
|
||||
MintMeltSettings::default(),
|
||||
MintMeltSettings::default(),
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
None,
|
||||
)
|
||||
ln_backends.insert(LnKey::new(CurrencyUnit::Sat, PaymentMethod::Bolt11), cln);
|
||||
supported_units.insert(CurrencyUnit::Sat, (input_fee_ppk, 64));
|
||||
vec![]
|
||||
}
|
||||
LnBackend::Strike => {
|
||||
let api_key = settings
|
||||
.ln
|
||||
.strike_api_key
|
||||
.expect("Checked when validaing config");
|
||||
let strike_settings = settings.strike.expect("Checked on config load");
|
||||
let api_key = strike_settings.api_key;
|
||||
|
||||
// Channel used for strike web hook
|
||||
let (sender, receiver) = tokio::sync::mpsc::channel(8);
|
||||
let units = strike_settings
|
||||
.supported_units
|
||||
.unwrap_or(vec![CurrencyUnit::Sat]);
|
||||
|
||||
let webhook_endpoint = "/webhook/invoice";
|
||||
let mut routers = vec![];
|
||||
|
||||
let webhook_url = format!("{}{}", settings.info.url, webhook_endpoint);
|
||||
for unit in units {
|
||||
// Channel used for strike web hook
|
||||
let (sender, receiver) = tokio::sync::mpsc::channel(8);
|
||||
let webhook_endpoint = format!("/webhook/{}/invoice", unit);
|
||||
|
||||
let strike = Strike::new(
|
||||
api_key,
|
||||
MintMeltSettings::default(),
|
||||
MintMeltSettings::default(),
|
||||
CurrencyUnit::Sat,
|
||||
Arc::new(Mutex::new(Some(receiver))),
|
||||
webhook_url,
|
||||
)
|
||||
.await?;
|
||||
let webhook_url = format!("{}{}", settings.info.url, webhook_endpoint);
|
||||
|
||||
let router = strike
|
||||
.create_invoice_webhook(webhook_endpoint, sender)
|
||||
let strike = Strike::new(
|
||||
api_key.clone(),
|
||||
MintMeltSettings::default(),
|
||||
MintMeltSettings::default(),
|
||||
unit,
|
||||
Arc::new(Mutex::new(Some(receiver))),
|
||||
webhook_url,
|
||||
)
|
||||
.await?;
|
||||
|
||||
(Arc::new(strike), Some(router))
|
||||
let router = strike
|
||||
.create_invoice_webhook(&webhook_endpoint, sender)
|
||||
.await?;
|
||||
routers.push(router);
|
||||
|
||||
let ln_key = LnKey::new(unit, PaymentMethod::Bolt11);
|
||||
|
||||
ln_backends.insert(ln_key, Arc::new(strike));
|
||||
|
||||
supported_units.insert(unit, (input_fee_ppk, 64));
|
||||
}
|
||||
|
||||
routers
|
||||
}
|
||||
LnBackend::FakeWallet => (
|
||||
Arc::new(FakeWallet::new(
|
||||
LnBackend::FakeWallet => {
|
||||
let ln_key = LnKey::new(CurrencyUnit::Sat, PaymentMethod::Bolt11);
|
||||
|
||||
let wallet = Arc::new(FakeWallet::new(
|
||||
fee_reserve,
|
||||
MintMeltSettings::default(),
|
||||
MintMeltSettings::default(),
|
||||
)),
|
||||
None,
|
||||
),
|
||||
));
|
||||
|
||||
ln_backends.insert(ln_key, wallet);
|
||||
supported_units.insert(CurrencyUnit::Sat, (input_fee_ppk, 64));
|
||||
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
|
||||
let mut ln_backends = HashMap::new();
|
||||
|
||||
ln_backends.insert(
|
||||
LnKey::new(CurrencyUnit::Sat, PaymentMethod::Bolt11),
|
||||
Arc::clone(&ln),
|
||||
);
|
||||
|
||||
let (nut04_settings, nut05_settings, mpp_settings): (
|
||||
nut04::Settings,
|
||||
nut05::Settings,
|
||||
@@ -271,12 +287,6 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
let mnemonic = Mnemonic::from_str(&settings.info.mnemonic)?;
|
||||
|
||||
let input_fee_ppk = settings.info.input_fee_ppk.unwrap_or(0);
|
||||
|
||||
let mut supported_units = HashMap::new();
|
||||
|
||||
supported_units.insert(CurrencyUnit::Sat, (input_fee_ppk, 64));
|
||||
|
||||
let mint = Mint::new(
|
||||
&settings.info.url,
|
||||
&mnemonic.to_seed_normalized(""),
|
||||
@@ -292,7 +302,9 @@ async fn main() -> anyhow::Result<()> {
|
||||
// In the event that the mint server is down but the ln node is not
|
||||
// it is possible that a mint quote was paid but the mint has not been updated
|
||||
// this will check and update the mint state of those quotes
|
||||
check_pending_quotes(Arc::clone(&mint), Arc::clone(&ln)).await?;
|
||||
for ln in ln_backends.values() {
|
||||
check_pending_quotes(Arc::clone(&mint), Arc::clone(ln)).await?;
|
||||
}
|
||||
|
||||
let mint_url = settings.info.url;
|
||||
let listen_addr = settings.info.listen_host;
|
||||
@@ -303,36 +315,40 @@ async fn main() -> anyhow::Result<()> {
|
||||
.unwrap_or(DEFAULT_QUOTE_TTL_SECS);
|
||||
|
||||
let v1_service =
|
||||
cdk_axum::create_mint_router(&mint_url, Arc::clone(&mint), ln_backends, quote_ttl).await?;
|
||||
cdk_axum::create_mint_router(&mint_url, Arc::clone(&mint), ln_backends.clone(), quote_ttl)
|
||||
.await?;
|
||||
|
||||
let mint_service = Router::new()
|
||||
.nest("/", v1_service)
|
||||
let mut mint_service = Router::new()
|
||||
.merge(v1_service)
|
||||
.layer(CorsLayer::permissive());
|
||||
|
||||
let mint_service = match ln_router {
|
||||
Some(ln_router) => mint_service.nest("/", ln_router),
|
||||
None => mint_service,
|
||||
};
|
||||
for router in ln_routers {
|
||||
mint_service = mint_service.merge(router);
|
||||
}
|
||||
|
||||
// Spawn task to wait for invoces to be paid and update mint quotes
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match ln.wait_any_invoice().await {
|
||||
Ok(mut stream) => {
|
||||
while let Some(request_lookup_id) = stream.next().await {
|
||||
if let Err(err) =
|
||||
handle_paid_invoice(Arc::clone(&mint), &request_lookup_id).await
|
||||
{
|
||||
tracing::warn!("{:?}", err);
|
||||
|
||||
for (_, ln) in ln_backends {
|
||||
let mint = Arc::clone(&mint);
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match ln.wait_any_invoice().await {
|
||||
Ok(mut stream) => {
|
||||
while let Some(request_lookup_id) = stream.next().await {
|
||||
if let Err(err) =
|
||||
handle_paid_invoice(mint.clone(), &request_lookup_id).await
|
||||
{
|
||||
tracing::warn!("{:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Could not get invoice stream: {}", err);
|
||||
Err(err) => {
|
||||
tracing::warn!("Could not get invoice stream: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let listener =
|
||||
tokio::net::TcpListener::bind(format!("{}:{}", listen_addr, listen_port)).await?;
|
||||
|
||||
@@ -20,4 +20,4 @@ tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
thiserror.workspace = true
|
||||
uuid.workspace = true
|
||||
strike-rs = "0.2.2"
|
||||
strike-rs = "0.2.3"
|
||||
|
||||
@@ -123,10 +123,18 @@ impl MintLightning for Strike {
|
||||
return Err(Self::Err::Anyhow(anyhow!("Unsupported unit")));
|
||||
}
|
||||
|
||||
let source_currency = match melt_quote_request.unit {
|
||||
CurrencyUnit::Sat => StrikeCurrencyUnit::BTC,
|
||||
CurrencyUnit::Msat => StrikeCurrencyUnit::BTC,
|
||||
CurrencyUnit::Usd => StrikeCurrencyUnit::USD,
|
||||
CurrencyUnit::Eur => StrikeCurrencyUnit::EUR,
|
||||
};
|
||||
|
||||
let payment_quote_request = PayInvoiceQuoteRequest {
|
||||
ln_invoice: melt_quote_request.request.to_string(),
|
||||
source_currency: strike_rs::Currency::BTC,
|
||||
source_currency,
|
||||
};
|
||||
|
||||
let quote = self.strike_api.payment_quote(payment_quote_request).await?;
|
||||
|
||||
let fee = from_strike_amount(quote.lightning_network_fee, &melt_quote_request.unit)?;
|
||||
@@ -249,14 +257,14 @@ pub(crate) fn from_strike_amount(
|
||||
if strike_amount.currency == StrikeCurrencyUnit::USD {
|
||||
Ok((strike_amount.amount * 100.0).round() as u64)
|
||||
} else {
|
||||
bail!("Could not convert ");
|
||||
bail!("Could not convert strike USD");
|
||||
}
|
||||
}
|
||||
CurrencyUnit::Eur => {
|
||||
if strike_amount.currency == StrikeCurrencyUnit::EUR {
|
||||
Ok((strike_amount.amount * 100.0).round() as u64)
|
||||
} else {
|
||||
bail!("Could not convert ");
|
||||
bail!("Could not convert to EUR");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +167,7 @@ where
|
||||
(CurrencyUnit::Sat, CurrencyUnit::Msat) => Ok(amount * MSAT_IN_SAT),
|
||||
(CurrencyUnit::Msat, CurrencyUnit::Sat) => Ok(amount / MSAT_IN_SAT),
|
||||
(CurrencyUnit::Usd, CurrencyUnit::Usd) => Ok(amount),
|
||||
(CurrencyUnit::Eur, CurrencyUnit::Eur) => Ok(amount),
|
||||
_ => Err(Error::CannotConvertUnits),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ impl Mint {
|
||||
acc.entry(ks.unit).or_default().push(ks.clone());
|
||||
acc
|
||||
});
|
||||
let mut keyset_units = vec![];
|
||||
|
||||
for (unit, keysets) in keysets_by_unit {
|
||||
let mut keysets = keysets;
|
||||
@@ -121,6 +122,28 @@ impl Mint {
|
||||
*input_fee_ppk,
|
||||
);
|
||||
|
||||
let id = keyset_info.id;
|
||||
localstore.add_keyset_info(keyset_info).await?;
|
||||
localstore.set_active_keyset(unit, id).await?;
|
||||
active_keysets.insert(id, keyset);
|
||||
keyset_units.push(unit);
|
||||
}
|
||||
}
|
||||
|
||||
for (unit, (fee, max_order)) in supported_units {
|
||||
if !keyset_units.contains(&unit) {
|
||||
let derivation_path = derivation_path_from_unit(unit, 0);
|
||||
|
||||
let (keyset, keyset_info) = create_new_keyset(
|
||||
&secp_ctx,
|
||||
xpriv,
|
||||
derivation_path,
|
||||
Some(0),
|
||||
unit,
|
||||
max_order,
|
||||
fee,
|
||||
);
|
||||
|
||||
let id = keyset_info.id;
|
||||
localstore.add_keyset_info(keyset_info).await?;
|
||||
localstore.set_active_keyset(unit, id).await?;
|
||||
|
||||
@@ -452,8 +452,6 @@ mod tests {
|
||||
Id::from_str("00ad268c4d1f5826").unwrap()
|
||||
);
|
||||
|
||||
let token: TokenV4 = token.try_into().unwrap();
|
||||
|
||||
let encoded = &token.to_string();
|
||||
|
||||
let token_data = TokenV4::from_str(encoded).unwrap();
|
||||
@@ -470,7 +468,7 @@ mod tests {
|
||||
|
||||
assert_eq!(amount, Amount::from(4));
|
||||
|
||||
let unit = token.unit().clone().unwrap();
|
||||
let unit = (*token.unit()).unwrap();
|
||||
|
||||
assert_eq!(CurrencyUnit::Sat, unit);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user