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
This commit is contained in:
callebtc
2024-07-08 18:05:57 +02:00
committed by GitHub
parent af636db545
commit 6a0a370ba5
46 changed files with 1933 additions and 1157 deletions

View File

@@ -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 # 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'"] # 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 MINT_DATABASE=data/mint
# To use PostgreSQL, set the connection string
# MINT_DATABASE=postgres://cashu:cashu@localhost:5432/cashu
# Funding source backends # Funding source backends
# Supported: FakeWallet, LndRestWallet, CLNRestWallet, BlinkWallet, LNbitsWallet, StrikeWallet, CoreLightningRestWallet (deprecated) # Supported: FakeWallet, LndRestWallet, CLNRestWallet, BlinkWallet, LNbitsWallet, StrikeWallet, CoreLightningRestWallet (deprecated)

View File

@@ -23,5 +23,5 @@ runs:
cache: "poetry" cache: "poetry"
- name: Install dependencies - name: Install dependencies
run: | run: |
poetry install --extras pgsql poetry install
shell: bash shell: bash

View File

@@ -28,13 +28,14 @@ jobs:
regtest: regtest:
uses: ./.github/workflows/regtest.yml uses: ./.github/workflows/regtest.yml
strategy: strategy:
fail-fast: false
matrix: matrix:
python-version: ["3.10"] python-version: ["3.10"]
poetry-version: ["1.7.1"] poetry-version: ["1.7.1"]
backend-wallet-class: backend-wallet-class:
["LndRestWallet", "CLNRestWallet", "CoreLightningRestWallet", "LNbitsWallet"] ["LndRestWallet", "CLNRestWallet", "CoreLightningRestWallet", "LNbitsWallet"]
# mint-database: ["./test_data/test_mint", "postgres://cashu:cashu@localhost:5432/cashu"] mint-database: ["./test_data/test_mint", "postgres://cashu:cashu@localhost:5432/cashu"]
mint-database: ["./test_data/test_mint"] # mint-database: ["./test_data/test_mint"]
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
backend-wallet-class: ${{ matrix.backend-wallet-class }} backend-wallet-class: ${{ matrix.backend-wallet-class }}

View File

@@ -23,21 +23,13 @@ jobs:
regtest: regtest:
runs-on: ${{ inputs.os-version }} runs-on: ${{ inputs.os-version }}
timeout-minutes: 10 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: 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: actions/checkout@v3
- uses: ./.github/actions/prepare - uses: ./.github/actions/prepare
@@ -78,7 +70,7 @@ jobs:
MINT_CLNREST_URL: https://localhost:3010 MINT_CLNREST_URL: https://localhost:3010
MINT_CLNREST_RUNE: ./regtest/data/clightning-2/rune MINT_CLNREST_RUNE: ./regtest/data/clightning-2/rune
MINT_CLNREST_CERT: ./regtest/data/clightning-2/regtest/ca.pem MINT_CLNREST_CERT: ./regtest/data/clightning-2/regtest/ca.pem
MINT_CLNENABLE_MPP: false MINT_CLNREST_ENABLE_MPP: false
run: | run: |
sudo chmod -R 777 . sudo chmod -R 777 .
make test make test

View File

@@ -23,21 +23,12 @@ jobs:
poetry: poetry:
name: Run (db ${{ inputs.mint-database }}, deprecated api ${{ inputs.mint-only-deprecated }}) name: Run (db ${{ inputs.mint-database }}, deprecated api ${{ inputs.mint-only-deprecated }})
runs-on: ${{ inputs.os }} 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: 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 - name: Checkout repository and submodules
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: ./.github/actions/prepare - uses: ./.github/actions/prepare

View File

@@ -83,8 +83,6 @@ pyenv local 3.10.4
poetry install poetry install
``` ```
If you would like to use PostgreSQL as the mint database, use the command `poetry install --extras pgsql`.
#### Poetry: Update Cashu #### Poetry: Update Cashu
To update Cashu to the newest version enter To update Cashu to the newest version enter
```bash ```bash

View File

@@ -344,11 +344,13 @@ class MeltQuote(LedgerEvent):
def __setattr__(self, name, value): def __setattr__(self, name, value):
# an unpaid quote can only be set to pending or paid # an unpaid quote can only be set to pending or paid
if name == "state" and self.state == MeltQuoteState.unpaid: if name == "state" and self.state == MeltQuoteState.unpaid:
if value != MeltQuoteState.pending and value != MeltQuoteState.paid: if value not in [MeltQuoteState.pending, MeltQuoteState.paid]:
raise Exception("Cannot change state of an unpaid quote.") raise Exception(
f"Cannot change state of an unpaid melt quote to {value}."
)
# a paid quote can not be changed # a paid quote can not be changed
if name == "state" and self.state == MeltQuoteState.paid: 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) super().__setattr__(name, value)
@@ -415,18 +417,20 @@ class MintQuote(LedgerEvent):
# un unpaid quote can only be set to paid # un unpaid quote can only be set to paid
if name == "state" and self.state == MintQuoteState.unpaid: if name == "state" and self.state == MintQuoteState.unpaid:
if value != MintQuoteState.paid: 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 # a paid quote can only be set to pending or issued
if name == "state" and self.state == MintQuoteState.paid: if name == "state" and self.state == MintQuoteState.paid:
if value != MintQuoteState.pending and value != MintQuoteState.issued: 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 # a pending quote can only be set to paid or issued
if name == "state" and self.state == MintQuoteState.pending: if name == "state" and self.state == MintQuoteState.pending:
if value not in [MintQuoteState.paid, MintQuoteState.issued]: 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 # an issued quote cannot be changed
if name == "state" and self.state == MintQuoteState.issued: 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) super().__setattr__(name, value)

View File

@@ -1,14 +1,18 @@
import asyncio import asyncio
import datetime import datetime
import os import os
import re
import time import time
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import Optional, Union from typing import Optional, Union
from loguru import logger from loguru import logger
from sqlalchemy import create_engine from sqlalchemy import text
from sqlalchemy_aio.base import AsyncConnection from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY # type: ignore from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import NullPool, QueuePool
from cashu.core.settings import settings
POSTGRES = "POSTGRES" POSTGRES = "POSTGRES"
COCKROACH = "COCKROACH" COCKROACH = "COCKROACH"
@@ -57,9 +61,12 @@ class Compat:
return "BIGINT" return "BIGINT"
return "INT" return "INT"
def table_with_schema(self, table: str):
return f"{self.references_schema if self.schema else ''}{table}"
class Connection(Compat): 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.conn = conn
self.txn = txn self.txn = txn
self.type = typ self.type = typ
@@ -70,23 +77,23 @@ class Connection(Compat):
if self.type in {POSTGRES, COCKROACH}: if self.type in {POSTGRES, COCKROACH}:
query = query.replace("%", "%%") query = query.replace("%", "%%")
query = query.replace("?", "%s") 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) 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) result = await self.conn.execute(self.rewrite_query(query), values)
row = await result.fetchone() return result.fetchone()
await result.close()
return row
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) return await self.conn.execute(self.rewrite_query(query), values)
class Database(Compat): class Database(Compat):
_connection: Optional[AsyncSession] = None
def __init__(self, db_name: str, db_location: str): def __init__(self, db_name: str, db_location: str):
self.name = db_name self.name = db_name
self.db_location = db_location self.db_location = db_location
@@ -99,43 +106,20 @@ class Database(Compat):
self.type = COCKROACH self.type = COCKROACH
else: else:
self.type = POSTGRES self.type = POSTGRES
database_uri = database_uri.replace(
import psycopg2 # type: ignore "postgres://", "postgresql+asyncpg://"
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(
psycopg2.extensions.register_type( # type: ignore "postgresql://", "postgresql+asyncpg://"
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
),
) )
) # Disble prepared statement cache: https://docs.sqlalchemy.org/en/14/dialects/postgresql.html#prepared-statement-cache
database_uri += "?prepared_statement_cache_size=0"
# psycopg2.extensions.register_type(
# psycopg2.extensions.new_type(
# (1184, 1114), "TIMESTAMP2INT", _parse_timestamp
# )
# )
else: else:
if not os.path.exists(self.db_location): if not os.path.exists(self.db_location):
logger.info(f"Creating database directory: {self.db_location}") logger.info(f"Creating database directory: {self.db_location}")
os.makedirs(self.db_location) os.makedirs(self.db_location)
self.path = os.path.join(self.db_location, f"{self.name}.sqlite3") 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.type = SQLITE
self.schema = self.name self.schema = self.name
@@ -144,44 +128,167 @@ class Database(Compat):
else: else:
self.schema = None self.schema = None
self.engine = create_engine(database_uri, strategy=ASYNCIO_STRATEGY) kwargs = {}
self.lock = asyncio.Lock() 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 @asynccontextmanager
async def connect(self): async def get_connection(
await self.lock.acquire() self,
try: conn: Optional[Connection] = None,
async with self.engine.connect() as conn: # type: ignore lock_table: Optional[str] = None,
async with conn.begin() as txn: lock_select_statement: Optional[str] = None,
wconn = Connection(conn, txn, self.type, self.name, self.schema) lock_timeout: Optional[float] = None,
):
"""Either yield the existing database connection (passthrough) or create a new one.
if self.schema: Args:
if self.type in {POSTGRES, COCKROACH}: conn (Optional[Connection], optional): Connection object. Defaults to None.
await wconn.execute( lock_table (Optional[str], optional): Table to lock. Defaults to None.
f"CREATE SCHEMA IF NOT EXISTS {self.schema}" lock_select_statement (Optional[str], optional): Lock select statement. Defaults to None.
) lock_timeout (Optional[float], optional): Lock timeout. Defaults to None.
elif self.type == SQLITE:
await wconn.execute(
f"ATTACH '{self.path}' AS {self.schema}"
)
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 yield wconn
finally: logger.trace(
self.lock.release() 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: async with self.connect() as conn:
result = await conn.execute(query, values) 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: async with self.connect() as conn:
result = await conn.execute(query, values) result = await conn.execute(query, values)
row = await result.fetchone() return result.fetchone()
await result.close()
return row
async def execute(self, query: str, values: tuple = ()): async def execute(self, query: str, values: dict = {}):
async with self.connect() as conn: async with self.connect() as conn:
return await conn.execute(query, values) return await conn.execute(query, values)
@@ -189,60 +296,51 @@ class Database(Compat):
async def reuse_conn(self, conn: Connection): async def reuse_conn(self, conn: Connection):
yield conn 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) if self.type == POSTGRES:
def table_with_schema(db: Union[Database, Connection], table: str): return (
return f"{db.references_schema if db.schema else ''}{table}" 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 "<nothing>"
def timestamp_from_seconds(
def lock_table(db: Database, table: str) -> str: self, seconds: Union[int, float, None]
if db.type == POSTGRES: ) -> Union[str, None]:
return f"LOCK TABLE {table_with_schema(db, table)} IN EXCLUSIVE MODE;" if seconds is None:
elif db.type == COCKROACH: return None
return f"LOCK TABLE {table};" seconds = int(seconds)
elif db.type == SQLITE: if self.type in {POSTGRES, COCKROACH}:
return "BEGIN EXCLUSIVE TRANSACTION;" return datetime.datetime.fromtimestamp(seconds).strftime(
return "<nothing>" "%Y-%m-%d %H:%M:%S"
)
elif self.type == SQLITE:
def timestamp_from_seconds( return str(seconds)
db: Database, seconds: Union[int, float, None]
) -> Union[str, None]:
if seconds is None:
return None 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: def to_timestamp(self, timestamp_str: str) -> Union[str, datetime.datetime]:
timestamp = timestamp_from_seconds(db, time.time()) if not timestamp_str:
if timestamp is None: timestamp_str = self.timestamp_now_str()
raise Exception("Timestamp is None") if self.type in {POSTGRES, COCKROACH}:
return timestamp return datetime.datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S")
elif self.type == SQLITE:
return timestamp_str
@asynccontextmanager return "<nothing>"
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

View File

@@ -4,7 +4,7 @@ import time
from loguru import logger 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 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): async def set_migration_version(conn, db_name, version):
await conn.execute( await conn.execute(
f""" f"""
INSERT INTO {table_with_schema(db, 'dbversions')} (db, version) VALUES (?, ?) INSERT INTO {db.table_with_schema('dbversions')} (db, version) VALUES (:db, :version)
ON CONFLICT (db) DO UPDATE SET 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): async def run_migration(db, migrations_module):
@@ -89,20 +89,21 @@ async def migrate_databases(db: Database, migrations_module):
if conn.type == SQLITE: if conn.type == SQLITE:
exists = await conn.fetchone( exists = await conn.fetchone(
"SELECT * FROM sqlite_master WHERE type='table' AND" "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}: elif conn.type in {POSTGRES, COCKROACH}:
exists = await conn.fetchone( exists = await conn.fetchone(
"SELECT * FROM information_schema.tables WHERE table_name =" "SELECT * FROM information_schema.tables WHERE table_name ="
f" '{table_with_schema(db, 'dbversions')}'" f" '{db.table_with_schema('dbversions')}'"
) )
if not exists: if not exists:
await migrations_module.m000_create_migrations_table(conn) await migrations_module.m000_create_migrations_table(conn)
rows = await ( result = await conn.execute(
await conn.execute(f"SELECT * FROM {table_with_schema(db, 'dbversions')}") f"SELECT * FROM {db.table_with_schema('dbversions')}"
).fetchall() )
rows = result.all()
current_versions = {row["db"]: row["version"] for row in rows} current_versions = {row["db"]: row["version"] for row in rows}
matcher = re.compile(r"^m(\d\d\d)_") matcher = re.compile(r"^m(\d\d\d)_")
await run_migration(db, migrations_module) await run_migration(db, migrations_module)

View File

@@ -46,6 +46,7 @@ class EnvSettings(CashuSettings):
debug_profiling: bool = Field(default=False) debug_profiling: bool = Field(default=False)
debug_mint_only_deprecated: bool = Field(default=False) debug_mint_only_deprecated: bool = Field(default=False)
db_backup_path: Optional[str] = Field(default=None) db_backup_path: Optional[str] = Field(default=None)
db_connection_pool: bool = Field(default=True)
class MintSettings(CashuSettings): class MintSettings(CashuSettings):
@@ -131,8 +132,8 @@ class MintLimits(MintSettings):
class FakeWalletSettings(MintSettings): class FakeWalletSettings(MintSettings):
fakewallet_brr: bool = Field(default=True) fakewallet_brr: bool = Field(default=True)
fakewallet_delay_outgoing_payment: Optional[int] = Field(default=3) fakewallet_delay_outgoing_payment: Optional[float] = Field(default=3.0)
fakewallet_delay_incoming_payment: Optional[int] = Field(default=3) fakewallet_delay_incoming_payment: Optional[float] = Field(default=3.0)
fakewallet_stochastic_invoice: bool = Field(default=False) fakewallet_stochastic_invoice: bool = Field(default=False)
fakewallet_payment_state: Optional[bool] = Field(default=None) fakewallet_payment_state: Optional[bool] = Field(default=None)

View File

@@ -1,7 +1,6 @@
import asyncio import asyncio
import hashlib import hashlib
import math import math
import random
from datetime import datetime from datetime import datetime
from os import urandom from os import urandom
from typing import AsyncGenerator, Dict, List, Optional from typing import AsyncGenerator, Dict, List, Optional
@@ -57,8 +56,12 @@ class FakeWallet(LightningBackend):
async def status(self) -> StatusResponse: async def status(self) -> StatusResponse:
return StatusResponse(error_message=None, balance=1337) return StatusResponse(error_message=None, balance=1337)
async def mark_invoice_paid(self, invoice: Bolt11) -> None: async def mark_invoice_paid(self, invoice: Bolt11, delay=True) -> None:
if settings.fakewallet_delay_incoming_payment: 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) await asyncio.sleep(settings.fakewallet_delay_incoming_payment)
self.paid_invoices_incoming.append(invoice) self.paid_invoices_incoming.append(invoice)
await self.paid_invoices_queue.put(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: async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
paid = False await self.mark_invoice_paid(self.create_dummy_bolt11(checking_id), delay=False)
if settings.fakewallet_brr or ( paid_chceking_ids = [i.payment_hash for i in self.paid_invoices_incoming]
settings.fakewallet_stochastic_invoice and random.random() > 0.7 if checking_id in paid_chceking_ids:
):
paid = True 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) return PaymentStatus(paid=paid)
async def get_payment_status(self, _: str) -> PaymentStatus: async def get_payment_status(self, _: str) -> PaymentStatus:

View File

@@ -12,6 +12,7 @@ from ..core.logging import configure_logger
from ..core.settings import settings from ..core.settings import settings
from .router import router from .router import router
from .router_deprecated import router_deprecated from .router_deprecated import router_deprecated
from .startup import shutdown_mint as shutdown_mint_init
from .startup import start_mint_init from .startup import start_mint_init
if settings.debug_profiling: if settings.debug_profiling:
@@ -103,3 +104,8 @@ else:
@app.on_event("startup") @app.on_event("startup")
async def startup_mint(): async def startup_mint():
await start_mint_init() await start_mint_init()
@app.on_event("shutdown")
async def shutdown_mint():
await shutdown_mint_init()

View File

@@ -1,6 +1,6 @@
import json import json
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any, List, Optional from typing import Any, Dict, List, Optional
from ..core.base import ( from ..core.base import (
BlindedSignature, BlindedSignature,
@@ -12,9 +12,6 @@ from ..core.base import (
from ..core.db import ( from ..core.db import (
Connection, Connection,
Database, Database,
table_with_schema,
timestamp_from_seconds,
timestamp_now,
) )
@@ -266,19 +263,19 @@ class LedgerCrudSqlite(LedgerCrud):
) -> None: ) -> None:
await (conn or db).execute( await (conn or db).execute(
f""" f"""
INSERT INTO {table_with_schema(db, 'promises')} INSERT INTO {db.table_with_schema('promises')}
(amount, b_, c_, dleq_e, dleq_s, id, created) (amount, b_, c_, dleq_e, dleq_s, id, created)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (:amount, :b_, :c_, :dleq_e, :dleq_s, :id, :created)
""", """,
( {
amount, "amount": amount,
b_, "b_": b_,
c_, "c_": c_,
e, "dleq_e": e,
s, "dleq_s": s,
id, "id": id,
timestamp_now(db), "created": db.to_timestamp(db.timestamp_now_str()),
), },
) )
async def get_promise( async def get_promise(
@@ -290,10 +287,10 @@ class LedgerCrudSqlite(LedgerCrud):
) -> Optional[BlindedSignature]: ) -> Optional[BlindedSignature]:
row = await (conn or db).fetchone( row = await (conn or db).fetchone(
f""" f"""
SELECT * from {table_with_schema(db, 'promises')} SELECT * from {db.table_with_schema('promises')}
WHERE b_ = ? WHERE b_ = :b_
""", """,
(str(b_),), {"b_": str(b_)},
) )
return BlindedSignature.from_row(row) if row else None return BlindedSignature.from_row(row) if row else None
@@ -305,7 +302,7 @@ class LedgerCrudSqlite(LedgerCrud):
) -> List[Proof]: ) -> List[Proof]:
rows = await (conn or db).fetchall( rows = await (conn or db).fetchall(
f""" 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 [] return [Proof(**r) for r in rows] if rows else []
@@ -318,23 +315,22 @@ class LedgerCrudSqlite(LedgerCrud):
quote_id: Optional[str] = None, quote_id: Optional[str] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ) -> None:
# we add the proof and secret to the used list
await (conn or db).execute( await (conn or db).execute(
f""" 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) (amount, c, secret, y, id, witness, created, melt_quote)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (:amount, :c, :secret, :y, :id, :witness, :created, :melt_quote)
""", """,
( {
proof.amount, "amount": proof.amount,
proof.C, "c": proof.C,
proof.secret, "secret": proof.secret,
proof.Y, "y": proof.Y,
proof.id, "id": proof.id,
proof.witness, "witness": proof.witness,
timestamp_now(db), "created": db.to_timestamp(db.timestamp_now_str()),
quote_id, "melt_quote": quote_id,
), },
) )
async def get_all_melt_quotes_from_pending_proofs( async def get_all_melt_quotes_from_pending_proofs(
@@ -345,7 +341,7 @@ class LedgerCrudSqlite(LedgerCrud):
) -> List[MeltQuote]: ) -> List[MeltQuote]:
rows = await (conn or db).fetchall( rows = await (conn or db).fetchall(
f""" 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] return [MeltQuote.from_row(r) for r in rows]
@@ -359,10 +355,10 @@ class LedgerCrudSqlite(LedgerCrud):
) -> List[Proof]: ) -> List[Proof]:
rows = await (conn or db).fetchall( rows = await (conn or db).fetchall(
f""" f"""
SELECT * from {table_with_schema(db, 'proofs_pending')} SELECT * from {db.table_with_schema('proofs_pending')}
WHERE melt_quote = ? WHERE melt_quote = :quote_id
""", """,
(quote_id,), {"quote_id": quote_id},
) )
return [Proof(**r) for r in rows] return [Proof(**r) for r in rows]
@@ -373,13 +369,12 @@ class LedgerCrudSqlite(LedgerCrud):
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> List[Proof]: ) -> List[Proof]:
rows = await (conn or db).fetchall( query = f"""
f""" SELECT * from {db.table_with_schema('proofs_pending')}
SELECT * from {table_with_schema(db, 'proofs_pending')} WHERE y IN ({','.join([':y_' + str(i) for i in range(len(Ys))])})
WHERE y IN ({','.join(['?']*len(Ys))}) """
""", values = {f"y_{i}": Ys[i] for i in range(len(Ys))}
tuple(Ys), rows = await (conn or db).fetchall(query, values)
)
return [Proof(**r) for r in rows] return [Proof(**r) for r in rows]
async def set_proof_pending( async def set_proof_pending(
@@ -390,23 +385,22 @@ class LedgerCrudSqlite(LedgerCrud):
quote_id: Optional[str] = None, quote_id: Optional[str] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ) -> None:
# we add the proof and secret to the used list
await (conn or db).execute( await (conn or db).execute(
f""" 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) (amount, c, secret, y, id, witness, created, melt_quote)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (:amount, :c, :secret, :y, :id, :witness, :created, :melt_quote)
""", """,
( {
proof.amount, "amount": proof.amount,
proof.C, "c": proof.C,
proof.secret, "secret": proof.secret,
proof.Y, "y": proof.Y,
proof.id, "id": proof.id,
proof.witness, "witness": proof.witness,
timestamp_now(db), "created": db.to_timestamp(db.timestamp_now_str()),
quote_id, "melt_quote": quote_id,
), },
) )
async def unset_proof_pending( async def unset_proof_pending(
@@ -418,10 +412,10 @@ class LedgerCrudSqlite(LedgerCrud):
) -> None: ) -> None:
await (conn or db).execute( await (conn or db).execute(
f""" f"""
DELETE FROM {table_with_schema(db, 'proofs_pending')} DELETE FROM {db.table_with_schema('proofs_pending')}
WHERE secret = ? WHERE secret = :secret
""", """,
(proof.secret,), {"secret": proof.secret},
) )
async def store_mint_quote( async def store_mint_quote(
@@ -433,23 +427,27 @@ class LedgerCrudSqlite(LedgerCrud):
) -> None: ) -> None:
await (conn or db).execute( await (conn or db).execute(
f""" 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) (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": quote.quote,
quote.method, "method": quote.method,
quote.request, "request": quote.request,
quote.checking_id, "checking_id": quote.checking_id,
quote.unit, "unit": quote.unit,
quote.amount, "amount": quote.amount,
quote.issued, "issued": quote.issued,
quote.paid, "paid": quote.paid,
quote.state.name, "state": quote.state.name,
timestamp_from_seconds(db, quote.created_time), "created_time": db.to_timestamp(
timestamp_from_seconds(db, quote.paid_time), 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( async def get_mint_quote(
@@ -462,26 +460,25 @@ class LedgerCrudSqlite(LedgerCrud):
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> Optional[MintQuote]: ) -> Optional[MintQuote]:
clauses = [] clauses = []
values: List[Any] = [] values: Dict[str, Any] = {}
if quote_id: if quote_id:
clauses.append("quote = ?") clauses.append("quote = :quote_id")
values.append(quote_id) values["quote_id"] = quote_id
if checking_id: if checking_id:
clauses.append("checking_id = ?") clauses.append("checking_id = :checking_id")
values.append(checking_id) values["checking_id"] = checking_id
if request: if request:
clauses.append("request = ?") clauses.append("request = :request")
values.append(request) values["request"] = request
if not any(clauses): if not any(clauses):
raise ValueError("No search criteria") raise ValueError("No search criteria")
where = f"WHERE {' AND '.join(clauses)}" where = f"WHERE {' AND '.join(clauses)}"
row = await (conn or db).fetchone( row = await (conn or db).fetchone(
f""" f"""
SELECT * from {table_with_schema(db, 'mint_quotes')} SELECT * from {db.table_with_schema('mint_quotes')}
{where} {where}
""", """,
tuple(values), values,
) )
if row is None: if row is None:
return None return None
@@ -496,10 +493,10 @@ class LedgerCrudSqlite(LedgerCrud):
) -> Optional[MintQuote]: ) -> Optional[MintQuote]:
row = await (conn or db).fetchone( row = await (conn or db).fetchone(
f""" f"""
SELECT * from {table_with_schema(db, 'mint_quotes')} SELECT * from {db.table_with_schema('mint_quotes')}
WHERE request = ? WHERE request = :request
""", """,
(request,), {"request": request},
) )
return MintQuote.from_row(row) if row else None return MintQuote.from_row(row) if row else None
@@ -511,15 +508,16 @@ class LedgerCrudSqlite(LedgerCrud):
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ) -> None:
await (conn or db).execute( await (conn or db).execute(
f"UPDATE {table_with_schema(db, 'mint_quotes')} SET issued = ?, paid = ?," f"UPDATE {db.table_with_schema('mint_quotes')} SET issued = :issued, paid = :paid, state = :state, paid_time = :paid_time WHERE quote = :quote",
" state = ?, paid_time = ? WHERE quote = ?", {
( "issued": quote.issued,
quote.issued, "paid": quote.paid,
quote.paid, "state": quote.state.name,
quote.state.name, "paid_time": db.to_timestamp(
timestamp_from_seconds(db, quote.paid_time), db.timestamp_from_seconds(quote.paid_time) or ""
quote.quote, ),
), "quote": quote.quote,
},
) )
# async def update_mint_quote_paid( # async def update_mint_quote_paid(
@@ -531,7 +529,7 @@ class LedgerCrudSqlite(LedgerCrud):
# conn: Optional[Connection] = None, # conn: Optional[Connection] = None,
# ) -> None: # ) -> None:
# await (conn or db).execute( # 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 = ?", # " quote = ?",
# ( # (
# paid, # paid,
@@ -548,27 +546,33 @@ class LedgerCrudSqlite(LedgerCrud):
) -> None: ) -> None:
await (conn or db).execute( await (conn or db).execute(
f""" 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) (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": quote.quote,
quote.method, "method": quote.method,
quote.request, "request": quote.request,
quote.checking_id, "checking_id": quote.checking_id,
quote.unit, "unit": quote.unit,
quote.amount, "amount": quote.amount,
quote.fee_reserve or 0, "fee_reserve": quote.fee_reserve or 0,
quote.paid, "paid": quote.paid,
quote.state.name, "state": quote.state.name,
timestamp_from_seconds(db, quote.created_time), "created_time": db.to_timestamp(
timestamp_from_seconds(db, quote.paid_time), db.timestamp_from_seconds(quote.created_time) or ""
quote.fee_paid, ),
quote.payment_preimage, "paid_time": db.to_timestamp(
json.dumps(quote.change) if quote.change else None, db.timestamp_from_seconds(quote.paid_time) or ""
timestamp_from_seconds(db, quote.expiry), ),
), "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( async def get_melt_quote(
@@ -581,26 +585,25 @@ class LedgerCrudSqlite(LedgerCrud):
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> Optional[MeltQuote]: ) -> Optional[MeltQuote]:
clauses = [] clauses = []
values: List[Any] = [] values: Dict[str, Any] = {}
if quote_id: if quote_id:
clauses.append("quote = ?") clauses.append("quote = :quote_id")
values.append(quote_id) values["quote_id"] = quote_id
if checking_id: if checking_id:
clauses.append("checking_id = ?") clauses.append("checking_id = :checking_id")
values.append(checking_id) values["checking_id"] = checking_id
if request: if request:
clauses.append("request = ?") clauses.append("request = :request")
values.append(request) values["request"] = request
if not any(clauses): if not any(clauses):
raise ValueError("No search criteria") raise ValueError("No search criteria")
where = f"WHERE {' AND '.join(clauses)}" where = f"WHERE {' AND '.join(clauses)}"
row = await (conn or db).fetchone( row = await (conn or db).fetchone(
f""" f"""
SELECT * from {table_with_schema(db, 'melt_quotes')} SELECT * from {db.table_with_schema('melt_quotes')}
{where} {where}
""", """,
tuple(values), values,
) )
if row is None: if row is None:
return None return None
@@ -614,17 +617,22 @@ class LedgerCrudSqlite(LedgerCrud):
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ) -> None:
await (conn or db).execute( await (conn or db).execute(
f"UPDATE {table_with_schema(db, 'melt_quotes')} SET paid = ?, state = ?," f"""
" fee_paid = ?, paid_time = ?, proof = ?, change = ? WHERE quote = ?", 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
( """,
quote.paid, {
quote.state.name, "paid": quote.paid,
quote.fee_paid, "state": quote.state.name,
timestamp_from_seconds(db, quote.paid_time), "fee_paid": quote.fee_paid,
quote.payment_preimage, "paid_time": db.to_timestamp(
json.dumps([s.dict() for s in quote.change]) if quote.change else None, db.timestamp_from_seconds(quote.paid_time) or ""
quote.quote, ),
), "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( async def store_keyset(
@@ -634,26 +642,30 @@ class LedgerCrudSqlite(LedgerCrud):
keyset: MintKeyset, keyset: MintKeyset,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ) -> None:
await (conn or db).execute( # type: ignore await (conn or db).execute(
f""" 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) (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, "id": keyset.id,
keyset.seed, "seed": keyset.seed,
keyset.encrypted_seed, "encrypted_seed": keyset.encrypted_seed,
keyset.seed_encryption_method, "seed_encryption_method": keyset.seed_encryption_method,
keyset.derivation_path, "derivation_path": keyset.derivation_path,
keyset.valid_from or timestamp_now(db), "valid_from": db.to_timestamp(
keyset.valid_to or timestamp_now(db), keyset.valid_from or db.timestamp_now_str()
keyset.first_seen or timestamp_now(db), ),
True, "valid_to": db.to_timestamp(keyset.valid_to or db.timestamp_now_str()),
keyset.version, "first_seen": db.to_timestamp(
keyset.unit.name, keyset.first_seen or db.timestamp_now_str()
keyset.input_fee_ppk, ),
), "active": True,
"version": keyset.version,
"unit": keyset.unit.name,
"input_fee_ppk": keyset.input_fee_ppk,
},
) )
async def get_balance( async def get_balance(
@@ -663,7 +675,7 @@ class LedgerCrudSqlite(LedgerCrud):
) -> int: ) -> int:
row = await (conn or db).fetchone( row = await (conn or db).fetchone(
f""" f"""
SELECT * from {table_with_schema(db, 'balance')} SELECT * from {db.table_with_schema('balance')}
""" """
) )
assert row, "Balance not found" assert row, "Balance not found"
@@ -681,32 +693,32 @@ class LedgerCrudSqlite(LedgerCrud):
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> List[MintKeyset]: ) -> List[MintKeyset]:
clauses = [] clauses = []
values: List[Any] = [] values: Dict = {}
if active is not None: if active is not None:
clauses.append("active = ?") clauses.append("active = :active")
values.append(active) values["active"] = active
if id is not None: if id is not None:
clauses.append("id = ?") clauses.append("id = :id")
values.append(id) values["id"] = id
if derivation_path is not None: if derivation_path is not None:
clauses.append("derivation_path = ?") clauses.append("derivation_path = :derivation_path")
values.append(derivation_path) values["derivation_path"] = derivation_path
if seed is not None: if seed is not None:
clauses.append("seed = ?") clauses.append("seed = :seed")
values.append(seed) values["seed"] = seed
if unit is not None: if unit is not None:
clauses.append("unit = ?") clauses.append("unit = :unit")
values.append(unit) values["unit"] = unit
where = "" where = ""
if clauses: if clauses:
where = f"WHERE {' AND '.join(clauses)}" where = f"WHERE {' AND '.join(clauses)}"
rows = await (conn or db).fetchall( # type: ignore rows = await (conn or db).fetchall( # type: ignore
f""" f"""
SELECT * from {table_with_schema(db, 'keysets')} SELECT * from {db.table_with_schema('keysets')}
{where} {where}
""", """,
tuple(values), values,
) )
return [MintKeyset(**row) for row in rows] return [MintKeyset(**row) for row in rows]
@@ -719,9 +731,9 @@ class LedgerCrudSqlite(LedgerCrud):
) -> Optional[Proof]: ) -> Optional[Proof]:
row = await (conn or db).fetchone( row = await (conn or db).fetchone(
f""" f"""
SELECT * from {table_with_schema(db, 'proofs_used')} SELECT * from {db.table_with_schema('proofs_used')}
WHERE y = ? WHERE y = :y
""", """,
(Y,), {"y": Y},
) )
return Proof(**row) if row else None return Proof(**row) if row else None

View File

@@ -1,7 +1,7 @@
from typing import Dict, List from typing import Dict, List, Optional
from ...core.base import Proof, ProofSpentState, ProofState from ...core.base import Proof, ProofSpentState, ProofState
from ...core.db import Database from ...core.db import Connection, Database
from ..crud import LedgerCrud from ..crud import LedgerCrud
@@ -13,28 +13,37 @@ class DbReadHelper:
self.db = db self.db = db
self.crud = crud 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. """Returns a dictionary of only those proofs that are pending.
The key is the Y=h2c(secret) and the value is the proof. The key is the Y=h2c(secret) and the value is the proof.
""" """
proofs_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} proofs_pending_dict = {p.Y: p for p in proofs_pending}
return proofs_pending_dict 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. """Returns a dictionary of all proofs that are spent.
The key is the Y=h2c(secret) and the value is the proof. The key is the Y=h2c(secret) and the value is the proof.
""" """
proofs_spent_dict: Dict[str, Proof] = {} proofs_spent_dict: Dict[str, Proof] = {}
# check used secrets in database # 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: for Y in Ys:
spent_proof = await self.crud.get_proof_used(db=self.db, Y=Y, conn=conn) spent_proof = await self.crud.get_proof_used(db=self.db, Y=Y, conn=conn)
if spent_proof: if spent_proof:
proofs_spent_dict[Y] = spent_proof proofs_spent_dict[Y] = spent_proof
return proofs_spent_dict 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. """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. 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) List[bool]: List of which proof are pending (True if pending, else False)
""" """
states: List[ProofState] = [] states: List[ProofState] = []
proofs_spent = await self._get_proofs_spent(Ys) async with self.db.get_connection(conn) as conn:
proofs_pending = await self._get_proofs_pending(Ys) proofs_spent = await self._get_proofs_spent(Ys, conn)
for Y in Ys: proofs_pending = await self._get_proofs_pending(Ys, conn)
if Y not in proofs_spent and Y not in proofs_pending: for Y in Ys:
states.append(ProofState(Y=Y, state=ProofSpentState.unspent)) if Y not in proofs_spent and Y not in proofs_pending:
elif Y not in proofs_spent and Y in proofs_pending: states.append(ProofState(Y=Y, state=ProofSpentState.unspent))
states.append(ProofState(Y=Y, state=ProofSpentState.pending)) elif Y not in proofs_spent and Y in proofs_pending:
else: states.append(ProofState(Y=Y, state=ProofSpentState.pending))
states.append( else:
ProofState( states.append(
Y=Y, ProofState(
state=ProofSpentState.spent, Y=Y,
witness=proofs_spent[Y].witness, state=ProofSpentState.spent,
witness=proofs_spent[Y].witness,
)
) )
)
return states return states

View File

@@ -1,10 +1,18 @@
import asyncio import random
from typing import List, Optional from typing import List, Optional, Union
from loguru import logger from loguru import logger
from ...core.base import Proof, ProofSpentState, ProofState from ...core.base import (
from ...core.db import Connection, Database, get_db_connection MeltQuote,
MeltQuoteState,
MintQuote,
MintQuoteState,
Proof,
ProofSpentState,
ProofState,
)
from ...core.db import Connection, Database
from ...core.errors import ( from ...core.errors import (
TransactionError, TransactionError,
) )
@@ -16,9 +24,6 @@ class DbWriteHelper:
db: Database db: Database
crud: LedgerCrud crud: LedgerCrud
events: LedgerEventManager events: LedgerEventManager
proofs_pending_lock: asyncio.Lock = (
asyncio.Lock()
) # holds locks for proofs_pending database
def __init__( def __init__(
self, db: Database, crud: LedgerCrud, events: LedgerEventManager self, db: Database, crud: LedgerCrud, events: LedgerEventManager
@@ -41,20 +46,28 @@ class DbWriteHelper:
Exception: At least one proof already in pending table. Exception: At least one proof already in pending table.
""" """
# first we check whether these proofs are pending already # first we check whether these proofs are pending already
async with self.proofs_pending_lock: random_id = random.randint(0, 1000000)
async with get_db_connection(self.db) as conn: 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) await self._validate_proofs_pending(proofs, conn)
try: for p in proofs:
for p in proofs: logger.trace(f"crud: setting proof {p.Y} as PENDING")
await self.crud.set_proof_pending( await self.crud.set_proof_pending(
proof=p, db=self.db, quote_id=quote_id, conn=conn proof=p, db=self.db, quote_id=quote_id, conn=conn
) )
await self.events.submit( logger.trace(f"crud: set proof {p.Y} as PENDING")
ProofState(Y=p.Y, state=ProofSpentState.pending) except Exception as e:
) logger.error(f"Failed to set proofs pending: {e}")
except Exception as e: raise TransactionError(f"Failed to set proofs pending: {str(e)}")
logger.error(f"Failed to set proofs pending: {e}") logger.trace("_set_proofs_pending released lock")
raise TransactionError("Failed to set proofs pending.") 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: async def _unset_proofs_pending(self, proofs: List[Proof], spent=True) -> None:
"""Deletes proofs from pending table. """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 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). by the _invalidate_proofs function when the proofs are spent).
""" """
async with self.proofs_pending_lock: async with self.db.get_connection() as conn:
async with get_db_connection(self.db) as conn: for p in proofs:
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) await self.crud.unset_proof_pending(proof=p, db=self.db, conn=conn)
if not spent:
await self.events.submit( if not spent:
ProofState(Y=p.Y, state=ProofSpentState.unspent) for p in proofs:
) await self.events.submit(
ProofState(Y=p.Y, state=ProofSpentState.unspent)
)
async def _validate_proofs_pending( async def _validate_proofs_pending(
self, proofs: List[Proof], conn: Optional[Connection] = None self, proofs: List[Proof], conn: Optional[Connection] = None
@@ -86,12 +101,120 @@ class DbWriteHelper:
Raises: Raises:
Exception: At least one of the proofs is in the pending table. Exception: At least one of the proofs is in the pending table.
""" """
if not ( logger.trace("crud: validating proofs pending")
len( pending_proofs = await self.crud.get_proofs_pending(
await self.crud.get_proofs_pending( Ys=[p.Y for p in proofs], db=self.db, conn=conn
Ys=[p.Y for p in proofs], db=self.db, conn=conn )
) if not (len(pending_proofs) == 0):
)
== 0
):
raise TransactionError("proofs are pending.") 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

View File

@@ -8,7 +8,7 @@ except ImportError:
import asyncio import asyncio
from functools import wraps 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.migrations import migrate_databases
from cashu.core.settings import settings from cashu.core.settings import settings
from cashu.mint import migrations from cashu.mint import migrations
@@ -95,7 +95,7 @@ async def migrate(no_dry_run):
# get all keysets # get all keysets
async with ledger.db.connect() as conn: async with ledger.db.connect() as conn:
rows = await conn.fetchall( 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" " NULL"
) )
click.echo(f"Found {len(rows)} keysets in database.") 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: for keyset_dict in keysets_migrate:
click.echo(f"Updating keyset {keyset_dict['id']}") click.echo(f"Updating keyset {keyset_dict['id']}")
await conn.execute( 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 = ?", " encrypted_seed = ?, seed_encryption_method = ? WHERE id = ?",
( (
keyset_dict["encrypted_seed"], keyset_dict["encrypted_seed"],

View File

@@ -28,7 +28,7 @@ from ..core.crypto.keys import (
random_hash, random_hash,
) )
from ..core.crypto.secp import PrivateKey, PublicKey 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 ( from ..core.errors import (
CashuError, CashuError,
KeysetError, KeysetError,
@@ -68,6 +68,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
keysets: Dict[str, MintKeyset] = {} keysets: Dict[str, MintKeyset] = {}
events = LedgerEventManager() events = LedgerEventManager()
db_read: DbReadHelper db_read: DbReadHelper
invoice_listener_tasks: List[asyncio.Task] = []
def __init__( def __init__(
self, self,
@@ -106,7 +107,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
async def startup_ledger(self): async def startup_ledger(self):
await self._startup_ledger() await self._startup_ledger()
await self._check_pending_proofs_and_melt_quotes() await self._check_pending_proofs_and_melt_quotes()
await self.dispatch_listeners() self.invoice_listener_tasks = await self.dispatch_listeners()
async def _startup_ledger(self): async def _startup_ledger(self):
await self.init_keysets() await self.init_keysets()
@@ -132,6 +133,11 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
logger.info(f"Data dir: {settings.cashu_dir}") 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): async def _check_pending_proofs_and_melt_quotes(self):
"""Startup routine that checks all pending proofs for their melt state and either invalidates """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. 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) await self.db_write._unset_proofs_pending(pending_proofs)
elif payment.failed: elif payment.failed:
logger.info(f"Melt quote {quote.quote} state: failed") logger.info(f"Melt quote {quote.quote} state: failed")
# unset pending # unset pending
await self.db_write._unset_proofs_pending(pending_proofs, spent=False) await self.db_write._unset_proofs_pending(pending_proofs, spent=False)
elif payment.pending: elif payment.pending:
@@ -298,9 +303,10 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
proofs (List[Proof]): Proofs to add to known secret table. proofs (List[Proof]): Proofs to add to known secret table.
conn: (Optional[Connection], optional): Database connection to reuse. Will create a new one if not given. Defaults to None. conn: (Optional[Connection], optional): Database connection to reuse. Will create a new one if not given. Defaults to None.
""" """
async with get_db_connection(self.db, conn) as conn: async with self.db.get_connection(conn) as conn:
# store in db # store in db
for p in proofs: for p in proofs:
logger.trace(f"Invalidating proof {p.Y}")
await self.crud.invalidate_proof( await self.crud.invalidate_proof(
proof=p, db=self.db, quote_id=quote_id, conn=conn proof=p, db=self.db, quote_id=quote_id, conn=conn
) )
@@ -469,12 +475,26 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
unit unit
].get_invoice_status(quote.checking_id) ].get_invoice_status(quote.checking_id)
if status.paid: if status.paid:
logger.trace(f"Setting quote {quote_id} as paid") # change state to paid in one transaction, it could have been marked paid
quote.paid = True # by the invoice listener in the mean time
quote.state = MintQuoteState.paid async with self.db.get_connection(
quote.paid_time = int(time.time()) lock_table="mint_quotes",
await self.crud.update_mint_quote(quote=quote, db=self.db) lock_select_statement=f"quote='{quote_id}'",
await self.events.submit(quote) ) 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 return quote
@@ -501,46 +521,39 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
Returns: Returns:
List[BlindedSignature]: Signatures on the outputs. List[BlindedSignature]: Signatures on the outputs.
""" """
logger.trace("called mint")
await self._verify_outputs(outputs) await self._verify_outputs(outputs)
sum_amount_outputs = sum([b.amount for b in 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 # 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 output_unit = self.keysets[outputs[0].id].unit
self.locks[quote_id] = ( quote = await self.get_mint_quote(quote_id)
self.locks.get(quote_id) or asyncio.Lock() if quote.state == MintQuoteState.pending:
) # create a new lock if it doesn't exist raise TransactionError("Mint quote already pending.")
async with self.locks[quote_id]: if quote.state == MintQuoteState.issued:
quote = await self.get_mint_quote(quote_id=quote_id) raise TransactionError("Mint quote already issued.")
if not quote.paid: if not quote.state == MintQuoteState.paid:
raise QuoteNotPaidError() raise QuoteNotPaidError()
if quote.issued: previous_state = quote.state
raise TransactionError("quote already issued") await self.db_write._set_mint_quote_pending(quote_id=quote_id)
try:
if not quote.state == MintQuoteState.paid:
raise QuoteNotPaidError()
if quote.state == MintQuoteState.issued:
raise TransactionError("quote already issued")
if not quote.unit == output_unit.name: if not quote.unit == output_unit.name:
raise TransactionError("quote unit does not match output unit") raise TransactionError("quote unit does not match output unit")
if not quote.amount == sum_amount_outputs: if not quote.amount == sum_amount_outputs:
raise TransactionError("amount to mint does not match quote amount") raise TransactionError("amount to mint does not match quote amount")
if quote.expiry and quote.expiry > int(time.time()): if quote.expiry and quote.expiry > int(time.time()):
raise TransactionError("quote expired") raise TransactionError("quote expired")
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) 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.db_write._unset_mint_quote_pending(
await self.events.submit(quote) quote_id=quote_id, state=MintQuoteState.issued
)
del self.locks[quote_id]
return promises return promises
def create_internal_melt_quote( def create_internal_melt_quote(
@@ -567,7 +580,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
if mint_quote.state == MintQuoteState.issued: if mint_quote.state == MintQuoteState.issued:
raise TransactionError("mint quote already issued") raise TransactionError("mint quote already issued")
if mint_quote.state != MintQuoteState.unpaid: 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: if not mint_quote.checking_id:
raise TransactionError("mint quote has no 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") raise TransactionError("mint quote already issued")
if mint_quote.state != MintQuoteState.unpaid: if mint_quote.state != MintQuoteState.unpaid:
raise TransactionError("mint quote already paid") raise TransactionError("mint quote is not unpaid")
logger.info( logger.info(
f"Settling bolt11 payment internally: {melt_quote.quote} ->" 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.state = MintQuoteState.paid
mint_quote.paid_time = melt_quote.paid_time 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_melt_quote(quote=melt_quote, db=self.db, conn=conn)
await self.crud.update_mint_quote(quote=mint_quote, db=self.db, conn=conn) await self.crud.update_mint_quote(quote=mint_quote, db=self.db, conn=conn)
@@ -970,22 +983,13 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
Tuple[List[BlindSignature],List[BlindSignature]]: Promises on both sides of the split. Tuple[List[BlindSignature],List[BlindSignature]]: Promises on both sides of the split.
""" """
logger.trace("split called") 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 # verify spending inputs, outputs, and spending conditions
await self.verify_inputs_and_outputs(proofs=proofs, outputs=outputs) await self.verify_inputs_and_outputs(proofs=proofs, outputs=outputs)
await self.db_write._set_proofs_pending(proofs) await self.db_write._set_proofs_pending(proofs)
try: try:
# Mark proofs as used and prepare new promises async with self.db.get_connection(lock_table="proofs_pending") as conn:
async with get_db_connection(self.db) as conn:
# we do this in a single db transaction
await self._invalidate_proofs(proofs=proofs, conn=conn) await self._invalidate_proofs(proofs=proofs, conn=conn)
promises = await self._generate_promises(outputs, keyset, conn) promises = await self._generate_promises(outputs, keyset, conn)
except Exception as e: except Exception as e:
logger.trace(f"split failed: {e}") logger.trace(f"split failed: {e}")
raise e raise e
@@ -1001,7 +1005,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
) -> Tuple[List[BlindedMessage], List[BlindedSignature]]: ) -> Tuple[List[BlindedMessage], List[BlindedSignature]]:
signatures: List[BlindedSignature] = [] signatures: List[BlindedSignature] = []
return_outputs: List[BlindedMessage] = [] return_outputs: List[BlindedMessage] = []
async with get_db_connection(self.db) as conn: async with self.db.get_connection() as conn:
for output in outputs: for output in outputs:
logger.trace(f"looking for promise: {output}") logger.trace(f"looking for promise: {output}")
promise = await self.crud.get_promise( promise = await self.crud.get_promise(
@@ -1057,7 +1061,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
keyset = keyset or self.keyset keyset = keyset or self.keyset
signatures = [] signatures = []
async with get_db_connection(self.db, conn) as conn: async with self.db.get_connection(conn) as conn:
for promise in promises: for promise in promises:
keyset_id, B_, amount, C_, e, s = promise keyset_id, B_, amount, C_, e, s = promise
logger.trace(f"crud: _generate_promise storing promise for {amount}") logger.trace(f"crud: _generate_promise storing promise for {amount}")

View File

@@ -2,14 +2,14 @@ import copy
from ..core.base import MintKeyset, Proof from ..core.base import MintKeyset, Proof
from ..core.crypto.keys import derive_keyset_id, derive_keyset_id_deprecated 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 from ..core.settings import settings
async def m000_create_migrations_table(conn: Connection): async def m000_create_migrations_table(conn: Connection):
await conn.execute( await conn.execute(
f""" 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, db TEXT PRIMARY KEY,
version INT NOT NULL version INT NOT NULL
) )
@@ -21,7 +21,7 @@ async def m001_initial(db: Database):
async with db.connect() as conn: async with db.connect() as conn:
await conn.execute( await conn.execute(
f""" 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, amount {db.big_int} NOT NULL,
b_b TEXT NOT NULL, b_b TEXT NOT NULL,
c_b TEXT NOT NULL, c_b TEXT NOT NULL,
@@ -34,7 +34,7 @@ async def m001_initial(db: Database):
await conn.execute( await conn.execute(
f""" 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, amount {db.big_int} NOT NULL,
c TEXT NOT NULL, c TEXT NOT NULL,
secret TEXT NOT NULL, secret TEXT NOT NULL,
@@ -47,7 +47,7 @@ async def m001_initial(db: Database):
await conn.execute( await conn.execute(
f""" 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, amount {db.big_int} NOT NULL,
pr TEXT NOT NULL, pr TEXT NOT NULL,
hash 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): 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 {db.table_with_schema('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_issued')}")
await conn.execute( 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): async def create_balance_views(db: Database, conn: Connection):
await conn.execute( await conn.execute(
f""" 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 COALESCE(SUM(s), 0) AS balance FROM (
SELECT SUM(amount) AS s SELECT SUM(amount) AS s
FROM {table_with_schema(db, 'promises')} FROM {db.table_with_schema('promises')}
WHERE amount > 0 WHERE amount > 0
) AS balance_issued; ) AS balance_issued;
""" """
@@ -82,10 +82,10 @@ async def create_balance_views(db: Database, conn: Connection):
await conn.execute( await conn.execute(
f""" 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 COALESCE(SUM(s), 0) AS balance FROM (
SELECT SUM(amount) AS s SELECT SUM(amount) AS s
FROM {table_with_schema(db, 'proofs_used')} FROM {db.table_with_schema('proofs_used')}
WHERE amount > 0 WHERE amount > 0
) AS balance_redeemed; ) AS balance_redeemed;
""" """
@@ -93,11 +93,11 @@ async def create_balance_views(db: Database, conn: Connection):
await conn.execute( await conn.execute(
f""" f"""
CREATE VIEW {table_with_schema(db, 'balance')} AS CREATE VIEW {db.table_with_schema('balance')} AS
SELECT s_issued - s_used FROM ( SELECT s_issued - s_used FROM (
SELECT bi.balance AS s_issued, bu.balance AS s_used SELECT bi.balance AS s_issued, bu.balance AS s_used
FROM {table_with_schema(db, 'balance_issued')} bi FROM {db.table_with_schema('balance_issued')} bi
CROSS JOIN {table_with_schema(db, 'balance_redeemed')} bu CROSS JOIN {db.table_with_schema('balance_redeemed')} bu
) AS balance; ) AS balance;
""" """
) )
@@ -115,7 +115,7 @@ async def m003_mint_keysets(db: Database):
async with db.connect() as conn: async with db.connect() as conn:
await conn.execute( await conn.execute(
f""" 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, id TEXT NOT NULL,
derivation_path TEXT, derivation_path TEXT,
valid_from TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, valid_from TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
@@ -130,7 +130,7 @@ async def m003_mint_keysets(db: Database):
) )
await conn.execute( await conn.execute(
f""" 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, id TEXT NOT NULL,
amount {db.big_int} NOT NULL, amount {db.big_int} NOT NULL,
pubkey TEXT NOT NULL, pubkey TEXT NOT NULL,
@@ -148,7 +148,7 @@ async def m004_keysets_add_version(db: Database):
""" """
async with db.connect() as conn: async with db.connect() as conn:
await conn.execute( 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: async with db.connect() as conn:
await conn.execute( await conn.execute(
f""" 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, amount {db.big_int} NOT NULL,
c TEXT NOT NULL, c TEXT NOT NULL,
secret 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: async with db.connect() as conn:
await conn.execute( 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" " TEXT"
) )
await conn.execute( 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: async with db.connect() as conn:
await conn.execute( 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( 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( 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: async with db.connect() as conn:
await conn.execute( 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( 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: async with db.connect() as conn:
# rename column pr to bolt11 # rename column pr to bolt11
await conn.execute( 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" " bolt11"
) )
# rename column hash to payment_hash # rename column hash to payment_hash
await conn.execute( 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( 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( await conn.execute(
"CREATE INDEX IF NOT EXISTS" "CREATE INDEX IF NOT EXISTS"
" proofs_used_secret_idx ON" " 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"] tables = ["invoices", "promises", "proofs_used", "proofs_pending"]
for table in tables: for table in tables:
await conn.execute( 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" " TIMESTAMP"
) )
await conn.execute( await conn.execute(
f"UPDATE {table_with_schema(db, table)} SET created =" f"UPDATE {db.table_with_schema(table)} SET created ="
f" '{timestamp_now(db)}'" f" '{db.timestamp_now_str()}'"
) )
# add column "witness" to table proofs_used # add column "witness" to table proofs_used
await conn.execute( 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" " TEXT"
) )
# add columns "seed" and "unit" to table keysets # add columns "seed" and "unit" to table keysets
await conn.execute( 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( 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 # fill columns "seed" and "unit" in table keysets
await conn.execute( 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'" f" '{settings.mint_private_key}', unit = 'sat'"
) )
await conn.execute( await conn.execute(
f""" 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, quote TEXT NOT NULL,
method TEXT NOT NULL, method TEXT NOT NULL,
request TEXT NOT NULL, request TEXT NOT NULL,
@@ -300,7 +300,7 @@ async def m011_add_quote_tables(db: Database):
await conn.execute( await conn.execute(
f""" 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, quote TEXT NOT NULL,
method TEXT NOT NULL, method TEXT NOT NULL,
request TEXT NOT NULL, request TEXT NOT NULL,
@@ -321,15 +321,15 @@ async def m011_add_quote_tables(db: Database):
) )
await conn.execute( 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," " request, checking_id, unit, amount, paid, issued, created_time,"
" paid_time) SELECT id, 'bolt11', bolt11, COALESCE(payment_hash, 'None')," " paid_time) SELECT id, 'bolt11', bolt11, COALESCE(payment_hash, 'None'),"
f" 'sat', amount, False, issued, COALESCE(created, '{timestamp_now(db)}')," f" 'sat', amount, False, issued, COALESCE(created, '{db.timestamp_now_str()}'),"
f" NULL FROM {table_with_schema(db, 'invoices')} " f" NULL FROM {db.table_with_schema('invoices')} "
) )
# drop table 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): 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 # and copy the data from keysets_old to keysets, then drop keysets_old
async with db.connect() as conn: async with db.connect() as conn:
await conn.execute( 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( await conn.execute(
f"CREATE TABLE {table_with_schema(db, 'keysets_old')} AS" f"CREATE TABLE {db.table_with_schema('keysets_old')} AS"
f" SELECT * FROM {table_with_schema(db, 'keysets')}" 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( await conn.execute(
f""" 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, id TEXT NOT NULL,
derivation_path TEXT, derivation_path TEXT,
seed TEXT, seed TEXT,
@@ -364,13 +364,13 @@ async def m012_keysets_uniqueness_with_seed(db: Database):
""" """
) )
await conn.execute( 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," " derivation_path, valid_from, valid_to, first_seen,"
" active, version, seed, unit) SELECT id, derivation_path," " active, version, seed, unit) SELECT id, derivation_path,"
" valid_from, valid_to, first_seen, active, version, seed," " 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): 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 # with the same columns but with a unique constraint on id
# and copy the data from keysets_old to keysets, then drop keysets_old # and copy the data from keysets_old to keysets, then drop keysets_old
await conn.execute( 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( await conn.execute(
f"CREATE TABLE {table_with_schema(db, 'keysets_old')} AS" f"CREATE TABLE {db.table_with_schema('keysets_old')} AS"
f" SELECT * FROM {table_with_schema(db, 'keysets')}" 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( await conn.execute(
f""" 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, id TEXT NOT NULL,
derivation_path TEXT, derivation_path TEXT,
seed TEXT, seed TEXT,
@@ -406,21 +406,21 @@ async def m013_keysets_add_encrypted_seed(db: Database):
""" """
) )
await conn.execute( 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," " derivation_path, valid_from, valid_to, first_seen,"
" active, version, seed, unit) SELECT id, derivation_path," " active, version, seed, unit) SELECT id, derivation_path,"
" valid_from, valid_to, first_seen, active, version, seed," " 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 # add columns encrypted_seed and seed_encryption_method to keysets
await conn.execute( 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" " TEXT"
) )
await conn.execute( 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" " 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 # get all proofs_used and proofs_pending from the database and compute Y for each of them
async with db.connect() as conn: async with db.connect() as conn:
rows = await conn.fetchall( 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 # Proof() will compute Y from secret upon initialization
proofs_used = [Proof(**r) for r in rows] proofs_used = [Proof(**r) for r in rows]
rows = await conn.fetchall( 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] proofs_pending = [Proof(**r) for r in rows]
async with db.connect() as conn: 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 drop_balance_views(db, conn)
await conn.execute( 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: for proof in proofs_used:
await conn.execute( 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}'" f" WHERE secret = '{proof.secret}'"
) )
# Copy proofs_used to proofs_used_old and create a new table proofs_used # 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) # 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 # and copy the data from proofs_used_old to proofs_used, then drop proofs_used_old
await conn.execute( 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( await conn.execute(
f"CREATE TABLE {table_with_schema(db, 'proofs_used_old')} AS" f"CREATE TABLE {db.table_with_schema('proofs_used_old')} AS"
f" SELECT * FROM {table_with_schema(db, 'proofs_used')}" 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( await conn.execute(
f""" 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, amount {db.big_int} NOT NULL,
c TEXT NOT NULL, c TEXT NOT NULL,
secret TEXT NOT NULL, secret TEXT NOT NULL,
@@ -478,19 +478,19 @@ async def m014_proofs_add_Y_column(db: Database):
""" """
) )
await conn.execute( 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," "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 # add column y to proofs_pending
await conn.execute( 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: for proof in proofs_pending:
await conn.execute( 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}'" 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) # 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 # and copy the data from proofs_pending_old to proofs_pending, then drop proofs_pending_old
await conn.execute( 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( await conn.execute(
f"CREATE TABLE {table_with_schema(db, 'proofs_pending_old')} AS" f"CREATE TABLE {db.table_with_schema('proofs_pending_old')} AS"
f" SELECT * FROM {table_with_schema(db, 'proofs_pending')}" 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( await conn.execute(
f""" 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, amount {db.big_int} NOT NULL,
c TEXT NOT NULL, c TEXT NOT NULL,
secret TEXT NOT NULL, secret TEXT NOT NULL,
y TEXT, y TEXT,
id TEXT, id TEXT,
created TIMESTAMP, created TIMESTAMP DEFAULT {db.timestamp_now},
UNIQUE (Y) UNIQUE (Y)
@@ -523,12 +523,12 @@ async def m014_proofs_add_Y_column(db: Database):
""" """
) )
await conn.execute( 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" "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 # recreate the balance views
await create_balance_views(db, conn) 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( await conn.execute(
"CREATE INDEX IF NOT EXISTS" "CREATE INDEX IF NOT EXISTS"
" proofs_used_Y_idx ON" " proofs_used_Y_idx ON"
f" {table_with_schema(db, 'proofs_used')} (Y)" f" {db.table_with_schema('proofs_used')} (Y)"
) )
await conn.execute( await conn.execute(
"CREATE INDEX IF NOT EXISTS" "CREATE INDEX IF NOT EXISTS"
" proofs_pending_Y_idx ON" " 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 # get all proofs_used and proofs_pending from the database and compute Y for each of them
async with db.connect() as conn: async with db.connect() as conn:
rows = await conn.fetchall( 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 # Proof() will compute Y from secret upon initialization
proofs_used = [Proof(**r) for r in rows] proofs_used = [Proof(**r) for r in rows]
async with db.connect() as conn:
rows = await conn.fetchall( 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] 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( await conn.execute(
f""" f"""
UPDATE {table_with_schema(db, 'proofs_used')} UPDATE {db.table_with_schema('proofs_used')}
SET y = tmp_proofs_used.y SET y = tmp_proofs_used.y
FROM tmp_proofs_used 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( await conn.execute(
f""" f"""
UPDATE {table_with_schema(db, 'proofs_pending')} UPDATE {db.table_with_schema('proofs_pending')}
SET y = tmp_proofs_pending.y SET y = tmp_proofs_pending.y
FROM tmp_proofs_pending 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 # add foreign key constraints to proofs_used table
await conn.execute( await conn.execute(
f""" 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, amount {db.big_int} NOT NULL,
id TEXT, id TEXT,
c TEXT NOT NULL, c TEXT NOT NULL,
@@ -646,24 +646,24 @@ async def m017_foreign_keys_proof_tables(db: Database):
created TIMESTAMP, created TIMESTAMP,
melt_quote TEXT, 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) UNIQUE (y)
); );
""" """
) )
await conn.execute( 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( 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 # add foreign key constraints to proofs_pending table
await conn.execute( await conn.execute(
f""" 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, amount {db.big_int} NOT NULL,
id TEXT, id TEXT,
c TEXT NOT NULL, c TEXT NOT NULL,
@@ -673,24 +673,24 @@ async def m017_foreign_keys_proof_tables(db: Database):
created TIMESTAMP, created TIMESTAMP,
melt_quote TEXT, 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) UNIQUE (y)
); );
""" """
) )
await conn.execute( 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( 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 # add foreign key constraints to promises table
await conn.execute( await conn.execute(
f""" 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, amount {db.big_int} NOT NULL,
id TEXT, id TEXT,
b_ TEXT NOT NULL, b_ TEXT NOT NULL,
@@ -701,7 +701,7 @@ async def m017_foreign_keys_proof_tables(db: Database):
mint_quote TEXT, mint_quote TEXT,
swap_id 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_) UNIQUE (b_)
); );
@@ -709,11 +709,11 @@ async def m017_foreign_keys_proof_tables(db: Database):
) )
await conn.execute( 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( 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 # recreate the balance views
@@ -727,7 +727,7 @@ async def m018_duplicate_deprecated_keyset_ids(db: Database):
async with db.connect() as conn: async with db.connect() as conn:
rows = await conn.fetchall( # type: ignore rows = await conn.fetchall( # type: ignore
f""" f"""
SELECT * from {table_with_schema(db, 'keysets')} SELECT * from {db.table_with_schema('keysets')}
""", """,
) )
keysets = [MintKeyset(**row) for row in rows] 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: for keyset in duplicated_keysets:
await conn.execute( await conn.execute(
f""" 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) (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, "id": keyset.id,
keyset.derivation_path, "derivation_path": keyset.derivation_path,
keyset.valid_from, "valid_from": keyset.valid_from,
keyset.valid_to, "valid_to": keyset.valid_to,
keyset.first_seen, "first_seen": keyset.first_seen,
keyset.active, "active": keyset.active,
keyset.version, "version": keyset.version,
keyset.seed, "seed": keyset.seed,
keyset.unit.name, "unit": keyset.unit.name,
keyset.encrypted_seed, "encrypted_seed": keyset.encrypted_seed,
keyset.seed_encryption_method, "seed_encryption_method": keyset.seed_encryption_method,
), },
) )
async def m019_add_fee_to_keysets(db: Database): async def m019_add_fee_to_keysets(db: Database):
async with db.connect() as conn: async with db.connect() as conn:
await conn.execute( 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( 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 def m020_add_state_to_mint_and_melt_quotes(db: Database):
async with db.connect() as conn: async with db.connect() as conn:
await conn.execute( 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( 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 # 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: # mint quotes:
async with db.connect() as conn: async with db.connect() as conn:
rows = await conn.fetchall( 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: for row in rows:
if row["issued"]: if row["issued"]:
@@ -799,13 +799,13 @@ async def m020_add_state_to_mint_and_melt_quotes(db: Database):
else: else:
state = "unpaid" state = "unpaid"
await conn.execute( 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: # melt quotes:
async with db.connect() as conn: async with db.connect() as conn:
rows = await conn.fetchall( 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: for row in rows:
if row["paid"]: if row["paid"]:
@@ -813,15 +813,15 @@ async def m020_add_state_to_mint_and_melt_quotes(db: Database):
else: else:
state = "unpaid" state = "unpaid"
await conn.execute( 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 def m021_add_change_and_expiry_to_melt_quotes(db: Database):
async with db.connect() as conn: async with db.connect() as conn:
await conn.execute( 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( 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"
) )

View File

@@ -98,3 +98,9 @@ async def start_mint_init():
await ledger.startup_ledger() await ledger.startup_ledger()
logger.info("Mint started.") logger.info("Mint started.")
# asyncio.create_task(rotate_keys()) # asyncio.create_task(rotate_keys())
async def shutdown_mint():
await ledger.shutdown_ledger()
logger.info("Mint shutdown.")
logger.remove()

View File

@@ -1,5 +1,5 @@
import asyncio import asyncio
from typing import Mapping from typing import List, Mapping
from loguru import logger from loguru import logger
@@ -17,13 +17,15 @@ class LedgerTasks(SupportsDb, SupportsBackends, SupportsEvents):
crud: LedgerCrud crud: LedgerCrud
events: LedgerEventManager 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 method, unitbackends in self.backends.items():
for unit, backend in unitbackends.items(): for unit, backend in unitbackends.items():
logger.debug( logger.debug(
f"Dispatching backend invoice listener for {method} {unit} {backend.__class__.__name__}" 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: async def invoice_listener(self, backend: LightningBackend) -> None:
if backend.supports_incoming_payment_stream: 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: async def invoice_callback_dispatcher(self, checking_id: str) -> None:
logger.debug(f"Invoice callback dispatcher: {checking_id}") logger.debug(f"Invoice callback dispatcher: {checking_id}")
# TODO: db read, quote.paid = True, db write should be refactored and moved to ledger.py async with self.db.get_connection(
quote = await self.crud.get_mint_quote(checking_id=checking_id, db=self.db) lock_table="mint_quotes",
if not quote: lock_select_statement=f"checking_id='{checking_id}'",
logger.error(f"Quote not found for {checking_id}") lock_timeout=5,
return ) as conn:
# set the quote as paid quote = await self.crud.get_mint_quote(
if not quote.paid: checking_id=checking_id, db=self.db, conn=conn
quote.paid = True )
quote.state = MintQuoteState.paid if not quote:
await self.crud.update_mint_quote(quote=quote, db=self.db) logger.error(f"Quote not found for {checking_id}")
logger.trace(f"Quote {quote} set as paid and ") 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) await self.events.submit(quote)

View File

@@ -12,7 +12,7 @@ from ..core.base import (
) )
from ..core.crypto import b_dhke from ..core.crypto import b_dhke
from ..core.crypto.secp import PublicKey from ..core.crypto.secp import PublicKey
from ..core.db import Database from ..core.db import Connection, Database
from ..core.errors import ( from ..core.errors import (
NoSecretInProofsError, NoSecretInProofsError,
NotAllowedError, NotAllowedError,
@@ -40,7 +40,11 @@ class LedgerVerification(
lightning: Dict[Unit, LightningBackend] lightning: Dict[Unit, LightningBackend]
async def verify_inputs_and_outputs( 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. """Checks all proofs and outputs for validity.
@@ -48,6 +52,7 @@ class LedgerVerification(
proofs (List[Proof]): List of proofs to check. proofs (List[Proof]): List of proofs to check.
outputs (Optional[List[BlindedMessage]], optional): List of outputs 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. Must be provided for a swap but not for a melt. Defaults to None.
conn (Optional[Connection], optional): Database connection. Defaults to None.
Raises: Raises:
Exception: Scripts did not validate. Exception: Scripts did not validate.
@@ -56,8 +61,10 @@ class LedgerVerification(
Exception: BDHKE verification failed. Exception: BDHKE verification failed.
""" """
# Verify inputs # Verify inputs
if not proofs:
raise TransactionError("no proofs provided.")
# Verify proofs are spendable # 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() raise TokenAlreadySpentError()
# Verify amounts of inputs # Verify amounts of inputs
if not all([self._verify_amount(p.amount) for p in proofs]): if not all([self._verify_amount(p.amount) for p in proofs]):
@@ -83,7 +90,7 @@ class LedgerVerification(
self._verify_equation_balanced(proofs, outputs) self._verify_equation_balanced(proofs, outputs)
# Verify outputs # Verify outputs
await self._verify_outputs(outputs) await self._verify_outputs(outputs, conn=conn)
# Verify inputs and outputs together # Verify inputs and outputs together
if not self._verify_input_output_amounts(proofs, outputs): if not self._verify_input_output_amounts(proofs, outputs):
@@ -104,7 +111,10 @@ class LedgerVerification(
raise TransactionError("validation of output spending conditions failed.") raise TransactionError("validation of output spending conditions failed.")
async def _verify_outputs( 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.""" """Verify that the outputs are valid."""
logger.trace(f"Verifying {len(outputs)} outputs.") logger.trace(f"Verifying {len(outputs)} outputs.")
@@ -127,13 +137,15 @@ class LedgerVerification(
if not self._verify_no_duplicate_outputs(outputs): if not self._verify_no_duplicate_outputs(outputs):
raise TransactionError("duplicate outputs.") raise TransactionError("duplicate outputs.")
# verify that outputs have not been signed previously # verify that outputs have not been signed previously
signed_before = await self._check_outputs_issued_before(outputs) signed_before = await self._check_outputs_issued_before(outputs, conn)
if any(signed_before): if any(signed_before):
raise TransactionError("outputs have already been signed before.") raise TransactionError("outputs have already been signed before.")
logger.trace(f"Verified {len(outputs)} outputs.") logger.trace(f"Verified {len(outputs)} outputs.")
async def _check_outputs_issued_before( async def _check_outputs_issued_before(
self, outputs: List[BlindedMessage] self,
outputs: List[BlindedMessage],
conn: Optional[Connection] = None,
) -> List[bool]: ) -> List[bool]:
"""Checks whether the provided outputs have previously been signed by the mint """Checks whether the provided outputs have previously been signed by the mint
(which would lead to a duplication error later when trying to store these outputs again). (which would lead to a duplication error later when trying to store these outputs again).
@@ -145,7 +157,7 @@ class LedgerVerification(
result (List[bool]): Whether outputs are already present in the database. result (List[bool]): Whether outputs are already present in the database.
""" """
result = [] result = []
async with self.db.connect() as conn: async with self.db.get_connection(conn) as conn:
for output in outputs: for output in outputs:
promise = await self.crud.get_promise( promise = await self.crud.get_promise(
b_=output.B_, db=self.db, conn=conn b_=output.B_, db=self.db, conn=conn
@@ -153,21 +165,15 @@ class LedgerVerification(
result.append(False if promise is None else True) result.append(False if promise is None else True)
return result return result
async def _get_proofs_pending(self, Ys: List[str]) -> Dict[str, Proof]: async def _get_proofs_spent(
"""Returns a dictionary of only those proofs that are pending. self, Ys: List[str], conn: Optional[Connection] = None
The key is the Y=h2c(secret) and the value is the proof. ) -> Dict[str, Proof]:
"""
proofs_pending = await self.crud.get_proofs_pending(Ys=Ys, db=self.db)
proofs_pending_dict = {p.Y: p for p in proofs_pending}
return proofs_pending_dict
async def _get_proofs_spent(self, Ys: List[str]) -> Dict[str, Proof]:
"""Returns a dictionary of all proofs that are spent. """Returns a dictionary of all proofs that are spent.
The key is the Y=h2c(secret) and the value is the proof. The key is the Y=h2c(secret) and the value is the proof.
""" """
proofs_spent_dict: Dict[str, Proof] = {} proofs_spent_dict: Dict[str, Proof] = {}
# check used secrets in database # 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: for Y in Ys:
spent_proof = await self.crud.get_proof_used(db=self.db, Y=Y, conn=conn) spent_proof = await self.crud.get_proof_used(db=self.db, Y=Y, conn=conn)
if spent_proof: if spent_proof:

View File

@@ -1,6 +1,6 @@
import json import json
import time 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.base import Invoice, Proof, WalletKeyset
from ..core.db import Connection, Database from ..core.db import Connection, Database
@@ -15,19 +15,19 @@ async def store_proof(
""" """
INSERT INTO proofs INSERT INTO proofs
(id, amount, C, secret, time_created, derivation_path, dleq, mint_id, melt_id) (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, "id": proof.id,
proof.amount, "amount": proof.amount,
str(proof.C), "C": str(proof.C),
str(proof.secret), "secret": str(proof.secret),
int(time.time()), "time_created": int(time.time()),
proof.derivation_path, "derivation_path": proof.derivation_path,
json.dumps(proof.dleq.dict()) if proof.dleq else "", "dleq": json.dumps(proof.dleq.dict()) if proof.dleq else "",
proof.mint_id, "mint_id": proof.mint_id,
proof.melt_id, "melt_id": proof.melt_id,
), },
) )
@@ -41,30 +41,28 @@ async def get_proofs(
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
): ):
clauses = [] clauses = []
values: List[Any] = [] values: Dict[str, Any] = {}
if id: if id:
clauses.append("id = ?") clauses.append("id = :id")
values.append(id) values["id"] = id
if melt_id: if melt_id:
clauses.append("melt_id = ?") clauses.append("melt_id = :melt_id")
values.append(melt_id) values["melt_id"] = melt_id
if mint_id: if mint_id:
clauses.append("mint_id = ?") clauses.append("mint_id = :mint_id")
values.append(mint_id) values["mint_id"] = mint_id
where = "" where = ""
if clauses: if clauses:
where = f"WHERE {' AND '.join(clauses)}" where = f"WHERE {' AND '.join(clauses)}"
rows = ( rows = await (conn or db).fetchall(
await (conn or db).fetchall( f"""
f""" SELECT * from {table}
SELECT * from {table} {where}
{where} """,
""", values,
tuple(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( async def get_reserved_proofs(
@@ -88,27 +86,27 @@ async def invalidate_proof(
await (conn or db).execute( await (conn or db).execute(
""" """
DELETE FROM proofs DELETE FROM proofs
WHERE secret = ? WHERE secret = :secret
""", """,
(str(proof["secret"]),), {"secret": str(proof["secret"])},
) )
await (conn or db).execute( await (conn or db).execute(
""" """
INSERT INTO proofs_used INSERT INTO proofs_used
(amount, C, secret, time_used, id, derivation_path, mint_id, melt_id) (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, "amount": proof.amount,
str(proof.C), "C": str(proof.C),
str(proof.secret), "secret": str(proof.secret),
int(time.time()), "time_used": int(time.time()),
proof.id, "id": proof.id,
proof.derivation_path, "derivation_path": proof.derivation_path,
proof.mint_id, "mint_id": proof.mint_id,
proof.melt_id, "melt_id": proof.melt_id,
), },
) )
@@ -123,29 +121,29 @@ async def update_proof(
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ) -> None:
clauses = [] clauses = []
values: List[Any] = [] values: Dict[str, Any] = {}
clauses.append("reserved = ?")
values.append(reserved)
if send_id is not None:
clauses.append("send_id = ?")
values.append(send_id)
if reserved is not None: if reserved is not None:
clauses.append("time_reserved = ?") clauses.append("reserved = :reserved")
values.append(int(time.time())) 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: if mint_id is not None:
clauses.append("mint_id = ?") clauses.append("mint_id = :mint_id")
values.append(mint_id) values["mint_id"] = mint_id
if melt_id is not None: if melt_id is not None:
clauses.append("melt_id = ?") clauses.append("melt_id = :melt_id")
values.append(melt_id) values["melt_id"] = melt_id
await (conn or db).execute( # type: ignore await (conn or db).execute( # type: ignore
f"UPDATE proofs SET {', '.join(clauses)} WHERE secret = ?", f"UPDATE proofs SET {', '.join(clauses)} WHERE secret = :secret",
(*values, str(proof.secret)), {**values, "secret": str(proof.secret)},
) )
@@ -157,9 +155,9 @@ async def secret_used(
rows = await (conn or db).fetchone( rows = await (conn or db).fetchone(
""" """
SELECT * from proofs SELECT * from proofs
WHERE secret = ? WHERE secret = :secret
""", """,
(secret,), {"secret": secret},
) )
return rows is not None return rows is not None
@@ -174,19 +172,19 @@ async def store_keyset(
""" """
INSERT INTO keysets INSERT INTO keysets
(id, mint_url, valid_from, valid_to, first_seen, active, public_keys, unit, input_fee_ppk) (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, "id": keyset.id,
mint_url or keyset.mint_url, "mint_url": mint_url or keyset.mint_url,
keyset.valid_from or int(time.time()), "valid_from": keyset.valid_from or int(time.time()),
keyset.valid_to or int(time.time()), "valid_to": keyset.valid_to or int(time.time()),
keyset.first_seen or int(time.time()), "first_seen": keyset.first_seen or int(time.time()),
keyset.active, "active": keyset.active,
keyset.serialize(), "public_keys": keyset.serialize(),
keyset.unit.name, "unit": keyset.unit.name,
keyset.input_fee_ppk, "input_fee_ppk": keyset.input_fee_ppk,
), },
) )
@@ -198,32 +196,28 @@ async def get_keysets(
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> List[WalletKeyset]: ) -> List[WalletKeyset]:
clauses = [] clauses = []
values: List[Any] = [] values: Dict[str, Any] = {}
if id: if id:
clauses.append("id = ?") clauses.append("id = :id")
values.append(id) values["id"] = id
if mint_url: if mint_url:
clauses.append("mint_url = ?") clauses.append("mint_url = :mint_url")
values.append(mint_url) values["mint_url"] = mint_url
if unit: if unit:
clauses.append("unit = ?") clauses.append("unit = :unit")
values.append(unit) values["unit"] = unit
where = "" where = ""
if clauses: if clauses:
where = f"WHERE {' AND '.join(clauses)}" where = f"WHERE {' AND '.join(clauses)}"
row = await (conn or db).fetchall( # type: ignore rows = await (conn or db).fetchall( # type: ignore
f""" f"""
SELECT * from keysets SELECT * from keysets
{where} {where}
""", """,
tuple(values), values,
) )
ret = [] return [WalletKeyset.from_row(r) for r in rows]
for r in row:
keyset = WalletKeyset.from_row(r)
ret.append(keyset)
return ret
async def update_keyset( async def update_keyset(
@@ -234,13 +228,13 @@ async def update_keyset(
await (conn or db).execute( await (conn or db).execute(
""" """
UPDATE keysets UPDATE keysets
SET active = ? SET active = :active
WHERE id = ? WHERE id = :id
""", """,
( {
keyset.active, "active": keyset.active,
keyset.id, "id": keyset.id,
), },
) )
@@ -253,19 +247,19 @@ async def store_lightning_invoice(
""" """
INSERT INTO invoices INSERT INTO invoices
(amount, bolt11, id, payment_hash, preimage, paid, time_created, time_paid, out) (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, "amount": invoice.amount,
invoice.bolt11, "bolt11": invoice.bolt11,
invoice.id, "id": invoice.id,
invoice.payment_hash, "payment_hash": invoice.payment_hash,
invoice.preimage, "preimage": invoice.preimage,
invoice.paid, "paid": invoice.paid,
invoice.time_created, "time_created": invoice.time_created,
invoice.time_paid, "time_paid": invoice.time_paid,
invoice.out, "out": invoice.out,
), },
) )
@@ -278,16 +272,16 @@ async def get_lightning_invoice(
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> Optional[Invoice]: ) -> Optional[Invoice]:
clauses = [] clauses = []
values: List[Any] = [] values: Dict[str, Any] = {}
if id: if id:
clauses.append("id = ?") clauses.append("id = :id")
values.append(id) values["id"] = id
if payment_hash: if payment_hash:
clauses.append("payment_hash = ?") clauses.append("payment_hash = :payment_hash")
values.append(payment_hash) values["payment_hash"] = payment_hash
if out is not None: if out is not None:
clauses.append("out = ?") clauses.append("out = :out")
values.append(out) values["out"] = out
where = "" where = ""
if clauses: if clauses:
@@ -298,7 +292,7 @@ async def get_lightning_invoice(
""" """
row = await (conn or db).fetchone( row = await (conn or db).fetchone(
query, query,
tuple(values), values,
) )
return Invoice(**row) if row else None return Invoice(**row) if row else None
@@ -309,18 +303,18 @@ async def get_lightning_invoices(
pending: Optional[bool] = None, pending: Optional[bool] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> List[Invoice]: ) -> List[Invoice]:
clauses: List[Any] = [] clauses = []
values: List[Any] = [] values: Dict[str, Any] = {}
if paid is not None and not pending: if paid is not None and not pending:
clauses.append("paid = ?") clauses.append("paid = :paid")
values.append(paid) values["paid"] = paid
if pending: if pending:
clauses.append("paid = ?") clauses.append("paid = :paid")
values.append(False) values["paid"] = False
clauses.append("out = ?") clauses.append("out = :out")
values.append(False) values["out"] = False
where = "" where = ""
if clauses: if clauses:
@@ -331,7 +325,7 @@ async def get_lightning_invoices(
SELECT * from invoices SELECT * from invoices
{where} {where}
""", """,
tuple(values), values,
) )
return [Invoice(**r) for r in rows] return [Invoice(**r) for r in rows]
@@ -345,23 +339,20 @@ async def update_lightning_invoice(
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ) -> None:
clauses = [] clauses = []
values: List[Any] = [] values: Dict[str, Any] = {}
clauses.append("paid = ?") clauses.append("paid = :paid")
values.append(paid) values["paid"] = paid
if time_paid: if time_paid:
clauses.append("time_paid = ?") clauses.append("time_paid = :time_paid")
values.append(time_paid) values["time_paid"] = time_paid
if preimage: if preimage:
clauses.append("preimage = ?") clauses.append("preimage = :preimage")
values.append(preimage) values["preimage"] = preimage
await (conn or db).execute( await (conn or db).execute(
f"UPDATE invoices SET {', '.join(clauses)} WHERE id = ?", f"UPDATE invoices SET {', '.join(clauses)} WHERE id = :id",
( {**values, "id": id},
*values,
id,
),
) )
@@ -373,16 +364,16 @@ async def bump_secret_derivation(
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> int: ) -> int:
rows = await (conn or db).fetchone( 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 no counter for this keyset, create one
if not rows: if not rows:
await (conn or db).execute( await (conn or db).execute(
"UPDATE keysets SET counter = ? WHERE id = ?", "UPDATE keysets SET counter = :counter WHERE id = :keyset_id",
( {
0, "counter": 0,
keyset_id, "keyset_id": keyset_id,
), },
) )
counter = 0 counter = 0
else: else:
@@ -390,8 +381,8 @@ async def bump_secret_derivation(
if not skip: if not skip:
await (conn or db).execute( await (conn or db).execute(
f"UPDATE keysets SET counter = counter + {by} WHERE id = ?", "UPDATE keysets SET counter = counter + :by WHERE id = :keyset_id",
(keyset_id,), {"by": by, "keyset_id": keyset_id},
) )
return counter return counter
@@ -403,11 +394,11 @@ async def set_secret_derivation(
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ) -> None:
await (conn or db).execute( await (conn or db).execute(
"UPDATE keysets SET counter = ? WHERE id = ?", "UPDATE keysets SET counter = :counter WHERE id = :keyset_id",
( {
counter, "counter": counter,
keyset_id, "keyset_id": keyset_id,
), },
) )
@@ -417,8 +408,8 @@ async def set_nostr_last_check_timestamp(
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ) -> None:
await (conn or db).execute( await (conn or db).execute(
"UPDATE nostr SET last = ? WHERE type = ?", "UPDATE nostr SET last = :last WHERE type = :type",
(timestamp, "dm"), {"last": timestamp, "type": "dm"},
) )
@@ -428,9 +419,9 @@ async def get_nostr_last_check_timestamp(
) -> Optional[int]: ) -> Optional[int]:
row = await (conn or db).fetchone( 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 return row[0] if row else None
@@ -442,7 +433,7 @@ async def get_seed_and_mnemonic(
row = await (conn or db).fetchone( row = await (conn or db).fetchone(
""" """
SELECT seed, mnemonic from seed SELECT seed, mnemonic from seed
""", """
) )
return ( return (
( (
@@ -464,10 +455,10 @@ async def store_seed_and_mnemonic(
""" """
INSERT INTO seed INSERT INTO seed
(seed, mnemonic) (seed, mnemonic)
VALUES (?, ?) VALUES (:seed, :mnemonic)
""", """,
( {
seed, "seed": seed,
mnemonic, "mnemonic": mnemonic,
), },
) )

View File

@@ -166,12 +166,12 @@ async def m007_nostr(db: Database):
""" """
INSERT INTO nostr INSERT INTO nostr
(type, last) (type, last)
VALUES (?, ?) VALUES (:type, :last)
""", """,
( {
"dm", "type": "db",
None, "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 def m020_add_state_to_mint_and_melt_quotes(db: Database):
# # async with db.connect() as conn: # # async with db.connect() as conn:
# # await conn.execute( # # 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( # # 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 # # # 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: # # # mint quotes:
# # async with db.connect() as conn: # # async with db.connect() as conn:
# # rows = await conn.fetchall( # # 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: # # for row in rows:
# # if row["issued"]: # # if row["issued"]:
@@ -269,13 +269,13 @@ async def m012_add_fee_to_keysets(db: Database):
# # else: # # else:
# # state = "unpaid" # # state = "unpaid"
# # await conn.execute( # # 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: # # # melt quotes:
# # async with db.connect() as conn: # # async with db.connect() as conn:
# # rows = await conn.fetchall( # # 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: # # for row in rows:
# # if row["paid"]: # # if row["paid"]:
@@ -283,7 +283,7 @@ async def m012_add_fee_to_keysets(db: Database):
# # else: # # else:
# # state = "unpaid" # # state = "unpaid"
# # await conn.execute( # # 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 # # 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 # # as they are defined in the wallet db

View File

@@ -310,9 +310,10 @@ class Wallet(
async def _check_used_secrets(self, secrets): async def _check_used_secrets(self, secrets):
"""Checks if any of the secrets have already been used""" """Checks if any of the secrets have already been used"""
logger.trace("Checking secrets.") logger.trace("Checking secrets.")
for s in secrets: async with self.db.get_connection() as conn:
if await secret_used(s, db=self.db): for s in secrets:
raise Exception(f"secret already used: {s}") if await secret_used(s, db=self.db, conn=conn):
raise Exception(f"secret already used: {s}")
logger.trace("Secret check complete.") logger.trace("Secret check complete.")
async def request_mint_with_callback( async def request_mint_with_callback(

417
poetry.lock generated
View File

@@ -1,5 +1,23 @@
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. # 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]] [[package]]
name = "anyio" name = "anyio"
version = "3.7.1" version = "3.7.1"
@@ -33,23 +51,72 @@ files = [
] ]
[[package]] [[package]]
name = "attrs" name = "async-timeout"
version = "23.2.0" version = "4.0.3"
description = "Classes Without Boilerplate" description = "Timeout context manager for asyncio programs"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
{file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, {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] [package.extras]
cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
dev = ["attrs[tests]", "pre-commit"] test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"]
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]"]
[[package]] [[package]]
name = "base58" name = "base58"
@@ -175,13 +242,13 @@ test = ["coverage (>=7)", "hypothesis", "pytest"]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2024.6.2" version = "2024.7.4"
description = "Python package for providing Mozilla's CA Bundle." description = "Python package for providing Mozilla's CA Bundle."
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
{file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
{file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
] ]
[[package]] [[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)"] 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)"] 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]] [[package]]
name = "h11" name = "h11"
version = "0.14.0" version = "0.14.0"
@@ -641,13 +779,13 @@ socks = ["socksio (==1.*)"]
[[package]] [[package]]
name = "identify" name = "identify"
version = "2.5.36" version = "2.6.0"
description = "File identification library for Python" description = "File identification library for Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"},
{file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"},
] ]
[package.extras] [package.extras]
@@ -858,20 +996,6 @@ files = [
{file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, {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]] [[package]]
name = "packaging" name = "packaging"
version = "24.1" version = "24.1"
@@ -932,84 +1056,6 @@ nodeenv = ">=0.11.1"
pyyaml = ">=5.1" pyyaml = ">=5.1"
virtualenv = ">=20.10.0" 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]] [[package]]
name = "pycparser" name = "pycparser"
version = "2.22" version = "2.22"
@@ -1318,21 +1364,6 @@ files = [
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, {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]] [[package]]
name = "respx" name = "respx"
version = "0.20.2" version = "0.20.2"
@@ -1476,79 +1507,82 @@ files = [
[[package]] [[package]]
name = "sqlalchemy" name = "sqlalchemy"
version = "1.3.24" version = "1.4.52"
description = "Database Abstraction Library" description = "Database Abstraction Library"
optional = false 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 = [ files = [
{file = "SQLAlchemy-1.3.24-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:87a2725ad7d41cd7376373c15fd8bf674e9c33ca56d0b8036add2d634dba372e"}, {file = "SQLAlchemy-1.4.52-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:f68016f9a5713684c1507cc37133c28035f29925c75c0df2f9d0f7571e23720a"},
{file = "SQLAlchemy-1.3.24-cp27-cp27m-win32.whl", hash = "sha256:f597a243b8550a3a0b15122b14e49d8a7e622ba1c9d29776af741f1845478d79"}, {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.3.24-cp27-cp27m-win_amd64.whl", hash = "sha256:fc4cddb0b474b12ed7bdce6be1b9edc65352e8ce66bc10ff8cbbfb3d4047dbf4"}, {file = "SQLAlchemy-1.4.52-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e93983cc0d2edae253b3f2141b0a3fb07e41c76cd79c2ad743fc27eb79c3f6db"},
{file = "SQLAlchemy-1.3.24-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:f1149d6e5c49d069163e58a3196865e4321bad1803d7886e07d8710de392c548"}, {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.3.24-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:14f0eb5db872c231b20c18b1e5806352723a3a89fb4254af3b3e14f22eaaec75"}, {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.3.24-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:e98d09f487267f1e8d1179bf3b9d7709b30a916491997137dd24d6ae44d18d79"}, {file = "SQLAlchemy-1.4.52-cp310-cp310-win32.whl", hash = "sha256:1296f2cdd6db09b98ceb3c93025f0da4835303b8ac46c15c2136e27ee4d18d94"},
{file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:fc1f2a5a5963e2e73bac4926bdaf7790c4d7d77e8fc0590817880e22dd9d0b8b"}, {file = "SQLAlchemy-1.4.52-cp310-cp310-win_amd64.whl", hash = "sha256:80e7f697bccc56ac6eac9e2df5c98b47de57e7006d2e46e1a3c17c546254f6ef"},
{file = "SQLAlchemy-1.3.24-cp35-cp35m-win32.whl", hash = "sha256:f3c5c52f7cb8b84bfaaf22d82cb9e6e9a8297f7c2ed14d806a0f5e4d22e83fb7"}, {file = "SQLAlchemy-1.4.52-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2f251af4c75a675ea42766880ff430ac33291c8d0057acca79710f9e5a77383d"},
{file = "SQLAlchemy-1.3.24-cp35-cp35m-win_amd64.whl", hash = "sha256:0352db1befcbed2f9282e72843f1963860bf0e0472a4fa5cf8ee084318e0e6ab"}, {file = "SQLAlchemy-1.4.52-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb8f9e4c4718f111d7b530c4e6fb4d28f9f110eb82e7961412955b3875b66de0"},
{file = "SQLAlchemy-1.3.24-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:2ed6343b625b16bcb63c5b10523fd15ed8934e1ed0f772c534985e9f5e73d894"}, {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.3.24-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:34fcec18f6e4b24b4a5f6185205a04f1eab1e56f8f1d028a2a03694ebcc2ddd4"}, {file = "SQLAlchemy-1.4.52-cp311-cp311-win32.whl", hash = "sha256:6e41cb5cda641f3754568d2ed8962f772a7f2b59403b95c60c89f3e0bd25f15e"},
{file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e47e257ba5934550d7235665eee6c911dc7178419b614ba9e1fbb1ce6325b14f"}, {file = "SQLAlchemy-1.4.52-cp311-cp311-win_amd64.whl", hash = "sha256:5bed4f8c3b69779de9d99eb03fd9ab67a850d74ab0243d1be9d4080e77b6af12"},
{file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:816de75418ea0953b5eb7b8a74933ee5a46719491cd2b16f718afc4b291a9658"}, {file = "SQLAlchemy-1.4.52-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:49e3772eb3380ac88d35495843daf3c03f094b713e66c7d017e322144a5c6b7c"},
{file = "SQLAlchemy-1.3.24-cp36-cp36m-win32.whl", hash = "sha256:26155ea7a243cbf23287f390dba13d7927ffa1586d3208e0e8d615d0c506f996"}, {file = "SQLAlchemy-1.4.52-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:618827c1a1c243d2540314c6e100aee7af09a709bd005bae971686fab6723554"},
{file = "SQLAlchemy-1.3.24-cp36-cp36m-win_amd64.whl", hash = "sha256:f03bd97650d2e42710fbe4cf8a59fae657f191df851fc9fc683ecef10746a375"}, {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.3.24-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a006d05d9aa052657ee3e4dc92544faae5fcbaafc6128217310945610d862d39"}, {file = "SQLAlchemy-1.4.52-cp312-cp312-win32.whl", hash = "sha256:763bd97c4ebc74136ecf3526b34808c58945023a59927b416acebcd68d1fc126"},
{file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1e2f89d2e5e3c7a88e25a3b0e43626dba8db2aa700253023b82e630d12b37109"}, {file = "SQLAlchemy-1.4.52-cp312-cp312-win_amd64.whl", hash = "sha256:f12aaf94f4d9679ca475975578739e12cc5b461172e04d66f7a3c39dd14ffc64"},
{file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0d5d862b1cfbec5028ce1ecac06a3b42bc7703eb80e4b53fceb2738724311443"}, {file = "SQLAlchemy-1.4.52-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:853fcfd1f54224ea7aabcf34b227d2b64a08cbac116ecf376907968b29b8e763"},
{file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:0172423a27fbcae3751ef016663b72e1a516777de324a76e30efa170dbd3dd2d"}, {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.3.24-cp37-cp37m-win32.whl", hash = "sha256:d37843fb8df90376e9e91336724d78a32b988d3d20ab6656da4eb8ee3a45b63c"}, {file = "SQLAlchemy-1.4.52-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e135fff2e84103bc15c07edd8569612ce317d64bdb391f49ce57124a73f45c5"},
{file = "SQLAlchemy-1.3.24-cp37-cp37m-win_amd64.whl", hash = "sha256:c10ff6112d119f82b1618b6dc28126798481b9355d8748b64b9b55051eb4f01b"}, {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.3.24-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:861e459b0e97673af6cc5e7f597035c2e3acdfb2608132665406cded25ba64c7"}, {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.3.24-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5de2464c254380d8a6c20a2746614d5a436260be1507491442cf1088e59430d2"}, {file = "SQLAlchemy-1.4.52-cp36-cp36m-win32.whl", hash = "sha256:427c282dd0deba1f07bcbf499cbcc9fe9a626743f5d4989bfdfd3ed3513003dd"},
{file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d375d8ccd3cebae8d90270f7aa8532fe05908f79e78ae489068f3b4eee5994e8"}, {file = "SQLAlchemy-1.4.52-cp36-cp36m-win_amd64.whl", hash = "sha256:ca5ce82b11731492204cff8845c5e8ca1a4bd1ade85e3b8fcf86e7601bfc6a39"},
{file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:014ea143572fee1c18322b7908140ad23b3994036ef4c0d630110faf942652f8"}, {file = "SQLAlchemy-1.4.52-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:29d4247313abb2015f8979137fe65f4eaceead5247d39603cc4b4a610936cd2b"},
{file = "SQLAlchemy-1.3.24-cp38-cp38-win32.whl", hash = "sha256:6607ae6cd3a07f8a4c3198ffbf256c261661965742e2b5265a77cd5c679c9bba"}, {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.3.24-cp38-cp38-win_amd64.whl", hash = "sha256:fcb251305fa24a490b6a9ee2180e5f8252915fb778d3dafc70f9cc3f863827b9"}, {file = "SQLAlchemy-1.4.52-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7ea11727feb2861deaa293c7971a4df57ef1c90e42cb53f0da40c3468388000"},
{file = "SQLAlchemy-1.3.24-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:01aa5f803db724447c1d423ed583e42bf5264c597fd55e4add4301f163b0be48"}, {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.3.24-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4d0e3515ef98aa4f0dc289ff2eebb0ece6260bbf37c2ea2022aad63797eacf60"}, {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.3.24-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:bce28277f308db43a6b4965734366f533b3ff009571ec7ffa583cb77539b84d6"}, {file = "SQLAlchemy-1.4.52-cp37-cp37m-win32.whl", hash = "sha256:1f8e1c6a6b7f8e9407ad9afc0ea41c1f65225ce505b79bc0342159de9c890782"},
{file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:8110e6c414d3efc574543109ee618fe2c1f96fa31833a1ff36cc34e968c4f233"}, {file = "SQLAlchemy-1.4.52-cp37-cp37m-win_amd64.whl", hash = "sha256:346ed50cb2c30f5d7a03d888e25744154ceac6f0e6e1ab3bc7b5b77138d37710"},
{file = "SQLAlchemy-1.3.24-cp39-cp39-win32.whl", hash = "sha256:ee5f5188edb20a29c1cc4a039b074fdc5575337c9a68f3063449ab47757bb064"}, {file = "SQLAlchemy-1.4.52-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:4dae6001457d4497736e3bc422165f107ecdd70b0d651fab7f731276e8b9e12d"},
{file = "SQLAlchemy-1.3.24-cp39-cp39-win_amd64.whl", hash = "sha256:09083c2487ca3c0865dc588e07aeaa25416da3d95f7482c07e92f47e080aa17b"}, {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.3.24.tar.gz", hash = "sha256:ebbb777cbf9312359b897bf81ba00dae0f5cb69fba2a18265dcc18a6f5ef7519"}, {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"},
[package.extras] {file = "SQLAlchemy-1.4.52-cp38-cp38-win32.whl", hash = "sha256:bcdfb4b47fe04967669874fb1ce782a006756fdbebe7263f6a000e1db969120e"},
mssql = ["pyodbc"] {file = "SQLAlchemy-1.4.52-cp38-cp38-win_amd64.whl", hash = "sha256:7d0dbc56cb6af5088f3658982d3d8c1d6a82691f31f7b0da682c7b98fa914e91"},
mssql-pymssql = ["pymssql"] {file = "SQLAlchemy-1.4.52-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:a551d5f3dc63f096ed41775ceec72fdf91462bb95abdc179010dc95a93957800"},
mssql-pyodbc = ["pyodbc"] {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"},
mysql = ["mysqlclient"] {file = "SQLAlchemy-1.4.52-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2de46f5d5396d5331127cfa71f837cca945f9a2b04f7cb5a01949cf676db7d1"},
oracle = ["cx-oracle"] {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"},
postgresql = ["psycopg2"] {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"},
postgresql-pg8000 = ["pg8000 (<1.16.6)"] {file = "SQLAlchemy-1.4.52-cp39-cp39-win32.whl", hash = "sha256:c124912fd4e1bb9d1e7dc193ed482a9f812769cb1e69363ab68e01801e859821"},
postgresql-psycopg2binary = ["psycopg2-binary"] {file = "SQLAlchemy-1.4.52-cp39-cp39-win_amd64.whl", hash = "sha256:2c286fab42e49db23c46ab02479f328b8bdb837d3e281cae546cc4085c83b680"},
postgresql-psycopg2cffi = ["psycopg2cffi"] {file = "SQLAlchemy-1.4.52.tar.gz", hash = "sha256:80e63bbdc5217dad3485059bdf6f65a7d43f33c8bde619df5c220edf03d87296"},
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"},
] ]
[package.dependencies] [package.dependencies]
outcome = "*" 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\")"}
represent = ">=1.4"
sqlalchemy = "<1.4"
[package.extras] [package.extras]
test = ["pytest (>=5.4)", "pytest-asyncio (>=0.14)", "pytest-trio (>=0.6)"] aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"]
test-noextras = ["pytest (>=5.4)", "pytest-asyncio (>=0.14)"] aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
trio = ["trio (>=0.15)"] 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]] [[package]]
name = "starlette" 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"] 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)"] 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] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.8.1" python-versions = "^3.8.1"
content-hash = "82dc8679e17cd4fb9f897d60ac55e90250886b9b95bfa1def11694d26a027b65" content-hash = "d312a7a7c367a8c7f3664f4691e8e39aae6bc9aaf2e116abaf9e7fa1f28fe479"

View File

@@ -7,7 +7,7 @@ license = "MIT"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.8.1" python = "^3.8.1"
SQLAlchemy = "^1.3.24" SQLAlchemy = {version = "1.4.52", extras = ["asyncio"]}
click = "^8.1.7" click = "^8.1.7"
pydantic = "^1.10.2" pydantic = "^1.10.2"
bech32 = "^1.2.0" bech32 = "^1.2.0"
@@ -18,7 +18,6 @@ loguru = "^0.7.0"
ecdsa = "^0.18.0" ecdsa = "^0.18.0"
bitstring = "^3.1.9" bitstring = "^3.1.9"
secp256k1 = "^0.14.0" secp256k1 = "^0.14.0"
sqlalchemy-aio = "^0.17.0"
h11 = "^0.14.0" h11 = "^0.14.0"
cryptography = "^41.0.3" cryptography = "^41.0.3"
websocket-client = "^1.3.3" websocket-client = "^1.3.3"
@@ -26,7 +25,6 @@ pycryptodomex = "^3.16.0"
setuptools = "^68.1.2" setuptools = "^68.1.2"
wheel = "^0.41.1" wheel = "^0.41.1"
importlib-metadata = "^6.8.0" importlib-metadata = "^6.8.0"
psycopg2-binary = { version = "^2.9.7", optional = true }
httpx = {extras = ["socks"], version = "^0.25.1"} httpx = {extras = ["socks"], version = "^0.25.1"}
bip32 = "^3.4" bip32 = "^3.4"
mnemonic = "^0.20" mnemonic = "^0.20"
@@ -35,9 +33,8 @@ pre-commit = "^3.5.0"
websockets = "^12.0" websockets = "^12.0"
slowapi = "^0.1.9" slowapi = "^0.1.9"
cbor2 = "^5.6.2" cbor2 = "^5.6.2"
asyncpg = "^0.29.0"
[tool.poetry.extras] aiosqlite = "^0.20.0"
pgsql = ["psycopg2-binary"]
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pytest-asyncio = "^0.21.1" pytest-asyncio = "^0.21.1"

View File

@@ -22,7 +22,8 @@ from cashu.mint.ledger import Ledger
SERVER_PORT = 3337 SERVER_PORT = 3337
SERVER_ENDPOINT = f"http://localhost:{SERVER_PORT}" SERVER_ENDPOINT = f"http://localhost:{SERVER_PORT}"
settings.debug = False settings.debug = True
settings.log_level = "TRACE"
settings.cashu_dir = "./test_data/" settings.cashu_dir = "./test_data/"
settings.mint_host = "localhost" settings.mint_host = "localhost"
settings.mint_port = SERVER_PORT 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_lnd_enable_mpp = True
settings.mint_clnrest_enable_mpp = False settings.mint_clnrest_enable_mpp = False
settings.mint_input_fee_ppk = 0 settings.mint_input_fee_ppk = 0
settings.db_connection_pool = True
assert "test" in settings.cashu_dir assert "test" in settings.cashu_dir
shutil.rmtree(settings.cashu_dir, ignore_errors=True) shutil.rmtree(settings.cashu_dir, ignore_errors=True)
@@ -83,11 +85,6 @@ class UvicornServer(multiprocessing.Process):
async def ledger(): async def ledger():
async def start_mint_init(ledger: Ledger) -> Ledger: async def start_mint_init(ledger: Ledger) -> Ledger:
await migrate_databases(ledger.db, migrations_mint) 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( ledger = Ledger(
db=Database("mint", settings.mint_database), db=Database("mint", settings.mint_database),
seed=settings.mint_private_key, seed=settings.mint_private_key,
@@ -107,8 +104,10 @@ async def ledger():
# clear postgres database # clear postgres database
db = Database("mint", settings.mint_database) db = Database("mint", settings.mint_database)
async with db.connect() as conn: async with db.connect() as conn:
# drop all tables
await conn.execute("DROP SCHEMA public CASCADE;") await conn.execute("DROP SCHEMA public CASCADE;")
await conn.execute("CREATE SCHEMA public;") await conn.execute("CREATE SCHEMA public;")
await db.engine.dispose()
wallets_module = importlib.import_module("cashu.lightning") wallets_module = importlib.import_module("cashu.lightning")
lightning_backend = getattr(wallets_module, settings.mint_backend_bolt11_sat)() lightning_backend = getattr(wallets_module, settings.mint_backend_bolt11_sat)()
@@ -125,6 +124,7 @@ async def ledger():
ledger = await start_mint_init(ledger) ledger = await start_mint_init(ledger)
yield ledger yield ledger
print("teardown") print("teardown")
await ledger.shutdown_ledger()
# # This fixture is used for tests that require API access to the mint # # This fixture is used for tests that require API access to the mint
@@ -134,6 +134,7 @@ def mint():
"cashu.mint.app:app", "cashu.mint.app:app",
port=settings.mint_listen_port, port=settings.mint_listen_port,
host=settings.mint_listen_host, host=settings.mint_listen_host,
log_level="trace",
) )
server = UvicornServer(config=config) server = UvicornServer(config=config)

View File

@@ -1,3 +1,4 @@
import asyncio
import hashlib import hashlib
import importlib import importlib
import json import json
@@ -6,13 +7,44 @@ import random
import string import string
import time import time
from subprocess import PIPE, Popen, TimeoutExpired from subprocess import PIPE, Popen, TimeoutExpired
from typing import Tuple from typing import List, Tuple, Union
from loguru import logger from loguru import logger
from cashu.core.errors import CashuError
from cashu.core.settings import settings 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): def get_random_string(N: int = 10):
return "".join( return "".join(
random.SystemRandom().choice(string.ascii_uppercase + string.digits) random.SystemRandom().choice(string.ascii_uppercase + string.digits)
@@ -157,6 +189,9 @@ def pay_onchain(address: str, sats: int) -> str:
return run_cmd(cmd) return run_cmd(cmd)
def pay_if_regtest(bolt11: str): async def pay_if_regtest(bolt11: str):
if is_regtest: if is_regtest:
pay_real_invoice(bolt11) pay_real_invoice(bolt11)
if is_fake:
await asyncio.sleep(settings.fakewallet_delay_incoming_payment or 0)
await asyncio.sleep(0.1)

View File

@@ -1,15 +1,81 @@
import asyncio
import datetime import datetime
import os import os
import time import time
from typing import List
import pytest import pytest
import pytest_asyncio
from cashu.core import db 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.migrations import backup_database
from cashu.core.settings import settings from cashu.core.settings import settings
from cashu.mint.ledger import Ledger 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 @pytest.mark.asyncio
@@ -27,7 +93,7 @@ async def test_backup_db_migration(ledger: Ledger):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_timestamp_now(ledger: Ledger): async def test_timestamp_now(ledger: Ledger):
ts = timestamp_now(ledger.db) ts = ledger.db.timestamp_now_str()
if ledger.db.type == db.SQLITE: if ledger.db.type == db.SQLITE:
assert isinstance(ts, str) assert isinstance(ts, str)
assert int(ts) <= time.time() assert int(ts) <= time.time()
@@ -37,33 +103,203 @@ async def test_timestamp_now(ledger: Ledger):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_connection(ledger: Ledger): async def test_db_connect(ledger: Ledger):
async with ledger.db.connect() as conn: async with ledger.db.connect() as conn:
assert isinstance(conn, Connection) assert isinstance(conn, Connection)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_db_tables(ledger: Ledger): async def test_db_get_connection(ledger: Ledger):
async with ledger.db.connect() as conn: async with ledger.db.get_connection() as conn:
if ledger.db.type == db.SQLITE: assert isinstance(conn, Connection)
tables_res = await conn.execute(
"SELECT name FROM sqlite_master WHERE type='table';"
) # @pytest.mark.asyncio
elif ledger.db.type in {db.POSTGRES, db.COCKROACH}: # async def test_db_get_connection_locked(wallet: Wallet, ledger: Ledger):
tables_res = await conn.execute( # invoice = await wallet.request_mint(64)
"SELECT table_name FROM information_schema.tables WHERE table_schema ="
" 'public';" # 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)"""
tables = [t[0] for t in await tables_res.fetchall()] # try:
tables_expected = [ # async with ledger.db.get_connection(lock_table="mint_quotes"):
"dbversions", # try:
"keysets", # async with ledger.db.get_connection(
"proofs_used", # lock_table="mint_quotes", lock_timeout=0.1
"proofs_pending", # ) as conn2:
"melt_quotes", # # write something with conn1, we never reach this point if the lock works
"mint_quotes", # await conn2.execute(
"mint_pubkeys", # f"INSERT INTO mint_quotes (quote, amount) VALUES ('{invoice.id}', 100);"
"promises", # )
] # except Exception as exc:
for table in tables_expected: # # this is expected to raise
assert table in tables # 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",
)

View File

@@ -69,7 +69,7 @@ async def test_get_keyset(ledger: Ledger):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_mint(ledger: Ledger): async def test_mint(ledger: Ledger):
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat")) quote = await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat"))
pay_if_regtest(quote.request) await pay_if_regtest(quote.request)
blinded_messages_mock = [ blinded_messages_mock = [
BlindedMessage( BlindedMessage(
amount=8, amount=8,
@@ -89,7 +89,7 @@ async def test_mint(ledger: Ledger):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_mint_invalid_blinded_message(ledger: Ledger): async def test_mint_invalid_blinded_message(ledger: Ledger):
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat")) 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 = [ blinded_messages_mock_invalid_key = [
BlindedMessage( BlindedMessage(
amount=8, amount=8,

View File

@@ -153,7 +153,7 @@ async def test_api_keyset_keys_old_keyset_id(ledger: Ledger):
) )
async def test_split(ledger: Ledger, wallet: Wallet): async def test_split(ledger: Ledger, wallet: Wallet):
invoice = await wallet.request_mint(64) invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id) await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64 assert wallet.balance == 64
secrets, rs, derivation_paths = await wallet.generate_n_secrets(2) 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] inputs_payload = [p.to_dict() for p in wallet.proofs]
outputs_payload = [o.dict() for o in outputs] outputs_payload = [o.dict() for o in outputs]
payload = {"inputs": inputs_payload, "outputs": outputs_payload} 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}" assert response.status_code == 200, f"{response.url} {response.status_code}"
result = response.json() result = response.json()
assert len(result["signatures"]) == 2 assert len(result["signatures"]) == 2
@@ -208,7 +208,7 @@ async def test_mint_quote(ledger: Ledger):
assert result["expiry"] == expiry assert result["expiry"] == expiry
# pay the invoice # pay the invoice
pay_if_regtest(result["request"]) await pay_if_regtest(result["request"])
# get mint quote again from api # get mint quote again from api
response = httpx.get( response = httpx.get(
@@ -234,7 +234,7 @@ async def test_mint_quote(ledger: Ledger):
) )
async def test_mint(ledger: Ledger, wallet: Wallet): async def test_mint(ledger: Ledger, wallet: Wallet):
invoice = await wallet.request_mint(64) invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
quote_id = invoice.id quote_id = invoice.id
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001) secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001)
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs) 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): async def test_melt_internal(ledger: Ledger, wallet: Wallet):
# internal invoice # internal invoice
invoice = await wallet.request_mint(64) invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id) await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64 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): async def test_melt_external(ledger: Ledger, wallet: Wallet):
# internal invoice # internal invoice
invoice = await wallet.request_mint(64) invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id) await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64 assert wallet.balance == 64
@@ -439,6 +439,7 @@ async def test_melt_external(ledger: Ledger, wallet: Wallet):
}, },
timeout=None, timeout=None,
) )
response.raise_for_status()
assert response.status_code == 200, f"{response.url} {response.status_code}" assert response.status_code == 200, f"{response.url} {response.status_code}"
result = response.json() result = response.json()
assert result.get("payment_preimage") is not None 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): async def test_api_restore(ledger: Ledger, wallet: Wallet):
invoice = await wallet.request_mint(64) invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id) await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64 assert wallet.balance == 64
secret_counter = await bump_secret_derivation( secret_counter = await bump_secret_derivation(

View File

@@ -67,7 +67,7 @@ async def test_api_keyset_keys(ledger: Ledger):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_split(ledger: Ledger, wallet: Wallet): async def test_split(ledger: Ledger, wallet: Wallet):
invoice = await wallet.request_mint(64) invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id) await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64 assert wallet.balance == 64
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(20000, 20001) 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 @pytest.mark.asyncio
async def test_split_deprecated_with_amount(ledger: Ledger, wallet: Wallet): async def test_split_deprecated_with_amount(ledger: Ledger, wallet: Wallet):
invoice = await wallet.request_mint(64) invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id) await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64 assert wallet.balance == 64
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(80000, 80001) 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 @pytest.mark.asyncio
async def test_mint(ledger: Ledger, wallet: Wallet): async def test_mint(ledger: Ledger, wallet: Wallet):
invoice = await wallet.request_mint(64) invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
quote_id = invoice.id quote_id = invoice.id
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001) secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001)
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs) outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
@@ -148,9 +148,9 @@ async def test_mint(ledger: Ledger, wallet: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_melt_internal(ledger: Ledger, wallet: Wallet): async def test_melt_internal(ledger: Ledger, wallet: Wallet):
# internal invoice # fill wallet
invoice = await wallet.request_mint(64) invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id) await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64 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 # Clients without NUT-08 will not send change outputs
# internal invoice # internal invoice
invoice = await wallet.request_mint(64) invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id) await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64 assert wallet.balance == 64
# create invoice to melt to # create invoice to melt to
invoice = await wallet.request_mint(64) invoice = await wallet.request_mint(64)
invoice_payment_request = invoice.bolt11 invoice_payment_request = invoice.bolt11
quote = await wallet.melt_quote(invoice_payment_request) quote = await wallet.melt_quote(invoice_payment_request)
assert quote.amount == 64 assert quote.amount == 64
assert quote.fee_reserve == 0 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): async def test_melt_external(ledger: Ledger, wallet: Wallet):
# internal invoice # internal invoice
invoice = await wallet.request_mint(64) invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id) await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64 assert wallet.balance == 64
@@ -325,7 +323,7 @@ async def test_api_check_state(ledger: Ledger):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_api_restore(ledger: Ledger, wallet: Wallet): async def test_api_restore(ledger: Ledger, wallet: Wallet):
invoice = await wallet.request_mint(64) invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id) await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64 assert wallet.balance == 64
secret_counter = await bump_secret_derivation( secret_counter = await bump_secret_derivation(

View File

@@ -1,23 +1,18 @@
import pytest import pytest
import pytest_asyncio import pytest_asyncio
from cashu.core.base import MeltQuoteState, MintQuoteState, ProofSpentState
from cashu.core.models import PostMeltQuoteRequest from cashu.core.models import PostMeltQuoteRequest
from cashu.mint.ledger import Ledger from cashu.mint.ledger import Ledger
from cashu.wallet.wallet import Wallet from cashu.wallet.wallet import Wallet
from cashu.wallet.wallet import Wallet as Wallet1 from cashu.wallet.wallet import Wallet as Wallet1
from tests.conftest import SERVER_ENDPOINT from tests.conftest import SERVER_ENDPOINT
from tests.helpers import is_postgres from tests.helpers import (
assert_err,
is_github_actions,
async def assert_err(f, msg): is_postgres,
"""Compute f() and expect an error message 'msg'.""" pay_if_regtest,
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")
@pytest_asyncio.fixture(scope="function") @pytest_asyncio.fixture(scope="function")
@@ -31,6 +26,35 @@ async def wallet1(ledger: Ledger):
yield wallet1 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 @pytest.mark.asyncio
async def test_mint_quote(wallet1: Wallet, ledger: Ledger): async def test_mint_quote(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(128) invoice = await wallet1.request_mint(128)
@@ -42,10 +66,63 @@ async def test_mint_quote(wallet1: Wallet, ledger: Ledger):
assert quote.unit == "sat" assert quote.unit == "sat"
assert not quote.paid assert not quote.paid
assert quote.checking_id == invoice.payment_hash assert quote.checking_id == invoice.payment_hash
assert quote.paid_time is None # assert quote.paid_time is None
assert quote.created_time 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 @pytest.mark.asyncio
async def test_get_mint_quote_by_request(wallet1: Wallet, ledger: Ledger): async def test_get_mint_quote_by_request(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(128) invoice = await wallet1.request_mint(128)
@@ -56,7 +133,7 @@ async def test_get_mint_quote_by_request(wallet1: Wallet, ledger: Ledger):
assert quote.amount == 128 assert quote.amount == 128
assert quote.unit == "sat" assert quote.unit == "sat"
assert not quote.paid assert not quote.paid
assert quote.paid_time is None # assert quote.paid_time is None
assert quote.created_time assert quote.created_time
@@ -74,10 +151,112 @@ async def test_melt_quote(wallet1: Wallet, ledger: Ledger):
assert quote.unit == "sat" assert quote.unit == "sat"
assert not quote.paid assert not quote.paid
assert quote.checking_id == invoice.payment_hash assert quote.checking_id == invoice.payment_hash
assert quote.paid_time is None # assert quote.paid_time is None
assert quote.created_time 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.asyncio
@pytest.mark.skipif(not is_postgres, reason="only works with Postgres") @pytest.mark.skipif(not is_postgres, reason="only works with Postgres")
async def test_postgres_working(): async def test_postgres_working():

View File

@@ -49,7 +49,7 @@ def set_ledger_keyset_fees(
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_fees_for_proofs(wallet1: Wallet, ledger: Ledger): async def test_get_fees_for_proofs(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(64) 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) await wallet1.mint(64, split=[1] * 64, id=invoice.id)
# two proofs # two proofs
@@ -115,7 +115,7 @@ async def test_split_with_fees(wallet1: Wallet, ledger: Ledger):
# set fees to 100 ppk # set fees to 100 ppk
set_ledger_keyset_fees(100, ledger) set_ledger_keyset_fees(100, ledger)
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
send_proofs, _ = await wallet1.select_to_send(wallet1.proofs, 10) 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 fees to 100 ppk
set_ledger_keyset_fees(1234, ledger) set_ledger_keyset_fees(1234, ledger)
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
send_proofs, _ = await wallet1.select_to_send(wallet1.proofs, 10) 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 fees to 100 ppk
set_ledger_keyset_fees(100, ledger) set_ledger_keyset_fees(100, ledger)
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
send_proofs, _ = await wallet1.select_to_send(wallet1.proofs, 10) 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 # mint twice so we have enough to pay the second invoice back
invoice = await wallet1.request_mint(128) invoice = await wallet1.request_mint(128)
await pay_if_regtest(invoice.bolt11)
await wallet1.mint(128, id=invoice.id) await wallet1.mint(128, id=invoice.id)
assert wallet1.balance == 128 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 # mint twice so we have enough to pay the second invoice back
invoice = await wallet1.request_mint(128) invoice = await wallet1.request_mint(128)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet1.mint(128, id=invoice.id) await wallet1.mint(128, id=invoice.id)
assert wallet1.balance == 128 assert wallet1.balance == 128

View File

@@ -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): async def test_startup_regtest_pending_quote_pending(wallet: Wallet, ledger: Ledger):
# fill wallet # fill wallet
invoice = await wallet.request_mint(64) invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id) await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64 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): async def test_startup_regtest_pending_quote_success(wallet: Wallet, ledger: Ledger):
# fill wallet # fill wallet
invoice = await wallet.request_mint(64) invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id) await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64 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.""" """Simulate a failure to pay the hodl invoice by canceling it."""
# fill wallet # fill wallet
invoice = await wallet.request_mint(64) invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id) await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64 assert wallet.balance == 64

View File

@@ -1,6 +1,7 @@
import pytest import pytest
import pytest_asyncio import pytest_asyncio
from cashu.core.base import MeltQuoteState, MintQuoteState
from cashu.core.helpers import sum_proofs from cashu.core.helpers import sum_proofs
from cashu.core.models import PostMeltQuoteRequest, PostMintQuoteRequest from cashu.core.models import PostMeltQuoteRequest, PostMintQuoteRequest
from cashu.mint.ledger import Ledger from cashu.mint.ledger import Ledger
@@ -37,7 +38,9 @@ async def wallet1(ledger: Ledger):
async def test_melt_internal(wallet1: Wallet, ledger: Ledger): async def test_melt_internal(wallet1: Wallet, ledger: Ledger):
# mint twice so we have enough to pay the second invoice back # mint twice so we have enough to pay the second invoice back
invoice = await wallet1.request_mint(128) invoice = await wallet1.request_mint(128)
await pay_if_regtest(invoice.bolt11)
await wallet1.mint(128, id=invoice.id) await wallet1.mint(128, id=invoice.id)
await pay_if_regtest(invoice.bolt11)
assert wallet1.balance == 128 assert wallet1.balance == 128
# create a mint quote so that we can melt to it internally # 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") PostMeltQuoteRequest(request=invoice_payment_request, unit="sat")
) )
assert not melt_quote.paid assert not melt_quote.paid
assert melt_quote.state == MeltQuoteState.unpaid.value
assert melt_quote.amount == 64 assert melt_quote.amount == 64
assert melt_quote.fee_reserve == 0 assert melt_quote.fee_reserve == 0
melt_quote_pre_payment = await ledger.get_melt_quote(melt_quote.quote) 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 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) keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 64)
await ledger.melt(proofs=send_proofs, quote=melt_quote.quote) await ledger.melt(proofs=send_proofs, quote=melt_quote.quote)
melt_quote_post_payment = await ledger.get_melt_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.paid, "melt quote should be paid"
assert melt_quote_post_payment.state == MeltQuoteState.paid
@pytest.mark.asyncio @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): async def test_melt_external(wallet1: Wallet, ledger: Ledger):
# mint twice so we have enough to pay the second invoice back # mint twice so we have enough to pay the second invoice back
invoice = await wallet1.request_mint(128) invoice = await wallet1.request_mint(128)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet1.mint(128, id=invoice.id) await wallet1.mint(128, id=invoice.id)
assert wallet1.balance == 128 assert wallet1.balance == 128
@@ -74,6 +81,9 @@ async def test_melt_external(wallet1: Wallet, ledger: Ledger):
invoice_payment_request = invoice_dict["payment_request"] invoice_payment_request = invoice_dict["payment_request"]
mint_quote = await wallet1.melt_quote(invoice_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 total_amount = mint_quote.amount + mint_quote.fee_reserve
keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount) keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount)
melt_quote = await ledger.melt_quote( 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) 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 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" assert not melt_quote.paid, "melt quote should not be paid"
await ledger.melt(proofs=send_proofs, quote=melt_quote.quote) await ledger.melt(proofs=send_proofs, quote=melt_quote.quote)
melt_quote_post_payment = await ledger.get_melt_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.paid, "melt quote should be paid"
assert melt_quote_post_payment.state == MeltQuoteState.paid
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet") @pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
async def test_mint_internal(wallet1: Wallet, ledger: Ledger): async def test_mint_internal(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(128) invoice = await wallet1.request_mint(128)
await pay_if_regtest(invoice.bolt11)
mint_quote = await ledger.get_mint_quote(invoice.id) mint_quote = await ledger.get_mint_quote(invoice.id)
assert mint_quote.paid, "mint quote should be paid" assert mint_quote.paid, "mint quote should be paid"
assert mint_quote.state == MintQuoteState.paid
output_amounts = [128] output_amounts = [128]
secrets, rs, derivation_paths = await wallet1.generate_n_secrets( 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.", "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.asyncio
@pytest.mark.skipif(is_fake, reason="only works with Regtest") @pytest.mark.skipif(is_fake, reason="only works with Regtest")
async def test_mint_external(wallet1: Wallet, ledger: Ledger): async def test_mint_external(wallet1: Wallet, ledger: Ledger):
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=128, unit="sat")) 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) mint_quote = await ledger.get_mint_quote(quote.quote)
assert not mint_quote.paid, "mint quote already paid" assert not mint_quote.paid, "mint quote already paid"
assert mint_quote.state == MintQuoteState.unpaid
await assert_err( await assert_err(
wallet1.mint(128, id=quote.quote), wallet1.mint(128, id=quote.quote),
"quote not paid", "quote not paid",
) )
pay_if_regtest(quote.request) await pay_if_regtest(quote.request)
mint_quote = await ledger.get_mint_quote(quote.quote) mint_quote = await ledger.get_mint_quote(quote.quote)
assert mint_quote.paid, "mint quote should be paid" assert mint_quote.paid, "mint quote should be paid"
assert mint_quote.state == MintQuoteState.paid
output_amounts = [128] output_amounts = [128]
secrets, rs, derivation_paths = await wallet1.generate_n_secrets( 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) outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
await ledger.mint(outputs=outputs, quote_id=quote.quote) 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 @pytest.mark.asyncio
async def test_split(wallet1: Wallet, ledger: Ledger): async def test_split(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10) 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 @pytest.mark.asyncio
async def test_split_with_no_outputs(wallet1: Wallet, ledger: Ledger): async def test_split_with_no_outputs(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10, set_reserved=False) _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10, set_reserved=False)
await assert_err( await assert_err(
@@ -170,7 +195,7 @@ async def test_split_with_no_outputs(wallet1: Wallet, ledger: Ledger):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_split_with_input_less_than_outputs(wallet1: Wallet, ledger: Ledger): async def test_split_with_input_less_than_outputs(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
keep_proofs, send_proofs = await wallet1.split_to_send( 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 @pytest.mark.asyncio
async def test_split_with_input_more_than_outputs(wallet1: Wallet, ledger: Ledger): async def test_split_with_input_more_than_outputs(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(128) invoice = await wallet1.request_mint(128)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet1.mint(128, id=invoice.id) await wallet1.mint(128, id=invoice.id)
inputs = wallet1.proofs inputs = wallet1.proofs
@@ -223,7 +248,7 @@ async def test_split_with_input_more_than_outputs(wallet1: Wallet, ledger: Ledge
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_split_twice_with_same_outputs(wallet1: Wallet, ledger: Ledger): async def test_split_twice_with_same_outputs(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(128) 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) await wallet1.mint(128, split=[64, 64], id=invoice.id)
inputs1 = wallet1.proofs[:1] inputs1 = wallet1.proofs[:1]
inputs2 = 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 @pytest.mark.asyncio
async def test_mint_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger): async def test_mint_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(128) invoice = await wallet1.request_mint(128)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
output_amounts = [128] output_amounts = [128]
secrets, rs, derivation_paths = await wallet1.generate_n_secrets( secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
len(output_amounts) 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 # now try to mint with the same outputs again
invoice2 = await wallet1.request_mint(128) invoice2 = await wallet1.request_mint(128)
pay_if_regtest(invoice2.bolt11) await pay_if_regtest(invoice2.bolt11)
await assert_err( await assert_err(
ledger.mint(outputs=outputs, quote_id=invoice2.id), 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 @pytest.mark.asyncio
async def test_melt_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger): async def test_melt_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(130) invoice = await wallet1.request_mint(130)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet1.mint(130, id=invoice.id) await wallet1.mint(130, id=invoice.id)
output_amounts = [128] 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 # we use the outputs once for minting
invoice2 = await wallet1.request_mint(128) 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) await ledger.mint(outputs=outputs, quote_id=invoice2.id)
# use the same outputs for melting # 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 @pytest.mark.asyncio
async def test_melt_with_less_inputs_than_invoice(wallet1: Wallet, ledger: Ledger): async def test_melt_with_less_inputs_than_invoice(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(32) invoice = await wallet1.request_mint(32)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet1.mint(32, id=invoice.id) await wallet1.mint(32, id=invoice.id)
# outputs for fee return # outputs for fee return
@@ -336,7 +361,7 @@ async def test_melt_with_less_inputs_than_invoice(wallet1: Wallet, ledger: Ledge
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_melt_with_more_inputs_than_invoice(wallet1: Wallet, ledger: Ledger): async def test_melt_with_more_inputs_than_invoice(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(130) 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) await wallet1.mint(130, split=[64, 64, 2], id=invoice.id)
# outputs for fee return # outputs for fee return
@@ -368,7 +393,7 @@ async def test_melt_with_more_inputs_than_invoice(wallet1: Wallet, ledger: Ledge
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_check_proof_state(wallet1: Wallet, ledger: Ledger): async def test_check_proof_state(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10) 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}" # f"ws://localhost:{SERVER_PORT}/v1/quote/{invoice.id}"
# ) # )
# await asyncio.sleep(0.1) # await asyncio.sleep(0.1)
# pay_if_regtest(invoice.bolt11) # await pay_if_regtest(invoice.bolt11)
# await wallet1.mint(64, id=invoice.id) # await wallet1.mint(64, id=invoice.id)
# await asyncio.sleep(0.1) # await asyncio.sleep(0.1)
# data = str(ws.recv()) # data = str(ws.recv())

View File

@@ -32,7 +32,7 @@ async def wallet():
async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger): async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger):
# fill wallet # fill wallet
invoice = await wallet.request_mint(64) invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id) await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64 assert wallet.balance == 64

View File

@@ -40,6 +40,18 @@ async def assert_err(f, msg: Union[str, CashuError]):
raise Exception(f"Expected error: {msg}, got no error") 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): def assert_amt(proofs: List[Proof], expected: int):
"""Assert amounts the proofs contain.""" """Assert amounts the proofs contain."""
assert sum([p.amount for p in proofs]) == expected assert sum([p.amount for p in proofs]) == expected
@@ -155,7 +167,7 @@ async def test_request_mint(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_mint(wallet1: Wallet): async def test_mint(wallet1: Wallet):
invoice = await wallet1.request_mint(64) 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) expected_proof_amounts = wallet1.split_wallet_state(64)
await wallet1.mint(64, id=invoice.id) await wallet1.mint(64, id=invoice.id)
assert wallet1.balance == 64 assert wallet1.balance == 64
@@ -179,7 +191,7 @@ async def test_mint_amounts(wallet1: Wallet):
"""Mint predefined amounts""" """Mint predefined amounts"""
amts = [1, 1, 1, 2, 2, 4, 16] amts = [1, 1, 1, 2, 2, 4, 16]
invoice = await wallet1.request_mint(sum(amts)) 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) await wallet1.mint(amount=sum(amts), split=amts, id=invoice.id)
assert wallet1.balance == 27 assert wallet1.balance == 27
assert wallet1.proof_amounts == amts assert wallet1.proof_amounts == amts
@@ -211,7 +223,7 @@ async def test_mint_amounts_wrong_order(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_split(wallet1: Wallet): async def test_split(wallet1: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
assert wallet1.balance == 64 assert wallet1.balance == 64
# the outputs we keep that we expect after the split # the outputs we keep that we expect after the split
@@ -231,7 +243,7 @@ async def test_split(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_split_to_send(wallet1: Wallet): async def test_split_to_send(wallet1: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
assert wallet1.balance == 64 assert wallet1.balance == 64
@@ -253,7 +265,7 @@ async def test_split_to_send(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_split_more_than_balance(wallet1: Wallet): async def test_split_more_than_balance(wallet1: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
await assert_err( await assert_err(
wallet1.split(wallet1.proofs, 128), wallet1.split(wallet1.proofs, 128),
@@ -267,7 +279,7 @@ async def test_split_more_than_balance(wallet1: Wallet):
async def test_melt(wallet1: Wallet): async def test_melt(wallet1: Wallet):
# mint twice so we have enough to pay the second invoice back # mint twice so we have enough to pay the second invoice back
topup_invoice = await wallet1.request_mint(128) 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) await wallet1.mint(128, id=topup_invoice.id)
assert wallet1.balance == 128 assert wallet1.balance == 128
@@ -333,7 +345,7 @@ async def test_melt(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_split_to_send_more_than_balance(wallet1: Wallet): async def test_split_to_send_more_than_balance(wallet1: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
await assert_err( await assert_err(
wallet1.split_to_send(wallet1.proofs, 128, set_reserved=True), 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 @pytest.mark.asyncio
async def test_double_spend(wallet1: Wallet): async def test_double_spend(wallet1: Wallet):
invoice = await wallet1.request_mint(64) 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) doublespend = await wallet1.mint(64, id=invoice.id)
await wallet1.split(wallet1.proofs, 20) await wallet1.split(wallet1.proofs, 20)
await assert_err( await assert_err(
@@ -360,7 +372,7 @@ async def test_double_spend(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_duplicate_proofs_double_spent(wallet1: Wallet): async def test_duplicate_proofs_double_spent(wallet1: Wallet):
invoice = await wallet1.request_mint(64) 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) doublespend = await wallet1.mint(64, id=invoice.id)
await assert_err( await assert_err(
wallet1.split(wallet1.proofs + doublespend, 20), 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") @pytest.mark.skipif(is_github_actions, reason="GITHUB_ACTIONS")
async def test_split_race_condition(wallet1: Wallet): async def test_split_race_condition(wallet1: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
# run two splits in parallel # run two splits in parallel
import asyncio import asyncio
await assert_err( await assert_err_multiple(
asyncio.gather( asyncio.gather(
wallet1.split(wallet1.proofs, 20), wallet1.split(wallet1.proofs, 20),
wallet1.split(wallet1.proofs, 20), wallet1.split(wallet1.proofs, 20),
), ),
"proofs are pending.", ["proofs are pending.", "already spent."],
) )
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_send_and_redeem(wallet1: Wallet, wallet2: Wallet): async def test_send_and_redeem(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
_, spendable_proofs = await wallet1.split_to_send( _, spendable_proofs = await wallet1.split_to_send(
wallet1.proofs, 32, set_reserved=True 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): async def test_invalidate_all_proofs(wallet1: Wallet):
"""Try to invalidate proofs that have not been spent yet. Should not work!""" """Try to invalidate proofs that have not been spent yet. Should not work!"""
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
await wallet1.invalidate(wallet1.proofs) await wallet1.invalidate(wallet1.proofs)
assert wallet1.balance == 0 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): async def test_invalidate_unspent_proofs_with_checking(wallet1: Wallet):
"""Try to invalidate proofs that have not been spent yet but force no check.""" """Try to invalidate proofs that have not been spent yet but force no check."""
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
await wallet1.invalidate(wallet1.proofs, check_spendable=True) await wallet1.invalidate(wallet1.proofs, check_spendable=True)
assert wallet1.balance == 64 assert wallet1.balance == 64
@@ -429,7 +441,7 @@ async def test_invalidate_unspent_proofs_with_checking(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_split_invalid_amount(wallet1: Wallet): async def test_split_invalid_amount(wallet1: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
await assert_err( await assert_err(
wallet1.split(wallet1.proofs, -1), wallet1.split(wallet1.proofs, -1),
@@ -440,7 +452,7 @@ async def test_split_invalid_amount(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_token_state(wallet1: Wallet): async def test_token_state(wallet1: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
assert wallet1.balance == 64 assert wallet1.balance == 64
resp = await wallet1.check_proof_state(wallet1.proofs) resp = await wallet1.check_proof_state(wallet1.proofs)

View File

@@ -117,7 +117,7 @@ def test_invoice_return_immediately(mint, cli_prefix):
assert result.exception is None assert result.exception is None
invoice, invoice_id = get_bolt11_and_invoice_id_from_invoice_command(result.output) 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( result = runner.invoke(
cli, cli,
@@ -146,7 +146,7 @@ def test_invoice_with_split(mint, cli_prefix):
assert result.exception is None assert result.exception is None
invoice, invoice_id = get_bolt11_and_invoice_id_from_invoice_command(result.output) 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( result = runner.invoke(
cli, cli,
[*cli_prefix, "invoice", "10", "-s", "1", "--id", invoice_id], [*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()) wallet1 = asyncio.run(init_wallet())
asyncio.run(reset_invoices(wallet=wallet1)) asyncio.run(reset_invoices(wallet=wallet1))
invoice = asyncio.run(wallet1.request_mint(64)) invoice = asyncio.run(wallet1.request_mint(64))
asyncio.run(pay_if_regtest(invoice.bolt11))
# act # act
runner = CliRunner() runner = CliRunner()
result = runner.invoke( result = runner.invoke(

View File

@@ -58,7 +58,7 @@ async def wallet2():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_create_htlc_secret(wallet1: Wallet): async def test_create_htlc_secret(wallet1: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000" preimage = "00000000000000000000000000000000"
preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
@@ -69,7 +69,7 @@ async def test_create_htlc_secret(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_htlc_split(wallet1: Wallet, wallet2: Wallet): async def test_htlc_split(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000" preimage = "00000000000000000000000000000000"
preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
@@ -82,7 +82,7 @@ async def test_htlc_split(wallet1: Wallet, wallet2: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_htlc_redeem_with_preimage(wallet1: Wallet, wallet2: Wallet): async def test_htlc_redeem_with_preimage(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000" preimage = "00000000000000000000000000000000"
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() # 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 @pytest.mark.asyncio
async def test_htlc_redeem_with_wrong_preimage(wallet1: Wallet, wallet2: Wallet): async def test_htlc_redeem_with_wrong_preimage(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000" preimage = "00000000000000000000000000000000"
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() # 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 @pytest.mark.asyncio
async def test_htlc_redeem_with_no_signature(wallet1: Wallet, wallet2: Wallet): async def test_htlc_redeem_with_no_signature(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000" preimage = "00000000000000000000000000000000"
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() 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 @pytest.mark.asyncio
async def test_htlc_redeem_with_wrong_signature(wallet1: Wallet, wallet2: Wallet): async def test_htlc_redeem_with_wrong_signature(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000" preimage = "00000000000000000000000000000000"
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() 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 @pytest.mark.asyncio
async def test_htlc_redeem_with_correct_signature(wallet1: Wallet, wallet2: Wallet): async def test_htlc_redeem_with_correct_signature(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000" preimage = "00000000000000000000000000000000"
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() 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 wallet1: Wallet, wallet2: Wallet
): ):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000" preimage = "00000000000000000000000000000000"
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() 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 wallet1: Wallet, wallet2: Wallet
): ):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000" preimage = "00000000000000000000000000000000"
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet1 = await wallet1.create_p2pk_pubkey()

View File

@@ -78,7 +78,7 @@ async def test_check_invoice_external(wallet: LightningWallet):
assert invoice.checking_id assert invoice.checking_id
status = await wallet.get_invoice_status(invoice.checking_id) status = await wallet.get_invoice_status(invoice.checking_id)
assert not status.paid 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) status = await wallet.get_invoice_status(invoice.checking_id)
assert status.paid assert status.paid
@@ -113,7 +113,7 @@ async def test_pay_invoice_external(wallet: LightningWallet):
invoice = await wallet.create_invoice(64) invoice = await wallet.create_invoice(64)
assert invoice.payment_request assert invoice.payment_request
assert invoice.checking_id 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) status = await wallet.get_invoice_status(invoice.checking_id)
assert status.paid assert status.paid
assert wallet.available_balance >= 64 assert wallet.available_balance >= 64

View File

@@ -60,7 +60,7 @@ async def wallet2():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_create_p2pk_pubkey(wallet1: Wallet): async def test_create_p2pk_pubkey(wallet1: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
pubkey = await wallet1.create_p2pk_pubkey() pubkey = await wallet1.create_p2pk_pubkey()
PublicKey(bytes.fromhex(pubkey), raw=True) PublicKey(bytes.fromhex(pubkey), raw=True)
@@ -69,7 +69,7 @@ async def test_create_p2pk_pubkey(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_p2pk(wallet1: Wallet, wallet2: Wallet): async def test_p2pk(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
# p2pk test # p2pk test
@@ -93,7 +93,7 @@ async def test_p2pk(wallet1: Wallet, wallet2: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_p2pk_sig_all(wallet1: Wallet, wallet2: Wallet): async def test_p2pk_sig_all(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
# p2pk test # p2pk test
@@ -109,7 +109,7 @@ async def test_p2pk_sig_all(wallet1: Wallet, wallet2: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_p2pk_receive_with_wrong_private_key(wallet1: Wallet, wallet2: Wallet): async def test_p2pk_receive_with_wrong_private_key(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
# sender side # sender side
@@ -130,7 +130,7 @@ async def test_p2pk_short_locktime_receive_with_wrong_private_key(
wallet1: Wallet, wallet2: Wallet wallet1: Wallet, wallet2: Wallet
): ):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
# sender side # sender side
@@ -156,7 +156,7 @@ async def test_p2pk_short_locktime_receive_with_wrong_private_key(
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_p2pk_locktime_with_refund_pubkey(wallet1: Wallet, wallet2: Wallet): async def test_p2pk_locktime_with_refund_pubkey(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
# sender side # sender side
@@ -185,7 +185,7 @@ async def test_p2pk_locktime_with_refund_pubkey(wallet1: Wallet, wallet2: Wallet
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_p2pk_locktime_with_wrong_refund_pubkey(wallet1: Wallet, wallet2: Wallet): async def test_p2pk_locktime_with_wrong_refund_pubkey(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
await wallet2.create_p2pk_pubkey() # receiver side await wallet2.create_p2pk_pubkey() # receiver side
# sender side # sender side
@@ -221,7 +221,7 @@ async def test_p2pk_locktime_with_second_refund_pubkey(
wallet1: Wallet, wallet2: Wallet wallet1: Wallet, wallet2: Wallet
): ):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() # receiver side pubkey_wallet1 = await wallet1.create_p2pk_pubkey() # receiver side
pubkey_wallet2 = await wallet2.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 @pytest.mark.asyncio
async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet): async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
pubkey_wallet2 = await wallet2.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 @pytest.mark.asyncio
async def test_p2pk_multisig_duplicate_signature(wallet1: Wallet, wallet2: Wallet): async def test_p2pk_multisig_duplicate_signature(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
pubkey_wallet2 = await wallet2.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 @pytest.mark.asyncio
async def test_p2pk_multisig_quorum_not_met_1_of_2(wallet1: Wallet, wallet2: Wallet): async def test_p2pk_multisig_quorum_not_met_1_of_2(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
pubkey_wallet2 = await wallet2.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 @pytest.mark.asyncio
async def test_p2pk_multisig_quorum_not_met_2_of_3(wallet1: Wallet, wallet2: Wallet): async def test_p2pk_multisig_quorum_not_met_2_of_3(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
pubkey_wallet2 = await wallet2.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 @pytest.mark.asyncio
async def test_p2pk_multisig_with_duplicate_publickey(wallet1: Wallet, wallet2: Wallet): async def test_p2pk_multisig_with_duplicate_publickey(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
# p2pk test # p2pk test
@@ -363,7 +363,7 @@ async def test_p2pk_multisig_with_wrong_first_private_key(
wallet1: Wallet, wallet2: Wallet wallet1: Wallet, wallet2: Wallet
): ):
invoice = await wallet1.request_mint(64) 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.mint(64, id=invoice.id)
await wallet1.create_p2pk_pubkey() await wallet1.create_p2pk_pubkey()
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey()

View File

@@ -34,7 +34,7 @@ async def wallet():
async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger): async def test_regtest_pending_quote(wallet: Wallet, ledger: Ledger):
# fill wallet # fill wallet
invoice = await wallet.request_mint(64) invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id) await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64 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): async def test_regtest_failed_quote(wallet: Wallet, ledger: Ledger):
# fill wallet # fill wallet
invoice = await wallet.request_mint(64) invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id) await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64 assert wallet.balance == 64

View File

@@ -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 # top up wallet twice so we have enough for two payments
topup_invoice = await wallet.request_mint(128) 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) proofs1 = await wallet.mint(128, id=topup_invoice.id)
assert wallet.balance == 128 assert wallet.balance == 128
topup_invoice = await wallet.request_mint(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) proofs2 = await wallet.mint(128, id=topup_invoice.id)
assert wallet.balance == 256 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 # top up wallet twice so we have enough for three payments
topup_invoice = await wallet.request_mint(128) 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) proofs1 = await wallet.mint(128, id=topup_invoice.id)
assert wallet.balance == 128 assert wallet.balance == 128
topup_invoice = await wallet.request_mint(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) proofs2 = await wallet.mint(128, id=topup_invoice.id)
assert wallet.balance == 256 assert wallet.balance == 256
topup_invoice = await wallet.request_mint(128) 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) proofs3 = await wallet.mint(128, id=topup_invoice.id)
assert wallet.balance == 384 assert wallet.balance == 384

View File

@@ -157,7 +157,7 @@ async def test_generate_secrets_from_to(wallet3: Wallet):
async def test_restore_wallet_after_mint(wallet3: Wallet): async def test_restore_wallet_after_mint(wallet3: Wallet):
await reset_wallet_db(wallet3) await reset_wallet_db(wallet3)
invoice = await wallet3.request_mint(64) invoice = await wallet3.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet3.mint(64, id=invoice.id) await wallet3.mint(64, id=invoice.id)
assert wallet3.balance == 64 assert wallet3.balance == 64
await reset_wallet_db(wallet3) 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) await reset_wallet_db(wallet3)
invoice = await wallet3.request_mint(64) invoice = await wallet3.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet3.mint(64, id=invoice.id) await wallet3.mint(64, id=invoice.id)
assert wallet3.balance == 64 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) await reset_wallet_db(wallet3)
invoice = await wallet3.request_mint(64) invoice = await wallet3.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet3.mint(64, id=invoice.id) await wallet3.mint(64, id=invoice.id)
assert wallet3.balance == 64 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) await reset_wallet_db(wallet3)
invoice = await wallet3.request_mint(64) invoice = await wallet3.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet3.mint(64, id=invoice.id) await wallet3.mint(64, id=invoice.id)
assert wallet3.balance == 64 assert wallet3.balance == 64
@@ -290,7 +290,7 @@ async def test_restore_wallet_after_send_twice(
await reset_wallet_db(wallet3) await reset_wallet_db(wallet3)
invoice = await wallet3.request_mint(2) invoice = await wallet3.request_mint(2)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet3.mint(2, id=invoice.id) await wallet3.mint(2, id=invoice.id)
box.add(wallet3.proofs) box.add(wallet3.proofs)
assert wallet3.balance == 2 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) await reset_wallet_db(wallet3)
invoice = await wallet3.request_mint(64) invoice = await wallet3.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet3.mint(64, id=invoice.id) await wallet3.mint(64, id=invoice.id)
box.add(wallet3.proofs) box.add(wallet3.proofs)
assert wallet3.balance == 64 assert wallet3.balance == 64

View File

@@ -47,7 +47,7 @@ async def test_wallet_subscription_mint(wallet: Wallet):
asyncio.run(wallet.mint(int(invoice.amount), id=invoice.id)) asyncio.run(wallet.mint(int(invoice.amount), id=invoice.id))
invoice, sub = await wallet.request_mint_with_callback(128, callback=callback) 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 wait = settings.fakewallet_delay_incoming_payment or 2
await asyncio.sleep(wait + 2) await asyncio.sleep(wait + 2)
@@ -70,7 +70,7 @@ async def test_wallet_subscription_swap(wallet: Wallet):
pytest.skip("No websocket support") pytest.skip("No websocket support")
invoice = await wallet.request_mint(64) invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11) await pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id) await wallet.mint(64, id=invoice.id)
triggered = False triggered = False