mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 10:34:20 +01:00
* auth server * cleaning up * auth ledger class * class variables -> instance variables * annotations * add models and api route * custom amount and api prefix * add auth db * blind auth token working * jwt working * clean up * JWT works * using openid connect server * use oauth server with password flow * new realm * add keycloak docker * hopefully not garbage * auth works * auth kinda working * fix cli * auth works for send and receive * pass auth_db to Wallet * auth in info * refactor * fix supported * cache mint info * fix settings and endpoints * add description to .env.example * track changes for openid connect client * store mint in db * store credentials * clean up v1_api.py * load mint info into auth wallet * fix first login * authenticate if refresh token fails * clear auth also middleware * use regex * add cli command * pw works * persist keyset amounts * add errors.py * do not start auth server if disabled in config * upadte poetry * disvoery url * fix test * support device code flow * adopt latest spec changes * fix code flow * mint max bat dynamic * mypy ignore * fix test * do not serialize amount in authproof * all auth flows working * fix tests * submodule * refactor * test * dont sleep * test * add wallet auth tests * test differently * test only keycloak for now * fix creds * daemon * fix test * install everything * install jinja * delete wallet for every test * auth: use global rate limiter * test auth rate limit * keycloak hostname * move keycloak test data * reactivate all tests * add readme * load proofs * remove unused code * remove unused code * implement change suggestions by ok300 * add error codes * test errors
637 lines
16 KiB
Python
637 lines
16 KiB
Python
import json
|
|
import time
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
from ..core.base import (
|
|
MeltQuote,
|
|
MeltQuoteState,
|
|
MintQuote,
|
|
MintQuoteState,
|
|
Proof,
|
|
WalletKeyset,
|
|
WalletMint,
|
|
)
|
|
from ..core.db import Connection, Database
|
|
|
|
|
|
async def store_proof(
|
|
proof: Proof,
|
|
db: Database,
|
|
conn: Optional[Connection] = None,
|
|
) -> None:
|
|
await (conn or db).execute(
|
|
"""
|
|
INSERT INTO proofs
|
|
(id, amount, C, secret, time_created, derivation_path, dleq, mint_id, melt_id)
|
|
VALUES (:id, :amount, :C, :secret, :time_created, :derivation_path, :dleq, :mint_id, :melt_id)
|
|
""",
|
|
{
|
|
"id": proof.id,
|
|
"amount": proof.amount,
|
|
"C": str(proof.C),
|
|
"secret": str(proof.secret),
|
|
"time_created": int(time.time()),
|
|
"derivation_path": proof.derivation_path,
|
|
"dleq": json.dumps(proof.dleq.dict()) if proof.dleq else "",
|
|
"mint_id": proof.mint_id,
|
|
"melt_id": proof.melt_id,
|
|
},
|
|
)
|
|
|
|
|
|
async def get_proofs(
|
|
*,
|
|
db: Database,
|
|
id: Optional[str] = "",
|
|
melt_id: str = "",
|
|
mint_id: str = "",
|
|
table: str = "proofs",
|
|
conn: Optional[Connection] = None,
|
|
):
|
|
clauses = []
|
|
values: Dict[str, Any] = {}
|
|
|
|
if id:
|
|
clauses.append("id = :id")
|
|
values["id"] = id
|
|
if melt_id:
|
|
clauses.append("melt_id = :melt_id")
|
|
values["melt_id"] = melt_id
|
|
if mint_id:
|
|
clauses.append("mint_id = :mint_id")
|
|
values["mint_id"] = mint_id
|
|
where = ""
|
|
if clauses:
|
|
where = f"WHERE {' AND '.join(clauses)}"
|
|
rows = await (conn or db).fetchall(
|
|
f"""
|
|
SELECT * from {table}
|
|
{where}
|
|
""",
|
|
values,
|
|
)
|
|
return [Proof.from_dict(dict(r)) for r in rows] if rows else []
|
|
|
|
|
|
async def get_reserved_proofs(
|
|
db: Database,
|
|
conn: Optional[Connection] = None,
|
|
) -> List[Proof]:
|
|
rows = await (conn or db).fetchall(
|
|
"""
|
|
SELECT * from proofs
|
|
WHERE reserved
|
|
"""
|
|
)
|
|
return [Proof.from_dict(dict(r)) for r in rows]
|
|
|
|
|
|
async def invalidate_proof(
|
|
proof: Proof,
|
|
db: Database,
|
|
conn: Optional[Connection] = None,
|
|
) -> None:
|
|
await (conn or db).execute(
|
|
"""
|
|
DELETE FROM proofs
|
|
WHERE secret = :secret
|
|
""",
|
|
{"secret": str(proof["secret"])},
|
|
)
|
|
|
|
await (conn or db).execute(
|
|
"""
|
|
INSERT INTO proofs_used
|
|
(amount, C, secret, time_used, id, derivation_path, mint_id, melt_id)
|
|
VALUES (:amount, :C, :secret, :time_used, :id, :derivation_path, :mint_id, :melt_id)
|
|
""",
|
|
{
|
|
"amount": proof.amount,
|
|
"C": str(proof.C),
|
|
"secret": str(proof.secret),
|
|
"time_used": int(time.time()),
|
|
"id": proof.id,
|
|
"derivation_path": proof.derivation_path,
|
|
"mint_id": proof.mint_id,
|
|
"melt_id": proof.melt_id,
|
|
},
|
|
)
|
|
|
|
|
|
async def update_proof(
|
|
proof: Proof,
|
|
*,
|
|
reserved: Optional[bool] = None,
|
|
send_id: Optional[str] = None,
|
|
mint_id: Optional[str] = None,
|
|
melt_id: Optional[str] = None,
|
|
db: Optional[Database] = None,
|
|
conn: Optional[Connection] = None,
|
|
) -> None:
|
|
clauses = []
|
|
values: Dict[str, Any] = {}
|
|
|
|
if reserved is not None:
|
|
clauses.append("reserved = :reserved")
|
|
values["reserved"] = reserved
|
|
clauses.append("time_reserved = :time_reserved")
|
|
values["time_reserved"] = int(time.time())
|
|
|
|
if send_id is not None:
|
|
clauses.append("send_id = :send_id")
|
|
values["send_id"] = send_id
|
|
|
|
if mint_id is not None:
|
|
clauses.append("mint_id = :mint_id")
|
|
values["mint_id"] = mint_id
|
|
|
|
if melt_id is not None:
|
|
clauses.append("melt_id = :melt_id")
|
|
values["melt_id"] = melt_id
|
|
|
|
await (conn or db).execute( # type: ignore
|
|
f"UPDATE proofs SET {', '.join(clauses)} WHERE secret = :secret",
|
|
{**values, "secret": str(proof.secret)},
|
|
)
|
|
|
|
|
|
async def secret_used(
|
|
secret: str,
|
|
db: Database,
|
|
conn: Optional[Connection] = None,
|
|
) -> bool:
|
|
rows = await (conn or db).fetchone(
|
|
"""
|
|
SELECT * from proofs
|
|
WHERE secret = :secret
|
|
""",
|
|
{"secret": secret},
|
|
)
|
|
return rows is not None
|
|
|
|
|
|
async def store_keyset(
|
|
keyset: WalletKeyset,
|
|
mint_url: str = "",
|
|
db: Optional[Database] = None,
|
|
conn: Optional[Connection] = None,
|
|
) -> None:
|
|
await (conn or db).execute( # type: ignore
|
|
"""
|
|
INSERT INTO keysets
|
|
(id, mint_url, valid_from, valid_to, first_seen, active, public_keys, unit, input_fee_ppk)
|
|
VALUES (:id, :mint_url, :valid_from, :valid_to, :first_seen, :active, :public_keys, :unit, :input_fee_ppk)
|
|
""",
|
|
{
|
|
"id": keyset.id,
|
|
"mint_url": mint_url or keyset.mint_url,
|
|
"valid_from": keyset.valid_from or int(time.time()),
|
|
"valid_to": keyset.valid_to or int(time.time()),
|
|
"first_seen": keyset.first_seen or int(time.time()),
|
|
"active": keyset.active,
|
|
"public_keys": keyset.serialize(),
|
|
"unit": keyset.unit.name,
|
|
"input_fee_ppk": keyset.input_fee_ppk,
|
|
},
|
|
)
|
|
|
|
|
|
async def get_keysets(
|
|
id: str = "",
|
|
mint_url: Optional[str] = None,
|
|
unit: Optional[str] = None,
|
|
db: Optional[Database] = None,
|
|
conn: Optional[Connection] = None,
|
|
) -> List[WalletKeyset]:
|
|
clauses = []
|
|
values: Dict[str, Any] = {}
|
|
if id:
|
|
clauses.append("id = :id")
|
|
values["id"] = id
|
|
if mint_url:
|
|
clauses.append("mint_url = :mint_url")
|
|
values["mint_url"] = mint_url
|
|
if unit:
|
|
clauses.append("unit = :unit")
|
|
values["unit"] = unit
|
|
where = ""
|
|
if clauses:
|
|
where = f"WHERE {' AND '.join(clauses)}"
|
|
|
|
rows = await (conn or db).fetchall( # type: ignore
|
|
f"""
|
|
SELECT * from keysets
|
|
{where}
|
|
""",
|
|
values,
|
|
)
|
|
return [WalletKeyset.from_row(r) for r in rows] # type: ignore
|
|
|
|
|
|
async def update_keyset(
|
|
keyset: WalletKeyset,
|
|
db: Database,
|
|
conn: Optional[Connection] = None,
|
|
) -> None:
|
|
await (conn or db).execute(
|
|
"""
|
|
UPDATE keysets
|
|
SET active = :active
|
|
WHERE id = :id
|
|
""",
|
|
{
|
|
"active": keyset.active,
|
|
"id": keyset.id,
|
|
},
|
|
)
|
|
|
|
|
|
async def store_bolt11_mint_quote(
|
|
db: Database,
|
|
quote: MintQuote,
|
|
conn: Optional[Connection] = None,
|
|
) -> None:
|
|
await (conn or db).execute(
|
|
"""
|
|
INSERT INTO bolt11_mint_quotes
|
|
(quote, mint, method, request, checking_id, unit, amount, state, created_time, paid_time, expiry, privkey)
|
|
VALUES (:quote, :mint, :method, :request, :checking_id, :unit, :amount, :state, :created_time, :paid_time, :expiry, :privkey)
|
|
""",
|
|
{
|
|
"quote": quote.quote,
|
|
"mint": quote.mint,
|
|
"method": quote.method,
|
|
"request": quote.request,
|
|
"checking_id": quote.checking_id,
|
|
"unit": quote.unit,
|
|
"amount": quote.amount,
|
|
"state": quote.state.value,
|
|
"created_time": quote.created_time,
|
|
"paid_time": quote.paid_time,
|
|
"expiry": quote.expiry,
|
|
"privkey": quote.privkey or "",
|
|
},
|
|
)
|
|
|
|
|
|
async def get_bolt11_mint_quote(
|
|
db: Database,
|
|
quote: str | None = None,
|
|
request: str | None = None,
|
|
conn: Optional[Connection] = None,
|
|
) -> Optional[MintQuote]:
|
|
if not quote and not request:
|
|
raise ValueError("quote or request must be provided")
|
|
clauses = []
|
|
values: Dict[str, Any] = {}
|
|
if quote:
|
|
clauses.append("quote = :quote")
|
|
values["quote"] = quote
|
|
if request:
|
|
clauses.append("request = :request")
|
|
values["request"] = request
|
|
|
|
where = ""
|
|
if clauses:
|
|
where = f"WHERE {' AND '.join(clauses)}"
|
|
|
|
row = await (conn or db).fetchone(
|
|
f"""
|
|
SELECT * from bolt11_mint_quotes
|
|
{where}
|
|
""",
|
|
values,
|
|
)
|
|
return MintQuote.from_row(row) if row else None # type: ignore
|
|
|
|
|
|
async def get_bolt11_mint_quotes(
|
|
db: Database,
|
|
mint: Optional[str] = None,
|
|
state: Optional[MintQuoteState] = None,
|
|
conn: Optional[Connection] = None,
|
|
) -> List[MintQuote]:
|
|
clauses = []
|
|
values: Dict[str, Any] = {}
|
|
if mint:
|
|
clauses.append("mint = :mint")
|
|
values["mint"] = mint
|
|
if state:
|
|
clauses.append("state = :state")
|
|
values["state"] = state.value
|
|
|
|
where = ""
|
|
if clauses:
|
|
where = f"WHERE {' AND '.join(clauses)}"
|
|
|
|
rows = await (conn or db).fetchall(
|
|
f"""
|
|
SELECT * from bolt11_mint_quotes
|
|
{where}
|
|
""",
|
|
values,
|
|
)
|
|
return [MintQuote.from_row(r) for r in rows] # type: ignore
|
|
|
|
|
|
async def update_bolt11_mint_quote(
|
|
db: Database,
|
|
quote: str,
|
|
state: MintQuoteState,
|
|
paid_time: int,
|
|
conn: Optional[Connection] = None,
|
|
) -> None:
|
|
await (conn or db).execute(
|
|
"""
|
|
UPDATE bolt11_mint_quotes
|
|
SET state = :state, paid_time = :paid_time
|
|
WHERE quote = :quote
|
|
""",
|
|
{
|
|
"state": state.value,
|
|
"paid_time": paid_time,
|
|
"quote": quote,
|
|
},
|
|
)
|
|
|
|
|
|
async def store_bolt11_melt_quote(
|
|
db: Database,
|
|
quote: MeltQuote,
|
|
conn: Optional[Connection] = None,
|
|
) -> None:
|
|
await (conn or db).execute(
|
|
"""
|
|
INSERT INTO bolt11_melt_quotes
|
|
(quote, mint, method, request, checking_id, unit, amount, fee_reserve, state, created_time, paid_time, fee_paid, payment_preimage, expiry, change)
|
|
VALUES (:quote, :mint, :method, :request, :checking_id, :unit, :amount, :fee_reserve, :state, :created_time, :paid_time, :fee_paid, :payment_preimage, :expiry, :change)
|
|
""",
|
|
{
|
|
"quote": quote.quote,
|
|
"mint": quote.mint,
|
|
"method": quote.method,
|
|
"request": quote.request,
|
|
"checking_id": quote.checking_id,
|
|
"unit": quote.unit,
|
|
"amount": quote.amount,
|
|
"fee_reserve": quote.fee_reserve,
|
|
"state": quote.state.value,
|
|
"created_time": quote.created_time,
|
|
"paid_time": quote.paid_time,
|
|
"fee_paid": quote.fee_paid,
|
|
"payment_preimage": quote.payment_preimage,
|
|
"expiry": quote.expiry,
|
|
"change": (
|
|
json.dumps([c.dict() for c in quote.change]) if quote.change else ""
|
|
),
|
|
},
|
|
)
|
|
|
|
|
|
async def get_bolt11_melt_quote(
|
|
db: Database,
|
|
quote: Optional[str] = None,
|
|
request: Optional[str] = None,
|
|
conn: Optional[Connection] = None,
|
|
) -> Optional[MeltQuote]:
|
|
if not quote and not request:
|
|
raise ValueError("quote or request must be provided")
|
|
clauses = []
|
|
values: Dict[str, Any] = {}
|
|
if quote:
|
|
clauses.append("quote = :quote")
|
|
values["quote"] = quote
|
|
if request:
|
|
clauses.append("request = :request")
|
|
values["request"] = request
|
|
|
|
where = ""
|
|
if clauses:
|
|
where = f"WHERE {' AND '.join(clauses)}"
|
|
row = await (conn or db).fetchone(
|
|
f"""
|
|
SELECT * from bolt11_melt_quotes
|
|
{where}
|
|
""",
|
|
values,
|
|
)
|
|
|
|
return MeltQuote.from_row(row) if row else None # type: ignore
|
|
|
|
|
|
async def get_bolt11_melt_quotes(
|
|
db: Database,
|
|
mint: Optional[str] = None,
|
|
state: Optional[MeltQuoteState] = None,
|
|
conn: Optional[Connection] = None,
|
|
) -> List[MeltQuote]:
|
|
clauses = []
|
|
values: Dict[str, Any] = {}
|
|
if mint:
|
|
clauses.append("mint = :mint")
|
|
values["mint"] = mint
|
|
if state:
|
|
clauses.append("state = :state")
|
|
values["state"] = state.value
|
|
|
|
where = ""
|
|
if clauses:
|
|
where = f"WHERE {' AND '.join(clauses)}"
|
|
rows = await (conn or db).fetchall(
|
|
f"""
|
|
SELECT * from bolt11_melt_quotes
|
|
{where}
|
|
""",
|
|
values,
|
|
)
|
|
return [MeltQuote.from_row(r) for r in rows] # type: ignore
|
|
|
|
|
|
async def update_bolt11_melt_quote(
|
|
db: Database,
|
|
quote: str,
|
|
state: MeltQuoteState,
|
|
paid_time: int,
|
|
fee_paid: int,
|
|
payment_preimage: str,
|
|
conn: Optional[Connection] = None,
|
|
) -> None:
|
|
await (conn or db).execute(
|
|
"""
|
|
UPDATE bolt11_melt_quotes
|
|
SET state = :state, paid_time = :paid_time, fee_paid = :fee_paid, payment_preimage = :payment_preimage
|
|
WHERE quote = :quote
|
|
""",
|
|
{
|
|
"state": state.value,
|
|
"paid_time": paid_time,
|
|
"fee_paid": fee_paid,
|
|
"payment_preimage": payment_preimage,
|
|
"quote": quote,
|
|
},
|
|
)
|
|
|
|
|
|
async def bump_secret_derivation(
|
|
db: Database,
|
|
keyset_id: str,
|
|
by: int = 1,
|
|
skip: bool = False,
|
|
conn: Optional[Connection] = None,
|
|
) -> int:
|
|
rows = await (conn or db).fetchone(
|
|
"SELECT counter from keysets WHERE id = :keyset_id", {"keyset_id": keyset_id}
|
|
)
|
|
# if no counter for this keyset, create one
|
|
if not rows:
|
|
await (conn or db).execute(
|
|
"UPDATE keysets SET counter = :counter WHERE id = :keyset_id",
|
|
{
|
|
"counter": 0,
|
|
"keyset_id": keyset_id,
|
|
},
|
|
)
|
|
counter = 0
|
|
else:
|
|
counter = int(rows["counter"])
|
|
|
|
if not skip:
|
|
await (conn or db).execute(
|
|
"UPDATE keysets SET counter = counter + :by WHERE id = :keyset_id",
|
|
{"by": by, "keyset_id": keyset_id},
|
|
)
|
|
return counter
|
|
|
|
|
|
async def set_secret_derivation(
|
|
db: Database,
|
|
keyset_id: str,
|
|
counter: int,
|
|
conn: Optional[Connection] = None,
|
|
) -> None:
|
|
await (conn or db).execute(
|
|
"UPDATE keysets SET counter = :counter WHERE id = :keyset_id",
|
|
{
|
|
"counter": counter,
|
|
"keyset_id": keyset_id,
|
|
},
|
|
)
|
|
|
|
|
|
async def set_nostr_last_check_timestamp(
|
|
db: Database,
|
|
timestamp: int,
|
|
conn: Optional[Connection] = None,
|
|
) -> None:
|
|
await (conn or db).execute(
|
|
"UPDATE nostr SET last = :last WHERE type = :type",
|
|
{"last": timestamp, "type": "dm"},
|
|
)
|
|
|
|
|
|
async def get_nostr_last_check_timestamp(
|
|
db: Database,
|
|
conn: Optional[Connection] = None,
|
|
) -> Optional[int]:
|
|
row = await (conn or db).fetchone(
|
|
"""
|
|
SELECT last from nostr WHERE type = :type
|
|
""",
|
|
{"type": "dm"},
|
|
)
|
|
return row[0] if row else None # type: ignore
|
|
|
|
|
|
async def get_seed_and_mnemonic(
|
|
db: Database,
|
|
conn: Optional[Connection] = None,
|
|
) -> Optional[Tuple[str, str]]:
|
|
row = await (conn or db).fetchone(
|
|
"""
|
|
SELECT seed, mnemonic from seed
|
|
"""
|
|
)
|
|
return (
|
|
(
|
|
row["seed"],
|
|
row["mnemonic"],
|
|
)
|
|
if row
|
|
else None
|
|
)
|
|
|
|
|
|
async def store_seed_and_mnemonic(
|
|
db: Database,
|
|
seed: str,
|
|
mnemonic: str,
|
|
conn: Optional[Connection] = None,
|
|
) -> None:
|
|
await (conn or db).execute(
|
|
"""
|
|
INSERT INTO seed
|
|
(seed, mnemonic)
|
|
VALUES (:seed, :mnemonic)
|
|
""",
|
|
{
|
|
"seed": seed,
|
|
"mnemonic": mnemonic,
|
|
},
|
|
)
|
|
|
|
|
|
async def store_mint(
|
|
db: Database,
|
|
mint: WalletMint,
|
|
conn: Optional[Connection] = None,
|
|
) -> None:
|
|
await (conn or db).execute(
|
|
"""
|
|
INSERT INTO mints
|
|
(url, info, updated)
|
|
VALUES (:url, :info, :updated)
|
|
""",
|
|
{
|
|
"url": mint.url,
|
|
"info": mint.info,
|
|
"updated": int(time.time()),
|
|
},
|
|
)
|
|
|
|
|
|
async def update_mint(
|
|
db: Database,
|
|
mint: WalletMint,
|
|
conn: Optional[Connection] = None,
|
|
) -> None:
|
|
await (conn or db).execute(
|
|
"""
|
|
UPDATE mints
|
|
SET info = :info, updated = :updated, access_token = :access_token, refresh_token = :refresh_token, username = :username, password = :password
|
|
WHERE url = :url
|
|
""",
|
|
{
|
|
"url": mint.url,
|
|
"info": mint.info,
|
|
"updated": int(time.time()),
|
|
"access_token": mint.access_token,
|
|
"refresh_token": mint.refresh_token,
|
|
"username": mint.username,
|
|
"password": mint.password,
|
|
},
|
|
)
|
|
|
|
|
|
async def get_mint_by_url(
|
|
db: Database,
|
|
url: str,
|
|
conn: Optional[Connection] = None,
|
|
) -> Optional[WalletMint]:
|
|
row = await (conn or db).fetchone(
|
|
"""
|
|
SELECT * from mints WHERE url = :url
|
|
""",
|
|
{"url": url},
|
|
)
|
|
return WalletMint.parse_obj(dict(row)) if row else None
|