Mint: add websockets for quote updates (#413)

* add websockets for quote updates

* add test (not working)

* wip: emit events to everyone

* wip: emit events to everyone

* wip, lots of things broken but invoice callback works

* wip

* add wip files

* tests almost passing

* add task

* refactor nut constants

* startup fix

* works with old mints

* wip cli

* fix mypy

* remove automatic invoice test now with websockets

* remove comment

* better logging

* send back response

* add rate limiter to websocket

* add rate limiter to subscriptions

* refactor websocket ratelimit

* websocket tests

* subscription kinds

* doesnt start

* remove circular import

* update

* fix mypy

* move test file in test because it fails if it runs later... dunno why

* adjust websocket NUT-06 settings

* local import and small fix

* disable websockets in CLI if "no_check" is selected

* move subscription test to where it was

* check proof state with callback, add tests

* tests: run mint fixture per module instead of per session

* subscription command name fix

* test per session again

* update test race conditions

* fix tests

* clean up

* tmp

* fix db issues and remove cached secrets

* fix tests

* blindly try pipeline

* remove comments

* comments
This commit is contained in:
callebtc
2024-06-25 19:20:03 +02:00
committed by GitHub
parent 75987beaf1
commit e846acf946
43 changed files with 1701 additions and 519 deletions

View File

@@ -15,7 +15,6 @@ jobs:
os: [ubuntu-latest] os: [ubuntu-latest]
python-version: ["3.10"] python-version: ["3.10"]
poetry-version: ["1.7.1"] poetry-version: ["1.7.1"]
mint-cache-secrets: ["false", "true"]
mint-only-deprecated: ["false", "true"] mint-only-deprecated: ["false", "true"]
mint-database: ["./test_data/test_mint", "postgres://cashu:cashu@localhost:5432/cashu"] mint-database: ["./test_data/test_mint", "postgres://cashu:cashu@localhost:5432/cashu"]
backend-wallet-class: ["FakeWallet"] backend-wallet-class: ["FakeWallet"]
@@ -24,7 +23,6 @@ jobs:
os: ${{ matrix.os }} os: ${{ matrix.os }}
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
poetry-version: ${{ matrix.poetry-version }} poetry-version: ${{ matrix.poetry-version }}
mint-cache-secrets: ${{ matrix.mint-cache-secrets }}
mint-only-deprecated: ${{ matrix.mint-only-deprecated }} mint-only-deprecated: ${{ matrix.mint-only-deprecated }}
mint-database: ${{ matrix.mint-database }} mint-database: ${{ matrix.mint-database }}
regtest: regtest:

View File

@@ -15,16 +15,13 @@ on:
os: os:
default: "ubuntu-latest" default: "ubuntu-latest"
type: string type: string
mint-cache-secrets:
default: "false"
type: string
mint-only-deprecated: mint-only-deprecated:
default: "false" default: "false"
type: string type: string
jobs: jobs:
poetry: 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 }} runs-on: ${{ inputs.os }}
services: services:
postgres: postgres:
@@ -54,7 +51,6 @@ jobs:
MINT_HOST: localhost MINT_HOST: localhost
MINT_PORT: 3337 MINT_PORT: 3337
MINT_TEST_DATABASE: ${{ inputs.mint-database }} MINT_TEST_DATABASE: ${{ inputs.mint-database }}
MINT_CACHE_SECRETS: ${{ inputs.mint-cache-secrets }}
DEBUG_MINT_ONLY_DEPRECATED: ${{ inputs.mint-only-deprecated }} DEBUG_MINT_ONLY_DEPRECATED: ${{ inputs.mint-only-deprecated }}
TOR: false TOR: false
run: | run: |

View File

@@ -7,8 +7,11 @@ from sqlite3 import Row
from typing import Dict, List, Optional, Union from typing import Dict, List, Optional, Union
from loguru import logger 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.aes import AESCipher
from .crypto.b_dhke import hash_to_curve from .crypto.b_dhke import hash_to_curve
from .crypto.keys import ( from .crypto.keys import (
@@ -54,11 +57,27 @@ class SpentState(Enum):
return self.name return self.name
class ProofState(BaseModel): class ProofState(LedgerEvent):
Y: str Y: str
state: SpentState state: SpentState
witness: Optional[str] = None 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): class HTLCWitness(BaseModel):
preimage: Optional[str] = None preimage: Optional[str] = None
@@ -249,7 +268,7 @@ class Invoice(BaseModel):
time_paid: Union[None, str, int, float] = "" time_paid: Union[None, str, int, float] = ""
class MeltQuote(BaseModel): class MeltQuote(LedgerEvent):
quote: str quote: str
method: str method: str
request: str request: str
@@ -290,8 +309,17 @@ class MeltQuote(BaseModel):
proof=row["proof"], proof=row["proof"],
) )
@property
def identifier(self) -> str:
"""Implementation of the abstract method from LedgerEventManager"""
return self.quote
class MintQuote(BaseModel): @property
def kind(self) -> JSONRPCSubscriptionKinds:
return JSONRPCSubscriptionKinds.BOLT11_MELT_QUOTE
class MintQuote(LedgerEvent):
quote: str quote: str
method: str method: str
request: str request: str
@@ -329,6 +357,15 @@ class MintQuote(BaseModel):
paid_time=paid_time, 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 ------- # ------- KEYSETS -------

View File

@@ -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

View File

@@ -33,6 +33,9 @@ class GetInfoResponse(BaseModel):
motd: Optional[str] = None motd: Optional[str] = None
nuts: Optional[Dict[int, Any]] = 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): class Nut15MppSupport(BaseModel):
method: str method: str

13
cashu/core/nuts.py Normal file
View File

@@ -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

View File

@@ -120,14 +120,20 @@ class MintLimits(MintSettings):
title="Maximum mint balance", title="Maximum mint balance",
description="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): class FakeWalletSettings(MintSettings):
fakewallet_brr: bool = Field(default=True) 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_stochastic_invoice: bool = Field(default=False)
fakewallet_payment_state: Optional[bool] = Field(default=None) fakewallet_payment_state: Optional[bool] = Field(default=None)
mint_cache_secrets: bool = Field(default=True)
class MintInformation(CashuSettings): class MintInformation(CashuSettings):

View File

@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Coroutine, Optional, Union from typing import AsyncGenerator, Coroutine, Optional, Union
from pydantic import BaseModel from pydantic import BaseModel
@@ -68,6 +68,7 @@ class PaymentStatus(BaseModel):
class LightningBackend(ABC): class LightningBackend(ABC):
supports_mpp: bool = False supports_mpp: bool = False
supports_incoming_payment_stream: bool = False
supported_units: set[Unit] supported_units: set[Unit]
unit: Unit unit: Unit
@@ -124,9 +125,9 @@ class LightningBackend(ABC):
# ) -> InvoiceQuoteResponse: # ) -> InvoiceQuoteResponse:
# pass # pass
# @abstractmethod @abstractmethod
# def paid_invoices_stream(self) -> AsyncGenerator[str, None]: def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
# pass pass
class Unsupported(Exception): class Unsupported(Exception):

View File

@@ -1,8 +1,7 @@
# type: ignore # type: ignore
import asyncio
import json import json
import math import math
from typing import Dict, Optional, Union from typing import AsyncGenerator, Dict, Optional, Union
import bolt11 import bolt11
import httpx import httpx
@@ -454,10 +453,5 @@ class BlinkWallet(LightningBackend):
amount=amount.to(self.unit, round="up"), amount=amount.to(self.unit, round="up"),
) )
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
async def main(): raise NotImplementedError("paid_invoices_stream not implemented")
pass
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -29,6 +29,7 @@ from .macaroon import load_macaroon
class CoreLightningRestWallet(LightningBackend): class CoreLightningRestWallet(LightningBackend):
supported_units = set([Unit.sat, Unit.msat]) supported_units = set([Unit.sat, Unit.msat])
unit = Unit.sat unit = Unit.sat
supports_incoming_payment_stream: bool = True
def __init__(self, unit: Unit = Unit.sat, **kwargs): def __init__(self, unit: Unit = Unit.sat, **kwargs):
self.assert_unit_supported(unit) self.assert_unit_supported(unit)

View File

@@ -4,7 +4,7 @@ import math
import random import random
from datetime import datetime from datetime import datetime
from os import urandom from os import urandom
from typing import AsyncGenerator, Dict, Optional, Set from typing import AsyncGenerator, Dict, List, Optional
from bolt11 import ( from bolt11 import (
Bolt11, Bolt11,
@@ -31,9 +31,11 @@ from .base import (
class FakeWallet(LightningBackend): class FakeWallet(LightningBackend):
fake_btc_price = 1e8 / 1337 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() 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" secret: str = "FAKEWALLET SECRET"
privkey: str = hashlib.pbkdf2_hmac( privkey: str = hashlib.pbkdf2_hmac(
"sha256", "sha256",
@@ -46,6 +48,8 @@ class FakeWallet(LightningBackend):
supported_units = set([Unit.sat, Unit.msat, Unit.usd]) supported_units = set([Unit.sat, Unit.msat, Unit.usd])
unit = Unit.sat unit = Unit.sat
supports_incoming_payment_stream: bool = True
def __init__(self, unit: Unit = Unit.sat, **kwargs): def __init__(self, unit: Unit = Unit.sat, **kwargs):
self.assert_unit_supported(unit) self.assert_unit_supported(unit)
self.unit = unit self.unit = unit
@@ -53,6 +57,23 @@ class FakeWallet(LightningBackend):
async def status(self) -> StatusResponse: async def status(self) -> StatusResponse:
return StatusResponse(error_message=None, balance=1337) 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( async def create_invoice(
self, self,
amount: Amount, amount: Amount,
@@ -106,8 +127,16 @@ class FakeWallet(LightningBackend):
tags=tags, 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) payment_request = encode(bolt11, self.privkey)
if settings.fakewallet_brr:
asyncio.create_task(self.mark_invoice_paid(bolt11))
return InvoiceResponse( return InvoiceResponse(
ok=True, checking_id=payment_hash, payment_request=payment_request ok=True, checking_id=payment_hash, payment_request=payment_request
) )
@@ -115,12 +144,15 @@ class FakeWallet(LightningBackend):
async def pay_invoice(self, quote: MeltQuote, fee_limit: int) -> PaymentResponse: async def pay_invoice(self, quote: MeltQuote, fee_limit: int) -> PaymentResponse:
invoice = decode(quote.request) invoice = decode(quote.request)
if settings.fakewallet_delay_payment: if settings.fakewallet_delay_outgoing_payment:
await asyncio.sleep(5) await asyncio.sleep(settings.fakewallet_delay_outgoing_payment)
if invoice.payment_hash in self.payment_secrets or settings.fakewallet_brr: if invoice.payment_hash in self.payment_secrets or settings.fakewallet_brr:
await self.queue.put(invoice) if invoice not in self.paid_invoices_outgoing:
self.paid_invoices.add(invoice.payment_hash) self.paid_invoices_outgoing.append(invoice)
else:
raise ValueError("Invoice already paid")
return PaymentResponse( return PaymentResponse(
ok=True, ok=True,
checking_id=invoice.payment_hash, checking_id=invoice.payment_hash,
@@ -133,26 +165,24 @@ class FakeWallet(LightningBackend):
) )
async def get_invoice_status(self, checking_id: str) -> PaymentStatus: async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
if settings.fakewallet_stochastic_invoice: paid = False
paid = random.random() > 0.7 if settings.fakewallet_brr or (
return PaymentStatus(paid=paid) settings.fakewallet_stochastic_invoice and random.random() > 0.7
paid = checking_id in self.paid_invoices or settings.fakewallet_brr ):
return PaymentStatus(paid=paid or None) 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: async def get_payment_status(self, _: str) -> PaymentStatus:
return PaymentStatus(paid=settings.fakewallet_payment_state) 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( async def get_payment_quote(
self, melt_quote: PostMeltQuoteRequest self, melt_quote: PostMeltQuoteRequest
) -> PaymentQuoteResponse: ) -> PaymentQuoteResponse:
@@ -176,3 +206,8 @@ class FakeWallet(LightningBackend):
fee=fees.to(self.unit, round="up"), fee=fees.to(self.unit, round="up"),
amount=amount.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

View File

@@ -1,5 +1,5 @@
# type: ignore # type: ignore
from typing import Optional from typing import AsyncGenerator, Optional
import httpx import httpx
from bolt11 import ( from bolt11 import (
@@ -182,3 +182,6 @@ class LNbitsWallet(LightningBackend):
fee=fees.to(self.unit, round="up"), fee=fees.to(self.unit, round="up"),
amount=amount.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")

View File

@@ -31,6 +31,7 @@ class LndRestWallet(LightningBackend):
"""https://api.lightning.community/rest/index.html#lnd-rest-api-reference""" """https://api.lightning.community/rest/index.html#lnd-rest-api-reference"""
supports_mpp = settings.mint_lnd_enable_mpp supports_mpp = settings.mint_lnd_enable_mpp
supports_incoming_payment_stream = True
supported_units = set([Unit.sat, Unit.msat]) supported_units = set([Unit.sat, Unit.msat])
unit = Unit.sat unit = Unit.sat

View File

@@ -1,6 +1,6 @@
# type: ignore # type: ignore
import secrets import secrets
from typing import Dict, Optional from typing import AsyncGenerator, Dict, Optional
import httpx import httpx
@@ -199,3 +199,6 @@ class StrikeUSDWallet(LightningBackend):
fee_msat=data["details"]["fee"], fee_msat=data["details"]["fee"],
preimage=data["preimage"], preimage=data["preimage"],
) )
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
raise NotImplementedError("paid_invoices_stream not implemented")

View File

@@ -173,7 +173,9 @@ class LedgerCrud(ABC):
async def get_mint_quote( async def get_mint_quote(
self, self,
*, *,
quote_id: str, quote_id: Optional[str] = None,
checking_id: Optional[str] = None,
request: Optional[str] = None,
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> Optional[MintQuote]: ) -> Optional[MintQuote]:
@@ -223,9 +225,10 @@ class LedgerCrud(ABC):
async def get_melt_quote( async def get_melt_quote(
self, self,
*, *,
quote_id: str, quote_id: Optional[str] = None,
db: Database,
checking_id: Optional[str] = None, checking_id: Optional[str] = None,
request: Optional[str] = None,
db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> Optional[MeltQuote]: ) -> Optional[MeltQuote]:
... ...
@@ -450,17 +453,36 @@ class LedgerCrudSqlite(LedgerCrud):
async def get_mint_quote( async def get_mint_quote(
self, self,
*, *,
quote_id: str, quote_id: Optional[str] = None,
checking_id: Optional[str] = None,
request: Optional[str] = None,
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> Optional[MintQuote]: ) -> 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( row = await (conn or db).fetchone(
f""" f"""
SELECT * from {table_with_schema(db, 'mint_quotes')} 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 return MintQuote.from_row(row) if row else None
async def get_mint_quote_by_request( async def get_mint_quote_by_request(
@@ -546,10 +568,10 @@ class LedgerCrudSqlite(LedgerCrud):
async def get_melt_quote( async def get_melt_quote(
self, self,
*, *,
quote_id: str, quote_id: Optional[str] = None,
db: Database,
checking_id: Optional[str] = None, checking_id: Optional[str] = None,
request: Optional[str] = None, request: Optional[str] = None,
db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> Optional[MeltQuote]: ) -> Optional[MeltQuote]:
clauses = [] clauses = []
@@ -563,9 +585,10 @@ class LedgerCrudSqlite(LedgerCrud):
if request: if request:
clauses.append("request = ?") clauses.append("request = ?")
values.append(request) values.append(request)
where = "" if not any(clauses):
if clauses: raise ValueError("No search criteria")
where = f"WHERE {' AND '.join(clauses)}" where = f"WHERE {' AND '.join(clauses)}"
row = await (conn or db).fetchone( row = await (conn or db).fetchone(
f""" f"""
SELECT * from {table_with_schema(db, 'melt_quotes')} SELECT * from {table_with_schema(db, 'melt_quotes')}

68
cashu/mint/db/read.py Normal file
View File

@@ -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

97
cashu/mint/db/write.py Normal file
View File

@@ -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.")

View File

231
cashu/mint/events/client.py Normal file
View File

@@ -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)

View File

@@ -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

View File

@@ -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)
)

102
cashu/mint/features.py Normal file
View File

@@ -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

View File

@@ -52,16 +52,20 @@ from ..lightning.base import (
) )
from ..mint.crud import LedgerCrudSqlite from ..mint.crud import LedgerCrudSqlite
from .conditions import LedgerSpendingConditions 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 from .verification import LedgerVerification
class Ledger(LedgerVerification, LedgerSpendingConditions): class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFeatures):
backends: Mapping[Method, Mapping[Unit, LightningBackend]] = {} backends: Mapping[Method, Mapping[Unit, LightningBackend]] = {}
locks: Dict[str, asyncio.Lock] = {} # holds multiprocessing locks 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] = {} keysets: Dict[str, MintKeyset] = {}
events = LedgerEventManager()
db_read: DbReadHelper
def __init__( def __init__(
self, self,
@@ -92,17 +96,17 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
self.crud = crud self.crud = crud
self.backends = backends self.backends = backends
self.pubkey = derive_pubkey(self.seed) 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 ------- # ------- STARTUP -------
async def startup_ledger(self): async def startup_ledger(self):
await self._startup_ledger() await self._startup_ledger()
await self._check_pending_proofs_and_melt_quotes() await self._check_pending_proofs_and_melt_quotes()
await self.dispatch_listeners()
async def _startup_ledger(self): async def _startup_ledger(self):
if settings.mint_cache_secrets:
await self.load_used_proofs()
await self.init_keysets() await self.init_keysets()
for derivation_path in settings.mint_derivation_path_list: for derivation_path in settings.mint_derivation_path_list:
@@ -158,12 +162,12 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
proofs=pending_proofs, quote_id=quote.quote proofs=pending_proofs, quote_id=quote.quote
) )
# unset pending # unset pending
await self._unset_proofs_pending(pending_proofs) await self.db_write._unset_proofs_pending(pending_proofs)
elif payment.failed: elif payment.failed:
logger.info(f"Melt quote {quote.quote} state: failed") logger.info(f"Melt quote {quote.quote} state: failed")
# unset pending # unset pending
await self._unset_proofs_pending(pending_proofs) await self.db_write._unset_proofs_pending(pending_proofs, spent=False)
elif payment.pending: elif payment.pending:
logger.info(f"Melt quote {quote.quote} state: pending") logger.info(f"Melt quote {quote.quote} state: pending")
pass pass
@@ -291,13 +295,15 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
proofs (List[Proof]): Proofs to add to known secret table. 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. 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: async with get_db_connection(self.db, conn) as conn:
# store in db # store in db
for p in proofs: for p in proofs:
await self.crud.invalidate_proof( await self.crud.invalidate_proof(
proof=p, db=self.db, quote_id=quote_id, conn=conn 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( async def _generate_change_promises(
self, self,
@@ -426,10 +432,9 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
created_time=int(time.time()), created_time=int(time.time()),
expiry=expiry, expiry=expiry,
) )
await self.crud.store_mint_quote( await self.crud.store_mint_quote(quote=quote, db=self.db)
quote=quote, await self.events.submit(quote)
db=self.db,
)
return quote return quote
async def get_mint_quote(self, quote_id: str) -> MintQuote: async def get_mint_quote(self, quote_id: str) -> MintQuote:
@@ -462,6 +467,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
quote.paid = True quote.paid = True
quote.paid_time = int(time.time()) quote.paid_time = int(time.time())
await self.crud.update_mint_quote(quote=quote, db=self.db) await self.crud.update_mint_quote(quote=quote, db=self.db)
await self.events.submit(quote)
return quote return quote
@@ -510,12 +516,16 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
if quote.expiry and quote.expiry > int(time.time()): if quote.expiry and quote.expiry > int(time.time()):
raise TransactionError("quote expired") raise TransactionError("quote expired")
promises = await self._generate_promises(outputs)
logger.trace("generated promises")
logger.trace(f"crud: setting quote {quote_id} as issued") logger.trace(f"crud: setting quote {quote_id} as issued")
quote.issued = True quote.issued = True
await self.crud.update_mint_quote(quote=quote, db=self.db) 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] del self.locks[quote_id]
return promises return promises
@@ -605,9 +615,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
# check if there is a mint quote with the same payment request # check if there is a mint quote with the same payment request
# so that we would be able to handle the transaction internally # so that we would be able to handle the transaction internally
# and therefore respond with internal transaction fees (0 for now) # and therefore respond with internal transaction fees (0 for now)
mint_quote = await self.crud.get_mint_quote_by_request( mint_quote = await self.crud.get_mint_quote(request=request, db=self.db)
request=request, db=self.db
)
if mint_quote: if mint_quote:
payment_quote = self.create_internal_melt_quote(mint_quote, melt_quote) payment_quote = self.create_internal_melt_quote(mint_quote, melt_quote)
@@ -655,6 +663,8 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
expiry=expiry, expiry=expiry,
) )
await self.crud.store_melt_quote(quote=quote, db=self.db) await self.crud.store_melt_quote(quote=quote, db=self.db)
await self.events.submit(quote)
return PostMeltQuoteResponse( return PostMeltQuoteResponse(
quote=quote.quote, quote=quote.quote,
amount=quote.amount, amount=quote.amount,
@@ -689,7 +699,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
# we only check the state with the backend if there is no associated internal # we only check the state with the backend if there is no associated internal
# mint quote for this melt quote # 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 request=melt_quote.request, db=self.db
) )
@@ -710,6 +720,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
melt_quote.proof = status.preimage melt_quote.proof = status.preimage
melt_quote.paid_time = int(time.time()) melt_quote.paid_time = int(time.time())
await self.crud.update_melt_quote(quote=melt_quote, db=self.db) await self.crud.update_melt_quote(quote=melt_quote, db=self.db)
await self.events.submit(melt_quote)
return melt_quote return melt_quote
@@ -733,7 +744,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
""" """
# first we check if there is a mint quote with the same payment request # 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 # 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 request=melt_quote.request, db=self.db
) )
if not mint_quote: if not mint_quote:
@@ -774,10 +785,13 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
mint_quote.paid = True mint_quote.paid = True
mint_quote.paid_time = melt_quote.paid_time 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_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.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 return melt_quote
async def melt( async def melt(
@@ -847,7 +861,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
await self.verify_inputs_and_outputs(proofs=proofs) await self.verify_inputs_and_outputs(proofs=proofs)
# set proofs to pending to avoid race conditions # 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: try:
# settle the transaction internally if there is a mint quote with the same payment request # 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) melt_quote = await self.melt_mint_settle_internally(melt_quote, proofs)
@@ -875,6 +889,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
melt_quote.paid = True melt_quote.paid = True
melt_quote.paid_time = int(time.time()) melt_quote.paid_time = int(time.time())
await self.crud.update_melt_quote(quote=melt_quote, db=self.db) await self.crud.update_melt_quote(quote=melt_quote, db=self.db)
await self.events.submit(melt_quote)
# melt successful, invalidate proofs # melt successful, invalidate proofs
await self._invalidate_proofs(proofs=proofs, quote_id=melt_quote.quote) await self._invalidate_proofs(proofs=proofs, quote_id=melt_quote.quote)
@@ -894,7 +909,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
raise e raise e
finally: finally:
# delete proofs from pending list # 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 return melt_quote.proof or "", return_promises
@@ -928,7 +943,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
# verify spending inputs, outputs, and spending conditions # verify spending inputs, outputs, and spending conditions
await self.verify_inputs_and_outputs(proofs=proofs, outputs=outputs) 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: try:
# Mark proofs as used and prepare new promises # Mark proofs as used and prepare new promises
async with get_db_connection(self.db) as conn: async with get_db_connection(self.db) as conn:
@@ -941,7 +956,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
raise e raise e
finally: finally:
# delete proofs from pending list # delete proofs from pending list
await self._unset_proofs_pending(proofs) await self.db_write._unset_proofs_pending(proofs)
logger.trace("split successful") logger.trace("split successful")
return promises return promises
@@ -951,7 +966,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
) -> Tuple[List[BlindedMessage], List[BlindedSignature]]: ) -> Tuple[List[BlindedMessage], List[BlindedSignature]]:
signatures: List[BlindedSignature] = [] signatures: List[BlindedSignature] = []
return_outputs: List[BlindedMessage] = [] return_outputs: List[BlindedMessage] = []
async with self.db.connect() as conn: async with get_db_connection(self.db) as conn:
for output in outputs: for output in outputs:
logger.trace(f"looking for promise: {output}") logger.trace(f"looking for promise: {output}")
promise = await self.crud.get_promise( promise = await self.crud.get_promise(
@@ -1030,105 +1045,3 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
) )
signatures.append(signature) signatures.append(signature)
return signatures 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.")

View File

@@ -1,5 +1,6 @@
from fastapi import status from fastapi import WebSocket, status
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from limits import RateLimitItemPerMinute
from loguru import logger from loguru import logger
from slowapi import Limiter from slowapi import Limiter
from slowapi.util import get_remote_address from slowapi.util import get_remote_address
@@ -39,3 +40,57 @@ limiter = Limiter(
default_limits=[f"{settings.mint_transaction_rate_limit_per_minute}/minute"], default_limits=[f"{settings.mint_transaction_rate_limit_per_minute}/minute"],
enabled=settings.mint_rate_limit, 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)

View File

@@ -4,6 +4,7 @@ from ..core.base import Method, MintKeyset, Unit
from ..core.db import Database from ..core.db import Database
from ..lightning.base import LightningBackend from ..lightning.base import LightningBackend
from ..mint.crud import LedgerCrud from ..mint.crud import LedgerCrud
from .events.events import LedgerEventManager
class SupportsKeysets(Protocol): class SupportsKeysets(Protocol):
@@ -18,3 +19,7 @@ class SupportsBackends(Protocol):
class SupportsDb(Protocol): class SupportsDb(Protocol):
db: Database db: Database
crud: LedgerCrud crud: LedgerCrud
class SupportsEvents(Protocol):
events: LedgerEventManager

View File

@@ -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 loguru import logger
from ..core.errors import KeysetNotFoundError from ..core.errors import KeysetNotFoundError
@@ -10,7 +10,6 @@ from ..core.models import (
KeysetsResponseKeyset, KeysetsResponseKeyset,
KeysResponse, KeysResponse,
KeysResponseKeyset, KeysResponseKeyset,
MintMeltMethodSetting,
PostCheckStateRequest, PostCheckStateRequest,
PostCheckStateResponse, PostCheckStateResponse,
PostMeltQuoteRequest, PostMeltQuoteRequest,
@@ -28,7 +27,7 @@ from ..core.models import (
) )
from ..core.settings import settings from ..core.settings import settings
from ..mint.startup import ledger from ..mint.startup import ledger
from .limit import limiter from .limit import limit_websocket, limiter
router: APIRouter = APIRouter() router: APIRouter = APIRouter()
@@ -42,59 +41,7 @@ router: APIRouter = APIRouter()
) )
async def info() -> GetInfoResponse: async def info() -> GetInfoResponse:
logger.trace("> GET /v1/info") logger.trace("> GET /v1/info")
mint_features = ledger.mint_features()
# 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,
}
)
return GetInfoResponse( return GetInfoResponse(
name=settings.mint_info_name, name=settings.mint_info_name,
pubkey=ledger.pubkey.serialize().hex() if ledger.pubkey else None, 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 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( @router.post(
"/v1/mint/bolt11", "/v1/mint/bolt11",
name="Mint tokens with a Lightning payment", name="Mint tokens with a Lightning payment",
@@ -385,7 +352,7 @@ async def check_state(
) -> PostCheckStateResponse: ) -> PostCheckStateResponse:
"""Check whether a secret has been spent already or not.""" """Check whether a secret has been spent already or not."""
logger.trace(f"> POST /v1/checkstate: {payload}") 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) return PostCheckStateResponse(states=proof_states)

View File

@@ -341,7 +341,7 @@ async def check_spendable_deprecated(
) -> CheckSpendableResponse_deprecated: ) -> CheckSpendableResponse_deprecated:
"""Check whether a secret has been spent already or not.""" """Check whether a secret has been spent already or not."""
logger.trace(f"> POST /check: {payload}") 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] = [] spendableList: List[bool] = []
pendingList: List[bool] = [] pendingList: List[bool] = []
for proof_state in proofs_state: for proof_state in proofs_state:

45
cashu/mint/tasks.py Normal file
View File

@@ -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)

View File

@@ -35,7 +35,6 @@ class LedgerVerification(
keyset: MintKeyset keyset: MintKeyset
keysets: Dict[str, MintKeyset] keysets: Dict[str, MintKeyset]
spent_proofs: Dict[str, Proof]
crud: LedgerCrud crud: LedgerCrud
db: Database db: Database
lightning: Dict[Unit, LightningBackend] lightning: Dict[Unit, LightningBackend]
@@ -128,11 +127,14 @@ class LedgerVerification(
if not self._verify_no_duplicate_outputs(outputs): if not self._verify_no_duplicate_outputs(outputs):
raise TransactionError("duplicate outputs.") raise TransactionError("duplicate outputs.")
# verify that outputs have not been signed previously # 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.") raise TransactionError("outputs have already been signed before.")
logger.trace(f"Verified {len(outputs)} outputs.") 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 """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). (which would lead to a duplication error later when trying to store these outputs again).
@@ -164,21 +166,12 @@ class LedgerVerification(
The key is the Y=h2c(secret) and the value is the proof. The key is the Y=h2c(secret) and the value is the proof.
""" """
proofs_spent_dict: Dict[str, Proof] = {} proofs_spent_dict: Dict[str, Proof] = {}
if settings.mint_cache_secrets: # check used secrets in database
# check used secrets in memory async with self.db.connect() as conn:
for Y in Ys: 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: if spent_proof:
proofs_spent_dict[Y] = 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 return proofs_spent_dict
def _verify_secret_criteria(self, proof: Proof) -> Literal[True]: def _verify_secret_criteria(self, proof: Proof) -> Literal[True]:

View File

@@ -9,15 +9,17 @@ from itertools import groupby, islice
from operator import itemgetter from operator import itemgetter
from os import listdir from os import listdir
from os.path import isdir, join from os.path import isdir, join
from typing import Optional from typing import Optional, Union
import click import click
from click import Context from click import Context
from loguru import logger 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.helpers import sum_proofs
from ...core.json_rpc.base import JSONRPCNotficationParams
from ...core.logging import configure_logger from ...core.logging import configure_logger
from ...core.models import PostMintQuoteResponse
from ...core.settings import settings from ...core.settings import settings
from ...nostr.client.client import NostrClient from ...nostr.client.client import NostrClient
from ...tor.tor import TorProxy from ...tor.tor import TorProxy
@@ -44,6 +46,7 @@ from ..helpers import (
send, send,
) )
from ..nostr import receive_nostr, send_nostr from ..nostr import receive_nostr, send_nostr
from ..subscriptions import SubscriptionManager
class NaturalOrderGroup(click.Group): 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." 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 # user requests an invoice
if amount and not id: 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: if invoice.bolt11:
print("") print("")
print(f"Pay invoice to mint {wallet.unit.str(amount)}:") 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: if no_check:
return return
check_until = time.time() + 5 * 60 # check for five minutes
print("") print("")
print( print(
"Checking invoice ...", "Checking invoice ...",
end="", end="",
flush=True, flush=True,
) )
paid = False if mint_supports_websockets:
while time.time() < check_until and not paid: while not paid:
time.sleep(3) await asyncio.sleep(0.1)
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."
)
# user paid invoice and want to check it # we still check manually every 10 seconds
check_until = time.time() + 5 * 60 # check for five minutes
while time.time() < check_until and not paid:
await asyncio.sleep(5)
try:
await wallet.mint(amount, split=optional_split, id=invoice.id)
paid = True
except Exception as e:
# TODO: user error codes!
if "not paid" in str(e):
print(".", end="", flush=True)
continue
else:
print(f"Error: {str(e)}")
if not paid:
print("\n")
print(
"Invoice is not paid yet, stopping check. Use the command above to"
" recheck after the invoice has been paid."
)
# user paid invoice before and wants to check the quote id
elif amount and id: elif amount and id:
await wallet.mint(amount, split=optional_split, id=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("") print("")
await print_balance(ctx) await print_balance(ctx)
return return
@@ -434,7 +493,6 @@ async def balance(ctx: Context, verbose):
) )
@click.option( @click.option(
"--legacy", "--legacy",
"-l",
default=False, default=False,
is_flag=True, is_flag=True,
help="Print legacy token without mint information.", help="Print legacy token without mint information.",

View File

@@ -2,7 +2,9 @@ from typing import Any, Dict, List, Optional
from pydantic import BaseModel 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 from ..core.models import Nut15MppSupport
@@ -27,8 +29,8 @@ class MintInfo(BaseModel):
def supports_mpp(self, method: str, unit: Unit) -> bool: def supports_mpp(self, method: str, unit: Unit) -> bool:
if not self.nuts: if not self.nuts:
return False return False
nut_15 = self.nuts.get(15) nut_15 = self.nuts.get(MPP_NUT)
if not nut_15 or not self.supports_nut(15): if not nut_15 or not self.supports_nut(MPP_NUT):
return False return False
for entry in nut_15: for entry in nut_15:
@@ -37,3 +39,15 @@ class MintInfo(BaseModel):
return True return True
return False 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

View File

@@ -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

View File

@@ -1,11 +1,14 @@
import copy import copy
import threading
import time import time
from typing import Dict, List, Optional, Tuple, Union from typing import Callable, Dict, List, Optional, Tuple, Union
import bolt11 import bolt11
from bip32 import BIP32 from bip32 import BIP32
from loguru import logger from loguru import logger
from cashu.core.json_rpc.base import JSONRPCSubscriptionKinds
from ..core.base import ( from ..core.base import (
BlindedMessage, BlindedMessage,
BlindedSignature, BlindedSignature,
@@ -50,6 +53,7 @@ from .mint_info import MintInfo
from .p2pk import WalletP2PK from .p2pk import WalletP2PK
from .proofs import WalletProofs from .proofs import WalletProofs
from .secrets import WalletSecrets from .secrets import WalletSecrets
from .subscriptions import SubscriptionManager
from .transactions import WalletTransactions from .transactions import WalletTransactions
from .v1_api import LedgerAPI from .v1_api import LedgerAPI
@@ -312,11 +316,47 @@ class Wallet(
raise Exception(f"secret already used: {s}") raise Exception(f"secret already used: {s}")
logger.trace("Secret check complete.") 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: async def request_mint(self, amount: int) -> Invoice:
"""Request a Lightning invoice for minting tokens. """Request a Lightning invoice for minting tokens.
Args: Args:
amount (int): Amount for Lightning invoice in satoshis 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: Returns:
PostMintQuoteResponse: Mint Quote Response PostMintQuoteResponse: Mint Quote Response
@@ -684,6 +724,20 @@ class Wallet(
async def check_proof_state(self, proofs) -> PostCheckStateResponse: async def check_proof_state(self, proofs) -> PostCheckStateResponse:
return await super().check_proof_state(proofs) 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 ---------- # ---------- TOKEN MECHANICS ----------
# ---------- DLEQ PROOFS ---------- # ---------- DLEQ PROOFS ----------
@@ -1070,6 +1124,8 @@ class Wallet(
balances_return[key]["unit"] = unit.name balances_return[key]["unit"] = unit.name
return dict(sorted(balances_return.items(), key=lambda item: item[0])) # type: ignore return dict(sorted(balances_return.items(), key=lambda item: item[0])) # type: ignore
# ---------- RESTORE WALLET ----------
async def restore_tokens_for_keyset( async def restore_tokens_for_keyset(
self, keyset_id: str, to: int = 2, batch: int = 25 self, keyset_id: str, to: int = 2, batch: int = 25
) -> None: ) -> None:

452
poetry.lock generated
View File

@@ -290,63 +290,63 @@ files = [
[[package]] [[package]]
name = "coverage" name = "coverage"
version = "7.4.1" version = "7.4.4"
description = "Code coverage measurement for Python" description = "Code coverage measurement for Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"},
{file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"},
{file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"},
{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.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"},
{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.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.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"},
{file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"},
{file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"},
{file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"},
{file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"},
{file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"},
{file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"},
{file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"},
{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.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"},
{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.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.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"},
{file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"},
{file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"},
{file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"},
{file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"},
{file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"},
{file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"},
{file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"},
{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.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"},
{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.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.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"},
{file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"},
{file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"},
{file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"},
{file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"},
{file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"},
{file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"},
{file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"},
{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.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"},
{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.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.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"},
{file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"},
{file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"},
{file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"},
{file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"},
{file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"},
{file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"},
{file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"},
{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.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"},
{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.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.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"},
{file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"},
{file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"},
{file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"},
{file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"},
{file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"},
{file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"},
] ]
[package.dependencies] [package.dependencies]
@@ -503,12 +503,12 @@ all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)"
[[package]] [[package]]
name = "fastapi-profiler" name = "fastapi-profiler"
version = "1.2.0" version = "1.3.0"
description = "A FastAPI Middleware of pyinstrument to check your service performance." description = "A FastAPI Middleware of pyinstrument to check your service performance."
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ 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] [package.dependencies]
@@ -517,18 +517,18 @@ pyinstrument = ">=4.4.0"
[[package]] [[package]]
name = "filelock" name = "filelock"
version = "3.13.1" version = "3.13.3"
description = "A platform independent file lock." description = "A platform independent file lock."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, {file = "filelock-3.13.3-py3-none-any.whl", hash = "sha256:5ffa845303983e7a0b7ae17636509bc97997d58afeafa72fb141a17b152284cb"},
{file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, {file = "filelock-3.13.3.tar.gz", hash = "sha256:a79895a25bbefdf55d1a2a0a80968f7dbb28edcd6d4234a0afb3f37ecde4b546"},
] ]
[package.extras] [package.extras]
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] 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)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.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)"] typing = ["typing-extensions (>=4.8)"]
[[package]] [[package]]
@@ -544,13 +544,13 @@ files = [
[[package]] [[package]]
name = "httpcore" name = "httpcore"
version = "1.0.3" version = "1.0.5"
description = "A minimal low-level HTTP client." description = "A minimal low-level HTTP client."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "httpcore-1.0.3-py3-none-any.whl", hash = "sha256:9a6a501c3099307d9fd76ac244e08503427679b1e81ceb1d922485e2f2462ad2"}, {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"},
{file = "httpcore-1.0.3.tar.gz", hash = "sha256:5c0f9546ad17dac4d0772b0808856eb616eb8b48ce94f49ed819fd6982a8a544"}, {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"},
] ]
[package.dependencies] [package.dependencies]
@@ -561,7 +561,7 @@ h11 = ">=0.13,<0.15"
asyncio = ["anyio (>=4.0,<5.0)"] asyncio = ["anyio (>=4.0,<5.0)"]
http2 = ["h2 (>=3,<5)"] http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"] socks = ["socksio (==1.*)"]
trio = ["trio (>=0.22.0,<0.24.0)"] trio = ["trio (>=0.22.0,<0.26.0)"]
[[package]] [[package]]
name = "httpx" name = "httpx"
@@ -590,13 +590,13 @@ socks = ["socksio (==1.*)"]
[[package]] [[package]]
name = "identify" name = "identify"
version = "2.5.34" version = "2.5.35"
description = "File identification library for Python" description = "File identification library for Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "identify-2.5.34-py2.py3-none-any.whl", hash = "sha256:a4316013779e433d08b96e5eabb7f641e6c7942e4ab5d4c509ebd2e7a8994aed"}, {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"},
{file = "identify-2.5.34.tar.gz", hash = "sha256:ee17bc9d499899bc9eaec1ac7bf2dc9eedd480db9d88b96d123d3b64a9d34f5d"}, {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"},
] ]
[package.extras] [package.extras]
@@ -634,13 +634,13 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs
[[package]] [[package]]
name = "importlib-resources" name = "importlib-resources"
version = "6.3.1" version = "6.4.0"
description = "Read resources from Python packages" description = "Read resources from Python packages"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "importlib_resources-6.3.1-py3-none-any.whl", hash = "sha256:4811639ca7fa830abdb8e9ca0a104dc6ad13de691d9fe0d3173a71304f068159"}, {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"},
{file = "importlib_resources-6.3.1.tar.gz", hash = "sha256:29a3d16556e330c3c8fb8202118c5ff41241cc34cbfb25989bbad226d99b7995"}, {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"},
] ]
[package.dependencies] [package.dependencies]
@@ -648,7 +648,7 @@ zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
[package.extras] [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"] 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]] [[package]]
name = "iniconfig" name = "iniconfig"
@@ -663,19 +663,19 @@ files = [
[[package]] [[package]]
name = "limits" name = "limits"
version = "3.10.0" version = "3.10.1"
description = "Rate limiting utilities" description = "Rate limiting utilities"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "limits-3.10.0-py3-none-any.whl", hash = "sha256:3e617a580f57a21b39393f833c27ad0378c87b309e908c154ee69e6740041959"}, {file = "limits-3.10.1-py3-none-any.whl", hash = "sha256:446242f5a6f7b8c7744e286a70793264ed81bca97860f94b821347284d14fbe9"},
{file = "limits-3.10.0.tar.gz", hash = "sha256:6e657dccafce64fd8ee023ebf4593cd47e9eac841fd1dec3448f48673ba10b7c"}, {file = "limits-3.10.1.tar.gz", hash = "sha256:1ee31d169d498da267a1b72183ae5940afc64b17b4ed4dfd977f6ea5607c2cfb"},
] ]
[package.dependencies] [package.dependencies]
deprecated = ">=1.2" deprecated = ">=1.2"
importlib-resources = ">=1.3" importlib-resources = ">=1.3"
packaging = ">=21,<24" packaging = ">=21,<25"
typing-extensions = "*" typing-extensions = "*"
[package.extras] [package.extras]
@@ -710,22 +710,21 @@ dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptio
[[package]] [[package]]
name = "marshmallow" name = "marshmallow"
version = "3.20.2" version = "3.21.1"
description = "A lightweight library for converting complex datatypes to and from native Python datatypes." description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "marshmallow-3.20.2-py3-none-any.whl", hash = "sha256:c21d4b98fee747c130e6bc8f45c4b3199ea66bc00c12ee1f639f0aeca034d5e9"}, {file = "marshmallow-3.21.1-py3-none-any.whl", hash = "sha256:f085493f79efb0644f270a9bf2892843142d80d7174bbbd2f3713f2a589dc633"},
{file = "marshmallow-3.20.2.tar.gz", hash = "sha256:4c1daff273513dc5eb24b219a8035559dc573c8f322558ef85f5438ddd1236dd"}, {file = "marshmallow-3.21.1.tar.gz", hash = "sha256:4e65e9e0d80fc9e609574b9983cf32579f305c718afb30d7233ab818571768c3"},
] ]
[package.dependencies] [package.dependencies]
packaging = ">=17.0" packaging = ">=17.0"
[package.extras] [package.extras]
dev = ["pre-commit (>=2.4,<4.0)", "pytest", "pytz", "simplejson", "tox"] dev = ["marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "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)"] docs = ["alabaster (==0.7.16)", "autodocsumm (==0.2.12)", "sphinx (==7.2.6)", "sphinx-issues (==4.0.0)", "sphinx-version-warning (==1.1.2)"]
lint = ["pre-commit (>=2.4,<4.0)"]
tests = ["pytest", "pytz", "simplejson"] tests = ["pytest", "pytz", "simplejson"]
[[package]] [[package]]
@@ -741,38 +740,38 @@ files = [
[[package]] [[package]]
name = "mypy" name = "mypy"
version = "1.8.0" version = "1.9.0"
description = "Optional static typing for Python" description = "Optional static typing for Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"},
{file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"},
{file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"},
{file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"},
{file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"},
{file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"},
{file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"},
{file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"},
{file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"},
{file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"},
{file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"},
{file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"},
{file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"},
{file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"},
{file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"},
{file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"},
{file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"},
{file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"},
{file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"},
{file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"},
{file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"},
{file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"},
{file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"},
{file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"},
{file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"},
{file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"},
{file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"},
] ]
[package.dependencies] [package.dependencies]
@@ -827,13 +826,13 @@ attrs = ">=19.2.0"
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "23.2" version = "24.0"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"},
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
] ]
[[package]] [[package]]
@@ -964,13 +963,13 @@ files = [
[[package]] [[package]]
name = "pycparser" name = "pycparser"
version = "2.21" version = "2.22"
description = "C parser in Python" description = "C parser in Python"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=3.8"
files = [ files = [
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
] ]
[[package]] [[package]]
@@ -1016,47 +1015,47 @@ files = [
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "1.10.14" version = "1.10.15"
description = "Data validation and settings management using python type hints" description = "Data validation and settings management using python type hints"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "pydantic-1.10.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4fcec873f90537c382840f330b90f4715eebc2bc9925f04cb92de593eae054"}, {file = "pydantic-1.10.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55"},
{file = "pydantic-1.10.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e3a76f571970fcd3c43ad982daf936ae39b3e90b8a2e96c04113a369869dc87"}, {file = "pydantic-1.10.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2"},
{file = "pydantic-1.10.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d886bd3c3fbeaa963692ef6b643159ccb4b4cefaf7ff1617720cbead04fd1d"}, {file = "pydantic-1.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb"},
{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.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8"},
{file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:23d47a4b57a38e8652bcab15a658fdb13c785b9ce217cc3a729504ab4e1d6bc9"}, {file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00"},
{file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f9f674b5c3bebc2eba401de64f29948ae1e646ba2735f884d1594c5f675d6f2a"}, {file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0"},
{file = "pydantic-1.10.14-cp310-cp310-win_amd64.whl", hash = "sha256:24a7679fab2e0eeedb5a8924fc4a694b3bcaac7d305aeeac72dd7d4e05ecbebf"}, {file = "pydantic-1.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c"},
{file = "pydantic-1.10.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d578ac4bf7fdf10ce14caba6f734c178379bd35c486c6deb6f49006e1ba78a7"}, {file = "pydantic-1.10.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0"},
{file = "pydantic-1.10.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa7790e94c60f809c95602a26d906eba01a0abee9cc24150e4ce2189352deb1b"}, {file = "pydantic-1.10.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654"},
{file = "pydantic-1.10.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad4e10efa5474ed1a611b6d7f0d130f4aafadceb73c11d9e72823e8f508e663"}, {file = "pydantic-1.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3"},
{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.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44"},
{file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:21efacc678a11114c765eb52ec0db62edffa89e9a562a94cbf8fa10b5db5c046"}, {file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4"},
{file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:412ab4a3f6dbd2bf18aefa9f79c7cca23744846b31f1d6555c2ee2b05a2e14ca"}, {file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53"},
{file = "pydantic-1.10.14-cp311-cp311-win_amd64.whl", hash = "sha256:e897c9f35281f7889873a3e6d6b69aa1447ceb024e8495a5f0d02ecd17742a7f"}, {file = "pydantic-1.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986"},
{file = "pydantic-1.10.14-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d604be0f0b44d473e54fdcb12302495fe0467c56509a2f80483476f3ba92b33c"}, {file = "pydantic-1.10.15-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf"},
{file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a42c7d17706911199798d4c464b352e640cab4351efe69c2267823d619a937e5"}, {file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d"},
{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.15-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f"},
{file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bfb113860e9288d0886e3b9e49d9cf4a9d48b441f52ded7d96db7819028514cc"}, {file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de"},
{file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bc3ed06ab13660b565eed80887fcfbc0070f0aa0691fbb351657041d3e874efe"}, {file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7"},
{file = "pydantic-1.10.14-cp37-cp37m-win_amd64.whl", hash = "sha256:ad8c2bc677ae5f6dbd3cf92f2c7dc613507eafe8f71719727cbc0a7dec9a8c01"}, {file = "pydantic-1.10.15-cp37-cp37m-win_amd64.whl", hash = "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1"},
{file = "pydantic-1.10.14-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c37c28449752bb1f47975d22ef2882d70513c546f8f37201e0fec3a97b816eee"}, {file = "pydantic-1.10.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022"},
{file = "pydantic-1.10.14-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49a46a0994dd551ec051986806122767cf144b9702e31d47f6d493c336462597"}, {file = "pydantic-1.10.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528"},
{file = "pydantic-1.10.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53e3819bd20a42470d6dd0fe7fc1c121c92247bca104ce608e609b59bc7a77ee"}, {file = "pydantic-1.10.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948"},
{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.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c"},
{file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:336709883c15c050b9c55a63d6c7ff09be883dbc17805d2b063395dd9d9d0022"}, {file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22"},
{file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4ae57b4d8e3312d486e2498d42aed3ece7b51848336964e43abbf9671584e67f"}, {file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b"},
{file = "pydantic-1.10.14-cp38-cp38-win_amd64.whl", hash = "sha256:dba49d52500c35cfec0b28aa8b3ea5c37c9df183ffc7210b10ff2a415c125c4a"}, {file = "pydantic-1.10.15-cp38-cp38-win_amd64.whl", hash = "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12"},
{file = "pydantic-1.10.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c66609e138c31cba607d8e2a7b6a5dc38979a06c900815495b2d90ce6ded35b4"}, {file = "pydantic-1.10.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51"},
{file = "pydantic-1.10.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d986e115e0b39604b9eee3507987368ff8148222da213cd38c359f6f57b3b347"}, {file = "pydantic-1.10.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0"},
{file = "pydantic-1.10.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:646b2b12df4295b4c3148850c85bff29ef6d0d9621a8d091e98094871a62e5c7"}, {file = "pydantic-1.10.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383"},
{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.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed"},
{file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:466669501d08ad8eb3c4fecd991c5e793c4e0bbd62299d05111d4f827cded64f"}, {file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc"},
{file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:13e86a19dca96373dcf3190fcb8797d40a6f12f154a244a8d1e8e03b8f280593"}, {file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4"},
{file = "pydantic-1.10.14-cp39-cp39-win_amd64.whl", hash = "sha256:08b6ec0917c30861e3fe71a93be1648a2aa4f62f866142ba21670b24444d7fd8"}, {file = "pydantic-1.10.15-cp39-cp39-win_amd64.whl", hash = "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7"},
{file = "pydantic-1.10.14-py3-none-any.whl", hash = "sha256:8ee853cd12ac2ddbf0ecbac1c289f95882b2d4482258048079d13be700aa114c"}, {file = "pydantic-1.10.15-py3-none-any.whl", hash = "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58"},
{file = "pydantic-1.10.14.tar.gz", hash = "sha256:46f17b832fe27de7850896f3afee50ea682220dd218f7e9c88d436788419dca6"}, {file = "pydantic-1.10.15.tar.gz", hash = "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb"},
] ]
[package.dependencies] [package.dependencies]
@@ -1294,28 +1293,28 @@ httpx = ">=0.21.0"
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.2.1" version = "0.2.2"
description = "An extremely fast Python linter and code formatter, written in Rust." description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ 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.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"},
{file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dc586724a95b7d980aa17f671e173df00f0a2eef23f8babbeee663229a938fec"}, {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"},
{file = "ruff-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c92db7101ef5bfc18e96777ed7bc7c822d545fa5977e90a585accac43d22f18a"}, {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"},
{file = "ruff-0.2.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13471684694d41ae0f1e8e3a7497e14cd57ccb7dd72ae08d56a159d6c9c3e30e"}, {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"},
{file = "ruff-0.2.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a11567e20ea39d1f51aebd778685582d4c56ccb082c1161ffc10f79bebe6df35"}, {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"},
{file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:00a818e2db63659570403e44383ab03c529c2b9678ba4ba6c105af7854008105"}, {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"},
{file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be60592f9d218b52f03384d1325efa9d3b41e4c4d55ea022cd548547cc42cd2b"}, {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"},
{file = "ruff-0.2.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbd2288890b88e8aab4499e55148805b58ec711053588cc2f0196a44f6e3d855"}, {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"},
{file = "ruff-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ef052283da7dec1987bba8d8733051c2325654641dfe5877a4022108098683"}, {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"},
{file = "ruff-0.2.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7022d66366d6fded4ba3889f73cd791c2d5621b2ccf34befc752cb0df70f5fad"}, {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"},
{file = "ruff-0.2.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0a725823cb2a3f08ee743a534cb6935727d9e47409e4ad72c10a3faf042ad5ba"}, {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"},
{file = "ruff-0.2.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0034d5b6323e6e8fe91b2a1e55b02d92d0b582d2953a2b37a67a2d7dedbb7acc"}, {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"},
{file = "ruff-0.2.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e5cb5526d69bb9143c2e4d2a115d08ffca3d8e0fddc84925a7b54931c96f5c02"}, {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"},
{file = "ruff-0.2.1-py3-none-win32.whl", hash = "sha256:6b95ac9ce49b4fb390634d46d6ece32ace3acdd52814671ccaf20b7f60adb232"}, {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"},
{file = "ruff-0.2.1-py3-none-win_amd64.whl", hash = "sha256:e3affdcbc2afb6f5bd0eb3130139ceedc5e3f28d206fe49f63073cb9e65988e0"}, {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"},
{file = "ruff-0.2.1-py3-none-win_arm64.whl", hash = "sha256:efababa8e12330aa94a53e90a81eb6e2d55f348bc2e71adbf17d9cad23c03ee6"}, {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"},
{file = "ruff-0.2.1.tar.gz", hash = "sha256:3b42b5d8677cd0c72b99fcaf068ffc62abb5a19e71b4a3b9cfa50658a0af02f1"}, {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"},
] ]
[[package]] [[package]]
@@ -1399,13 +1398,13 @@ redis = ["redis (>=3.4.1,<4.0.0)"]
[[package]] [[package]]
name = "sniffio" name = "sniffio"
version = "1.3.0" version = "1.3.1"
description = "Sniff out which async library your code is running under" description = "Sniff out which async library your code is running under"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
{file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
] ]
[[package]] [[package]]
@@ -1526,13 +1525,13 @@ files = [
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.9.0" version = "4.10.0"
description = "Backported and Experimental Type Hints for Python 3.8+" description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"},
{file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"},
] ]
[[package]] [[package]]
@@ -1556,13 +1555,13 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
[[package]] [[package]]
name = "virtualenv" name = "virtualenv"
version = "20.25.0" version = "20.25.1"
description = "Virtual Python Environment builder" description = "Virtual Python Environment builder"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"},
{file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"},
] ]
[package.dependencies] [package.dependencies]
@@ -1590,6 +1589,87 @@ docs = ["Sphinx (>=6.0)", "sphinx-rtd-theme (>=1.1.0)"]
optional = ["python-socks", "wsaccel"] optional = ["python-socks", "wsaccel"]
test = ["websockets"] 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]] [[package]]
name = "wheel" name = "wheel"
version = "0.41.3" version = "0.41.3"
@@ -1699,18 +1779,18 @@ files = [
[[package]] [[package]]
name = "zipp" name = "zipp"
version = "3.17.0" version = "3.18.1"
description = "Backport of pathlib-compatible object wrapper for zip files" description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"},
{file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"},
] ]
[package.extras] [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"] 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-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] 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] [extras]
pgsql = ["psycopg2-binary"] pgsql = ["psycopg2-binary"]
@@ -1718,4 +1798,4 @@ pgsql = ["psycopg2-binary"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.8.1" python-versions = "^3.8.1"
content-hash = "d941bf9a1f3f01b6d9e9e16118b1ae6dfa2244b80a6433728a4e67a77420a527" content-hash = "fc67d56b3e5fe8c4172a3d24a9bd9c552b7ef3e1a9b3ea93db78a098b1d30154"

View File

@@ -32,6 +32,7 @@ bip32 = "^3.4"
mnemonic = "^0.20" mnemonic = "^0.20"
bolt11 = "^2.0.5" bolt11 = "^2.0.5"
pre-commit = "^3.5.0" pre-commit = "^3.5.0"
websockets = "^12.0"
slowapi = "^0.1.9" slowapi = "^0.1.9"
[tool.poetry.extras] [tool.poetry.extras]

View File

@@ -33,7 +33,8 @@ settings.tor = False
settings.wallet_unit = "sat" settings.wallet_unit = "sat"
settings.mint_backend_bolt11_sat = settings.mint_backend_bolt11_sat or "FakeWallet" settings.mint_backend_bolt11_sat = settings.mint_backend_bolt11_sat or "FakeWallet"
settings.fakewallet_brr = True 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 settings.fakewallet_stochastic_invoice = False
assert ( assert (
settings.mint_test_database != settings.mint_database settings.mint_test_database != settings.mint_database
@@ -44,6 +45,7 @@ settings.mint_derivation_path_list = []
settings.mint_private_key = "TEST_PRIVATE_KEY" settings.mint_private_key = "TEST_PRIVATE_KEY"
settings.mint_seed_decryption_key = "" settings.mint_seed_decryption_key = ""
settings.mint_max_balance = 0 settings.mint_max_balance = 0
settings.mint_transaction_rate_limit_per_minute = 60
settings.mint_lnd_enable_mpp = True settings.mint_lnd_enable_mpp = True
settings.mint_input_fee_ppk = 0 settings.mint_input_fee_ppk = 0

View File

@@ -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): async def test_get_mint_quote_by_request(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(128) invoice = await wallet1.request_mint(128)
assert invoice is not None assert invoice is not None
quote = await ledger.crud.get_mint_quote_by_request( quote = await ledger.crud.get_mint_quote(request=invoice.bolt11, db=ledger.db)
request=invoice.bolt11, db=ledger.db
)
assert quote is not None assert quote is not None
assert quote.quote == invoice.id assert quote.quote == invoice.id
assert quote.amount == 128 assert quote.amount == 128

View File

@@ -95,7 +95,7 @@ async def test_get_fees_for_proofs(wallet1: Wallet, ledger: Ledger):
@pytest.mark.asyncio @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): 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 # 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 # It would be better to test if the wallet can get the fees from the mint itself

View File

@@ -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 """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.""" after the startup routine determines that the associated melt quote was paid."""
pending_proof, quote = await create_pending_melts(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 assert states[0].state == SpentState.pending
settings.fakewallet_payment_state = True settings.fakewallet_payment_state = True
# run startup routinge # run startup routinge
@@ -184,7 +184,7 @@ async def test_startup_fakewallet_pending_quote_success(ledger: Ledger):
assert not melt_quotes assert not melt_quotes
# expect that proofs are spent # 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 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. The failure is simulated by setting the fakewallet_payment_state to False.
""" """
pending_proof, quote = await create_pending_melts(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 assert states[0].state == SpentState.pending
settings.fakewallet_payment_state = False settings.fakewallet_payment_state = False
# run startup routinge # run startup routinge
@@ -210,7 +210,7 @@ async def test_startup_fakewallet_pending_quote_failure(ledger: Ledger):
assert not melt_quotes assert not melt_quotes
# expect that proofs are unspent # 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 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") @pytest.mark.skipif(is_regtest, reason="only for fake wallet")
async def test_startup_fakewallet_pending_quote_pending(ledger: Ledger): async def test_startup_fakewallet_pending_quote_pending(ledger: Ledger):
pending_proof, quote = await create_pending_melts(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 assert states[0].state == SpentState.pending
settings.fakewallet_payment_state = None settings.fakewallet_payment_state = None
# run startup routinge # run startup routinge
@@ -231,7 +231,7 @@ async def test_startup_fakewallet_pending_quote_pending(ledger: Ledger):
assert melt_quotes assert melt_quotes
# expect that proofs are still pending # 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 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 assert melt_quotes
# expect that proofs are still pending # 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]) assert all([s.state == SpentState.pending for s in states])
# only now settle the invoice # 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) await asyncio.sleep(SLEEP_TIME)
# expect that proofs are pending # 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]) assert all([s.state == SpentState.pending for s in states])
settle_invoice(preimage=preimage) settle_invoice(preimage=preimage)
@@ -323,7 +323,7 @@ async def test_startup_regtest_pending_quote_success(wallet: Wallet, ledger: Led
assert not melt_quotes assert not melt_quotes
# expect that proofs are spent # 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]) 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) await asyncio.sleep(SLEEP_TIME)
# expect that proofs are pending # 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]) assert all([s.state == SpentState.pending for s in states])
cancel_invoice(preimage_hash=preimage_hash) 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 assert not melt_quotes
# expect that proofs are unspent # 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]) assert all([s.state == SpentState.unspent for s in states])

View File

@@ -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) 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]) 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

View File

@@ -62,7 +62,7 @@ async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger):
assert melt_quotes assert melt_quotes
# expect that proofs are still pending # 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]) assert all([s.state == SpentState.pending for s in states])
# only now settle the invoice # only now settle the invoice
@@ -70,7 +70,7 @@ async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger):
await asyncio.sleep(SLEEP_TIME) await asyncio.sleep(SLEEP_TIME)
# expect that proofs are now spent # 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]) assert all([s.state == SpentState.spent for s in states])
# expect that no melt quote is pending # expect that no melt quote is pending

View File

@@ -109,23 +109,7 @@ def test_balance(cli_prefix):
assert result.exit_code == 0 assert result.exit_code == 0
@pytest.mark.skipif(not is_fake, reason="only on fakewallet") def test_invoice_return_immediately(mint, cli_prefix):
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):
runner = CliRunner() runner = CliRunner()
result = runner.invoke( result = runner.invoke(
cli, cli,

View File

@@ -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"