mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-21 11:04:19 +01:00
Mint: add websockets for quote updates (#413)
* add websockets for quote updates * add test (not working) * wip: emit events to everyone * wip: emit events to everyone * wip, lots of things broken but invoice callback works * wip * add wip files * tests almost passing * add task * refactor nut constants * startup fix * works with old mints * wip cli * fix mypy * remove automatic invoice test now with websockets * remove comment * better logging * send back response * add rate limiter to websocket * add rate limiter to subscriptions * refactor websocket ratelimit * websocket tests * subscription kinds * doesnt start * remove circular import * update * fix mypy * move test file in test because it fails if it runs later... dunno why * adjust websocket NUT-06 settings * local import and small fix * disable websockets in CLI if "no_check" is selected * move subscription test to where it was * check proof state with callback, add tests * tests: run mint fixture per module instead of per session * subscription command name fix * test per session again * update test race conditions * fix tests * clean up * tmp * fix db issues and remove cached secrets * fix tests * blindly try pipeline * remove comments * comments
This commit is contained in:
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -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:
|
||||||
|
|||||||
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
@@ -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: |
|
||||||
|
|||||||
@@ -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 -------
|
||||||
|
|
||||||
|
|||||||
86
cashu/core/json_rpc/base.py
Normal file
86
cashu/core/json_rpc/base.py
Normal 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
|
||||||
@@ -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
13
cashu/core/nuts.py
Normal 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
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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())
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
68
cashu/mint/db/read.py
Normal 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
97
cashu/mint/db/write.py
Normal 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.")
|
||||||
0
cashu/mint/events/__init__.py
Normal file
0
cashu/mint/events/__init__.py
Normal file
231
cashu/mint/events/client.py
Normal file
231
cashu/mint/events/client.py
Normal 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)
|
||||||
21
cashu/mint/events/event_model.py
Normal file
21
cashu/mint/events/event_model.py
Normal 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
|
||||||
61
cashu/mint/events/events.py
Normal file
61
cashu/mint/events/events.py
Normal 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
102
cashu/mint/features.py
Normal 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
|
||||||
@@ -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.")
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
45
cashu/mint/tasks.py
Normal 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)
|
||||||
@@ -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]:
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
96
cashu/wallet/subscriptions.py
Normal file
96
cashu/wallet/subscriptions.py
Normal 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
|
||||||
@@ -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
452
poetry.lock
generated
@@ -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"
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
118
tests/test_wallet_subscription.py
Normal file
118
tests/test_wallet_subscription.py
Normal 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"
|
||||||
Reference in New Issue
Block a user