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 sqlite3 import Row
|
||||||
from typing import List, Union
|
from typing import List, Union, Dict
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@@ -9,6 +9,26 @@ class CashuError(BaseModel):
|
|||||||
error = "CashuError"
|
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):
|
class P2SHScript(BaseModel):
|
||||||
script: str
|
script: str
|
||||||
signature: str
|
signature: str
|
||||||
@@ -25,6 +45,7 @@ class P2SHScript(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class Proof(BaseModel):
|
class Proof(BaseModel):
|
||||||
|
id: str = ""
|
||||||
amount: int
|
amount: int
|
||||||
secret: str = ""
|
secret: str = ""
|
||||||
C: str
|
C: str
|
||||||
@@ -60,10 +81,10 @@ class Proof(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def to_dict(self):
|
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):
|
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):
|
def __getitem__(self, key):
|
||||||
return self.__getattribute__(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.b_dhke as b_dhke
|
||||||
import cashu.core.bolt11 as bolt11
|
import cashu.core.bolt11 as bolt11
|
||||||
from cashu.core.base import BlindedMessage, BlindedSignature, Invoice, Proof
|
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.db import Database
|
||||||
from cashu.core.helpers import fee_reserve
|
from cashu.core.helpers import fee_reserve
|
||||||
from cashu.core.script import verify_script
|
from cashu.core.script import verify_script
|
||||||
@@ -34,6 +35,7 @@ class Ledger:
|
|||||||
|
|
||||||
self.master_key = secret_key
|
self.master_key = secret_key
|
||||||
self.keys = self._derive_keys(self.master_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.pub_keys = self._derive_pubkeys(self.keys)
|
||||||
self.db: Database = Database("mint", db)
|
self.db: Database = Database("mint", db)
|
||||||
|
|
||||||
@@ -41,12 +43,12 @@ class Ledger:
|
|||||||
self.proofs_used = set(await get_proofs_used(db=self.db))
|
self.proofs_used = set(await get_proofs_used(db=self.db))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _derive_keys(master_key: str):
|
def _derive_keys(master_key: str, keyset_id: str = ""):
|
||||||
"""Deterministic derivation of keys for 2^n values."""
|
"""Deterministic derivation of keys for 2^n values."""
|
||||||
return {
|
return {
|
||||||
2
|
2
|
||||||
** i: PrivateKey(
|
** 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()
|
.hexdigest()
|
||||||
.encode("utf-8")[:32],
|
.encode("utf-8")[:32],
|
||||||
raw=True,
|
raw=True,
|
||||||
@@ -221,6 +223,9 @@ class Ledger:
|
|||||||
"""Returns public keys for possible amounts."""
|
"""Returns public keys for possible amounts."""
|
||||||
return {a: p.serialize().hex() for a, p in self.pub_keys.items()}
|
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):
|
async def request_mint(self, amount):
|
||||||
"""Returns Lightning invoice and stores it in the db."""
|
"""Returns Lightning invoice and stores it in the db."""
|
||||||
payment_request, checking_id = await self._request_lightning_invoice(amount)
|
payment_request, checking_id = await self._request_lightning_invoice(amount)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ router: APIRouter = APIRouter()
|
|||||||
@router.get("/keys")
|
@router.get("/keys")
|
||||||
def keys():
|
def keys():
|
||||||
"""Get the public keys of the mint"""
|
"""Get the public keys of the mint"""
|
||||||
return ledger.get_pubkeys()
|
return ledger.get_keyset()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/mint")
|
@router.get("/mint")
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ def coro(f):
|
|||||||
@coro
|
@coro
|
||||||
async def mint(ctx, amount: int, hash: str):
|
async def mint(ctx, amount: int, hash: str):
|
||||||
wallet: Wallet = ctx.obj["WALLET"]
|
wallet: Wallet = ctx.obj["WALLET"]
|
||||||
wallet.load_mint()
|
await wallet.load_mint()
|
||||||
wallet.status()
|
wallet.status()
|
||||||
if not LIGHTNING:
|
if not LIGHTNING:
|
||||||
r = await wallet.mint(amount)
|
r = await wallet.mint(amount)
|
||||||
@@ -122,7 +122,7 @@ async def mint(ctx, amount: int, hash: str):
|
|||||||
@coro
|
@coro
|
||||||
async def pay(ctx, invoice: str):
|
async def pay(ctx, invoice: str):
|
||||||
wallet: Wallet = ctx.obj["WALLET"]
|
wallet: Wallet = ctx.obj["WALLET"]
|
||||||
wallet.load_mint()
|
await wallet.load_mint()
|
||||||
wallet.status()
|
wallet.status()
|
||||||
decoded_invoice: Invoice = bolt11.decode(invoice)
|
decoded_invoice: Invoice = bolt11.decode(invoice)
|
||||||
# check if it's an internal payment
|
# 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:
|
if lock and len(lock.split("P2SH:")) == 2:
|
||||||
p2sh = True
|
p2sh = True
|
||||||
wallet: Wallet = ctx.obj["WALLET"]
|
wallet: Wallet = ctx.obj["WALLET"]
|
||||||
wallet.load_mint()
|
await wallet.load_mint()
|
||||||
wallet.status()
|
wallet.status()
|
||||||
_, send_proofs = await wallet.split_to_send(wallet.proofs, amount, lock)
|
_, send_proofs = await wallet.split_to_send(wallet.proofs, amount, lock)
|
||||||
await wallet.set_reserved(send_proofs, reserved=True)
|
await wallet.set_reserved(send_proofs, reserved=True)
|
||||||
@@ -181,7 +181,7 @@ async def send(ctx, amount: int, lock: str):
|
|||||||
@coro
|
@coro
|
||||||
async def receive(ctx, coin: str, lock: str):
|
async def receive(ctx, coin: str, lock: str):
|
||||||
wallet: Wallet = ctx.obj["WALLET"]
|
wallet: Wallet = ctx.obj["WALLET"]
|
||||||
wallet.load_mint()
|
await wallet.load_mint()
|
||||||
wallet.status()
|
wallet.status()
|
||||||
if lock:
|
if lock:
|
||||||
# load the script and signature of this address from the database
|
# load the script and signature of this address from the database
|
||||||
@@ -211,7 +211,7 @@ async def receive(ctx, coin: str, lock: str):
|
|||||||
@coro
|
@coro
|
||||||
async def burn(ctx, coin: str, all: bool, force: bool):
|
async def burn(ctx, coin: str, all: bool, force: bool):
|
||||||
wallet: Wallet = ctx.obj["WALLET"]
|
wallet: Wallet = ctx.obj["WALLET"]
|
||||||
wallet.load_mint()
|
await wallet.load_mint()
|
||||||
if not (all or coin or force) or (coin and all):
|
if not (all or coin or force) or (coin and all):
|
||||||
print(
|
print(
|
||||||
"Error: enter a coin or use --all to burn all pending coins or --force to check all coins."
|
"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
|
@coro
|
||||||
async def pending(ctx):
|
async def pending(ctx):
|
||||||
wallet: Wallet = ctx.obj["WALLET"]
|
wallet: Wallet = ctx.obj["WALLET"]
|
||||||
wallet.load_mint()
|
await wallet.load_mint()
|
||||||
reserved_proofs = await get_reserved_proofs(wallet.db)
|
reserved_proofs = await get_reserved_proofs(wallet.db)
|
||||||
if len(reserved_proofs):
|
if len(reserved_proofs):
|
||||||
print(f"--------------------------\n")
|
print(f"--------------------------\n")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import time
|
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
|
from cashu.core.db import Connection, Database
|
||||||
|
|
||||||
|
|
||||||
@@ -180,3 +180,50 @@ async def update_p2sh_used(
|
|||||||
f"UPDATE proofs SET {', '.join(clauses)} WHERE address = ?",
|
f"UPDATE proofs SET {', '.join(clauses)} WHERE address = ?",
|
||||||
(*values, str(p2sh.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 json
|
||||||
import secrets as scrts
|
import secrets as scrts
|
||||||
import uuid
|
import uuid
|
||||||
from typing import List
|
from typing import List, Dict
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -18,6 +18,7 @@ from cashu.core.base import (
|
|||||||
P2SHScript,
|
P2SHScript,
|
||||||
Proof,
|
Proof,
|
||||||
SplitRequest,
|
SplitRequest,
|
||||||
|
Keyset,
|
||||||
)
|
)
|
||||||
from cashu.core.db import Database
|
from cashu.core.db import Database
|
||||||
from cashu.core.script import (
|
from cashu.core.script import (
|
||||||
@@ -36,20 +37,37 @@ from cashu.wallet.crud import (
|
|||||||
store_p2sh,
|
store_p2sh,
|
||||||
store_proof,
|
store_proof,
|
||||||
update_proof_reserved,
|
update_proof_reserved,
|
||||||
|
store_keyset,
|
||||||
|
get_keyset,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class LedgerAPI:
|
class LedgerAPI:
|
||||||
|
keys: Dict[int, str]
|
||||||
|
keyset: str
|
||||||
|
|
||||||
def __init__(self, url):
|
def __init__(self, url):
|
||||||
self.url = url
|
self.url = url
|
||||||
|
|
||||||
@staticmethod
|
def _get_keys(self, url):
|
||||||
def _get_keys(url):
|
|
||||||
resp = requests.get(url + "/keys").json()
|
resp = requests.get(url + "/keys").json()
|
||||||
return {
|
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)
|
int(amt): PublicKey(bytes.fromhex(val), raw=True)
|
||||||
for amt, val in resp.items()
|
for amt, val in keys.items()
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
print(resp)
|
||||||
|
return Keyset(id=keyset_id, keys=keyset_keys, mint_url=self.url)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_output_split(amount):
|
def _get_output_split(amount):
|
||||||
@@ -69,7 +87,12 @@ class LedgerAPI:
|
|||||||
for promise, secret, r in zip(promises, secrets, rs):
|
for promise, secret, r in zip(promises, secrets, rs):
|
||||||
C_ = PublicKey(bytes.fromhex(promise.C_), raw=True)
|
C_ = PublicKey(bytes.fromhex(promise.C_), raw=True)
|
||||||
C = b_dhke.step3_alice(C_, r, self.keys[promise.amount])
|
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)
|
proofs.append(proof)
|
||||||
return proofs
|
return proofs
|
||||||
|
|
||||||
@@ -78,11 +101,16 @@ class LedgerAPI:
|
|||||||
"""Returns base64 encoded random string."""
|
"""Returns base64 encoded random string."""
|
||||||
return scrts.token_urlsafe(randombits // 8)
|
return scrts.token_urlsafe(randombits // 8)
|
||||||
|
|
||||||
def _load_mint(self):
|
async def _load_mint(self):
|
||||||
assert len(
|
assert len(
|
||||||
self.url
|
self.url
|
||||||
), "Ledger not initialized correctly: mint URL not specified yet. "
|
), "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."
|
assert len(self.keys) > 0, "did not receive keys from mint."
|
||||||
|
|
||||||
def request_mint(self, amount):
|
def request_mint(self, amount):
|
||||||
@@ -238,8 +266,8 @@ class Wallet(LedgerAPI):
|
|||||||
self.proofs: List[Proof] = []
|
self.proofs: List[Proof] = []
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
def load_mint(self):
|
async def load_mint(self):
|
||||||
super()._load_mint()
|
await super()._load_mint()
|
||||||
|
|
||||||
async def load_proofs(self):
|
async def load_proofs(self):
|
||||||
self.proofs = await get_proofs(db=self.db)
|
self.proofs = await get_proofs(db=self.db)
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ def assert_amt(proofs, expected):
|
|||||||
async def run_test():
|
async def run_test():
|
||||||
wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet1", "wallet1")
|
wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet1", "wallet1")
|
||||||
await migrate_databases(wallet1.db, migrations)
|
await migrate_databases(wallet1.db, migrations)
|
||||||
wallet1.load_mint()
|
await wallet1.load_mint()
|
||||||
wallet1.status()
|
wallet1.status()
|
||||||
|
|
||||||
wallet2 = Wallet2(SERVER_ENDPOINT, "data/wallet2", "wallet2")
|
wallet2 = Wallet2(SERVER_ENDPOINT, "data/wallet2", "wallet2")
|
||||||
await migrate_databases(wallet2.db, migrations)
|
await migrate_databases(wallet2.db, migrations)
|
||||||
wallet2.load_mint()
|
await wallet2.load_mint()
|
||||||
wallet2.status()
|
wallet2.status()
|
||||||
|
|
||||||
proofs = []
|
proofs = []
|
||||||
|
|||||||
Reference in New Issue
Block a user