Merge pull request #29 from callebtc/chore/comments_and_refactors

Comments, refactor, new CLI command: cashu wallets
This commit is contained in:
calle
2022-10-09 22:14:38 +02:00
committed by GitHub
11 changed files with 79 additions and 45 deletions

View File

@@ -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],

View File

@@ -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]

View File

@@ -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):

View File

@@ -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"

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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
View File

@@ -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]]

View File

@@ -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"

View File

@@ -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",