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:
callebtc
2024-06-25 19:20:03 +02:00
committed by GitHub
parent 75987beaf1
commit e846acf946
43 changed files with 1701 additions and 519 deletions

View File

@@ -9,15 +9,17 @@ from itertools import groupby, islice
from operator import itemgetter
from os import listdir
from os.path import isdir, join
from typing import Optional
from typing import Optional, Union
import click
from click import Context
from loguru import logger
from ...core.base import Invoice, TokenV3, Unit
from ...core.base import Invoice, Method, TokenV3, Unit
from ...core.helpers import sum_proofs
from ...core.json_rpc.base import JSONRPCNotficationParams
from ...core.logging import configure_logger
from ...core.models import PostMintQuoteResponse
from ...core.settings import settings
from ...nostr.client.client import NostrClient
from ...tor.tor import TorProxy
@@ -44,6 +46,7 @@ from ..helpers import (
send,
)
from ..nostr import receive_nostr, send_nostr
from ..subscriptions import SubscriptionManager
class NaturalOrderGroup(click.Group):
@@ -272,9 +275,54 @@ async def invoice(ctx: Context, amount: float, id: str, split: int, no_check: bo
f"Requesting split with {n_splits} * {wallet.unit.str(split)} tokens."
)
paid = False
invoice_nonlocal: Union[None, Invoice] = None
subscription_nonlocal: Union[None, SubscriptionManager] = None
def mint_invoice_callback(msg: JSONRPCNotficationParams):
nonlocal \
ctx, \
wallet, \
amount, \
optional_split, \
paid, \
invoice_nonlocal, \
subscription_nonlocal
logger.trace(f"Received callback: {msg}")
if paid:
return
try:
quote = PostMintQuoteResponse.parse_obj(msg.payload)
except Exception:
return
logger.debug(f"Received callback for quote: {quote}")
if (
quote.paid
and quote.request == invoice.bolt11
and msg.subId in subscription.callback_map.keys()
):
try:
asyncio.run(
wallet.mint(int(amount), split=optional_split, id=invoice.id)
)
# set paid so we won't react to any more callbacks
paid = True
except Exception as e:
print(f"Error during mint: {str(e)}")
return
# user requests an invoice
if amount and not id:
invoice = await wallet.request_mint(amount)
mint_supports_websockets = wallet.mint_info.supports_websocket_mint_quote(
Method["bolt11"], wallet.unit
)
if mint_supports_websockets and not no_check:
invoice, subscription = await wallet.request_mint_with_callback(
amount, callback=mint_invoice_callback
)
invoice_nonlocal, subscription_nonlocal = invoice, subscription
else:
invoice = await wallet.request_mint(amount)
if invoice.bolt11:
print("")
print(f"Pay invoice to mint {wallet.unit.str(amount)}:")
@@ -287,37 +335,48 @@ async def invoice(ctx: Context, amount: float, id: str, split: int, no_check: bo
)
if no_check:
return
check_until = time.time() + 5 * 60 # check for five minutes
print("")
print(
"Checking invoice ...",
end="",
flush=True,
)
paid = False
while time.time() < check_until and not paid:
time.sleep(3)
try:
await wallet.mint(amount, split=optional_split, id=invoice.id)
paid = True
print(" Invoice paid.")
except Exception as e:
# TODO: user error codes!
if "not paid" in str(e):
print(".", end="", flush=True)
continue
else:
print(f"Error: {str(e)}")
if not paid:
print("\n")
print(
"Invoice is not paid yet, stopping check. Use the command above to"
" recheck after the invoice has been paid."
)
if mint_supports_websockets:
while not paid:
await asyncio.sleep(0.1)
# user paid invoice and want to check it
# we still check manually every 10 seconds
check_until = time.time() + 5 * 60 # check for five minutes
while time.time() < check_until and not paid:
await asyncio.sleep(5)
try:
await wallet.mint(amount, split=optional_split, id=invoice.id)
paid = True
except Exception as e:
# TODO: user error codes!
if "not paid" in str(e):
print(".", end="", flush=True)
continue
else:
print(f"Error: {str(e)}")
if not paid:
print("\n")
print(
"Invoice is not paid yet, stopping check. Use the command above to"
" recheck after the invoice has been paid."
)
# user paid invoice before and wants to check the quote id
elif amount and id:
await wallet.mint(amount, split=optional_split, id=id)
# close open subscriptions so we can exit
try:
subscription.close()
except Exception:
pass
print(" Invoice paid.")
print("")
await print_balance(ctx)
return
@@ -434,7 +493,6 @@ async def balance(ctx: Context, verbose):
)
@click.option(
"--legacy",
"-l",
default=False,
is_flag=True,
help="Print legacy token without mint information.",