From 6a0a370ba54bec8bbe64779f08dcb331b5a5359a Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 8 Jul 2024 18:05:57 +0200 Subject: [PATCH] Mint: table locks (#566) * clean up db * db: table lock * db.table_with_schema * fix encrypt.py * postgres nowait * add timeout to lock * melt quote state in db * kinda working * kinda working with postgres * remove dispose * getting there * porperly clean up db for tests * faster tests * configure connection pooling * try github with connection pool * invoice dispatcher does not lock db * fakewallet: pay_if_regtest waits * pay fakewallet invoices * add more * faster * slower * pay_if_regtest async * do not lock the invoice dispatcher * test: do I get disk I/O errors if we disable the invoice_callback_dispatcher? * fix fake so it workss without a callback dispatchert * test on github * readd tasks * refactor * increase time for lock invoice disatcher * try avoiding a race * remove task * github actions: test regtest with postgres * mint per module * no connection pool for testing * enable pool * do not resend paid event * reuse connection * close db connections * sessions * enable debug * dispose engine * disable connection pool for tests * enable connection pool for postgres only * clean up shutdown routine * remove wait for lightning fakewallet lightning invoice * cancel invoice listener tasks on shutdown * fakewallet conftest: decrease outgoing delay * delay payment and set postgres only if needed * disable fail fast for regtest * clean up regtest.yml * change order of tests_db.py * row-specific mint_quote locking * refactor * fix lock statement * refactor swap * refactor * remove psycopg2 * add connection string example to .env.example * remove unnecessary pay * shorter sleep in test_wallet_subscription_swap --- .env.example | 3 + .github/actions/prepare/action.yml | 2 +- .github/workflows/ci.yml | 5 +- .github/workflows/regtest.yml | 22 +- .github/workflows/tests.yml | 19 +- README.md | 2 - cashu/core/base.py | 18 +- cashu/core/db.py | 338 ++++++++++++++--------- cashu/core/migrations.py | 19 +- cashu/core/settings.py | 5 +- cashu/lightning/fake.py | 25 +- cashu/mint/app.py | 6 + cashu/mint/crud.py | 354 ++++++++++++------------ cashu/mint/db/read.py | 52 ++-- cashu/mint/db/write.py | 195 +++++++++++--- cashu/mint/encrypt.py | 6 +- cashu/mint/ledger.py | 106 ++++---- cashu/mint/migrations.py | 268 +++++++++--------- cashu/mint/startup.py | 6 + cashu/mint/tasks.py | 43 ++- cashu/mint/verification.py | 42 +-- cashu/wallet/crud.py | 301 ++++++++++----------- cashu/wallet/migrations.py | 22 +- cashu/wallet/wallet.py | 7 +- poetry.lock | 417 ++++++++++++++++------------- pyproject.toml | 9 +- tests/conftest.py | 13 +- tests/helpers.py | 39 ++- tests/test_db.py | 292 ++++++++++++++++++-- tests/test_mint.py | 4 +- tests/test_mint_api.py | 15 +- tests/test_mint_api_deprecated.py | 18 +- tests/test_mint_db.py | 209 +++++++++++++-- tests/test_mint_fees.py | 11 +- tests/test_mint_init.py | 6 +- tests/test_mint_operations.py | 57 ++-- tests/test_mint_regtest.py | 2 +- tests/test_wallet.py | 46 ++-- tests/test_wallet_cli.py | 6 +- tests/test_wallet_htlc.py | 18 +- tests/test_wallet_lightning.py | 4 +- tests/test_wallet_p2pk.py | 28 +- tests/test_wallet_regtest.py | 4 +- tests/test_wallet_regtest_mpp.py | 10 +- tests/test_wallet_restore.py | 12 +- tests/test_wallet_subscription.py | 4 +- 46 files changed, 1933 insertions(+), 1157 deletions(-) diff --git a/.env.example b/.env.example index 5e4e5c0..8f2a810 100644 --- a/.env.example +++ b/.env.example @@ -54,7 +54,10 @@ MINT_DERIVATION_PATH="m/0'/0'/0'" # In this example, we have 2 keysets for sat, 1 for msat and 1 for usd # MINT_DERIVATION_PATH_LIST=["m/0'/0'/0'", "m/0'/0'/1'", "m/0'/1'/0'", "m/0'/2'/0'"] +# To use SQLite, choose a directory to store the database MINT_DATABASE=data/mint +# To use PostgreSQL, set the connection string +# MINT_DATABASE=postgres://cashu:cashu@localhost:5432/cashu # Funding source backends # Supported: FakeWallet, LndRestWallet, CLNRestWallet, BlinkWallet, LNbitsWallet, StrikeWallet, CoreLightningRestWallet (deprecated) diff --git a/.github/actions/prepare/action.yml b/.github/actions/prepare/action.yml index b2c5f09..42e7361 100644 --- a/.github/actions/prepare/action.yml +++ b/.github/actions/prepare/action.yml @@ -23,5 +23,5 @@ runs: cache: "poetry" - name: Install dependencies run: | - poetry install --extras pgsql + poetry install shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d4d6ca..94f7254 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,13 +28,14 @@ jobs: regtest: uses: ./.github/workflows/regtest.yml strategy: + fail-fast: false matrix: python-version: ["3.10"] poetry-version: ["1.7.1"] backend-wallet-class: ["LndRestWallet", "CLNRestWallet", "CoreLightningRestWallet", "LNbitsWallet"] - # mint-database: ["./test_data/test_mint", "postgres://cashu:cashu@localhost:5432/cashu"] - mint-database: ["./test_data/test_mint"] + mint-database: ["./test_data/test_mint", "postgres://cashu:cashu@localhost:5432/cashu"] + # mint-database: ["./test_data/test_mint"] with: python-version: ${{ matrix.python-version }} backend-wallet-class: ${{ matrix.backend-wallet-class }} diff --git a/.github/workflows/regtest.yml b/.github/workflows/regtest.yml index cc39634..b585848 100644 --- a/.github/workflows/regtest.yml +++ b/.github/workflows/regtest.yml @@ -23,21 +23,13 @@ jobs: regtest: runs-on: ${{ inputs.os-version }} timeout-minutes: 10 - services: - postgres: - image: postgres:latest - env: - POSTGRES_USER: cashu - POSTGRES_PASSWORD: cashu - POSTGRES_DB: cashu - ports: - - 5432:5432 - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 steps: + - name: Start PostgreSQL service + if: contains(inputs.mint-database, 'postgres') + run: | + docker run -d --name postgres -e POSTGRES_USER=cashu -e POSTGRES_PASSWORD=cashu -e POSTGRES_DB=cashu -p 5432:5432 postgres:latest + until docker exec postgres pg_isready; do sleep 1; done + - uses: actions/checkout@v3 - uses: ./.github/actions/prepare @@ -78,7 +70,7 @@ jobs: MINT_CLNREST_URL: https://localhost:3010 MINT_CLNREST_RUNE: ./regtest/data/clightning-2/rune MINT_CLNREST_CERT: ./regtest/data/clightning-2/regtest/ca.pem - MINT_CLNENABLE_MPP: false + MINT_CLNREST_ENABLE_MPP: false run: | sudo chmod -R 777 . make test diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 40ecb60..c0c1b83 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,21 +23,12 @@ jobs: poetry: name: Run (db ${{ inputs.mint-database }}, deprecated api ${{ inputs.mint-only-deprecated }}) runs-on: ${{ inputs.os }} - services: - postgres: - image: postgres:latest - env: - POSTGRES_USER: cashu - POSTGRES_PASSWORD: cashu - POSTGRES_DB: cashu - ports: - - 5432:5432 - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 steps: + - name: Start PostgreSQL service + if: contains(inputs.mint-database, 'postgres') + run: | + docker run -d --name postgres -e POSTGRES_USER=cashu -e POSTGRES_PASSWORD=cashu -e POSTGRES_DB=cashu -p 5432:5432 postgres:latest + until docker exec postgres pg_isready; do sleep 1; done - name: Checkout repository and submodules uses: actions/checkout@v2 - uses: ./.github/actions/prepare diff --git a/README.md b/README.md index 188baf7..b6a44d1 100644 --- a/README.md +++ b/README.md @@ -83,8 +83,6 @@ pyenv local 3.10.4 poetry install ``` -If you would like to use PostgreSQL as the mint database, use the command `poetry install --extras pgsql`. - #### Poetry: Update Cashu To update Cashu to the newest version enter ```bash diff --git a/cashu/core/base.py b/cashu/core/base.py index 22a9c37..96c7313 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -344,11 +344,13 @@ class MeltQuote(LedgerEvent): def __setattr__(self, name, value): # an unpaid quote can only be set to pending or paid if name == "state" and self.state == MeltQuoteState.unpaid: - if value != MeltQuoteState.pending and value != MeltQuoteState.paid: - raise Exception("Cannot change state of an unpaid quote.") + if value not in [MeltQuoteState.pending, MeltQuoteState.paid]: + raise Exception( + f"Cannot change state of an unpaid melt quote to {value}." + ) # a paid quote can not be changed if name == "state" and self.state == MeltQuoteState.paid: - raise Exception("Cannot change state of a paid quote.") + raise Exception("Cannot change state of a paid melt quote.") super().__setattr__(name, value) @@ -415,18 +417,20 @@ class MintQuote(LedgerEvent): # un unpaid quote can only be set to paid if name == "state" and self.state == MintQuoteState.unpaid: if value != MintQuoteState.paid: - raise Exception("Cannot change state of an unpaid quote.") + raise Exception( + f"Cannot change state of an unpaid mint quote to {value}." + ) # a paid quote can only be set to pending or issued if name == "state" and self.state == MintQuoteState.paid: if value != MintQuoteState.pending and value != MintQuoteState.issued: - raise Exception(f"Cannot change state of a paid quote to {value}.") + raise Exception(f"Cannot change state of a paid mint quote to {value}.") # a pending quote can only be set to paid or issued if name == "state" and self.state == MintQuoteState.pending: if value not in [MintQuoteState.paid, MintQuoteState.issued]: - raise Exception("Cannot change state of a pending quote.") + raise Exception("Cannot change state of a pending mint quote.") # an issued quote cannot be changed if name == "state" and self.state == MintQuoteState.issued: - raise Exception("Cannot change state of an issued quote.") + raise Exception("Cannot change state of an issued mint quote.") super().__setattr__(name, value) diff --git a/cashu/core/db.py b/cashu/core/db.py index 1863c60..43aabf2 100644 --- a/cashu/core/db.py +++ b/cashu/core/db.py @@ -1,14 +1,18 @@ import asyncio import datetime import os +import re import time from contextlib import asynccontextmanager from typing import Optional, Union from loguru import logger -from sqlalchemy import create_engine -from sqlalchemy_aio.base import AsyncConnection -from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY # type: ignore +from sqlalchemy import text +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import NullPool, QueuePool + +from cashu.core.settings import settings POSTGRES = "POSTGRES" COCKROACH = "COCKROACH" @@ -57,9 +61,12 @@ class Compat: return "BIGINT" return "INT" + def table_with_schema(self, table: str): + return f"{self.references_schema if self.schema else ''}{table}" + class Connection(Compat): - def __init__(self, conn: AsyncConnection, txn, typ, name, schema): + def __init__(self, conn: AsyncSession, txn, typ, name, schema): self.conn = conn self.txn = txn self.type = typ @@ -70,23 +77,23 @@ class Connection(Compat): if self.type in {POSTGRES, COCKROACH}: query = query.replace("%", "%%") query = query.replace("?", "%s") - return query + return text(query) - async def fetchall(self, query: str, values: tuple = ()) -> list: + async def fetchall(self, query: str, values: dict = {}) -> list: result = await self.conn.execute(self.rewrite_query(query), values) - return await result.fetchall() + return result.all() - async def fetchone(self, query: str, values: tuple = ()): + async def fetchone(self, query: str, values: dict = {}): result = await self.conn.execute(self.rewrite_query(query), values) - row = await result.fetchone() - await result.close() - return row + return result.fetchone() - async def execute(self, query: str, values: tuple = ()): + async def execute(self, query: str, values: dict = {}): return await self.conn.execute(self.rewrite_query(query), values) class Database(Compat): + _connection: Optional[AsyncSession] = None + def __init__(self, db_name: str, db_location: str): self.name = db_name self.db_location = db_location @@ -99,43 +106,20 @@ class Database(Compat): self.type = COCKROACH else: self.type = POSTGRES - - import psycopg2 # type: ignore - - def _parse_timestamp(value, _): - f = "%Y-%m-%d %H:%M:%S.%f" - if "." not in value: - f = "%Y-%m-%d %H:%M:%S" - return time.mktime(datetime.datetime.strptime(value, f).timetuple()) - - psycopg2.extensions.register_type( # type: ignore - psycopg2.extensions.new_type( # type: ignore - psycopg2.extensions.DECIMAL.values, # type: ignore - "DEC2FLOAT", - lambda value, curs: float(value) if value is not None else None, + database_uri = database_uri.replace( + "postgres://", "postgresql+asyncpg://" ) - ) - psycopg2.extensions.register_type( # type: ignore - psycopg2.extensions.new_type( # type: ignore - (1082, 1083, 1266), - "DATE2INT", - lambda value, curs: ( - time.mktime(value.timetuple()) if value is not None else None # type: ignore - ), + database_uri = database_uri.replace( + "postgresql://", "postgresql+asyncpg://" ) - ) - - # psycopg2.extensions.register_type( - # psycopg2.extensions.new_type( - # (1184, 1114), "TIMESTAMP2INT", _parse_timestamp - # ) - # ) + # Disble prepared statement cache: https://docs.sqlalchemy.org/en/14/dialects/postgresql.html#prepared-statement-cache + database_uri += "?prepared_statement_cache_size=0" else: if not os.path.exists(self.db_location): logger.info(f"Creating database directory: {self.db_location}") os.makedirs(self.db_location) self.path = os.path.join(self.db_location, f"{self.name}.sqlite3") - database_uri = f"sqlite:///{self.path}" + database_uri = f"sqlite+aiosqlite:///{self.path}?check_same_thread=false" self.type = SQLITE self.schema = self.name @@ -144,44 +128,167 @@ class Database(Compat): else: self.schema = None - self.engine = create_engine(database_uri, strategy=ASYNCIO_STRATEGY) - self.lock = asyncio.Lock() + kwargs = {} + if not settings.db_connection_pool: + kwargs["poolclass"] = NullPool + elif self.type == POSTGRES: + kwargs["poolclass"] = QueuePool + kwargs["pool_size"] = 50 + kwargs["max_overflow"] = 100 + + self.engine = create_async_engine(database_uri, **kwargs) + self.async_session = sessionmaker( + self.engine, + expire_on_commit=False, + class_=AsyncSession, # type: ignore + ) @asynccontextmanager - async def connect(self): - await self.lock.acquire() - try: - async with self.engine.connect() as conn: # type: ignore - async with conn.begin() as txn: - wconn = Connection(conn, txn, self.type, self.name, self.schema) + async def get_connection( + self, + conn: Optional[Connection] = None, + lock_table: Optional[str] = None, + lock_select_statement: Optional[str] = None, + lock_timeout: Optional[float] = None, + ): + """Either yield the existing database connection (passthrough) or create a new one. - if self.schema: - if self.type in {POSTGRES, COCKROACH}: - await wconn.execute( - f"CREATE SCHEMA IF NOT EXISTS {self.schema}" - ) - elif self.type == SQLITE: - await wconn.execute( - f"ATTACH '{self.path}' AS {self.schema}" - ) + Args: + conn (Optional[Connection], optional): Connection object. Defaults to None. + lock_table (Optional[str], optional): Table to lock. Defaults to None. + lock_select_statement (Optional[str], optional): Lock select statement. Defaults to None. + lock_timeout (Optional[float], optional): Lock timeout. Defaults to None. + Yields: + Connection: Connection object. + """ + if conn is not None: + # Yield the existing connection + logger.trace("Reusing existing connection") + yield conn + else: + logger.trace("get_connection: Creating new connection") + async with self.connect( + lock_table, lock_select_statement, lock_timeout + ) as new_conn: + yield new_conn + + @asynccontextmanager + async def connect( + self, + lock_table: Optional[str] = None, + lock_select_statement: Optional[str] = None, + lock_timeout: Optional[float] = None, + ): + async def _handle_lock_retry(retry_delay, timeout, start_time) -> float: + await asyncio.sleep(retry_delay) + retry_delay = min(retry_delay * 2, timeout - (time.time() - start_time)) + return retry_delay + + def _is_lock_exception(e): + if "database is locked" in str(e) or "could not obtain lock" in str(e): + logger.trace(f"Lock exception: {e}") + return True + + timeout = lock_timeout or 5 # default to 5 seconds + start_time = time.time() + retry_delay = 0.1 + random_int = int(time.time() * 1000) + trial = 0 + + while time.time() - start_time < timeout: + trial += 1 + session: AsyncSession = self.async_session() # type: ignore + try: + logger.trace(f"Connecting to database trial: {trial} ({random_int})") + async with session.begin() as txn: # type: ignore + logger.trace("Connected to database. Starting transaction") + wconn = Connection(session, txn, self.type, self.name, self.schema) + if lock_table: + await self.acquire_lock( + wconn, lock_table, lock_select_statement + ) + logger.trace( + f"> Yielding connection. Lock: {lock_table} - trial {trial} ({random_int})" + ) yield wconn - finally: - self.lock.release() + logger.trace( + f"< Connection yielded. Unlock: {lock_table} - trial {trial} ({random_int})" + ) + return + except Exception as e: + if _is_lock_exception(e): + retry_delay = await _handle_lock_retry( + retry_delay, timeout, start_time + ) + else: + logger.error(f"Error in session trial: {trial} ({random_int}): {e}") + raise e + finally: + logger.trace(f"Closing session trial: {trial} ({random_int})") + await session.close() + # if not inherited: + # logger.trace("Closing session") + # await session.close() + # self._connection = None + raise Exception( + f"failed to acquire database lock on {lock_table} after {timeout}s and {trial} trials ({random_int})" + ) - async def fetchall(self, query: str, values: tuple = ()) -> list: + async def acquire_lock( + self, + wconn: Connection, + lock_table: str, + lock_select_statement: Optional[str] = None, + ): + """Acquire a lock on a table or a row in a table. + + Args: + wconn (Connection): Connection object. + lock_table (str): Table to lock. + lock_select_statement (Optional[str], optional): + lock_timeout (Optional[float], optional): + + Raises: + Exception: _description_ + """ + if lock_select_statement: + assert ( + len(re.findall(r"^[^=]+='[^']+'$", lock_select_statement)) == 1 + ), "lock_select_statement must have exactly one {column}='{value}' pattern." + try: + logger.trace( + f"Acquiring lock on {lock_table} with statement {self.lock_table(lock_table, lock_select_statement)}" + ) + await wconn.execute(self.lock_table(lock_table, lock_select_statement)) + logger.trace(f"Success: Acquired lock on {lock_table}") + return + except Exception as e: + if ( + ( + self.type == POSTGRES + and "could not obtain lock on relation" in str(e) + ) + or (self.type == COCKROACH and "already locked" in str(e)) + or (self.type == SQLITE and "database is locked" in str(e)) + ): + logger.trace(f"Table {lock_table} is already locked: {e}") + else: + logger.trace(f"Failed to acquire lock on {lock_table}: {e}") + + raise e + + async def fetchall(self, query: str, values: dict = {}) -> list: async with self.connect() as conn: result = await conn.execute(query, values) - return await result.fetchall() + return result.all() - async def fetchone(self, query: str, values: tuple = ()): + async def fetchone(self, query: str, values: dict = {}): async with self.connect() as conn: result = await conn.execute(query, values) - row = await result.fetchone() - await result.close() - return row + return result.fetchone() - async def execute(self, query: str, values: tuple = ()): + async def execute(self, query: str, values: dict = {}): async with self.connect() as conn: return await conn.execute(query, values) @@ -189,60 +296,51 @@ class Database(Compat): async def reuse_conn(self, conn: Connection): yield conn + def lock_table( + self, + table: str, + lock_select_statement: Optional[str] = None, + ) -> str: + # with postgres, we can lock a row with a SELECT statement with FOR UPDATE NOWAIT + if lock_select_statement: + if self.type == POSTGRES: + return f"SELECT 1 FROM {self.table_with_schema(table)} WHERE {lock_select_statement} FOR UPDATE NOWAIT;" -# public functions for LNbits to use (we don't want to change the Database or Compat classes above) -def table_with_schema(db: Union[Database, Connection], table: str): - return f"{db.references_schema if db.schema else ''}{table}" + if self.type == POSTGRES: + return ( + f"LOCK TABLE {self.table_with_schema(table)} IN EXCLUSIVE MODE NOWAIT;" + ) + elif self.type == COCKROACH: + return f"LOCK TABLE {table};" + elif self.type == SQLITE: + return "BEGIN EXCLUSIVE TRANSACTION;" + return "" - -def lock_table(db: Database, table: str) -> str: - if db.type == POSTGRES: - return f"LOCK TABLE {table_with_schema(db, table)} IN EXCLUSIVE MODE;" - elif db.type == COCKROACH: - return f"LOCK TABLE {table};" - elif db.type == SQLITE: - return "BEGIN EXCLUSIVE TRANSACTION;" - return "" - - -def timestamp_from_seconds( - db: Database, seconds: Union[int, float, None] -) -> Union[str, None]: - if seconds is None: + def timestamp_from_seconds( + self, seconds: Union[int, float, None] + ) -> Union[str, None]: + if seconds is None: + return None + seconds = int(seconds) + if self.type in {POSTGRES, COCKROACH}: + return datetime.datetime.fromtimestamp(seconds).strftime( + "%Y-%m-%d %H:%M:%S" + ) + elif self.type == SQLITE: + return str(seconds) return None - seconds = int(seconds) - if db.type in {POSTGRES, COCKROACH}: - return datetime.datetime.fromtimestamp(seconds).strftime("%Y-%m-%d %H:%M:%S") - elif db.type == SQLITE: - return str(seconds) - return None + def timestamp_now_str(self) -> str: + timestamp = self.timestamp_from_seconds(time.time()) + if timestamp is None: + raise Exception("Timestamp is None") + return timestamp -def timestamp_now(db: Database) -> str: - timestamp = timestamp_from_seconds(db, time.time()) - if timestamp is None: - raise Exception("Timestamp is None") - return timestamp - - -@asynccontextmanager -async def get_db_connection(db: Database, conn: Optional[Connection] = None): - """Either yield the existing database connection or create a new one. - - Note: This should be implemented as Database.get_db_connection(self, conn) but - since we want to use it in LNbits, we can't change the Database class their. - - Args: - db (Database): Database object. - conn (Optional[Connection], optional): Connection object. Defaults to None. - - Yields: - Connection: Connection object. - """ - if conn is not None: - # Yield the existing connection - yield conn - else: - # Create and yield a new connection - async with db.connect() as new_conn: - yield new_conn + def to_timestamp(self, timestamp_str: str) -> Union[str, datetime.datetime]: + if not timestamp_str: + timestamp_str = self.timestamp_now_str() + if self.type in {POSTGRES, COCKROACH}: + return datetime.datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S") + elif self.type == SQLITE: + return timestamp_str + return "" diff --git a/cashu/core/migrations.py b/cashu/core/migrations.py index 7bab5ee..ddeb523 100644 --- a/cashu/core/migrations.py +++ b/cashu/core/migrations.py @@ -4,7 +4,7 @@ import time from loguru import logger -from ..core.db import COCKROACH, POSTGRES, SQLITE, Database, table_with_schema +from ..core.db import COCKROACH, POSTGRES, SQLITE, Database from ..core.settings import settings @@ -47,10 +47,10 @@ async def migrate_databases(db: Database, migrations_module): async def set_migration_version(conn, db_name, version): await conn.execute( f""" - INSERT INTO {table_with_schema(db, 'dbversions')} (db, version) VALUES (?, ?) - ON CONFLICT (db) DO UPDATE SET version = ? + INSERT INTO {db.table_with_schema('dbversions')} (db, version) VALUES (:db, :version) + ON CONFLICT (db) DO UPDATE SET version = :version """, - (db_name, version, version), + {"db": db_name, "version": version}, ) async def run_migration(db, migrations_module): @@ -89,20 +89,21 @@ async def migrate_databases(db: Database, migrations_module): if conn.type == SQLITE: exists = await conn.fetchone( "SELECT * FROM sqlite_master WHERE type='table' AND" - f" name='{table_with_schema(db, 'dbversions')}'" + f" name='{db.table_with_schema('dbversions')}'" ) elif conn.type in {POSTGRES, COCKROACH}: exists = await conn.fetchone( "SELECT * FROM information_schema.tables WHERE table_name =" - f" '{table_with_schema(db, 'dbversions')}'" + f" '{db.table_with_schema('dbversions')}'" ) if not exists: await migrations_module.m000_create_migrations_table(conn) - rows = await ( - await conn.execute(f"SELECT * FROM {table_with_schema(db, 'dbversions')}") - ).fetchall() + result = await conn.execute( + f"SELECT * FROM {db.table_with_schema('dbversions')}" + ) + rows = result.all() current_versions = {row["db"]: row["version"] for row in rows} matcher = re.compile(r"^m(\d\d\d)_") await run_migration(db, migrations_module) diff --git a/cashu/core/settings.py b/cashu/core/settings.py index c5208b2..263d8e4 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -46,6 +46,7 @@ class EnvSettings(CashuSettings): debug_profiling: bool = Field(default=False) debug_mint_only_deprecated: bool = Field(default=False) db_backup_path: Optional[str] = Field(default=None) + db_connection_pool: bool = Field(default=True) class MintSettings(CashuSettings): @@ -131,8 +132,8 @@ class MintLimits(MintSettings): class FakeWalletSettings(MintSettings): fakewallet_brr: bool = Field(default=True) - fakewallet_delay_outgoing_payment: Optional[int] = Field(default=3) - fakewallet_delay_incoming_payment: Optional[int] = Field(default=3) + fakewallet_delay_outgoing_payment: Optional[float] = Field(default=3.0) + fakewallet_delay_incoming_payment: Optional[float] = Field(default=3.0) fakewallet_stochastic_invoice: bool = Field(default=False) fakewallet_payment_state: Optional[bool] = Field(default=None) diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index 6fca6dd..7517bfe 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -1,7 +1,6 @@ import asyncio import hashlib import math -import random from datetime import datetime from os import urandom from typing import AsyncGenerator, Dict, List, Optional @@ -57,8 +56,12 @@ class FakeWallet(LightningBackend): async def status(self) -> StatusResponse: return StatusResponse(error_message=None, balance=1337) - async def mark_invoice_paid(self, invoice: Bolt11) -> None: - if settings.fakewallet_delay_incoming_payment: + async def mark_invoice_paid(self, invoice: Bolt11, delay=True) -> None: + if invoice in self.paid_invoices_incoming: + return + if not settings.fakewallet_brr: + return + if settings.fakewallet_delay_incoming_payment and delay: await asyncio.sleep(settings.fakewallet_delay_incoming_payment) self.paid_invoices_incoming.append(invoice) await self.paid_invoices_queue.put(invoice) @@ -165,19 +168,13 @@ class FakeWallet(LightningBackend): ) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: - paid = False - if settings.fakewallet_brr or ( - settings.fakewallet_stochastic_invoice and random.random() > 0.7 - ): + await self.mark_invoice_paid(self.create_dummy_bolt11(checking_id), delay=False) + paid_chceking_ids = [i.payment_hash for i in self.paid_invoices_incoming] + if checking_id in paid_chceking_ids: paid = True + else: + paid = False - # 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: diff --git a/cashu/mint/app.py b/cashu/mint/app.py index 619481a..0b37c02 100644 --- a/cashu/mint/app.py +++ b/cashu/mint/app.py @@ -12,6 +12,7 @@ from ..core.logging import configure_logger from ..core.settings import settings from .router import router from .router_deprecated import router_deprecated +from .startup import shutdown_mint as shutdown_mint_init from .startup import start_mint_init if settings.debug_profiling: @@ -103,3 +104,8 @@ else: @app.on_event("startup") async def startup_mint(): await start_mint_init() + + +@app.on_event("shutdown") +async def shutdown_mint(): + await shutdown_mint_init() diff --git a/cashu/mint/crud.py b/cashu/mint/crud.py index 18b5bd7..cf376e2 100644 --- a/cashu/mint/crud.py +++ b/cashu/mint/crud.py @@ -1,6 +1,6 @@ import json from abc import ABC, abstractmethod -from typing import Any, List, Optional +from typing import Any, Dict, List, Optional from ..core.base import ( BlindedSignature, @@ -12,9 +12,6 @@ from ..core.base import ( from ..core.db import ( Connection, Database, - table_with_schema, - timestamp_from_seconds, - timestamp_now, ) @@ -266,19 +263,19 @@ class LedgerCrudSqlite(LedgerCrud): ) -> None: await (conn or db).execute( f""" - INSERT INTO {table_with_schema(db, 'promises')} + INSERT INTO {db.table_with_schema('promises')} (amount, b_, c_, dleq_e, dleq_s, id, created) - VALUES (?, ?, ?, ?, ?, ?, ?) + VALUES (:amount, :b_, :c_, :dleq_e, :dleq_s, :id, :created) """, - ( - amount, - b_, - c_, - e, - s, - id, - timestamp_now(db), - ), + { + "amount": amount, + "b_": b_, + "c_": c_, + "dleq_e": e, + "dleq_s": s, + "id": id, + "created": db.to_timestamp(db.timestamp_now_str()), + }, ) async def get_promise( @@ -290,10 +287,10 @@ class LedgerCrudSqlite(LedgerCrud): ) -> Optional[BlindedSignature]: row = await (conn or db).fetchone( f""" - SELECT * from {table_with_schema(db, 'promises')} - WHERE b_ = ? + SELECT * from {db.table_with_schema('promises')} + WHERE b_ = :b_ """, - (str(b_),), + {"b_": str(b_)}, ) return BlindedSignature.from_row(row) if row else None @@ -305,7 +302,7 @@ class LedgerCrudSqlite(LedgerCrud): ) -> List[Proof]: rows = await (conn or db).fetchall( f""" - SELECT * from {table_with_schema(db, 'proofs_used')} + SELECT * from {db.table_with_schema('proofs_used')} """ ) return [Proof(**r) for r in rows] if rows else [] @@ -318,23 +315,22 @@ class LedgerCrudSqlite(LedgerCrud): quote_id: Optional[str] = None, conn: Optional[Connection] = None, ) -> None: - # we add the proof and secret to the used list await (conn or db).execute( f""" - INSERT INTO {table_with_schema(db, 'proofs_used')} + INSERT INTO {db.table_with_schema('proofs_used')} (amount, c, secret, y, id, witness, created, melt_quote) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + VALUES (:amount, :c, :secret, :y, :id, :witness, :created, :melt_quote) """, - ( - proof.amount, - proof.C, - proof.secret, - proof.Y, - proof.id, - proof.witness, - timestamp_now(db), - quote_id, - ), + { + "amount": proof.amount, + "c": proof.C, + "secret": proof.secret, + "y": proof.Y, + "id": proof.id, + "witness": proof.witness, + "created": db.to_timestamp(db.timestamp_now_str()), + "melt_quote": quote_id, + }, ) async def get_all_melt_quotes_from_pending_proofs( @@ -345,7 +341,7 @@ class LedgerCrudSqlite(LedgerCrud): ) -> List[MeltQuote]: rows = await (conn or db).fetchall( f""" - SELECT * from {table_with_schema(db, 'melt_quotes')} WHERE quote in (SELECT DISTINCT melt_quote FROM {table_with_schema(db, 'proofs_pending')}) + SELECT * from {db.table_with_schema('melt_quotes')} WHERE quote in (SELECT DISTINCT melt_quote FROM {db.table_with_schema('proofs_pending')}) """ ) return [MeltQuote.from_row(r) for r in rows] @@ -359,10 +355,10 @@ class LedgerCrudSqlite(LedgerCrud): ) -> List[Proof]: rows = await (conn or db).fetchall( f""" - SELECT * from {table_with_schema(db, 'proofs_pending')} - WHERE melt_quote = ? + SELECT * from {db.table_with_schema('proofs_pending')} + WHERE melt_quote = :quote_id """, - (quote_id,), + {"quote_id": quote_id}, ) return [Proof(**r) for r in rows] @@ -373,13 +369,12 @@ class LedgerCrudSqlite(LedgerCrud): db: Database, conn: Optional[Connection] = None, ) -> List[Proof]: - rows = await (conn or db).fetchall( - f""" - SELECT * from {table_with_schema(db, 'proofs_pending')} - WHERE y IN ({','.join(['?']*len(Ys))}) - """, - tuple(Ys), - ) + query = f""" + SELECT * from {db.table_with_schema('proofs_pending')} + WHERE y IN ({','.join([':y_' + str(i) for i in range(len(Ys))])}) + """ + values = {f"y_{i}": Ys[i] for i in range(len(Ys))} + rows = await (conn or db).fetchall(query, values) return [Proof(**r) for r in rows] async def set_proof_pending( @@ -390,23 +385,22 @@ class LedgerCrudSqlite(LedgerCrud): quote_id: Optional[str] = None, conn: Optional[Connection] = None, ) -> None: - # we add the proof and secret to the used list await (conn or db).execute( f""" - INSERT INTO {table_with_schema(db, 'proofs_pending')} + INSERT INTO {db.table_with_schema('proofs_pending')} (amount, c, secret, y, id, witness, created, melt_quote) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + VALUES (:amount, :c, :secret, :y, :id, :witness, :created, :melt_quote) """, - ( - proof.amount, - proof.C, - proof.secret, - proof.Y, - proof.id, - proof.witness, - timestamp_now(db), - quote_id, - ), + { + "amount": proof.amount, + "c": proof.C, + "secret": proof.secret, + "y": proof.Y, + "id": proof.id, + "witness": proof.witness, + "created": db.to_timestamp(db.timestamp_now_str()), + "melt_quote": quote_id, + }, ) async def unset_proof_pending( @@ -418,10 +412,10 @@ class LedgerCrudSqlite(LedgerCrud): ) -> None: await (conn or db).execute( f""" - DELETE FROM {table_with_schema(db, 'proofs_pending')} - WHERE secret = ? + DELETE FROM {db.table_with_schema('proofs_pending')} + WHERE secret = :secret """, - (proof.secret,), + {"secret": proof.secret}, ) async def store_mint_quote( @@ -433,23 +427,27 @@ class LedgerCrudSqlite(LedgerCrud): ) -> None: await (conn or db).execute( f""" - INSERT INTO {table_with_schema(db, 'mint_quotes')} + INSERT INTO {db.table_with_schema('mint_quotes')} (quote, method, request, checking_id, unit, amount, issued, paid, state, created_time, paid_time) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (:quote, :method, :request, :checking_id, :unit, :amount, :issued, :paid, :state, :created_time, :paid_time) """, - ( - quote.quote, - quote.method, - quote.request, - quote.checking_id, - quote.unit, - quote.amount, - quote.issued, - quote.paid, - quote.state.name, - timestamp_from_seconds(db, quote.created_time), - timestamp_from_seconds(db, quote.paid_time), - ), + { + "quote": quote.quote, + "method": quote.method, + "request": quote.request, + "checking_id": quote.checking_id, + "unit": quote.unit, + "amount": quote.amount, + "issued": quote.issued, + "paid": quote.paid, + "state": quote.state.name, + "created_time": db.to_timestamp( + db.timestamp_from_seconds(quote.created_time) or "" + ), + "paid_time": db.to_timestamp( + db.timestamp_from_seconds(quote.paid_time) or "" + ), + }, ) async def get_mint_quote( @@ -462,26 +460,25 @@ class LedgerCrudSqlite(LedgerCrud): conn: Optional[Connection] = None, ) -> Optional[MintQuote]: clauses = [] - values: List[Any] = [] + values: Dict[str, Any] = {} if quote_id: - clauses.append("quote = ?") - values.append(quote_id) + clauses.append("quote = :quote_id") + values["quote_id"] = quote_id if checking_id: - clauses.append("checking_id = ?") - values.append(checking_id) + clauses.append("checking_id = :checking_id") + values["checking_id"] = checking_id if request: - clauses.append("request = ?") - values.append(request) + clauses.append("request = :request") + values["request"] = request if not any(clauses): raise ValueError("No search criteria") - where = f"WHERE {' AND '.join(clauses)}" row = await (conn or db).fetchone( f""" - SELECT * from {table_with_schema(db, 'mint_quotes')} + SELECT * from {db.table_with_schema('mint_quotes')} {where} """, - tuple(values), + values, ) if row is None: return None @@ -496,10 +493,10 @@ class LedgerCrudSqlite(LedgerCrud): ) -> Optional[MintQuote]: row = await (conn or db).fetchone( f""" - SELECT * from {table_with_schema(db, 'mint_quotes')} - WHERE request = ? + SELECT * from {db.table_with_schema('mint_quotes')} + WHERE request = :request """, - (request,), + {"request": request}, ) return MintQuote.from_row(row) if row else None @@ -511,15 +508,16 @@ class LedgerCrudSqlite(LedgerCrud): conn: Optional[Connection] = None, ) -> None: await (conn or db).execute( - f"UPDATE {table_with_schema(db, 'mint_quotes')} SET issued = ?, paid = ?," - " state = ?, paid_time = ? WHERE quote = ?", - ( - quote.issued, - quote.paid, - quote.state.name, - timestamp_from_seconds(db, quote.paid_time), - quote.quote, - ), + f"UPDATE {db.table_with_schema('mint_quotes')} SET issued = :issued, paid = :paid, state = :state, paid_time = :paid_time WHERE quote = :quote", + { + "issued": quote.issued, + "paid": quote.paid, + "state": quote.state.name, + "paid_time": db.to_timestamp( + db.timestamp_from_seconds(quote.paid_time) or "" + ), + "quote": quote.quote, + }, ) # async def update_mint_quote_paid( @@ -531,7 +529,7 @@ class LedgerCrudSqlite(LedgerCrud): # conn: Optional[Connection] = None, # ) -> None: # await (conn or db).execute( - # f"UPDATE {table_with_schema(db, 'mint_quotes')} SET paid = ? WHERE" + # f"UPDATE {db.table_with_schema('mint_quotes')} SET paid = ? WHERE" # " quote = ?", # ( # paid, @@ -548,27 +546,33 @@ class LedgerCrudSqlite(LedgerCrud): ) -> None: await (conn or db).execute( f""" - INSERT INTO {table_with_schema(db, 'melt_quotes')} + INSERT INTO {db.table_with_schema('melt_quotes')} (quote, method, request, checking_id, unit, amount, fee_reserve, paid, state, created_time, paid_time, fee_paid, proof, change, expiry) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (:quote, :method, :request, :checking_id, :unit, :amount, :fee_reserve, :paid, :state, :created_time, :paid_time, :fee_paid, :proof, :change, :expiry) """, - ( - quote.quote, - quote.method, - quote.request, - quote.checking_id, - quote.unit, - quote.amount, - quote.fee_reserve or 0, - quote.paid, - quote.state.name, - timestamp_from_seconds(db, quote.created_time), - timestamp_from_seconds(db, quote.paid_time), - quote.fee_paid, - quote.payment_preimage, - json.dumps(quote.change) if quote.change else None, - timestamp_from_seconds(db, quote.expiry), - ), + { + "quote": quote.quote, + "method": quote.method, + "request": quote.request, + "checking_id": quote.checking_id, + "unit": quote.unit, + "amount": quote.amount, + "fee_reserve": quote.fee_reserve or 0, + "paid": quote.paid, + "state": quote.state.name, + "created_time": db.to_timestamp( + db.timestamp_from_seconds(quote.created_time) or "" + ), + "paid_time": db.to_timestamp( + db.timestamp_from_seconds(quote.paid_time) or "" + ), + "fee_paid": quote.fee_paid, + "proof": quote.payment_preimage, + "change": json.dumps(quote.change) if quote.change else None, + "expiry": db.to_timestamp( + db.timestamp_from_seconds(quote.expiry) or "" + ), + }, ) async def get_melt_quote( @@ -581,26 +585,25 @@ class LedgerCrudSqlite(LedgerCrud): conn: Optional[Connection] = None, ) -> Optional[MeltQuote]: clauses = [] - values: List[Any] = [] + values: Dict[str, Any] = {} if quote_id: - clauses.append("quote = ?") - values.append(quote_id) + clauses.append("quote = :quote_id") + values["quote_id"] = quote_id if checking_id: - clauses.append("checking_id = ?") - values.append(checking_id) + clauses.append("checking_id = :checking_id") + values["checking_id"] = checking_id if request: - clauses.append("request = ?") - values.append(request) + clauses.append("request = :request") + values["request"] = request if not any(clauses): raise ValueError("No search criteria") where = f"WHERE {' AND '.join(clauses)}" - row = await (conn or db).fetchone( f""" - SELECT * from {table_with_schema(db, 'melt_quotes')} + SELECT * from {db.table_with_schema('melt_quotes')} {where} """, - tuple(values), + values, ) if row is None: return None @@ -614,17 +617,22 @@ class LedgerCrudSqlite(LedgerCrud): conn: Optional[Connection] = None, ) -> None: await (conn or db).execute( - f"UPDATE {table_with_schema(db, 'melt_quotes')} SET paid = ?, state = ?," - " fee_paid = ?, paid_time = ?, proof = ?, change = ? WHERE quote = ?", - ( - quote.paid, - quote.state.name, - quote.fee_paid, - timestamp_from_seconds(db, quote.paid_time), - quote.payment_preimage, - json.dumps([s.dict() for s in quote.change]) if quote.change else None, - quote.quote, - ), + f""" + UPDATE {db.table_with_schema('melt_quotes')} SET paid = :paid, state = :state, fee_paid = :fee_paid, paid_time = :paid_time, proof = :proof, change = :change WHERE quote = :quote + """, + { + "paid": quote.paid, + "state": quote.state.name, + "fee_paid": quote.fee_paid, + "paid_time": db.to_timestamp( + db.timestamp_from_seconds(quote.paid_time) or "" + ), + "proof": quote.payment_preimage, + "change": json.dumps([s.dict() for s in quote.change]) + if quote.change + else None, + "quote": quote.quote, + }, ) async def store_keyset( @@ -634,26 +642,30 @@ class LedgerCrudSqlite(LedgerCrud): keyset: MintKeyset, conn: Optional[Connection] = None, ) -> None: - await (conn or db).execute( # type: ignore + await (conn or db).execute( f""" - INSERT INTO {table_with_schema(db, 'keysets')} + INSERT INTO {db.table_with_schema('keysets')} (id, seed, encrypted_seed, seed_encryption_method, derivation_path, valid_from, valid_to, first_seen, active, version, unit, input_fee_ppk) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (:id, :seed, :encrypted_seed, :seed_encryption_method, :derivation_path, :valid_from, :valid_to, :first_seen, :active, :version, :unit, :input_fee_ppk) """, - ( - keyset.id, - keyset.seed, - keyset.encrypted_seed, - keyset.seed_encryption_method, - keyset.derivation_path, - keyset.valid_from or timestamp_now(db), - keyset.valid_to or timestamp_now(db), - keyset.first_seen or timestamp_now(db), - True, - keyset.version, - keyset.unit.name, - keyset.input_fee_ppk, - ), + { + "id": keyset.id, + "seed": keyset.seed, + "encrypted_seed": keyset.encrypted_seed, + "seed_encryption_method": keyset.seed_encryption_method, + "derivation_path": keyset.derivation_path, + "valid_from": db.to_timestamp( + keyset.valid_from or db.timestamp_now_str() + ), + "valid_to": db.to_timestamp(keyset.valid_to or db.timestamp_now_str()), + "first_seen": db.to_timestamp( + keyset.first_seen or db.timestamp_now_str() + ), + "active": True, + "version": keyset.version, + "unit": keyset.unit.name, + "input_fee_ppk": keyset.input_fee_ppk, + }, ) async def get_balance( @@ -663,7 +675,7 @@ class LedgerCrudSqlite(LedgerCrud): ) -> int: row = await (conn or db).fetchone( f""" - SELECT * from {table_with_schema(db, 'balance')} + SELECT * from {db.table_with_schema('balance')} """ ) assert row, "Balance not found" @@ -681,32 +693,32 @@ class LedgerCrudSqlite(LedgerCrud): conn: Optional[Connection] = None, ) -> List[MintKeyset]: clauses = [] - values: List[Any] = [] + values: Dict = {} if active is not None: - clauses.append("active = ?") - values.append(active) + clauses.append("active = :active") + values["active"] = active if id is not None: - clauses.append("id = ?") - values.append(id) + clauses.append("id = :id") + values["id"] = id if derivation_path is not None: - clauses.append("derivation_path = ?") - values.append(derivation_path) + clauses.append("derivation_path = :derivation_path") + values["derivation_path"] = derivation_path if seed is not None: - clauses.append("seed = ?") - values.append(seed) + clauses.append("seed = :seed") + values["seed"] = seed if unit is not None: - clauses.append("unit = ?") - values.append(unit) + clauses.append("unit = :unit") + values["unit"] = unit where = "" if clauses: where = f"WHERE {' AND '.join(clauses)}" rows = await (conn or db).fetchall( # type: ignore f""" - SELECT * from {table_with_schema(db, 'keysets')} + SELECT * from {db.table_with_schema('keysets')} {where} """, - tuple(values), + values, ) return [MintKeyset(**row) for row in rows] @@ -719,9 +731,9 @@ class LedgerCrudSqlite(LedgerCrud): ) -> Optional[Proof]: row = await (conn or db).fetchone( f""" - SELECT * from {table_with_schema(db, 'proofs_used')} - WHERE y = ? + SELECT * from {db.table_with_schema('proofs_used')} + WHERE y = :y """, - (Y,), + {"y": Y}, ) return Proof(**row) if row else None diff --git a/cashu/mint/db/read.py b/cashu/mint/db/read.py index c3a8409..ac35ec8 100644 --- a/cashu/mint/db/read.py +++ b/cashu/mint/db/read.py @@ -1,7 +1,7 @@ -from typing import Dict, List +from typing import Dict, List, Optional from ...core.base import Proof, ProofSpentState, ProofState -from ...core.db import Database +from ...core.db import Connection, Database from ..crud import LedgerCrud @@ -13,28 +13,37 @@ class DbReadHelper: self.db = db self.crud = crud - async def _get_proofs_pending(self, Ys: List[str]) -> Dict[str, Proof]: + async def _get_proofs_pending( + self, Ys: List[str], conn: Optional[Connection] = None + ) -> 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) + async with self.db.get_connection(conn) as conn: + proofs_pending = await self.crud.get_proofs_pending( + Ys=Ys, db=self.db, conn=conn + ) 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]: + async def _get_proofs_spent( + self, Ys: List[str], conn: Optional[Connection] = None + ) -> 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: + async with self.db.get_connection(conn) 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]: + async def get_proofs_states( + self, Ys: List[str], conn: Optional[Connection] = None + ) -> 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. @@ -50,19 +59,20 @@ class DbReadHelper: 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=ProofSpentState.unspent)) - elif Y not in proofs_spent and Y in proofs_pending: - states.append(ProofState(Y=Y, state=ProofSpentState.pending)) - else: - states.append( - ProofState( - Y=Y, - state=ProofSpentState.spent, - witness=proofs_spent[Y].witness, + async with self.db.get_connection(conn) as conn: + proofs_spent = await self._get_proofs_spent(Ys, conn) + proofs_pending = await self._get_proofs_pending(Ys, conn) + for Y in Ys: + if Y not in proofs_spent and Y not in proofs_pending: + states.append(ProofState(Y=Y, state=ProofSpentState.unspent)) + elif Y not in proofs_spent and Y in proofs_pending: + states.append(ProofState(Y=Y, state=ProofSpentState.pending)) + else: + states.append( + ProofState( + Y=Y, + state=ProofSpentState.spent, + witness=proofs_spent[Y].witness, + ) ) - ) return states diff --git a/cashu/mint/db/write.py b/cashu/mint/db/write.py index 21495c5..7f3a305 100644 --- a/cashu/mint/db/write.py +++ b/cashu/mint/db/write.py @@ -1,10 +1,18 @@ -import asyncio -from typing import List, Optional +import random +from typing import List, Optional, Union from loguru import logger -from ...core.base import Proof, ProofSpentState, ProofState -from ...core.db import Connection, Database, get_db_connection +from ...core.base import ( + MeltQuote, + MeltQuoteState, + MintQuote, + MintQuoteState, + Proof, + ProofSpentState, + ProofState, +) +from ...core.db import Connection, Database from ...core.errors import ( TransactionError, ) @@ -16,9 +24,6 @@ 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 @@ -41,20 +46,28 @@ class DbWriteHelper: 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: + random_id = random.randint(0, 1000000) + try: + logger.debug("trying to set proofs pending") + logger.trace(f"get_connection: random_id: {random_id}") + async with self.db.get_connection( + lock_table="proofs_pending", + lock_timeout=1, + ) as conn: + logger.trace(f"get_connection: got connection {random_id}") 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=ProofSpentState.pending) - ) - except Exception as e: - logger.error(f"Failed to set proofs pending: {e}") - raise TransactionError("Failed to set proofs pending.") + for p in proofs: + logger.trace(f"crud: setting proof {p.Y} as PENDING") + await self.crud.set_proof_pending( + proof=p, db=self.db, quote_id=quote_id, conn=conn + ) + logger.trace(f"crud: set proof {p.Y} as PENDING") + except Exception as e: + logger.error(f"Failed to set proofs pending: {e}") + raise TransactionError(f"Failed to set proofs pending: {str(e)}") + logger.trace("_set_proofs_pending released lock") + for p in proofs: + await self.events.submit(ProofState(Y=p.Y, state=ProofSpentState.pending)) async def _unset_proofs_pending(self, proofs: List[Proof], spent=True) -> None: """Deletes proofs from pending table. @@ -66,14 +79,16 @@ class DbWriteHelper: 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=ProofSpentState.unspent) - ) + async with self.db.get_connection() as conn: + for p in proofs: + logger.trace(f"crud: un-setting proof {p.Y} as PENDING") + await self.crud.unset_proof_pending(proof=p, db=self.db, conn=conn) + + if not spent: + for p in proofs: + await self.events.submit( + ProofState(Y=p.Y, state=ProofSpentState.unspent) + ) async def _validate_proofs_pending( self, proofs: List[Proof], conn: Optional[Connection] = None @@ -86,12 +101,120 @@ class DbWriteHelper: 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 - ): + logger.trace("crud: validating proofs pending") + pending_proofs = await self.crud.get_proofs_pending( + Ys=[p.Y for p in proofs], db=self.db, conn=conn + ) + if not (len(pending_proofs) == 0): raise TransactionError("proofs are pending.") + + async def _set_mint_quote_pending(self, quote_id: str) -> MintQuote: + """Sets the mint quote as pending. + + Args: + quote (MintQuote): Mint quote to set as pending. + """ + quote: Union[MintQuote, None] = None + async with self.db.get_connection( + lock_table="mint_quotes", lock_select_statement=f"quote='{quote_id}'" + ) as conn: + # get mint quote from db and check if it is already pending + quote = await self.crud.get_mint_quote( + quote_id=quote_id, db=self.db, conn=conn + ) + if not quote: + raise TransactionError("Mint quote not found.") + if quote.state == MintQuoteState.pending: + raise TransactionError("Mint quote already pending.") + if not quote.state == MintQuoteState.paid: + raise TransactionError("Mint quote is not paid yet.") + # set the quote as pending + quote.state = MintQuoteState.pending + logger.trace(f"crud: setting quote {quote_id} as PENDING") + await self.crud.update_mint_quote(quote=quote, db=self.db, conn=conn) + if quote is None: + raise TransactionError("Mint quote not found.") + return quote + + async def _unset_mint_quote_pending( + self, quote_id: str, state: MintQuoteState + ) -> MintQuote: + """Unsets the mint quote as pending. + + Args: + quote (MintQuote): Mint quote to unset as pending. + state (MintQuoteState): New state of the mint quote. + """ + quote: Union[MintQuote, None] = None + async with self.db.get_connection(lock_table="mint_quotes") as conn: + # get mint quote from db and check if it is pending + quote = await self.crud.get_mint_quote( + quote_id=quote_id, db=self.db, conn=conn + ) + if not quote: + raise TransactionError("Mint quote not found.") + if quote.state != MintQuoteState.pending: + raise TransactionError( + f"Mint quote not pending: {quote.state.value}. Cannot set as {state.value}." + ) + # set the quote as pending + quote.state = state + logger.trace(f"crud: setting quote {quote_id} as {state.value}") + await self.crud.update_mint_quote(quote=quote, db=self.db, conn=conn) + if quote is None: + raise TransactionError("Mint quote not found.") + + await self.events.submit(quote) + return quote + + async def _set_melt_quote_pending(self, quote: MeltQuote) -> MeltQuote: + """Sets the melt quote as pending. + + Args: + quote (MeltQuote): Melt quote to set as pending. + """ + quote_copy = quote.copy() + async with self.db.get_connection( + lock_table="melt_quotes", + lock_select_statement=f"checking_id='{quote.checking_id}'", + ) as conn: + # get melt quote from db and check if it is already pending + quote_db = await self.crud.get_melt_quote( + checking_id=quote.checking_id, db=self.db, conn=conn + ) + if not quote_db: + raise TransactionError("Melt quote not found.") + if quote_db.state == MeltQuoteState.pending: + raise TransactionError("Melt quote already pending.") + # set the quote as pending + quote_copy.state = MeltQuoteState.pending + await self.crud.update_melt_quote(quote=quote_copy, db=self.db, conn=conn) + + await self.events.submit(quote_copy) + return quote_copy + + async def _unset_melt_quote_pending( + self, quote: MeltQuote, state: MeltQuoteState + ) -> MeltQuote: + """Unsets the melt quote as pending. + + Args: + quote (MeltQuote): Melt quote to unset as pending. + state (MeltQuoteState): New state of the melt quote. + """ + quote_copy = quote.copy() + async with self.db.get_connection(lock_table="melt_quotes") as conn: + # get melt quote from db and check if it is pending + quote_db = await self.crud.get_melt_quote( + checking_id=quote.checking_id, db=self.db, conn=conn + ) + if not quote_db: + raise TransactionError("Melt quote not found.") + if quote_db.state != MeltQuoteState.pending: + raise TransactionError("Melt quote not pending.") + # set the quote as pending + quote_copy.state = state + await self.crud.update_melt_quote(quote=quote_copy, db=self.db, conn=conn) + + await self.events.submit(quote_copy) + return quote_copy diff --git a/cashu/mint/encrypt.py b/cashu/mint/encrypt.py index adf0113..f2f22b9 100644 --- a/cashu/mint/encrypt.py +++ b/cashu/mint/encrypt.py @@ -8,7 +8,7 @@ except ImportError: import asyncio from functools import wraps -from cashu.core.db import Database, table_with_schema +from cashu.core.db import Database from cashu.core.migrations import migrate_databases from cashu.core.settings import settings from cashu.mint import migrations @@ -95,7 +95,7 @@ async def migrate(no_dry_run): # get all keysets async with ledger.db.connect() as conn: rows = await conn.fetchall( - f"SELECT * FROM {table_with_schema(ledger.db, 'keysets')} WHERE seed IS NOT" + f"SELECT * FROM {ledger.db.table_with_schema('keysets')} WHERE seed IS NOT" " NULL" ) click.echo(f"Found {len(rows)} keysets in database.") @@ -138,7 +138,7 @@ async def migrate(no_dry_run): for keyset_dict in keysets_migrate: click.echo(f"Updating keyset {keyset_dict['id']}") await conn.execute( - f"UPDATE {table_with_schema(ledger.db, 'keysets')} SET seed=''," + f"UPDATE {ledger.db.table_with_schema('keysets')} SET seed=''," " encrypted_seed = ?, seed_encryption_method = ? WHERE id = ?", ( keyset_dict["encrypted_seed"], diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 57aff80..5ff9b3b 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -28,7 +28,7 @@ from ..core.crypto.keys import ( random_hash, ) from ..core.crypto.secp import PrivateKey, PublicKey -from ..core.db import Connection, Database, get_db_connection +from ..core.db import Connection, Database from ..core.errors import ( CashuError, KeysetError, @@ -68,6 +68,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe keysets: Dict[str, MintKeyset] = {} events = LedgerEventManager() db_read: DbReadHelper + invoice_listener_tasks: List[asyncio.Task] = [] def __init__( self, @@ -106,7 +107,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe async def startup_ledger(self): await self._startup_ledger() await self._check_pending_proofs_and_melt_quotes() - await self.dispatch_listeners() + self.invoice_listener_tasks = await self.dispatch_listeners() async def _startup_ledger(self): await self.init_keysets() @@ -132,6 +133,11 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe logger.info(f"Data dir: {settings.cashu_dir}") + async def shutdown_ledger(self): + await self.db.engine.dispose() + for task in self.invoice_listener_tasks: + task.cancel() + async def _check_pending_proofs_and_melt_quotes(self): """Startup routine that checks all pending proofs for their melt state and either invalidates them for a successful melt or deletes them if the melt failed. @@ -168,7 +174,6 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe await self.db_write._unset_proofs_pending(pending_proofs) elif payment.failed: logger.info(f"Melt quote {quote.quote} state: failed") - # unset pending await self.db_write._unset_proofs_pending(pending_proofs, spent=False) elif payment.pending: @@ -298,9 +303,10 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe 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. """ - async with get_db_connection(self.db, conn) as conn: + async with self.db.get_connection(conn) as conn: # store in db for p in proofs: + logger.trace(f"Invalidating proof {p.Y}") await self.crud.invalidate_proof( proof=p, db=self.db, quote_id=quote_id, conn=conn ) @@ -469,12 +475,26 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe unit ].get_invoice_status(quote.checking_id) if status.paid: - logger.trace(f"Setting quote {quote_id} as paid") - quote.paid = True - quote.state = MintQuoteState.paid - quote.paid_time = int(time.time()) - await self.crud.update_mint_quote(quote=quote, db=self.db) - await self.events.submit(quote) + # change state to paid in one transaction, it could have been marked paid + # by the invoice listener in the mean time + async with self.db.get_connection( + lock_table="mint_quotes", + lock_select_statement=f"quote='{quote_id}'", + ) as conn: + quote = await self.crud.get_mint_quote( + quote_id=quote_id, db=self.db, conn=conn + ) + if not quote: + raise Exception("quote not found") + if quote.state == MintQuoteState.unpaid: + logger.trace(f"Setting quote {quote_id} as paid") + quote.paid = True + quote.state = MintQuoteState.paid + quote.paid_time = int(time.time()) + await self.crud.update_mint_quote( + quote=quote, db=self.db, conn=conn + ) + await self.events.submit(quote) return quote @@ -501,46 +521,39 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe Returns: List[BlindedSignature]: Signatures on the outputs. """ - logger.trace("called mint") + await self._verify_outputs(outputs) sum_amount_outputs = sum([b.amount for b in outputs]) # we already know from _verify_outputs that all outputs have the same unit because they have the same keyset output_unit = self.keysets[outputs[0].id].unit - self.locks[quote_id] = ( - self.locks.get(quote_id) or asyncio.Lock() - ) # create a new lock if it doesn't exist - async with self.locks[quote_id]: - quote = await self.get_mint_quote(quote_id=quote_id) - if not quote.paid: - raise QuoteNotPaidError() - if quote.issued: - raise TransactionError("quote already issued") - - if not quote.state == MintQuoteState.paid: - raise QuoteNotPaidError() - if quote.state == MintQuoteState.issued: - raise TransactionError("quote already issued") - + quote = await self.get_mint_quote(quote_id) + if quote.state == MintQuoteState.pending: + raise TransactionError("Mint quote already pending.") + if quote.state == MintQuoteState.issued: + raise TransactionError("Mint quote already issued.") + if not quote.state == MintQuoteState.paid: + raise QuoteNotPaidError() + previous_state = quote.state + await self.db_write._set_mint_quote_pending(quote_id=quote_id) + try: if not quote.unit == output_unit.name: raise TransactionError("quote unit does not match output unit") if not quote.amount == sum_amount_outputs: raise TransactionError("amount to mint does not match quote amount") if quote.expiry and quote.expiry > int(time.time()): raise TransactionError("quote expired") - - logger.trace(f"crud: setting quote {quote_id} as issued") - quote.issued = True - quote.state = MintQuoteState.issued - await self.crud.update_mint_quote(quote=quote, db=self.db) - promises = await self._generate_promises(outputs) - logger.trace("generated promises") + except Exception as e: + await self.db_write._unset_mint_quote_pending( + quote_id=quote_id, state=previous_state + ) + raise e - # submit the quote update to the event manager - await self.events.submit(quote) + await self.db_write._unset_mint_quote_pending( + quote_id=quote_id, state=MintQuoteState.issued + ) - del self.locks[quote_id] return promises def create_internal_melt_quote( @@ -567,7 +580,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe if mint_quote.state == MintQuoteState.issued: raise TransactionError("mint quote already issued") if mint_quote.state != MintQuoteState.unpaid: - raise TransactionError("mint quote already paid") + raise TransactionError("mint quote is not unpaid") if not mint_quote.checking_id: raise TransactionError("mint quote has no checking id") @@ -800,7 +813,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe raise TransactionError("mint quote already issued") if mint_quote.state != MintQuoteState.unpaid: - raise TransactionError("mint quote already paid") + raise TransactionError("mint quote is not unpaid") logger.info( f"Settling bolt11 payment internally: {melt_quote.quote} ->" @@ -816,7 +829,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe mint_quote.state = MintQuoteState.paid mint_quote.paid_time = melt_quote.paid_time - async with get_db_connection(self.db) as conn: + async with self.db.get_connection() as conn: await self.crud.update_melt_quote(quote=melt_quote, db=self.db, conn=conn) await self.crud.update_mint_quote(quote=mint_quote, db=self.db, conn=conn) @@ -970,22 +983,13 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe Tuple[List[BlindSignature],List[BlindSignature]]: Promises on both sides of the split. """ logger.trace("split called") - # explicitly check that amount of inputs is equal to amount of outputs - # note: we check this again in verify_inputs_and_outputs but only if any - # outputs are provided at all. To make sure of that before calling - # verify_inputs_and_outputs, we check it here. - self._verify_equation_balanced(proofs, outputs) # verify spending inputs, outputs, and spending conditions await self.verify_inputs_and_outputs(proofs=proofs, outputs=outputs) - await self.db_write._set_proofs_pending(proofs) try: - # Mark proofs as used and prepare new promises - async with get_db_connection(self.db) as conn: - # we do this in a single db transaction + async with self.db.get_connection(lock_table="proofs_pending") as conn: await self._invalidate_proofs(proofs=proofs, conn=conn) promises = await self._generate_promises(outputs, keyset, conn) - except Exception as e: logger.trace(f"split failed: {e}") raise e @@ -1001,7 +1005,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe ) -> Tuple[List[BlindedMessage], List[BlindedSignature]]: signatures: List[BlindedSignature] = [] return_outputs: List[BlindedMessage] = [] - async with get_db_connection(self.db) as conn: + async with self.db.get_connection() as conn: for output in outputs: logger.trace(f"looking for promise: {output}") promise = await self.crud.get_promise( @@ -1057,7 +1061,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe keyset = keyset or self.keyset signatures = [] - async with get_db_connection(self.db, conn) as conn: + async with self.db.get_connection(conn) as conn: for promise in promises: keyset_id, B_, amount, C_, e, s = promise logger.trace(f"crud: _generate_promise storing promise for {amount}") diff --git a/cashu/mint/migrations.py b/cashu/mint/migrations.py index 3c03250..88ec94e 100644 --- a/cashu/mint/migrations.py +++ b/cashu/mint/migrations.py @@ -2,14 +2,14 @@ import copy from ..core.base import MintKeyset, Proof from ..core.crypto.keys import derive_keyset_id, derive_keyset_id_deprecated -from ..core.db import Connection, Database, table_with_schema, timestamp_now +from ..core.db import Connection, Database from ..core.settings import settings async def m000_create_migrations_table(conn: Connection): await conn.execute( f""" - CREATE TABLE IF NOT EXISTS {table_with_schema(conn, 'dbversions')} ( + CREATE TABLE IF NOT EXISTS {conn.table_with_schema('dbversions')} ( db TEXT PRIMARY KEY, version INT NOT NULL ) @@ -21,7 +21,7 @@ async def m001_initial(db: Database): async with db.connect() as conn: await conn.execute( f""" - CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'promises')} ( + CREATE TABLE IF NOT EXISTS {db.table_with_schema('promises')} ( amount {db.big_int} NOT NULL, b_b TEXT NOT NULL, c_b TEXT NOT NULL, @@ -34,7 +34,7 @@ async def m001_initial(db: Database): await conn.execute( f""" - CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_used')} ( + CREATE TABLE IF NOT EXISTS {db.table_with_schema('proofs_used')} ( amount {db.big_int} NOT NULL, c TEXT NOT NULL, secret TEXT NOT NULL, @@ -47,7 +47,7 @@ async def m001_initial(db: Database): await conn.execute( f""" - CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'invoices')} ( + CREATE TABLE IF NOT EXISTS {db.table_with_schema('invoices')} ( amount {db.big_int} NOT NULL, pr TEXT NOT NULL, hash TEXT NOT NULL, @@ -61,20 +61,20 @@ async def m001_initial(db: Database): async def drop_balance_views(db: Database, conn: Connection): - await conn.execute(f"DROP VIEW IF EXISTS {table_with_schema(db, 'balance')}") - await conn.execute(f"DROP VIEW IF EXISTS {table_with_schema(db, 'balance_issued')}") + await conn.execute(f"DROP VIEW IF EXISTS {db.table_with_schema('balance')}") + await conn.execute(f"DROP VIEW IF EXISTS {db.table_with_schema('balance_issued')}") await conn.execute( - f"DROP VIEW IF EXISTS {table_with_schema(db, 'balance_redeemed')}" + f"DROP VIEW IF EXISTS {db.table_with_schema('balance_redeemed')}" ) async def create_balance_views(db: Database, conn: Connection): await conn.execute( f""" - CREATE VIEW {table_with_schema(db, 'balance_issued')} AS + CREATE VIEW {db.table_with_schema('balance_issued')} AS SELECT COALESCE(SUM(s), 0) AS balance FROM ( SELECT SUM(amount) AS s - FROM {table_with_schema(db, 'promises')} + FROM {db.table_with_schema('promises')} WHERE amount > 0 ) AS balance_issued; """ @@ -82,10 +82,10 @@ async def create_balance_views(db: Database, conn: Connection): await conn.execute( f""" - CREATE VIEW {table_with_schema(db, 'balance_redeemed')} AS + CREATE VIEW {db.table_with_schema('balance_redeemed')} AS SELECT COALESCE(SUM(s), 0) AS balance FROM ( SELECT SUM(amount) AS s - FROM {table_with_schema(db, 'proofs_used')} + FROM {db.table_with_schema('proofs_used')} WHERE amount > 0 ) AS balance_redeemed; """ @@ -93,11 +93,11 @@ async def create_balance_views(db: Database, conn: Connection): await conn.execute( f""" - CREATE VIEW {table_with_schema(db, 'balance')} AS + CREATE VIEW {db.table_with_schema('balance')} AS SELECT s_issued - s_used FROM ( SELECT bi.balance AS s_issued, bu.balance AS s_used - FROM {table_with_schema(db, 'balance_issued')} bi - CROSS JOIN {table_with_schema(db, 'balance_redeemed')} bu + FROM {db.table_with_schema('balance_issued')} bi + CROSS JOIN {db.table_with_schema('balance_redeemed')} bu ) AS balance; """ ) @@ -115,7 +115,7 @@ async def m003_mint_keysets(db: Database): async with db.connect() as conn: await conn.execute( f""" - CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'keysets')} ( + CREATE TABLE IF NOT EXISTS {db.table_with_schema('keysets')} ( id TEXT NOT NULL, derivation_path TEXT, valid_from TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, @@ -130,7 +130,7 @@ async def m003_mint_keysets(db: Database): ) await conn.execute( f""" - CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'mint_pubkeys')} ( + CREATE TABLE IF NOT EXISTS {db.table_with_schema('mint_pubkeys')} ( id TEXT NOT NULL, amount {db.big_int} NOT NULL, pubkey TEXT NOT NULL, @@ -148,7 +148,7 @@ async def m004_keysets_add_version(db: Database): """ async with db.connect() as conn: await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'keysets')} ADD COLUMN version TEXT" + f"ALTER TABLE {db.table_with_schema('keysets')} ADD COLUMN version TEXT" ) @@ -159,7 +159,7 @@ async def m005_pending_proofs_table(db: Database) -> None: async with db.connect() as conn: await conn.execute( f""" - CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_pending')} ( + CREATE TABLE IF NOT EXISTS {db.table_with_schema('proofs_pending')} ( amount {db.big_int} NOT NULL, c TEXT NOT NULL, secret TEXT NOT NULL, @@ -179,11 +179,11 @@ async def m006_invoices_add_payment_hash(db: Database): """ async with db.connect() as conn: await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'invoices')} ADD COLUMN payment_hash" + f"ALTER TABLE {db.table_with_schema('invoices')} ADD COLUMN payment_hash" " TEXT" ) await conn.execute( - f"UPDATE {table_with_schema(db, 'invoices')} SET payment_hash = hash" + f"UPDATE {db.table_with_schema('invoices')} SET payment_hash = hash" ) @@ -193,13 +193,13 @@ async def m007_proofs_and_promises_store_id(db: Database): """ async with db.connect() as conn: await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'proofs_used')} ADD COLUMN id TEXT" + f"ALTER TABLE {db.table_with_schema('proofs_used')} ADD COLUMN id TEXT" ) await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'proofs_pending')} ADD COLUMN id TEXT" + f"ALTER TABLE {db.table_with_schema('proofs_pending')} ADD COLUMN id TEXT" ) await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN id TEXT" + f"ALTER TABLE {db.table_with_schema('promises')} ADD COLUMN id TEXT" ) @@ -209,10 +209,10 @@ async def m008_promises_dleq(db: Database): """ async with db.connect() as conn: await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN e TEXT" + f"ALTER TABLE {db.table_with_schema('promises')} ADD COLUMN e TEXT" ) await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN s TEXT" + f"ALTER TABLE {db.table_with_schema('promises')} ADD COLUMN s TEXT" ) @@ -221,16 +221,16 @@ async def m009_add_out_to_invoices(db: Database): async with db.connect() as conn: # rename column pr to bolt11 await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'invoices')} RENAME COLUMN pr TO" + f"ALTER TABLE {db.table_with_schema('invoices')} RENAME COLUMN pr TO" " bolt11" ) # rename column hash to payment_hash await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'invoices')} RENAME COLUMN hash TO id" + f"ALTER TABLE {db.table_with_schema('invoices')} RENAME COLUMN hash TO id" ) await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'invoices')} ADD COLUMN out BOOL" + f"ALTER TABLE {db.table_with_schema('invoices')} ADD COLUMN out BOOL" ) @@ -240,7 +240,7 @@ async def m010_add_index_to_proofs_used(db: Database): await conn.execute( "CREATE INDEX IF NOT EXISTS" " proofs_used_secret_idx ON" - f" {table_with_schema(db, 'proofs_used')} (secret)" + f" {db.table_with_schema('proofs_used')} (secret)" ) @@ -250,37 +250,37 @@ async def m011_add_quote_tables(db: Database): tables = ["invoices", "promises", "proofs_used", "proofs_pending"] for table in tables: await conn.execute( - f"ALTER TABLE {table_with_schema(db, table)} ADD COLUMN created" + f"ALTER TABLE {db.table_with_schema(table)} ADD COLUMN created" " TIMESTAMP" ) await conn.execute( - f"UPDATE {table_with_schema(db, table)} SET created =" - f" '{timestamp_now(db)}'" + f"UPDATE {db.table_with_schema(table)} SET created =" + f" '{db.timestamp_now_str()}'" ) # add column "witness" to table proofs_used await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'proofs_used')} ADD COLUMN witness" + f"ALTER TABLE {db.table_with_schema('proofs_used')} ADD COLUMN witness" " TEXT" ) # add columns "seed" and "unit" to table keysets await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'keysets')} ADD COLUMN seed TEXT" + f"ALTER TABLE {db.table_with_schema('keysets')} ADD COLUMN seed TEXT" ) await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'keysets')} ADD COLUMN unit TEXT" + f"ALTER TABLE {db.table_with_schema('keysets')} ADD COLUMN unit TEXT" ) # fill columns "seed" and "unit" in table keysets await conn.execute( - f"UPDATE {table_with_schema(db, 'keysets')} SET seed =" + f"UPDATE {db.table_with_schema('keysets')} SET seed =" f" '{settings.mint_private_key}', unit = 'sat'" ) await conn.execute( f""" - CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'mint_quotes')} ( + CREATE TABLE IF NOT EXISTS {db.table_with_schema('mint_quotes')} ( quote TEXT NOT NULL, method TEXT NOT NULL, request TEXT NOT NULL, @@ -300,7 +300,7 @@ async def m011_add_quote_tables(db: Database): await conn.execute( f""" - CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'melt_quotes')} ( + CREATE TABLE IF NOT EXISTS {db.table_with_schema('melt_quotes')} ( quote TEXT NOT NULL, method TEXT NOT NULL, request TEXT NOT NULL, @@ -321,15 +321,15 @@ async def m011_add_quote_tables(db: Database): ) await conn.execute( - f"INSERT INTO {table_with_schema(db, 'mint_quotes')} (quote, method," + f"INSERT INTO {db.table_with_schema('mint_quotes')} (quote, method," " request, checking_id, unit, amount, paid, issued, created_time," " paid_time) SELECT id, 'bolt11', bolt11, COALESCE(payment_hash, 'None')," - f" 'sat', amount, False, issued, COALESCE(created, '{timestamp_now(db)}')," - f" NULL FROM {table_with_schema(db, 'invoices')} " + f" 'sat', amount, False, issued, COALESCE(created, '{db.timestamp_now_str()}')," + f" NULL FROM {db.table_with_schema('invoices')} " ) # drop table invoices - await conn.execute(f"DROP TABLE {table_with_schema(db, 'invoices')}") + await conn.execute(f"DROP TABLE {db.table_with_schema('invoices')}") async def m012_keysets_uniqueness_with_seed(db: Database): @@ -338,16 +338,16 @@ async def m012_keysets_uniqueness_with_seed(db: Database): # and copy the data from keysets_old to keysets, then drop keysets_old async with db.connect() as conn: await conn.execute( - f"DROP TABLE IF EXISTS {table_with_schema(db, 'keysets_old')}" + f"DROP TABLE IF EXISTS {db.table_with_schema('keysets_old')}" ) await conn.execute( - f"CREATE TABLE {table_with_schema(db, 'keysets_old')} AS" - f" SELECT * FROM {table_with_schema(db, 'keysets')}" + f"CREATE TABLE {db.table_with_schema('keysets_old')} AS" + f" SELECT * FROM {db.table_with_schema('keysets')}" ) - await conn.execute(f"DROP TABLE {table_with_schema(db, 'keysets')}") + await conn.execute(f"DROP TABLE {db.table_with_schema('keysets')}") await conn.execute( f""" - CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'keysets')} ( + CREATE TABLE IF NOT EXISTS {db.table_with_schema('keysets')} ( id TEXT NOT NULL, derivation_path TEXT, seed TEXT, @@ -364,13 +364,13 @@ async def m012_keysets_uniqueness_with_seed(db: Database): """ ) await conn.execute( - f"INSERT INTO {table_with_schema(db, 'keysets')} (id," + f"INSERT INTO {db.table_with_schema('keysets')} (id," " derivation_path, valid_from, valid_to, first_seen," " active, version, seed, unit) SELECT id, derivation_path," " valid_from, valid_to, first_seen, active, version, seed," - f" unit FROM {table_with_schema(db, 'keysets_old')}" + f" unit FROM {db.table_with_schema('keysets_old')}" ) - await conn.execute(f"DROP TABLE {table_with_schema(db, 'keysets_old')}") + await conn.execute(f"DROP TABLE {db.table_with_schema('keysets_old')}") async def m013_keysets_add_encrypted_seed(db: Database): @@ -380,16 +380,16 @@ async def m013_keysets_add_encrypted_seed(db: Database): # with the same columns but with a unique constraint on id # and copy the data from keysets_old to keysets, then drop keysets_old await conn.execute( - f"DROP TABLE IF EXISTS {table_with_schema(db, 'keysets_old')}" + f"DROP TABLE IF EXISTS {db.table_with_schema('keysets_old')}" ) await conn.execute( - f"CREATE TABLE {table_with_schema(db, 'keysets_old')} AS" - f" SELECT * FROM {table_with_schema(db, 'keysets')}" + f"CREATE TABLE {db.table_with_schema('keysets_old')} AS" + f" SELECT * FROM {db.table_with_schema('keysets')}" ) - await conn.execute(f"DROP TABLE {table_with_schema(db, 'keysets')}") + await conn.execute(f"DROP TABLE {db.table_with_schema('keysets')}") await conn.execute( f""" - CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'keysets')} ( + CREATE TABLE IF NOT EXISTS {db.table_with_schema('keysets')} ( id TEXT NOT NULL, derivation_path TEXT, seed TEXT, @@ -406,21 +406,21 @@ async def m013_keysets_add_encrypted_seed(db: Database): """ ) await conn.execute( - f"INSERT INTO {table_with_schema(db, 'keysets')} (id," + f"INSERT INTO {db.table_with_schema('keysets')} (id," " derivation_path, valid_from, valid_to, first_seen," " active, version, seed, unit) SELECT id, derivation_path," " valid_from, valid_to, first_seen, active, version, seed," - f" unit FROM {table_with_schema(db, 'keysets_old')}" + f" unit FROM {db.table_with_schema('keysets_old')}" ) - await conn.execute(f"DROP TABLE {table_with_schema(db, 'keysets_old')}") + await conn.execute(f"DROP TABLE {db.table_with_schema('keysets_old')}") # add columns encrypted_seed and seed_encryption_method to keysets await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'keysets')} ADD COLUMN encrypted_seed" + f"ALTER TABLE {db.table_with_schema('keysets')} ADD COLUMN encrypted_seed" " TEXT" ) await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'keysets')} ADD COLUMN" + f"ALTER TABLE {db.table_with_schema('keysets')} ADD COLUMN" " seed_encryption_method TEXT" ) @@ -429,13 +429,13 @@ async def m014_proofs_add_Y_column(db: Database): # get all proofs_used and proofs_pending from the database and compute Y for each of them async with db.connect() as conn: rows = await conn.fetchall( - f"SELECT * FROM {table_with_schema(db, 'proofs_used')}" + f"SELECT * FROM {db.table_with_schema('proofs_used')}" ) # Proof() will compute Y from secret upon initialization proofs_used = [Proof(**r) for r in rows] rows = await conn.fetchall( - f"SELECT * FROM {table_with_schema(db, 'proofs_pending')}" + f"SELECT * FROM {db.table_with_schema('proofs_pending')}" ) proofs_pending = [Proof(**r) for r in rows] async with db.connect() as conn: @@ -443,27 +443,27 @@ async def m014_proofs_add_Y_column(db: Database): await drop_balance_views(db, conn) await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'proofs_used')} ADD COLUMN y TEXT" + f"ALTER TABLE {db.table_with_schema('proofs_used')} ADD COLUMN y TEXT" ) for proof in proofs_used: await conn.execute( - f"UPDATE {table_with_schema(db, 'proofs_used')} SET y = '{proof.Y}'" + f"UPDATE {db.table_with_schema('proofs_used')} SET y = '{proof.Y}'" f" WHERE secret = '{proof.secret}'" ) # Copy proofs_used to proofs_used_old and create a new table proofs_used # with the same columns but with a unique constraint on (Y) # and copy the data from proofs_used_old to proofs_used, then drop proofs_used_old await conn.execute( - f"DROP TABLE IF EXISTS {table_with_schema(db, 'proofs_used_old')}" + f"DROP TABLE IF EXISTS {db.table_with_schema('proofs_used_old')}" ) await conn.execute( - f"CREATE TABLE {table_with_schema(db, 'proofs_used_old')} AS" - f" SELECT * FROM {table_with_schema(db, 'proofs_used')}" + f"CREATE TABLE {db.table_with_schema('proofs_used_old')} AS" + f" SELECT * FROM {db.table_with_schema('proofs_used')}" ) - await conn.execute(f"DROP TABLE {table_with_schema(db, 'proofs_used')}") + await conn.execute(f"DROP TABLE {db.table_with_schema('proofs_used')}") await conn.execute( f""" - CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_used')} ( + CREATE TABLE IF NOT EXISTS {db.table_with_schema('proofs_used')} ( amount {db.big_int} NOT NULL, c TEXT NOT NULL, secret TEXT NOT NULL, @@ -478,19 +478,19 @@ async def m014_proofs_add_Y_column(db: Database): """ ) await conn.execute( - f"INSERT INTO {table_with_schema(db, 'proofs_used')} (amount, c, " + f"INSERT INTO {db.table_with_schema('proofs_used')} (amount, c, " "secret, id, y, created, witness) SELECT amount, c, secret, id, y," - f" created, witness FROM {table_with_schema(db, 'proofs_used_old')}" + f" created, witness FROM {db.table_with_schema('proofs_used_old')}" ) - await conn.execute(f"DROP TABLE {table_with_schema(db, 'proofs_used_old')}") + await conn.execute(f"DROP TABLE {db.table_with_schema('proofs_used_old')}") # add column y to proofs_pending await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'proofs_pending')} ADD COLUMN y TEXT" + f"ALTER TABLE {db.table_with_schema('proofs_pending')} ADD COLUMN y TEXT" ) for proof in proofs_pending: await conn.execute( - f"UPDATE {table_with_schema(db, 'proofs_pending')} SET y = '{proof.Y}'" + f"UPDATE {db.table_with_schema('proofs_pending')} SET y = '{proof.Y}'" f" WHERE secret = '{proof.secret}'" ) @@ -498,24 +498,24 @@ async def m014_proofs_add_Y_column(db: Database): # with the same columns but with a unique constraint on (Y) # and copy the data from proofs_pending_old to proofs_pending, then drop proofs_pending_old await conn.execute( - f"DROP TABLE IF EXISTS {table_with_schema(db, 'proofs_pending_old')}" + f"DROP TABLE IF EXISTS {db.table_with_schema('proofs_pending_old')}" ) await conn.execute( - f"CREATE TABLE {table_with_schema(db, 'proofs_pending_old')} AS" - f" SELECT * FROM {table_with_schema(db, 'proofs_pending')}" + f"CREATE TABLE {db.table_with_schema('proofs_pending_old')} AS" + f" SELECT * FROM {db.table_with_schema('proofs_pending')}" ) - await conn.execute(f"DROP TABLE {table_with_schema(db, 'proofs_pending')}") + await conn.execute(f"DROP TABLE {db.table_with_schema('proofs_pending')}") await conn.execute( f""" - CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_pending')} ( + CREATE TABLE IF NOT EXISTS {db.table_with_schema('proofs_pending')} ( amount {db.big_int} NOT NULL, c TEXT NOT NULL, secret TEXT NOT NULL, y TEXT, id TEXT, - created TIMESTAMP, + created TIMESTAMP DEFAULT {db.timestamp_now}, UNIQUE (Y) @@ -523,12 +523,12 @@ async def m014_proofs_add_Y_column(db: Database): """ ) await conn.execute( - f"INSERT INTO {table_with_schema(db, 'proofs_pending')} (amount, c, " + f"INSERT INTO {db.table_with_schema('proofs_pending')} (amount, c, " "secret, y, id, created) SELECT amount, c, secret, y, id, created" - f" FROM {table_with_schema(db, 'proofs_pending_old')}" + f" FROM {db.table_with_schema('proofs_pending_old')}" ) - await conn.execute(f"DROP TABLE {table_with_schema(db, 'proofs_pending_old')}") + await conn.execute(f"DROP TABLE {db.table_with_schema('proofs_pending_old')}") # recreate the balance views await create_balance_views(db, conn) @@ -540,13 +540,13 @@ async def m015_add_index_Y_to_proofs_used_and_pending(db: Database): await conn.execute( "CREATE INDEX IF NOT EXISTS" " proofs_used_Y_idx ON" - f" {table_with_schema(db, 'proofs_used')} (Y)" + f" {db.table_with_schema('proofs_used')} (Y)" ) await conn.execute( "CREATE INDEX IF NOT EXISTS" " proofs_pending_Y_idx ON" - f" {table_with_schema(db, 'proofs_pending')} (Y)" + f" {db.table_with_schema('proofs_pending')} (Y)" ) @@ -554,13 +554,13 @@ async def m016_recompute_Y_with_new_h2c(db: Database): # get all proofs_used and proofs_pending from the database and compute Y for each of them async with db.connect() as conn: rows = await conn.fetchall( - f"SELECT * FROM {table_with_schema(db, 'proofs_used')}" + f"SELECT * FROM {db.table_with_schema('proofs_used')}" ) # Proof() will compute Y from secret upon initialization proofs_used = [Proof(**r) for r in rows] - + async with db.connect() as conn: rows = await conn.fetchall( - f"SELECT * FROM {table_with_schema(db, 'proofs_pending')}" + f"SELECT * FROM {db.table_with_schema('proofs_pending')}" ) proofs_pending = [Proof(**r) for r in rows] @@ -583,10 +583,10 @@ async def m016_recompute_Y_with_new_h2c(db: Database): ) await conn.execute( f""" - UPDATE {table_with_schema(db, 'proofs_used')} + UPDATE {db.table_with_schema('proofs_used')} SET y = tmp_proofs_used.y FROM tmp_proofs_used - WHERE {table_with_schema(db, 'proofs_used')}.secret = tmp_proofs_used.secret + WHERE {db.table_with_schema('proofs_used')}.secret = tmp_proofs_used.secret """ ) @@ -603,10 +603,10 @@ async def m016_recompute_Y_with_new_h2c(db: Database): ) await conn.execute( f""" - UPDATE {table_with_schema(db, 'proofs_pending')} + UPDATE {db.table_with_schema('proofs_pending')} SET y = tmp_proofs_pending.y FROM tmp_proofs_pending - WHERE {table_with_schema(db, 'proofs_pending')}.secret = tmp_proofs_pending.secret + WHERE {db.table_with_schema('proofs_pending')}.secret = tmp_proofs_pending.secret """ ) @@ -636,7 +636,7 @@ async def m017_foreign_keys_proof_tables(db: Database): # add foreign key constraints to proofs_used table await conn.execute( f""" - CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_used_new')} ( + CREATE TABLE IF NOT EXISTS {db.table_with_schema('proofs_used_new')} ( amount {db.big_int} NOT NULL, id TEXT, c TEXT NOT NULL, @@ -646,24 +646,24 @@ async def m017_foreign_keys_proof_tables(db: Database): created TIMESTAMP, melt_quote TEXT, - FOREIGN KEY (melt_quote) REFERENCES {table_with_schema(db, 'melt_quotes')}(quote), + FOREIGN KEY (melt_quote) REFERENCES {db.table_with_schema('melt_quotes')}(quote), UNIQUE (y) ); """ ) await conn.execute( - f"INSERT INTO {table_with_schema(db, 'proofs_used_new')} (amount, id, c, secret, y, witness, created) SELECT amount, id, c, secret, y, witness, created FROM {table_with_schema(db, 'proofs_used')}" + f"INSERT INTO {db.table_with_schema('proofs_used_new')} (amount, id, c, secret, y, witness, created) SELECT amount, id, c, secret, y, witness, created FROM {db.table_with_schema('proofs_used')}" ) - await conn.execute(f"DROP TABLE {table_with_schema(db, 'proofs_used')}") + await conn.execute(f"DROP TABLE {db.table_with_schema('proofs_used')}") await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'proofs_used_new')} RENAME TO {table_with_schema(db, 'proofs_used')}" + f"ALTER TABLE {db.table_with_schema('proofs_used_new')} RENAME TO {db.table_with_schema('proofs_used')}" ) # add foreign key constraints to proofs_pending table await conn.execute( f""" - CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_pending_new')} ( + CREATE TABLE IF NOT EXISTS {db.table_with_schema('proofs_pending_new')} ( amount {db.big_int} NOT NULL, id TEXT, c TEXT NOT NULL, @@ -673,24 +673,24 @@ async def m017_foreign_keys_proof_tables(db: Database): created TIMESTAMP, melt_quote TEXT, - FOREIGN KEY (melt_quote) REFERENCES {table_with_schema(db, 'melt_quotes')}(quote), + FOREIGN KEY (melt_quote) REFERENCES {db.table_with_schema('melt_quotes')}(quote), UNIQUE (y) ); """ ) await conn.execute( - f"INSERT INTO {table_with_schema(db, 'proofs_pending_new')} (amount, id, c, secret, y, created) SELECT amount, id, c, secret, y, created FROM {table_with_schema(db, 'proofs_pending')}" + f"INSERT INTO {db.table_with_schema('proofs_pending_new')} (amount, id, c, secret, y, created) SELECT amount, id, c, secret, y, created FROM {db.table_with_schema('proofs_pending')}" ) - await conn.execute(f"DROP TABLE {table_with_schema(db, 'proofs_pending')}") + await conn.execute(f"DROP TABLE {db.table_with_schema('proofs_pending')}") await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'proofs_pending_new')} RENAME TO {table_with_schema(db, 'proofs_pending')}" + f"ALTER TABLE {db.table_with_schema('proofs_pending_new')} RENAME TO {db.table_with_schema('proofs_pending')}" ) # add foreign key constraints to promises table await conn.execute( f""" - CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'promises_new')} ( + CREATE TABLE IF NOT EXISTS {db.table_with_schema('promises_new')} ( amount {db.big_int} NOT NULL, id TEXT, b_ TEXT NOT NULL, @@ -701,7 +701,7 @@ async def m017_foreign_keys_proof_tables(db: Database): mint_quote TEXT, swap_id TEXT, - FOREIGN KEY (mint_quote) REFERENCES {table_with_schema(db, 'mint_quotes')}(quote), + FOREIGN KEY (mint_quote) REFERENCES {db.table_with_schema('mint_quotes')}(quote), UNIQUE (b_) ); @@ -709,11 +709,11 @@ async def m017_foreign_keys_proof_tables(db: Database): ) await conn.execute( - f"INSERT INTO {table_with_schema(db, 'promises_new')} (amount, id, b_, c_, dleq_e, dleq_s, created) SELECT amount, id, b_b, c_b, e, s, created FROM {table_with_schema(db, 'promises')}" + f"INSERT INTO {db.table_with_schema('promises_new')} (amount, id, b_, c_, dleq_e, dleq_s, created) SELECT amount, id, b_b, c_b, e, s, created FROM {db.table_with_schema('promises')}" ) - await conn.execute(f"DROP TABLE {table_with_schema(db, 'promises')}") + await conn.execute(f"DROP TABLE {db.table_with_schema('promises')}") await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'promises_new')} RENAME TO {table_with_schema(db, 'promises')}" + f"ALTER TABLE {db.table_with_schema('promises_new')} RENAME TO {db.table_with_schema('promises')}" ) # recreate the balance views @@ -727,7 +727,7 @@ async def m018_duplicate_deprecated_keyset_ids(db: Database): async with db.connect() as conn: rows = await conn.fetchall( # type: ignore f""" - SELECT * from {table_with_schema(db, 'keysets')} + SELECT * from {db.table_with_schema('keysets')} """, ) keysets = [MintKeyset(**row) for row in rows] @@ -745,43 +745,43 @@ async def m018_duplicate_deprecated_keyset_ids(db: Database): for keyset in duplicated_keysets: await conn.execute( f""" - INSERT INTO {table_with_schema(db, 'keysets')} + INSERT INTO {db.table_with_schema('keysets')} (id, derivation_path, valid_from, valid_to, first_seen, active, version, seed, unit, encrypted_seed, seed_encryption_method) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (:id, :derivation_path, :valid_from, :valid_to, :first_seen, :active, :version, :seed, :unit, :encrypted_seed, :seed_encryption_method) """, - ( - keyset.id, - keyset.derivation_path, - keyset.valid_from, - keyset.valid_to, - keyset.first_seen, - keyset.active, - keyset.version, - keyset.seed, - keyset.unit.name, - keyset.encrypted_seed, - keyset.seed_encryption_method, - ), + { + "id": keyset.id, + "derivation_path": keyset.derivation_path, + "valid_from": keyset.valid_from, + "valid_to": keyset.valid_to, + "first_seen": keyset.first_seen, + "active": keyset.active, + "version": keyset.version, + "seed": keyset.seed, + "unit": keyset.unit.name, + "encrypted_seed": keyset.encrypted_seed, + "seed_encryption_method": keyset.seed_encryption_method, + }, ) async def m019_add_fee_to_keysets(db: Database): async with db.connect() as conn: await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'keysets')} ADD COLUMN input_fee_ppk INTEGER" + f"ALTER TABLE {db.table_with_schema('keysets')} ADD COLUMN input_fee_ppk INTEGER" ) await conn.execute( - f"UPDATE {table_with_schema(db, 'keysets')} SET input_fee_ppk = 0" + f"UPDATE {db.table_with_schema('keysets')} SET input_fee_ppk = 0" ) async def m020_add_state_to_mint_and_melt_quotes(db: Database): async with db.connect() as conn: await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'mint_quotes')} ADD COLUMN state TEXT" + f"ALTER TABLE {db.table_with_schema('mint_quotes')} ADD COLUMN state TEXT" ) await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'melt_quotes')} ADD COLUMN state TEXT" + f"ALTER TABLE {db.table_with_schema('melt_quotes')} ADD COLUMN state TEXT" ) # get all melt and mint quotes and figure out the state to set using the `paid` column @@ -789,7 +789,7 @@ async def m020_add_state_to_mint_and_melt_quotes(db: Database): # mint quotes: async with db.connect() as conn: rows = await conn.fetchall( - f"SELECT * FROM {table_with_schema(db, 'mint_quotes')}" + f"SELECT * FROM {db.table_with_schema('mint_quotes')}" ) for row in rows: if row["issued"]: @@ -799,13 +799,13 @@ async def m020_add_state_to_mint_and_melt_quotes(db: Database): else: state = "unpaid" await conn.execute( - f"UPDATE {table_with_schema(db, 'mint_quotes')} SET state = '{state}' WHERE quote = '{row['quote']}'" + f"UPDATE {db.table_with_schema('mint_quotes')} SET state = '{state}' WHERE quote = '{row['quote']}'" ) # melt quotes: async with db.connect() as conn: rows = await conn.fetchall( - f"SELECT * FROM {table_with_schema(db, 'melt_quotes')}" + f"SELECT * FROM {db.table_with_schema('melt_quotes')}" ) for row in rows: if row["paid"]: @@ -813,15 +813,15 @@ async def m020_add_state_to_mint_and_melt_quotes(db: Database): else: state = "unpaid" await conn.execute( - f"UPDATE {table_with_schema(db, 'melt_quotes')} SET state = '{state}' WHERE quote = '{row['quote']}'" + f"UPDATE {db.table_with_schema('melt_quotes')} SET state = '{state}' WHERE quote = '{row['quote']}'" ) async def m021_add_change_and_expiry_to_melt_quotes(db: Database): async with db.connect() as conn: await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'melt_quotes')} ADD COLUMN change TEXT" + f"ALTER TABLE {db.table_with_schema('melt_quotes')} ADD COLUMN change TEXT" ) await conn.execute( - f"ALTER TABLE {table_with_schema(db, 'melt_quotes')} ADD COLUMN expiry TIMESTAMP" + f"ALTER TABLE {db.table_with_schema('melt_quotes')} ADD COLUMN expiry TIMESTAMP" ) diff --git a/cashu/mint/startup.py b/cashu/mint/startup.py index 942f71b..f846061 100644 --- a/cashu/mint/startup.py +++ b/cashu/mint/startup.py @@ -98,3 +98,9 @@ async def start_mint_init(): await ledger.startup_ledger() logger.info("Mint started.") # asyncio.create_task(rotate_keys()) + + +async def shutdown_mint(): + await ledger.shutdown_ledger() + logger.info("Mint shutdown.") + logger.remove() diff --git a/cashu/mint/tasks.py b/cashu/mint/tasks.py index 4149ce6..2ce0541 100644 --- a/cashu/mint/tasks.py +++ b/cashu/mint/tasks.py @@ -1,5 +1,5 @@ import asyncio -from typing import Mapping +from typing import List, Mapping from loguru import logger @@ -17,13 +17,15 @@ class LedgerTasks(SupportsDb, SupportsBackends, SupportsEvents): crud: LedgerCrud events: LedgerEventManager - async def dispatch_listeners(self) -> None: + async def dispatch_listeners(self) -> List[asyncio.Task]: + tasks = [] 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)) + tasks.append(asyncio.create_task(self.invoice_listener(backend))) + return tasks async def invoice_listener(self, backend: LightningBackend) -> None: if backend.supports_incoming_payment_stream: @@ -38,15 +40,28 @@ class LedgerTasks(SupportsDb, SupportsBackends, SupportsEvents): async def invoice_callback_dispatcher(self, checking_id: str) -> None: logger.debug(f"Invoice callback dispatcher: {checking_id}") - # TODO: 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 - quote.state = MintQuoteState.paid - await self.crud.update_mint_quote(quote=quote, db=self.db) - logger.trace(f"Quote {quote} set as paid and ") + async with self.db.get_connection( + lock_table="mint_quotes", + lock_select_statement=f"checking_id='{checking_id}'", + lock_timeout=5, + ) as conn: + quote = await self.crud.get_mint_quote( + checking_id=checking_id, db=self.db, conn=conn + ) + if not quote: + logger.error(f"Quote not found for {checking_id}") + return + + logger.trace( + f"Invoice callback dispatcher: quote {quote} trying to set as {MintQuoteState.paid}" + ) + # set the quote as paid + if quote.state == MintQuoteState.unpaid: + quote.paid = True + quote.state = MintQuoteState.paid + await self.crud.update_mint_quote(quote=quote, db=self.db, conn=conn) + logger.trace( + f"Quote {quote.quote} with {MintQuoteState.unpaid} set as {quote.state.value}" + ) + await self.events.submit(quote) diff --git a/cashu/mint/verification.py b/cashu/mint/verification.py index 006a5da..f0ce828 100644 --- a/cashu/mint/verification.py +++ b/cashu/mint/verification.py @@ -12,7 +12,7 @@ from ..core.base import ( ) from ..core.crypto import b_dhke from ..core.crypto.secp import PublicKey -from ..core.db import Database +from ..core.db import Connection, Database from ..core.errors import ( NoSecretInProofsError, NotAllowedError, @@ -40,7 +40,11 @@ class LedgerVerification( lightning: Dict[Unit, LightningBackend] async def verify_inputs_and_outputs( - self, *, proofs: List[Proof], outputs: Optional[List[BlindedMessage]] = None + self, + *, + proofs: List[Proof], + outputs: Optional[List[BlindedMessage]] = None, + conn: Optional[Connection] = None, ): """Checks all proofs and outputs for validity. @@ -48,6 +52,7 @@ class LedgerVerification( proofs (List[Proof]): List of proofs to check. outputs (Optional[List[BlindedMessage]], optional): List of outputs to check. Must be provided for a swap but not for a melt. Defaults to None. + conn (Optional[Connection], optional): Database connection. Defaults to None. Raises: Exception: Scripts did not validate. @@ -56,8 +61,10 @@ class LedgerVerification( Exception: BDHKE verification failed. """ # Verify inputs + if not proofs: + raise TransactionError("no proofs provided.") # Verify proofs are spendable - if not len(await self._get_proofs_spent([p.Y for p in proofs])) == 0: + if not len(await self._get_proofs_spent([p.Y for p in proofs], conn)) == 0: raise TokenAlreadySpentError() # Verify amounts of inputs if not all([self._verify_amount(p.amount) for p in proofs]): @@ -83,7 +90,7 @@ class LedgerVerification( self._verify_equation_balanced(proofs, outputs) # Verify outputs - await self._verify_outputs(outputs) + await self._verify_outputs(outputs, conn=conn) # Verify inputs and outputs together if not self._verify_input_output_amounts(proofs, outputs): @@ -104,7 +111,10 @@ class LedgerVerification( raise TransactionError("validation of output spending conditions failed.") async def _verify_outputs( - self, outputs: List[BlindedMessage], skip_amount_check=False + self, + outputs: List[BlindedMessage], + skip_amount_check=False, + conn: Optional[Connection] = None, ): """Verify that the outputs are valid.""" logger.trace(f"Verifying {len(outputs)} outputs.") @@ -127,13 +137,15 @@ class LedgerVerification( if not self._verify_no_duplicate_outputs(outputs): raise TransactionError("duplicate outputs.") # verify that outputs have not been signed previously - signed_before = await self._check_outputs_issued_before(outputs) + signed_before = await self._check_outputs_issued_before(outputs, conn) if any(signed_before): raise TransactionError("outputs have already been signed before.") logger.trace(f"Verified {len(outputs)} outputs.") async def _check_outputs_issued_before( - self, outputs: List[BlindedMessage] + self, + outputs: List[BlindedMessage], + conn: Optional[Connection] = None, ) -> List[bool]: """Checks whether the provided outputs have previously been signed by the mint (which would lead to a duplication error later when trying to store these outputs again). @@ -145,7 +157,7 @@ class LedgerVerification( result (List[bool]): Whether outputs are already present in the database. """ result = [] - async with self.db.connect() as conn: + async with self.db.get_connection(conn) as conn: for output in outputs: promise = await self.crud.get_promise( b_=output.B_, db=self.db, conn=conn @@ -153,21 +165,15 @@ class LedgerVerification( result.append(False if promise is None else True) return result - 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]: + async def _get_proofs_spent( + self, Ys: List[str], conn: Optional[Connection] = None + ) -> 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: + async with self.db.get_connection(conn=conn) as conn: for Y in Ys: spent_proof = await self.crud.get_proof_used(db=self.db, Y=Y, conn=conn) if spent_proof: diff --git a/cashu/wallet/crud.py b/cashu/wallet/crud.py index aab64f3..febd05d 100644 --- a/cashu/wallet/crud.py +++ b/cashu/wallet/crud.py @@ -1,6 +1,6 @@ import json import time -from typing import Any, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple from ..core.base import Invoice, Proof, WalletKeyset from ..core.db import Connection, Database @@ -15,19 +15,19 @@ async def store_proof( """ INSERT INTO proofs (id, amount, C, secret, time_created, derivation_path, dleq, mint_id, melt_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (:id, :amount, :C, :secret, :time_created, :derivation_path, :dleq, :mint_id, :melt_id) """, - ( - proof.id, - proof.amount, - str(proof.C), - str(proof.secret), - int(time.time()), - proof.derivation_path, - json.dumps(proof.dleq.dict()) if proof.dleq else "", - proof.mint_id, - proof.melt_id, - ), + { + "id": proof.id, + "amount": proof.amount, + "C": str(proof.C), + "secret": str(proof.secret), + "time_created": int(time.time()), + "derivation_path": proof.derivation_path, + "dleq": json.dumps(proof.dleq.dict()) if proof.dleq else "", + "mint_id": proof.mint_id, + "melt_id": proof.melt_id, + }, ) @@ -41,30 +41,28 @@ async def get_proofs( conn: Optional[Connection] = None, ): clauses = [] - values: List[Any] = [] + values: Dict[str, Any] = {} if id: - clauses.append("id = ?") - values.append(id) + clauses.append("id = :id") + values["id"] = id if melt_id: - clauses.append("melt_id = ?") - values.append(melt_id) + clauses.append("melt_id = :melt_id") + values["melt_id"] = melt_id if mint_id: - clauses.append("mint_id = ?") - values.append(mint_id) + clauses.append("mint_id = :mint_id") + values["mint_id"] = mint_id where = "" if clauses: where = f"WHERE {' AND '.join(clauses)}" - rows = ( - await (conn or db).fetchall( - f""" - SELECT * from {table} - {where} - """, - tuple(values), - ), + rows = await (conn or db).fetchall( + f""" + SELECT * from {table} + {where} + """, + values, ) - return [Proof.from_dict(dict(r)) for r in rows[0]] if rows else [] + return [Proof.from_dict(dict(r)) for r in rows] if rows else [] async def get_reserved_proofs( @@ -88,27 +86,27 @@ async def invalidate_proof( await (conn or db).execute( """ DELETE FROM proofs - WHERE secret = ? + WHERE secret = :secret """, - (str(proof["secret"]),), + {"secret": str(proof["secret"])}, ) await (conn or db).execute( """ INSERT INTO proofs_used (amount, C, secret, time_used, id, derivation_path, mint_id, melt_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + VALUES (:amount, :C, :secret, :time_used, :id, :derivation_path, :mint_id, :melt_id) """, - ( - proof.amount, - str(proof.C), - str(proof.secret), - int(time.time()), - proof.id, - proof.derivation_path, - proof.mint_id, - proof.melt_id, - ), + { + "amount": proof.amount, + "C": str(proof.C), + "secret": str(proof.secret), + "time_used": int(time.time()), + "id": proof.id, + "derivation_path": proof.derivation_path, + "mint_id": proof.mint_id, + "melt_id": proof.melt_id, + }, ) @@ -123,29 +121,29 @@ async def update_proof( conn: Optional[Connection] = None, ) -> None: clauses = [] - values: List[Any] = [] - clauses.append("reserved = ?") - values.append(reserved) - - if send_id is not None: - clauses.append("send_id = ?") - values.append(send_id) + values: Dict[str, Any] = {} if reserved is not None: - clauses.append("time_reserved = ?") - values.append(int(time.time())) + clauses.append("reserved = :reserved") + values["reserved"] = reserved + clauses.append("time_reserved = :time_reserved") + values["time_reserved"] = int(time.time()) + + if send_id is not None: + clauses.append("send_id = :send_id") + values["send_id"] = send_id if mint_id is not None: - clauses.append("mint_id = ?") - values.append(mint_id) + clauses.append("mint_id = :mint_id") + values["mint_id"] = mint_id if melt_id is not None: - clauses.append("melt_id = ?") - values.append(melt_id) + clauses.append("melt_id = :melt_id") + values["melt_id"] = melt_id await (conn or db).execute( # type: ignore - f"UPDATE proofs SET {', '.join(clauses)} WHERE secret = ?", - (*values, str(proof.secret)), + f"UPDATE proofs SET {', '.join(clauses)} WHERE secret = :secret", + {**values, "secret": str(proof.secret)}, ) @@ -157,9 +155,9 @@ async def secret_used( rows = await (conn or db).fetchone( """ SELECT * from proofs - WHERE secret = ? + WHERE secret = :secret """, - (secret,), + {"secret": secret}, ) return rows is not None @@ -174,19 +172,19 @@ async def store_keyset( """ INSERT INTO keysets (id, mint_url, valid_from, valid_to, first_seen, active, public_keys, unit, input_fee_ppk) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (:id, :mint_url, :valid_from, :valid_to, :first_seen, :active, :public_keys, :unit, :input_fee_ppk) """, - ( - keyset.id, - mint_url or keyset.mint_url, - keyset.valid_from or int(time.time()), - keyset.valid_to or int(time.time()), - keyset.first_seen or int(time.time()), - keyset.active, - keyset.serialize(), - keyset.unit.name, - keyset.input_fee_ppk, - ), + { + "id": keyset.id, + "mint_url": mint_url or keyset.mint_url, + "valid_from": keyset.valid_from or int(time.time()), + "valid_to": keyset.valid_to or int(time.time()), + "first_seen": keyset.first_seen or int(time.time()), + "active": keyset.active, + "public_keys": keyset.serialize(), + "unit": keyset.unit.name, + "input_fee_ppk": keyset.input_fee_ppk, + }, ) @@ -198,32 +196,28 @@ async def get_keysets( conn: Optional[Connection] = None, ) -> List[WalletKeyset]: clauses = [] - values: List[Any] = [] + values: Dict[str, Any] = {} if id: - clauses.append("id = ?") - values.append(id) + clauses.append("id = :id") + values["id"] = id if mint_url: - clauses.append("mint_url = ?") - values.append(mint_url) + clauses.append("mint_url = :mint_url") + values["mint_url"] = mint_url if unit: - clauses.append("unit = ?") - values.append(unit) + clauses.append("unit = :unit") + values["unit"] = unit where = "" if clauses: where = f"WHERE {' AND '.join(clauses)}" - row = await (conn or db).fetchall( # type: ignore + rows = await (conn or db).fetchall( # type: ignore f""" SELECT * from keysets {where} """, - tuple(values), + values, ) - ret = [] - for r in row: - keyset = WalletKeyset.from_row(r) - ret.append(keyset) - return ret + return [WalletKeyset.from_row(r) for r in rows] async def update_keyset( @@ -234,13 +228,13 @@ async def update_keyset( await (conn or db).execute( """ UPDATE keysets - SET active = ? - WHERE id = ? + SET active = :active + WHERE id = :id """, - ( - keyset.active, - keyset.id, - ), + { + "active": keyset.active, + "id": keyset.id, + }, ) @@ -253,19 +247,19 @@ async def store_lightning_invoice( """ INSERT INTO invoices (amount, bolt11, id, payment_hash, preimage, paid, time_created, time_paid, out) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (:amount, :bolt11, :id, :payment_hash, :preimage, :paid, :time_created, :time_paid, :out) """, - ( - invoice.amount, - invoice.bolt11, - invoice.id, - invoice.payment_hash, - invoice.preimage, - invoice.paid, - invoice.time_created, - invoice.time_paid, - invoice.out, - ), + { + "amount": invoice.amount, + "bolt11": invoice.bolt11, + "id": invoice.id, + "payment_hash": invoice.payment_hash, + "preimage": invoice.preimage, + "paid": invoice.paid, + "time_created": invoice.time_created, + "time_paid": invoice.time_paid, + "out": invoice.out, + }, ) @@ -278,16 +272,16 @@ async def get_lightning_invoice( conn: Optional[Connection] = None, ) -> Optional[Invoice]: clauses = [] - values: List[Any] = [] + values: Dict[str, Any] = {} if id: - clauses.append("id = ?") - values.append(id) + clauses.append("id = :id") + values["id"] = id if payment_hash: - clauses.append("payment_hash = ?") - values.append(payment_hash) + clauses.append("payment_hash = :payment_hash") + values["payment_hash"] = payment_hash if out is not None: - clauses.append("out = ?") - values.append(out) + clauses.append("out = :out") + values["out"] = out where = "" if clauses: @@ -298,7 +292,7 @@ async def get_lightning_invoice( """ row = await (conn or db).fetchone( query, - tuple(values), + values, ) return Invoice(**row) if row else None @@ -309,18 +303,18 @@ async def get_lightning_invoices( pending: Optional[bool] = None, conn: Optional[Connection] = None, ) -> List[Invoice]: - clauses: List[Any] = [] - values: List[Any] = [] + clauses = [] + values: Dict[str, Any] = {} if paid is not None and not pending: - clauses.append("paid = ?") - values.append(paid) + clauses.append("paid = :paid") + values["paid"] = paid if pending: - clauses.append("paid = ?") - values.append(False) - clauses.append("out = ?") - values.append(False) + clauses.append("paid = :paid") + values["paid"] = False + clauses.append("out = :out") + values["out"] = False where = "" if clauses: @@ -331,7 +325,7 @@ async def get_lightning_invoices( SELECT * from invoices {where} """, - tuple(values), + values, ) return [Invoice(**r) for r in rows] @@ -345,23 +339,20 @@ async def update_lightning_invoice( conn: Optional[Connection] = None, ) -> None: clauses = [] - values: List[Any] = [] - clauses.append("paid = ?") - values.append(paid) + values: Dict[str, Any] = {} + clauses.append("paid = :paid") + values["paid"] = paid if time_paid: - clauses.append("time_paid = ?") - values.append(time_paid) + clauses.append("time_paid = :time_paid") + values["time_paid"] = time_paid if preimage: - clauses.append("preimage = ?") - values.append(preimage) + clauses.append("preimage = :preimage") + values["preimage"] = preimage await (conn or db).execute( - f"UPDATE invoices SET {', '.join(clauses)} WHERE id = ?", - ( - *values, - id, - ), + f"UPDATE invoices SET {', '.join(clauses)} WHERE id = :id", + {**values, "id": id}, ) @@ -373,16 +364,16 @@ async def bump_secret_derivation( conn: Optional[Connection] = None, ) -> int: rows = await (conn or db).fetchone( - "SELECT counter from keysets WHERE id = ?", (keyset_id,) + "SELECT counter from keysets WHERE id = :keyset_id", {"keyset_id": keyset_id} ) # if no counter for this keyset, create one if not rows: await (conn or db).execute( - "UPDATE keysets SET counter = ? WHERE id = ?", - ( - 0, - keyset_id, - ), + "UPDATE keysets SET counter = :counter WHERE id = :keyset_id", + { + "counter": 0, + "keyset_id": keyset_id, + }, ) counter = 0 else: @@ -390,8 +381,8 @@ async def bump_secret_derivation( if not skip: await (conn or db).execute( - f"UPDATE keysets SET counter = counter + {by} WHERE id = ?", - (keyset_id,), + "UPDATE keysets SET counter = counter + :by WHERE id = :keyset_id", + {"by": by, "keyset_id": keyset_id}, ) return counter @@ -403,11 +394,11 @@ async def set_secret_derivation( conn: Optional[Connection] = None, ) -> None: await (conn or db).execute( - "UPDATE keysets SET counter = ? WHERE id = ?", - ( - counter, - keyset_id, - ), + "UPDATE keysets SET counter = :counter WHERE id = :keyset_id", + { + "counter": counter, + "keyset_id": keyset_id, + }, ) @@ -417,8 +408,8 @@ async def set_nostr_last_check_timestamp( conn: Optional[Connection] = None, ) -> None: await (conn or db).execute( - "UPDATE nostr SET last = ? WHERE type = ?", - (timestamp, "dm"), + "UPDATE nostr SET last = :last WHERE type = :type", + {"last": timestamp, "type": "dm"}, ) @@ -428,9 +419,9 @@ async def get_nostr_last_check_timestamp( ) -> Optional[int]: row = await (conn or db).fetchone( """ - SELECT last from nostr WHERE type = ? + SELECT last from nostr WHERE type = :type """, - ("dm",), + {"type": "dm"}, ) return row[0] if row else None @@ -442,7 +433,7 @@ async def get_seed_and_mnemonic( row = await (conn or db).fetchone( """ SELECT seed, mnemonic from seed - """, + """ ) return ( ( @@ -464,10 +455,10 @@ async def store_seed_and_mnemonic( """ INSERT INTO seed (seed, mnemonic) - VALUES (?, ?) + VALUES (:seed, :mnemonic) """, - ( - seed, - mnemonic, - ), + { + "seed": seed, + "mnemonic": mnemonic, + }, ) diff --git a/cashu/wallet/migrations.py b/cashu/wallet/migrations.py index 05decf4..4193731 100644 --- a/cashu/wallet/migrations.py +++ b/cashu/wallet/migrations.py @@ -166,12 +166,12 @@ async def m007_nostr(db: Database): """ INSERT INTO nostr (type, last) - VALUES (?, ?) + VALUES (:type, :last) """, - ( - "dm", - None, - ), + { + "type": "db", + "last": None, + }, ) @@ -248,10 +248,10 @@ async def m012_add_fee_to_keysets(db: Database): # # async def m020_add_state_to_mint_and_melt_quotes(db: Database): # # async with db.connect() as conn: # # await conn.execute( -# # f"ALTER TABLE {table_with_schema(db, 'mint_quotes')} ADD COLUMN state TEXT" +# # f"ALTER TABLE {db.table_with_schema('mint_quotes')} ADD COLUMN state TEXT" # # ) # # await conn.execute( -# # f"ALTER TABLE {table_with_schema(db, 'melt_quotes')} ADD COLUMN state TEXT" +# # f"ALTER TABLE {db.table_with_schema('melt_quotes')} ADD COLUMN state TEXT" # # ) # # # get all melt and mint quotes and figure out the state to set using the `paid` column @@ -259,7 +259,7 @@ async def m012_add_fee_to_keysets(db: Database): # # # mint quotes: # # async with db.connect() as conn: # # rows = await conn.fetchall( -# # f"SELECT * FROM {table_with_schema(db, 'mint_quotes')}" +# # f"SELECT * FROM {db.table_with_schema('mint_quotes')}" # # ) # # for row in rows: # # if row["issued"]: @@ -269,13 +269,13 @@ async def m012_add_fee_to_keysets(db: Database): # # else: # # state = "unpaid" # # await conn.execute( -# # f"UPDATE {table_with_schema(db, 'mint_quotes')} SET state = '{state}' WHERE quote = '{row['quote']}'" +# # f"UPDATE {db.table_with_schema('mint_quotes')} SET state = '{state}' WHERE quote = '{row['quote']}'" # # ) # # # melt quotes: # # async with db.connect() as conn: # # rows = await conn.fetchall( -# # f"SELECT * FROM {table_with_schema(db, 'melt_quotes')}" +# # f"SELECT * FROM {db.table_with_schema('melt_quotes')}" # # ) # # for row in rows: # # if row["paid"]: @@ -283,7 +283,7 @@ async def m012_add_fee_to_keysets(db: Database): # # else: # # state = "unpaid" # # await conn.execute( -# # f"UPDATE {table_with_schema(db, 'melt_quotes')} SET state = '{state}' WHERE quote = '{row['quote']}'" +# # f"UPDATE {db.table_with_schema('melt_quotes')} SET state = '{state}' WHERE quote = '{row['quote']}'" # # ) # # add the equivalent of the above migration for the wallet here. do not use table_with_schema. use the tables and columns # # as they are defined in the wallet db diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index f5d2e69..0c4bcd1 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -310,9 +310,10 @@ class Wallet( async def _check_used_secrets(self, secrets): """Checks if any of the secrets have already been used""" logger.trace("Checking secrets.") - for s in secrets: - if await secret_used(s, db=self.db): - raise Exception(f"secret already used: {s}") + async with self.db.get_connection() as conn: + for s in secrets: + if await secret_used(s, db=self.db, conn=conn): + raise Exception(f"secret already used: {s}") logger.trace("Secret check complete.") async def request_mint_with_callback( diff --git a/poetry.lock b/poetry.lock index 97b77f0..42edf8d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,23 @@ # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +[[package]] +name = "aiosqlite" +version = "0.20.0" +description = "asyncio bridge to the standard sqlite3 module" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"}, + {file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"}, +] + +[package.dependencies] +typing_extensions = ">=4.0" + +[package.extras] +dev = ["attribution (==1.7.0)", "black (==24.2.0)", "coverage[toml] (==7.4.1)", "flake8 (==7.0.0)", "flake8-bugbear (==24.2.6)", "flit (==3.9.0)", "mypy (==1.8.0)", "ufmt (==2.3.0)", "usort (==1.0.8.post1)"] +docs = ["sphinx (==7.2.6)", "sphinx-mdinclude (==0.5.3)"] + [[package]] name = "anyio" version = "3.7.1" @@ -33,23 +51,72 @@ files = [ ] [[package]] -name = "attrs" -version = "23.2.0" -description = "Classes Without Boilerplate" +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.7" files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, ] +[[package]] +name = "asyncpg" +version = "0.29.0" +description = "An asyncio PostgreSQL driver" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "asyncpg-0.29.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72fd0ef9f00aeed37179c62282a3d14262dbbafb74ec0ba16e1b1864d8a12169"}, + {file = "asyncpg-0.29.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52e8f8f9ff6e21f9b39ca9f8e3e33a5fcdceaf5667a8c5c32bee158e313be385"}, + {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e6823a7012be8b68301342ba33b4740e5a166f6bbda0aee32bc01638491a22"}, + {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:746e80d83ad5d5464cfbf94315eb6744222ab00aa4e522b704322fb182b83610"}, + {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ff8e8109cd6a46ff852a5e6bab8b0a047d7ea42fcb7ca5ae6eaae97d8eacf397"}, + {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97eb024685b1d7e72b1972863de527c11ff87960837919dac6e34754768098eb"}, + {file = "asyncpg-0.29.0-cp310-cp310-win32.whl", hash = "sha256:5bbb7f2cafd8d1fa3e65431833de2642f4b2124be61a449fa064e1a08d27e449"}, + {file = "asyncpg-0.29.0-cp310-cp310-win_amd64.whl", hash = "sha256:76c3ac6530904838a4b650b2880f8e7af938ee049e769ec2fba7cd66469d7772"}, + {file = "asyncpg-0.29.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4900ee08e85af01adb207519bb4e14b1cae8fd21e0ccf80fac6aa60b6da37b4"}, + {file = "asyncpg-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a65c1dcd820d5aea7c7d82a3fdcb70e096f8f70d1a8bf93eb458e49bfad036ac"}, + {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b52e46f165585fd6af4863f268566668407c76b2c72d366bb8b522fa66f1870"}, + {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc600ee8ef3dd38b8d67421359779f8ccec30b463e7aec7ed481c8346decf99f"}, + {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:039a261af4f38f949095e1e780bae84a25ffe3e370175193174eb08d3cecab23"}, + {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6feaf2d8f9138d190e5ec4390c1715c3e87b37715cd69b2c3dfca616134efd2b"}, + {file = "asyncpg-0.29.0-cp311-cp311-win32.whl", hash = "sha256:1e186427c88225ef730555f5fdda6c1812daa884064bfe6bc462fd3a71c4b675"}, + {file = "asyncpg-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfe73ffae35f518cfd6e4e5f5abb2618ceb5ef02a2365ce64f132601000587d3"}, + {file = "asyncpg-0.29.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178"}, + {file = "asyncpg-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb"}, + {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364"}, + {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106"}, + {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59"}, + {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175"}, + {file = "asyncpg-0.29.0-cp312-cp312-win32.whl", hash = "sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02"}, + {file = "asyncpg-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe"}, + {file = "asyncpg-0.29.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0009a300cae37b8c525e5b449233d59cd9868fd35431abc470a3e364d2b85cb9"}, + {file = "asyncpg-0.29.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cad1324dbb33f3ca0cd2074d5114354ed3be2b94d48ddfd88af75ebda7c43cc"}, + {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:012d01df61e009015944ac7543d6ee30c2dc1eb2f6b10b62a3f598beb6531548"}, + {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000c996c53c04770798053e1730d34e30cb645ad95a63265aec82da9093d88e7"}, + {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e0bfe9c4d3429706cf70d3249089de14d6a01192d617e9093a8e941fea8ee775"}, + {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:642a36eb41b6313ffa328e8a5c5c2b5bea6ee138546c9c3cf1bffaad8ee36dd9"}, + {file = "asyncpg-0.29.0-cp38-cp38-win32.whl", hash = "sha256:a921372bbd0aa3a5822dd0409da61b4cd50df89ae85150149f8c119f23e8c408"}, + {file = "asyncpg-0.29.0-cp38-cp38-win_amd64.whl", hash = "sha256:103aad2b92d1506700cbf51cd8bb5441e7e72e87a7b3a2ca4e32c840f051a6a3"}, + {file = "asyncpg-0.29.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5340dd515d7e52f4c11ada32171d87c05570479dc01dc66d03ee3e150fb695da"}, + {file = "asyncpg-0.29.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e17b52c6cf83e170d3d865571ba574577ab8e533e7361a2b8ce6157d02c665d3"}, + {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f100d23f273555f4b19b74a96840aa27b85e99ba4b1f18d4ebff0734e78dc090"}, + {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48e7c58b516057126b363cec8ca02b804644fd012ef8e6c7e23386b7d5e6ce83"}, + {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9ea3f24eb4c49a615573724d88a48bd1b7821c890c2effe04f05382ed9e8810"}, + {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8d36c7f14a22ec9e928f15f92a48207546ffe68bc412f3be718eedccdf10dc5c"}, + {file = "asyncpg-0.29.0-cp39-cp39-win32.whl", hash = "sha256:797ab8123ebaed304a1fad4d7576d5376c3a006a4100380fb9d517f0b59c1ab2"}, + {file = "asyncpg-0.29.0-cp39-cp39-win_amd64.whl", hash = "sha256:cce08a178858b426ae1aa8409b5cc171def45d4293626e7aa6510696d46decd8"}, + {file = "asyncpg-0.29.0.tar.gz", hash = "sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.12.0\""} + [package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"] [[package]] name = "base58" @@ -175,13 +242,13 @@ test = ["coverage (>=7)", "hypothesis", "pytest"] [[package]] name = "certifi" -version = "2024.6.2" +version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, - {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] [[package]] @@ -582,6 +649,77 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1 testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] typing = ["typing-extensions (>=4.8)"] +[[package]] +name = "greenlet" +version = "3.0.3" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, + {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, + {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, + {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, + {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, + {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] + [[package]] name = "h11" version = "0.14.0" @@ -641,13 +779,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "identify" -version = "2.5.36" +version = "2.6.0" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, - {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, + {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, + {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, ] [package.extras] @@ -858,20 +996,6 @@ files = [ {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] -[[package]] -name = "outcome" -version = "1.3.0.post0" -description = "Capture the outcome of Python function calls." -optional = false -python-versions = ">=3.7" -files = [ - {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, - {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, -] - -[package.dependencies] -attrs = ">=19.2.0" - [[package]] name = "packaging" version = "24.1" @@ -932,84 +1056,6 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" -[[package]] -name = "psycopg2-binary" -version = "2.9.9" -description = "psycopg2 - Python-PostgreSQL Database Adapter" -optional = true -python-versions = ">=3.7" -files = [ - {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"}, -] - [[package]] name = "pycparser" version = "2.22" @@ -1318,21 +1364,6 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] -[[package]] -name = "represent" -version = "2.1" -description = "Create __repr__ automatically or declaratively." -optional = false -python-versions = ">=3.8" -files = [ - {file = "Represent-2.1-py3-none-any.whl", hash = "sha256:94fd22d7fec378240c598b20b233f80545ec7eb1131076e2d3d759cee9be2588"}, - {file = "Represent-2.1.tar.gz", hash = "sha256:0b2d015c14e7ba6b3b5e6a7ba131a952013fe944339ac538764ce728a75dbcac"}, -] - -[package.extras] -docstest = ["furo", "parver", "sphinx"] -test = ["ipython", "pytest", "rich"] - [[package]] name = "respx" version = "0.20.2" @@ -1476,79 +1507,82 @@ files = [ [[package]] name = "sqlalchemy" -version = "1.3.24" +version = "1.4.52" description = "Database Abstraction Library" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "SQLAlchemy-1.3.24-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:87a2725ad7d41cd7376373c15fd8bf674e9c33ca56d0b8036add2d634dba372e"}, - {file = "SQLAlchemy-1.3.24-cp27-cp27m-win32.whl", hash = "sha256:f597a243b8550a3a0b15122b14e49d8a7e622ba1c9d29776af741f1845478d79"}, - {file = "SQLAlchemy-1.3.24-cp27-cp27m-win_amd64.whl", hash = "sha256:fc4cddb0b474b12ed7bdce6be1b9edc65352e8ce66bc10ff8cbbfb3d4047dbf4"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:f1149d6e5c49d069163e58a3196865e4321bad1803d7886e07d8710de392c548"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:14f0eb5db872c231b20c18b1e5806352723a3a89fb4254af3b3e14f22eaaec75"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:e98d09f487267f1e8d1179bf3b9d7709b30a916491997137dd24d6ae44d18d79"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:fc1f2a5a5963e2e73bac4926bdaf7790c4d7d77e8fc0590817880e22dd9d0b8b"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-win32.whl", hash = "sha256:f3c5c52f7cb8b84bfaaf22d82cb9e6e9a8297f7c2ed14d806a0f5e4d22e83fb7"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-win_amd64.whl", hash = "sha256:0352db1befcbed2f9282e72843f1963860bf0e0472a4fa5cf8ee084318e0e6ab"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:2ed6343b625b16bcb63c5b10523fd15ed8934e1ed0f772c534985e9f5e73d894"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:34fcec18f6e4b24b4a5f6185205a04f1eab1e56f8f1d028a2a03694ebcc2ddd4"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e47e257ba5934550d7235665eee6c911dc7178419b614ba9e1fbb1ce6325b14f"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:816de75418ea0953b5eb7b8a74933ee5a46719491cd2b16f718afc4b291a9658"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-win32.whl", hash = "sha256:26155ea7a243cbf23287f390dba13d7927ffa1586d3208e0e8d615d0c506f996"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-win_amd64.whl", hash = "sha256:f03bd97650d2e42710fbe4cf8a59fae657f191df851fc9fc683ecef10746a375"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a006d05d9aa052657ee3e4dc92544faae5fcbaafc6128217310945610d862d39"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1e2f89d2e5e3c7a88e25a3b0e43626dba8db2aa700253023b82e630d12b37109"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0d5d862b1cfbec5028ce1ecac06a3b42bc7703eb80e4b53fceb2738724311443"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:0172423a27fbcae3751ef016663b72e1a516777de324a76e30efa170dbd3dd2d"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-win32.whl", hash = "sha256:d37843fb8df90376e9e91336724d78a32b988d3d20ab6656da4eb8ee3a45b63c"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-win_amd64.whl", hash = "sha256:c10ff6112d119f82b1618b6dc28126798481b9355d8748b64b9b55051eb4f01b"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:861e459b0e97673af6cc5e7f597035c2e3acdfb2608132665406cded25ba64c7"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5de2464c254380d8a6c20a2746614d5a436260be1507491442cf1088e59430d2"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d375d8ccd3cebae8d90270f7aa8532fe05908f79e78ae489068f3b4eee5994e8"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:014ea143572fee1c18322b7908140ad23b3994036ef4c0d630110faf942652f8"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-win32.whl", hash = "sha256:6607ae6cd3a07f8a4c3198ffbf256c261661965742e2b5265a77cd5c679c9bba"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-win_amd64.whl", hash = "sha256:fcb251305fa24a490b6a9ee2180e5f8252915fb778d3dafc70f9cc3f863827b9"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:01aa5f803db724447c1d423ed583e42bf5264c597fd55e4add4301f163b0be48"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4d0e3515ef98aa4f0dc289ff2eebb0ece6260bbf37c2ea2022aad63797eacf60"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:bce28277f308db43a6b4965734366f533b3ff009571ec7ffa583cb77539b84d6"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:8110e6c414d3efc574543109ee618fe2c1f96fa31833a1ff36cc34e968c4f233"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-win32.whl", hash = "sha256:ee5f5188edb20a29c1cc4a039b074fdc5575337c9a68f3063449ab47757bb064"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-win_amd64.whl", hash = "sha256:09083c2487ca3c0865dc588e07aeaa25416da3d95f7482c07e92f47e080aa17b"}, - {file = "SQLAlchemy-1.3.24.tar.gz", hash = "sha256:ebbb777cbf9312359b897bf81ba00dae0f5cb69fba2a18265dcc18a6f5ef7519"}, -] - -[package.extras] -mssql = ["pyodbc"] -mssql-pymssql = ["pymssql"] -mssql-pyodbc = ["pyodbc"] -mysql = ["mysqlclient"] -oracle = ["cx-oracle"] -postgresql = ["psycopg2"] -postgresql-pg8000 = ["pg8000 (<1.16.6)"] -postgresql-psycopg2binary = ["psycopg2-binary"] -postgresql-psycopg2cffi = ["psycopg2cffi"] -pymysql = ["pymysql", "pymysql (<1)"] - -[[package]] -name = "sqlalchemy-aio" -version = "0.17.0" -description = "Async support for SQLAlchemy." -optional = false -python-versions = ">=3.6" -files = [ - {file = "sqlalchemy_aio-0.17.0-py3-none-any.whl", hash = "sha256:3f4aa392c38f032d6734826a4138a0f02ed3122d442ed142be1e5964f2a33b60"}, - {file = "sqlalchemy_aio-0.17.0.tar.gz", hash = "sha256:f531c7982662d71dfc0b117e77bb2ed544e25cd5361e76cf9f5208edcfb71f7b"}, + {file = "SQLAlchemy-1.4.52-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:f68016f9a5713684c1507cc37133c28035f29925c75c0df2f9d0f7571e23720a"}, + {file = "SQLAlchemy-1.4.52-cp310-cp310-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24bb0f81fbbb13d737b7f76d1821ec0b117ce8cbb8ee5e8641ad2de41aa916d3"}, + {file = "SQLAlchemy-1.4.52-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e93983cc0d2edae253b3f2141b0a3fb07e41c76cd79c2ad743fc27eb79c3f6db"}, + {file = "SQLAlchemy-1.4.52-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:84e10772cfc333eb08d0b7ef808cd76e4a9a30a725fb62a0495877a57ee41d81"}, + {file = "SQLAlchemy-1.4.52-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:427988398d2902de042093d17f2b9619a5ebc605bf6372f7d70e29bde6736842"}, + {file = "SQLAlchemy-1.4.52-cp310-cp310-win32.whl", hash = "sha256:1296f2cdd6db09b98ceb3c93025f0da4835303b8ac46c15c2136e27ee4d18d94"}, + {file = "SQLAlchemy-1.4.52-cp310-cp310-win_amd64.whl", hash = "sha256:80e7f697bccc56ac6eac9e2df5c98b47de57e7006d2e46e1a3c17c546254f6ef"}, + {file = "SQLAlchemy-1.4.52-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2f251af4c75a675ea42766880ff430ac33291c8d0057acca79710f9e5a77383d"}, + {file = "SQLAlchemy-1.4.52-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb8f9e4c4718f111d7b530c4e6fb4d28f9f110eb82e7961412955b3875b66de0"}, + {file = "SQLAlchemy-1.4.52-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afb1672b57f58c0318ad2cff80b384e816735ffc7e848d8aa51e0b0fc2f4b7bb"}, + {file = "SQLAlchemy-1.4.52-cp311-cp311-win32.whl", hash = "sha256:6e41cb5cda641f3754568d2ed8962f772a7f2b59403b95c60c89f3e0bd25f15e"}, + {file = "SQLAlchemy-1.4.52-cp311-cp311-win_amd64.whl", hash = "sha256:5bed4f8c3b69779de9d99eb03fd9ab67a850d74ab0243d1be9d4080e77b6af12"}, + {file = "SQLAlchemy-1.4.52-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:49e3772eb3380ac88d35495843daf3c03f094b713e66c7d017e322144a5c6b7c"}, + {file = "SQLAlchemy-1.4.52-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:618827c1a1c243d2540314c6e100aee7af09a709bd005bae971686fab6723554"}, + {file = "SQLAlchemy-1.4.52-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de9acf369aaadb71a725b7e83a5ef40ca3de1cf4cdc93fa847df6b12d3cd924b"}, + {file = "SQLAlchemy-1.4.52-cp312-cp312-win32.whl", hash = "sha256:763bd97c4ebc74136ecf3526b34808c58945023a59927b416acebcd68d1fc126"}, + {file = "SQLAlchemy-1.4.52-cp312-cp312-win_amd64.whl", hash = "sha256:f12aaf94f4d9679ca475975578739e12cc5b461172e04d66f7a3c39dd14ffc64"}, + {file = "SQLAlchemy-1.4.52-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:853fcfd1f54224ea7aabcf34b227d2b64a08cbac116ecf376907968b29b8e763"}, + {file = "SQLAlchemy-1.4.52-cp36-cp36m-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f98dbb8fcc6d1c03ae8ec735d3c62110949a3b8bc6e215053aa27096857afb45"}, + {file = "SQLAlchemy-1.4.52-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e135fff2e84103bc15c07edd8569612ce317d64bdb391f49ce57124a73f45c5"}, + {file = "SQLAlchemy-1.4.52-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b5de6af8852500d01398f5047d62ca3431d1e29a331d0b56c3e14cb03f8094c"}, + {file = "SQLAlchemy-1.4.52-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3491c85df263a5c2157c594f54a1a9c72265b75d3777e61ee13c556d9e43ffc9"}, + {file = "SQLAlchemy-1.4.52-cp36-cp36m-win32.whl", hash = "sha256:427c282dd0deba1f07bcbf499cbcc9fe9a626743f5d4989bfdfd3ed3513003dd"}, + {file = "SQLAlchemy-1.4.52-cp36-cp36m-win_amd64.whl", hash = "sha256:ca5ce82b11731492204cff8845c5e8ca1a4bd1ade85e3b8fcf86e7601bfc6a39"}, + {file = "SQLAlchemy-1.4.52-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:29d4247313abb2015f8979137fe65f4eaceead5247d39603cc4b4a610936cd2b"}, + {file = "SQLAlchemy-1.4.52-cp37-cp37m-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a752bff4796bf22803d052d4841ebc3c55c26fb65551f2c96e90ac7c62be763a"}, + {file = "SQLAlchemy-1.4.52-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7ea11727feb2861deaa293c7971a4df57ef1c90e42cb53f0da40c3468388000"}, + {file = "SQLAlchemy-1.4.52-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d913f8953e098ca931ad7f58797f91deed26b435ec3756478b75c608aa80d139"}, + {file = "SQLAlchemy-1.4.52-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a251146b921725547ea1735b060a11e1be705017b568c9f8067ca61e6ef85f20"}, + {file = "SQLAlchemy-1.4.52-cp37-cp37m-win32.whl", hash = "sha256:1f8e1c6a6b7f8e9407ad9afc0ea41c1f65225ce505b79bc0342159de9c890782"}, + {file = "SQLAlchemy-1.4.52-cp37-cp37m-win_amd64.whl", hash = "sha256:346ed50cb2c30f5d7a03d888e25744154ceac6f0e6e1ab3bc7b5b77138d37710"}, + {file = "SQLAlchemy-1.4.52-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:4dae6001457d4497736e3bc422165f107ecdd70b0d651fab7f731276e8b9e12d"}, + {file = "SQLAlchemy-1.4.52-cp38-cp38-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5d2e08d79f5bf250afb4a61426b41026e448da446b55e4770c2afdc1e200fce"}, + {file = "SQLAlchemy-1.4.52-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bbce5dd7c7735e01d24f5a60177f3e589078f83c8a29e124a6521b76d825b85"}, + {file = "SQLAlchemy-1.4.52-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bdb7b4d889631a3b2a81a3347c4c3f031812eb4adeaa3ee4e6b0d028ad1852b5"}, + {file = "SQLAlchemy-1.4.52-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c294ae4e6bbd060dd79e2bd5bba8b6274d08ffd65b58d106394cb6abbf35cf45"}, + {file = "SQLAlchemy-1.4.52-cp38-cp38-win32.whl", hash = "sha256:bcdfb4b47fe04967669874fb1ce782a006756fdbebe7263f6a000e1db969120e"}, + {file = "SQLAlchemy-1.4.52-cp38-cp38-win_amd64.whl", hash = "sha256:7d0dbc56cb6af5088f3658982d3d8c1d6a82691f31f7b0da682c7b98fa914e91"}, + {file = "SQLAlchemy-1.4.52-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:a551d5f3dc63f096ed41775ceec72fdf91462bb95abdc179010dc95a93957800"}, + {file = "SQLAlchemy-1.4.52-cp39-cp39-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab773f9ad848118df7a9bbabca53e3f1002387cdbb6ee81693db808b82aaab0"}, + {file = "SQLAlchemy-1.4.52-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2de46f5d5396d5331127cfa71f837cca945f9a2b04f7cb5a01949cf676db7d1"}, + {file = "SQLAlchemy-1.4.52-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7027be7930a90d18a386b25ee8af30514c61f3852c7268899f23fdfbd3107181"}, + {file = "SQLAlchemy-1.4.52-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99224d621affbb3c1a4f72b631f8393045f4ce647dd3262f12fe3576918f8bf3"}, + {file = "SQLAlchemy-1.4.52-cp39-cp39-win32.whl", hash = "sha256:c124912fd4e1bb9d1e7dc193ed482a9f812769cb1e69363ab68e01801e859821"}, + {file = "SQLAlchemy-1.4.52-cp39-cp39-win_amd64.whl", hash = "sha256:2c286fab42e49db23c46ab02479f328b8bdb837d3e281cae546cc4085c83b680"}, + {file = "SQLAlchemy-1.4.52.tar.gz", hash = "sha256:80e63bbdc5217dad3485059bdf6f65a7d43f33c8bde619df5c220edf03d87296"}, ] [package.dependencies] -outcome = "*" -represent = ">=1.4" -sqlalchemy = "<1.4" +greenlet = {version = "!=0.4.17", optional = true, markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\" or extra == \"asyncio\")"} [package.extras] -test = ["pytest (>=5.4)", "pytest-asyncio (>=0.14)", "pytest-trio (>=0.6)"] -test-noextras = ["pytest (>=5.4)", "pytest-asyncio (>=0.14)"] -trio = ["trio (>=0.15)"] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"] +mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +pymysql = ["pymysql", "pymysql (<1)"] +sqlcipher = ["sqlcipher3_binary"] [[package]] name = "starlette" @@ -1848,10 +1882,7 @@ files = [ doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] -[extras] -pgsql = ["psycopg2-binary"] - [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "82dc8679e17cd4fb9f897d60ac55e90250886b9b95bfa1def11694d26a027b65" +content-hash = "d312a7a7c367a8c7f3664f4691e8e39aae6bc9aaf2e116abaf9e7fa1f28fe479" diff --git a/pyproject.toml b/pyproject.toml index 1470a6c..712ec14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ license = "MIT" [tool.poetry.dependencies] python = "^3.8.1" -SQLAlchemy = "^1.3.24" +SQLAlchemy = {version = "1.4.52", extras = ["asyncio"]} click = "^8.1.7" pydantic = "^1.10.2" bech32 = "^1.2.0" @@ -18,7 +18,6 @@ loguru = "^0.7.0" ecdsa = "^0.18.0" bitstring = "^3.1.9" secp256k1 = "^0.14.0" -sqlalchemy-aio = "^0.17.0" h11 = "^0.14.0" cryptography = "^41.0.3" websocket-client = "^1.3.3" @@ -26,7 +25,6 @@ pycryptodomex = "^3.16.0" setuptools = "^68.1.2" wheel = "^0.41.1" importlib-metadata = "^6.8.0" -psycopg2-binary = { version = "^2.9.7", optional = true } httpx = {extras = ["socks"], version = "^0.25.1"} bip32 = "^3.4" mnemonic = "^0.20" @@ -35,9 +33,8 @@ pre-commit = "^3.5.0" websockets = "^12.0" slowapi = "^0.1.9" cbor2 = "^5.6.2" - -[tool.poetry.extras] -pgsql = ["psycopg2-binary"] +asyncpg = "^0.29.0" +aiosqlite = "^0.20.0" [tool.poetry.group.dev.dependencies] pytest-asyncio = "^0.21.1" diff --git a/tests/conftest.py b/tests/conftest.py index 260bf76..76d39d3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,7 +22,8 @@ from cashu.mint.ledger import Ledger SERVER_PORT = 3337 SERVER_ENDPOINT = f"http://localhost:{SERVER_PORT}" -settings.debug = False +settings.debug = True +settings.log_level = "TRACE" settings.cashu_dir = "./test_data/" settings.mint_host = "localhost" settings.mint_port = SERVER_PORT @@ -49,6 +50,7 @@ settings.mint_transaction_rate_limit_per_minute = 60 settings.mint_lnd_enable_mpp = True settings.mint_clnrest_enable_mpp = False settings.mint_input_fee_ppk = 0 +settings.db_connection_pool = True assert "test" in settings.cashu_dir shutil.rmtree(settings.cashu_dir, ignore_errors=True) @@ -83,11 +85,6 @@ class UvicornServer(multiprocessing.Process): async def ledger(): async def start_mint_init(ledger: Ledger) -> Ledger: await migrate_databases(ledger.db, migrations_mint) - # add a new keyset (with a new ID) which will be duplicated with a keyset with an - # old ID by mint migration m018_duplicate_deprecated_keyset_ids - # await ledger.activate_keyset(derivation_path=settings.mint_derivation_path, version="0.15.0") - # await migrations_mint.m018_duplicate_deprecated_keyset_ids(ledger.db) - ledger = Ledger( db=Database("mint", settings.mint_database), seed=settings.mint_private_key, @@ -107,8 +104,10 @@ async def ledger(): # clear postgres database db = Database("mint", settings.mint_database) async with db.connect() as conn: + # drop all tables await conn.execute("DROP SCHEMA public CASCADE;") await conn.execute("CREATE SCHEMA public;") + await db.engine.dispose() wallets_module = importlib.import_module("cashu.lightning") lightning_backend = getattr(wallets_module, settings.mint_backend_bolt11_sat)() @@ -125,6 +124,7 @@ async def ledger(): ledger = await start_mint_init(ledger) yield ledger print("teardown") + await ledger.shutdown_ledger() # # This fixture is used for tests that require API access to the mint @@ -134,6 +134,7 @@ def mint(): "cashu.mint.app:app", port=settings.mint_listen_port, host=settings.mint_listen_host, + log_level="trace", ) server = UvicornServer(config=config) diff --git a/tests/helpers.py b/tests/helpers.py index 674952d..185fa1b 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,3 +1,4 @@ +import asyncio import hashlib import importlib import json @@ -6,13 +7,44 @@ import random import string import time from subprocess import PIPE, Popen, TimeoutExpired -from typing import Tuple +from typing import List, Tuple, Union from loguru import logger +from cashu.core.errors import CashuError from cashu.core.settings import settings +async def assert_err(f, msg: Union[str, CashuError]): + """Compute f() and expect an error message 'msg'.""" + try: + await f + except Exception as exc: + error_message: str = str(exc.args[0]) + if isinstance(msg, CashuError): + if msg.detail not in error_message: + raise Exception( + f"CashuError. Expected error: {msg.detail}, got: {error_message}" + ) + return + if msg not in error_message: + raise Exception(f"Expected error: {msg}, got: {error_message}") + return + raise Exception(f"Expected error: {msg}, got no error") + + +async def assert_err_multiple(f, msgs: List[str]): + """Compute f() and expect an error message 'msg'.""" + try: + await f + except Exception as exc: + for msg in msgs: + if msg in str(exc.args[0]): + return + raise Exception(f"Expected error: {msgs}, got: {exc.args[0]}") + raise Exception(f"Expected error: {msgs}, got no error") + + def get_random_string(N: int = 10): return "".join( random.SystemRandom().choice(string.ascii_uppercase + string.digits) @@ -157,6 +189,9 @@ def pay_onchain(address: str, sats: int) -> str: return run_cmd(cmd) -def pay_if_regtest(bolt11: str): +async def pay_if_regtest(bolt11: str): if is_regtest: pay_real_invoice(bolt11) + if is_fake: + await asyncio.sleep(settings.fakewallet_delay_incoming_payment or 0) + await asyncio.sleep(0.1) diff --git a/tests/test_db.py b/tests/test_db.py index 3b2af37..a247363 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -1,15 +1,81 @@ +import asyncio import datetime import os import time +from typing import List import pytest +import pytest_asyncio from cashu.core import db -from cashu.core.db import Connection, timestamp_now +from cashu.core.db import Connection from cashu.core.migrations import backup_database from cashu.core.settings import settings from cashu.mint.ledger import Ledger -from tests.helpers import is_github_actions, is_postgres +from cashu.wallet.wallet import Wallet +from tests.conftest import SERVER_ENDPOINT +from tests.helpers import is_github_actions, is_postgres, pay_if_regtest + + +async def assert_err(f, msg): + """Compute f() and expect an error message 'msg'.""" + try: + await f + except Exception as exc: + if msg not in str(exc.args[0]): + raise Exception(f"Expected error: {msg}, got: {exc.args[0]}") + return + raise Exception(f"Expected error: {msg}, got no error") + + +async def assert_err_multiple(f, msgs: List[str]): + """Compute f() and expect an error message 'msg'.""" + try: + await f + except Exception as exc: + for msg in msgs: + if msg in str(exc.args[0]): + return + raise Exception(f"Expected error: {msgs}, got: {exc.args[0]}") + raise Exception(f"Expected error: {msgs}, got no error") + + +@pytest_asyncio.fixture(scope="function") +async def wallet(): + wallet = await Wallet.with_db( + url=SERVER_ENDPOINT, + db="test_data/wallet", + name="wallet", + ) + await wallet.load_mint() + yield wallet + + +@pytest.mark.asyncio +async def test_db_tables(ledger: Ledger): + async with ledger.db.connect() as conn: + if ledger.db.type == db.SQLITE: + tables_res = await conn.execute( + "SELECT name FROM sqlite_master WHERE type='table';" + ) + elif ledger.db.type in {db.POSTGRES, db.COCKROACH}: + tables_res = await conn.execute( + "SELECT table_name FROM information_schema.tables WHERE table_schema =" + " 'public';" + ) + tables = [t[0] for t in tables_res.all()] + tables_expected = [ + "dbversions", + "keysets", + "proofs_used", + "proofs_pending", + "melt_quotes", + "mint_quotes", + "mint_pubkeys", + "promises", + ] + for table in tables_expected: + assert table in tables @pytest.mark.asyncio @@ -27,7 +93,7 @@ async def test_backup_db_migration(ledger: Ledger): @pytest.mark.asyncio async def test_timestamp_now(ledger: Ledger): - ts = timestamp_now(ledger.db) + ts = ledger.db.timestamp_now_str() if ledger.db.type == db.SQLITE: assert isinstance(ts, str) assert int(ts) <= time.time() @@ -37,33 +103,203 @@ async def test_timestamp_now(ledger: Ledger): @pytest.mark.asyncio -async def test_get_connection(ledger: Ledger): +async def test_db_connect(ledger: Ledger): async with ledger.db.connect() as conn: assert isinstance(conn, Connection) @pytest.mark.asyncio -async def test_db_tables(ledger: Ledger): - async with ledger.db.connect() as conn: - if ledger.db.type == db.SQLITE: - tables_res = await conn.execute( - "SELECT name FROM sqlite_master WHERE type='table';" - ) - elif ledger.db.type in {db.POSTGRES, db.COCKROACH}: - tables_res = await conn.execute( - "SELECT table_name FROM information_schema.tables WHERE table_schema =" - " 'public';" - ) - tables = [t[0] for t in await tables_res.fetchall()] - tables_expected = [ - "dbversions", - "keysets", - "proofs_used", - "proofs_pending", - "melt_quotes", - "mint_quotes", - "mint_pubkeys", - "promises", - ] - for table in tables_expected: - assert table in tables +async def test_db_get_connection(ledger: Ledger): + async with ledger.db.get_connection() as conn: + assert isinstance(conn, Connection) + + +# @pytest.mark.asyncio +# async def test_db_get_connection_locked(wallet: Wallet, ledger: Ledger): +# invoice = await wallet.request_mint(64) + +# async def get_connection(): +# """This code makes sure that only the error of the second connection is raised (which we check in the assert_err)""" +# try: +# async with ledger.db.get_connection(lock_table="mint_quotes"): +# try: +# async with ledger.db.get_connection( +# lock_table="mint_quotes", lock_timeout=0.1 +# ) as conn2: +# # write something with conn1, we never reach this point if the lock works +# await conn2.execute( +# f"INSERT INTO mint_quotes (quote, amount) VALUES ('{invoice.id}', 100);" +# ) +# except Exception as exc: +# # this is expected to raise +# raise Exception(f"conn2: {str(exc)}") + +# except Exception as exc: +# if str(exc).startswith("conn2"): +# raise exc +# else: +# raise Exception("not expected to happen") + +# await assert_err(get_connection(), "failed to acquire database lock") + + +@pytest.mark.asyncio +async def test_db_get_connection_lock_row(wallet: Wallet, ledger: Ledger): + if ledger.db.type == db.SQLITE: + pytest.skip("SQLite does not support row locking") + + invoice = await wallet.request_mint(64) + + async def get_connection(): + """This code makes sure that only the error of the second connection is raised (which we check in the assert_err)""" + try: + async with ledger.db.get_connection( + lock_table="mint_quotes", + lock_select_statement=f"quote='{invoice.id}'", + lock_timeout=0.1, + ) as conn1: + await conn1.execute( + f"UPDATE mint_quotes SET amount=100 WHERE quote='{invoice.id}';" + ) + try: + async with ledger.db.get_connection( + lock_table="mint_quotes", + lock_select_statement=f"quote='{invoice.id}'", + lock_timeout=0.1, + ) as conn2: + # write something with conn1, we never reach this point if the lock works + await conn2.execute( + f"UPDATE mint_quotes SET amount=101 WHERE quote='{invoice.id}';" + ) + except Exception as exc: + # this is expected to raise + raise Exception(f"conn2: {str(exc)}") + except Exception as exc: + if "conn2" in str(exc): + raise exc + else: + raise Exception(f"not expected to happen: {str(exc)}") + + await assert_err(get_connection(), "failed to acquire database lock") + + +@pytest.mark.asyncio +async def test_db_set_proofs_pending_race_condition(wallet: Wallet, ledger: Ledger): + # fill wallet + invoice = await wallet.request_mint(64) + await pay_if_regtest(invoice.bolt11) + await wallet.mint(64, id=invoice.id) + assert wallet.balance == 64 + + await assert_err_multiple( + asyncio.gather( + ledger.db_write._set_proofs_pending(wallet.proofs), + ledger.db_write._set_proofs_pending(wallet.proofs), + ), + [ + "failed to acquire database lock", + "proofs are pending", + ], # depending on how fast the database is, it can be either + ) + + +@pytest.mark.asyncio +async def test_db_set_proofs_pending_delayed_no_race_condition( + wallet: Wallet, ledger: Ledger +): + # fill wallet + invoice = await wallet.request_mint(64) + await pay_if_regtest(invoice.bolt11) + await wallet.mint(64, id=invoice.id) + assert wallet.balance == 64 + + async def delayed_set_proofs_pending(): + await asyncio.sleep(0.1) + await ledger.db_write._set_proofs_pending(wallet.proofs) + + await assert_err( + asyncio.gather( + ledger.db_write._set_proofs_pending(wallet.proofs), + delayed_set_proofs_pending(), + ), + "proofs are pending", + ) + + +@pytest.mark.asyncio +async def test_db_set_proofs_pending_no_race_condition_different_proofs( + wallet: Wallet, ledger: Ledger +): + # fill wallet + invoice = await wallet.request_mint(64) + await pay_if_regtest(invoice.bolt11) + await wallet.mint(64, id=invoice.id, split=[32, 32]) + assert wallet.balance == 64 + assert len(wallet.proofs) == 2 + + asyncio.gather( + ledger.db_write._set_proofs_pending(wallet.proofs[:1]), + ledger.db_write._set_proofs_pending(wallet.proofs[1:]), + ) + + +@pytest.mark.asyncio +async def test_db_get_connection_lock_different_row(wallet: Wallet, ledger: Ledger): + if ledger.db.type == db.SQLITE: + pytest.skip("SQLite does not support row locking") + # this should work since we lock two different rows + invoice = await wallet.request_mint(64) + invoice2 = await wallet.request_mint(64) + + async def get_connection2(): + """This code makes sure that only the error of the second connection is raised (which we check in the assert_err)""" + try: + async with ledger.db.get_connection( + lock_table="mint_quotes", + lock_select_statement=f"quote='{invoice.id}'", + lock_timeout=0.1, + ): + try: + async with ledger.db.get_connection( + lock_table="mint_quotes", + lock_select_statement=f"quote='{invoice2.id}'", + lock_timeout=0.1, + ) as conn2: + # write something with conn1, this time we should reach this block with postgres + quote = await ledger.crud.get_mint_quote( + quote_id=invoice2.id, db=ledger.db, conn=conn2 + ) + assert quote is not None + quote.amount = 100 + await ledger.crud.update_mint_quote( + quote=quote, db=ledger.db, conn=conn2 + ) + + except Exception as exc: + # this is expected to raise + raise Exception(f"conn2: {str(exc)}") + + except Exception as exc: + if "conn2" in str(exc): + raise exc + else: + raise Exception(f"not expected to happen: {str(exc)}") + + await get_connection2() + + +@pytest.mark.asyncio +async def test_db_lock_table(wallet: Wallet, ledger: Ledger): + # fill wallet + invoice = await wallet.request_mint(64) + await pay_if_regtest(invoice.bolt11) + + await wallet.mint(64, id=invoice.id) + assert wallet.balance == 64 + + async with ledger.db.connect(lock_table="proofs_pending", lock_timeout=0.1) as conn: + assert isinstance(conn, Connection) + await assert_err( + ledger.db_write._set_proofs_pending(wallet.proofs), + "failed to acquire database lock", + ) diff --git a/tests/test_mint.py b/tests/test_mint.py index e842e35..f5976a8 100644 --- a/tests/test_mint.py +++ b/tests/test_mint.py @@ -69,7 +69,7 @@ async def test_get_keyset(ledger: Ledger): @pytest.mark.asyncio async def test_mint(ledger: Ledger): quote = await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat")) - pay_if_regtest(quote.request) + await pay_if_regtest(quote.request) blinded_messages_mock = [ BlindedMessage( amount=8, @@ -89,7 +89,7 @@ async def test_mint(ledger: Ledger): @pytest.mark.asyncio async def test_mint_invalid_blinded_message(ledger: Ledger): quote = await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat")) - pay_if_regtest(quote.request) + await pay_if_regtest(quote.request) blinded_messages_mock_invalid_key = [ BlindedMessage( amount=8, diff --git a/tests/test_mint_api.py b/tests/test_mint_api.py index 5887a15..e1f69e2 100644 --- a/tests/test_mint_api.py +++ b/tests/test_mint_api.py @@ -153,7 +153,7 @@ async def test_api_keyset_keys_old_keyset_id(ledger: Ledger): ) async def test_split(ledger: Ledger, wallet: Wallet): invoice = await wallet.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet.mint(64, id=invoice.id) assert wallet.balance == 64 secrets, rs, derivation_paths = await wallet.generate_n_secrets(2) @@ -162,7 +162,7 @@ async def test_split(ledger: Ledger, wallet: Wallet): inputs_payload = [p.to_dict() for p in wallet.proofs] outputs_payload = [o.dict() for o in outputs] payload = {"inputs": inputs_payload, "outputs": outputs_payload} - response = httpx.post(f"{BASE_URL}/v1/swap", json=payload) + response = httpx.post(f"{BASE_URL}/v1/swap", json=payload, timeout=None) assert response.status_code == 200, f"{response.url} {response.status_code}" result = response.json() assert len(result["signatures"]) == 2 @@ -208,7 +208,7 @@ async def test_mint_quote(ledger: Ledger): assert result["expiry"] == expiry # pay the invoice - pay_if_regtest(result["request"]) + await pay_if_regtest(result["request"]) # get mint quote again from api response = httpx.get( @@ -234,7 +234,7 @@ async def test_mint_quote(ledger: Ledger): ) async def test_mint(ledger: Ledger, wallet: Wallet): invoice = await wallet.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) quote_id = invoice.id secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001) outputs, rs = wallet._construct_outputs([32, 32], secrets, rs) @@ -352,7 +352,7 @@ async def test_melt_quote_external(ledger: Ledger, wallet: Wallet): async def test_melt_internal(ledger: Ledger, wallet: Wallet): # internal invoice invoice = await wallet.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet.mint(64, id=invoice.id) assert wallet.balance == 64 @@ -411,7 +411,7 @@ async def test_melt_internal(ledger: Ledger, wallet: Wallet): async def test_melt_external(ledger: Ledger, wallet: Wallet): # internal invoice invoice = await wallet.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet.mint(64, id=invoice.id) assert wallet.balance == 64 @@ -439,6 +439,7 @@ async def test_melt_external(ledger: Ledger, wallet: Wallet): }, timeout=None, ) + response.raise_for_status() assert response.status_code == 200, f"{response.url} {response.status_code}" result = response.json() assert result.get("payment_preimage") is not None @@ -486,7 +487,7 @@ async def test_api_check_state(ledger: Ledger): ) async def test_api_restore(ledger: Ledger, wallet: Wallet): invoice = await wallet.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet.mint(64, id=invoice.id) assert wallet.balance == 64 secret_counter = await bump_secret_derivation( diff --git a/tests/test_mint_api_deprecated.py b/tests/test_mint_api_deprecated.py index e172256..b9aac81 100644 --- a/tests/test_mint_api_deprecated.py +++ b/tests/test_mint_api_deprecated.py @@ -67,7 +67,7 @@ async def test_api_keyset_keys(ledger: Ledger): @pytest.mark.asyncio async def test_split(ledger: Ledger, wallet: Wallet): invoice = await wallet.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet.mint(64, id=invoice.id) assert wallet.balance == 64 secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(20000, 20001) @@ -88,7 +88,7 @@ async def test_split(ledger: Ledger, wallet: Wallet): @pytest.mark.asyncio async def test_split_deprecated_with_amount(ledger: Ledger, wallet: Wallet): invoice = await wallet.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet.mint(64, id=invoice.id) assert wallet.balance == 64 secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(80000, 80001) @@ -124,7 +124,7 @@ async def test_api_mint_validation(ledger): @pytest.mark.asyncio async def test_mint(ledger: Ledger, wallet: Wallet): invoice = await wallet.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) quote_id = invoice.id secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001) outputs, rs = wallet._construct_outputs([32, 32], secrets, rs) @@ -148,9 +148,9 @@ async def test_mint(ledger: Ledger, wallet: Wallet): @pytest.mark.asyncio async def test_melt_internal(ledger: Ledger, wallet: Wallet): - # internal invoice + # fill wallet invoice = await wallet.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet.mint(64, id=invoice.id) assert wallet.balance == 64 @@ -190,15 +190,13 @@ async def test_melt_internal_no_change_outputs(ledger: Ledger, wallet: Wallet): # Clients without NUT-08 will not send change outputs # internal invoice invoice = await wallet.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet.mint(64, id=invoice.id) assert wallet.balance == 64 # create invoice to melt to invoice = await wallet.request_mint(64) - invoice_payment_request = invoice.bolt11 - quote = await wallet.melt_quote(invoice_payment_request) assert quote.amount == 64 assert quote.fee_reserve == 0 @@ -231,7 +229,7 @@ async def test_melt_internal_no_change_outputs(ledger: Ledger, wallet: Wallet): async def test_melt_external(ledger: Ledger, wallet: Wallet): # internal invoice invoice = await wallet.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet.mint(64, id=invoice.id) assert wallet.balance == 64 @@ -325,7 +323,7 @@ async def test_api_check_state(ledger: Ledger): @pytest.mark.asyncio async def test_api_restore(ledger: Ledger, wallet: Wallet): invoice = await wallet.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet.mint(64, id=invoice.id) assert wallet.balance == 64 secret_counter = await bump_secret_derivation( diff --git a/tests/test_mint_db.py b/tests/test_mint_db.py index d4a7852..489204b 100644 --- a/tests/test_mint_db.py +++ b/tests/test_mint_db.py @@ -1,23 +1,18 @@ import pytest import pytest_asyncio +from cashu.core.base import MeltQuoteState, MintQuoteState, ProofSpentState from cashu.core.models import PostMeltQuoteRequest from cashu.mint.ledger import Ledger from cashu.wallet.wallet import Wallet from cashu.wallet.wallet import Wallet as Wallet1 from tests.conftest import SERVER_ENDPOINT -from tests.helpers import is_postgres - - -async def assert_err(f, msg): - """Compute f() and expect an error message 'msg'.""" - try: - await f - except Exception as exc: - if msg not in str(exc.args[0]): - raise Exception(f"Expected error: {msg}, got: {exc.args[0]}") - return - raise Exception(f"Expected error: {msg}, got no error") +from tests.helpers import ( + assert_err, + is_github_actions, + is_postgres, + pay_if_regtest, +) @pytest_asyncio.fixture(scope="function") @@ -31,6 +26,35 @@ async def wallet1(ledger: Ledger): yield wallet1 +@pytest.mark.asyncio +@pytest.mark.skipif(is_github_actions, reason="GITHUB_ACTIONS") +async def test_mint_proofs_pending(wallet1: Wallet, ledger: Ledger): + invoice = await wallet1.request_mint(64) + await pay_if_regtest(invoice.bolt11) + await wallet1.mint(64, id=invoice.id) + proofs = wallet1.proofs.copy() + + proofs_states_before_split = await wallet1.check_proof_state(proofs) + assert all( + [s.state == ProofSpentState.unspent for s in proofs_states_before_split.states] + ) + + await ledger.db_write._set_proofs_pending(proofs) + + proof_states = await wallet1.check_proof_state(proofs) + assert all([s.state == ProofSpentState.pending for s in proof_states.states]) + await assert_err(wallet1.split(wallet1.proofs, 20), "proofs are pending.") + + await ledger.db_write._unset_proofs_pending(proofs) + + await wallet1.split(proofs, 20) + + proofs_states_after_split = await wallet1.check_proof_state(proofs) + assert all( + [s.state == ProofSpentState.spent for s in proofs_states_after_split.states] + ) + + @pytest.mark.asyncio async def test_mint_quote(wallet1: Wallet, ledger: Ledger): invoice = await wallet1.request_mint(128) @@ -42,10 +66,63 @@ async def test_mint_quote(wallet1: Wallet, ledger: Ledger): assert quote.unit == "sat" assert not quote.paid assert quote.checking_id == invoice.payment_hash - assert quote.paid_time is None + # assert quote.paid_time is None assert quote.created_time +@pytest.mark.asyncio +async def test_mint_quote_state_transitions(wallet1: Wallet, ledger: Ledger): + invoice = await wallet1.request_mint(128) + assert invoice is not None + quote = await ledger.crud.get_mint_quote(quote_id=invoice.id, db=ledger.db) + assert quote is not None + assert quote.quote == invoice.id + assert quote.state == MintQuoteState.unpaid + + # set pending again + async def set_state(quote, state): + quote.state = state + + # set pending + await assert_err( + set_state(quote, MintQuoteState.pending), + "Cannot change state of an unpaid mint quote", + ) + + # set unpaid + await assert_err( + set_state(quote, MintQuoteState.unpaid), + "Cannot change state of an unpaid mint quote", + ) + + # set paid + quote.state = MintQuoteState.paid + + # set unpaid + await assert_err( + set_state(quote, MintQuoteState.unpaid), + "Cannot change state of a paid mint quote to unpaid.", + ) + + # set pending + quote.state = MintQuoteState.pending + + # set paid again + quote.state = MintQuoteState.paid + + # set pending again + quote.state = MintQuoteState.pending + + # set issued + quote.state = MintQuoteState.issued + + # set pending again + await assert_err( + set_state(quote, MintQuoteState.pending), + "Cannot change state of an issued mint quote.", + ) + + @pytest.mark.asyncio async def test_get_mint_quote_by_request(wallet1: Wallet, ledger: Ledger): invoice = await wallet1.request_mint(128) @@ -56,7 +133,7 @@ async def test_get_mint_quote_by_request(wallet1: Wallet, ledger: Ledger): assert quote.amount == 128 assert quote.unit == "sat" assert not quote.paid - assert quote.paid_time is None + # assert quote.paid_time is None assert quote.created_time @@ -74,10 +151,112 @@ async def test_melt_quote(wallet1: Wallet, ledger: Ledger): assert quote.unit == "sat" assert not quote.paid assert quote.checking_id == invoice.payment_hash - assert quote.paid_time is None + # assert quote.paid_time is None assert quote.created_time +@pytest.mark.asyncio +async def test_melt_quote_set_pending(wallet1: Wallet, ledger: Ledger): + invoice = await wallet1.request_mint(128) + assert invoice is not None + melt_quote = await ledger.melt_quote( + PostMeltQuoteRequest(request=invoice.bolt11, unit="sat") + ) + assert melt_quote is not None + assert melt_quote.state == MeltQuoteState.unpaid.value + quote = await ledger.crud.get_melt_quote(quote_id=melt_quote.quote, db=ledger.db) + assert quote is not None + assert quote.quote == melt_quote.quote + assert quote.state == MeltQuoteState.unpaid + previous_state = quote.state + await ledger.db_write._set_melt_quote_pending(quote) + quote = await ledger.crud.get_melt_quote(quote_id=melt_quote.quote, db=ledger.db) + assert quote is not None + assert quote.state == MeltQuoteState.pending + + # set unpending + await ledger.db_write._unset_melt_quote_pending(quote, previous_state) + quote = await ledger.crud.get_melt_quote(quote_id=melt_quote.quote, db=ledger.db) + assert quote is not None + assert quote.state == previous_state + + +@pytest.mark.asyncio +async def test_melt_quote_state_transitions(wallet1: Wallet, ledger: Ledger): + invoice = await wallet1.request_mint(128) + assert invoice is not None + melt_quote = await ledger.melt_quote( + PostMeltQuoteRequest(request=invoice.bolt11, unit="sat") + ) + quote = await ledger.crud.get_melt_quote(quote_id=melt_quote.quote, db=ledger.db) + assert quote is not None + assert quote.quote == melt_quote.quote + assert quote.state == MeltQuoteState.unpaid + + # set pending + quote.state = MeltQuoteState.pending + + # set unpaid + quote.state = MeltQuoteState.unpaid + + # set paid + quote.state = MeltQuoteState.paid + + # set pending again + async def set_state(quote, state): + quote.state = state + + await assert_err( + set_state(quote, MeltQuoteState.pending), + "Cannot change state of a paid melt quote.", + ) + + +@pytest.mark.asyncio +async def test_mint_quote_set_pending(wallet1: Wallet, ledger: Ledger): + invoice = await wallet1.request_mint(128) + assert invoice is not None + quote = await ledger.crud.get_mint_quote(quote_id=invoice.id, db=ledger.db) + assert quote is not None + assert quote.state == MintQuoteState.unpaid + + # pay_if_regtest pays on regtest, get_mint_quote pays on FakeWallet + await pay_if_regtest(invoice.bolt11) + _ = await ledger.get_mint_quote(invoice.id) + + quote = await ledger.crud.get_mint_quote(quote_id=invoice.id, db=ledger.db) + assert quote is not None + assert quote.state == MintQuoteState.paid + + previous_state = MintQuoteState.paid + await ledger.db_write._set_mint_quote_pending(quote.quote) + quote = await ledger.crud.get_mint_quote(quote_id=invoice.id, db=ledger.db) + assert quote is not None + assert quote.state == MintQuoteState.pending + + # try to mint while pending + await assert_err(wallet1.mint(128, id=invoice.id), "Mint quote already pending.") + + # set unpending + await ledger.db_write._unset_mint_quote_pending(quote.quote, previous_state) + + quote = await ledger.crud.get_mint_quote(quote_id=invoice.id, db=ledger.db) + assert quote is not None + assert quote.state == previous_state + assert quote.state == MintQuoteState.paid + + # # set paid and mint again + # quote.state = MintQuoteState.paid + # await ledger.crud.update_mint_quote(quote=quote, db=ledger.db) + + await wallet1.mint(quote.amount, id=quote.quote) + + # check if quote is issued + quote = await ledger.crud.get_mint_quote(quote_id=invoice.id, db=ledger.db) + assert quote is not None + assert quote.state == MintQuoteState.issued + + @pytest.mark.asyncio @pytest.mark.skipif(not is_postgres, reason="only works with Postgres") async def test_postgres_working(): diff --git a/tests/test_mint_fees.py b/tests/test_mint_fees.py index 673989b..b1be77f 100644 --- a/tests/test_mint_fees.py +++ b/tests/test_mint_fees.py @@ -49,7 +49,7 @@ def set_ledger_keyset_fees( @pytest.mark.asyncio async def test_get_fees_for_proofs(wallet1: Wallet, ledger: Ledger): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, split=[1] * 64, id=invoice.id) # two proofs @@ -115,7 +115,7 @@ async def test_split_with_fees(wallet1: Wallet, ledger: Ledger): # set fees to 100 ppk set_ledger_keyset_fees(100, ledger) invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) send_proofs, _ = await wallet1.select_to_send(wallet1.proofs, 10) @@ -133,7 +133,7 @@ async def test_split_with_high_fees(wallet1: Wallet, ledger: Ledger): # set fees to 100 ppk set_ledger_keyset_fees(1234, ledger) invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) send_proofs, _ = await wallet1.select_to_send(wallet1.proofs, 10) @@ -151,7 +151,7 @@ async def test_split_not_enough_fees(wallet1: Wallet, ledger: Ledger): # set fees to 100 ppk set_ledger_keyset_fees(100, ledger) invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) send_proofs, _ = await wallet1.select_to_send(wallet1.proofs, 10) @@ -173,6 +173,7 @@ async def test_melt_internal(wallet1: Wallet, ledger: Ledger): # mint twice so we have enough to pay the second invoice back invoice = await wallet1.request_mint(128) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(128, id=invoice.id) assert wallet1.balance == 128 @@ -217,7 +218,7 @@ async def test_melt_external_with_fees(wallet1: Wallet, ledger: Ledger): # mint twice so we have enough to pay the second invoice back invoice = await wallet1.request_mint(128) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(128, id=invoice.id) assert wallet1.balance == 128 diff --git a/tests/test_mint_init.py b/tests/test_mint_init.py index 20c8000..a33a6cf 100644 --- a/tests/test_mint_init.py +++ b/tests/test_mint_init.py @@ -241,7 +241,7 @@ async def test_startup_fakewallet_pending_quote_pending(ledger: Ledger): async def test_startup_regtest_pending_quote_pending(wallet: Wallet, ledger: Ledger): # fill wallet invoice = await wallet.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet.mint(64, id=invoice.id) assert wallet.balance == 64 @@ -286,7 +286,7 @@ async def test_startup_regtest_pending_quote_pending(wallet: Wallet, ledger: Led async def test_startup_regtest_pending_quote_success(wallet: Wallet, ledger: Ledger): # fill wallet invoice = await wallet.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet.mint(64, id=invoice.id) assert wallet.balance == 64 @@ -334,7 +334,7 @@ async def test_startup_regtest_pending_quote_failure(wallet: Wallet, ledger: Led """Simulate a failure to pay the hodl invoice by canceling it.""" # fill wallet invoice = await wallet.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet.mint(64, id=invoice.id) assert wallet.balance == 64 diff --git a/tests/test_mint_operations.py b/tests/test_mint_operations.py index c8a36dc..518ae24 100644 --- a/tests/test_mint_operations.py +++ b/tests/test_mint_operations.py @@ -1,6 +1,7 @@ import pytest import pytest_asyncio +from cashu.core.base import MeltQuoteState, MintQuoteState from cashu.core.helpers import sum_proofs from cashu.core.models import PostMeltQuoteRequest, PostMintQuoteRequest from cashu.mint.ledger import Ledger @@ -37,7 +38,9 @@ async def wallet1(ledger: Ledger): async def test_melt_internal(wallet1: Wallet, ledger: Ledger): # mint twice so we have enough to pay the second invoice back invoice = await wallet1.request_mint(128) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(128, id=invoice.id) + await pay_if_regtest(invoice.bolt11) assert wallet1.balance == 128 # create a mint quote so that we can melt to it internally @@ -48,17 +51,21 @@ async def test_melt_internal(wallet1: Wallet, ledger: Ledger): PostMeltQuoteRequest(request=invoice_payment_request, unit="sat") ) assert not melt_quote.paid + assert melt_quote.state == MeltQuoteState.unpaid.value + assert melt_quote.amount == 64 assert melt_quote.fee_reserve == 0 melt_quote_pre_payment = await ledger.get_melt_quote(melt_quote.quote) assert not melt_quote_pre_payment.paid, "melt quote should not be paid" + assert melt_quote_pre_payment.state == MeltQuoteState.unpaid keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 64) await ledger.melt(proofs=send_proofs, quote=melt_quote.quote) melt_quote_post_payment = await ledger.get_melt_quote(melt_quote.quote) assert melt_quote_post_payment.paid, "melt quote should be paid" + assert melt_quote_post_payment.state == MeltQuoteState.paid @pytest.mark.asyncio @@ -66,7 +73,7 @@ async def test_melt_internal(wallet1: Wallet, ledger: Ledger): async def test_melt_external(wallet1: Wallet, ledger: Ledger): # mint twice so we have enough to pay the second invoice back invoice = await wallet1.request_mint(128) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(128, id=invoice.id) assert wallet1.balance == 128 @@ -74,6 +81,9 @@ async def test_melt_external(wallet1: Wallet, ledger: Ledger): invoice_payment_request = invoice_dict["payment_request"] mint_quote = await wallet1.melt_quote(invoice_payment_request) + assert not mint_quote.paid, "mint quote should not be paid" + assert mint_quote.state == MeltQuoteState.unpaid.value + total_amount = mint_quote.amount + mint_quote.fee_reserve keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount) melt_quote = await ledger.melt_quote( @@ -82,22 +92,25 @@ async def test_melt_external(wallet1: Wallet, ledger: Ledger): melt_quote_pre_payment = await ledger.get_melt_quote(melt_quote.quote) assert not melt_quote_pre_payment.paid, "melt quote should not be paid" + assert melt_quote_pre_payment.state == MeltQuoteState.unpaid assert not melt_quote.paid, "melt quote should not be paid" await ledger.melt(proofs=send_proofs, quote=melt_quote.quote) melt_quote_post_payment = await ledger.get_melt_quote(melt_quote.quote) assert melt_quote_post_payment.paid, "melt quote should be paid" + assert melt_quote_post_payment.state == MeltQuoteState.paid @pytest.mark.asyncio @pytest.mark.skipif(is_regtest, reason="only works with FakeWallet") async def test_mint_internal(wallet1: Wallet, ledger: Ledger): invoice = await wallet1.request_mint(128) - + await pay_if_regtest(invoice.bolt11) mint_quote = await ledger.get_mint_quote(invoice.id) assert mint_quote.paid, "mint quote should be paid" + assert mint_quote.state == MintQuoteState.paid output_amounts = [128] secrets, rs, derivation_paths = await wallet1.generate_n_secrets( @@ -111,24 +124,32 @@ async def test_mint_internal(wallet1: Wallet, ledger: Ledger): "outputs have already been signed before.", ) + mint_quote_after_payment = await ledger.get_mint_quote(invoice.id) + assert mint_quote_after_payment.paid, "mint quote should be paid" + assert mint_quote_after_payment.state == MintQuoteState.issued + @pytest.mark.asyncio @pytest.mark.skipif(is_fake, reason="only works with Regtest") async def test_mint_external(wallet1: Wallet, ledger: Ledger): quote = await ledger.mint_quote(PostMintQuoteRequest(amount=128, unit="sat")) + assert not quote.paid, "mint quote should not be paid" + assert quote.state == MintQuoteState.unpaid mint_quote = await ledger.get_mint_quote(quote.quote) assert not mint_quote.paid, "mint quote already paid" + assert mint_quote.state == MintQuoteState.unpaid await assert_err( wallet1.mint(128, id=quote.quote), "quote not paid", ) - pay_if_regtest(quote.request) + await pay_if_regtest(quote.request) mint_quote = await ledger.get_mint_quote(quote.quote) assert mint_quote.paid, "mint quote should be paid" + assert mint_quote.state == MintQuoteState.paid output_amounts = [128] secrets, rs, derivation_paths = await wallet1.generate_n_secrets( @@ -137,11 +158,15 @@ async def test_mint_external(wallet1: Wallet, ledger: Ledger): outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs) await ledger.mint(outputs=outputs, quote_id=quote.quote) + mint_quote_after_payment = await ledger.get_mint_quote(quote.quote) + assert mint_quote_after_payment.paid, "mint quote should be paid" + assert mint_quote_after_payment.state == MintQuoteState.issued + @pytest.mark.asyncio async def test_split(wallet1: Wallet, ledger: Ledger): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10) @@ -158,7 +183,7 @@ async def test_split(wallet1: Wallet, ledger: Ledger): @pytest.mark.asyncio async def test_split_with_no_outputs(wallet1: Wallet, ledger: Ledger): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10, set_reserved=False) await assert_err( @@ -170,7 +195,7 @@ async def test_split_with_no_outputs(wallet1: Wallet, ledger: Ledger): @pytest.mark.asyncio async def test_split_with_input_less_than_outputs(wallet1: Wallet, ledger: Ledger): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) keep_proofs, send_proofs = await wallet1.split_to_send( @@ -199,7 +224,7 @@ async def test_split_with_input_less_than_outputs(wallet1: Wallet, ledger: Ledge @pytest.mark.asyncio async def test_split_with_input_more_than_outputs(wallet1: Wallet, ledger: Ledger): invoice = await wallet1.request_mint(128) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(128, id=invoice.id) inputs = wallet1.proofs @@ -223,7 +248,7 @@ async def test_split_with_input_more_than_outputs(wallet1: Wallet, ledger: Ledge @pytest.mark.asyncio async def test_split_twice_with_same_outputs(wallet1: Wallet, ledger: Ledger): invoice = await wallet1.request_mint(128) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(128, split=[64, 64], id=invoice.id) inputs1 = wallet1.proofs[:1] inputs2 = wallet1.proofs[1:] @@ -258,7 +283,7 @@ async def test_split_twice_with_same_outputs(wallet1: Wallet, ledger: Ledger): @pytest.mark.asyncio async def test_mint_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger): invoice = await wallet1.request_mint(128) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) output_amounts = [128] secrets, rs, derivation_paths = await wallet1.generate_n_secrets( len(output_amounts) @@ -268,7 +293,7 @@ async def test_mint_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger): # now try to mint with the same outputs again invoice2 = await wallet1.request_mint(128) - pay_if_regtest(invoice2.bolt11) + await pay_if_regtest(invoice2.bolt11) await assert_err( ledger.mint(outputs=outputs, quote_id=invoice2.id), @@ -279,7 +304,7 @@ async def test_mint_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger): @pytest.mark.asyncio async def test_melt_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger): invoice = await wallet1.request_mint(130) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(130, id=invoice.id) output_amounts = [128] @@ -290,7 +315,7 @@ async def test_melt_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger): # we use the outputs once for minting invoice2 = await wallet1.request_mint(128) - pay_if_regtest(invoice2.bolt11) + await pay_if_regtest(invoice2.bolt11) await ledger.mint(outputs=outputs, quote_id=invoice2.id) # use the same outputs for melting @@ -307,7 +332,7 @@ async def test_melt_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger): @pytest.mark.asyncio async def test_melt_with_less_inputs_than_invoice(wallet1: Wallet, ledger: Ledger): invoice = await wallet1.request_mint(32) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(32, id=invoice.id) # outputs for fee return @@ -336,7 +361,7 @@ async def test_melt_with_less_inputs_than_invoice(wallet1: Wallet, ledger: Ledge @pytest.mark.asyncio async def test_melt_with_more_inputs_than_invoice(wallet1: Wallet, ledger: Ledger): invoice = await wallet1.request_mint(130) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(130, split=[64, 64, 2], id=invoice.id) # outputs for fee return @@ -368,7 +393,7 @@ async def test_melt_with_more_inputs_than_invoice(wallet1: Wallet, ledger: Ledge @pytest.mark.asyncio async def test_check_proof_state(wallet1: Wallet, ledger: Ledger): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10) @@ -385,7 +410,7 @@ async def test_check_proof_state(wallet1: Wallet, ledger: Ledger): # f"ws://localhost:{SERVER_PORT}/v1/quote/{invoice.id}" # ) # await asyncio.sleep(0.1) -# pay_if_regtest(invoice.bolt11) +# await pay_if_regtest(invoice.bolt11) # await wallet1.mint(64, id=invoice.id) # await asyncio.sleep(0.1) # data = str(ws.recv()) diff --git a/tests/test_mint_regtest.py b/tests/test_mint_regtest.py index 60c2ced..8a934f0 100644 --- a/tests/test_mint_regtest.py +++ b/tests/test_mint_regtest.py @@ -32,7 +32,7 @@ async def wallet(): async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger): # fill wallet invoice = await wallet.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet.mint(64, id=invoice.id) assert wallet.balance == 64 diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 1cfc471..7896090 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -40,6 +40,18 @@ async def assert_err(f, msg: Union[str, CashuError]): raise Exception(f"Expected error: {msg}, got no error") +async def assert_err_multiple(f, msgs: List[str]): + """Compute f() and expect an error message 'msg'.""" + try: + await f + except Exception as exc: + for msg in msgs: + if msg in str(exc.args[0]): + return + raise Exception(f"Expected error: {msgs}, got: {exc.args[0]}") + raise Exception(f"Expected error: {msgs}, got no error") + + def assert_amt(proofs: List[Proof], expected: int): """Assert amounts the proofs contain.""" assert sum([p.amount for p in proofs]) == expected @@ -155,7 +167,7 @@ async def test_request_mint(wallet1: Wallet): @pytest.mark.asyncio async def test_mint(wallet1: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) expected_proof_amounts = wallet1.split_wallet_state(64) await wallet1.mint(64, id=invoice.id) assert wallet1.balance == 64 @@ -179,7 +191,7 @@ async def test_mint_amounts(wallet1: Wallet): """Mint predefined amounts""" amts = [1, 1, 1, 2, 2, 4, 16] invoice = await wallet1.request_mint(sum(amts)) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(amount=sum(amts), split=amts, id=invoice.id) assert wallet1.balance == 27 assert wallet1.proof_amounts == amts @@ -211,7 +223,7 @@ async def test_mint_amounts_wrong_order(wallet1: Wallet): @pytest.mark.asyncio async def test_split(wallet1: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) assert wallet1.balance == 64 # the outputs we keep that we expect after the split @@ -231,7 +243,7 @@ async def test_split(wallet1: Wallet): @pytest.mark.asyncio async def test_split_to_send(wallet1: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) assert wallet1.balance == 64 @@ -253,7 +265,7 @@ async def test_split_to_send(wallet1: Wallet): @pytest.mark.asyncio async def test_split_more_than_balance(wallet1: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) await assert_err( wallet1.split(wallet1.proofs, 128), @@ -267,7 +279,7 @@ async def test_split_more_than_balance(wallet1: Wallet): async def test_melt(wallet1: Wallet): # mint twice so we have enough to pay the second invoice back topup_invoice = await wallet1.request_mint(128) - pay_if_regtest(topup_invoice.bolt11) + await pay_if_regtest(topup_invoice.bolt11) await wallet1.mint(128, id=topup_invoice.id) assert wallet1.balance == 128 @@ -333,7 +345,7 @@ async def test_melt(wallet1: Wallet): @pytest.mark.asyncio async def test_split_to_send_more_than_balance(wallet1: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) await assert_err( wallet1.split_to_send(wallet1.proofs, 128, set_reserved=True), @@ -346,7 +358,7 @@ async def test_split_to_send_more_than_balance(wallet1: Wallet): @pytest.mark.asyncio async def test_double_spend(wallet1: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) doublespend = await wallet1.mint(64, id=invoice.id) await wallet1.split(wallet1.proofs, 20) await assert_err( @@ -360,7 +372,7 @@ async def test_double_spend(wallet1: Wallet): @pytest.mark.asyncio async def test_duplicate_proofs_double_spent(wallet1: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) doublespend = await wallet1.mint(64, id=invoice.id) await assert_err( wallet1.split(wallet1.proofs + doublespend, 20), @@ -374,24 +386,24 @@ async def test_duplicate_proofs_double_spent(wallet1: Wallet): @pytest.mark.skipif(is_github_actions, reason="GITHUB_ACTIONS") async def test_split_race_condition(wallet1: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) # run two splits in parallel import asyncio - await assert_err( + await assert_err_multiple( asyncio.gather( wallet1.split(wallet1.proofs, 20), wallet1.split(wallet1.proofs, 20), ), - "proofs are pending.", + ["proofs are pending.", "already spent."], ) @pytest.mark.asyncio async def test_send_and_redeem(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) _, spendable_proofs = await wallet1.split_to_send( wallet1.proofs, 32, set_reserved=True @@ -410,7 +422,7 @@ async def test_send_and_redeem(wallet1: Wallet, wallet2: Wallet): async def test_invalidate_all_proofs(wallet1: Wallet): """Try to invalidate proofs that have not been spent yet. Should not work!""" invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) await wallet1.invalidate(wallet1.proofs) assert wallet1.balance == 0 @@ -420,7 +432,7 @@ async def test_invalidate_all_proofs(wallet1: Wallet): async def test_invalidate_unspent_proofs_with_checking(wallet1: Wallet): """Try to invalidate proofs that have not been spent yet but force no check.""" invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) await wallet1.invalidate(wallet1.proofs, check_spendable=True) assert wallet1.balance == 64 @@ -429,7 +441,7 @@ async def test_invalidate_unspent_proofs_with_checking(wallet1: Wallet): @pytest.mark.asyncio async def test_split_invalid_amount(wallet1: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) await assert_err( wallet1.split(wallet1.proofs, -1), @@ -440,7 +452,7 @@ async def test_split_invalid_amount(wallet1: Wallet): @pytest.mark.asyncio async def test_token_state(wallet1: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) assert wallet1.balance == 64 resp = await wallet1.check_proof_state(wallet1.proofs) diff --git a/tests/test_wallet_cli.py b/tests/test_wallet_cli.py index ccf98d5..f058966 100644 --- a/tests/test_wallet_cli.py +++ b/tests/test_wallet_cli.py @@ -117,7 +117,7 @@ def test_invoice_return_immediately(mint, cli_prefix): assert result.exception is None invoice, invoice_id = get_bolt11_and_invoice_id_from_invoice_command(result.output) - pay_if_regtest(invoice) + asyncio.run(pay_if_regtest(invoice)) result = runner.invoke( cli, @@ -146,7 +146,7 @@ def test_invoice_with_split(mint, cli_prefix): assert result.exception is None invoice, invoice_id = get_bolt11_and_invoice_id_from_invoice_command(result.output) - pay_if_regtest(invoice) + asyncio.run(pay_if_regtest(invoice)) result = runner.invoke( cli, [*cli_prefix, "invoice", "10", "-s", "1", "--id", invoice_id], @@ -164,7 +164,7 @@ def test_invoices_with_minting(cli_prefix): wallet1 = asyncio.run(init_wallet()) asyncio.run(reset_invoices(wallet=wallet1)) invoice = asyncio.run(wallet1.request_mint(64)) - + asyncio.run(pay_if_regtest(invoice.bolt11)) # act runner = CliRunner() result = runner.invoke( diff --git a/tests/test_wallet_htlc.py b/tests/test_wallet_htlc.py index 2900402..bda5726 100644 --- a/tests/test_wallet_htlc.py +++ b/tests/test_wallet_htlc.py @@ -58,7 +58,7 @@ async def wallet2(): @pytest.mark.asyncio async def test_create_htlc_secret(wallet1: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) preimage = "00000000000000000000000000000000" preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() @@ -69,7 +69,7 @@ async def test_create_htlc_secret(wallet1: Wallet): @pytest.mark.asyncio async def test_htlc_split(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) preimage = "00000000000000000000000000000000" preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() @@ -82,7 +82,7 @@ async def test_htlc_split(wallet1: Wallet, wallet2: Wallet): @pytest.mark.asyncio async def test_htlc_redeem_with_preimage(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) preimage = "00000000000000000000000000000000" # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() @@ -96,7 +96,7 @@ async def test_htlc_redeem_with_preimage(wallet1: Wallet, wallet2: Wallet): @pytest.mark.asyncio async def test_htlc_redeem_with_wrong_preimage(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) preimage = "00000000000000000000000000000000" # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() @@ -114,7 +114,7 @@ async def test_htlc_redeem_with_wrong_preimage(wallet1: Wallet, wallet2: Wallet) @pytest.mark.asyncio async def test_htlc_redeem_with_no_signature(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) preimage = "00000000000000000000000000000000" pubkey_wallet1 = await wallet1.create_p2pk_pubkey() @@ -134,7 +134,7 @@ async def test_htlc_redeem_with_no_signature(wallet1: Wallet, wallet2: Wallet): @pytest.mark.asyncio async def test_htlc_redeem_with_wrong_signature(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) preimage = "00000000000000000000000000000000" pubkey_wallet1 = await wallet1.create_p2pk_pubkey() @@ -158,7 +158,7 @@ async def test_htlc_redeem_with_wrong_signature(wallet1: Wallet, wallet2: Wallet @pytest.mark.asyncio async def test_htlc_redeem_with_correct_signature(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) preimage = "00000000000000000000000000000000" pubkey_wallet1 = await wallet1.create_p2pk_pubkey() @@ -180,7 +180,7 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_correct_signature( wallet1: Wallet, wallet2: Wallet ): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) preimage = "00000000000000000000000000000000" pubkey_wallet1 = await wallet1.create_p2pk_pubkey() @@ -214,7 +214,7 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_wrong_signature( wallet1: Wallet, wallet2: Wallet ): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) preimage = "00000000000000000000000000000000" pubkey_wallet1 = await wallet1.create_p2pk_pubkey() diff --git a/tests/test_wallet_lightning.py b/tests/test_wallet_lightning.py index 0e89ac9..3b3cdd4 100644 --- a/tests/test_wallet_lightning.py +++ b/tests/test_wallet_lightning.py @@ -78,7 +78,7 @@ async def test_check_invoice_external(wallet: LightningWallet): assert invoice.checking_id status = await wallet.get_invoice_status(invoice.checking_id) assert not status.paid - pay_if_regtest(invoice.payment_request) + await pay_if_regtest(invoice.payment_request) status = await wallet.get_invoice_status(invoice.checking_id) assert status.paid @@ -113,7 +113,7 @@ async def test_pay_invoice_external(wallet: LightningWallet): invoice = await wallet.create_invoice(64) assert invoice.payment_request assert invoice.checking_id - pay_if_regtest(invoice.payment_request) + await pay_if_regtest(invoice.payment_request) status = await wallet.get_invoice_status(invoice.checking_id) assert status.paid assert wallet.available_balance >= 64 diff --git a/tests/test_wallet_p2pk.py b/tests/test_wallet_p2pk.py index e396f2a..ac236d5 100644 --- a/tests/test_wallet_p2pk.py +++ b/tests/test_wallet_p2pk.py @@ -60,7 +60,7 @@ async def wallet2(): @pytest.mark.asyncio async def test_create_p2pk_pubkey(wallet1: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey = await wallet1.create_p2pk_pubkey() PublicKey(bytes.fromhex(pubkey), raw=True) @@ -69,7 +69,7 @@ async def test_create_p2pk_pubkey(wallet1: Wallet): @pytest.mark.asyncio async def test_p2pk(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # p2pk test @@ -93,7 +93,7 @@ async def test_p2pk(wallet1: Wallet, wallet2: Wallet): @pytest.mark.asyncio async def test_p2pk_sig_all(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # p2pk test @@ -109,7 +109,7 @@ async def test_p2pk_sig_all(wallet1: Wallet, wallet2: Wallet): @pytest.mark.asyncio async def test_p2pk_receive_with_wrong_private_key(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side # sender side @@ -130,7 +130,7 @@ async def test_p2pk_short_locktime_receive_with_wrong_private_key( wallet1: Wallet, wallet2: Wallet ): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side # sender side @@ -156,7 +156,7 @@ async def test_p2pk_short_locktime_receive_with_wrong_private_key( @pytest.mark.asyncio async def test_p2pk_locktime_with_refund_pubkey(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side # sender side @@ -185,7 +185,7 @@ async def test_p2pk_locktime_with_refund_pubkey(wallet1: Wallet, wallet2: Wallet @pytest.mark.asyncio async def test_p2pk_locktime_with_wrong_refund_pubkey(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) await wallet2.create_p2pk_pubkey() # receiver side # sender side @@ -221,7 +221,7 @@ async def test_p2pk_locktime_with_second_refund_pubkey( wallet1: Wallet, wallet2: Wallet ): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet1 = await wallet1.create_p2pk_pubkey() # receiver side pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side @@ -253,7 +253,7 @@ async def test_p2pk_locktime_with_second_refund_pubkey( @pytest.mark.asyncio async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey() @@ -275,7 +275,7 @@ async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet): @pytest.mark.asyncio async def test_p2pk_multisig_duplicate_signature(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey() @@ -299,7 +299,7 @@ async def test_p2pk_multisig_duplicate_signature(wallet1: Wallet, wallet2: Walle @pytest.mark.asyncio async def test_p2pk_multisig_quorum_not_met_1_of_2(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey() @@ -320,7 +320,7 @@ async def test_p2pk_multisig_quorum_not_met_1_of_2(wallet1: Wallet, wallet2: Wal @pytest.mark.asyncio async def test_p2pk_multisig_quorum_not_met_2_of_3(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey() @@ -345,7 +345,7 @@ async def test_p2pk_multisig_quorum_not_met_2_of_3(wallet1: Wallet, wallet2: Wal @pytest.mark.asyncio async def test_p2pk_multisig_with_duplicate_publickey(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # p2pk test @@ -363,7 +363,7 @@ async def test_p2pk_multisig_with_wrong_first_private_key( wallet1: Wallet, wallet2: Wallet ): invoice = await wallet1.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) await wallet1.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey() diff --git a/tests/test_wallet_regtest.py b/tests/test_wallet_regtest.py index 4916fcf..3e6d31b 100644 --- a/tests/test_wallet_regtest.py +++ b/tests/test_wallet_regtest.py @@ -34,7 +34,7 @@ async def wallet(): async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger): # fill wallet invoice = await wallet.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet.mint(64, id=invoice.id) assert wallet.balance == 64 @@ -72,7 +72,7 @@ async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger): async def test_regtest_failed_quote(wallet: Wallet, ledger: Ledger): # fill wallet invoice = await wallet.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet.mint(64, id=invoice.id) assert wallet.balance == 64 diff --git a/tests/test_wallet_regtest_mpp.py b/tests/test_wallet_regtest_mpp.py index 0059c9d..4ece5eb 100644 --- a/tests/test_wallet_regtest_mpp.py +++ b/tests/test_wallet_regtest_mpp.py @@ -38,12 +38,12 @@ async def test_regtest_pay_mpp(wallet: Wallet, ledger: Ledger): # top up wallet twice so we have enough for two payments topup_invoice = await wallet.request_mint(128) - pay_if_regtest(topup_invoice.bolt11) + await pay_if_regtest(topup_invoice.bolt11) proofs1 = await wallet.mint(128, id=topup_invoice.id) assert wallet.balance == 128 topup_invoice = await wallet.request_mint(128) - pay_if_regtest(topup_invoice.bolt11) + await pay_if_regtest(topup_invoice.bolt11) proofs2 = await wallet.mint(128, id=topup_invoice.id) assert wallet.balance == 256 @@ -82,17 +82,17 @@ async def test_regtest_pay_mpp_incomplete_payment(wallet: Wallet, ledger: Ledger # top up wallet twice so we have enough for three payments topup_invoice = await wallet.request_mint(128) - pay_if_regtest(topup_invoice.bolt11) + await pay_if_regtest(topup_invoice.bolt11) proofs1 = await wallet.mint(128, id=topup_invoice.id) assert wallet.balance == 128 topup_invoice = await wallet.request_mint(128) - pay_if_regtest(topup_invoice.bolt11) + await pay_if_regtest(topup_invoice.bolt11) proofs2 = await wallet.mint(128, id=topup_invoice.id) assert wallet.balance == 256 topup_invoice = await wallet.request_mint(128) - pay_if_regtest(topup_invoice.bolt11) + await pay_if_regtest(topup_invoice.bolt11) proofs3 = await wallet.mint(128, id=topup_invoice.id) assert wallet.balance == 384 diff --git a/tests/test_wallet_restore.py b/tests/test_wallet_restore.py index 4a558d1..4b92ca7 100644 --- a/tests/test_wallet_restore.py +++ b/tests/test_wallet_restore.py @@ -157,7 +157,7 @@ async def test_generate_secrets_from_to(wallet3: Wallet): async def test_restore_wallet_after_mint(wallet3: Wallet): await reset_wallet_db(wallet3) invoice = await wallet3.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet3.mint(64, id=invoice.id) assert wallet3.balance == 64 await reset_wallet_db(wallet3) @@ -193,7 +193,7 @@ async def test_restore_wallet_after_split_to_send(wallet3: Wallet): await reset_wallet_db(wallet3) invoice = await wallet3.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet3.mint(64, id=invoice.id) assert wallet3.balance == 64 @@ -218,7 +218,7 @@ async def test_restore_wallet_after_send_and_receive(wallet3: Wallet, wallet2: W ) await reset_wallet_db(wallet3) invoice = await wallet3.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet3.mint(64, id=invoice.id) assert wallet3.balance == 64 @@ -261,7 +261,7 @@ async def test_restore_wallet_after_send_and_self_receive(wallet3: Wallet): await reset_wallet_db(wallet3) invoice = await wallet3.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet3.mint(64, id=invoice.id) assert wallet3.balance == 64 @@ -290,7 +290,7 @@ async def test_restore_wallet_after_send_twice( await reset_wallet_db(wallet3) invoice = await wallet3.request_mint(2) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet3.mint(2, id=invoice.id) box.add(wallet3.proofs) assert wallet3.balance == 2 @@ -349,7 +349,7 @@ async def test_restore_wallet_after_send_and_self_receive_nonquadratic_value( await reset_wallet_db(wallet3) invoice = await wallet3.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet3.mint(64, id=invoice.id) box.add(wallet3.proofs) assert wallet3.balance == 64 diff --git a/tests/test_wallet_subscription.py b/tests/test_wallet_subscription.py index d470592..ce77a16 100644 --- a/tests/test_wallet_subscription.py +++ b/tests/test_wallet_subscription.py @@ -47,7 +47,7 @@ async def test_wallet_subscription_mint(wallet: Wallet): 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) + await pay_if_regtest(invoice.bolt11) wait = settings.fakewallet_delay_incoming_payment or 2 await asyncio.sleep(wait + 2) @@ -70,7 +70,7 @@ async def test_wallet_subscription_swap(wallet: Wallet): pytest.skip("No websocket support") invoice = await wallet.request_mint(64) - pay_if_regtest(invoice.bolt11) + await pay_if_regtest(invoice.bolt11) await wallet.mint(64, id=invoice.id) triggered = False