mirror of
https://github.com/aljazceru/nutshell.git
synced 2026-01-07 02:44:19 +01:00
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:
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
@@ -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})")
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')}")
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
63
tests/test_mint_db.py
Normal 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
|
||||
57
tests/test_mint_keysets.py
Normal file
57
tests/test_mint_keysets.py
Normal 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"
|
||||
Reference in New Issue
Block a user