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:
thesimplekid
2024-07-26 08:21:41 -04:00
parent da229cdb25
commit 169f5f1533
10 changed files with 156 additions and 112 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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