mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-07-31 07:46:15 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b535e88282 |
+125
-10
@@ -183,11 +183,112 @@ async def create_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
yield session
|
||||
|
||||
|
||||
def _get_table_columns(cursor: sqlite3.Cursor, table: str) -> set[str]:
|
||||
"""Return the set of column names for a table, or empty set if table doesn't exist."""
|
||||
cursor.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table,)
|
||||
)
|
||||
if not cursor.fetchone():
|
||||
return set()
|
||||
cursor.execute(f"PRAGMA table_info({table})")
|
||||
return {row[1] for row in cursor.fetchall()}
|
||||
|
||||
|
||||
def _detect_schema_version(cursor: sqlite3.Cursor) -> int:
|
||||
"""
|
||||
Detect the actual schema version of a cashu wallet database by inspecting
|
||||
which columns/tables exist, matching them to migration milestones.
|
||||
|
||||
Cashu wallet migrations (m000-m015) add columns non-idempotently.
|
||||
We detect the highest migration that has already been fully applied.
|
||||
"""
|
||||
proofs_cols = _get_table_columns(cursor, "proofs")
|
||||
keysets_cols = _get_table_columns(cursor, "keysets")
|
||||
|
||||
if not proofs_cols:
|
||||
return 0 # no proofs table means nothing has run
|
||||
|
||||
# Walk backwards from the highest migration to find the actual version.
|
||||
# Each check tests for schema artifacts introduced by that migration.
|
||||
|
||||
# m015: mints table
|
||||
mints_cols = _get_table_columns(cursor, "mints")
|
||||
if mints_cols:
|
||||
return 15
|
||||
|
||||
# m014: bolt11_mint_quotes.key column
|
||||
mint_quotes_cols = _get_table_columns(cursor, "bolt11_mint_quotes")
|
||||
if "key" in mint_quotes_cols:
|
||||
return 14
|
||||
|
||||
# m013: bolt11_mint_quotes table
|
||||
if mint_quotes_cols:
|
||||
return 13
|
||||
|
||||
# m012: keysets.input_fee_ppk
|
||||
if "input_fee_ppk" in keysets_cols:
|
||||
return 12
|
||||
|
||||
# m011: keysets.unit
|
||||
if "unit" in keysets_cols:
|
||||
return 11
|
||||
|
||||
# m010: proofs.dleq, proofs.mint_id
|
||||
if "mint_id" in proofs_cols:
|
||||
return 10
|
||||
if "dleq" in proofs_cols:
|
||||
return 10
|
||||
|
||||
# m009: keysets.counter, proofs.derivation_path
|
||||
if "derivation_path" in proofs_cols:
|
||||
return 9
|
||||
|
||||
# m008: keysets.public_keys
|
||||
if "public_keys" in keysets_cols:
|
||||
return 8
|
||||
|
||||
# m007: nostr table
|
||||
nostr_cols = _get_table_columns(cursor, "nostr")
|
||||
if nostr_cols:
|
||||
return 7
|
||||
|
||||
# m006: invoices table
|
||||
invoices_cols = _get_table_columns(cursor, "invoices")
|
||||
if invoices_cols:
|
||||
return 6
|
||||
|
||||
# m005: proofs.id column, keysets table
|
||||
if "id" in proofs_cols and keysets_cols:
|
||||
return 5
|
||||
|
||||
# m004: p2sh table
|
||||
p2sh_cols = _get_table_columns(cursor, "p2sh")
|
||||
if p2sh_cols:
|
||||
return 4
|
||||
|
||||
# m003: proofs.send_id
|
||||
if "send_id" in proofs_cols:
|
||||
return 3
|
||||
|
||||
# m002: proofs.reserved
|
||||
if "reserved" in proofs_cols:
|
||||
return 2
|
||||
|
||||
# m001: proofs table exists
|
||||
return 1
|
||||
|
||||
|
||||
def fix_cashu_migrations() -> None:
|
||||
"""
|
||||
Fixes Cashu wallet migrations that are not idempotent.
|
||||
This specifically addresses the 'duplicate column name: public_keys' error
|
||||
in the keysets table of Cashu's internal SQLite databases.
|
||||
|
||||
Cashu's migration runner uses ALTER TABLE ADD COLUMN without checking
|
||||
if the column already exists. If the dbversions table is out of sync
|
||||
with the actual schema, re-running migrations crashes.
|
||||
|
||||
This function detects the real schema version by inspecting existing
|
||||
columns/tables and updates dbversions accordingly so cashu skips
|
||||
already-applied migrations.
|
||||
"""
|
||||
project_root = pathlib.Path(__file__).resolve().parents[2]
|
||||
wallet_dir = project_root / ".wallet"
|
||||
@@ -202,21 +303,35 @@ def fix_cashu_migrations() -> None:
|
||||
conn = sqlite3.connect(db_file)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if keysets table exists
|
||||
detected_version = _detect_schema_version(cursor)
|
||||
if detected_version == 0:
|
||||
conn.close()
|
||||
continue
|
||||
|
||||
# Ensure dbversions table and row exist
|
||||
cursor.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='keysets'"
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='dbversions'"
|
||||
)
|
||||
if not cursor.fetchone():
|
||||
conn.close()
|
||||
continue
|
||||
|
||||
# Check if public_keys column exists
|
||||
cursor.execute("PRAGMA table_info(keysets)")
|
||||
columns = [info[1] for info in cursor.fetchall()]
|
||||
cursor.execute("SELECT version FROM dbversions WHERE db = 'wallet'")
|
||||
row = cursor.fetchone()
|
||||
if row is None:
|
||||
conn.close()
|
||||
continue
|
||||
|
||||
if "public_keys" not in columns:
|
||||
logger.info(f"Adding missing public_keys column to {db_file.name}")
|
||||
cursor.execute("ALTER TABLE keysets ADD COLUMN public_keys TEXT")
|
||||
recorded_version = row[0]
|
||||
if recorded_version < detected_version:
|
||||
logger.info(
|
||||
f"Fixing {db_file.name}: dbversions says v{recorded_version} "
|
||||
f"but schema is v{detected_version}"
|
||||
)
|
||||
cursor.execute(
|
||||
"UPDATE dbversions SET version = ? WHERE db = 'wallet'",
|
||||
(detected_version,),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
conn.close()
|
||||
|
||||
+48
-6
@@ -2,6 +2,7 @@ import asyncio
|
||||
import math
|
||||
from typing import TypedDict
|
||||
|
||||
import httpx
|
||||
from cashu.core.base import Proof, Token
|
||||
from cashu.wallet.helpers import deserialize_token_from_string
|
||||
from cashu.wallet.wallet import Wallet
|
||||
@@ -13,6 +14,41 @@ from .payment.lnurl import raw_send_to_lnurl
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Cache of mint_url -> list of supported unit strings
|
||||
_mint_units_cache: dict[str, list[str]] = {}
|
||||
|
||||
|
||||
async def get_mint_units(mint_url: str) -> list[str]:
|
||||
"""Query a mint's /v1/keysets endpoint to discover its supported units."""
|
||||
if mint_url in _mint_units_cache:
|
||||
return _mint_units_cache[mint_url]
|
||||
|
||||
try:
|
||||
url = f"{mint_url.rstrip('/')}/v1/keysets"
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
resp = await client.get(url)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
units = list(
|
||||
{
|
||||
ks["unit"]
|
||||
for ks in data.get("keysets", [])
|
||||
if ks.get("active", False)
|
||||
}
|
||||
)
|
||||
if not units:
|
||||
logger.warning(f"No active keysets found for {mint_url}, defaulting to sat")
|
||||
units = ["sat"]
|
||||
|
||||
_mint_units_cache[mint_url] = units
|
||||
logger.info(f"Mint {mint_url} supports units: {units}")
|
||||
return units
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to query keysets for {mint_url}: {e}, defaulting to sat")
|
||||
_mint_units_cache[mint_url] = ["sat"]
|
||||
return ["sat"]
|
||||
|
||||
|
||||
async def get_balance(unit: str) -> int:
|
||||
wallet = await get_wallet(settings.primary_mint, unit)
|
||||
@@ -224,8 +260,6 @@ async def fetch_all_balances(
|
||||
- Total user balance in sats
|
||||
- Owner balance in sats (wallet - user)
|
||||
"""
|
||||
if units is None:
|
||||
units = ["sat", "msat"]
|
||||
|
||||
async def fetch_balance(
|
||||
session: db.AsyncSession, mint_url: str, unit: str
|
||||
@@ -261,12 +295,19 @@ async def fetch_all_balances(
|
||||
}
|
||||
return error_result
|
||||
|
||||
# Create tasks for all mint/unit combinations
|
||||
# Discover supported units for each mint, then create tasks
|
||||
mint_units_map: dict[str, list[str]] = {}
|
||||
for mint_url in settings.cashu_mints:
|
||||
if units is not None:
|
||||
mint_units_map[mint_url] = units
|
||||
else:
|
||||
mint_units_map[mint_url] = await get_mint_units(mint_url)
|
||||
|
||||
async with db.create_session() as session:
|
||||
tasks = [
|
||||
fetch_balance(session, mint_url, unit)
|
||||
for mint_url in settings.cashu_mints
|
||||
for unit in units
|
||||
for mint_url, mint_units in mint_units_map.items()
|
||||
for unit in mint_units
|
||||
]
|
||||
|
||||
# Run all tasks concurrently
|
||||
@@ -313,7 +354,8 @@ async def periodic_payout() -> None:
|
||||
try:
|
||||
async with db.create_session() as session:
|
||||
for mint_url in settings.cashu_mints:
|
||||
for unit in ["sat", "msat"]:
|
||||
mint_supported_units = await get_mint_units(mint_url)
|
||||
for unit in mint_supported_units:
|
||||
wallet = await get_wallet(mint_url, unit)
|
||||
proofs = get_proofs_per_mint_and_unit(
|
||||
wallet, mint_url, unit, not_reserved=True
|
||||
|
||||
Reference in New Issue
Block a user