diff --git a/mint/__init__.py b/mint/__init__.py index e69de29..9fa043b 100644 --- a/mint/__init__.py +++ b/mint/__init__.py @@ -0,0 +1,6 @@ +from core.settings import MINT_PRIVATE_KEY +from mint.ledger import Ledger + +print("init") + +ledger = Ledger(MINT_PRIVATE_KEY, "data/mint") diff --git a/mint/__main__.py b/mint/__main__.py new file mode 100644 index 0000000..3681527 --- /dev/null +++ b/mint/__main__.py @@ -0,0 +1,7 @@ +from .app import create_app, main + +print("main") + +app = create_app() + +# main() diff --git a/mint/api.py b/mint/api.py new file mode 100644 index 0000000..e69de29 diff --git a/mint/app.py b/mint/app.py index 377ed1f..e79d433 100644 --- a/mint/app.py +++ b/mint/app.py @@ -1,221 +1,89 @@ -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.md", - }, - ) - - 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() +import asyncio +import logging +import sys +from typing import Union + + +from fastapi import FastAPI +from loguru import logger +from secp256k1 import PublicKey + +import core.settings as settings +from core.settings import ( + CASHU_DIR, +) +from lightning import WALLET +from mint.ledger import Ledger +from mint.migrations import m001_initial + +from . import ledger + + +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 + + +# if __name__ == "__main__": +# main() diff --git a/mint/main.py b/mint/main.py new file mode 100644 index 0000000..3eafd4c --- /dev/null +++ b/mint/main.py @@ -0,0 +1,52 @@ +import click +import uvicorn + +from 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( + "mint.__main__:app", + port=port, + host=host, + ssl_keyfile=ssl_keyfile, + ssl_certfile=ssl_certfile, + **d, + ) + server = uvicorn.Server(config) + server.run() diff --git a/mint/router.py b/mint/router.py new file mode 100644 index 0000000..7f5ac35 --- /dev/null +++ b/mint/router.py @@ -0,0 +1,86 @@ +from fastapi import APIRouter +from mint import ledger +from core.base import CheckPayload, MeltPayload, MintPayloads, SplitPayload +from typing import Union +from secp256k1 import PublicKey + +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}