From 80a696d097961ccf1dc333bd51e2fbece55db510 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 21 Sep 2022 17:07:25 +0300 Subject: [PATCH 1/6] pending --- .env.example | 2 ++ core/base.py | 3 +++ core/settings.py | 6 ++++++ lightning/lnbits.py | 9 +++++++-- mint/app.py | 9 +++++++-- setup.py | 4 ++-- wallet/cashu.py | 32 +++++++++++++++++++++++++------- wallet/crud.py | 43 +++++++++++++++++++++++++------------------ wallet/migrations.py | 11 +++++++++++ wallet/wallet.py | 16 +++++++++++++++- 10 files changed, 103 insertions(+), 32 deletions(-) diff --git a/.env.example b/.env.example index b10432b..bf04238 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,7 @@ DEBUG = true +CASHU_DIR=~/.cashu + # WALLET MINT_HOST=127.0.0.1 diff --git a/core/base.py b/core/base.py index cf391b4..f1b4ebd 100644 --- a/core/base.py +++ b/core/base.py @@ -9,6 +9,7 @@ class Proof(BaseModel): C: str secret: str reserved: bool = False # whether this proof is reserved for sending + send_id: str = "" # unique ID of send attempt @classmethod def from_row(cls, row: Row): @@ -17,6 +18,7 @@ class Proof(BaseModel): C=row[1], secret=row[2], reserved=row[3] or False, + send_id=row[4] or "", ) @classmethod @@ -26,6 +28,7 @@ class Proof(BaseModel): C=d["C"], secret=d["secret"], reserved=d["reserved"] or False, + send_id=d["send_id"] or "", ) def __getitem__(self, key): diff --git a/core/settings.py b/core/settings.py index b8159aa..d24c0c4 100644 --- a/core/settings.py +++ b/core/settings.py @@ -1,9 +1,15 @@ +from pathlib import Path + from environs import Env # type: ignore env = Env() env.read_env() DEBUG = env.bool("DEBUG", default=False) +CASHU_DIR = env.str("CASHU_DIR", default="~/.cashu") +CASHU_DIR = CASHU_DIR.replace("~", str(Path.home())) +assert len(CASHU_DIR), "CASHU_DIR not defined" + LIGHTNING = env.bool("LIGHTNING", default=True) LIGHTNING_FEE_PERCENT = env.float("LIGHTNING_FEE_PERCENT", default=1.0) assert LIGHTNING_FEE_PERCENT >= 0, "LIGHTNING_FEE_PERCENT must be at least 0" diff --git a/lightning/lnbits.py b/lightning/lnbits.py index 94a49eb..5623b4f 100644 --- a/lightning/lnbits.py +++ b/lightning/lnbits.py @@ -8,8 +8,13 @@ import requests from core.settings import LNBITS_ENDPOINT, LNBITS_KEY -from .base import (InvoiceResponse, PaymentResponse, PaymentStatus, - StatusResponse, Wallet) +from .base import ( + InvoiceResponse, + PaymentResponse, + PaymentStatus, + StatusResponse, + Wallet, +) class LNbitsWallet(Wallet): diff --git a/mint/app.py b/mint/app.py index 360f318..c115c6c 100644 --- a/mint/app.py +++ b/mint/app.py @@ -11,7 +11,12 @@ from secp256k1 import PublicKey import core.settings as settings from core.base import CheckPayload, MeltPayload, MintPayloads, SplitPayload -from core.settings import MINT_PRIVATE_KEY, MINT_SERVER_HOST, MINT_SERVER_PORT +from core.settings import ( + CASHU_DIR, + MINT_PRIVATE_KEY, + MINT_SERVER_HOST, + MINT_SERVER_PORT, +) from lightning import WALLET from mint.ledger import Ledger from mint.migrations import m001_initial @@ -33,7 +38,7 @@ def startup(app: FastAPI): ) logger.info(f"Lightning balance: {balance} sat") - + logger.info(f"Data dir: {CASHU_DIR}") logger.info("Mint started.") diff --git a/setup.py b/setup.py index f9069e7..6563f5c 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ -import setuptools - from os import path +import setuptools + this_directory = path.abspath(path.dirname(__file__)) with open(path.join(this_directory, "README.md"), encoding="utf-8") as f: long_description = f.read() diff --git a/wallet/cashu.py b/wallet/cashu.py index 37bf8bf..4590f56 100755 --- a/wallet/cashu.py +++ b/wallet/cashu.py @@ -5,7 +5,8 @@ import base64 import json import math from functools import wraps -from pathlib import Path +from itertools import groupby +from operator import itemgetter import click from bech32 import bech32_decode, bech32_encode, convertbits @@ -15,7 +16,7 @@ from core.base import Proof from core.bolt11 import Invoice from core.helpers import fee_reserve from core.migrations import migrate_databases -from core.settings import LIGHTNING, MINT_URL +from core.settings import CASHU_DIR, LIGHTNING, MINT_URL from wallet import migrations from wallet.crud import get_reserved_proofs from wallet.wallet import Wallet as Wallet @@ -46,9 +47,7 @@ def cli( ctx.ensure_object(dict) ctx.obj["HOST"] = host ctx.obj["WALLET_NAME"] = walletname - ctx.obj["WALLET"] = Wallet( - ctx.obj["HOST"], f"{str(Path.home())}/.cashu/{walletname}", walletname - ) + ctx.obj["WALLET"] = Wallet(ctx.obj["HOST"], f"{CASHU_DIR}/{walletname}", walletname) pass @@ -106,8 +105,8 @@ async def send(ctx, amount: int): wallet.status() _, send_proofs = await wallet.split_to_send(wallet.proofs, amount) await wallet.set_reserved(send_proofs, reserved=True) - proofs_serialized = [p.dict() for p in send_proofs] - print(base64.urlsafe_b64encode(json.dumps(proofs_serialized).encode()).decode()) + token = await wallet.serialize_proofs(send_proofs) + print(token) wallet.status() @@ -146,6 +145,25 @@ async def burn(ctx, token: str, all: bool): wallet.status() +@cli.command("pending", help="Show pending tokens.") +@click.pass_context +@coro +async def pending(ctx): + wallet: Wallet = ctx.obj["WALLET"] + await init_wallet(wallet) + wallet.status() + reserved_proofs = await get_reserved_proofs(wallet.db) + if len(reserved_proofs): + sorted_proofs = sorted(reserved_proofs, key=itemgetter("send_id")) + for key, value in groupby(sorted_proofs, key=itemgetter("send_id")): + grouped_proofs = list(value) + token = await wallet.serialize_proofs(grouped_proofs) + print( + f"Amount: {sum([p['amount'] for p in grouped_proofs])} sat. ID: {key}" + ) + print(token) + + @cli.command("pay", help="Pay lightning invoice.") @click.argument("invoice", type=str) @click.pass_context diff --git a/wallet/crud.py b/wallet/crud.py index 555715f..55bb293 100644 --- a/wallet/crud.py +++ b/wallet/crud.py @@ -1,4 +1,4 @@ -import secrets +import time from typing import Optional from core.base import Proof @@ -14,14 +14,10 @@ async def store_proof( await (conn or db).execute( """ INSERT INTO proofs - (amount, C, secret) - VALUES (?, ?, ?) + (amount, C, secret, time_created) + VALUES (?, ?, ?, ?) """, - ( - proof.amount, - str(proof.C), - str(proof.secret), - ), + (proof.amount, str(proof.C), str(proof.secret), int(time.time())), ) @@ -69,24 +65,35 @@ async def invalidate_proof( await (conn or db).execute( """ INSERT INTO proofs_used - (amount, C, secret) - VALUES (?, ?, ?) + (amount, C, secret, time_used) + VALUES (?, ?, ?, ?) """, - ( - proof.amount, - str(proof.C), - str(proof.secret), - ), + (proof.amount, str(proof.C), str(proof.secret), int(time.time())), ) async def update_proof_reserved( proof: Proof, reserved: bool, - db: Database, + send_id: str = None, + db: Database = None, conn: Optional[Connection] = None, ): + clauses = [] + values = [] + clauses.append("reserved = ?") + values.append(reserved) + + if send_id: + clauses.append("send_id = ?") + values.append(send_id) + + if reserved: + # set the time of reserving + clauses.append("time_reserved = ?") + values.append(int(time.time())) + await (conn or db).execute( - "UPDATE proofs SET reserved = ? WHERE secret = ?", - (reserved, str(proof.secret)), + f"UPDATE proofs SET {', '.join(clauses)} WHERE secret = ?", + (*values, str(proof.secret)), ) diff --git a/wallet/migrations.py b/wallet/migrations.py index 8dc0229..269f495 100644 --- a/wallet/migrations.py +++ b/wallet/migrations.py @@ -68,3 +68,14 @@ async def m002_add_proofs_reserved(db): """ await db.execute("ALTER TABLE proofs ADD COLUMN reserved BOOL") + + +async def m003_add_proofs_sendid(db): + """ + Column with unique ID for each initiated send attempt + so proofs can be later grouped together for each send attempt. + """ + await db.execute("ALTER TABLE proofs ADD COLUMN send_id TEXT") + await db.execute("ALTER TABLE proofs ADD COLUMN time_created TIMESTAMP") + await db.execute("ALTER TABLE proofs ADD COLUMN time_reserved TIMESTAMP") + await db.execute("ALTER TABLE proofs_used ADD COLUMN time_used TIMESTAMP") diff --git a/wallet/wallet.py b/wallet/wallet.py index c34a946..58929c5 100644 --- a/wallet/wallet.py +++ b/wallet/wallet.py @@ -1,4 +1,7 @@ +import base64 +import json import random +import uuid from typing import List import requests @@ -186,6 +189,14 @@ class Wallet(LedgerAPI): raise Exception("could not pay invoice.") return status["paid"] + @staticmethod + async def serialize_proofs(proofs: List[Proof]): + proofs_serialized = [p.dict() for p in proofs] + token = base64.urlsafe_b64encode( + json.dumps(proofs_serialized).encode() + ).decode() + return token + async def split_to_send(self, proofs: List[Proof], amount): """Like self.split but only considers non-reserved tokens.""" if len([p for p in proofs if not p.reserved]) <= 0: @@ -194,9 +205,12 @@ class Wallet(LedgerAPI): async def set_reserved(self, proofs: List[Proof], reserved: bool): """Mark a proof as reserved to avoid reuse or delete marking.""" + uuid_str = str(uuid.uuid1()) for proof in proofs: proof.reserved = True - await update_proof_reserved(proof, reserved=reserved, db=self.db) + await update_proof_reserved( + proof, reserved=reserved, send_id=uuid_str, db=self.db + ) async def check_spendable(self, proofs): return await super().check_spendable(proofs) From edaedf01b4a7e42aa7acf1c34bd9c048a40099dc Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 21 Sep 2022 17:18:42 +0300 Subject: [PATCH 2/6] print timestamp --- core/base.py | 6 ++++++ wallet/cashu.py | 10 ++++++++-- wallet/migrations.py | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/core/base.py b/core/base.py index f1b4ebd..dbd70e7 100644 --- a/core/base.py +++ b/core/base.py @@ -10,6 +10,8 @@ class Proof(BaseModel): secret: str reserved: bool = False # whether this proof is reserved for sending send_id: str = "" # unique ID of send attempt + time_created: str = "" + time_reserved: str = "" @classmethod def from_row(cls, row: Row): @@ -19,6 +21,8 @@ class Proof(BaseModel): secret=row[2], reserved=row[3] or False, send_id=row[4] or "", + time_created=row[5] or "", + time_reserved=row[6] or "", ) @classmethod @@ -29,6 +33,8 @@ class Proof(BaseModel): secret=d["secret"], reserved=d["reserved"] or False, send_id=d["send_id"] or "", + time_created=d["time_created"] or "", + time_reserved=d["time_reserved"] or "", ) def __getitem__(self, key): diff --git a/wallet/cashu.py b/wallet/cashu.py index 4590f56..3a82531 100755 --- a/wallet/cashu.py +++ b/wallet/cashu.py @@ -4,6 +4,8 @@ import asyncio import base64 import json import math +from datetime import datetime + from functools import wraps from itertools import groupby from operator import itemgetter @@ -151,17 +153,21 @@ async def burn(ctx, token: str, all: bool): async def pending(ctx): wallet: Wallet = ctx.obj["WALLET"] await init_wallet(wallet) - wallet.status() reserved_proofs = await get_reserved_proofs(wallet.db) if len(reserved_proofs): sorted_proofs = sorted(reserved_proofs, key=itemgetter("send_id")) for key, value in groupby(sorted_proofs, key=itemgetter("send_id")): grouped_proofs = list(value) token = await wallet.serialize_proofs(grouped_proofs) + reserved_date = datetime.utcfromtimestamp( + int(grouped_proofs[0].time_reserved) + ).strftime("%Y-%m-%d %H:%M:%S") print( - f"Amount: {sum([p['amount'] for p in grouped_proofs])} sat. ID: {key}" + f"Amount: {sum([p['amount'] for p in grouped_proofs])} sat Sent: {reserved_date} ID: {key}\n" ) print(token) + print("") + wallet.status() @cli.command("pay", help="Pay lightning invoice.") diff --git a/wallet/migrations.py b/wallet/migrations.py index 269f495..8745577 100644 --- a/wallet/migrations.py +++ b/wallet/migrations.py @@ -70,7 +70,7 @@ async def m002_add_proofs_reserved(db): await db.execute("ALTER TABLE proofs ADD COLUMN reserved BOOL") -async def m003_add_proofs_sendid(db): +async def m003_add_proofs_sendid_and_timestamps(db): """ Column with unique ID for each initiated send attempt so proofs can be later grouped together for each send attempt. From 43e7889cab014f9f0df5725677d5ed882a39a618 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 22 Sep 2022 11:52:40 +0300 Subject: [PATCH 3/6] use compressed secret --- .env.example | 2 +- core/base.py | 21 +++++++++++++-------- lightning/lnbits.py | 9 ++------- mint/app.py | 8 ++------ mint/ledger.py | 11 +++-------- poetry.lock | 46 ++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 3 +++ wallet/cashu.py | 3 +-- wallet/wallet.py | 28 ++++++++++++++------------- 9 files changed, 85 insertions(+), 46 deletions(-) diff --git a/.env.example b/.env.example index bf04238..78504b0 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -DEBUG = true +DEBUG=FALSE CASHU_DIR=~/.cashu diff --git a/core/base.py b/core/base.py index dbd70e7..a78f7ee 100644 --- a/core/base.py +++ b/core/base.py @@ -6,8 +6,8 @@ from pydantic import BaseModel class Proof(BaseModel): amount: int - C: str secret: str + C: str reserved: bool = False # whether this proof is reserved for sending send_id: str = "" # unique ID of send attempt time_created: str = "" @@ -27,16 +27,21 @@ class Proof(BaseModel): @classmethod def from_dict(cls, d: dict): + assert "secret" in d, "no secret in proof" + assert "amount" in d, "no amount in proof" return cls( - amount=d["amount"], - C=d["C"], - secret=d["secret"], - reserved=d["reserved"] or False, - send_id=d["send_id"] or "", - time_created=d["time_created"] or "", - time_reserved=d["time_reserved"] or "", + amount=d.get("amount"), + C=d.get("C"), + secret=d.get("secret"), + reserved=d.get("reserved") or False, + send_id=d.get("send_id") or "", + time_created=d.get("time_created") or "", + time_reserved=d.get("time_reserved") or "", ) + def to_dict(self): + return dict(amount=self.amount, secret=self.secret, C=self.C) + def __getitem__(self, key): return self.__getattribute__(key) diff --git a/lightning/lnbits.py b/lightning/lnbits.py index 5623b4f..94a49eb 100644 --- a/lightning/lnbits.py +++ b/lightning/lnbits.py @@ -8,13 +8,8 @@ import requests from core.settings import LNBITS_ENDPOINT, LNBITS_KEY -from .base import ( - InvoiceResponse, - PaymentResponse, - PaymentStatus, - StatusResponse, - Wallet, -) +from .base import (InvoiceResponse, PaymentResponse, PaymentStatus, + StatusResponse, Wallet) class LNbitsWallet(Wallet): diff --git a/mint/app.py b/mint/app.py index c115c6c..ddd5315 100644 --- a/mint/app.py +++ b/mint/app.py @@ -11,12 +11,8 @@ from secp256k1 import PublicKey import core.settings as settings from core.base import CheckPayload, MeltPayload, MintPayloads, SplitPayload -from core.settings import ( - CASHU_DIR, - MINT_PRIVATE_KEY, - MINT_SERVER_HOST, - MINT_SERVER_PORT, -) +from core.settings import (CASHU_DIR, MINT_PRIVATE_KEY, MINT_SERVER_HOST, + MINT_SERVER_PORT) from lightning import WALLET from mint.ledger import Ledger from mint.migrations import m001_initial diff --git a/mint/ledger.py b/mint/ledger.py index d80aa10..0ec9e51 100644 --- a/mint/ledger.py +++ b/mint/ledger.py @@ -13,14 +13,9 @@ from core.secp import PrivateKey, PublicKey from core.settings import LIGHTNING, MAX_ORDER from core.split import amount_split from lightning import WALLET -from mint.crud import ( - get_lightning_invoice, - get_proofs_used, - invalidate_proof, - store_lightning_invoice, - store_promise, - update_lightning_invoice, -) +from mint.crud import (get_lightning_invoice, get_proofs_used, + invalidate_proof, store_lightning_invoice, + store_promise, update_lightning_invoice) class Ledger: diff --git a/poetry.lock b/poetry.lock index 43234b4..e7c7e05 100644 --- a/poetry.lock +++ b/poetry.lock @@ -323,6 +323,25 @@ docs = ["alabaster (==0.7.12)", "autodocsumm (==0.2.9)", "sphinx (==5.1.1)", "sp lint = ["flake8 (==5.0.4)", "flake8-bugbear (==22.9.11)", "mypy (==0.971)", "pre-commit (>=2.4,<3.0)"] tests = ["pytest", "pytz", "simplejson"] +[[package]] +name = "mypy" +version = "0.971" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +mypy-extensions = ">=0.4.3" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} +typing-extensions = ">=3.10" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + [[package]] name = "mypy-extensions" version = "0.4.3" @@ -690,7 +709,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "68f6f6e33100fdd1bd86879f8c146a96a5ee822b0a6e8b9ec27205f6eab1fcd8" +content-hash = "a4553430bb7df5a66a7006d638986509d065e931ee5009ded5244eaa017475b2" [metadata.files] anyio = [ @@ -915,6 +934,31 @@ marshmallow = [ {file = "marshmallow-3.18.0-py3-none-any.whl", hash = "sha256:35e02a3a06899c9119b785c12a22f4cda361745d66a71ab691fd7610202ae104"}, {file = "marshmallow-3.18.0.tar.gz", hash = "sha256:6804c16114f7fce1f5b4dadc31f4674af23317fcc7f075da21e35c1a35d781f7"}, ] +mypy = [ + {file = "mypy-0.971-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"}, + {file = "mypy-0.971-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5"}, + {file = "mypy-0.971-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3"}, + {file = "mypy-0.971-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655"}, + {file = "mypy-0.971-cp310-cp310-win_amd64.whl", hash = "sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103"}, + {file = "mypy-0.971-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca"}, + {file = "mypy-0.971-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417"}, + {file = "mypy-0.971-cp36-cp36m-win_amd64.whl", hash = "sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09"}, + {file = "mypy-0.971-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8"}, + {file = "mypy-0.971-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0"}, + {file = "mypy-0.971-cp37-cp37m-win_amd64.whl", hash = "sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2"}, + {file = "mypy-0.971-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27"}, + {file = "mypy-0.971-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856"}, + {file = "mypy-0.971-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71"}, + {file = "mypy-0.971-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27"}, + {file = "mypy-0.971-cp38-cp38-win_amd64.whl", hash = "sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58"}, + {file = "mypy-0.971-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6"}, + {file = "mypy-0.971-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe"}, + {file = "mypy-0.971-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9"}, + {file = "mypy-0.971-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf"}, + {file = "mypy-0.971-cp39-cp39-win_amd64.whl", hash = "sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0"}, + {file = "mypy-0.971-py3-none-any.whl", hash = "sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9"}, + {file = "mypy-0.971.tar.gz", hash = "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56"}, +] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, diff --git a/pyproject.toml b/pyproject.toml index 0c96694..22ab0e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,9 @@ secp256k1 = "^0.14.0" black = {version = "^22.8.0", allow-prereleases = true} isort = "^5.10.1" +[tool.poetry.group.dev.dependencies] +mypy = "^0.971" + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/wallet/cashu.py b/wallet/cashu.py index 3a82531..165692f 100755 --- a/wallet/cashu.py +++ b/wallet/cashu.py @@ -5,7 +5,6 @@ import base64 import json import math from datetime import datetime - from functools import wraps from itertools import groupby from operator import itemgetter @@ -18,7 +17,7 @@ from core.base import Proof from core.bolt11 import Invoice from core.helpers import fee_reserve from core.migrations import migrate_databases -from core.settings import CASHU_DIR, LIGHTNING, MINT_URL +from core.settings import CASHU_DIR, DEBUG, LIGHTNING, MINT_URL from wallet import migrations from wallet.crud import get_reserved_proofs from wallet.wallet import Wallet as Wallet diff --git a/wallet/wallet.py b/wallet/wallet.py index 58929c5..c8280e0 100644 --- a/wallet/wallet.py +++ b/wallet/wallet.py @@ -1,25 +1,21 @@ import base64 import json import random +import secrets as scrts import uuid from typing import List import requests import core.b_dhke as b_dhke -from core.base import ( - BlindedMessage, - BlindedSignature, - CheckPayload, - MeltPayload, - MintPayloads, - Proof, - SplitPayload, -) +from core.base import (BlindedMessage, BlindedSignature, CheckPayload, + MeltPayload, MintPayloads, Proof, SplitPayload) from core.db import Database from core.secp import PublicKey +from core.settings import DEBUG from core.split import amount_split -from wallet.crud import get_proofs, invalidate_proof, store_proof, update_proof_reserved +from wallet.crud import (get_proofs, invalidate_proof, store_proof, + update_proof_reserved) class LedgerAPI: @@ -55,6 +51,10 @@ class LedgerAPI: proofs.append(proof) return proofs + def _generate_secret(self, randombits=128): + """Returns base64 encoded random string.""" + return scrts.token_urlsafe(randombits // 8) + def request_mint(self, amount): """Requests a mint from the server and returns Lightning invoice.""" r = requests.get(self.url + "/mint", params={"amount": amount}) @@ -66,7 +66,7 @@ class LedgerAPI: secrets = [] rs = [] for amount in amounts: - secret = str(random.getrandbits(128)) + secret = self._generate_secret() secrets.append(secret) B_, r = b_dhke.step1_bob(secret) rs.append(r) @@ -94,7 +94,7 @@ class LedgerAPI: secrets = [] payloads: MintPayloads = MintPayloads() for output_amt in fst_outputs + snd_outputs: - secret = str(random.getrandbits(128)) + secret = self._generate_secret() B_, r = b_dhke.step1_bob(secret) secrets.append((r, secret)) payload: BlindedMessage = BlindedMessage( @@ -191,7 +191,9 @@ class Wallet(LedgerAPI): @staticmethod async def serialize_proofs(proofs: List[Proof]): - proofs_serialized = [p.dict() for p in proofs] + proofs_serialized = [p.to_dict() for p in proofs] + if DEBUG: + print(proofs_serialized) token = base64.urlsafe_b64encode( json.dumps(proofs_serialized).encode() ).decode() From 307b58267eeeda054ea7fa511bb12ac054784cf8 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 22 Sep 2022 11:59:07 +0300 Subject: [PATCH 4/6] do not print json --- wallet/wallet.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/wallet/wallet.py b/wallet/wallet.py index c8280e0..0e48f05 100644 --- a/wallet/wallet.py +++ b/wallet/wallet.py @@ -8,14 +8,20 @@ from typing import List import requests import core.b_dhke as b_dhke -from core.base import (BlindedMessage, BlindedSignature, CheckPayload, - MeltPayload, MintPayloads, Proof, SplitPayload) +from core.base import ( + BlindedMessage, + BlindedSignature, + CheckPayload, + MeltPayload, + MintPayloads, + Proof, + SplitPayload, +) from core.db import Database from core.secp import PublicKey from core.settings import DEBUG from core.split import amount_split -from wallet.crud import (get_proofs, invalidate_proof, store_proof, - update_proof_reserved) +from wallet.crud import get_proofs, invalidate_proof, store_proof, update_proof_reserved class LedgerAPI: @@ -192,8 +198,6 @@ class Wallet(LedgerAPI): @staticmethod async def serialize_proofs(proofs: List[Proof]): proofs_serialized = [p.to_dict() for p in proofs] - if DEBUG: - print(proofs_serialized) token = base64.urlsafe_b64encode( json.dumps(proofs_serialized).encode() ).decode() From de6a527d4492323d8a82fdc4537c2ca0415a7bb6 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 22 Sep 2022 12:01:20 +0300 Subject: [PATCH 5/6] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6563f5c..43f3db4 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = wallet.cashu:cli"]} setuptools.setup( name="cashu", - version="0.1.7", + version="0.1.8", description="Ecash wallet and mint with Bitcoin Lightning support", long_description=long_description, long_description_content_type="text/markdown", From 8dfe2b8511cefd252430da7c5aec80d7f1f7ae95 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 22 Sep 2022 14:23:07 +0300 Subject: [PATCH 6/6] fix info --- README.md | 4 +++- cashu/cashu/__init__.py | 0 cashu/tests/__init__.py | 0 poetry.lock | 2 +- pyproject.toml | 4 +++- wallet/cashu.py | 35 +++++++++++++++++++++++++++-------- 6 files changed, 34 insertions(+), 11 deletions(-) delete mode 100644 cashu/cashu/__init__.py delete mode 100644 cashu/tests/__init__.py diff --git a/README.md b/README.md index 0123304..d54dc7e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,9 @@ The easiest way to use Cashu is to install the package it via pip: pip install cashu ``` -To update Cashu, use `pip install cashu -U`. You can skip the entire next section about Poetry and jump right to [Using Cashu](#using-cashu). +To update Cashu, use `pip install cashu -U`. If you have problems running the command above on Ubuntu, run `sudo apt install -y pip pkg-config libpq-dev`. + +You can skip the entire next section about Poetry and jump right to [Using Cashu](#using-cashu). ### Hard install: Poetry These steps help you install Python via pyenv and Poetry. If you already have Poetry running on your computer, you can skip this step and jump right to [Install Cashu](#install-cashu). diff --git a/cashu/cashu/__init__.py b/cashu/cashu/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/cashu/tests/__init__.py b/cashu/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/poetry.lock b/poetry.lock index e7c7e05..4f84b80 100644 --- a/poetry.lock +++ b/poetry.lock @@ -709,7 +709,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "a4553430bb7df5a66a7006d638986509d065e931ee5009ded5244eaa017475b2" +content-hash = "d5ee88384ec1ec1774f9fab845d7e8ac6e8ab1859506bcce178f6783b98e535c" [metadata.files] anyio = [ diff --git a/pyproject.toml b/pyproject.toml index 22ab0e8..9bd3ea3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cashu" -version = "0.0.1" +version = "0.1.8" description = "Ecash wallet and mint." authors = ["calle "] license = "MIT" @@ -38,6 +38,8 @@ isort = "^5.10.1" [tool.poetry.group.dev.dependencies] mypy = "^0.971" +black = {version = "^22.8.0", allow-prereleases = true} +isort = "^5.10.1" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/wallet/cashu.py b/wallet/cashu.py index 165692f..94394a2 100755 --- a/wallet/cashu.py +++ b/wallet/cashu.py @@ -40,11 +40,7 @@ class NaturalOrderGroup(click.Group): @click.option("--host", "-h", default=MINT_URL, help="Mint address.") @click.option("--wallet", "-w", "walletname", default="wallet", help="Wallet to use.") @click.pass_context -def cli( - ctx, - host: str, - walletname: str, -): +def cli(ctx, host: str, walletname: str): ctx.ensure_object(dict) ctx.obj["HOST"] = host ctx.obj["WALLET_NAME"] = walletname @@ -127,17 +123,27 @@ async def receive(ctx, token: str): @cli.command("burn", help="Burn spent tokens.") @click.argument("token", required=False, type=str) @click.option("--all", "-a", default=False, is_flag=True, help="Burn all spent tokens.") +@click.option( + "--force", "-f", default=False, is_flag=True, help="Force check on all tokens." +) @click.pass_context @coro -async def burn(ctx, token: str, all: bool): +async def burn(ctx, token: str, all: bool, force: bool): wallet: Wallet = ctx.obj["WALLET"] await init_wallet(wallet) - if not (all or token) or (token and all): - print("Error: enter a token or use --all to burn all pending tokens.") + if not (all or token or force) or (token and all): + print( + "Error: enter a token or use --all to burn all pending tokens or --force to check all tokens." + ) return if all: + # check only those who are flagged as reserved proofs = await get_reserved_proofs(wallet.db) + if force: + # check all proofs in db + proofs = wallet.proofs else: + # check only the specified ones proofs = [ Proof.from_dict(p) for p in json.loads(base64.urlsafe_b64decode(token)) ] @@ -191,3 +197,16 @@ async def pay(ctx, invoice: str): _, send_proofs = await wallet.split_to_send(wallet.proofs, amount) await wallet.pay_lightning(send_proofs, amount, invoice) wallet.status() + + +@cli.command("info", help="Information about Cashu wallet.") +@click.pass_context +@coro +async def info(ctx): + wallet: Wallet = ctx.obj["WALLET"] + await init_wallet(wallet) + wallet.status() + print(f"Debug: {DEBUG}") + print(f"Cashu dir: {CASHU_DIR}") + print(f"Mint URL: {MINT_URL}") + return