diff --git a/cashu/core/base.py b/cashu/core/base.py index e30168b..0aa0244 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -153,10 +153,14 @@ class CheckRequest(BaseModel): proofs: List[Proof] -class CheckInternalRequest(BaseModel): +class CheckFeesRequest(BaseModel): pr: str +class CheckFeesResponse(BaseModel): + fee: Union[int, None] + + class MeltRequest(BaseModel): proofs: List[Proof] amount: int = None # deprecated diff --git a/cashu/core/helpers.py b/cashu/core/helpers.py index 0f8c885..cbc1dc5 100644 --- a/cashu/core/helpers.py +++ b/cashu/core/helpers.py @@ -28,8 +28,10 @@ def async_unwrap(to_await): return async_response[0] -def fee_reserve(amount_msat: int) -> int: +def fee_reserve(amount_msat: int, internal=False) -> int: """Function for calculating the Lightning fee reserve""" + if internal: + return 0 return max( int(LIGHTNING_RESERVE_FEE_MIN), int(amount_msat * LIGHTNING_FEE_PERCENT / 100.0) ) diff --git a/cashu/lightning/lnbits.py b/cashu/lightning/lnbits.py index ed655bf..97f8907 100644 --- a/cashu/lightning/lnbits.py +++ b/cashu/lightning/lnbits.py @@ -116,6 +116,8 @@ class LNbitsWallet(Wallet): ) except: return PaymentStatus(None) + if r.json().get("detail"): + return PaymentStatus(None) return PaymentStatus(r.json()["paid"]) async def get_payment_status(self, checking_id: str) -> PaymentStatus: diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 97c621f..501eaa4 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -197,13 +197,13 @@ class Ledger: await update_lightning_invoice(payment_hash, issued=True, db=self.db) return status.paid - async def _pay_lightning_invoice(self, invoice: str, amount: int): + async def _pay_lightning_invoice(self, invoice: str, fees_msat: int): """Returns an invoice from the Lightning backend.""" error, _ = await WALLET.status() if error: raise Exception(f"Lightning wallet not responding: {error}") ok, checking_id, fee_msat, preimage, error_message = await WALLET.pay_invoice( - invoice, fee_limit_msat=fee_reserve(amount * 1000) + invoice, fee_limit_msat=fees_msat ) return ok, preimage @@ -258,16 +258,15 @@ class Ledger: if not all([self._verify_proof(p) for p in proofs]): raise Exception("could not verify proofs.") - total = sum([p["amount"] for p in proofs]) + total_provided = sum([p["amount"] for p in proofs]) invoice_obj = bolt11.decode(invoice) amount = math.ceil(invoice_obj.amount_msat / 1000) - - # check that lightning fees are included - assert total + fee_reserve(amount * 1000) >= amount, Exception( + fees_msat = await self.check_fees(invoice) + assert total_provided >= amount + fees_msat / 1000, Exception( "provided proofs not enough for Lightning payment." ) - status, preimage = await self._pay_lightning_invoice(invoice, amount) + status, preimage = await self._pay_lightning_invoice(invoice, fees_msat) if status == True: await self._invalidate_proofs(proofs) return status, preimage @@ -276,6 +275,17 @@ class Ledger: """Checks if all provided proofs are valid and still spendable (i.e. have not been spent).""" return {i: self._check_spendable(p) for i, p in enumerate(proofs)} + async def check_fees(self, pr: str): + """Returns the fees (in msat) required to pay this pr.""" + decoded_invoice = bolt11.decode(pr) + amount = math.ceil(decoded_invoice.amount_msat / 1000) + # hack: check if it's internal, if it exists, it will return paid = False, + # if id does not exist (not internal), it returns paid = None + paid = await WALLET.get_invoice_status(decoded_invoice.payment_hash) + internal = paid.paid == False + fees_msat = fee_reserve(amount * 1000, internal) + return fees_msat + async def split( self, proofs: List[Proof], amount: int, outputs: List[BlindedMessage] ): diff --git a/cashu/mint/router.py b/cashu/mint/router.py index 7a0378c..3888514 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -5,7 +5,9 @@ from secp256k1 import PublicKey from cashu.core.base import ( CashuError, + CheckFeesResponse, CheckRequest, + CheckFeesRequest, GetMeltResponse, GetMintResponse, MeltRequest, @@ -72,6 +74,12 @@ async def check_spendable(payload: CheckRequest): return await ledger.check_spendable(payload.proofs) +@router.post("/checkfees") +async def check_fees(payload: CheckFeesRequest): + fees_msat = await ledger.check_fees(payload.pr) + return CheckFeesResponse(fee=fees_msat / 1000) + + @router.post("/split") async def split(payload: SplitRequest): """ diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index f53eb78..39086ac 100755 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -17,7 +17,7 @@ from loguru import logger import cashu.core.bolt11 as bolt11 from cashu.core.base import Proof -from cashu.core.bolt11 import Invoice +from cashu.core.bolt11 import Invoice, decode from cashu.core.helpers import fee_reserve from cashu.core.migrations import migrate_databases from cashu.core.settings import CASHU_DIR, DEBUG, ENV_FILE, LIGHTNING, MINT_URL, VERSION @@ -125,11 +125,13 @@ async def pay(ctx, invoice: str): wallet.load_mint() wallet.status() decoded_invoice: Invoice = bolt11.decode(invoice) + # check if it's an internal payment + fees = (await wallet.check_fees(invoice))["fee"] amount = math.ceil( - (decoded_invoice.amount_msat + fee_reserve(decoded_invoice.amount_msat)) / 1000 + (decoded_invoice.amount_msat + fees * 1000) / 1000 ) # 1% fee for Lightning print( - f"Paying Lightning invoice of {decoded_invoice.amount_msat // 1000} sat ({amount} sat incl. fees)" + f"Paying Lightning invoice of {decoded_invoice.amount_msat//1000} sat ({amount} sat incl. fees)" ) assert amount > 0, "amount is not positive" if wallet.available_balance < amount: diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index cfd6a3a..719ffee 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -11,7 +11,7 @@ import cashu.core.b_dhke as b_dhke from cashu.core.base import ( BlindedMessage, BlindedSignature, - CheckInternalRequest, + CheckFeesRequest, CheckRequest, MeltRequest, MintRequest, @@ -211,14 +211,13 @@ class LedgerAPI: return return_dict - async def check_internal(self, payment_request: str): + async def check_fees(self, payment_request: str): """Checks whether the Lightning payment is internal.""" - payload = CheckInternalRequest(pr=payment_request) + payload = CheckFeesRequest(pr=payment_request) return_dict = requests.post( - self.url + "/checkinternal", + self.url + "/checkfees", json=payload.dict(), ).json() - return return_dict async def pay_lightning(self, proofs: List[Proof], invoice: str):