Skip to content

Commit

Permalink
WIP: New melt flow (cashubtc#622)
Browse files Browse the repository at this point in the history
* `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 <[email protected]>
  • Loading branch information
callebtc and lollerfirst authored Sep 24, 2024
1 parent 25f0763 commit d8d3037
Show file tree
Hide file tree
Showing 39 changed files with 1,565 additions and 672 deletions.
68 changes: 56 additions & 12 deletions cashu/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,18 @@ def identifier(self) -> str:
def kind(self) -> JSONRPCSubscriptionKinds:
return JSONRPCSubscriptionKinds.PROOF_STATE

@property
def unspent(self) -> bool:
return self.state == ProofSpentState.unspent

@property
def spent(self) -> bool:
return self.state == ProofSpentState.spent

@property
def pending(self) -> bool:
return self.state == ProofSpentState.pending


class HTLCWitness(BaseModel):
preimage: Optional[str] = None
Expand Down Expand Up @@ -290,7 +302,6 @@ class MeltQuote(LedgerEvent):
unit: str
amount: int
fee_reserve: int
paid: bool
state: MeltQuoteState
created_time: Union[int, None] = None
paid_time: Union[int, None] = None
Expand Down Expand Up @@ -325,7 +336,6 @@ def from_row(cls, row: Row):
unit=row["unit"],
amount=row["amount"],
fee_reserve=row["fee_reserve"],
paid=row["paid"],
state=MeltQuoteState[row["state"]],
created_time=created_time,
paid_time=paid_time,
Expand All @@ -344,17 +354,34 @@ def identifier(self) -> str:
def kind(self) -> JSONRPCSubscriptionKinds:
return JSONRPCSubscriptionKinds.BOLT11_MELT_QUOTE

@property
def unpaid(self) -> bool:
return self.state == MeltQuoteState.unpaid

@property
def pending(self) -> bool:
return self.state == MeltQuoteState.pending

@property
def paid(self) -> bool:
return self.state == MeltQuoteState.paid

# method that is invoked when the `state` attribute is changed. to protect the state from being set to anything else if the current state is paid
def __setattr__(self, name, value):
# an unpaid quote can only be set to pending or paid
if name == "state" and self.state == MeltQuoteState.unpaid:
if name == "state" and self.unpaid:
if value not in [MeltQuoteState.pending, MeltQuoteState.paid]:
raise Exception(
f"Cannot change state of an unpaid melt quote to {value}."
)
# a paid quote can not be changed
if name == "state" and self.state == MeltQuoteState.paid:
if name == "state" and self.paid:
raise Exception("Cannot change state of a paid melt quote.")

if name == "paid":
raise Exception(
"MeltQuote does not support `paid` anymore! Use `state` instead."
)
super().__setattr__(name, value)


Expand All @@ -375,8 +402,6 @@ class MintQuote(LedgerEvent):
checking_id: str
unit: str
amount: int
paid: bool
issued: bool
state: MintQuoteState
created_time: Union[int, None] = None
paid_time: Union[int, None] = None
Expand All @@ -401,8 +426,6 @@ def from_row(cls, row: Row):
checking_id=row["checking_id"],
unit=row["unit"],
amount=row["amount"],
paid=row["paid"],
issued=row["issued"],
state=MintQuoteState[row["state"]],
created_time=created_time,
paid_time=paid_time,
Expand All @@ -417,24 +440,45 @@ def identifier(self) -> str:
def kind(self) -> JSONRPCSubscriptionKinds:
return JSONRPCSubscriptionKinds.BOLT11_MINT_QUOTE

@property
def unpaid(self) -> bool:
return self.state == MintQuoteState.unpaid

@property
def paid(self) -> bool:
return self.state == MintQuoteState.paid

@property
def pending(self) -> bool:
return self.state == MintQuoteState.pending

@property
def issued(self) -> bool:
return self.state == MintQuoteState.issued

def __setattr__(self, name, value):
# un unpaid quote can only be set to paid
if name == "state" and self.state == MintQuoteState.unpaid:
if name == "state" and self.unpaid:
if value != MintQuoteState.paid:
raise Exception(
f"Cannot change state of an unpaid mint quote to {value}."
)
# a paid quote can only be set to pending or issued
if name == "state" and self.state == MintQuoteState.paid:
if name == "state" and self.paid:
if value != MintQuoteState.pending and value != MintQuoteState.issued:
raise Exception(f"Cannot change state of a paid mint quote to {value}.")
# a pending quote can only be set to paid or issued
if name == "state" and self.state == MintQuoteState.pending:
if name == "state" and self.pending:
if value not in [MintQuoteState.paid, MintQuoteState.issued]:
raise Exception("Cannot change state of a pending mint quote.")
# an issued quote cannot be changed
if name == "state" and self.state == MintQuoteState.issued:
if name == "state" and self.issued:
raise Exception("Cannot change state of an issued mint quote.")

if name == "paid":
raise Exception(
"MintQuote does not support `paid` anymore! Use `state` instead."
)
super().__setattr__(name, value)


Expand Down
4 changes: 3 additions & 1 deletion cashu/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ class PostMeltQuoteResponse(BaseModel):
fee_reserve: int # input fee reserve
paid: Optional[
bool
] # whether the request has been paid # DEPRECATED as per NUT PR #136
] = None # whether the request has been paid # DEPRECATED as per NUT PR #136
state: Optional[str] # state of the quote
expiry: Optional[int] # expiry of the quote
payment_preimage: Optional[str] = None # payment preimage
Expand All @@ -224,6 +224,8 @@ def from_melt_quote(self, melt_quote: MeltQuote) -> "PostMeltQuoteResponse":
to_dict = melt_quote.dict()
# turn state into string
to_dict["state"] = melt_quote.state.value
# add deprecated "paid" field
to_dict["paid"] = melt_quote.paid
return PostMeltQuoteResponse.parse_obj(to_dict)


Expand Down
3 changes: 2 additions & 1 deletion cashu/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ class FakeWalletSettings(MintSettings):
fakewallet_delay_outgoing_payment: Optional[float] = Field(default=3.0)
fakewallet_delay_incoming_payment: Optional[float] = Field(default=3.0)
fakewallet_stochastic_invoice: bool = Field(default=False)
fakewallet_payment_state: Optional[bool] = Field(default=None)
fakewallet_payment_state: Optional[str] = Field(default="SETTLED")
fakewallet_pay_invoice_state: Optional[str] = Field(default="SETTLED")


class MintInformation(CashuSettings):
Expand Down
64 changes: 53 additions & 11 deletions cashu/lightning/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from abc import ABC, abstractmethod
from enum import Enum, auto
from typing import AsyncGenerator, Coroutine, Optional, Union

from pydantic import BaseModel
Expand All @@ -12,8 +13,8 @@


class StatusResponse(BaseModel):
error_message: Optional[str]
balance: Union[int, float]
error_message: Optional[str] = None


class InvoiceQuoteResponse(BaseModel):
Expand All @@ -34,36 +35,77 @@ class InvoiceResponse(BaseModel):
error_message: Optional[str] = None


class PaymentResult(Enum):
SETTLED = auto()
FAILED = auto()
PENDING = auto()
UNKNOWN = auto()

def __str__(self):
return self.name


class PaymentResponse(BaseModel):
ok: Optional[bool] = None # True: paid, False: failed, None: pending or unknown
result: PaymentResult
checking_id: Optional[str] = None
fee: Optional[Amount] = None
preimage: Optional[str] = None
error_message: Optional[str] = None

@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


class PaymentStatus(BaseModel):
paid: Optional[bool] = None
result: PaymentResult
fee: Optional[Amount] = None
preimage: Optional[str] = None
error_message: Optional[str] = None

@property
def pending(self) -> bool:
return self.paid is not True
return self.result == PaymentResult.PENDING

@property
def settled(self) -> bool:
return self.result == PaymentResult.SETTLED

@property
def failed(self) -> bool:
return self.paid is False
return self.result == PaymentResult.FAILED

@property
def unknown(self) -> bool:
return self.result == PaymentResult.UNKNOWN

def __str__(self) -> str:
if self.paid is True:
return "settled"
elif self.paid is False:
if self.result == PaymentResult.SETTLED:
return (
"settled"
+ (f" (preimage: {self.preimage})" if self.preimage else "")
+ (f" (fee: {self.fee})" if self.fee else "")
)
elif self.result == PaymentResult.FAILED:
return "failed"
elif self.paid is None:
elif self.result == PaymentResult.PENDING:
return "still pending"
else:
return "unknown (should never happen)"
else: # self.result == PaymentResult.UNKNOWN:
return "unknown" + (
f" (Error: {self.error_message})" if self.error_message else ""
)


class LightningBackend(ABC):
Expand Down
Loading

0 comments on commit d8d3037

Please sign in to comment.