mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 10:34:20 +01:00
WIP: New melt flow (#622)
* `PaymentResult` * ledger: rely on PaymentResult instead of paid flag. Double check for payments marked pending. * `None` is `PENDING` * make format * reflected changes API tests where `PaymentStatus` is used + reflected changes in lnbits * reflect changes in blink backend and tests * fix lnbits get_payment_status * remove paid flag * fix mypy * remove more paid flags * fix strike mypy * green * shorten all state checks * fix * fix some tests * gimme ✅ * fix............ * fix lnbits * fix error * lightning refactor * add more regtest tests * add tests for pending state and failure * shorten checks * use match case for startup check - and remember modified checking_id from pay_invoice * fix strike pending return * new tests? * refactor startup routine into get_melt_quote * test with purge * refactor blink * cleanup responses * blink: return checking_id on failure * fix lndgrpc try except * add more testing for melt branches * speed things up a bit * remove comments * remove comments * block pending melt quotes * remove comments --------- Co-authored-by: lollerfirst <lollerfirst@gmail.com>
This commit is contained in:
@@ -80,6 +80,18 @@ class ProofState(LedgerEvent):
|
||||
def kind(self) -> JSONRPCSubscriptionKinds:
|
||||
return JSONRPCSubscriptionKinds.PROOF_STATE
|
||||
|
||||
@property
|
||||
def unspent(self) -> bool:
|
||||
return self.state == ProofSpentState.unspent
|
||||
|
||||
@property
|
||||
def spent(self) -> bool:
|
||||
return self.state == ProofSpentState.spent
|
||||
|
||||
@property
|
||||
def pending(self) -> bool:
|
||||
return self.state == ProofSpentState.pending
|
||||
|
||||
|
||||
class HTLCWitness(BaseModel):
|
||||
preimage: Optional[str] = None
|
||||
@@ -290,7 +302,6 @@ class MeltQuote(LedgerEvent):
|
||||
unit: str
|
||||
amount: int
|
||||
fee_reserve: int
|
||||
paid: bool
|
||||
state: MeltQuoteState
|
||||
created_time: Union[int, None] = None
|
||||
paid_time: Union[int, None] = None
|
||||
@@ -325,7 +336,6 @@ class MeltQuote(LedgerEvent):
|
||||
unit=row["unit"],
|
||||
amount=row["amount"],
|
||||
fee_reserve=row["fee_reserve"],
|
||||
paid=row["paid"],
|
||||
state=MeltQuoteState[row["state"]],
|
||||
created_time=created_time,
|
||||
paid_time=paid_time,
|
||||
@@ -344,17 +354,34 @@ class MeltQuote(LedgerEvent):
|
||||
def kind(self) -> JSONRPCSubscriptionKinds:
|
||||
return JSONRPCSubscriptionKinds.BOLT11_MELT_QUOTE
|
||||
|
||||
@property
|
||||
def unpaid(self) -> bool:
|
||||
return self.state == MeltQuoteState.unpaid
|
||||
|
||||
@property
|
||||
def pending(self) -> bool:
|
||||
return self.state == MeltQuoteState.pending
|
||||
|
||||
@property
|
||||
def paid(self) -> bool:
|
||||
return self.state == MeltQuoteState.paid
|
||||
|
||||
# method that is invoked when the `state` attribute is changed. to protect the state from being set to anything else if the current state is paid
|
||||
def __setattr__(self, name, value):
|
||||
# an unpaid quote can only be set to pending or paid
|
||||
if name == "state" and self.state == MeltQuoteState.unpaid:
|
||||
if name == "state" and self.unpaid:
|
||||
if value not in [MeltQuoteState.pending, MeltQuoteState.paid]:
|
||||
raise Exception(
|
||||
f"Cannot change state of an unpaid melt quote to {value}."
|
||||
)
|
||||
# a paid quote can not be changed
|
||||
if name == "state" and self.state == MeltQuoteState.paid:
|
||||
if name == "state" and self.paid:
|
||||
raise Exception("Cannot change state of a paid melt quote.")
|
||||
|
||||
if name == "paid":
|
||||
raise Exception(
|
||||
"MeltQuote does not support `paid` anymore! Use `state` instead."
|
||||
)
|
||||
super().__setattr__(name, value)
|
||||
|
||||
|
||||
@@ -375,8 +402,6 @@ class MintQuote(LedgerEvent):
|
||||
checking_id: str
|
||||
unit: str
|
||||
amount: int
|
||||
paid: bool
|
||||
issued: bool
|
||||
state: MintQuoteState
|
||||
created_time: Union[int, None] = None
|
||||
paid_time: Union[int, None] = None
|
||||
@@ -401,8 +426,6 @@ class MintQuote(LedgerEvent):
|
||||
checking_id=row["checking_id"],
|
||||
unit=row["unit"],
|
||||
amount=row["amount"],
|
||||
paid=row["paid"],
|
||||
issued=row["issued"],
|
||||
state=MintQuoteState[row["state"]],
|
||||
created_time=created_time,
|
||||
paid_time=paid_time,
|
||||
@@ -417,24 +440,45 @@ class MintQuote(LedgerEvent):
|
||||
def kind(self) -> JSONRPCSubscriptionKinds:
|
||||
return JSONRPCSubscriptionKinds.BOLT11_MINT_QUOTE
|
||||
|
||||
@property
|
||||
def unpaid(self) -> bool:
|
||||
return self.state == MintQuoteState.unpaid
|
||||
|
||||
@property
|
||||
def paid(self) -> bool:
|
||||
return self.state == MintQuoteState.paid
|
||||
|
||||
@property
|
||||
def pending(self) -> bool:
|
||||
return self.state == MintQuoteState.pending
|
||||
|
||||
@property
|
||||
def issued(self) -> bool:
|
||||
return self.state == MintQuoteState.issued
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
# un unpaid quote can only be set to paid
|
||||
if name == "state" and self.state == MintQuoteState.unpaid:
|
||||
if name == "state" and self.unpaid:
|
||||
if value != MintQuoteState.paid:
|
||||
raise Exception(
|
||||
f"Cannot change state of an unpaid mint quote to {value}."
|
||||
)
|
||||
# a paid quote can only be set to pending or issued
|
||||
if name == "state" and self.state == MintQuoteState.paid:
|
||||
if name == "state" and self.paid:
|
||||
if value != MintQuoteState.pending and value != MintQuoteState.issued:
|
||||
raise Exception(f"Cannot change state of a paid mint quote to {value}.")
|
||||
# a pending quote can only be set to paid or issued
|
||||
if name == "state" and self.state == MintQuoteState.pending:
|
||||
if name == "state" and self.pending:
|
||||
if value not in [MintQuoteState.paid, MintQuoteState.issued]:
|
||||
raise Exception("Cannot change state of a pending mint quote.")
|
||||
# an issued quote cannot be changed
|
||||
if name == "state" and self.state == MintQuoteState.issued:
|
||||
if name == "state" and self.issued:
|
||||
raise Exception("Cannot change state of an issued mint quote.")
|
||||
|
||||
if name == "paid":
|
||||
raise Exception(
|
||||
"MintQuote does not support `paid` anymore! Use `state` instead."
|
||||
)
|
||||
super().__setattr__(name, value)
|
||||
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ class PostMeltQuoteResponse(BaseModel):
|
||||
fee_reserve: int # input fee reserve
|
||||
paid: Optional[
|
||||
bool
|
||||
] # whether the request has been paid # DEPRECATED as per NUT PR #136
|
||||
] = None # whether the request has been paid # DEPRECATED as per NUT PR #136
|
||||
state: Optional[str] # state of the quote
|
||||
expiry: Optional[int] # expiry of the quote
|
||||
payment_preimage: Optional[str] = None # payment preimage
|
||||
@@ -224,6 +224,8 @@ class PostMeltQuoteResponse(BaseModel):
|
||||
to_dict = melt_quote.dict()
|
||||
# turn state into string
|
||||
to_dict["state"] = melt_quote.state.value
|
||||
# add deprecated "paid" field
|
||||
to_dict["paid"] = melt_quote.paid
|
||||
return PostMeltQuoteResponse.parse_obj(to_dict)
|
||||
|
||||
|
||||
|
||||
@@ -135,7 +135,8 @@ class FakeWalletSettings(MintSettings):
|
||||
fakewallet_delay_outgoing_payment: Optional[float] = Field(default=3.0)
|
||||
fakewallet_delay_incoming_payment: Optional[float] = Field(default=3.0)
|
||||
fakewallet_stochastic_invoice: bool = Field(default=False)
|
||||
fakewallet_payment_state: Optional[bool] = Field(default=None)
|
||||
fakewallet_payment_state: Optional[str] = Field(default="SETTLED")
|
||||
fakewallet_pay_invoice_state: Optional[str] = Field(default="SETTLED")
|
||||
|
||||
|
||||
class MintInformation(CashuSettings):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum, auto
|
||||
from typing import AsyncGenerator, Coroutine, Optional, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
@@ -12,8 +13,8 @@ from ..core.models import PostMeltQuoteRequest
|
||||
|
||||
|
||||
class StatusResponse(BaseModel):
|
||||
error_message: Optional[str]
|
||||
balance: Union[int, float]
|
||||
error_message: Optional[str] = None
|
||||
|
||||
|
||||
class InvoiceQuoteResponse(BaseModel):
|
||||
@@ -34,36 +35,77 @@ class InvoiceResponse(BaseModel):
|
||||
error_message: Optional[str] = None
|
||||
|
||||
|
||||
class PaymentResult(Enum):
|
||||
SETTLED = auto()
|
||||
FAILED = auto()
|
||||
PENDING = auto()
|
||||
UNKNOWN = auto()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class PaymentResponse(BaseModel):
|
||||
ok: Optional[bool] = None # True: paid, False: failed, None: pending or unknown
|
||||
result: PaymentResult
|
||||
checking_id: Optional[str] = None
|
||||
fee: Optional[Amount] = None
|
||||
preimage: Optional[str] = None
|
||||
error_message: Optional[str] = None
|
||||
|
||||
|
||||
class PaymentStatus(BaseModel):
|
||||
paid: Optional[bool] = None
|
||||
fee: Optional[Amount] = None
|
||||
preimage: Optional[str] = None
|
||||
|
||||
@property
|
||||
def pending(self) -> bool:
|
||||
return self.paid is not True
|
||||
return self.result == PaymentResult.PENDING
|
||||
|
||||
@property
|
||||
def settled(self) -> bool:
|
||||
return self.result == PaymentResult.SETTLED
|
||||
|
||||
@property
|
||||
def failed(self) -> bool:
|
||||
return self.paid is False
|
||||
return self.result == PaymentResult.FAILED
|
||||
|
||||
@property
|
||||
def unknown(self) -> bool:
|
||||
return self.result == PaymentResult.UNKNOWN
|
||||
|
||||
|
||||
class PaymentStatus(BaseModel):
|
||||
result: PaymentResult
|
||||
fee: Optional[Amount] = None
|
||||
preimage: Optional[str] = None
|
||||
error_message: Optional[str] = None
|
||||
|
||||
@property
|
||||
def pending(self) -> bool:
|
||||
return self.result == PaymentResult.PENDING
|
||||
|
||||
@property
|
||||
def settled(self) -> bool:
|
||||
return self.result == PaymentResult.SETTLED
|
||||
|
||||
@property
|
||||
def failed(self) -> bool:
|
||||
return self.result == PaymentResult.FAILED
|
||||
|
||||
@property
|
||||
def unknown(self) -> bool:
|
||||
return self.result == PaymentResult.UNKNOWN
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.paid is True:
|
||||
return "settled"
|
||||
elif self.paid is False:
|
||||
if self.result == PaymentResult.SETTLED:
|
||||
return (
|
||||
"settled"
|
||||
+ (f" (preimage: {self.preimage})" if self.preimage else "")
|
||||
+ (f" (fee: {self.fee})" if self.fee else "")
|
||||
)
|
||||
elif self.result == PaymentResult.FAILED:
|
||||
return "failed"
|
||||
elif self.paid is None:
|
||||
elif self.result == PaymentResult.PENDING:
|
||||
return "still pending"
|
||||
else:
|
||||
return "unknown (should never happen)"
|
||||
else: # self.result == PaymentResult.UNKNOWN:
|
||||
return "unknown" + (
|
||||
f" (Error: {self.error_message})" if self.error_message else ""
|
||||
)
|
||||
|
||||
|
||||
class LightningBackend(ABC):
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# type: ignore
|
||||
import json
|
||||
import math
|
||||
from typing import AsyncGenerator, Dict, Optional, Union
|
||||
@@ -18,6 +17,7 @@ from .base import (
|
||||
LightningBackend,
|
||||
PaymentQuoteResponse,
|
||||
PaymentResponse,
|
||||
PaymentResult,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
)
|
||||
@@ -30,6 +30,22 @@ DIRECTION_RECEIVE = "RECEIVE"
|
||||
PROBE_FEE_TIMEOUT_SEC = 1
|
||||
MINIMUM_FEE_MSAT = 2000
|
||||
|
||||
INVOICE_RESULT_MAP = {
|
||||
"PENDING": PaymentResult.PENDING,
|
||||
"PAID": PaymentResult.SETTLED,
|
||||
"EXPIRED": PaymentResult.FAILED,
|
||||
}
|
||||
PAYMENT_EXECUTION_RESULT_MAP = {
|
||||
"SUCCESS": PaymentResult.SETTLED,
|
||||
"ALREADY_PAID": PaymentResult.FAILED,
|
||||
"FAILURE": PaymentResult.FAILED,
|
||||
}
|
||||
PAYMENT_RESULT_MAP = {
|
||||
"SUCCESS": PaymentResult.SETTLED,
|
||||
"PENDING": PaymentResult.PENDING,
|
||||
"FAILURE": PaymentResult.FAILED,
|
||||
}
|
||||
|
||||
|
||||
class BlinkWallet(LightningBackend):
|
||||
"""https://dev.blink.sv/
|
||||
@@ -38,13 +54,6 @@ 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,
|
||||
"FAILURE": False,
|
||||
}
|
||||
payment_statuses = {"SUCCESS": True, "PENDING": None, "FAILURE": False}
|
||||
|
||||
supported_units = {Unit.sat, Unit.msat}
|
||||
supports_description: bool = True
|
||||
@@ -66,12 +75,13 @@ class BlinkWallet(LightningBackend):
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
try:
|
||||
data = {
|
||||
"query": "query me { me { defaultAccount { wallets { id walletCurrency balance }}}}",
|
||||
"variables": {},
|
||||
}
|
||||
r = await self.client.post(
|
||||
url=self.endpoint,
|
||||
data=(
|
||||
'{"query":"query me { me { defaultAccount { wallets { id'
|
||||
' walletCurrency balance }}}}", "variables":{}}'
|
||||
),
|
||||
data=json.dumps(data), # type: ignore
|
||||
)
|
||||
r.raise_for_status()
|
||||
except Exception as exc:
|
||||
@@ -96,10 +106,10 @@ class BlinkWallet(LightningBackend):
|
||||
resp.get("data", {}).get("me", {}).get("defaultAccount", {}).get("wallets")
|
||||
):
|
||||
if wallet_dict.get("walletCurrency") == "USD":
|
||||
self.wallet_ids[Unit.usd] = wallet_dict["id"]
|
||||
self.wallet_ids[Unit.usd] = wallet_dict["id"] # type: ignore
|
||||
elif wallet_dict.get("walletCurrency") == "BTC":
|
||||
self.wallet_ids[Unit.sat] = wallet_dict["id"]
|
||||
balance = wallet_dict["balance"]
|
||||
self.wallet_ids[Unit.sat] = wallet_dict["id"] # type: ignore
|
||||
balance = wallet_dict["balance"] # type: ignore
|
||||
|
||||
return StatusResponse(error_message=None, balance=balance)
|
||||
|
||||
@@ -144,7 +154,7 @@ class BlinkWallet(LightningBackend):
|
||||
try:
|
||||
r = await self.client.post(
|
||||
url=self.endpoint,
|
||||
data=json.dumps(data),
|
||||
data=json.dumps(data), # type: ignore
|
||||
)
|
||||
r.raise_for_status()
|
||||
except Exception as e:
|
||||
@@ -197,13 +207,16 @@ class BlinkWallet(LightningBackend):
|
||||
try:
|
||||
r = await self.client.post(
|
||||
url=self.endpoint,
|
||||
data=json.dumps(data),
|
||||
data=json.dumps(data), # type: ignore
|
||||
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))
|
||||
return PaymentResponse(
|
||||
result=PaymentResult.UNKNOWN,
|
||||
error_message=str(e),
|
||||
)
|
||||
|
||||
resp: dict = r.json()
|
||||
|
||||
@@ -211,15 +224,22 @@ class BlinkWallet(LightningBackend):
|
||||
fee: Union[None, int] = None
|
||||
if resp.get("data", {}).get("lnInvoicePaymentSend", {}).get("errors"):
|
||||
error_message = (
|
||||
resp["data"]["lnInvoicePaymentSend"]["errors"][0].get("message")
|
||||
resp["data"]["lnInvoicePaymentSend"]["errors"][0].get("message") # type: ignore
|
||||
or "Unknown error"
|
||||
)
|
||||
|
||||
paid = self.payment_execution_statuses[
|
||||
resp.get("data", {}).get("lnInvoicePaymentSend", {}).get("status")
|
||||
]
|
||||
if paid is None:
|
||||
error_message = "Invoice already paid."
|
||||
status_str = resp.get("data", {}).get("lnInvoicePaymentSend", {}).get("status")
|
||||
result = PAYMENT_EXECUTION_RESULT_MAP[status_str]
|
||||
|
||||
if status_str == "ALREADY_PAID":
|
||||
error_message = "Invoice already paid"
|
||||
|
||||
if result == PaymentResult.FAILED:
|
||||
return PaymentResponse(
|
||||
result=result,
|
||||
error_message=error_message,
|
||||
checking_id=quote.request,
|
||||
)
|
||||
|
||||
if resp.get("data", {}).get("lnInvoicePaymentSend", {}).get("transaction", {}):
|
||||
fee = (
|
||||
@@ -230,15 +250,14 @@ class BlinkWallet(LightningBackend):
|
||||
)
|
||||
|
||||
checking_id = quote.request
|
||||
|
||||
# we check the payment status to get the preimage
|
||||
preimage: Union[None, str] = None
|
||||
payment_status = await self.get_payment_status(checking_id)
|
||||
if payment_status.paid:
|
||||
if payment_status.settled:
|
||||
preimage = payment_status.preimage
|
||||
|
||||
return PaymentResponse(
|
||||
ok=paid,
|
||||
result=result,
|
||||
checking_id=checking_id,
|
||||
fee=Amount(Unit.sat, fee) if fee else None,
|
||||
preimage=preimage,
|
||||
@@ -261,22 +280,27 @@ class BlinkWallet(LightningBackend):
|
||||
"variables": variables,
|
||||
}
|
||||
try:
|
||||
r = await self.client.post(url=self.endpoint, data=json.dumps(data))
|
||||
r = await self.client.post(url=self.endpoint, data=json.dumps(data)) # type: ignore
|
||||
r.raise_for_status()
|
||||
except Exception as e:
|
||||
logger.error(f"Blink API error: {str(e)}")
|
||||
return PaymentStatus(paid=None)
|
||||
return PaymentStatus(result=PaymentResult.UNKNOWN, error_message=str(e))
|
||||
resp: dict = r.json()
|
||||
if resp.get("data", {}).get("lnInvoicePaymentStatus", {}).get("errors"):
|
||||
error_message = (
|
||||
resp.get("data", {}).get("lnInvoicePaymentStatus", {}).get("errors")
|
||||
)
|
||||
if error_message:
|
||||
logger.error(
|
||||
"Blink Error",
|
||||
resp.get("data", {}).get("lnInvoicePaymentStatus", {}).get("errors"),
|
||||
error_message,
|
||||
)
|
||||
return PaymentStatus(paid=None)
|
||||
paid = self.invoice_statuses[
|
||||
return PaymentStatus(
|
||||
result=PaymentResult.UNKNOWN, error_message=error_message
|
||||
)
|
||||
result = INVOICE_RESULT_MAP[
|
||||
resp.get("data", {}).get("lnInvoicePaymentStatus", {}).get("status")
|
||||
]
|
||||
return PaymentStatus(paid=paid)
|
||||
return PaymentStatus(result=result)
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
# Checking ID is the payment request and blink wants the payment hash
|
||||
@@ -311,16 +335,11 @@ class BlinkWallet(LightningBackend):
|
||||
""",
|
||||
"variables": variables,
|
||||
}
|
||||
|
||||
try:
|
||||
r = await self.client.post(
|
||||
url=self.endpoint,
|
||||
data=json.dumps(data),
|
||||
)
|
||||
r.raise_for_status()
|
||||
except Exception as e:
|
||||
logger.error(f"Blink API error: {str(e)}")
|
||||
return PaymentResponse(ok=False, error_message=str(e))
|
||||
r = await self.client.post(
|
||||
url=self.endpoint,
|
||||
data=json.dumps(data), # type: ignore
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
resp: dict = r.json()
|
||||
|
||||
@@ -332,7 +351,9 @@ class BlinkWallet(LightningBackend):
|
||||
.get("walletById", {})
|
||||
.get("transactionsByPaymentHash")
|
||||
):
|
||||
return PaymentStatus(paid=None)
|
||||
return PaymentStatus(
|
||||
result=PaymentResult.UNKNOWN, error_message="No payment found"
|
||||
)
|
||||
|
||||
all_payments_with_this_hash = (
|
||||
resp.get("data", {})
|
||||
@@ -345,12 +366,14 @@ class BlinkWallet(LightningBackend):
|
||||
# Blink API edge case: for a previously failed payment attempt, it returns the 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)
|
||||
# PaymentStatus(status=FAILED)
|
||||
if len(all_payments_with_this_hash) == 2 and all(
|
||||
p["direction"] in [DIRECTION_SEND, DIRECTION_RECEIVE]
|
||||
p["direction"] in [DIRECTION_SEND, DIRECTION_RECEIVE] # type: ignore
|
||||
for p in all_payments_with_this_hash
|
||||
):
|
||||
return PaymentStatus(paid=None)
|
||||
return PaymentStatus(
|
||||
result=PaymentResult.FAILED, error_message="Payment failed"
|
||||
)
|
||||
|
||||
# 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"
|
||||
@@ -363,15 +386,17 @@ class BlinkWallet(LightningBackend):
|
||||
None,
|
||||
)
|
||||
if not payment:
|
||||
return PaymentStatus(paid=None)
|
||||
return PaymentStatus(
|
||||
result=PaymentResult.UNKNOWN, error_message="No payment found"
|
||||
)
|
||||
|
||||
# we read the status of the payment
|
||||
paid = self.payment_statuses[payment["status"]]
|
||||
fee = payment["settlementFee"]
|
||||
preimage = payment["settlementVia"].get("preImage")
|
||||
result = PAYMENT_RESULT_MAP[payment["status"]] # type: ignore
|
||||
fee = payment["settlementFee"] # type: ignore
|
||||
preimage = payment["settlementVia"].get("preImage") # type: ignore
|
||||
|
||||
return PaymentStatus(
|
||||
paid=paid,
|
||||
result=result,
|
||||
fee=Amount(Unit.sat, fee),
|
||||
preimage=preimage,
|
||||
)
|
||||
@@ -404,7 +429,7 @@ class BlinkWallet(LightningBackend):
|
||||
try:
|
||||
r = await self.client.post(
|
||||
url=self.endpoint,
|
||||
data=json.dumps(data),
|
||||
data=json.dumps(data), # type: ignore
|
||||
timeout=PROBE_FEE_TIMEOUT_SEC,
|
||||
)
|
||||
r.raise_for_status()
|
||||
@@ -413,7 +438,7 @@ class BlinkWallet(LightningBackend):
|
||||
# if there was an error, we simply ignore the response and decide the fees ourselves
|
||||
fees_response_msat = 0
|
||||
logger.debug(
|
||||
f"Blink probe error: {resp['data']['lnInvoiceFeeProbe']['errors'][0].get('message')}"
|
||||
f"Blink probe error: {resp['data']['lnInvoiceFeeProbe']['errors'][0].get('message')}" # type: ignore
|
||||
)
|
||||
|
||||
else:
|
||||
@@ -454,5 +479,5 @@ class BlinkWallet(LightningBackend):
|
||||
amount=amount.to(self.unit, round="up"),
|
||||
)
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: # type: ignore
|
||||
raise NotImplementedError("paid_invoices_stream not implemented")
|
||||
|
||||
@@ -20,11 +20,26 @@ from .base import (
|
||||
LightningBackend,
|
||||
PaymentQuoteResponse,
|
||||
PaymentResponse,
|
||||
PaymentResult,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
Unsupported,
|
||||
)
|
||||
|
||||
# https://docs.corelightning.org/reference/lightning-pay
|
||||
PAYMENT_RESULT_MAP = {
|
||||
"complete": PaymentResult.SETTLED,
|
||||
"pending": PaymentResult.PENDING,
|
||||
"failed": PaymentResult.FAILED,
|
||||
}
|
||||
|
||||
# https://docs.corelightning.org/reference/lightning-listinvoices
|
||||
INVOICE_RESULT_MAP = {
|
||||
"paid": PaymentResult.SETTLED,
|
||||
"unpaid": PaymentResult.PENDING,
|
||||
"expired": PaymentResult.FAILED,
|
||||
}
|
||||
|
||||
|
||||
class CLNRestWallet(LightningBackend):
|
||||
supported_units = {Unit.sat, Unit.msat}
|
||||
@@ -68,12 +83,6 @@ class CLNRestWallet(LightningBackend):
|
||||
base_url=self.url, verify=self.cert, headers=self.auth
|
||||
)
|
||||
self.last_pay_index = 0
|
||||
self.statuses = {
|
||||
"paid": True,
|
||||
"complete": True,
|
||||
"failed": False,
|
||||
"pending": None,
|
||||
}
|
||||
|
||||
async def cleanup(self):
|
||||
try:
|
||||
@@ -101,7 +110,7 @@ class CLNRestWallet(LightningBackend):
|
||||
if len(data) == 0:
|
||||
return StatusResponse(error_message="no data", balance=0)
|
||||
balance_msat = int(sum([c["our_amount_msat"] for c in data["channels"]]))
|
||||
return StatusResponse(error_message=None, balance=balance_msat)
|
||||
return StatusResponse(balance=balance_msat)
|
||||
|
||||
async def create_invoice(
|
||||
self,
|
||||
@@ -147,8 +156,6 @@ class CLNRestWallet(LightningBackend):
|
||||
|
||||
return InvoiceResponse(
|
||||
ok=False,
|
||||
checking_id=None,
|
||||
payment_request=None,
|
||||
error_message=error_message,
|
||||
)
|
||||
|
||||
@@ -159,7 +166,6 @@ class CLNRestWallet(LightningBackend):
|
||||
ok=True,
|
||||
checking_id=data["payment_hash"],
|
||||
payment_request=data["bolt11"],
|
||||
error_message=None,
|
||||
)
|
||||
|
||||
async def pay_invoice(
|
||||
@@ -169,20 +175,14 @@ class CLNRestWallet(LightningBackend):
|
||||
invoice = decode(quote.request)
|
||||
except Bolt11Exception as exc:
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
checking_id=None,
|
||||
fee=None,
|
||||
preimage=None,
|
||||
result=PaymentResult.FAILED,
|
||||
error_message=str(exc),
|
||||
)
|
||||
|
||||
if not invoice.amount_msat or invoice.amount_msat <= 0:
|
||||
error_message = "0 amount invoices are not allowed"
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
checking_id=None,
|
||||
fee=None,
|
||||
preimage=None,
|
||||
result=PaymentResult.FAILED,
|
||||
error_message=error_message,
|
||||
)
|
||||
|
||||
@@ -205,11 +205,7 @@ class CLNRestWallet(LightningBackend):
|
||||
error_message = "mint does not support MPP"
|
||||
logger.error(error_message)
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
checking_id=None,
|
||||
fee=None,
|
||||
preimage=None,
|
||||
error_message=error_message,
|
||||
result=PaymentResult.FAILED, error_message=error_message
|
||||
)
|
||||
r = await self.client.post("/v1/pay", data=post_data, timeout=None)
|
||||
|
||||
@@ -220,34 +216,20 @@ class CLNRestWallet(LightningBackend):
|
||||
except Exception:
|
||||
error_message = r.text
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
checking_id=None,
|
||||
fee=None,
|
||||
preimage=None,
|
||||
error_message=error_message,
|
||||
result=PaymentResult.FAILED, error_message=error_message
|
||||
)
|
||||
|
||||
data = r.json()
|
||||
|
||||
if data["status"] != "complete":
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
checking_id=None,
|
||||
fee=None,
|
||||
preimage=None,
|
||||
error_message="payment failed",
|
||||
)
|
||||
|
||||
checking_id = data["payment_hash"]
|
||||
preimage = data["payment_preimage"]
|
||||
fee_msat = data["amount_sent_msat"] - data["amount_msat"]
|
||||
|
||||
return PaymentResponse(
|
||||
ok=self.statuses.get(data["status"]),
|
||||
result=PAYMENT_RESULT_MAP[data["status"]],
|
||||
checking_id=checking_id,
|
||||
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
||||
preimage=preimage,
|
||||
error_message=None,
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
@@ -261,45 +243,44 @@ class CLNRestWallet(LightningBackend):
|
||||
|
||||
if r.is_error or "message" in data or data.get("invoices") is None:
|
||||
raise Exception("error in cln response")
|
||||
return PaymentStatus(paid=self.statuses.get(data["invoices"][0]["status"]))
|
||||
return PaymentStatus(
|
||||
result=INVOICE_RESULT_MAP[data["invoices"][0]["status"]],
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting invoice status: {e}")
|
||||
return PaymentStatus(paid=None)
|
||||
return PaymentStatus(result=PaymentResult.UNKNOWN, error_message=str(e))
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = await self.client.post(
|
||||
"/v1/listpays",
|
||||
data={"payment_hash": checking_id},
|
||||
)
|
||||
try:
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
|
||||
if not data.get("pays"):
|
||||
# payment not found
|
||||
logger.error(f"payment not found: {data.get('pays')}")
|
||||
raise Exception("payment not found")
|
||||
|
||||
if r.is_error or "message" in data:
|
||||
message = data.get("message") or data
|
||||
raise Exception(f"error in clnrest response: {message}")
|
||||
|
||||
pay = data["pays"][0]
|
||||
|
||||
fee_msat, preimage = None, None
|
||||
if self.statuses[pay["status"]]:
|
||||
# cut off "msat" and convert to int
|
||||
fee_msat = -int(pay["amount_sent_msat"]) - int(pay["amount_msat"])
|
||||
preimage = pay["preimage"]
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
|
||||
if not data.get("pays"):
|
||||
# payment not found
|
||||
logger.error(f"payment not found: {data.get('pays')}")
|
||||
return PaymentStatus(
|
||||
paid=self.statuses.get(pay["status"]),
|
||||
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
||||
preimage=preimage,
|
||||
result=PaymentResult.UNKNOWN, error_message="payment not found"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting payment status: {e}")
|
||||
return PaymentStatus(paid=None)
|
||||
|
||||
if r.is_error or "message" in data:
|
||||
message = data.get("message") or data
|
||||
raise Exception(f"error in clnrest response: {message}")
|
||||
|
||||
pay = data["pays"][0]
|
||||
|
||||
fee_msat, preimage = None, None
|
||||
if PAYMENT_RESULT_MAP[pay["status"]] == PaymentResult.SETTLED:
|
||||
fee_msat = -int(pay["amount_sent_msat"]) - int(pay["amount_msat"])
|
||||
preimage = pay["preimage"]
|
||||
|
||||
return PaymentStatus(
|
||||
result=PAYMENT_RESULT_MAP[pay["status"]],
|
||||
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
||||
preimage=preimage,
|
||||
)
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
# call listinvoices to determine the last pay_index
|
||||
|
||||
@@ -19,12 +19,27 @@ from .base import (
|
||||
LightningBackend,
|
||||
PaymentQuoteResponse,
|
||||
PaymentResponse,
|
||||
PaymentResult,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
Unsupported,
|
||||
)
|
||||
from .macaroon import load_macaroon
|
||||
|
||||
# https://docs.corelightning.org/reference/lightning-pay
|
||||
PAYMENT_RESULT_MAP = {
|
||||
"complete": PaymentResult.SETTLED,
|
||||
"pending": PaymentResult.PENDING,
|
||||
"failed": PaymentResult.FAILED,
|
||||
}
|
||||
|
||||
# https://docs.corelightning.org/reference/lightning-listinvoices
|
||||
INVOICE_RESULT_MAP = {
|
||||
"paid": PaymentResult.SETTLED,
|
||||
"unpaid": PaymentResult.PENDING,
|
||||
"expired": PaymentResult.FAILED,
|
||||
}
|
||||
|
||||
|
||||
class CoreLightningRestWallet(LightningBackend):
|
||||
supported_units = {Unit.sat, Unit.msat}
|
||||
@@ -61,12 +76,6 @@ class CoreLightningRestWallet(LightningBackend):
|
||||
base_url=self.url, verify=self.cert, headers=self.auth
|
||||
)
|
||||
self.last_pay_index = 0
|
||||
self.statuses = {
|
||||
"paid": True,
|
||||
"complete": True,
|
||||
"failed": False,
|
||||
"pending": None,
|
||||
}
|
||||
|
||||
async def cleanup(self):
|
||||
try:
|
||||
@@ -140,8 +149,6 @@ class CoreLightningRestWallet(LightningBackend):
|
||||
|
||||
return InvoiceResponse(
|
||||
ok=False,
|
||||
checking_id=None,
|
||||
payment_request=None,
|
||||
error_message=error_message,
|
||||
)
|
||||
|
||||
@@ -152,7 +159,6 @@ class CoreLightningRestWallet(LightningBackend):
|
||||
ok=True,
|
||||
checking_id=data["payment_hash"],
|
||||
payment_request=data["bolt11"],
|
||||
error_message=None,
|
||||
)
|
||||
|
||||
async def pay_invoice(
|
||||
@@ -162,20 +168,14 @@ class CoreLightningRestWallet(LightningBackend):
|
||||
invoice = decode(quote.request)
|
||||
except Bolt11Exception as exc:
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
checking_id=None,
|
||||
fee=None,
|
||||
preimage=None,
|
||||
result=PaymentResult.FAILED,
|
||||
error_message=str(exc),
|
||||
)
|
||||
|
||||
if not invoice.amount_msat or invoice.amount_msat <= 0:
|
||||
error_message = "0 amount invoices are not allowed"
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
checking_id=None,
|
||||
fee=None,
|
||||
preimage=None,
|
||||
result=PaymentResult.FAILED,
|
||||
error_message=error_message,
|
||||
)
|
||||
fee_limit_percent = fee_limit_msat / invoice.amount_msat * 100
|
||||
@@ -193,38 +193,25 @@ class CoreLightningRestWallet(LightningBackend):
|
||||
if r.is_error or "error" in r.json():
|
||||
try:
|
||||
data = r.json()
|
||||
error_message = data["error"]
|
||||
error_message = data["error"]["message"]
|
||||
except Exception:
|
||||
error_message = r.text
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
checking_id=None,
|
||||
fee=None,
|
||||
preimage=None,
|
||||
result=PaymentResult.FAILED,
|
||||
error_message=error_message,
|
||||
)
|
||||
|
||||
data = r.json()
|
||||
|
||||
if data["status"] != "complete":
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
checking_id=None,
|
||||
fee=None,
|
||||
preimage=None,
|
||||
error_message="payment failed",
|
||||
)
|
||||
|
||||
checking_id = data["payment_hash"]
|
||||
preimage = data["payment_preimage"]
|
||||
fee_msat = data["amount_sent_msat"] - data["amount_msat"]
|
||||
|
||||
return PaymentResponse(
|
||||
ok=self.statuses.get(data["status"]),
|
||||
result=PAYMENT_RESULT_MAP[data["status"]],
|
||||
checking_id=checking_id,
|
||||
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
||||
preimage=preimage,
|
||||
error_message=None,
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
@@ -238,45 +225,44 @@ class CoreLightningRestWallet(LightningBackend):
|
||||
|
||||
if r.is_error or "error" in data or data.get("invoices") is None:
|
||||
raise Exception("error in cln response")
|
||||
return PaymentStatus(paid=self.statuses.get(data["invoices"][0]["status"]))
|
||||
return PaymentStatus(
|
||||
result=INVOICE_RESULT_MAP[data["invoices"][0]["status"]],
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting invoice status: {e}")
|
||||
return PaymentStatus(paid=None)
|
||||
return PaymentStatus(result=PaymentResult.UNKNOWN, error_message=str(e))
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = await self.client.get(
|
||||
"/v1/pay/listPays",
|
||||
params={"payment_hash": checking_id},
|
||||
)
|
||||
try:
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
|
||||
if not data.get("pays"):
|
||||
# payment not found
|
||||
logger.error(f"payment not found: {data.get('pays')}")
|
||||
raise Exception("payment not found")
|
||||
|
||||
if r.is_error or "error" in data:
|
||||
message = data.get("error") or data
|
||||
raise Exception(f"error in corelightning-rest response: {message}")
|
||||
|
||||
pay = data["pays"][0]
|
||||
|
||||
fee_msat, preimage = None, None
|
||||
if self.statuses[pay["status"]]:
|
||||
# cut off "msat" and convert to int
|
||||
fee_msat = -int(pay["amount_sent_msat"]) - int(pay["amount_msat"])
|
||||
preimage = pay["preimage"]
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
|
||||
if not data.get("pays"):
|
||||
# payment not found
|
||||
logger.error(f"payment not found: {data.get('pays')}")
|
||||
return PaymentStatus(
|
||||
paid=self.statuses.get(pay["status"]),
|
||||
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
||||
preimage=preimage,
|
||||
result=PaymentResult.UNKNOWN, error_message="payment not found"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting payment status: {e}")
|
||||
return PaymentStatus(paid=None)
|
||||
|
||||
if r.is_error or "error" in data:
|
||||
message = data.get("error") or data
|
||||
raise Exception(f"error in corelightning-rest response: {message}")
|
||||
|
||||
pay = data["pays"][0]
|
||||
|
||||
fee_msat, preimage = None, None
|
||||
if PAYMENT_RESULT_MAP.get(pay["status"]) == PaymentResult.SETTLED:
|
||||
fee_msat = -int(pay["amount_sent_msat"]) - int(pay["amount_msat"])
|
||||
preimage = pay["preimage"]
|
||||
|
||||
return PaymentStatus(
|
||||
result=PAYMENT_RESULT_MAP[pay["status"]],
|
||||
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
||||
preimage=preimage,
|
||||
)
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
# call listinvoices to determine the last pay_index
|
||||
@@ -285,7 +271,8 @@ class CoreLightningRestWallet(LightningBackend):
|
||||
data = r.json()
|
||||
if r.is_error or "error" in data:
|
||||
raise Exception("error in cln response")
|
||||
self.last_pay_index = data["invoices"][-1]["pay_index"]
|
||||
if data.get("invoices"):
|
||||
self.last_pay_index = data["invoices"][-1]["pay_index"]
|
||||
|
||||
while True:
|
||||
try:
|
||||
@@ -315,9 +302,11 @@ class CoreLightningRestWallet(LightningBackend):
|
||||
)
|
||||
paid_invoce = r.json()
|
||||
logger.trace(f"paid invoice: {paid_invoce}")
|
||||
assert self.statuses[
|
||||
paid_invoce["invoices"][0]["status"]
|
||||
], "streamed invoice not paid"
|
||||
if (
|
||||
INVOICE_RESULT_MAP[paid_invoce["invoices"][0]["status"]]
|
||||
!= PaymentResult.SETTLED
|
||||
):
|
||||
raise Exception("invoice not paid")
|
||||
assert "invoices" in paid_invoce, "no invoices in response"
|
||||
assert len(paid_invoce["invoices"]), "no invoices in response"
|
||||
yield paid_invoce["invoices"][0]["payment_hash"]
|
||||
|
||||
@@ -23,6 +23,7 @@ from .base import (
|
||||
LightningBackend,
|
||||
PaymentQuoteResponse,
|
||||
PaymentResponse,
|
||||
PaymentResult,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
)
|
||||
@@ -151,6 +152,14 @@ class FakeWallet(LightningBackend):
|
||||
if settings.fakewallet_delay_outgoing_payment:
|
||||
await asyncio.sleep(settings.fakewallet_delay_outgoing_payment)
|
||||
|
||||
if settings.fakewallet_pay_invoice_state:
|
||||
return PaymentResponse(
|
||||
result=PaymentResult[settings.fakewallet_pay_invoice_state],
|
||||
checking_id=invoice.payment_hash,
|
||||
fee=Amount(unit=self.unit, amount=1),
|
||||
preimage=self.payment_secrets.get(invoice.payment_hash) or "0" * 64,
|
||||
)
|
||||
|
||||
if invoice.payment_hash in self.payment_secrets or settings.fakewallet_brr:
|
||||
if invoice not in self.paid_invoices_outgoing:
|
||||
self.paid_invoices_outgoing.append(invoice)
|
||||
@@ -158,28 +167,33 @@ class FakeWallet(LightningBackend):
|
||||
raise ValueError("Invoice already paid")
|
||||
|
||||
return PaymentResponse(
|
||||
ok=True,
|
||||
result=PaymentResult.SETTLED,
|
||||
checking_id=invoice.payment_hash,
|
||||
fee=Amount(unit=self.unit, amount=1),
|
||||
preimage=self.payment_secrets.get(invoice.payment_hash) or "0" * 64,
|
||||
)
|
||||
else:
|
||||
return PaymentResponse(
|
||||
ok=False, error_message="Only internal invoices can be used!"
|
||||
result=PaymentResult.FAILED,
|
||||
error_message="Only internal invoices can be used!",
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
await self.mark_invoice_paid(self.create_dummy_bolt11(checking_id), delay=False)
|
||||
paid_chceking_ids = [i.payment_hash for i in self.paid_invoices_incoming]
|
||||
if checking_id in paid_chceking_ids:
|
||||
paid = True
|
||||
return PaymentStatus(result=PaymentResult.SETTLED)
|
||||
else:
|
||||
paid = False
|
||||
return PaymentStatus(
|
||||
result=PaymentResult.UNKNOWN, error_message="Invoice not found"
|
||||
)
|
||||
|
||||
return PaymentStatus(paid=paid)
|
||||
|
||||
async def get_payment_status(self, _: str) -> PaymentStatus:
|
||||
return PaymentStatus(paid=settings.fakewallet_payment_state)
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
if settings.fakewallet_payment_state:
|
||||
return PaymentStatus(
|
||||
result=PaymentResult[settings.fakewallet_payment_state]
|
||||
)
|
||||
return PaymentStatus(result=PaymentResult.SETTLED)
|
||||
|
||||
async def get_payment_quote(
|
||||
self, melt_quote: PostMeltQuoteRequest
|
||||
|
||||
@@ -17,6 +17,7 @@ from .base import (
|
||||
LightningBackend,
|
||||
PaymentQuoteResponse,
|
||||
PaymentResponse,
|
||||
PaymentResult,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
)
|
||||
@@ -112,11 +113,18 @@ class LNbitsWallet(LightningBackend):
|
||||
)
|
||||
r.raise_for_status()
|
||||
except Exception:
|
||||
return PaymentResponse(error_message=r.json()["detail"])
|
||||
return PaymentResponse(
|
||||
result=PaymentResult.FAILED, error_message=r.json()["detail"]
|
||||
)
|
||||
if r.status_code > 299:
|
||||
return PaymentResponse(error_message=(f"HTTP status: {r.reason_phrase}",))
|
||||
return PaymentResponse(
|
||||
result=PaymentResult.FAILED,
|
||||
error_message=(f"HTTP status: {r.reason_phrase}",),
|
||||
)
|
||||
if "detail" in r.json():
|
||||
return PaymentResponse(error_message=(r.json()["detail"],))
|
||||
return PaymentResponse(
|
||||
result=PaymentResult.FAILED, error_message=(r.json()["detail"],)
|
||||
)
|
||||
|
||||
data: dict = r.json()
|
||||
checking_id = data["payment_hash"]
|
||||
@@ -125,7 +133,7 @@ class LNbitsWallet(LightningBackend):
|
||||
payment: PaymentStatus = await self.get_payment_status(checking_id)
|
||||
|
||||
return PaymentResponse(
|
||||
ok=True,
|
||||
result=payment.result,
|
||||
checking_id=checking_id,
|
||||
fee=payment.fee,
|
||||
preimage=payment.preimage,
|
||||
@@ -137,12 +145,28 @@ class LNbitsWallet(LightningBackend):
|
||||
url=f"{self.endpoint}/api/v1/payments/{checking_id}"
|
||||
)
|
||||
r.raise_for_status()
|
||||
except Exception:
|
||||
return PaymentStatus(paid=None)
|
||||
except Exception as e:
|
||||
return PaymentStatus(result=PaymentResult.UNKNOWN, error_message=str(e))
|
||||
data: dict = r.json()
|
||||
if data.get("detail"):
|
||||
return PaymentStatus(paid=None)
|
||||
return PaymentStatus(paid=r.json()["paid"])
|
||||
return PaymentStatus(
|
||||
result=PaymentResult.UNKNOWN, error_message=data["detail"]
|
||||
)
|
||||
|
||||
if data["paid"]:
|
||||
result = PaymentResult.SETTLED
|
||||
elif not data["paid"] and data["details"]["pending"]:
|
||||
result = PaymentResult.PENDING
|
||||
elif not data["paid"] and not data["details"]["pending"]:
|
||||
result = PaymentResult.FAILED
|
||||
else:
|
||||
result = PaymentResult.UNKNOWN
|
||||
|
||||
return PaymentStatus(
|
||||
result=result,
|
||||
fee=Amount(unit=Unit.msat, amount=abs(data["details"]["fee"])),
|
||||
preimage=data["preimage"],
|
||||
)
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
@@ -150,26 +174,32 @@ class LNbitsWallet(LightningBackend):
|
||||
url=f"{self.endpoint}/api/v1/payments/{checking_id}"
|
||||
)
|
||||
r.raise_for_status()
|
||||
except Exception:
|
||||
return PaymentStatus(paid=None)
|
||||
except httpx.HTTPStatusError as e:
|
||||
if e.response.status_code != 404:
|
||||
raise e
|
||||
return PaymentStatus(
|
||||
result=PaymentResult.UNKNOWN, error_message=e.response.text
|
||||
)
|
||||
|
||||
data = r.json()
|
||||
if "paid" not in data and "details" not in data:
|
||||
return PaymentStatus(paid=None)
|
||||
return PaymentStatus(
|
||||
result=PaymentResult.UNKNOWN, error_message="invalid response"
|
||||
)
|
||||
|
||||
paid_value = None
|
||||
if data["paid"]:
|
||||
paid_value = True
|
||||
result = PaymentResult.SETTLED
|
||||
elif not data["paid"] and data["details"]["pending"]:
|
||||
paid_value = None
|
||||
result = PaymentResult.PENDING
|
||||
elif not data["paid"] and not data["details"]["pending"]:
|
||||
paid_value = False
|
||||
result = PaymentResult.FAILED
|
||||
else:
|
||||
raise ValueError(f"unexpected value for paid: {data['paid']}")
|
||||
result = PaymentResult.UNKNOWN
|
||||
|
||||
return PaymentStatus(
|
||||
paid=paid_value,
|
||||
result=result,
|
||||
fee=Amount(unit=Unit.msat, amount=abs(data["details"]["fee"])),
|
||||
preimage=data["preimage"],
|
||||
preimage=data.get("preimage"),
|
||||
)
|
||||
|
||||
async def get_payment_quote(
|
||||
|
||||
@@ -24,6 +24,7 @@ from cashu.lightning.base import (
|
||||
LightningBackend,
|
||||
PaymentQuoteResponse,
|
||||
PaymentResponse,
|
||||
PaymentResult,
|
||||
PaymentStatus,
|
||||
PostMeltQuoteRequest,
|
||||
StatusResponse,
|
||||
@@ -31,18 +32,18 @@ from cashu.lightning.base import (
|
||||
|
||||
# maps statuses to None, False, True:
|
||||
# https://api.lightning.community/?python=#paymentpaymentstatus
|
||||
PAYMENT_STATUSES = {
|
||||
lnrpc.Payment.PaymentStatus.UNKNOWN: None,
|
||||
lnrpc.Payment.PaymentStatus.IN_FLIGHT: None,
|
||||
lnrpc.Payment.PaymentStatus.INITIATED: None,
|
||||
lnrpc.Payment.PaymentStatus.SUCCEEDED: True,
|
||||
lnrpc.Payment.PaymentStatus.FAILED: False,
|
||||
PAYMENT_RESULT_MAP = {
|
||||
lnrpc.Payment.PaymentStatus.UNKNOWN: PaymentResult.UNKNOWN,
|
||||
lnrpc.Payment.PaymentStatus.IN_FLIGHT: PaymentResult.PENDING,
|
||||
lnrpc.Payment.PaymentStatus.INITIATED: PaymentResult.PENDING,
|
||||
lnrpc.Payment.PaymentStatus.SUCCEEDED: PaymentResult.SETTLED,
|
||||
lnrpc.Payment.PaymentStatus.FAILED: PaymentResult.FAILED,
|
||||
}
|
||||
INVOICE_STATUSES = {
|
||||
lnrpc.Invoice.InvoiceState.OPEN: None,
|
||||
lnrpc.Invoice.InvoiceState.SETTLED: True,
|
||||
lnrpc.Invoice.InvoiceState.CANCELED: None,
|
||||
lnrpc.Invoice.InvoiceState.ACCEPTED: None,
|
||||
INVOICE_RESULT_MAP = {
|
||||
lnrpc.Invoice.InvoiceState.OPEN: PaymentResult.PENDING,
|
||||
lnrpc.Invoice.InvoiceState.SETTLED: PaymentResult.SETTLED,
|
||||
lnrpc.Invoice.InvoiceState.CANCELED: PaymentResult.FAILED,
|
||||
lnrpc.Invoice.InvoiceState.ACCEPTED: PaymentResult.PENDING,
|
||||
}
|
||||
|
||||
|
||||
@@ -181,13 +182,13 @@ class LndRPCWallet(LightningBackend):
|
||||
except AioRpcError as e:
|
||||
error_message = f"SendPaymentSync failed: {e}"
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
result=PaymentResult.FAILED,
|
||||
error_message=error_message,
|
||||
)
|
||||
|
||||
if r.payment_error:
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
result=PaymentResult.FAILED,
|
||||
error_message=r.payment_error,
|
||||
)
|
||||
|
||||
@@ -195,7 +196,7 @@ class LndRPCWallet(LightningBackend):
|
||||
fee_msat = r.payment_route.total_fees_msat
|
||||
preimage = r.payment_preimage.hex()
|
||||
return PaymentResponse(
|
||||
ok=True,
|
||||
result=PaymentResult.SETTLED,
|
||||
checking_id=checking_id,
|
||||
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
||||
preimage=preimage,
|
||||
@@ -262,7 +263,7 @@ class LndRPCWallet(LightningBackend):
|
||||
except AioRpcError as e:
|
||||
logger.error(f"QueryRoute or SendToRouteV2 failed: {e}")
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
result=PaymentResult.FAILED,
|
||||
error_message=str(e),
|
||||
)
|
||||
|
||||
@@ -270,16 +271,23 @@ class LndRPCWallet(LightningBackend):
|
||||
error_message = f"Sending to route failed with code {r.failure.code}"
|
||||
logger.error(error_message)
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
result=PaymentResult.FAILED,
|
||||
error_message=error_message,
|
||||
)
|
||||
|
||||
ok = r.status == lnrpc.HTLCAttempt.HTLCStatus.SUCCEEDED
|
||||
result = PaymentResult.UNKNOWN
|
||||
if r.status == lnrpc.HTLCAttempt.HTLCStatus.SUCCEEDED:
|
||||
result = PaymentResult.SETTLED
|
||||
elif r.status == lnrpc.HTLCAttempt.HTLCStatus.IN_FLIGHT:
|
||||
result = PaymentResult.PENDING
|
||||
else:
|
||||
result = PaymentResult.FAILED
|
||||
|
||||
checking_id = invoice.payment_hash
|
||||
fee_msat = r.route.total_fees_msat
|
||||
preimage = r.preimage.hex()
|
||||
return PaymentResponse(
|
||||
ok=ok,
|
||||
result=result,
|
||||
checking_id=checking_id,
|
||||
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
||||
preimage=preimage,
|
||||
@@ -299,44 +307,47 @@ class LndRPCWallet(LightningBackend):
|
||||
except AioRpcError as e:
|
||||
error_message = f"LookupInvoice failed: {e}"
|
||||
logger.error(error_message)
|
||||
return PaymentStatus(paid=None)
|
||||
return PaymentStatus(result=PaymentResult.UNKNOWN)
|
||||
|
||||
return PaymentStatus(paid=INVOICE_STATUSES[r.state])
|
||||
return PaymentStatus(
|
||||
result=INVOICE_RESULT_MAP[r.state],
|
||||
)
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
"""
|
||||
This routine checks the payment status using routerpc.TrackPaymentV2.
|
||||
"""
|
||||
# convert checking_id from hex to bytes and some LND magic
|
||||
try:
|
||||
checking_id_bytes = bytes.fromhex(checking_id)
|
||||
except ValueError:
|
||||
logger.error(f"Couldn't convert {checking_id = } to bytes")
|
||||
return PaymentStatus(paid=None)
|
||||
|
||||
checking_id_bytes = bytes.fromhex(checking_id)
|
||||
request = routerrpc.TrackPaymentRequest(payment_hash=checking_id_bytes)
|
||||
|
||||
try:
|
||||
async with grpc.aio.secure_channel(
|
||||
self.endpoint, self.combined_creds
|
||||
) as channel:
|
||||
router_stub = routerstub.RouterStub(channel)
|
||||
async with grpc.aio.secure_channel(
|
||||
self.endpoint, self.combined_creds
|
||||
) as channel:
|
||||
router_stub = routerstub.RouterStub(channel)
|
||||
try:
|
||||
async for payment in router_stub.TrackPaymentV2(request):
|
||||
if payment is not None and payment.status:
|
||||
preimage = (
|
||||
payment.payment_preimage
|
||||
if payment.payment_preimage != "0" * 64
|
||||
else None
|
||||
)
|
||||
return PaymentStatus(
|
||||
paid=PAYMENT_STATUSES[payment.status],
|
||||
result=PAYMENT_RESULT_MAP[payment.status],
|
||||
fee=(
|
||||
Amount(unit=Unit.msat, amount=payment.fee_msat)
|
||||
if payment.fee_msat
|
||||
else None
|
||||
),
|
||||
preimage=payment.payment_preimage,
|
||||
preimage=preimage,
|
||||
)
|
||||
except AioRpcError as e:
|
||||
error_message = f"TrackPaymentV2 failed: {e}"
|
||||
logger.error(error_message)
|
||||
except AioRpcError as e:
|
||||
# status = StatusCode.NOT_FOUND
|
||||
if e.code() == grpc.StatusCode.NOT_FOUND:
|
||||
return PaymentStatus(result=PaymentResult.UNKNOWN)
|
||||
|
||||
return PaymentStatus(paid=None)
|
||||
return PaymentStatus(result=PaymentResult.UNKNOWN)
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
while True:
|
||||
|
||||
@@ -21,11 +21,26 @@ from .base import (
|
||||
LightningBackend,
|
||||
PaymentQuoteResponse,
|
||||
PaymentResponse,
|
||||
PaymentResult,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
)
|
||||
from .macaroon import load_macaroon
|
||||
|
||||
PAYMENT_RESULT_MAP = {
|
||||
"UNKNOWN": PaymentResult.UNKNOWN,
|
||||
"IN_FLIGHT": PaymentResult.PENDING,
|
||||
"INITIATED": PaymentResult.PENDING,
|
||||
"SUCCEEDED": PaymentResult.SETTLED,
|
||||
"FAILED": PaymentResult.FAILED,
|
||||
}
|
||||
INVOICE_RESULT_MAP = {
|
||||
"OPEN": PaymentResult.PENDING,
|
||||
"SETTLED": PaymentResult.SETTLED,
|
||||
"CANCELED": PaymentResult.FAILED,
|
||||
"ACCEPTED": PaymentResult.PENDING,
|
||||
}
|
||||
|
||||
|
||||
class LndRestWallet(LightningBackend):
|
||||
"""https://api.lightning.community/rest/index.html#lnd-rest-api-reference"""
|
||||
@@ -187,11 +202,7 @@ class LndRestWallet(LightningBackend):
|
||||
if r.is_error or r.json().get("payment_error"):
|
||||
error_message = r.json().get("payment_error") or r.text
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
checking_id=None,
|
||||
fee=None,
|
||||
preimage=None,
|
||||
error_message=error_message,
|
||||
result=PaymentResult.FAILED, error_message=error_message
|
||||
)
|
||||
|
||||
data = r.json()
|
||||
@@ -199,11 +210,10 @@ class LndRestWallet(LightningBackend):
|
||||
fee_msat = int(data["payment_route"]["total_fees_msat"])
|
||||
preimage = base64.b64decode(data["payment_preimage"]).hex()
|
||||
return PaymentResponse(
|
||||
ok=True,
|
||||
result=PaymentResult.SETTLED,
|
||||
checking_id=checking_id,
|
||||
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
||||
preimage=preimage,
|
||||
error_message=None,
|
||||
)
|
||||
|
||||
async def pay_partial_invoice(
|
||||
@@ -237,11 +247,7 @@ class LndRestWallet(LightningBackend):
|
||||
if r.is_error or data.get("message"):
|
||||
error_message = data.get("message") or r.text
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
checking_id=None,
|
||||
fee=None,
|
||||
preimage=None,
|
||||
error_message=error_message,
|
||||
result=PaymentResult.FAILED, error_message=error_message
|
||||
)
|
||||
|
||||
# We need to set the mpp_record for a partial payment
|
||||
@@ -272,58 +278,52 @@ class LndRestWallet(LightningBackend):
|
||||
if r.is_error or data.get("message"):
|
||||
error_message = data.get("message") or r.text
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
checking_id=None,
|
||||
fee=None,
|
||||
preimage=None,
|
||||
error_message=error_message,
|
||||
result=PaymentResult.FAILED, error_message=error_message
|
||||
)
|
||||
|
||||
ok = data.get("status") == "SUCCEEDED"
|
||||
result = PAYMENT_RESULT_MAP.get(data.get("status"), PaymentResult.UNKNOWN)
|
||||
checking_id = invoice.payment_hash
|
||||
fee_msat = int(data["route"]["total_fees_msat"])
|
||||
preimage = base64.b64decode(data["preimage"]).hex()
|
||||
fee_msat = int(data["route"]["total_fees_msat"]) if data.get("route") else None
|
||||
preimage = (
|
||||
base64.b64decode(data["preimage"]).hex() if data.get("preimage") else None
|
||||
)
|
||||
return PaymentResponse(
|
||||
ok=ok,
|
||||
result=result,
|
||||
checking_id=checking_id,
|
||||
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
||||
preimage=preimage,
|
||||
error_message=None,
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = await self.client.get(url=f"/v1/invoice/{checking_id}")
|
||||
|
||||
if r.is_error or not r.json().get("settled"):
|
||||
# this must also work when checking_id is not a hex recognizable by lnd
|
||||
# it will return an error and no "settled" attribute on the object
|
||||
return PaymentStatus(paid=None)
|
||||
if r.is_error:
|
||||
logger.error(f"Couldn't get invoice status: {r.text}")
|
||||
return PaymentStatus(result=PaymentResult.UNKNOWN, error_message=r.text)
|
||||
|
||||
return PaymentStatus(paid=True)
|
||||
data = None
|
||||
try:
|
||||
data = r.json()
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Incomprehensible response: {str(e)}")
|
||||
return PaymentStatus(result=PaymentResult.UNKNOWN, error_message=str(e))
|
||||
if not data or not data.get("state"):
|
||||
return PaymentStatus(
|
||||
result=PaymentResult.UNKNOWN, error_message="no invoice state"
|
||||
)
|
||||
return PaymentStatus(
|
||||
result=INVOICE_RESULT_MAP[data["state"]],
|
||||
)
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
"""
|
||||
This routine checks the payment status using routerpc.TrackPaymentV2.
|
||||
"""
|
||||
# convert checking_id from hex to base64 and some LND magic
|
||||
try:
|
||||
checking_id = base64.urlsafe_b64encode(bytes.fromhex(checking_id)).decode(
|
||||
"ascii"
|
||||
)
|
||||
except ValueError:
|
||||
return PaymentStatus(paid=None)
|
||||
|
||||
checking_id = base64.urlsafe_b64encode(bytes.fromhex(checking_id)).decode(
|
||||
"ascii"
|
||||
)
|
||||
url = f"/v2/router/track/{checking_id}"
|
||||
|
||||
# check payment.status:
|
||||
# https://api.lightning.community/?python=#paymentpaymentstatus
|
||||
statuses = {
|
||||
"UNKNOWN": None,
|
||||
"IN_FLIGHT": None,
|
||||
"SUCCEEDED": True,
|
||||
"FAILED": False,
|
||||
}
|
||||
|
||||
async with self.client.stream("GET", url, timeout=None) as r:
|
||||
async for json_line in r.aiter_lines():
|
||||
try:
|
||||
@@ -337,27 +337,37 @@ class LndRestWallet(LightningBackend):
|
||||
else line["error"]
|
||||
)
|
||||
logger.error(f"LND get_payment_status error: {message}")
|
||||
return PaymentStatus(paid=None)
|
||||
return PaymentStatus(
|
||||
result=PaymentResult.UNKNOWN, error_message=message
|
||||
)
|
||||
|
||||
payment = line.get("result")
|
||||
|
||||
# payment exists
|
||||
if payment is not None and payment.get("status"):
|
||||
preimage = (
|
||||
payment.get("payment_preimage")
|
||||
if payment.get("payment_preimage") != "0" * 64
|
||||
else None
|
||||
)
|
||||
return PaymentStatus(
|
||||
paid=statuses[payment["status"]],
|
||||
result=PAYMENT_RESULT_MAP[payment["status"]],
|
||||
fee=(
|
||||
Amount(unit=Unit.msat, amount=payment.get("fee_msat"))
|
||||
if payment.get("fee_msat")
|
||||
else None
|
||||
),
|
||||
preimage=payment.get("payment_preimage"),
|
||||
preimage=preimage,
|
||||
)
|
||||
else:
|
||||
return PaymentStatus(paid=None)
|
||||
return PaymentStatus(
|
||||
result=PaymentResult.UNKNOWN,
|
||||
error_message="no payment status",
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return PaymentStatus(paid=None)
|
||||
return PaymentStatus(result=PaymentResult.UNKNOWN, error_message="timeout")
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
while True:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# type: ignore
|
||||
import secrets
|
||||
from typing import AsyncGenerator, Optional
|
||||
from typing import AsyncGenerator, Dict, Optional, Union
|
||||
|
||||
import httpx
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..core.base import Amount, MeltQuote, Unit
|
||||
from ..core.models import PostMeltQuoteRequest
|
||||
@@ -12,6 +12,7 @@ from .base import (
|
||||
LightningBackend,
|
||||
PaymentQuoteResponse,
|
||||
PaymentResponse,
|
||||
PaymentResult,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
)
|
||||
@@ -19,13 +20,92 @@ from .base import (
|
||||
USDT = "USDT"
|
||||
|
||||
|
||||
class StrikeAmount(BaseModel):
|
||||
amount: str
|
||||
currency: str
|
||||
|
||||
|
||||
class StrikeRate(BaseModel):
|
||||
amount: str
|
||||
sourceCurrency: str
|
||||
targetCurrency: str
|
||||
|
||||
|
||||
class StrikeCreateInvoiceResponse(BaseModel):
|
||||
invoiceId: str
|
||||
amount: StrikeAmount
|
||||
state: str
|
||||
description: str
|
||||
|
||||
|
||||
class StrikePaymentQuoteResponse(BaseModel):
|
||||
lightningNetworkFee: StrikeAmount
|
||||
paymentQuoteId: str
|
||||
validUntil: str
|
||||
amount: StrikeAmount
|
||||
totalFee: StrikeAmount
|
||||
totalAmount: StrikeAmount
|
||||
|
||||
|
||||
class InvoiceQuoteResponse(BaseModel):
|
||||
quoteId: str
|
||||
description: str
|
||||
lnInvoice: str
|
||||
expiration: str
|
||||
expirationInSec: int
|
||||
targetAmount: StrikeAmount
|
||||
sourceAmount: StrikeAmount
|
||||
conversionRate: StrikeRate
|
||||
|
||||
|
||||
class StrikePaymentResponse(BaseModel):
|
||||
paymentId: str
|
||||
state: str
|
||||
result: str
|
||||
completed: Optional[str]
|
||||
delivered: Optional[str]
|
||||
amount: StrikeAmount
|
||||
totalFee: StrikeAmount
|
||||
lightningNetworkFee: StrikeAmount
|
||||
totalAmount: StrikeAmount
|
||||
lightning: Dict[str, StrikeAmount]
|
||||
|
||||
|
||||
PAYMENT_RESULT_MAP = {
|
||||
"PENDING": PaymentResult.PENDING,
|
||||
"COMPLETED": PaymentResult.SETTLED,
|
||||
"FAILED": PaymentResult.FAILED,
|
||||
}
|
||||
|
||||
|
||||
INVOICE_RESULT_MAP = {
|
||||
"PENDING": PaymentResult.PENDING,
|
||||
"UNPAID": PaymentResult.PENDING,
|
||||
"PAID": PaymentResult.SETTLED,
|
||||
"CANCELLED": PaymentResult.FAILED,
|
||||
}
|
||||
|
||||
|
||||
class StrikeWallet(LightningBackend):
|
||||
"""https://docs.strike.me/api/"""
|
||||
|
||||
supported_units = [Unit.sat, Unit.usd, Unit.eur]
|
||||
supported_units = set([Unit.sat, Unit.usd, Unit.eur])
|
||||
supports_description: bool = False
|
||||
currency_map = {Unit.sat: "BTC", Unit.usd: "USD", Unit.eur: "EUR"}
|
||||
|
||||
def fee_int(
|
||||
self, strike_quote: Union[StrikePaymentQuoteResponse, StrikePaymentResponse]
|
||||
) -> int:
|
||||
fee_str = strike_quote.totalFee.amount
|
||||
if strike_quote.totalFee.currency == self.currency_map[Unit.sat]:
|
||||
fee = int(float(fee_str) * 1e8)
|
||||
elif strike_quote.totalFee.currency in [
|
||||
self.currency_map[Unit.usd],
|
||||
self.currency_map[Unit.eur],
|
||||
]:
|
||||
fee = int(float(fee_str) * 100)
|
||||
return fee
|
||||
|
||||
def __init__(self, unit: Unit, **kwargs):
|
||||
self.assert_unit_supported(unit)
|
||||
self.unit = unit
|
||||
@@ -98,45 +178,29 @@ class StrikeWallet(LightningBackend):
|
||||
|
||||
payload = {
|
||||
"correlationId": secrets.token_hex(16),
|
||||
"description": "Invoice for order 123",
|
||||
"description": memo or "Invoice for order 123",
|
||||
"amount": {"amount": amount.to_float_string(), "currency": self.currency},
|
||||
}
|
||||
try:
|
||||
r = await self.client.post(url=f"{self.endpoint}/v1/invoices", json=payload)
|
||||
r.raise_for_status()
|
||||
except Exception:
|
||||
return InvoiceResponse(
|
||||
paid=False,
|
||||
checking_id=None,
|
||||
payment_request=None,
|
||||
error_message=r.json()["detail"],
|
||||
)
|
||||
return InvoiceResponse(ok=False, error_message=r.json()["detail"])
|
||||
|
||||
quote = r.json()
|
||||
invoice_id = quote.get("invoiceId")
|
||||
invoice = StrikeCreateInvoiceResponse.parse_obj(r.json())
|
||||
|
||||
try:
|
||||
payload = {"descriptionHash": secrets.token_hex(32)}
|
||||
r2 = await self.client.post(
|
||||
f"{self.endpoint}/v1/invoices/{invoice_id}/quote", json=payload
|
||||
f"{self.endpoint}/v1/invoices/{invoice.invoiceId}/quote", json=payload
|
||||
)
|
||||
r2.raise_for_status()
|
||||
except Exception:
|
||||
return InvoiceResponse(
|
||||
paid=False,
|
||||
checking_id=None,
|
||||
payment_request=None,
|
||||
error_message=r.json()["detail"],
|
||||
)
|
||||
return InvoiceResponse(ok=False, error_message=r.json()["detail"])
|
||||
|
||||
data2 = r2.json()
|
||||
payment_request = data2.get("lnInvoice")
|
||||
assert payment_request, "Did not receive an invoice"
|
||||
checking_id = invoice_id
|
||||
quote = InvoiceQuoteResponse.parse_obj(r2.json())
|
||||
return InvoiceResponse(
|
||||
ok=True,
|
||||
checking_id=checking_id,
|
||||
payment_request=payment_request,
|
||||
error_message=None,
|
||||
ok=True, checking_id=invoice.invoiceId, payment_request=quote.lnInvoice
|
||||
)
|
||||
|
||||
async def get_payment_quote(
|
||||
@@ -153,13 +217,18 @@ class StrikeWallet(LightningBackend):
|
||||
except Exception:
|
||||
error_message = r.json()["data"]["message"]
|
||||
raise Exception(error_message)
|
||||
data = r.json()
|
||||
strike_quote = StrikePaymentQuoteResponse.parse_obj(r.json())
|
||||
if strike_quote.amount.currency != self.currency_map[self.unit]:
|
||||
raise Exception(
|
||||
f"Expected currency {self.currency_map[self.unit]}, got {strike_quote.amount.currency}"
|
||||
)
|
||||
amount = Amount.from_float(float(strike_quote.amount.amount), self.unit)
|
||||
fee = self.fee_int(strike_quote)
|
||||
|
||||
amount = Amount.from_float(float(data.get("amount").get("amount")), self.unit)
|
||||
quote = PaymentQuoteResponse(
|
||||
amount=amount,
|
||||
checking_id=data.get("paymentQuoteId"),
|
||||
fee=Amount(self.unit, 0),
|
||||
checking_id=strike_quote.paymentQuoteId,
|
||||
fee=Amount(self.unit, fee),
|
||||
)
|
||||
return quote
|
||||
|
||||
@@ -176,49 +245,42 @@ class StrikeWallet(LightningBackend):
|
||||
except Exception:
|
||||
error_message = r.json()["data"]["message"]
|
||||
return PaymentResponse(
|
||||
ok=None,
|
||||
checking_id=None,
|
||||
fee=None,
|
||||
preimage=None,
|
||||
error_message=error_message,
|
||||
result=PaymentResult.FAILED, error_message=error_message
|
||||
)
|
||||
|
||||
data = r.json()
|
||||
states = {"PENDING": None, "COMPLETED": True, "FAILED": False}
|
||||
if states[data.get("state")]:
|
||||
return PaymentResponse(
|
||||
ok=True, checking_id=None, fee=None, preimage=None, error_message=None
|
||||
)
|
||||
else:
|
||||
return PaymentResponse(
|
||||
ok=False, checking_id=None, fee=None, preimage=None, error_message=None
|
||||
)
|
||||
payment = StrikePaymentResponse.parse_obj(r.json())
|
||||
fee = self.fee_int(payment)
|
||||
return PaymentResponse(
|
||||
result=PAYMENT_RESULT_MAP[payment.state],
|
||||
checking_id=payment.paymentId,
|
||||
fee=Amount(self.unit, fee),
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
r = await self.client.get(url=f"{self.endpoint}/v1/invoices/{checking_id}")
|
||||
r.raise_for_status()
|
||||
except Exception:
|
||||
return PaymentStatus(paid=None)
|
||||
except Exception as e:
|
||||
return PaymentStatus(result=PaymentResult.UNKNOWN, error_message=str(e))
|
||||
data = r.json()
|
||||
states = {"PENDING": None, "UNPAID": None, "PAID": True, "CANCELLED": False}
|
||||
return PaymentStatus(paid=states[data["state"]])
|
||||
return PaymentStatus(result=INVOICE_RESULT_MAP[data.get("state")])
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
r = await self.client.get(url=f"{self.endpoint}/v1/payments/{checking_id}")
|
||||
r.raise_for_status()
|
||||
except Exception:
|
||||
return PaymentStatus(paid=None)
|
||||
data = r.json()
|
||||
if "paid" not in data and "details" not in data:
|
||||
return PaymentStatus(paid=None)
|
||||
payment = StrikePaymentResponse.parse_obj(r.json())
|
||||
fee = self.fee_int(payment)
|
||||
return PaymentStatus(
|
||||
result=PAYMENT_RESULT_MAP[payment.state],
|
||||
fee=Amount(self.unit, fee),
|
||||
)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
if exc.response.status_code != 404:
|
||||
raise exc
|
||||
return PaymentStatus(
|
||||
result=PaymentResult.UNKNOWN, error_message=exc.response.text
|
||||
)
|
||||
|
||||
return PaymentStatus(
|
||||
paid=data["paid"],
|
||||
fee_msat=data["details"]["fee"],
|
||||
preimage=data["preimage"],
|
||||
)
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: # type: ignore
|
||||
raise NotImplementedError("paid_invoices_stream not implemented")
|
||||
|
||||
@@ -201,16 +201,6 @@ class LedgerCrud(ABC):
|
||||
) -> None:
|
||||
...
|
||||
|
||||
# @abstractmethod
|
||||
# async def update_mint_quote_paid(
|
||||
# self,
|
||||
# *,
|
||||
# quote_id: str,
|
||||
# paid: bool,
|
||||
# db: Database,
|
||||
# conn: Optional[Connection] = None,
|
||||
# ) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
async def store_melt_quote(
|
||||
self,
|
||||
@@ -233,6 +223,16 @@ class LedgerCrud(ABC):
|
||||
) -> Optional[MeltQuote]:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def get_melt_quote_by_request(
|
||||
self,
|
||||
*,
|
||||
request: str,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Optional[MeltQuote]:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def update_melt_quote(
|
||||
self,
|
||||
@@ -433,8 +433,8 @@ class LedgerCrudSqlite(LedgerCrud):
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
INSERT INTO {db.table_with_schema('mint_quotes')}
|
||||
(quote, method, request, checking_id, unit, amount, issued, paid, state, created_time, paid_time)
|
||||
VALUES (:quote, :method, :request, :checking_id, :unit, :amount, :issued, :paid, :state, :created_time, :paid_time)
|
||||
(quote, method, request, checking_id, unit, amount, issued, state, created_time, paid_time)
|
||||
VALUES (:quote, :method, :request, :checking_id, :unit, :amount, :issued, :state, :created_time, :paid_time)
|
||||
""",
|
||||
{
|
||||
"quote": quote.quote,
|
||||
@@ -443,8 +443,7 @@ class LedgerCrudSqlite(LedgerCrud):
|
||||
"checking_id": quote.checking_id,
|
||||
"unit": quote.unit,
|
||||
"amount": quote.amount,
|
||||
"issued": quote.issued,
|
||||
"paid": quote.paid,
|
||||
"issued": quote.issued, # this is deprecated! we need to store it because we have a NOT NULL constraint | we could also remove the column but sqlite doesn't support that (we would have to make a new table)
|
||||
"state": quote.state.name,
|
||||
"created_time": db.to_timestamp(
|
||||
db.timestamp_from_seconds(quote.created_time) or ""
|
||||
@@ -513,10 +512,8 @@ class LedgerCrudSqlite(LedgerCrud):
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
f"UPDATE {db.table_with_schema('mint_quotes')} SET issued = :issued, paid = :paid, state = :state, paid_time = :paid_time WHERE quote = :quote",
|
||||
f"UPDATE {db.table_with_schema('mint_quotes')} SET state = :state, paid_time = :paid_time WHERE quote = :quote",
|
||||
{
|
||||
"issued": quote.issued,
|
||||
"paid": quote.paid,
|
||||
"state": quote.state.name,
|
||||
"paid_time": db.to_timestamp(
|
||||
db.timestamp_from_seconds(quote.paid_time) or ""
|
||||
@@ -525,23 +522,6 @@ class LedgerCrudSqlite(LedgerCrud):
|
||||
},
|
||||
)
|
||||
|
||||
# async def update_mint_quote_paid(
|
||||
# self,
|
||||
# *,
|
||||
# quote_id: str,
|
||||
# paid: bool,
|
||||
# db: Database,
|
||||
# conn: Optional[Connection] = None,
|
||||
# ) -> None:
|
||||
# await (conn or db).execute(
|
||||
# f"UPDATE {db.table_with_schema('mint_quotes')} SET paid = ? WHERE"
|
||||
# " quote = ?",
|
||||
# (
|
||||
# paid,
|
||||
# quote_id,
|
||||
# ),
|
||||
# )
|
||||
|
||||
async def store_melt_quote(
|
||||
self,
|
||||
*,
|
||||
@@ -552,8 +532,8 @@ class LedgerCrudSqlite(LedgerCrud):
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
INSERT INTO {db.table_with_schema('melt_quotes')}
|
||||
(quote, method, request, checking_id, unit, amount, fee_reserve, paid, state, created_time, paid_time, fee_paid, proof, change, expiry)
|
||||
VALUES (:quote, :method, :request, :checking_id, :unit, :amount, :fee_reserve, :paid, :state, :created_time, :paid_time, :fee_paid, :proof, :change, :expiry)
|
||||
(quote, method, request, checking_id, unit, amount, fee_reserve, state, created_time, paid_time, fee_paid, proof, change, expiry)
|
||||
VALUES (:quote, :method, :request, :checking_id, :unit, :amount, :fee_reserve, :state, :created_time, :paid_time, :fee_paid, :proof, :change, :expiry)
|
||||
""",
|
||||
{
|
||||
"quote": quote.quote,
|
||||
@@ -563,7 +543,6 @@ class LedgerCrudSqlite(LedgerCrud):
|
||||
"unit": quote.unit,
|
||||
"amount": quote.amount,
|
||||
"fee_reserve": quote.fee_reserve or 0,
|
||||
"paid": quote.paid,
|
||||
"state": quote.state.name,
|
||||
"created_time": db.to_timestamp(
|
||||
db.timestamp_from_seconds(quote.created_time) or ""
|
||||
@@ -610,8 +589,22 @@ class LedgerCrudSqlite(LedgerCrud):
|
||||
""",
|
||||
values,
|
||||
)
|
||||
if row is None:
|
||||
return None
|
||||
return MeltQuote.from_row(row) if row else None
|
||||
|
||||
async def get_melt_quote_by_request(
|
||||
self,
|
||||
*,
|
||||
request: str,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Optional[MeltQuote]:
|
||||
row = await (conn or db).fetchone(
|
||||
f"""
|
||||
SELECT * from {db.table_with_schema('melt_quotes')}
|
||||
WHERE request = :request
|
||||
""",
|
||||
{"request": request},
|
||||
)
|
||||
return MeltQuote.from_row(row) if row else None
|
||||
|
||||
async def update_melt_quote(
|
||||
@@ -623,10 +616,9 @@ class LedgerCrudSqlite(LedgerCrud):
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
UPDATE {db.table_with_schema('melt_quotes')} SET paid = :paid, state = :state, fee_paid = :fee_paid, paid_time = :paid_time, proof = :proof, change = :change WHERE quote = :quote
|
||||
UPDATE {db.table_with_schema('melt_quotes')} SET state = :state, fee_paid = :fee_paid, paid_time = :paid_time, proof = :proof, change = :change, checking_id = :checking_id WHERE quote = :quote
|
||||
""",
|
||||
{
|
||||
"paid": quote.paid,
|
||||
"state": quote.state.name,
|
||||
"fee_paid": quote.fee_paid,
|
||||
"paid_time": db.to_timestamp(
|
||||
@@ -637,6 +629,7 @@ class LedgerCrudSqlite(LedgerCrud):
|
||||
if quote.change
|
||||
else None,
|
||||
"quote": quote.quote,
|
||||
"checking_id": quote.checking_id,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -678,7 +671,7 @@ class LedgerCrudSqlite(LedgerCrud):
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> int:
|
||||
row = await (conn or db).fetchone(
|
||||
row: List = await (conn or db).fetchone(
|
||||
f"""
|
||||
SELECT * from {db.table_with_schema('balance')}
|
||||
"""
|
||||
|
||||
@@ -129,9 +129,9 @@ class DbWriteHelper:
|
||||
)
|
||||
if not quote:
|
||||
raise TransactionError("Mint quote not found.")
|
||||
if quote.state == MintQuoteState.pending:
|
||||
if quote.pending:
|
||||
raise TransactionError("Mint quote already pending.")
|
||||
if not quote.state == MintQuoteState.paid:
|
||||
if not quote.paid:
|
||||
raise TransactionError("Mint quote is not paid yet.")
|
||||
# set the quote as pending
|
||||
quote.state = MintQuoteState.pending
|
||||
@@ -181,15 +181,15 @@ class DbWriteHelper:
|
||||
quote_copy = quote.copy()
|
||||
async with self.db.get_connection(
|
||||
lock_table="melt_quotes",
|
||||
lock_select_statement=f"checking_id='{quote.checking_id}'",
|
||||
lock_select_statement=f"quote='{quote.quote}'",
|
||||
) as conn:
|
||||
# get melt quote from db and check if it is already pending
|
||||
quote_db = await self.crud.get_melt_quote(
|
||||
checking_id=quote.checking_id, db=self.db, conn=conn
|
||||
quote_id=quote.quote, db=self.db, conn=conn
|
||||
)
|
||||
if not quote_db:
|
||||
raise TransactionError("Melt quote not found.")
|
||||
if quote_db.state == MeltQuoteState.pending:
|
||||
if quote_db.pending:
|
||||
raise TransactionError("Melt quote already pending.")
|
||||
# set the quote as pending
|
||||
quote_copy.state = MeltQuoteState.pending
|
||||
|
||||
@@ -50,6 +50,8 @@ from ..lightning.base import (
|
||||
InvoiceResponse,
|
||||
LightningBackend,
|
||||
PaymentQuoteResponse,
|
||||
PaymentResponse,
|
||||
PaymentResult,
|
||||
PaymentStatus,
|
||||
)
|
||||
from ..mint.crud import LedgerCrudSqlite
|
||||
@@ -123,12 +125,13 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
||||
)
|
||||
status = await self.backends[method][unit].status()
|
||||
if status.error_message:
|
||||
logger.warning(
|
||||
logger.error(
|
||||
"The backend for"
|
||||
f" {self.backends[method][unit].__class__.__name__} isn't"
|
||||
f" working properly: '{status.error_message}'",
|
||||
RuntimeWarning,
|
||||
)
|
||||
exit(1)
|
||||
logger.info(f"Backend balance: {status.balance} {unit.name}")
|
||||
|
||||
logger.info(f"Data dir: {settings.cashu_dir}")
|
||||
@@ -148,40 +151,10 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
||||
)
|
||||
if not melt_quotes:
|
||||
return
|
||||
logger.info("Checking pending melt quotes")
|
||||
for quote in melt_quotes:
|
||||
# get pending proofs for quote
|
||||
pending_proofs = await self.crud.get_pending_proofs_for_quote(
|
||||
quote_id=quote.quote, db=self.db
|
||||
)
|
||||
# check with the backend whether the quote has been paid during downtime
|
||||
payment = await self.backends[Method[quote.method]][
|
||||
Unit[quote.unit]
|
||||
].get_payment_status(quote.checking_id)
|
||||
if payment.paid:
|
||||
logger.info(f"Melt quote {quote.quote} state: paid")
|
||||
quote.paid_time = int(time.time())
|
||||
quote.paid = True
|
||||
quote.state = MeltQuoteState.paid
|
||||
if payment.fee:
|
||||
quote.fee_paid = payment.fee.to(Unit[quote.unit]).amount
|
||||
quote.payment_preimage = payment.preimage or ""
|
||||
await self.crud.update_melt_quote(quote=quote, db=self.db)
|
||||
# invalidate proofs
|
||||
await self._invalidate_proofs(
|
||||
proofs=pending_proofs, quote_id=quote.quote
|
||||
)
|
||||
# unset pending
|
||||
await self.db_write._unset_proofs_pending(pending_proofs)
|
||||
elif payment.failed:
|
||||
logger.info(f"Melt quote {quote.quote} state: failed")
|
||||
# unset pending
|
||||
await self.db_write._unset_proofs_pending(pending_proofs, spent=False)
|
||||
elif payment.pending:
|
||||
logger.info(f"Melt quote {quote.quote} state: pending")
|
||||
pass
|
||||
else:
|
||||
logger.error("Melt quote state unknown")
|
||||
pass
|
||||
quote = await self.get_melt_quote(quote_id=quote.quote, purge_unknown=True)
|
||||
logger.info(f"Melt quote {quote.quote} state: {quote.state}")
|
||||
|
||||
# ------- KEYS -------
|
||||
|
||||
@@ -447,8 +420,6 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
||||
checking_id=invoice_response.checking_id,
|
||||
unit=quote_request.unit,
|
||||
amount=quote_request.amount,
|
||||
issued=False,
|
||||
paid=False,
|
||||
state=MintQuoteState.unpaid,
|
||||
created_time=int(time.time()),
|
||||
expiry=expiry,
|
||||
@@ -476,14 +447,14 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
||||
|
||||
unit, method = self._verify_and_get_unit_method(quote.unit, quote.method)
|
||||
|
||||
if quote.state == MintQuoteState.unpaid:
|
||||
if quote.unpaid:
|
||||
if not quote.checking_id:
|
||||
raise CashuError("quote has no checking id")
|
||||
logger.trace(f"Lightning: checking invoice {quote.checking_id}")
|
||||
status: PaymentStatus = await self.backends[method][
|
||||
unit
|
||||
].get_invoice_status(quote.checking_id)
|
||||
if status.paid:
|
||||
if status.settled:
|
||||
# change state to paid in one transaction, it could have been marked paid
|
||||
# by the invoice listener in the mean time
|
||||
async with self.db.get_connection(
|
||||
@@ -495,9 +466,8 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
||||
)
|
||||
if not quote:
|
||||
raise Exception("quote not found")
|
||||
if quote.state == MintQuoteState.unpaid:
|
||||
if quote.unpaid:
|
||||
logger.trace(f"Setting quote {quote_id} as paid")
|
||||
quote.paid = True
|
||||
quote.state = MintQuoteState.paid
|
||||
quote.paid_time = int(time.time())
|
||||
await self.crud.update_mint_quote(
|
||||
@@ -537,11 +507,11 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
||||
output_unit = self.keysets[outputs[0].id].unit
|
||||
|
||||
quote = await self.get_mint_quote(quote_id)
|
||||
if quote.state == MintQuoteState.pending:
|
||||
if quote.pending:
|
||||
raise TransactionError("Mint quote already pending.")
|
||||
if quote.state == MintQuoteState.issued:
|
||||
if quote.issued:
|
||||
raise TransactionError("Mint quote already issued.")
|
||||
if not quote.state == MintQuoteState.paid:
|
||||
if not quote.paid:
|
||||
raise QuoteNotPaidError()
|
||||
previous_state = quote.state
|
||||
await self.db_write._set_mint_quote_pending(quote_id=quote_id)
|
||||
@@ -558,7 +528,6 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
||||
quote_id=quote_id, state=previous_state
|
||||
)
|
||||
raise e
|
||||
|
||||
await self.db_write._unset_mint_quote_pending(
|
||||
quote_id=quote_id, state=MintQuoteState.issued
|
||||
)
|
||||
@@ -585,10 +554,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
||||
raise TransactionError("mint quote already paid")
|
||||
if mint_quote.issued:
|
||||
raise TransactionError("mint quote already issued")
|
||||
|
||||
if mint_quote.state == MintQuoteState.issued:
|
||||
raise TransactionError("mint quote already issued")
|
||||
if mint_quote.state != MintQuoteState.unpaid:
|
||||
if not mint_quote.unpaid:
|
||||
raise TransactionError("mint quote is not unpaid")
|
||||
|
||||
if not mint_quote.checking_id:
|
||||
@@ -660,7 +626,6 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
||||
mint_quote = await self.crud.get_mint_quote(request=request, db=self.db)
|
||||
if mint_quote:
|
||||
payment_quote = self.create_internal_melt_quote(mint_quote, melt_quote)
|
||||
|
||||
else:
|
||||
# not internal
|
||||
# verify that the backend supports mpp if the quote request has an amount
|
||||
@@ -699,7 +664,6 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
||||
checking_id=payment_quote.checking_id,
|
||||
unit=unit.name,
|
||||
amount=payment_quote.amount.to(unit).amount,
|
||||
paid=False,
|
||||
state=MeltQuoteState.unpaid,
|
||||
fee_reserve=payment_quote.fee.to(unit).amount,
|
||||
created_time=int(time.time()),
|
||||
@@ -712,20 +676,22 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
||||
quote=quote.quote,
|
||||
amount=quote.amount,
|
||||
fee_reserve=quote.fee_reserve,
|
||||
paid=quote.paid,
|
||||
paid=quote.paid, # deprecated
|
||||
state=quote.state.value,
|
||||
expiry=quote.expiry,
|
||||
)
|
||||
|
||||
async def get_melt_quote(self, quote_id: str) -> MeltQuote:
|
||||
async def get_melt_quote(self, quote_id: str, purge_unknown=False) -> MeltQuote:
|
||||
"""Returns a melt quote.
|
||||
|
||||
If melt quote is not paid yet and no internal mint quote is associated with it,
|
||||
checks with the backend for the state of the payment request. If the backend
|
||||
says that the quote has been paid, updates the melt quote in the database.
|
||||
If the melt quote is pending, checks status of the payment with the backend.
|
||||
- If settled, sets the quote as paid and invalidates pending proofs (commit).
|
||||
- If failed, sets the quote as unpaid and unsets pending proofs (rollback).
|
||||
- If purge_unknown is set, do the same for unknown states as for failed states.
|
||||
|
||||
Args:
|
||||
quote_id (str): ID of the melt quote.
|
||||
purge_unknown (bool, optional): Rollback unknown payment states to unpaid. Defaults to False.
|
||||
|
||||
Raises:
|
||||
Exception: Quote not found.
|
||||
@@ -743,21 +709,21 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
||||
|
||||
# we only check the state with the backend if there is no associated internal
|
||||
# mint quote for this melt quote
|
||||
mint_quote = await self.crud.get_mint_quote(
|
||||
is_internal = await self.crud.get_mint_quote(
|
||||
request=melt_quote.request, db=self.db
|
||||
)
|
||||
|
||||
if not melt_quote.paid and not mint_quote:
|
||||
logger.trace(
|
||||
if melt_quote.pending and not is_internal:
|
||||
logger.debug(
|
||||
"Lightning: checking outgoing Lightning payment"
|
||||
f" {melt_quote.checking_id}"
|
||||
)
|
||||
status: PaymentStatus = await self.backends[method][
|
||||
unit
|
||||
].get_payment_status(melt_quote.checking_id)
|
||||
if status.paid:
|
||||
logger.trace(f"Setting quote {quote_id} as paid")
|
||||
melt_quote.paid = True
|
||||
logger.debug(f"State: {status.result}")
|
||||
if status.settled:
|
||||
logger.debug(f"Setting quote {quote_id} as paid")
|
||||
melt_quote.state = MeltQuoteState.paid
|
||||
if status.fee:
|
||||
melt_quote.fee_paid = status.fee.to(unit).amount
|
||||
@@ -766,6 +732,20 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
||||
melt_quote.paid_time = int(time.time())
|
||||
await self.crud.update_melt_quote(quote=melt_quote, db=self.db)
|
||||
await self.events.submit(melt_quote)
|
||||
pending_proofs = await self.crud.get_pending_proofs_for_quote(
|
||||
quote_id=quote_id, db=self.db
|
||||
)
|
||||
await self._invalidate_proofs(proofs=pending_proofs, quote_id=quote_id)
|
||||
await self.db_write._unset_proofs_pending(pending_proofs)
|
||||
if status.failed or (purge_unknown and status.unknown):
|
||||
logger.debug(f"Setting quote {quote_id} as unpaid")
|
||||
melt_quote.state = MeltQuoteState.unpaid
|
||||
await self.crud.update_melt_quote(quote=melt_quote, db=self.db)
|
||||
await self.events.submit(melt_quote)
|
||||
pending_proofs = await self.crud.get_pending_proofs_for_quote(
|
||||
quote_id=quote_id, db=self.db
|
||||
)
|
||||
await self.db_write._unset_proofs_pending(pending_proofs)
|
||||
|
||||
return melt_quote
|
||||
|
||||
@@ -798,8 +778,6 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
||||
# we settle the transaction internally
|
||||
if melt_quote.paid:
|
||||
raise TransactionError("melt quote already paid")
|
||||
if melt_quote.state != MeltQuoteState.unpaid:
|
||||
raise TransactionError("melt quote already paid")
|
||||
|
||||
# verify amounts from bolt11 invoice
|
||||
bolt11_request = melt_quote.request
|
||||
@@ -830,11 +808,9 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
||||
)
|
||||
|
||||
melt_quote.fee_paid = 0 # no internal fees
|
||||
melt_quote.paid = True
|
||||
melt_quote.state = MeltQuoteState.paid
|
||||
melt_quote.paid_time = int(time.time())
|
||||
|
||||
mint_quote.paid = True
|
||||
mint_quote.state = MintQuoteState.paid
|
||||
mint_quote.paid_time = melt_quote.paid_time
|
||||
|
||||
@@ -869,14 +845,13 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
||||
"""
|
||||
# get melt quote and check if it was already paid
|
||||
melt_quote = await self.get_melt_quote(quote_id=quote)
|
||||
if not melt_quote.unpaid:
|
||||
raise TransactionError(f"melt quote is not unpaid: {melt_quote.state}")
|
||||
|
||||
unit, method = self._verify_and_get_unit_method(
|
||||
melt_quote.unit, melt_quote.method
|
||||
)
|
||||
|
||||
if melt_quote.state != MeltQuoteState.unpaid:
|
||||
raise TransactionError("melt quote already paid")
|
||||
|
||||
# make sure that the outputs (for fee return) are in the same unit as the quote
|
||||
if outputs:
|
||||
# _verify_outputs checks if all outputs have the same unit
|
||||
@@ -917,36 +892,91 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
||||
await self.db_write._verify_spent_proofs_and_set_pending(
|
||||
proofs, quote_id=melt_quote.quote
|
||||
)
|
||||
previous_state = melt_quote.state
|
||||
melt_quote = await self.db_write._set_melt_quote_pending(melt_quote)
|
||||
try:
|
||||
# settle the transaction internally if there is a mint quote with the same payment request
|
||||
# if the melt corresponds to an internal mint, mark both as paid
|
||||
melt_quote = await self.melt_mint_settle_internally(melt_quote, proofs)
|
||||
# quote not paid yet (not internal), pay it with the backend
|
||||
if not melt_quote.paid and melt_quote.state == MeltQuoteState.unpaid:
|
||||
if not melt_quote.paid:
|
||||
logger.debug(f"Lightning: pay invoice {melt_quote.request}")
|
||||
payment = await self.backends[method][unit].pay_invoice(
|
||||
melt_quote, melt_quote.fee_reserve * 1000
|
||||
)
|
||||
logger.debug(
|
||||
f"Melt – Ok: {payment.ok}: preimage: {payment.preimage},"
|
||||
f" fee: {payment.fee.str() if payment.fee is not None else 'None'}"
|
||||
)
|
||||
if not payment.ok:
|
||||
raise LightningError(
|
||||
f"Lightning payment unsuccessful. {payment.error_message}"
|
||||
try:
|
||||
payment = await self.backends[method][unit].pay_invoice(
|
||||
melt_quote, melt_quote.fee_reserve * 1000
|
||||
)
|
||||
if payment.fee:
|
||||
melt_quote.fee_paid = payment.fee.to(
|
||||
to_unit=unit, round="up"
|
||||
).amount
|
||||
if payment.preimage:
|
||||
melt_quote.payment_preimage = payment.preimage
|
||||
# set quote as paid
|
||||
melt_quote.paid = True
|
||||
melt_quote.state = MeltQuoteState.paid
|
||||
melt_quote.paid_time = int(time.time())
|
||||
logger.debug(
|
||||
f"Melt – Result: {str(payment.result)}: preimage: {payment.preimage},"
|
||||
f" fee: {payment.fee.str() if payment.fee is not None else 'None'}"
|
||||
)
|
||||
if (
|
||||
payment.checking_id
|
||||
and payment.checking_id != melt_quote.checking_id
|
||||
):
|
||||
logger.warning(
|
||||
f"pay_invoice returned different checking_id: {payment.checking_id} than melt quote: {melt_quote.checking_id}. Will use it for potentially checking payment status later."
|
||||
)
|
||||
melt_quote.checking_id = payment.checking_id
|
||||
await self.crud.update_melt_quote(quote=melt_quote, db=self.db)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Exception during pay_invoice: {e}")
|
||||
payment = PaymentResponse(
|
||||
result=PaymentResult.UNKNOWN,
|
||||
error_message=str(e),
|
||||
)
|
||||
|
||||
match payment.result:
|
||||
case PaymentResult.FAILED | PaymentResult.UNKNOWN:
|
||||
# explicitly check payment status for failed or unknown payment states
|
||||
checking_id = payment.checking_id or melt_quote.checking_id
|
||||
logger.debug(
|
||||
f"Payment state is {payment.result}. Checking status for {checking_id}"
|
||||
)
|
||||
try:
|
||||
status = await self.backends[method][
|
||||
unit
|
||||
].get_payment_status(checking_id)
|
||||
except Exception as e:
|
||||
# Something went wrong, better to keep the proofs in pending state
|
||||
logger.error(
|
||||
f"Lightning backend error: could not check payment status. Proofs for melt quote {melt_quote.quote} are stuck as PENDING. Error: {e}"
|
||||
)
|
||||
return PostMeltQuoteResponse.from_melt_quote(melt_quote)
|
||||
|
||||
match status.result:
|
||||
case PaymentResult.FAILED | PaymentResult.UNKNOWN:
|
||||
# NOTE: We only throw a payment error if the payment AND a subsequent status check failed
|
||||
raise LightningError(
|
||||
f"Lightning payment failed: {payment.error_message}. Error: {status.error_message}"
|
||||
)
|
||||
case _:
|
||||
logger.error(
|
||||
f"Payment state is {status.result} and payment was {payment.result}. Proofs for melt quote {melt_quote.quote} are stuck as PENDING."
|
||||
)
|
||||
return PostMeltQuoteResponse.from_melt_quote(melt_quote)
|
||||
|
||||
case PaymentResult.SETTLED:
|
||||
# payment successful
|
||||
if payment.fee:
|
||||
melt_quote.fee_paid = payment.fee.to(
|
||||
to_unit=unit, round="up"
|
||||
).amount
|
||||
if payment.preimage:
|
||||
melt_quote.payment_preimage = payment.preimage
|
||||
# set quote as paid
|
||||
melt_quote.state = MeltQuoteState.paid
|
||||
melt_quote.paid_time = int(time.time())
|
||||
# NOTE: This is the only return point for a successful payment
|
||||
|
||||
case PaymentResult.PENDING | _:
|
||||
logger.debug(
|
||||
f"Lightning payment is pending: {payment.checking_id}"
|
||||
)
|
||||
return PostMeltQuoteResponse.from_melt_quote(melt_quote)
|
||||
|
||||
# melt successful, invalidate proofs
|
||||
await self._invalidate_proofs(proofs=proofs, quote_id=melt_quote.quote)
|
||||
await self.db_write._unset_proofs_pending(proofs)
|
||||
|
||||
# prepare change to compensate wallet for overpaid fees
|
||||
return_promises: List[BlindedSignature] = []
|
||||
@@ -963,14 +993,15 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
||||
await self.crud.update_melt_quote(quote=melt_quote, db=self.db)
|
||||
await self.events.submit(melt_quote)
|
||||
|
||||
except Exception as e:
|
||||
logger.trace(f"Melt exception: {e}")
|
||||
raise e
|
||||
finally:
|
||||
# delete proofs from pending list
|
||||
await self.db_write._unset_proofs_pending(proofs)
|
||||
return PostMeltQuoteResponse.from_melt_quote(melt_quote)
|
||||
|
||||
return PostMeltQuoteResponse.from_melt_quote(melt_quote)
|
||||
except Exception as e:
|
||||
logger.trace(f"Payment has failed: {e}")
|
||||
await self.db_write._unset_proofs_pending(proofs)
|
||||
await self.db_write._unset_melt_quote_pending(
|
||||
quote=melt_quote, state=previous_state
|
||||
)
|
||||
raise e
|
||||
|
||||
async def swap(
|
||||
self,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import copy
|
||||
from typing import Dict, List
|
||||
|
||||
from ..core.base import MintKeyset, Proof
|
||||
from ..core.crypto.keys import derive_keyset_id, derive_keyset_id_deprecated
|
||||
@@ -287,7 +288,6 @@ async def m011_add_quote_tables(db: Database):
|
||||
checking_id TEXT NOT NULL,
|
||||
unit TEXT NOT NULL,
|
||||
amount {db.big_int} NOT NULL,
|
||||
paid BOOL NOT NULL,
|
||||
issued BOOL NOT NULL,
|
||||
created_time TIMESTAMP,
|
||||
paid_time TIMESTAMP,
|
||||
@@ -296,6 +296,7 @@ async def m011_add_quote_tables(db: Database):
|
||||
|
||||
);
|
||||
"""
|
||||
# NOTE: We remove the paid BOOL NOT NULL column
|
||||
)
|
||||
|
||||
await conn.execute(
|
||||
@@ -308,7 +309,6 @@ async def m011_add_quote_tables(db: Database):
|
||||
unit TEXT NOT NULL,
|
||||
amount {db.big_int} NOT NULL,
|
||||
fee_reserve {db.big_int},
|
||||
paid BOOL NOT NULL,
|
||||
created_time TIMESTAMP,
|
||||
paid_time TIMESTAMP,
|
||||
fee_paid {db.big_int},
|
||||
@@ -318,13 +318,14 @@ async def m011_add_quote_tables(db: Database):
|
||||
|
||||
);
|
||||
"""
|
||||
# NOTE: We remove the paid BOOL NOT NULL column
|
||||
)
|
||||
|
||||
await conn.execute(
|
||||
f"INSERT INTO {db.table_with_schema('mint_quotes')} (quote, method,"
|
||||
" request, checking_id, unit, amount, paid, issued, created_time,"
|
||||
" request, checking_id, unit, amount, issued, created_time,"
|
||||
" paid_time) SELECT id, 'bolt11', bolt11, COALESCE(payment_hash, 'None'),"
|
||||
f" 'sat', amount, False, issued, COALESCE(created, '{db.timestamp_now_str()}'),"
|
||||
f" 'sat', amount, issued, COALESCE(created, '{db.timestamp_now_str()}'),"
|
||||
f" NULL FROM {db.table_with_schema('invoices')} "
|
||||
)
|
||||
|
||||
@@ -788,13 +789,13 @@ async def m020_add_state_to_mint_and_melt_quotes(db: Database):
|
||||
# and the `paid` and `issued` column respectively
|
||||
# mint quotes:
|
||||
async with db.connect() as conn:
|
||||
rows = await conn.fetchall(
|
||||
rows: List[Dict] = await conn.fetchall(
|
||||
f"SELECT * FROM {db.table_with_schema('mint_quotes')}"
|
||||
)
|
||||
for row in rows:
|
||||
if row["issued"]:
|
||||
if row.get("issued"):
|
||||
state = "issued"
|
||||
elif row["paid"]:
|
||||
elif row.get("paid"):
|
||||
state = "paid"
|
||||
else:
|
||||
state = "unpaid"
|
||||
@@ -804,10 +805,10 @@ async def m020_add_state_to_mint_and_melt_quotes(db: Database):
|
||||
|
||||
# melt quotes:
|
||||
async with db.connect() as conn:
|
||||
rows = await conn.fetchall(
|
||||
rows2: List[Dict] = await conn.fetchall(
|
||||
f"SELECT * FROM {db.table_with_schema('melt_quotes')}"
|
||||
)
|
||||
for row in rows:
|
||||
for row in rows2:
|
||||
if row["paid"]:
|
||||
state = "paid"
|
||||
else:
|
||||
|
||||
@@ -168,7 +168,7 @@ async def mint_quote(
|
||||
resp = PostMintQuoteResponse(
|
||||
request=quote.request,
|
||||
quote=quote.quote,
|
||||
paid=quote.paid,
|
||||
paid=quote.paid, # deprecated
|
||||
state=quote.state.value,
|
||||
expiry=quote.expiry,
|
||||
)
|
||||
@@ -192,7 +192,7 @@ async def get_mint_quote(request: Request, quote: str) -> PostMintQuoteResponse:
|
||||
resp = PostMintQuoteResponse(
|
||||
quote=mint_quote.quote,
|
||||
request=mint_quote.request,
|
||||
paid=mint_quote.paid,
|
||||
paid=mint_quote.paid, # deprecated
|
||||
state=mint_quote.state.value,
|
||||
expiry=mint_quote.expiry,
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Dict, List, Optional
|
||||
from fastapi import APIRouter, Request
|
||||
from loguru import logger
|
||||
|
||||
from ..core.base import BlindedMessage, BlindedSignature, ProofSpentState
|
||||
from ..core.base import BlindedMessage, BlindedSignature
|
||||
from ..core.errors import CashuError
|
||||
from ..core.models import (
|
||||
CheckFeesRequest_deprecated,
|
||||
@@ -345,13 +345,13 @@ async def check_spendable_deprecated(
|
||||
spendableList: List[bool] = []
|
||||
pendingList: List[bool] = []
|
||||
for proof_state in proofs_state:
|
||||
if proof_state.state == ProofSpentState.unspent:
|
||||
if proof_state.unspent:
|
||||
spendableList.append(True)
|
||||
pendingList.append(False)
|
||||
elif proof_state.state == ProofSpentState.spent:
|
||||
elif proof_state.spent:
|
||||
spendableList.append(False)
|
||||
pendingList.append(False)
|
||||
elif proof_state.state == ProofSpentState.pending:
|
||||
elif proof_state.pending:
|
||||
spendableList.append(True)
|
||||
pendingList.append(True)
|
||||
return CheckSpendableResponse_deprecated(
|
||||
|
||||
@@ -56,8 +56,7 @@ class LedgerTasks(SupportsDb, SupportsBackends, SupportsEvents):
|
||||
f"Invoice callback dispatcher: quote {quote} trying to set as {MintQuoteState.paid}"
|
||||
)
|
||||
# set the quote as paid
|
||||
if quote.state == MintQuoteState.unpaid:
|
||||
quote.paid = True
|
||||
if quote.unpaid:
|
||||
quote.state = MintQuoteState.paid
|
||||
await self.crud.update_mint_quote(quote=quote, db=self.db, conn=conn)
|
||||
logger.trace(
|
||||
|
||||
@@ -222,7 +222,7 @@ async def pay(
|
||||
print(" Error: Balance too low.")
|
||||
return
|
||||
send_proofs, fees = await wallet.select_to_send(
|
||||
wallet.proofs, total_amount, include_fees=True
|
||||
wallet.proofs, total_amount, include_fees=True, set_reserved=True
|
||||
)
|
||||
try:
|
||||
melt_response = await wallet.melt(
|
||||
@@ -231,11 +231,25 @@ async def pay(
|
||||
except Exception as e:
|
||||
print(f" Error paying invoice: {str(e)}")
|
||||
return
|
||||
print(" Invoice paid", end="", flush=True)
|
||||
if melt_response.payment_preimage and melt_response.payment_preimage != "0" * 64:
|
||||
print(f" (Preimage: {melt_response.payment_preimage}).")
|
||||
if (
|
||||
melt_response.state
|
||||
and MintQuoteState(melt_response.state) == MintQuoteState.paid
|
||||
):
|
||||
print(" Invoice paid", end="", flush=True)
|
||||
if (
|
||||
melt_response.payment_preimage
|
||||
and melt_response.payment_preimage != "0" * 64
|
||||
):
|
||||
print(f" (Preimage: {melt_response.payment_preimage}).")
|
||||
else:
|
||||
print(".")
|
||||
elif MintQuoteState(melt_response.state) == MintQuoteState.pending:
|
||||
print(" Invoice pending.")
|
||||
elif MintQuoteState(melt_response.state) == MintQuoteState.unpaid:
|
||||
print(" Invoice unpaid.")
|
||||
else:
|
||||
print(".")
|
||||
print(" Error paying invoice.")
|
||||
|
||||
await print_balance(ctx)
|
||||
|
||||
|
||||
|
||||
@@ -2,12 +2,13 @@ from typing import Optional
|
||||
|
||||
import bolt11
|
||||
|
||||
from ...core.base import Amount, ProofSpentState, Unit
|
||||
from ...core.base import Amount, Unit
|
||||
from ...core.helpers import sum_promises
|
||||
from ...core.settings import settings
|
||||
from ...lightning.base import (
|
||||
InvoiceResponse,
|
||||
PaymentResponse,
|
||||
PaymentResult,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
)
|
||||
@@ -58,14 +59,14 @@ class LightningWallet(Wallet):
|
||||
pr (str): bolt11 payment request
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
PaymentResponse: containing details of the operation
|
||||
"""
|
||||
quote = await self.melt_quote(pr)
|
||||
total_amount = quote.amount + quote.fee_reserve
|
||||
assert total_amount > 0, "amount is not positive"
|
||||
if self.available_balance < total_amount:
|
||||
print("Error: Balance too low.")
|
||||
return PaymentResponse(ok=False)
|
||||
return PaymentResponse(result=PaymentResult.FAILED)
|
||||
_, send_proofs = await self.swap_to_send(self.proofs, total_amount)
|
||||
try:
|
||||
resp = await self.melt(send_proofs, pr, quote.fee_reserve, quote.quote)
|
||||
@@ -76,14 +77,14 @@ class LightningWallet(Wallet):
|
||||
|
||||
invoice_obj = bolt11.decode(pr)
|
||||
return PaymentResponse(
|
||||
ok=True,
|
||||
result=PaymentResult.SETTLED,
|
||||
checking_id=invoice_obj.payment_hash,
|
||||
preimage=resp.payment_preimage,
|
||||
fee=Amount(Unit.msat, fees_paid_sat),
|
||||
)
|
||||
except Exception as e:
|
||||
print("Exception:", e)
|
||||
return PaymentResponse(ok=False, error_message=str(e))
|
||||
return PaymentResponse(result=PaymentResult.FAILED, error_message=str(e))
|
||||
|
||||
async def get_invoice_status(self, payment_hash: str) -> PaymentStatus:
|
||||
"""Get lightning invoice status (incoming)
|
||||
@@ -98,16 +99,16 @@ class LightningWallet(Wallet):
|
||||
db=self.db, payment_hash=payment_hash, out=False
|
||||
)
|
||||
if not invoice:
|
||||
return PaymentStatus(paid=None)
|
||||
return PaymentStatus(result=PaymentResult.UNKNOWN)
|
||||
if invoice.paid:
|
||||
return PaymentStatus(paid=True)
|
||||
return PaymentStatus(result=PaymentResult.SETTLED)
|
||||
try:
|
||||
# to check the invoice state, we try minting tokens
|
||||
await self.mint(invoice.amount, id=invoice.id)
|
||||
return PaymentStatus(paid=True)
|
||||
return PaymentStatus(result=PaymentResult.SETTLED)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return PaymentStatus(paid=False)
|
||||
return PaymentStatus(result=PaymentResult.FAILED)
|
||||
|
||||
async def get_payment_status(self, payment_hash: str) -> PaymentStatus:
|
||||
"""Get lightning payment status (outgoing)
|
||||
@@ -126,24 +127,30 @@ class LightningWallet(Wallet):
|
||||
)
|
||||
|
||||
if not invoice:
|
||||
return PaymentStatus(paid=False) # "invoice not found (in db)"
|
||||
return PaymentStatus(
|
||||
result=PaymentResult.FAILED
|
||||
) # "invoice not found (in db)"
|
||||
if invoice.paid:
|
||||
return PaymentStatus(paid=True, preimage=invoice.preimage) # "paid (in db)"
|
||||
return PaymentStatus(
|
||||
result=PaymentResult.SETTLED, preimage=invoice.preimage
|
||||
) # "paid (in db)"
|
||||
proofs = await get_proofs(db=self.db, melt_id=invoice.id)
|
||||
if not proofs:
|
||||
return PaymentStatus(paid=False) # "proofs not fount (in db)"
|
||||
return PaymentStatus(
|
||||
result=PaymentResult.FAILED
|
||||
) # "proofs not fount (in db)"
|
||||
proofs_states = await self.check_proof_state(proofs)
|
||||
if not proofs_states:
|
||||
return PaymentStatus(paid=False) # "states not fount"
|
||||
return PaymentStatus(result=PaymentResult.FAILED) # "states not fount"
|
||||
|
||||
if all([p.state == ProofSpentState.pending for p in proofs_states.states]):
|
||||
return PaymentStatus(paid=None) # "pending (with check)"
|
||||
if any([p.state == ProofSpentState.spent for p in proofs_states.states]):
|
||||
if all([p.state.pending for p in proofs_states.states]):
|
||||
return PaymentStatus(result=PaymentResult.PENDING) # "pending (with check)"
|
||||
if any([p.state.spent for p in proofs_states.states]):
|
||||
# NOTE: consider adding this check in wallet.py and mark the invoice as paid if all proofs are spent
|
||||
return PaymentStatus(paid=True) # "paid (with check)"
|
||||
if all([p.state == ProofSpentState.unspent for p in proofs_states.states]):
|
||||
return PaymentStatus(paid=False) # "failed (with check)"
|
||||
return PaymentStatus(paid=None) # "undefined state"
|
||||
return PaymentStatus(result=PaymentResult.SETTLED) # "paid (with check)"
|
||||
if all([p.state.unspent for p in proofs_states.states]):
|
||||
return PaymentStatus(result=PaymentResult.FAILED) # "failed (with check)"
|
||||
return PaymentStatus(result=PaymentResult.UNKNOWN) # "undefined state"
|
||||
|
||||
async def get_balance(self) -> StatusResponse:
|
||||
"""Get lightning balance
|
||||
|
||||
@@ -13,8 +13,8 @@ from ..core.base import (
|
||||
BlindedSignature,
|
||||
DLEQWallet,
|
||||
Invoice,
|
||||
MeltQuoteState,
|
||||
Proof,
|
||||
ProofSpentState,
|
||||
Unit,
|
||||
WalletKeyset,
|
||||
)
|
||||
@@ -759,15 +759,18 @@ class Wallet(
|
||||
status = await super().melt(quote_id, proofs, change_outputs)
|
||||
|
||||
# if payment fails
|
||||
if not status.paid:
|
||||
# remove the melt_id in proofs
|
||||
if MeltQuoteState(status.state) == MeltQuoteState.unpaid:
|
||||
# remove the melt_id in proofs and set reserved to False
|
||||
for p in proofs:
|
||||
p.melt_id = None
|
||||
await update_proof(p, melt_id=None, db=self.db)
|
||||
p.reserved = False
|
||||
await update_proof(p, melt_id="", db=self.db)
|
||||
raise Exception("could not pay invoice.")
|
||||
elif MeltQuoteState(status.state) == MeltQuoteState.pending:
|
||||
# payment is still pending
|
||||
return status
|
||||
|
||||
# invoice was paid successfully
|
||||
|
||||
await self.invalidate(proofs)
|
||||
|
||||
# update paid status in db
|
||||
@@ -995,7 +998,7 @@ class Wallet(
|
||||
if check_spendable:
|
||||
proof_states = await self.check_proof_state(proofs)
|
||||
for i, state in enumerate(proof_states.states):
|
||||
if state.state == ProofSpentState.spent:
|
||||
if state.spent:
|
||||
invalidated_proofs.append(proofs[i])
|
||||
else:
|
||||
invalidated_proofs = proofs
|
||||
|
||||
2
mypy.ini
2
mypy.ini
@@ -1,5 +1,5 @@
|
||||
[mypy]
|
||||
python_version = 3.9
|
||||
python_version = 3.10
|
||||
# disallow_untyped_defs = True
|
||||
; check_untyped_defs = True
|
||||
ignore_missing_imports = True
|
||||
|
||||
@@ -48,7 +48,7 @@ settings.mint_seed_decryption_key = ""
|
||||
settings.mint_max_balance = 0
|
||||
settings.mint_transaction_rate_limit_per_minute = 60
|
||||
settings.mint_lnd_enable_mpp = True
|
||||
settings.mint_clnrest_enable_mpp = False
|
||||
settings.mint_clnrest_enable_mpp = True
|
||||
settings.mint_input_fee_ppk = 0
|
||||
settings.db_connection_pool = True
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ docker_lightning_unconnected_cli = [
|
||||
"--rpcserver=lnd-2",
|
||||
]
|
||||
|
||||
|
||||
def docker_clightning_cli(index):
|
||||
return [
|
||||
"docker",
|
||||
@@ -104,9 +105,9 @@ def docker_clightning_cli(index):
|
||||
"lightning-cli",
|
||||
"--network",
|
||||
"regtest",
|
||||
"--keywords",
|
||||
]
|
||||
|
||||
|
||||
def run_cmd(cmd: list) -> str:
|
||||
timeout = 20
|
||||
process = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
||||
@@ -171,11 +172,22 @@ def pay_real_invoice(invoice: str) -> str:
|
||||
cmd.extend(["payinvoice", "--force", invoice])
|
||||
return run_cmd(cmd)
|
||||
|
||||
|
||||
def partial_pay_real_invoice(invoice: str, amount: int, node: int) -> str:
|
||||
cmd = docker_clightning_cli(node)
|
||||
cmd.extend(["pay", f"bolt11={invoice}", f"partial_msat={amount*1000}"])
|
||||
return run_cmd(cmd)
|
||||
|
||||
|
||||
def get_real_invoice_cln(sats: int) -> str:
|
||||
cmd = docker_clightning_cli(1)
|
||||
cmd.extend(
|
||||
["invoice", f"{sats*1000}", hashlib.sha256(os.urandom(32)).hexdigest(), "test"]
|
||||
)
|
||||
result = run_cmd_json(cmd)
|
||||
return result["bolt11"]
|
||||
|
||||
|
||||
def mine_blocks(blocks: int = 1) -> str:
|
||||
cmd = docker_bitcoin_cli.copy()
|
||||
cmd.extend(["-generate", str(blocks)])
|
||||
|
||||
@@ -2,7 +2,7 @@ import asyncio
|
||||
import datetime
|
||||
import os
|
||||
import time
|
||||
from typing import List
|
||||
from typing import List, Tuple
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
@@ -63,7 +63,8 @@ async def test_db_tables(ledger: Ledger):
|
||||
"SELECT table_name FROM information_schema.tables WHERE table_schema ="
|
||||
" 'public';"
|
||||
)
|
||||
tables = [t[0] for t in tables_res.all()]
|
||||
tables_all: List[Tuple[str]] = tables_res.all()
|
||||
tables = [t[0] for t in tables_all]
|
||||
tables_expected = [
|
||||
"dbversions",
|
||||
"keysets",
|
||||
|
||||
@@ -3,7 +3,7 @@ import httpx
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import MeltQuoteState, MintQuoteState, ProofSpentState
|
||||
from cashu.core.base import MeltQuoteState, MintQuoteState
|
||||
from cashu.core.models import (
|
||||
GetInfoResponse,
|
||||
MintMethodSetting,
|
||||
@@ -478,7 +478,7 @@ async def test_api_check_state(ledger: Ledger):
|
||||
response = PostCheckStateResponse.parse_obj(response.json())
|
||||
assert response
|
||||
assert len(response.states) == 2
|
||||
assert response.states[0].state == ProofSpentState.unspent
|
||||
assert response.states[0].state.unspent
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import MeltQuoteState, MintQuoteState, ProofSpentState
|
||||
from cashu.core.base import MeltQuoteState, MintQuoteState
|
||||
from cashu.core.models import PostMeltQuoteRequest
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.wallet import Wallet
|
||||
@@ -35,14 +35,12 @@ async def test_mint_proofs_pending(wallet1: Wallet, ledger: Ledger):
|
||||
proofs = wallet1.proofs.copy()
|
||||
|
||||
proofs_states_before_split = await wallet1.check_proof_state(proofs)
|
||||
assert all(
|
||||
[s.state == ProofSpentState.unspent for s in proofs_states_before_split.states]
|
||||
)
|
||||
assert all([s.unspent for s in proofs_states_before_split.states])
|
||||
|
||||
await ledger.db_write._verify_spent_proofs_and_set_pending(proofs)
|
||||
|
||||
proof_states = await wallet1.check_proof_state(proofs)
|
||||
assert all([s.state == ProofSpentState.pending for s in proof_states.states])
|
||||
assert all([s.pending for s in proof_states.states])
|
||||
await assert_err(wallet1.split(wallet1.proofs, 20), "proofs are pending.")
|
||||
|
||||
await ledger.db_write._unset_proofs_pending(proofs)
|
||||
@@ -50,9 +48,7 @@ async def test_mint_proofs_pending(wallet1: Wallet, ledger: Ledger):
|
||||
await wallet1.split(proofs, 20)
|
||||
|
||||
proofs_states_after_split = await wallet1.check_proof_state(proofs)
|
||||
assert all(
|
||||
[s.state == ProofSpentState.spent for s in proofs_states_after_split.states]
|
||||
)
|
||||
assert all([s.spent for s in proofs_states_after_split.states])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -77,7 +73,7 @@ async def test_mint_quote_state_transitions(wallet1: Wallet, ledger: Ledger):
|
||||
quote = await ledger.crud.get_mint_quote(quote_id=invoice.id, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.quote == invoice.id
|
||||
assert quote.state == MintQuoteState.unpaid
|
||||
assert quote.unpaid
|
||||
|
||||
# set pending again
|
||||
async def set_state(quote, state):
|
||||
@@ -167,12 +163,12 @@ async def test_melt_quote_set_pending(wallet1: Wallet, ledger: Ledger):
|
||||
quote = await ledger.crud.get_melt_quote(quote_id=melt_quote.quote, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.quote == melt_quote.quote
|
||||
assert quote.state == MeltQuoteState.unpaid
|
||||
assert quote.unpaid
|
||||
previous_state = quote.state
|
||||
await ledger.db_write._set_melt_quote_pending(quote)
|
||||
quote = await ledger.crud.get_melt_quote(quote_id=melt_quote.quote, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.state == MeltQuoteState.pending
|
||||
assert quote.pending
|
||||
|
||||
# set unpending
|
||||
await ledger.db_write._unset_melt_quote_pending(quote, previous_state)
|
||||
@@ -191,7 +187,7 @@ async def test_melt_quote_state_transitions(wallet1: Wallet, ledger: Ledger):
|
||||
quote = await ledger.crud.get_melt_quote(quote_id=melt_quote.quote, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.quote == melt_quote.quote
|
||||
assert quote.state == MeltQuoteState.unpaid
|
||||
assert quote.unpaid
|
||||
|
||||
# set pending
|
||||
quote.state = MeltQuoteState.pending
|
||||
@@ -218,7 +214,7 @@ async def test_mint_quote_set_pending(wallet1: Wallet, ledger: Ledger):
|
||||
assert invoice is not None
|
||||
quote = await ledger.crud.get_mint_quote(quote_id=invoice.id, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.state == MintQuoteState.unpaid
|
||||
assert quote.unpaid
|
||||
|
||||
# pay_if_regtest pays on regtest, get_mint_quote pays on FakeWallet
|
||||
await pay_if_regtest(invoice.bolt11)
|
||||
@@ -226,13 +222,13 @@ async def test_mint_quote_set_pending(wallet1: Wallet, ledger: Ledger):
|
||||
|
||||
quote = await ledger.crud.get_mint_quote(quote_id=invoice.id, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.state == MintQuoteState.paid
|
||||
assert quote.paid
|
||||
|
||||
previous_state = MintQuoteState.paid
|
||||
await ledger.db_write._set_mint_quote_pending(quote.quote)
|
||||
quote = await ledger.crud.get_mint_quote(quote_id=invoice.id, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.state == MintQuoteState.pending
|
||||
assert quote.pending
|
||||
|
||||
# try to mint while pending
|
||||
await assert_err(wallet1.mint(128, id=invoice.id), "Mint quote already pending.")
|
||||
@@ -243,7 +239,7 @@ async def test_mint_quote_set_pending(wallet1: Wallet, ledger: Ledger):
|
||||
quote = await ledger.crud.get_mint_quote(quote_id=invoice.id, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.state == previous_state
|
||||
assert quote.state == MintQuoteState.paid
|
||||
assert quote.paid
|
||||
|
||||
# # set paid and mint again
|
||||
# quote.state = MintQuoteState.paid
|
||||
@@ -254,7 +250,7 @@ async def test_mint_quote_set_pending(wallet1: Wallet, ledger: Ledger):
|
||||
# check if quote is issued
|
||||
quote = await ledger.crud.get_mint_quote(quote_id=invoice.id, db=ledger.db)
|
||||
assert quote is not None
|
||||
assert quote.state == MintQuoteState.issued
|
||||
assert quote.issued
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -5,10 +5,11 @@ import bolt11
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import MeltQuote, MeltQuoteState, Proof, ProofSpentState
|
||||
from cashu.core.base import MeltQuote, MeltQuoteState, Proof
|
||||
from cashu.core.crypto.aes import AESCipher
|
||||
from cashu.core.db import Database
|
||||
from cashu.core.settings import settings
|
||||
from cashu.lightning.base import PaymentResult
|
||||
from cashu.mint.crud import LedgerCrudSqlite
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.wallet import Wallet
|
||||
@@ -143,8 +144,7 @@ async def create_pending_melts(
|
||||
request="asdasd",
|
||||
checking_id=check_id,
|
||||
unit="sat",
|
||||
paid=False,
|
||||
state=MeltQuoteState.unpaid,
|
||||
state=MeltQuoteState.pending,
|
||||
amount=100,
|
||||
fee_reserve=1,
|
||||
)
|
||||
@@ -173,8 +173,8 @@ async def test_startup_fakewallet_pending_quote_success(ledger: Ledger):
|
||||
after the startup routine determines that the associated melt quote was paid."""
|
||||
pending_proof, quote = await create_pending_melts(ledger)
|
||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].state == ProofSpentState.pending
|
||||
settings.fakewallet_payment_state = True
|
||||
assert states[0].pending
|
||||
settings.fakewallet_payment_state = PaymentResult.SETTLED.name
|
||||
# run startup routinge
|
||||
await ledger.startup_ledger()
|
||||
|
||||
@@ -186,7 +186,7 @@ async def test_startup_fakewallet_pending_quote_success(ledger: Ledger):
|
||||
|
||||
# expect that proofs are spent
|
||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].state == ProofSpentState.spent
|
||||
assert states[0].spent
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -199,8 +199,8 @@ async def test_startup_fakewallet_pending_quote_failure(ledger: Ledger):
|
||||
"""
|
||||
pending_proof, quote = await create_pending_melts(ledger)
|
||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].state == ProofSpentState.pending
|
||||
settings.fakewallet_payment_state = False
|
||||
assert states[0].pending
|
||||
settings.fakewallet_payment_state = PaymentResult.FAILED.name
|
||||
# run startup routinge
|
||||
await ledger.startup_ledger()
|
||||
|
||||
@@ -212,7 +212,7 @@ async def test_startup_fakewallet_pending_quote_failure(ledger: Ledger):
|
||||
|
||||
# expect that proofs are unspent
|
||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].state == ProofSpentState.unspent
|
||||
assert states[0].unspent
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -220,8 +220,8 @@ async def test_startup_fakewallet_pending_quote_failure(ledger: Ledger):
|
||||
async def test_startup_fakewallet_pending_quote_pending(ledger: Ledger):
|
||||
pending_proof, quote = await create_pending_melts(ledger)
|
||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].state == ProofSpentState.pending
|
||||
settings.fakewallet_payment_state = None
|
||||
assert states[0].pending
|
||||
settings.fakewallet_payment_state = PaymentResult.PENDING.name
|
||||
# run startup routinge
|
||||
await ledger.startup_ledger()
|
||||
|
||||
@@ -233,7 +233,28 @@ async def test_startup_fakewallet_pending_quote_pending(ledger: Ledger):
|
||||
|
||||
# expect that proofs are still pending
|
||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].state == ProofSpentState.pending
|
||||
assert states[0].pending
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only for fake wallet")
|
||||
async def test_startup_fakewallet_pending_quote_unknown(ledger: Ledger):
|
||||
pending_proof, quote = await create_pending_melts(ledger)
|
||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].pending
|
||||
settings.fakewallet_payment_state = PaymentResult.UNKNOWN.name
|
||||
# run startup routinge
|
||||
await ledger.startup_ledger()
|
||||
|
||||
# expect that melt quote is still pending
|
||||
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
||||
db=ledger.db
|
||||
)
|
||||
assert not melt_quotes
|
||||
|
||||
# expect that proofs are still pending
|
||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].unspent
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -262,7 +283,6 @@ async def test_startup_regtest_pending_quote_pending(wallet: Wallet, ledger: Led
|
||||
)
|
||||
)
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
# settle_invoice(preimage=preimage)
|
||||
|
||||
# run startup routinge
|
||||
await ledger.startup_ledger()
|
||||
@@ -275,7 +295,7 @@ async def test_startup_regtest_pending_quote_pending(wallet: Wallet, ledger: Led
|
||||
|
||||
# expect that proofs are still pending
|
||||
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
|
||||
assert all([s.state == ProofSpentState.pending for s in states])
|
||||
assert all([s.pending for s in states])
|
||||
|
||||
# only now settle the invoice
|
||||
settle_invoice(preimage=preimage)
|
||||
@@ -309,7 +329,7 @@ async def test_startup_regtest_pending_quote_success(wallet: Wallet, ledger: Led
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
# expect that proofs are pending
|
||||
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
|
||||
assert all([s.state == ProofSpentState.pending for s in states])
|
||||
assert all([s.pending for s in states])
|
||||
|
||||
settle_invoice(preimage=preimage)
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
@@ -325,7 +345,7 @@ async def test_startup_regtest_pending_quote_success(wallet: Wallet, ledger: Led
|
||||
|
||||
# expect that proofs are spent
|
||||
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
|
||||
assert all([s.state == ProofSpentState.spent for s in states])
|
||||
assert all([s.spent for s in states])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -360,7 +380,7 @@ async def test_startup_regtest_pending_quote_failure(wallet: Wallet, ledger: Led
|
||||
|
||||
# expect that proofs are pending
|
||||
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
|
||||
assert all([s.state == ProofSpentState.pending for s in states])
|
||||
assert all([s.pending for s in states])
|
||||
|
||||
cancel_invoice(preimage_hash=preimage_hash)
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
@@ -376,4 +396,72 @@ async def test_startup_regtest_pending_quote_failure(wallet: Wallet, ledger: Led
|
||||
|
||||
# expect that proofs are unspent
|
||||
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
|
||||
assert all([s.state == ProofSpentState.unspent for s in states])
|
||||
assert all([s.unspent for s in states])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_startup_regtest_pending_quote_unknown(wallet: Wallet, ledger: Ledger):
|
||||
"""Simulate an unknown payment by executing a pending payment, then
|
||||
manipulating the melt_quote in the mint's db so that its checking_id
|
||||
points to an unknown payment."""
|
||||
|
||||
# fill wallet
|
||||
invoice = await wallet.request_mint(64)
|
||||
await pay_if_regtest(invoice.bolt11)
|
||||
await wallet.mint(64, id=invoice.id)
|
||||
assert wallet.balance == 64
|
||||
|
||||
# create hodl invoice
|
||||
preimage, invoice_dict = get_hold_invoice(16)
|
||||
invoice_payment_request = str(invoice_dict["payment_request"])
|
||||
invoice_obj = bolt11.decode(invoice_payment_request)
|
||||
preimage_hash = invoice_obj.payment_hash
|
||||
|
||||
# wallet pays the invoice
|
||||
quote = await wallet.melt_quote(invoice_payment_request)
|
||||
total_amount = quote.amount + quote.fee_reserve
|
||||
_, send_proofs = await wallet.swap_to_send(wallet.proofs, total_amount)
|
||||
asyncio.create_task(
|
||||
wallet.melt(
|
||||
proofs=send_proofs,
|
||||
invoice=invoice_payment_request,
|
||||
fee_reserve_sat=quote.fee_reserve,
|
||||
quote_id=quote.quote,
|
||||
)
|
||||
)
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
# expect that proofs are pending
|
||||
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
|
||||
assert all([s.pending for s in states])
|
||||
|
||||
# before we cancel the payment, we manipulate the melt_quote's checking_id so
|
||||
# that the mint fails to look up the payment and treats the payment as failed during startup
|
||||
melt_quote = await ledger.crud.get_melt_quote_by_request(
|
||||
db=ledger.db, request=invoice_payment_request
|
||||
)
|
||||
assert melt_quote
|
||||
assert melt_quote.pending
|
||||
|
||||
# manipulate the checking_id 32 bytes hexadecmial
|
||||
melt_quote.checking_id = "a" * 64
|
||||
await ledger.crud.update_melt_quote(quote=melt_quote, db=ledger.db)
|
||||
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
# run startup routinge
|
||||
await ledger.startup_ledger()
|
||||
|
||||
# expect that no melt quote is pending
|
||||
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
||||
db=ledger.db
|
||||
)
|
||||
assert not melt_quotes
|
||||
|
||||
# expect that proofs are unspent
|
||||
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
|
||||
assert all([s.unspent for s in states])
|
||||
|
||||
# clean up
|
||||
cancel_invoice(preimage_hash=preimage_hash)
|
||||
|
||||
@@ -98,11 +98,10 @@ async def test_blink_pay_invoice():
|
||||
unit="sat",
|
||||
amount=100,
|
||||
fee_reserve=12,
|
||||
paid=False,
|
||||
state=MeltQuoteState.unpaid,
|
||||
)
|
||||
payment = await blink.pay_invoice(quote, 1000)
|
||||
assert payment.ok
|
||||
assert payment.settled
|
||||
assert payment.fee
|
||||
assert payment.fee.amount == 10
|
||||
assert payment.error_message is None
|
||||
@@ -131,11 +130,10 @@ async def test_blink_pay_invoice_failure():
|
||||
unit="sat",
|
||||
amount=100,
|
||||
fee_reserve=12,
|
||||
paid=False,
|
||||
state=MeltQuoteState.unpaid,
|
||||
)
|
||||
payment = await blink.pay_invoice(quote, 1000)
|
||||
assert not payment.ok
|
||||
assert not payment.settled
|
||||
assert payment.fee is None
|
||||
assert payment.error_message
|
||||
assert "This is the error" in payment.error_message
|
||||
@@ -155,7 +153,7 @@ async def test_blink_get_invoice_status():
|
||||
}
|
||||
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
||||
status = await blink.get_invoice_status("123")
|
||||
assert status.paid
|
||||
assert status.settled
|
||||
|
||||
|
||||
@respx.mock
|
||||
@@ -183,7 +181,7 @@ async def test_blink_get_payment_status():
|
||||
}
|
||||
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
||||
status = await blink.get_payment_status(payment_request)
|
||||
assert status.paid
|
||||
assert status.settled
|
||||
assert status.fee
|
||||
assert status.fee.amount == 10
|
||||
assert status.preimage == "123"
|
||||
|
||||
297
tests/test_mint_melt.py
Normal file
297
tests/test_mint_melt.py
Normal file
@@ -0,0 +1,297 @@
|
||||
from typing import List, Tuple
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import MeltQuote, MeltQuoteState, Proof
|
||||
from cashu.core.errors import LightningError
|
||||
from cashu.core.models import PostMeltQuoteRequest
|
||||
from cashu.core.settings import settings
|
||||
from cashu.lightning.base import PaymentResult
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import (
|
||||
is_regtest,
|
||||
)
|
||||
|
||||
SEED = "TEST_PRIVATE_KEY"
|
||||
DERIVATION_PATH = "m/0'/0'/0'"
|
||||
DECRYPTON_KEY = "testdecryptionkey"
|
||||
ENCRYPTED_SEED = "U2FsdGVkX1_7UU_-nVBMBWDy_9yDu4KeYb7MH8cJTYQGD4RWl82PALH8j-HKzTrI"
|
||||
|
||||
|
||||
async def assert_err(f, msg):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
assert exc.args[0] == msg, Exception(
|
||||
f"Expected error: {msg}, got: {exc.args[0]}"
|
||||
)
|
||||
|
||||
|
||||
def assert_amt(proofs: List[Proof], expected: int):
|
||||
"""Assert amounts the proofs contain."""
|
||||
assert [p.amount for p in proofs] == expected
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet(ledger: Ledger):
|
||||
wallet1 = await Wallet.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet_mint_api_deprecated",
|
||||
name="wallet_mint_api_deprecated",
|
||||
)
|
||||
await wallet1.load_mint()
|
||||
yield wallet1
|
||||
|
||||
|
||||
async def create_pending_melts(
|
||||
ledger: Ledger, check_id: str = "checking_id"
|
||||
) -> Tuple[Proof, MeltQuote]:
|
||||
"""Helper function for startup tests for fakewallet. Creates fake pending melt
|
||||
quote and fake proofs that are in the pending table that look like they're being
|
||||
used to pay the pending melt quote."""
|
||||
quote_id = "quote_id"
|
||||
quote = MeltQuote(
|
||||
quote=quote_id,
|
||||
method="bolt11",
|
||||
request="asdasd",
|
||||
checking_id=check_id,
|
||||
unit="sat",
|
||||
state=MeltQuoteState.pending,
|
||||
amount=100,
|
||||
fee_reserve=1,
|
||||
)
|
||||
await ledger.crud.store_melt_quote(
|
||||
quote=quote,
|
||||
db=ledger.db,
|
||||
)
|
||||
pending_proof = Proof(amount=123, C="asdasd", secret="asdasd", id=quote_id)
|
||||
await ledger.crud.set_proof_pending(
|
||||
db=ledger.db,
|
||||
proof=pending_proof,
|
||||
quote_id=quote_id,
|
||||
)
|
||||
# expect a pending melt quote
|
||||
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
||||
db=ledger.db
|
||||
)
|
||||
assert melt_quotes
|
||||
return pending_proof, quote
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only fake wallet")
|
||||
async def test_fakewallet_pending_quote_get_melt_quote_success(ledger: Ledger):
|
||||
"""Startup routine test. Expects that a pending proofs are removed form the pending db
|
||||
after the startup routine determines that the associated melt quote was paid."""
|
||||
pending_proof, quote = await create_pending_melts(ledger)
|
||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].pending
|
||||
settings.fakewallet_payment_state = PaymentResult.SETTLED.name
|
||||
|
||||
# get_melt_quote should check the payment status and update the db
|
||||
quote2 = await ledger.get_melt_quote(quote_id=quote.quote)
|
||||
assert quote2.state == MeltQuoteState.paid
|
||||
|
||||
# expect that no pending tokens are in db anymore
|
||||
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
||||
db=ledger.db
|
||||
)
|
||||
assert not melt_quotes
|
||||
|
||||
# expect that proofs are spent
|
||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].spent
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only fake wallet")
|
||||
async def test_fakewallet_pending_quote_get_melt_quote_pending(ledger: Ledger):
|
||||
"""Startup routine test. Expects that a pending proofs are removed form the pending db
|
||||
after the startup routine determines that the associated melt quote was paid."""
|
||||
pending_proof, quote = await create_pending_melts(ledger)
|
||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].pending
|
||||
settings.fakewallet_payment_state = PaymentResult.PENDING.name
|
||||
|
||||
# get_melt_quote should check the payment status and update the db
|
||||
quote2 = await ledger.get_melt_quote(quote_id=quote.quote)
|
||||
assert quote2.state == MeltQuoteState.pending
|
||||
|
||||
# expect that pending tokens are still in db
|
||||
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
||||
db=ledger.db
|
||||
)
|
||||
assert melt_quotes
|
||||
|
||||
# expect that proofs are pending
|
||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].pending
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only fake wallet")
|
||||
async def test_fakewallet_pending_quote_get_melt_quote_failed(ledger: Ledger):
|
||||
"""Startup routine test. Expects that a pending proofs are removed form the pending db
|
||||
after the startup routine determines that the associated melt quote was paid."""
|
||||
pending_proof, quote = await create_pending_melts(ledger)
|
||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].pending
|
||||
settings.fakewallet_payment_state = PaymentResult.FAILED.name
|
||||
|
||||
# get_melt_quote should check the payment status and update the db
|
||||
quote2 = await ledger.get_melt_quote(quote_id=quote.quote)
|
||||
assert quote2.state == MeltQuoteState.unpaid
|
||||
|
||||
# expect that pending tokens are still in db
|
||||
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
||||
db=ledger.db
|
||||
)
|
||||
assert not melt_quotes
|
||||
|
||||
# expect that proofs are pending
|
||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].unspent
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only fake wallet")
|
||||
async def test_fakewallet_pending_quote_get_melt_quote_unknown(ledger: Ledger):
|
||||
"""Startup routine test. Expects that a pending proofs are removed form the pending db
|
||||
after the startup routine determines that the associated melt quote was paid."""
|
||||
pending_proof, quote = await create_pending_melts(ledger)
|
||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].pending
|
||||
settings.fakewallet_payment_state = PaymentResult.UNKNOWN.name
|
||||
|
||||
# get_melt_quote(..., purge_unknown=True) should check the payment status and update the db
|
||||
quote2 = await ledger.get_melt_quote(quote_id=quote.quote, purge_unknown=True)
|
||||
assert quote2.state == MeltQuoteState.unpaid
|
||||
|
||||
# expect that pending tokens are still in db
|
||||
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
||||
db=ledger.db
|
||||
)
|
||||
assert not melt_quotes
|
||||
|
||||
# expect that proofs are pending
|
||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||
assert states[0].unspent
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only fake wallet")
|
||||
async def test_melt_lightning_pay_invoice_settled(ledger: Ledger, wallet: Wallet):
|
||||
invoice = await wallet.request_mint(64)
|
||||
await ledger.get_mint_quote(invoice.id) # fakewallet: set the quote to paid
|
||||
await wallet.mint(64, id=invoice.id)
|
||||
# invoice_64_sat = "lnbcrt640n1pn0r3tfpp5e30xac756gvd26cn3tgsh8ug6ct555zrvl7vsnma5cwp4g7auq5qdqqcqzzsxqyz5vqsp5xfhtzg0y3mekv6nsdnj43c346smh036t4f8gcfa2zwpxzwcryqvs9qxpqysgqw5juev8y3zxpdu0mvdrced5c6a852f9x7uh57g6fgjgcg5muqzd5474d7xgh770frazel67eejfwelnyr507q46hxqehala880rhlqspw07ta0"
|
||||
invoice_62_sat = "lnbcrt620n1pn0r3vepp5zljn7g09fsyeahl4rnhuy0xax2puhua5r3gspt7ttlfrley6valqdqqcqzzsxqyz5vqsp577h763sel3q06tfnfe75kvwn5pxn344sd5vnays65f9wfgx4fpzq9qxpqysgqg3re9afz9rwwalytec04pdhf9mvh3e2k4r877tw7dr4g0fvzf9sny5nlfggdy6nduy2dytn06w50ls34qfldgsj37x0ymxam0a687mspp0ytr8"
|
||||
quote_id = (
|
||||
await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(unit="sat", request=invoice_62_sat)
|
||||
)
|
||||
).quote
|
||||
# quote = await ledger.get_melt_quote(quote_id)
|
||||
settings.fakewallet_payment_state = PaymentResult.SETTLED.name
|
||||
settings.fakewallet_pay_invoice_state = PaymentResult.SETTLED.name
|
||||
melt_response = await ledger.melt(proofs=wallet.proofs, quote=quote_id)
|
||||
assert melt_response.state == MeltQuoteState.paid.value
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only fake wallet")
|
||||
async def test_melt_lightning_pay_invoice_failed_failed(ledger: Ledger, wallet: Wallet):
|
||||
invoice = await wallet.request_mint(64)
|
||||
await ledger.get_mint_quote(invoice.id) # fakewallet: set the quote to paid
|
||||
await wallet.mint(64, id=invoice.id)
|
||||
# invoice_64_sat = "lnbcrt640n1pn0r3tfpp5e30xac756gvd26cn3tgsh8ug6ct555zrvl7vsnma5cwp4g7auq5qdqqcqzzsxqyz5vqsp5xfhtzg0y3mekv6nsdnj43c346smh036t4f8gcfa2zwpxzwcryqvs9qxpqysgqw5juev8y3zxpdu0mvdrced5c6a852f9x7uh57g6fgjgcg5muqzd5474d7xgh770frazel67eejfwelnyr507q46hxqehala880rhlqspw07ta0"
|
||||
invoice_62_sat = "lnbcrt620n1pn0r3vepp5zljn7g09fsyeahl4rnhuy0xax2puhua5r3gspt7ttlfrley6valqdqqcqzzsxqyz5vqsp577h763sel3q06tfnfe75kvwn5pxn344sd5vnays65f9wfgx4fpzq9qxpqysgqg3re9afz9rwwalytec04pdhf9mvh3e2k4r877tw7dr4g0fvzf9sny5nlfggdy6nduy2dytn06w50ls34qfldgsj37x0ymxam0a687mspp0ytr8"
|
||||
quote_id = (
|
||||
await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(unit="sat", request=invoice_62_sat)
|
||||
)
|
||||
).quote
|
||||
# quote = await ledger.get_melt_quote(quote_id)
|
||||
settings.fakewallet_payment_state = PaymentResult.FAILED.name
|
||||
settings.fakewallet_pay_invoice_state = PaymentResult.FAILED.name
|
||||
try:
|
||||
await ledger.melt(proofs=wallet.proofs, quote=quote_id)
|
||||
raise AssertionError("Expected LightningError")
|
||||
except LightningError:
|
||||
pass
|
||||
|
||||
settings.fakewallet_payment_state = PaymentResult.UNKNOWN.name
|
||||
settings.fakewallet_pay_invoice_state = PaymentResult.FAILED.name
|
||||
try:
|
||||
await ledger.melt(proofs=wallet.proofs, quote=quote_id)
|
||||
raise AssertionError("Expected LightningError")
|
||||
except LightningError:
|
||||
pass
|
||||
|
||||
settings.fakewallet_payment_state = PaymentResult.FAILED.name
|
||||
settings.fakewallet_pay_invoice_state = PaymentResult.UNKNOWN.name
|
||||
try:
|
||||
await ledger.melt(proofs=wallet.proofs, quote=quote_id)
|
||||
raise AssertionError("Expected LightningError")
|
||||
except LightningError:
|
||||
pass
|
||||
|
||||
settings.fakewallet_payment_state = PaymentResult.UNKNOWN.name
|
||||
settings.fakewallet_pay_invoice_state = PaymentResult.UNKNOWN.name
|
||||
try:
|
||||
await ledger.melt(proofs=wallet.proofs, quote=quote_id)
|
||||
raise AssertionError("Expected LightningError")
|
||||
except LightningError:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only fake wallet")
|
||||
async def test_melt_lightning_pay_invoice_failed_settled(
|
||||
ledger: Ledger, wallet: Wallet
|
||||
):
|
||||
invoice = await wallet.request_mint(64)
|
||||
await ledger.get_mint_quote(invoice.id) # fakewallet: set the quote to paid
|
||||
await wallet.mint(64, id=invoice.id)
|
||||
invoice_62_sat = "lnbcrt620n1pn0r3vepp5zljn7g09fsyeahl4rnhuy0xax2puhua5r3gspt7ttlfrley6valqdqqcqzzsxqyz5vqsp577h763sel3q06tfnfe75kvwn5pxn344sd5vnays65f9wfgx4fpzq9qxpqysgqg3re9afz9rwwalytec04pdhf9mvh3e2k4r877tw7dr4g0fvzf9sny5nlfggdy6nduy2dytn06w50ls34qfldgsj37x0ymxam0a687mspp0ytr8"
|
||||
quote_id = (
|
||||
await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(unit="sat", request=invoice_62_sat)
|
||||
)
|
||||
).quote
|
||||
settings.fakewallet_pay_invoice_state = PaymentResult.FAILED.name
|
||||
settings.fakewallet_payment_state = PaymentResult.SETTLED.name
|
||||
|
||||
melt_response = await ledger.melt(proofs=wallet.proofs, quote=quote_id)
|
||||
assert melt_response.state == MeltQuoteState.pending.value
|
||||
# expect that proofs are pending
|
||||
states = await ledger.db_read.get_proofs_states([p.Y for p in wallet.proofs])
|
||||
assert all([s.pending for s in states])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only fake wallet")
|
||||
async def test_melt_lightning_pay_invoice_failed_pending(
|
||||
ledger: Ledger, wallet: Wallet
|
||||
):
|
||||
invoice = await wallet.request_mint(64)
|
||||
await ledger.get_mint_quote(invoice.id) # fakewallet: set the quote to paid
|
||||
await wallet.mint(64, id=invoice.id)
|
||||
invoice_62_sat = "lnbcrt620n1pn0r3vepp5zljn7g09fsyeahl4rnhuy0xax2puhua5r3gspt7ttlfrley6valqdqqcqzzsxqyz5vqsp577h763sel3q06tfnfe75kvwn5pxn344sd5vnays65f9wfgx4fpzq9qxpqysgqg3re9afz9rwwalytec04pdhf9mvh3e2k4r877tw7dr4g0fvzf9sny5nlfggdy6nduy2dytn06w50ls34qfldgsj37x0ymxam0a687mspp0ytr8"
|
||||
quote_id = (
|
||||
await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(unit="sat", request=invoice_62_sat)
|
||||
)
|
||||
).quote
|
||||
settings.fakewallet_pay_invoice_state = PaymentResult.FAILED.name
|
||||
settings.fakewallet_payment_state = PaymentResult.PENDING.name
|
||||
|
||||
melt_response = await ledger.melt(proofs=wallet.proofs, quote=quote_id)
|
||||
assert melt_response.state == MeltQuoteState.pending.value
|
||||
# expect that proofs are pending
|
||||
states = await ledger.db_read.get_proofs_states([p.Y for p in wallet.proofs])
|
||||
assert all([s.pending for s in states])
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import MeltQuoteState, MintQuoteState
|
||||
from cashu.core.base import MeltQuoteState
|
||||
from cashu.core.helpers import sum_proofs
|
||||
from cashu.core.models import PostMeltQuoteRequest, PostMintQuoteRequest
|
||||
from cashu.mint.ledger import Ledger
|
||||
@@ -38,9 +38,8 @@ async def wallet1(ledger: Ledger):
|
||||
async def test_melt_internal(wallet1: Wallet, ledger: Ledger):
|
||||
# mint twice so we have enough to pay the second invoice back
|
||||
invoice = await wallet1.request_mint(128)
|
||||
await pay_if_regtest(invoice.bolt11)
|
||||
await ledger.get_mint_quote(invoice.id)
|
||||
await wallet1.mint(128, id=invoice.id)
|
||||
await pay_if_regtest(invoice.bolt11)
|
||||
assert wallet1.balance == 128
|
||||
|
||||
# create a mint quote so that we can melt to it internally
|
||||
@@ -58,14 +57,14 @@ async def test_melt_internal(wallet1: Wallet, ledger: Ledger):
|
||||
|
||||
melt_quote_pre_payment = await ledger.get_melt_quote(melt_quote.quote)
|
||||
assert not melt_quote_pre_payment.paid, "melt quote should not be paid"
|
||||
assert melt_quote_pre_payment.state == MeltQuoteState.unpaid
|
||||
assert melt_quote_pre_payment.unpaid
|
||||
|
||||
keep_proofs, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 64)
|
||||
await ledger.melt(proofs=send_proofs, quote=melt_quote.quote)
|
||||
|
||||
melt_quote_post_payment = await ledger.get_melt_quote(melt_quote.quote)
|
||||
assert melt_quote_post_payment.paid, "melt quote should be paid"
|
||||
assert melt_quote_post_payment.state == MeltQuoteState.paid
|
||||
assert melt_quote_post_payment.paid
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -92,25 +91,25 @@ async def test_melt_external(wallet1: Wallet, ledger: Ledger):
|
||||
|
||||
melt_quote_pre_payment = await ledger.get_melt_quote(melt_quote.quote)
|
||||
assert not melt_quote_pre_payment.paid, "melt quote should not be paid"
|
||||
assert melt_quote_pre_payment.state == MeltQuoteState.unpaid
|
||||
assert melt_quote_pre_payment.unpaid
|
||||
|
||||
assert not melt_quote.paid, "melt quote should not be paid"
|
||||
await ledger.melt(proofs=send_proofs, quote=melt_quote.quote)
|
||||
|
||||
melt_quote_post_payment = await ledger.get_melt_quote(melt_quote.quote)
|
||||
assert melt_quote_post_payment.paid, "melt quote should be paid"
|
||||
assert melt_quote_post_payment.state == MeltQuoteState.paid
|
||||
assert melt_quote_post_payment.paid
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
|
||||
async def test_mint_internal(wallet1: Wallet, ledger: Ledger):
|
||||
invoice = await wallet1.request_mint(128)
|
||||
await pay_if_regtest(invoice.bolt11)
|
||||
await ledger.get_mint_quote(invoice.id)
|
||||
mint_quote = await ledger.get_mint_quote(invoice.id)
|
||||
|
||||
assert mint_quote.paid, "mint quote should be paid"
|
||||
assert mint_quote.state == MintQuoteState.paid
|
||||
assert mint_quote.paid
|
||||
|
||||
output_amounts = [128]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
@@ -125,8 +124,8 @@ async def test_mint_internal(wallet1: Wallet, ledger: Ledger):
|
||||
)
|
||||
|
||||
mint_quote_after_payment = await ledger.get_mint_quote(invoice.id)
|
||||
assert mint_quote_after_payment.paid, "mint quote should be paid"
|
||||
assert mint_quote_after_payment.state == MintQuoteState.issued
|
||||
assert mint_quote_after_payment.issued, "mint quote should be issued"
|
||||
assert mint_quote_after_payment.issued
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -134,11 +133,11 @@ async def test_mint_internal(wallet1: Wallet, ledger: Ledger):
|
||||
async def test_mint_external(wallet1: Wallet, ledger: Ledger):
|
||||
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=128, unit="sat"))
|
||||
assert not quote.paid, "mint quote should not be paid"
|
||||
assert quote.state == MintQuoteState.unpaid
|
||||
assert quote.unpaid
|
||||
|
||||
mint_quote = await ledger.get_mint_quote(quote.quote)
|
||||
assert not mint_quote.paid, "mint quote already paid"
|
||||
assert mint_quote.state == MintQuoteState.unpaid
|
||||
assert mint_quote.unpaid
|
||||
|
||||
await assert_err(
|
||||
wallet1.mint(128, id=quote.quote),
|
||||
@@ -149,7 +148,7 @@ async def test_mint_external(wallet1: Wallet, ledger: Ledger):
|
||||
|
||||
mint_quote = await ledger.get_mint_quote(quote.quote)
|
||||
assert mint_quote.paid, "mint quote should be paid"
|
||||
assert mint_quote.state == MintQuoteState.paid
|
||||
assert mint_quote.paid
|
||||
|
||||
output_amounts = [128]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
@@ -159,8 +158,7 @@ async def test_mint_external(wallet1: Wallet, ledger: Ledger):
|
||||
await ledger.mint(outputs=outputs, quote_id=quote.quote)
|
||||
|
||||
mint_quote_after_payment = await ledger.get_mint_quote(quote.quote)
|
||||
assert mint_quote_after_payment.paid, "mint quote should be paid"
|
||||
assert mint_quote_after_payment.state == MintQuoteState.issued
|
||||
assert mint_quote_after_payment.issued, "mint quote should be issued"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import asyncio
|
||||
|
||||
import bolt11
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import ProofSpentState
|
||||
from cashu.core.base import Amount, MeltQuote, MeltQuoteState, Method, Unit
|
||||
from cashu.core.models import PostMeltQuoteRequest
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import (
|
||||
SLEEP_TIME,
|
||||
cancel_invoice,
|
||||
get_hold_invoice,
|
||||
get_real_invoice,
|
||||
get_real_invoice_cln,
|
||||
is_fake,
|
||||
pay_if_regtest,
|
||||
pay_real_invoice,
|
||||
settle_invoice,
|
||||
)
|
||||
|
||||
@@ -27,6 +33,243 @@ async def wallet():
|
||||
yield wallet
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_lightning_create_invoice(ledger: Ledger):
|
||||
invoice = await ledger.backends[Method.bolt11][Unit.sat].create_invoice(
|
||||
Amount(Unit.sat, 1000)
|
||||
)
|
||||
assert invoice.ok
|
||||
assert invoice.payment_request
|
||||
assert invoice.checking_id
|
||||
|
||||
# TEST 2: check the invoice status
|
||||
status = await ledger.backends[Method.bolt11][Unit.sat].get_invoice_status(
|
||||
invoice.checking_id
|
||||
)
|
||||
assert status.pending
|
||||
|
||||
# settle the invoice
|
||||
await pay_if_regtest(invoice.payment_request)
|
||||
|
||||
# TEST 3: check the invoice status
|
||||
status = await ledger.backends[Method.bolt11][Unit.sat].get_invoice_status(
|
||||
invoice.checking_id
|
||||
)
|
||||
assert status.settled
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_lightning_get_payment_quote(ledger: Ledger):
|
||||
invoice_dict = get_real_invoice(64)
|
||||
request = invoice_dict["payment_request"]
|
||||
payment_quote = await ledger.backends[Method.bolt11][Unit.sat].get_payment_quote(
|
||||
PostMeltQuoteRequest(request=request, unit=Unit.sat.name)
|
||||
)
|
||||
assert payment_quote.amount == Amount(Unit.sat, 64)
|
||||
assert payment_quote.checking_id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_lightning_pay_invoice(ledger: Ledger):
|
||||
invoice_dict = get_real_invoice(64)
|
||||
request = invoice_dict["payment_request"]
|
||||
quote = MeltQuote(
|
||||
quote="test",
|
||||
method=Method.bolt11.name,
|
||||
unit=Unit.sat.name,
|
||||
state=MeltQuoteState.unpaid,
|
||||
request=request,
|
||||
checking_id="test",
|
||||
amount=64,
|
||||
fee_reserve=0,
|
||||
)
|
||||
payment = await ledger.backends[Method.bolt11][Unit.sat].pay_invoice(quote, 1000)
|
||||
assert payment.settled
|
||||
assert payment.preimage
|
||||
assert payment.checking_id
|
||||
assert not payment.error_message
|
||||
|
||||
# TEST 2: check the payment status
|
||||
status = await ledger.backends[Method.bolt11][Unit.sat].get_payment_status(
|
||||
payment.checking_id
|
||||
)
|
||||
assert status.settled
|
||||
assert status.preimage
|
||||
assert not status.error_message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_lightning_pay_invoice_failure(ledger: Ledger):
|
||||
# create an invoice with the external CLN node and pay it with the external LND – so that our mint backend can't pay it
|
||||
request = get_real_invoice_cln(64)
|
||||
# pay the invoice so that the attempt later fails
|
||||
pay_real_invoice(request)
|
||||
|
||||
# we call get_payment_quote to get a checking_id that we will use to check for the failed pending state later with get_payment_status
|
||||
payment_quote = await ledger.backends[Method.bolt11][Unit.sat].get_payment_quote(
|
||||
PostMeltQuoteRequest(request=request, unit=Unit.sat.name)
|
||||
)
|
||||
checking_id = payment_quote.checking_id
|
||||
|
||||
# TEST 1: check the payment status
|
||||
status = await ledger.backends[Method.bolt11][Unit.sat].get_payment_status(
|
||||
checking_id
|
||||
)
|
||||
assert status.unknown
|
||||
|
||||
# TEST 2: pay the invoice
|
||||
quote = MeltQuote(
|
||||
quote="test",
|
||||
method=Method.bolt11.name,
|
||||
unit=Unit.sat.name,
|
||||
state=MeltQuoteState.unpaid,
|
||||
request=request,
|
||||
checking_id="test",
|
||||
amount=64,
|
||||
fee_reserve=0,
|
||||
)
|
||||
payment = await ledger.backends[Method.bolt11][Unit.sat].pay_invoice(quote, 1000)
|
||||
|
||||
assert payment.failed
|
||||
assert not payment.preimage
|
||||
assert payment.error_message
|
||||
assert not payment.checking_id
|
||||
|
||||
# TEST 3: check the payment status
|
||||
status = await ledger.backends[Method.bolt11][Unit.sat].get_payment_status(
|
||||
checking_id
|
||||
)
|
||||
|
||||
assert status.failed or status.unknown
|
||||
assert not status.preimage
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_lightning_pay_invoice_pending_success(ledger: Ledger):
|
||||
# create a hold invoice
|
||||
preimage, invoice_dict = get_hold_invoice(64)
|
||||
request = str(invoice_dict["payment_request"])
|
||||
|
||||
# we call get_payment_quote to get a checking_id that we will use to check for the failed pending state later with get_payment_status
|
||||
payment_quote = await ledger.backends[Method.bolt11][Unit.sat].get_payment_quote(
|
||||
PostMeltQuoteRequest(request=request, unit=Unit.sat.name)
|
||||
)
|
||||
checking_id = payment_quote.checking_id
|
||||
|
||||
# pay the invoice
|
||||
quote = MeltQuote(
|
||||
quote="test",
|
||||
method=Method.bolt11.name,
|
||||
unit=Unit.sat.name,
|
||||
state=MeltQuoteState.unpaid,
|
||||
request=request,
|
||||
checking_id=checking_id,
|
||||
amount=64,
|
||||
fee_reserve=0,
|
||||
)
|
||||
|
||||
async def pay():
|
||||
payment = await ledger.backends[Method.bolt11][Unit.sat].pay_invoice(
|
||||
quote, 1000
|
||||
)
|
||||
return payment
|
||||
|
||||
task = asyncio.create_task(pay())
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
# check the payment status
|
||||
status = await ledger.backends[Method.bolt11][Unit.sat].get_payment_status(
|
||||
quote.checking_id
|
||||
)
|
||||
assert status.pending
|
||||
|
||||
# settle the invoice
|
||||
settle_invoice(preimage=preimage)
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
# collect the payment
|
||||
payment = await task
|
||||
assert payment.settled
|
||||
assert payment.preimage
|
||||
assert payment.checking_id
|
||||
assert not payment.error_message
|
||||
|
||||
# check the payment status
|
||||
status = await ledger.backends[Method.bolt11][Unit.sat].get_payment_status(
|
||||
quote.checking_id
|
||||
)
|
||||
assert status.settled
|
||||
assert status.preimage
|
||||
assert not status.error_message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_lightning_pay_invoice_pending_failure(ledger: Ledger):
|
||||
# create a hold invoice
|
||||
preimage, invoice_dict = get_hold_invoice(64)
|
||||
request = str(invoice_dict["payment_request"])
|
||||
payment_hash = bolt11.decode(request).payment_hash
|
||||
|
||||
# we call get_payment_quote to get a checking_id that we will use to check for the failed pending state later with get_payment_status
|
||||
payment_quote = await ledger.backends[Method.bolt11][Unit.sat].get_payment_quote(
|
||||
PostMeltQuoteRequest(request=request, unit=Unit.sat.name)
|
||||
)
|
||||
checking_id = payment_quote.checking_id
|
||||
|
||||
# pay the invoice
|
||||
quote = MeltQuote(
|
||||
quote="test",
|
||||
method=Method.bolt11.name,
|
||||
unit=Unit.sat.name,
|
||||
state=MeltQuoteState.unpaid,
|
||||
request=request,
|
||||
checking_id=checking_id,
|
||||
amount=64,
|
||||
fee_reserve=0,
|
||||
)
|
||||
|
||||
async def pay():
|
||||
payment = await ledger.backends[Method.bolt11][Unit.sat].pay_invoice(
|
||||
quote, 1000
|
||||
)
|
||||
return payment
|
||||
|
||||
task = asyncio.create_task(pay())
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
# check the payment status
|
||||
status = await ledger.backends[Method.bolt11][Unit.sat].get_payment_status(
|
||||
quote.checking_id
|
||||
)
|
||||
assert status.pending
|
||||
|
||||
# cancel the invoice
|
||||
cancel_invoice(payment_hash)
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
# collect the payment
|
||||
payment = await task
|
||||
assert payment.failed
|
||||
assert not payment.preimage
|
||||
# assert payment.error_message
|
||||
|
||||
# check the payment status
|
||||
status = await ledger.backends[Method.bolt11][Unit.sat].get_payment_status(
|
||||
quote.checking_id
|
||||
)
|
||||
assert (
|
||||
status.failed or status.unknown
|
||||
) # some backends send unknown instead of failed if they can't find the payment
|
||||
assert not status.preimage
|
||||
# assert status.error_message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||
async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger):
|
||||
@@ -63,7 +306,7 @@ async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger):
|
||||
|
||||
# expect that proofs are still pending
|
||||
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
|
||||
assert all([s.state == ProofSpentState.pending for s in states])
|
||||
assert all([s.pending for s in states])
|
||||
|
||||
# only now settle the invoice
|
||||
settle_invoice(preimage=preimage)
|
||||
@@ -71,7 +314,7 @@ async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger):
|
||||
|
||||
# expect that proofs are now spent
|
||||
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
|
||||
assert all([s.state == ProofSpentState.spent for s in states])
|
||||
assert all([s.spent for s in states])
|
||||
|
||||
# expect that no melt quote is pending
|
||||
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
||||
|
||||
@@ -4,7 +4,7 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from cashu.lightning.base import InvoiceResponse, PaymentStatus
|
||||
from cashu.lightning.base import InvoiceResponse, PaymentResult, PaymentStatus
|
||||
from cashu.wallet.api.app import app
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
@@ -29,8 +29,8 @@ async def test_invoice(wallet: Wallet):
|
||||
response = client.post("/lightning/create_invoice?amount=100")
|
||||
assert response.status_code == 200
|
||||
invoice_response = InvoiceResponse.parse_obj(response.json())
|
||||
state = PaymentStatus(paid=False)
|
||||
while not state.paid:
|
||||
state = PaymentStatus(result=PaymentResult.PENDING)
|
||||
while state.pending:
|
||||
print("checking invoice state")
|
||||
response2 = client.get(
|
||||
f"/lightning/invoice_state?payment_hash={invoice_response.checking_id}"
|
||||
@@ -171,8 +171,8 @@ async def test_flow(wallet: Wallet):
|
||||
initial_balance = response.json()["balance"]
|
||||
response = client.post("/lightning/create_invoice?amount=100")
|
||||
invoice_response = InvoiceResponse.parse_obj(response.json())
|
||||
state = PaymentStatus(paid=False)
|
||||
while not state.paid:
|
||||
state = PaymentStatus(result=PaymentResult.PENDING)
|
||||
while state.pending:
|
||||
print("checking invoice state")
|
||||
response2 = client.get(
|
||||
f"/lightning/invoice_state?payment_hash={invoice_response.checking_id}"
|
||||
|
||||
@@ -83,7 +83,7 @@ async def test_check_invoice_internal(wallet: LightningWallet):
|
||||
assert invoice.payment_request
|
||||
assert invoice.checking_id
|
||||
status = await wallet.get_invoice_status(invoice.checking_id)
|
||||
assert status.paid
|
||||
assert status.settled
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -94,10 +94,10 @@ async def test_check_invoice_external(wallet: LightningWallet):
|
||||
assert invoice.payment_request
|
||||
assert invoice.checking_id
|
||||
status = await wallet.get_invoice_status(invoice.checking_id)
|
||||
assert not status.paid
|
||||
assert not status.settled
|
||||
await pay_if_regtest(invoice.payment_request)
|
||||
status = await wallet.get_invoice_status(invoice.checking_id)
|
||||
assert status.paid
|
||||
assert status.settled
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -115,12 +115,12 @@ async def test_pay_invoice_internal(wallet: LightningWallet):
|
||||
assert invoice2.payment_request
|
||||
status = await wallet.pay_invoice(invoice2.payment_request)
|
||||
|
||||
assert status.ok
|
||||
assert status.settled
|
||||
|
||||
# check payment
|
||||
assert invoice2.checking_id
|
||||
status = await wallet.get_payment_status(invoice2.checking_id)
|
||||
assert status.paid
|
||||
assert status.settled
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -132,16 +132,16 @@ async def test_pay_invoice_external(wallet: LightningWallet):
|
||||
assert invoice.checking_id
|
||||
await pay_if_regtest(invoice.payment_request)
|
||||
status = await wallet.get_invoice_status(invoice.checking_id)
|
||||
assert status.paid
|
||||
assert status.settled
|
||||
assert wallet.available_balance >= 64
|
||||
|
||||
# pay invoice
|
||||
invoice_real = get_real_invoice(16)
|
||||
status = await wallet.pay_invoice(invoice_real["payment_request"])
|
||||
|
||||
assert status.ok
|
||||
assert status.settled
|
||||
|
||||
# check payment
|
||||
assert status.checking_id
|
||||
status = await wallet.get_payment_status(status.checking_id)
|
||||
assert status.paid
|
||||
assert status.settled
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import List
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import Proof, ProofSpentState
|
||||
from cashu.core.base import Proof
|
||||
from cashu.core.crypto.secp import PrivateKey, PublicKey
|
||||
from cashu.core.migrations import migrate_databases
|
||||
from cashu.core.p2pk import SigFlags
|
||||
@@ -80,7 +80,7 @@ async def test_p2pk(wallet1: Wallet, wallet2: Wallet):
|
||||
await wallet2.redeem(send_proofs)
|
||||
|
||||
proof_states = await wallet2.check_proof_state(send_proofs)
|
||||
assert all([p.state == ProofSpentState.spent for p in proof_states.states])
|
||||
assert all([p.spent for p in proof_states.states])
|
||||
|
||||
if not is_deprecated_api_only:
|
||||
for state in proof_states.states:
|
||||
|
||||
@@ -4,7 +4,6 @@ import bolt11
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import ProofSpentState
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
@@ -57,14 +56,14 @@ async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger):
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
states = await wallet.check_proof_state(send_proofs)
|
||||
assert all([s.state == ProofSpentState.pending for s in states.states])
|
||||
assert all([s.pending for s in states.states])
|
||||
|
||||
settle_invoice(preimage=preimage)
|
||||
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
states = await wallet.check_proof_state(send_proofs)
|
||||
assert all([s.state == ProofSpentState.spent for s in states.states])
|
||||
assert all([s.spent for s in states.states])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -97,11 +96,11 @@ async def test_regtest_failed_quote(wallet: Wallet, ledger: Ledger):
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
states = await wallet.check_proof_state(send_proofs)
|
||||
assert all([s.state == ProofSpentState.pending for s in states.states])
|
||||
assert all([s.pending for s in states.states])
|
||||
|
||||
cancel_invoice(preimage_hash=preimage_hash)
|
||||
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
states = await wallet.check_proof_state(send_proofs)
|
||||
assert all([s.state == ProofSpentState.unspent for s in states.states])
|
||||
assert all([s.unspent for s in states.states])
|
||||
|
||||
@@ -59,19 +59,24 @@ async def test_regtest_pay_mpp(wallet: Wallet, ledger: Ledger):
|
||||
fee_reserve_sat=quote.fee_reserve,
|
||||
quote_id=quote.quote,
|
||||
)
|
||||
|
||||
def mint_pay_mpp(invoice: str, amount: int, proofs: List[Proof]):
|
||||
asyncio.run(_mint_pay_mpp(invoice, amount, proofs))
|
||||
|
||||
# call pay_mpp twice in parallel to pay the full invoice
|
||||
t1 = threading.Thread(target=mint_pay_mpp, args=(invoice_payment_request, 32, proofs1))
|
||||
t2 = threading.Thread(target=partial_pay_real_invoice, args=(invoice_payment_request, 32, 1))
|
||||
t1 = threading.Thread(
|
||||
target=mint_pay_mpp, args=(invoice_payment_request, 32, proofs1)
|
||||
)
|
||||
t2 = threading.Thread(
|
||||
target=partial_pay_real_invoice, args=(invoice_payment_request, 32, 1)
|
||||
)
|
||||
|
||||
t1.start()
|
||||
t2.start()
|
||||
t1.join()
|
||||
t2.join()
|
||||
|
||||
assert wallet.balance <= 256 - 32
|
||||
assert wallet.balance == 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -3,7 +3,7 @@ import asyncio
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import Method, MintQuoteState, ProofSpentState, ProofState
|
||||
from cashu.core.base import Method, MintQuoteState, ProofState
|
||||
from cashu.core.json_rpc.base import JSONRPCNotficationParams
|
||||
from cashu.core.nuts import WEBSOCKETS_NUT
|
||||
from cashu.core.settings import settings
|
||||
@@ -54,13 +54,10 @@ async def test_wallet_subscription_mint(wallet: Wallet):
|
||||
assert triggered
|
||||
assert len(msg_stack) == 3
|
||||
|
||||
assert msg_stack[0].payload["paid"] is False
|
||||
assert msg_stack[0].payload["state"] == MintQuoteState.unpaid.value
|
||||
|
||||
assert msg_stack[1].payload["paid"] is True
|
||||
assert msg_stack[1].payload["state"] == MintQuoteState.paid.value
|
||||
|
||||
assert msg_stack[2].payload["paid"] is True
|
||||
assert msg_stack[2].payload["state"] == MintQuoteState.issued.value
|
||||
|
||||
|
||||
@@ -100,16 +97,16 @@ async def test_wallet_subscription_swap(wallet: Wallet):
|
||||
pending_stack = msg_stack[:n_subscriptions]
|
||||
for msg in pending_stack:
|
||||
proof_state = ProofState.parse_obj(msg.payload)
|
||||
assert proof_state.state == ProofSpentState.unspent
|
||||
assert proof_state.unspent
|
||||
|
||||
# the second one is the PENDING state
|
||||
spent_stack = msg_stack[n_subscriptions : n_subscriptions * 2]
|
||||
for msg in spent_stack:
|
||||
proof_state = ProofState.parse_obj(msg.payload)
|
||||
assert proof_state.state == ProofSpentState.pending
|
||||
assert proof_state.pending
|
||||
|
||||
# the third one is the SPENT state
|
||||
spent_stack = msg_stack[n_subscriptions * 2 :]
|
||||
for msg in spent_stack:
|
||||
proof_state = ProofState.parse_obj(msg.payload)
|
||||
assert proof_state.state == ProofSpentState.spent
|
||||
assert proof_state.spent
|
||||
|
||||
Reference in New Issue
Block a user