NUT-04 and NUT-05: Add state field to quotes (#560)

* wip adding states, tests failing

* add state field to quotes

* responses from quotes

* store correct state

* cleaner test

* fix swap check

* oops
This commit is contained in:
callebtc
2024-06-26 03:06:01 +02:00
committed by GitHub
parent e846acf946
commit 6b38ef6c29
26 changed files with 330 additions and 84 deletions

View File

@@ -48,7 +48,7 @@ class DLEQWallet(BaseModel):
# ------- PROOFS -------
class SpentState(Enum):
class ProofSpentState(Enum):
unspent = "UNSPENT"
spent = "SPENT"
pending = "PENDING"
@@ -59,13 +59,13 @@ class SpentState(Enum):
class ProofState(LedgerEvent):
Y: str
state: SpentState
state: ProofSpentState
witness: Optional[str] = None
@root_validator()
def check_witness(cls, values):
state, witness = values.get("state"), values.get("witness")
if witness is not None and state != SpentState.spent:
if witness is not None and state != ProofSpentState.spent:
raise ValueError('Witness can only be set if the spent state is "SPENT"')
return values
@@ -268,6 +268,15 @@ class Invoice(BaseModel):
time_paid: Union[None, str, int, float] = ""
class MeltQuoteState(Enum):
unpaid = "UNPAID"
pending = "PENDING"
paid = "PAID"
def __str__(self):
return self.name
class MeltQuote(LedgerEvent):
quote: str
method: str
@@ -277,6 +286,7 @@ class MeltQuote(LedgerEvent):
amount: int
fee_reserve: int
paid: bool
state: MeltQuoteState
created_time: Union[int, None] = None
paid_time: Union[int, None] = None
fee_paid: int = 0
@@ -303,6 +313,7 @@ class MeltQuote(LedgerEvent):
amount=row["amount"],
fee_reserve=row["fee_reserve"],
paid=row["paid"],
state=MeltQuoteState[row["state"]],
created_time=created_time,
paid_time=paid_time,
fee_paid=row["fee_paid"],
@@ -318,6 +329,27 @@ class MeltQuote(LedgerEvent):
def kind(self) -> JSONRPCSubscriptionKinds:
return JSONRPCSubscriptionKinds.BOLT11_MELT_QUOTE
# 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 value != MeltQuoteState.pending and value != MeltQuoteState.paid:
raise Exception("Cannot change state of an unpaid quote.")
# a paid quote can not be changed
if name == "state" and self.state == MeltQuoteState.paid:
raise Exception("Cannot change state of a paid quote.")
super().__setattr__(name, value)
class MintQuoteState(Enum):
unpaid = "UNPAID"
paid = "PAID"
pending = "PENDING"
issued = "ISSUED"
def __str__(self):
return self.name
class MintQuote(LedgerEvent):
quote: str
@@ -328,6 +360,7 @@ class MintQuote(LedgerEvent):
amount: int
paid: bool
issued: bool
state: MintQuoteState
created_time: Union[int, None] = None
paid_time: Union[int, None] = None
expiry: Optional[int] = None
@@ -353,6 +386,7 @@ class MintQuote(LedgerEvent):
amount=row["amount"],
paid=row["paid"],
issued=row["issued"],
state=MintQuoteState[row["state"]],
created_time=created_time,
paid_time=paid_time,
)
@@ -366,6 +400,24 @@ class MintQuote(LedgerEvent):
def kind(self) -> JSONRPCSubscriptionKinds:
return JSONRPCSubscriptionKinds.BOLT11_MINT_QUOTE
def __setattr__(self, name, value):
# un unpaid quote can only be set to paid
if name == "state" and self.state == MintQuoteState.unpaid:
if value != MintQuoteState.paid:
raise Exception("Cannot change state of an unpaid quote.")
# a paid quote can only be set to pending or issued
if name == "state" and self.state == MintQuoteState.paid:
if value != MintQuoteState.pending and value != MintQuoteState.issued:
raise Exception(f"Cannot change state of a paid quote to {value}.")
# a pending quote can only be set to paid or issued
if name == "state" and self.state == MintQuoteState.pending:
if value not in [MintQuoteState.paid, MintQuoteState.issued]:
raise Exception("Cannot change state of a pending quote.")
# an issued quote cannot be changed
if name == "state" and self.state == MintQuoteState.issued:
raise Exception("Cannot change state of an issued quote.")
super().__setattr__(name, value)
# ------- KEYSETS -------

View File

@@ -6,6 +6,8 @@ from .base import (
BlindedMessage,
BlindedMessage_Deprecated,
BlindedSignature,
MeltQuote,
MintQuote,
Proof,
ProofState,
)
@@ -98,9 +100,19 @@ class PostMintQuoteRequest(BaseModel):
class PostMintQuoteResponse(BaseModel):
quote: str # quote id
request: str # input payment request
paid: bool # whether the request has been paid
paid: Optional[
bool
] # whether the request has been paid # DEPRECATED as per NUT PR #141
state: str # state of the quote
expiry: Optional[int] # expiry of the quote
@classmethod
def from_mint_quote(self, mint_quote: MintQuote) -> "PostMintQuoteResponse":
to_dict = mint_quote.dict()
# turn state into string
to_dict["state"] = mint_quote.state.value
return PostMintQuoteResponse.parse_obj(to_dict)
# ------- API: MINT -------
@@ -168,9 +180,17 @@ class PostMeltQuoteResponse(BaseModel):
quote: str # quote id
amount: int # input amount
fee_reserve: int # input fee reserve
paid: bool # whether the request has been paid
paid: bool # whether the request has been paid # DEPRECATED as per NUT PR #136
state: str # state of the quote
expiry: Optional[int] # expiry of the quote
@classmethod
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
return PostMeltQuoteResponse.parse_obj(to_dict)
# ------- API: MELT -------

View File

@@ -433,8 +433,8 @@ class LedgerCrudSqlite(LedgerCrud):
await (conn or db).execute(
f"""
INSERT INTO {table_with_schema(db, 'mint_quotes')}
(quote, method, request, checking_id, unit, amount, issued, paid, created_time, paid_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
(quote, method, request, checking_id, unit, amount, issued, paid, state, created_time, paid_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
quote.quote,
@@ -445,6 +445,7 @@ class LedgerCrudSqlite(LedgerCrud):
quote.amount,
quote.issued,
quote.paid,
quote.state.name,
timestamp_from_seconds(db, quote.created_time),
timestamp_from_seconds(db, quote.paid_time),
),
@@ -510,10 +511,11 @@ class LedgerCrudSqlite(LedgerCrud):
) -> None:
await (conn or db).execute(
f"UPDATE {table_with_schema(db, 'mint_quotes')} SET issued = ?, paid = ?,"
" paid_time = ? WHERE quote = ?",
" state = ?, paid_time = ? WHERE quote = ?",
(
quote.issued,
quote.paid,
quote.state.name,
timestamp_from_seconds(db, quote.paid_time),
quote.quote,
),
@@ -546,8 +548,8 @@ class LedgerCrudSqlite(LedgerCrud):
await (conn or db).execute(
f"""
INSERT INTO {table_with_schema(db, 'melt_quotes')}
(quote, method, request, checking_id, unit, amount, fee_reserve, paid, created_time, paid_time, fee_paid, proof)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
(quote, method, request, checking_id, unit, amount, fee_reserve, paid, state, created_time, paid_time, fee_paid, proof)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
quote.quote,
@@ -558,6 +560,7 @@ class LedgerCrudSqlite(LedgerCrud):
quote.amount,
quote.fee_reserve or 0,
quote.paid,
quote.state.name,
timestamp_from_seconds(db, quote.created_time),
timestamp_from_seconds(db, quote.paid_time),
quote.fee_paid,
@@ -608,10 +611,11 @@ class LedgerCrudSqlite(LedgerCrud):
conn: Optional[Connection] = None,
) -> None:
await (conn or db).execute(
f"UPDATE {table_with_schema(db, 'melt_quotes')} SET paid = ?, fee_paid = ?,"
" paid_time = ?, proof = ? WHERE quote = ?",
f"UPDATE {table_with_schema(db, 'melt_quotes')} SET paid = ?, state = ?,"
" fee_paid = ?, paid_time = ?, proof = ? WHERE quote = ?",
(
quote.paid,
quote.state.name,
quote.fee_paid,
timestamp_from_seconds(db, quote.paid_time),
quote.proof,

View File

@@ -1,6 +1,6 @@
from typing import Dict, List
from ...core.base import Proof, ProofState, SpentState
from ...core.base import Proof, ProofSpentState, ProofState
from ...core.db import Database
from ..crud import LedgerCrud
@@ -54,14 +54,14 @@ class DbReadHelper:
proofs_pending = await self._get_proofs_pending(Ys)
for Y in Ys:
if Y not in proofs_spent and Y not in proofs_pending:
states.append(ProofState(Y=Y, state=SpentState.unspent))
states.append(ProofState(Y=Y, state=ProofSpentState.unspent))
elif Y not in proofs_spent and Y in proofs_pending:
states.append(ProofState(Y=Y, state=SpentState.pending))
states.append(ProofState(Y=Y, state=ProofSpentState.pending))
else:
states.append(
ProofState(
Y=Y,
state=SpentState.spent,
state=ProofSpentState.spent,
witness=proofs_spent[Y].witness,
)
)

View File

@@ -3,7 +3,7 @@ from typing import List, Optional
from loguru import logger
from ...core.base import Proof, ProofState, SpentState
from ...core.base import Proof, ProofSpentState, ProofState
from ...core.db import Connection, Database, get_db_connection
from ...core.errors import (
TransactionError,
@@ -50,7 +50,7 @@ class DbWriteHelper:
proof=p, db=self.db, quote_id=quote_id, conn=conn
)
await self.events.submit(
ProofState(Y=p.Y, state=SpentState.pending)
ProofState(Y=p.Y, state=ProofSpentState.pending)
)
except Exception as e:
logger.error(f"Failed to set proofs pending: {e}")
@@ -72,7 +72,7 @@ class DbWriteHelper:
await self.crud.unset_proof_pending(proof=p, db=self.db, conn=conn)
if not spent:
await self.events.submit(
ProofState(Y=p.Y, state=SpentState.unspent)
ProofState(Y=p.Y, state=ProofSpentState.unspent)
)
async def _validate_proofs_pending(

View File

@@ -38,9 +38,9 @@ class LedgerEventManager:
def serialize_event(self, event: LedgerEvent) -> dict:
if isinstance(event, MintQuote):
return_dict = PostMintQuoteResponse.parse_obj(event.dict()).dict()
return_dict = PostMintQuoteResponse.from_mint_quote(event).dict()
elif isinstance(event, MeltQuote):
return_dict = PostMeltQuoteResponse.parse_obj(event.dict()).dict()
return_dict = PostMeltQuoteResponse.from_melt_quote(event).dict()
elif isinstance(event, ProofState):
return_dict = event.dict(exclude_unset=True, exclude_none=True)
return return_dict

View File

@@ -11,12 +11,14 @@ from ..core.base import (
BlindedMessage,
BlindedSignature,
MeltQuote,
MeltQuoteState,
Method,
MintKeyset,
MintQuote,
MintQuoteState,
Proof,
ProofSpentState,
ProofState,
SpentState,
Unit,
)
from ..core.crypto import b_dhke
@@ -153,6 +155,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
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.proof = payment.preimage or ""
@@ -302,7 +305,9 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
proof=p, db=self.db, quote_id=quote_id, conn=conn
)
await self.events.submit(
ProofState(Y=p.Y, state=SpentState.spent, witness=p.witness or None)
ProofState(
Y=p.Y, state=ProofSpentState.spent, witness=p.witness or None
)
)
async def _generate_change_promises(
@@ -429,6 +434,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
amount=quote_request.amount,
issued=False,
paid=False,
state=MintQuoteState.unpaid,
created_time=int(time.time()),
expiry=expiry,
)
@@ -455,7 +461,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
unit, method = self._verify_and_get_unit_method(quote.unit, quote.method)
if not quote.paid:
if quote.state == MintQuoteState.unpaid:
if not quote.checking_id:
raise CashuError("quote has no checking id")
logger.trace(f"Lightning: checking invoice {quote.checking_id}")
@@ -465,6 +471,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
if status.paid:
logger.trace(f"Setting quote {quote_id} as paid")
quote.paid = True
quote.state = MintQuoteState.paid
quote.paid_time = int(time.time())
await self.crud.update_mint_quote(quote=quote, db=self.db)
await self.events.submit(quote)
@@ -509,6 +516,12 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
raise QuoteNotPaidError()
if quote.issued:
raise TransactionError("quote already issued")
if not quote.state == MintQuoteState.paid:
raise QuoteNotPaidError()
if quote.state == MintQuoteState.issued:
raise TransactionError("quote already issued")
if not quote.unit == output_unit.name:
raise TransactionError("quote unit does not match output unit")
if not quote.amount == sum_amount_outputs:
@@ -518,6 +531,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
logger.trace(f"crud: setting quote {quote_id} as issued")
quote.issued = True
quote.state = MintQuoteState.issued
await self.crud.update_mint_quote(quote=quote, db=self.db)
promises = await self._generate_promises(outputs)
@@ -549,6 +563,12 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
raise TransactionError("mint quote already paid")
if mint_quote.issued:
raise TransactionError("mint quote already issued")
if mint_quote.state == MintQuoteState.issued:
raise TransactionError("mint quote already issued")
if mint_quote.state != MintQuoteState.unpaid:
raise TransactionError("mint quote already paid")
if not mint_quote.checking_id:
raise TransactionError("mint quote has no checking id")
if melt_quote.is_mpp:
@@ -658,6 +678,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
unit=unit.name,
amount=payment_quote.amount.to(unit).amount,
paid=False,
state=MeltQuoteState.unpaid,
fee_reserve=payment_quote.fee.to(unit).amount,
created_time=int(time.time()),
expiry=expiry,
@@ -670,6 +691,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
amount=quote.amount,
fee_reserve=quote.fee_reserve,
paid=quote.paid,
state=quote.state.value,
expiry=quote.expiry,
)
@@ -714,6 +736,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
if status.paid:
logger.trace(f"Setting quote {quote_id} as paid")
melt_quote.paid = True
melt_quote.state = MeltQuoteState.paid
if status.fee:
melt_quote.fee_paid = status.fee.to(unit).amount
if status.preimage:
@@ -753,6 +776,8 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
# we settle the transaction internally
if melt_quote.paid:
raise TransactionError("melt quote already paid")
if melt_quote.state != MeltQuoteState.unpaid:
raise TransactionError("melt quote already paid")
# verify amounts from bolt11 invoice
bolt11_request = melt_quote.request
@@ -768,11 +793,15 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
raise TransactionError("units do not match")
if not mint_quote.method == melt_quote.method:
raise TransactionError("methods do not match")
if mint_quote.paid:
raise TransactionError("mint quote already paid")
if mint_quote.issued:
raise TransactionError("mint quote already issued")
if mint_quote.state != MintQuoteState.unpaid:
raise TransactionError("mint quote already paid")
logger.info(
f"Settling bolt11 payment internally: {melt_quote.quote} ->"
f" {mint_quote.quote} ({melt_quote.amount} {melt_quote.unit})"
@@ -780,9 +809,11 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
melt_quote.fee_paid = 0 # no internal fees
melt_quote.paid = True
melt_quote.state = MeltQuoteState.paid
melt_quote.paid_time = int(time.time())
mint_quote.paid = True
mint_quote.state = MintQuoteState.paid
mint_quote.paid_time = melt_quote.paid_time
async with get_db_connection(self.db) as conn:
@@ -821,7 +852,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
melt_quote.unit, melt_quote.method
)
if melt_quote.paid:
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
@@ -866,7 +897,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
# settle the transaction internally if there is a mint quote with the same payment request
melt_quote = await self.melt_mint_settle_internally(melt_quote, proofs)
# quote not paid yet (not internal), pay it with the backend
if not melt_quote.paid:
if not melt_quote.paid and melt_quote.state == MeltQuoteState.unpaid:
logger.debug(f"Lightning: pay invoice {melt_quote.request}")
payment = await self.backends[method][unit].pay_invoice(
melt_quote, melt_quote.fee_reserve * 1000
@@ -887,6 +918,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
melt_quote.proof = payment.preimage
# set quote as paid
melt_quote.paid = True
melt_quote.state = MeltQuoteState.paid
melt_quote.paid_time = int(time.time())
await self.crud.update_melt_quote(quote=melt_quote, db=self.db)
await self.events.submit(melt_quote)

View File

@@ -773,3 +773,45 @@ async def m019_add_fee_to_keysets(db: Database):
await conn.execute(
f"UPDATE {table_with_schema(db, 'keysets')} SET input_fee_ppk = 0"
)
async def m020_add_state_to_mint_and_melt_quotes(db: Database):
async with db.connect() as conn:
await conn.execute(
f"ALTER TABLE {table_with_schema(db, 'mint_quotes')} ADD COLUMN state TEXT"
)
await conn.execute(
f"ALTER TABLE {table_with_schema(db, 'melt_quotes')} ADD COLUMN state TEXT"
)
# get all melt and mint quotes and figure out the state to set using the `paid` column
# and the `paid` and `issued` column respectively
# mint quotes:
async with db.connect() as conn:
rows = await conn.fetchall(
f"SELECT * FROM {table_with_schema(db, 'mint_quotes')}"
)
for row in rows:
if row["issued"]:
state = "issued"
elif row["paid"]:
state = "paid"
else:
state = "unpaid"
await conn.execute(
f"UPDATE {table_with_schema(db, 'mint_quotes')} SET state = '{state}' WHERE quote = '{row['quote']}'"
)
# melt quotes:
async with db.connect() as conn:
rows = await conn.fetchall(
f"SELECT * FROM {table_with_schema(db, 'melt_quotes')}"
)
for row in rows:
if row["paid"]:
state = "paid"
else:
state = "unpaid"
await conn.execute(
f"UPDATE {table_with_schema(db, 'melt_quotes')} SET state = '{state}' WHERE quote = '{row['quote']}'"
)

View File

@@ -161,6 +161,7 @@ async def mint_quote(
request=quote.request,
quote=quote.quote,
paid=quote.paid,
state=quote.state.value,
expiry=quote.expiry,
)
logger.trace(f"< POST /v1/mint/quote/bolt11: {resp}")
@@ -184,6 +185,7 @@ async def get_mint_quote(request: Request, quote: str) -> PostMintQuoteResponse:
quote=mint_quote.quote,
request=mint_quote.request,
paid=mint_quote.paid,
state=mint_quote.state.value,
expiry=mint_quote.expiry,
)
logger.trace(f"< GET /v1/mint/quote/bolt11/{quote}")
@@ -274,6 +276,7 @@ async def get_melt_quote(request: Request, quote: str) -> PostMeltQuoteResponse:
amount=melt_quote.amount,
fee_reserve=melt_quote.fee_reserve,
paid=melt_quote.paid,
state=melt_quote.state.value,
expiry=melt_quote.expiry,
)
logger.trace(f"< GET /v1/melt/quote/bolt11/{quote}")

View File

@@ -3,7 +3,7 @@ from typing import Dict, List, Optional
from fastapi import APIRouter, Request
from loguru import logger
from ..core.base import BlindedMessage, BlindedSignature, SpentState
from ..core.base import BlindedMessage, BlindedSignature, ProofSpentState
from ..core.errors import CashuError
from ..core.models import (
CheckFeesRequest_deprecated,
@@ -345,13 +345,13 @@ async def check_spendable_deprecated(
spendableList: List[bool] = []
pendingList: List[bool] = []
for proof_state in proofs_state:
if proof_state.state == SpentState.unspent:
if proof_state.state == ProofSpentState.unspent:
spendableList.append(True)
pendingList.append(False)
elif proof_state.state == SpentState.spent:
elif proof_state.state == ProofSpentState.spent:
spendableList.append(False)
pendingList.append(False)
elif proof_state.state == SpentState.pending:
elif proof_state.state == ProofSpentState.pending:
spendableList.append(True)
pendingList.append(True)
return CheckSpendableResponse_deprecated(

View File

@@ -3,7 +3,7 @@ from typing import Mapping
from loguru import logger
from ..core.base import Method, Unit
from ..core.base import Method, MintQuoteState, Unit
from ..core.db import Database
from ..lightning.base import LightningBackend
from ..mint.crud import LedgerCrud
@@ -40,6 +40,7 @@ class LedgerTasks(SupportsDb, SupportsBackends, SupportsEvents):
# set the quote as paid
if not quote.paid:
quote.paid = True
quote.state = MintQuoteState.paid
await self.crud.update_mint_quote(quote=quote, db=self.db)
logger.trace(f"Quote {quote} set as paid and ")
await self.events.submit(quote)

View File

@@ -15,7 +15,7 @@ import click
from click import Context
from loguru import logger
from ...core.base import Invoice, Method, TokenV3, Unit
from ...core.base import Invoice, Method, MintQuoteState, TokenV3, Unit
from ...core.helpers import sum_proofs
from ...core.json_rpc.base import JSONRPCNotficationParams
from ...core.logging import configure_logger
@@ -296,8 +296,10 @@ async def invoice(ctx: Context, amount: float, id: str, split: int, no_check: bo
except Exception:
return
logger.debug(f"Received callback for quote: {quote}")
# we need to sleep to give the callback map some time to be populated
time.sleep(0.1)
if (
quote.paid
(quote.paid or quote.state == MintQuoteState.paid.value)
and quote.request == invoice.bolt11
and msg.subId in subscription.callback_map.keys()
):
@@ -310,6 +312,9 @@ async def invoice(ctx: Context, amount: float, id: str, split: int, no_check: bo
except Exception as e:
print(f"Error during mint: {str(e)}")
return
else:
logger.debug("Quote not paid yet.")
return
# user requests an invoice
if amount and not id:

View File

@@ -1,6 +1,6 @@
import bolt11
from ...core.base import Amount, SpentState, Unit
from ...core.base import Amount, ProofSpentState, Unit
from ...core.helpers import sum_promises
from ...core.settings import settings
from ...lightning.base import (
@@ -131,12 +131,12 @@ class LightningWallet(Wallet):
if not proofs_states:
return PaymentStatus(paid=False) # "states not fount"
if all([p.state == SpentState.pending for p in proofs_states.states]):
if all([p.state == ProofSpentState.pending for p in proofs_states.states]):
return PaymentStatus(paid=None) # "pending (with check)"
if any([p.state == SpentState.spent for p in proofs_states.states]):
if any([p.state == ProofSpentState.spent for p in proofs_states.states]):
# NOTE: consider adding this check in wallet.py and mark the invoice as paid if all proofs are spent
return PaymentStatus(paid=True) # "paid (with check)"
if all([p.state == SpentState.unspent for p in proofs_states.states]):
if all([p.state == ProofSpentState.unspent for p in proofs_states.states]):
return PaymentStatus(paid=False) # "failed (with check)"
return PaymentStatus(paid=None) # "undefined state"

View File

@@ -243,3 +243,81 @@ async def m012_add_fee_to_keysets(db: Database):
# add column for storing the fee of a keyset
await conn.execute("ALTER TABLE keysets ADD COLUMN input_fee_ppk INTEGER")
await conn.execute("UPDATE keysets SET input_fee_ppk = 0")
# # async def m020_add_state_to_mint_and_melt_quotes(db: Database):
# # async with db.connect() as conn:
# # await conn.execute(
# # f"ALTER TABLE {table_with_schema(db, 'mint_quotes')} ADD COLUMN state TEXT"
# # )
# # await conn.execute(
# # f"ALTER TABLE {table_with_schema(db, 'melt_quotes')} ADD COLUMN state TEXT"
# # )
# # # get all melt and mint quotes and figure out the state to set using the `paid` column
# # # and the `paid` and `issued` column respectively
# # # mint quotes:
# # async with db.connect() as conn:
# # rows = await conn.fetchall(
# # f"SELECT * FROM {table_with_schema(db, 'mint_quotes')}"
# # )
# # for row in rows:
# # if row["issued"]:
# # state = "issued"
# # elif row["paid"]:
# # state = "paid"
# # else:
# # state = "unpaid"
# # await conn.execute(
# # f"UPDATE {table_with_schema(db, 'mint_quotes')} SET state = '{state}' WHERE quote = '{row['quote']}'"
# # )
# # # melt quotes:
# # async with db.connect() as conn:
# # rows = await conn.fetchall(
# # f"SELECT * FROM {table_with_schema(db, 'melt_quotes')}"
# # )
# # for row in rows:
# # if row["paid"]:
# # state = "paid"
# # else:
# # state = "unpaid"
# # await conn.execute(
# # f"UPDATE {table_with_schema(db, 'melt_quotes')} SET state = '{state}' WHERE quote = '{row['quote']}'"
# # )
# # add the equivalent of the above migration for the wallet here. do not use table_with_schema. use the tables and columns
# # as they are defined in the wallet db
# async def m020_add_state_to_mint_and_melt_quotes(db: Database):
# async with db.connect() as conn:
# await conn.execute("ALTER TABLE mint_quotes ADD COLUMN state TEXT")
# await conn.execute("ALTER TABLE melt_quotes ADD COLUMN state TEXT")
# # get all melt and mint quotes and figure out the state to set using the `paid` column
# # and the `paid` and `issued` column respectively
# # mint quotes:
# async with db.connect() as conn:
# rows = await conn.fetchall("SELECT * FROM mint_quotes")
# for row in rows:
# if row["issued"]:
# state = "issued"
# elif row["paid"]:
# state = "paid"
# else:
# state = "unpaid"
# await conn.execute(
# f"UPDATE mint_quotes SET state = '{state}' WHERE quote = '{row['quote']}'"
# )
# # melt quotes:
# async with db.connect() as conn:
# rows = await conn.fetchall("SELECT * FROM melt_quotes")
# for row in rows:
# if row["paid"]:
# state = "paid"
# else:
# state = "unpaid"
# await conn.execute(
# f"UPDATE melt_quotes SET state = '{state}' WHERE quote = '{row['quote']}'"
# )

View File

@@ -47,14 +47,19 @@ class SubscriptionManager:
try:
msg = JSONRPCNotification.parse_raw(message)
params = JSONRPCNotficationParams.parse_obj(msg.params)
logger.debug(f"Received notification: {msg}")
self.callback_map[params.subId](params)
except Exception as e:
logger.error(f"Error parsing notification: {e}")
return
try:
params = JSONRPCNotficationParams.parse_obj(msg.params)
logger.trace(f"Notification params: {params}")
except Exception as e:
logger.error(f"Error parsing notification params: {e}")
return
except Exception:
pass
logger.error(f"Error parsing message: {message}")
self.callback_map[params.subId](params)
return
def connect(self):
self.websocket.run_forever(ping_interval=10, ping_timeout=5)

View File

@@ -11,9 +11,10 @@ from loguru import logger
from ..core.base import (
BlindedMessage,
BlindedSignature,
MeltQuoteState,
Proof,
ProofSpentState,
ProofState,
SpentState,
Unit,
WalletKeyset,
)
@@ -390,6 +391,7 @@ class LedgerAPI(LedgerAPIDeprecated, object):
amount=amount or invoice_obj.amount_msat // 1000,
fee_reserve=ret.fee or 0,
paid=False,
state=MeltQuoteState.unpaid.value,
expiry=invoice_obj.expiry,
)
# END backwards compatibility < 0.15.0
@@ -509,11 +511,11 @@ class LedgerAPI(LedgerAPIDeprecated, object):
states: List[ProofState] = []
for spendable, pending, p in zip(ret.spendable, ret.pending, proofs):
if spendable and not pending:
states.append(ProofState(Y=p.Y, state=SpentState.unspent))
states.append(ProofState(Y=p.Y, state=ProofSpentState.unspent))
elif spendable and pending:
states.append(ProofState(Y=p.Y, state=SpentState.pending))
states.append(ProofState(Y=p.Y, state=ProofSpentState.pending))
else:
states.append(ProofState(Y=p.Y, state=SpentState.spent))
states.append(ProofState(Y=p.Y, state=ProofSpentState.spent))
ret = PostCheckStateResponse(states=states)
return ret
# END backwards compatibility < 0.15.0

View File

@@ -15,7 +15,7 @@ from ..core.base import (
DLEQWallet,
Invoice,
Proof,
SpentState,
ProofSpentState,
Unit,
WalletKeyset,
)
@@ -924,7 +924,7 @@ class Wallet(
if check_spendable:
proof_states = await self.check_proof_state(proofs)
for i, state in enumerate(proof_states.states):
if state.state == SpentState.spent:
if state.state == ProofSpentState.spent:
invalidated_proofs.append(proofs[i])
else:
invalidated_proofs = proofs

View File

@@ -10,6 +10,7 @@ from ..core.base import (
BlindedMessage,
BlindedMessage_Deprecated,
BlindedSignature,
MintQuoteState,
Proof,
WalletKeyset,
)
@@ -252,6 +253,7 @@ class LedgerAPIDeprecated(SupportsHttpxClient, SupportsMintURL):
quote=mint_response.hash,
request=mint_response.pr,
paid=False,
state=MintQuoteState.unpaid.value,
expiry=decoded_invoice.date + (decoded_invoice.expiry or 0),
)

View File

@@ -33,7 +33,7 @@ settings.tor = False
settings.wallet_unit = "sat"
settings.mint_backend_bolt11_sat = settings.mint_backend_bolt11_sat or "FakeWallet"
settings.fakewallet_brr = True
settings.fakewallet_delay_outgoing_payment = None
settings.fakewallet_delay_outgoing_payment = 0
settings.fakewallet_delay_incoming_payment = 1
settings.fakewallet_stochastic_invoice = False
assert (

View File

@@ -3,7 +3,7 @@ import httpx
import pytest
import pytest_asyncio
from cashu.core.base import SpentState
from cashu.core.base import ProofSpentState
from cashu.core.models import (
GetInfoResponse,
MintMeltMethodSetting,
@@ -403,7 +403,7 @@ async def test_api_check_state(ledger: Ledger):
response = PostCheckStateResponse.parse_obj(response.json())
assert response
assert len(response.states) == 2
assert response.states[0].state == SpentState.unspent
assert response.states[0].state == ProofSpentState.unspent
@pytest.mark.asyncio

View File

@@ -5,7 +5,7 @@ import bolt11
import pytest
import pytest_asyncio
from cashu.core.base import MeltQuote, Proof, SpentState
from cashu.core.base import MeltQuote, MeltQuoteState, Proof, ProofSpentState
from cashu.core.crypto.aes import AESCipher
from cashu.core.db import Database
from cashu.core.settings import settings
@@ -144,6 +144,7 @@ async def create_pending_melts(
checking_id=check_id,
unit="sat",
paid=False,
state=MeltQuoteState.unpaid,
amount=100,
fee_reserve=1,
)
@@ -172,7 +173,7 @@ async def test_startup_fakewallet_pending_quote_success(ledger: Ledger):
after the startup routine determines that the associated melt quote was paid."""
pending_proof, quote = await create_pending_melts(ledger)
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
assert states[0].state == SpentState.pending
assert states[0].state == ProofSpentState.pending
settings.fakewallet_payment_state = True
# run startup routinge
await ledger.startup_ledger()
@@ -185,7 +186,7 @@ async def test_startup_fakewallet_pending_quote_success(ledger: Ledger):
# expect that proofs are spent
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
assert states[0].state == SpentState.spent
assert states[0].state == ProofSpentState.spent
@pytest.mark.asyncio
@@ -198,7 +199,7 @@ async def test_startup_fakewallet_pending_quote_failure(ledger: Ledger):
"""
pending_proof, quote = await create_pending_melts(ledger)
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
assert states[0].state == SpentState.pending
assert states[0].state == ProofSpentState.pending
settings.fakewallet_payment_state = False
# run startup routinge
await ledger.startup_ledger()
@@ -211,7 +212,7 @@ async def test_startup_fakewallet_pending_quote_failure(ledger: Ledger):
# expect that proofs are unspent
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
assert states[0].state == SpentState.unspent
assert states[0].state == ProofSpentState.unspent
@pytest.mark.asyncio
@@ -219,7 +220,7 @@ async def test_startup_fakewallet_pending_quote_failure(ledger: Ledger):
async def test_startup_fakewallet_pending_quote_pending(ledger: Ledger):
pending_proof, quote = await create_pending_melts(ledger)
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
assert states[0].state == SpentState.pending
assert states[0].state == ProofSpentState.pending
settings.fakewallet_payment_state = None
# run startup routinge
await ledger.startup_ledger()
@@ -232,7 +233,7 @@ async def test_startup_fakewallet_pending_quote_pending(ledger: Ledger):
# expect that proofs are still pending
states = await ledger.db_read.get_proofs_states([pending_proof.Y])
assert states[0].state == SpentState.pending
assert states[0].state == ProofSpentState.pending
@pytest.mark.asyncio
@@ -274,7 +275,7 @@ async def test_startup_regtest_pending_quote_pending(wallet: Wallet, ledger: Led
# expect that proofs are still pending
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
assert all([s.state == SpentState.pending for s in states])
assert all([s.state == ProofSpentState.pending for s in states])
# only now settle the invoice
settle_invoice(preimage=preimage)
@@ -308,7 +309,7 @@ async def test_startup_regtest_pending_quote_success(wallet: Wallet, ledger: Led
await asyncio.sleep(SLEEP_TIME)
# expect that proofs are pending
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
assert all([s.state == SpentState.pending for s in states])
assert all([s.state == ProofSpentState.pending for s in states])
settle_invoice(preimage=preimage)
await asyncio.sleep(SLEEP_TIME)
@@ -324,7 +325,7 @@ async def test_startup_regtest_pending_quote_success(wallet: Wallet, ledger: Led
# expect that proofs are spent
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
assert all([s.state == SpentState.spent for s in states])
assert all([s.state == ProofSpentState.spent for s in states])
@pytest.mark.asyncio
@@ -359,7 +360,7 @@ async def test_startup_regtest_pending_quote_failure(wallet: Wallet, ledger: Led
# expect that proofs are pending
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
assert all([s.state == SpentState.pending for s in states])
assert all([s.state == ProofSpentState.pending for s in states])
cancel_invoice(preimage_hash=preimage_hash)
await asyncio.sleep(SLEEP_TIME)
@@ -375,4 +376,4 @@ async def test_startup_regtest_pending_quote_failure(wallet: Wallet, ledger: Led
# expect that proofs are unspent
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
assert all([s.state == SpentState.unspent for s in states])
assert all([s.state == ProofSpentState.unspent for s in states])

View File

@@ -2,7 +2,7 @@ import pytest
import respx
from httpx import Response
from cashu.core.base import Amount, MeltQuote, Unit
from cashu.core.base import Amount, MeltQuote, MeltQuoteState, Unit
from cashu.core.models import PostMeltQuoteRequest
from cashu.core.settings import settings
from cashu.lightning.blink import MINIMUM_FEE_MSAT, BlinkWallet # type: ignore
@@ -99,6 +99,7 @@ async def test_blink_pay_invoice():
amount=100,
fee_reserve=12,
paid=False,
state=MeltQuoteState.unpaid,
)
payment = await blink.pay_invoice(quote, 1000)
assert payment.ok
@@ -131,6 +132,7 @@ async def test_blink_pay_invoice_failure():
amount=100,
fee_reserve=12,
paid=False,
state=MeltQuoteState.unpaid,
)
payment = await blink.pay_invoice(quote, 1000)
assert not payment.ok

View File

@@ -3,7 +3,7 @@ import asyncio
import pytest
import pytest_asyncio
from cashu.core.base import SpentState
from cashu.core.base import ProofSpentState
from cashu.mint.ledger import Ledger
from cashu.wallet.wallet import Wallet
from tests.conftest import SERVER_ENDPOINT
@@ -63,7 +63,7 @@ async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger):
# expect that proofs are still pending
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
assert all([s.state == SpentState.pending for s in states])
assert all([s.state == ProofSpentState.pending for s in states])
# only now settle the invoice
settle_invoice(preimage=preimage)
@@ -71,7 +71,7 @@ async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger):
# expect that proofs are now spent
states = await ledger.db_read.get_proofs_states([p.Y for p in send_proofs])
assert all([s.state == SpentState.spent for s in states])
assert all([s.state == ProofSpentState.spent for s in states])
# expect that no melt quote is pending
melt_quotes = await ledger.crud.get_all_melt_quotes_from_pending_proofs(

View File

@@ -7,7 +7,7 @@ from typing import List
import pytest
import pytest_asyncio
from cashu.core.base import Proof, SpentState
from cashu.core.base import Proof, ProofSpentState
from cashu.core.crypto.secp import PrivateKey, PublicKey
from cashu.core.migrations import migrate_databases
from cashu.core.p2pk import SigFlags
@@ -80,7 +80,7 @@ async def test_p2pk(wallet1: Wallet, wallet2: Wallet):
await wallet2.redeem(send_proofs)
proof_states = await wallet2.check_proof_state(send_proofs)
assert all([p.state == SpentState.spent for p in proof_states.states])
assert all([p.state == ProofSpentState.spent for p in proof_states.states])
if not is_deprecated_api_only:
for state in proof_states.states:

View File

@@ -4,7 +4,7 @@ import bolt11
import pytest
import pytest_asyncio
from cashu.core.base import SpentState
from cashu.core.base import ProofSpentState
from cashu.mint.ledger import Ledger
from cashu.wallet.wallet import Wallet
from tests.conftest import SERVER_ENDPOINT
@@ -57,14 +57,14 @@ async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger):
await asyncio.sleep(SLEEP_TIME)
states = await wallet.check_proof_state(send_proofs)
assert all([s.state == SpentState.pending for s in states.states])
assert all([s.state == ProofSpentState.pending for s in states.states])
settle_invoice(preimage=preimage)
await asyncio.sleep(SLEEP_TIME)
states = await wallet.check_proof_state(send_proofs)
assert all([s.state == SpentState.spent for s in states.states])
assert all([s.state == ProofSpentState.spent for s in states.states])
@pytest.mark.asyncio
@@ -97,11 +97,11 @@ async def test_regtest_failed_quote(wallet: Wallet, ledger: Ledger):
await asyncio.sleep(SLEEP_TIME)
states = await wallet.check_proof_state(send_proofs)
assert all([s.state == SpentState.pending for s in states.states])
assert all([s.state == ProofSpentState.pending for s in states.states])
cancel_invoice(preimage_hash=preimage_hash)
await asyncio.sleep(SLEEP_TIME)
states = await wallet.check_proof_state(send_proofs)
assert all([s.state == SpentState.unspent for s in states.states])
assert all([s.state == ProofSpentState.unspent for s in states.states])

View File

@@ -3,7 +3,7 @@ import asyncio
import pytest
import pytest_asyncio
from cashu.core.base import Method, ProofState
from cashu.core.base import Method, MintQuoteState, ProofSpentState, ProofState
from cashu.core.json_rpc.base import JSONRPCNotficationParams
from cashu.core.nuts import WEBSOCKETS_NUT
from cashu.core.settings import settings
@@ -51,20 +51,17 @@ async def test_wallet_subscription_mint(wallet: Wallet):
wait = settings.fakewallet_delay_incoming_payment or 2
await asyncio.sleep(wait + 2)
# TODO: check for pending and paid states according to: https://github.com/cashubtc/nuts/pull/136
# TODO: we have three messages here, but the value "paid" only changes once
# the mint sends an update when the quote is pending but the API does not express that yet
# first we expect the issued=False state to arrive
assert triggered
assert len(msg_stack) == 3
assert msg_stack[0].payload["paid"] is False
assert msg_stack[0].payload["state"] == MintQuoteState.unpaid.value
assert msg_stack[1].payload["paid"] is True
assert msg_stack[1].payload["state"] == MintQuoteState.paid.value
assert msg_stack[2].payload["paid"] is True
assert msg_stack[2].payload["state"] == MintQuoteState.issued.value
@pytest.mark.asyncio
@@ -103,16 +100,16 @@ async def test_wallet_subscription_swap(wallet: Wallet):
pending_stack = msg_stack[:n_subscriptions]
for msg in pending_stack:
proof_state = ProofState.parse_obj(msg.payload)
assert proof_state.state.value == "UNSPENT"
assert proof_state.state == ProofSpentState.unspent
# the second one is the PENDING state
spent_stack = msg_stack[n_subscriptions : n_subscriptions * 2]
for msg in spent_stack:
proof_state = ProofState.parse_obj(msg.payload)
assert proof_state.state.value == "PENDING"
assert proof_state.state == ProofSpentState.pending
# the third one is the SPENT state
spent_stack = msg_stack[n_subscriptions * 2 :]
for msg in spent_stack:
proof_state = ProofState.parse_obj(msg.payload)
assert proof_state.state.value == "SPENT"
assert proof_state.state == ProofSpentState.spent