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:
|
def kind(self) -> JSONRPCSubscriptionKinds:
|
||||||
return JSONRPCSubscriptionKinds.PROOF_STATE
|
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):
|
class HTLCWitness(BaseModel):
|
||||||
preimage: Optional[str] = None
|
preimage: Optional[str] = None
|
||||||
@@ -290,7 +302,6 @@ class MeltQuote(LedgerEvent):
|
|||||||
unit: str
|
unit: str
|
||||||
amount: int
|
amount: int
|
||||||
fee_reserve: int
|
fee_reserve: int
|
||||||
paid: bool
|
|
||||||
state: MeltQuoteState
|
state: MeltQuoteState
|
||||||
created_time: Union[int, None] = None
|
created_time: Union[int, None] = None
|
||||||
paid_time: Union[int, None] = None
|
paid_time: Union[int, None] = None
|
||||||
@@ -325,7 +336,6 @@ class MeltQuote(LedgerEvent):
|
|||||||
unit=row["unit"],
|
unit=row["unit"],
|
||||||
amount=row["amount"],
|
amount=row["amount"],
|
||||||
fee_reserve=row["fee_reserve"],
|
fee_reserve=row["fee_reserve"],
|
||||||
paid=row["paid"],
|
|
||||||
state=MeltQuoteState[row["state"]],
|
state=MeltQuoteState[row["state"]],
|
||||||
created_time=created_time,
|
created_time=created_time,
|
||||||
paid_time=paid_time,
|
paid_time=paid_time,
|
||||||
@@ -344,17 +354,34 @@ class MeltQuote(LedgerEvent):
|
|||||||
def kind(self) -> JSONRPCSubscriptionKinds:
|
def kind(self) -> JSONRPCSubscriptionKinds:
|
||||||
return JSONRPCSubscriptionKinds.BOLT11_MELT_QUOTE
|
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
|
# 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):
|
def __setattr__(self, name, value):
|
||||||
# an unpaid quote can only be set to pending or paid
|
# 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]:
|
if value not in [MeltQuoteState.pending, MeltQuoteState.paid]:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
f"Cannot change state of an unpaid melt quote to {value}."
|
f"Cannot change state of an unpaid melt quote to {value}."
|
||||||
)
|
)
|
||||||
# a paid quote can not be changed
|
# 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.")
|
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)
|
super().__setattr__(name, value)
|
||||||
|
|
||||||
|
|
||||||
@@ -375,8 +402,6 @@ class MintQuote(LedgerEvent):
|
|||||||
checking_id: str
|
checking_id: str
|
||||||
unit: str
|
unit: str
|
||||||
amount: int
|
amount: int
|
||||||
paid: bool
|
|
||||||
issued: bool
|
|
||||||
state: MintQuoteState
|
state: MintQuoteState
|
||||||
created_time: Union[int, None] = None
|
created_time: Union[int, None] = None
|
||||||
paid_time: Union[int, None] = None
|
paid_time: Union[int, None] = None
|
||||||
@@ -401,8 +426,6 @@ class MintQuote(LedgerEvent):
|
|||||||
checking_id=row["checking_id"],
|
checking_id=row["checking_id"],
|
||||||
unit=row["unit"],
|
unit=row["unit"],
|
||||||
amount=row["amount"],
|
amount=row["amount"],
|
||||||
paid=row["paid"],
|
|
||||||
issued=row["issued"],
|
|
||||||
state=MintQuoteState[row["state"]],
|
state=MintQuoteState[row["state"]],
|
||||||
created_time=created_time,
|
created_time=created_time,
|
||||||
paid_time=paid_time,
|
paid_time=paid_time,
|
||||||
@@ -417,24 +440,45 @@ class MintQuote(LedgerEvent):
|
|||||||
def kind(self) -> JSONRPCSubscriptionKinds:
|
def kind(self) -> JSONRPCSubscriptionKinds:
|
||||||
return JSONRPCSubscriptionKinds.BOLT11_MINT_QUOTE
|
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):
|
def __setattr__(self, name, value):
|
||||||
# un unpaid quote can only be set to paid
|
# 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:
|
if value != MintQuoteState.paid:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
f"Cannot change state of an unpaid mint quote to {value}."
|
f"Cannot change state of an unpaid mint quote to {value}."
|
||||||
)
|
)
|
||||||
# a paid quote can only be set to pending or issued
|
# 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:
|
if value != MintQuoteState.pending and value != MintQuoteState.issued:
|
||||||
raise Exception(f"Cannot change state of a paid mint quote to {value}.")
|
raise Exception(f"Cannot change state of a paid mint quote to {value}.")
|
||||||
# a pending quote can only be set to paid or issued
|
# 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]:
|
if value not in [MintQuoteState.paid, MintQuoteState.issued]:
|
||||||
raise Exception("Cannot change state of a pending mint quote.")
|
raise Exception("Cannot change state of a pending mint quote.")
|
||||||
# an issued quote cannot be changed
|
# 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.")
|
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)
|
super().__setattr__(name, value)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ class PostMeltQuoteResponse(BaseModel):
|
|||||||
fee_reserve: int # input fee reserve
|
fee_reserve: int # input fee reserve
|
||||||
paid: Optional[
|
paid: Optional[
|
||||||
bool
|
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
|
state: Optional[str] # state of the quote
|
||||||
expiry: Optional[int] # expiry of the quote
|
expiry: Optional[int] # expiry of the quote
|
||||||
payment_preimage: Optional[str] = None # payment preimage
|
payment_preimage: Optional[str] = None # payment preimage
|
||||||
@@ -224,6 +224,8 @@ class PostMeltQuoteResponse(BaseModel):
|
|||||||
to_dict = melt_quote.dict()
|
to_dict = melt_quote.dict()
|
||||||
# turn state into string
|
# turn state into string
|
||||||
to_dict["state"] = melt_quote.state.value
|
to_dict["state"] = melt_quote.state.value
|
||||||
|
# add deprecated "paid" field
|
||||||
|
to_dict["paid"] = melt_quote.paid
|
||||||
return PostMeltQuoteResponse.parse_obj(to_dict)
|
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_outgoing_payment: Optional[float] = Field(default=3.0)
|
||||||
fakewallet_delay_incoming_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_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):
|
class MintInformation(CashuSettings):
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from enum import Enum, auto
|
||||||
from typing import AsyncGenerator, Coroutine, Optional, Union
|
from typing import AsyncGenerator, Coroutine, Optional, Union
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -12,8 +13,8 @@ from ..core.models import PostMeltQuoteRequest
|
|||||||
|
|
||||||
|
|
||||||
class StatusResponse(BaseModel):
|
class StatusResponse(BaseModel):
|
||||||
error_message: Optional[str]
|
|
||||||
balance: Union[int, float]
|
balance: Union[int, float]
|
||||||
|
error_message: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class InvoiceQuoteResponse(BaseModel):
|
class InvoiceQuoteResponse(BaseModel):
|
||||||
@@ -34,36 +35,77 @@ class InvoiceResponse(BaseModel):
|
|||||||
error_message: Optional[str] = None
|
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):
|
class PaymentResponse(BaseModel):
|
||||||
ok: Optional[bool] = None # True: paid, False: failed, None: pending or unknown
|
result: PaymentResult
|
||||||
checking_id: Optional[str] = None
|
checking_id: Optional[str] = None
|
||||||
fee: Optional[Amount] = None
|
fee: Optional[Amount] = None
|
||||||
preimage: Optional[str] = None
|
preimage: Optional[str] = None
|
||||||
error_message: Optional[str] = None
|
error_message: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class PaymentStatus(BaseModel):
|
|
||||||
paid: Optional[bool] = None
|
|
||||||
fee: Optional[Amount] = None
|
|
||||||
preimage: Optional[str] = None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pending(self) -> bool:
|
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
|
@property
|
||||||
def failed(self) -> bool:
|
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:
|
def __str__(self) -> str:
|
||||||
if self.paid is True:
|
if self.result == PaymentResult.SETTLED:
|
||||||
return "settled"
|
return (
|
||||||
elif self.paid is False:
|
"settled"
|
||||||
|
+ (f" (preimage: {self.preimage})" if self.preimage else "")
|
||||||
|
+ (f" (fee: {self.fee})" if self.fee else "")
|
||||||
|
)
|
||||||
|
elif self.result == PaymentResult.FAILED:
|
||||||
return "failed"
|
return "failed"
|
||||||
elif self.paid is None:
|
elif self.result == PaymentResult.PENDING:
|
||||||
return "still pending"
|
return "still pending"
|
||||||
else:
|
else: # self.result == PaymentResult.UNKNOWN:
|
||||||
return "unknown (should never happen)"
|
return "unknown" + (
|
||||||
|
f" (Error: {self.error_message})" if self.error_message else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class LightningBackend(ABC):
|
class LightningBackend(ABC):
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
# type: ignore
|
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
from typing import AsyncGenerator, Dict, Optional, Union
|
from typing import AsyncGenerator, Dict, Optional, Union
|
||||||
@@ -18,6 +17,7 @@ from .base import (
|
|||||||
LightningBackend,
|
LightningBackend,
|
||||||
PaymentQuoteResponse,
|
PaymentQuoteResponse,
|
||||||
PaymentResponse,
|
PaymentResponse,
|
||||||
|
PaymentResult,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
StatusResponse,
|
StatusResponse,
|
||||||
)
|
)
|
||||||
@@ -30,6 +30,22 @@ DIRECTION_RECEIVE = "RECEIVE"
|
|||||||
PROBE_FEE_TIMEOUT_SEC = 1
|
PROBE_FEE_TIMEOUT_SEC = 1
|
||||||
MINIMUM_FEE_MSAT = 2000
|
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):
|
class BlinkWallet(LightningBackend):
|
||||||
"""https://dev.blink.sv/
|
"""https://dev.blink.sv/
|
||||||
@@ -38,13 +54,6 @@ class BlinkWallet(LightningBackend):
|
|||||||
|
|
||||||
wallet_ids: Dict[Unit, str] = {}
|
wallet_ids: Dict[Unit, str] = {}
|
||||||
endpoint = "https://api.blink.sv/graphql"
|
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}
|
supported_units = {Unit.sat, Unit.msat}
|
||||||
supports_description: bool = True
|
supports_description: bool = True
|
||||||
@@ -66,12 +75,13 @@ class BlinkWallet(LightningBackend):
|
|||||||
|
|
||||||
async def status(self) -> StatusResponse:
|
async def status(self) -> StatusResponse:
|
||||||
try:
|
try:
|
||||||
|
data = {
|
||||||
|
"query": "query me { me { defaultAccount { wallets { id walletCurrency balance }}}}",
|
||||||
|
"variables": {},
|
||||||
|
}
|
||||||
r = await self.client.post(
|
r = await self.client.post(
|
||||||
url=self.endpoint,
|
url=self.endpoint,
|
||||||
data=(
|
data=json.dumps(data), # type: ignore
|
||||||
'{"query":"query me { me { defaultAccount { wallets { id'
|
|
||||||
' walletCurrency balance }}}}", "variables":{}}'
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -96,10 +106,10 @@ class BlinkWallet(LightningBackend):
|
|||||||
resp.get("data", {}).get("me", {}).get("defaultAccount", {}).get("wallets")
|
resp.get("data", {}).get("me", {}).get("defaultAccount", {}).get("wallets")
|
||||||
):
|
):
|
||||||
if wallet_dict.get("walletCurrency") == "USD":
|
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":
|
elif wallet_dict.get("walletCurrency") == "BTC":
|
||||||
self.wallet_ids[Unit.sat] = wallet_dict["id"]
|
self.wallet_ids[Unit.sat] = wallet_dict["id"] # type: ignore
|
||||||
balance = wallet_dict["balance"]
|
balance = wallet_dict["balance"] # type: ignore
|
||||||
|
|
||||||
return StatusResponse(error_message=None, balance=balance)
|
return StatusResponse(error_message=None, balance=balance)
|
||||||
|
|
||||||
@@ -144,7 +154,7 @@ class BlinkWallet(LightningBackend):
|
|||||||
try:
|
try:
|
||||||
r = await self.client.post(
|
r = await self.client.post(
|
||||||
url=self.endpoint,
|
url=self.endpoint,
|
||||||
data=json.dumps(data),
|
data=json.dumps(data), # type: ignore
|
||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -197,13 +207,16 @@ class BlinkWallet(LightningBackend):
|
|||||||
try:
|
try:
|
||||||
r = await self.client.post(
|
r = await self.client.post(
|
||||||
url=self.endpoint,
|
url=self.endpoint,
|
||||||
data=json.dumps(data),
|
data=json.dumps(data), # type: ignore
|
||||||
timeout=None,
|
timeout=None,
|
||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Blink API error: {str(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()
|
resp: dict = r.json()
|
||||||
|
|
||||||
@@ -211,15 +224,22 @@ class BlinkWallet(LightningBackend):
|
|||||||
fee: Union[None, int] = None
|
fee: Union[None, int] = None
|
||||||
if resp.get("data", {}).get("lnInvoicePaymentSend", {}).get("errors"):
|
if resp.get("data", {}).get("lnInvoicePaymentSend", {}).get("errors"):
|
||||||
error_message = (
|
error_message = (
|
||||||
resp["data"]["lnInvoicePaymentSend"]["errors"][0].get("message")
|
resp["data"]["lnInvoicePaymentSend"]["errors"][0].get("message") # type: ignore
|
||||||
or "Unknown error"
|
or "Unknown error"
|
||||||
)
|
)
|
||||||
|
|
||||||
paid = self.payment_execution_statuses[
|
status_str = resp.get("data", {}).get("lnInvoicePaymentSend", {}).get("status")
|
||||||
resp.get("data", {}).get("lnInvoicePaymentSend", {}).get("status")
|
result = PAYMENT_EXECUTION_RESULT_MAP[status_str]
|
||||||
]
|
|
||||||
if paid is None:
|
if status_str == "ALREADY_PAID":
|
||||||
error_message = "Invoice 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", {}):
|
if resp.get("data", {}).get("lnInvoicePaymentSend", {}).get("transaction", {}):
|
||||||
fee = (
|
fee = (
|
||||||
@@ -230,15 +250,14 @@ class BlinkWallet(LightningBackend):
|
|||||||
)
|
)
|
||||||
|
|
||||||
checking_id = quote.request
|
checking_id = quote.request
|
||||||
|
|
||||||
# we check the payment status to get the preimage
|
# we check the payment status to get the preimage
|
||||||
preimage: Union[None, str] = None
|
preimage: Union[None, str] = None
|
||||||
payment_status = await self.get_payment_status(checking_id)
|
payment_status = await self.get_payment_status(checking_id)
|
||||||
if payment_status.paid:
|
if payment_status.settled:
|
||||||
preimage = payment_status.preimage
|
preimage = payment_status.preimage
|
||||||
|
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=paid,
|
result=result,
|
||||||
checking_id=checking_id,
|
checking_id=checking_id,
|
||||||
fee=Amount(Unit.sat, fee) if fee else None,
|
fee=Amount(Unit.sat, fee) if fee else None,
|
||||||
preimage=preimage,
|
preimage=preimage,
|
||||||
@@ -261,22 +280,27 @@ class BlinkWallet(LightningBackend):
|
|||||||
"variables": variables,
|
"variables": variables,
|
||||||
}
|
}
|
||||||
try:
|
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()
|
r.raise_for_status()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Blink API error: {str(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()
|
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(
|
logger.error(
|
||||||
"Blink Error",
|
"Blink Error",
|
||||||
resp.get("data", {}).get("lnInvoicePaymentStatus", {}).get("errors"),
|
error_message,
|
||||||
)
|
)
|
||||||
return PaymentStatus(paid=None)
|
return PaymentStatus(
|
||||||
paid = self.invoice_statuses[
|
result=PaymentResult.UNKNOWN, error_message=error_message
|
||||||
|
)
|
||||||
|
result = INVOICE_RESULT_MAP[
|
||||||
resp.get("data", {}).get("lnInvoicePaymentStatus", {}).get("status")
|
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:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
# Checking ID is the payment request and blink wants the payment hash
|
# Checking ID is the payment request and blink wants the payment hash
|
||||||
@@ -311,16 +335,11 @@ class BlinkWallet(LightningBackend):
|
|||||||
""",
|
""",
|
||||||
"variables": variables,
|
"variables": variables,
|
||||||
}
|
}
|
||||||
|
r = await self.client.post(
|
||||||
try:
|
url=self.endpoint,
|
||||||
r = await self.client.post(
|
data=json.dumps(data), # type: ignore
|
||||||
url=self.endpoint,
|
)
|
||||||
data=json.dumps(data),
|
r.raise_for_status()
|
||||||
)
|
|
||||||
r.raise_for_status()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Blink API error: {str(e)}")
|
|
||||||
return PaymentResponse(ok=False, error_message=str(e))
|
|
||||||
|
|
||||||
resp: dict = r.json()
|
resp: dict = r.json()
|
||||||
|
|
||||||
@@ -332,7 +351,9 @@ class BlinkWallet(LightningBackend):
|
|||||||
.get("walletById", {})
|
.get("walletById", {})
|
||||||
.get("transactionsByPaymentHash")
|
.get("transactionsByPaymentHash")
|
||||||
):
|
):
|
||||||
return PaymentStatus(paid=None)
|
return PaymentStatus(
|
||||||
|
result=PaymentResult.UNKNOWN, error_message="No payment found"
|
||||||
|
)
|
||||||
|
|
||||||
all_payments_with_this_hash = (
|
all_payments_with_this_hash = (
|
||||||
resp.get("data", {})
|
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
|
# 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"
|
# 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
|
# 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(
|
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
|
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
|
# 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"
|
# we only care about the payment with "direction" == "SEND"
|
||||||
@@ -363,15 +386,17 @@ class BlinkWallet(LightningBackend):
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
if not payment:
|
if not payment:
|
||||||
return PaymentStatus(paid=None)
|
return PaymentStatus(
|
||||||
|
result=PaymentResult.UNKNOWN, error_message="No payment found"
|
||||||
|
)
|
||||||
|
|
||||||
# we read the status of the payment
|
# we read the status of the payment
|
||||||
paid = self.payment_statuses[payment["status"]]
|
result = PAYMENT_RESULT_MAP[payment["status"]] # type: ignore
|
||||||
fee = payment["settlementFee"]
|
fee = payment["settlementFee"] # type: ignore
|
||||||
preimage = payment["settlementVia"].get("preImage")
|
preimage = payment["settlementVia"].get("preImage") # type: ignore
|
||||||
|
|
||||||
return PaymentStatus(
|
return PaymentStatus(
|
||||||
paid=paid,
|
result=result,
|
||||||
fee=Amount(Unit.sat, fee),
|
fee=Amount(Unit.sat, fee),
|
||||||
preimage=preimage,
|
preimage=preimage,
|
||||||
)
|
)
|
||||||
@@ -404,7 +429,7 @@ class BlinkWallet(LightningBackend):
|
|||||||
try:
|
try:
|
||||||
r = await self.client.post(
|
r = await self.client.post(
|
||||||
url=self.endpoint,
|
url=self.endpoint,
|
||||||
data=json.dumps(data),
|
data=json.dumps(data), # type: ignore
|
||||||
timeout=PROBE_FEE_TIMEOUT_SEC,
|
timeout=PROBE_FEE_TIMEOUT_SEC,
|
||||||
)
|
)
|
||||||
r.raise_for_status()
|
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
|
# if there was an error, we simply ignore the response and decide the fees ourselves
|
||||||
fees_response_msat = 0
|
fees_response_msat = 0
|
||||||
logger.debug(
|
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:
|
else:
|
||||||
@@ -454,5 +479,5 @@ class BlinkWallet(LightningBackend):
|
|||||||
amount=amount.to(self.unit, round="up"),
|
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")
|
raise NotImplementedError("paid_invoices_stream not implemented")
|
||||||
|
|||||||
@@ -20,11 +20,26 @@ from .base import (
|
|||||||
LightningBackend,
|
LightningBackend,
|
||||||
PaymentQuoteResponse,
|
PaymentQuoteResponse,
|
||||||
PaymentResponse,
|
PaymentResponse,
|
||||||
|
PaymentResult,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
StatusResponse,
|
StatusResponse,
|
||||||
Unsupported,
|
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):
|
class CLNRestWallet(LightningBackend):
|
||||||
supported_units = {Unit.sat, Unit.msat}
|
supported_units = {Unit.sat, Unit.msat}
|
||||||
@@ -68,12 +83,6 @@ class CLNRestWallet(LightningBackend):
|
|||||||
base_url=self.url, verify=self.cert, headers=self.auth
|
base_url=self.url, verify=self.cert, headers=self.auth
|
||||||
)
|
)
|
||||||
self.last_pay_index = 0
|
self.last_pay_index = 0
|
||||||
self.statuses = {
|
|
||||||
"paid": True,
|
|
||||||
"complete": True,
|
|
||||||
"failed": False,
|
|
||||||
"pending": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
try:
|
try:
|
||||||
@@ -101,7 +110,7 @@ class CLNRestWallet(LightningBackend):
|
|||||||
if len(data) == 0:
|
if len(data) == 0:
|
||||||
return StatusResponse(error_message="no data", balance=0)
|
return StatusResponse(error_message="no data", balance=0)
|
||||||
balance_msat = int(sum([c["our_amount_msat"] for c in data["channels"]]))
|
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(
|
async def create_invoice(
|
||||||
self,
|
self,
|
||||||
@@ -147,8 +156,6 @@ class CLNRestWallet(LightningBackend):
|
|||||||
|
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
ok=False,
|
ok=False,
|
||||||
checking_id=None,
|
|
||||||
payment_request=None,
|
|
||||||
error_message=error_message,
|
error_message=error_message,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -159,7 +166,6 @@ class CLNRestWallet(LightningBackend):
|
|||||||
ok=True,
|
ok=True,
|
||||||
checking_id=data["payment_hash"],
|
checking_id=data["payment_hash"],
|
||||||
payment_request=data["bolt11"],
|
payment_request=data["bolt11"],
|
||||||
error_message=None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def pay_invoice(
|
async def pay_invoice(
|
||||||
@@ -169,20 +175,14 @@ class CLNRestWallet(LightningBackend):
|
|||||||
invoice = decode(quote.request)
|
invoice = decode(quote.request)
|
||||||
except Bolt11Exception as exc:
|
except Bolt11Exception as exc:
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=False,
|
result=PaymentResult.FAILED,
|
||||||
checking_id=None,
|
|
||||||
fee=None,
|
|
||||||
preimage=None,
|
|
||||||
error_message=str(exc),
|
error_message=str(exc),
|
||||||
)
|
)
|
||||||
|
|
||||||
if not invoice.amount_msat or invoice.amount_msat <= 0:
|
if not invoice.amount_msat or invoice.amount_msat <= 0:
|
||||||
error_message = "0 amount invoices are not allowed"
|
error_message = "0 amount invoices are not allowed"
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=False,
|
result=PaymentResult.FAILED,
|
||||||
checking_id=None,
|
|
||||||
fee=None,
|
|
||||||
preimage=None,
|
|
||||||
error_message=error_message,
|
error_message=error_message,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -205,11 +205,7 @@ class CLNRestWallet(LightningBackend):
|
|||||||
error_message = "mint does not support MPP"
|
error_message = "mint does not support MPP"
|
||||||
logger.error(error_message)
|
logger.error(error_message)
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=False,
|
result=PaymentResult.FAILED, error_message=error_message
|
||||||
checking_id=None,
|
|
||||||
fee=None,
|
|
||||||
preimage=None,
|
|
||||||
error_message=error_message,
|
|
||||||
)
|
)
|
||||||
r = await self.client.post("/v1/pay", data=post_data, timeout=None)
|
r = await self.client.post("/v1/pay", data=post_data, timeout=None)
|
||||||
|
|
||||||
@@ -220,34 +216,20 @@ class CLNRestWallet(LightningBackend):
|
|||||||
except Exception:
|
except Exception:
|
||||||
error_message = r.text
|
error_message = r.text
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=False,
|
result=PaymentResult.FAILED, error_message=error_message
|
||||||
checking_id=None,
|
|
||||||
fee=None,
|
|
||||||
preimage=None,
|
|
||||||
error_message=error_message,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data = r.json()
|
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"]
|
checking_id = data["payment_hash"]
|
||||||
preimage = data["payment_preimage"]
|
preimage = data["payment_preimage"]
|
||||||
fee_msat = data["amount_sent_msat"] - data["amount_msat"]
|
fee_msat = data["amount_sent_msat"] - data["amount_msat"]
|
||||||
|
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=self.statuses.get(data["status"]),
|
result=PAYMENT_RESULT_MAP[data["status"]],
|
||||||
checking_id=checking_id,
|
checking_id=checking_id,
|
||||||
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
||||||
preimage=preimage,
|
preimage=preimage,
|
||||||
error_message=None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
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:
|
if r.is_error or "message" in data or data.get("invoices") is None:
|
||||||
raise Exception("error in cln response")
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Error getting invoice status: {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:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
r = await self.client.post(
|
r = await self.client.post(
|
||||||
"/v1/listpays",
|
"/v1/listpays",
|
||||||
data={"payment_hash": checking_id},
|
data={"payment_hash": checking_id},
|
||||||
)
|
)
|
||||||
try:
|
r.raise_for_status()
|
||||||
r.raise_for_status()
|
data = r.json()
|
||||||
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"]
|
|
||||||
|
|
||||||
|
if not data.get("pays"):
|
||||||
|
# payment not found
|
||||||
|
logger.error(f"payment not found: {data.get('pays')}")
|
||||||
return PaymentStatus(
|
return PaymentStatus(
|
||||||
paid=self.statuses.get(pay["status"]),
|
result=PaymentResult.UNKNOWN, error_message="payment not found"
|
||||||
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
|
||||||
preimage=preimage,
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error getting payment status: {e}")
|
if r.is_error or "message" in data:
|
||||||
return PaymentStatus(paid=None)
|
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]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
# call listinvoices to determine the last pay_index
|
# call listinvoices to determine the last pay_index
|
||||||
|
|||||||
@@ -19,12 +19,27 @@ from .base import (
|
|||||||
LightningBackend,
|
LightningBackend,
|
||||||
PaymentQuoteResponse,
|
PaymentQuoteResponse,
|
||||||
PaymentResponse,
|
PaymentResponse,
|
||||||
|
PaymentResult,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
StatusResponse,
|
StatusResponse,
|
||||||
Unsupported,
|
Unsupported,
|
||||||
)
|
)
|
||||||
from .macaroon import load_macaroon
|
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):
|
class CoreLightningRestWallet(LightningBackend):
|
||||||
supported_units = {Unit.sat, Unit.msat}
|
supported_units = {Unit.sat, Unit.msat}
|
||||||
@@ -61,12 +76,6 @@ class CoreLightningRestWallet(LightningBackend):
|
|||||||
base_url=self.url, verify=self.cert, headers=self.auth
|
base_url=self.url, verify=self.cert, headers=self.auth
|
||||||
)
|
)
|
||||||
self.last_pay_index = 0
|
self.last_pay_index = 0
|
||||||
self.statuses = {
|
|
||||||
"paid": True,
|
|
||||||
"complete": True,
|
|
||||||
"failed": False,
|
|
||||||
"pending": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
try:
|
try:
|
||||||
@@ -140,8 +149,6 @@ class CoreLightningRestWallet(LightningBackend):
|
|||||||
|
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
ok=False,
|
ok=False,
|
||||||
checking_id=None,
|
|
||||||
payment_request=None,
|
|
||||||
error_message=error_message,
|
error_message=error_message,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -152,7 +159,6 @@ class CoreLightningRestWallet(LightningBackend):
|
|||||||
ok=True,
|
ok=True,
|
||||||
checking_id=data["payment_hash"],
|
checking_id=data["payment_hash"],
|
||||||
payment_request=data["bolt11"],
|
payment_request=data["bolt11"],
|
||||||
error_message=None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def pay_invoice(
|
async def pay_invoice(
|
||||||
@@ -162,20 +168,14 @@ class CoreLightningRestWallet(LightningBackend):
|
|||||||
invoice = decode(quote.request)
|
invoice = decode(quote.request)
|
||||||
except Bolt11Exception as exc:
|
except Bolt11Exception as exc:
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=False,
|
result=PaymentResult.FAILED,
|
||||||
checking_id=None,
|
|
||||||
fee=None,
|
|
||||||
preimage=None,
|
|
||||||
error_message=str(exc),
|
error_message=str(exc),
|
||||||
)
|
)
|
||||||
|
|
||||||
if not invoice.amount_msat or invoice.amount_msat <= 0:
|
if not invoice.amount_msat or invoice.amount_msat <= 0:
|
||||||
error_message = "0 amount invoices are not allowed"
|
error_message = "0 amount invoices are not allowed"
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=False,
|
result=PaymentResult.FAILED,
|
||||||
checking_id=None,
|
|
||||||
fee=None,
|
|
||||||
preimage=None,
|
|
||||||
error_message=error_message,
|
error_message=error_message,
|
||||||
)
|
)
|
||||||
fee_limit_percent = fee_limit_msat / invoice.amount_msat * 100
|
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():
|
if r.is_error or "error" in r.json():
|
||||||
try:
|
try:
|
||||||
data = r.json()
|
data = r.json()
|
||||||
error_message = data["error"]
|
error_message = data["error"]["message"]
|
||||||
except Exception:
|
except Exception:
|
||||||
error_message = r.text
|
error_message = r.text
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=False,
|
result=PaymentResult.FAILED,
|
||||||
checking_id=None,
|
|
||||||
fee=None,
|
|
||||||
preimage=None,
|
|
||||||
error_message=error_message,
|
error_message=error_message,
|
||||||
)
|
)
|
||||||
|
|
||||||
data = r.json()
|
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"]
|
checking_id = data["payment_hash"]
|
||||||
preimage = data["payment_preimage"]
|
preimage = data["payment_preimage"]
|
||||||
fee_msat = data["amount_sent_msat"] - data["amount_msat"]
|
fee_msat = data["amount_sent_msat"] - data["amount_msat"]
|
||||||
|
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=self.statuses.get(data["status"]),
|
result=PAYMENT_RESULT_MAP[data["status"]],
|
||||||
checking_id=checking_id,
|
checking_id=checking_id,
|
||||||
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
||||||
preimage=preimage,
|
preimage=preimage,
|
||||||
error_message=None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
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:
|
if r.is_error or "error" in data or data.get("invoices") is None:
|
||||||
raise Exception("error in cln response")
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Error getting invoice status: {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:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
r = await self.client.get(
|
r = await self.client.get(
|
||||||
"/v1/pay/listPays",
|
"/v1/pay/listPays",
|
||||||
params={"payment_hash": checking_id},
|
params={"payment_hash": checking_id},
|
||||||
)
|
)
|
||||||
try:
|
r.raise_for_status()
|
||||||
r.raise_for_status()
|
data = r.json()
|
||||||
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"]
|
|
||||||
|
|
||||||
|
if not data.get("pays"):
|
||||||
|
# payment not found
|
||||||
|
logger.error(f"payment not found: {data.get('pays')}")
|
||||||
return PaymentStatus(
|
return PaymentStatus(
|
||||||
paid=self.statuses.get(pay["status"]),
|
result=PaymentResult.UNKNOWN, error_message="payment not found"
|
||||||
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
|
||||||
preimage=preimage,
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error getting payment status: {e}")
|
if r.is_error or "error" in data:
|
||||||
return PaymentStatus(paid=None)
|
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]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
# call listinvoices to determine the last pay_index
|
# call listinvoices to determine the last pay_index
|
||||||
@@ -285,7 +271,8 @@ class CoreLightningRestWallet(LightningBackend):
|
|||||||
data = r.json()
|
data = r.json()
|
||||||
if r.is_error or "error" in data:
|
if r.is_error or "error" in data:
|
||||||
raise Exception("error in cln response")
|
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:
|
while True:
|
||||||
try:
|
try:
|
||||||
@@ -315,9 +302,11 @@ class CoreLightningRestWallet(LightningBackend):
|
|||||||
)
|
)
|
||||||
paid_invoce = r.json()
|
paid_invoce = r.json()
|
||||||
logger.trace(f"paid invoice: {paid_invoce}")
|
logger.trace(f"paid invoice: {paid_invoce}")
|
||||||
assert self.statuses[
|
if (
|
||||||
paid_invoce["invoices"][0]["status"]
|
INVOICE_RESULT_MAP[paid_invoce["invoices"][0]["status"]]
|
||||||
], "streamed invoice not paid"
|
!= PaymentResult.SETTLED
|
||||||
|
):
|
||||||
|
raise Exception("invoice not paid")
|
||||||
assert "invoices" in paid_invoce, "no invoices in response"
|
assert "invoices" in paid_invoce, "no invoices in response"
|
||||||
assert len(paid_invoce["invoices"]), "no invoices in response"
|
assert len(paid_invoce["invoices"]), "no invoices in response"
|
||||||
yield paid_invoce["invoices"][0]["payment_hash"]
|
yield paid_invoce["invoices"][0]["payment_hash"]
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from .base import (
|
|||||||
LightningBackend,
|
LightningBackend,
|
||||||
PaymentQuoteResponse,
|
PaymentQuoteResponse,
|
||||||
PaymentResponse,
|
PaymentResponse,
|
||||||
|
PaymentResult,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
StatusResponse,
|
StatusResponse,
|
||||||
)
|
)
|
||||||
@@ -151,6 +152,14 @@ class FakeWallet(LightningBackend):
|
|||||||
if settings.fakewallet_delay_outgoing_payment:
|
if settings.fakewallet_delay_outgoing_payment:
|
||||||
await asyncio.sleep(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.payment_hash in self.payment_secrets or settings.fakewallet_brr:
|
||||||
if invoice not in self.paid_invoices_outgoing:
|
if invoice not in self.paid_invoices_outgoing:
|
||||||
self.paid_invoices_outgoing.append(invoice)
|
self.paid_invoices_outgoing.append(invoice)
|
||||||
@@ -158,28 +167,33 @@ class FakeWallet(LightningBackend):
|
|||||||
raise ValueError("Invoice already paid")
|
raise ValueError("Invoice already paid")
|
||||||
|
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=True,
|
result=PaymentResult.SETTLED,
|
||||||
checking_id=invoice.payment_hash,
|
checking_id=invoice.payment_hash,
|
||||||
fee=Amount(unit=self.unit, amount=1),
|
fee=Amount(unit=self.unit, amount=1),
|
||||||
preimage=self.payment_secrets.get(invoice.payment_hash) or "0" * 64,
|
preimage=self.payment_secrets.get(invoice.payment_hash) or "0" * 64,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return PaymentResponse(
|
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:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
await self.mark_invoice_paid(self.create_dummy_bolt11(checking_id), delay=False)
|
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]
|
paid_chceking_ids = [i.payment_hash for i in self.paid_invoices_incoming]
|
||||||
if checking_id in paid_chceking_ids:
|
if checking_id in paid_chceking_ids:
|
||||||
paid = True
|
return PaymentStatus(result=PaymentResult.SETTLED)
|
||||||
else:
|
else:
|
||||||
paid = False
|
return PaymentStatus(
|
||||||
|
result=PaymentResult.UNKNOWN, error_message="Invoice not found"
|
||||||
|
)
|
||||||
|
|
||||||
return PaymentStatus(paid=paid)
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
|
if settings.fakewallet_payment_state:
|
||||||
async def get_payment_status(self, _: str) -> PaymentStatus:
|
return PaymentStatus(
|
||||||
return PaymentStatus(paid=settings.fakewallet_payment_state)
|
result=PaymentResult[settings.fakewallet_payment_state]
|
||||||
|
)
|
||||||
|
return PaymentStatus(result=PaymentResult.SETTLED)
|
||||||
|
|
||||||
async def get_payment_quote(
|
async def get_payment_quote(
|
||||||
self, melt_quote: PostMeltQuoteRequest
|
self, melt_quote: PostMeltQuoteRequest
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from .base import (
|
|||||||
LightningBackend,
|
LightningBackend,
|
||||||
PaymentQuoteResponse,
|
PaymentQuoteResponse,
|
||||||
PaymentResponse,
|
PaymentResponse,
|
||||||
|
PaymentResult,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
StatusResponse,
|
StatusResponse,
|
||||||
)
|
)
|
||||||
@@ -112,11 +113,18 @@ class LNbitsWallet(LightningBackend):
|
|||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
except Exception:
|
except Exception:
|
||||||
return PaymentResponse(error_message=r.json()["detail"])
|
return PaymentResponse(
|
||||||
|
result=PaymentResult.FAILED, error_message=r.json()["detail"]
|
||||||
|
)
|
||||||
if r.status_code > 299:
|
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():
|
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()
|
data: dict = r.json()
|
||||||
checking_id = data["payment_hash"]
|
checking_id = data["payment_hash"]
|
||||||
@@ -125,7 +133,7 @@ class LNbitsWallet(LightningBackend):
|
|||||||
payment: PaymentStatus = await self.get_payment_status(checking_id)
|
payment: PaymentStatus = await self.get_payment_status(checking_id)
|
||||||
|
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=True,
|
result=payment.result,
|
||||||
checking_id=checking_id,
|
checking_id=checking_id,
|
||||||
fee=payment.fee,
|
fee=payment.fee,
|
||||||
preimage=payment.preimage,
|
preimage=payment.preimage,
|
||||||
@@ -137,12 +145,28 @@ class LNbitsWallet(LightningBackend):
|
|||||||
url=f"{self.endpoint}/api/v1/payments/{checking_id}"
|
url=f"{self.endpoint}/api/v1/payments/{checking_id}"
|
||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
return PaymentStatus(paid=None)
|
return PaymentStatus(result=PaymentResult.UNKNOWN, error_message=str(e))
|
||||||
data: dict = r.json()
|
data: dict = r.json()
|
||||||
if data.get("detail"):
|
if data.get("detail"):
|
||||||
return PaymentStatus(paid=None)
|
return PaymentStatus(
|
||||||
return PaymentStatus(paid=r.json()["paid"])
|
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:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
try:
|
try:
|
||||||
@@ -150,26 +174,32 @@ class LNbitsWallet(LightningBackend):
|
|||||||
url=f"{self.endpoint}/api/v1/payments/{checking_id}"
|
url=f"{self.endpoint}/api/v1/payments/{checking_id}"
|
||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
except Exception:
|
except httpx.HTTPStatusError as e:
|
||||||
return PaymentStatus(paid=None)
|
if e.response.status_code != 404:
|
||||||
|
raise e
|
||||||
|
return PaymentStatus(
|
||||||
|
result=PaymentResult.UNKNOWN, error_message=e.response.text
|
||||||
|
)
|
||||||
|
|
||||||
data = r.json()
|
data = r.json()
|
||||||
if "paid" not in data and "details" not in data:
|
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"]:
|
if data["paid"]:
|
||||||
paid_value = True
|
result = PaymentResult.SETTLED
|
||||||
elif not data["paid"] and data["details"]["pending"]:
|
elif not data["paid"] and data["details"]["pending"]:
|
||||||
paid_value = None
|
result = PaymentResult.PENDING
|
||||||
elif not data["paid"] and not data["details"]["pending"]:
|
elif not data["paid"] and not data["details"]["pending"]:
|
||||||
paid_value = False
|
result = PaymentResult.FAILED
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"unexpected value for paid: {data['paid']}")
|
result = PaymentResult.UNKNOWN
|
||||||
|
|
||||||
return PaymentStatus(
|
return PaymentStatus(
|
||||||
paid=paid_value,
|
result=result,
|
||||||
fee=Amount(unit=Unit.msat, amount=abs(data["details"]["fee"])),
|
fee=Amount(unit=Unit.msat, amount=abs(data["details"]["fee"])),
|
||||||
preimage=data["preimage"],
|
preimage=data.get("preimage"),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_payment_quote(
|
async def get_payment_quote(
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from cashu.lightning.base import (
|
|||||||
LightningBackend,
|
LightningBackend,
|
||||||
PaymentQuoteResponse,
|
PaymentQuoteResponse,
|
||||||
PaymentResponse,
|
PaymentResponse,
|
||||||
|
PaymentResult,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
PostMeltQuoteRequest,
|
PostMeltQuoteRequest,
|
||||||
StatusResponse,
|
StatusResponse,
|
||||||
@@ -31,18 +32,18 @@ from cashu.lightning.base import (
|
|||||||
|
|
||||||
# maps statuses to None, False, True:
|
# maps statuses to None, False, True:
|
||||||
# https://api.lightning.community/?python=#paymentpaymentstatus
|
# https://api.lightning.community/?python=#paymentpaymentstatus
|
||||||
PAYMENT_STATUSES = {
|
PAYMENT_RESULT_MAP = {
|
||||||
lnrpc.Payment.PaymentStatus.UNKNOWN: None,
|
lnrpc.Payment.PaymentStatus.UNKNOWN: PaymentResult.UNKNOWN,
|
||||||
lnrpc.Payment.PaymentStatus.IN_FLIGHT: None,
|
lnrpc.Payment.PaymentStatus.IN_FLIGHT: PaymentResult.PENDING,
|
||||||
lnrpc.Payment.PaymentStatus.INITIATED: None,
|
lnrpc.Payment.PaymentStatus.INITIATED: PaymentResult.PENDING,
|
||||||
lnrpc.Payment.PaymentStatus.SUCCEEDED: True,
|
lnrpc.Payment.PaymentStatus.SUCCEEDED: PaymentResult.SETTLED,
|
||||||
lnrpc.Payment.PaymentStatus.FAILED: False,
|
lnrpc.Payment.PaymentStatus.FAILED: PaymentResult.FAILED,
|
||||||
}
|
}
|
||||||
INVOICE_STATUSES = {
|
INVOICE_RESULT_MAP = {
|
||||||
lnrpc.Invoice.InvoiceState.OPEN: None,
|
lnrpc.Invoice.InvoiceState.OPEN: PaymentResult.PENDING,
|
||||||
lnrpc.Invoice.InvoiceState.SETTLED: True,
|
lnrpc.Invoice.InvoiceState.SETTLED: PaymentResult.SETTLED,
|
||||||
lnrpc.Invoice.InvoiceState.CANCELED: None,
|
lnrpc.Invoice.InvoiceState.CANCELED: PaymentResult.FAILED,
|
||||||
lnrpc.Invoice.InvoiceState.ACCEPTED: None,
|
lnrpc.Invoice.InvoiceState.ACCEPTED: PaymentResult.PENDING,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -181,13 +182,13 @@ class LndRPCWallet(LightningBackend):
|
|||||||
except AioRpcError as e:
|
except AioRpcError as e:
|
||||||
error_message = f"SendPaymentSync failed: {e}"
|
error_message = f"SendPaymentSync failed: {e}"
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=False,
|
result=PaymentResult.FAILED,
|
||||||
error_message=error_message,
|
error_message=error_message,
|
||||||
)
|
)
|
||||||
|
|
||||||
if r.payment_error:
|
if r.payment_error:
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=False,
|
result=PaymentResult.FAILED,
|
||||||
error_message=r.payment_error,
|
error_message=r.payment_error,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -195,7 +196,7 @@ class LndRPCWallet(LightningBackend):
|
|||||||
fee_msat = r.payment_route.total_fees_msat
|
fee_msat = r.payment_route.total_fees_msat
|
||||||
preimage = r.payment_preimage.hex()
|
preimage = r.payment_preimage.hex()
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=True,
|
result=PaymentResult.SETTLED,
|
||||||
checking_id=checking_id,
|
checking_id=checking_id,
|
||||||
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
||||||
preimage=preimage,
|
preimage=preimage,
|
||||||
@@ -262,7 +263,7 @@ class LndRPCWallet(LightningBackend):
|
|||||||
except AioRpcError as e:
|
except AioRpcError as e:
|
||||||
logger.error(f"QueryRoute or SendToRouteV2 failed: {e}")
|
logger.error(f"QueryRoute or SendToRouteV2 failed: {e}")
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=False,
|
result=PaymentResult.FAILED,
|
||||||
error_message=str(e),
|
error_message=str(e),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -270,16 +271,23 @@ class LndRPCWallet(LightningBackend):
|
|||||||
error_message = f"Sending to route failed with code {r.failure.code}"
|
error_message = f"Sending to route failed with code {r.failure.code}"
|
||||||
logger.error(error_message)
|
logger.error(error_message)
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=False,
|
result=PaymentResult.FAILED,
|
||||||
error_message=error_message,
|
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
|
checking_id = invoice.payment_hash
|
||||||
fee_msat = r.route.total_fees_msat
|
fee_msat = r.route.total_fees_msat
|
||||||
preimage = r.preimage.hex()
|
preimage = r.preimage.hex()
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=ok,
|
result=result,
|
||||||
checking_id=checking_id,
|
checking_id=checking_id,
|
||||||
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
||||||
preimage=preimage,
|
preimage=preimage,
|
||||||
@@ -299,44 +307,47 @@ class LndRPCWallet(LightningBackend):
|
|||||||
except AioRpcError as e:
|
except AioRpcError as e:
|
||||||
error_message = f"LookupInvoice failed: {e}"
|
error_message = f"LookupInvoice failed: {e}"
|
||||||
logger.error(error_message)
|
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:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
"""
|
"""
|
||||||
This routine checks the payment status using routerpc.TrackPaymentV2.
|
This routine checks the payment status using routerpc.TrackPaymentV2.
|
||||||
"""
|
"""
|
||||||
# convert checking_id from hex to bytes and some LND magic
|
# convert checking_id from hex to bytes and some LND magic
|
||||||
try:
|
checking_id_bytes = bytes.fromhex(checking_id)
|
||||||
checking_id_bytes = bytes.fromhex(checking_id)
|
|
||||||
except ValueError:
|
|
||||||
logger.error(f"Couldn't convert {checking_id = } to bytes")
|
|
||||||
return PaymentStatus(paid=None)
|
|
||||||
|
|
||||||
request = routerrpc.TrackPaymentRequest(payment_hash=checking_id_bytes)
|
request = routerrpc.TrackPaymentRequest(payment_hash=checking_id_bytes)
|
||||||
|
|
||||||
try:
|
async with grpc.aio.secure_channel(
|
||||||
async with grpc.aio.secure_channel(
|
self.endpoint, self.combined_creds
|
||||||
self.endpoint, self.combined_creds
|
) as channel:
|
||||||
) as channel:
|
router_stub = routerstub.RouterStub(channel)
|
||||||
router_stub = routerstub.RouterStub(channel)
|
try:
|
||||||
async for payment in router_stub.TrackPaymentV2(request):
|
async for payment in router_stub.TrackPaymentV2(request):
|
||||||
if payment is not None and payment.status:
|
if payment is not None and payment.status:
|
||||||
|
preimage = (
|
||||||
|
payment.payment_preimage
|
||||||
|
if payment.payment_preimage != "0" * 64
|
||||||
|
else None
|
||||||
|
)
|
||||||
return PaymentStatus(
|
return PaymentStatus(
|
||||||
paid=PAYMENT_STATUSES[payment.status],
|
result=PAYMENT_RESULT_MAP[payment.status],
|
||||||
fee=(
|
fee=(
|
||||||
Amount(unit=Unit.msat, amount=payment.fee_msat)
|
Amount(unit=Unit.msat, amount=payment.fee_msat)
|
||||||
if payment.fee_msat
|
if payment.fee_msat
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
preimage=payment.payment_preimage,
|
preimage=preimage,
|
||||||
)
|
)
|
||||||
except AioRpcError as e:
|
except AioRpcError as e:
|
||||||
error_message = f"TrackPaymentV2 failed: {e}"
|
# status = StatusCode.NOT_FOUND
|
||||||
logger.error(error_message)
|
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]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
@@ -21,11 +21,26 @@ from .base import (
|
|||||||
LightningBackend,
|
LightningBackend,
|
||||||
PaymentQuoteResponse,
|
PaymentQuoteResponse,
|
||||||
PaymentResponse,
|
PaymentResponse,
|
||||||
|
PaymentResult,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
StatusResponse,
|
StatusResponse,
|
||||||
)
|
)
|
||||||
from .macaroon import load_macaroon
|
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):
|
class LndRestWallet(LightningBackend):
|
||||||
"""https://api.lightning.community/rest/index.html#lnd-rest-api-reference"""
|
"""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"):
|
if r.is_error or r.json().get("payment_error"):
|
||||||
error_message = r.json().get("payment_error") or r.text
|
error_message = r.json().get("payment_error") or r.text
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=False,
|
result=PaymentResult.FAILED, error_message=error_message
|
||||||
checking_id=None,
|
|
||||||
fee=None,
|
|
||||||
preimage=None,
|
|
||||||
error_message=error_message,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data = r.json()
|
data = r.json()
|
||||||
@@ -199,11 +210,10 @@ class LndRestWallet(LightningBackend):
|
|||||||
fee_msat = int(data["payment_route"]["total_fees_msat"])
|
fee_msat = int(data["payment_route"]["total_fees_msat"])
|
||||||
preimage = base64.b64decode(data["payment_preimage"]).hex()
|
preimage = base64.b64decode(data["payment_preimage"]).hex()
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=True,
|
result=PaymentResult.SETTLED,
|
||||||
checking_id=checking_id,
|
checking_id=checking_id,
|
||||||
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
||||||
preimage=preimage,
|
preimage=preimage,
|
||||||
error_message=None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def pay_partial_invoice(
|
async def pay_partial_invoice(
|
||||||
@@ -237,11 +247,7 @@ class LndRestWallet(LightningBackend):
|
|||||||
if r.is_error or data.get("message"):
|
if r.is_error or data.get("message"):
|
||||||
error_message = data.get("message") or r.text
|
error_message = data.get("message") or r.text
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=False,
|
result=PaymentResult.FAILED, error_message=error_message
|
||||||
checking_id=None,
|
|
||||||
fee=None,
|
|
||||||
preimage=None,
|
|
||||||
error_message=error_message,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# We need to set the mpp_record for a partial payment
|
# 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"):
|
if r.is_error or data.get("message"):
|
||||||
error_message = data.get("message") or r.text
|
error_message = data.get("message") or r.text
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=False,
|
result=PaymentResult.FAILED, error_message=error_message
|
||||||
checking_id=None,
|
|
||||||
fee=None,
|
|
||||||
preimage=None,
|
|
||||||
error_message=error_message,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ok = data.get("status") == "SUCCEEDED"
|
result = PAYMENT_RESULT_MAP.get(data.get("status"), PaymentResult.UNKNOWN)
|
||||||
checking_id = invoice.payment_hash
|
checking_id = invoice.payment_hash
|
||||||
fee_msat = int(data["route"]["total_fees_msat"])
|
fee_msat = int(data["route"]["total_fees_msat"]) if data.get("route") else None
|
||||||
preimage = base64.b64decode(data["preimage"]).hex()
|
preimage = (
|
||||||
|
base64.b64decode(data["preimage"]).hex() if data.get("preimage") else None
|
||||||
|
)
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=ok,
|
result=result,
|
||||||
checking_id=checking_id,
|
checking_id=checking_id,
|
||||||
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
||||||
preimage=preimage,
|
preimage=preimage,
|
||||||
error_message=None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
r = await self.client.get(url=f"/v1/invoice/{checking_id}")
|
r = await self.client.get(url=f"/v1/invoice/{checking_id}")
|
||||||
|
|
||||||
if r.is_error or not r.json().get("settled"):
|
if r.is_error:
|
||||||
# this must also work when checking_id is not a hex recognizable by lnd
|
logger.error(f"Couldn't get invoice status: {r.text}")
|
||||||
# it will return an error and no "settled" attribute on the object
|
return PaymentStatus(result=PaymentResult.UNKNOWN, error_message=r.text)
|
||||||
return PaymentStatus(paid=None)
|
|
||||||
|
|
||||||
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:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
"""
|
"""
|
||||||
This routine checks the payment status using routerpc.TrackPaymentV2.
|
This routine checks the payment status using routerpc.TrackPaymentV2.
|
||||||
"""
|
"""
|
||||||
# convert checking_id from hex to base64 and some LND magic
|
# convert checking_id from hex to base64 and some LND magic
|
||||||
try:
|
checking_id = base64.urlsafe_b64encode(bytes.fromhex(checking_id)).decode(
|
||||||
checking_id = base64.urlsafe_b64encode(bytes.fromhex(checking_id)).decode(
|
"ascii"
|
||||||
"ascii"
|
)
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
return PaymentStatus(paid=None)
|
|
||||||
|
|
||||||
url = f"/v2/router/track/{checking_id}"
|
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 with self.client.stream("GET", url, timeout=None) as r:
|
||||||
async for json_line in r.aiter_lines():
|
async for json_line in r.aiter_lines():
|
||||||
try:
|
try:
|
||||||
@@ -337,27 +337,37 @@ class LndRestWallet(LightningBackend):
|
|||||||
else line["error"]
|
else line["error"]
|
||||||
)
|
)
|
||||||
logger.error(f"LND get_payment_status error: {message}")
|
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 = line.get("result")
|
||||||
|
|
||||||
# payment exists
|
# payment exists
|
||||||
if payment is not None and payment.get("status"):
|
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(
|
return PaymentStatus(
|
||||||
paid=statuses[payment["status"]],
|
result=PAYMENT_RESULT_MAP[payment["status"]],
|
||||||
fee=(
|
fee=(
|
||||||
Amount(unit=Unit.msat, amount=payment.get("fee_msat"))
|
Amount(unit=Unit.msat, amount=payment.get("fee_msat"))
|
||||||
if payment.get("fee_msat")
|
if payment.get("fee_msat")
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
preimage=payment.get("payment_preimage"),
|
preimage=preimage,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return PaymentStatus(paid=None)
|
return PaymentStatus(
|
||||||
|
result=PaymentResult.UNKNOWN,
|
||||||
|
error_message="no payment status",
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return PaymentStatus(paid=None)
|
return PaymentStatus(result=PaymentResult.UNKNOWN, error_message="timeout")
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# type: ignore
|
|
||||||
import secrets
|
import secrets
|
||||||
from typing import AsyncGenerator, Optional
|
from typing import AsyncGenerator, Dict, Optional, Union
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from ..core.base import Amount, MeltQuote, Unit
|
from ..core.base import Amount, MeltQuote, Unit
|
||||||
from ..core.models import PostMeltQuoteRequest
|
from ..core.models import PostMeltQuoteRequest
|
||||||
@@ -12,6 +12,7 @@ from .base import (
|
|||||||
LightningBackend,
|
LightningBackend,
|
||||||
PaymentQuoteResponse,
|
PaymentQuoteResponse,
|
||||||
PaymentResponse,
|
PaymentResponse,
|
||||||
|
PaymentResult,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
StatusResponse,
|
StatusResponse,
|
||||||
)
|
)
|
||||||
@@ -19,13 +20,92 @@ from .base import (
|
|||||||
USDT = "USDT"
|
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):
|
class StrikeWallet(LightningBackend):
|
||||||
"""https://docs.strike.me/api/"""
|
"""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
|
supports_description: bool = False
|
||||||
currency_map = {Unit.sat: "BTC", Unit.usd: "USD", Unit.eur: "EUR"}
|
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):
|
def __init__(self, unit: Unit, **kwargs):
|
||||||
self.assert_unit_supported(unit)
|
self.assert_unit_supported(unit)
|
||||||
self.unit = unit
|
self.unit = unit
|
||||||
@@ -98,45 +178,29 @@ class StrikeWallet(LightningBackend):
|
|||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"correlationId": secrets.token_hex(16),
|
"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},
|
"amount": {"amount": amount.to_float_string(), "currency": self.currency},
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
r = await self.client.post(url=f"{self.endpoint}/v1/invoices", json=payload)
|
r = await self.client.post(url=f"{self.endpoint}/v1/invoices", json=payload)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
except Exception:
|
except Exception:
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(ok=False, error_message=r.json()["detail"])
|
||||||
paid=False,
|
|
||||||
checking_id=None,
|
|
||||||
payment_request=None,
|
|
||||||
error_message=r.json()["detail"],
|
|
||||||
)
|
|
||||||
|
|
||||||
quote = r.json()
|
invoice = StrikeCreateInvoiceResponse.parse_obj(r.json())
|
||||||
invoice_id = quote.get("invoiceId")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = {"descriptionHash": secrets.token_hex(32)}
|
payload = {"descriptionHash": secrets.token_hex(32)}
|
||||||
r2 = await self.client.post(
|
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:
|
except Exception:
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(ok=False, error_message=r.json()["detail"])
|
||||||
paid=False,
|
|
||||||
checking_id=None,
|
|
||||||
payment_request=None,
|
|
||||||
error_message=r.json()["detail"],
|
|
||||||
)
|
|
||||||
|
|
||||||
data2 = r2.json()
|
quote = InvoiceQuoteResponse.parse_obj(r2.json())
|
||||||
payment_request = data2.get("lnInvoice")
|
|
||||||
assert payment_request, "Did not receive an invoice"
|
|
||||||
checking_id = invoice_id
|
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
ok=True,
|
ok=True, checking_id=invoice.invoiceId, payment_request=quote.lnInvoice
|
||||||
checking_id=checking_id,
|
|
||||||
payment_request=payment_request,
|
|
||||||
error_message=None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_payment_quote(
|
async def get_payment_quote(
|
||||||
@@ -153,13 +217,18 @@ class StrikeWallet(LightningBackend):
|
|||||||
except Exception:
|
except Exception:
|
||||||
error_message = r.json()["data"]["message"]
|
error_message = r.json()["data"]["message"]
|
||||||
raise Exception(error_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(
|
quote = PaymentQuoteResponse(
|
||||||
amount=amount,
|
amount=amount,
|
||||||
checking_id=data.get("paymentQuoteId"),
|
checking_id=strike_quote.paymentQuoteId,
|
||||||
fee=Amount(self.unit, 0),
|
fee=Amount(self.unit, fee),
|
||||||
)
|
)
|
||||||
return quote
|
return quote
|
||||||
|
|
||||||
@@ -176,49 +245,42 @@ class StrikeWallet(LightningBackend):
|
|||||||
except Exception:
|
except Exception:
|
||||||
error_message = r.json()["data"]["message"]
|
error_message = r.json()["data"]["message"]
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=None,
|
result=PaymentResult.FAILED, error_message=error_message
|
||||||
checking_id=None,
|
|
||||||
fee=None,
|
|
||||||
preimage=None,
|
|
||||||
error_message=error_message,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data = r.json()
|
payment = StrikePaymentResponse.parse_obj(r.json())
|
||||||
states = {"PENDING": None, "COMPLETED": True, "FAILED": False}
|
fee = self.fee_int(payment)
|
||||||
if states[data.get("state")]:
|
return PaymentResponse(
|
||||||
return PaymentResponse(
|
result=PAYMENT_RESULT_MAP[payment.state],
|
||||||
ok=True, checking_id=None, fee=None, preimage=None, error_message=None
|
checking_id=payment.paymentId,
|
||||||
)
|
fee=Amount(self.unit, fee),
|
||||||
else:
|
)
|
||||||
return PaymentResponse(
|
|
||||||
ok=False, checking_id=None, fee=None, preimage=None, error_message=None
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
try:
|
try:
|
||||||
r = await self.client.get(url=f"{self.endpoint}/v1/invoices/{checking_id}")
|
r = await self.client.get(url=f"{self.endpoint}/v1/invoices/{checking_id}")
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
return PaymentStatus(paid=None)
|
return PaymentStatus(result=PaymentResult.UNKNOWN, error_message=str(e))
|
||||||
data = r.json()
|
data = r.json()
|
||||||
states = {"PENDING": None, "UNPAID": None, "PAID": True, "CANCELLED": False}
|
return PaymentStatus(result=INVOICE_RESULT_MAP[data.get("state")])
|
||||||
return PaymentStatus(paid=states[data["state"]])
|
|
||||||
|
|
||||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
try:
|
try:
|
||||||
r = await self.client.get(url=f"{self.endpoint}/v1/payments/{checking_id}")
|
r = await self.client.get(url=f"{self.endpoint}/v1/payments/{checking_id}")
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
except Exception:
|
payment = StrikePaymentResponse.parse_obj(r.json())
|
||||||
return PaymentStatus(paid=None)
|
fee = self.fee_int(payment)
|
||||||
data = r.json()
|
return PaymentStatus(
|
||||||
if "paid" not in data and "details" not in data:
|
result=PAYMENT_RESULT_MAP[payment.state],
|
||||||
return PaymentStatus(paid=None)
|
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(
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: # type: ignore
|
||||||
paid=data["paid"],
|
|
||||||
fee_msat=data["details"]["fee"],
|
|
||||||
preimage=data["preimage"],
|
|
||||||
)
|
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
|
||||||
raise NotImplementedError("paid_invoices_stream not implemented")
|
raise NotImplementedError("paid_invoices_stream not implemented")
|
||||||
|
|||||||
@@ -201,16 +201,6 @@ class LedgerCrud(ABC):
|
|||||||
) -> None:
|
) -> None:
|
||||||
...
|
...
|
||||||
|
|
||||||
# @abstractmethod
|
|
||||||
# async def update_mint_quote_paid(
|
|
||||||
# self,
|
|
||||||
# *,
|
|
||||||
# quote_id: str,
|
|
||||||
# paid: bool,
|
|
||||||
# db: Database,
|
|
||||||
# conn: Optional[Connection] = None,
|
|
||||||
# ) -> None: ...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def store_melt_quote(
|
async def store_melt_quote(
|
||||||
self,
|
self,
|
||||||
@@ -233,6 +223,16 @@ class LedgerCrud(ABC):
|
|||||||
) -> Optional[MeltQuote]:
|
) -> Optional[MeltQuote]:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_melt_quote_by_request(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
request: str,
|
||||||
|
db: Database,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
) -> Optional[MeltQuote]:
|
||||||
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def update_melt_quote(
|
async def update_melt_quote(
|
||||||
self,
|
self,
|
||||||
@@ -433,8 +433,8 @@ class LedgerCrudSqlite(LedgerCrud):
|
|||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
f"""
|
f"""
|
||||||
INSERT INTO {db.table_with_schema('mint_quotes')}
|
INSERT INTO {db.table_with_schema('mint_quotes')}
|
||||||
(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, :paid, :state, :created_time, :paid_time)
|
VALUES (:quote, :method, :request, :checking_id, :unit, :amount, :issued, :state, :created_time, :paid_time)
|
||||||
""",
|
""",
|
||||||
{
|
{
|
||||||
"quote": quote.quote,
|
"quote": quote.quote,
|
||||||
@@ -443,8 +443,7 @@ class LedgerCrudSqlite(LedgerCrud):
|
|||||||
"checking_id": quote.checking_id,
|
"checking_id": quote.checking_id,
|
||||||
"unit": quote.unit,
|
"unit": quote.unit,
|
||||||
"amount": quote.amount,
|
"amount": quote.amount,
|
||||||
"issued": quote.issued,
|
"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)
|
||||||
"paid": quote.paid,
|
|
||||||
"state": quote.state.name,
|
"state": quote.state.name,
|
||||||
"created_time": db.to_timestamp(
|
"created_time": db.to_timestamp(
|
||||||
db.timestamp_from_seconds(quote.created_time) or ""
|
db.timestamp_from_seconds(quote.created_time) or ""
|
||||||
@@ -513,10 +512,8 @@ class LedgerCrudSqlite(LedgerCrud):
|
|||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
await (conn or db).execute(
|
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,
|
"state": quote.state.name,
|
||||||
"paid_time": db.to_timestamp(
|
"paid_time": db.to_timestamp(
|
||||||
db.timestamp_from_seconds(quote.paid_time) or ""
|
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(
|
async def store_melt_quote(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -552,8 +532,8 @@ class LedgerCrudSqlite(LedgerCrud):
|
|||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
f"""
|
f"""
|
||||||
INSERT INTO {db.table_with_schema('melt_quotes')}
|
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)
|
(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, :paid, :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,
|
"quote": quote.quote,
|
||||||
@@ -563,7 +543,6 @@ class LedgerCrudSqlite(LedgerCrud):
|
|||||||
"unit": quote.unit,
|
"unit": quote.unit,
|
||||||
"amount": quote.amount,
|
"amount": quote.amount,
|
||||||
"fee_reserve": quote.fee_reserve or 0,
|
"fee_reserve": quote.fee_reserve or 0,
|
||||||
"paid": quote.paid,
|
|
||||||
"state": quote.state.name,
|
"state": quote.state.name,
|
||||||
"created_time": db.to_timestamp(
|
"created_time": db.to_timestamp(
|
||||||
db.timestamp_from_seconds(quote.created_time) or ""
|
db.timestamp_from_seconds(quote.created_time) or ""
|
||||||
@@ -610,8 +589,22 @@ class LedgerCrudSqlite(LedgerCrud):
|
|||||||
""",
|
""",
|
||||||
values,
|
values,
|
||||||
)
|
)
|
||||||
if row is None:
|
return MeltQuote.from_row(row) if row else None
|
||||||
return 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
|
return MeltQuote.from_row(row) if row else None
|
||||||
|
|
||||||
async def update_melt_quote(
|
async def update_melt_quote(
|
||||||
@@ -623,10 +616,9 @@ class LedgerCrudSqlite(LedgerCrud):
|
|||||||
) -> None:
|
) -> None:
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
f"""
|
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,
|
"state": quote.state.name,
|
||||||
"fee_paid": quote.fee_paid,
|
"fee_paid": quote.fee_paid,
|
||||||
"paid_time": db.to_timestamp(
|
"paid_time": db.to_timestamp(
|
||||||
@@ -637,6 +629,7 @@ class LedgerCrudSqlite(LedgerCrud):
|
|||||||
if quote.change
|
if quote.change
|
||||||
else None,
|
else None,
|
||||||
"quote": quote.quote,
|
"quote": quote.quote,
|
||||||
|
"checking_id": quote.checking_id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -678,7 +671,7 @@ class LedgerCrudSqlite(LedgerCrud):
|
|||||||
db: Database,
|
db: Database,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
row = await (conn or db).fetchone(
|
row: List = await (conn or db).fetchone(
|
||||||
f"""
|
f"""
|
||||||
SELECT * from {db.table_with_schema('balance')}
|
SELECT * from {db.table_with_schema('balance')}
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -129,9 +129,9 @@ class DbWriteHelper:
|
|||||||
)
|
)
|
||||||
if not quote:
|
if not quote:
|
||||||
raise TransactionError("Mint quote not found.")
|
raise TransactionError("Mint quote not found.")
|
||||||
if quote.state == MintQuoteState.pending:
|
if quote.pending:
|
||||||
raise TransactionError("Mint quote already 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.")
|
raise TransactionError("Mint quote is not paid yet.")
|
||||||
# set the quote as pending
|
# set the quote as pending
|
||||||
quote.state = MintQuoteState.pending
|
quote.state = MintQuoteState.pending
|
||||||
@@ -181,15 +181,15 @@ class DbWriteHelper:
|
|||||||
quote_copy = quote.copy()
|
quote_copy = quote.copy()
|
||||||
async with self.db.get_connection(
|
async with self.db.get_connection(
|
||||||
lock_table="melt_quotes",
|
lock_table="melt_quotes",
|
||||||
lock_select_statement=f"checking_id='{quote.checking_id}'",
|
lock_select_statement=f"quote='{quote.quote}'",
|
||||||
) as conn:
|
) as conn:
|
||||||
# get melt quote from db and check if it is already pending
|
# get melt quote from db and check if it is already pending
|
||||||
quote_db = await self.crud.get_melt_quote(
|
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:
|
if not quote_db:
|
||||||
raise TransactionError("Melt quote not found.")
|
raise TransactionError("Melt quote not found.")
|
||||||
if quote_db.state == MeltQuoteState.pending:
|
if quote_db.pending:
|
||||||
raise TransactionError("Melt quote already pending.")
|
raise TransactionError("Melt quote already pending.")
|
||||||
# set the quote as pending
|
# set the quote as pending
|
||||||
quote_copy.state = MeltQuoteState.pending
|
quote_copy.state = MeltQuoteState.pending
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ from ..lightning.base import (
|
|||||||
InvoiceResponse,
|
InvoiceResponse,
|
||||||
LightningBackend,
|
LightningBackend,
|
||||||
PaymentQuoteResponse,
|
PaymentQuoteResponse,
|
||||||
|
PaymentResponse,
|
||||||
|
PaymentResult,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
)
|
)
|
||||||
from ..mint.crud import LedgerCrudSqlite
|
from ..mint.crud import LedgerCrudSqlite
|
||||||
@@ -123,12 +125,13 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
|||||||
)
|
)
|
||||||
status = await self.backends[method][unit].status()
|
status = await self.backends[method][unit].status()
|
||||||
if status.error_message:
|
if status.error_message:
|
||||||
logger.warning(
|
logger.error(
|
||||||
"The backend for"
|
"The backend for"
|
||||||
f" {self.backends[method][unit].__class__.__name__} isn't"
|
f" {self.backends[method][unit].__class__.__name__} isn't"
|
||||||
f" working properly: '{status.error_message}'",
|
f" working properly: '{status.error_message}'",
|
||||||
RuntimeWarning,
|
RuntimeWarning,
|
||||||
)
|
)
|
||||||
|
exit(1)
|
||||||
logger.info(f"Backend balance: {status.balance} {unit.name}")
|
logger.info(f"Backend balance: {status.balance} {unit.name}")
|
||||||
|
|
||||||
logger.info(f"Data dir: {settings.cashu_dir}")
|
logger.info(f"Data dir: {settings.cashu_dir}")
|
||||||
@@ -148,40 +151,10 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
|||||||
)
|
)
|
||||||
if not melt_quotes:
|
if not melt_quotes:
|
||||||
return
|
return
|
||||||
|
logger.info("Checking pending melt quotes")
|
||||||
for quote in melt_quotes:
|
for quote in melt_quotes:
|
||||||
# get pending proofs for quote
|
quote = await self.get_melt_quote(quote_id=quote.quote, purge_unknown=True)
|
||||||
pending_proofs = await self.crud.get_pending_proofs_for_quote(
|
logger.info(f"Melt quote {quote.quote} state: {quote.state}")
|
||||||
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
|
|
||||||
|
|
||||||
# ------- KEYS -------
|
# ------- KEYS -------
|
||||||
|
|
||||||
@@ -447,8 +420,6 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
|||||||
checking_id=invoice_response.checking_id,
|
checking_id=invoice_response.checking_id,
|
||||||
unit=quote_request.unit,
|
unit=quote_request.unit,
|
||||||
amount=quote_request.amount,
|
amount=quote_request.amount,
|
||||||
issued=False,
|
|
||||||
paid=False,
|
|
||||||
state=MintQuoteState.unpaid,
|
state=MintQuoteState.unpaid,
|
||||||
created_time=int(time.time()),
|
created_time=int(time.time()),
|
||||||
expiry=expiry,
|
expiry=expiry,
|
||||||
@@ -476,14 +447,14 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
|||||||
|
|
||||||
unit, method = self._verify_and_get_unit_method(quote.unit, quote.method)
|
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:
|
if not quote.checking_id:
|
||||||
raise CashuError("quote has no checking id")
|
raise CashuError("quote has no checking id")
|
||||||
logger.trace(f"Lightning: checking invoice {quote.checking_id}")
|
logger.trace(f"Lightning: checking invoice {quote.checking_id}")
|
||||||
status: PaymentStatus = await self.backends[method][
|
status: PaymentStatus = await self.backends[method][
|
||||||
unit
|
unit
|
||||||
].get_invoice_status(quote.checking_id)
|
].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
|
# change state to paid in one transaction, it could have been marked paid
|
||||||
# by the invoice listener in the mean time
|
# by the invoice listener in the mean time
|
||||||
async with self.db.get_connection(
|
async with self.db.get_connection(
|
||||||
@@ -495,9 +466,8 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
|||||||
)
|
)
|
||||||
if not quote:
|
if not quote:
|
||||||
raise Exception("quote not found")
|
raise Exception("quote not found")
|
||||||
if quote.state == MintQuoteState.unpaid:
|
if quote.unpaid:
|
||||||
logger.trace(f"Setting quote {quote_id} as paid")
|
logger.trace(f"Setting quote {quote_id} as paid")
|
||||||
quote.paid = True
|
|
||||||
quote.state = MintQuoteState.paid
|
quote.state = MintQuoteState.paid
|
||||||
quote.paid_time = int(time.time())
|
quote.paid_time = int(time.time())
|
||||||
await self.crud.update_mint_quote(
|
await self.crud.update_mint_quote(
|
||||||
@@ -537,11 +507,11 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
|||||||
output_unit = self.keysets[outputs[0].id].unit
|
output_unit = self.keysets[outputs[0].id].unit
|
||||||
|
|
||||||
quote = await self.get_mint_quote(quote_id)
|
quote = await self.get_mint_quote(quote_id)
|
||||||
if quote.state == MintQuoteState.pending:
|
if quote.pending:
|
||||||
raise TransactionError("Mint quote already pending.")
|
raise TransactionError("Mint quote already pending.")
|
||||||
if quote.state == MintQuoteState.issued:
|
if quote.issued:
|
||||||
raise TransactionError("Mint quote already issued.")
|
raise TransactionError("Mint quote already issued.")
|
||||||
if not quote.state == MintQuoteState.paid:
|
if not quote.paid:
|
||||||
raise QuoteNotPaidError()
|
raise QuoteNotPaidError()
|
||||||
previous_state = quote.state
|
previous_state = quote.state
|
||||||
await self.db_write._set_mint_quote_pending(quote_id=quote_id)
|
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
|
quote_id=quote_id, state=previous_state
|
||||||
)
|
)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
await self.db_write._unset_mint_quote_pending(
|
await self.db_write._unset_mint_quote_pending(
|
||||||
quote_id=quote_id, state=MintQuoteState.issued
|
quote_id=quote_id, state=MintQuoteState.issued
|
||||||
)
|
)
|
||||||
@@ -585,10 +554,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
|||||||
raise TransactionError("mint quote already paid")
|
raise TransactionError("mint quote already paid")
|
||||||
if mint_quote.issued:
|
if mint_quote.issued:
|
||||||
raise TransactionError("mint quote already issued")
|
raise TransactionError("mint quote already issued")
|
||||||
|
if not mint_quote.unpaid:
|
||||||
if mint_quote.state == MintQuoteState.issued:
|
|
||||||
raise TransactionError("mint quote already issued")
|
|
||||||
if mint_quote.state != MintQuoteState.unpaid:
|
|
||||||
raise TransactionError("mint quote is not unpaid")
|
raise TransactionError("mint quote is not unpaid")
|
||||||
|
|
||||||
if not mint_quote.checking_id:
|
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)
|
mint_quote = await self.crud.get_mint_quote(request=request, db=self.db)
|
||||||
if mint_quote:
|
if mint_quote:
|
||||||
payment_quote = self.create_internal_melt_quote(mint_quote, melt_quote)
|
payment_quote = self.create_internal_melt_quote(mint_quote, melt_quote)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# not internal
|
# not internal
|
||||||
# verify that the backend supports mpp if the quote request has an amount
|
# 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,
|
checking_id=payment_quote.checking_id,
|
||||||
unit=unit.name,
|
unit=unit.name,
|
||||||
amount=payment_quote.amount.to(unit).amount,
|
amount=payment_quote.amount.to(unit).amount,
|
||||||
paid=False,
|
|
||||||
state=MeltQuoteState.unpaid,
|
state=MeltQuoteState.unpaid,
|
||||||
fee_reserve=payment_quote.fee.to(unit).amount,
|
fee_reserve=payment_quote.fee.to(unit).amount,
|
||||||
created_time=int(time.time()),
|
created_time=int(time.time()),
|
||||||
@@ -712,20 +676,22 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
|||||||
quote=quote.quote,
|
quote=quote.quote,
|
||||||
amount=quote.amount,
|
amount=quote.amount,
|
||||||
fee_reserve=quote.fee_reserve,
|
fee_reserve=quote.fee_reserve,
|
||||||
paid=quote.paid,
|
paid=quote.paid, # deprecated
|
||||||
state=quote.state.value,
|
state=quote.state.value,
|
||||||
expiry=quote.expiry,
|
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.
|
"""Returns a melt quote.
|
||||||
|
|
||||||
If melt quote is not paid yet and no internal mint quote is associated with it,
|
If the melt quote is pending, checks status of the payment with the backend.
|
||||||
checks with the backend for the state of the payment request. If the backend
|
- If settled, sets the quote as paid and invalidates pending proofs (commit).
|
||||||
says that the quote has been paid, updates the melt quote in the database.
|
- 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:
|
Args:
|
||||||
quote_id (str): ID of the melt quote.
|
quote_id (str): ID of the melt quote.
|
||||||
|
purge_unknown (bool, optional): Rollback unknown payment states to unpaid. Defaults to False.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
Exception: Quote not found.
|
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
|
# we only check the state with the backend if there is no associated internal
|
||||||
# mint quote for this melt quote
|
# 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
|
request=melt_quote.request, db=self.db
|
||||||
)
|
)
|
||||||
|
|
||||||
if not melt_quote.paid and not mint_quote:
|
if melt_quote.pending and not is_internal:
|
||||||
logger.trace(
|
logger.debug(
|
||||||
"Lightning: checking outgoing Lightning payment"
|
"Lightning: checking outgoing Lightning payment"
|
||||||
f" {melt_quote.checking_id}"
|
f" {melt_quote.checking_id}"
|
||||||
)
|
)
|
||||||
status: PaymentStatus = await self.backends[method][
|
status: PaymentStatus = await self.backends[method][
|
||||||
unit
|
unit
|
||||||
].get_payment_status(melt_quote.checking_id)
|
].get_payment_status(melt_quote.checking_id)
|
||||||
if status.paid:
|
logger.debug(f"State: {status.result}")
|
||||||
logger.trace(f"Setting quote {quote_id} as paid")
|
if status.settled:
|
||||||
melt_quote.paid = True
|
logger.debug(f"Setting quote {quote_id} as paid")
|
||||||
melt_quote.state = MeltQuoteState.paid
|
melt_quote.state = MeltQuoteState.paid
|
||||||
if status.fee:
|
if status.fee:
|
||||||
melt_quote.fee_paid = status.fee.to(unit).amount
|
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())
|
melt_quote.paid_time = int(time.time())
|
||||||
await self.crud.update_melt_quote(quote=melt_quote, db=self.db)
|
await self.crud.update_melt_quote(quote=melt_quote, db=self.db)
|
||||||
await self.events.submit(melt_quote)
|
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
|
return melt_quote
|
||||||
|
|
||||||
@@ -798,8 +778,6 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
|
|||||||
# we settle the transaction internally
|
# we settle the transaction internally
|
||||||
if melt_quote.paid:
|
if melt_quote.paid:
|
||||||
raise TransactionError("melt quote already paid")
|
raise TransactionError("melt quote already paid")
|
||||||
if melt_quote.state != MeltQuoteState.unpaid:
|
|
||||||
raise TransactionError("melt quote already paid")
|
|
||||||
|
|
||||||
# verify amounts from bolt11 invoice
|
# verify amounts from bolt11 invoice
|
||||||
bolt11_request = melt_quote.request
|
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.fee_paid = 0 # no internal fees
|
||||||
melt_quote.paid = True
|
|
||||||
melt_quote.state = MeltQuoteState.paid
|
melt_quote.state = MeltQuoteState.paid
|
||||||
melt_quote.paid_time = int(time.time())
|
melt_quote.paid_time = int(time.time())
|
||||||
|
|
||||||
mint_quote.paid = True
|
|
||||||
mint_quote.state = MintQuoteState.paid
|
mint_quote.state = MintQuoteState.paid
|
||||||
mint_quote.paid_time = melt_quote.paid_time
|
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
|
# get melt quote and check if it was already paid
|
||||||
melt_quote = await self.get_melt_quote(quote_id=quote)
|
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(
|
unit, method = self._verify_and_get_unit_method(
|
||||||
melt_quote.unit, melt_quote.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
|
# make sure that the outputs (for fee return) are in the same unit as the quote
|
||||||
if outputs:
|
if outputs:
|
||||||
# _verify_outputs checks if all outputs have the same unit
|
# _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(
|
await self.db_write._verify_spent_proofs_and_set_pending(
|
||||||
proofs, quote_id=melt_quote.quote
|
proofs, quote_id=melt_quote.quote
|
||||||
)
|
)
|
||||||
|
previous_state = melt_quote.state
|
||||||
|
melt_quote = await self.db_write._set_melt_quote_pending(melt_quote)
|
||||||
try:
|
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)
|
melt_quote = await self.melt_mint_settle_internally(melt_quote, proofs)
|
||||||
# quote not paid yet (not internal), pay it with the backend
|
# 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}")
|
logger.debug(f"Lightning: pay invoice {melt_quote.request}")
|
||||||
payment = await self.backends[method][unit].pay_invoice(
|
try:
|
||||||
melt_quote, melt_quote.fee_reserve * 1000
|
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}"
|
|
||||||
)
|
)
|
||||||
if payment.fee:
|
logger.debug(
|
||||||
melt_quote.fee_paid = payment.fee.to(
|
f"Melt – Result: {str(payment.result)}: preimage: {payment.preimage},"
|
||||||
to_unit=unit, round="up"
|
f" fee: {payment.fee.str() if payment.fee is not None else 'None'}"
|
||||||
).amount
|
)
|
||||||
if payment.preimage:
|
if (
|
||||||
melt_quote.payment_preimage = payment.preimage
|
payment.checking_id
|
||||||
# set quote as paid
|
and payment.checking_id != melt_quote.checking_id
|
||||||
melt_quote.paid = True
|
):
|
||||||
melt_quote.state = MeltQuoteState.paid
|
logger.warning(
|
||||||
melt_quote.paid_time = int(time.time())
|
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
|
# melt successful, invalidate proofs
|
||||||
await self._invalidate_proofs(proofs=proofs, quote_id=melt_quote.quote)
|
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
|
# prepare change to compensate wallet for overpaid fees
|
||||||
return_promises: List[BlindedSignature] = []
|
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.crud.update_melt_quote(quote=melt_quote, db=self.db)
|
||||||
await self.events.submit(melt_quote)
|
await self.events.submit(melt_quote)
|
||||||
|
|
||||||
except Exception as e:
|
return PostMeltQuoteResponse.from_melt_quote(melt_quote)
|
||||||
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)
|
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(
|
async def swap(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import copy
|
import copy
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
from ..core.base import MintKeyset, Proof
|
from ..core.base import MintKeyset, Proof
|
||||||
from ..core.crypto.keys import derive_keyset_id, derive_keyset_id_deprecated
|
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,
|
checking_id TEXT NOT NULL,
|
||||||
unit TEXT NOT NULL,
|
unit TEXT NOT NULL,
|
||||||
amount {db.big_int} NOT NULL,
|
amount {db.big_int} NOT NULL,
|
||||||
paid BOOL NOT NULL,
|
|
||||||
issued BOOL NOT NULL,
|
issued BOOL NOT NULL,
|
||||||
created_time TIMESTAMP,
|
created_time TIMESTAMP,
|
||||||
paid_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(
|
await conn.execute(
|
||||||
@@ -308,7 +309,6 @@ async def m011_add_quote_tables(db: Database):
|
|||||||
unit TEXT NOT NULL,
|
unit TEXT NOT NULL,
|
||||||
amount {db.big_int} NOT NULL,
|
amount {db.big_int} NOT NULL,
|
||||||
fee_reserve {db.big_int},
|
fee_reserve {db.big_int},
|
||||||
paid BOOL NOT NULL,
|
|
||||||
created_time TIMESTAMP,
|
created_time TIMESTAMP,
|
||||||
paid_time TIMESTAMP,
|
paid_time TIMESTAMP,
|
||||||
fee_paid {db.big_int},
|
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(
|
await conn.execute(
|
||||||
f"INSERT INTO {db.table_with_schema('mint_quotes')} (quote, method,"
|
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'),"
|
" 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')} "
|
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
|
# and the `paid` and `issued` column respectively
|
||||||
# mint quotes:
|
# mint quotes:
|
||||||
async with db.connect() as conn:
|
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')}"
|
f"SELECT * FROM {db.table_with_schema('mint_quotes')}"
|
||||||
)
|
)
|
||||||
for row in rows:
|
for row in rows:
|
||||||
if row["issued"]:
|
if row.get("issued"):
|
||||||
state = "issued"
|
state = "issued"
|
||||||
elif row["paid"]:
|
elif row.get("paid"):
|
||||||
state = "paid"
|
state = "paid"
|
||||||
else:
|
else:
|
||||||
state = "unpaid"
|
state = "unpaid"
|
||||||
@@ -804,10 +805,10 @@ async def m020_add_state_to_mint_and_melt_quotes(db: Database):
|
|||||||
|
|
||||||
# melt quotes:
|
# melt quotes:
|
||||||
async with db.connect() as conn:
|
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')}"
|
f"SELECT * FROM {db.table_with_schema('melt_quotes')}"
|
||||||
)
|
)
|
||||||
for row in rows:
|
for row in rows2:
|
||||||
if row["paid"]:
|
if row["paid"]:
|
||||||
state = "paid"
|
state = "paid"
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ async def mint_quote(
|
|||||||
resp = PostMintQuoteResponse(
|
resp = PostMintQuoteResponse(
|
||||||
request=quote.request,
|
request=quote.request,
|
||||||
quote=quote.quote,
|
quote=quote.quote,
|
||||||
paid=quote.paid,
|
paid=quote.paid, # deprecated
|
||||||
state=quote.state.value,
|
state=quote.state.value,
|
||||||
expiry=quote.expiry,
|
expiry=quote.expiry,
|
||||||
)
|
)
|
||||||
@@ -192,7 +192,7 @@ async def get_mint_quote(request: Request, quote: str) -> PostMintQuoteResponse:
|
|||||||
resp = PostMintQuoteResponse(
|
resp = PostMintQuoteResponse(
|
||||||
quote=mint_quote.quote,
|
quote=mint_quote.quote,
|
||||||
request=mint_quote.request,
|
request=mint_quote.request,
|
||||||
paid=mint_quote.paid,
|
paid=mint_quote.paid, # deprecated
|
||||||
state=mint_quote.state.value,
|
state=mint_quote.state.value,
|
||||||
expiry=mint_quote.expiry,
|
expiry=mint_quote.expiry,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from typing import Dict, List, Optional
|
|||||||
from fastapi import APIRouter, Request
|
from fastapi import APIRouter, Request
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from ..core.base import BlindedMessage, BlindedSignature, ProofSpentState
|
from ..core.base import BlindedMessage, BlindedSignature
|
||||||
from ..core.errors import CashuError
|
from ..core.errors import CashuError
|
||||||
from ..core.models import (
|
from ..core.models import (
|
||||||
CheckFeesRequest_deprecated,
|
CheckFeesRequest_deprecated,
|
||||||
@@ -345,13 +345,13 @@ async def check_spendable_deprecated(
|
|||||||
spendableList: List[bool] = []
|
spendableList: List[bool] = []
|
||||||
pendingList: List[bool] = []
|
pendingList: List[bool] = []
|
||||||
for proof_state in proofs_state:
|
for proof_state in proofs_state:
|
||||||
if proof_state.state == ProofSpentState.unspent:
|
if proof_state.unspent:
|
||||||
spendableList.append(True)
|
spendableList.append(True)
|
||||||
pendingList.append(False)
|
pendingList.append(False)
|
||||||
elif proof_state.state == ProofSpentState.spent:
|
elif proof_state.spent:
|
||||||
spendableList.append(False)
|
spendableList.append(False)
|
||||||
pendingList.append(False)
|
pendingList.append(False)
|
||||||
elif proof_state.state == ProofSpentState.pending:
|
elif proof_state.pending:
|
||||||
spendableList.append(True)
|
spendableList.append(True)
|
||||||
pendingList.append(True)
|
pendingList.append(True)
|
||||||
return CheckSpendableResponse_deprecated(
|
return CheckSpendableResponse_deprecated(
|
||||||
|
|||||||
@@ -56,8 +56,7 @@ class LedgerTasks(SupportsDb, SupportsBackends, SupportsEvents):
|
|||||||
f"Invoice callback dispatcher: quote {quote} trying to set as {MintQuoteState.paid}"
|
f"Invoice callback dispatcher: quote {quote} trying to set as {MintQuoteState.paid}"
|
||||||
)
|
)
|
||||||
# set the quote as paid
|
# set the quote as paid
|
||||||
if quote.state == MintQuoteState.unpaid:
|
if quote.unpaid:
|
||||||
quote.paid = True
|
|
||||||
quote.state = MintQuoteState.paid
|
quote.state = MintQuoteState.paid
|
||||||
await self.crud.update_mint_quote(quote=quote, db=self.db, conn=conn)
|
await self.crud.update_mint_quote(quote=quote, db=self.db, conn=conn)
|
||||||
logger.trace(
|
logger.trace(
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ async def pay(
|
|||||||
print(" Error: Balance too low.")
|
print(" Error: Balance too low.")
|
||||||
return
|
return
|
||||||
send_proofs, fees = await wallet.select_to_send(
|
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:
|
try:
|
||||||
melt_response = await wallet.melt(
|
melt_response = await wallet.melt(
|
||||||
@@ -231,11 +231,25 @@ async def pay(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" Error paying invoice: {str(e)}")
|
print(f" Error paying invoice: {str(e)}")
|
||||||
return
|
return
|
||||||
print(" Invoice paid", end="", flush=True)
|
if (
|
||||||
if melt_response.payment_preimage and melt_response.payment_preimage != "0" * 64:
|
melt_response.state
|
||||||
print(f" (Preimage: {melt_response.payment_preimage}).")
|
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:
|
else:
|
||||||
print(".")
|
print(" Error paying invoice.")
|
||||||
|
|
||||||
await print_balance(ctx)
|
await print_balance(ctx)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ from typing import Optional
|
|||||||
|
|
||||||
import bolt11
|
import bolt11
|
||||||
|
|
||||||
from ...core.base import Amount, ProofSpentState, Unit
|
from ...core.base import Amount, Unit
|
||||||
from ...core.helpers import sum_promises
|
from ...core.helpers import sum_promises
|
||||||
from ...core.settings import settings
|
from ...core.settings import settings
|
||||||
from ...lightning.base import (
|
from ...lightning.base import (
|
||||||
InvoiceResponse,
|
InvoiceResponse,
|
||||||
PaymentResponse,
|
PaymentResponse,
|
||||||
|
PaymentResult,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
StatusResponse,
|
StatusResponse,
|
||||||
)
|
)
|
||||||
@@ -58,14 +59,14 @@ class LightningWallet(Wallet):
|
|||||||
pr (str): bolt11 payment request
|
pr (str): bolt11 payment request
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if successful
|
PaymentResponse: containing details of the operation
|
||||||
"""
|
"""
|
||||||
quote = await self.melt_quote(pr)
|
quote = await self.melt_quote(pr)
|
||||||
total_amount = quote.amount + quote.fee_reserve
|
total_amount = quote.amount + quote.fee_reserve
|
||||||
assert total_amount > 0, "amount is not positive"
|
assert total_amount > 0, "amount is not positive"
|
||||||
if self.available_balance < total_amount:
|
if self.available_balance < total_amount:
|
||||||
print("Error: Balance too low.")
|
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)
|
_, send_proofs = await self.swap_to_send(self.proofs, total_amount)
|
||||||
try:
|
try:
|
||||||
resp = await self.melt(send_proofs, pr, quote.fee_reserve, quote.quote)
|
resp = await self.melt(send_proofs, pr, quote.fee_reserve, quote.quote)
|
||||||
@@ -76,14 +77,14 @@ class LightningWallet(Wallet):
|
|||||||
|
|
||||||
invoice_obj = bolt11.decode(pr)
|
invoice_obj = bolt11.decode(pr)
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
ok=True,
|
result=PaymentResult.SETTLED,
|
||||||
checking_id=invoice_obj.payment_hash,
|
checking_id=invoice_obj.payment_hash,
|
||||||
preimage=resp.payment_preimage,
|
preimage=resp.payment_preimage,
|
||||||
fee=Amount(Unit.msat, fees_paid_sat),
|
fee=Amount(Unit.msat, fees_paid_sat),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Exception:", 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:
|
async def get_invoice_status(self, payment_hash: str) -> PaymentStatus:
|
||||||
"""Get lightning invoice status (incoming)
|
"""Get lightning invoice status (incoming)
|
||||||
@@ -98,16 +99,16 @@ class LightningWallet(Wallet):
|
|||||||
db=self.db, payment_hash=payment_hash, out=False
|
db=self.db, payment_hash=payment_hash, out=False
|
||||||
)
|
)
|
||||||
if not invoice:
|
if not invoice:
|
||||||
return PaymentStatus(paid=None)
|
return PaymentStatus(result=PaymentResult.UNKNOWN)
|
||||||
if invoice.paid:
|
if invoice.paid:
|
||||||
return PaymentStatus(paid=True)
|
return PaymentStatus(result=PaymentResult.SETTLED)
|
||||||
try:
|
try:
|
||||||
# to check the invoice state, we try minting tokens
|
# to check the invoice state, we try minting tokens
|
||||||
await self.mint(invoice.amount, id=invoice.id)
|
await self.mint(invoice.amount, id=invoice.id)
|
||||||
return PaymentStatus(paid=True)
|
return PaymentStatus(result=PaymentResult.SETTLED)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
return PaymentStatus(paid=False)
|
return PaymentStatus(result=PaymentResult.FAILED)
|
||||||
|
|
||||||
async def get_payment_status(self, payment_hash: str) -> PaymentStatus:
|
async def get_payment_status(self, payment_hash: str) -> PaymentStatus:
|
||||||
"""Get lightning payment status (outgoing)
|
"""Get lightning payment status (outgoing)
|
||||||
@@ -126,24 +127,30 @@ class LightningWallet(Wallet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not invoice:
|
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:
|
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)
|
proofs = await get_proofs(db=self.db, melt_id=invoice.id)
|
||||||
if not proofs:
|
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)
|
proofs_states = await self.check_proof_state(proofs)
|
||||||
if not proofs_states:
|
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]):
|
if all([p.state.pending for p in proofs_states.states]):
|
||||||
return PaymentStatus(paid=None) # "pending (with check)"
|
return PaymentStatus(result=PaymentResult.PENDING) # "pending (with check)"
|
||||||
if any([p.state == ProofSpentState.spent for p in proofs_states.states]):
|
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
|
# 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)"
|
return PaymentStatus(result=PaymentResult.SETTLED) # "paid (with check)"
|
||||||
if all([p.state == ProofSpentState.unspent for p in proofs_states.states]):
|
if all([p.state.unspent for p in proofs_states.states]):
|
||||||
return PaymentStatus(paid=False) # "failed (with check)"
|
return PaymentStatus(result=PaymentResult.FAILED) # "failed (with check)"
|
||||||
return PaymentStatus(paid=None) # "undefined state"
|
return PaymentStatus(result=PaymentResult.UNKNOWN) # "undefined state"
|
||||||
|
|
||||||
async def get_balance(self) -> StatusResponse:
|
async def get_balance(self) -> StatusResponse:
|
||||||
"""Get lightning balance
|
"""Get lightning balance
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ from ..core.base import (
|
|||||||
BlindedSignature,
|
BlindedSignature,
|
||||||
DLEQWallet,
|
DLEQWallet,
|
||||||
Invoice,
|
Invoice,
|
||||||
|
MeltQuoteState,
|
||||||
Proof,
|
Proof,
|
||||||
ProofSpentState,
|
|
||||||
Unit,
|
Unit,
|
||||||
WalletKeyset,
|
WalletKeyset,
|
||||||
)
|
)
|
||||||
@@ -759,15 +759,18 @@ class Wallet(
|
|||||||
status = await super().melt(quote_id, proofs, change_outputs)
|
status = await super().melt(quote_id, proofs, change_outputs)
|
||||||
|
|
||||||
# if payment fails
|
# if payment fails
|
||||||
if not status.paid:
|
if MeltQuoteState(status.state) == MeltQuoteState.unpaid:
|
||||||
# remove the melt_id in proofs
|
# remove the melt_id in proofs and set reserved to False
|
||||||
for p in proofs:
|
for p in proofs:
|
||||||
p.melt_id = None
|
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.")
|
raise Exception("could not pay invoice.")
|
||||||
|
elif MeltQuoteState(status.state) == MeltQuoteState.pending:
|
||||||
|
# payment is still pending
|
||||||
|
return status
|
||||||
|
|
||||||
# invoice was paid successfully
|
# invoice was paid successfully
|
||||||
|
|
||||||
await self.invalidate(proofs)
|
await self.invalidate(proofs)
|
||||||
|
|
||||||
# update paid status in db
|
# update paid status in db
|
||||||
@@ -995,7 +998,7 @@ class Wallet(
|
|||||||
if check_spendable:
|
if check_spendable:
|
||||||
proof_states = await self.check_proof_state(proofs)
|
proof_states = await self.check_proof_state(proofs)
|
||||||
for i, state in enumerate(proof_states.states):
|
for i, state in enumerate(proof_states.states):
|
||||||
if state.state == ProofSpentState.spent:
|
if state.spent:
|
||||||
invalidated_proofs.append(proofs[i])
|
invalidated_proofs.append(proofs[i])
|
||||||
else:
|
else:
|
||||||
invalidated_proofs = proofs
|
invalidated_proofs = proofs
|
||||||
|
|||||||
2
mypy.ini
2
mypy.ini
@@ -1,5 +1,5 @@
|
|||||||
[mypy]
|
[mypy]
|
||||||
python_version = 3.9
|
python_version = 3.10
|
||||||
# disallow_untyped_defs = True
|
# disallow_untyped_defs = True
|
||||||
; check_untyped_defs = True
|
; check_untyped_defs = True
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ settings.mint_seed_decryption_key = ""
|
|||||||
settings.mint_max_balance = 0
|
settings.mint_max_balance = 0
|
||||||
settings.mint_transaction_rate_limit_per_minute = 60
|
settings.mint_transaction_rate_limit_per_minute = 60
|
||||||
settings.mint_lnd_enable_mpp = True
|
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.mint_input_fee_ppk = 0
|
||||||
settings.db_connection_pool = True
|
settings.db_connection_pool = True
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ docker_lightning_unconnected_cli = [
|
|||||||
"--rpcserver=lnd-2",
|
"--rpcserver=lnd-2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def docker_clightning_cli(index):
|
def docker_clightning_cli(index):
|
||||||
return [
|
return [
|
||||||
"docker",
|
"docker",
|
||||||
@@ -104,9 +105,9 @@ def docker_clightning_cli(index):
|
|||||||
"lightning-cli",
|
"lightning-cli",
|
||||||
"--network",
|
"--network",
|
||||||
"regtest",
|
"regtest",
|
||||||
"--keywords",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def run_cmd(cmd: list) -> str:
|
def run_cmd(cmd: list) -> str:
|
||||||
timeout = 20
|
timeout = 20
|
||||||
process = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
process = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
||||||
@@ -171,11 +172,22 @@ def pay_real_invoice(invoice: str) -> str:
|
|||||||
cmd.extend(["payinvoice", "--force", invoice])
|
cmd.extend(["payinvoice", "--force", invoice])
|
||||||
return run_cmd(cmd)
|
return run_cmd(cmd)
|
||||||
|
|
||||||
|
|
||||||
def partial_pay_real_invoice(invoice: str, amount: int, node: int) -> str:
|
def partial_pay_real_invoice(invoice: str, amount: int, node: int) -> str:
|
||||||
cmd = docker_clightning_cli(node)
|
cmd = docker_clightning_cli(node)
|
||||||
cmd.extend(["pay", f"bolt11={invoice}", f"partial_msat={amount*1000}"])
|
cmd.extend(["pay", f"bolt11={invoice}", f"partial_msat={amount*1000}"])
|
||||||
return run_cmd(cmd)
|
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:
|
def mine_blocks(blocks: int = 1) -> str:
|
||||||
cmd = docker_bitcoin_cli.copy()
|
cmd = docker_bitcoin_cli.copy()
|
||||||
cmd.extend(["-generate", str(blocks)])
|
cmd.extend(["-generate", str(blocks)])
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import asyncio
|
|||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from typing import List
|
from typing import List, Tuple
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
@@ -63,7 +63,8 @@ async def test_db_tables(ledger: Ledger):
|
|||||||
"SELECT table_name FROM information_schema.tables WHERE table_schema ="
|
"SELECT table_name FROM information_schema.tables WHERE table_schema ="
|
||||||
" 'public';"
|
" '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 = [
|
tables_expected = [
|
||||||
"dbversions",
|
"dbversions",
|
||||||
"keysets",
|
"keysets",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import httpx
|
|||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
|
|
||||||
from cashu.core.base import MeltQuoteState, MintQuoteState, ProofSpentState
|
from cashu.core.base import MeltQuoteState, MintQuoteState
|
||||||
from cashu.core.models import (
|
from cashu.core.models import (
|
||||||
GetInfoResponse,
|
GetInfoResponse,
|
||||||
MintMethodSetting,
|
MintMethodSetting,
|
||||||
@@ -478,7 +478,7 @@ async def test_api_check_state(ledger: Ledger):
|
|||||||
response = PostCheckStateResponse.parse_obj(response.json())
|
response = PostCheckStateResponse.parse_obj(response.json())
|
||||||
assert response
|
assert response
|
||||||
assert len(response.states) == 2
|
assert len(response.states) == 2
|
||||||
assert response.states[0].state == ProofSpentState.unspent
|
assert response.states[0].state.unspent
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
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.core.models import PostMeltQuoteRequest
|
||||||
from cashu.mint.ledger import Ledger
|
from cashu.mint.ledger import Ledger
|
||||||
from cashu.wallet.wallet import Wallet
|
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 = wallet1.proofs.copy()
|
||||||
|
|
||||||
proofs_states_before_split = await wallet1.check_proof_state(proofs)
|
proofs_states_before_split = await wallet1.check_proof_state(proofs)
|
||||||
assert all(
|
assert all([s.unspent for s in proofs_states_before_split.states])
|
||||||
[s.state == ProofSpentState.unspent for s in proofs_states_before_split.states]
|
|
||||||
)
|
|
||||||
|
|
||||||
await ledger.db_write._verify_spent_proofs_and_set_pending(proofs)
|
await ledger.db_write._verify_spent_proofs_and_set_pending(proofs)
|
||||||
|
|
||||||
proof_states = await wallet1.check_proof_state(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 assert_err(wallet1.split(wallet1.proofs, 20), "proofs are pending.")
|
||||||
|
|
||||||
await ledger.db_write._unset_proofs_pending(proofs)
|
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)
|
await wallet1.split(proofs, 20)
|
||||||
|
|
||||||
proofs_states_after_split = await wallet1.check_proof_state(proofs)
|
proofs_states_after_split = await wallet1.check_proof_state(proofs)
|
||||||
assert all(
|
assert all([s.spent for s in proofs_states_after_split.states])
|
||||||
[s.state == ProofSpentState.spent for s in proofs_states_after_split.states]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@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)
|
quote = await ledger.crud.get_mint_quote(quote_id=invoice.id, db=ledger.db)
|
||||||
assert quote is not None
|
assert quote is not None
|
||||||
assert quote.quote == invoice.id
|
assert quote.quote == invoice.id
|
||||||
assert quote.state == MintQuoteState.unpaid
|
assert quote.unpaid
|
||||||
|
|
||||||
# set pending again
|
# set pending again
|
||||||
async def set_state(quote, state):
|
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)
|
quote = await ledger.crud.get_melt_quote(quote_id=melt_quote.quote, db=ledger.db)
|
||||||
assert quote is not None
|
assert quote is not None
|
||||||
assert quote.quote == melt_quote.quote
|
assert quote.quote == melt_quote.quote
|
||||||
assert quote.state == MeltQuoteState.unpaid
|
assert quote.unpaid
|
||||||
previous_state = quote.state
|
previous_state = quote.state
|
||||||
await ledger.db_write._set_melt_quote_pending(quote)
|
await ledger.db_write._set_melt_quote_pending(quote)
|
||||||
quote = await ledger.crud.get_melt_quote(quote_id=melt_quote.quote, db=ledger.db)
|
quote = await ledger.crud.get_melt_quote(quote_id=melt_quote.quote, db=ledger.db)
|
||||||
assert quote is not None
|
assert quote is not None
|
||||||
assert quote.state == MeltQuoteState.pending
|
assert quote.pending
|
||||||
|
|
||||||
# set unpending
|
# set unpending
|
||||||
await ledger.db_write._unset_melt_quote_pending(quote, previous_state)
|
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)
|
quote = await ledger.crud.get_melt_quote(quote_id=melt_quote.quote, db=ledger.db)
|
||||||
assert quote is not None
|
assert quote is not None
|
||||||
assert quote.quote == melt_quote.quote
|
assert quote.quote == melt_quote.quote
|
||||||
assert quote.state == MeltQuoteState.unpaid
|
assert quote.unpaid
|
||||||
|
|
||||||
# set pending
|
# set pending
|
||||||
quote.state = MeltQuoteState.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
|
assert invoice is not None
|
||||||
quote = await ledger.crud.get_mint_quote(quote_id=invoice.id, db=ledger.db)
|
quote = await ledger.crud.get_mint_quote(quote_id=invoice.id, db=ledger.db)
|
||||||
assert quote is not None
|
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
|
# pay_if_regtest pays on regtest, get_mint_quote pays on FakeWallet
|
||||||
await pay_if_regtest(invoice.bolt11)
|
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)
|
quote = await ledger.crud.get_mint_quote(quote_id=invoice.id, db=ledger.db)
|
||||||
assert quote is not None
|
assert quote is not None
|
||||||
assert quote.state == MintQuoteState.paid
|
assert quote.paid
|
||||||
|
|
||||||
previous_state = MintQuoteState.paid
|
previous_state = MintQuoteState.paid
|
||||||
await ledger.db_write._set_mint_quote_pending(quote.quote)
|
await ledger.db_write._set_mint_quote_pending(quote.quote)
|
||||||
quote = await ledger.crud.get_mint_quote(quote_id=invoice.id, db=ledger.db)
|
quote = await ledger.crud.get_mint_quote(quote_id=invoice.id, db=ledger.db)
|
||||||
assert quote is not None
|
assert quote is not None
|
||||||
assert quote.state == MintQuoteState.pending
|
assert quote.pending
|
||||||
|
|
||||||
# try to mint while pending
|
# try to mint while pending
|
||||||
await assert_err(wallet1.mint(128, id=invoice.id), "Mint quote already 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)
|
quote = await ledger.crud.get_mint_quote(quote_id=invoice.id, db=ledger.db)
|
||||||
assert quote is not None
|
assert quote is not None
|
||||||
assert quote.state == previous_state
|
assert quote.state == previous_state
|
||||||
assert quote.state == MintQuoteState.paid
|
assert quote.paid
|
||||||
|
|
||||||
# # set paid and mint again
|
# # set paid and mint again
|
||||||
# quote.state = MintQuoteState.paid
|
# quote.state = MintQuoteState.paid
|
||||||
@@ -254,7 +250,7 @@ async def test_mint_quote_set_pending(wallet1: Wallet, ledger: Ledger):
|
|||||||
# check if quote is issued
|
# check if quote is issued
|
||||||
quote = await ledger.crud.get_mint_quote(quote_id=invoice.id, db=ledger.db)
|
quote = await ledger.crud.get_mint_quote(quote_id=invoice.id, db=ledger.db)
|
||||||
assert quote is not None
|
assert quote is not None
|
||||||
assert quote.state == MintQuoteState.issued
|
assert quote.issued
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import bolt11
|
|||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
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.crypto.aes import AESCipher
|
||||||
from cashu.core.db import Database
|
from cashu.core.db import Database
|
||||||
from cashu.core.settings import settings
|
from cashu.core.settings import settings
|
||||||
|
from cashu.lightning.base import PaymentResult
|
||||||
from cashu.mint.crud import LedgerCrudSqlite
|
from cashu.mint.crud import LedgerCrudSqlite
|
||||||
from cashu.mint.ledger import Ledger
|
from cashu.mint.ledger import Ledger
|
||||||
from cashu.wallet.wallet import Wallet
|
from cashu.wallet.wallet import Wallet
|
||||||
@@ -143,8 +144,7 @@ async def create_pending_melts(
|
|||||||
request="asdasd",
|
request="asdasd",
|
||||||
checking_id=check_id,
|
checking_id=check_id,
|
||||||
unit="sat",
|
unit="sat",
|
||||||
paid=False,
|
state=MeltQuoteState.pending,
|
||||||
state=MeltQuoteState.unpaid,
|
|
||||||
amount=100,
|
amount=100,
|
||||||
fee_reserve=1,
|
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."""
|
after the startup routine determines that the associated melt quote was paid."""
|
||||||
pending_proof, quote = await create_pending_melts(ledger)
|
pending_proof, quote = await create_pending_melts(ledger)
|
||||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||||
assert states[0].state == ProofSpentState.pending
|
assert states[0].pending
|
||||||
settings.fakewallet_payment_state = True
|
settings.fakewallet_payment_state = PaymentResult.SETTLED.name
|
||||||
# run startup routinge
|
# run startup routinge
|
||||||
await ledger.startup_ledger()
|
await ledger.startup_ledger()
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ async def test_startup_fakewallet_pending_quote_success(ledger: Ledger):
|
|||||||
|
|
||||||
# expect that proofs are spent
|
# expect that proofs are spent
|
||||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||||
assert states[0].state == ProofSpentState.spent
|
assert states[0].spent
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@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)
|
pending_proof, quote = await create_pending_melts(ledger)
|
||||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||||
assert states[0].state == ProofSpentState.pending
|
assert states[0].pending
|
||||||
settings.fakewallet_payment_state = False
|
settings.fakewallet_payment_state = PaymentResult.FAILED.name
|
||||||
# run startup routinge
|
# run startup routinge
|
||||||
await ledger.startup_ledger()
|
await ledger.startup_ledger()
|
||||||
|
|
||||||
@@ -212,7 +212,7 @@ async def test_startup_fakewallet_pending_quote_failure(ledger: Ledger):
|
|||||||
|
|
||||||
# expect that proofs are unspent
|
# expect that proofs are unspent
|
||||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||||
assert states[0].state == ProofSpentState.unspent
|
assert states[0].unspent
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@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):
|
async def test_startup_fakewallet_pending_quote_pending(ledger: Ledger):
|
||||||
pending_proof, quote = await create_pending_melts(ledger)
|
pending_proof, quote = await create_pending_melts(ledger)
|
||||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
||||||
assert states[0].state == ProofSpentState.pending
|
assert states[0].pending
|
||||||
settings.fakewallet_payment_state = None
|
settings.fakewallet_payment_state = PaymentResult.PENDING.name
|
||||||
# run startup routinge
|
# run startup routinge
|
||||||
await ledger.startup_ledger()
|
await ledger.startup_ledger()
|
||||||
|
|
||||||
@@ -233,7 +233,28 @@ async def test_startup_fakewallet_pending_quote_pending(ledger: Ledger):
|
|||||||
|
|
||||||
# expect that proofs are still pending
|
# expect that proofs are still pending
|
||||||
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
|
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
|
@pytest.mark.asyncio
|
||||||
@@ -262,7 +283,6 @@ async def test_startup_regtest_pending_quote_pending(wallet: Wallet, ledger: Led
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
await asyncio.sleep(SLEEP_TIME)
|
await asyncio.sleep(SLEEP_TIME)
|
||||||
# settle_invoice(preimage=preimage)
|
|
||||||
|
|
||||||
# run startup routinge
|
# run startup routinge
|
||||||
await ledger.startup_ledger()
|
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
|
# expect that proofs are still pending
|
||||||
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
|
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
|
# only now settle the invoice
|
||||||
settle_invoice(preimage=preimage)
|
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)
|
await asyncio.sleep(SLEEP_TIME)
|
||||||
# expect that proofs are pending
|
# expect that proofs are pending
|
||||||
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
|
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)
|
settle_invoice(preimage=preimage)
|
||||||
await asyncio.sleep(SLEEP_TIME)
|
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
|
# expect that proofs are spent
|
||||||
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
|
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
|
@pytest.mark.asyncio
|
||||||
@@ -360,7 +380,7 @@ async def test_startup_regtest_pending_quote_failure(wallet: Wallet, ledger: Led
|
|||||||
|
|
||||||
# expect that proofs are pending
|
# expect that proofs are pending
|
||||||
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
|
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)
|
cancel_invoice(preimage_hash=preimage_hash)
|
||||||
await asyncio.sleep(SLEEP_TIME)
|
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
|
# expect that proofs are unspent
|
||||||
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
|
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",
|
unit="sat",
|
||||||
amount=100,
|
amount=100,
|
||||||
fee_reserve=12,
|
fee_reserve=12,
|
||||||
paid=False,
|
|
||||||
state=MeltQuoteState.unpaid,
|
state=MeltQuoteState.unpaid,
|
||||||
)
|
)
|
||||||
payment = await blink.pay_invoice(quote, 1000)
|
payment = await blink.pay_invoice(quote, 1000)
|
||||||
assert payment.ok
|
assert payment.settled
|
||||||
assert payment.fee
|
assert payment.fee
|
||||||
assert payment.fee.amount == 10
|
assert payment.fee.amount == 10
|
||||||
assert payment.error_message is None
|
assert payment.error_message is None
|
||||||
@@ -131,11 +130,10 @@ async def test_blink_pay_invoice_failure():
|
|||||||
unit="sat",
|
unit="sat",
|
||||||
amount=100,
|
amount=100,
|
||||||
fee_reserve=12,
|
fee_reserve=12,
|
||||||
paid=False,
|
|
||||||
state=MeltQuoteState.unpaid,
|
state=MeltQuoteState.unpaid,
|
||||||
)
|
)
|
||||||
payment = await blink.pay_invoice(quote, 1000)
|
payment = await blink.pay_invoice(quote, 1000)
|
||||||
assert not payment.ok
|
assert not payment.settled
|
||||||
assert payment.fee is None
|
assert payment.fee is None
|
||||||
assert payment.error_message
|
assert payment.error_message
|
||||||
assert "This is the error" in 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))
|
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
||||||
status = await blink.get_invoice_status("123")
|
status = await blink.get_invoice_status("123")
|
||||||
assert status.paid
|
assert status.settled
|
||||||
|
|
||||||
|
|
||||||
@respx.mock
|
@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))
|
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
|
||||||
status = await blink.get_payment_status(payment_request)
|
status = await blink.get_payment_status(payment_request)
|
||||||
assert status.paid
|
assert status.settled
|
||||||
assert status.fee
|
assert status.fee
|
||||||
assert status.fee.amount == 10
|
assert status.fee.amount == 10
|
||||||
assert status.preimage == "123"
|
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
|
||||||
import pytest_asyncio
|
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.helpers import sum_proofs
|
||||||
from cashu.core.models import PostMeltQuoteRequest, PostMintQuoteRequest
|
from cashu.core.models import PostMeltQuoteRequest, PostMintQuoteRequest
|
||||||
from cashu.mint.ledger import Ledger
|
from cashu.mint.ledger import Ledger
|
||||||
@@ -38,9 +38,8 @@ async def wallet1(ledger: Ledger):
|
|||||||
async def test_melt_internal(wallet1: Wallet, ledger: Ledger):
|
async def test_melt_internal(wallet1: Wallet, ledger: Ledger):
|
||||||
# mint twice so we have enough to pay the second invoice back
|
# mint twice so we have enough to pay the second invoice back
|
||||||
invoice = await wallet1.request_mint(128)
|
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 wallet1.mint(128, id=invoice.id)
|
||||||
await pay_if_regtest(invoice.bolt11)
|
|
||||||
assert wallet1.balance == 128
|
assert wallet1.balance == 128
|
||||||
|
|
||||||
# create a mint quote so that we can melt to it internally
|
# 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)
|
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 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)
|
keep_proofs, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 64)
|
||||||
await ledger.melt(proofs=send_proofs, quote=melt_quote.quote)
|
await ledger.melt(proofs=send_proofs, quote=melt_quote.quote)
|
||||||
|
|
||||||
melt_quote_post_payment = await ledger.get_melt_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.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.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)
|
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 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"
|
assert not melt_quote.paid, "melt quote should not be paid"
|
||||||
await ledger.melt(proofs=send_proofs, quote=melt_quote.quote)
|
await ledger.melt(proofs=send_proofs, quote=melt_quote.quote)
|
||||||
|
|
||||||
melt_quote_post_payment = await ledger.get_melt_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.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.asyncio
|
||||||
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
|
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
|
||||||
async def test_mint_internal(wallet1: Wallet, ledger: Ledger):
|
async def test_mint_internal(wallet1: Wallet, ledger: Ledger):
|
||||||
invoice = await wallet1.request_mint(128)
|
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)
|
mint_quote = await ledger.get_mint_quote(invoice.id)
|
||||||
|
|
||||||
assert mint_quote.paid, "mint quote should be paid"
|
assert mint_quote.paid, "mint quote should be paid"
|
||||||
assert mint_quote.state == MintQuoteState.paid
|
assert mint_quote.paid
|
||||||
|
|
||||||
output_amounts = [128]
|
output_amounts = [128]
|
||||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
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)
|
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.issued, "mint quote should be issued"
|
||||||
assert mint_quote_after_payment.state == MintQuoteState.issued
|
assert mint_quote_after_payment.issued
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@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):
|
async def test_mint_external(wallet1: Wallet, ledger: Ledger):
|
||||||
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=128, unit="sat"))
|
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=128, unit="sat"))
|
||||||
assert not quote.paid, "mint quote should not be paid"
|
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)
|
mint_quote = await ledger.get_mint_quote(quote.quote)
|
||||||
assert not mint_quote.paid, "mint quote already paid"
|
assert not mint_quote.paid, "mint quote already paid"
|
||||||
assert mint_quote.state == MintQuoteState.unpaid
|
assert mint_quote.unpaid
|
||||||
|
|
||||||
await assert_err(
|
await assert_err(
|
||||||
wallet1.mint(128, id=quote.quote),
|
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)
|
mint_quote = await ledger.get_mint_quote(quote.quote)
|
||||||
assert mint_quote.paid, "mint quote should be paid"
|
assert mint_quote.paid, "mint quote should be paid"
|
||||||
assert mint_quote.state == MintQuoteState.paid
|
assert mint_quote.paid
|
||||||
|
|
||||||
output_amounts = [128]
|
output_amounts = [128]
|
||||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
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)
|
await ledger.mint(outputs=outputs, quote_id=quote.quote)
|
||||||
|
|
||||||
mint_quote_after_payment = await ledger.get_mint_quote(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.issued, "mint quote should be issued"
|
||||||
assert mint_quote_after_payment.state == MintQuoteState.issued
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
import bolt11
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
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.mint.ledger import Ledger
|
||||||
from cashu.wallet.wallet import Wallet
|
from cashu.wallet.wallet import Wallet
|
||||||
from tests.conftest import SERVER_ENDPOINT
|
from tests.conftest import SERVER_ENDPOINT
|
||||||
from tests.helpers import (
|
from tests.helpers import (
|
||||||
SLEEP_TIME,
|
SLEEP_TIME,
|
||||||
|
cancel_invoice,
|
||||||
get_hold_invoice,
|
get_hold_invoice,
|
||||||
|
get_real_invoice,
|
||||||
|
get_real_invoice_cln,
|
||||||
is_fake,
|
is_fake,
|
||||||
pay_if_regtest,
|
pay_if_regtest,
|
||||||
|
pay_real_invoice,
|
||||||
settle_invoice,
|
settle_invoice,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,6 +33,243 @@ async def wallet():
|
|||||||
yield 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.asyncio
|
||||||
@pytest.mark.skipif(is_fake, reason="only regtest")
|
@pytest.mark.skipif(is_fake, reason="only regtest")
|
||||||
async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger):
|
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
|
# expect that proofs are still pending
|
||||||
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
|
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
|
# only now settle the invoice
|
||||||
settle_invoice(preimage=preimage)
|
settle_invoice(preimage=preimage)
|
||||||
@@ -71,7 +314,7 @@ async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger):
|
|||||||
|
|
||||||
# expect that proofs are now spent
|
# expect that proofs are now spent
|
||||||
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
|
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
|
# expect that no melt quote is pending
|
||||||
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import pytest
|
|||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
from fastapi.testclient import TestClient
|
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.api.app import app
|
||||||
from cashu.wallet.wallet import Wallet
|
from cashu.wallet.wallet import Wallet
|
||||||
from tests.conftest import SERVER_ENDPOINT
|
from tests.conftest import SERVER_ENDPOINT
|
||||||
@@ -29,8 +29,8 @@ async def test_invoice(wallet: Wallet):
|
|||||||
response = client.post("/lightning/create_invoice?amount=100")
|
response = client.post("/lightning/create_invoice?amount=100")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
invoice_response = InvoiceResponse.parse_obj(response.json())
|
invoice_response = InvoiceResponse.parse_obj(response.json())
|
||||||
state = PaymentStatus(paid=False)
|
state = PaymentStatus(result=PaymentResult.PENDING)
|
||||||
while not state.paid:
|
while state.pending:
|
||||||
print("checking invoice state")
|
print("checking invoice state")
|
||||||
response2 = client.get(
|
response2 = client.get(
|
||||||
f"/lightning/invoice_state?payment_hash={invoice_response.checking_id}"
|
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"]
|
initial_balance = response.json()["balance"]
|
||||||
response = client.post("/lightning/create_invoice?amount=100")
|
response = client.post("/lightning/create_invoice?amount=100")
|
||||||
invoice_response = InvoiceResponse.parse_obj(response.json())
|
invoice_response = InvoiceResponse.parse_obj(response.json())
|
||||||
state = PaymentStatus(paid=False)
|
state = PaymentStatus(result=PaymentResult.PENDING)
|
||||||
while not state.paid:
|
while state.pending:
|
||||||
print("checking invoice state")
|
print("checking invoice state")
|
||||||
response2 = client.get(
|
response2 = client.get(
|
||||||
f"/lightning/invoice_state?payment_hash={invoice_response.checking_id}"
|
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.payment_request
|
||||||
assert invoice.checking_id
|
assert invoice.checking_id
|
||||||
status = await wallet.get_invoice_status(invoice.checking_id)
|
status = await wallet.get_invoice_status(invoice.checking_id)
|
||||||
assert status.paid
|
assert status.settled
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -94,10 +94,10 @@ async def test_check_invoice_external(wallet: LightningWallet):
|
|||||||
assert invoice.payment_request
|
assert invoice.payment_request
|
||||||
assert invoice.checking_id
|
assert invoice.checking_id
|
||||||
status = await wallet.get_invoice_status(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)
|
await pay_if_regtest(invoice.payment_request)
|
||||||
status = await wallet.get_invoice_status(invoice.checking_id)
|
status = await wallet.get_invoice_status(invoice.checking_id)
|
||||||
assert status.paid
|
assert status.settled
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -115,12 +115,12 @@ async def test_pay_invoice_internal(wallet: LightningWallet):
|
|||||||
assert invoice2.payment_request
|
assert invoice2.payment_request
|
||||||
status = await wallet.pay_invoice(invoice2.payment_request)
|
status = await wallet.pay_invoice(invoice2.payment_request)
|
||||||
|
|
||||||
assert status.ok
|
assert status.settled
|
||||||
|
|
||||||
# check payment
|
# check payment
|
||||||
assert invoice2.checking_id
|
assert invoice2.checking_id
|
||||||
status = await wallet.get_payment_status(invoice2.checking_id)
|
status = await wallet.get_payment_status(invoice2.checking_id)
|
||||||
assert status.paid
|
assert status.settled
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -132,16 +132,16 @@ async def test_pay_invoice_external(wallet: LightningWallet):
|
|||||||
assert invoice.checking_id
|
assert invoice.checking_id
|
||||||
await pay_if_regtest(invoice.payment_request)
|
await pay_if_regtest(invoice.payment_request)
|
||||||
status = await wallet.get_invoice_status(invoice.checking_id)
|
status = await wallet.get_invoice_status(invoice.checking_id)
|
||||||
assert status.paid
|
assert status.settled
|
||||||
assert wallet.available_balance >= 64
|
assert wallet.available_balance >= 64
|
||||||
|
|
||||||
# pay invoice
|
# pay invoice
|
||||||
invoice_real = get_real_invoice(16)
|
invoice_real = get_real_invoice(16)
|
||||||
status = await wallet.pay_invoice(invoice_real["payment_request"])
|
status = await wallet.pay_invoice(invoice_real["payment_request"])
|
||||||
|
|
||||||
assert status.ok
|
assert status.settled
|
||||||
|
|
||||||
# check payment
|
# check payment
|
||||||
assert status.checking_id
|
assert status.checking_id
|
||||||
status = await wallet.get_payment_status(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
|
||||||
import pytest_asyncio
|
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.crypto.secp import PrivateKey, PublicKey
|
||||||
from cashu.core.migrations import migrate_databases
|
from cashu.core.migrations import migrate_databases
|
||||||
from cashu.core.p2pk import SigFlags
|
from cashu.core.p2pk import SigFlags
|
||||||
@@ -80,7 +80,7 @@ async def test_p2pk(wallet1: Wallet, wallet2: Wallet):
|
|||||||
await wallet2.redeem(send_proofs)
|
await wallet2.redeem(send_proofs)
|
||||||
|
|
||||||
proof_states = await wallet2.check_proof_state(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:
|
if not is_deprecated_api_only:
|
||||||
for state in proof_states.states:
|
for state in proof_states.states:
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import bolt11
|
|||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
|
|
||||||
from cashu.core.base import ProofSpentState
|
|
||||||
from cashu.mint.ledger import Ledger
|
from cashu.mint.ledger import Ledger
|
||||||
from cashu.wallet.wallet import Wallet
|
from cashu.wallet.wallet import Wallet
|
||||||
from tests.conftest import SERVER_ENDPOINT
|
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)
|
await asyncio.sleep(SLEEP_TIME)
|
||||||
|
|
||||||
states = await wallet.check_proof_state(send_proofs)
|
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)
|
settle_invoice(preimage=preimage)
|
||||||
|
|
||||||
await asyncio.sleep(SLEEP_TIME)
|
await asyncio.sleep(SLEEP_TIME)
|
||||||
|
|
||||||
states = await wallet.check_proof_state(send_proofs)
|
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
|
@pytest.mark.asyncio
|
||||||
@@ -97,11 +96,11 @@ async def test_regtest_failed_quote(wallet: Wallet, ledger: Ledger):
|
|||||||
await asyncio.sleep(SLEEP_TIME)
|
await asyncio.sleep(SLEEP_TIME)
|
||||||
|
|
||||||
states = await wallet.check_proof_state(send_proofs)
|
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)
|
cancel_invoice(preimage_hash=preimage_hash)
|
||||||
|
|
||||||
await asyncio.sleep(SLEEP_TIME)
|
await asyncio.sleep(SLEEP_TIME)
|
||||||
|
|
||||||
states = await wallet.check_proof_state(send_proofs)
|
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,
|
fee_reserve_sat=quote.fee_reserve,
|
||||||
quote_id=quote.quote,
|
quote_id=quote.quote,
|
||||||
)
|
)
|
||||||
|
|
||||||
def mint_pay_mpp(invoice: str, amount: int, proofs: List[Proof]):
|
def mint_pay_mpp(invoice: str, amount: int, proofs: List[Proof]):
|
||||||
asyncio.run(_mint_pay_mpp(invoice, amount, proofs))
|
asyncio.run(_mint_pay_mpp(invoice, amount, proofs))
|
||||||
|
|
||||||
# call pay_mpp twice in parallel to pay the full invoice
|
# call pay_mpp twice in parallel to pay the full invoice
|
||||||
t1 = threading.Thread(target=mint_pay_mpp, args=(invoice_payment_request, 32, proofs1))
|
t1 = threading.Thread(
|
||||||
t2 = threading.Thread(target=partial_pay_real_invoice, args=(invoice_payment_request, 32, 1))
|
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()
|
t1.start()
|
||||||
t2.start()
|
t2.start()
|
||||||
t1.join()
|
t1.join()
|
||||||
t2.join()
|
t2.join()
|
||||||
|
|
||||||
assert wallet.balance <= 256 - 32
|
assert wallet.balance == 64
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -80,7 +85,7 @@ async def test_regtest_pay_mpp_incomplete_payment(wallet: Wallet, ledger: Ledger
|
|||||||
# make sure that mpp is supported by the bolt11-sat backend
|
# make sure that mpp is supported by the bolt11-sat backend
|
||||||
if not ledger.backends[Method["bolt11"]][wallet.unit].supports_mpp:
|
if not ledger.backends[Method["bolt11"]][wallet.unit].supports_mpp:
|
||||||
pytest.skip("backend does not support mpp")
|
pytest.skip("backend does not support mpp")
|
||||||
|
|
||||||
# This test cannot be done with CLN because we only have one mint
|
# This test cannot be done with CLN because we only have one mint
|
||||||
# and CLN hates multiple partial payment requests
|
# and CLN hates multiple partial payment requests
|
||||||
if isinstance(ledger.backends[Method["bolt11"]][wallet.unit], CLNRestWallet):
|
if isinstance(ledger.backends[Method["bolt11"]][wallet.unit], CLNRestWallet):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import asyncio
|
|||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
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.json_rpc.base import JSONRPCNotficationParams
|
||||||
from cashu.core.nuts import WEBSOCKETS_NUT
|
from cashu.core.nuts import WEBSOCKETS_NUT
|
||||||
from cashu.core.settings import settings
|
from cashu.core.settings import settings
|
||||||
@@ -54,13 +54,10 @@ async def test_wallet_subscription_mint(wallet: Wallet):
|
|||||||
assert triggered
|
assert triggered
|
||||||
assert len(msg_stack) == 3
|
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[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[1].payload["state"] == MintQuoteState.paid.value
|
||||||
|
|
||||||
assert msg_stack[2].payload["paid"] is True
|
|
||||||
assert msg_stack[2].payload["state"] == MintQuoteState.issued.value
|
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]
|
pending_stack = msg_stack[:n_subscriptions]
|
||||||
for msg in pending_stack:
|
for msg in pending_stack:
|
||||||
proof_state = ProofState.parse_obj(msg.payload)
|
proof_state = ProofState.parse_obj(msg.payload)
|
||||||
assert proof_state.state == ProofSpentState.unspent
|
assert proof_state.unspent
|
||||||
|
|
||||||
# the second one is the PENDING state
|
# the second one is the PENDING state
|
||||||
spent_stack = msg_stack[n_subscriptions : n_subscriptions * 2]
|
spent_stack = msg_stack[n_subscriptions : n_subscriptions * 2]
|
||||||
for msg in spent_stack:
|
for msg in spent_stack:
|
||||||
proof_state = ProofState.parse_obj(msg.payload)
|
proof_state = ProofState.parse_obj(msg.payload)
|
||||||
assert proof_state.state == ProofSpentState.pending
|
assert proof_state.pending
|
||||||
|
|
||||||
# the third one is the SPENT state
|
# the third one is the SPENT state
|
||||||
spent_stack = msg_stack[n_subscriptions * 2 :]
|
spent_stack = msg_stack[n_subscriptions * 2 :]
|
||||||
for msg in spent_stack:
|
for msg in spent_stack:
|
||||||
proof_state = ProofState.parse_obj(msg.payload)
|
proof_state = ProofState.parse_obj(msg.payload)
|
||||||
assert proof_state.state == ProofSpentState.spent
|
assert proof_state.spent
|
||||||
|
|||||||
Reference in New Issue
Block a user