diff --git a/README.md b/README.md index 2d49e35..bf9d857 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,17 @@ Cashu is an Ecash implementation based on David Wagner's variant of Chaumian blinding. Token logic based on [minicash](https://github.com/phyro/minicash) ([description](https://gist.github.com/phyro/935badc682057f418842c72961cf096c)) which implements a [Blind Diffie-Hellman Key Exchange](https://cypherpunks.venona.com/date/1996/03/msg01848.html) scheme written down by Ruben Somsen [here](https://gist.github.com/RubenSomsen/be7a4760dd4596d06963d67baf140406). The database mechanics and the Lightning backend uses parts from [LNbits](https://github.com/lnbits/lnbits-legend). +

+Quick links: +Cashu client protocol · +Quick Install · +Manual install · +Configuration · +Using Cashu · +Run a mint +

+

+ ## Cashu client protocol There are ongoing efforts to implement alternative Cashu clients that use the same protocol such as a [Cashu Javascript wallet](https://github.com/motorina0/cashu-js-wallet). If you are interested in helping with Cashu development, please see the [docs](docs/) for the notation and conventions used. @@ -20,8 +31,8 @@ To update Cashu, use `pip install cashu -U`. If you have problems running the co 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). +## 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](#poetry-install-cashu). #### Poetry: Prerequisites @@ -168,7 +179,7 @@ Balance: 351 sat (Available: 351 sat in 7 tokens) Balance: 339 sat (Available: 339 sat in 8 tokens) ``` -## Run a mint yourself +# Running a mint This command runs the mint on your local computer. Skip this step if you want to use the [public test mint](#test-instance) instead. ```bash mint diff --git a/core/__init__.py b/cashu/core/__init__.py similarity index 100% rename from core/__init__.py rename to cashu/core/__init__.py diff --git a/core/b_dhke.py b/cashu/core/b_dhke.py similarity index 100% rename from core/b_dhke.py rename to cashu/core/b_dhke.py diff --git a/core/base.py b/cashu/core/base.py similarity index 100% rename from core/base.py rename to cashu/core/base.py diff --git a/core/bolt11.py b/cashu/core/bolt11.py similarity index 100% rename from core/bolt11.py rename to cashu/core/bolt11.py diff --git a/core/db.py b/cashu/core/db.py similarity index 100% rename from core/db.py rename to cashu/core/db.py diff --git a/core/helpers.py b/cashu/core/helpers.py similarity index 91% rename from core/helpers.py rename to cashu/core/helpers.py index 75dab61..0f8c885 100644 --- a/core/helpers.py +++ b/cashu/core/helpers.py @@ -1,7 +1,7 @@ import asyncio from functools import partial, wraps -from core.settings import LIGHTNING_FEE_PERCENT, LIGHTNING_RESERVE_FEE_MIN +from cashu.core.settings import LIGHTNING_FEE_PERCENT, LIGHTNING_RESERVE_FEE_MIN def async_wrap(func): diff --git a/core/migrations.py b/cashu/core/migrations.py similarity index 96% rename from core/migrations.py rename to cashu/core/migrations.py index 1133de6..6beaa91 100644 --- a/core/migrations.py +++ b/cashu/core/migrations.py @@ -2,7 +2,7 @@ import re from loguru import logger -from core.db import COCKROACH, POSTGRES, SQLITE, Database +from cashu.core.db import COCKROACH, POSTGRES, SQLITE, Database async def migrate_databases(db: Database, migrations_module): diff --git a/core/secp.py b/cashu/core/secp.py similarity index 100% rename from core/secp.py rename to cashu/core/secp.py diff --git a/core/settings.py b/cashu/core/settings.py similarity index 98% rename from core/settings.py rename to cashu/core/settings.py index d24c0c4..36ec220 100644 --- a/core/settings.py +++ b/cashu/core/settings.py @@ -32,3 +32,4 @@ LNBITS_ENDPOINT = env.str("LNBITS_ENDPOINT", default=None) LNBITS_KEY = env.str("LNBITS_KEY", default=None) MAX_ORDER = 64 +VERSION = "0.1.10" diff --git a/core/split.py b/cashu/core/split.py similarity index 100% rename from core/split.py rename to cashu/core/split.py diff --git a/cashu/lightning/__init__.py b/cashu/lightning/__init__.py new file mode 100644 index 0000000..bc14007 --- /dev/null +++ b/cashu/lightning/__init__.py @@ -0,0 +1,3 @@ +from cashu.lightning.lnbits import LNbitsWallet + +WALLET = LNbitsWallet() diff --git a/lightning/base.py b/cashu/lightning/base.py similarity index 100% rename from lightning/base.py rename to cashu/lightning/base.py diff --git a/lightning/lnbits.py b/cashu/lightning/lnbits.py similarity index 96% rename from lightning/lnbits.py rename to cashu/lightning/lnbits.py index 94a49eb..f174f2d 100644 --- a/lightning/lnbits.py +++ b/cashu/lightning/lnbits.py @@ -6,10 +6,15 @@ from typing import AsyncGenerator, Dict, Optional import requests -from core.settings import LNBITS_ENDPOINT, LNBITS_KEY +from cashu.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/cashu/mint/__init__.py b/cashu/mint/__init__.py new file mode 100644 index 0000000..cfe9d4c --- /dev/null +++ b/cashu/mint/__init__.py @@ -0,0 +1,4 @@ +from cashu.core.settings import MINT_PRIVATE_KEY +from cashu.mint.ledger import Ledger + +ledger = Ledger(MINT_PRIVATE_KEY, "data/mint") diff --git a/cashu/mint/__main__.py b/cashu/mint/__main__.py new file mode 100644 index 0000000..5d6a810 --- /dev/null +++ b/cashu/mint/__main__.py @@ -0,0 +1,3 @@ +from .main import main + +main() diff --git a/cashu/mint/app.py b/cashu/mint/app.py new file mode 100644 index 0000000..428bc05 --- /dev/null +++ b/cashu/mint/app.py @@ -0,0 +1,71 @@ +import asyncio +import logging +import sys + +from fastapi import FastAPI +from loguru import logger + +from cashu.core.settings import CASHU_DIR, DEBUG +from cashu.lightning import WALLET +from cashu.mint.migrations import m001_initial + +from . import ledger +from .router import router +from .startup import load_ledger + + +def create_app(config_object="core.settings") -> FastAPI: + def configure_logger() -> None: + class Formatter: + def __init__(self): + self.padding = 0 + self.minimal_fmt: str = "{time:YYYY-MM-DD HH:mm:ss.SS} | {level} | {message}\n" + if DEBUG: + self.fmt: str = "{time:YYYY-MM-DD HH:mm:ss.SS} | {level: <4} | {name}:{function}:{line} | {message}\n" + else: + self.fmt: str = self.minimal_fmt + + def format(self, record): + function = "{function}".format(**record) + if function == "emit": # uvicorn logs + return self.minimal_fmt + return self.fmt + + class InterceptHandler(logging.Handler): + def emit(self, record): + try: + level = logger.level(record.levelname).name + except ValueError: + level = record.levelno + logger.log(level, record.getMessage()) + + logger.remove() + log_level: str = "INFO" + formatter = Formatter() + logger.add(sys.stderr, level=log_level, format=formatter.format) + + logging.getLogger("uvicorn").handlers = [InterceptHandler()] + logging.getLogger("uvicorn.access").handlers = [InterceptHandler()] + + configure_logger() + + app = FastAPI( + title="Cashu Mint", + description="Ecash wallet and mint.", + license_info={ + "name": "MIT License", + "url": "https://raw.githubusercontent.com/callebtc/cashu/main/LICENSE", + }, + ) + + return app + + +app = create_app() + +app.include_router(router=router) + + +@app.on_event("startup") +async def startup_load_ledger(): + await load_ledger() diff --git a/mint/crud.py b/cashu/mint/crud.py similarity index 95% rename from mint/crud.py rename to cashu/mint/crud.py index 5b92271..18a7195 100644 --- a/mint/crud.py +++ b/cashu/mint/crud.py @@ -1,8 +1,8 @@ import secrets from typing import Optional -from core.base import Invoice, Proof -from core.db import Connection, Database +from cashu.core.base import Invoice, Proof +from cashu.core.db import Connection, Database async def store_promise( diff --git a/mint/ledger.py b/cashu/mint/ledger.py similarity index 93% rename from mint/ledger.py rename to cashu/mint/ledger.py index f5ce49f..13f6310 100644 --- a/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -5,15 +5,15 @@ Implementation of https://gist.github.com/phyro/935badc682057f418842c72961cf096c import hashlib from typing import List, Set -import core.b_dhke as b_dhke -from core.base import BlindedMessage, BlindedSignature, Invoice, Proof -from core.db import Database -from core.helpers import fee_reserve -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 ( +import cashu.core.b_dhke as b_dhke +from cashu.core.base import BlindedMessage, BlindedSignature, Invoice, Proof +from cashu.core.db import Database +from cashu.core.helpers import fee_reserve +from cashu.core.secp import PrivateKey, PublicKey +from cashu.core.settings import LIGHTNING, MAX_ORDER +from cashu.core.split import amount_split +from cashu.lightning import WALLET +from cashu.mint.crud import ( get_lightning_invoice, get_proofs_used, invalidate_proof, diff --git a/cashu/mint/main.py b/cashu/mint/main.py new file mode 100644 index 0000000..50c4a6a --- /dev/null +++ b/cashu/mint/main.py @@ -0,0 +1,49 @@ +import click +import uvicorn + +from cashu.core.settings import MINT_SERVER_HOST, MINT_SERVER_PORT + + +@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( + "cashu.mint.app:app", + port=port, + host=host, + ssl_keyfile=ssl_keyfile, + ssl_certfile=ssl_certfile, + **d, + ) + server = uvicorn.Server(config) + server.run() diff --git a/mint/migrations.py b/cashu/mint/migrations.py similarity index 98% rename from mint/migrations.py rename to cashu/mint/migrations.py index 231013c..967b9d3 100644 --- a/mint/migrations.py +++ b/cashu/mint/migrations.py @@ -1,4 +1,4 @@ -from core.db import Database +from cashu.core.db import Database async def m000_create_migrations_table(db): diff --git a/cashu/mint/router.py b/cashu/mint/router.py new file mode 100644 index 0000000..b7ebf05 --- /dev/null +++ b/cashu/mint/router.py @@ -0,0 +1,88 @@ +from typing import Union + +from fastapi import APIRouter +from secp256k1 import PublicKey + +from cashu.core.base import CheckPayload, MeltPayload, MintPayloads, SplitPayload +from cashu.mint import ledger + +router: APIRouter = APIRouter() + + +@router.get("/keys") +def keys(): + """Get the public keys of the mint""" + return ledger.get_pubkeys() + + +@router.get("/mint") +async def request_mint(amount: int = 0): + """Request minting of tokens. Server responds with a Lightning invoice.""" + payment_request, payment_hash = await ledger.request_mint(amount) + print(f"Lightning invoice: {payment_request}") + return {"pr": payment_request, "hash": payment_hash} + + +@router.post("/mint") +async def mint(payloads: MintPayloads, payment_hash: Union[str, None] = None): + """ + Requests the minting of tokens belonging to a paid payment request. + + Parameters: + pr: payment_request of the Lightning paid invoice. + + Body (JSON): + payloads: contains a list of blinded messages waiting to be signed. + + NOTE: + - This needs to be replaced by the preimage otherwise someone knowing + the payment_request can request the tokens instead of the rightful + owner. + - The blinded message should ideally be provided to the server *before* payment + in the GET /mint endpoint so that the server knows to sign only these tokens + when the invoice is paid. + """ + amounts = [] + B_s = [] + for payload in payloads.blinded_messages: + amounts.append(payload.amount) + B_s.append(PublicKey(bytes.fromhex(payload.B_), raw=True)) + try: + promises = await ledger.mint(B_s, amounts, payment_hash=payment_hash) + return promises + except Exception as exc: + return {"error": str(exc)} + + +@router.post("/melt") +async def melt(payload: MeltPayload): + """ + Requests tokens to be destroyed and sent out via Lightning. + """ + ok, preimage = await ledger.melt(payload.proofs, payload.amount, payload.invoice) + return {"paid": ok, "preimage": preimage} + + +@router.post("/check") +async def check_spendable(payload: CheckPayload): + return await ledger.check_spendable(payload.proofs) + + +@router.post("/split") +async def split(payload: SplitPayload): + """ + Requetst a set of tokens with amount "total" to be split into two + newly minted sets with amount "split" and "total-split". + """ + proofs = payload.proofs + amount = payload.amount + output_data = payload.output_data.blinded_messages + try: + split_return = await ledger.split(proofs, amount, output_data) + except Exception as exc: + return {"error": str(exc)} + if not split_return: + """There was a problem with the split""" + raise Exception("could not split tokens.") + fst_promises, snd_promises = split_return + return {"fst": fst_promises, "snd": snd_promises} diff --git a/cashu/mint/startup.py b/cashu/mint/startup.py new file mode 100644 index 0000000..a2c4653 --- /dev/null +++ b/cashu/mint/startup.py @@ -0,0 +1,25 @@ +import asyncio + +from loguru import logger + +from cashu.core.settings import CASHU_DIR +from cashu.lightning import WALLET +from cashu.mint.migrations import m001_initial + +from . import ledger + + +async def load_ledger(): + await asyncio.wait([m001_initial(ledger.db)]) + await ledger.load_used_proofs() + + error_message, balance = await WALLET.status() + if error_message: + logger.warning( + f"The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'", + RuntimeWarning, + ) + + logger.info(f"Lightning balance: {balance} sat") + logger.info(f"Data dir: {CASHU_DIR}") + logger.info("Mint started.") diff --git a/wallet/__init__.py b/cashu/wallet/__init__.py similarity index 100% rename from wallet/__init__.py rename to cashu/wallet/__init__.py diff --git a/cashu/wallet/__main__.py b/cashu/wallet/__main__.py new file mode 100644 index 0000000..4cafccb --- /dev/null +++ b/cashu/wallet/__main__.py @@ -0,0 +1,3 @@ +from .cli import cli + +cli() diff --git a/wallet/cashu.py b/cashu/wallet/cli.py similarity index 93% rename from wallet/cashu.py rename to cashu/wallet/cli.py index c252b46..09a3f4a 100755 --- a/wallet/cashu.py +++ b/cashu/wallet/cli.py @@ -12,15 +12,15 @@ from operator import itemgetter import click from bech32 import bech32_decode, bech32_encode, convertbits -import core.bolt11 as bolt11 -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, DEBUG, LIGHTNING, MINT_URL -from wallet import migrations -from wallet.crud import get_reserved_proofs -from wallet.wallet import Wallet as Wallet +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 +from cashu.wallet import migrations +from cashu.wallet.crud import get_reserved_proofs +from cashu.wallet.wallet import Wallet as Wallet async def init_wallet(wallet: Wallet): @@ -203,9 +203,7 @@ async def pay(ctx, invoice: str): @click.pass_context @coro async def info(ctx): - wallet: Wallet = ctx.obj["WALLET"] - await init_wallet(wallet) - wallet.status() + print(f"Version: {VERSION}") print(f"Debug: {DEBUG}") print(f"Cashu dir: {CASHU_DIR}") print(f"Mint URL: {MINT_URL}") diff --git a/wallet/crud.py b/cashu/wallet/crud.py similarity index 96% rename from wallet/crud.py rename to cashu/wallet/crud.py index 55bb293..c5741b6 100644 --- a/wallet/crud.py +++ b/cashu/wallet/crud.py @@ -1,8 +1,8 @@ import time from typing import Optional -from core.base import Proof -from core.db import Connection, Database +from cashu.core.base import Proof +from cashu.core.db import Connection, Database async def store_proof( diff --git a/wallet/migrations.py b/cashu/wallet/migrations.py similarity index 98% rename from wallet/migrations.py rename to cashu/wallet/migrations.py index 8745577..c8b3ec0 100644 --- a/wallet/migrations.py +++ b/cashu/wallet/migrations.py @@ -1,4 +1,4 @@ -from core.db import Database +from cashu.core.db import Database async def m000_create_migrations_table(db): diff --git a/wallet/wallet.py b/cashu/wallet/wallet.py similarity index 93% rename from wallet/wallet.py rename to cashu/wallet/wallet.py index b3dccfc..27d1ba6 100644 --- a/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -1,14 +1,13 @@ 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 ( +import cashu.core.b_dhke as b_dhke +from cashu.core.base import ( BlindedMessage, BlindedSignature, CheckPayload, @@ -17,11 +16,16 @@ from core.base import ( 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 cashu.core.db import Database +from cashu.core.secp import PublicKey +from cashu.core.settings import DEBUG +from cashu.core.split import amount_split +from cashu.wallet.crud import ( + get_proofs, + invalidate_proof, + store_proof, + update_proof_reserved, +) class LedgerAPI: @@ -97,6 +101,7 @@ class LedgerAPI: fst_outputs = amount_split(fst_amt) snd_outputs = amount_split(snd_amt) + # TODO: Refactor together with the same procedure in self.mint() secrets = [] payloads: MintPayloads = MintPayloads() for output_amt in fst_outputs + snd_outputs: diff --git a/cashu/wallet/wallet_live/.DS_Store b/cashu/wallet/wallet_live/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/cashu/wallet/wallet_live/.DS_Store differ diff --git a/mint/__init__.py b/cashu/wallet/wallet_live/.placeholder similarity index 100% rename from mint/__init__.py rename to cashu/wallet/wallet_live/.placeholder diff --git a/cashu/wallet/wallet_live/wallet.sqlite3 b/cashu/wallet/wallet_live/wallet.sqlite3 new file mode 100644 index 0000000..d3e29bb Binary files /dev/null and b/cashu/wallet/wallet_live/wallet.sqlite3 differ diff --git a/lightning/__init__.py b/lightning/__init__.py deleted file mode 100644 index baa53c7..0000000 --- a/lightning/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from lightning.lnbits import LNbitsWallet - -WALLET = LNbitsWallet() diff --git a/mint/app.py b/mint/app.py deleted file mode 100644 index ddd5315..0000000 --- a/mint/app.py +++ /dev/null @@ -1,217 +0,0 @@ -import asyncio -import logging -import sys -from typing import Union - -import click -import uvicorn -from fastapi import FastAPI -from loguru import logger -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 lightning import WALLET -from mint.ledger import Ledger -from mint.migrations import m001_initial - -ledger = Ledger(MINT_PRIVATE_KEY, "data/mint") - - -def startup(app: FastAPI): - @app.on_event("startup") - async def load_ledger(): - await asyncio.wait([m001_initial(ledger.db)]) - await ledger.load_used_proofs() - - error_message, balance = await WALLET.status() - if error_message: - logger.warning( - f"The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'", - RuntimeWarning, - ) - - logger.info(f"Lightning balance: {balance} sat") - logger.info(f"Data dir: {CASHU_DIR}") - logger.info("Mint started.") - - -def create_app(config_object="core.settings") -> FastAPI: - def configure_logger() -> None: - class Formatter: - def __init__(self): - self.padding = 0 - self.minimal_fmt: str = "{time:YYYY-MM-DD HH:mm:ss.SS} | {level} | {message}\n" - if settings.DEBUG: - self.fmt: str = "{time:YYYY-MM-DD HH:mm:ss.SS} | {level: <4} | {name}:{function}:{line} | {message}\n" - else: - self.fmt: str = self.minimal_fmt - - def format(self, record): - function = "{function}".format(**record) - if function == "emit": # uvicorn logs - return self.minimal_fmt - return self.fmt - - class InterceptHandler(logging.Handler): - def emit(self, record): - try: - level = logger.level(record.levelname).name - except ValueError: - level = record.levelno - logger.log(level, record.getMessage()) - - logger.remove() - log_level: str = "INFO" - formatter = Formatter() - logger.add(sys.stderr, level=log_level, format=formatter.format) - - logging.getLogger("uvicorn").handlers = [InterceptHandler()] - logging.getLogger("uvicorn.access").handlers = [InterceptHandler()] - - configure_logger() - - app = FastAPI( - title="Cashu Mint", - description="Ecash wallet and mint.", - license_info={ - "name": "MIT License", - "url": "https://raw.githubusercontent.com/callebtc/cashu/main/LICENSE", - }, - ) - - startup(app) - return app - - -app = create_app() - - -@app.get("/keys") -def keys(): - """Get the public keys of the mint""" - return ledger.get_pubkeys() - - -@app.get("/mint") -async def request_mint(amount: int = 0): - """Request minting of tokens. Server responds with a Lightning invoice.""" - payment_request, payment_hash = await ledger.request_mint(amount) - print(f"Lightning invoice: {payment_request}") - return {"pr": payment_request, "hash": payment_hash} - - -@app.post("/mint") -async def mint(payloads: MintPayloads, payment_hash: Union[str, None] = None): - """ - Requests the minting of tokens belonging to a paid payment request. - - Parameters: - pr: payment_request of the Lightning paid invoice. - - Body (JSON): - payloads: contains a list of blinded messages waiting to be signed. - - NOTE: - - This needs to be replaced by the preimage otherwise someone knowing - the payment_request can request the tokens instead of the rightful - owner. - - The blinded message should ideally be provided to the server *before* payment - in the GET /mint endpoint so that the server knows to sign only these tokens - when the invoice is paid. - """ - amounts = [] - B_s = [] - for payload in payloads.blinded_messages: - amounts.append(payload.amount) - B_s.append(PublicKey(bytes.fromhex(payload.B_), raw=True)) - try: - promises = await ledger.mint(B_s, amounts, payment_hash=payment_hash) - return promises - except Exception as exc: - return {"error": str(exc)} - - -@app.post("/melt") -async def melt(payload: MeltPayload): - """ - Requests tokens to be destroyed and sent out via Lightning. - """ - ok, preimage = await ledger.melt(payload.proofs, payload.amount, payload.invoice) - return {"paid": ok, "preimage": preimage} - - -@app.post("/check") -async def check_spendable(payload: CheckPayload): - return await ledger.check_spendable(payload.proofs) - - -@app.post("/split") -async def split(payload: SplitPayload): - """ - Requetst a set of tokens with amount "total" to be split into two - newly minted sets with amount "split" and "total-split". - """ - proofs = payload.proofs - amount = payload.amount - output_data = payload.output_data.blinded_messages - try: - split_return = await ledger.split(proofs, amount, output_data) - except Exception as exc: - return {"error": str(exc)} - if not split_return: - """There was a problem with the split""" - raise Exception("could not split tokens.") - fst_promises, snd_promises = split_return - return {"fst": fst_promises, "snd": snd_promises} - - -@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/poetry.lock b/poetry.lock index 4f84b80..f8a5ae0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -85,7 +85,7 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2022.9.14" +version = "2022.9.24" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -184,32 +184,13 @@ dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "passlib[bcrypt] 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)"] 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]] -name = "Flask" -version = "2.2.2" -description = "A simple framework for building complex web applications." -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -click = ">=8.0" -importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} -itsdangerous = ">=2.0" -Jinja2 = ">=3.0" -Werkzeug = ">=2.2.2" - -[package.extras] -async = ["asgiref (>=3.2)"] -dotenv = ["python-dotenv"] - [[package]] name = "h11" -version = "0.13.0" +version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] typing-extensions = {version = "*", markers = "python_version < \"3.8\""} @@ -709,7 +690,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "d5ee88384ec1ec1774f9fab845d7e8ac6e8ab1859506bcce178f6783b98e535c" +content-hash = "4058e11929b71dee85bceb89e1fb5254411db9bde0e087f65b5cf7c0c7fbae8f" [metadata.files] anyio = [ @@ -759,8 +740,8 @@ black = [ {file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"}, ] certifi = [ - {file = "certifi-2022.9.14-py3-none-any.whl", hash = "sha256:e232343de1ab72c2aa521b625c80f699e356830fd0e2c620b465b304b17b0516"}, - {file = "certifi-2022.9.14.tar.gz", hash = "sha256:36973885b9542e6bd01dea287b2b4b3b21236307c56324fcc3f1160f2d655ed5"}, + {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, + {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, ] cffi = [ {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, @@ -852,13 +833,9 @@ fastapi = [ {file = "fastapi-0.83.0-py3-none-any.whl", hash = "sha256:694a2b6c2607a61029a4be1c6613f84d74019cb9f7a41c7a475dca8e715f9368"}, {file = "fastapi-0.83.0.tar.gz", hash = "sha256:96eb692350fe13d7a9843c3c87a874f0d45102975257dd224903efd6c0fde3bd"}, ] -Flask = [ - {file = "Flask-2.2.2-py3-none-any.whl", hash = "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526"}, - {file = "Flask-2.2.2.tar.gz", hash = "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b"}, -] h11 = [ - {file = "h11-0.13.0-py3-none-any.whl", hash = "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"}, - {file = "h11-0.13.0.tar.gz", hash = "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06"}, + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, diff --git a/pyproject.toml b/pyproject.toml index 4bf03ad..52c07a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cashu" -version = "0.1.9" +version = "0.1.10" description = "Ecash wallet and mint." authors = ["calle "] license = "MIT" @@ -13,7 +13,6 @@ SQLAlchemy = "1.3.24" sqlalchemy-aio = "0.17.0" charset-normalizer = "2.0.12" click = "8.0.4" -Flask = "2.2.2" idna = "3.3" itsdangerous = "2.1.1" Jinja2 = "3.0.3" @@ -46,6 +45,6 @@ requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] -mint = "mint.app:main" -cashu = "wallet.cashu:cli" +mint = "cashu.mint.main:main" +cashu = "cashu.wallet.cli:cli" wallet-test = "tests.test_wallet:test" \ No newline at end of file diff --git a/setup.py b/setup.py index f80f9ca..b7176cc 100644 --- a/setup.py +++ b/setup.py @@ -9,11 +9,11 @@ with open(path.join(this_directory, "README.md"), encoding="utf-8") as f: with open("requirements.txt") as f: requirements = f.read().splitlines() -entry_points = {"console_scripts": ["cashu = wallet.cashu:cli"]} +entry_points = {"console_scripts": ["cashu = cahu.wallet.cli:cli"]} setuptools.setup( name="cashu", - version="0.1.9", + version="0.1.10", description="Ecash wallet and mint with Bitcoin Lightning support", long_description=long_description, long_description_content_type="text/markdown", diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 9def85f..a77b141 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -1,10 +1,8 @@ -import asyncio - -from core.helpers import async_unwrap -from core.migrations import migrate_databases -from wallet import migrations -from wallet.wallet import Wallet as Wallet1 -from wallet.wallet import Wallet as Wallet2 +from cashu.core.helpers import async_unwrap +from cashu.core.migrations import migrate_databases +from cashu.wallet import migrations +from cashu.wallet.wallet import Wallet as Wallet1 +from cashu.wallet.wallet import Wallet as Wallet2 SERVER_ENDPOINT = "http://localhost:3338" @@ -29,7 +27,7 @@ async def run_test(): await migrate_databases(wallet1.db, migrations) wallet1.status() - wallet2 = Wallet1(SERVER_ENDPOINT, "data/wallet2", "wallet2") + wallet2 = Wallet2(SERVER_ENDPOINT, "data/wallet2", "wallet2") await migrate_databases(wallet2.db, migrations) wallet2.status()