diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fcae1ade..d035af3a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,6 @@ jobs: os: [ubuntu-latest] python-version: ["3.10"] poetry-version: ["1.7.1"] - mint-cache-secrets: ["false", "true"] mint-only-deprecated: ["false", "true"] mint-database: ["./test_data/test_mint", "postgres://cashu:cashu@localhost:5432/cashu"] backend-wallet-class: ["FakeWallet"] @@ -24,7 +23,6 @@ jobs: os: ${{ matrix.os }} python-version: ${{ matrix.python-version }} poetry-version: ${{ matrix.poetry-version }} - mint-cache-secrets: ${{ matrix.mint-cache-secrets }} mint-only-deprecated: ${{ matrix.mint-only-deprecated }} mint-database: ${{ matrix.mint-database }} regtest: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 42069702..40ecb60f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,16 +15,13 @@ on: os: default: "ubuntu-latest" type: string - mint-cache-secrets: - default: "false" - type: string mint-only-deprecated: default: "false" type: string jobs: poetry: - name: Run (mint-cache-secrets ${{ inputs.mint-cache-secrets }}, mint-only-deprecated ${{ inputs.mint-only-deprecated }}, mint-database ${{ inputs.mint-database }}) + name: Run (db ${{ inputs.mint-database }}, deprecated api ${{ inputs.mint-only-deprecated }}) runs-on: ${{ inputs.os }} services: postgres: @@ -54,7 +51,6 @@ jobs: MINT_HOST: localhost MINT_PORT: 3337 MINT_TEST_DATABASE: ${{ inputs.mint-database }} - MINT_CACHE_SECRETS: ${{ inputs.mint-cache-secrets }} DEBUG_MINT_ONLY_DEPRECATED: ${{ inputs.mint-only-deprecated }} TOR: false run: | diff --git a/cashu/core/base.py b/cashu/core/base.py index 88c83552..699cacea 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -7,8 +7,11 @@ from typing import Dict, List, Optional, Union from loguru import logger -from pydantic import BaseModel +from pydantic import BaseModel, root_validator +from cashu.core.json_rpc.base import JSONRPCSubscriptionKinds + +from ..mint.events.event_model import LedgerEvent from .crypto.aes import AESCipher from .crypto.b_dhke import hash_to_curve from .crypto.keys import ( @@ -54,11 +57,27 @@ def __str__(self): return self.name -class ProofState(BaseModel): +class ProofState(LedgerEvent): Y: str state: SpentState 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: + raise ValueError('Witness can only be set if the spent state is "SPENT"') + return values + + @property + def identifier(self) -> str: + """Implementation of the abstract method from LedgerEventManager""" + return self.Y + + @property + def kind(self) -> JSONRPCSubscriptionKinds: + return JSONRPCSubscriptionKinds.PROOF_STATE + class HTLCWitness(BaseModel): preimage: Optional[str] = None @@ -249,7 +268,7 @@ class Invoice(BaseModel): time_paid: Union[None, str, int, float] = "" -class MeltQuote(BaseModel): +class MeltQuote(LedgerEvent): quote: str method: str request: str @@ -290,8 +309,17 @@ def from_row(cls, row: Row): proof=row["proof"], ) + @property + def identifier(self) -> str: + """Implementation of the abstract method from LedgerEventManager""" + return self.quote + + @property + def kind(self) -> JSONRPCSubscriptionKinds: + return JSONRPCSubscriptionKinds.BOLT11_MELT_QUOTE + -class MintQuote(BaseModel): +class MintQuote(LedgerEvent): quote: str method: str request: str @@ -329,6 +357,15 @@ def from_row(cls, row: Row): paid_time=paid_time, ) + @property + def identifier(self) -> str: + """Implementation of the abstract method from LedgerEventManager""" + return self.quote + + @property + def kind(self) -> JSONRPCSubscriptionKinds: + return JSONRPCSubscriptionKinds.BOLT11_MINT_QUOTE + # ------- KEYSETS ------- diff --git a/cashu/core/json_rpc/base.py b/cashu/core/json_rpc/base.py new file mode 100644 index 00000000..b293b5af --- /dev/null +++ b/cashu/core/json_rpc/base.py @@ -0,0 +1,86 @@ +from enum import Enum +from typing import List + +from pydantic import BaseModel, Field + +from ..settings import settings + + +class JSONRPCRequest(BaseModel): + jsonrpc: str = "2.0" + id: int + method: str + params: dict + + +class JSONRPCResponse(BaseModel): + jsonrpc: str = "2.0" + result: dict + id: int + + +class JSONRPCNotification(BaseModel): + jsonrpc: str = "2.0" + method: str + params: dict + + +class JSONRPCErrorCode(Enum): + PARSE_ERROR = -32700 + INVALID_REQUEST = -32600 + METHOD_NOT_FOUND = -32601 + INVALID_PARAMS = -32602 + INTERNAL_ERROR = -32603 + SERVER_ERROR = -32000 + APPLICATION_ERROR = -32099 + SYSTEM_ERROR = -32098 + TRANSPORT_ERROR = -32097 + + +class JSONRPCError(BaseModel): + code: JSONRPCErrorCode + message: str + + +class JSONRPCErrorResponse(BaseModel): + jsonrpc: str = "2.0" + error: JSONRPCError + id: int + + +# Cashu Websocket protocol + + +class JSONRPCMethods(Enum): + SUBSCRIBE = "subscribe" + UNSUBSCRIBE = "unsubscribe" + + +class JSONRPCSubscriptionKinds(Enum): + BOLT11_MINT_QUOTE = "bolt11_mint_quote" + BOLT11_MELT_QUOTE = "bolt11_melt_quote" + PROOF_STATE = "proof_state" + + +class JSONRPCStatus(Enum): + OK = "OK" + + +class JSONRPCSubscribeParams(BaseModel): + kind: JSONRPCSubscriptionKinds + filters: List[str] = Field(..., max_length=settings.mint_max_request_length) + subId: str + + +class JSONRPCUnsubscribeParams(BaseModel): + subId: str + + +class JSONRPCNotficationParams(BaseModel): + subId: str + payload: dict + + +class JSONRRPCSubscribeResponse(BaseModel): + status: JSONRPCStatus + subId: str diff --git a/cashu/core/models.py b/cashu/core/models.py index 7777a8fd..133c1514 100644 --- a/cashu/core/models.py +++ b/cashu/core/models.py @@ -33,6 +33,9 @@ class GetInfoResponse(BaseModel): motd: Optional[str] = None nuts: Optional[Dict[int, Any]] = None + def supports(self, nut: int) -> Optional[bool]: + return nut in self.nuts if self.nuts else None + class Nut15MppSupport(BaseModel): method: str diff --git a/cashu/core/nuts.py b/cashu/core/nuts.py new file mode 100644 index 00000000..6c0b5287 --- /dev/null +++ b/cashu/core/nuts.py @@ -0,0 +1,13 @@ +SWAP_NUT = 3 +MINT_NUT = 4 +MELT_NUT = 5 +INFO_NUT = 6 +STATE_NUT = 7 +FEE_RETURN_NUT = 8 +RESTORE_NUT = 9 +SCRIPT_NUT = 10 +P2PK_NUT = 11 +DLEQ_NUT = 12 +DETERMINSTIC_SECRETS_NUT = 13 +MPP_NUT = 15 +WEBSOCKETS_NUT = 17 diff --git a/cashu/core/settings.py b/cashu/core/settings.py index 205c3099..b4c3269c 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -120,14 +120,20 @@ class MintLimits(MintSettings): title="Maximum mint balance", description="Maximum mint balance.", ) + mint_websocket_read_timeout: int = Field( + default=10 * 60, + gt=0, + title="Websocket read timeout", + description="Timeout for reading from a websocket.", + ) class FakeWalletSettings(MintSettings): fakewallet_brr: bool = Field(default=True) - fakewallet_delay_payment: bool = Field(default=False) + fakewallet_delay_outgoing_payment: Optional[int] = Field(default=3) + fakewallet_delay_incoming_payment: Optional[int] = Field(default=3) fakewallet_stochastic_invoice: bool = Field(default=False) fakewallet_payment_state: Optional[bool] = Field(default=None) - mint_cache_secrets: bool = Field(default=True) class MintInformation(CashuSettings): diff --git a/cashu/lightning/base.py b/cashu/lightning/base.py index afde6786..06fbe975 100644 --- a/cashu/lightning/base.py +++ b/cashu/lightning/base.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Coroutine, Optional, Union +from typing import AsyncGenerator, Coroutine, Optional, Union from pydantic import BaseModel @@ -68,6 +68,7 @@ def __str__(self) -> str: class LightningBackend(ABC): supports_mpp: bool = False + supports_incoming_payment_stream: bool = False supported_units: set[Unit] unit: Unit @@ -124,9 +125,9 @@ async def get_payment_quote( # ) -> InvoiceQuoteResponse: # pass - # @abstractmethod - # def paid_invoices_stream(self) -> AsyncGenerator[str, None]: - # pass + @abstractmethod + def paid_invoices_stream(self) -> AsyncGenerator[str, None]: + pass class Unsupported(Exception): diff --git a/cashu/lightning/blink.py b/cashu/lightning/blink.py index 42f6016a..2320baad 100644 --- a/cashu/lightning/blink.py +++ b/cashu/lightning/blink.py @@ -1,8 +1,7 @@ # type: ignore -import asyncio import json import math -from typing import Dict, Optional, Union +from typing import AsyncGenerator, Dict, Optional, Union import bolt11 import httpx @@ -454,10 +453,5 @@ async def get_payment_quote( amount=amount.to(self.unit, round="up"), ) - -async def main(): - pass - - -if __name__ == "__main__": - asyncio.run(main()) + async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: + raise NotImplementedError("paid_invoices_stream not implemented") diff --git a/cashu/lightning/corelightningrest.py b/cashu/lightning/corelightningrest.py index ccc06772..ce62a51f 100644 --- a/cashu/lightning/corelightningrest.py +++ b/cashu/lightning/corelightningrest.py @@ -29,6 +29,7 @@ class CoreLightningRestWallet(LightningBackend): supported_units = set([Unit.sat, Unit.msat]) unit = Unit.sat + supports_incoming_payment_stream: bool = True def __init__(self, unit: Unit = Unit.sat, **kwargs): self.assert_unit_supported(unit) diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index 9ad5682c..7c50306c 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -4,7 +4,7 @@ import random from datetime import datetime from os import urandom -from typing import AsyncGenerator, Dict, Optional, Set +from typing import AsyncGenerator, Dict, List, Optional from bolt11 import ( Bolt11, @@ -31,9 +31,11 @@ class FakeWallet(LightningBackend): fake_btc_price = 1e8 / 1337 - queue: asyncio.Queue[Bolt11] = asyncio.Queue(0) + paid_invoices_queue: asyncio.Queue[Bolt11] = asyncio.Queue(0) payment_secrets: Dict[str, str] = dict() - paid_invoices: Set[str] = set() + created_invoices: List[Bolt11] = [] + paid_invoices_outgoing: List[Bolt11] = [] + paid_invoices_incoming: List[Bolt11] = [] secret: str = "FAKEWALLET SECRET" privkey: str = hashlib.pbkdf2_hmac( "sha256", @@ -46,6 +48,8 @@ class FakeWallet(LightningBackend): supported_units = set([Unit.sat, Unit.msat, Unit.usd]) unit = Unit.sat + supports_incoming_payment_stream: bool = True + def __init__(self, unit: Unit = Unit.sat, **kwargs): self.assert_unit_supported(unit) self.unit = unit @@ -53,6 +57,23 @@ def __init__(self, unit: Unit = Unit.sat, **kwargs): async def status(self) -> StatusResponse: return StatusResponse(error_message=None, balance=1337) + async def mark_invoice_paid(self, invoice: Bolt11) -> None: + if settings.fakewallet_delay_incoming_payment: + await asyncio.sleep(settings.fakewallet_delay_incoming_payment) + self.paid_invoices_incoming.append(invoice) + await self.paid_invoices_queue.put(invoice) + + def create_dummy_bolt11(self, payment_hash: str) -> Bolt11: + tags = Tags() + tags.add(TagChar.payment_hash, payment_hash) + tags.add(TagChar.payment_secret, urandom(32).hex()) + return Bolt11( + currency="bc", + amount_msat=MilliSatoshi(1337), + date=int(datetime.now().timestamp()), + tags=tags, + ) + async def create_invoice( self, amount: Amount, @@ -106,8 +127,16 @@ async def create_invoice( tags=tags, ) + if bolt11 not in self.created_invoices: + self.created_invoices.append(bolt11) + else: + raise ValueError("Invoice already created") + payment_request = encode(bolt11, self.privkey) + if settings.fakewallet_brr: + asyncio.create_task(self.mark_invoice_paid(bolt11)) + return InvoiceResponse( ok=True, checking_id=payment_hash, payment_request=payment_request ) @@ -115,12 +144,15 @@ async def create_invoice( async def pay_invoice(self, quote: MeltQuote, fee_limit: int) -> PaymentResponse: invoice = decode(quote.request) - if settings.fakewallet_delay_payment: - await asyncio.sleep(5) + if settings.fakewallet_delay_outgoing_payment: + await asyncio.sleep(settings.fakewallet_delay_outgoing_payment) if invoice.payment_hash in self.payment_secrets or settings.fakewallet_brr: - await self.queue.put(invoice) - self.paid_invoices.add(invoice.payment_hash) + if invoice not in self.paid_invoices_outgoing: + self.paid_invoices_outgoing.append(invoice) + else: + raise ValueError("Invoice already paid") + return PaymentResponse( ok=True, checking_id=invoice.payment_hash, @@ -133,26 +165,24 @@ async def pay_invoice(self, quote: MeltQuote, fee_limit: int) -> PaymentResponse ) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: - if settings.fakewallet_stochastic_invoice: - paid = random.random() > 0.7 - return PaymentStatus(paid=paid) - paid = checking_id in self.paid_invoices or settings.fakewallet_brr - return PaymentStatus(paid=paid or None) + paid = False + if settings.fakewallet_brr or ( + settings.fakewallet_stochastic_invoice and random.random() > 0.7 + ): + paid = True + + # invoice is paid but not in paid_invoices_incoming yet + # so we add it to the paid_invoices_queue + # if paid and invoice not in self.paid_invoices_incoming: + if paid: + await self.paid_invoices_queue.put( + self.create_dummy_bolt11(payment_hash=checking_id) + ) + return PaymentStatus(paid=paid) async def get_payment_status(self, _: str) -> PaymentStatus: return PaymentStatus(paid=settings.fakewallet_payment_state) - async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: - while True: - value: Bolt11 = await self.queue.get() - yield value.payment_hash - - # async def get_invoice_quote(self, bolt11: str) -> InvoiceQuoteResponse: - # invoice_obj = decode(bolt11) - # assert invoice_obj.amount_msat, "invoice has no amount." - # amount = invoice_obj.amount_msat - # return InvoiceQuoteResponse(checking_id="", amount=amount) - async def get_payment_quote( self, melt_quote: PostMeltQuoteRequest ) -> PaymentQuoteResponse: @@ -176,3 +206,8 @@ async def get_payment_quote( fee=fees.to(self.unit, round="up"), amount=amount.to(self.unit, round="up"), ) + + async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: + while True: + value: Bolt11 = await self.paid_invoices_queue.get() + yield value.payment_hash diff --git a/cashu/lightning/lnbits.py b/cashu/lightning/lnbits.py index 721e7046..55cdd2bf 100644 --- a/cashu/lightning/lnbits.py +++ b/cashu/lightning/lnbits.py @@ -1,5 +1,5 @@ # type: ignore -from typing import Optional +from typing import AsyncGenerator, Optional import httpx from bolt11 import ( @@ -182,3 +182,6 @@ async def get_payment_quote( fee=fees.to(self.unit, round="up"), amount=amount.to(self.unit, round="up"), ) + + async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: + raise NotImplementedError("paid_invoices_stream not implemented") diff --git a/cashu/lightning/lndrest.py b/cashu/lightning/lndrest.py index 477e84f4..e534f353 100644 --- a/cashu/lightning/lndrest.py +++ b/cashu/lightning/lndrest.py @@ -31,6 +31,7 @@ class LndRestWallet(LightningBackend): """https://api.lightning.community/rest/index.html#lnd-rest-api-reference""" supports_mpp = settings.mint_lnd_enable_mpp + supports_incoming_payment_stream = True supported_units = set([Unit.sat, Unit.msat]) unit = Unit.sat diff --git a/cashu/lightning/strike.py b/cashu/lightning/strike.py index 41c8d718..422a701d 100644 --- a/cashu/lightning/strike.py +++ b/cashu/lightning/strike.py @@ -1,6 +1,6 @@ # type: ignore import secrets -from typing import Dict, Optional +from typing import AsyncGenerator, Dict, Optional import httpx @@ -199,3 +199,6 @@ async def get_payment_status(self, checking_id: str) -> PaymentStatus: fee_msat=data["details"]["fee"], preimage=data["preimage"], ) + + async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: + raise NotImplementedError("paid_invoices_stream not implemented") diff --git a/cashu/mint/crud.py b/cashu/mint/crud.py index a5d5a71a..33bfbed9 100644 --- a/cashu/mint/crud.py +++ b/cashu/mint/crud.py @@ -173,7 +173,9 @@ async def store_mint_quote( async def get_mint_quote( self, *, - quote_id: str, + quote_id: Optional[str] = None, + checking_id: Optional[str] = None, + request: Optional[str] = None, db: Database, conn: Optional[Connection] = None, ) -> Optional[MintQuote]: @@ -223,9 +225,10 @@ async def store_melt_quote( async def get_melt_quote( self, *, - quote_id: str, - db: Database, + quote_id: Optional[str] = None, checking_id: Optional[str] = None, + request: Optional[str] = None, + db: Database, conn: Optional[Connection] = None, ) -> Optional[MeltQuote]: ... @@ -450,17 +453,36 @@ async def store_mint_quote( async def get_mint_quote( self, *, - quote_id: str, + quote_id: Optional[str] = None, + checking_id: Optional[str] = None, + request: Optional[str] = None, db: Database, conn: Optional[Connection] = None, ) -> Optional[MintQuote]: + clauses = [] + values: List[Any] = [] + if quote_id: + clauses.append("quote = ?") + values.append(quote_id) + if checking_id: + clauses.append("checking_id = ?") + values.append(checking_id) + if request: + clauses.append("request = ?") + values.append(request) + if not any(clauses): + raise ValueError("No search criteria") + + where = f"WHERE {' AND '.join(clauses)}" row = await (conn or db).fetchone( f""" SELECT * from {table_with_schema(db, 'mint_quotes')} - WHERE quote = ? + {where} """, - (quote_id,), + tuple(values), ) + if row is None: + return None return MintQuote.from_row(row) if row else None async def get_mint_quote_by_request( @@ -546,10 +568,10 @@ async def store_melt_quote( async def get_melt_quote( self, *, - quote_id: str, - db: Database, + quote_id: Optional[str] = None, checking_id: Optional[str] = None, request: Optional[str] = None, + db: Database, conn: Optional[Connection] = None, ) -> Optional[MeltQuote]: clauses = [] @@ -563,9 +585,10 @@ async def get_melt_quote( if request: clauses.append("request = ?") values.append(request) - where = "" - if clauses: - where = f"WHERE {' AND '.join(clauses)}" + if not any(clauses): + raise ValueError("No search criteria") + where = f"WHERE {' AND '.join(clauses)}" + row = await (conn or db).fetchone( f""" SELECT * from {table_with_schema(db, 'melt_quotes')} diff --git a/cashu/mint/db/read.py b/cashu/mint/db/read.py new file mode 100644 index 00000000..b245b08f --- /dev/null +++ b/cashu/mint/db/read.py @@ -0,0 +1,68 @@ +from typing import Dict, List + +from ...core.base import Proof, ProofState, SpentState +from ...core.db import Database +from ..crud import LedgerCrud + + +class DbReadHelper: + db: Database + crud: LedgerCrud + + def __init__(self, db: Database, crud: LedgerCrud) -> None: + self.db = db + self.crud = crud + + async def _get_proofs_pending(self, Ys: List[str]) -> Dict[str, Proof]: + """Returns a dictionary of only those proofs that are pending. + The key is the Y=h2c(secret) and the value is the proof. + """ + proofs_pending = await self.crud.get_proofs_pending(Ys=Ys, db=self.db) + proofs_pending_dict = {p.Y: p for p in proofs_pending} + return proofs_pending_dict + + async def _get_proofs_spent(self, Ys: List[str]) -> Dict[str, Proof]: + """Returns a dictionary of all proofs that are spent. + The key is the Y=h2c(secret) and the value is the proof. + """ + proofs_spent_dict: Dict[str, Proof] = {} + # check used secrets in database + async with self.db.connect() as conn: + for Y in Ys: + spent_proof = await self.crud.get_proof_used(db=self.db, Y=Y, conn=conn) + if spent_proof: + proofs_spent_dict[Y] = spent_proof + return proofs_spent_dict + + async def get_proofs_states(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 diff --git a/cashu/mint/db/write.py b/cashu/mint/db/write.py new file mode 100644 index 00000000..2f307bf7 --- /dev/null +++ b/cashu/mint/db/write.py @@ -0,0 +1,97 @@ +import asyncio +from typing import List, Optional + +from loguru import logger + +from ...core.base import Proof, ProofState, SpentState +from ...core.db import Connection, Database, get_db_connection +from ...core.errors import ( + TransactionError, +) +from ..crud import LedgerCrud +from ..events.events import LedgerEventManager + + +class DbWriteHelper: + db: Database + crud: LedgerCrud + events: LedgerEventManager + proofs_pending_lock: asyncio.Lock = ( + asyncio.Lock() + ) # holds locks for proofs_pending database + + def __init__( + self, db: Database, crud: LedgerCrud, events: LedgerEventManager + ) -> None: + self.db = db + self.crud = crud + self.events = events + + 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 get_db_connection(self.db) 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 + ) + await self.events.submit( + ProofState(Y=p.Y, state=SpentState.pending) + ) + 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], spent=True) -> None: + """Deletes proofs from pending table. + + Args: + proofs (List[Proof]): Proofs to delete. + spent (bool): Whether the proofs have been spent or not. Defaults to True. + This should be False if the proofs were NOT invalidated before calling this function. + It is used to emit the unspent state for the proofs (otherwise the spent state is emitted + by the _invalidate_proofs function when the proofs are spent). + """ + async with self.proofs_pending_lock: + async with get_db_connection(self.db) as conn: + for p in proofs: + 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) + ) + + 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.") diff --git a/cashu/mint/events/__init__.py b/cashu/mint/events/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cashu/mint/events/client.py b/cashu/mint/events/client.py new file mode 100644 index 00000000..e54c0736 --- /dev/null +++ b/cashu/mint/events/client.py @@ -0,0 +1,231 @@ +import asyncio +import json +from typing import List, Union + +from fastapi import WebSocket +from loguru import logger + +from ...core.base import MeltQuote, MintQuote, ProofState +from ...core.db import Database +from ...core.json_rpc.base import ( + JSONRPCError, + JSONRPCErrorCode, + JSONRPCErrorResponse, + JSONRPCMethods, + JSONRPCNotficationParams, + JSONRPCNotification, + JSONRPCRequest, + JSONRPCResponse, + JSONRPCStatus, + JSONRPCSubscribeParams, + JSONRPCSubscriptionKinds, + JSONRPCUnsubscribeParams, + JSONRRPCSubscribeResponse, +) +from ...core.models import PostMeltQuoteResponse, PostMintQuoteResponse +from ...core.settings import settings +from ..crud import LedgerCrud +from ..db.read import DbReadHelper +from ..limit import limit_websocket +from .event_model import LedgerEvent + + +class LedgerEventClientManager: + websocket: WebSocket + subscriptions: dict[ + JSONRPCSubscriptionKinds, dict[str, List[str]] + ] = {} # [kind, [filter, List[subId]]] + max_subscriptions = 1000 + db_read: DbReadHelper + + def __init__(self, websocket: WebSocket, db: Database, crud: LedgerCrud): + self.websocket = websocket + self.subscriptions = {} + self.db_read = DbReadHelper(db, crud) + + async def start(self): + await self.websocket.accept() + + while True: + message = await asyncio.wait_for( + self.websocket.receive(), + timeout=settings.mint_websocket_read_timeout, + ) + message_text = message.get("text") + + # Check the rate limit + try: + limit_websocket(self.websocket) + except Exception as e: + logger.error(f"Error: {e}") + err = JSONRPCErrorResponse( + error=JSONRPCError( + code=JSONRPCErrorCode.SERVER_ERROR, + message=f"Error: {e}", + ), + id=0, + ) + await self._send_msg(err) + continue + + # Check if message contains text + if not message_text: + continue + + # Parse the JSON data + try: + data = json.loads(message_text) + except json.JSONDecodeError as e: + logger.error(f"Error decoding JSON: {e}") + err = JSONRPCErrorResponse( + error=JSONRPCError( + code=JSONRPCErrorCode.PARSE_ERROR, + message=f"Error: {e}", + ), + id=0, + ) + await self._send_msg(err) + continue + + # Parse the JSONRPCRequest + try: + req = JSONRPCRequest.parse_obj(data) + except Exception as e: + err = JSONRPCErrorResponse( + error=JSONRPCError( + code=JSONRPCErrorCode.INVALID_REQUEST, + message=f"Error: {e}", + ), + id=0, + ) + await self._send_msg(err) + logger.warning(f"Error handling websocket message: {e}") + continue + + # Check if the method is valid + try: + JSONRPCMethods(req.method) + except ValueError: + err = JSONRPCErrorResponse( + error=JSONRPCError( + code=JSONRPCErrorCode.METHOD_NOT_FOUND, + message=f"Method not found: {req.method}", + ), + id=req.id, + ) + await self._send_msg(err) + continue + + # Handle the request + try: + logger.debug(f"Request: {req.json()}") + resp = await self._handle_request(req) + # Send the response + await self._send_msg(resp) + except Exception as e: + err = JSONRPCErrorResponse( + error=JSONRPCError( + code=JSONRPCErrorCode.INTERNAL_ERROR, + message=f"Error: {e}", + ), + id=req.id, + ) + await self._send_msg(err) + continue + + async def _handle_request(self, data: JSONRPCRequest) -> JSONRPCResponse: + logger.debug(f"Received websocket message: {data}") + if data.method == JSONRPCMethods.SUBSCRIBE.value: + subscribe_params = JSONRPCSubscribeParams.parse_obj(data.params) + self.add_subscription( + subscribe_params.kind, subscribe_params.filters, subscribe_params.subId + ) + result = JSONRRPCSubscribeResponse( + status=JSONRPCStatus.OK, + subId=subscribe_params.subId, + ) + return JSONRPCResponse(result=result.dict(), id=data.id) + elif data.method == JSONRPCMethods.UNSUBSCRIBE.value: + unsubscribe_params = JSONRPCUnsubscribeParams.parse_obj(data.params) + self.remove_subscription(unsubscribe_params.subId) + result = JSONRRPCSubscribeResponse( + status=JSONRPCStatus.OK, + subId=unsubscribe_params.subId, + ) + return JSONRPCResponse(result=result.dict(), id=data.id) + else: + raise ValueError(f"Invalid method: {data.method}") + + async def _send_obj(self, data: dict, subId: str): + resp = JSONRPCNotification( + method=JSONRPCMethods.SUBSCRIBE.value, + params=JSONRPCNotficationParams(subId=subId, payload=data).dict(), + ) + await self._send_msg(resp) + + async def _send_msg( + self, data: Union[JSONRPCResponse, JSONRPCNotification, JSONRPCErrorResponse] + ): + logger.debug(f"Sending websocket message: {data.json()}") + await self.websocket.send_text(data.json()) + + def add_subscription( + self, + kind: JSONRPCSubscriptionKinds, + filters: List[str], + subId: str, + ) -> None: + if kind not in self.subscriptions: + self.subscriptions[kind] = {} + + if len(self.subscriptions[kind]) >= self.max_subscriptions: + raise ValueError("Max subscriptions reached") + + for filter in filters: + if filter not in self.subscriptions: + self.subscriptions[kind][filter] = [] + logger.debug(f"Adding subscription {subId} for filter {filter}") + self.subscriptions[kind][filter].append(subId) + # Initialize the subscription + asyncio.create_task(self._init_subscription(subId, filter, kind)) + + def remove_subscription(self, subId: str) -> None: + for kind, sub_filters in self.subscriptions.items(): + for filter, subs in sub_filters.items(): + for sub in subs: + if sub == subId: + logger.debug( + f"Removing subscription {subId} for filter {filter}" + ) + self.subscriptions[kind][filter].remove(sub) + return + raise ValueError(f"Subscription not found: {subId}") + + def serialize_event(self, event: LedgerEvent) -> dict: + if isinstance(event, MintQuote): + return_dict = PostMintQuoteResponse.parse_obj(event.dict()).dict() + elif isinstance(event, MeltQuote): + return_dict = PostMeltQuoteResponse.parse_obj(event.dict()).dict() + elif isinstance(event, ProofState): + return_dict = event.dict(exclude_unset=True, exclude_none=True) + return return_dict + + async def _init_subscription( + self, subId: str, filter: str, kind: JSONRPCSubscriptionKinds + ): + if kind == JSONRPCSubscriptionKinds.BOLT11_MINT_QUOTE: + mint_quote = await self.db_read.crud.get_mint_quote( + quote_id=filter, db=self.db_read.db + ) + if mint_quote: + await self._send_obj(mint_quote.dict(), subId) + elif kind == JSONRPCSubscriptionKinds.BOLT11_MELT_QUOTE: + melt_quote = await self.db_read.crud.get_melt_quote( + quote_id=filter, db=self.db_read.db + ) + if melt_quote: + await self._send_obj(melt_quote.dict(), subId) + elif kind == JSONRPCSubscriptionKinds.PROOF_STATE: + proofs = await self.db_read.get_proofs_states(Ys=[filter]) + if len(proofs): + await self._send_obj(proofs[0].dict(), subId) diff --git a/cashu/mint/events/event_model.py b/cashu/mint/events/event_model.py new file mode 100644 index 00000000..a6220b4a --- /dev/null +++ b/cashu/mint/events/event_model.py @@ -0,0 +1,21 @@ +from abc import ABC, abstractmethod + +from pydantic import BaseModel + +from ...core.json_rpc.base import JSONRPCSubscriptionKinds + + +class LedgerEvent(ABC, BaseModel): + """AbstractBaseClass for BaseModels that can be sent to the + LedgerEventManager for broadcasting subscription events to clients. + """ + + @property + @abstractmethod + def identifier(self) -> str: + pass + + @property + @abstractmethod + def kind(self) -> JSONRPCSubscriptionKinds: + pass diff --git a/cashu/mint/events/events.py b/cashu/mint/events/events.py new file mode 100644 index 00000000..10b40a9e --- /dev/null +++ b/cashu/mint/events/events.py @@ -0,0 +1,61 @@ +import asyncio + +from fastapi import WebSocket +from loguru import logger + +from ...core.base import MeltQuote, MintQuote, ProofState +from ...core.db import Database +from ...core.models import PostMeltQuoteResponse, PostMintQuoteResponse +from ..crud import LedgerCrud +from .client import LedgerEventClientManager +from .event_model import LedgerEvent + + +class LedgerEventManager: + """LedgerEventManager is a subscription service from the mint + for client websockets that subscribe to event updates. + + Yields: + _type_: Union[MintQuote, MeltQuote] + """ + + clients: list[LedgerEventClientManager] = [] + + MAX_CLIENTS = 1000 + + def add_client( + self, websocket: WebSocket, db: Database, crud: LedgerCrud + ) -> LedgerEventClientManager: + client = LedgerEventClientManager(websocket, db, crud) + if len(self.clients) >= self.MAX_CLIENTS: + raise Exception("too many clients") + self.clients.append(client) + logger.debug(f"Added websocket subscription client {client}") + return client + + def remove_client(self, client: LedgerEventClientManager) -> None: + self.clients.remove(client) + + def serialize_event(self, event: LedgerEvent) -> dict: + if isinstance(event, MintQuote): + return_dict = PostMintQuoteResponse.parse_obj(event.dict()).dict() + elif isinstance(event, MeltQuote): + return_dict = PostMeltQuoteResponse.parse_obj(event.dict()).dict() + elif isinstance(event, ProofState): + return_dict = event.dict(exclude_unset=True, exclude_none=True) + return return_dict + + async def submit(self, event: LedgerEvent) -> None: + if not isinstance(event, LedgerEvent): + raise ValueError(f"Unsupported event object type {type(event)}") + + # check if any clients are subscribed to this event + for client in self.clients: + kind_sub = client.subscriptions.get(event.kind, {}) + for sub in kind_sub.get(event.identifier, []): + logger.trace( + f"Submitting event to sub {sub}: {self.serialize_event(event)}" + ) + asyncio.create_task( + client._send_obj(self.serialize_event(event), subId=sub) + ) diff --git a/cashu/mint/features.py b/cashu/mint/features.py new file mode 100644 index 00000000..494033e8 --- /dev/null +++ b/cashu/mint/features.py @@ -0,0 +1,102 @@ +from typing import Any, Dict, List, Union + +from ..core.base import Method +from ..core.models import ( + MintMeltMethodSetting, +) +from ..core.nuts import ( + DLEQ_NUT, + FEE_RETURN_NUT, + MELT_NUT, + MINT_NUT, + MPP_NUT, + P2PK_NUT, + RESTORE_NUT, + SCRIPT_NUT, + STATE_NUT, + WEBSOCKETS_NUT, +) +from ..core.settings import settings +from ..mint.protocols import SupportsBackends + + +class LedgerFeatures(SupportsBackends): + def mint_features(self) -> Dict[int, Union[List[Any], Dict[str, Any]]]: + # determine all method-unit pairs + method_settings: Dict[int, List[MintMeltMethodSetting]] = {} + for nut in [MINT_NUT, MELT_NUT]: + method_settings[nut] = [] + for method, unit_dict in self.backends.items(): + for unit in unit_dict.keys(): + setting = MintMeltMethodSetting(method=method.name, unit=unit.name) + + if nut == MINT_NUT and settings.mint_max_peg_in: + setting.max_amount = settings.mint_max_peg_in + setting.min_amount = 0 + elif nut == MELT_NUT and settings.mint_max_peg_out: + setting.max_amount = settings.mint_max_peg_out + setting.min_amount = 0 + + method_settings[nut].append(setting) + + supported_dict = dict(supported=True) + + mint_features: Dict[int, Union[List[Any], Dict[str, Any]]] = { + MINT_NUT: dict( + methods=method_settings[MINT_NUT], + disabled=settings.mint_peg_out_only, + ), + MELT_NUT: dict( + methods=method_settings[MELT_NUT], + disabled=False, + ), + STATE_NUT: supported_dict, + FEE_RETURN_NUT: supported_dict, + RESTORE_NUT: supported_dict, + SCRIPT_NUT: supported_dict, + P2PK_NUT: supported_dict, + DLEQ_NUT: supported_dict, + } + + # signal which method-unit pairs support MPP + mpp_features = [] + for method, unit_dict in self.backends.items(): + for unit in unit_dict.keys(): + if unit_dict[unit].supports_mpp: + mpp_features.append( + { + "method": method.name, + "unit": unit.name, + "mpp": True, + } + ) + + if mpp_features: + mint_features[MPP_NUT] = mpp_features + + # specify which websocket features are supported + # these two are supported by default + websocket_features: List[Dict[str, Union[str, List[str]]]] = [] + # we check the backend to see if "bolt11_mint_quote" is supported as well + for method, unit_dict in self.backends.items(): + if method == Method["bolt11"]: + for unit in unit_dict.keys(): + websocket_features.append( + { + "method": method.name, + "unit": unit.name, + "commands": ["bolt11_melt_quote", "proof_state"], + } + ) + if unit_dict[unit].supports_incoming_payment_stream: + supported_features: List[str] = list( + websocket_features[-1]["commands"] + ) + websocket_features[-1]["commands"] = supported_features + [ + "bolt11_mint_quote" + ] + + if websocket_features: + mint_features[WEBSOCKETS_NUT] = websocket_features + + return mint_features diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 01cb936c..37a23cec 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -52,16 +52,20 @@ ) 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 @@ def __init__( 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 @@ async def _check_pending_proofs_and_melt_quotes(self): 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 @@ async def _invalidate_proofs( 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 @@ async def mint_quote(self, quote_request: PostMintQuoteRequest) -> MintQuote: 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 @@ async def get_mint_quote(self, quote_id: str) -> MintQuote: 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 @@ async def mint( 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 @@ async def melt_quote( # 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 @@ async def melt_quote( 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 @@ async def get_melt_quote(self, quote_id: str) -> MeltQuote: # 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 @@ async def get_melt_quote(self, quote_id: str) -> MeltQuote: 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 @@ async def melt_mint_settle_internally( """ # 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 @@ async def melt_mint_settle_internally( 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 @@ async def melt( 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 @@ async def melt( 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 @@ async def melt( 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 @@ async def split( # 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 @@ async def split( 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 @@ async def restore( ) -> 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 @@ async def _generate_promises( ) 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.") diff --git a/cashu/mint/limit.py b/cashu/mint/limit.py index 1a8a4c28..b0840ff0 100644 --- a/cashu/mint/limit.py +++ b/cashu/mint/limit.py @@ -1,5 +1,6 @@ -from fastapi import status +from fastapi import WebSocket, status from fastapi.responses import JSONResponse +from limits import RateLimitItemPerMinute from loguru import logger from slowapi import Limiter from slowapi.util import get_remote_address @@ -39,3 +40,57 @@ def get_remote_address_excluding_local(request: Request) -> str: default_limits=[f"{settings.mint_transaction_rate_limit_per_minute}/minute"], enabled=settings.mint_rate_limit, ) + + +def assert_limit(identifier: str): + """Custom rate limit handler that accepts a string identifier + and raises an exception if the rate limit is exceeded. Uses the + setting `mint_transaction_rate_limit_per_minute` for the rate limit. + + Args: + identifier (str): The identifier to use for the rate limit. IP address for example. + + Raises: + Exception: If the rate limit is exceeded. + """ + global limiter + success = limiter._limiter.hit( + RateLimitItemPerMinute(settings.mint_transaction_rate_limit_per_minute), + identifier, + ) + if not success: + logger.warning( + f"Rate limit {settings.mint_transaction_rate_limit_per_minute}/minute exceeded: {identifier}" + ) + raise Exception("Rate limit exceeded") + + +def get_ws_remote_address(ws: WebSocket) -> str: + """Returns the ip address for the current websocket (or 127.0.0.1 if none found) + + Args: + ws (WebSocket): The FastAPI WebSocket object. + + Returns: + str: The ip address for the current websocket. + """ + if not ws.client or not ws.client.host: + return "127.0.0.1" + + return ws.client.host + + +def limit_websocket(ws: WebSocket): + """Websocket rate limit handler that accepts a FastAPI WebSocket object. + This function will raise an exception if the rate limit is exceeded. + + Args: + ws (WebSocket): The FastAPI WebSocket object. + + Raises: + Exception: If the rate limit is exceeded. + """ + remote_address = get_ws_remote_address(ws) + if remote_address == "127.0.0.1": + return + assert_limit(remote_address) diff --git a/cashu/mint/protocols.py b/cashu/mint/protocols.py index 04d24c0c..ff576d96 100644 --- a/cashu/mint/protocols.py +++ b/cashu/mint/protocols.py @@ -4,6 +4,7 @@ from ..core.db import Database from ..lightning.base import LightningBackend from ..mint.crud import LedgerCrud +from .events.events import LedgerEventManager class SupportsKeysets(Protocol): @@ -18,3 +19,7 @@ class SupportsBackends(Protocol): class SupportsDb(Protocol): db: Database crud: LedgerCrud + + +class SupportsEvents(Protocol): + events: LedgerEventManager diff --git a/cashu/mint/router.py b/cashu/mint/router.py index 79277348..1bc42167 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -1,6 +1,6 @@ -from typing import Any, Dict, List +import asyncio -from fastapi import APIRouter, Request +from fastapi import APIRouter, Request, WebSocket from loguru import logger from ..core.errors import KeysetNotFoundError @@ -10,7 +10,6 @@ KeysetsResponseKeyset, KeysResponse, KeysResponseKeyset, - MintMeltMethodSetting, PostCheckStateRequest, PostCheckStateResponse, PostMeltQuoteRequest, @@ -28,7 +27,7 @@ ) from ..core.settings import settings from ..mint.startup import ledger -from .limit import limiter +from .limit import limit_websocket, limiter router: APIRouter = APIRouter() @@ -42,59 +41,7 @@ ) async def info() -> GetInfoResponse: logger.trace("> GET /v1/info") - - # determine all method-unit pairs - method_settings: Dict[int, List[MintMeltMethodSetting]] = {} - for nut in [4, 5]: - method_settings[nut] = [] - for method, unit_dict in ledger.backends.items(): - for unit in unit_dict.keys(): - setting = MintMeltMethodSetting(method=method.name, unit=unit.name) - - if nut == 4 and settings.mint_max_peg_in: - setting.max_amount = settings.mint_max_peg_in - setting.min_amount = 0 - elif nut == 5 and settings.mint_max_peg_out: - setting.max_amount = settings.mint_max_peg_out - setting.min_amount = 0 - - method_settings[nut].append(setting) - - supported_dict = dict(supported=True) - - supported_dict = dict(supported=True) - mint_features: Dict[int, Any] = { - 4: dict( - methods=method_settings[4], - disabled=settings.mint_peg_out_only, - ), - 5: dict( - methods=method_settings[5], - disabled=False, - ), - 7: supported_dict, - 8: supported_dict, - 9: supported_dict, - 10: supported_dict, - 11: supported_dict, - 12: supported_dict, - } - - # signal which method-unit pairs support MPP - for method, unit_dict in ledger.backends.items(): - for unit in unit_dict.keys(): - logger.trace( - f"method={method.name} unit={unit} supports_mpp={unit_dict[unit].supports_mpp}" - ) - if unit_dict[unit].supports_mpp: - mint_features.setdefault(15, []).append( - { - "method": method.name, - "unit": unit.name, - "mpp": True, - } - ) - + mint_features = ledger.mint_features() return GetInfoResponse( name=settings.mint_info_name, pubkey=ledger.pubkey.serialize().hex() if ledger.pubkey else None, @@ -243,6 +190,26 @@ async def get_mint_quote(request: Request, quote: str) -> PostMintQuoteResponse: return resp +@router.websocket("/v1/ws", name="Websocket endpoint for subscriptions") +async def websocket_endpoint(websocket: WebSocket): + limit_websocket(websocket) + try: + client = ledger.events.add_client(websocket, ledger.db, ledger.crud) + except Exception as e: + logger.debug(f"Exception: {e}") + await asyncio.wait_for(websocket.close(), timeout=1) + return + + try: + # this will block until the session is closed + await client.start() + except Exception as e: + logger.debug(f"Exception: {e}") + ledger.events.remove_client(client) + finally: + await asyncio.wait_for(websocket.close(), timeout=1) + + @router.post( "/v1/mint/bolt11", name="Mint tokens with a Lightning payment", @@ -385,7 +352,7 @@ async def check_state( ) -> PostCheckStateResponse: """Check whether a secret has been spent already or not.""" logger.trace(f"> POST /v1/checkstate: {payload}") - proof_states = await ledger.check_proofs_state(payload.Ys) + proof_states = await ledger.db_read.get_proofs_states(payload.Ys) return PostCheckStateResponse(states=proof_states) diff --git a/cashu/mint/router_deprecated.py b/cashu/mint/router_deprecated.py index 67976d2a..520ec8d7 100644 --- a/cashu/mint/router_deprecated.py +++ b/cashu/mint/router_deprecated.py @@ -341,7 +341,7 @@ async def check_spendable_deprecated( ) -> CheckSpendableResponse_deprecated: """Check whether a secret has been spent already or not.""" logger.trace(f"> POST /check: {payload}") - proofs_state = await ledger.check_proofs_state([p.Y for p in payload.proofs]) + proofs_state = await ledger.db_read.get_proofs_states([p.Y for p in payload.proofs]) spendableList: List[bool] = [] pendingList: List[bool] = [] for proof_state in proofs_state: diff --git a/cashu/mint/tasks.py b/cashu/mint/tasks.py new file mode 100644 index 00000000..413421ce --- /dev/null +++ b/cashu/mint/tasks.py @@ -0,0 +1,45 @@ +import asyncio +from typing import Mapping + +from loguru import logger + +from ..core.base import Method, Unit +from ..core.db import Database +from ..lightning.base import LightningBackend +from ..mint.crud import LedgerCrud +from .events.events import LedgerEventManager +from .protocols import SupportsBackends, SupportsDb, SupportsEvents + + +class LedgerTasks(SupportsDb, SupportsBackends, SupportsEvents): + backends: Mapping[Method, Mapping[Unit, LightningBackend]] = {} + db: Database + crud: LedgerCrud + events: LedgerEventManager + + async def dispatch_listeners(self) -> None: + for method, unitbackends in self.backends.items(): + for unit, backend in unitbackends.items(): + logger.debug( + f"Dispatching backend invoice listener for {method} {unit} {backend.__class__.__name__}" + ) + asyncio.create_task(self.invoice_listener(backend)) + + async def invoice_listener(self, backend: LightningBackend) -> None: + async for checking_id in backend.paid_invoices_stream(): + await self.invoice_callback_dispatcher(checking_id) + + async def invoice_callback_dispatcher(self, checking_id: str) -> None: + logger.debug(f"Invoice callback dispatcher: {checking_id}") + # TODO: Explicitly check for the quote payment state before setting it as paid + # db read, quote.paid = True, db write should be refactored and moved to ledger.py + quote = await self.crud.get_mint_quote(checking_id=checking_id, db=self.db) + if not quote: + logger.error(f"Quote not found for {checking_id}") + return + # set the quote as paid + if not quote.paid: + quote.paid = True + 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) diff --git a/cashu/mint/verification.py b/cashu/mint/verification.py index 68bbbc42..006a5daf 100644 --- a/cashu/mint/verification.py +++ b/cashu/mint/verification.py @@ -35,7 +35,6 @@ class LedgerVerification( keyset: MintKeyset keysets: Dict[str, MintKeyset] - spent_proofs: Dict[str, Proof] crud: LedgerCrud db: Database lightning: Dict[Unit, LightningBackend] @@ -128,11 +127,14 @@ async def _verify_outputs( if not self._verify_no_duplicate_outputs(outputs): raise TransactionError("duplicate outputs.") # verify that outputs have not been signed previously - if any(await self._check_outputs_issued_before(outputs)): + signed_before = await self._check_outputs_issued_before(outputs) + if any(signed_before): raise TransactionError("outputs have already been signed before.") logger.trace(f"Verified {len(outputs)} outputs.") - async def _check_outputs_issued_before(self, outputs: List[BlindedMessage]): + async def _check_outputs_issued_before( + self, outputs: List[BlindedMessage] + ) -> List[bool]: """Checks whether the provided outputs have previously been signed by the mint (which would lead to a duplication error later when trying to store these outputs again). @@ -164,21 +166,12 @@ async def _get_proofs_spent(self, Ys: List[str]) -> Dict[str, Proof]: The key is the Y=h2c(secret) and the value is the proof. """ proofs_spent_dict: Dict[str, Proof] = {} - if settings.mint_cache_secrets: - # check used secrets in memory + # check used secrets in database + async with self.db.connect() as conn: for Y in Ys: - spent_proof = self.spent_proofs.get(Y) + spent_proof = await self.crud.get_proof_used(db=self.db, Y=Y, conn=conn) if spent_proof: proofs_spent_dict[Y] = spent_proof - else: - # check used secrets in database - async with self.db.connect() as conn: - for Y in Ys: - spent_proof = await self.crud.get_proof_used( - db=self.db, Y=Y, conn=conn - ) - if spent_proof: - proofs_spent_dict[Y] = spent_proof return proofs_spent_dict def _verify_secret_criteria(self, proof: Proof) -> Literal[True]: diff --git a/cashu/wallet/cli/cli.py b/cashu/wallet/cli/cli.py index 60747f9d..3c600433 100644 --- a/cashu/wallet/cli/cli.py +++ b/cashu/wallet/cli/cli.py @@ -9,15 +9,17 @@ 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 @@ 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) + + # 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 and want to check it + # 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.", diff --git a/cashu/wallet/mint_info.py b/cashu/wallet/mint_info.py index e9092dd6..84035beb 100644 --- a/cashu/wallet/mint_info.py +++ b/cashu/wallet/mint_info.py @@ -2,7 +2,9 @@ from pydantic import BaseModel -from ..core.base import Unit +from cashu.core.nuts import MPP_NUT, WEBSOCKETS_NUT + +from ..core.base import Method, Unit from ..core.models import Nut15MppSupport @@ -27,8 +29,8 @@ def supports_nut(self, nut: int) -> bool: def supports_mpp(self, method: str, unit: Unit) -> bool: if not self.nuts: return False - nut_15 = self.nuts.get(15) - if not nut_15 or not self.supports_nut(15): + nut_15 = self.nuts.get(MPP_NUT) + if not nut_15 or not self.supports_nut(MPP_NUT): return False for entry in nut_15: @@ -37,3 +39,15 @@ def supports_mpp(self, method: str, unit: Unit) -> bool: return True return False + + def supports_websocket_mint_quote(self, method: Method, unit: Unit) -> bool: + if not self.nuts or not self.supports_nut(WEBSOCKETS_NUT): + return False + websocket_settings = self.nuts[WEBSOCKETS_NUT] + if not websocket_settings: + return False + for entry in websocket_settings: + if entry["method"] == method.name and entry["unit"] == unit.name: + if "bolt11_mint_quote" in entry["commands"]: + return True + return False diff --git a/cashu/wallet/subscriptions.py b/cashu/wallet/subscriptions.py new file mode 100644 index 00000000..35c0b301 --- /dev/null +++ b/cashu/wallet/subscriptions.py @@ -0,0 +1,96 @@ +import time +from typing import Callable, List +from urllib.parse import urlparse + +from loguru import logger +from websocket._app import WebSocketApp + +from ..core.crypto.keys import random_hash +from ..core.json_rpc.base import ( + JSONRPCMethods, + JSONRPCNotficationParams, + JSONRPCNotification, + JSONRPCRequest, + JSONRPCResponse, + JSONRPCSubscribeParams, + JSONRPCSubscriptionKinds, + JSONRPCUnsubscribeParams, +) + + +class SubscriptionManager: + url: str + websocket: WebSocketApp + id_counter: int = 0 + callback_map: dict[str, Callable] = {} + + def __init__(self, url: str): + # parse hostname from url with urlparse + hostname = urlparse(url).hostname + port = urlparse(url).port + if port: + hostname = f"{hostname}:{port}" + scheme = urlparse(url).scheme + ws_scheme = "wss" if scheme == "https" else "ws" + ws_url = f"{ws_scheme}://{hostname}/v1/ws" + self.url = ws_url + self.websocket = WebSocketApp(ws_url, on_message=self._on_message) + + def _on_message(self, ws, message): + logger.trace(f"Received message: {message}") + try: + # return if message is a response + JSONRPCResponse.parse_raw(message) + return + except Exception: + pass + + try: + msg = JSONRPCNotification.parse_raw(message) + params = JSONRPCNotficationParams.parse_obj(msg.params) + logger.debug(f"Received notification: {msg}") + self.callback_map[params.subId](params) + return + except Exception: + pass + + logger.error(f"Error parsing message: {message}") + + def connect(self): + self.websocket.run_forever(ping_interval=10, ping_timeout=5) + + def close(self): + # unsubscribe from all subscriptions + for subId in self.callback_map.keys(): + req = JSONRPCRequest( + method=JSONRPCMethods.UNSUBSCRIBE.value, + params=JSONRPCUnsubscribeParams(subId=subId).dict(), + id=self.id_counter, + ) + logger.trace(f"Unsubscribing: {req.json()}") + self.websocket.send(req.json()) + self.id_counter += 1 + + self.websocket.keep_running = False + self.websocket.close() + + def wait_until_connected(self): + while not self.websocket.sock or not self.websocket.sock.connected: + time.sleep(0.025) + + def subscribe( + self, kind: JSONRPCSubscriptionKinds, filters: List[str], callback: Callable + ): + self.wait_until_connected() + subId = random_hash() + req = JSONRPCRequest( + method=JSONRPCMethods.SUBSCRIBE.value, + params=JSONRPCSubscribeParams( + kind=kind, filters=filters, subId=subId + ).dict(), + id=self.id_counter, + ) + logger.trace(f"Subscribing: {req.json()}") + self.websocket.send(req.json()) + self.id_counter += 1 + self.callback_map[subId] = callback diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 107bb12a..2426f024 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -1,11 +1,14 @@ import copy +import threading import time -from typing import Dict, List, Optional, Tuple, Union +from typing import Callable, Dict, List, Optional, Tuple, Union import bolt11 from bip32 import BIP32 from loguru import logger +from cashu.core.json_rpc.base import JSONRPCSubscriptionKinds + from ..core.base import ( BlindedMessage, BlindedSignature, @@ -50,6 +53,7 @@ from .p2pk import WalletP2PK from .proofs import WalletProofs from .secrets import WalletSecrets +from .subscriptions import SubscriptionManager from .transactions import WalletTransactions from .v1_api import LedgerAPI @@ -312,11 +316,47 @@ async def _check_used_secrets(self, secrets): raise Exception(f"secret already used: {s}") logger.trace("Secret check complete.") + async def request_mint_with_callback( + self, amount: int, callback: Callable + ) -> Tuple[Invoice, SubscriptionManager]: + """Request a Lightning invoice for minting tokens. + + Args: + amount (int): Amount for Lightning invoice in satoshis + callback (Callable): Callback function to be called when the invoice is paid. + + Returns: + Invoice: Lightning invoice + """ + mint_qoute = await super().mint_quote(amount, self.unit) + subscriptions = SubscriptionManager(self.url) + threading.Thread( + target=subscriptions.connect, name="SubscriptionManager", daemon=True + ).start() + subscriptions.subscribe( + kind=JSONRPCSubscriptionKinds.BOLT11_MINT_QUOTE, + filters=[mint_qoute.quote], + callback=callback, + ) + # return the invoice + decoded_invoice = bolt11.decode(mint_qoute.request) + invoice = Invoice( + amount=amount, + bolt11=mint_qoute.request, + payment_hash=decoded_invoice.payment_hash, + id=mint_qoute.quote, + out=False, + time_created=int(time.time()), + ) + await store_lightning_invoice(db=self.db, invoice=invoice) + return invoice, subscriptions + async def request_mint(self, amount: int) -> Invoice: """Request a Lightning invoice for minting tokens. Args: amount (int): Amount for Lightning invoice in satoshis + callback (Optional[Callable], optional): Callback function to be called when the invoice is paid. Defaults to None. Returns: PostMintQuoteResponse: Mint Quote Response @@ -684,6 +724,20 @@ async def melt( async def check_proof_state(self, proofs) -> PostCheckStateResponse: return await super().check_proof_state(proofs) + async def check_proof_state_with_callback( + self, proofs: List[Proof], callback: Callable + ) -> Tuple[PostCheckStateResponse, SubscriptionManager]: + subscriptions = SubscriptionManager(self.url) + threading.Thread( + target=subscriptions.connect, name="SubscriptionManager", daemon=True + ).start() + subscriptions.subscribe( + kind=JSONRPCSubscriptionKinds.PROOF_STATE, + filters=[proof.Y for proof in proofs], + callback=callback, + ) + return await self.check_proof_state(proofs), subscriptions + # ---------- TOKEN MECHANICS ---------- # ---------- DLEQ PROOFS ---------- @@ -1070,6 +1124,8 @@ async def balance_per_minturl( balances_return[key]["unit"] = unit.name return dict(sorted(balances_return.items(), key=lambda item: item[0])) # type: ignore + # ---------- RESTORE WALLET ---------- + async def restore_tokens_for_keyset( self, keyset_id: str, to: int = 2, batch: int = 25 ) -> None: diff --git a/poetry.lock b/poetry.lock index d34e7582..ec9edbe2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -290,63 +290,63 @@ files = [ [[package]] name = "coverage" -version = "7.4.1" +version = "7.4.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, - {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, - {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, - {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, - {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, - {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, - {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, - {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, - {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, - {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, - {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, - {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, - {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, - {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, - {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, - {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, - {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, - {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, - {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, - {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, - {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, - {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, - {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, - {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, - {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, - {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, - {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, - {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, - {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, - {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, - {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, - {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, - {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, - {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, - {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, - {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, - {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, - {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, - {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, - {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, - {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, - {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, - {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, - {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, - {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, - {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, - {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, - {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, - {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, - {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, - {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, - {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, + {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, + {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, + {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, + {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, + {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, + {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, + {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, + {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, + {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, + {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, + {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, + {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, ] [package.dependencies] @@ -503,12 +503,12 @@ all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)" [[package]] name = "fastapi-profiler" -version = "1.2.0" +version = "1.3.0" description = "A FastAPI Middleware of pyinstrument to check your service performance." optional = false python-versions = "*" files = [ - {file = "fastapi_profiler-1.2.0-py3-none-any.whl", hash = "sha256:71615f815c5ff4fe193c14b1ecf6bfc250502c6adfca64a219340df5eb0a7a9b"}, + {file = "fastapi_profiler-1.3.0-py3-none-any.whl", hash = "sha256:371f766de6aa12f525c8c42c6ce0d701cdf82d0738730c1a5681759881ed62e1"}, ] [package.dependencies] @@ -517,18 +517,18 @@ pyinstrument = ">=4.4.0" [[package]] name = "filelock" -version = "3.13.1" +version = "3.13.3" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, - {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, + {file = "filelock-3.13.3-py3-none-any.whl", hash = "sha256:5ffa845303983e7a0b7ae17636509bc97997d58afeafa72fb141a17b152284cb"}, + {file = "filelock-3.13.3.tar.gz", hash = "sha256:a79895a25bbefdf55d1a2a0a80968f7dbb28edcd6d4234a0afb3f37ecde4b546"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] typing = ["typing-extensions (>=4.8)"] [[package]] @@ -544,13 +544,13 @@ files = [ [[package]] name = "httpcore" -version = "1.0.3" +version = "1.0.5" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.3-py3-none-any.whl", hash = "sha256:9a6a501c3099307d9fd76ac244e08503427679b1e81ceb1d922485e2f2462ad2"}, - {file = "httpcore-1.0.3.tar.gz", hash = "sha256:5c0f9546ad17dac4d0772b0808856eb616eb8b48ce94f49ed819fd6982a8a544"}, + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, ] [package.dependencies] @@ -561,7 +561,7 @@ h11 = ">=0.13,<0.15" asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<0.24.0)"] +trio = ["trio (>=0.22.0,<0.26.0)"] [[package]] name = "httpx" @@ -590,13 +590,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "identify" -version = "2.5.34" +version = "2.5.35" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.34-py2.py3-none-any.whl", hash = "sha256:a4316013779e433d08b96e5eabb7f641e6c7942e4ab5d4c509ebd2e7a8994aed"}, - {file = "identify-2.5.34.tar.gz", hash = "sha256:ee17bc9d499899bc9eaec1ac7bf2dc9eedd480db9d88b96d123d3b64a9d34f5d"}, + {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, + {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, ] [package.extras] @@ -634,13 +634,13 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs [[package]] name = "importlib-resources" -version = "6.3.1" +version = "6.4.0" description = "Read resources from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_resources-6.3.1-py3-none-any.whl", hash = "sha256:4811639ca7fa830abdb8e9ca0a104dc6ad13de691d9fe0d3173a71304f068159"}, - {file = "importlib_resources-6.3.1.tar.gz", hash = "sha256:29a3d16556e330c3c8fb8202118c5ff41241cc34cbfb25989bbad226d99b7995"}, + {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"}, + {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"}, ] [package.dependencies] @@ -648,7 +648,7 @@ zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["jaraco.collections", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] +testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] [[package]] name = "iniconfig" @@ -663,19 +663,19 @@ files = [ [[package]] name = "limits" -version = "3.10.0" +version = "3.10.1" description = "Rate limiting utilities" optional = false python-versions = ">=3.7" files = [ - {file = "limits-3.10.0-py3-none-any.whl", hash = "sha256:3e617a580f57a21b39393f833c27ad0378c87b309e908c154ee69e6740041959"}, - {file = "limits-3.10.0.tar.gz", hash = "sha256:6e657dccafce64fd8ee023ebf4593cd47e9eac841fd1dec3448f48673ba10b7c"}, + {file = "limits-3.10.1-py3-none-any.whl", hash = "sha256:446242f5a6f7b8c7744e286a70793264ed81bca97860f94b821347284d14fbe9"}, + {file = "limits-3.10.1.tar.gz", hash = "sha256:1ee31d169d498da267a1b72183ae5940afc64b17b4ed4dfd977f6ea5607c2cfb"}, ] [package.dependencies] deprecated = ">=1.2" importlib-resources = ">=1.3" -packaging = ">=21,<24" +packaging = ">=21,<25" typing-extensions = "*" [package.extras] @@ -710,22 +710,21 @@ dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptio [[package]] name = "marshmallow" -version = "3.20.2" +version = "3.21.1" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." optional = false python-versions = ">=3.8" files = [ - {file = "marshmallow-3.20.2-py3-none-any.whl", hash = "sha256:c21d4b98fee747c130e6bc8f45c4b3199ea66bc00c12ee1f639f0aeca034d5e9"}, - {file = "marshmallow-3.20.2.tar.gz", hash = "sha256:4c1daff273513dc5eb24b219a8035559dc573c8f322558ef85f5438ddd1236dd"}, + {file = "marshmallow-3.21.1-py3-none-any.whl", hash = "sha256:f085493f79efb0644f270a9bf2892843142d80d7174bbbd2f3713f2a589dc633"}, + {file = "marshmallow-3.21.1.tar.gz", hash = "sha256:4e65e9e0d80fc9e609574b9983cf32579f305c718afb30d7233ab818571768c3"}, ] [package.dependencies] packaging = ">=17.0" [package.extras] -dev = ["pre-commit (>=2.4,<4.0)", "pytest", "pytz", "simplejson", "tox"] -docs = ["alabaster (==0.7.15)", "autodocsumm (==0.2.12)", "sphinx (==7.2.6)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"] -lint = ["pre-commit (>=2.4,<4.0)"] +dev = ["marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "tox"] +docs = ["alabaster (==0.7.16)", "autodocsumm (==0.2.12)", "sphinx (==7.2.6)", "sphinx-issues (==4.0.0)", "sphinx-version-warning (==1.1.2)"] tests = ["pytest", "pytz", "simplejson"] [[package]] @@ -741,38 +740,38 @@ files = [ [[package]] name = "mypy" -version = "1.8.0" +version = "1.9.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, - {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, - {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, - {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, - {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, - {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, - {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, - {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, - {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, - {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, - {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, - {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, - {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, - {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, - {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, - {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, - {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, - {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, - {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, - {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, - {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, - {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, - {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, - {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, - {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, - {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, - {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, + {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, + {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, + {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, + {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, + {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, + {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, + {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, + {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, + {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, + {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, + {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, + {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, + {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, + {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, + {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, + {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, + {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, + {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, + {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, ] [package.dependencies] @@ -827,13 +826,13 @@ attrs = ">=19.2.0" [[package]] name = "packaging" -version = "23.2" +version = "24.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] @@ -964,13 +963,13 @@ files = [ [[package]] name = "pycparser" -version = "2.21" +version = "2.22" description = "C parser in Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.8" files = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] [[package]] @@ -1016,47 +1015,47 @@ files = [ [[package]] name = "pydantic" -version = "1.10.14" +version = "1.10.15" description = "Data validation and settings management using python type hints" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4fcec873f90537c382840f330b90f4715eebc2bc9925f04cb92de593eae054"}, - {file = "pydantic-1.10.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e3a76f571970fcd3c43ad982daf936ae39b3e90b8a2e96c04113a369869dc87"}, - {file = "pydantic-1.10.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d886bd3c3fbeaa963692ef6b643159ccb4b4cefaf7ff1617720cbead04fd1d"}, - {file = "pydantic-1.10.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:798a3d05ee3b71967844a1164fd5bdb8c22c6d674f26274e78b9f29d81770c4e"}, - {file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:23d47a4b57a38e8652bcab15a658fdb13c785b9ce217cc3a729504ab4e1d6bc9"}, - {file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f9f674b5c3bebc2eba401de64f29948ae1e646ba2735f884d1594c5f675d6f2a"}, - {file = "pydantic-1.10.14-cp310-cp310-win_amd64.whl", hash = "sha256:24a7679fab2e0eeedb5a8924fc4a694b3bcaac7d305aeeac72dd7d4e05ecbebf"}, - {file = "pydantic-1.10.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d578ac4bf7fdf10ce14caba6f734c178379bd35c486c6deb6f49006e1ba78a7"}, - {file = "pydantic-1.10.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa7790e94c60f809c95602a26d906eba01a0abee9cc24150e4ce2189352deb1b"}, - {file = "pydantic-1.10.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad4e10efa5474ed1a611b6d7f0d130f4aafadceb73c11d9e72823e8f508e663"}, - {file = "pydantic-1.10.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245f4f61f467cb3dfeced2b119afef3db386aec3d24a22a1de08c65038b255f"}, - {file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:21efacc678a11114c765eb52ec0db62edffa89e9a562a94cbf8fa10b5db5c046"}, - {file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:412ab4a3f6dbd2bf18aefa9f79c7cca23744846b31f1d6555c2ee2b05a2e14ca"}, - {file = "pydantic-1.10.14-cp311-cp311-win_amd64.whl", hash = "sha256:e897c9f35281f7889873a3e6d6b69aa1447ceb024e8495a5f0d02ecd17742a7f"}, - {file = "pydantic-1.10.14-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d604be0f0b44d473e54fdcb12302495fe0467c56509a2f80483476f3ba92b33c"}, - {file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a42c7d17706911199798d4c464b352e640cab4351efe69c2267823d619a937e5"}, - {file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:596f12a1085e38dbda5cbb874d0973303e34227b400b6414782bf205cc14940c"}, - {file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bfb113860e9288d0886e3b9e49d9cf4a9d48b441f52ded7d96db7819028514cc"}, - {file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bc3ed06ab13660b565eed80887fcfbc0070f0aa0691fbb351657041d3e874efe"}, - {file = "pydantic-1.10.14-cp37-cp37m-win_amd64.whl", hash = "sha256:ad8c2bc677ae5f6dbd3cf92f2c7dc613507eafe8f71719727cbc0a7dec9a8c01"}, - {file = "pydantic-1.10.14-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c37c28449752bb1f47975d22ef2882d70513c546f8f37201e0fec3a97b816eee"}, - {file = "pydantic-1.10.14-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49a46a0994dd551ec051986806122767cf144b9702e31d47f6d493c336462597"}, - {file = "pydantic-1.10.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53e3819bd20a42470d6dd0fe7fc1c121c92247bca104ce608e609b59bc7a77ee"}, - {file = "pydantic-1.10.14-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbb503bbbbab0c588ed3cd21975a1d0d4163b87e360fec17a792f7d8c4ff29f"}, - {file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:336709883c15c050b9c55a63d6c7ff09be883dbc17805d2b063395dd9d9d0022"}, - {file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4ae57b4d8e3312d486e2498d42aed3ece7b51848336964e43abbf9671584e67f"}, - {file = "pydantic-1.10.14-cp38-cp38-win_amd64.whl", hash = "sha256:dba49d52500c35cfec0b28aa8b3ea5c37c9df183ffc7210b10ff2a415c125c4a"}, - {file = "pydantic-1.10.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c66609e138c31cba607d8e2a7b6a5dc38979a06c900815495b2d90ce6ded35b4"}, - {file = "pydantic-1.10.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d986e115e0b39604b9eee3507987368ff8148222da213cd38c359f6f57b3b347"}, - {file = "pydantic-1.10.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:646b2b12df4295b4c3148850c85bff29ef6d0d9621a8d091e98094871a62e5c7"}, - {file = "pydantic-1.10.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282613a5969c47c83a8710cc8bfd1e70c9223feb76566f74683af889faadc0ea"}, - {file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:466669501d08ad8eb3c4fecd991c5e793c4e0bbd62299d05111d4f827cded64f"}, - {file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:13e86a19dca96373dcf3190fcb8797d40a6f12f154a244a8d1e8e03b8f280593"}, - {file = "pydantic-1.10.14-cp39-cp39-win_amd64.whl", hash = "sha256:08b6ec0917c30861e3fe71a93be1648a2aa4f62f866142ba21670b24444d7fd8"}, - {file = "pydantic-1.10.14-py3-none-any.whl", hash = "sha256:8ee853cd12ac2ddbf0ecbac1c289f95882b2d4482258048079d13be700aa114c"}, - {file = "pydantic-1.10.14.tar.gz", hash = "sha256:46f17b832fe27de7850896f3afee50ea682220dd218f7e9c88d436788419dca6"}, + {file = "pydantic-1.10.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55"}, + {file = "pydantic-1.10.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2"}, + {file = "pydantic-1.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb"}, + {file = "pydantic-1.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8"}, + {file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00"}, + {file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0"}, + {file = "pydantic-1.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c"}, + {file = "pydantic-1.10.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0"}, + {file = "pydantic-1.10.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654"}, + {file = "pydantic-1.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3"}, + {file = "pydantic-1.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44"}, + {file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4"}, + {file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53"}, + {file = "pydantic-1.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986"}, + {file = "pydantic-1.10.15-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf"}, + {file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d"}, + {file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f"}, + {file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de"}, + {file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7"}, + {file = "pydantic-1.10.15-cp37-cp37m-win_amd64.whl", hash = "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1"}, + {file = "pydantic-1.10.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022"}, + {file = "pydantic-1.10.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528"}, + {file = "pydantic-1.10.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948"}, + {file = "pydantic-1.10.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c"}, + {file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22"}, + {file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b"}, + {file = "pydantic-1.10.15-cp38-cp38-win_amd64.whl", hash = "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12"}, + {file = "pydantic-1.10.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51"}, + {file = "pydantic-1.10.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0"}, + {file = "pydantic-1.10.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383"}, + {file = "pydantic-1.10.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed"}, + {file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc"}, + {file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4"}, + {file = "pydantic-1.10.15-cp39-cp39-win_amd64.whl", hash = "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7"}, + {file = "pydantic-1.10.15-py3-none-any.whl", hash = "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58"}, + {file = "pydantic-1.10.15.tar.gz", hash = "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb"}, ] [package.dependencies] @@ -1294,28 +1293,28 @@ httpx = ">=0.21.0" [[package]] name = "ruff" -version = "0.2.1" +version = "0.2.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dd81b911d28925e7e8b323e8d06951554655021df8dd4ac3045d7212ac4ba080"}, - {file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dc586724a95b7d980aa17f671e173df00f0a2eef23f8babbeee663229a938fec"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c92db7101ef5bfc18e96777ed7bc7c822d545fa5977e90a585accac43d22f18a"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13471684694d41ae0f1e8e3a7497e14cd57ccb7dd72ae08d56a159d6c9c3e30e"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a11567e20ea39d1f51aebd778685582d4c56ccb082c1161ffc10f79bebe6df35"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:00a818e2db63659570403e44383ab03c529c2b9678ba4ba6c105af7854008105"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be60592f9d218b52f03384d1325efa9d3b41e4c4d55ea022cd548547cc42cd2b"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbd2288890b88e8aab4499e55148805b58ec711053588cc2f0196a44f6e3d855"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ef052283da7dec1987bba8d8733051c2325654641dfe5877a4022108098683"}, - {file = "ruff-0.2.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7022d66366d6fded4ba3889f73cd791c2d5621b2ccf34befc752cb0df70f5fad"}, - {file = "ruff-0.2.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0a725823cb2a3f08ee743a534cb6935727d9e47409e4ad72c10a3faf042ad5ba"}, - {file = "ruff-0.2.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0034d5b6323e6e8fe91b2a1e55b02d92d0b582d2953a2b37a67a2d7dedbb7acc"}, - {file = "ruff-0.2.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e5cb5526d69bb9143c2e4d2a115d08ffca3d8e0fddc84925a7b54931c96f5c02"}, - {file = "ruff-0.2.1-py3-none-win32.whl", hash = "sha256:6b95ac9ce49b4fb390634d46d6ece32ace3acdd52814671ccaf20b7f60adb232"}, - {file = "ruff-0.2.1-py3-none-win_amd64.whl", hash = "sha256:e3affdcbc2afb6f5bd0eb3130139ceedc5e3f28d206fe49f63073cb9e65988e0"}, - {file = "ruff-0.2.1-py3-none-win_arm64.whl", hash = "sha256:efababa8e12330aa94a53e90a81eb6e2d55f348bc2e71adbf17d9cad23c03ee6"}, - {file = "ruff-0.2.1.tar.gz", hash = "sha256:3b42b5d8677cd0c72b99fcaf068ffc62abb5a19e71b4a3b9cfa50658a0af02f1"}, + {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"}, + {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"}, + {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"}, + {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"}, + {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"}, + {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"}, ] [[package]] @@ -1399,13 +1398,13 @@ redis = ["redis (>=3.4.1,<4.0.0)"] [[package]] name = "sniffio" -version = "1.3.0" +version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" files = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] [[package]] @@ -1526,13 +1525,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.9.0" +version = "4.10.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, - {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, ] [[package]] @@ -1556,13 +1555,13 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", [[package]] name = "virtualenv" -version = "20.25.0" +version = "20.25.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, - {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, + {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, + {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, ] [package.dependencies] @@ -1590,6 +1589,87 @@ docs = ["Sphinx (>=6.0)", "sphinx-rtd-theme (>=1.1.0)"] optional = ["python-socks", "wsaccel"] test = ["websockets"] +[[package]] +name = "websockets" +version = "12.0" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"}, + {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"}, + {file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"}, + {file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"}, + {file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"}, + {file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"}, + {file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"}, + {file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"}, + {file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"}, + {file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"}, + {file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"}, + {file = "websockets-12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438"}, + {file = "websockets-12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2"}, + {file = "websockets-12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7"}, + {file = "websockets-12.0-cp38-cp38-win32.whl", hash = "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62"}, + {file = "websockets-12.0-cp38-cp38-win_amd64.whl", hash = "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892"}, + {file = "websockets-12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d"}, + {file = "websockets-12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28"}, + {file = "websockets-12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9"}, + {file = "websockets-12.0-cp39-cp39-win32.whl", hash = "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6"}, + {file = "websockets-12.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8"}, + {file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"}, + {file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"}, + {file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2"}, + {file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"}, + {file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611"}, + {file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"}, + {file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"}, + {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, +] + [[package]] name = "wheel" version = "0.41.3" @@ -1699,18 +1779,18 @@ files = [ [[package]] name = "zipp" -version = "3.17.0" +version = "3.18.1" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, - {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, + {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, + {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [extras] pgsql = ["psycopg2-binary"] @@ -1718,4 +1798,4 @@ pgsql = ["psycopg2-binary"] [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "d941bf9a1f3f01b6d9e9e16118b1ae6dfa2244b80a6433728a4e67a77420a527" +content-hash = "fc67d56b3e5fe8c4172a3d24a9bd9c552b7ef3e1a9b3ea93db78a098b1d30154" diff --git a/pyproject.toml b/pyproject.toml index d3e7d289..c2931c2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ bip32 = "^3.4" mnemonic = "^0.20" bolt11 = "^2.0.5" pre-commit = "^3.5.0" +websockets = "^12.0" slowapi = "^0.1.9" [tool.poetry.extras] diff --git a/tests/conftest.py b/tests/conftest.py index 6a23880a..ed3dc2f1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,7 +33,8 @@ settings.wallet_unit = "sat" settings.mint_backend_bolt11_sat = settings.mint_backend_bolt11_sat or "FakeWallet" settings.fakewallet_brr = True -settings.fakewallet_delay_payment = False +settings.fakewallet_delay_outgoing_payment = None +settings.fakewallet_delay_incoming_payment = 1 settings.fakewallet_stochastic_invoice = False assert ( settings.mint_test_database != settings.mint_database @@ -44,6 +45,7 @@ settings.mint_private_key = "TEST_PRIVATE_KEY" settings.mint_seed_decryption_key = "" settings.mint_max_balance = 0 +settings.mint_transaction_rate_limit_per_minute = 60 settings.mint_lnd_enable_mpp = True settings.mint_input_fee_ppk = 0 diff --git a/tests/test_mint_db.py b/tests/test_mint_db.py index ea2d9a0d..d4a78529 100644 --- a/tests/test_mint_db.py +++ b/tests/test_mint_db.py @@ -50,9 +50,7 @@ async def test_mint_quote(wallet1: Wallet, ledger: Ledger): async def test_get_mint_quote_by_request(wallet1: Wallet, ledger: Ledger): invoice = await wallet1.request_mint(128) assert invoice is not None - quote = await ledger.crud.get_mint_quote_by_request( - request=invoice.bolt11, db=ledger.db - ) + quote = await ledger.crud.get_mint_quote(request=invoice.bolt11, db=ledger.db) assert quote is not None assert quote.quote == invoice.id assert quote.amount == 128 diff --git a/tests/test_mint_fees.py b/tests/test_mint_fees.py index 106d1fbd..673989bf 100644 --- a/tests/test_mint_fees.py +++ b/tests/test_mint_fees.py @@ -95,7 +95,7 @@ async def test_get_fees_for_proofs(wallet1: Wallet, ledger: Ledger): @pytest.mark.asyncio -@pytest.mark.skipif_with_fees(is_regtest, reason="only works with FakeWallet") +@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet") async def test_wallet_fee(wallet1: Wallet, ledger: Ledger): # THIS TEST IS A FAKE, WE SET THE WALLET FEES MANUALLY IN set_ledger_keyset_fees # It would be better to test if the wallet can get the fees from the mint itself diff --git a/tests/test_mint_init.py b/tests/test_mint_init.py index 3b3c75c8..de8cedd9 100644 --- a/tests/test_mint_init.py +++ b/tests/test_mint_init.py @@ -171,7 +171,7 @@ async def test_startup_fakewallet_pending_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.check_proofs_state([pending_proof.Y]) + states = await ledger.db_read.get_proofs_states([pending_proof.Y]) assert states[0].state == SpentState.pending settings.fakewallet_payment_state = True # run startup routinge @@ -184,7 +184,7 @@ async def test_startup_fakewallet_pending_quote_success(ledger: Ledger): assert not melt_quotes # expect that proofs are spent - states = await ledger.check_proofs_state([pending_proof.Y]) + states = await ledger.db_read.get_proofs_states([pending_proof.Y]) assert states[0].state == SpentState.spent @@ -197,7 +197,7 @@ async def test_startup_fakewallet_pending_quote_failure(ledger: Ledger): The failure is simulated by setting the fakewallet_payment_state to False. """ pending_proof, quote = await create_pending_melts(ledger) - states = await ledger.check_proofs_state([pending_proof.Y]) + states = await ledger.db_read.get_proofs_states([pending_proof.Y]) assert states[0].state == SpentState.pending settings.fakewallet_payment_state = False # run startup routinge @@ -210,7 +210,7 @@ async def test_startup_fakewallet_pending_quote_failure(ledger: Ledger): assert not melt_quotes # expect that proofs are unspent - states = await ledger.check_proofs_state([pending_proof.Y]) + states = await ledger.db_read.get_proofs_states([pending_proof.Y]) assert states[0].state == SpentState.unspent @@ -218,7 +218,7 @@ async def test_startup_fakewallet_pending_quote_failure(ledger: Ledger): @pytest.mark.skipif(is_regtest, reason="only for fake wallet") async def test_startup_fakewallet_pending_quote_pending(ledger: Ledger): pending_proof, quote = await create_pending_melts(ledger) - states = await ledger.check_proofs_state([pending_proof.Y]) + states = await ledger.db_read.get_proofs_states([pending_proof.Y]) assert states[0].state == SpentState.pending settings.fakewallet_payment_state = None # run startup routinge @@ -231,7 +231,7 @@ async def test_startup_fakewallet_pending_quote_pending(ledger: Ledger): assert melt_quotes # expect that proofs are still pending - states = await ledger.check_proofs_state([pending_proof.Y]) + states = await ledger.db_read.get_proofs_states([pending_proof.Y]) assert states[0].state == SpentState.pending @@ -273,7 +273,7 @@ async def test_startup_regtest_pending_quote_pending(wallet: Wallet, ledger: Led assert melt_quotes # expect that proofs are still pending - states = await ledger.check_proofs_state([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 == SpentState.pending for s in states]) # only now settle the invoice @@ -307,7 +307,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.check_proofs_state([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 == SpentState.pending for s in states]) settle_invoice(preimage=preimage) @@ -323,7 +323,7 @@ async def test_startup_regtest_pending_quote_success(wallet: Wallet, ledger: Led assert not melt_quotes # expect that proofs are spent - states = await ledger.check_proofs_state([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 == SpentState.spent for s in states]) @@ -358,7 +358,7 @@ async def test_startup_regtest_pending_quote_failure(wallet: Wallet, ledger: Led await asyncio.sleep(SLEEP_TIME) # expect that proofs are pending - states = await ledger.check_proofs_state([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 == SpentState.pending for s in states]) cancel_invoice(preimage_hash=preimage_hash) @@ -374,5 +374,5 @@ async def test_startup_regtest_pending_quote_failure(wallet: Wallet, ledger: Led assert not melt_quotes # expect that proofs are unspent - states = await ledger.check_proofs_state([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 == SpentState.unspent for s in states]) diff --git a/tests/test_mint_operations.py b/tests/test_mint_operations.py index f3884fb5..2681aa55 100644 --- a/tests/test_mint_operations.py +++ b/tests/test_mint_operations.py @@ -372,5 +372,22 @@ async def test_check_proof_state(wallet1: Wallet, ledger: Ledger): keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10) - proof_states = await ledger.check_proofs_state(Ys=[p.Y for p in send_proofs]) + proof_states = await ledger.db_read.get_proofs_states(Ys=[p.Y for p in send_proofs]) assert all([p.state.value == "UNSPENT" for p in proof_states]) + + +# TODO: test keeps running forever, needs to be fixed +# @pytest.mark.asyncio +# async def test_websocket_quote_updates(wallet1: Wallet, ledger: Ledger): +# invoice = await wallet1.request_mint(64) +# ws = websocket.create_connection( +# f"ws://localhost:{SERVER_PORT}/v1/quote/{invoice.id}" +# ) +# await asyncio.sleep(0.1) +# pay_if_regtest(invoice.bolt11) +# await wallet1.mint(64, id=invoice.id) +# await asyncio.sleep(0.1) +# data = str(ws.recv()) +# ws.close() +# n_lines = len(data.split("\n")) +# assert n_lines == 1 diff --git a/tests/test_mint_regtest.py b/tests/test_mint_regtest.py index e065eaac..bb5eccee 100644 --- a/tests/test_mint_regtest.py +++ b/tests/test_mint_regtest.py @@ -62,7 +62,7 @@ async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger): assert melt_quotes # expect that proofs are still pending - states = await ledger.check_proofs_state([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 == SpentState.pending for s in states]) # only now settle the invoice @@ -70,7 +70,7 @@ async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger): await asyncio.sleep(SLEEP_TIME) # expect that proofs are now spent - states = await ledger.check_proofs_state([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 == SpentState.spent for s in states]) # expect that no melt quote is pending diff --git a/tests/test_wallet_cli.py b/tests/test_wallet_cli.py index eceebe6e..26102da5 100644 --- a/tests/test_wallet_cli.py +++ b/tests/test_wallet_cli.py @@ -109,23 +109,7 @@ def test_balance(cli_prefix): assert result.exit_code == 0 -@pytest.mark.skipif(not is_fake, reason="only on fakewallet") -def test_invoice_automatic_fakewallet(mint, cli_prefix): - runner = CliRunner() - result = runner.invoke( - cli, - [*cli_prefix, "invoice", "1000"], - ) - assert result.exception is None - print("INVOICE") - print(result.output) - wallet = asyncio.run(init_wallet()) - assert wallet.available_balance >= 1000 - assert f"Balance: {wallet.available_balance} sat" in result.output - assert result.exit_code == 0 - - -def test_invoice(mint, cli_prefix): +def test_invoice_return_immediately(mint, cli_prefix): runner = CliRunner() result = runner.invoke( cli, diff --git a/tests/test_wallet_subscription.py b/tests/test_wallet_subscription.py new file mode 100644 index 00000000..d81896da --- /dev/null +++ b/tests/test_wallet_subscription.py @@ -0,0 +1,118 @@ +import asyncio + +import pytest +import pytest_asyncio + +from cashu.core.base import Method, ProofState +from cashu.core.json_rpc.base import JSONRPCNotficationParams +from cashu.core.nuts import WEBSOCKETS_NUT +from cashu.core.settings import settings +from cashu.wallet.wallet import Wallet +from tests.conftest import SERVER_ENDPOINT +from tests.helpers import ( + is_fake, + pay_if_regtest, +) + + +@pytest_asyncio.fixture(scope="function") +async def wallet(mint): + wallet1 = await Wallet.with_db( + url=SERVER_ENDPOINT, + db="test_data/wallet_subscriptions", + name="wallet_subscriptions", + ) + await wallet1.load_mint() + yield wallet1 + + +@pytest.mark.asyncio +@pytest.mark.skipif(is_fake, reason="only regtest") +async def test_wallet_subscription_mint(wallet: Wallet): + if not wallet.mint_info.supports_nut(WEBSOCKETS_NUT): + pytest.skip("No websocket support") + + if not wallet.mint_info.supports_websocket_mint_quote( + Method["bolt11"], wallet.unit + ): + pytest.skip("No websocket support for bolt11_mint_quote") + + triggered = False + msg_stack: list[JSONRPCNotficationParams] = [] + + def callback(msg: JSONRPCNotficationParams): + nonlocal triggered, msg_stack + triggered = True + msg_stack.append(msg) + asyncio.run(wallet.mint(int(invoice.amount), id=invoice.id)) + + invoice, sub = await wallet.request_mint_with_callback(128, callback=callback) + pay_if_regtest(invoice.bolt11) + 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[1].payload["paid"] is True + + assert msg_stack[2].payload["paid"] is True + + +@pytest.mark.asyncio +async def test_wallet_subscription_swap(wallet: Wallet): + if not wallet.mint_info.supports_nut(WEBSOCKETS_NUT): + pytest.skip("No websocket support") + + invoice = await wallet.request_mint(64) + pay_if_regtest(invoice.bolt11) + await wallet.mint(64, id=invoice.id) + + triggered = False + msg_stack: list[JSONRPCNotficationParams] = [] + + def callback(msg: JSONRPCNotficationParams): + nonlocal triggered, msg_stack + triggered = True + msg_stack.append(msg) + + n_subscriptions = len(wallet.proofs) + state, sub = await wallet.check_proof_state_with_callback( + wallet.proofs, callback=callback + ) + + _ = await wallet.split_to_send(wallet.proofs, 64) + + wait = 1 + await asyncio.sleep(wait) + assert triggered + + # we receive 3 messages for each subscription: + # initial state (UNSPENT), pending state (PENDING), spent state (SPENT) + assert len(msg_stack) == n_subscriptions * 3 + + # the first one is the UNSPENT state + pending_stack = msg_stack[:n_subscriptions] + for msg in pending_stack: + proof_state = ProofState.parse_obj(msg.payload) + assert proof_state.state.value == "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" + + # 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"