Keysets per seed and postgres (#400)

* allow generation of keys per seed phrase

* emit errors correctly

* parse timestamps for melt and mint quotes correctly

* error messages

* adjust error message

* postgres works

* prepare postgres tests

* timestamps refactor

* add command to key activation

* generate keys per seed

* add keyset tests

* keyest uniqueness constaint on (derivation_path, seed)

* add tables ony if not exists

* log leve
This commit is contained in:
callebtc
2024-02-02 21:25:02 +01:00
committed by GitHub
parent bc2b555c16
commit 6ddce571a0
12 changed files with 352 additions and 103 deletions

View File

@@ -17,7 +17,7 @@ jobs:
poetry-version: ["1.7.1"]
mint-cache-secrets: ["false", "true"]
mint-only-deprecated: ["false", "true"]
# db-url: ["", "postgres://cashu:cashu@localhost:5432/test"] # TODO: Postgres test not working
# db-url: ["", "postgres://cashu:cashu@localhost:5432/cashu"] # TODO: Postgres test not working
db-url: [""]
backend-wallet-class: ["FakeWallet"]
uses: ./.github/workflows/tests.yml

View File

@@ -218,11 +218,37 @@ class MeltQuote(BaseModel):
amount: int
fee_reserve: int
paid: bool
created_time: int = 0
paid_time: int = 0
created_time: Union[int, None] = None
paid_time: Union[int, None] = None
fee_paid: int = 0
proof: str = ""
@classmethod
def from_row(cls, row: Row):
try:
created_time = int(row["created_time"]) if row["created_time"] else None
paid_time = int(row["paid_time"]) if row["paid_time"] else None
except Exception:
created_time = (
int(row["created_time"].timestamp()) if row["created_time"] else None
)
paid_time = int(row["paid_time"].timestamp()) if row["paid_time"] else None
return cls(
quote=row["quote"],
method=row["method"],
request=row["request"],
checking_id=row["checking_id"],
unit=row["unit"],
amount=row["amount"],
fee_reserve=row["fee_reserve"],
paid=row["paid"],
created_time=created_time,
paid_time=paid_time,
fee_paid=row["fee_paid"],
proof=row["proof"],
)
class MintQuote(BaseModel):
quote: str
@@ -233,10 +259,37 @@ class MintQuote(BaseModel):
amount: int
paid: bool
issued: bool
created_time: int = 0
paid_time: int = 0
created_time: Union[int, None] = None
paid_time: Union[int, None] = None
expiry: int = 0
@classmethod
def from_row(cls, row: Row):
try:
# SQLITE: row is timestamp (string)
created_time = int(row["created_time"]) if row["created_time"] else None
paid_time = int(row["paid_time"]) if row["paid_time"] else None
except Exception:
# POSTGRES: row is datetime.datetime
created_time = (
int(row["created_time"].timestamp()) if row["created_time"] else None
)
paid_time = int(row["paid_time"].timestamp()) if row["paid_time"] else None
return cls(
quote=row["quote"],
method=row["method"],
request=row["request"],
checking_id=row["checking_id"],
unit=row["unit"],
amount=row["amount"],
paid=row["paid"],
issued=row["issued"],
created_time=created_time,
paid_time=paid_time,
expiry=0,
)
# ------- API -------
@@ -640,7 +693,7 @@ class MintKeyset:
active: bool
unit: Unit
derivation_path: str
seed: Optional[str] = None
seed: str
public_keys: Union[Dict[int, PublicKey], None] = None
valid_from: Union[str, None] = None
valid_to: Union[str, None] = None
@@ -652,17 +705,17 @@ class MintKeyset:
def __init__(
self,
*,
seed: str,
derivation_path: str,
id="",
valid_from=None,
valid_to=None,
first_seen=None,
active=None,
seed: Optional[str] = None,
derivation_path: Optional[str] = None,
unit: Optional[str] = None,
version: str = "0",
):
self.derivation_path = derivation_path or ""
self.derivation_path = derivation_path
self.seed = seed
self.id = id
self.valid_from = valid_from
@@ -696,8 +749,10 @@ class MintKeyset:
self.unit = Unit[unit]
# generate keys from seed
if self.seed and self.derivation_path:
self.generate_keys()
assert self.seed, "seed not set"
assert self.derivation_path, "derivation path not set"
self.generate_keys()
logger.debug(f"Keyset id: {self.id} ({self.unit.name})")

View File

@@ -31,7 +31,8 @@ class Compat:
if self.type in {POSTGRES, COCKROACH}:
return "now()"
elif self.type == SQLITE:
return "(strftime('%s', 'now'))"
# return "(strftime('%s', 'now'))"
return str(int(time.time()))
return "<nothing>"
@property
@@ -204,6 +205,26 @@ def lock_table(db: Database, table: str) -> str:
return "<nothing>"
def timestamp_from_seconds(
db: Database, seconds: Union[int, float, None]
) -> Union[str, None]:
if seconds is 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(db: Database) -> str:
timestamp = timestamp_from_seconds(db, time.time())
if timestamp is None:
raise Exception("Timestamp is None")
return timestamp
@asynccontextmanager
async def get_db_connection(db: Database, conn: Optional[Connection] = None):
"""Either yield the existing database connection or create a new one.

View File

@@ -12,7 +12,7 @@ class CashuError(Exception):
class NotAllowedError(CashuError):
detail = "Not allowed."
detail = "not allowed"
code = 10000
def __init__(self, detail: Optional[str] = None, code: Optional[int] = None):
@@ -20,7 +20,7 @@ class NotAllowedError(CashuError):
class TransactionError(CashuError):
detail = "Transaction error."
detail = "transaction error"
code = 11000
def __init__(self, detail: Optional[str] = None, code: Optional[int] = None):
@@ -36,7 +36,7 @@ class TokenAlreadySpentError(TransactionError):
class SecretTooLongError(TransactionError):
detail = "Secret too long."
detail = "secret too long"
code = 11003
def __init__(self):
@@ -44,7 +44,7 @@ class SecretTooLongError(TransactionError):
class NoSecretInProofsError(TransactionError):
detail = "No secret in proofs."
detail = "no secret in proofs"
code = 11004
def __init__(self):
@@ -52,7 +52,7 @@ class NoSecretInProofsError(TransactionError):
class KeysetError(CashuError):
detail = "Keyset error."
detail = "keyset error"
code = 12000
def __init__(self, detail: Optional[str] = None, code: Optional[int] = None):
@@ -60,7 +60,7 @@ class KeysetError(CashuError):
class KeysetNotFoundError(KeysetError):
detail = "Keyset not found."
detail = "keyset not found"
code = 12001
def __init__(self):
@@ -68,15 +68,15 @@ class KeysetNotFoundError(KeysetError):
class LightningError(CashuError):
detail = "Lightning error."
detail = "Lightning error"
code = 20000
def __init__(self, detail: Optional[str] = None, code: Optional[int] = None):
super().__init__(detail or self.detail, code=code or self.code)
class InvoiceNotPaidError(CashuError):
detail = "Lightning invoice not paid yet."
class QuoteNotPaidError(CashuError):
detail = "quote not paid"
code = 20001
def __init__(self):

View File

@@ -1,4 +1,3 @@
import time
from abc import ABC, abstractmethod
from typing import Any, List, Optional
@@ -9,7 +8,13 @@ from ..core.base import (
MintQuote,
Proof,
)
from ..core.db import Connection, Database, table_with_schema
from ..core.db import (
Connection,
Database,
table_with_schema,
timestamp_from_seconds,
timestamp_now,
)
class LedgerCrud(ABC):
@@ -27,6 +32,7 @@ class LedgerCrud(ABC):
db: Database,
id: str = "",
derivation_path: str = "",
seed: str = "",
conn: Optional[Connection] = None,
) -> List[MintKeyset]: ...
@@ -223,7 +229,7 @@ class LedgerCrudSqlite(LedgerCrud):
e,
s,
id,
int(time.time()),
timestamp_now(db),
),
)
@@ -274,7 +280,7 @@ class LedgerCrudSqlite(LedgerCrud):
proof.secret,
proof.id,
proof.witness,
int(time.time()),
timestamp_now(db),
),
)
@@ -307,7 +313,7 @@ class LedgerCrudSqlite(LedgerCrud):
proof.amount,
str(proof.C),
str(proof.secret),
int(time.time()),
timestamp_now(db),
),
)
@@ -348,8 +354,8 @@ class LedgerCrudSqlite(LedgerCrud):
quote.amount,
quote.issued,
quote.paid,
quote.created_time,
quote.paid_time,
timestamp_from_seconds(db, quote.created_time),
timestamp_from_seconds(db, quote.paid_time),
),
)
@@ -367,7 +373,7 @@ class LedgerCrudSqlite(LedgerCrud):
""",
(quote_id,),
)
return MintQuote(**dict(row)) if row else None
return MintQuote.from_row(row) if row else None
async def get_mint_quote_by_checking_id(
self,
@@ -383,7 +389,7 @@ class LedgerCrudSqlite(LedgerCrud):
""",
(checking_id,),
)
return MintQuote(**dict(row)) if row else None
return MintQuote.from_row(row) if row else None
async def update_mint_quote(
self,
@@ -398,7 +404,7 @@ class LedgerCrudSqlite(LedgerCrud):
(
quote.issued,
quote.paid,
quote.paid_time,
timestamp_from_seconds(db, quote.paid_time),
quote.quote,
),
)
@@ -442,8 +448,8 @@ class LedgerCrudSqlite(LedgerCrud):
quote.amount,
quote.fee_reserve or 0,
quote.paid,
quote.created_time,
quote.paid_time,
timestamp_from_seconds(db, quote.created_time),
timestamp_from_seconds(db, quote.paid_time),
quote.fee_paid,
quote.proof,
),
@@ -481,7 +487,7 @@ class LedgerCrudSqlite(LedgerCrud):
)
if row is None:
return None
return MeltQuote(**dict(row)) if row else None
return MeltQuote.from_row(row) if row else None
async def update_melt_quote(
self,
@@ -496,7 +502,7 @@ class LedgerCrudSqlite(LedgerCrud):
(
quote.paid,
quote.fee_paid,
quote.paid_time,
timestamp_from_seconds(db, quote.paid_time),
quote.proof,
quote.quote,
),
@@ -519,9 +525,9 @@ class LedgerCrudSqlite(LedgerCrud):
keyset.id,
keyset.seed,
keyset.derivation_path,
keyset.valid_from or int(time.time()),
keyset.valid_to or int(time.time()),
keyset.first_seen or int(time.time()),
keyset.valid_from or timestamp_now(db),
keyset.valid_to or timestamp_now(db),
keyset.first_seen or timestamp_now(db),
True,
keyset.version,
keyset.unit.name,
@@ -545,6 +551,7 @@ class LedgerCrudSqlite(LedgerCrud):
db: Database,
id: Optional[str] = None,
derivation_path: Optional[str] = None,
seed: Optional[str] = None,
unit: Optional[str] = None,
active: Optional[bool] = None,
conn: Optional[Connection] = None,
@@ -560,6 +567,9 @@ class LedgerCrudSqlite(LedgerCrud):
if derivation_path is not None:
clauses.append("derivation_path = ?")
values.append(derivation_path)
if seed is not None:
clauses.append("seed = ?")
values.append(seed)
if unit is not None:
clauses.append("unit = ?")
values.append(unit)

View File

@@ -38,6 +38,7 @@ from ..core.errors import (
KeysetNotFoundError,
LightningError,
NotAllowedError,
QuoteNotPaidError,
TransactionError,
)
from ..core.helpers import sum_proofs
@@ -81,7 +82,14 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
# ------- KEYS -------
async def activate_keyset(self, derivation_path, autosave=True) -> MintKeyset:
async def activate_keyset(
self,
*,
derivation_path: str,
seed: Optional[str] = None,
version: Optional[str] = None,
autosave=True,
) -> MintKeyset:
"""Load the keyset for a derivation path if it already exists. If not generate new one and store in the db.
Args:
@@ -91,29 +99,25 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
Returns:
MintKeyset: Keyset
"""
assert derivation_path, "derivation path not set"
seed = seed or self.master_key
logger.debug(f"Activating keyset for derivation path {derivation_path}")
# load the keyset from db
logger.trace(f"crud: loading keyset for {derivation_path}")
tmp_keyset_local: List[MintKeyset] = await self.crud.get_keyset(
derivation_path=derivation_path, db=self.db
derivation_path=derivation_path, seed=seed, db=self.db
)
logger.trace(f"crud: loaded {len(tmp_keyset_local)} keysets")
if tmp_keyset_local:
# we have a keyset with this derivation path in the database
keyset = tmp_keyset_local[0]
# we keys are not stored in the database but only their derivation path
# so we might need to generate the keys for keysets loaded from the database
if not len(keyset.private_keys):
keyset.generate_keys()
else:
logger.trace(f"crud: no keyset for {derivation_path}")
# no keyset for this derivation path yet
# we create a new keyset (keys will be generated at instantiation)
keyset = MintKeyset(
seed=self.master_key,
seed=seed or self.master_key,
derivation_path=derivation_path,
version=settings.version,
version=version or settings.version,
)
logger.debug(f"Generated new keyset {keyset.id}.")
if autosave:
@@ -144,33 +148,24 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
not in the database yet. Will be passed to `self.activate_keyset` where it is
generated from `self.derivation_path`. Defaults to True.
"""
# load all past keysets from db
# load all past keysets from db, the keys will be generated at instantiation
tmp_keysets: List[MintKeyset] = await self.crud.get_keyset(db=self.db)
logger.debug(
f"Loaded {len(tmp_keysets)} keysets from database. Generating keys..."
)
# add keysets from db to current keysets
# add keysets from db to memory
for k in tmp_keysets:
self.keysets[k.id] = k
# generate keys for all keysets in the database
for _, v in self.keysets.items():
# if we already generated the keys for this keyset, skip
if v.id and v.public_keys and len(v.public_keys):
continue
logger.trace(f"Generating keys for keyset {v.id}")
v.seed = self.master_key
v.generate_keys()
logger.info(f"Initialized {len(self.keysets)} keysets from the database.")
# activate the current keyset set by self.derivation_path
self.keyset = await self.activate_keyset(self.derivation_path, autosave)
if self.derivation_path:
self.keyset = await self.activate_keyset(
derivation_path=self.derivation_path, autosave=autosave
)
logger.info(f"Current keyset: {self.keyset.id}")
logger.info(
"Activated keysets from database:"
f"Loaded {len(self.keysets)} keysets:"
f" {[f'{k} ({v.unit.name})' for k, v in self.keysets.items()]}"
)
logger.info(f"Current keyset: {self.keyset.id}")
# check that we have a least one active keyset
assert any([k.active for k in self.keysets.values()]), "No active keyset found."
@@ -189,7 +184,6 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
self.keysets[keyset_copy.id] = keyset_copy
# remember which keyset this keyset was duplicated from
logger.debug(f"Duplicated keyset id {keyset.id} -> {keyset_copy.id}")
# END BACKWARDS COMPATIBILITY < 0.15.0
def get_keyset(self, keyset_id: Optional[str] = None) -> Dict[int, str]:
@@ -295,6 +289,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
MintQuote: Mint quote object.
"""
logger.trace("called request_mint")
assert quote_request.amount > 0, "amount must be positive"
if settings.mint_max_peg_in and quote_request.amount > settings.mint_max_peg_in:
raise NotAllowedError(
f"Maximum mint amount is {settings.mint_max_peg_in} sat."
@@ -368,6 +363,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
if status.paid:
logger.trace(f"Setting quote {quote_id} as paid")
quote.paid = True
quote.paid_time = int(time.time())
await self.crud.update_mint_quote(quote=quote, db=self.db)
return quote
@@ -404,7 +400,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
) # create a new lock if it doesn't exist
async with self.locks[quote_id]:
quote = await self.get_mint_quote(quote_id=quote_id)
assert quote.paid, "quote not paid"
assert quote.paid, QuoteNotPaidError()
assert not quote.issued, "quote already issued"
assert (
quote.amount == sum_amount_outputs
@@ -593,6 +589,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
await self.crud.update_melt_quote(quote=melt_quote, db=self.db)
mint_quote.paid = True
mint_quote.paid_time = melt_quote.paid_time
await self.crud.update_mint_quote(quote=mint_quote, db=self.db)
return melt_quote

View File

@@ -1,6 +1,4 @@
import time
from ..core.db import Connection, Database, table_with_schema
from ..core.db import SQLITE, Connection, Database, table_with_schema, timestamp_now
from ..core.settings import settings
@@ -224,18 +222,34 @@ async def m010_add_index_to_proofs_used(db: Database):
async def m011_add_quote_tables(db: Database):
async def get_columns(db: Database, conn: Connection, table: str):
if db.type == SQLITE:
query = f"PRAGMA table_info({table})"
else:
query = (
"SELECT column_name FROM information_schema.columns WHERE table_name ="
f" '{table}'"
)
res = await conn.execute(query)
if db.type == SQLITE:
return [r["name"] async for r in res]
else:
return [r["column_name"] async for r in res]
async with db.connect() as conn:
# add column "created" to tables invoices, promises, proofs_used, proofs_pending
tables = ["invoices", "promises", "proofs_used", "proofs_pending"]
for table in tables:
await conn.execute(
f"ALTER TABLE {table_with_schema(db, table)} ADD COLUMN created"
" TIMESTAMP"
)
await conn.execute(
f"UPDATE {table_with_schema(db, table)} SET created ="
f" '{int(time.time())}'"
)
columns = await get_columns(db, conn, table)
if "created" not in columns:
await conn.execute(
f"ALTER TABLE {table_with_schema(db, table)} ADD COLUMN created"
" TIMESTAMP"
)
await conn.execute(
f"UPDATE {table_with_schema(db, table)} SET created ="
f" '{timestamp_now(db)}'"
)
# add column "witness" to table proofs_used
await conn.execute(
@@ -299,8 +313,47 @@ async def m011_add_quote_tables(db: Database):
f"INSERT INTO {table_with_schema(db, 'mint_quotes')} (quote, method,"
" request, checking_id, unit, amount, paid, issued, created_time,"
" paid_time) SELECT id, 'bolt11', bolt11, payment_hash, 'sat', amount,"
f" False, issued, created, 0 FROM {table_with_schema(db, 'invoices')} "
f" False, issued, created, NULL FROM {table_with_schema(db, 'invoices')} "
)
# drop table invoices
await conn.execute(f"DROP TABLE {table_with_schema(db, 'invoices')}")
async def m012_keysets_uniqueness_with_seed(db: Database):
# copy table keysets to keysets_old, create a new table keysets
# with the same columns but with a unique constraint on (seed, derivation_path)
# and copy the data from keysets_old to keysets, then drop keysets_old
async with db.connect() as conn:
await conn.execute(
f"DROP TABLE IF EXISTS {table_with_schema(db, 'keysets_old')}"
)
await conn.execute(
f"CREATE TABLE {table_with_schema(db, 'keysets_old')} AS"
f" SELECT * FROM {table_with_schema(db, 'keysets')}"
)
await conn.execute(f"DROP TABLE {table_with_schema(db, 'keysets')}")
await conn.execute(f"""
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'keysets')} (
id TEXT NOT NULL,
derivation_path TEXT,
seed TEXT,
valid_from TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
valid_to TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
first_seen TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
active BOOL DEFAULT TRUE,
version TEXT,
unit TEXT,
UNIQUE (seed, derivation_path)
);
""")
await conn.execute(
f"INSERT INTO {table_with_schema(db, 'keysets')} (id,"
" derivation_path, valid_from, valid_to, first_seen,"
" active, version, seed, unit) SELECT id, derivation_path,"
" valid_from, valid_to, first_seen, active, version, seed,"
f" unit FROM {table_with_schema(db, 'keysets_old')}"
)
await conn.execute(f"DROP TABLE {table_with_schema(db, 'keysets_old')}")

View File

@@ -1,6 +1,6 @@
from typing import Any, Dict, List
from fastapi import APIRouter, Request
from fastapi import APIRouter
from loguru import logger
from ..core.base import (
@@ -80,8 +80,7 @@ async def info() -> GetInfoResponse:
name="Mint public keys",
summary="Get the public keys of the newest mint keyset",
response_description=(
"A dictionary of all supported token values of the mint and their associated"
" public key of the current keyset."
"All supported token values their associated public keys for all active keysets"
),
response_model=KeysResponse,
)
@@ -107,12 +106,12 @@ async def keys():
name="Keyset public keys",
summary="Public keys of a specific keyset",
response_description=(
"A dictionary of all supported token values of the mint and their associated"
"All supported token values of the mint and their associated"
" public key for a specific keyset."
),
response_model=KeysResponse,
)
async def keyset_keys(keyset_id: str, request: Request) -> KeysResponse:
async def keyset_keys(keyset_id: str) -> KeysResponse:
"""
Get the public keys of the mint from a specific keyset id.
"""
@@ -127,7 +126,7 @@ async def keyset_keys(keyset_id: str, request: Request) -> KeysResponse:
keyset = ledger.keysets.get(keyset_id)
if keyset is None:
raise CashuError(code=0, detail="Keyset not found.")
raise CashuError(code=0, detail="keyset not found")
keyset_for_response = KeysResponseKeyset(
id=keyset.id,
@@ -172,12 +171,6 @@ async def mint_quote(payload: PostMintQuoteRequest) -> PostMintQuoteResponse:
Call `POST /v1/mint/bolt11` after paying the invoice.
"""
logger.trace(f"> POST /v1/mint/quote/bolt11: payload={payload}")
amount = payload.amount
if amount > 21_000_000 * 100_000_000 or amount <= 0:
raise CashuError(code=0, detail="Amount must be a valid amount of sat.")
if settings.mint_peg_out_only:
raise CashuError(code=0, detail="Mint does not allow minting new tokens.")
quote = await ledger.mint_quote(payload)
resp = PostMintQuoteResponse(
request=quote.request,
@@ -213,8 +206,8 @@ async def get_mint_quote(quote: str) -> PostMintQuoteResponse:
@router.post(
"/v1/mint/bolt11",
name="Mint tokens",
summary="Mint tokens in exchange for a Bitcoin payment that the user has made",
name="Mint tokens with a Lightning payment",
summary="Mint tokens by paying a bolt11 Lightning invoice.",
response_model=PostMintResponse,
response_description=(
"A list of blinded signatures that can be used to create proofs."
@@ -311,7 +304,7 @@ async def melt(payload: PostMeltRequest) -> PostMeltResponse:
"An array of blinded signatures that can be used to create proofs."
),
)
async def split(
async def swap(
payload: PostSplitRequest,
) -> PostSplitResponse:
"""

View File

@@ -1,4 +1,4 @@
from typing import List, Optional
from typing import Dict, List, Optional
from fastapi import APIRouter
from loguru import logger
@@ -70,7 +70,7 @@ async def info() -> GetInfoResponse_deprecated:
response_model=KeysResponse_deprecated,
deprecated=True,
)
async def keys_deprecated():
async def keys_deprecated() -> Dict[str, str]:
"""This endpoint returns a dictionary of all supported token values of the mint and their associated public key."""
logger.trace("> GET /keys")
keyset = ledger.get_keyset()
@@ -86,10 +86,10 @@ async def keys_deprecated():
"A dictionary of all supported token values of the mint and their associated"
" public key for a specific keyset."
),
response_model=KeysResponse_deprecated,
response_model=Dict[str, str],
deprecated=True,
)
async def keyset_deprecated(idBase64Urlsafe: str):
async def keyset_deprecated(idBase64Urlsafe: str) -> Dict[str, str]:
"""
Get the public keys of the mint from a specific keyset id.
The id is encoded in idBase64Urlsafe (by a wallet) and is converted back to
@@ -323,7 +323,7 @@ async def split_deprecated(
),
deprecated=True,
)
async def check_spendable(
async def check_spendable_deprecated(
payload: CheckSpendableRequest_deprecated,
) -> CheckSpendableResponse_deprecated:
"""Check whether a secret has been spent already or not."""

View File

@@ -56,7 +56,7 @@ async def rotate_keys(n_seconds=60):
incremented_derivation_path = (
"/".join(ledger.derivation_path.split("/")[:-1]) + f"/{i}"
)
await ledger.activate_keyset(incremented_derivation_path)
await ledger.activate_keyset(derivation_path=incremented_derivation_path)
logger.info(f"Current keyset: {ledger.keyset.id}")
await asyncio.sleep(n_seconds)
@@ -68,7 +68,7 @@ async def start_mint_init():
await ledger.init_keysets()
for derivation_path in settings.mint_derivation_path_list:
await ledger.activate_keyset(derivation_path)
await ledger.activate_keyset(derivation_path=derivation_path)
for method in ledger.backends:
for unit in ledger.backends[method]:

63
tests/test_mint_db.py Normal file
View File

@@ -0,0 +1,63 @@
import pytest
import pytest_asyncio
from cashu.core.base import PostMeltQuoteRequest
from cashu.mint.ledger import Ledger
from cashu.wallet.wallet import Wallet
from cashu.wallet.wallet import Wallet as Wallet1
from tests.conftest import SERVER_ENDPOINT
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")
@pytest_asyncio.fixture(scope="function")
async def wallet1(ledger: Ledger):
wallet1 = await Wallet1.with_db(
url=SERVER_ENDPOINT,
db="test_data/wallet1",
name="wallet1",
)
await wallet1.load_mint()
yield wallet1
@pytest.mark.asyncio
async def test_mint_quote(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.amount == 128
assert quote.unit == "sat"
assert not quote.paid
assert quote.checking_id == invoice.payment_hash
assert quote.paid_time is None
assert quote.created_time
@pytest.mark.asyncio
async def test_melt_quote(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.amount == 128
assert quote.unit == "sat"
assert not quote.paid
assert quote.checking_id == invoice.payment_hash
assert quote.paid_time is None
assert quote.created_time

View File

@@ -0,0 +1,57 @@
import pytest
from cashu.core.base import MintKeyset
from cashu.core.settings import settings
SEED = "TEST_PRIVATE_KEY"
DERIVATION_PATH = "m/0'/0'/0'"
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")
@pytest.mark.asyncio
async def test_keyset_0_15_0():
keyset = MintKeyset(seed=SEED, derivation_path=DERIVATION_PATH, version="0.15.0")
assert len(keyset.public_keys_hex) == settings.max_order
assert keyset.seed == "TEST_PRIVATE_KEY"
assert keyset.derivation_path == "m/0'/0'/0'"
assert (
keyset.public_keys_hex[1]
== "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104"
)
assert keyset.id == "009a1f293253e41e"
@pytest.mark.asyncio
async def test_keyset_0_14_0():
keyset = MintKeyset(seed=SEED, derivation_path=DERIVATION_PATH, version="0.14.0")
assert len(keyset.public_keys_hex) == settings.max_order
assert keyset.seed == "TEST_PRIVATE_KEY"
assert keyset.derivation_path == "m/0'/0'/0'"
assert (
keyset.public_keys_hex[1]
== "036d6f3adf897e88e16ece3bffb2ce57a0b635fa76f2e46dbe7c636a937cd3c2f2"
)
assert keyset.id == "xnI+Y0j7cT1/"
@pytest.mark.asyncio
async def test_keyset_0_11_0():
keyset = MintKeyset(seed=SEED, derivation_path=DERIVATION_PATH, version="0.11.0")
assert len(keyset.public_keys_hex) == settings.max_order
assert keyset.seed == "TEST_PRIVATE_KEY"
assert keyset.derivation_path == "m/0'/0'/0'"
assert (
keyset.public_keys_hex[1]
== "026b714529f157d4c3de5a93e3a67618475711889b6434a497ae6ad8ace6682120"
)
assert keyset.id == "Zkdws9zWxNc4"