mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-23 19:54:18 +01:00
started
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
from sqlite3 import Row
|
||||
from typing import List, Union
|
||||
from typing import List, Union, Dict
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -9,6 +9,26 @@ class CashuError(BaseModel):
|
||||
error = "CashuError"
|
||||
|
||||
|
||||
class Keyset(BaseModel):
|
||||
id: str
|
||||
keys: Dict
|
||||
mint_url: Union[str, None] = None
|
||||
first_seen: Union[str, None] = None
|
||||
active: bool = True
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row):
|
||||
if row is None:
|
||||
return cls
|
||||
return cls(
|
||||
id=row[0],
|
||||
keys=row[1],
|
||||
mint_url=row[2],
|
||||
first_seen=row[3],
|
||||
active=row[4],
|
||||
)
|
||||
|
||||
|
||||
class P2SHScript(BaseModel):
|
||||
script: str
|
||||
signature: str
|
||||
@@ -25,6 +45,7 @@ class P2SHScript(BaseModel):
|
||||
|
||||
|
||||
class Proof(BaseModel):
|
||||
id: str = ""
|
||||
amount: int
|
||||
secret: str = ""
|
||||
C: str
|
||||
@@ -60,10 +81,10 @@ class Proof(BaseModel):
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return dict(amount=self.amount, secret=self.secret, C=self.C)
|
||||
return dict(id=self.id, amount=self.amount, secret=self.secret, C=self.C)
|
||||
|
||||
def to_dict_no_secret(self):
|
||||
return dict(amount=self.amount, C=self.C)
|
||||
return dict(id=self.id, amount=self.amount, C=self.C)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.__getattribute__(key)
|
||||
|
||||
@@ -11,6 +11,7 @@ from typing import List, Set
|
||||
import cashu.core.b_dhke as b_dhke
|
||||
import cashu.core.bolt11 as bolt11
|
||||
from cashu.core.base import BlindedMessage, BlindedSignature, Invoice, Proof
|
||||
from cashu.core.crypto import derive_keyset_id
|
||||
from cashu.core.db import Database
|
||||
from cashu.core.helpers import fee_reserve
|
||||
from cashu.core.script import verify_script
|
||||
@@ -34,6 +35,7 @@ class Ledger:
|
||||
|
||||
self.master_key = secret_key
|
||||
self.keys = self._derive_keys(self.master_key)
|
||||
self.keyset_id = derive_keyset_id(self.keys)
|
||||
self.pub_keys = self._derive_pubkeys(self.keys)
|
||||
self.db: Database = Database("mint", db)
|
||||
|
||||
@@ -41,12 +43,12 @@ class Ledger:
|
||||
self.proofs_used = set(await get_proofs_used(db=self.db))
|
||||
|
||||
@staticmethod
|
||||
def _derive_keys(master_key: str):
|
||||
def _derive_keys(master_key: str, keyset_id: str = ""):
|
||||
"""Deterministic derivation of keys for 2^n values."""
|
||||
return {
|
||||
2
|
||||
** i: PrivateKey(
|
||||
hashlib.sha256((str(master_key) + str(i)).encode("utf-8"))
|
||||
hashlib.sha256((str(master_key) + str(i) + keyset_id).encode("utf-8"))
|
||||
.hexdigest()
|
||||
.encode("utf-8")[:32],
|
||||
raw=True,
|
||||
@@ -221,6 +223,9 @@ class Ledger:
|
||||
"""Returns public keys for possible amounts."""
|
||||
return {a: p.serialize().hex() for a, p in self.pub_keys.items()}
|
||||
|
||||
def get_keyset(self):
|
||||
return {"id": self.keyset_id, "keys": self.get_pubkeys()}
|
||||
|
||||
async def request_mint(self, amount):
|
||||
"""Returns Lightning invoice and stores it in the db."""
|
||||
payment_request, checking_id = await self._request_lightning_invoice(amount)
|
||||
|
||||
@@ -23,7 +23,7 @@ router: APIRouter = APIRouter()
|
||||
@router.get("/keys")
|
||||
def keys():
|
||||
"""Get the public keys of the mint"""
|
||||
return ledger.get_pubkeys()
|
||||
return ledger.get_keyset()
|
||||
|
||||
|
||||
@router.get("/mint")
|
||||
|
||||
@@ -78,7 +78,7 @@ def coro(f):
|
||||
@coro
|
||||
async def mint(ctx, amount: int, hash: str):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
wallet.load_mint()
|
||||
await wallet.load_mint()
|
||||
wallet.status()
|
||||
if not LIGHTNING:
|
||||
r = await wallet.mint(amount)
|
||||
@@ -122,7 +122,7 @@ async def mint(ctx, amount: int, hash: str):
|
||||
@coro
|
||||
async def pay(ctx, invoice: str):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
wallet.load_mint()
|
||||
await wallet.load_mint()
|
||||
wallet.status()
|
||||
decoded_invoice: Invoice = bolt11.decode(invoice)
|
||||
# check if it's an internal payment
|
||||
@@ -163,7 +163,7 @@ async def send(ctx, amount: int, lock: str):
|
||||
if lock and len(lock.split("P2SH:")) == 2:
|
||||
p2sh = True
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
wallet.load_mint()
|
||||
await wallet.load_mint()
|
||||
wallet.status()
|
||||
_, send_proofs = await wallet.split_to_send(wallet.proofs, amount, lock)
|
||||
await wallet.set_reserved(send_proofs, reserved=True)
|
||||
@@ -181,7 +181,7 @@ async def send(ctx, amount: int, lock: str):
|
||||
@coro
|
||||
async def receive(ctx, coin: str, lock: str):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
wallet.load_mint()
|
||||
await wallet.load_mint()
|
||||
wallet.status()
|
||||
if lock:
|
||||
# load the script and signature of this address from the database
|
||||
@@ -211,7 +211,7 @@ async def receive(ctx, coin: str, lock: str):
|
||||
@coro
|
||||
async def burn(ctx, coin: str, all: bool, force: bool):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
wallet.load_mint()
|
||||
await wallet.load_mint()
|
||||
if not (all or coin or force) or (coin and all):
|
||||
print(
|
||||
"Error: enter a coin or use --all to burn all pending coins or --force to check all coins."
|
||||
@@ -238,7 +238,7 @@ async def burn(ctx, coin: str, all: bool, force: bool):
|
||||
@coro
|
||||
async def pending(ctx):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
wallet.load_mint()
|
||||
await wallet.load_mint()
|
||||
reserved_proofs = await get_reserved_proofs(wallet.db)
|
||||
if len(reserved_proofs):
|
||||
print(f"--------------------------\n")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import time
|
||||
from typing import Any, List, Optional
|
||||
from typing import Any, List, Optional, Dict
|
||||
|
||||
from cashu.core.base import P2SHScript, Proof
|
||||
from cashu.core.base import P2SHScript, Proof, Keyset
|
||||
from cashu.core.db import Connection, Database
|
||||
|
||||
|
||||
@@ -180,3 +180,50 @@ async def update_p2sh_used(
|
||||
f"UPDATE proofs SET {', '.join(clauses)} WHERE address = ?",
|
||||
(*values, str(p2sh.address)),
|
||||
)
|
||||
|
||||
|
||||
async def store_keyset(
|
||||
keyset: Keyset,
|
||||
mint_url: str,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO keysets
|
||||
(id, mint_url, keys, first_seen, active)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(keyset.id, mint_url, keyset.keys, keyset.first_seen, True),
|
||||
)
|
||||
|
||||
|
||||
async def get_keyset(
|
||||
id: str = None,
|
||||
mint_url: str = None,
|
||||
db: Database = None,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
clauses = []
|
||||
values = []
|
||||
clauses.append("active = ?")
|
||||
values.append(True)
|
||||
if id:
|
||||
clauses.append("id = ?")
|
||||
values.append(id)
|
||||
if mint_url:
|
||||
clauses.append("mint_url = ?")
|
||||
values.append(mint_url)
|
||||
where = ""
|
||||
if clauses:
|
||||
where = f"WHERE {' AND '.join(clauses)}"
|
||||
|
||||
row = await (conn or db).fetchone(
|
||||
f"""
|
||||
SELECT * from keysets
|
||||
{where}
|
||||
""",
|
||||
tuple(values),
|
||||
)
|
||||
return Keyset.from_row(row)
|
||||
|
||||
@@ -98,3 +98,23 @@ async def m004_p2sh_locks(db: Database):
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m005_mint_keysets(db: Database):
|
||||
"""
|
||||
Stores mint keysets from different mints and epochs.
|
||||
"""
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS keysets (
|
||||
id TEXT NOT NULL,
|
||||
keys TEXT NOT NULL,
|
||||
mint_url TEXT NOT NULL,
|
||||
first_seen TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||
active BOOL NOT NULL DEFAULT TRUE,
|
||||
|
||||
UNIQUE (id, mint_url)
|
||||
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ import base64
|
||||
import json
|
||||
import secrets as scrts
|
||||
import uuid
|
||||
from typing import List
|
||||
from typing import List, Dict
|
||||
|
||||
import requests
|
||||
from loguru import logger
|
||||
@@ -18,6 +18,7 @@ from cashu.core.base import (
|
||||
P2SHScript,
|
||||
Proof,
|
||||
SplitRequest,
|
||||
Keyset,
|
||||
)
|
||||
from cashu.core.db import Database
|
||||
from cashu.core.script import (
|
||||
@@ -36,20 +37,37 @@ from cashu.wallet.crud import (
|
||||
store_p2sh,
|
||||
store_proof,
|
||||
update_proof_reserved,
|
||||
store_keyset,
|
||||
get_keyset,
|
||||
)
|
||||
|
||||
|
||||
class LedgerAPI:
|
||||
keys: Dict[int, str]
|
||||
keyset: str
|
||||
|
||||
def __init__(self, url):
|
||||
self.url = url
|
||||
|
||||
@staticmethod
|
||||
def _get_keys(url):
|
||||
def _get_keys(self, url):
|
||||
resp = requests.get(url + "/keys").json()
|
||||
return {
|
||||
int(amt): PublicKey(bytes.fromhex(val), raw=True)
|
||||
for amt, val in resp.items()
|
||||
}
|
||||
keyset_id = resp["id"]
|
||||
keys = resp["keys"]
|
||||
# return {
|
||||
# "id": keyset_id,
|
||||
# "keys": {
|
||||
# int(amt): PublicKey(bytes.fromhex(val), raw=True)
|
||||
# for amt, val in keys.items()
|
||||
# },
|
||||
# }
|
||||
keyset_keys = (
|
||||
{
|
||||
int(amt): PublicKey(bytes.fromhex(val), raw=True)
|
||||
for amt, val in keys.items()
|
||||
},
|
||||
)
|
||||
print(resp)
|
||||
return Keyset(id=keyset_id, keys=keyset_keys, mint_url=self.url)
|
||||
|
||||
@staticmethod
|
||||
def _get_output_split(amount):
|
||||
@@ -69,7 +87,12 @@ class LedgerAPI:
|
||||
for promise, secret, r in zip(promises, secrets, rs):
|
||||
C_ = PublicKey(bytes.fromhex(promise.C_), raw=True)
|
||||
C = b_dhke.step3_alice(C_, r, self.keys[promise.amount])
|
||||
proof = Proof(amount=promise.amount, C=C.serialize().hex(), secret=secret)
|
||||
proof = Proof(
|
||||
id=self.keyset_id,
|
||||
amount=promise.amount,
|
||||
C=C.serialize().hex(),
|
||||
secret=secret,
|
||||
)
|
||||
proofs.append(proof)
|
||||
return proofs
|
||||
|
||||
@@ -78,11 +101,16 @@ class LedgerAPI:
|
||||
"""Returns base64 encoded random string."""
|
||||
return scrts.token_urlsafe(randombits // 8)
|
||||
|
||||
def _load_mint(self):
|
||||
async def _load_mint(self):
|
||||
assert len(
|
||||
self.url
|
||||
), "Ledger not initialized correctly: mint URL not specified yet. "
|
||||
self.keys = self._get_keys(self.url)
|
||||
keyset = self._get_keys(self.url)
|
||||
keyset_local: Keyset = await get_keyset(keyset.id, self.url, db=self.db)
|
||||
if keyset_local is None:
|
||||
await store_keyset(keyset=keyset, db=self.db)
|
||||
self.keys = keyset.keys
|
||||
self.keyset_id = keyset.id
|
||||
assert len(self.keys) > 0, "did not receive keys from mint."
|
||||
|
||||
def request_mint(self, amount):
|
||||
@@ -238,8 +266,8 @@ class Wallet(LedgerAPI):
|
||||
self.proofs: List[Proof] = []
|
||||
self.name = name
|
||||
|
||||
def load_mint(self):
|
||||
super()._load_mint()
|
||||
async def load_mint(self):
|
||||
await super()._load_mint()
|
||||
|
||||
async def load_proofs(self):
|
||||
self.proofs = await get_proofs(db=self.db)
|
||||
|
||||
@@ -31,12 +31,12 @@ def assert_amt(proofs, expected):
|
||||
async def run_test():
|
||||
wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet1", "wallet1")
|
||||
await migrate_databases(wallet1.db, migrations)
|
||||
wallet1.load_mint()
|
||||
await wallet1.load_mint()
|
||||
wallet1.status()
|
||||
|
||||
wallet2 = Wallet2(SERVER_ENDPOINT, "data/wallet2", "wallet2")
|
||||
await migrate_databases(wallet2.db, migrations)
|
||||
wallet2.load_mint()
|
||||
await wallet2.load_mint()
|
||||
wallet2.status()
|
||||
|
||||
proofs = []
|
||||
|
||||
Reference in New Issue
Block a user