From e4747910c9a3c39d952f3cd3e1f8059feca6f13a Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 9 Oct 2022 17:51:37 +0200 Subject: [PATCH 1/6] refactor --- cashu/core/helpers.py | 6 ++++++ cashu/mint/ledger.py | 15 ++++++--------- cashu/mint/router.py | 6 ++++++ cashu/wallet/cli.py | 4 ++-- cashu/wallet/wallet.py | 21 ++++++++------------- 5 files changed, 28 insertions(+), 24 deletions(-) diff --git a/cashu/core/helpers.py b/cashu/core/helpers.py index cbc1dc5..9b38c71 100644 --- a/cashu/core/helpers.py +++ b/cashu/core/helpers.py @@ -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): diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 8d3c558..73126c5 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -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) diff --git a/cashu/mint/router.py b/cashu/mint/router.py index ae78859..260d8bd 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -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) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 2ce2272..69c0bf3 100755 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -18,7 +18,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 @@ -267,7 +267,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") diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 4c150e0..79a1488 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -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, @@ -190,7 +191,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 +313,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 +340,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 +400,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 +448,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 +462,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() } From ae2ff8481897f8d425021b5252dcccf9b1322d0b Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 9 Oct 2022 18:14:50 +0200 Subject: [PATCH 2/6] cashu wallets --- cashu/wallet/cli.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 69c0bf3..25f5e45 100755 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -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 @@ -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("") @@ -312,9 +314,33 @@ async def locks(ctx): print(f"Receive: cashu receive --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 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 From 6785f79da9b5291e1a7d928dc6438853e0c06793 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 9 Oct 2022 18:16:20 +0200 Subject: [PATCH 3/6] cashu wallets help text --- cashu/wallet/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 25f5e45..67f0ff6 100755 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -319,7 +319,7 @@ async def locks(ctx): return True -@cli.command("wallets", help="List available wallets.") +@cli.command("wallets", help="List of all available wallets.") @click.pass_context @coro async def wallets(ctx): From a5eefb80b015153464de4b80643118659cc63aac Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 9 Oct 2022 21:58:19 +0200 Subject: [PATCH 4/6] keyset not byte array --- cashu/core/base.py | 6 ++---- cashu/core/crypto.py | 6 +++--- cashu/wallet/wallet.py | 22 ++++++++++++++-------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index d54913a..d99449f 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -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,8 +268,6 @@ 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, derivation_path=row[1], diff --git a/cashu/core/crypto.py b/cashu/core/crypto.py index 379d3ed..6cad72b 100644 --- a/cashu/core/crypto.py +++ b/cashu/core/crypto.py @@ -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] diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 79a1488..b00c982 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -108,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}) From 80597cecb3b359238b5ac907a6b288e59689775a Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 9 Oct 2022 22:09:43 +0200 Subject: [PATCH 5/6] fix typo --- cashu/core/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index d99449f..b76b6c4 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -269,7 +269,7 @@ class MintKeyset: if row is None: return cls return cls( - id=id, + id=row[0], derivation_path=row[1], valid_from=row[2], valid_to=row[3], From c832b339ab0f381a2dd252b7b2bfac31b0a30c49 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 9 Oct 2022 22:12:35 +0200 Subject: [PATCH 6/6] bump to 0.3.1 --- cashu/core/settings.py | 2 +- poetry.lock | 2 +- pyproject.toml | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cashu/core/settings.py b/cashu/core/settings.py index 1f23b89..e2a3f85 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -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" diff --git a/poetry.lock b/poetry.lock index a0dffb8..2bba9d6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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]] diff --git a/pyproject.toml b/pyproject.toml index 1c4d8a9..0563828 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cashu" -version = "0.3.0" +version = "0.3.1" description = "Ecash wallet and mint." authors = ["calle "] license = "MIT" diff --git a/setup.py b/setup.py index d06a8c9..77ab95c 100644 --- a/setup.py +++ b/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",