mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-24 08:05:02 +01:00
feat: check outgoing payment status flow
This commit is contained in:
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
@@ -75,7 +75,7 @@ jobs:
|
||||
run: nix develop -i -L .#stable --command cargo test ${{ matrix.build-args }}
|
||||
|
||||
itest:
|
||||
name: "Integration tests"
|
||||
name: "Integration regtest tests"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -103,6 +103,35 @@ jobs:
|
||||
- name: Test
|
||||
run: nix develop -i -L .#stable --command just itest ${{ matrix.database }}
|
||||
|
||||
fake-wallet-itest:
|
||||
name: "Integration fake wallet tests"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
build-args:
|
||||
[
|
||||
-p cdk-integration-tests,
|
||||
]
|
||||
database:
|
||||
[
|
||||
REDB,
|
||||
SQLITE,
|
||||
MEMORY
|
||||
]
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v11
|
||||
- name: Nix Cache
|
||||
uses: DeterminateSystems/magic-nix-cache-action@v6
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Clippy
|
||||
run: nix develop -i -L .#stable --command cargo clippy ${{ matrix.build-args }} -- -D warnings
|
||||
- name: Test fake mint
|
||||
run: nix develop -i -L .#stable --command just fake-mint-itest ${{ matrix.database }}
|
||||
|
||||
msrv-build:
|
||||
name: "MSRV build"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -13,7 +13,7 @@ use axum::Router;
|
||||
use cdk::cdk_lightning::{self, MintLightning};
|
||||
use cdk::mint::Mint;
|
||||
use cdk::mint_url::MintUrl;
|
||||
use cdk::nuts::{CurrencyUnit, PaymentMethod};
|
||||
use cdk::types::LnKey;
|
||||
use router_handlers::*;
|
||||
|
||||
mod router_handlers;
|
||||
@@ -66,20 +66,3 @@ pub struct MintState {
|
||||
mint_url: MintUrl,
|
||||
quote_ttl: u64,
|
||||
}
|
||||
|
||||
/// Key used in hashmap of ln backends to identify what unit and payment method
|
||||
/// it is for
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct LnKey {
|
||||
/// Unit of Payment backend
|
||||
pub unit: CurrencyUnit,
|
||||
/// Method of payment backend
|
||||
pub method: PaymentMethod,
|
||||
}
|
||||
|
||||
impl LnKey {
|
||||
/// Create new [`LnKey`]
|
||||
pub fn new(unit: CurrencyUnit, method: PaymentMethod) -> Self {
|
||||
Self { unit, method }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{bail, Result};
|
||||
use axum::extract::{Json, Path, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use cdk::amount::Amount;
|
||||
use cdk::cdk_lightning::to_unit;
|
||||
use cdk::cdk_lightning::{to_unit, MintLightning, PayInvoiceResponse};
|
||||
use cdk::error::{Error, ErrorResponse};
|
||||
use cdk::mint::MeltQuote;
|
||||
use cdk::nuts::nut05::MeltBolt11Response;
|
||||
use cdk::nuts::{
|
||||
CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeysResponse, KeysetResponse,
|
||||
MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltQuoteState,
|
||||
MintBolt11Request, MintBolt11Response, MintInfo, MintQuoteBolt11Request,
|
||||
MintQuoteBolt11Response, MintQuoteState, PaymentMethod, RestoreRequest, RestoreResponse,
|
||||
SwapRequest, SwapResponse,
|
||||
MintQuoteBolt11Response, PaymentMethod, RestoreRequest, RestoreResponse, SwapRequest,
|
||||
SwapResponse,
|
||||
};
|
||||
use cdk::util::unix_time;
|
||||
use cdk::Bolt11Invoice;
|
||||
|
||||
use crate::{LnKey, MintState};
|
||||
|
||||
@@ -196,6 +193,28 @@ pub async fn post_melt_bolt11(
|
||||
State(state): State<MintState>,
|
||||
Json(payload): Json<MeltBolt11Request>,
|
||||
) -> Result<Json<MeltBolt11Response>, Response> {
|
||||
use std::sync::Arc;
|
||||
async fn check_payment_state(
|
||||
ln: Arc<dyn MintLightning<Err = cdk::cdk_lightning::Error> + Send + Sync>,
|
||||
melt_quote: &MeltQuote,
|
||||
) -> Result<PayInvoiceResponse> {
|
||||
match ln
|
||||
.check_outgoing_payment(&melt_quote.request_lookup_id)
|
||||
.await
|
||||
{
|
||||
Ok(response) => Ok(response),
|
||||
Err(check_err) => {
|
||||
// If we cannot check the status of the payment we keep the proofs stuck as pending.
|
||||
tracing::error!(
|
||||
"Could not check the status of payment for {},. Proofs stuck as pending",
|
||||
melt_quote.id
|
||||
);
|
||||
tracing::error!("Checking payment error: {}", check_err);
|
||||
bail!("Could not check payment status")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let quote = match state.mint.verify_melt_request(&payload).await {
|
||||
Ok(quote) => quote,
|
||||
Err(err) => {
|
||||
@@ -212,139 +231,53 @@ pub async fn post_melt_bolt11(
|
||||
}
|
||||
};
|
||||
|
||||
// Check to see if there is a corresponding mint quote for a melt.
|
||||
// In this case the mint can settle the payment internally and no ln payment is
|
||||
// needed
|
||||
let mint_quote = match state
|
||||
.mint
|
||||
.localstore
|
||||
.get_mint_quote_by_request("e.request)
|
||||
.await
|
||||
{
|
||||
Ok(mint_quote) => mint_quote,
|
||||
let settled_internally_amount =
|
||||
match state.mint.handle_internal_melt_mint("e, &payload).await {
|
||||
Ok(amount) => amount,
|
||||
Err(err) => {
|
||||
tracing::debug!("Error attempting to get mint quote: {}", err);
|
||||
|
||||
tracing::error!("Attempting to settle internally failed");
|
||||
if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
|
||||
tracing::error!("Could not reset melt quote state: {}", err);
|
||||
}
|
||||
return Err(into_response(Error::Internal));
|
||||
}
|
||||
};
|
||||
|
||||
let inputs_amount_quote_unit = payload.proofs_amount().map_err(|_| {
|
||||
tracing::error!("Proof inputs in melt quote overflowed");
|
||||
into_response(Error::AmountOverflow)
|
||||
})?;
|
||||
|
||||
let (preimage, amount_spent_quote_unit) = match mint_quote {
|
||||
Some(mint_quote) => {
|
||||
if mint_quote.state == MintQuoteState::Issued
|
||||
|| mint_quote.state == MintQuoteState::Paid
|
||||
{
|
||||
return Err(into_response(Error::RequestAlreadyPaid));
|
||||
}
|
||||
|
||||
let mut mint_quote = mint_quote;
|
||||
|
||||
if mint_quote.amount > inputs_amount_quote_unit {
|
||||
tracing::debug!(
|
||||
"Not enough inuts provided: {} needed {}",
|
||||
inputs_amount_quote_unit,
|
||||
mint_quote.amount
|
||||
tracing::error!(
|
||||
"Could not reset melt quote {} state: {}",
|
||||
payload.quote,
|
||||
err
|
||||
);
|
||||
if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
|
||||
tracing::error!("Could not reset melt quote state: {}", err);
|
||||
}
|
||||
return Err(into_response(Error::InsufficientFunds));
|
||||
}
|
||||
|
||||
mint_quote.state = MintQuoteState::Paid;
|
||||
|
||||
let amount = quote.amount;
|
||||
|
||||
if let Err(_err) = state.mint.update_mint_quote(mint_quote).await {
|
||||
if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
|
||||
tracing::error!("Could not reset melt quote state: {}", err);
|
||||
}
|
||||
return Err(into_response(Error::Internal));
|
||||
}
|
||||
|
||||
(None, amount)
|
||||
}
|
||||
None => {
|
||||
let invoice = match Bolt11Invoice::from_str("e.request) {
|
||||
Ok(bolt11) => bolt11,
|
||||
Err(_) => {
|
||||
tracing::error!("Melt quote has invalid payment request");
|
||||
if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
|
||||
tracing::error!("Could not reset melt quote state: {}", err);
|
||||
}
|
||||
return Err(into_response(Error::InvalidPaymentRequest));
|
||||
return Err(into_response(err));
|
||||
}
|
||||
};
|
||||
|
||||
let mut partial_amount = None;
|
||||
|
||||
let (preimage, amount_spent_quote_unit) = match settled_internally_amount {
|
||||
Some(amount_spent) => (None, amount_spent),
|
||||
None => {
|
||||
// If the quote unit is SAT or MSAT we can check that the expected fees are
|
||||
// provided. We also check if the quote is less then the invoice
|
||||
// amount in the case that it is a mmp However, if the quote id not
|
||||
// amount in the case that it is a mmp However, if the quote is not
|
||||
// of a bitcoin unit we cannot do these checks as the mint
|
||||
// is unaware of a conversion rate. In this case it is assumed that the quote is
|
||||
// correct and the mint should pay the full invoice amount if inputs
|
||||
// > then quote.amount are included. This is checked in the
|
||||
// verify_melt method.
|
||||
if quote.unit == CurrencyUnit::Msat || quote.unit == CurrencyUnit::Sat {
|
||||
let quote_msats = to_unit(quote.amount, "e.unit, &CurrencyUnit::Msat)
|
||||
.expect("Quote unit is checked above that it can convert to msat");
|
||||
|
||||
let invoice_amount_msats: Amount = match invoice.amount_milli_satoshis() {
|
||||
Some(amount) => amount.into(),
|
||||
None => {
|
||||
// > `then quote.amount` are included. This is checked in the
|
||||
// `verify_melt` method.
|
||||
let partial_amount = match quote.unit {
|
||||
CurrencyUnit::Sat | CurrencyUnit::Msat => {
|
||||
match state
|
||||
.mint
|
||||
.check_melt_expected_ln_fees("e, &payload)
|
||||
.await
|
||||
{
|
||||
Ok(amount) => amount,
|
||||
Err(err) => {
|
||||
tracing::error!("Fee is not expected: {}", err);
|
||||
if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
|
||||
tracing::error!("Could not reset melt quote state: {}", err);
|
||||
}
|
||||
return Err(into_response(Error::InvoiceAmountUndefined));
|
||||
return Err(into_response(Error::Internal));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
partial_amount = match invoice_amount_msats > quote_msats {
|
||||
true => {
|
||||
let partial_msats = invoice_amount_msats - quote_msats;
|
||||
|
||||
Some(
|
||||
to_unit(partial_msats, &CurrencyUnit::Msat, "e.unit)
|
||||
.map_err(|_| into_response(Error::UnitUnsupported))?,
|
||||
)
|
||||
}
|
||||
false => None,
|
||||
};
|
||||
|
||||
let amount_to_pay = match partial_amount {
|
||||
Some(amount_to_pay) => amount_to_pay,
|
||||
None => to_unit(invoice_amount_msats, &CurrencyUnit::Msat, "e.unit)
|
||||
.map_err(|_| into_response(Error::UnitUnsupported))?,
|
||||
};
|
||||
|
||||
if amount_to_pay + quote.fee_reserve > inputs_amount_quote_unit {
|
||||
tracing::debug!(
|
||||
"Not enough inuts provided: {} msats needed {} msats",
|
||||
inputs_amount_quote_unit,
|
||||
amount_to_pay
|
||||
);
|
||||
|
||||
if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
|
||||
tracing::error!("Could not reset melt quote state: {}", err);
|
||||
}
|
||||
|
||||
return Err(into_response(Error::TransactionUnbalanced(
|
||||
inputs_amount_quote_unit.into(),
|
||||
amount_to_pay.into(),
|
||||
quote.fee_reserve.into(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let ln = match state.ln.get(&LnKey::new(quote.unit, PaymentMethod::Bolt11)) {
|
||||
Some(ln) => ln,
|
||||
None => {
|
||||
@@ -361,46 +294,95 @@ pub async fn post_melt_bolt11(
|
||||
.pay_invoice(quote.clone(), partial_amount, Some(quote.fee_reserve))
|
||||
.await
|
||||
{
|
||||
Ok(pay)
|
||||
if pay.status == MeltQuoteState::Unknown
|
||||
|| pay.status == MeltQuoteState::Failed =>
|
||||
{
|
||||
let check_response = check_payment_state(Arc::clone(ln), "e)
|
||||
.await
|
||||
.map_err(|_| into_response(Error::Internal))?;
|
||||
|
||||
if check_response.status == MeltQuoteState::Paid {
|
||||
tracing::warn!("Pay invoice returned {} but check returned {}. Proofs stuck as pending", pay.status.to_string(), check_response.status.to_string());
|
||||
|
||||
return Err(into_response(Error::Internal));
|
||||
}
|
||||
|
||||
check_response
|
||||
}
|
||||
Ok(pay) => pay,
|
||||
Err(err) => {
|
||||
tracing::error!("Could not pay invoice: {}", err);
|
||||
// If the error is that the invoice was already paid we do not want to hold
|
||||
// hold the proofs as pending to we reset them and return an error.
|
||||
if matches!(err, cdk::cdk_lightning::Error::InvoiceAlreadyPaid) {
|
||||
tracing::debug!("Invoice already paid, resetting melt quote");
|
||||
if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
|
||||
tracing::error!("Could not reset melt quote state: {}", err);
|
||||
}
|
||||
return Err(into_response(Error::RequestAlreadyPaid));
|
||||
}
|
||||
|
||||
let err = match err {
|
||||
cdk::cdk_lightning::Error::InvoiceAlreadyPaid => Error::RequestAlreadyPaid,
|
||||
_ => Error::PaymentFailed,
|
||||
};
|
||||
tracing::error!("Error returned attempting to pay: {} {}", quote.id, err);
|
||||
|
||||
return Err(into_response(err));
|
||||
let check_response = check_payment_state(Arc::clone(ln), "e)
|
||||
.await
|
||||
.map_err(|_| into_response(Error::Internal))?;
|
||||
// If there error is something else we want to check the status of the payment ensure it is not pending or has been made.
|
||||
if check_response.status == MeltQuoteState::Paid {
|
||||
tracing::warn!("Pay invoice returned an error but check returned {}. Proofs stuck as pending", check_response.status.to_string());
|
||||
|
||||
return Err(into_response(Error::Internal));
|
||||
}
|
||||
check_response
|
||||
}
|
||||
};
|
||||
|
||||
// Check that melt quote status paid by in ln backend
|
||||
if pre.status != MeltQuoteState::Paid {
|
||||
match pre.status {
|
||||
MeltQuoteState::Paid => (),
|
||||
MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => {
|
||||
tracing::info!("Lightning payment for quote {} failed.", payload.quote);
|
||||
if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
|
||||
tracing::error!("Could not reset melt quote state: {}", err);
|
||||
}
|
||||
|
||||
return Err(into_response(Error::PaymentFailed));
|
||||
}
|
||||
MeltQuoteState::Pending => {
|
||||
tracing::warn!(
|
||||
"LN payment pending, proofs are stuck as pending for quote: {}",
|
||||
payload.quote
|
||||
);
|
||||
return Err(into_response(Error::PendingQuote));
|
||||
}
|
||||
}
|
||||
|
||||
// Convert from unit of backend to quote unit
|
||||
let amount_spent = to_unit(pre.total_spent, &pre.unit, "e.unit).map_err(|_| {
|
||||
tracing::error!(
|
||||
"Could not convert from {} to {} in melt.",
|
||||
pre.unit,
|
||||
quote.unit
|
||||
// Note: this should never fail since these conversions happen earlier and would fail there.
|
||||
// Since it will not fail and even if it does the ln payment has already been paid, proofs should still be burned
|
||||
let amount_spent = to_unit(pre.total_spent, &pre.unit, "e.unit).unwrap_or_default();
|
||||
|
||||
let payment_lookup_id = pre.payment_lookup_id;
|
||||
|
||||
if payment_lookup_id != quote.request_lookup_id {
|
||||
tracing::info!(
|
||||
"Payment lookup id changed post payment from {} to {}",
|
||||
quote.request_lookup_id,
|
||||
payment_lookup_id
|
||||
);
|
||||
|
||||
into_response(Error::UnitUnsupported)
|
||||
})?;
|
||||
let mut melt_quote = quote;
|
||||
melt_quote.request_lookup_id = payment_lookup_id;
|
||||
|
||||
if let Err(err) = state.mint.localstore.add_melt_quote(melt_quote).await {
|
||||
tracing::warn!("Could not update payment lookup id: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
(pre.payment_preimage, amount_spent)
|
||||
}
|
||||
};
|
||||
|
||||
// If we made it here the payment has been made.
|
||||
// We process the melt burning the inputs and returning change
|
||||
let res = state
|
||||
.mint
|
||||
.process_melt_request(&payload, preimage, amount_spent_quote_unit)
|
||||
|
||||
@@ -14,6 +14,9 @@ pub enum Error {
|
||||
/// Unknown invoice
|
||||
#[error("Unknown invoice")]
|
||||
UnknownInvoice,
|
||||
/// Invalid payment hash
|
||||
#[error("Invalid hash")]
|
||||
InvalidHash,
|
||||
/// Cln Error
|
||||
#[error(transparent)]
|
||||
Cln(#[from] cln_rpc::Error),
|
||||
|
||||
@@ -113,7 +113,7 @@ impl MintLightning for Cln {
|
||||
|
||||
last_pay_idx = invoice.pay_index;
|
||||
|
||||
break Some((invoice.label, (cln_client, last_pay_idx)));
|
||||
break Some((invoice.payment_hash.to_string(), (cln_client, last_pay_idx)));
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -159,12 +159,13 @@ impl MintLightning for Cln {
|
||||
partial_amount: Option<Amount>,
|
||||
max_fee: Option<Amount>,
|
||||
) -> Result<PayInvoiceResponse, Self::Err> {
|
||||
let mut cln_client = self.cln_client.lock().await;
|
||||
let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?;
|
||||
let pay_state = self
|
||||
.check_outgoing_payment(&bolt11.payment_hash().to_string())
|
||||
.await?;
|
||||
|
||||
let pay_state =
|
||||
check_pay_invoice_status(&mut cln_client, melt_quote.request.to_string()).await?;
|
||||
|
||||
match pay_state {
|
||||
match pay_state.status {
|
||||
MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => (),
|
||||
MeltQuoteState::Paid => {
|
||||
tracing::debug!("Melt attempted on invoice already paid");
|
||||
return Err(Self::Err::InvoiceAlreadyPaid);
|
||||
@@ -173,9 +174,9 @@ impl MintLightning for Cln {
|
||||
tracing::debug!("Melt attempted on invoice already pending");
|
||||
return Err(Self::Err::InvoicePaymentPending);
|
||||
}
|
||||
MeltQuoteState::Unpaid => (),
|
||||
}
|
||||
|
||||
let mut cln_client = self.cln_client.lock().await;
|
||||
let cln_response = cln_client
|
||||
.call(Request::Pay(PayRequest {
|
||||
bolt11: melt_quote.request.to_string(),
|
||||
@@ -207,19 +208,18 @@ impl MintLightning for Cln {
|
||||
})
|
||||
.transpose()?,
|
||||
}))
|
||||
.await
|
||||
.map_err(Error::from)?;
|
||||
.await;
|
||||
|
||||
let response = match cln_response {
|
||||
cln_rpc::Response::Pay(pay_response) => {
|
||||
Ok(cln_rpc::Response::Pay(pay_response)) => {
|
||||
let status = match pay_response.status {
|
||||
PayStatus::COMPLETE => MeltQuoteState::Paid,
|
||||
PayStatus::PENDING => MeltQuoteState::Pending,
|
||||
PayStatus::FAILED => MeltQuoteState::Unpaid,
|
||||
PayStatus::FAILED => MeltQuoteState::Failed,
|
||||
};
|
||||
PayInvoiceResponse {
|
||||
payment_preimage: Some(hex::encode(pay_response.payment_preimage.to_vec())),
|
||||
payment_hash: pay_response.payment_hash.to_string(),
|
||||
payment_lookup_id: pay_response.payment_hash.to_string(),
|
||||
status,
|
||||
total_spent: to_unit(
|
||||
pay_response.amount_sent_msat.msat(),
|
||||
@@ -230,8 +230,11 @@ impl MintLightning for Cln {
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("CLN returned wrong response kind");
|
||||
return Err(cdk_lightning::Error::from(Error::WrongClnResponse));
|
||||
tracing::error!(
|
||||
"Error attempting to pay invoice: {}",
|
||||
bolt11.payment_hash().to_string()
|
||||
);
|
||||
return Err(Error::WrongClnResponse.into());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -274,9 +277,10 @@ impl MintLightning for Cln {
|
||||
cln_rpc::Response::Invoice(invoice_res) => {
|
||||
let request = Bolt11Invoice::from_str(&invoice_res.bolt11)?;
|
||||
let expiry = request.expires_at().map(|t| t.as_secs());
|
||||
let payment_hash = request.payment_hash();
|
||||
|
||||
Ok(CreateInvoiceResponse {
|
||||
request_lookup_id: label,
|
||||
request_lookup_id: payment_hash.to_string(),
|
||||
request,
|
||||
expiry,
|
||||
})
|
||||
@@ -288,16 +292,16 @@ impl MintLightning for Cln {
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_invoice_status(
|
||||
async fn check_incoming_invoice_status(
|
||||
&self,
|
||||
request_lookup_id: &str,
|
||||
payment_hash: &str,
|
||||
) -> Result<MintQuoteState, Self::Err> {
|
||||
let mut cln_client = self.cln_client.lock().await;
|
||||
|
||||
let cln_response = cln_client
|
||||
.call(Request::ListInvoices(ListinvoicesRequest {
|
||||
payment_hash: None,
|
||||
label: Some(request_lookup_id.to_string()),
|
||||
payment_hash: Some(payment_hash.to_string()),
|
||||
label: None,
|
||||
invstring: None,
|
||||
offer_id: None,
|
||||
index: None,
|
||||
@@ -316,7 +320,7 @@ impl MintLightning for Cln {
|
||||
None => {
|
||||
tracing::info!(
|
||||
"Check invoice called on unknown look up id: {}",
|
||||
request_lookup_id
|
||||
payment_hash
|
||||
);
|
||||
return Err(Error::WrongClnResponse.into());
|
||||
}
|
||||
@@ -330,6 +334,51 @@ impl MintLightning for Cln {
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
async fn check_outgoing_payment(
|
||||
&self,
|
||||
payment_hash: &str,
|
||||
) -> Result<PayInvoiceResponse, Self::Err> {
|
||||
let mut cln_client = self.cln_client.lock().await;
|
||||
|
||||
let cln_response = cln_client
|
||||
.call(Request::ListPays(ListpaysRequest {
|
||||
payment_hash: Some(payment_hash.parse().map_err(|_| Error::InvalidHash)?),
|
||||
bolt11: None,
|
||||
status: None,
|
||||
}))
|
||||
.await
|
||||
.map_err(Error::from)?;
|
||||
|
||||
match cln_response {
|
||||
cln_rpc::Response::ListPays(pays_response) => match pays_response.pays.first() {
|
||||
Some(pays_response) => {
|
||||
let status = cln_pays_status_to_mint_state(pays_response.status);
|
||||
|
||||
Ok(PayInvoiceResponse {
|
||||
payment_lookup_id: pays_response.payment_hash.to_string(),
|
||||
payment_preimage: pays_response.preimage.map(|p| hex::encode(p.to_vec())),
|
||||
status,
|
||||
total_spent: pays_response
|
||||
.amount_sent_msat
|
||||
.map_or(Amount::ZERO, |a| a.msat().into()),
|
||||
unit: CurrencyUnit::Msat,
|
||||
})
|
||||
}
|
||||
None => Ok(PayInvoiceResponse {
|
||||
payment_lookup_id: payment_hash.to_string(),
|
||||
payment_preimage: None,
|
||||
status: MeltQuoteState::Unknown,
|
||||
total_spent: Amount::ZERO,
|
||||
unit: CurrencyUnit::Msat,
|
||||
}),
|
||||
},
|
||||
_ => {
|
||||
tracing::warn!("CLN returned wrong response kind");
|
||||
Err(Error::WrongClnResponse.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Cln {
|
||||
@@ -370,37 +419,10 @@ fn cln_invoice_status_to_mint_state(status: ListinvoicesInvoicesStatus) -> MintQ
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_pay_invoice_status(
|
||||
cln_client: &mut cln_rpc::ClnRpc,
|
||||
bolt11: String,
|
||||
) -> Result<MeltQuoteState, cdk_lightning::Error> {
|
||||
let cln_response = cln_client
|
||||
.call(Request::ListPays(ListpaysRequest {
|
||||
bolt11: Some(bolt11),
|
||||
payment_hash: None,
|
||||
status: None,
|
||||
}))
|
||||
.await
|
||||
.map_err(Error::from)?;
|
||||
|
||||
let state = match cln_response {
|
||||
cln_rpc::Response::ListPays(pay_response) => {
|
||||
let pay = pay_response.pays.first();
|
||||
|
||||
match pay {
|
||||
Some(pay) => match pay.status {
|
||||
ListpaysPaysStatus::COMPLETE => MeltQuoteState::Paid,
|
||||
fn cln_pays_status_to_mint_state(status: ListpaysPaysStatus) -> MeltQuoteState {
|
||||
match status {
|
||||
ListpaysPaysStatus::PENDING => MeltQuoteState::Pending,
|
||||
ListpaysPaysStatus::FAILED => MeltQuoteState::Unpaid,
|
||||
},
|
||||
None => MeltQuoteState::Unpaid,
|
||||
ListpaysPaysStatus::COMPLETE => MeltQuoteState::Paid,
|
||||
ListpaysPaysStatus::FAILED => MeltQuoteState::Failed,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("CLN returned wrong response kind. When checking pay status");
|
||||
return Err(cdk_lightning::Error::from(Error::WrongClnResponse));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ futures = { version = "0.3.28", default-features = false }
|
||||
tokio = { version = "1", default-features = false }
|
||||
tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
|
||||
thiserror = "1"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
lightning-invoice = { version = "0.32.0", features = ["serde", "std"] }
|
||||
tokio-stream = "0.1.15"
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
#![warn(missing_docs)]
|
||||
#![warn(rustdoc::bare_urls)]
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
@@ -26,11 +28,12 @@ use cdk::util::unix_time;
|
||||
use error::Error;
|
||||
use futures::stream::StreamExt;
|
||||
use futures::Stream;
|
||||
use lightning_invoice::{Currency, InvoiceBuilder, PaymentSecret};
|
||||
use lightning_invoice::{Bolt11Invoice, Currency, InvoiceBuilder, PaymentSecret};
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time;
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod error;
|
||||
|
||||
@@ -42,6 +45,9 @@ pub struct FakeWallet {
|
||||
receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<String>>>>,
|
||||
mint_settings: MintMethodSettings,
|
||||
melt_settings: MeltMethodSettings,
|
||||
payment_states: Arc<Mutex<HashMap<String, MeltQuoteState>>>,
|
||||
failed_payment_check: Arc<Mutex<HashSet<String>>>,
|
||||
payment_delay: u64,
|
||||
}
|
||||
|
||||
impl FakeWallet {
|
||||
@@ -50,6 +56,9 @@ impl FakeWallet {
|
||||
fee_reserve: FeeReserve,
|
||||
mint_settings: MintMethodSettings,
|
||||
melt_settings: MeltMethodSettings,
|
||||
payment_states: HashMap<String, MeltQuoteState>,
|
||||
fail_payment_check: HashSet<String>,
|
||||
payment_delay: u64,
|
||||
) -> Self {
|
||||
let (sender, receiver) = tokio::sync::mpsc::channel(8);
|
||||
|
||||
@@ -59,10 +68,26 @@ impl FakeWallet {
|
||||
receiver: Arc::new(Mutex::new(Some(receiver))),
|
||||
mint_settings,
|
||||
melt_settings,
|
||||
payment_states: Arc::new(Mutex::new(payment_states)),
|
||||
failed_payment_check: Arc::new(Mutex::new(fail_payment_check)),
|
||||
payment_delay,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct for signaling what methods should respond via invoice description
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct FakeInvoiceDescription {
|
||||
/// State to be returned from pay invoice state
|
||||
pub pay_invoice_state: MeltQuoteState,
|
||||
/// State to be returned by check payment state
|
||||
pub check_payment_state: MeltQuoteState,
|
||||
/// Should pay invoice error
|
||||
pub pay_err: bool,
|
||||
/// Should check failure
|
||||
pub check_err: bool,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MintLightning for FakeWallet {
|
||||
type Err = cdk_lightning::Error;
|
||||
@@ -124,10 +149,42 @@ impl MintLightning for FakeWallet {
|
||||
_partial_msats: Option<Amount>,
|
||||
_max_fee_msats: Option<Amount>,
|
||||
) -> Result<PayInvoiceResponse, Self::Err> {
|
||||
let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?;
|
||||
|
||||
let payment_hash = bolt11.payment_hash().to_string();
|
||||
|
||||
let description = bolt11.description().to_string();
|
||||
|
||||
let status: Option<FakeInvoiceDescription> = serde_json::from_str(&description).ok();
|
||||
|
||||
let mut payment_states = self.payment_states.lock().await;
|
||||
let payment_status = status
|
||||
.clone()
|
||||
.map(|s| s.pay_invoice_state)
|
||||
.unwrap_or(MeltQuoteState::Paid);
|
||||
|
||||
let checkout_going_status = status
|
||||
.clone()
|
||||
.map(|s| s.check_payment_state)
|
||||
.unwrap_or(MeltQuoteState::Paid);
|
||||
|
||||
payment_states.insert(payment_hash.clone(), checkout_going_status);
|
||||
|
||||
if let Some(description) = status {
|
||||
if description.check_err {
|
||||
let mut fail = self.failed_payment_check.lock().await;
|
||||
fail.insert(payment_hash.clone());
|
||||
}
|
||||
|
||||
if description.pay_err {
|
||||
return Err(Error::UnknownInvoice.into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(PayInvoiceResponse {
|
||||
payment_preimage: Some("".to_string()),
|
||||
payment_hash: "".to_string(),
|
||||
status: MeltQuoteState::Paid,
|
||||
payment_lookup_id: payment_hash,
|
||||
status: payment_status,
|
||||
total_spent: melt_quote.amount,
|
||||
unit: melt_quote.unit,
|
||||
})
|
||||
@@ -143,8 +200,72 @@ impl MintLightning for FakeWallet {
|
||||
let time_now = unix_time();
|
||||
assert!(unix_expiry > time_now);
|
||||
|
||||
let label = Uuid::new_v4().to_string();
|
||||
let amount_msat = to_unit(amount, unit, &CurrencyUnit::Msat)?;
|
||||
|
||||
let invoice = create_fake_invoice(amount_msat.into(), description);
|
||||
|
||||
let sender = self.sender.clone();
|
||||
|
||||
let payment_hash = invoice.payment_hash();
|
||||
|
||||
let payment_hash_clone = payment_hash.to_string();
|
||||
|
||||
let duration = time::Duration::from_secs(self.payment_delay);
|
||||
|
||||
tokio::spawn(async move {
|
||||
// Wait for the random delay to elapse
|
||||
time::sleep(duration).await;
|
||||
|
||||
// Send the message after waiting for the specified duration
|
||||
if sender.send(payment_hash_clone.clone()).await.is_err() {
|
||||
tracing::error!("Failed to send label: {}", payment_hash_clone);
|
||||
}
|
||||
});
|
||||
|
||||
let expiry = invoice.expires_at().map(|t| t.as_secs());
|
||||
|
||||
Ok(CreateInvoiceResponse {
|
||||
request_lookup_id: payment_hash.to_string(),
|
||||
request: invoice,
|
||||
expiry,
|
||||
})
|
||||
}
|
||||
|
||||
async fn check_incoming_invoice_status(
|
||||
&self,
|
||||
_request_lookup_id: &str,
|
||||
) -> Result<MintQuoteState, Self::Err> {
|
||||
Ok(MintQuoteState::Paid)
|
||||
}
|
||||
|
||||
async fn check_outgoing_payment(
|
||||
&self,
|
||||
request_lookup_id: &str,
|
||||
) -> Result<PayInvoiceResponse, Self::Err> {
|
||||
// For fake wallet if the state is not explicitly set default to paid
|
||||
let states = self.payment_states.lock().await;
|
||||
let status = states.get(request_lookup_id).cloned();
|
||||
|
||||
let status = status.unwrap_or(MeltQuoteState::Paid);
|
||||
|
||||
let fail_payments = self.failed_payment_check.lock().await;
|
||||
|
||||
if fail_payments.contains(request_lookup_id) {
|
||||
return Err(cdk_lightning::Error::InvoicePaymentPending);
|
||||
}
|
||||
|
||||
Ok(PayInvoiceResponse {
|
||||
payment_preimage: Some("".to_string()),
|
||||
payment_lookup_id: request_lookup_id.to_string(),
|
||||
status,
|
||||
total_spent: Amount::ZERO,
|
||||
unit: self.get_settings().unit,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Create fake invoice
|
||||
pub fn create_fake_invoice(amount_msat: u64, description: String) -> Bolt11Invoice {
|
||||
let private_key = SecretKey::from_slice(
|
||||
&[
|
||||
0xe1, 0x26, 0xf6, 0x8f, 0x7e, 0xaf, 0xcc, 0x8b, 0x74, 0xf5, 0x4d, 0x26, 0x9f, 0xe2,
|
||||
@@ -154,51 +275,20 @@ impl MintLightning for FakeWallet {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let payment_hash = sha256::Hash::from_slice(&[0; 32][..]).unwrap();
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut random_bytes = [0u8; 32];
|
||||
rng.fill(&mut random_bytes);
|
||||
|
||||
let payment_hash = sha256::Hash::from_slice(&random_bytes).unwrap();
|
||||
let payment_secret = PaymentSecret([42u8; 32]);
|
||||
|
||||
let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?;
|
||||
|
||||
let invoice = InvoiceBuilder::new(Currency::Bitcoin)
|
||||
InvoiceBuilder::new(Currency::Bitcoin)
|
||||
.description(description)
|
||||
.payment_hash(payment_hash)
|
||||
.payment_secret(payment_secret)
|
||||
.amount_milli_satoshis(amount.into())
|
||||
.amount_milli_satoshis(amount_msat)
|
||||
.current_timestamp()
|
||||
.min_final_cltv_expiry_delta(144)
|
||||
.build_signed(|hash| Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key))
|
||||
.unwrap();
|
||||
|
||||
// Create a random delay between 3 and 6 seconds
|
||||
let duration = time::Duration::from_secs(3)
|
||||
+ time::Duration::from_millis(rand::random::<u64>() % 3001);
|
||||
|
||||
let sender = self.sender.clone();
|
||||
let label_clone = label.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
// Wait for the random delay to elapse
|
||||
time::sleep(duration).await;
|
||||
|
||||
// Send the message after waiting for the specified duration
|
||||
if sender.send(label_clone.clone()).await.is_err() {
|
||||
tracing::error!("Failed to send label: {}", label_clone);
|
||||
}
|
||||
});
|
||||
|
||||
let expiry = invoice.expires_at().map(|t| t.as_secs());
|
||||
|
||||
Ok(CreateInvoiceResponse {
|
||||
request_lookup_id: label,
|
||||
request: invoice,
|
||||
expiry,
|
||||
})
|
||||
}
|
||||
|
||||
async fn check_invoice_status(
|
||||
&self,
|
||||
_request_lookup_id: &str,
|
||||
) -> Result<MintQuoteState, Self::Err> {
|
||||
Ok(MintQuoteState::Paid)
|
||||
}
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ tower-http = { version = "0.4.4", features = ["cors"] }
|
||||
futures = { version = "0.3.28", default-features = false, features = ["executor"] }
|
||||
once_cell = "1.19.0"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
# ln-regtest-rs = { path = "../../../../ln-regtest-rs" }
|
||||
ln-regtest-rs = { git = "https://github.com/thesimplekid/ln-regtest-rs", rev = "1d88d3d0b" }
|
||||
lightning-invoice = { version = "0.32.0", features = ["serde", "std"] }
|
||||
|
||||
32
crates/cdk-integration-tests/src/bin/fake_wallet.rs
Normal file
32
crates/cdk-integration-tests/src/bin/fake_wallet.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use std::env;
|
||||
|
||||
use anyhow::Result;
|
||||
use cdk::cdk_database::mint_memory::MintMemoryDatabase;
|
||||
use cdk_integration_tests::{init_fake_wallet::start_fake_mint, init_regtest::get_temp_dir};
|
||||
use cdk_redb::MintRedbDatabase;
|
||||
use cdk_sqlite::MintSqliteDatabase;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let addr = "127.0.0.1";
|
||||
let port = 8086;
|
||||
|
||||
let mint_db_kind = env::var("MINT_DATABASE")?;
|
||||
|
||||
match mint_db_kind.as_str() {
|
||||
"MEMORY" => {
|
||||
start_fake_mint(addr, port, MintMemoryDatabase::default()).await?;
|
||||
}
|
||||
"SQLITE" => {
|
||||
let sqlite_db = MintSqliteDatabase::new(&get_temp_dir().join("mint")).await?;
|
||||
sqlite_db.migrate().await;
|
||||
start_fake_mint(addr, port, sqlite_db).await?;
|
||||
}
|
||||
"REDB" => {
|
||||
let redb_db = MintRedbDatabase::new(&get_temp_dir().join("mint")).unwrap();
|
||||
start_fake_mint(addr, port, redb_db).await?;
|
||||
}
|
||||
_ => panic!("Unknown mint db type: {}", mint_db_kind),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
112
crates/cdk-integration-tests/src/init_fake_wallet.rs
Normal file
112
crates/cdk-integration-tests/src/init_fake_wallet.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use axum::Router;
|
||||
use cdk::{
|
||||
cdk_database::{self, MintDatabase},
|
||||
cdk_lightning::MintLightning,
|
||||
mint::FeeReserve,
|
||||
nuts::{CurrencyUnit, MeltMethodSettings, MintMethodSettings},
|
||||
types::LnKey,
|
||||
};
|
||||
use cdk_fake_wallet::FakeWallet;
|
||||
use futures::StreamExt;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use crate::{handle_paid_invoice, init_regtest::create_mint};
|
||||
|
||||
pub async fn start_fake_mint<D>(addr: &str, port: u16, database: D) -> Result<()>
|
||||
where
|
||||
D: MintDatabase<Err = cdk_database::Error> + Send + Sync + 'static,
|
||||
{
|
||||
let default_filter = "debug";
|
||||
|
||||
let sqlx_filter = "sqlx=warn";
|
||||
let hyper_filter = "hyper=warn";
|
||||
|
||||
let env_filter = EnvFilter::new(format!(
|
||||
"{},{},{}",
|
||||
default_filter, sqlx_filter, hyper_filter
|
||||
));
|
||||
|
||||
// Parse input
|
||||
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
||||
|
||||
let mint = create_mint(database).await?;
|
||||
|
||||
let fee_reserve = FeeReserve {
|
||||
min_fee_reserve: 1.into(),
|
||||
percent_fee_reserve: 1.0,
|
||||
};
|
||||
|
||||
let fake_wallet = FakeWallet::new(
|
||||
fee_reserve,
|
||||
MintMethodSettings::default(),
|
||||
MeltMethodSettings::default(),
|
||||
HashMap::default(),
|
||||
HashSet::default(),
|
||||
0,
|
||||
);
|
||||
|
||||
let mut ln_backends: HashMap<
|
||||
LnKey,
|
||||
Arc<dyn MintLightning<Err = cdk::cdk_lightning::Error> + Sync + Send>,
|
||||
> = HashMap::new();
|
||||
|
||||
ln_backends.insert(
|
||||
LnKey::new(CurrencyUnit::Sat, cdk::nuts::PaymentMethod::Bolt11),
|
||||
Arc::new(fake_wallet),
|
||||
);
|
||||
|
||||
let quote_ttl = 100000;
|
||||
|
||||
let mint_arc = Arc::new(mint);
|
||||
|
||||
let v1_service = cdk_axum::create_mint_router(
|
||||
&format!("http://{}:{}", addr, port),
|
||||
Arc::clone(&mint_arc),
|
||||
ln_backends.clone(),
|
||||
quote_ttl,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mint_service = Router::new()
|
||||
.merge(v1_service)
|
||||
.layer(CorsLayer::permissive());
|
||||
|
||||
let mint = Arc::clone(&mint_arc);
|
||||
|
||||
for wallet in ln_backends.values() {
|
||||
let wallet_clone = Arc::clone(wallet);
|
||||
let mint = Arc::clone(&mint);
|
||||
tokio::spawn(async move {
|
||||
match wallet_clone.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
|
||||
{
|
||||
// nosemgrep: direct-panic
|
||||
panic!("{:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// nosemgrep: direct-panic
|
||||
panic!("Could not get invoice stream: {}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
println!("Staring Axum server");
|
||||
axum::Server::bind(&format!("{}:{}", addr, port).as_str().parse().unwrap())
|
||||
.serve(mint_service.into_make_service())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -8,8 +8,8 @@ use cdk::{
|
||||
cdk_lightning::MintLightning,
|
||||
mint::{FeeReserve, Mint},
|
||||
nuts::{CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings},
|
||||
types::LnKey,
|
||||
};
|
||||
use cdk_axum::LnKey;
|
||||
use cdk_cln::Cln as CdkCln;
|
||||
use futures::StreamExt;
|
||||
use ln_regtest_rs::{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{bail, Result};
|
||||
use axum::Router;
|
||||
use bip39::Mnemonic;
|
||||
use cdk::amount::{Amount, SplitTarget};
|
||||
@@ -12,17 +12,18 @@ use cdk::dhke::construct_proofs;
|
||||
use cdk::mint::FeeReserve;
|
||||
use cdk::nuts::{
|
||||
CurrencyUnit, Id, KeySet, MeltMethodSettings, MintInfo, MintMethodSettings, MintQuoteState,
|
||||
Nuts, PaymentMethod, PreMintSecrets, Proofs,
|
||||
Nuts, PaymentMethod, PreMintSecrets, Proofs, State,
|
||||
};
|
||||
use cdk::types::LnKey;
|
||||
use cdk::wallet::client::HttpClient;
|
||||
use cdk::{Mint, Wallet};
|
||||
use cdk_axum::LnKey;
|
||||
use cdk_fake_wallet::FakeWallet;
|
||||
use futures::StreamExt;
|
||||
use init_regtest::{get_mint_addr, get_mint_port, get_mint_url};
|
||||
use tokio::time::sleep;
|
||||
use tower_http::cors::CorsLayer;
|
||||
|
||||
pub mod init_fake_wallet;
|
||||
pub mod init_regtest;
|
||||
|
||||
pub fn create_backends_fake_wallet(
|
||||
@@ -41,6 +42,9 @@ pub fn create_backends_fake_wallet(
|
||||
fee_reserve.clone(),
|
||||
MintMethodSettings::default(),
|
||||
MeltMethodSettings::default(),
|
||||
HashMap::default(),
|
||||
HashSet::default(),
|
||||
0,
|
||||
));
|
||||
|
||||
ln_backends.insert(ln_key, wallet.clone());
|
||||
@@ -224,3 +228,40 @@ pub async fn mint_proofs(
|
||||
|
||||
Ok(pre_swap_proofs)
|
||||
}
|
||||
|
||||
// Get all pending from wallet and attempt to swap
|
||||
// Will panic if there are no pending
|
||||
// Will return Ok if swap fails as expected
|
||||
pub async fn attempt_to_swap_pending(wallet: &Wallet) -> Result<()> {
|
||||
let pending = wallet
|
||||
.localstore
|
||||
.get_proofs(None, None, Some(vec![State::Pending]), None)
|
||||
.await?;
|
||||
|
||||
assert!(!pending.is_empty());
|
||||
|
||||
let swap = wallet
|
||||
.swap(
|
||||
None,
|
||||
SplitTarget::None,
|
||||
pending.into_iter().map(|p| p.proof).collect(),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
match swap {
|
||||
Ok(_swap) => {
|
||||
bail!("These proofs should be pending")
|
||||
}
|
||||
Err(err) => match err {
|
||||
cdk::error::Error::TokenPending => (),
|
||||
_ => {
|
||||
println!("{:?}", err);
|
||||
bail!("Wrong error")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
313
crates/cdk-integration-tests/tests/fake_wallet.rs
Normal file
313
crates/cdk-integration-tests/tests/fake_wallet.rs
Normal file
@@ -0,0 +1,313 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::Result;
|
||||
use bip39::Mnemonic;
|
||||
use cdk::{
|
||||
amount::SplitTarget,
|
||||
cdk_database::WalletMemoryDatabase,
|
||||
nuts::{CurrencyUnit, MeltQuoteState, State},
|
||||
wallet::Wallet,
|
||||
};
|
||||
use cdk_fake_wallet::{create_fake_invoice, FakeInvoiceDescription};
|
||||
use cdk_integration_tests::attempt_to_swap_pending;
|
||||
use tokio::time::sleep;
|
||||
|
||||
const MINT_URL: &str = "http://127.0.0.1:8086";
|
||||
|
||||
// If both pay and check return pending input proofs should remain pending
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn test_fake_tokens_pending() -> Result<()> {
|
||||
let wallet = Wallet::new(
|
||||
MINT_URL,
|
||||
CurrencyUnit::Sat,
|
||||
Arc::new(WalletMemoryDatabase::default()),
|
||||
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||
None,
|
||||
)?;
|
||||
|
||||
let mint_quote = wallet.mint_quote(100.into(), None).await?;
|
||||
|
||||
sleep(Duration::from_secs(5)).await;
|
||||
|
||||
let _mint_amount = wallet
|
||||
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||
.await?;
|
||||
|
||||
let fake_description = FakeInvoiceDescription {
|
||||
pay_invoice_state: MeltQuoteState::Pending,
|
||||
check_payment_state: MeltQuoteState::Pending,
|
||||
pay_err: false,
|
||||
check_err: false,
|
||||
};
|
||||
|
||||
let invoice = create_fake_invoice(1000, serde_json::to_string(&fake_description).unwrap());
|
||||
|
||||
let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
|
||||
|
||||
let melt = wallet.melt(&melt_quote.id).await;
|
||||
|
||||
assert!(melt.is_err());
|
||||
|
||||
attempt_to_swap_pending(&wallet).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// If the pay error fails and the check returns unknown or failed
|
||||
// The inputs proofs should be unset as spending
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn test_fake_melt_payment_fail() -> Result<()> {
|
||||
let wallet = Wallet::new(
|
||||
MINT_URL,
|
||||
CurrencyUnit::Sat,
|
||||
Arc::new(WalletMemoryDatabase::default()),
|
||||
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||
None,
|
||||
)?;
|
||||
|
||||
let mint_quote = wallet.mint_quote(100.into(), None).await?;
|
||||
|
||||
let _mint_amount = wallet
|
||||
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||
.await?;
|
||||
|
||||
let fake_description = FakeInvoiceDescription {
|
||||
pay_invoice_state: MeltQuoteState::Unknown,
|
||||
check_payment_state: MeltQuoteState::Unknown,
|
||||
pay_err: true,
|
||||
check_err: false,
|
||||
};
|
||||
|
||||
let invoice = create_fake_invoice(1000, serde_json::to_string(&fake_description).unwrap());
|
||||
|
||||
let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
|
||||
|
||||
// The melt should error at the payment invoice command
|
||||
let melt = wallet.melt(&melt_quote.id).await;
|
||||
assert!(melt.is_err());
|
||||
|
||||
let fake_description = FakeInvoiceDescription {
|
||||
pay_invoice_state: MeltQuoteState::Failed,
|
||||
check_payment_state: MeltQuoteState::Failed,
|
||||
pay_err: true,
|
||||
check_err: false,
|
||||
};
|
||||
|
||||
let invoice = create_fake_invoice(1000, serde_json::to_string(&fake_description).unwrap());
|
||||
|
||||
let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
|
||||
|
||||
// The melt should error at the payment invoice command
|
||||
let melt = wallet.melt(&melt_quote.id).await;
|
||||
assert!(melt.is_err());
|
||||
|
||||
// The mint should have unset proofs from pending since payment failed
|
||||
let all_proof = wallet.get_proofs().await?;
|
||||
let states = wallet.check_proofs_spent(all_proof).await?;
|
||||
for state in states {
|
||||
assert!(state.state == State::Unspent);
|
||||
}
|
||||
|
||||
let wallet_bal = wallet.total_balance().await?;
|
||||
assert!(wallet_bal == 100.into());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// When both the pay_invoice and check_invoice both fail
|
||||
// the proofs should remain as pending
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn test_fake_melt_payment_fail_and_check() -> Result<()> {
|
||||
let wallet = Wallet::new(
|
||||
MINT_URL,
|
||||
CurrencyUnit::Sat,
|
||||
Arc::new(WalletMemoryDatabase::default()),
|
||||
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||
None,
|
||||
)?;
|
||||
|
||||
let mint_quote = wallet.mint_quote(100.into(), None).await?;
|
||||
|
||||
let _mint_amount = wallet
|
||||
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||
.await?;
|
||||
|
||||
let fake_description = FakeInvoiceDescription {
|
||||
pay_invoice_state: MeltQuoteState::Unknown,
|
||||
check_payment_state: MeltQuoteState::Unknown,
|
||||
pay_err: true,
|
||||
check_err: true,
|
||||
};
|
||||
|
||||
let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap());
|
||||
|
||||
let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
|
||||
|
||||
// The melt should error at the payment invoice command
|
||||
let melt = wallet.melt(&melt_quote.id).await;
|
||||
assert!(melt.is_err());
|
||||
|
||||
let pending = wallet
|
||||
.localstore
|
||||
.get_proofs(None, None, Some(vec![State::Pending]), None)
|
||||
.await?;
|
||||
|
||||
assert!(!pending.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// In the case that the ln backend returns a failed status but does not error
|
||||
// The mint should do a second check, then remove proofs from pending
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn test_fake_melt_payment_return_fail_status() -> Result<()> {
|
||||
let wallet = Wallet::new(
|
||||
MINT_URL,
|
||||
CurrencyUnit::Sat,
|
||||
Arc::new(WalletMemoryDatabase::default()),
|
||||
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||
None,
|
||||
)?;
|
||||
|
||||
let mint_quote = wallet.mint_quote(100.into(), None).await?;
|
||||
|
||||
let _mint_amount = wallet
|
||||
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||
.await?;
|
||||
|
||||
let fake_description = FakeInvoiceDescription {
|
||||
pay_invoice_state: MeltQuoteState::Failed,
|
||||
check_payment_state: MeltQuoteState::Failed,
|
||||
pay_err: false,
|
||||
check_err: false,
|
||||
};
|
||||
|
||||
let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap());
|
||||
|
||||
let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
|
||||
|
||||
// The melt should error at the payment invoice command
|
||||
let melt = wallet.melt(&melt_quote.id).await;
|
||||
assert!(melt.is_err());
|
||||
|
||||
let fake_description = FakeInvoiceDescription {
|
||||
pay_invoice_state: MeltQuoteState::Unknown,
|
||||
check_payment_state: MeltQuoteState::Unknown,
|
||||
pay_err: false,
|
||||
check_err: false,
|
||||
};
|
||||
|
||||
let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap());
|
||||
|
||||
let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
|
||||
|
||||
// The melt should error at the payment invoice command
|
||||
let melt = wallet.melt(&melt_quote.id).await;
|
||||
assert!(melt.is_err());
|
||||
|
||||
let pending = wallet
|
||||
.localstore
|
||||
.get_proofs(None, None, Some(vec![State::Pending]), None)
|
||||
.await?;
|
||||
|
||||
assert!(pending.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// In the case that the ln backend returns a failed status but does not error
|
||||
// The mint should do a second check, then remove proofs from pending
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn test_fake_melt_payment_error_unknown() -> Result<()> {
|
||||
let wallet = Wallet::new(
|
||||
MINT_URL,
|
||||
CurrencyUnit::Sat,
|
||||
Arc::new(WalletMemoryDatabase::default()),
|
||||
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||
None,
|
||||
)?;
|
||||
|
||||
let mint_quote = wallet.mint_quote(100.into(), None).await?;
|
||||
|
||||
let _mint_amount = wallet
|
||||
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||
.await?;
|
||||
|
||||
let fake_description = FakeInvoiceDescription {
|
||||
pay_invoice_state: MeltQuoteState::Failed,
|
||||
check_payment_state: MeltQuoteState::Unknown,
|
||||
pay_err: true,
|
||||
check_err: false,
|
||||
};
|
||||
|
||||
let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap());
|
||||
|
||||
let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
|
||||
|
||||
// The melt should error at the payment invoice command
|
||||
let melt = wallet.melt(&melt_quote.id).await;
|
||||
assert!(melt.is_err());
|
||||
|
||||
let fake_description = FakeInvoiceDescription {
|
||||
pay_invoice_state: MeltQuoteState::Unknown,
|
||||
check_payment_state: MeltQuoteState::Unknown,
|
||||
pay_err: true,
|
||||
check_err: false,
|
||||
};
|
||||
|
||||
let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap());
|
||||
|
||||
let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
|
||||
|
||||
// The melt should error at the payment invoice command
|
||||
let melt = wallet.melt(&melt_quote.id).await;
|
||||
assert!(melt.is_err());
|
||||
|
||||
let pending = wallet
|
||||
.localstore
|
||||
.get_proofs(None, None, Some(vec![State::Pending]), None)
|
||||
.await?;
|
||||
|
||||
assert!(pending.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// In the case that the ln backend returns an err
|
||||
// The mint should do a second check, that returns paid
|
||||
// Proofs should remain pending
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn test_fake_melt_payment_err_paid() -> Result<()> {
|
||||
let wallet = Wallet::new(
|
||||
MINT_URL,
|
||||
CurrencyUnit::Sat,
|
||||
Arc::new(WalletMemoryDatabase::default()),
|
||||
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||
None,
|
||||
)?;
|
||||
|
||||
let mint_quote = wallet.mint_quote(100.into(), None).await?;
|
||||
|
||||
let _mint_amount = wallet
|
||||
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||
.await?;
|
||||
|
||||
let fake_description = FakeInvoiceDescription {
|
||||
pay_invoice_state: MeltQuoteState::Failed,
|
||||
check_payment_state: MeltQuoteState::Paid,
|
||||
pay_err: true,
|
||||
check_err: false,
|
||||
};
|
||||
|
||||
let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap());
|
||||
|
||||
let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
|
||||
|
||||
// The melt should error at the payment invoice command
|
||||
let melt = wallet.melt(&melt_quote.id).await;
|
||||
assert!(melt.is_err());
|
||||
|
||||
attempt_to_swap_pending(&wallet).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
//! Mint tests
|
||||
|
||||
use cdk::amount::{Amount, SplitTarget};
|
||||
use cdk::dhke::construct_proofs;
|
||||
use cdk::util::unix_time;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use bip39::Mnemonic;
|
||||
use cdk::amount::{Amount, SplitTarget};
|
||||
use cdk::cdk_database::mint_memory::MintMemoryDatabase;
|
||||
use cdk::dhke::construct_proofs;
|
||||
use cdk::nuts::{
|
||||
CurrencyUnit, Id, MintBolt11Request, MintInfo, Nuts, PreMintSecrets, Proofs, SecretKey,
|
||||
SpendingConditions, SwapRequest,
|
||||
};
|
||||
use cdk::util::unix_time;
|
||||
use cdk::Mint;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
pub const MINT_URL: &str = "http://127.0.0.1:8088";
|
||||
|
||||
|
||||
@@ -19,4 +19,6 @@ futures = { version = "0.3.28", default-features = false }
|
||||
tokio = { version = "1", default-features = false }
|
||||
tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
|
||||
thiserror = "1"
|
||||
lnbits-rs = "0.2.0"
|
||||
# lnbits-rs = "0.2.0"
|
||||
# lnbits-rs = { path = "../../../../lnbits-rs" }
|
||||
lnbits-rs = { git = "https://github.com/thesimplekid/lnbits-rs.git", rev = "9fff4d" }
|
||||
|
||||
@@ -12,7 +12,7 @@ use axum::Router;
|
||||
use cdk::amount::Amount;
|
||||
use cdk::cdk_lightning::{
|
||||
self, to_unit, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse,
|
||||
Settings,
|
||||
Settings, MSAT_IN_SAT,
|
||||
};
|
||||
use cdk::mint::FeeReserve;
|
||||
use cdk::nuts::{
|
||||
@@ -189,7 +189,7 @@ impl MintLightning for LNbits {
|
||||
let total_spent = Amount::from((invoice_info.amount + invoice_info.fee).unsigned_abs());
|
||||
|
||||
Ok(PayInvoiceResponse {
|
||||
payment_hash: pay_response.payment_hash,
|
||||
payment_lookup_id: pay_response.payment_hash,
|
||||
payment_preimage: Some(invoice_info.payment_hash),
|
||||
status,
|
||||
total_spent,
|
||||
@@ -243,13 +243,13 @@ impl MintLightning for LNbits {
|
||||
})
|
||||
}
|
||||
|
||||
async fn check_invoice_status(
|
||||
async fn check_incoming_invoice_status(
|
||||
&self,
|
||||
request_lookup_id: &str,
|
||||
payment_hash: &str,
|
||||
) -> Result<MintQuoteState, Self::Err> {
|
||||
let paid = self
|
||||
.lnbits_api
|
||||
.is_invoice_paid(request_lookup_id)
|
||||
.is_invoice_paid(payment_hash)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!("Could not check invoice status");
|
||||
@@ -264,6 +264,43 @@ impl MintLightning for LNbits {
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
async fn check_outgoing_payment(
|
||||
&self,
|
||||
payment_hash: &str,
|
||||
) -> Result<PayInvoiceResponse, Self::Err> {
|
||||
let payment = self
|
||||
.lnbits_api
|
||||
.get_payment_info(payment_hash)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!("Could not check invoice status");
|
||||
tracing::error!("{}", err.to_string());
|
||||
Self::Err::Anyhow(anyhow!("Could not check invoice status"))
|
||||
})?;
|
||||
|
||||
let pay_response = PayInvoiceResponse {
|
||||
payment_lookup_id: payment.details.payment_hash,
|
||||
payment_preimage: Some(payment.preimage),
|
||||
status: lnbits_to_melt_status(&payment.details.status, payment.details.pending),
|
||||
total_spent: Amount::from(
|
||||
payment.details.amount.unsigned_abs()
|
||||
+ payment.details.fee.unsigned_abs() / MSAT_IN_SAT,
|
||||
),
|
||||
unit: self.get_settings().unit,
|
||||
};
|
||||
|
||||
Ok(pay_response)
|
||||
}
|
||||
}
|
||||
|
||||
fn lnbits_to_melt_status(status: &str, pending: bool) -> MeltQuoteState {
|
||||
match (status, pending) {
|
||||
("success", false) => MeltQuoteState::Paid,
|
||||
("failed", false) => MeltQuoteState::Unpaid,
|
||||
(_, false) => MeltQuoteState::Unknown,
|
||||
(_, true) => MeltQuoteState::Pending,
|
||||
}
|
||||
}
|
||||
|
||||
impl LNbits {
|
||||
|
||||
@@ -14,9 +14,15 @@ pub enum Error {
|
||||
/// Connection error
|
||||
#[error("LND connection error")]
|
||||
Connection,
|
||||
/// Invalid hash
|
||||
#[error("Invalid hash")]
|
||||
InvalidHash,
|
||||
/// Payment failed
|
||||
#[error("LND payment failed")]
|
||||
PaymentFailed,
|
||||
/// Unknown payment status
|
||||
#[error("LND unknown payment status")]
|
||||
UnknownPaymentStatus,
|
||||
}
|
||||
|
||||
impl From<Error> for cdk::cdk_lightning::Error {
|
||||
|
||||
@@ -26,6 +26,7 @@ use cdk::util::{hex, unix_time};
|
||||
use cdk::{mint, Bolt11Invoice};
|
||||
use error::Error;
|
||||
use fedimint_tonic_lnd::lnrpc::fee_limit::Limit;
|
||||
use fedimint_tonic_lnd::lnrpc::payment::PaymentStatus;
|
||||
use fedimint_tonic_lnd::lnrpc::FeeLimit;
|
||||
use fedimint_tonic_lnd::Client;
|
||||
use futures::{Stream, StreamExt};
|
||||
@@ -206,7 +207,7 @@ impl MintLightning for Lnd {
|
||||
};
|
||||
|
||||
Ok(PayInvoiceResponse {
|
||||
payment_hash: hex::encode(payment_response.payment_hash),
|
||||
payment_lookup_id: hex::encode(payment_response.payment_hash),
|
||||
payment_preimage,
|
||||
status,
|
||||
total_spent: total_amount.into(),
|
||||
@@ -251,7 +252,7 @@ impl MintLightning for Lnd {
|
||||
})
|
||||
}
|
||||
|
||||
async fn check_invoice_status(
|
||||
async fn check_incoming_invoice_status(
|
||||
&self,
|
||||
request_lookup_id: &str,
|
||||
) -> Result<MintQuoteState, Self::Err> {
|
||||
@@ -282,4 +283,68 @@ impl MintLightning for Lnd {
|
||||
_ => Err(Self::Err::Anyhow(anyhow!("Invalid status"))),
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_outgoing_payment(
|
||||
&self,
|
||||
payment_hash: &str,
|
||||
) -> Result<PayInvoiceResponse, Self::Err> {
|
||||
let track_request = fedimint_tonic_lnd::routerrpc::TrackPaymentRequest {
|
||||
payment_hash: hex::decode(payment_hash).map_err(|_| Error::InvalidHash)?,
|
||||
no_inflight_updates: true,
|
||||
};
|
||||
let mut payment_stream = self
|
||||
.client
|
||||
.lock()
|
||||
.await
|
||||
.router()
|
||||
.track_payment_v2(track_request)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_inner();
|
||||
|
||||
while let Some(update_result) = payment_stream.next().await {
|
||||
match update_result {
|
||||
Ok(update) => {
|
||||
let status = update.status();
|
||||
|
||||
let response = match status {
|
||||
PaymentStatus::Unknown => PayInvoiceResponse {
|
||||
payment_lookup_id: payment_hash.to_string(),
|
||||
payment_preimage: Some(update.payment_preimage),
|
||||
status: MeltQuoteState::Unknown,
|
||||
total_spent: Amount::ZERO,
|
||||
unit: self.get_settings().unit,
|
||||
},
|
||||
PaymentStatus::InFlight => {
|
||||
// Continue waiting for the next update
|
||||
continue;
|
||||
}
|
||||
PaymentStatus::Succeeded => PayInvoiceResponse {
|
||||
payment_lookup_id: payment_hash.to_string(),
|
||||
payment_preimage: Some(update.payment_preimage),
|
||||
status: MeltQuoteState::Paid,
|
||||
total_spent: Amount::from((update.value_sat + update.fee_sat) as u64),
|
||||
unit: CurrencyUnit::Sat,
|
||||
},
|
||||
PaymentStatus::Failed => PayInvoiceResponse {
|
||||
payment_lookup_id: payment_hash.to_string(),
|
||||
payment_preimage: Some(update.payment_preimage),
|
||||
status: MeltQuoteState::Failed,
|
||||
total_spent: Amount::ZERO,
|
||||
unit: self.get_settings().unit,
|
||||
},
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
Err(_) => {
|
||||
// Handle the case where the update itself is an error (e.g., stream failure)
|
||||
return Err(Error::UnknownPaymentStatus.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the stream is exhausted without a final status
|
||||
Err(Error::UnknownPaymentStatus.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#![warn(missing_docs)]
|
||||
#![warn(rustdoc::bare_urls)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
@@ -14,13 +14,13 @@ use bip39::Mnemonic;
|
||||
use cdk::cdk_database::{self, MintDatabase};
|
||||
use cdk::cdk_lightning;
|
||||
use cdk::cdk_lightning::MintLightning;
|
||||
use cdk::mint::{FeeReserve, Mint};
|
||||
use cdk::mint::{FeeReserve, MeltQuote, Mint};
|
||||
use cdk::mint_url::MintUrl;
|
||||
use cdk::nuts::{
|
||||
nut04, nut05, ContactInfo, CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings,
|
||||
MintVersion, MppMethodSettings, Nuts, PaymentMethod,
|
||||
nut04, nut05, ContactInfo, CurrencyUnit, MeltMethodSettings, MeltQuoteState, MintInfo,
|
||||
MintMethodSettings, MintVersion, MppMethodSettings, Nuts, PaymentMethod,
|
||||
};
|
||||
use cdk_axum::LnKey;
|
||||
use cdk::types::LnKey;
|
||||
use cdk_cln::Cln;
|
||||
use cdk_fake_wallet::FakeWallet;
|
||||
use cdk_lnbits::LNbits;
|
||||
@@ -329,6 +329,9 @@ async fn main() -> anyhow::Result<()> {
|
||||
fee_reserve.clone(),
|
||||
MintMethodSettings::default(),
|
||||
MeltMethodSettings::default(),
|
||||
HashMap::default(),
|
||||
HashSet::default(),
|
||||
0,
|
||||
));
|
||||
|
||||
ln_backends.insert(ln_key, wallet);
|
||||
@@ -438,9 +441,14 @@ async fn main() -> anyhow::Result<()> {
|
||||
// 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
|
||||
for ln in ln_backends.values() {
|
||||
check_pending_quotes(Arc::clone(&mint), Arc::clone(ln)).await?;
|
||||
check_pending_mint_quotes(Arc::clone(&mint), Arc::clone(ln)).await?;
|
||||
}
|
||||
|
||||
// Checks the status of all pending melt quotes
|
||||
// Pending melt quotes where the payment has gone through inputs are burnt
|
||||
// Pending melt quotes where the paynment has **failed** inputs are reset to unspent
|
||||
check_pending_melt_quotes(Arc::clone(&mint), &ln_backends).await?;
|
||||
|
||||
let mint_url = settings.info.url;
|
||||
let listen_addr = settings.info.listen_host;
|
||||
let listen_port = settings.info.listen_port;
|
||||
@@ -462,7 +470,6 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
// Spawn task to wait for invoces to be paid and update mint quotes
|
||||
|
||||
for (_, ln) in ln_backends {
|
||||
let mint = Arc::clone(&mint);
|
||||
tokio::spawn(async move {
|
||||
@@ -505,7 +512,7 @@ async fn handle_paid_invoice(mint: Arc<Mint>, request_lookup_id: &str) -> Result
|
||||
}
|
||||
|
||||
/// Used on mint start up to check status of all pending mint quotes
|
||||
async fn check_pending_quotes(
|
||||
async fn check_pending_mint_quotes(
|
||||
mint: Arc<Mint>,
|
||||
ln: Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>,
|
||||
) -> Result<()> {
|
||||
@@ -519,10 +526,10 @@ async fn check_pending_quotes(
|
||||
for quote in unpaid_quotes {
|
||||
tracing::trace!("Checking status of mint quote: {}", quote.id);
|
||||
let lookup_id = quote.request_lookup_id;
|
||||
match ln.check_invoice_status(&lookup_id).await {
|
||||
match ln.check_incoming_invoice_status(&lookup_id).await {
|
||||
Ok(state) => {
|
||||
if state != quote.state {
|
||||
tracing::trace!("Mintquote status changed: {}", quote.id);
|
||||
tracing::trace!("Mint quote status changed: {}", quote.id);
|
||||
mint.localstore
|
||||
.update_mint_quote_state("e.id, state)
|
||||
.await?;
|
||||
@@ -539,6 +546,93 @@ async fn check_pending_quotes(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_pending_melt_quotes(
|
||||
mint: Arc<Mint>,
|
||||
ln_backends: &HashMap<LnKey, Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>>,
|
||||
) -> Result<()> {
|
||||
let melt_quotes = mint.localstore.get_melt_quotes().await?;
|
||||
let pending_quotes: Vec<MeltQuote> = melt_quotes
|
||||
.into_iter()
|
||||
.filter(|q| q.state == MeltQuoteState::Pending || q.state == MeltQuoteState::Unknown)
|
||||
.collect();
|
||||
|
||||
for pending_quote in pending_quotes {
|
||||
let melt_request_ln_key = mint.localstore.get_melt_request(&pending_quote.id).await?;
|
||||
|
||||
let (melt_request, ln_key) = match melt_request_ln_key {
|
||||
None => (
|
||||
None,
|
||||
LnKey {
|
||||
unit: pending_quote.unit,
|
||||
method: PaymentMethod::Bolt11,
|
||||
},
|
||||
),
|
||||
Some((melt_request, ln_key)) => (Some(melt_request), ln_key),
|
||||
};
|
||||
|
||||
let ln_backend = match ln_backends.get(&ln_key) {
|
||||
Some(ln_backend) => ln_backend,
|
||||
None => {
|
||||
tracing::warn!("No backend for ln key: {:?}", ln_key);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let pay_invoice_response = ln_backend
|
||||
.check_outgoing_payment(&pending_quote.request_lookup_id)
|
||||
.await?;
|
||||
|
||||
match melt_request {
|
||||
Some(melt_request) => {
|
||||
match pay_invoice_response.status {
|
||||
MeltQuoteState::Paid => {
|
||||
if let Err(err) = mint
|
||||
.process_melt_request(
|
||||
&melt_request,
|
||||
pay_invoice_response.payment_preimage,
|
||||
pay_invoice_response.total_spent,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Could not process melt request for pending quote: {}",
|
||||
melt_request.quote
|
||||
);
|
||||
tracing::error!("{}", err);
|
||||
}
|
||||
}
|
||||
MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => {
|
||||
// Payment has not been made we want to unset
|
||||
tracing::info!("Lightning payment for quote {} failed.", pending_quote.id);
|
||||
if let Err(err) = mint.process_unpaid_melt(&melt_request).await {
|
||||
tracing::error!("Could not reset melt quote state: {}", err);
|
||||
}
|
||||
}
|
||||
MeltQuoteState::Pending => {
|
||||
tracing::warn!(
|
||||
"LN payment pending, proofs are stuck as pending for quote: {}",
|
||||
melt_request.quote
|
||||
);
|
||||
// Quote is still pending we do not want to do anything
|
||||
// continue to check next quote
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
tracing::warn!(
|
||||
"There is no stored melt request for pending melt quote: {}",
|
||||
pending_quote.id
|
||||
);
|
||||
|
||||
mint.localstore
|
||||
.update_melt_quote_state(&pending_quote.id, pay_invoice_response.status)
|
||||
.await?;
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn expand_path(path: &str) -> Option<PathBuf> {
|
||||
if path.starts_with('~') {
|
||||
if let Some(home_dir) = home::home_dir().as_mut() {
|
||||
|
||||
@@ -19,5 +19,6 @@ futures = { version = "0.3.28", default-features = false }
|
||||
tokio = { version = "1", default-features = false }
|
||||
tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
|
||||
thiserror = "1"
|
||||
phoenixd-rs = "0.3.0"
|
||||
# phoenixd-rs = "0.3.0"
|
||||
phoenixd-rs = { git = "https://github.com/thesimplekid/phoenixd-rs", rev = "22a44f0"}
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
@@ -14,6 +14,9 @@ pub enum Error {
|
||||
/// Unsupported unit
|
||||
#[error("Unit Unsupported")]
|
||||
UnsupportedUnit,
|
||||
/// phd error
|
||||
#[error(transparent)]
|
||||
Phd(#[from] phoenixd_rs::Error),
|
||||
/// Anyhow error
|
||||
#[error(transparent)]
|
||||
Anyhow(#[from] anyhow::Error),
|
||||
|
||||
@@ -175,24 +175,18 @@ impl MintLightning for Phoenixd {
|
||||
.pay_bolt11_invoice(&melt_quote.request, partial_amount.map(|a| a.into()))
|
||||
.await?;
|
||||
|
||||
// The pay response does not include the fee paided to Aciq so we check it here
|
||||
// The pay invoice response does not give the needed fee info so we have to check.
|
||||
let check_outgoing_response = self
|
||||
.check_outgoing_invoice(&pay_response.payment_id)
|
||||
.check_outgoing_payment(&pay_response.payment_id)
|
||||
.await?;
|
||||
|
||||
if check_outgoing_response.state != MeltQuoteState::Paid {
|
||||
return Err(anyhow!("Invoice is not paid").into());
|
||||
}
|
||||
|
||||
let total_spent_sats = check_outgoing_response.fee + check_outgoing_response.amount;
|
||||
|
||||
let bolt11: Bolt11Invoice = melt_quote.request.parse()?;
|
||||
|
||||
Ok(PayInvoiceResponse {
|
||||
payment_hash: bolt11.payment_hash().to_string(),
|
||||
payment_lookup_id: bolt11.payment_hash().to_string(),
|
||||
payment_preimage: Some(pay_response.payment_preimage),
|
||||
status: MeltQuoteState::Paid,
|
||||
total_spent: total_spent_sats,
|
||||
total_spent: check_outgoing_response.total_spent,
|
||||
unit: CurrencyUnit::Sat,
|
||||
})
|
||||
}
|
||||
@@ -226,7 +220,10 @@ impl MintLightning for Phoenixd {
|
||||
})
|
||||
}
|
||||
|
||||
async fn check_invoice_status(&self, payment_hash: &str) -> Result<MintQuoteState, Self::Err> {
|
||||
async fn check_incoming_invoice_status(
|
||||
&self,
|
||||
payment_hash: &str,
|
||||
) -> Result<MintQuoteState, Self::Err> {
|
||||
let invoice = self.phoenixd_api.get_incoming_invoice(payment_hash).await?;
|
||||
|
||||
let state = match invoice.is_paid {
|
||||
@@ -236,33 +233,45 @@ impl MintLightning for Phoenixd {
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl Phoenixd {
|
||||
/// Check the status of an outgooing invoice
|
||||
// TODO: This should likely bee added to the trait. Both CLN and PhD use a form
|
||||
// of it
|
||||
async fn check_outgoing_invoice(
|
||||
/// Check the status of an outgoing invoice
|
||||
async fn check_outgoing_payment(
|
||||
&self,
|
||||
payment_hash: &str,
|
||||
) -> Result<PaymentQuoteResponse, Error> {
|
||||
let res = self.phoenixd_api.get_outgoing_invoice(payment_hash).await?;
|
||||
payment_id: &str,
|
||||
) -> Result<PayInvoiceResponse, Self::Err> {
|
||||
let res = self.phoenixd_api.get_outgoing_invoice(payment_id).await;
|
||||
|
||||
// Phenixd gives fees in msats so we need to round up to the nearst sat
|
||||
let fee_sats = (res.fees + 999) / MSAT_IN_SAT;
|
||||
|
||||
let state = match res.is_paid {
|
||||
let state = match res {
|
||||
Ok(res) => {
|
||||
let status = match res.is_paid {
|
||||
true => MeltQuoteState::Paid,
|
||||
false => MeltQuoteState::Unpaid,
|
||||
};
|
||||
|
||||
let quote_response = PaymentQuoteResponse {
|
||||
request_lookup_id: res.payment_hash,
|
||||
amount: res.sent.into(),
|
||||
fee: fee_sats.into(),
|
||||
state,
|
||||
let total_spent = res.sent + (res.fees + 999) / MSAT_IN_SAT;
|
||||
|
||||
PayInvoiceResponse {
|
||||
payment_lookup_id: res.payment_hash,
|
||||
payment_preimage: Some(res.preimage),
|
||||
status,
|
||||
total_spent: total_spent.into(),
|
||||
unit: CurrencyUnit::Sat,
|
||||
}
|
||||
}
|
||||
Err(err) => match err {
|
||||
phoenixd_rs::Error::NotFound => PayInvoiceResponse {
|
||||
payment_lookup_id: payment_id.to_string(),
|
||||
payment_preimage: None,
|
||||
status: MeltQuoteState::Unknown,
|
||||
total_spent: Amount::ZERO,
|
||||
unit: CurrencyUnit::Sat,
|
||||
},
|
||||
_ => {
|
||||
return Err(Error::from(err).into());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Ok(quote_response)
|
||||
Ok(state)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,10 @@ use cdk::cdk_database::MintDatabase;
|
||||
use cdk::dhke::hash_to_curve;
|
||||
use cdk::mint::{MintKeySetInfo, MintQuote};
|
||||
use cdk::nuts::{
|
||||
BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, Proofs, PublicKey,
|
||||
State,
|
||||
BlindSignature, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintQuoteState, Proof,
|
||||
Proofs, PublicKey, State,
|
||||
};
|
||||
use cdk::types::LnKey;
|
||||
use cdk::{cdk_database, mint};
|
||||
use migrations::migrate_01_to_02;
|
||||
use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition};
|
||||
@@ -39,6 +40,8 @@ const QUOTE_PROOFS_TABLE: MultimapTableDefinition<&str, [u8; 33]> =
|
||||
const QUOTE_SIGNATURES_TABLE: MultimapTableDefinition<&str, &str> =
|
||||
MultimapTableDefinition::new("quote_signatures");
|
||||
|
||||
const MELT_REQUESTS: TableDefinition<&str, (&str, &str)> = TableDefinition::new("melt_requests");
|
||||
|
||||
const DATABASE_VERSION: u32 = 4;
|
||||
|
||||
/// Mint Redbdatabase
|
||||
@@ -735,4 +738,45 @@ impl MintDatabase for MintRedbDatabase {
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Add melt request
|
||||
async fn add_melt_request(
|
||||
&self,
|
||||
melt_request: MeltBolt11Request,
|
||||
ln_key: LnKey,
|
||||
) -> Result<(), Self::Err> {
|
||||
let write_txn = self.db.begin_write().map_err(Error::from)?;
|
||||
let mut table = write_txn.open_table(MELT_REQUESTS).map_err(Error::from)?;
|
||||
|
||||
table
|
||||
.insert(
|
||||
melt_request.quote.as_str(),
|
||||
(
|
||||
serde_json::to_string(&melt_request)?.as_str(),
|
||||
serde_json::to_string(&ln_key)?.as_str(),
|
||||
),
|
||||
)
|
||||
.map_err(Error::from)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
/// Get melt request
|
||||
async fn get_melt_request(
|
||||
&self,
|
||||
quote_id: &str,
|
||||
) -> Result<Option<(MeltBolt11Request, LnKey)>, Self::Err> {
|
||||
let read_txn = self.db.begin_read().map_err(Error::from)?;
|
||||
let table = read_txn.open_table(MELT_REQUESTS).map_err(Error::from)?;
|
||||
|
||||
match table.get(quote_id).map_err(Error::from)? {
|
||||
Some(melt_request) => {
|
||||
let (melt_request_str, ln_key_str) = melt_request.value();
|
||||
let melt_request = serde_json::from_str(melt_request_str)?;
|
||||
let ln_key = serde_json::from_str(ln_key_str)?;
|
||||
|
||||
Ok(Some((melt_request, ln_key)))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,9 @@ pub enum Error {
|
||||
/// Invalid Database Path
|
||||
#[error("Invalid database path")]
|
||||
InvalidDbPath,
|
||||
/// Serde Error
|
||||
#[error(transparent)]
|
||||
Serde(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
impl From<Error> for cdk::cdk_database::Error {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Melt Request Table
|
||||
CREATE TABLE IF NOT EXISTS melt_request (
|
||||
id TEXT PRIMARY KEY,
|
||||
inputs TEXT NOT NULL,
|
||||
outputs TEXT,
|
||||
method TEXT NOT NULL,
|
||||
unit TEXT NOT NULL
|
||||
);
|
||||
@@ -12,10 +12,11 @@ use cdk::mint::{MintKeySetInfo, MintQuote};
|
||||
use cdk::mint_url::MintUrl;
|
||||
use cdk::nuts::nut05::QuoteState;
|
||||
use cdk::nuts::{
|
||||
BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, Proofs, PublicKey,
|
||||
State,
|
||||
BlindSignature, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintQuoteState,
|
||||
PaymentMethod, Proof, Proofs, PublicKey, State,
|
||||
};
|
||||
use cdk::secret::Secret;
|
||||
use cdk::types::LnKey;
|
||||
use cdk::{mint, Amount};
|
||||
use error::Error;
|
||||
use lightning_invoice::Bolt11Invoice;
|
||||
@@ -1121,6 +1122,86 @@ WHERE keyset_id=?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_melt_request(
|
||||
&self,
|
||||
melt_request: MeltBolt11Request,
|
||||
ln_key: LnKey,
|
||||
) -> Result<(), Self::Err> {
|
||||
let mut transaction = self.pool.begin().await.map_err(Error::from)?;
|
||||
|
||||
let res = sqlx::query(
|
||||
r#"
|
||||
INSERT OR REPLACE INTO melt_request
|
||||
(id, inputs, outputs, method, unit)
|
||||
VALUES (?, ?, ?, ?, ?);
|
||||
"#,
|
||||
)
|
||||
.bind(melt_request.quote)
|
||||
.bind(serde_json::to_string(&melt_request.inputs)?)
|
||||
.bind(serde_json::to_string(&melt_request.outputs)?)
|
||||
.bind(ln_key.method.to_string())
|
||||
.bind(ln_key.unit.to_string())
|
||||
.execute(&mut transaction)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
transaction.commit().await.map_err(Error::from)?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("SQLite Could not update keyset");
|
||||
if let Err(err) = transaction.rollback().await {
|
||||
tracing::error!("Could not rollback sql transaction: {}", err);
|
||||
}
|
||||
|
||||
Err(Error::from(err).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_melt_request(
|
||||
&self,
|
||||
quote_id: &str,
|
||||
) -> Result<Option<(MeltBolt11Request, LnKey)>, Self::Err> {
|
||||
let mut transaction = self.pool.begin().await.map_err(Error::from)?;
|
||||
|
||||
let rec = sqlx::query(
|
||||
r#"
|
||||
SELECT *
|
||||
FROM melt_request
|
||||
WHERE id=?;
|
||||
"#,
|
||||
)
|
||||
.bind(quote_id)
|
||||
.fetch_one(&mut transaction)
|
||||
.await;
|
||||
|
||||
match rec {
|
||||
Ok(rec) => {
|
||||
transaction.commit().await.map_err(Error::from)?;
|
||||
|
||||
let (request, key) = sqlite_row_to_melt_request(rec)?;
|
||||
|
||||
Ok(Some((request, key)))
|
||||
}
|
||||
Err(err) => match err {
|
||||
sqlx::Error::RowNotFound => {
|
||||
transaction.commit().await.map_err(Error::from)?;
|
||||
return Ok(None);
|
||||
}
|
||||
_ => {
|
||||
return {
|
||||
if let Err(err) = transaction.rollback().await {
|
||||
tracing::error!("Could not rollback sql transaction: {}", err);
|
||||
}
|
||||
Err(Error::SQLX(err).into())
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sqlite_row_to_keyset_info(row: SqliteRow) -> Result<MintKeySetInfo, Error> {
|
||||
@@ -1259,3 +1340,24 @@ fn sqlite_row_to_blind_signature(row: SqliteRow) -> Result<BlindSignature, Error
|
||||
dleq: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn sqlite_row_to_melt_request(row: SqliteRow) -> Result<(MeltBolt11Request, LnKey), Error> {
|
||||
let quote_id: String = row.try_get("id").map_err(Error::from)?;
|
||||
let row_inputs: String = row.try_get("inputs").map_err(Error::from)?;
|
||||
let row_outputs: Option<String> = row.try_get("outputs").map_err(Error::from)?;
|
||||
let row_method: String = row.try_get("method").map_err(Error::from)?;
|
||||
let row_unit: String = row.try_get("unit").map_err(Error::from)?;
|
||||
|
||||
let melt_request = MeltBolt11Request {
|
||||
quote: quote_id,
|
||||
inputs: serde_json::from_str(&row_inputs)?,
|
||||
outputs: row_outputs.and_then(|o| serde_json::from_str(&o).ok()),
|
||||
};
|
||||
|
||||
let ln_key = LnKey {
|
||||
unit: CurrencyUnit::from_str(&row_unit)?,
|
||||
method: PaymentMethod::from_str(&row_method)?,
|
||||
};
|
||||
|
||||
Ok((melt_request, ln_key))
|
||||
}
|
||||
|
||||
@@ -20,4 +20,6 @@ tokio = { version = "1", default-features = false }
|
||||
tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
|
||||
thiserror = "1"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
strike-rs = "0.3.0"
|
||||
# strike-rs = "0.3.0"
|
||||
# strike-rs = { path = "../../../../strike-rs" }
|
||||
strike-rs = { git = "https://github.com/thesimplekid/strike-rs.git", rev = "c6167ce" }
|
||||
|
||||
@@ -11,6 +11,9 @@ pub enum Error {
|
||||
/// Unknown invoice
|
||||
#[error("Unknown invoice")]
|
||||
UnknownInvoice,
|
||||
/// Strikers error
|
||||
#[error(transparent)]
|
||||
StrikeRs(#[from] strike_rs::Error),
|
||||
/// Anyhow error
|
||||
#[error(transparent)]
|
||||
Anyhow(#[from] anyhow::Error),
|
||||
|
||||
@@ -99,7 +99,7 @@ impl MintLightning for Strike {
|
||||
|(mut receiver, strike_api)| async move {
|
||||
match receiver.recv().await {
|
||||
Some(msg) => {
|
||||
let check = strike_api.find_invoice(&msg).await;
|
||||
let check = strike_api.get_incoming_invoice(&msg).await;
|
||||
|
||||
match check {
|
||||
Ok(state) => {
|
||||
@@ -172,10 +172,8 @@ impl MintLightning for Strike {
|
||||
|
||||
let total_spent = from_strike_amount(pay_response.total_amount, &melt_quote.unit)?.into();
|
||||
|
||||
let bolt11: Bolt11Invoice = melt_quote.request.parse()?;
|
||||
|
||||
Ok(PayInvoiceResponse {
|
||||
payment_hash: bolt11.payment_hash().to_string(),
|
||||
payment_lookup_id: pay_response.payment_id,
|
||||
payment_preimage: None,
|
||||
status: state,
|
||||
total_spent,
|
||||
@@ -217,11 +215,14 @@ impl MintLightning for Strike {
|
||||
})
|
||||
}
|
||||
|
||||
async fn check_invoice_status(
|
||||
async fn check_incoming_invoice_status(
|
||||
&self,
|
||||
request_lookup_id: &str,
|
||||
) -> Result<MintQuoteState, Self::Err> {
|
||||
let invoice = self.strike_api.find_invoice(request_lookup_id).await?;
|
||||
let invoice = self
|
||||
.strike_api
|
||||
.get_incoming_invoice(request_lookup_id)
|
||||
.await?;
|
||||
|
||||
let state = match invoice.state {
|
||||
InvoiceState::Paid => MintQuoteState::Paid,
|
||||
@@ -232,6 +233,46 @@ impl MintLightning for Strike {
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
async fn check_outgoing_payment(
|
||||
&self,
|
||||
payment_id: &str,
|
||||
) -> Result<PayInvoiceResponse, Self::Err> {
|
||||
let invoice = self.strike_api.get_outgoing_payment(payment_id).await;
|
||||
|
||||
let pay_invoice_response = match invoice {
|
||||
Ok(invoice) => {
|
||||
let state = match invoice.state {
|
||||
InvoiceState::Paid => MeltQuoteState::Paid,
|
||||
InvoiceState::Unpaid => MeltQuoteState::Unpaid,
|
||||
InvoiceState::Completed => MeltQuoteState::Paid,
|
||||
InvoiceState::Pending => MeltQuoteState::Pending,
|
||||
};
|
||||
|
||||
PayInvoiceResponse {
|
||||
payment_lookup_id: invoice.payment_id,
|
||||
payment_preimage: None,
|
||||
status: state,
|
||||
total_spent: from_strike_amount(invoice.total_amount, &self.unit)?.into(),
|
||||
unit: self.unit,
|
||||
}
|
||||
}
|
||||
Err(err) => match err {
|
||||
strike_rs::Error::NotFound => PayInvoiceResponse {
|
||||
payment_lookup_id: payment_id.to_string(),
|
||||
payment_preimage: None,
|
||||
status: MeltQuoteState::Unknown,
|
||||
total_spent: Amount::ZERO,
|
||||
unit: self.unit,
|
||||
},
|
||||
_ => {
|
||||
return Err(Error::from(err).into());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Ok(pay_invoice_response)
|
||||
}
|
||||
}
|
||||
|
||||
impl Strike {
|
||||
|
||||
@@ -11,9 +11,10 @@ use crate::dhke::hash_to_curve;
|
||||
use crate::mint::{self, MintKeySetInfo, MintQuote};
|
||||
use crate::nuts::nut07::State;
|
||||
use crate::nuts::{
|
||||
nut07, BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, Proofs,
|
||||
PublicKey,
|
||||
nut07, BlindSignature, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintQuoteState,
|
||||
Proof, Proofs, PublicKey,
|
||||
};
|
||||
use crate::types::LnKey;
|
||||
|
||||
/// Mint Memory Database
|
||||
#[derive(Debug, Clone, Default)]
|
||||
@@ -27,6 +28,7 @@ pub struct MintMemoryDatabase {
|
||||
quote_proofs: Arc<Mutex<HashMap<String, Vec<PublicKey>>>>,
|
||||
blinded_signatures: Arc<RwLock<HashMap<[u8; 33], BlindSignature>>>,
|
||||
quote_signatures: Arc<RwLock<HashMap<String, Vec<BlindSignature>>>>,
|
||||
melt_requests: Arc<RwLock<HashMap<String, (MeltBolt11Request, LnKey)>>>,
|
||||
}
|
||||
|
||||
impl MintMemoryDatabase {
|
||||
@@ -42,6 +44,7 @@ impl MintMemoryDatabase {
|
||||
quote_proofs: HashMap<String, Vec<PublicKey>>,
|
||||
blinded_signatures: HashMap<[u8; 33], BlindSignature>,
|
||||
quote_signatures: HashMap<String, Vec<BlindSignature>>,
|
||||
melt_request: Vec<(MeltBolt11Request, LnKey)>,
|
||||
) -> Result<Self, Error> {
|
||||
let mut proofs = HashMap::new();
|
||||
let mut proof_states = HashMap::new();
|
||||
@@ -58,6 +61,11 @@ impl MintMemoryDatabase {
|
||||
proof_states.insert(y, State::Spent);
|
||||
}
|
||||
|
||||
let melt_requests = melt_request
|
||||
.into_iter()
|
||||
.map(|(request, ln_key)| (request.quote.clone(), (request, ln_key)))
|
||||
.collect();
|
||||
|
||||
Ok(Self {
|
||||
active_keysets: Arc::new(RwLock::new(active_keysets)),
|
||||
keysets: Arc::new(RwLock::new(
|
||||
@@ -74,6 +82,7 @@ impl MintMemoryDatabase {
|
||||
blinded_signatures: Arc::new(RwLock::new(blinded_signatures)),
|
||||
quote_proofs: Arc::new(Mutex::new(quote_proofs)),
|
||||
quote_signatures: Arc::new(RwLock::new(quote_signatures)),
|
||||
melt_requests: Arc::new(RwLock::new(melt_requests)),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -225,6 +234,27 @@ impl MintDatabase for MintMemoryDatabase {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_melt_request(
|
||||
&self,
|
||||
melt_request: MeltBolt11Request,
|
||||
ln_key: LnKey,
|
||||
) -> Result<(), Self::Err> {
|
||||
let mut melt_requests = self.melt_requests.write().await;
|
||||
melt_requests.insert(melt_request.quote.clone(), (melt_request, ln_key));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_melt_request(
|
||||
&self,
|
||||
quote_id: &str,
|
||||
) -> Result<Option<(MeltBolt11Request, LnKey)>, Self::Err> {
|
||||
let melt_requests = self.melt_requests.read().await;
|
||||
|
||||
let melt_request = melt_requests.get(quote_id);
|
||||
|
||||
Ok(melt_request.cloned())
|
||||
}
|
||||
|
||||
async fn add_proofs(&self, proofs: Proofs, quote_id: Option<String>) -> Result<(), Self::Err> {
|
||||
let mut db_proofs = self.proofs.write().await;
|
||||
|
||||
|
||||
@@ -17,11 +17,15 @@ use crate::mint::MintQuote as MintMintQuote;
|
||||
#[cfg(feature = "wallet")]
|
||||
use crate::mint_url::MintUrl;
|
||||
#[cfg(feature = "mint")]
|
||||
use crate::nuts::MeltBolt11Request;
|
||||
#[cfg(feature = "mint")]
|
||||
use crate::nuts::{BlindSignature, MeltQuoteState, MintQuoteState, Proof, Proofs};
|
||||
#[cfg(any(feature = "wallet", feature = "mint"))]
|
||||
use crate::nuts::{CurrencyUnit, Id, PublicKey, State};
|
||||
#[cfg(feature = "wallet")]
|
||||
use crate::nuts::{KeySetInfo, Keys, MintInfo, SpendingConditions};
|
||||
#[cfg(feature = "mint")]
|
||||
use crate::types::LnKey;
|
||||
#[cfg(feature = "wallet")]
|
||||
use crate::types::ProofInfo;
|
||||
#[cfg(feature = "wallet")]
|
||||
@@ -220,6 +224,18 @@ pub trait MintDatabase {
|
||||
/// Remove [`mint::MeltQuote`]
|
||||
async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
|
||||
|
||||
/// Add melt request
|
||||
async fn add_melt_request(
|
||||
&self,
|
||||
melt_request: MeltBolt11Request,
|
||||
ln_key: LnKey,
|
||||
) -> Result<(), Self::Err>;
|
||||
/// Get melt request
|
||||
async fn get_melt_request(
|
||||
&self,
|
||||
quote_id: &str,
|
||||
) -> Result<Option<(MeltBolt11Request, LnKey)>, Self::Err>;
|
||||
|
||||
/// Add [`MintKeySetInfo`]
|
||||
async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err>;
|
||||
/// Get [`MintKeySetInfo`]
|
||||
|
||||
@@ -26,6 +26,9 @@ pub enum Error {
|
||||
/// Unsupported unit
|
||||
#[error("Unsupported unit")]
|
||||
UnsupportedUnit,
|
||||
/// Payment state is unknown
|
||||
#[error("Payment state is unknown")]
|
||||
UnknownPaymentState,
|
||||
/// Lightning Error
|
||||
#[error(transparent)]
|
||||
Lightning(Box<dyn std::error::Error + Send + Sync>),
|
||||
@@ -83,10 +86,16 @@ pub trait MintLightning {
|
||||
) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err>;
|
||||
|
||||
/// Check the status of an incoming payment
|
||||
async fn check_invoice_status(
|
||||
async fn check_incoming_invoice_status(
|
||||
&self,
|
||||
request_lookup_id: &str,
|
||||
) -> Result<MintQuoteState, Self::Err>;
|
||||
|
||||
/// Check the status of an outgoing payment
|
||||
async fn check_outgoing_payment(
|
||||
&self,
|
||||
request_lookup_id: &str,
|
||||
) -> Result<PayInvoiceResponse, Self::Err>;
|
||||
}
|
||||
|
||||
/// Create invoice response
|
||||
@@ -104,7 +113,7 @@ pub struct CreateInvoiceResponse {
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PayInvoiceResponse {
|
||||
/// Payment hash
|
||||
pub payment_hash: String,
|
||||
pub payment_lookup_id: String,
|
||||
/// Payment Preimage
|
||||
pub payment_preimage: Option<String>,
|
||||
/// Status
|
||||
|
||||
@@ -25,6 +25,9 @@ pub enum Error {
|
||||
/// Payment failed
|
||||
#[error("Payment failed")]
|
||||
PaymentFailed,
|
||||
/// Payment pending
|
||||
#[error("Payment pending")]
|
||||
PaymentPending,
|
||||
/// Invoice already paid
|
||||
#[error("Request already paid")]
|
||||
RequestAlreadyPaid,
|
||||
@@ -66,6 +69,9 @@ pub enum Error {
|
||||
/// Quote has already been paid
|
||||
#[error("Quote is already paid")]
|
||||
PaidQuote,
|
||||
/// Payment state is unknown
|
||||
#[error("Payment state is unknown")]
|
||||
UnknownPaymentState,
|
||||
/// Melting is disabled
|
||||
#[error("Minting is disabled")]
|
||||
MeltingDisabled,
|
||||
@@ -352,6 +358,11 @@ impl From<Error> for ErrorResponse {
|
||||
error: Some(err.to_string()),
|
||||
detail: None,
|
||||
},
|
||||
Error::TokenPending => ErrorResponse {
|
||||
code: ErrorCode::TokenPending,
|
||||
error: Some(err.to_string()),
|
||||
detail: None,
|
||||
},
|
||||
_ => ErrorResponse {
|
||||
code: ErrorCode::Unknown(9999),
|
||||
error: Some(err.to_string()),
|
||||
@@ -380,6 +391,7 @@ impl From<ErrorResponse> for Error {
|
||||
ErrorCode::AmountOutofLimitRange => {
|
||||
Self::AmountOutofLimitRange(Amount::default(), Amount::default(), Amount::default())
|
||||
}
|
||||
ErrorCode::TokenPending => Self::TokenPending,
|
||||
_ => Self::UnknownErrorResponse(err.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -390,6 +402,8 @@ impl From<ErrorResponse> for Error {
|
||||
pub enum ErrorCode {
|
||||
/// Token is already spent
|
||||
TokenAlreadySpent,
|
||||
/// Token Pending
|
||||
TokenPending,
|
||||
/// Quote is not paid
|
||||
QuoteNotPaid,
|
||||
/// Quote is not expired
|
||||
@@ -432,6 +446,7 @@ impl ErrorCode {
|
||||
11002 => Self::TransactionUnbalanced,
|
||||
11005 => Self::UnitUnsupported,
|
||||
11006 => Self::AmountOutofLimitRange,
|
||||
11007 => Self::TokenPending,
|
||||
12001 => Self::KeysetNotFound,
|
||||
12002 => Self::KeysetInactive,
|
||||
20000 => Self::LightningError,
|
||||
@@ -454,6 +469,7 @@ impl ErrorCode {
|
||||
Self::TransactionUnbalanced => 11002,
|
||||
Self::UnitUnsupported => 11005,
|
||||
Self::AmountOutofLimitRange => 11006,
|
||||
Self::TokenPending => 11007,
|
||||
Self::KeysetNotFound => 12001,
|
||||
Self::KeysetInactive => 12002,
|
||||
Self::LightningError => 20000,
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::sync::Arc;
|
||||
|
||||
use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv};
|
||||
use bitcoin::secp256k1::{self, Secp256k1};
|
||||
use lightning_invoice::Bolt11Invoice;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::instrument;
|
||||
@@ -13,6 +14,7 @@ use tracing::instrument;
|
||||
use self::nut05::QuoteState;
|
||||
use self::nut11::EnforceSigFlag;
|
||||
use crate::cdk_database::{self, MintDatabase};
|
||||
use crate::cdk_lightning::to_unit;
|
||||
use crate::dhke::{hash_to_curve, sign_message, verify_message};
|
||||
use crate::error::Error;
|
||||
use crate::fees::calculate_fee;
|
||||
@@ -993,6 +995,117 @@ impl Mint {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check melt has expected fees
|
||||
#[instrument(skip_all)]
|
||||
pub async fn check_melt_expected_ln_fees(
|
||||
&self,
|
||||
melt_quote: &MeltQuote,
|
||||
melt_request: &MeltBolt11Request,
|
||||
) -> Result<Option<Amount>, Error> {
|
||||
let invoice = Bolt11Invoice::from_str(&melt_quote.request)?;
|
||||
|
||||
let quote_msats = to_unit(melt_quote.amount, &melt_quote.unit, &CurrencyUnit::Msat)
|
||||
.expect("Quote unit is checked above that it can convert to msat");
|
||||
|
||||
let invoice_amount_msats: Amount = invoice
|
||||
.amount_milli_satoshis()
|
||||
.ok_or(Error::InvoiceAmountUndefined)?
|
||||
.into();
|
||||
|
||||
let partial_amount = match invoice_amount_msats > quote_msats {
|
||||
true => {
|
||||
let partial_msats = invoice_amount_msats - quote_msats;
|
||||
|
||||
Some(
|
||||
to_unit(partial_msats, &CurrencyUnit::Msat, &melt_quote.unit)
|
||||
.map_err(|_| Error::UnitUnsupported)?,
|
||||
)
|
||||
}
|
||||
false => None,
|
||||
};
|
||||
|
||||
let amount_to_pay = match partial_amount {
|
||||
Some(amount_to_pay) => amount_to_pay,
|
||||
None => to_unit(invoice_amount_msats, &CurrencyUnit::Msat, &melt_quote.unit)
|
||||
.map_err(|_| Error::UnitUnsupported)?,
|
||||
};
|
||||
|
||||
let inputs_amount_quote_unit = melt_request.proofs_amount().map_err(|_| {
|
||||
tracing::error!("Proof inputs in melt quote overflowed");
|
||||
Error::AmountOverflow
|
||||
})?;
|
||||
|
||||
if amount_to_pay + melt_quote.fee_reserve > inputs_amount_quote_unit {
|
||||
tracing::debug!(
|
||||
"Not enough inputs provided: {} msats needed {} msats",
|
||||
inputs_amount_quote_unit,
|
||||
amount_to_pay
|
||||
);
|
||||
|
||||
return Err(Error::TransactionUnbalanced(
|
||||
inputs_amount_quote_unit.into(),
|
||||
amount_to_pay.into(),
|
||||
melt_quote.fee_reserve.into(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(partial_amount)
|
||||
}
|
||||
|
||||
/// Verify melt request is valid
|
||||
/// Check to see if there is a corresponding mint quote for a melt.
|
||||
/// In this case the mint can settle the payment internally and no ln payment is
|
||||
/// needed
|
||||
#[instrument(skip_all)]
|
||||
pub async fn handle_internal_melt_mint(
|
||||
&self,
|
||||
melt_quote: &MeltQuote,
|
||||
melt_request: &MeltBolt11Request,
|
||||
) -> Result<Option<Amount>, Error> {
|
||||
let mint_quote = match self
|
||||
.localstore
|
||||
.get_mint_quote_by_request(&melt_quote.request)
|
||||
.await
|
||||
{
|
||||
Ok(Some(mint_quote)) => mint_quote,
|
||||
// Not an internal melt -> mint
|
||||
Ok(None) => return Ok(None),
|
||||
Err(err) => {
|
||||
tracing::debug!("Error attempting to get mint quote: {}", err);
|
||||
return Err(Error::Internal);
|
||||
}
|
||||
};
|
||||
|
||||
// Mint quote has already been settled, proofs should not be burned or held.
|
||||
if mint_quote.state == MintQuoteState::Issued || mint_quote.state == MintQuoteState::Paid {
|
||||
return Err(Error::RequestAlreadyPaid);
|
||||
}
|
||||
|
||||
let inputs_amount_quote_unit = melt_request.proofs_amount().map_err(|_| {
|
||||
tracing::error!("Proof inputs in melt quote overflowed");
|
||||
Error::AmountOverflow
|
||||
})?;
|
||||
|
||||
let mut mint_quote = mint_quote;
|
||||
|
||||
if mint_quote.amount > inputs_amount_quote_unit {
|
||||
tracing::debug!(
|
||||
"Not enough inuts provided: {} needed {}",
|
||||
inputs_amount_quote_unit,
|
||||
mint_quote.amount
|
||||
);
|
||||
return Err(Error::InsufficientFunds);
|
||||
}
|
||||
|
||||
mint_quote.state = MintQuoteState::Paid;
|
||||
|
||||
let amount = melt_quote.amount;
|
||||
|
||||
self.update_mint_quote(mint_quote).await?;
|
||||
|
||||
Ok(Some(amount))
|
||||
}
|
||||
|
||||
/// Verify melt request is valid
|
||||
#[instrument(skip_all)]
|
||||
pub async fn verify_melt_request(
|
||||
@@ -1005,13 +1118,16 @@ impl Mint {
|
||||
.await?;
|
||||
|
||||
match state {
|
||||
MeltQuoteState::Unpaid => (),
|
||||
MeltQuoteState::Unpaid | MeltQuoteState::Failed => (),
|
||||
MeltQuoteState::Pending => {
|
||||
return Err(Error::PendingQuote);
|
||||
}
|
||||
MeltQuoteState::Paid => {
|
||||
return Err(Error::PaidQuote);
|
||||
}
|
||||
MeltQuoteState::Unknown => {
|
||||
return Err(Error::UnknownPaymentState);
|
||||
}
|
||||
}
|
||||
|
||||
let ys = melt_request
|
||||
@@ -1456,6 +1572,8 @@ mod tests {
|
||||
use bitcoin::Network;
|
||||
use secp256k1::Secp256k1;
|
||||
|
||||
use crate::types::LnKey;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@@ -1561,6 +1679,7 @@ mod tests {
|
||||
seed: &'a [u8],
|
||||
mint_info: MintInfo,
|
||||
supported_units: HashMap<CurrencyUnit, (u64, u8)>,
|
||||
melt_requests: Vec<(MeltBolt11Request, LnKey)>,
|
||||
}
|
||||
|
||||
async fn create_mint(config: MintConfig<'_>) -> Result<Mint, Error> {
|
||||
@@ -1575,6 +1694,7 @@ mod tests {
|
||||
config.quote_proofs,
|
||||
config.blinded_signatures,
|
||||
config.quote_signatures,
|
||||
config.melt_requests,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
@@ -48,6 +48,10 @@ pub enum QuoteState {
|
||||
Paid,
|
||||
/// Paying quote is in progress
|
||||
Pending,
|
||||
/// Unknown state
|
||||
Unknown,
|
||||
/// Failed
|
||||
Failed,
|
||||
}
|
||||
|
||||
impl fmt::Display for QuoteState {
|
||||
@@ -56,6 +60,8 @@ impl fmt::Display for QuoteState {
|
||||
Self::Unpaid => write!(f, "UNPAID"),
|
||||
Self::Paid => write!(f, "PAID"),
|
||||
Self::Pending => write!(f, "PENDING"),
|
||||
Self::Unknown => write!(f, "UNKNOWN"),
|
||||
Self::Failed => write!(f, "FAILED"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,6 +74,8 @@ impl FromStr for QuoteState {
|
||||
"PENDING" => Ok(Self::Pending),
|
||||
"PAID" => Ok(Self::Paid),
|
||||
"UNPAID" => Ok(Self::Unpaid),
|
||||
"UNKNOWN" => Ok(Self::Unknown),
|
||||
"FAILED" => Ok(Self::Failed),
|
||||
_ => Err(Error::UnknownState),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::error::Error;
|
||||
use crate::mint_url::MintUrl;
|
||||
use crate::nuts::{
|
||||
CurrencyUnit, MeltQuoteState, Proof, Proofs, PublicKey, SpendingConditions, State,
|
||||
CurrencyUnit, MeltQuoteState, PaymentMethod, Proof, Proofs, PublicKey, SpendingConditions,
|
||||
State,
|
||||
};
|
||||
use crate::Amount;
|
||||
|
||||
@@ -137,6 +138,23 @@ impl ProofInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Key used in hashmap of ln backends to identify what unit and payment method
|
||||
/// it is for
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LnKey {
|
||||
/// Unit of Payment backend
|
||||
pub unit: CurrencyUnit,
|
||||
/// Method of payment backend
|
||||
pub method: PaymentMethod,
|
||||
}
|
||||
|
||||
impl LnKey {
|
||||
/// Create new [`LnKey`]
|
||||
pub fn new(unit: CurrencyUnit, method: PaymentMethod) -> Self {
|
||||
Self { unit, method }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
86
misc/fake_itests.sh
Executable file
86
misc/fake_itests.sh
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Function to perform cleanup
|
||||
cleanup() {
|
||||
echo "Cleaning up..."
|
||||
|
||||
# Kill the Rust binary process
|
||||
echo "Killing the Rust binary with PID $RUST_BIN_PID"
|
||||
kill $CDK_ITEST_MINT_BIN_PID
|
||||
|
||||
# Wait for the Rust binary to terminate
|
||||
wait $CDK_ITEST_MINT_BIN_PID
|
||||
|
||||
echo "Mint binary terminated"
|
||||
|
||||
# Remove the temporary directory
|
||||
rm -rf "$cdk_itests"
|
||||
echo "Temp directory removed: $cdk_itests"
|
||||
unset cdk_itests
|
||||
unset cdk_itests_mint_addr
|
||||
unset cdk_itests_mint_port
|
||||
}
|
||||
|
||||
# Set up trap to call cleanup on script exit
|
||||
trap cleanup EXIT
|
||||
|
||||
# Create a temporary directory
|
||||
export cdk_itests=$(mktemp -d)
|
||||
export cdk_itests_mint_addr="127.0.0.1";
|
||||
export cdk_itests_mint_port=8086;
|
||||
|
||||
URL="http://$cdk_itests_mint_addr:$cdk_itests_mint_port/v1/info"
|
||||
# Check if the temporary directory was created successfully
|
||||
if [[ ! -d "$cdk_itests" ]]; then
|
||||
echo "Failed to create temp directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Temp directory created: $cdk_itests"
|
||||
export MINT_DATABASE="$1";
|
||||
|
||||
cargo build -p cdk-integration-tests
|
||||
cargo build --bin fake_wallet
|
||||
cargo run --bin fake_wallet &
|
||||
# Capture its PID
|
||||
CDK_ITEST_MINT_BIN_PID=$!
|
||||
|
||||
TIMEOUT=100
|
||||
START_TIME=$(date +%s)
|
||||
# Loop until the endpoint returns a 200 OK status or timeout is reached
|
||||
while true; do
|
||||
# Get the current time
|
||||
CURRENT_TIME=$(date +%s)
|
||||
|
||||
# Calculate the elapsed time
|
||||
ELAPSED_TIME=$((CURRENT_TIME - START_TIME))
|
||||
|
||||
# Check if the elapsed time exceeds the timeout
|
||||
if [ $ELAPSED_TIME -ge $TIMEOUT ]; then
|
||||
echo "Timeout of $TIMEOUT seconds reached. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Make a request to the endpoint and capture the HTTP status code
|
||||
HTTP_STATUS=$(curl -o /dev/null -s -w "%{http_code}" $URL)
|
||||
|
||||
# Check if the HTTP status is 200 OK
|
||||
if [ "$HTTP_STATUS" -eq 200 ]; then
|
||||
echo "Received 200 OK from $URL"
|
||||
break
|
||||
else
|
||||
echo "Waiting for 200 OK response, current status: $HTTP_STATUS"
|
||||
sleep 2 # Wait for 2 seconds before retrying
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
# Run cargo test
|
||||
cargo test -p cdk-integration-tests --test fake_wallet
|
||||
cargo test -p cdk-integration-tests --test mint
|
||||
|
||||
# Capture the exit status of cargo test
|
||||
test_status=$?
|
||||
|
||||
# Exit with the status of the tests
|
||||
exit $test_status
|
||||
@@ -2,3 +2,8 @@ itest db:
|
||||
#!/usr/bin/env bash
|
||||
./misc/itests.sh "{{db}}"
|
||||
|
||||
|
||||
fake-mint-itest db:
|
||||
#!/usr/bin/env bash
|
||||
./misc/fake_itests.sh "{{db}}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user