mirror of
https://github.com/aljazceru/nutshell.git
synced 2026-01-06 02:14:21 +01:00
Mint: add websockets for quote updates (#413)
* add websockets for quote updates * add test (not working) * wip: emit events to everyone * wip: emit events to everyone * wip, lots of things broken but invoice callback works * wip * add wip files * tests almost passing * add task * refactor nut constants * startup fix * works with old mints * wip cli * fix mypy * remove automatic invoice test now with websockets * remove comment * better logging * send back response * add rate limiter to websocket * add rate limiter to subscriptions * refactor websocket ratelimit * websocket tests * subscription kinds * doesnt start * remove circular import * update * fix mypy * move test file in test because it fails if it runs later... dunno why * adjust websocket NUT-06 settings * local import and small fix * disable websockets in CLI if "no_check" is selected * move subscription test to where it was * check proof state with callback, add tests * tests: run mint fixture per module instead of per session * subscription command name fix * test per session again * update test race conditions * fix tests * clean up * tmp * fix db issues and remove cached secrets * fix tests * blindly try pipeline * remove comments * comments
This commit is contained in:
@@ -52,16 +52,20 @@ from ..lightning.base import (
|
||||
)
|
||||
from ..mint.crud import LedgerCrudSqlite
|
||||
from .conditions import LedgerSpendingConditions
|
||||
from .db.read import DbReadHelper
|
||||
from .db.write import DbWriteHelper
|
||||
from .events.events import LedgerEventManager
|
||||
from .features import LedgerFeatures
|
||||
from .tasks import LedgerTasks
|
||||
from .verification import LedgerVerification
|
||||
|
||||
|
||||
class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFeatures):
|
||||
backends: Mapping[Method, Mapping[Unit, LightningBackend]] = {}
|
||||
locks: Dict[str, asyncio.Lock] = {} # holds multiprocessing locks
|
||||
proofs_pending_lock: asyncio.Lock = (
|
||||
asyncio.Lock()
|
||||
) # holds locks for proofs_pending database
|
||||
keysets: Dict[str, MintKeyset] = {}
|
||||
events = LedgerEventManager()
|
||||
db_read: DbReadHelper
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -92,17 +96,17 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
self.crud = crud
|
||||
self.backends = backends
|
||||
self.pubkey = derive_pubkey(self.seed)
|
||||
self.spent_proofs: Dict[str, Proof] = {}
|
||||
self.db_read = DbReadHelper(self.db, self.crud)
|
||||
self.db_write = DbWriteHelper(self.db, self.crud, self.events)
|
||||
|
||||
# ------- STARTUP -------
|
||||
|
||||
async def startup_ledger(self):
|
||||
await self._startup_ledger()
|
||||
await self._check_pending_proofs_and_melt_quotes()
|
||||
await self.dispatch_listeners()
|
||||
|
||||
async def _startup_ledger(self):
|
||||
if settings.mint_cache_secrets:
|
||||
await self.load_used_proofs()
|
||||
await self.init_keysets()
|
||||
|
||||
for derivation_path in settings.mint_derivation_path_list:
|
||||
@@ -158,12 +162,12 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
proofs=pending_proofs, quote_id=quote.quote
|
||||
)
|
||||
# unset pending
|
||||
await self._unset_proofs_pending(pending_proofs)
|
||||
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._unset_proofs_pending(pending_proofs)
|
||||
await self.db_write._unset_proofs_pending(pending_proofs, spent=False)
|
||||
elif payment.pending:
|
||||
logger.info(f"Melt quote {quote.quote} state: pending")
|
||||
pass
|
||||
@@ -291,13 +295,15 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
proofs (List[Proof]): Proofs to add to known secret table.
|
||||
conn: (Optional[Connection], optional): Database connection to reuse. Will create a new one if not given. Defaults to None.
|
||||
"""
|
||||
self.spent_proofs.update({p.Y: p for p in proofs})
|
||||
async with get_db_connection(self.db, conn) as conn:
|
||||
# store in db
|
||||
for p in proofs:
|
||||
await self.crud.invalidate_proof(
|
||||
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)
|
||||
)
|
||||
|
||||
async def _generate_change_promises(
|
||||
self,
|
||||
@@ -426,10 +432,9 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
created_time=int(time.time()),
|
||||
expiry=expiry,
|
||||
)
|
||||
await self.crud.store_mint_quote(
|
||||
quote=quote,
|
||||
db=self.db,
|
||||
)
|
||||
await self.crud.store_mint_quote(quote=quote, db=self.db)
|
||||
await self.events.submit(quote)
|
||||
|
||||
return quote
|
||||
|
||||
async def get_mint_quote(self, quote_id: str) -> MintQuote:
|
||||
@@ -462,6 +467,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
quote.paid = True
|
||||
quote.paid_time = int(time.time())
|
||||
await self.crud.update_mint_quote(quote=quote, db=self.db)
|
||||
await self.events.submit(quote)
|
||||
|
||||
return quote
|
||||
|
||||
@@ -510,12 +516,16 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
if quote.expiry and quote.expiry > int(time.time()):
|
||||
raise TransactionError("quote expired")
|
||||
|
||||
promises = await self._generate_promises(outputs)
|
||||
logger.trace("generated promises")
|
||||
|
||||
logger.trace(f"crud: setting quote {quote_id} as issued")
|
||||
quote.issued = True
|
||||
await self.crud.update_mint_quote(quote=quote, db=self.db)
|
||||
|
||||
promises = await self._generate_promises(outputs)
|
||||
logger.trace("generated promises")
|
||||
|
||||
# submit the quote update to the event manager
|
||||
await self.events.submit(quote)
|
||||
|
||||
del self.locks[quote_id]
|
||||
return promises
|
||||
|
||||
@@ -605,9 +615,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
# check if there is a mint quote with the same payment request
|
||||
# so that we would be able to handle the transaction internally
|
||||
# and therefore respond with internal transaction fees (0 for now)
|
||||
mint_quote = await self.crud.get_mint_quote_by_request(
|
||||
request=request, db=self.db
|
||||
)
|
||||
mint_quote = await self.crud.get_mint_quote(request=request, db=self.db)
|
||||
if mint_quote:
|
||||
payment_quote = self.create_internal_melt_quote(mint_quote, melt_quote)
|
||||
|
||||
@@ -655,6 +663,8 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
expiry=expiry,
|
||||
)
|
||||
await self.crud.store_melt_quote(quote=quote, db=self.db)
|
||||
await self.events.submit(quote)
|
||||
|
||||
return PostMeltQuoteResponse(
|
||||
quote=quote.quote,
|
||||
amount=quote.amount,
|
||||
@@ -689,7 +699,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
|
||||
# we only check the state with the backend if there is no associated internal
|
||||
# mint quote for this melt quote
|
||||
mint_quote = await self.crud.get_mint_quote_by_request(
|
||||
mint_quote = await self.crud.get_mint_quote(
|
||||
request=melt_quote.request, db=self.db
|
||||
)
|
||||
|
||||
@@ -710,6 +720,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
melt_quote.proof = status.preimage
|
||||
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)
|
||||
|
||||
return melt_quote
|
||||
|
||||
@@ -733,7 +744,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
"""
|
||||
# first we check if there is a mint quote with the same payment request
|
||||
# so that we can handle the transaction internally without the backend
|
||||
mint_quote = await self.crud.get_mint_quote_by_request(
|
||||
mint_quote = await self.crud.get_mint_quote(
|
||||
request=melt_quote.request, db=self.db
|
||||
)
|
||||
if not mint_quote:
|
||||
@@ -774,10 +785,13 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
mint_quote.paid = True
|
||||
mint_quote.paid_time = melt_quote.paid_time
|
||||
|
||||
async with self.db.connect() as conn:
|
||||
async with get_db_connection(self.db) as conn:
|
||||
await self.crud.update_melt_quote(quote=melt_quote, db=self.db, conn=conn)
|
||||
await self.crud.update_mint_quote(quote=mint_quote, db=self.db, conn=conn)
|
||||
|
||||
await self.events.submit(melt_quote)
|
||||
await self.events.submit(mint_quote)
|
||||
|
||||
return melt_quote
|
||||
|
||||
async def melt(
|
||||
@@ -847,7 +861,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
await self.verify_inputs_and_outputs(proofs=proofs)
|
||||
|
||||
# set proofs to pending to avoid race conditions
|
||||
await self._set_proofs_pending(proofs, quote_id=melt_quote.quote)
|
||||
await self.db_write._set_proofs_pending(proofs, quote_id=melt_quote.quote)
|
||||
try:
|
||||
# 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)
|
||||
@@ -875,6 +889,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
melt_quote.paid = True
|
||||
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)
|
||||
|
||||
# melt successful, invalidate proofs
|
||||
await self._invalidate_proofs(proofs=proofs, quote_id=melt_quote.quote)
|
||||
@@ -894,7 +909,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
raise e
|
||||
finally:
|
||||
# delete proofs from pending list
|
||||
await self._unset_proofs_pending(proofs)
|
||||
await self.db_write._unset_proofs_pending(proofs)
|
||||
|
||||
return melt_quote.proof or "", return_promises
|
||||
|
||||
@@ -928,7 +943,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
# verify spending inputs, outputs, and spending conditions
|
||||
await self.verify_inputs_and_outputs(proofs=proofs, outputs=outputs)
|
||||
|
||||
await self._set_proofs_pending(proofs)
|
||||
await self.db_write._set_proofs_pending(proofs)
|
||||
try:
|
||||
# Mark proofs as used and prepare new promises
|
||||
async with get_db_connection(self.db) as conn:
|
||||
@@ -941,7 +956,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
raise e
|
||||
finally:
|
||||
# delete proofs from pending list
|
||||
await self._unset_proofs_pending(proofs)
|
||||
await self.db_write._unset_proofs_pending(proofs)
|
||||
|
||||
logger.trace("split successful")
|
||||
return promises
|
||||
@@ -951,7 +966,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
) -> Tuple[List[BlindedMessage], List[BlindedSignature]]:
|
||||
signatures: List[BlindedSignature] = []
|
||||
return_outputs: List[BlindedMessage] = []
|
||||
async with self.db.connect() as conn:
|
||||
async with get_db_connection(self.db) as conn:
|
||||
for output in outputs:
|
||||
logger.trace(f"looking for promise: {output}")
|
||||
promise = await self.crud.get_promise(
|
||||
@@ -1030,105 +1045,3 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
|
||||
)
|
||||
signatures.append(signature)
|
||||
return signatures
|
||||
|
||||
# ------- PROOFS -------
|
||||
|
||||
async def load_used_proofs(self) -> None:
|
||||
"""Load all used proofs from database."""
|
||||
if not settings.mint_cache_secrets:
|
||||
raise Exception("MINT_CACHE_SECRETS must be set to TRUE")
|
||||
logger.debug("Loading used proofs into memory")
|
||||
spent_proofs_list = await self.crud.get_spent_proofs(db=self.db) or []
|
||||
logger.debug(f"Loaded {len(spent_proofs_list)} used proofs")
|
||||
self.spent_proofs = {p.Y: p for p in spent_proofs_list}
|
||||
|
||||
async def check_proofs_state(self, Ys: List[str]) -> List[ProofState]:
|
||||
"""Checks if provided proofs are spend or are pending.
|
||||
Used by wallets to check if their proofs have been redeemed by a receiver or they are still in-flight in a transaction.
|
||||
|
||||
Returns two lists that are in the same order as the provided proofs. Wallet must match the list
|
||||
to the proofs they have provided in order to figure out which proof is spendable or pending
|
||||
and which isn't.
|
||||
|
||||
Args:
|
||||
Ys (List[str]): List of Y's of proofs to check
|
||||
|
||||
Returns:
|
||||
List[bool]: List of which proof is still spendable (True if still spendable, else False)
|
||||
List[bool]: List of which proof are pending (True if pending, else False)
|
||||
"""
|
||||
states: List[ProofState] = []
|
||||
proofs_spent = await self._get_proofs_spent(Ys)
|
||||
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))
|
||||
elif Y not in proofs_spent and Y in proofs_pending:
|
||||
states.append(ProofState(Y=Y, state=SpentState.pending))
|
||||
else:
|
||||
states.append(
|
||||
ProofState(
|
||||
Y=Y,
|
||||
state=SpentState.spent,
|
||||
witness=proofs_spent[Y].witness,
|
||||
)
|
||||
)
|
||||
return states
|
||||
|
||||
async def _set_proofs_pending(
|
||||
self, proofs: List[Proof], quote_id: Optional[str] = None
|
||||
) -> None:
|
||||
"""If none of the proofs is in the pending table (_validate_proofs_pending), adds proofs to
|
||||
the list of pending proofs or removes them. Used as a mutex for proofs.
|
||||
|
||||
Args:
|
||||
proofs (List[Proof]): Proofs to add to pending table.
|
||||
quote_id (Optional[str]): Melt quote ID. If it is not set, we assume the pending tokens to be from a swap.
|
||||
|
||||
Raises:
|
||||
Exception: At least one proof already in pending table.
|
||||
"""
|
||||
# first we check whether these proofs are pending already
|
||||
async with self.proofs_pending_lock:
|
||||
async with self.db.connect() as conn:
|
||||
await self._validate_proofs_pending(proofs, conn)
|
||||
try:
|
||||
for p in proofs:
|
||||
await self.crud.set_proof_pending(
|
||||
proof=p, db=self.db, quote_id=quote_id, conn=conn
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to set proofs pending: {e}")
|
||||
raise TransactionError("Failed to set proofs pending.")
|
||||
|
||||
async def _unset_proofs_pending(self, proofs: List[Proof]) -> None:
|
||||
"""Deletes proofs from pending table.
|
||||
|
||||
Args:
|
||||
proofs (List[Proof]): Proofs to delete.
|
||||
"""
|
||||
async with self.proofs_pending_lock:
|
||||
async with self.db.connect() as conn:
|
||||
for p in proofs:
|
||||
await self.crud.unset_proof_pending(proof=p, db=self.db, conn=conn)
|
||||
|
||||
async def _validate_proofs_pending(
|
||||
self, proofs: List[Proof], conn: Optional[Connection] = None
|
||||
) -> None:
|
||||
"""Checks if any of the provided proofs is in the pending proofs table.
|
||||
|
||||
Args:
|
||||
proofs (List[Proof]): Proofs to check.
|
||||
|
||||
Raises:
|
||||
Exception: At least one of the proofs is in the pending table.
|
||||
"""
|
||||
if not (
|
||||
len(
|
||||
await self.crud.get_proofs_pending(
|
||||
Ys=[p.Y for p in proofs], db=self.db, conn=conn
|
||||
)
|
||||
)
|
||||
== 0
|
||||
):
|
||||
raise TransactionError("proofs are pending.")
|
||||
|
||||
Reference in New Issue
Block a user