#!/usr/bin/env python import asyncio import base64 import json import math import os import sys from datetime import datetime from functools import wraps from itertools import groupby from operator import itemgetter import click from loguru import logger import cashu.core.bolt11 as bolt11 from cashu.core.base import Proof from cashu.core.bolt11 import Invoice from cashu.core.helpers import fee_reserve from cashu.core.migrations import migrate_databases from cashu.core.settings import CASHU_DIR, DEBUG, LIGHTNING, MINT_URL, VERSION, ENV_FILE from cashu.wallet import migrations from cashu.wallet.crud import get_reserved_proofs, get_unused_locks from cashu.wallet.wallet import Wallet as Wallet async def init_wallet(wallet: Wallet): """Performs migrations and loads proofs from db.""" await migrate_databases(wallet.db, migrations) await wallet.load_proofs() class NaturalOrderGroup(click.Group): """For listing commands in help in order of definition""" def list_commands(self, ctx): return self.commands.keys() @click.group(cls=NaturalOrderGroup) @click.option("--host", "-h", default=MINT_URL, help="Mint URL.") @click.option("--wallet", "-w", "walletname", default="wallet", help="Wallet name.") @click.pass_context def cli(ctx, host: str, walletname: str): # configure logger logger.remove() logger.add(sys.stderr, level="DEBUG" if DEBUG else "INFO") ctx.ensure_object(dict) ctx.obj["HOST"] = host ctx.obj["WALLET_NAME"] = walletname wallet = Wallet(ctx.obj["HOST"], os.path.join(CASHU_DIR, walletname)) ctx.obj["WALLET"] = wallet asyncio.run(init_wallet(wallet)) pass # https://github.com/pallets/click/issues/85#issuecomment-503464628 def coro(f): @wraps(f) def wrapper(*args, **kwargs): return asyncio.run(f(*args, **kwargs)) return wrapper @cli.command("mint", help="Mint.") @click.argument("amount", type=int) @click.option("--hash", default="", help="Hash of the paid invoice.", type=str) @click.pass_context @coro async def mint(ctx, amount: int, hash: str): wallet: Wallet = ctx.obj["WALLET"] wallet.load_mint() wallet.status() if not LIGHTNING: r = await wallet.mint(amount) elif amount and not hash: r = await wallet.request_mint(amount) if "pr" in r: print(f"Pay this invoice to mint {amount} sat:") print(f"Invoice: {r['pr']}") print("") print( f"After paying the invoice, run this command:\ncashu mint {amount} --hash {r['hash']}" ) elif amount and hash: await wallet.mint(amount, hash) wallet.status() return @cli.command("balance", help="Balance.") @click.pass_context @coro async def balance(ctx): wallet: Wallet = ctx.obj["WALLET"] wallet.status() @cli.command("send", help="Send coins.") @click.argument("amount", type=int) @click.option("--lock", "-l", default=None, help="Lock coins (P2SH).", type=str) @click.pass_context @coro async def send(ctx, amount: int, lock: str): if lock and len(lock) < 22: print("Error: lock has to be at least 22 characters long.") return p2sh = False if lock and len(lock.split("P2SH:")) == 2: p2sh = True wallet: Wallet = ctx.obj["WALLET"] wallet.load_mint() wallet.status() _, send_proofs = await wallet.split_to_send(wallet.proofs, amount, lock) await wallet.set_reserved(send_proofs, reserved=True) coin = await wallet.serialize_proofs( send_proofs, hide_secrets=True if lock and not p2sh else False ) print(coin) wallet.status() @cli.command("receive", help="Receive coins.") @click.argument("coin", type=str) @click.option("--unlock", "-u", default=None, help="Unlock coins.", type=str) @click.pass_context @coro async def receive(ctx, coin: str, unlock: str): wallet: Wallet = ctx.obj["WALLET"] wallet.load_mint() wallet.status() if unlock: assert ( len(unlock.split(":")) == 2 ), "unlock format wrong, expected