mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 10:34:20 +01:00
Nutshell cleanup wishlist (#332)
* fix keys * fix tests * backwards compatible api upgrade * upgrade seems to work * fix tests * add deprecated api functions * add more tests of backwards compat * add test serialization for nut00 * remove a redundant test * move mint and melt to new api * mypy works * CI: mypy --check-untyped-defs * add deprecated router * add hints and remove logs * fix tests * cleanup * use new mint and melt endpoints * tests passing? * fix mypy * make format * make format * make format * commit * errors gone * save * adjust the API * store quotes in db * make mypy happy * add fakewallet settings * remove LIGHTNING=True and pass quote id for melt * format * tests passing * add CoreLightningRestWallet * add macaroon loader * add correct config * preimage -> proof * move wallet.status() to cli.helpers.print_status() * remove statuses from tests * remove * make format * Use httpx in deprecated wallet * fix cln interface * create invoice before quote * internal transactions and deprecated api testing * fix tests * add deprecated API tests * fastapi type hints break things * fix duplicate wallet error * make format * update poetry in CI to 1.7.1 * precommit restore * remove bolt11 * oops * default poetry * store fee reserve for melt quotes and refactor melt() * works? * make format * test * finally * fix deprecated models * rename v1 endpoints to bolt11 * raise restore and check to v1, bump version to 0.15.0 * add version byte to keyset id * remove redundant fields in json * checks * generate bip32 keyset wip * migrate old keysets * load duplicate keys * duplicate old keysets * revert router changes * add deprecated /check and /restore endpoints * try except invalidate * parse unit from derivation path, adjust keyset id calculation with bytes * remove keyest id from functions again and rely on self.keyset_id * mosts tests work * mint loads multiple derivation paths * make format * properly print units * fix tests * wallet works with multiple units * add strike wallet and choose backend dynamically * fix mypy * add get_payment_quote to lightning backends * make format * fix startup * fix lnbitswallet * fix tests * LightningWallet -> LightningBackend * remove comments * make format * remove msat conversion * add Amount type * fix regtest * use melt_quote as argument for pay_invoice * test old api * fees in sats * fix deprecated fees * fixes * print balance correctly * internally index keyset response by int * add pydantic validation to input models * add timestamps to mint db * store timestamps for invoices, promises, proofs_used * fix wallet migration * rotate keys correctly for testing * remove print * update latest keyset * fix tests * fix test * make format * make format with correct black version * remove nsat and cheese * test against deprecated mint * fix tests? * actually use env var * mint run with env vars * moar test * cleanup * simplify tests, load all keys * try out testing with internal invoices * fix internal melt test * fix test * deprecated checkfees expects appropriate fees * adjust comment * drop lightning table * split migration for testing for now, remove it later * remove unused lightning table * skip_private_key -> skip_db_read * throw error on migration error * reorder * fix migrations * fix lnbits fee return value negative * fix typo * comments * add type * make format * split must use correct amount * fix tests * test deprecated api with internal/external melts * do not split if not necessary * refactor * fix test * make format with new black * cleanup and add comments * add quote state check endpoints * fix deprecated wallet response * split -> swap endpoint * make format * add expiry to quotes, get quote endpoints, and adjust to nut review comments * allow overpayment of melt * add lightning wallet tests * commiting to save * fix tests a bit * make format * remove comments * get mint info * check_spendable default False, and return payment quote checking id * make format * bump version in pyproject * update to /v1/checkstate * make format * fix mint api checks * return witness on /v1/checkstate * no failfast * try fail-fast: false in ci.yaml * fix db lookup * clean up literals
This commit is contained in:
@@ -14,7 +14,9 @@ import click
|
||||
from click import Context
|
||||
from loguru import logger
|
||||
|
||||
from ...core.base import TokenV3
|
||||
from cashu.core.logging import configure_logger
|
||||
|
||||
from ...core.base import TokenV3, Unit
|
||||
from ...core.helpers import sum_proofs
|
||||
from ...core.settings import settings
|
||||
from ...nostr.client.client import NostrClient
|
||||
@@ -26,7 +28,13 @@ from ...wallet.crud import (
|
||||
)
|
||||
from ...wallet.wallet import Wallet as Wallet
|
||||
from ..api.api_server import start_api_server
|
||||
from ..cli.cli_helpers import get_mint_wallet, print_mint_balances, verify_mint
|
||||
from ..cli.cli_helpers import (
|
||||
get_mint_wallet,
|
||||
get_unit_wallet,
|
||||
print_balance,
|
||||
print_mint_balances,
|
||||
verify_mint,
|
||||
)
|
||||
from ..helpers import (
|
||||
deserialize_token_from_string,
|
||||
init_wallet,
|
||||
@@ -74,6 +82,13 @@ def coro(f):
|
||||
default=settings.wallet_name,
|
||||
help=f"Wallet name (default: {settings.wallet_name}).",
|
||||
)
|
||||
@click.option(
|
||||
"--unit",
|
||||
"-u",
|
||||
"unit",
|
||||
default=None,
|
||||
help=f"Wallet unit (default: {settings.wallet_unit}).",
|
||||
)
|
||||
@click.option(
|
||||
"--daemon",
|
||||
"-d",
|
||||
@@ -92,7 +107,9 @@ def coro(f):
|
||||
)
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def cli(ctx: Context, host: str, walletname: str, tests: bool):
|
||||
async def cli(ctx: Context, host: str, walletname: str, unit: str, tests: bool):
|
||||
if settings.debug:
|
||||
configure_logger()
|
||||
if settings.tor and not TorProxy().check_platform():
|
||||
error_str = (
|
||||
"Your settings say TOR=true but the built-in Tor bundle is not supported on"
|
||||
@@ -120,6 +137,7 @@ async def cli(ctx: Context, host: str, walletname: str, tests: bool):
|
||||
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj["HOST"] = host or settings.mint_url
|
||||
ctx.obj["UNIT"] = unit
|
||||
ctx.obj["WALLET_NAME"] = walletname
|
||||
settings.wallet_name = walletname
|
||||
|
||||
@@ -128,13 +146,13 @@ async def cli(ctx: Context, host: str, walletname: str, tests: bool):
|
||||
# otherwise it will create a mnemonic and store it in the database
|
||||
if ctx.invoked_subcommand == "restore":
|
||||
wallet = await Wallet.with_db(
|
||||
ctx.obj["HOST"], db_path, name=walletname, skip_private_key=True
|
||||
ctx.obj["HOST"], db_path, name=walletname, skip_db_read=True
|
||||
)
|
||||
else:
|
||||
# # we need to run the migrations before we load the wallet for the first time
|
||||
# # otherwise the wallet will not be able to generate a new private key and store it
|
||||
wallet = await Wallet.with_db(
|
||||
ctx.obj["HOST"], db_path, name=walletname, skip_private_key=True
|
||||
ctx.obj["HOST"], db_path, name=walletname, skip_db_read=True
|
||||
)
|
||||
# now with the migrations done, we can load the wallet and generate a new mnemonic if needed
|
||||
wallet = await Wallet.with_db(ctx.obj["HOST"], db_path, name=walletname)
|
||||
@@ -143,11 +161,13 @@ async def cli(ctx: Context, host: str, walletname: str, tests: bool):
|
||||
ctx.obj["WALLET"] = wallet
|
||||
# await init_wallet(ctx.obj["WALLET"], load_proofs=False)
|
||||
|
||||
# ------ MUTLIMINT ------- : Select a wallet
|
||||
# only if a command is one of a subset that needs to specify a mint host
|
||||
# if a mint host is already specified as an argument `host`, use it
|
||||
if ctx.invoked_subcommand not in ["send", "invoice", "pay"] or host:
|
||||
return
|
||||
# ------ MULTIUNIT ------- : Select a unit
|
||||
ctx.obj["WALLET"] = await get_unit_wallet(ctx)
|
||||
# ------ MUTLIMINT ------- : Select a wallet
|
||||
# else: we ask the user to select one
|
||||
ctx.obj["WALLET"] = await get_mint_wallet(
|
||||
ctx
|
||||
@@ -165,13 +185,17 @@ async def cli(ctx: Context, host: str, walletname: str, tests: bool):
|
||||
async def pay(ctx: Context, invoice: str, yes: bool):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await wallet.load_mint()
|
||||
wallet.status()
|
||||
total_amount, fee_reserve_sat = await wallet.get_pay_amount_with_fees(invoice)
|
||||
print_balance(ctx)
|
||||
quote = await wallet.get_pay_amount_with_fees(invoice)
|
||||
logger.debug(f"Quote: {quote}")
|
||||
total_amount = quote.amount + quote.fee_reserve
|
||||
if not yes:
|
||||
potential = (
|
||||
f" ({total_amount} sat with potential fees)" if fee_reserve_sat else ""
|
||||
f" ({wallet.unit.str(total_amount)} with potential fees)"
|
||||
if quote.fee_reserve
|
||||
else ""
|
||||
)
|
||||
message = f"Pay {total_amount - fee_reserve_sat} sat{potential}?"
|
||||
message = f"Pay {wallet.unit.str(quote.amount)}{potential}?"
|
||||
click.confirm(
|
||||
message,
|
||||
abort=True,
|
||||
@@ -181,27 +205,26 @@ async def pay(ctx: Context, invoice: str, yes: bool):
|
||||
print("Paying Lightning invoice ...", end="", flush=True)
|
||||
assert total_amount > 0, "amount is not positive"
|
||||
if wallet.available_balance < total_amount:
|
||||
print("Error: Balance too low.")
|
||||
print(" Error: Balance too low.")
|
||||
return
|
||||
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
||||
try:
|
||||
melt_response = await wallet.pay_lightning(
|
||||
send_proofs, invoice, fee_reserve_sat
|
||||
send_proofs, invoice, quote.fee_reserve, quote.quote
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nError paying invoice: {str(e)}")
|
||||
print(f" Error paying invoice: {str(e)}")
|
||||
return
|
||||
print(" Invoice paid", end="", flush=True)
|
||||
if melt_response.preimage and melt_response.preimage != "0" * 64:
|
||||
print(f" (Proof: {melt_response.preimage}).")
|
||||
if melt_response.payment_preimage and melt_response.payment_preimage != "0" * 64:
|
||||
print(f" (Preimage: {melt_response.payment_preimage}).")
|
||||
else:
|
||||
print(".")
|
||||
wallet.status()
|
||||
print_balance(ctx)
|
||||
|
||||
|
||||
@cli.command("invoice", help="Create Lighting invoice.")
|
||||
@click.argument("amount", type=int)
|
||||
@click.argument("amount", type=float)
|
||||
@click.option("--id", default="", help="Id of the paid invoice.", type=str)
|
||||
@click.option(
|
||||
"--split",
|
||||
@@ -223,7 +246,8 @@ async def pay(ctx: Context, invoice: str, yes: bool):
|
||||
async def invoice(ctx: Context, amount: int, id: str, split: int, no_check: bool):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await wallet.load_mint()
|
||||
wallet.status()
|
||||
print_balance(ctx)
|
||||
amount = int(amount * 100) if wallet.unit == Unit.usd else int(amount)
|
||||
# in case the user wants a specific split, we create a list of amounts
|
||||
optional_split = None
|
||||
if split:
|
||||
@@ -231,15 +255,16 @@ async def invoice(ctx: Context, amount: int, id: str, split: int, no_check: bool
|
||||
assert amount >= split, "split must smaller or equal amount"
|
||||
n_splits = amount // split
|
||||
optional_split = [split] * n_splits
|
||||
logger.debug(f"Requesting split with {n_splits} * {split} sat tokens.")
|
||||
logger.debug(
|
||||
f"Requesting split with {n_splits} * {wallet.unit.str(split)} tokens."
|
||||
)
|
||||
|
||||
if not settings.lightning:
|
||||
await wallet.mint(amount, split=optional_split)
|
||||
# user requests an invoice
|
||||
elif amount and not id:
|
||||
if amount and not id:
|
||||
invoice = await wallet.request_mint(amount)
|
||||
if invoice.bolt11:
|
||||
print(f"Pay invoice to mint {amount} sat:")
|
||||
print("")
|
||||
print(f"Pay invoice to mint {wallet.unit.str(amount)}:")
|
||||
print("")
|
||||
print(f"Invoice: {invoice.bolt11}")
|
||||
print("")
|
||||
@@ -280,7 +305,8 @@ async def invoice(ctx: Context, amount: int, id: str, split: int, no_check: bool
|
||||
# user paid invoice and want to check it
|
||||
elif amount and id:
|
||||
await wallet.mint(amount, split=optional_split, id=id)
|
||||
wallet.status()
|
||||
print("")
|
||||
print_balance(ctx)
|
||||
return
|
||||
|
||||
|
||||
@@ -288,8 +314,6 @@ async def invoice(ctx: Context, amount: int, id: str, split: int, no_check: bool
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def swap(ctx: Context):
|
||||
if not settings.lightning:
|
||||
raise Exception("lightning not supported.")
|
||||
print("Select the mint to swap from:")
|
||||
outgoing_wallet = await get_mint_wallet(ctx, force_select=True)
|
||||
|
||||
@@ -302,22 +326,23 @@ async def swap(ctx: Context):
|
||||
if incoming_wallet.url == outgoing_wallet.url:
|
||||
raise Exception("mints for swap have to be different")
|
||||
|
||||
amount = int(input("Enter amount to swap in sat: "))
|
||||
amount = int(input(f"Enter amount to swap in {incoming_wallet.unit.name}: "))
|
||||
assert amount > 0, "amount is not positive"
|
||||
|
||||
# request invoice from incoming mint
|
||||
invoice = await incoming_wallet.request_mint(amount)
|
||||
|
||||
# pay invoice from outgoing mint
|
||||
total_amount, fee_reserve_sat = await outgoing_wallet.get_pay_amount_with_fees(
|
||||
invoice.bolt11
|
||||
)
|
||||
quote = await outgoing_wallet.get_pay_amount_with_fees(invoice.bolt11)
|
||||
total_amount = quote.amount + quote.fee_reserve
|
||||
if outgoing_wallet.available_balance < total_amount:
|
||||
raise Exception("balance too low")
|
||||
_, send_proofs = await outgoing_wallet.split_to_send(
|
||||
outgoing_wallet.proofs, total_amount, set_reserved=True
|
||||
)
|
||||
await outgoing_wallet.pay_lightning(send_proofs, invoice.bolt11, fee_reserve_sat)
|
||||
await outgoing_wallet.pay_lightning(
|
||||
send_proofs, invoice.bolt11, quote.fee_reserve, quote.quote
|
||||
)
|
||||
|
||||
# mint token in incoming mint
|
||||
await incoming_wallet.mint(amount, id=invoice.id)
|
||||
@@ -339,34 +364,44 @@ async def swap(ctx: Context):
|
||||
@coro
|
||||
async def balance(ctx: Context, verbose):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await wallet.load_proofs()
|
||||
await wallet.load_proofs(unit=False)
|
||||
unit_balances = wallet.balance_per_unit()
|
||||
if len(unit_balances) > 1 and not ctx.obj["UNIT"]:
|
||||
print(f"You have balances in {len(unit_balances)} units:")
|
||||
print("")
|
||||
for i, (k, v) in enumerate(unit_balances.items()):
|
||||
unit = k
|
||||
print(f"Unit {i+1} ({unit}) – Balance: {unit.str(int(v['available']))}")
|
||||
print("")
|
||||
if verbose:
|
||||
# show balances per keyset
|
||||
keyset_balances = wallet.balance_per_keyset()
|
||||
if len(keyset_balances) > 1:
|
||||
if len(keyset_balances):
|
||||
print(f"You have balances in {len(keyset_balances)} keysets:")
|
||||
print("")
|
||||
for k, v in keyset_balances.items():
|
||||
for k, v in keyset_balances.items(): # type: ignore
|
||||
unit = Unit[str(v["unit"])]
|
||||
print(
|
||||
f"Keyset: {k} - Balance: {v['available']} sat (pending:"
|
||||
f" {v['balance']-v['available']} sat)"
|
||||
f"Keyset: {k} - Balance: {unit.str(int(v['available']))} (pending:"
|
||||
f" {unit.str(int(v['balance'])-int(v['available']))})"
|
||||
)
|
||||
print("")
|
||||
|
||||
await print_mint_balances(wallet)
|
||||
|
||||
await wallet.load_proofs(reload=True)
|
||||
if verbose:
|
||||
print(
|
||||
f"Balance: {wallet.available_balance} sat (pending:"
|
||||
f" {wallet.balance-wallet.available_balance} sat) in"
|
||||
f"Balance: {wallet.unit.str(wallet.available_balance)} (pending:"
|
||||
f" {wallet.unit.str(wallet.balance-wallet.available_balance)}) in"
|
||||
f" {len([p for p in wallet.proofs if not p.reserved])} tokens"
|
||||
)
|
||||
else:
|
||||
print(f"Balance: {wallet.available_balance} sat")
|
||||
print(f"Balance: {wallet.unit.str(wallet.available_balance)}")
|
||||
|
||||
|
||||
@cli.command("send", help="Send tokens.")
|
||||
@click.argument("amount", type=int)
|
||||
@click.argument("amount", type=float)
|
||||
@click.argument("nostr", type=str, required=False)
|
||||
@click.option(
|
||||
"--nostr",
|
||||
@@ -426,6 +461,7 @@ async def send_command(
|
||||
nosplit: bool,
|
||||
):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
amount = int(amount * 100) if wallet.unit == Unit.usd else int(amount)
|
||||
if not nostr and not nopt:
|
||||
await send(
|
||||
wallet,
|
||||
@@ -439,6 +475,7 @@ async def send_command(
|
||||
await send_nostr(
|
||||
wallet, amount=amount, pubkey=nostr or nopt, verbose=verbose, yes=yes
|
||||
)
|
||||
print_balance(ctx)
|
||||
|
||||
|
||||
@cli.command("receive", help="Receive tokens.")
|
||||
@@ -493,6 +530,8 @@ async def receive_cli(
|
||||
await receive(wallet, tokenObj)
|
||||
else:
|
||||
print("Error: enter token or use either flag --nostr or --all.")
|
||||
return
|
||||
print_balance(ctx)
|
||||
|
||||
|
||||
@cli.command("burn", help="Burn spent tokens.")
|
||||
@@ -536,10 +575,10 @@ async def burn(ctx: Context, token: str, all: bool, force: bool, delete: str):
|
||||
proofs = tokenObj.get_proofs()
|
||||
|
||||
if delete:
|
||||
await wallet.invalidate(proofs, check_spendable=False)
|
||||
else:
|
||||
await wallet.invalidate(proofs)
|
||||
wallet.status()
|
||||
else:
|
||||
await wallet.invalidate(proofs, check_spendable=True)
|
||||
print_balance(ctx)
|
||||
|
||||
|
||||
@cli.command("pending", help="Show pending tokens.")
|
||||
@@ -591,7 +630,8 @@ async def pending(ctx: Context, legacy, number: int, offset: int):
|
||||
int(grouped_proofs[0].time_reserved)
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(
|
||||
f"#{i} Amount: {sum_proofs(grouped_proofs)} sat Time:"
|
||||
f"#{i} Amount:"
|
||||
f" {wallet.unit.str(sum_proofs(grouped_proofs))} Time:"
|
||||
f" {reserved_date} ID: {key} Mint: {mint}\n"
|
||||
)
|
||||
print(f"{token}\n")
|
||||
@@ -697,9 +737,10 @@ async def wallets(ctx):
|
||||
if w == ctx.obj["WALLET_NAME"]:
|
||||
active_wallet = True
|
||||
print(
|
||||
f"Wallet: {w}\tBalance: {sum_proofs(wallet.proofs)} sat"
|
||||
f"Wallet: {w}\tBalance:"
|
||||
f" {wallet.unit.str(sum_proofs(wallet.proofs))}"
|
||||
" (available: "
|
||||
f"{sum_proofs([p for p in wallet.proofs if not p.reserved])} sat){' *' if active_wallet else ''}"
|
||||
f"{wallet.unit.str(sum_proofs([p for p in wallet.proofs if not p.reserved]))}){' *' if active_wallet else ''}"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -737,25 +778,32 @@ async def info(ctx: Context, mint: bool, mnemonic: bool):
|
||||
if mint:
|
||||
for mint_url in mint_list:
|
||||
wallet.url = mint_url
|
||||
mint_info: dict = (await wallet._load_mint_info()).dict()
|
||||
print("")
|
||||
print("Mint information:")
|
||||
print("")
|
||||
print(f"Mint URL: {mint_url}")
|
||||
if mint_info:
|
||||
print(f"Mint name: {mint_info['name']}")
|
||||
if mint_info["description"]:
|
||||
print(f"Description: {mint_info['description']}")
|
||||
if mint_info["description_long"]:
|
||||
print(f"Long description: {mint_info['description_long']}")
|
||||
if mint_info["contact"]:
|
||||
print(f"Contact: {mint_info['contact']}")
|
||||
if mint_info["version"]:
|
||||
print(f"Version: {mint_info['version']}")
|
||||
if mint_info["motd"]:
|
||||
print(f"Message of the day: {mint_info['motd']}")
|
||||
if mint_info["parameter"]:
|
||||
print(f"Parameter: {mint_info['parameter']}")
|
||||
try:
|
||||
mint_info: dict = (await wallet._load_mint_info()).dict()
|
||||
print("")
|
||||
print("---- Mint information ----")
|
||||
print("")
|
||||
print(f"Mint URL: {mint_url}")
|
||||
if mint_info:
|
||||
print(f"Mint name: {mint_info['name']}")
|
||||
if mint_info.get("description"):
|
||||
print(f"Description: {mint_info['description']}")
|
||||
if mint_info.get("description_long"):
|
||||
print(f"Long description: {mint_info['description_long']}")
|
||||
if mint_info.get("contact"):
|
||||
print(f"Contact: {mint_info['contact']}")
|
||||
if mint_info.get("version"):
|
||||
print(f"Version: {mint_info['version']}")
|
||||
if mint_info.get("motd"):
|
||||
print(f"Message of the day: {mint_info['motd']}")
|
||||
if mint_info.get("nuts"):
|
||||
print(
|
||||
"Supported NUTS:"
|
||||
f" {', '.join(['NUT-'+str(k) for k in mint_info['nuts'].keys()])}"
|
||||
)
|
||||
except Exception as e:
|
||||
print("")
|
||||
print(f"Error fetching mint information for {mint_url}: {e}")
|
||||
|
||||
if mnemonic:
|
||||
assert wallet.mnemonic
|
||||
@@ -807,7 +855,7 @@ async def restore(ctx: Context, to: int, batch: int):
|
||||
|
||||
await wallet.restore_wallet_from_mnemonic(mnemonic, to=to, batch=batch)
|
||||
await wallet.load_proofs()
|
||||
wallet.status()
|
||||
print_balance(ctx)
|
||||
|
||||
|
||||
@cli.command("selfpay", help="Refresh tokens.")
|
||||
@@ -820,7 +868,7 @@ async def selfpay(ctx: Context, all: bool = False):
|
||||
|
||||
# get balance on this mint
|
||||
mint_balance_dict = await wallet.balance_per_minturl()
|
||||
mint_balance = mint_balance_dict[wallet.url]["available"]
|
||||
mint_balance = int(mint_balance_dict[wallet.url]["available"])
|
||||
# send balance once to mark as reserved
|
||||
await wallet.split_to_send(wallet.proofs, mint_balance, None, set_reserved=True)
|
||||
# load all reserved proofs (including the one we just sent)
|
||||
|
||||
Reference in New Issue
Block a user