From 17ba6e0841a8c4f4d9375c2e075bc7080d11dbd0 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 15 Sep 2022 02:48:22 +0300 Subject: [PATCH 1/4] poetry commands, docker, click --- cashu | 75 ------------------------------------------------ cashu.py | 1 + mint/app.py | 53 ++++++++++++++++++++++++++++++++-- mint/ledger.py | 11 ++----- pyproject.toml | 4 +++ wallet/wallet.py | 1 + 6 files changed, 59 insertions(+), 86 deletions(-) delete mode 100755 cashu create mode 100755 cashu.py diff --git a/cashu b/cashu deleted file mode 100755 index 69e131c..0000000 --- a/cashu +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env python - -import asyncio -import base64 -import json -from functools import wraps - -import click -from bech32 import bech32_decode, bech32_encode, convertbits - -from wallet.migrations import m001_initial -from wallet.wallet import Wallet as Wallet -from core.settings import MINT_URL - -# 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 - - -@click.command("mint") -@click.option("--host", default=MINT_URL, help="Mint hostname.") -@click.option("--wallet", default="wallet", help="Wallet to use.") -@click.option("--mint", default=0, help="Mint tokens.") -@click.option("--hash", default="", help="Hash of the paid invoice.") -@click.option("--send", default=0, help="Send tokens.") -@click.option("--receive", default="", help="Receive tokens.") -@click.option("--invalidate", default="", help="Invalidate tokens.") -@coro -async def main(host, wallet, mint, hash, send, receive, invalidate): - wallet = Wallet(host, f"data/{wallet}", wallet) - await m001_initial(db=wallet.db) - await wallet.load_proofs() - if mint and not hash: - print(f"Balance: {wallet.balance}") - r = await wallet.request_mint(mint) - print(r) - - if mint and hash: - print(f"Balance: {wallet.balance}") - await wallet.mint(mint, hash) - print(f"Balance: {wallet.balance}") - - if send: - wallet.status() - _, send_proofs = await wallet.split(wallet.proofs, send) - print(base64.urlsafe_b64encode(json.dumps(send_proofs).encode()).decode()) - - if receive: - wallet.status() - proofs = json.loads(base64.urlsafe_b64decode(receive)) - _, _ = await wallet.redeem(proofs) - wallet.status() - - if invalidate: - wallet.status() - proofs = json.loads(base64.urlsafe_b64decode(invalidate)) - await wallet.invalidate(proofs) - wallet.status() - - -if __name__ == "__main__": - main() - - -@click.command("send") -@click.option("--send", default=1, help="Mint tokens.") -@coro -async def send(send): - print("asd") - # w1_fst_proofs, w1_snd_proofs = await wallet.split(proofs, 20) - return "asd" diff --git a/cashu.py b/cashu.py new file mode 100755 index 0000000..91328f5 --- /dev/null +++ b/cashu.py @@ -0,0 +1 @@ +# for mysterious reasons, this file needs to exist for `poetry run cashu` to work diff --git a/mint/app.py b/mint/app.py index da0189d..d290751 100644 --- a/mint/app.py +++ b/mint/app.py @@ -3,13 +3,15 @@ import logging import sys from typing import Union +import click +import uvicorn from ecc.curve import Point, secp256k1 from fastapi import FastAPI from loguru import logger import core.settings as settings from core.base import MintPayloads, SplitPayload -from core.settings import MINT_PRIVATE_KEY +from core.settings import MINT_PRIVATE_KEY, MINT_SERVER_HOST, MINT_SERVER_PORT from lightning import WALLET from mint.ledger import Ledger from mint.migrations import m001_initial @@ -148,5 +150,50 @@ async def split(payload: SplitPayload): return {"error": str(exc)} -# if __name__ == "__main__": -# uvicorn.run(app, host="127.0.0.1", port=5049) +@click.command( + context_settings=dict( + ignore_unknown_options=True, + allow_extra_args=True, + ) +) +@click.option("--port", default=MINT_SERVER_PORT, help="Port to listen on") +@click.option("--host", default=MINT_SERVER_HOST, help="Host to run mint on") +@click.option("--ssl-keyfile", default=None, help="Path to SSL keyfile") +@click.option("--ssl-certfile", default=None, help="Path to SSL certificate") +@click.pass_context +def main( + ctx, + port: int = MINT_SERVER_PORT, + host: str = MINT_SERVER_HOST, + ssl_keyfile: str = None, + ssl_certfile: str = None, +): + """Launched with `poetry run mint` at root level""" + # this beautiful beast parses all command line arguments and passes them to the uvicorn server + d = dict() + for a in ctx.args: + item = a.split("=") + if len(item) > 1: # argument like --key=value + print(a, item) + d[item[0].strip("--").replace("-", "_")] = ( + int(item[1]) # need to convert to int if it's a number + if item[1].isdigit() + else item[1] + ) + else: + d[a.strip("--")] = True # argument like --key + + config = uvicorn.Config( + "mint.app:app", + port=port, + host=host, + ssl_keyfile=ssl_keyfile, + ssl_certfile=ssl_certfile, + **d, + ) + server = uvicorn.Server(config) + server.run() + + +if __name__ == "__main__": + main() diff --git a/mint/ledger.py b/mint/ledger.py index f4027c7..36fa5a1 100644 --- a/mint/ledger.py +++ b/mint/ledger.py @@ -13,14 +13,9 @@ from core.db import Database from core.settings import 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/pyproject.toml b/pyproject.toml index 0ab6164..fab6fd1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,3 +39,7 @@ isort = "^5.10.1" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +mint = "mint.app:main" +cashu = "wallet.cashu:cli" \ No newline at end of file diff --git a/wallet/wallet.py b/wallet/wallet.py index 311abdb..c31cd6c 100644 --- a/wallet/wallet.py +++ b/wallet/wallet.py @@ -139,6 +139,7 @@ class Wallet(LedgerAPI): return await self.split(proofs, sum(p["amount"] for p in proofs)) async def split(self, proofs, amount): + assert len(proofs) > 0, ValueError("no proofs provided.") fst_proofs, snd_proofs = super().split(proofs, amount) if len(fst_proofs) == 0 and len(snd_proofs) == 0: raise Exception("received no splits") From 2ba0b17a3f09f32e1627389db992decd76a6ee1d Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 15 Sep 2022 02:51:05 +0300 Subject: [PATCH 2/4] update new commands --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9333baf..dad23bb 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ To use the wallet with the [public test mint](#test-instance), you need to chang ## Run a mint yourself This runs the mint on your local computer. Skip this step if you want to use the [public test mint](#test-instance) instead. ```bash -poetry run uvicorn mint.app:app --port 3338 +poetry run mint ``` ## Use wallet @@ -59,7 +59,7 @@ poetry run uvicorn mint.app:app --port 3338 This command will return a Lightning invoice and a payment hash. You have to pay the invoice before you can receive the tokens. Note: Minting tokens involves two steps: requesting a mint, and actually minting tokens (see below). ```bash -poetry run ./cashu --wallet=wallet --mint=420 +poetry run cashu mint 420 ``` Returns: ```bash @@ -73,7 +73,7 @@ Balance: 0 #### Mint tokens After paying the invoice, copy the `hash` value from above and add it to the command ```bash -poetry run ./cashu --wallet=wallet --mint=420 --hash=009d6eb02da8769b37602ac6d98ecafc1d65bd2408114a5d50e60da200bc85c5 +poetry run cashu mint 420 --hash=009d6eb02da8769b37602ac6d98ecafc1d65bd2408114a5d50e60da200bc85c5 ``` You should see your balance update accordingly: ```bash @@ -84,7 +84,7 @@ Balance: 420 #### Send tokens To send tokens to another user, enter ```bash -poetry run ./cashu --send=69 +poetry run cashu send 69 ``` You should see the encoded token. Copy the token and send it to another user such as via email or a messenger. The token looks like this: ```bash @@ -94,7 +94,7 @@ W3siYW1vdW50IjogMSwgIkMiOiB7IngiOiAzMzg0Mzg0NDYzNzAwMTY1NDA2MTQxMDY3Mzg1MDg5MjA2 #### Receive tokens To receive tokens, another user enters: ```bash -poetry run ./cashu --receive=W3siYW1vdW50IjogMSwgIkMiOi... +poetry run cashu receive W3siYW1vdW50IjogMSwgIkMiOi... ``` You should see the balance increase: ```bash @@ -102,10 +102,10 @@ wallet balance: 0 wallet balance: 69 ``` -#### Invalidate tokens -The sending user needs to invalidate their tokens from above, otherwise they will try to double spend them (which won't work because the server keeps a list of all spent tokens): +#### Burn tokens +The sending user needs to burn (invalidate) their tokens from above, otherwise they will try to double spend them (which won't work because the server keeps a list of all spent tokens): ```bash -poetry run ./cashu --invalidate=W3siYW1vdW50IjogMSwgIkMiOi... +poetry run cashu burn W3siYW1vdW50IjogMSwgIkMiOi... ``` Returns: ```bash From 28e76826b88294a000fe0980d4405bc0f217df28 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 15 Sep 2022 02:58:42 +0300 Subject: [PATCH 3/4] add balance command --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index dad23bb..4f6bbff 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,11 @@ Balance: 0 Balance: 420 ``` +#### Check balance +```bash +poetry run cashu balance +``` + #### Send tokens To send tokens to another user, enter ```bash From aed30fb2e5cc007497fda6a3f9c2f1b5550346ea Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 15 Sep 2022 03:00:15 +0300 Subject: [PATCH 4/4] Dockerfile --- Dockerfile | 11 +++++ wallet/cashu.py | 118 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 Dockerfile create mode 100755 wallet/cashu.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4dde36e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.9-slim +RUN apt-get update +RUN apt-get install -y curl python3-dev autoconf g++ +RUN curl -sSL https://install.python-poetry.org | python3 - +ENV PATH="/root/.local/bin:$PATH" +WORKDIR /app +COPY . . +RUN poetry config virtualenvs.create false +RUN poetry install --no-dev --no-root +EXPOSE 3338 +CMD ["poetry", "run", "mint", "--port", "3338", "--host", "0.0.0.0"] \ No newline at end of file diff --git a/wallet/cashu.py b/wallet/cashu.py new file mode 100755 index 0000000..694c177 --- /dev/null +++ b/wallet/cashu.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python + +import asyncio +import base64 +import json +from functools import wraps + +import click +from bech32 import bech32_decode, bech32_encode, convertbits + +from core.settings import MINT_URL +from wallet.migrations import m001_initial +from wallet.wallet import Wallet as Wallet + + +async def init_wallet(wallet: Wallet): + """Performs migrations and loads proofs from db.""" + await m001_initial(db=wallet.db) + 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 address.") +@click.option("--wallet", "-w", "walletname", default="wallet", help="Wallet to use.") +@click.pass_context +def cli( + ctx, + host: str, + walletname: str, +): + ctx.ensure_object(dict) + ctx.obj["HOST"] = host + ctx.obj["WALLET_NAME"] = walletname + ctx.obj["WALLET"] = Wallet(ctx.obj["HOST"], f"data/{walletname}", walletname) + 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 tokens.") +@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"] + await m001_initial(db=wallet.db) + await wallet.load_proofs() + if amount and not hash: + print(f"Balance: {wallet.balance}") + r = await wallet.request_mint(amount) + print(r) + + if amount and hash: + print(f"Balance: {wallet.balance}") + await wallet.mint(amount, hash) + print(f"Balance: {wallet.balance}") + + +@cli.command("balance", help="See balance.") +@click.pass_context +@coro +async def receive(ctx): + wallet: Wallet = ctx.obj["WALLET"] + await init_wallet(wallet) + wallet.status() + + +@cli.command("send", help="Send tokens.") +@click.argument("amount", type=int) +@click.pass_context +@coro +async def send(ctx, amount: int): + wallet: Wallet = ctx.obj["WALLET"] + await init_wallet(wallet) + wallet.status() + _, send_proofs = await wallet.split(wallet.proofs, amount) + print(base64.urlsafe_b64encode(json.dumps(send_proofs).encode()).decode()) + + +@cli.command("receive", help="Receive tokens.") +@click.argument("token", type=str) +@click.pass_context +@coro +async def receive(ctx, token: str): + wallet: Wallet = ctx.obj["WALLET"] + await init_wallet(wallet) + wallet.status() + proofs = json.loads(base64.urlsafe_b64decode(token)) + _, _ = await wallet.redeem(proofs) + wallet.status() + + +@cli.command("burn", help="Burn spent tokens.") +@click.argument("token", type=str) +@click.pass_context +@coro +async def receive(ctx, token: str): + wallet: Wallet = ctx.obj["WALLET"] + await init_wallet(wallet) + wallet.status() + proofs = json.loads(base64.urlsafe_b64decode(token)) + await wallet.invalidate(proofs) + wallet.status()