diff --git a/cashu/lightning/blink.py b/cashu/lightning/blink.py index 24b223d..961f4c6 100644 --- a/cashu/lightning/blink.py +++ b/cashu/lightning/blink.py @@ -24,6 +24,8 @@ from .base import ( # according to https://github.com/GaloyMoney/galoy/blob/7e79cc27304de9b9c2e7d7f4fdd3bac09df23aac/core/api/src/domain/bitcoin/index.ts#L59 BLINK_MAX_FEE_PERCENT = 0.5 +DIRECTION_SEND = "SEND" +DIRECTION_RECEIVE = "RECEIVE" class BlinkWallet(LightningBackend): @@ -35,7 +37,11 @@ class BlinkWallet(LightningBackend): wallet_ids: Dict[Unit, str] = {} endpoint = "https://api.blink.sv/graphql" invoice_statuses = {"PENDING": None, "PAID": True, "EXPIRED": False} - payment_execution_statuses = {"SUCCESS": True, "ALREADY_PAID": None} + payment_execution_statuses = { + "SUCCESS": True, + "ALREADY_PAID": None, + "FAILURE": False, + } payment_statuses = {"SUCCESS": True, "PENDING": None, "FAILURE": False} def __init__(self): @@ -78,10 +84,12 @@ class BlinkWallet(LightningBackend): ) balance = 0 - for wallet_dict in resp["data"]["me"]["defaultAccount"]["wallets"]: - if wallet_dict["walletCurrency"] == "USD": + for wallet_dict in ( + resp.get("data", {}).get("me", {}).get("defaultAccount", {}).get("wallets") + ): + if wallet_dict.get("walletCurrency") == "USD": self.wallet_ids[Unit.usd] = wallet_dict["id"] - elif wallet_dict["walletCurrency"] == "BTC": + elif wallet_dict.get("walletCurrency") == "BTC": self.wallet_ids[Unit.sat] = wallet_dict["id"] balance = wallet_dict["balance"] @@ -137,9 +145,13 @@ class BlinkWallet(LightningBackend): resp = r.json() assert resp, "invalid response" - payment_request = resp["data"]["lnInvoiceCreateOnBehalfOfRecipient"]["invoice"][ - "paymentRequest" - ] + payment_request = ( + resp.get("data", {}) + .get("lnInvoiceCreateOnBehalfOfRecipient", {}) + .get("invoice", {}) + .get("paymentRequest") + ) + assert payment_request, "payment request not found" checking_id = payment_request return InvoiceResponse( @@ -178,6 +190,7 @@ class BlinkWallet(LightningBackend): r = await self.client.post( url=self.endpoint, data=json.dumps(data), + timeout=None, ) r.raise_for_status() except Exception as e: @@ -186,9 +199,14 @@ class BlinkWallet(LightningBackend): resp: dict = r.json() paid = self.payment_execution_statuses[ - resp["data"]["lnInvoicePaymentSend"]["status"] + resp.get("data", {}).get("lnInvoicePaymentSend", {}).get("status") ] - fee = resp["data"]["lnInvoicePaymentSend"]["transaction"]["settlementFee"] + fee = ( + resp.get("data", {}) + .get("lnInvoicePaymentSend", {}) + .get("transaction", {}) + .get("settlementFee") + ) checking_id = quote.request return PaymentResponse( @@ -221,12 +239,15 @@ class BlinkWallet(LightningBackend): logger.error(f"Blink API error: {str(e)}") return PaymentStatus(paid=None) resp: dict = r.json() - if resp["data"]["lnInvoicePaymentStatus"]["errors"]: + if resp.get("data", {}).get("lnInvoicePaymentStatus", {}).get("errors"): logger.error( - "Blink Error", resp["data"]["lnInvoicePaymentStatus"]["errors"] + "Blink Error", + resp.get("data", {}).get("lnInvoicePaymentStatus", {}).get("errors"), ) return PaymentStatus(paid=None) - paid = self.invoice_statuses[resp["data"]["lnInvoicePaymentStatus"]["status"]] + paid = self.invoice_statuses[ + resp.get("data", {}).get("lnInvoicePaymentStatus", {}).get("status") + ] return PaymentStatus(paid=paid) async def get_payment_status(self, checking_id: str) -> PaymentStatus: @@ -267,19 +288,49 @@ class BlinkWallet(LightningBackend): resp: dict = r.json() # no result found - if not resp["data"]["me"]["defaultAccount"]["walletById"][ - "transactionsByPaymentHash" - ]: + if ( + not resp.get("data", {}) + .get("me", {}) + .get("defaultAccount", {}) + .get("walletById", {}) + .get("transactionsByPaymentHash") + ): return PaymentStatus(paid=None) - paid = self.payment_statuses[ - resp["data"]["me"]["defaultAccount"]["walletById"][ - "transactionsByPaymentHash" - ][0]["status"] - ] - fee = resp["data"]["me"]["defaultAccount"]["walletById"][ - "transactionsByPaymentHash" - ][0]["settlementFee"] + all_payments_with_this_hash = ( + resp.get("data", {}) + .get("me", {}) + .get("defaultAccount", {}) + .get("walletById", {}) + .get("transactionsByPaymentHash") + ) + + # Blink API edge case: for a failed payment attempt, it returns two payments with the same hash + # if there are two payments with the same hash with "direction" == "SEND" and "RECEIVE" + # it means that the payment previously failed and we can ignore the attempt and return + # PaymentStatus(paid=None) + if len(all_payments_with_this_hash) == 2 and all( + p["direction"] in [DIRECTION_SEND, DIRECTION_RECEIVE] + for p in all_payments_with_this_hash + ): + return PaymentStatus(paid=None) + + # if there is only one payment with the same hash, it means that the payment might have succeeded + # we only care about the payment with "direction" == "SEND" + payment = next( + ( + p + for p in all_payments_with_this_hash + if p.get("direction") == DIRECTION_SEND + ), + None, + ) + if not payment: + return PaymentStatus(paid=None) + + # we read the status of the payment + paid = self.payment_statuses[payment["status"]] + fee = payment["settlementFee"] return PaymentStatus( paid=paid, @@ -312,22 +363,30 @@ class BlinkWallet(LightningBackend): r = await self.client.post( url=self.endpoint, data=json.dumps(data), + timeout=None, ) r.raise_for_status() except Exception as e: logger.error(f"Blink API error: {str(e)}") - return PaymentResponse(ok=False, error_message=str(e)) + raise e resp: dict = r.json() - + if resp.get("data", {}).get("lnInvoiceFeeProbe", {}).get("errors"): + raise Exception( + resp["data"]["lnInvoiceFeeProbe"]["errors"][0].get("message") + or "Unknown error" + ) invoice_obj = decode(bolt11) assert invoice_obj.amount_msat, "invoice has no amount." amount_msat = int(invoice_obj.amount_msat) - fees_response_msat = int(resp["data"]["lnInvoiceFeeProbe"]["amount"]) * 1000 + fees_response_msat = ( + int(resp.get("data", {}).get("lnInvoiceFeeProbe", {}).get("amount")) * 1000 + ) # we either take fee_msat_response or the BLINK_MAX_FEE_PERCENT, whichever is higher fees_msat = max( - fees_response_msat, math.ceil(amount_msat / 100 * BLINK_MAX_FEE_PERCENT) + fees_response_msat, + max(math.ceil(amount_msat / 100 * BLINK_MAX_FEE_PERCENT), 1000), ) fees = Amount(unit=Unit.msat, amount=fees_msat) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 3782b0c..52eb25a 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -498,6 +498,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions): payment_quote = await self.backends[method][unit].get_payment_quote( melt_quote.request ) + assert payment_quote.checking_id, "quote has no checking id" quote = MeltQuote( quote=random_hash(), diff --git a/tests/test_mint_lightning_blink.py b/tests/test_mint_lightning_blink.py index 3071c69..393829a 100644 --- a/tests/test_mint_lightning_blink.py +++ b/tests/test_mint_lightning_blink.py @@ -110,7 +110,11 @@ async def test_blink_get_payment_status(): "defaultAccount": { "walletById": { "transactionsByPaymentHash": [ - {"status": "SUCCESS", "settlementFee": 10} + { + "status": "SUCCESS", + "settlementFee": 10, + "direction": "SEND", + } ] } }