mirror of
https://github.com/aljazceru/nutshell.git
synced 2026-01-09 11:44:20 +01:00
Merge pull request #29 from callebtc/chore/comments_and_refactors
Comments, refactor, new CLI command: cashu wallets
This commit is contained in:
@@ -101,14 +101,14 @@ class BlindedMessage(BaseModel):
|
||||
|
||||
|
||||
class BlindedSignature(BaseModel):
|
||||
id: str = ""
|
||||
id: Union[str, None] = None
|
||||
amount: int
|
||||
C_: str
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict):
|
||||
return cls(
|
||||
id=d["id"],
|
||||
id=d.get("id"),
|
||||
amount=d["amount"],
|
||||
C_=d["C_"],
|
||||
)
|
||||
@@ -268,10 +268,8 @@ class MintKeyset:
|
||||
def from_row(cls, row: Row):
|
||||
if row is None:
|
||||
return cls
|
||||
# fix to convert byte to string, unclear why this is necessary
|
||||
id = row[0].decode("ascii") if type(row[0]) == bytes else row[0]
|
||||
return cls(
|
||||
id=id,
|
||||
id=row[0],
|
||||
derivation_path=row[1],
|
||||
valid_from=row[2],
|
||||
valid_to=row[3],
|
||||
|
||||
@@ -30,6 +30,6 @@ def derive_pubkeys(keys: Dict[int, PrivateKey]):
|
||||
def derive_keyset_id(keys: Dict[str, PublicKey]):
|
||||
"""Deterministic derivation keyset_id from set of public keys."""
|
||||
pubkeys_concat = "".join([p.serialize().hex() for _, p in keys.items()])
|
||||
return base64.b64encode(hashlib.sha256((pubkeys_concat).encode("utf-8")).digest())[
|
||||
:12
|
||||
]
|
||||
return base64.b64encode(
|
||||
hashlib.sha256((pubkeys_concat).encode("utf-8")).digest()
|
||||
).decode()[:12]
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import asyncio
|
||||
from functools import partial, wraps
|
||||
from typing import List
|
||||
|
||||
from cashu.core.base import Proof
|
||||
from cashu.core.settings import LIGHTNING_FEE_PERCENT, LIGHTNING_RESERVE_FEE_MIN
|
||||
|
||||
|
||||
def sum_proofs(proofs: List[Proof]):
|
||||
return sum([p.amount for p in proofs])
|
||||
|
||||
|
||||
def async_wrap(func):
|
||||
@wraps(func)
|
||||
async def run(*args, loop=None, executor=None, **kwargs):
|
||||
|
||||
@@ -48,4 +48,4 @@ LNBITS_ENDPOINT = env.str("LNBITS_ENDPOINT", default=None)
|
||||
LNBITS_KEY = env.str("LNBITS_KEY", default=None)
|
||||
|
||||
MAX_ORDER = 64
|
||||
VERSION = "0.3.0"
|
||||
VERSION = "0.3.1"
|
||||
|
||||
@@ -19,7 +19,7 @@ from cashu.core.base import (
|
||||
)
|
||||
from cashu.core.crypto import derive_keys, derive_keyset_id, derive_pubkeys
|
||||
from cashu.core.db import Database
|
||||
from cashu.core.helpers import fee_reserve
|
||||
from cashu.core.helpers import fee_reserve, sum_proofs
|
||||
from cashu.core.script import verify_script
|
||||
from cashu.core.secp import PublicKey
|
||||
from cashu.core.settings import LIGHTNING, MAX_ORDER
|
||||
@@ -45,6 +45,7 @@ class Ledger:
|
||||
self.db: Database = Database("mint", db)
|
||||
|
||||
async def load_used_proofs(self):
|
||||
"""Load all used proofs from database."""
|
||||
self.proofs_used = set(await get_proofs_used(db=self.db))
|
||||
|
||||
async def init_keysets(self):
|
||||
@@ -93,12 +94,8 @@ class Ledger:
|
||||
"""Checks whether the proof was already spent."""
|
||||
return not proof.secret in self.proofs_used
|
||||
|
||||
def _verify_secret_or_script(self, proof: Proof):
|
||||
if proof.secret and proof.script:
|
||||
raise Exception("secret and script present at the same time.")
|
||||
return True
|
||||
|
||||
def _verify_secret_criteria(self, proof: Proof):
|
||||
"""Verifies that a secret is present"""
|
||||
if proof.secret is None or proof.secret == "":
|
||||
raise Exception("no secret in proof.")
|
||||
return True
|
||||
@@ -213,7 +210,7 @@ class Ledger:
|
||||
invoice: Invoice = await get_lightning_invoice(payment_hash, db=self.db)
|
||||
if invoice.issued:
|
||||
raise Exception("tokens already issued for this invoice.")
|
||||
total_requested = sum([amount for amount in amounts])
|
||||
total_requested = sum(amounts)
|
||||
if total_requested > invoice.amount:
|
||||
raise Exception(
|
||||
f"Requested amount too high: {total_requested}. Invoice amount: {invoice.amount}"
|
||||
@@ -287,7 +284,7 @@ class Ledger:
|
||||
if not all([self._verify_proof(p) for p in proofs]):
|
||||
raise Exception("could not verify proofs.")
|
||||
|
||||
total_provided = sum([p["amount"] for p in proofs])
|
||||
total_provided = sum_proofs(proofs)
|
||||
invoice_obj = bolt11.decode(invoice)
|
||||
amount = math.ceil(invoice_obj.amount_msat / 1000)
|
||||
fees_msat = await self.check_fees(invoice)
|
||||
@@ -319,7 +316,7 @@ class Ledger:
|
||||
self, proofs: List[Proof], amount: int, outputs: List[BlindedMessage]
|
||||
):
|
||||
"""Consumes proofs and prepares new promises based on the amount split."""
|
||||
total = sum([p.amount for p in proofs])
|
||||
total = sum_proofs(proofs)
|
||||
|
||||
# verify that amount is kosher
|
||||
self._verify_split_amount(amount)
|
||||
|
||||
@@ -81,11 +81,17 @@ async def melt(payload: MeltRequest):
|
||||
|
||||
@router.post("/check")
|
||||
async def check_spendable(payload: CheckRequest):
|
||||
"""Check whether a secret has been spent already or not."""
|
||||
return await ledger.check_spendable(payload.proofs)
|
||||
|
||||
|
||||
@router.post("/checkfees")
|
||||
async def check_fees(payload: CheckFeesRequest):
|
||||
"""
|
||||
Responds with the fees necessary to pay a Lightning invoice.
|
||||
Used by wallets for figuring out the fees they need to supply.
|
||||
This is can be useful for checking whether an invoice is internal (Cashu-to-Cashu).
|
||||
"""
|
||||
fees_msat = await ledger.check_fees(payload.pr)
|
||||
return CheckFeesResponse(fee=fees_msat / 1000)
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ from datetime import datetime
|
||||
from functools import wraps
|
||||
from itertools import groupby
|
||||
from operator import itemgetter
|
||||
from os import listdir
|
||||
from os.path import isdir, join
|
||||
|
||||
import click
|
||||
from loguru import logger
|
||||
@@ -18,7 +20,7 @@ from loguru import logger
|
||||
import cashu.core.bolt11 as bolt11
|
||||
from cashu.core.base import Proof
|
||||
from cashu.core.bolt11 import Invoice, decode
|
||||
from cashu.core.helpers import fee_reserve
|
||||
from cashu.core.helpers import fee_reserve, sum_proofs
|
||||
from cashu.core.migrations import migrate_databases
|
||||
from cashu.core.settings import CASHU_DIR, DEBUG, ENV_FILE, LIGHTNING, MINT_URL, VERSION
|
||||
from cashu.wallet import migrations
|
||||
@@ -90,7 +92,7 @@ async def mint(ctx, amount: int, hash: str):
|
||||
print(f"Invoice: {r['pr']}")
|
||||
print("")
|
||||
print(
|
||||
f"Execute this command if you abort the check:\ncashu mint {amount} --hash {r['hash']}"
|
||||
f"Execute this command if you abort the check:\ncashu invoice {amount} --hash {r['hash']}"
|
||||
)
|
||||
check_until = time.time() + 5 * 60 # check for five minutes
|
||||
print("")
|
||||
@@ -267,7 +269,7 @@ async def pending(ctx):
|
||||
int(grouped_proofs[0].time_reserved)
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(
|
||||
f"#{i} Amount: {sum([p['amount'] for p in grouped_proofs])} sat Time: {reserved_date} ID: {key}\n"
|
||||
f"#{i} Amount: {sum_proofs(grouped_proofs)} sat Time: {reserved_date} ID: {key}\n"
|
||||
)
|
||||
print(f"With secret: {coin}\n\nSecretless: {coin_hidden_secret}\n")
|
||||
print(f"--------------------------\n")
|
||||
@@ -312,9 +314,33 @@ async def locks(ctx):
|
||||
print(f"Receive: cashu receive <coin> --lock P2SH:{l.address}")
|
||||
print("")
|
||||
print(f"--------------------------\n")
|
||||
else:
|
||||
print("No locks found. Create one using: cashu lock")
|
||||
return True
|
||||
|
||||
|
||||
@cli.command("wallets", help="List of all available wallets.")
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def wallets(ctx):
|
||||
# list all directories
|
||||
wallets = [d for d in listdir(CASHU_DIR) if isdir(join(CASHU_DIR, d))]
|
||||
wallets.remove("mint")
|
||||
for w in wallets:
|
||||
wallet = Wallet(ctx.obj["HOST"], os.path.join(CASHU_DIR, w))
|
||||
try:
|
||||
await init_wallet(wallet)
|
||||
if wallet.proofs and len(wallet.proofs):
|
||||
active_wallet = False
|
||||
if w == ctx.obj["WALLET_NAME"]:
|
||||
active_wallet = True
|
||||
print(
|
||||
f"Wallet: {w}\tBalance: {sum_proofs(wallet.proofs)} sat (available: {sum_proofs([p for p in wallet.proofs if not p.reserved])}){' *' if active_wallet else ''}"
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@cli.command("info", help="Information about Cashu wallet.")
|
||||
@click.pass_context
|
||||
@coro
|
||||
|
||||
@@ -22,6 +22,7 @@ from cashu.core.base import (
|
||||
WalletKeyset,
|
||||
)
|
||||
from cashu.core.db import Database
|
||||
from cashu.core.helpers import sum_proofs
|
||||
from cashu.core.script import (
|
||||
step0_carol_checksig_redeemscrip,
|
||||
step0_carol_privkey,
|
||||
@@ -107,24 +108,30 @@ class LedgerAPI:
|
||||
), "Ledger not initialized correctly: mint URL not specified yet. "
|
||||
# get current keyset
|
||||
keyset = await self._get_keys(self.url)
|
||||
logger.debug(f"Current mint keyset: {keyset.id}")
|
||||
# get all active keysets
|
||||
keysets = await self._get_keysets(self.url)
|
||||
logger.debug(f"Mint keysets: {keysets}")
|
||||
mint_keysets = []
|
||||
try:
|
||||
keysets_resp = await self._get_keysets(self.url)
|
||||
mint_keysets = keysets_resp["keysets"]
|
||||
# store active keysets
|
||||
except:
|
||||
pass
|
||||
self.keysets = mint_keysets if len(mint_keysets) else [keyset.id]
|
||||
|
||||
# store current keyset
|
||||
assert len(keyset.public_keys) > 0, "did not receive keys from mint."
|
||||
|
||||
# check if current keyset is in db
|
||||
keyset_local: WalletKeyset = await get_keyset(keyset.id, db=self.db)
|
||||
if keyset_local is None:
|
||||
await store_keyset(keyset=keyset, db=self.db)
|
||||
|
||||
# store current keyset
|
||||
assert len(keyset.public_keys) > 0, "did not receive keys from mint."
|
||||
logger.debug(f"Mint keysets: {self.keysets}")
|
||||
logger.debug(f"Current mint keyset: {keyset.id}")
|
||||
|
||||
self.keys = keyset.public_keys
|
||||
self.keyset_id = keyset.id
|
||||
|
||||
# store active keysets
|
||||
self.keysets = keysets["keysets"]
|
||||
|
||||
def request_mint(self, amount):
|
||||
"""Requests a mint from the server and returns Lightning invoice."""
|
||||
r = requests.get(self.url + "/mint", params={"amount": amount})
|
||||
@@ -190,7 +197,7 @@ class LedgerAPI:
|
||||
If scnd_secret is provided, the wallet will create blinded secrets with those to attach a
|
||||
predefined spending condition to the tokens they want to send."""
|
||||
|
||||
total = sum([p["amount"] for p in proofs])
|
||||
total = sum_proofs(proofs)
|
||||
frst_amt, scnd_amt = total - amount, amount
|
||||
frst_outputs = amount_split(frst_amt)
|
||||
scnd_outputs = amount_split(scnd_amt)
|
||||
@@ -312,12 +319,6 @@ class Wallet(LedgerAPI):
|
||||
for proof in proofs:
|
||||
await store_proof(proof, db=self.db)
|
||||
|
||||
@staticmethod
|
||||
def _sum_proofs(proofs: List[Proof], available_only=False):
|
||||
if available_only:
|
||||
return sum([p.amount for p in proofs if not p.reserved])
|
||||
return sum([p.amount for p in proofs])
|
||||
|
||||
@staticmethod
|
||||
def _get_proofs_per_keyset(proofs: List[Proof]):
|
||||
return {key: list(group) for key, group in groupby(proofs, lambda p: p.id)}
|
||||
@@ -345,7 +346,7 @@ class Wallet(LedgerAPI):
|
||||
# attach unlock scripts to proofs
|
||||
for p in proofs:
|
||||
p.script = P2SHScript(script=scnd_script, signature=scnd_siganture)
|
||||
return await self.split(proofs, sum(p["amount"] for p in proofs))
|
||||
return await self.split(proofs, sum_proofs(proofs))
|
||||
|
||||
async def split(
|
||||
self,
|
||||
@@ -405,7 +406,7 @@ class Wallet(LedgerAPI):
|
||||
if scnd_secret:
|
||||
logger.debug(f"Spending conditions: {scnd_secret}")
|
||||
spendable_proofs = await self._get_spendable_proofs(proofs)
|
||||
if sum([p.amount for p in spendable_proofs]) < amount:
|
||||
if sum_proofs(spendable_proofs) < amount:
|
||||
raise Exception("balance too low.")
|
||||
return await self.split(
|
||||
[p for p in spendable_proofs if not p.reserved], amount, scnd_secret
|
||||
@@ -453,11 +454,11 @@ class Wallet(LedgerAPI):
|
||||
|
||||
@property
|
||||
def balance(self):
|
||||
return sum(p["amount"] for p in self.proofs)
|
||||
return sum_proofs(self.proofs)
|
||||
|
||||
@property
|
||||
def available_balance(self):
|
||||
return sum(p["amount"] for p in self.proofs if not p.reserved)
|
||||
return sum_proofs([p for p in self.proofs if not p.reserved])
|
||||
|
||||
def status(self):
|
||||
print(
|
||||
@@ -467,8 +468,8 @@ class Wallet(LedgerAPI):
|
||||
def balance_per_keyset(self):
|
||||
return {
|
||||
key: {
|
||||
"balance": self._sum_proofs(proofs),
|
||||
"available": self._sum_proofs(proofs, available_only=True),
|
||||
"balance": sum_proofs(proofs),
|
||||
"available": sum_proofs([p for p in proofs if not p.reserved]),
|
||||
}
|
||||
for key, proofs in self._get_proofs_per_keyset(self.proofs).items()
|
||||
}
|
||||
|
||||
2
poetry.lock
generated
2
poetry.lock
generated
@@ -167,7 +167,7 @@ starlette = "0.19.1"
|
||||
[package.extras]
|
||||
all = ["email_validator (>=1.1.1,<2.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"]
|
||||
dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "pre-commit (>=2.17.0,<3.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"]
|
||||
doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.5.0)"]
|
||||
doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.1)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.5.0)"]
|
||||
test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.3.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "email_validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "orjson (>=3.2.1,<4.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-dataclasses (==0.6.5)", "types-orjson (==3.6.2)", "types-ujson (==4.2.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "cashu"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
description = "Ecash wallet and mint."
|
||||
authors = ["calle <callebtc@protonmail.com>"]
|
||||
license = "MIT"
|
||||
|
||||
2
setup.py
2
setup.py
@@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli:cli"]}
|
||||
|
||||
setuptools.setup(
|
||||
name="cashu",
|
||||
version="0.3.0",
|
||||
version="0.3.1",
|
||||
description="Ecash wallet and mint with Bitcoin Lightning support",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
|
||||
Reference in New Issue
Block a user