From 93208a0df931d06609bb4240705c8cb983e5b406 Mon Sep 17 00:00:00 2001 From: fusion44 Date: Sun, 30 Oct 2022 18:52:46 +0100 Subject: [PATCH 1/7] feat: add first iteration of a cashu service --- .gitignore | 1 + app/cashu/__init__.py | 0 app/cashu/constants.py | 3 + app/cashu/docs.py | 21 ++ app/cashu/exceptions.py | 66 ++++++ app/cashu/models.py | 149 ++++++++++++++ app/cashu/router.py | 306 +++++++++++++++++++++++++++ app/cashu/service.py | 281 +++++++++++++++++++++++++ app/main.py | 2 + poetry.lock | 444 +++++++++++++++++++++++++++++++++++----- pyproject.toml | 5 +- 11 files changed, 1229 insertions(+), 49 deletions(-) create mode 100644 app/cashu/__init__.py create mode 100644 app/cashu/constants.py create mode 100644 app/cashu/docs.py create mode 100644 app/cashu/exceptions.py create mode 100644 app/cashu/models.py create mode 100644 app/cashu/router.py create mode 100644 app/cashu/service.py diff --git a/.gitignore b/.gitignore index d75d93f..307a916 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__ htmlcov scripts/sync_to_blitz.personal.sh docker/.env +cashu.db \ No newline at end of file diff --git a/app/cashu/__init__.py b/app/cashu/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/cashu/constants.py b/app/cashu/constants.py new file mode 100644 index 0000000..517f8ca --- /dev/null +++ b/app/cashu/constants.py @@ -0,0 +1,3 @@ +DATA_FOLDER = "cashu.db" +DEFAULT_WALLET_NAME = "DefaultCashuWallet" +DEFAULT_MINT_URL = "http://localhost:3338" diff --git a/app/cashu/docs.py b/app/cashu/docs.py new file mode 100644 index 0000000..c7a3b36 --- /dev/null +++ b/app/cashu/docs.py @@ -0,0 +1,21 @@ +pin_mint_summary = "Pins the mint URL. Calling this endpoint with no URL will reset the mint to the system default." +pin_mint_desc = "When calling Cashu endpoints without a mint URL, the system will use the mint URL that was pinned with this endpoint." + +pin_wallet_summary = "Pins the current wallet name. Calling this endpoint with no name will reset the wallet to the system default." +pin_wallet_desc = "When calling Cashu endpoints without a wallet name, the system will use the wallet that was pinned with this endpoint." + +get_balance_summary = "Get the combined balance of all the known Cashu wallets. To get balances of single wallets use the /list-wallets endpoint." + +estimate_pay_summary = "Decodes the amount from a Lightning invoice and returns the total amount (amount+fees) to be paid." + +pay_invoice_summary = ( + "Pay a lightning invoice via available tokens on a mint or a lightning payment." +) + + +pay_invoice_description = """ +This endpoint will try to pay the invoice using the available tokens on the mint. +If it fails it will try to pay the invoice using a lightning payment. + +> 👉 This is different from the /lightning/pay-invoice endpoint which will only try to pay the invoice using a lightning payment. +""" diff --git a/app/cashu/exceptions.py b/app/cashu/exceptions.py new file mode 100644 index 0000000..6e8f7b4 --- /dev/null +++ b/app/cashu/exceptions.py @@ -0,0 +1,66 @@ +from fastapi import HTTPException, status + + +class LockNotFoundException(HTTPException): + def __init__(self): + super().__init__( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Lock not found.", + ) + + +class LockFormatException(HTTPException): + def __init__(self): + super().__init__( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Lock has wrong format. Expected P2SH:
.", + ) + + +class TokensSpentException(HTTPException): + def __init__(self, secret: str): + self.secret = secret + super().__init__( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Mint Error: tokens already spent.", + ) + + +class IsDefaultMintException(HTTPException): + def __init__(self): + super().__init__( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Default mint is already added.", + ) + + +class MintExistsException(HTTPException): + def __init__(self, mint_name: str): + super().__init__( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Mint with name {mint_name} was already added.", + ) + + +class WalletExistsException(HTTPException): + def __init__(self, wallet_name: str): + super().__init__( + status_code=status.HTTP_409_CONFLICT, + detail=f"Wallet {wallet_name} exists.", + ) + + +class WalletNotFoundException(HTTPException): + def __init__(self, wallet_name: str): + super().__init__( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Wallet {wallet_name} not found.", + ) + + +class ZeroInvoiceException(HTTPException): + def __init__(self): + super().__init__( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Zero invoices not allowed. Amount must be positive.", + ) diff --git a/app/cashu/models.py b/app/cashu/models.py new file mode 100644 index 0000000..6441c87 --- /dev/null +++ b/app/cashu/models.py @@ -0,0 +1,149 @@ +import base64 +import json +from os.path import join + +from cashu.core.migrations import migrate_databases +from cashu.wallet import migrations +from cashu.wallet.crud import get_unused_locks +from cashu.wallet.wallet import Proof, Wallet +from fastapi import Query +from pydantic import BaseModel + +import app.cashu.exceptions as ce +from app.cashu.constants import DATA_FOLDER + + +# Public +class CashuMintInput(BaseModel): + url: str = Query("http://localhost:3338", description="URL of the mint.") + pinned: bool = Query(False, description="Whether the mint is pinned.") + + +class CashuMint(CashuMintInput): + default: bool = Query(False, description="Whether the mint is the system default.") + + +class CashuInfo(BaseModel): + version: str = Query(..., description="Cashu library version") + debug: bool = Query( + ..., description="Whether Cashu is running in debug mode or not." + ) + default_wallet: str = Query(..., description="Default Cashu wallet name") + default_mint: str = Query(..., description="Default Cashu mint server URL") + + +class CashuPayEstimation(BaseModel): + amount: int = Query(..., description="Amount to pay in satoshis") + fee: int = Query(..., description="Fee to pay in satoshis") + balance_ok: bool = Query(..., description="Whether the balance is sufficient") + + +class CashuWalletBalance(BaseModel): + total: int = Query(0, description="Total cashu wallet balance") + available: int = Query(0, description="Available cashu wallet balance") + tokens: int = Query(0, description="Number of tokens in the cashu wallet") + + def __add__(self, o): + return CashuWalletBalance( + total=self.total + o.total, + available=self.available + o.available, + tokens=self.tokens + o.tokens, + ) + + def __sub__(self, o): + return CashuWalletBalance( + total=self.total - o.total, + available=self.available - o.available, + tokens=self.tokens - o.tokens, + ) + + +class CashuMintBalance(BaseModel): + mint: str = Query(None, description="Mint server URL") + total: int = Query(..., description="Total cashu wallet balance for this mint") + available: int = Query( + ..., description="Available cashu wallet balance for this mint" + ) + tokens: int = Query( + ..., description="Number of tokens in the cashu wallet for this mint" + ) + + +class CashuWalletData(BaseModel): + name: str + balance: CashuWalletBalance = Query( + ..., description="Total cashu wallet balance. This includes all mints" + ) + balances_per_mint: list[CashuMintBalance] + + +# Internal only +class CashuWallet(Wallet): + initialized: bool = False + + def __init__(self, mint: CashuMint, name: str = "no_name") -> None: + super().__init__(url=mint.url, db=join(DATA_FOLDER, name), name=name) + + async def initialize(self): + if self.initialized: + raise RuntimeError("Cashu wallet is already initialized") + + await migrate_databases(self.db, migrations) + await self.load_proofs() + + @property + def balance_overview(self) -> CashuWalletBalance: + return CashuWalletBalance( + total=self.balance, + available=self.available_balance, + tokens=len([p for p in self.proofs if not p.reserved]), + ) + + async def receive(self, coin: str, lock: str): + await super().load_mint() + + script, signature = None, None + if lock: + # load the script and signature of this address from the database + if len(lock.split("P2SH:")) != 2: + raise ce.LockFormatException() + + address_split = lock.split("P2SH:")[1] + p2sh_scripts = await get_unused_locks(address_split, db=self.db) + + if len(p2sh_scripts) == 0: + raise ce.LockNotFoundException() + + script = p2sh_scripts[0].script + signature = p2sh_scripts[0].signature + try: + proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(coin))] + _, _ = await self.redeem( + proofs, scnd_script=script, scnd_siganture=signature + ) + except Exception as e: + if "Mint Error: tokens already spent. Secret:" in e.args[0]: + raise ce.TokensSpentException(secret=e.args[0].split("Secret:")[1]) + + raise + + def get_wallet_data_for_client( + self, include_mint_balances: bool = False + ) -> CashuWalletData: + balances = [] + + if include_mint_balances: + balances.append( + CashuMintBalance( + mint=self.url, + total=self.balance, + available=self.available_balance, + tokens=len([p for p in self.proofs if not p.reserved]), + ) + ) + + return CashuWalletData( + name=self.name, + balance=self.balance_overview, + balances_per_mint=balances, + ) diff --git a/app/cashu/router.py b/app/cashu/router.py new file mode 100644 index 0000000..0ded561 --- /dev/null +++ b/app/cashu/router.py @@ -0,0 +1,306 @@ +import asyncio +from typing import Union + +from fastapi import APIRouter, Body, HTTPException, Query, status + +import app.cashu.constants as c +import app.cashu.docs as docs +from app.cashu.models import ( + CashuInfo, + CashuMint, + CashuMintInput, + CashuPayEstimation, + CashuWalletBalance, + CashuWalletData, +) +from app.cashu.service import CashuService + +_PREFIX = "cashu" + +router = APIRouter(prefix=f"/{_PREFIX}", tags=["Cashu"]) +service = CashuService() +loop = asyncio.get_event_loop() +loop.create_task(service.init_wallets()) + + +@router.post( + "/add-mint", + name=f"{_PREFIX}.add-mint", + summary="Adds a mint URL to the known mint database.", + response_model=CashuMint, + status_code=status.HTTP_201_CREATED, + # dependencies=[Depends(JWTBearer())], +) +async def add_mint(mint: CashuMintInput): + return await service.add_mint(mint) + + +@router.post( + "/pin-mint", + name=f"{_PREFIX}.pin-mint", + summary=docs.pin_mint_summary, + description=docs.pin_mint_desc, + response_model=CashuMint, + response_description="The url of the mint that was set.", + # dependencies=[Depends(JWTBearer())], +) +def pin_mint_path( + url: str = Query( + c.DEFAULT_MINT_URL, + description=f"URL of the mint. Will be set to the system default if empty.", + ) +) -> CashuMint: + try: + return service.pin_mint(url) + except HTTPException: + raise + except NotImplementedError: + raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED) + + +@router.get( + "/list-mints", + name=f"{_PREFIX}.list-mints", + summary="Lists all known mints.", + response_model=list[CashuMint], + status_code=status.HTTP_200_OK, + # dependencies=[Depends(JWTBearer())], +) +async def list_mints(): + try: + return await service.list_mints() + except HTTPException: + raise + except NotImplementedError: + raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED) + + +@router.post( + "/add-wallet", + name=f"{_PREFIX}.add-wallet", + summary="Initializes a new wallet.", + response_model=CashuWalletData, + responses={ + status.HTTP_409_CONFLICT: { + "description": "Wallet already exists.", + } + } + # dependencies=[Depends(JWTBearer())], +) +async def add_wallet_path( + wallet_name: str = Query(..., min_length=3, description=f"Name of the wallet.") +) -> str: + try: + return await service.add_wallet(wallet_name) + except HTTPException: + raise + except NotImplementedError: + raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED) + + +@router.post( + "/pin-wallet", + name=f"{_PREFIX}.pin-wallet", + summary=docs.pin_wallet_summary, + description=docs.pin_wallet_desc, + response_model=str, + response_description="The name of the wallet that was set.", + # dependencies=[Depends(JWTBearer())], +) +def pin_wallet_path( + wallet_name: str = Query( + None, + description=f"Name of the wallet. Will be set to the system default if empty.", + ) +) -> str: + try: + return service.pin_wallet(wallet_name) + except HTTPException: + raise + except NotImplementedError: + raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED) + + +@router.get( + "/list-wallets", + name=f"{_PREFIX}.get-wallets", + summary="Lists all available Cashu wallets", + # dependencies=[Depends(JWTBearer())], +) +async def cashu_list_wallets_path(include_balances: bool = False): + try: + return await service.list_wallets(include_balances) + except HTTPException: + raise + except NotImplementedError: + raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED) + + +@router.get( + "/get-wallet", + name=f"{_PREFIX}.get-wallet", + summary="Get info about a specific Cashu wallet", + # dependencies=[Depends(JWTBearer())], +) +async def cashu_get_wallet_path( + mint_name: Union[None, str] = Query( + None, + description=f"Name of the mint. Will use the pinned mint if empty.", + ), + wallet_name: Union[None, str] = Query( + None, + description=f"Name of the wallet. Will use the pinned wallet if empty.", + ), + include_balances: bool = False, +): + try: + return await service.get_wallet(mint_name, wallet_name, include_balances) + except HTTPException: + raise + except NotImplementedError: + raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED) + + +@router.get( + "/get-balance", + name=f"{_PREFIX}.get-balance", + summary=docs.get_balance_summary, + response_model=CashuWalletBalance + # dependencies=[Depends(JWTBearer())], +) +def cashu_balance_path(): + try: + return service.balance() + except HTTPException: + raise + except NotImplementedError: + raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED) + + +@router.get( + "/get-info", + name=f"{_PREFIX}.get-info", + summary="Get Cashu environment infos", + response_model=CashuInfo, + # dependencies=[Depends(JWTBearer())], +) +def cashu_info_path(): + try: + return service.info() + except HTTPException: + raise + except NotImplementedError: + raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED) + + +@router.post( + "/mint-tokens", + name=f"{_PREFIX}.mint-tokens", + summary="Mint Cashu tokens", + # dependencies=[Depends(JWTBearer())], +) +async def cashu_mint_path( + amount: int, + mint_name: Union[None, str] = Query( + None, + description=f"Name of the mint. Will use the pinned mint if empty.", + ), + wallet_name: Union[None, str] = Query( + None, + description=f"Name of the wallet. Will use the pinned wallet if empty.", + ), +): + try: + return await service.mint(amount, wallet_name, mint_name) + except HTTPException: + raise + except NotImplementedError: + raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED) + + +@router.post( + "/receive-tokens", + name=f"{_PREFIX}.receive-tokens", + summary="Receive Cashu tokens", + response_model=CashuWalletBalance, + # dependencies=[Depends(JWTBearer())], +) +async def cashu_receive_path( + coin: str = Body(..., description="The coins to receive."), + lock: str = Body(None, description="Unlock coins."), + mint_name: Union[None, str] = Body( + None, + description=f"Name of the mint. Will use the pinned mint if empty.", + ), + wallet_name: Union[None, str] = Body( + None, + description=f"Name of the wallet. Will use the pinned wallet if empty.", + ), +) -> CashuWalletBalance: + try: + return await service.receive(coin, lock, wallet_name, mint_name) + except HTTPException: + raise + except NotImplementedError: + raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED) + + +@router.post( + "/pay-invoice", + name=f"{_PREFIX}.pay-invoice", + summary=docs.pay_invoice_summary, + description=docs.pay_invoice_description, + response_model=CashuWalletBalance, + # dependencies=[Depends(JWTBearer())], +) +async def cashu_pay_path( + invoice: str = Body(..., description="The coins to receive."), + mint_name: Union[None, str] = Body( + None, + description=f"Name of the mint. Will use the pinned mint if empty.", + ), + wallet_name: Union[None, str] = Body( + None, + description=f"Name of the wallet. Will use the pinned wallet if empty.", + ), +) -> CashuWalletBalance: + try: + return await service.pay(invoice, wallet_name, mint_name) + except HTTPException: + raise + except NotImplementedError: + raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED) + + +@router.post( + "/estimate-pay", + name=f"{_PREFIX}.estimate-pay", + summary=docs.estimate_pay_summary, + response_model=CashuPayEstimation, + responses={ + status.HTTP_400_BAD_REQUEST: { + "description": "Zero invoices not allowed. Amount must be positive.", + } + } + # dependencies=[Depends(JWTBearer())], +) +async def cashu_estimate_pay_path( + invoice: str = Body( + ..., + description="The invoice to be estimated", + ), + mint_name: Union[None, str] = Body( + None, + description=f"Name of the mint. Will use the pinned mint if empty.", + ), + wallet_name: Union[None, str] = Body( + None, + description=f"Name of the wallet. Will use the pinned wallet if empty.", + ), +) -> CashuWalletBalance: + try: + return await service.estimate_pay(invoice, wallet_name, mint_name) + except HTTPException: + raise + except NotImplementedError: + raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED) diff --git a/app/cashu/service.py b/app/cashu/service.py new file mode 100644 index 0000000..021065a --- /dev/null +++ b/app/cashu/service.py @@ -0,0 +1,281 @@ +import asyncio +import os +from os import listdir +from os.path import isdir, join +from typing import Union + +from cashu.core.settings import DEBUG, MINT_URL, VERSION +from fastapi import HTTPException + +import app.cashu.constants as c +from app.cashu.models import ( + CashuInfo, + CashuMint, + CashuMintInput, + CashuPayEstimation, + CashuWallet, + CashuWalletBalance, + CashuWalletData, +) +import app.cashu.exceptions as ce + +from app.lightning.models import PaymentStatus +from app.lightning.service import send_payment + + +class CashuService: + _DEFAULT_WALLET: CashuWallet = None + _DEFAULT_MINT: CashuMint = None + _pinned_mint: CashuMint = None + _pinned_wallet: CashuWallet = None + _wallets: list[CashuWallet] = [] + _mints: list[CashuMint] = [] + + async def init_wallets(self) -> None: + if len(self._wallets) > 0: + raise RuntimeError("Wallets are already initialized") + + if not os.path.exists(c.DATA_FOLDER): + os.makedirs(c.DATA_FOLDER) + + self._pinned_mint = self._DEFAULT_MINT = CashuMint( + url=c.DEFAULT_MINT_URL, pinned=True, default=True + ) + self._mints.append(self._pinned_mint) + + wallets = [d for d in listdir(c.DATA_FOLDER) if isdir(join(c.DATA_FOLDER, d))] + + try: + wallets.remove("mint") + except ValueError: + pass + + for wallet_name in wallets: + wallet = CashuWallet(self._pinned_mint, wallet_name) + await wallet.initialize() + self._wallets.append(wallet) + if wallet.name == c.DEFAULT_WALLET_NAME: + self._DEFAULT_WALLET = wallet + self._pinned_wallet = wallet + + if len(wallets) == 0: + # First run, initialize default wallet + self._pinned_wallet = self._DEFAULT_WALLET = CashuWallet( + mint=self._pinned_mint, name=c.DEFAULT_WALLET_NAME + ) + await self._DEFAULT_WALLET.initialize() + self._wallets.append(self._DEFAULT_WALLET) + + def info(self) -> CashuInfo: + return CashuInfo( + version=VERSION, + debug=DEBUG, + default_wallet=c.DEFAULT_WALLET_NAME, + default_mint=MINT_URL, + ) + + # add mint function + async def add_mint(self, mint_in: CashuMintInput) -> CashuMint: + if mint_in.url == c.DEFAULT_MINT_URL: + raise ce.IsDefaultMintException() + + for m in self._mints: + if m.url == mint_in.url: + raise ce.MintExistsException(mint_in.url) + + m = CashuMint(url=mint_in.url) + + # pretend doing a DB operation + await asyncio.sleep(0.01) + + self._mints.append(m) + + if mint_in.pinned: + m = self.pin_mint(m.url) + + return m + + async def list_mints(self) -> list[CashuMint]: + # pretend doing a DB operation + await asyncio.sleep(0.01) + + return self._mints + + def get_mint(self, mint_name: str) -> CashuMint: + for m in self._mints: + if m.url == mint_name: + return m + + return None + + def pin_mint(self, url: str) -> CashuMint: + if not url or url == "": + self._pinned_mint = self._DEFAULT_MINT + return self._pinned_mint + + for m in self._mints: + if m.url == url: + m.pinned = True + self._pinned_mint = m + continue + + m.pinned = False + + return self._pinned_mint + + async def add_wallet(self, wallet_name: CashuWalletData): + if self._resolve_wallet(wallet_name): + raise ce.WalletExistsException(wallet_name) + + w = CashuWallet(mint=self._pinned_mint, name=wallet_name) + await w.initialize() + self._wallets.append(w) + return w.get_wallet_data_for_client() + + def pin_wallet(self, wallet_name: str) -> str: + if not wallet_name or wallet_name == "": + self._pinned_wallet = self._DEFAULT_WALLET + return self._pinned_wallet.name + + try: + wallet = self._resolve_wallet(wallet_name) + self._pinned_wallet = wallet + return wallet.name + except HTTPException: + raise + + def balance(self) -> CashuWalletBalance: + + b = CashuWalletBalance() + + for w in self._wallets: + b += w.balance_overview + + return b + + async def mint( + self, + amount: int, + mint_name: Union[None, str] = None, + wallet_name: Union[None, str] = None, + ) -> bool: + wallet = self._resolve_wallet(wallet_name) + + if mint_name and not self.get_mint(mint_name=mint_name): + self.add_mint(mint_in=CashuMintInput(url=mint_name)) + wallet.url = mint_name + + await wallet.load_mint() + + wallet.status() # TODO: remove me, debug only + + invoice = await wallet.request_mint(amount) + + res = await send_payment( + pay_req=invoice.pr, timeout_seconds=5, fee_limit_msat=8000 + ) + + if res.status == PaymentStatus.SUCCEEDED: + try: + await wallet.mint(amount, invoice.hash) + wallet.status() # TODO: remove me, debug only + if mint_name != self._pinned_mint.url: + # If it is not the pinned mint, change it back to the pinned one + # We assume that the user want to mint one time on this one. + wallet.url = self._pinned_mint.url + + return True + except Exception as e: + # TODO: cashu wallet lib throws an Exception here => + # submit PR with a more specific exception + raise HTTPException(status_code=500, detail="Error while minting {e}") + + async def receive( + self, + coin: str, + lock: str, + mint_name: Union[None, str], + wallet_name: Union[None, str], + ) -> CashuWalletBalance: + wallet = self._resolve_wallet(wallet_name) + + wallet.status() # TODO: remove me, debug only + + await wallet.receive(coin, lock) + + wallet.status() # TODO: remove me, debug only + + return wallet.balance_overview + + async def pay( + self, + invoice: str, + mint_name: Union[None, str], + wallet_name: Union[None, str], + ): + wallet = self._resolve_wallet(wallet_name) + await wallet.load_mint() + + wallet.status() + + res = await self.estimate_pay(invoice, mint_name, wallet_name) + + _, send_proofs = await wallet.split_to_send(wallet.proofs, res.amount) + await wallet.pay_lightning(send_proofs, invoice) + wallet.status() + + return wallet.available_balance + + async def estimate_pay( + self, + invoice: str, + mint_name: Union[None, str], + wallet_name: Union[None, str], + ) -> CashuPayEstimation: + wallet = self._resolve_wallet(wallet_name) + await wallet.load_mint() + amount, fee = await wallet.get_pay_amount_with_fees(invoice) + + if amount < 1: + raise ce.ZeroInvoiceException() + + return CashuPayEstimation( + amount=amount, + fee=fee, + balance_ok=wallet.available_balance > amount, + ) + + async def list_wallets( + self, include_balances: bool = False + ) -> list[CashuWalletData]: + wallets_data = [] + + for w in self._wallets: + wallets_data.append(w.get_wallet_data_for_client(include_balances)) + + return wallets_data + + async def get_wallet( + self, + mint_name: Union[None, str] = None, + wallet_name: Union[None, str] = None, + include_balances: bool = False, + ) -> CashuWalletData: + wallet = self._resolve_wallet(wallet_name) + return wallet.get_wallet_data_for_client(include_balances) + + def _resolve_wallet(self, wallet_name: Union[None, str]) -> CashuWallet: + wallet = self._pinned_wallet + + if wallet_name and len(wallet_name) > 0: + # User explicitly specified a wallet + wallet = None + for w in self._wallets: + if w.name == wallet_name: + wallet = w + break + + if not wallet: + ce.WalletNotFoundException(wallet_name) + + return wallet diff --git a/app/main.py b/app/main.py index 4db58d8..1574462 100644 --- a/app/main.py +++ b/app/main.py @@ -35,6 +35,7 @@ register_bitcoin_status_gatherer, register_bitcoin_zmq_sub, ) +from app.cashu.router import router as cashu_router from app.external.fastapi_versioning import VersionedFastAPI from app.lightning.models import LnInitState from app.lightning.router import router as ln_router @@ -63,6 +64,7 @@ class AppSettings(RedisSettings): unversioned_app.include_router(system_router) if setup_router is not None: unversioned_app.include_router(setup_router) +unversioned_app.include_router(cashu_router) app = VersionedFastAPI( diff --git a/poetry.lock b/poetry.lock index cecdc41..606c50f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -96,7 +96,23 @@ python-versions = ">=3.5" dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] + +[[package]] +name = "bech32" +version = "1.2.0" +description = "Reference implementation for Bech32 and segwit addresses." +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "bitstring" +version = "3.1.9" +description = "Simple construction, analysis and modification of binary data." +category = "main" +optional = false +python-versions = "*" [[package]] name = "black" @@ -120,6 +136,57 @@ d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "cashu" +version = "0.4.2" +description = "Ecash wallet and mint with Bitcoin Lightning support" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +anyio = {version = "3.6.2", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +attrs = {version = "22.1.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +bech32 = {version = "1.2.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +bitstring = {version = "3.1.9", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +certifi = {version = "2022.9.24", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +cffi = {version = "1.15.1", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +charset-normalizer = {version = "2.0.12", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +click = {version = "8.0.4", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +colorama = {version = "0.4.5", markers = "python_version >= \"3.7\" and python_version < \"4.0\" and platform_system == \"Windows\" or python_version >= \"3.7\" and python_version < \"4.0\" and sys_platform == \"win32\""} +ecdsa = {version = "0.18.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +environs = {version = "9.5.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +fastapi = {version = "0.83.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +h11 = {version = "0.12.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +idna = {version = "3.4", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +iniconfig = {version = "1.1.1", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +loguru = {version = "0.6.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +marshmallow = {version = "3.18.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +outcome = {version = "1.2.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +packaging = {version = "21.3", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +pluggy = {version = "1.0.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +py = {version = "1.11.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +pycparser = {version = "2.21", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +pydantic = {version = "1.10.2", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +pyparsing = {version = "3.0.9", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +pytest = {version = "7.1.3", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +pytest-asyncio = {version = "0.19.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +python-bitcoinlib = {version = "0.11.2", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +python-dotenv = {version = "0.21.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +represent = {version = "1.6.0.post0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +requests = {version = "2.27.1", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +secp256k1 = {version = "0.14.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +six = {version = "1.16.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +sniffio = {version = "1.3.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +sqlalchemy = {version = "1.3.24", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +sqlalchemy-aio = {version = "0.17.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +starlette = {version = "0.19.1", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +tomli = {version = "2.0.1", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +typing-extensions = {version = "4.4.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +urllib3 = {version = "1.26.12", markers = "python_version >= \"3.7\" and python_version < \"4\""} +uvicorn = {version = "0.18.3", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} +win32-setctime = {version = "1.1.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\" and sys_platform == \"win32\""} + [[package]] name = "cchardet" version = "2.1.7" @@ -157,22 +224,22 @@ python-versions = ">=3.6.1" [[package]] name = "charset-normalizer" -version = "2.1.1" +version = "2.0.12" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false -python-versions = ">=3.6.0" +python-versions = ">=3.5.0" [package.extras] -unicode_backport = ["unicodedata2"] +unicode-backport = ["unicodedata2"] [[package]] name = "click" -version = "8.1.3" +version = "8.0.4" description = "Composable command line interface toolkit" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -211,7 +278,7 @@ ordered-set = ">=4.1.0,<4.2.0" cli = ["clevercsv (==0.7.1)", "click (==8.0.3)", "pyyaml (==5.4.1)", "toml (==0.10.2)"] [[package]] -name = "Deprecated" +name = "deprecated" version = "1.2.13" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." category = "main" @@ -232,9 +299,42 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "ecdsa" +version = "0.18.0" +description = "ECDSA cryptographic signature library (pure python)" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[package.dependencies] +six = ">=1.9.0" + +[package.extras] +gmpy = ["gmpy"] +gmpy2 = ["gmpy2"] + +[[package]] +name = "environs" +version = "9.5.0" +description = "simplified environment variable parsing" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +marshmallow = ">=3.0.0" +python-dotenv = "*" + +[package.extras] +dev = ["dj-database-url", "dj-email-url", "django-cache-url", "flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)", "pytest", "tox"] +django = ["dj-database-url", "dj-email-url", "django-cache-url"] +lint = ["flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)"] +tests = ["dj-database-url", "dj-email-url", "django-cache-url", "pytest"] + [[package]] name = "fastapi" -version = "0.82.0" +version = "0.83.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" category = "main" optional = false @@ -335,11 +435,11 @@ setuptools = "*" [[package]] name = "h11" -version = "0.14.0" +version = "0.12.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [[package]] name = "hiredis" @@ -351,7 +451,7 @@ python-versions = ">=3.6" [[package]] name = "identify" -version = "2.5.6" +version = "2.5.8" description = "File identification library for Python" category = "dev" optional = false @@ -372,7 +472,7 @@ python-versions = ">=3.5" name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" -category = "dev" +category = "main" optional = false python-versions = "*" @@ -386,9 +486,41 @@ python-versions = ">=3.6.1,<4.0" [package.extras] colors = ["colorama (>=0.4.3,<0.5.0)"] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +pipfile-deprecated-finder = ["pipreqs", "requirementslib"] plugins = ["setuptools"] -requirements_deprecated_finder = ["pip-api", "pipreqs"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + +[[package]] +name = "loguru" +version = "0.6.0" +description = "Python logging made (stupidly) simple" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (>=4.1.1)", "black (>=19.10b0)", "colorama (>=0.3.4)", "docutils (==0.16)", "flake8 (>=3.7.7)", "isort (>=5.1.1)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)"] + +[[package]] +name = "marshmallow" +version = "3.18.0" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +packaging = ">=17.0" + +[package.extras] +dev = ["flake8 (==5.0.4)", "flake8-bugbear (==22.9.11)", "mypy (==0.971)", "pre-commit (>=2.4,<3.0)", "pytest", "pytz", "simplejson", "tox"] +docs = ["alabaster (==0.7.12)", "autodocsumm (==0.2.9)", "sphinx (==5.1.1)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"] +lint = ["flake8 (==5.0.4)", "flake8-bugbear (==22.9.11)", "mypy (==0.971)", "pre-commit (>=2.4,<3.0)"] +tests = ["pytest", "pytz", "simplejson"] [[package]] name = "multidict" @@ -428,6 +560,17 @@ python-versions = ">=3.7" [package.extras] dev = ["black", "mypy", "pytest"] +[[package]] +name = "outcome" +version = "1.2.0" +description = "Capture the outcome of Python function calls." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +attrs = ">=19.2.0" + [[package]] name = "packaging" version = "21.3" @@ -463,7 +606,7 @@ test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -546,7 +689,7 @@ dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] [[package]] -name = "PyJWT" +name = "pyjwt" version = "2.4.0" description = "JSON Web Token implementation in Python" category = "main" @@ -574,7 +717,7 @@ diagrams = ["jinja2", "railroad-diagrams"] name = "pytest" version = "7.1.3" description = "pytest: simple powerful testing with Python" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" @@ -594,7 +737,7 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2. name = "pytest-asyncio" version = "0.19.0" description = "Pytest support for asyncio" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" @@ -604,6 +747,14 @@ pytest = ">=6.1.0" [package.extras] testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] +[[package]] +name = "python-bitcoinlib" +version = "0.11.2" +description = "The Swiss Army Knife of the Bitcoin protocol." +category = "main" +optional = false +python-versions = "*" + [[package]] name = "python-decouple" version = "3.6" @@ -612,6 +763,17 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "python-dotenv" +version = "0.21.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +cli = ["click (>=5.0)"] + [[package]] name = "python-json-logger" version = "2.0.4" @@ -621,7 +783,7 @@ optional = false python-versions = ">=3.5" [[package]] -name = "PyYAML" +name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" category = "dev" @@ -657,23 +819,48 @@ packaging = ">=20.4" hiredis = ["hiredis (>=1.0.0)"] ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] +[[package]] +name = "represent" +version = "1.6.0.post0" +description = "Create __repr__ automatically or declaratively." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +six = ">=1.8.0" + +[package.extras] +test = ["ipython", "mock", "pytest (>=3.0.5)"] + [[package]] name = "requests" -version = "2.28.1" +version = "2.27.1" description = "Python HTTP for Humans." category = "main" optional = false -python-versions = ">=3.7, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<3" -idna = ">=2.5,<4" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} urllib3 = ">=1.21.1,<1.27" [package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "secp256k1" +version = "0.14.0" +description = "FFI bindings to libsecp256k1" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +cffi = ">=1.3.0" [[package]] name = "setuptools" @@ -704,6 +891,44 @@ category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "sqlalchemy" +version = "1.3.24" +description = "Database Abstraction Library" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mysql = ["mysqlclient"] +oracle = ["cx_oracle"] +postgresql = ["psycopg2"] +postgresql-pg8000 = ["pg8000 (<1.16.6)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +pymysql = ["pymysql", "pymysql (<1)"] + +[[package]] +name = "sqlalchemy-aio" +version = "0.17.0" +description = "Async support for SQLAlchemy." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +outcome = "*" +represent = ">=1.4" +sqlalchemy = "<1.4" + +[package.extras] +test = ["pytest (>=5.4)", "pytest-asyncio (>=0.14)", "pytest-trio (>=0.6)"] +test-noextras = ["pytest (>=5.4)", "pytest-asyncio (>=0.14)"] +trio = ["trio (>=0.15)"] + [[package]] name = "starlette" version = "0.19.1" @@ -742,7 +967,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" @@ -784,21 +1009,32 @@ standard = ["colorama (>=0.4)", "httptools (>=0.4.0)", "python-dotenv (>=0.13)", [[package]] name = "virtualenv" -version = "20.16.5" +version = "20.16.6" description = "Virtual Python Environment builder" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -distlib = ">=0.3.5,<1" +distlib = ">=0.3.6,<1" filelock = ">=3.4.1,<4" platformdirs = ">=2.4,<3" [package.extras] -docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] +docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + [[package]] name = "wrapt" version = "1.14.1" @@ -822,7 +1058,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "fa6690483ef051795b45182aabc573fb8f1e5995c980796776e525bcd352eca4" +content-hash = "b581405fab5af93c65443034f385103782a9133b7d09a85f35e6b69a215ccfd5" [metadata.files] aiohttp = [ @@ -923,6 +1159,15 @@ attrs = [ {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, ] +bech32 = [ + {file = "bech32-1.2.0-py3-none-any.whl", hash = "sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981"}, + {file = "bech32-1.2.0.tar.gz", hash = "sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899"}, +] +bitstring = [ + {file = "bitstring-3.1.9-py2-none-any.whl", hash = "sha256:e3e340e58900a948787a05e8c08772f1ccbe133f6f41fe3f0fa19a18a22bbf4f"}, + {file = "bitstring-3.1.9-py3-none-any.whl", hash = "sha256:0de167daa6a00c9386255a7cac931b45e6e24e0ad7ea64f1f92a64ac23ad4578"}, + {file = "bitstring-3.1.9.tar.gz", hash = "sha256:a5848a3f63111785224dca8bb4c0a75b62ecdef56a042c8d6be74b16f7e860e7"}, +] black = [ {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"}, {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"}, @@ -946,6 +1191,10 @@ black = [ {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"}, {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, ] +cashu = [ + {file = "cashu-0.4.2-py3-none-any.whl", hash = "sha256:6d24f5e921c33dae1b6823f5e34feab0d6d5662b56a67c29095d48241163a887"}, + {file = "cashu-0.4.2.tar.gz", hash = "sha256:97564481501cbe163e6be4d3cdd0d52d2841e15b830a0185c3c329657e4b8c36"}, +] cchardet = [ {file = "cchardet-2.1.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c6f70139aaf47ffb94d89db603af849b82efdf756f187cdd3e566e30976c519f"}, {file = "cchardet-2.1.7-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5a25f9577e9bebe1a085eec2d6fdd72b7a9dd680811bba652ea6090fb2ff472f"}, @@ -1052,12 +1301,12 @@ cfgv = [ {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, - {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, ] click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, + {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, + {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, ] colorama = [ {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, @@ -1119,7 +1368,7 @@ deepdiff = [ {file = "deepdiff-5.8.1-py3-none-any.whl", hash = "sha256:e9aea49733f34fab9a0897038d8f26f9d94a97db1790f1b814cced89e9e0d2b7"}, {file = "deepdiff-5.8.1.tar.gz", hash = "sha256:8d4eb2c4e6cbc80b811266419cb71dd95a157094a3947ccf937a94d44943c7b8"}, ] -Deprecated = [ +deprecated = [ {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, ] @@ -1127,9 +1376,17 @@ distlib = [ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, ] +ecdsa = [ + {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, + {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, +] +environs = [ + {file = "environs-9.5.0-py2.py3-none-any.whl", hash = "sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124"}, + {file = "environs-9.5.0.tar.gz", hash = "sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9"}, +] fastapi = [ - {file = "fastapi-0.82.0-py3-none-any.whl", hash = "sha256:a4269329a7374c78f6e92c195d14cc4ce3a525e25b79e62edf2df8196469743f"}, - {file = "fastapi-0.82.0.tar.gz", hash = "sha256:5ee7b7473a55940a18d4869ff57d29c372363bf8d3033a0e660a8cf38b1d3d9e"}, + {file = "fastapi-0.83.0-py3-none-any.whl", hash = "sha256:694a2b6c2607a61029a4be1c6613f84d74019cb9f7a41c7a475dca8e715f9368"}, + {file = "fastapi-0.83.0.tar.gz", hash = "sha256:96eb692350fe13d7a9843c3c87a874f0d45102975257dd224903efd6c0fde3bd"}, ] fastapi-plugins = [ {file = "fastapi-plugins-0.10.0.tar.gz", hash = "sha256:f2fd807fb4e59f38dccc9945d25d11438a98e800def649fc3c9ec46b1ebc6c89"}, @@ -1301,8 +1558,8 @@ grpcio-tools = [ {file = "grpcio_tools-1.48.1-cp39-cp39-win_amd64.whl", hash = "sha256:306bd078d20739026e481ef9f353e5b3025a761b443e96a47e5c5db7e3cdcd8a"}, ] h11 = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, + {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, + {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, ] hiredis = [ {file = "hiredis-2.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b4c8b0bc5841e578d5fb32a16e0c305359b987b850a06964bd5a62739d688048"}, @@ -1348,8 +1605,8 @@ hiredis = [ {file = "hiredis-2.0.0.tar.gz", hash = "sha256:81d6d8e39695f2c37954d1011c0480ef7cf444d4e3ae24bc5e89ee5de360139a"}, ] identify = [ - {file = "identify-2.5.6-py2.py3-none-any.whl", hash = "sha256:b276db7ec52d7e89f5bc4653380e33054ddc803d25875952ad90b0f012cbcdaa"}, - {file = "identify-2.5.6.tar.gz", hash = "sha256:6c32dbd747aa4ceee1df33f25fed0b0f6e0d65721b15bd151307ff7056d50245"}, + {file = "identify-2.5.8-py2.py3-none-any.whl", hash = "sha256:48b7925fe122720088aeb7a6c34f17b27e706b72c61070f27fe3789094233440"}, + {file = "identify-2.5.8.tar.gz", hash = "sha256:7a214a10313b9489a0d61467db2856ae8d0b8306fc923e03a9effa53d8aedc58"}, ] idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, @@ -1363,6 +1620,14 @@ isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] +loguru = [ + {file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"}, + {file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"}, +] +marshmallow = [ + {file = "marshmallow-3.18.0-py3-none-any.whl", hash = "sha256:35e02a3a06899c9119b785c12a22f4cda361745d66a71ab691fd7610202ae104"}, + {file = "marshmallow-3.18.0.tar.gz", hash = "sha256:6804c16114f7fce1f5b4dadc31f4674af23317fcc7f075da21e35c1a35d781f7"}, +] multidict = [ {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"}, {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"}, @@ -1436,6 +1701,10 @@ ordered-set = [ {file = "ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8"}, {file = "ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562"}, ] +outcome = [ + {file = "outcome-1.2.0-py2.py3-none-any.whl", hash = "sha256:c4ab89a56575d6d38a05aa16daeaa333109c1f96167aba8901ab18b6b5e0f7f5"}, + {file = "outcome-1.2.0.tar.gz", hash = "sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672"}, +] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, @@ -1565,7 +1834,7 @@ pydantic = [ {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"}, {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"}, ] -PyJWT = [ +pyjwt = [ {file = "PyJWT-2.4.0-py3-none-any.whl", hash = "sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf"}, {file = "PyJWT-2.4.0.tar.gz", hash = "sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba"}, ] @@ -1581,15 +1850,23 @@ pytest-asyncio = [ {file = "pytest-asyncio-0.19.0.tar.gz", hash = "sha256:ac4ebf3b6207259750bc32f4c1d8fcd7e79739edbc67ad0c58dd150b1d072fed"}, {file = "pytest_asyncio-0.19.0-py3-none-any.whl", hash = "sha256:7a97e37cfe1ed296e2e84941384bdd37c376453912d397ed39293e0916f521fa"}, ] +python-bitcoinlib = [ + {file = "python-bitcoinlib-0.11.2.tar.gz", hash = "sha256:61ba514e0d232cc84741e49862dcedaf37199b40bba252a17edc654f63d13f39"}, + {file = "python_bitcoinlib-0.11.2-py3-none-any.whl", hash = "sha256:78bd4ee717fe805cd760dfdd08765e77b7c7dbef4627f8596285e84953756508"}, +] python-decouple = [ {file = "python-decouple-3.6.tar.gz", hash = "sha256:2838cdf77a5cf127d7e8b339ce14c25bceb3af3e674e039d4901ba16359968c7"}, {file = "python_decouple-3.6-py3-none-any.whl", hash = "sha256:6cf502dc963a5c642ea5ead069847df3d916a6420cad5599185de6bab11d8c2e"}, ] +python-dotenv = [ + {file = "python-dotenv-0.21.0.tar.gz", hash = "sha256:b77d08274639e3d34145dfa6c7008e66df0f04b7be7a75fd0d5292c191d79045"}, + {file = "python_dotenv-0.21.0-py3-none-any.whl", hash = "sha256:1684eb44636dd462b66c3ee016599815514527ad99965de77f43e0944634a7e5"}, +] python-json-logger = [ {file = "python-json-logger-2.0.4.tar.gz", hash = "sha256:764d762175f99fcc4630bd4853b09632acb60a6224acb27ce08cd70f0b1b81bd"}, {file = "python_json_logger-2.0.4-py3-none-any.whl", hash = "sha256:3b03487b14eb9e4f77e4fc2a023358b5394b82fd89cecf5586259baed57d8c6f"}, ] -PyYAML = [ +pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, @@ -1704,9 +1981,38 @@ redis = [ {file = "redis-4.3.4-py3-none-any.whl", hash = "sha256:a52d5694c9eb4292770084fa8c863f79367ca19884b329ab574d5cb2036b3e54"}, {file = "redis-4.3.4.tar.gz", hash = "sha256:ddf27071df4adf3821c4f2ca59d67525c3a82e5f268bed97b813cb4fabf87880"}, ] +represent = [ + {file = "Represent-1.6.0.post0-py2.py3-none-any.whl", hash = "sha256:99142650756ef1998ce0661568f54a47dac8c638fb27e3816c02536575dbba8c"}, + {file = "Represent-1.6.0.post0.tar.gz", hash = "sha256:026c0de2ee8385d1255b9c2426cd4f03fe9177ac94c09979bc601946c8493aa0"}, +] requests = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, + {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, +] +secp256k1 = [ + {file = "secp256k1-0.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f666c67dcf1dc69e1448b2ede5e12aaf382b600204a61dbc65e4f82cea444405"}, + {file = "secp256k1-0.14.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fcabb3c3497a902fb61eec72d1b69bf72747d7bcc2a732d56d9319a1e8322262"}, + {file = "secp256k1-0.14.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7a27c479ab60571502516a1506a562d0a9df062de8ad645313fabfcc97252816"}, + {file = "secp256k1-0.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f4b9306bff6dde020444dfee9ca9b9f5b20ca53a2c0b04898361a3f43d5daf2e"}, + {file = "secp256k1-0.14.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:72735da6cb28273e924431cd40aa607e7f80ef09608c8c9300be2e0e1d2417b4"}, + {file = "secp256k1-0.14.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:87f4ad42a370f768910585989a301d1d65de17dcd86f6e8def9b021364b34d5c"}, + {file = "secp256k1-0.14.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:130f119b06142e597c10eb4470b5a38eae865362d01aaef06b113478d77f728d"}, + {file = "secp256k1-0.14.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:3aedcfe6eb1c5fa7c6be25b7cc91c76d8eb984271920ba0f7a934ae41ed56f51"}, + {file = "secp256k1-0.14.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c91dd3154f6c46ac798d9a41166120e1751222587f54516cc3f378f56ce4ac82"}, + {file = "secp256k1-0.14.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fec790cb6d0d37129ca0ce5b3f8e85692d5fb618d1c440f189453d18694035df"}, + {file = "secp256k1-0.14.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:63eb148196b8f646922d4be6739b17fbbf50ebb3a020078c823e2445d88b7a81"}, + {file = "secp256k1-0.14.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:adc23a4c5d24c95191638eb2ca313097827f07db102e77b59faed15d50c98cae"}, + {file = "secp256k1-0.14.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ce0314788d3248b275426501228969fd32f6501c9d1837902ee0e7bd8264a36f"}, + {file = "secp256k1-0.14.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bc761894b3634021686714278fc62b73395fa3eded33453eadfd8a00a6c44ef3"}, + {file = "secp256k1-0.14.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:373dc8bca735f3c2d73259aa2711a9ecea2f3c7edbb663555fe3422e3dd76102"}, + {file = "secp256k1-0.14.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fe3f503c9dfdf663b500d3e0688ad842e116c2907ad3f1e1d685812df3f56290"}, + {file = "secp256k1-0.14.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4b1bf09953cde181132cf5e9033065615e5c2694e803165e2db763efa47695e5"}, + {file = "secp256k1-0.14.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6af07be5f8612628c3638dc7b208f6cc78d0abae3e25797eadb13890c7d5da81"}, + {file = "secp256k1-0.14.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a8dbd75a9fb6f42de307f3c5e24573fe59c3374637cbf39136edc66c200a4029"}, + {file = "secp256k1-0.14.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:97a30c8dae633cb18135c76b6517ae99dc59106818e8985be70dbc05dcc06c0d"}, + {file = "secp256k1-0.14.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f4062d8c101aa63b9ecb3709f1f075ad9c01b6672869bbaa1bd77271816936a7"}, + {file = "secp256k1-0.14.0-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c9e7c024ff17e9b9d7c392bb2a917da231d6cb40ab119389ff1f51dca10339a4"}, + {file = "secp256k1-0.14.0.tar.gz", hash = "sha256:82c06712d69ef945220c8b53c1a0d424c2ff6a1f64aee609030df79ad8383397"}, ] setuptools = [ {file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"}, @@ -1720,6 +2026,46 @@ sniffio = [ {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, ] +sqlalchemy = [ + {file = "SQLAlchemy-1.3.24-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:87a2725ad7d41cd7376373c15fd8bf674e9c33ca56d0b8036add2d634dba372e"}, + {file = "SQLAlchemy-1.3.24-cp27-cp27m-win32.whl", hash = "sha256:f597a243b8550a3a0b15122b14e49d8a7e622ba1c9d29776af741f1845478d79"}, + {file = "SQLAlchemy-1.3.24-cp27-cp27m-win_amd64.whl", hash = "sha256:fc4cddb0b474b12ed7bdce6be1b9edc65352e8ce66bc10ff8cbbfb3d4047dbf4"}, + {file = "SQLAlchemy-1.3.24-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:f1149d6e5c49d069163e58a3196865e4321bad1803d7886e07d8710de392c548"}, + {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:14f0eb5db872c231b20c18b1e5806352723a3a89fb4254af3b3e14f22eaaec75"}, + {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:e98d09f487267f1e8d1179bf3b9d7709b30a916491997137dd24d6ae44d18d79"}, + {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:fc1f2a5a5963e2e73bac4926bdaf7790c4d7d77e8fc0590817880e22dd9d0b8b"}, + {file = "SQLAlchemy-1.3.24-cp35-cp35m-win32.whl", hash = "sha256:f3c5c52f7cb8b84bfaaf22d82cb9e6e9a8297f7c2ed14d806a0f5e4d22e83fb7"}, + {file = "SQLAlchemy-1.3.24-cp35-cp35m-win_amd64.whl", hash = "sha256:0352db1befcbed2f9282e72843f1963860bf0e0472a4fa5cf8ee084318e0e6ab"}, + {file = "SQLAlchemy-1.3.24-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:2ed6343b625b16bcb63c5b10523fd15ed8934e1ed0f772c534985e9f5e73d894"}, + {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:34fcec18f6e4b24b4a5f6185205a04f1eab1e56f8f1d028a2a03694ebcc2ddd4"}, + {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e47e257ba5934550d7235665eee6c911dc7178419b614ba9e1fbb1ce6325b14f"}, + {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:816de75418ea0953b5eb7b8a74933ee5a46719491cd2b16f718afc4b291a9658"}, + {file = "SQLAlchemy-1.3.24-cp36-cp36m-win32.whl", hash = "sha256:26155ea7a243cbf23287f390dba13d7927ffa1586d3208e0e8d615d0c506f996"}, + {file = "SQLAlchemy-1.3.24-cp36-cp36m-win_amd64.whl", hash = "sha256:f03bd97650d2e42710fbe4cf8a59fae657f191df851fc9fc683ecef10746a375"}, + {file = "SQLAlchemy-1.3.24-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a006d05d9aa052657ee3e4dc92544faae5fcbaafc6128217310945610d862d39"}, + {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1e2f89d2e5e3c7a88e25a3b0e43626dba8db2aa700253023b82e630d12b37109"}, + {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0d5d862b1cfbec5028ce1ecac06a3b42bc7703eb80e4b53fceb2738724311443"}, + {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:0172423a27fbcae3751ef016663b72e1a516777de324a76e30efa170dbd3dd2d"}, + {file = "SQLAlchemy-1.3.24-cp37-cp37m-win32.whl", hash = "sha256:d37843fb8df90376e9e91336724d78a32b988d3d20ab6656da4eb8ee3a45b63c"}, + {file = "SQLAlchemy-1.3.24-cp37-cp37m-win_amd64.whl", hash = "sha256:c10ff6112d119f82b1618b6dc28126798481b9355d8748b64b9b55051eb4f01b"}, + {file = "SQLAlchemy-1.3.24-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:861e459b0e97673af6cc5e7f597035c2e3acdfb2608132665406cded25ba64c7"}, + {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5de2464c254380d8a6c20a2746614d5a436260be1507491442cf1088e59430d2"}, + {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d375d8ccd3cebae8d90270f7aa8532fe05908f79e78ae489068f3b4eee5994e8"}, + {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:014ea143572fee1c18322b7908140ad23b3994036ef4c0d630110faf942652f8"}, + {file = "SQLAlchemy-1.3.24-cp38-cp38-win32.whl", hash = "sha256:6607ae6cd3a07f8a4c3198ffbf256c261661965742e2b5265a77cd5c679c9bba"}, + {file = "SQLAlchemy-1.3.24-cp38-cp38-win_amd64.whl", hash = "sha256:fcb251305fa24a490b6a9ee2180e5f8252915fb778d3dafc70f9cc3f863827b9"}, + {file = "SQLAlchemy-1.3.24-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:01aa5f803db724447c1d423ed583e42bf5264c597fd55e4add4301f163b0be48"}, + {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4d0e3515ef98aa4f0dc289ff2eebb0ece6260bbf37c2ea2022aad63797eacf60"}, + {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:bce28277f308db43a6b4965734366f533b3ff009571ec7ffa583cb77539b84d6"}, + {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:8110e6c414d3efc574543109ee618fe2c1f96fa31833a1ff36cc34e968c4f233"}, + {file = "SQLAlchemy-1.3.24-cp39-cp39-win32.whl", hash = "sha256:ee5f5188edb20a29c1cc4a039b074fdc5575337c9a68f3063449ab47757bb064"}, + {file = "SQLAlchemy-1.3.24-cp39-cp39-win_amd64.whl", hash = "sha256:09083c2487ca3c0865dc588e07aeaa25416da3d95f7482c07e92f47e080aa17b"}, + {file = "SQLAlchemy-1.3.24.tar.gz", hash = "sha256:ebbb777cbf9312359b897bf81ba00dae0f5cb69fba2a18265dcc18a6f5ef7519"}, +] +sqlalchemy-aio = [ + {file = "sqlalchemy_aio-0.17.0-py3-none-any.whl", hash = "sha256:3f4aa392c38f032d6734826a4138a0f02ed3122d442ed142be1e5964f2a33b60"}, + {file = "sqlalchemy_aio-0.17.0.tar.gz", hash = "sha256:f531c7982662d71dfc0b117e77bb2ed544e25cd5361e76cf9f5208edcfb71f7b"}, +] starlette = [ {file = "starlette-0.19.1-py3-none-any.whl", hash = "sha256:5a60c5c2d051f3a8eb546136aa0c9399773a689595e099e0877704d5888279bf"}, {file = "starlette-0.19.1.tar.gz", hash = "sha256:c6d21096774ecb9639acad41b86b7706e52ba3bf1dc13ea4ed9ad593d47e24c7"}, @@ -1749,8 +2095,12 @@ uvicorn = [ {file = "uvicorn-0.18.3.tar.gz", hash = "sha256:9a66e7c42a2a95222f76ec24a4b754c158261c4696e683b9dadc72b590e0311b"}, ] virtualenv = [ - {file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"}, - {file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"}, + {file = "virtualenv-20.16.6-py3-none-any.whl", hash = "sha256:186ca84254abcbde98180fd17092f9628c5fe742273c02724972a1d8a2035108"}, + {file = "virtualenv-20.16.6.tar.gz", hash = "sha256:530b850b523c6449406dfba859d6345e48ef19b8439606c5d74d7d3c9e14d76e"}, +] +win32-setctime = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, ] wrapt = [ {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, diff --git a/pyproject.toml b/pyproject.toml index b47dce3..db0e40f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ license = "MIT" [tool.poetry.dependencies] python = "^3.9" -fastapi = "0.82.0" +fastapi = "0.83.0" fastapi-plugins = "0.10.0" anyio = "^3.6.1" redis = "4.3.4" @@ -17,7 +17,7 @@ uvicorn = "0.18.3" PyJWT = "2.4.0" python-decouple = "3.6" psutil = "5.9.2" -requests = "2.28.1" +requests = "2.27.1" pyzmq = "23.2.1" cchardet = "2.1.7" aiohttp = "3.8.1" @@ -26,6 +26,7 @@ grpcio-tools = "1.48.1" googleapis-common-protos = "1.56.4" protobuf = "^3.20.3" deepdiff = "5.8.1" +cashu = "^0.4.2" [tool.poetry.group.dev.dependencies] black = "22.10.0" From 56344c58f20183ac81187a7846c1a86e5da96e60 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 30 Oct 2022 18:17:14 +0000 Subject: [PATCH 2/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .gitignore | 2 +- app/cashu/service.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 307a916..df165c2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ __pycache__ htmlcov scripts/sync_to_blitz.personal.sh docker/.env -cashu.db \ No newline at end of file +cashu.db diff --git a/app/cashu/service.py b/app/cashu/service.py index 021065a..cc10f36 100644 --- a/app/cashu/service.py +++ b/app/cashu/service.py @@ -8,6 +8,7 @@ from fastapi import HTTPException import app.cashu.constants as c +import app.cashu.exceptions as ce from app.cashu.models import ( CashuInfo, CashuMint, @@ -17,8 +18,6 @@ CashuWalletBalance, CashuWalletData, ) -import app.cashu.exceptions as ce - from app.lightning.models import PaymentStatus from app.lightning.service import send_payment From 0ed264ac35e1e6474e4aa6d5a7779f2e6e2fa9a8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 15 Feb 2023 17:26:43 +0000 Subject: [PATCH 3/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- app/cashu/service.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/cashu/service.py b/app/cashu/service.py index cc10f36..b6f1e81 100644 --- a/app/cashu/service.py +++ b/app/cashu/service.py @@ -144,7 +144,6 @@ def pin_wallet(self, wallet_name: str) -> str: raise def balance(self) -> CashuWalletBalance: - b = CashuWalletBalance() for w in self._wallets: From 86af3c41cc906cb477d47d2169a686aab4896c13 Mon Sep 17 00:00:00 2001 From: fusion44 Date: Thu, 2 Mar 2023 19:39:10 +0100 Subject: [PATCH 4/7] feat add custom cashu implementation Why not use the pypi package? * includes TOR binaries * incompatible with several dependencies --- app/external/cashu/README.md | 178 ++++ app/external/cashu/__init__.py | 0 app/external/cashu/alembic.ini | 106 +++ app/external/cashu/alembic/README | 1 + app/external/cashu/alembic/env.py | 74 ++ app/external/cashu/alembic/script.py.mako | 24 + .../alembic/versions/3fabf693571f_initial.py | 121 +++ app/external/cashu/core/__init__.py | 0 app/external/cashu/core/b_dhke.py | 95 ++ app/external/cashu/core/base.py | 330 +++++++ app/external/cashu/core/bolt11.py | 370 ++++++++ app/external/cashu/core/crypto.py | 45 + app/external/cashu/core/db.py | 75 ++ app/external/cashu/core/errors.py | 30 + app/external/cashu/core/helpers.py | 46 + app/external/cashu/core/legacy.py | 31 + app/external/cashu/core/script.py | 104 +++ app/external/cashu/core/secp.py | 52 ++ app/external/cashu/core/settings.py | 68 ++ app/external/cashu/core/split.py | 8 + app/external/cashu/lightning/__init__.py | 3 + app/external/cashu/lightning/base.py | 88 ++ app/external/cashu/lightning/lnbits.py | 154 ++++ app/external/cashu/migrations.py | 51 ++ app/external/cashu/tor/tor.py | 92 ++ app/external/cashu/wallet/crud.py | 324 +++++++ app/external/cashu/wallet/models.py | 6 + app/external/cashu/wallet/wallet.py | 852 ++++++++++++++++++ app/external/cashu/wallet/wallet_helpers.py | 167 ++++ 29 files changed, 3495 insertions(+) create mode 100644 app/external/cashu/README.md create mode 100644 app/external/cashu/__init__.py create mode 100644 app/external/cashu/alembic.ini create mode 100644 app/external/cashu/alembic/README create mode 100644 app/external/cashu/alembic/env.py create mode 100644 app/external/cashu/alembic/script.py.mako create mode 100644 app/external/cashu/alembic/versions/3fabf693571f_initial.py create mode 100644 app/external/cashu/core/__init__.py create mode 100644 app/external/cashu/core/b_dhke.py create mode 100644 app/external/cashu/core/base.py create mode 100644 app/external/cashu/core/bolt11.py create mode 100644 app/external/cashu/core/crypto.py create mode 100644 app/external/cashu/core/db.py create mode 100644 app/external/cashu/core/errors.py create mode 100644 app/external/cashu/core/helpers.py create mode 100644 app/external/cashu/core/legacy.py create mode 100644 app/external/cashu/core/script.py create mode 100644 app/external/cashu/core/secp.py create mode 100644 app/external/cashu/core/settings.py create mode 100644 app/external/cashu/core/split.py create mode 100644 app/external/cashu/lightning/__init__.py create mode 100644 app/external/cashu/lightning/base.py create mode 100644 app/external/cashu/lightning/lnbits.py create mode 100644 app/external/cashu/migrations.py create mode 100644 app/external/cashu/tor/tor.py create mode 100644 app/external/cashu/wallet/crud.py create mode 100644 app/external/cashu/wallet/models.py create mode 100644 app/external/cashu/wallet/wallet.py create mode 100644 app/external/cashu/wallet/wallet_helpers.py diff --git a/app/external/cashu/README.md b/app/external/cashu/README.md new file mode 100644 index 0000000..034e18a --- /dev/null +++ b/app/external/cashu/README.md @@ -0,0 +1,178 @@ +# cashu + +**Cashu is a Chaumian Ecash wallet and mint for Bitcoin Lightning.** + +Release Downloads Coverage + + +*Disclaimer: The author is NOT a cryptographer and this work has not been reviewed. This means that there is very likely a fatal flaw somewhere. Cashu is still experimental and not production-ready.* + +Cashu is an Ecash implementation based on David Wagner's variant of Chaumian blinding ([protocol specs](https://github.com/cashubtc/nuts)). 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 [here](https://gist.github.com/RubenSomsen/be7a4760dd4596d06963d67baf140406). The database mechanics and the Lightning backend uses parts from [LNbits](https://github.com/lnbits/lnbits-legend). + +

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

+ +### Feature overview + +- Full Bitcoin Lightning support +- Standalone CLI wallet and mint server +- Mint library includable into other Python projects +- PostgreSQL and SQLite database support +- Builtin Tor for hiding IPs for wallet and mint interactions +- Multimint wallet for tokens from different mints +- Send tokens to nostr pubkeys + +## Cashu client protocol +There are ongoing efforts to implement alternative Cashu clients that use the same protocol. If you are interested in helping with Cashu development, please refer to the protocol specs [protocol specs](https://github.com/cashubtc/nuts). + +## Easy Install + +The easiest way to use Cashu is to install the package it via pip: +```bash +pip install cashu +``` + +To update Cashu, use `pip install cashu -U`. + +If you have problems running the command above on Ubuntu, run `sudo apt install -y pip pkg-config` and `pip install wheel`. On macOS, you might have to run `pip install wheel` and `brew install pkg-config`. + +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](#poetry-install-cashu). + +#### Poetry: Prerequisites + +```bash +# on ubuntu: +sudo apt install -y build-essential pkg-config libffi-dev libpq-dev zlib1g-dev libssl-dev python3-dev libsqlite3-dev ncurses-dev libbz2-dev libreadline-dev lzma-dev + +# install python using pyenv +curl https://pyenv.run | bash + +# !! follow the instructions of pyenv init to setup pyenv !! +pyenv init + +# restart your shell (or source your .rc file), then install python: +pyenv install 3.9.13 + +# install poetry +curl -sSL https://install.python-poetry.org | python3 - +echo export PATH=\"$HOME/.local/bin:$PATH\" >> ~/.bashrc +source ~/.bashrc +``` +#### Poetry: Install Cashu +```bash +# install cashu +git clone https://github.com/callebtc/cashu.git --recurse-submodules +cd cashu +pyenv local 3.9.13 +poetry install +``` + +#### Poetry: Update Cashu +To update Cashu to the newest version enter +```bash +git pull && poetry install +``` +#### Poetry: Using Cashu + +Cashu should be now installed. To execute the following commands, activate your virtual Poetry environment via + +```bash +poetry shell +``` + +If you don't activate your environment, just prepend `poetry run` to all following commands. +## Configuration +```bash +mv .env.example .env +# edit .env file +vim .env +``` + +To use the wallet with the [public test mint](#test-instance), you need to change the appropriate entries in the `.env` file. + +#### Test instance +*Warning: this instance is just for demonstration only. The server could vanish at any moment so consider any Satoshis you deposit a donation.* + +Change the appropriate `.env` file settings to +```bash +MINT_HOST=8333.space +MINT_PORT=3338 +``` + +# Using Cashu +```bash +cashu info +``` + +Returns: +```bash +Version: 0.9.2 +Debug: False +Cashu dir: /home/user/.cashu +Wallet: wallet +Mint URL: https://8333.space:3338 +``` + +#### Check balance +```bash +cashu balance +``` + +#### Generate a Lightning invoice + +This command will return a Lightning invoice that you need to pay to mint new ecash tokens. + +```bash +cashu invoice 420 +``` + +The client will check every few seconds if the invoice has been paid. If you abort this step but still pay the invoice, you can use the command `cashu invoice --hash `. + +#### Pay a Lightning invoice +```bash +cashu pay lnbc120n1p3jfmdapp5r9jz... +``` + +#### Send tokens +To send tokens to another user, enter +```bash +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 +eyJwcm9vZnMiOiBbey... +``` + +You can now see that your available balance has dropped by the amount that you reserved for sending if you enter `cashu balance`: +```bash +Balance: 420 sat +``` + +#### Receive tokens +To receive tokens, another user enters: +```bash +cashu receive eyJwcm9vZnMiOiBbey... +``` +You should see the balance increase: +```bash +Balance: 0 sat +Balance: 69 sat +``` + + +# 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 +python -m cashu.mint +``` + +You can turn off Lightning support and mint as many tokens as you like by setting `LIGHTNING=FALSE` in the `.env` file. diff --git a/app/external/cashu/__init__.py b/app/external/cashu/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/external/cashu/alembic.ini b/app/external/cashu/alembic.ini new file mode 100644 index 0000000..c597c10 --- /dev/null +++ b/app/external/cashu/alembic.ini @@ -0,0 +1,106 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = sqlite:////home/f44/dev/blitz/api/add-cashu/cashu.db/data/test.sqlite +; sqlalchemy.url = sqlite+aiosqlite:///./data/test.sqlite + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/app/external/cashu/alembic/README b/app/external/cashu/alembic/README new file mode 100644 index 0000000..2500aa1 --- /dev/null +++ b/app/external/cashu/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. diff --git a/app/external/cashu/alembic/env.py b/app/external/cashu/alembic/env.py new file mode 100644 index 0000000..9dd6c6c --- /dev/null +++ b/app/external/cashu/alembic/env.py @@ -0,0 +1,74 @@ +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import engine_from_config, pool + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = None + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/app/external/cashu/alembic/script.py.mako b/app/external/cashu/alembic/script.py.mako new file mode 100644 index 0000000..55df286 --- /dev/null +++ b/app/external/cashu/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/app/external/cashu/alembic/versions/3fabf693571f_initial.py b/app/external/cashu/alembic/versions/3fabf693571f_initial.py new file mode 100644 index 0000000..7a639e3 --- /dev/null +++ b/app/external/cashu/alembic/versions/3fabf693571f_initial.py @@ -0,0 +1,121 @@ +"""initial + +Revision ID: 3fabf693571f +Revises: +Create Date: 2023-02-17 21:21:34.224256 + +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.engine.reflection import Inspector + +import app.external.cashu.wallet.models as wm + +# revision identifiers, used by Alembic. +revision = "3fabf693571f" +down_revision = None +branch_labels = None +depends_on = None + + +conn = op.get_bind() +inspector = Inspector.from_engine(conn) +tables = inspector.get_table_names() + + +def create_table(name, *columns, **kwargs): + if name not in tables: + op.create_table(name, *columns, **kwargs) + + +def upgrade() -> None: + # proofs table + create_table( + wm.proofs_table_name, + sa.Column("id", sa.String(), nullable=False), + sa.Column("amount", sa.Integer(), nullable=False), + sa.Column("C", sa.String(), nullable=False), + sa.Column("secret", sa.String(), nullable=False, unique=True), + sa.Column("reserved", sa.Boolean()), + sa.Column("send_id", sa.String()), + sa.Column("time_created", sa.DateTime()), + sa.Column("time_reserved", sa.DateTime()), + ) + + # proofs_used table + create_table( + wm.proofs_used_table_name, + sa.Column("id", sa.String(), nullable=False), + sa.Column("amount", sa.Integer(), nullable=False), + sa.Column("C", sa.String(), nullable=False), + sa.Column("secret", sa.String(), nullable=False, unique=True), + sa.Column("time_used", sa.TIMESTAMP), + ) + + op.execute( + """ + CREATE VIEW IF NOT EXISTS balance AS + SELECT COALESCE(SUM(s), 0) AS balance FROM ( + SELECT SUM(amount) AS s + FROM proofs + WHERE amount > 0 + ); + """ + ) + + op.execute( + """ + CREATE VIEW IF NOT EXISTS balance_used AS + SELECT COALESCE(SUM(s), 0) AS used FROM ( + SELECT SUM(amount) AS s + FROM proofs_used + WHERE amount > 0 + ); + """ + ) + + create_table( + wm.p2sh_table_name, + sa.Column("address", sa.String(), nullable=False, unique=True), + sa.Column("script", sa.String(), nullable=False, unique=True), + sa.Column("signature", sa.String(), nullable=False, unique=True), + sa.Column("used", sa.Boolean(), nullable=False), + ) + + create_table( + wm.keysets_table_name, + sa.Column("id", sa.String(), unique=True), + sa.Column("mint_url", sa.String(), unique=True), + sa.Column("valid_from", sa.DateTime(), default=sa.func.now()), + sa.Column("valid_to", sa.DateTime(), default=sa.func.now()), + sa.Column("first_seen", sa.DateTime(), default=sa.func.now()), + sa.Column("active", sa.Boolean(), nullable=False, default=True), + ) + + create_table( + wm.invoices_table_name, + sa.Column("amount", sa.Integer(), nullable=False), + sa.Column("pr", sa.String(), nullable=False), + sa.Column("hash", sa.String(), unique=True), + sa.Column("preimage", sa.String()), + sa.Column("paid", sa.Boolean(), default=False), + sa.Column("time_created", sa.DateTime(), default=sa.func.now()), + sa.Column("time_paid", sa.DateTime(), default=sa.func.now()), + ) + + create_table( + wm.nostr_table_name, + sa.Column("type", sa.String(), nullable=False), + sa.Column("last", sa.DateTime(), default=None), + ) + + op.execute("INSERT INTO nostr (type, last) VALUES ('dm', NULL)") + + +def downgrade() -> None: + op.drop_table(wm.proofs_table_name) + op.drop_table(wm.proofs_used_table_name) + op.drop_table(wm.p2sh_table_name) + op.drop_table(wm.keysets_table_name) + op.drop_table(wm.invoices_table_name) + op.drop_table(wm.nostr_table_name) diff --git a/app/external/cashu/core/__init__.py b/app/external/cashu/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/external/cashu/core/b_dhke.py b/app/external/cashu/core/b_dhke.py new file mode 100644 index 0000000..80735ef --- /dev/null +++ b/app/external/cashu/core/b_dhke.py @@ -0,0 +1,95 @@ +# Don't trust me with cryptography. + +""" +Implementation of https://gist.github.com/RubenSomsen/be7a4760dd4596d06963d67baf140406 + +Bob (Mint): +A = a*G +return A + +Alice (Client): +Y = hash_to_curve(secret_message) +r = random blinding factor +B'= Y + r*G +return B' + +Bob: +C' = a*B' + (= a*Y + a*r*G) +return C' + +Alice: +C = C' - r*A + (= C' - a*r*G) + (= a*Y) +return C, secret_message + +Bob: +Y = hash_to_curve(secret_message) +C == a*Y +If true, C must have originated from Bob +""" + +import hashlib + +from secp256k1 import PrivateKey, PublicKey + + +def hash_to_curve(message: bytes): + """Generates a point from the message hash and checks if the point lies on the curve. + If it does not, it tries computing a new point from the hash.""" + point = None + msg_to_hash = message + while point is None: + try: + _hash = hashlib.sha256(msg_to_hash).digest() + point = PublicKey(b"\x02" + _hash, raw=True) + except: + msg_to_hash = _hash + return point + + +def step1_alice(secret_msg: str, blinding_factor: bytes = None): + Y = hash_to_curve(secret_msg.encode("utf-8")) + if blinding_factor: + r = PrivateKey(privkey=blinding_factor, raw=True) + else: + r = PrivateKey() + B_ = Y + r.pubkey + return B_, r + + +def step2_bob(B_, a): + C_ = B_.mult(a) + return C_ + + +def step3_alice(C_, r, A): + C = C_ - A.mult(r) + return C + + +def verify(a, C, secret_msg): + Y = hash_to_curve(secret_msg.encode("utf-8")) + return C == Y.mult(a) + + +### Below is a test of a simple positive and negative case + +# # Alice's keys +# a = PrivateKey() +# A = a.pubkey +# secret_msg = "test" +# B_, r = step1_alice(secret_msg) +# C_ = step2_bob(B_, a) +# C = step3_alice(C_, r, A) +# print("C:{}, secret_msg:{}".format(C, secret_msg)) +# assert verify(a, C, secret_msg) +# assert verify(a, C + C, secret_msg) == False # adding C twice shouldn't pass +# assert verify(a, A, secret_msg) == False # A shouldn't pass + +# # Test operations +# b = PrivateKey() +# B = b.pubkey +# assert -A -A + A == -A # neg +# assert B.mult(a) == A.mult(b) # a*B = A*b diff --git a/app/external/cashu/core/base.py b/app/external/cashu/core/base.py new file mode 100644 index 0000000..b657b2c --- /dev/null +++ b/app/external/cashu/core/base.py @@ -0,0 +1,330 @@ +from typing import Dict, List, Optional, Union + +from pydantic import BaseModel + +from app.external.cashu.core.crypto import derive_keys, derive_keyset_id, derive_pubkeys +from app.external.cashu.core.secp import PrivateKey, PublicKey + +# ------- PROOFS ------- + + +class P2SHScript(BaseModel): + """ + Describes spending condition of a Proof + """ + + script: str + signature: str + address: Union[str, None] = None + + +class Proof(BaseModel): + """ + Value token + """ + + id: Union[ + None, str + ] = "" # NOTE: None for backwards compatibility for old clients that do not include the keyset id < 0.3 + amount: int = 0 + secret: str = "" # secret or message to be blinded and signed + C: str = "" # signature on secret, unblinded by wallet + script: Union[P2SHScript, None] = None # P2SH spending condition + reserved: Union[ + None, bool + ] = False # whether this proof is reserved for sending, used for coin management in the wallet + send_id: Union[ + None, str + ] = "" # unique ID of send attempt, used for grouping pending tokens in the wallet + time_created: Union[None, str] = "" + time_reserved: Union[None, str] = "" + + def to_dict(self): + # dictionary without the fields that don't need to be send to Carol + return dict(id=self.id, amount=self.amount, secret=self.secret, C=self.C) + + def to_dict_no_secret(self): + # dictionary but without the secret itself + return dict(id=self.id, amount=self.amount, C=self.C) + + def __getitem__(self, key): + return self.__getattribute__(key) + + def __setitem__(self, key, val): + self.__setattr__(key, val) + + +class Proofs(BaseModel): + # NOTE: not used in Pydantic validation + __root__: List[Proof] + + +class BlindedMessage(BaseModel): + """ + Blinded message or blinded secret or "output" which is to be signed by the mint + """ + + amount: int + B_: str # Hex-encoded blinded message + + +class BlindedSignature(BaseModel): + """ + Blinded signature or "promise" which is the signature on a `BlindedMessage` + """ + + id: Union[str, None] = None + amount: int + C_: str # Hex-encoded signature + + +class BlindedMessages(BaseModel): + # NOTE: not used in Pydantic validation + __root__: List[BlindedMessage] = [] + + +# ------- LIGHTNING INVOICE ------- + + +class Invoice(BaseModel): + amount: int + pr: str + hash: Union[None, str] = None + preimage: Union[str, None] = None + issued: Union[None, bool] = False + paid: Union[None, bool] = False + time_created: Union[None, str, int, float] = "" + time_paid: Union[None, str, int, float] = "" + + +# ------- API ------- + + +# ------- API: KEYS ------- + + +class KeysResponse(BaseModel): + __root__: Dict[str, str] + + +class KeysetsResponse(BaseModel): + keysets: list[str] + + +# ------- API: MINT ------- + + +class PostMintRequest(BaseModel): + outputs: List[BlindedMessage] + + +class PostMintResponseLegacy(BaseModel): + # NOTE: Backwards compatibility for < 0.8.0 where we used a simple list and not a key-value dictionary + __root__: List[BlindedSignature] = [] + + +class PostMintResponse(BaseModel): + promises: List[BlindedSignature] = [] + + +class GetMintResponse(BaseModel): + pr: str + hash: str + + +# ------- API: MELT ------- + + +class PostMeltRequest(BaseModel): + proofs: List[Proof] + pr: str + + +class GetMeltResponse(BaseModel): + paid: Union[bool, None] + preimage: Union[str, None] + + +# ------- API: SPLIT ------- + + +class PostSplitRequest(BaseModel): + proofs: List[Proof] + amount: int + outputs: List[BlindedMessage] + + +class PostSplitResponse(BaseModel): + fst: List[BlindedSignature] + snd: List[BlindedSignature] + + +# ------- API: CHECK ------- + + +class CheckSpendableRequest(BaseModel): + proofs: List[Proof] + + +class CheckSpendableResponse(BaseModel): + spendable: List[bool] + + +class CheckFeesRequest(BaseModel): + pr: str + + +class CheckFeesResponse(BaseModel): + fee: Union[int, None] + + +# ------- KEYSETS ------- + + +class KeyBase(BaseModel): + """ + Public key from a keyset id for a given amount. + """ + + id: str + amount: int + pubkey: str + + +class WalletKeyset: + """ + Contains the keyset from the wallets's perspective. + """ + + id: Union[str, None] + public_keys: Union[Dict[int, PublicKey], None] + mint_url: Union[str, None] = None + valid_from: Union[str, None] = None + valid_to: Union[str, None] = None + first_seen: Union[str, None] = None + active: Union[bool, None] = True + + def __init__( + self, + public_keys=None, + mint_url=None, + id=None, + valid_from=None, + valid_to=None, + first_seen=None, + active=None, + ): + self.id = id + self.valid_from = valid_from + self.valid_to = valid_to + self.first_seen = first_seen + self.active = active + self.mint_url = mint_url + if public_keys: + self.public_keys = public_keys + self.id = derive_keyset_id(self.public_keys) + + +class MintKeyset: + """ + Contains the keyset from the mint's perspective. + """ + + id: Union[str, None] + derivation_path: str + private_keys: Dict[int, PrivateKey] + public_keys: Union[Dict[int, PublicKey], None] = None + valid_from: Union[str, None] = None + valid_to: Union[str, None] = None + first_seen: Union[str, None] = None + active: Union[bool, None] = True + version: Union[str, None] = None + + def __init__( + self, + id=None, + valid_from=None, + valid_to=None, + first_seen=None, + active=None, + seed: str = "", + derivation_path: str = "", + version: str = "", + ): + self.derivation_path = derivation_path + self.id = id + self.valid_from = valid_from + self.valid_to = valid_to + self.first_seen = first_seen + self.active = active + self.version = version + # generate keys from seed + if seed: + self.generate_keys(seed) + + def generate_keys(self, seed): + """Generates keys of a keyset from a seed.""" + self.private_keys = derive_keys(seed, self.derivation_path) + self.public_keys = derive_pubkeys(self.private_keys) # type: ignore + self.id = derive_keyset_id(self.public_keys) # type: ignore + + def get_keybase(self): + assert self.id is not None + return { + k: KeyBase(id=self.id, amount=k, pubkey=v.serialize().hex()) + for k, v in self.public_keys.items() # type: ignore + } + + +class MintKeysets: + """ + Collection of keyset IDs and the corresponding keyset of the mint. + """ + + keysets: Dict[str, MintKeyset] + + def __init__(self, keysets: List[MintKeyset]): + self.keysets = {k.id: k for k in keysets} # type: ignore + + def get_ids(self): + return [k for k, _ in self.keysets.items()] + + +# ------- TOKEN ------- + + +class TokenV1(BaseModel): + """ + A (legacy) Cashu token that includes proofs. This can only be received if the receiver knows the mint associated with the + keyset ids of the proofs. + """ + + # NOTE: not used in Pydantic validation + __root__: List[Proof] + + +class TokenV2Mint(BaseModel): + """ + Object that describes how to reach the mints associated with the proofs in a TokenV2 object. + """ + + url: str # mint URL + ids: List[str] # List of keyset id's that are from this mint + + +class TokenV2(BaseModel): + """ + A Cashu token that includes proofs and their respective mints. Can include proofs from multiple different mints and keysets. + """ + + proofs: List[Proof] + mints: Optional[List[TokenV2Mint]] = None + + def to_dict(self): + if self.mints: + return dict( + proofs=[p.to_dict() for p in self.proofs], + mints=[m.dict() for m in self.mints], + ) + else: + return dict(proofs=[p.to_dict() for p in self.proofs]) diff --git a/app/external/cashu/core/bolt11.py b/app/external/cashu/core/bolt11.py new file mode 100644 index 0000000..890ea7e --- /dev/null +++ b/app/external/cashu/core/bolt11.py @@ -0,0 +1,370 @@ +import hashlib +import re +import time +from binascii import hexlify, unhexlify +from decimal import Decimal +from typing import List, NamedTuple, Optional + +import bitstring # type: ignore +import secp256k1 +from bech32 import CHARSET, bech32_decode, bech32_encode +from ecdsa import SECP256k1, VerifyingKey # type: ignore +from ecdsa.util import sigdecode_string # type: ignore + + +class Route(NamedTuple): + pubkey: str + short_channel_id: str + base_fee_msat: int + ppm_fee: int + cltv: int + + +class Invoice(object): + payment_hash: str + amount_msat: int = 0 + description: Optional[str] = None + description_hash: Optional[str] = None + payee: Optional[str] = None + date: int + expiry: int = 3600 + secret: Optional[str] = None + route_hints: List[Route] = [] + min_final_cltv_expiry: int = 18 + + +def decode(pr: str) -> Invoice: + """bolt11 decoder, + based on https://github.com/rustyrussell/lightning-payencode/blob/master/lnaddr.py + """ + + hrp, decoded_data = bech32_decode(pr) + if hrp is None or decoded_data is None: + raise ValueError("Bad bech32 checksum") + if not hrp.startswith("ln"): + raise ValueError("Does not start with ln") + + bitarray = _u5_to_bitarray(decoded_data) + + # final signature 65 bytes, split it off. + if len(bitarray) < 65 * 8: + raise ValueError("Too short to contain signature") + + # extract the signature + signature = bitarray[-65 * 8 :].tobytes() + + # the tagged fields as a bitstream + data = bitstring.ConstBitStream(bitarray[: -65 * 8]) + + # build the invoice object + invoice = Invoice() + + # decode the amount from the hrp + m = re.search(r"[^\d]+", hrp[2:]) + if m: + amountstr = hrp[2 + m.end() :] + if amountstr != "": + invoice.amount_msat = _unshorten_amount(amountstr) + + # pull out date + invoice.date = data.read(35).uint + + while data.pos != data.len: + tag, tagdata, data = _pull_tagged(data) + data_length = len(tagdata) / 5 + + if tag == "d": + invoice.description = _trim_to_bytes(tagdata).decode("utf-8") + elif tag == "h" and data_length == 52: + invoice.description_hash = _trim_to_bytes(tagdata).hex() + elif tag == "p" and data_length == 52: + invoice.payment_hash = _trim_to_bytes(tagdata).hex() + elif tag == "x": + invoice.expiry = tagdata.uint + elif tag == "n": + invoice.payee = _trim_to_bytes(tagdata).hex() + # this won't work in most cases, we must extract the payee + # from the signature + elif tag == "s": + invoice.secret = _trim_to_bytes(tagdata).hex() + elif tag == "r": + s = bitstring.ConstBitStream(tagdata) + while s.pos + 264 + 64 + 32 + 32 + 16 < s.len: + route = Route( + pubkey=s.read(264).tobytes().hex(), + short_channel_id=_readable_scid(s.read(64).intbe), + base_fee_msat=s.read(32).intbe, + ppm_fee=s.read(32).intbe, + cltv=s.read(16).intbe, + ) + invoice.route_hints.append(route) + + # BOLT #11: + # A reader MUST check that the `signature` is valid (see the `n` tagged + # field specified below). + # A reader MUST use the `n` field to validate the signature instead of + # performing signature recovery if a valid `n` field is provided. + message = bytearray([ord(c) for c in hrp]) + data.tobytes() + sig = signature[0:64] + if invoice.payee: + key = VerifyingKey.from_string(unhexlify(invoice.payee), curve=SECP256k1) + key.verify(sig, message, hashlib.sha256, sigdecode=sigdecode_string) + else: + keys = VerifyingKey.from_public_key_recovery( + sig, message, SECP256k1, hashlib.sha256 + ) + signaling_byte = signature[64] + key = keys[int(signaling_byte)] + invoice.payee = key.to_string("compressed").hex() + + return invoice + + +def encode(options): + """Convert options into LnAddr and pass it to the encoder""" + addr = LnAddr() + addr.currency = options["currency"] + addr.fallback = options["fallback"] if options["fallback"] else None + if options["amount"]: + addr.amount = options["amount"] + if options["timestamp"]: + addr.date = int(options["timestamp"]) + + addr.paymenthash = unhexlify(options["paymenthash"]) + + if options["description"]: + addr.tags.append(("d", options["description"])) + if options["description_hash"]: + addr.tags.append(("h", options["description_hash"])) + if options["expires"]: + addr.tags.append(("x", options["expires"])) + + if options["fallback"]: + addr.tags.append(("f", options["fallback"])) + if options["route"]: + for r in options["route"]: + splits = r.split("/") + route = [] + while len(splits) >= 5: + route.append( + ( + unhexlify(splits[0]), + unhexlify(splits[1]), + int(splits[2]), + int(splits[3]), + int(splits[4]), + ) + ) + splits = splits[5:] + assert len(splits) == 0 + addr.tags.append(("r", route)) + return lnencode(addr, options["privkey"]) + + +def lnencode(addr, privkey): + if addr.amount: + amount = Decimal(str(addr.amount)) + # We can only send down to millisatoshi. + if amount * 10**12 % 10: + raise ValueError( + "Cannot encode {}: too many decimal places".format(addr.amount) + ) + + amount = addr.currency + shorten_amount(amount) + else: + amount = addr.currency if addr.currency else "" + + hrp = "ln" + amount + "0n" + + # Start with the timestamp + data = bitstring.pack("uint:35", addr.date) + + # Payment hash + data += tagged_bytes("p", addr.paymenthash) + tags_set = set() + + for k, v in addr.tags: + + # BOLT #11: + # + # A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields, + if k in ("d", "h", "n", "x"): + if k in tags_set: + raise ValueError("Duplicate '{}' tag".format(k)) + + if k == "r": + route = bitstring.BitArray() + for step in v: + pubkey, channel, feebase, feerate, cltv = step + route.append( + bitstring.BitArray(pubkey) + + bitstring.BitArray(channel) + + bitstring.pack("intbe:32", feebase) + + bitstring.pack("intbe:32", feerate) + + bitstring.pack("intbe:16", cltv) + ) + data += tagged("r", route) + elif k == "f": + data += encode_fallback(v, addr.currency) + elif k == "d": + data += tagged_bytes("d", v.encode()) + elif k == "x": + # Get minimal length by trimming leading 5 bits at a time. + expirybits = bitstring.pack("intbe:64", v)[4:64] + while expirybits.startswith("0b00000"): + expirybits = expirybits[5:] + data += tagged("x", expirybits) + elif k == "h": + data += tagged_bytes("h", v) + elif k == "n": + data += tagged_bytes("n", v) + else: + # FIXME: Support unknown tags? + raise ValueError("Unknown tag {}".format(k)) + + tags_set.add(k) + + # BOLT #11: + # + # A writer MUST include either a `d` or `h` field, and MUST NOT include + # both. + if "d" in tags_set and "h" in tags_set: + raise ValueError("Cannot include both 'd' and 'h'") + if not "d" in tags_set and not "h" in tags_set: + raise ValueError("Must include either 'd' or 'h'") + + # We actually sign the hrp, then data (padded to 8 bits with zeroes). + privkey = secp256k1.PrivateKey(bytes(unhexlify(privkey))) + sig = privkey.ecdsa_sign_recoverable( + bytearray([ord(c) for c in hrp]) + data.tobytes() + ) + # This doesn't actually serialize, but returns a pair of values :( + sig, recid = privkey.ecdsa_recoverable_serialize(sig) + data += bytes(sig) + bytes([recid]) + + return bech32_encode(hrp, bitarray_to_u5(data)) + + +class LnAddr(object): + def __init__( + self, paymenthash=None, amount=None, currency="bc", tags=None, date=None + ): + self.date = int(time.time()) if not date else int(date) + self.tags = [] if not tags else tags + self.unknown_tags = [] + self.paymenthash = paymenthash + self.signature = None + self.pubkey = None + self.currency = currency + self.amount = amount + + def __str__(self): + return "LnAddr[{}, amount={}{} tags=[{}]]".format( + hexlify(self.pubkey.serialize()).decode("utf-8"), + self.amount, + self.currency, + ", ".join([k + "=" + str(v) for k, v in self.tags]), + ) + + +def shorten_amount(amount): + """Given an amount in bitcoin, shorten it""" + # Convert to pico initially + amount = int(amount * 10**12) + units = ["p", "n", "u", "m", ""] + for unit in units: + if amount % 1000 == 0: + amount //= 1000 + else: + break + return str(amount) + unit + + +def _unshorten_amount(amount: str) -> int: + """Given a shortened amount, return millisatoshis""" + # BOLT #11: + # The following `multiplier` letters are defined: + # + # * `m` (milli): multiply by 0.001 + # * `u` (micro): multiply by 0.000001 + # * `n` (nano): multiply by 0.000000001 + # * `p` (pico): multiply by 0.000000000001 + units = {"p": 10**12, "n": 10**9, "u": 10**6, "m": 10**3} + unit = str(amount)[-1] + + # BOLT #11: + # A reader SHOULD fail if `amount` contains a non-digit, or is followed by + # anything except a `multiplier` in the table above. + if not re.fullmatch(r"\d+[pnum]?", str(amount)): + raise ValueError("Invalid amount '{}'".format(amount)) + + if unit in units: + return int(int(amount[:-1]) * 100_000_000_000 / units[unit]) + else: + return int(amount) * 100_000_000_000 + + +def _pull_tagged(stream): + tag = stream.read(5).uint + length = stream.read(5).uint * 32 + stream.read(5).uint + return (CHARSET[tag], stream.read(length * 5), stream) + + +def is_p2pkh(currency, prefix): + return prefix == base58_prefix_map[currency][0] + + +def is_p2sh(currency, prefix): + return prefix == base58_prefix_map[currency][1] + + +# Tagged field containing BitArray +def tagged(char, l): + # Tagged fields need to be zero-padded to 5 bits. + while l.len % 5 != 0: + l.append("0b0") + return ( + bitstring.pack( + "uint:5, uint:5, uint:5", + CHARSET.find(char), + (l.len / 5) / 32, + (l.len / 5) % 32, + ) + + l + ) + + +def tagged_bytes(char, l): + return tagged(char, bitstring.BitArray(l)) + + +def _trim_to_bytes(barr): + # Adds a byte if necessary. + b = barr.tobytes() + if barr.len % 8 != 0: + return b[:-1] + return b + + +def _readable_scid(short_channel_id: int) -> str: + return "{blockheight}x{transactionindex}x{outputindex}".format( + blockheight=((short_channel_id >> 40) & 0xFFFFFF), + transactionindex=((short_channel_id >> 16) & 0xFFFFFF), + outputindex=(short_channel_id & 0xFFFF), + ) + + +def _u5_to_bitarray(arr: List[int]) -> bitstring.BitArray: + ret = bitstring.BitArray() + for a in arr: + ret += bitstring.pack("uint:5", a) + return ret + + +def bitarray_to_u5(barr): + assert barr.len % 5 == 0 + ret = [] + s = bitstring.ConstBitStream(barr) + while s.pos != s.len: + ret.append(s.read(5).uint) + return ret diff --git a/app/external/cashu/core/crypto.py b/app/external/cashu/core/crypto.py new file mode 100644 index 0000000..1d9c24e --- /dev/null +++ b/app/external/cashu/core/crypto.py @@ -0,0 +1,45 @@ +import base64 +import hashlib +from typing import Dict + +from app.external.cashu.core.secp import PrivateKey, PublicKey +from app.external.cashu.core.settings import MAX_ORDER + +# entropy = bytes([random.getrandbits(8) for i in range(16)]) +# mnemonic = bip39.mnemonic_from_bytes(entropy) +# seed = bip39.mnemonic_to_seed(mnemonic) +# root = bip32.HDKey.from_seed(seed, version=NETWORKS["main"]["xprv"]) + +# bip44_xprv = root.derive("m/44h/1h/0h") +# bip44_xpub = bip44_xprv.to_public() + + +def derive_keys(master_key: str, derivation_path: str = ""): + """ + Deterministic derivation of keys for 2^n values. + TODO: Implement BIP32. + """ + return { + 2 + ** i: PrivateKey( + hashlib.sha256((str(master_key) + derivation_path + str(i)).encode("utf-8")) + .hexdigest() + .encode("utf-8")[:32], + raw=True, + ) + for i in range(MAX_ORDER) + } + + +def derive_pubkeys(keys: Dict[int, PrivateKey]): + return {amt: keys[amt].pubkey for amt in [2**i for i in range(MAX_ORDER)]} + + +def derive_keyset_id(keys: Dict[int, PublicKey]): + """Deterministic derivation keyset_id from set of public keys.""" + # sort public keys by amount + sorted_keys = dict(sorted(keys.items())) + pubkeys_concat = "".join([p.serialize().hex() for _, p in sorted_keys.items()]) + return base64.b64encode( + hashlib.sha256((pubkeys_concat).encode("utf-8")).digest() + ).decode()[:12] diff --git a/app/external/cashu/core/db.py b/app/external/cashu/core/db.py new file mode 100644 index 0000000..fa8d2c5 --- /dev/null +++ b/app/external/cashu/core/db.py @@ -0,0 +1,75 @@ +from enum import Enum +from typing import AsyncGenerator + +import sqlalchemy +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base +from sqlalchemy.orm import sessionmaker + +from app.external.cashu.migrations import run_migrations + +Base: DeclarativeMeta = declarative_base() + + +class DatabaseType(Enum): + SQLITE = 1 + UNSUPPORTED = 2 + + +def _get_url(name: str, db_type: DatabaseType = DatabaseType.SQLITE) -> str: + if db_type == DatabaseType.SQLITE: + return f"sqlite+aiosqlite:///./cashu.db/{name}/wallet.sqlite3" + + raise NotImplementedError("Only SQlite is supported for now.") + + +class Database: + initialized = False + + def __init__(self, name: str) -> None: + self.name = name + + # only SQlite for now + self.db_type = DatabaseType.SQLITE + self.db_url = _get_url(name) + + async def initialize(self) -> None: + if self.initialized: + raise RuntimeError(f"Database {self.db_url} already initialized.") + + run_migrations(self.db_url) + + # connect_args={"check_same_thread": False} is only necessary for SQlite + self.engine = create_async_engine( + self.db_url, connect_args={"check_same_thread": False}, echo=True + ) + self.async_session_builder = sessionmaker( + self.engine, class_=AsyncSession, expire_on_commit=False + ) + + self._literal_processor = sqlalchemy.String("").literal_processor( + dialect=self.engine.dialect + ) + + self.initialized = True + + @property + def async_session(self) -> AsyncSession: + if not self.initialized: + raise RuntimeError("Database not initialized.") + + return self.async_session_builder() + + @property + def l_proc(self): + if not self.initialized: + raise RuntimeError("Database not initialized.") + + return self._literal_processor + + async def get_async_session(self) -> AsyncGenerator[AsyncSession, None]: + if not self.initialized: + raise RuntimeError("Database not initialized.") + + async with self.async_session_builder() as session: + yield session diff --git a/app/external/cashu/core/errors.py b/app/external/cashu/core/errors.py new file mode 100644 index 0000000..ec30e0d --- /dev/null +++ b/app/external/cashu/core/errors.py @@ -0,0 +1,30 @@ +from pydantic import BaseModel + + +class CashuError(BaseModel): + code: int + error: str + + +class MintException(CashuError): + code = 100 + error = "Mint" + + +class LightningException(MintException): + code = 200 + error = "Lightning" + + +class InvoiceNotPaidException(LightningException): + code = 201 + error = "invoice not paid." + + +class DatabaseException(CashuError): + code = 500 + error = "database error." + message: str + + def __init__(self, message: str = None): + self.message = message diff --git a/app/external/cashu/core/helpers.py b/app/external/cashu/core/helpers.py new file mode 100644 index 0000000..2bd3627 --- /dev/null +++ b/app/external/cashu/core/helpers.py @@ -0,0 +1,46 @@ +import asyncio +from functools import partial, wraps +from typing import List + +from app.external.cashu.core.base import Proof +from app.external.cashu.core.settings import ( + LIGHTNING_FEE_PERCENT, + LIGHTNING_RESERVE_FEE_MIN, +) + + +def sum_proofs(proofs: List[Proof]): + return sum([p.amount for p in proofs]) + + +def async_wrap(func): + @wraps(func) + async def run(*args, loop=None, executor=None, **kwargs): + if loop is None: + loop = asyncio.get_event_loop() + partial_func = partial(func, *args, **kwargs) + return await loop.run_in_executor(executor, partial_func) + + return run + + +def async_unwrap(to_await): + async_response = [] + + async def run_and_capture_result(): + r = await to_await + async_response.append(r) + + loop = asyncio.get_event_loop() + coroutine = run_and_capture_result() + loop.run_until_complete(coroutine) + return async_response[0] + + +def fee_reserve(amount_msat: int, internal=False) -> int: + """Function for calculating the Lightning fee reserve""" + if internal: + return 0 + return max( + int(LIGHTNING_RESERVE_FEE_MIN), int(amount_msat * LIGHTNING_FEE_PERCENT / 100.0) + ) diff --git a/app/external/cashu/core/legacy.py b/app/external/cashu/core/legacy.py new file mode 100644 index 0000000..7434bdf --- /dev/null +++ b/app/external/cashu/core/legacy.py @@ -0,0 +1,31 @@ +import hashlib + +from secp256k1 import PublicKey + + +def hash_to_point_pre_0_3_3(secret_msg): + """ + NOTE: Clients pre 0.3.3 used a different hash_to_curve + + Generates x coordinate from the message hash and checks if the point lies on the curve. + If it does not, it tries computing again a new x coordinate from the hash of the coordinate. + """ + point = None + msg = secret_msg + while point is None: + _hash = hashlib.sha256(msg).hexdigest().encode("utf-8") + try: + # We construct compressed pub which has x coordinate encoded with even y + _hash = list(_hash[:33]) # take the 33 bytes and get a list of bytes + _hash[0] = 0x02 # set first byte to represent even y coord + _hash = bytes(_hash) + point = PublicKey(_hash, raw=True) + except: + msg = _hash + + return point + + +def verify_pre_0_3_3(a, C, secret_msg): + Y = hash_to_point_pre_0_3_3(secret_msg.encode("utf-8")) + return C == Y.mult(a) diff --git a/app/external/cashu/core/script.py b/app/external/cashu/core/script.py new file mode 100644 index 0000000..4c035c8 --- /dev/null +++ b/app/external/cashu/core/script.py @@ -0,0 +1,104 @@ +import base64 +import hashlib +import random + +COIN = 100_000_000 +TXID = "bff785da9f8169f49be92fa95e31f0890c385bfb1bd24d6b94d7900057c617ae" +SEED = b"__not__used" + +from bitcoin.core import CMutableTxIn, CMutableTxOut, COutPoint, CTransaction, lx +from bitcoin.core.script import * +from bitcoin.core.script import CScript +from bitcoin.core.scripteval import ( + SCRIPT_VERIFY_P2SH, + EvalScriptError, + VerifyScript, + VerifyScriptError, +) +from bitcoin.wallet import CBitcoinSecret, P2SHBitcoinAddress + + +def step0_carol_privkey(): + """Private key""" + # h = hashlib.sha256(SEED).digest() + h = hashlib.sha256(str(random.getrandbits(256)).encode()).digest() + seckey = CBitcoinSecret.from_secret_bytes(h) + return seckey + + +def step0_carol_checksig_redeemscrip(carol_pubkey): + """Create script""" + txin_redeemScript = CScript([carol_pubkey, OP_CHECKSIG]) + # txin_redeemScript = CScript([-123, OP_CHECKLOCKTIMEVERIFY]) + # txin_redeemScript = CScript([3, 3, OP_LESSTHAN, OP_VERIFY]) + return txin_redeemScript + + +def step1_carol_create_p2sh_address(txin_redeemScript): + """Create address (serialized scriptPubKey) to share with Alice""" + txin_p2sh_address = P2SHBitcoinAddress.from_redeemScript(txin_redeemScript) + return txin_p2sh_address + + +def step1_bob_carol_create_tx(txin_p2sh_address): + """Create transaction""" + txid = lx(TXID) + vout = 0 + txin = CMutableTxIn(COutPoint(txid, vout)) + txout = CMutableTxOut( + int(0.0005 * COIN), + P2SHBitcoinAddress(str(txin_p2sh_address)).to_scriptPubKey(), + ) + tx = CTransaction([txin], [txout]) + return tx, txin + + +def step2_carol_sign_tx(txin_redeemScript, privatekey): + """Sign transaction with private key""" + txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript) + tx, txin = step1_bob_carol_create_tx(txin_p2sh_address) + sighash = SignatureHash(txin_redeemScript, tx, 0, SIGHASH_ALL) + sig = privatekey.sign(sighash) + bytes([SIGHASH_ALL]) + txin.scriptSig = CScript([sig, txin_redeemScript]) + return txin + + +def step3_bob_verify_script(txin_signature, txin_redeemScript, tx): + txin_scriptPubKey = txin_redeemScript.to_p2sh_scriptPubKey() + try: + VerifyScript( + txin_signature, txin_scriptPubKey, tx, 0, flags=[SCRIPT_VERIFY_P2SH] + ) + return True + except VerifyScriptError as e: + raise Exception("Script verification failed:", e) + except EvalScriptError as e: + print(f"Script: {txin_scriptPubKey.__repr__()}") + raise Exception("Script evaluation failed:", e) + except Exception as e: + raise Exception("Script execution failed:", e) + + +def verify_script(txin_redeemScript_b64, txin_signature_b64): + txin_redeemScript = CScript(base64.urlsafe_b64decode(txin_redeemScript_b64)) + print("Redeem script:", txin_redeemScript.__repr__()) + # txin_redeemScript = CScript([2, 3, OP_LESSTHAN, OP_VERIFY]) + txin_signature = CScript(value=base64.urlsafe_b64decode(txin_signature_b64)) + + txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript) + print(f"Bob recreates secret: P2SH:{txin_p2sh_address}") + # MINT checks that P2SH:txin_p2sh_address has not been spent yet + # ... + tx, _ = step1_bob_carol_create_tx(txin_p2sh_address) + + print( + f"Bob verifies:\nscript: {txin_redeemScript_b64}\nsignature: {txin_signature_b64}\n" + ) + script_valid = step3_bob_verify_script(txin_signature, txin_redeemScript, tx) + # MINT redeems tokens and stores P2SH:txin_p2sh_address + # ... + if script_valid: + print("Successful.") + else: + print("Error.") + return txin_p2sh_address, script_valid diff --git a/app/external/cashu/core/secp.py b/app/external/cashu/core/secp.py new file mode 100644 index 0000000..3341643 --- /dev/null +++ b/app/external/cashu/core/secp.py @@ -0,0 +1,52 @@ +from secp256k1 import PrivateKey, PublicKey + + +# We extend the public key to define some operations on points +# Picked from https://github.com/WTRMQDev/secp256k1-zkp-py/blob/master/secp256k1_zkp/__init__.py +class PublicKeyExt(PublicKey): + def __add__(self, pubkey2): + if isinstance(pubkey2, PublicKey): + new_pub = PublicKey() + new_pub.combine([self.public_key, pubkey2.public_key]) + return new_pub + else: + raise TypeError("Cant add pubkey and %s" % pubkey2.__class__) + + def __neg__(self): + serialized = self.serialize() + first_byte, remainder = serialized[:1], serialized[1:] + # flip odd/even byte + first_byte = {b"\x03": b"\x02", b"\x02": b"\x03"}[first_byte] + return PublicKey(first_byte + remainder, raw=True) + + def __sub__(self, pubkey2): + if isinstance(pubkey2, PublicKey): + return self + (-pubkey2) + else: + raise TypeError("Can't add pubkey and %s" % pubkey2.__class__) + + def mult(self, privkey): + if isinstance(privkey, PrivateKey): + return self.tweak_mul(privkey.private_key) + else: + raise TypeError("Can't multiply with non privatekey") + + def __eq__(self, pubkey2): + if isinstance(pubkey2, PublicKey): + seq1 = self.to_data() + seq2 = pubkey2.to_data() + return seq1 == seq2 + else: + raise TypeError("Can't compare pubkey and %s" % pubkey2.__class__) + + def to_data(self): + return [self.public_key.data[i] for i in range(64)] + + +# Horrible monkeypatching +PublicKey.__add__ = PublicKeyExt.__add__ +PublicKey.__neg__ = PublicKeyExt.__neg__ +PublicKey.__sub__ = PublicKeyExt.__sub__ +PublicKey.mult = PublicKeyExt.mult +PublicKey.__eq__ = PublicKeyExt.__eq__ +PublicKey.to_data = PublicKeyExt.to_data diff --git a/app/external/cashu/core/settings.py b/app/external/cashu/core/settings.py new file mode 100644 index 0000000..15a859a --- /dev/null +++ b/app/external/cashu/core/settings.py @@ -0,0 +1,68 @@ +import os +import sys +from pathlib import Path + +from environs import Env # type: ignore + +env = Env() + +# env file: default to current dir, else home dir +ENV_FILE = os.path.join(os.getcwd(), ".cashu") +if not os.path.isfile(ENV_FILE): + ENV_FILE = os.path.join(str(Path.home()), ".cashu", ".env") +if os.path.isfile(ENV_FILE): + env.read_env(ENV_FILE) +else: + ENV_FILE = "" + env.read_env(recurse=False) + +DEBUG = env.bool("DEBUG", default=False) +if not DEBUG: + sys.tracebacklimit = 0 + +CASHU_DIR = env.str("CASHU_DIR", default=os.path.join(str(Path.home()), ".cashu")) +CASHU_DIR = CASHU_DIR.replace("~", str(Path.home())) +assert len(CASHU_DIR), "CASHU_DIR not defined" + +TOR = env.bool("TOR", default=True) + +SOCKS_HOST = env.str("SOCKS_HOST", default=None) +SOCKS_PORT = env.int("SOCKS_PORT", default=9050) + +LIGHTNING = env.bool("LIGHTNING", default=True) +LIGHTNING_FEE_PERCENT = env.float("LIGHTNING_FEE_PERCENT", default=1.0) +assert LIGHTNING_FEE_PERCENT >= 0, "LIGHTNING_FEE_PERCENT must be at least 0" +LIGHTNING_RESERVE_FEE_MIN = env.float("LIGHTNING_RESERVE_FEE_MIN", default=2000) + +MINT_PRIVATE_KEY = env.str("MINT_PRIVATE_KEY", default=None) + +MINT_SERVER_HOST = env.str("MINT_SERVER_HOST", default="127.0.0.1") +MINT_SERVER_PORT = env.int("MINT_SERVER_PORT", default=3338) + +MINT_URL = env.str("MINT_URL", default=None) +MINT_HOST = env.str("MINT_HOST", default="8333.space") +MINT_PORT = env.int("MINT_PORT", default=3338) + +if not MINT_URL: + if MINT_HOST in ["localhost", "127.0.0.1"]: + MINT_URL = f"http://{MINT_HOST}:{MINT_PORT}" + else: + MINT_URL = f"https://{MINT_HOST}:{MINT_PORT}" + +LNBITS_ENDPOINT = env.str("LNBITS_ENDPOINT", default=None) +LNBITS_KEY = env.str("LNBITS_KEY", default=None) + +NOSTR_PRIVATE_KEY = env.str("NOSTR_PRIVATE_KEY", default=None) +NOSTR_RELAYS = env.list( + "NOSTR_RELAYS", + default=[ + "wss://nostr-pub.wellorder.net", + "wss://relay.damus.io", + "wss://nostr.zebedee.cloud", + "wss://relay.snort.social", + "wss://nostr.fmt.wiz.biz", + ], +) + +MAX_ORDER = 64 +VERSION = "0.9.3" diff --git a/app/external/cashu/core/split.py b/app/external/cashu/core/split.py new file mode 100644 index 0000000..44b9cf5 --- /dev/null +++ b/app/external/cashu/core/split.py @@ -0,0 +1,8 @@ +def amount_split(amount): + """Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8].""" + bits_amt = bin(amount)[::-1][:-2] + rv = [] + for (pos, bit) in enumerate(bits_amt): + if bit == "1": + rv.append(2**pos) + return rv diff --git a/app/external/cashu/lightning/__init__.py b/app/external/cashu/lightning/__init__.py new file mode 100644 index 0000000..54af20c --- /dev/null +++ b/app/external/cashu/lightning/__init__.py @@ -0,0 +1,3 @@ +# from cashu.lightning.lnbits import LNbitsWallet + +# WALLET = LNbitsWallet() diff --git a/app/external/cashu/lightning/base.py b/app/external/cashu/lightning/base.py new file mode 100644 index 0000000..adde18b --- /dev/null +++ b/app/external/cashu/lightning/base.py @@ -0,0 +1,88 @@ +from abc import ABC, abstractmethod +from typing import AsyncGenerator, Coroutine, NamedTuple, Optional + + +class StatusResponse(NamedTuple): + error_message: Optional[str] + balance_msat: int + + +class InvoiceResponse(NamedTuple): + ok: bool + checking_id: Optional[str] = None # payment_hash, rpc_id + payment_request: Optional[str] = None + error_message: Optional[str] = None + + +class PaymentResponse(NamedTuple): + # when ok is None it means we don't know if this succeeded + ok: Optional[bool] = None + checking_id: Optional[str] = None # payment_hash, rcp_id + fee_msat: Optional[int] = None + preimage: Optional[str] = None + error_message: Optional[str] = None + + +class PaymentStatus(NamedTuple): + paid: Optional[bool] = None + fee_msat: Optional[int] = None + preimage: Optional[str] = None + + @property + def pending(self) -> bool: + return self.paid is not True + + @property + def failed(self) -> bool: + return self.paid == False + + def __str__(self) -> str: + if self.paid == True: + return "settled" + elif self.paid == False: + return "failed" + elif self.paid == None: + return "still pending" + else: + return "unknown (should never happen)" + + +class Wallet(ABC): + @abstractmethod + def status(self) -> Coroutine[None, None, StatusResponse]: + pass + + @abstractmethod + def create_invoice( + self, + amount: int, + memo: Optional[str] = None, + description_hash: Optional[bytes] = None, + ) -> Coroutine[None, None, InvoiceResponse]: + pass + + @abstractmethod + def pay_invoice( + self, bolt11: str, fee_limit_msat: int + ) -> Coroutine[None, None, PaymentResponse]: + pass + + @abstractmethod + def get_invoice_status( + self, checking_id: str + ) -> Coroutine[None, None, PaymentStatus]: + pass + + @abstractmethod + def get_payment_status( + self, checking_id: str + ) -> Coroutine[None, None, PaymentStatus]: + pass + + # @abstractmethod + # def paid_invoices_stream(self) -> AsyncGenerator[str, None]: + # pass + + +class Unsupported(Exception): + pass diff --git a/app/external/cashu/lightning/lnbits.py b/app/external/cashu/lightning/lnbits.py new file mode 100644 index 0000000..5ccd0ea --- /dev/null +++ b/app/external/cashu/lightning/lnbits.py @@ -0,0 +1,154 @@ +# type: ignore +from typing import Dict, Optional + +import requests +from cashu.core.settings import LNBITS_ENDPOINT, LNBITS_KEY + +from .base import ( + InvoiceResponse, + PaymentResponse, + PaymentStatus, + StatusResponse, + Wallet, +) + + +class LNbitsWallet(Wallet): + """https://github.com/lnbits/lnbits""" + + def __init__(self): + self.endpoint = LNBITS_ENDPOINT + + key = LNBITS_KEY + self.key = {"X-Api-Key": key} + self.s = requests.Session() + self.s.auth = ("user", "pass") + self.s.headers.update({"X-Api-Key": key}) + + async def status(self) -> StatusResponse: + try: + r = self.s.get(url=f"{self.endpoint}/api/v1/wallet", timeout=15) + except Exception as exc: + return StatusResponse( + f"Failed to connect to {self.endpoint} due to: {exc}", 0 + ) + + try: + data = r.json() + except: + return StatusResponse( + f"Failed to connect to {self.endpoint}, got: '{r.text[:200]}...'", 0 + ) + if "detail" in data: + return StatusResponse(f"LNbits error: {data['detail']}", 0) + return StatusResponse(None, data["balance"]) + + async def create_invoice( + self, + amount: int, + memo: Optional[str] = None, + description_hash: Optional[bytes] = None, + unhashed_description: Optional[bytes] = None, + ) -> InvoiceResponse: + data: Dict = {"out": False, "amount": amount} + if description_hash: + data["description_hash"] = description_hash.hex() + if unhashed_description: + data["unhashed_description"] = unhashed_description.hex() + + data["memo"] = memo or "" + try: + r = self.s.post(url=f"{self.endpoint}/api/v1/payments", json=data) + except: + return InvoiceResponse(False, None, None, r.json()["detail"]) + ok, checking_id, payment_request, error_message = ( + True, + None, + None, + None, + ) + + data = r.json() + checking_id, payment_request = data["checking_id"], data["payment_request"] + + return InvoiceResponse(ok, checking_id, payment_request, error_message) + + async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: + try: + r = self.s.post( + url=f"{self.endpoint}/api/v1/payments", + json={"out": True, "bolt11": bolt11}, + timeout=None, + ) + except: + error_message = r.json()["detail"] + return PaymentResponse(None, None, None, None, error_message) + if r.status_code > 299: + return PaymentResponse(None, None, None, None, f"HTTP status: {r.reason}") + if "detail" in r.json(): + return PaymentResponse(None, None, None, None, r.json()["detail"]) + ok, checking_id, fee_msat, preimage, error_message = ( + True, + None, + None, + None, + None, + ) + + data = r.json() + checking_id = data["payment_hash"] + + # we do this to get the fee and preimage + payment: PaymentStatus = await self.get_payment_status(checking_id) + + return PaymentResponse(ok, checking_id, payment.fee_msat, payment.preimage) + + async def get_invoice_status(self, checking_id: str) -> PaymentStatus: + try: + + r = self.s.get( + url=f"{self.endpoint}/api/v1/payments/{checking_id}", + headers=self.key, + ) + except: + return PaymentStatus(None) + if r.json().get("detail"): + return PaymentStatus(None) + return PaymentStatus(r.json()["paid"]) + + async def get_payment_status(self, checking_id: str) -> PaymentStatus: + try: + r = self.s.get( + url=f"{self.endpoint}/api/v1/payments/{checking_id}", headers=self.key + ) + except: + return PaymentStatus(None) + data = r.json() + if "paid" not in data and "details" not in data: + return PaymentStatus(None) + + return PaymentStatus(data["paid"], data["details"]["fee"], data["preimage"]) + + # async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: + # url = f"{self.endpoint}/api/v1/payments/sse" + + # while True: + # try: + # async with requests.stream("GET", url) as r: + # async for line in r.aiter_lines(): + # if line.startswith("data:"): + # try: + # data = json.loads(line[5:]) + # except json.decoder.JSONDecodeError: + # continue + + # if type(data) is not dict: + # continue + + # yield data["payment_hash"] # payment_hash + + # except: + # pass + + # print("lost connection to lnbits /payments/sse, retrying in 5 seconds") + # await asyncio.sleep(5) diff --git a/app/external/cashu/migrations.py b/app/external/cashu/migrations.py new file mode 100644 index 0000000..666ceb9 --- /dev/null +++ b/app/external/cashu/migrations.py @@ -0,0 +1,51 @@ +import os +import sqlite3 +from sqlite3 import Error + +from alembic import command +from alembic.config import Config + + +def ensure_sqlite_db_file(db_file): + """ensures that the SQlite database file exists""" + + # look up if the file exists + if os.path.exists(db_file): + return + + # if not, create the directory + os.makedirs(os.path.dirname(db_file), exist_ok=True) + + # create the file + conn = None + try: + conn = sqlite3.connect(db_file) + except Error as e: + print(e) + finally: + if conn: + conn.close() + + +def run_migrations(db_url): + ensure_sqlite_db_file(db_url.split("///")[1]) + + alembic_cfg = Config() + alembic_cfg.set_main_option("script_location", "./app/external/cashu/alembic") + + if "+aiosqlite" in db_url: + db_url = db_url.replace("+aiosqlite", "") + + alembic_cfg.set_main_option("sqlalchemy.url", db_url) + command.upgrade(alembic_cfg, "head") + + +# PYTHONPATH=. python app/external/cashu/migrations.py + +# file_name = "/home/f44/dev/blitz/api/add-cashu/cashu.db/test1.sqlite3" + +# # create the database file +# ensure_sqlite_db_file(file_name) + +# # now run alembic on the file +# run_migrations(file_name) diff --git a/app/external/cashu/tor/tor.py b/app/external/cashu/tor/tor.py new file mode 100644 index 0000000..bfaa8b0 --- /dev/null +++ b/app/external/cashu/tor/tor.py @@ -0,0 +1,92 @@ +import os +import pathlib +import platform +import socket + +from loguru import logger + + +class TorProxy: + def __init__(self, timeout=False): + self.base_path = pathlib.Path(__file__).parent.resolve() + self.platform = platform.system() + self.timeout = 60 * 60 if timeout else 0 # seconds + self.tor_proc = None + self.pid_file = os.path.join(self.base_path, "tor.pid") + self.tor_pid = None + self.startup_finished = True + self.tor_running = self.is_running() + + def log_status(self): + logger.debug(f"Tor binary path: {self.tor_path()}") + logger.debug(f"Tor config path: {self.tor_config_path()}") + logger.debug(f"Tor running: {self.tor_running}") + logger.debug( + f"Tor port open: {self.is_port_open()}", + ) + logger.debug(f"Tor PID in tor.pid: {self.read_pid()}") + logger.debug(f"Tor PID running: {self.signal_pid(self.read_pid())}") + + def tor_path(self): + PATHS = { + "Windows": os.path.join(self.base_path, "bundle", "win", "Tor", "tor.exe"), + "Linux": os.path.join(self.base_path, "bundle", "linux", "tor"), + "Darwin": os.path.join(self.base_path, "bundle", "mac", "tor"), + } + # make sure that file has correct permissions + try: + logger.debug(f"Setting permissions of {PATHS[platform.system()]} to 755") + os.chmod(PATHS[platform.system()], 0o755) + except: + logger.debug("Exception: could not set permissions of Tor binary") + return PATHS[platform.system()] + + def tor_config_path(self): + return os.path.join(self.base_path, "torrc") + + def is_running(self): + # another tor proxy is running + if not self.is_port_open(): + return False + # our tor proxy running from a previous session + if self.signal_pid(self.read_pid()): + return True + # current attached process running + return self.tor_proc and self.tor_proc.poll() is None + + def is_port_open(self): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + location = ("127.0.0.1", 9050) + try: + s.connect(location) + s.close() + return True + except Exception as e: + return False + + def read_pid(self): + if not os.path.isfile(self.pid_file): + return None + with open(self.pid_file, "r") as f: + pid = f.readlines() + # check if pid is valid + if len(pid) == 0 or not int(pid[0]) > 0: + return None + return pid[0] + + def signal_pid(self, pid, signal=0): + """ + Checks whether a process with pid is running (signal 0 is not a kill signal!) + or stops (signal 15) or kills it (signal 9). + """ + if not pid: + return False + if not int(pid) > 0: + return False + pid = int(pid) + try: + os.kill(pid, signal) + except: + return False + else: + return True diff --git a/app/external/cashu/wallet/crud.py b/app/external/cashu/wallet/crud.py new file mode 100644 index 0000000..bb72648 --- /dev/null +++ b/app/external/cashu/wallet/crud.py @@ -0,0 +1,324 @@ +import time +from typing import Any, List + +from loguru import logger +from sqlalchemy.engine import Result +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.sql import text + +from app.external.cashu.core.base import Invoice, P2SHScript, Proof, WalletKeyset +from app.external.cashu.core.db import Base, Database +from app.external.cashu.core.errors import DatabaseException + + +def _to_values(data: tuple): + key_string = ", ".join([f":{i}" for i in range(len(data))]) + values = {f"{i}": data[i] for i in range(len(data))} + + return key_string, values + + +async def _exec_and_raise( + session: AsyncSession, query, values={}, commit: bool = False +) -> Result: + try: + q = query + if isinstance(q, str): + q = text(q) + + res = await session.execute(q, values) + + if commit: + await session.commit() + + return res + except SQLAlchemyError as e: + logger.error(e) + session.rollback() + + raise DatabaseException(e) + + +async def store_proof(db: Database, proof: Proof): + async with db.async_session as session: + keys, values = _to_values( + (proof.id, proof.amount, str(proof.C), str(proof.secret), int(time.time())) + ) + + query = f""" + INSERT INTO proofs (id, amount, C, secret, time_created) + VALUES ({keys}) + """ + + await _exec_and_raise(query=query, values=values, session=session, commit=True) + + +async def get_proofs(db: Database) -> List[Proof]: + async with db.async_session as session: + cursor = await _exec_and_raise( + session=session, query="SELECT * from proofs", values={} + ) + + return [Proof(**r._mapping) for r in cursor.fetchall()] + + +async def get_reserved_proofs(db: Database): + async with db.async_session as session: + query = "SELECT * from proofs WHERE reserved" + cursor = await _exec_and_raise(session=session, query=query, values={}) + + return [Proof(**r._mapping) for r in cursor.fetchall()] + + +async def invalidate_proof(proof: Proof, db: Database): + async with db.async_session as session: + keys, values = _to_values((str(proof["secret"]),)) + query = f"DELETE FROM proofs WHERE secret = {keys}" + await _exec_and_raise(session=session, query=query, values=values, commit=True) + + keys, values = _to_values( + (proof.amount, str(proof.C), str(proof.secret), int(time.time()), proof.id) + ) + query = f""" + INSERT INTO proofs_used + (amount, C, secret, time_used, id) + VALUES ({keys}) + """ + + await _exec_and_raise(session=session, query=query, values=values, commit=True) + + +async def update_proof_reserved( + proof: Proof, + reserved: bool, + db: Database, + send_id: str = None, +): + clauses = [] + values: dict[str, Any] = {} + clauses.append("reserved = :reserved") + values["reserved"] = reserved + + if send_id: + clauses.append("send_id = :send_id") + values["send_id"] = send_id + + if reserved: + # set the time of reserving + clauses.append("time_reserved = :time_reserved") + values["time_reserved"] = int(time.time()) + + async with db.async_session as session: + query = f"UPDATE proofs SET {', '.join(clauses)} WHERE secret = :secret" + values["secret"] = str(proof.secret) + + await _exec_and_raise(session=session, query=query, values=values, commit=True) + + +async def secret_used(db: Database, secret: str): + async with db.async_session as session: + query = "SELECT * from proofs WHERE secret = :s" + values = {"s": secret} + cursor = await _exec_and_raise(session=session, query=query, values=values) + rows = cursor.fetchone() + + return rows is not None + + +async def store_p2sh(db: Database, p2sh: P2SHScript): + async with db.async_session as session: + keys = values = _to_values((p2sh.address, p2sh.script, p2sh.signature, False)) + query = f""" + INSERT INTO p2sh (address, script, signature, used) + VALUES ({keys}) + """ + + await _exec_and_raise(session=session, query=query, values=values, commit=True) + + +async def get_unused_locks(db: Database, address: str = None): + + clause: List[str] = [] + values: dict[str, Any] = {} + + clause.append("used = 0") + + if address: + clause.append("address = :address") + values["address"] = address + + where = "" + if clause: + where = f"WHERE {' AND '.join(clause)}" + + async with db.async_session as session: + query = f"SELECT * from p2sh {where}" + cursor = await _exec_and_raise(session=session, query=query, values=values) + rows = cursor.fetchall() + + return [P2SHScript(**r._mapping) for r in rows] + + +async def update_p2sh_used(db: Database, p2sh: P2SHScript, used: bool): + clauses = [] + values: dict[str, Any] = {"address": str(p2sh.address)} + clauses.append("used = :used") + values["used"] = used + + async with db.async_session as session: + query = f"UPDATE proofs SET {', '.join(clauses)} WHERE address = :address" + + await _exec_and_raise(session=session, query=query, values=values, commit=True) + + +async def store_keyset(db: Database, keyset: WalletKeyset, mint_url: str = None): + async with db.async_session as session: + keys, values = _to_values( + ( + keyset.id, + mint_url or keyset.mint_url, + keyset.valid_from or int(time.time()), + keyset.valid_to or int(time.time()), + keyset.first_seen or int(time.time()), + True, + ) + ) + query = f""" + INSERT INTO keysets + (id, mint_url, valid_from, valid_to, first_seen, active) + VALUES ({keys}) + """ + + await _exec_and_raise(session=session, query=query, values=values, commit=True) + + +async def get_keyset(id: str = "", mint_url: str = "", db: Database = None): + logger.trace(f"get_keyset({id}, {mint_url}, {db.name}) called") + clauses = [] + values: dict[str, Any] = {} + clauses.append("active = :active") + values["active"] = True + + if id: + clauses.append("id = :id") + values["id"] = id + + if mint_url: + clauses.append("mint_url = :mint_url") + values["mint_url"] = db.l_proc(mint_url) + + where = "" + if clauses: + where = f"WHERE {' AND '.join(clauses)}" + + async with db.async_session as session: + query = f"SELECT * FROM keysets {where}" + + if mint_url: + # TODO: this is a dirty hack, fix it. SQLAlchemy has trouble replacing + # :mint_url with the value of mint_url, and thus, never finds the db entry + query = query.replace(":mint_url", values["mint_url"]) + + c = await _exec_and_raise(session=session, query=query, values=values) + row = c.fetchone() + + return WalletKeyset(**row._mapping) if row is not None else None + + +async def store_lightning_invoice(db: Database, invoice: Invoice): + async with db.async_session as session: + keys, values = _to_values( + ( + invoice.amount, + invoice.pr, + invoice.hash, + invoice.preimage, + invoice.paid, + invoice.time_created, + invoice.time_paid, + ) + ) + query = f""" + INSERT INTO invoices + (amount, pr, hash, preimage, paid, time_created, time_paid) + VALUES ({keys}) + """ + + await _exec_and_raise(session=session, query=query, values=values, commit=True) + + +async def get_lightning_invoice(db: Database, hash: str = None): + clauses = [] + values: dict[str, Any] = {} + if hash: + clauses.append("hash = :hash") + values["hash"] = hash + + where = "" + if clauses: + where = f"WHERE {' AND '.join(clauses)}" + + async with db.async_session as session: + query = f"SELECT * from invoices {where}" + cursor = await _exec_and_raise(session=session, query=query, values=values) + row = cursor.fetchone() + + return Invoice(**row._mapping) + + +async def get_lightning_invoices(db: Database, paid: bool = None): + clauses: List[Any] = [] + values: dict[str, Any] = {} + + if paid is not None: + clauses.append("paid = :paid") + values["paid"] = paid + + where = "" + if clauses: + where = f"WHERE {' AND '.join(clauses)}" + + async with db.async_session as session: + query = f"SELECT * from invoices {where}" + cursor = await _exec_and_raise(session=session, query=query, values=values) + rows = cursor.fetchall() + + return [Invoice(**r._mapping) for r in rows] + + +async def update_lightning_invoice( + db: Database, hash: str, paid: bool, time_paid: int = None +): + clauses = [] + values: dict[str, Any] = {} + clauses.append("paid = :paid") + values["paid"] = paid + + if time_paid: + clauses.append("time_paid = :time_paid") + values["time_paid"] = time_paid + + async with db.async_session as session: + query = f"UPDATE invoices SET {', '.join(clauses)} WHERE hash = :hash" + values["hash"] = hash + + await _exec_and_raise(session=session, query=query, values=values) + + +async def set_nostr_last_check_timestamp(db: Database, timestamp: int): + async with db.async_session as session: + query = ("UPDATE nostr SET last = :last WHERE type = :type",) + values = ({"last": timestamp, "type": "dm"},) + + await _exec_and_raise(session=session, query=query, values=values, commit=True) + + +async def get_nostr_last_check_timestamp(db: Database): + async with db.async_session as session: + query = "SELECT last from nostr WHERE type = :type" + values = {"type": "dm"} + cursor = await _exec_and_raise(session=session, query=query, values=values) + row = await cursor.fetchone() + + return row[0] if row else None diff --git a/app/external/cashu/wallet/models.py b/app/external/cashu/wallet/models.py new file mode 100644 index 0000000..60fc537 --- /dev/null +++ b/app/external/cashu/wallet/models.py @@ -0,0 +1,6 @@ +proofs_table_name = "proofs" +proofs_used_table_name = "proofs_used" +p2sh_table_name = "p2sh" +keysets_table_name = "keysets" +invoices_table_name = "invoices" +nostr_table_name = "nostr" diff --git a/app/external/cashu/wallet/wallet.py b/app/external/cashu/wallet/wallet.py new file mode 100644 index 0000000..310e3c4 --- /dev/null +++ b/app/external/cashu/wallet/wallet.py @@ -0,0 +1,852 @@ +import base64 +import json +import math +import secrets as scrts +import time +import uuid +from itertools import groupby +from typing import Dict, List, Optional + +import requests +from loguru import logger + +import app.external.cashu.core.b_dhke as b_dhke +import app.external.cashu.core.bolt11 as bolt11 +from app.cashu.errors import ( + CashuException, + CashuReceiveFailReason, + UntrustedMintException, +) +from app.external.cashu.core.base import ( + BlindedMessage, + BlindedSignature, + CheckFeesRequest, + CheckSpendableRequest, + CheckSpendableResponse, + GetMintResponse, + Invoice, + KeysetsResponse, + P2SHScript, + PostMeltRequest, + PostMintRequest, + PostMintResponse, + PostMintResponseLegacy, + PostSplitRequest, + Proof, + TokenV2, + TokenV2Mint, + WalletKeyset, +) +from app.external.cashu.core.bolt11 import Invoice as InvoiceBolt11 +from app.external.cashu.core.db import Database +from app.external.cashu.core.helpers import sum_proofs +from app.external.cashu.core.script import ( + step0_carol_checksig_redeemscrip, + step0_carol_privkey, + step1_carol_create_p2sh_address, + step2_carol_sign_tx, +) +from app.external.cashu.core.secp import PublicKey +from app.external.cashu.core.settings import DEBUG, SOCKS_HOST, SOCKS_PORT, TOR, VERSION +from app.external.cashu.core.split import amount_split +from app.external.cashu.tor.tor import TorProxy +from app.external.cashu.wallet.crud import ( + get_keyset, + get_proofs, + invalidate_proof, + secret_used, + store_keyset, + store_lightning_invoice, + store_p2sh, + store_proof, + update_lightning_invoice, + update_proof_reserved, +) + + +def async_set_requests(func): + """ + Decorator that wraps around any async class method of LedgerAPI that makes + API calls. Sets some HTTP headers and starts a Tor instance if none is + already running and and sets local proxy to use it. + """ + + async def wrapper(self, *args, **kwargs): + self.s.headers.update({"Client-version": VERSION}) + if DEBUG: + self.s.verify = False + socks_host, socks_port = None, None + if TOR and TorProxy().check_platform(): + self.tor = TorProxy(timeout=True) + self.tor.run_daemon(verbose=True) + socks_host, socks_port = "localhost", 9050 + else: + socks_host, socks_port = SOCKS_HOST, SOCKS_PORT + + if socks_host and socks_port: + proxies = { + "http": f"socks5://{socks_host}:{socks_port}", + "https": f"socks5://{socks_host}:{socks_port}", + } + self.s.proxies.update(proxies) + self.s.headers.update({"User-Agent": scrts.token_urlsafe(8)}) + + return await func(self, *args, **kwargs) + + return wrapper + + +class LedgerAPI: + keys: Dict[int, PublicKey] + keyset: str + tor: TorProxy + s: requests.Session + + _db: Database + _initialized: bool = False + + @async_set_requests + async def _init_s(self): + """Dummy function that can be called from outside to use LedgerAPI.s""" + return + + def __init__(self, url): + self.url = url + self.s = requests.Session() + + def _construct_proofs( + self, promises: List[BlindedSignature], secrets: List[str], rs: List[str] + ): + """Returns proofs of promise from promises. Wants secrets and blinding factors rs.""" + proofs = [] + for promise, secret, r in zip(promises, secrets, rs): + C_ = PublicKey(bytes.fromhex(promise.C_), raw=True) + C = b_dhke.step3_alice(C_, r, self.keys[promise.amount]) + proof = Proof( + id=self.keyset_id, + amount=promise.amount, + C=C.serialize().hex(), + secret=secret, + ) + proofs.append(proof) + return proofs + + async def initialize(self): + if self._initialized: + raise RuntimeError("Cashu wallet is already initialized") + + await self._db.initialize() + await self.load_proofs() + + self._initialized = True + + @property + def database(self) -> Database: + if not self._initialized: + raise RuntimeError("Cashu wallet is not initialized") + + return self._db + + @staticmethod + def raise_on_error(resp_dict): + if "error" in resp_dict: + raise Exception("Mint Error: {}".format(resp_dict["error"])) + + @staticmethod + def _generate_secret(randombits=128): + """Returns base64 encoded random string.""" + return scrts.token_urlsafe(randombits // 8) + + async def _load_mint(self, keyset_id: str = ""): + """ + Loads the public keys of the mint. Either gets the keys for the specified + `keyset_id` or loads the most recent one from the mint. + Gets and the active keyset ids of the mint and stores in `self.keysets`. + """ + assert len( + self.url + ), "Ledger not initialized correctly: mint URL not specified yet. " + + if keyset_id: + # get requested keyset + keyset = await self._get_keyset(self.url, keyset_id) + else: + # get current keyset + keyset = await self._get_keys(self.url) + + # store current keyset + assert keyset.public_keys + assert keyset.id + assert len(keyset.public_keys) > 0, "did not receive keys from mint." + + # check if current keyset is in db + keyset_local: Optional[WalletKeyset] = await get_keyset(keyset.id, db=self._db) + if keyset_local is None: + await store_keyset(keyset=keyset, db=self._db) + + # get all active keysets of this mint + mint_keysets = [] + try: + keysets_resp = await self._get_keyset_ids(self.url) + mint_keysets = keysets_resp["keysets"] + # store active keysets + except: + pass + self.keysets = mint_keysets if len(mint_keysets) else [keyset.id] + + logger.debug(f"Mint keysets: {self.keysets}") + logger.debug(f"Current mint keyset: {keyset.id}") + + self.keys = keyset.public_keys + self.keyset_id = keyset.id + + @staticmethod + def _construct_outputs(amounts: List[int], secrets: List[str]): + """Takes a list of amounts and secrets and returns outputs. + Outputs are blinded messages `outputs` and blinding factors `rs`""" + assert len(amounts) == len( + secrets + ), f"len(amounts)={len(amounts)} not equal to len(secrets)={len(secrets)}" + outputs: List[BlindedMessage] = [] + rs = [] + for secret, amount in zip(secrets, amounts): + B_, r = b_dhke.step1_alice(secret) + rs.append(r) + output: BlindedMessage = BlindedMessage( + amount=amount, B_=B_.serialize().hex() + ) + outputs.append(output) + return outputs, rs + + async def _check_used_secrets(self, secrets): + for s in secrets: + if await secret_used(secret=s, db=self._db): + raise Exception(f"secret already used: {s}") + + def generate_secrets(self, secret, n): + """`secret` is the base string that will be tweaked n times""" + if len(secret.split("P2SH:")) == 2: + return [f"{secret}:{self._generate_secret()}" for i in range(n)] + return [f"{i}:{secret}" for i in range(n)] + + """ + ENDPOINTS + """ + + @async_set_requests + async def _get_keys(self, url: str): + resp = self.s.get( + url + "/keys", + ) + resp.raise_for_status() + keys = resp.json() + assert len(keys), Exception("did not receive any keys") + keyset_keys = { + int(amt): PublicKey(bytes.fromhex(val), raw=True) + for amt, val in keys.items() + } + keyset = WalletKeyset(public_keys=keyset_keys, mint_url=url) + return keyset + + @async_set_requests + async def _get_keyset(self, url: str, keyset_id: str): + """ + keyset_id is base64, needs to be urlsafe-encoded. + """ + keyset_id_urlsafe = keyset_id.replace("+", "-").replace("/", "_") + resp = self.s.get( + url + f"/keys/{keyset_id_urlsafe}", + ) + resp.raise_for_status() + keys = resp.json() + assert len(keys), Exception("did not receive any keys") + keyset_keys = { + int(amt): PublicKey(bytes.fromhex(val), raw=True) + for amt, val in keys.items() + } + keyset = WalletKeyset(public_keys=keyset_keys, mint_url=url) + return keyset + + @async_set_requests + async def _get_keyset_ids(self, url: str): + resp = self.s.get( + url + "/keysets", + ) + resp.raise_for_status() + keysets_dict = resp.json() + keysets = KeysetsResponse.parse_obj(keysets_dict) + assert len(keysets.keysets), Exception("did not receive any keysets") + return keysets.dict() + + @async_set_requests + async def request_mint(self, amount): + """Requests a mint from the server and returns Lightning invoice.""" + resp = self.s.get(self.url + "/mint", params={"amount": amount}) + resp.raise_for_status() + return_dict = resp.json() + self.raise_on_error(return_dict) + mint_response = GetMintResponse.parse_obj(return_dict) + return Invoice(amount=amount, pr=mint_response.pr, hash=mint_response.hash) + + @async_set_requests + async def mint(self, amounts, payment_hash=None): + """Mints new coins and returns a proof of promise.""" + secrets = [self._generate_secret() for s in range(len(amounts))] + await self._check_used_secrets(secrets) + outputs, rs = self._construct_outputs(amounts, secrets) + outputs_payload = PostMintRequest(outputs=outputs) + resp = self.s.post( + self.url + "/mint", + json=outputs_payload.dict(), + params={"payment_hash": payment_hash}, + ) + resp.raise_for_status() + response_dict = resp.json() + self.raise_on_error(response_dict) + try: + # backwards compatibility: parse promises < 0.8.0 with no "promises" field + promises = PostMintResponseLegacy.parse_obj(response_dict).__root__ + except: + promises = PostMintResponse.parse_obj(response_dict).promises + + return self._construct_proofs(promises, secrets, rs) + + @async_set_requests + async def split(self, proofs, amount, scnd_secret: Optional[str] = None): + """Consume proofs and create new promises based on amount split. + If scnd_secret is None, random secrets will be generated for the tokens to keep (frst_outputs) + and the promises to send (scnd_outputs). + + If scnd_secret is provided, the wallet will create blinded secrets with those to attach a + predefined spending condition to the tokens they want to send.""" + + total = sum_proofs(proofs) + frst_amt, scnd_amt = total - amount, amount + frst_outputs = amount_split(frst_amt) + scnd_outputs = amount_split(scnd_amt) + + amounts = frst_outputs + scnd_outputs + if scnd_secret is None: + secrets = [self._generate_secret() for _ in range(len(amounts))] + else: + scnd_secrets = self.generate_secrets(scnd_secret, len(scnd_outputs)) + logger.debug(f"Creating proofs with custom secrets: {scnd_secrets}") + assert len(scnd_secrets) == len( + scnd_outputs + ), "number of scnd_secrets does not match number of ouptus." + # append predefined secrets (to send) to random secrets (to keep) + secrets = [ + self._generate_secret() for s in range(len(frst_outputs)) + ] + scnd_secrets + + assert len(secrets) == len( + amounts + ), "number of secrets does not match number of outputs" + await self._check_used_secrets(secrets) + outputs, rs = self._construct_outputs(amounts, secrets) + split_payload = PostSplitRequest(proofs=proofs, amount=amount, outputs=outputs) + + # construct payload + def _split_request_include_fields(proofs): + """strips away fields from the model that aren't necessary for the /split""" + proofs_include = {"id", "amount", "secret", "C", "script"} + return { + "amount": ..., + "outputs": ..., + "proofs": {i: proofs_include for i in range(len(proofs))}, + } + + resp = self.s.post( + self.url + "/split", + json=split_payload.dict(include=_split_request_include_fields(proofs)), # type: ignore + ) + resp.raise_for_status() + promises_dict = resp.json() + self.raise_on_error(promises_dict) + + promises_fst = [BlindedSignature(**p) for p in promises_dict["fst"]] + promises_snd = [BlindedSignature(**p) for p in promises_dict["snd"]] + # Construct proofs from promises (i.e., unblind signatures) + frst_proofs = self._construct_proofs( + promises_fst, secrets[: len(promises_fst)], rs[: len(promises_fst)] + ) + scnd_proofs = self._construct_proofs( + promises_snd, secrets[len(promises_fst) :], rs[len(promises_fst) :] + ) + + return frst_proofs, scnd_proofs + + @async_set_requests + async def check_spendable(self, proofs: List[Proof]): + """ + Checks whether the secrets in proofs are already spent or not and returns a list of booleans. + """ + payload = CheckSpendableRequest(proofs=proofs) + + def _check_spendable_include_fields(proofs): + """strips away fields from the model that aren't necessary for the /split""" + return { + "proofs": {i: {"secret"} for i in range(len(proofs))}, + } + + resp = self.s.post( + self.url + "/check", + json=payload.dict(include=_check_spendable_include_fields(proofs)), # type: ignore + ) + resp.raise_for_status() + return_dict = resp.json() + self.raise_on_error(return_dict) + spendable = CheckSpendableResponse.parse_obj(return_dict) + return spendable + + @async_set_requests + async def check_fees(self, payment_request: str): + """Checks whether the Lightning payment is internal.""" + payload = CheckFeesRequest(pr=payment_request) + resp = self.s.post( + self.url + "/checkfees", + json=payload.dict(), + ) + resp.raise_for_status() + return_dict = resp.json() + self.raise_on_error(return_dict) + return return_dict + + @async_set_requests + async def pay_lightning(self, proofs: List[Proof], invoice: str): + """ + Accepts proofs and a lightning invoice to pay in exchange. + """ + payload = PostMeltRequest(proofs=proofs, pr=invoice) + + def _meltrequest_include_fields(proofs): + """strips away fields from the model that aren't necessary for the /melt""" + proofs_include = {"id", "amount", "secret", "C", "script"} + return { + "amount": ..., + "pr": ..., + "proofs": {i: proofs_include for i in range(len(proofs))}, + } + + resp = self.s.post( + self.url + "/melt", + json=payload.dict(include=_meltrequest_include_fields(proofs)), # type: ignore + ) + resp.raise_for_status() + return_dict = resp.json() + self.raise_on_error(return_dict) + return return_dict + + +class Wallet(LedgerAPI): + """Minimal wallet wrapper.""" + + def __init__(self, url: str, db: str, name: str = "no_name"): + super().__init__(url) + self._db = Database(name) + self.proofs: List[Proof] = [] + self.name = name + logger.debug(f"Wallet initialized with mint URL {url}") + + # ---------- API ---------- + + async def load_mint(self, keyset_id: str = ""): + await super()._load_mint(keyset_id) + + async def load_proofs(self): + self.proofs = await get_proofs(db=self._db) + + async def request_mint(self, amount): + invoice = await super().request_mint(amount) + invoice.time_created = int(time.time()) + await store_lightning_invoice(db=self._db, invoice=invoice) + + return invoice + + async def mint(self, amount: int, payment_hash: Optional[str] = None): + split = amount_split(amount) + proofs = await super().mint(split, payment_hash) + if proofs == []: + raise Exception("received no proofs.") + await self._store_proofs(proofs) + if payment_hash: + await update_lightning_invoice( + db=self._db, hash=payment_hash, paid=True, time_paid=int(time.time()) + ) + self.proofs += proofs + + return proofs + + async def redeem( + self, + proofs: List[Proof], + scnd_script: Optional[str] = None, + scnd_siganture: Optional[str] = None, + ): + if scnd_script and scnd_siganture: + logger.debug(f"Unlock script: {scnd_script}") + # attach unlock scripts to proofs + for p in proofs: + p.script = P2SHScript(script=scnd_script, signature=scnd_siganture) + + return await self.split(proofs, sum_proofs(proofs)) + + async def split( + self, + proofs: List[Proof], + amount: int, + scnd_secret: Optional[str] = None, + ): + assert len(proofs) > 0, ValueError("no proofs provided.") + frst_proofs, scnd_proofs = await super().split(proofs, amount, scnd_secret) + if len(frst_proofs) == 0 and len(scnd_proofs) == 0: + raise Exception("received no splits.") + used_secrets = [p["secret"] for p in proofs] + self.proofs = list( + filter(lambda p: p["secret"] not in used_secrets, self.proofs) + ) + self.proofs += frst_proofs + scnd_proofs + await self._store_proofs(frst_proofs + scnd_proofs) + + # TODO: batch this db call + for proof in proofs: + await invalidate_proof(proof, db=self._db) + + return frst_proofs, scnd_proofs + + async def pay_lightning(self, proofs: List[Proof], invoice: str): + """Pays a lightning invoice""" + status = await super().pay_lightning(proofs, invoice) + if status["paid"] == True: + await self.invalidate(proofs) + invoice_obj = Invoice( + amount=-sum_proofs(proofs), + pr=invoice, + preimage=status.get("preimage"), + paid=True, + time_paid=time.time(), + ) + await store_lightning_invoice(db=self._db, invoice=invoice_obj) + else: + raise Exception("could not pay invoice.") + + return status["paid"] + + async def check_spendable(self, proofs): + return await super().check_spendable(proofs) + + # ---------- TOKEN MECHANICS ---------- + + async def _store_proofs(self, proofs): + # TODO: batch this db call + for proof in proofs: + await store_proof(proof=proof, db=self._db) + + @staticmethod + def _get_proofs_per_keyset(proofs: List[Proof]): + return {key: list(group) for key, group in groupby(proofs, lambda p: p.id)} + + async def _get_proofs_per_minturl(self, proofs: List[Proof]): + ret = {} + for id in set([p.id for p in proofs]): + if id is None: + continue + keyset_crud = await get_keyset(id=id, db=self._db) + assert keyset_crud is not None, "keyset not found" + keyset: WalletKeyset = keyset_crud + if keyset.mint_url not in ret: + ret[keyset.mint_url] = [p for p in proofs if p.id == id] + else: + ret[keyset.mint_url].extend([p for p in proofs if p.id == id]) + + return ret + + async def _make_token(self, proofs: List[Proof], include_mints=True): + """ + Takes list of proofs and produces a TokenV2 by looking up + the keyset id and mint URLs from the database. + """ + # build token + token = TokenV2(proofs=proofs) + # add mint information to the token, if requested + if include_mints: + # dummy object to hold information about the mint + mints: Dict[str, TokenV2Mint] = {} + # dummy object to hold all keyset id's we need to fetch from the db later + keysets: List[str] = [] + # iterate through all proofs and remember their keyset ids for the next step + for proof in proofs: + if proof.id: + keysets.append(proof.id) + # iterate through unique keyset ids + for id in set(keysets): + # load the keyset from the db + keyset = await get_keyset(id=id, db=self._db) + if keyset and keyset.mint_url and keyset.id: + # we group all mints according to URL + if keyset.mint_url not in mints: + mints[keyset.mint_url] = TokenV2Mint( + url=keyset.mint_url, + ids=[keyset.id], + ) + else: + # if a mint URL has multiple keysets, append to the already existing list + mints[keyset.mint_url].ids.append(keyset.id) + if len(mints) > 0: + # add mints grouped by url to the token + token.mints = list(mints.values()) + + return token + + async def _serialize_token_base64(self, token: TokenV2): + """ + Takes a TokenV2 and serializes it in urlsafe_base64. + """ + # encode the token as a base64 string + token_base64 = base64.urlsafe_b64encode( + json.dumps(token.to_dict()).encode() + ).decode() + + return token_base64 + + async def serialize_proofs( + self, proofs: List[Proof], include_mints=True, legacy=False + ): + """ + Produces sharable token with proofs and mint information. + """ + + if legacy: + proofs_serialized = [p.to_dict() for p in proofs] + return base64.urlsafe_b64encode( + json.dumps(proofs_serialized).encode() + ).decode() + + token = await self._make_token(proofs, include_mints) + + return await self._serialize_token_base64(token) + + async def _select_proofs_to_send(self, proofs: List[Proof], amount_to_send: int): + """ + Selects proofs that can be used with the current mint. + Chooses: + 1) Proofs that are not marked as reserved + 2) Proofs that have a keyset id that is in self.keysets (active keysets of mint) - !!! optional for backwards compatibility with legacy clients + """ + # select proofs that are in the active keysets of the mint + proofs = [ + p for p in proofs if p.id in self.keysets or not p.id + ] # "or not p.id" is for backwards compatibility with proofs without a keyset id + # select proofs that are not reserved + proofs = [p for p in proofs if not p.reserved] + # check that enough spendable proofs exist + if sum_proofs(proofs) < amount_to_send: + raise Exception("balance too low.") + + # coinselect based on amount to send + sorted_proofs = sorted(proofs, key=lambda p: p.amount) + send_proofs: List[Proof] = [] + while sum_proofs(send_proofs) < amount_to_send: + send_proofs.append(sorted_proofs[len(send_proofs)]) + + return send_proofs + + async def set_reserved(self, proofs: List[Proof], reserved: bool): + """Mark a proof as reserved to avoid reuse or delete marking.""" + uuid_str = str(uuid.uuid1()) + for proof in proofs: + proof.reserved = True + # TODO: batch this db call + await update_proof_reserved( + proof, reserved=reserved, send_id=uuid_str, db=self._db + ) + + async def invalidate(self, proofs): + """Invalidates all spendable tokens supplied in proofs.""" + spendables = await self.check_spendable(proofs) + invalidated_proofs = [] + for i, spendable in enumerate(spendables.spendable): + # TODO: batch this db call + if not spendable: + invalidated_proofs.append(proofs[i]) + await invalidate_proof(proofs[i], db=self._db) + invalidate_secrets = [p["secret"] for p in invalidated_proofs] + self.proofs = list( + filter(lambda p: p["secret"] not in invalidate_secrets, self.proofs) + ) + + # ---------- TRANSACTION HELPERS ---------- + + async def get_pay_amount_with_fees(self, invoice: str): + """ + Decodes the amount from a Lightning invoice and returns the + total amount (amount+fees) to be paid. + """ + decoded_invoice: InvoiceBolt11 = bolt11.decode(invoice) + # check if it's an internal payment + fees = int((await self.check_fees(invoice))["fee"]) + amount = math.ceil((decoded_invoice.amount_msat + fees * 1000) / 1000) # 1% fee + + return amount, fees + + async def split_to_pay(self, invoice: str): + """ + Splits proofs such that a Lightning invoice can be paid. + """ + amount, _ = await self.get_pay_amount_with_fees(invoice) + # TODO: fix mypy asyncio return multiple values + _, send_proofs = await self.split_to_send(self.proofs, amount) # type: ignore + + return send_proofs + + async def split_to_send( + self, + proofs: List[Proof], + amount, + scnd_secret: Optional[str] = None, + set_reserved: bool = False, + ): + """Like self.split but only considers non-reserved tokens.""" + if scnd_secret: + logger.debug(f"Spending conditions: {scnd_secret}") + spendable_proofs = await self._select_proofs_to_send(proofs, amount) + + keep_proofs, send_proofs = await self.split( + [p for p in spendable_proofs if not p.reserved], amount, scnd_secret + ) + if set_reserved: + await self.set_reserved(send_proofs, reserved=True) + + return keep_proofs, send_proofs + + # ---------- P2SH ---------- + + async def create_p2sh_lock(self): + alice_privkey = step0_carol_privkey() + txin_redeemScript = step0_carol_checksig_redeemscrip(alice_privkey.pub) + txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript) + txin_signature = step2_carol_sign_tx(txin_redeemScript, alice_privkey).scriptSig + txin_redeemScript_b64 = base64.urlsafe_b64encode(txin_redeemScript).decode() + txin_signature_b64 = base64.urlsafe_b64encode(txin_signature).decode() + p2shScript = P2SHScript( + script=txin_redeemScript_b64, + signature=txin_signature_b64, + address=str(txin_p2sh_address), + ) + await store_p2sh(p2shScript, db=self._db) + + return p2shScript + + # ---------- BALANCE CHECKS ---------- + + @property + def balance(self): + return sum_proofs(self.proofs) + + @property + def available_balance(self): + return sum_proofs([p for p in self.proofs if not p.reserved]) + + def status(self): + # print( + # f"Balance: {self.balance} sat (available: {self.available_balance} sat in {len([p for p in self.proofs if not p.reserved])} tokens)" + # ) + print(f"Balance: {self.available_balance} sat") + + def balance_per_keyset(self): + return { + key: { + "balance": sum_proofs(proofs), + "available": sum_proofs([p for p in proofs if not p.reserved]), + } + for key, proofs in self._get_proofs_per_keyset(self.proofs).items() + } + + async def balance_per_minturl(self): + balances = await self._get_proofs_per_minturl(self.proofs) + balances_return = { + key: { + "balance": sum_proofs(proofs), + "available": sum_proofs([p for p in proofs if not p.reserved]), + } + for key, proofs in balances.items() + } + + return dict(sorted(balances_return.items(), key=lambda item: item[1]["available"], reverse=True)) # type: ignore + + def proof_amounts(self): + return [p["amount"] for p in sorted(self.proofs, key=lambda p: p["amount"])] + + async def _verify_mints(self, token: TokenV2, trust_mints: bool = False): + """ + A helper function that iterates through all mints in the token and if it has + not been encountered before, asks the user to confirm. + + It will instantiate a Wallet with each keyset and check whether the mint supports it. + It will then get the keys for that keyset from the mint and check whether the keyset id is correct. + """ + + if token.mints is None: + return + + proofs_keysets = set([p.id for p in token.proofs]) + + logger.debug(f"Verifying mints for wallet {self.name}") + for mint in token.mints: + for keyset in set([id for id in mint.ids if id in proofs_keysets]): + mint_keysets = await self._get_keyset_ids(mint.url) + + if not keyset in mint_keysets["keysets"]: + raise CashuException( + message="Mint does not have this keyset.", + reason=CashuReceiveFailReason.MINT_KEYSET_MISMATCH, + ) + + # we validate the keyset id by fetching the keys from + # the mint and computing the id locally + mint_keyset = await self._get_keyset(mint.url, keyset) + + if keyset != mint_keyset.id: + raise CashuException( + message="keyset not valid.", + reason=CashuReceiveFailReason.MINT_KEYSET_INVALID, + ) + + if trust_mints: + continue + + # we check the db whether we know this mint already and ask the user if not + mint_keysets = await get_keyset(mint_url=mint.url, db=self.database) + + if mint_keysets is None: + # we encountered a new mint and ask for a user confirmation + logger.debug( + f"Encountered untrusted mint {mint.url} with keyset {mint_keyset.id}" + ) + + raise UntrustedMintException(mint_url=mint.url, mint_keyset=keyset) + + async def _redeem_multimint(self, token: TokenV2, script, signature): + """ + Helper function to iterate thruogh a token with multiple mints and redeem them from + these mints one keyset at a time. + """ + # we get the mint information in the token and load the keys of each mint + # we then redeem the tokens for each keyset individually + if token.mints is None: + return + + proofs_keysets = set([p.id for p in token.proofs]) + + for mint in token.mints: + for keyset in set([id for id in mint.ids if id in proofs_keysets]): + logger.debug(f"Redeeming tokens from keyset {keyset}") + await self.load_mint(keyset_id=keyset) + + # redeem proofs of this keyset + redeem_proofs = [p for p in token.proofs if p.id == keyset] + _, _ = await self.redeem( + redeem_proofs, scnd_script=script, scnd_siganture=signature + ) + + logger.trace(f"Received {sum_proofs(redeem_proofs)} sats") diff --git a/app/external/cashu/wallet/wallet_helpers.py b/app/external/cashu/wallet/wallet_helpers.py new file mode 100644 index 0000000..f76371c --- /dev/null +++ b/app/external/cashu/wallet/wallet_helpers.py @@ -0,0 +1,167 @@ +import os +import urllib.parse + +from app.external.cashu.core.settings import CASHU_DIR, MINT_HOST +from app.external.cashu.wallet.crud import get_keyset +from app.external.cashu.wallet.wallet import Wallet + + +async def print_mint_balances(wallet: Wallet, show_mints=False): + """ + Helper function that prints the balances for each mint URL that we have tokens from. + """ + # get balances per mint + mint_balances = await wallet.balance_per_minturl() + + # if we have a balance on a non-default mint, we show its URL + keysets = [k for k, v in wallet.balance_per_keyset().items()] + for k in keysets: + ks = await get_keyset(id=str(k), db=wallet.database) + if ks and ks.mint_url != MINT_HOST: + show_mints = True + + # or we have a balance on more than one mint + # show balances per mint + if len(mint_balances) > 1 or show_mints: + print(f"You have balances in {len(mint_balances)} mints:") + print("") + for i, (k, v) in enumerate(mint_balances.items()): + print( + f"Mint {i+1}: Balance: {v['available']} sat (pending: {v['balance']-v['available']} sat) URL: {k}" + ) + print("") + + +async def get_mint_wallet(wallet: Wallet, wallet_name: str): + """ + Helper function that asks the user for an input to select which mint they want to load. + Useful for selecting the mint that the user wants to send tokens from. + """ + await wallet.load_mint() + + mint_balances = await wallet.balance_per_minturl() + + if len(mint_balances) > 1: + await print_mint_balances(wallet, show_mints=True) + + mint_nr_str = ( + input(f"Select mint [1-{len(mint_balances)}, press enter for default 1]: ") + or "1" + ) + if not mint_nr_str.isdigit(): + raise Exception("invalid input.") + mint_nr = int(mint_nr_str) + else: + mint_nr = 1 + + mint_url = list(mint_balances.keys())[mint_nr - 1] + + # load this mint_url into a wallet + mint_wallet = Wallet(mint_url, os.path.join(CASHU_DIR, wallet_name)) + mint_keysets: WalletKeyset = await get_keyset(mint_url=mint_url, db=mint_wallet.database) # type: ignore + + # load the keys + assert mint_keysets.id + await mint_wallet.load_mint(keyset_id=mint_keysets.id) + + return mint_wallet + + +# LNbits token link parsing +# can extract mint URL from LNbits token links like: +# https://lnbits.server/cashu/wallet?mint_id=aMintId&recv_token=W3siaWQiOiJHY2... +def token_from_lnbits_link(link): + url, token = "", "" + if len(link.split("&recv_token=")) == 2: + # extract URL params + params = urllib.parse.parse_qs(link.split("?")[1]) + # extract URL + if "mint_id" in params: + url = ( + link.split("?")[0].split("/wallet")[0] + + "/api/v1/" + + params["mint_id"][0] + ) + # extract token + token = params["recv_token"][0] + return token, url + else: + return link, "" + + +# async def send_nostr(ctx: Context, amount: int, pubkey: str, verbose: bool, yes: bool): +# """ +# Sends tokens via nostr. +# """ +# # load a wallet for the chosen mint +# wallet = await get_mint_wallet(ctx) +# await wallet.load_proofs() +# _, send_proofs = await wallet.split_to_send( +# wallet.proofs, amount, set_reserved=True +# ) +# token = await wallet.serialize_proofs(send_proofs) + +# print("") +# print(token) + +# if not yes: +# print("") +# click.confirm( +# f"Send {amount} sat to nostr pubkey {pubkey}?", +# abort=True, +# default=True, +# ) + +# # we only use ephemeral private keys for sending +# client = NostrClient(relays=NOSTR_RELAYS) +# if verbose: +# print(f"Your ephemeral nostr private key: {client.private_key.bech32()}") + +# if pubkey.startswith("npub"): +# pubkey_to = PublicKey().from_npub(pubkey) +# else: +# pubkey_to = PublicKey(bytes.fromhex(pubkey)) + +# client.dm(token, pubkey_to) +# print(f"Token sent to {pubkey}") +# client.close() + + +# async def receive_nostr(ctx: Context, verbose: bool): +# if NOSTR_PRIVATE_KEY is None: +# print( +# "Warning: No nostr private key set! You don't have NOSTR_PRIVATE_KEY set in your .env file. I will create a random private key for this session but I will not remember it." +# ) +# print("") +# client = NostrClient(private_key=NOSTR_PRIVATE_KEY, relays=NOSTR_RELAYS) +# print(f"Your nostr public key: {client.public_key.bech32()}") +# if verbose: +# print(f"Your nostr private key (do not share!): {client.private_key.bech32()}") +# await asyncio.sleep(2) + +# def get_token_callback(event: Event, decrypted_content): +# if verbose: +# print( +# f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}" +# ) +# try: +# # call the receive method +# from cashu.wallet.cli import receive + +# asyncio.run(receive(ctx, decrypted_content, "")) +# except Exception as e: +# pass + +# # determine timestamp of last check so we don't scan all historical DMs +# wallet: Wallet = ctx.obj["WALLET"] +# last_check = await get_nostr_last_check_timestamp(db=wallet.database) +# if last_check: +# last_check -= 60 * 60 # 1 hour tolerance +# await set_nostr_last_check_timestamp(timestamp=int(time.time()), db=wallet.database) + +# t = threading.Thread( +# target=client.get_dm, +# args=(client.public_key, get_token_callback, {"since": last_check}), +# name="Nostr DM", +# ) +# t.start() From e34ee924d9f26ec7e4f024309b6c60b279d6887b Mon Sep 17 00:00:00 2001 From: fusion44 Date: Thu, 2 Mar 2023 19:41:46 +0100 Subject: [PATCH 5/7] feat: improve cashu module --- .cashu | 38 ++++ app/cashu/docs.py | 9 + app/cashu/errors.py | 32 +++ app/cashu/models.py | 194 ++++++++++++++--- app/cashu/router.py | 35 +++- app/cashu/service.py | 46 ++++- poetry.lock | 482 ++++++++++++++++++++++++------------------- pyproject.toml | 18 +- 8 files changed, 593 insertions(+), 261 deletions(-) create mode 100644 .cashu create mode 100644 app/cashu/errors.py diff --git a/.cashu b/.cashu new file mode 100644 index 0000000..cec21e3 --- /dev/null +++ b/.cashu @@ -0,0 +1,38 @@ +DEBUG=FALSE + +CASHU_DIR=cashu.db + +# WALLET + +# MINT_URL=https://localhost:3338 +MINT_HOST=127.0.0.1 +MINT_PORT=3338 + +# use builtin tor, this overrides SOCKS_HOST and SOCKS_PORT +TOR=FALSE + +# use custom tor proxy, use with TOR=false +# SOCKS_HOST=localhost +# SOCKS_PORT=9050 + +# MINT + +MINT_PRIVATE_KEY=supersecretprivatekey + +MINT_SERVER_HOST=127.0.0.1 +MINT_SERVER_PORT=3338 + +LIGHTNING=TRUE +# fee to reserve in percent of the amount +LIGHTNING_FEE_PERCENT=1.0 +# minimum fee to reserve +LIGHTNING_RESERVE_FEE_MIN=4000 + +# LNBITS_ENDPOINT=https://legend.lnbits.com +# LNBITS_KEY=yourkeyasdasdasd + +# NOSTR +# nostr private key to which to receive tokens to +NOSTR_PRIVATE_KEY=nostr_privatekey_here_hex_or_bech32 +# nostr relays (comma separated list) +NOSTR_RELAYS="wss://nostr-pub.wellorder.net" diff --git a/app/cashu/docs.py b/app/cashu/docs.py index c7a3b36..988ef36 100644 --- a/app/cashu/docs.py +++ b/app/cashu/docs.py @@ -19,3 +19,12 @@ > 👉 This is different from the /lightning/pay-invoice endpoint which will only try to pay the invoice using a lightning payment. """ + +receive_error_description = """Reason why the token could not be received. + +Possible values: +`none`: No error +`mint_offline`: Mint is not reachable +`mint_error`: Mint returned an error +`mint_untrusted`: Mint is unknown, must be added as trusted mint first +""" diff --git a/app/cashu/errors.py b/app/cashu/errors.py new file mode 100644 index 0000000..505c3eb --- /dev/null +++ b/app/cashu/errors.py @@ -0,0 +1,32 @@ +from enum import Enum + + +class CashuReceiveFailReason(str, Enum): + NONE = "none" + MINT_OFFLINE = "mint_offline" + MINT_ERROR = "mint_error" + MINT_UNTRUSTED = "mint_untrusted" + MINT_KEYSET_MISMATCH = "mint_keyset_mismatch" + MINT_KEYSET_INVALID = "mint_keyset_invalid" + FORMAT_ERROR = "format_error" + LOCK_ERROR = "lock_error" + PROOF_ERROR = "proof_error" + UNKNOWN = "unknown" + + +class CashuException(Exception): + def __init__(self, message: str, reason: CashuReceiveFailReason) -> None: + super().__init__() + self.message: str = message + self.reason: CashuReceiveFailReason = reason + + +class UntrustedMintException(CashuException): + def __init__(self, mint_url: str, mint_keyset: str) -> None: + super().__init__( + message="Mint is untrusted", + reason=CashuReceiveFailReason.MINT_UNTRUSTED, + ) + + self.mint_url: str = mint_url + self.mint_keyset: str = mint_keyset diff --git a/app/cashu/models.py b/app/cashu/models.py index 6441c87..ce0111c 100644 --- a/app/cashu/models.py +++ b/app/cashu/models.py @@ -1,24 +1,35 @@ import base64 import json from os.path import join +from typing import List -from cashu.core.migrations import migrate_databases -from cashu.wallet import migrations -from cashu.wallet.crud import get_unused_locks -from cashu.wallet.wallet import Proof, Wallet from fastapi import Query +from loguru import logger from pydantic import BaseModel -import app.cashu.exceptions as ce -from app.cashu.constants import DATA_FOLDER +import app.cashu.errors as errors +from app.cashu.constants import DATA_FOLDER, DEFAULT_MINT_URL, DEFAULT_WALLET_NAME +from app.external.cashu.core.base import TokenV2, TokenV2Mint +from app.external.cashu.core.helpers import sum_proofs +from app.external.cashu.core.settings import MINT_URL +from app.external.cashu.wallet.crud import get_keyset, get_unused_locks +from app.external.cashu.wallet.wallet import Proof, Wallet +from app.external.cashu.wallet.wallet_helpers import token_from_lnbits_link # Public class CashuMintInput(BaseModel): - url: str = Query("http://localhost:3338", description="URL of the mint.") + url: str = Query(DEFAULT_MINT_URL, description="URL of the mint.") pinned: bool = Query(False, description="Whether the mint is pinned.") +class CashuMintKeyInput(BaseModel): + wallet_name: str = Query(DEFAULT_WALLET_NAME, description="Name of the wallet.") + url: str = Query(DEFAULT_MINT_URL, description="URL of the mint.") + key: str = Query(..., description="The keyset id for which to update the data.") + trusted: bool = Query(..., description="Whether this keyset is to be trusted.") + + class CashuMint(CashuMintInput): default: bool = Query(False, description="Whether the mint is the system default.") @@ -77,20 +88,51 @@ class CashuWalletData(BaseModel): balances_per_mint: list[CashuMintBalance] +class CashuReceiveResult(BaseModel): + is_success: bool = Query( + False, description="Whether the token was received successfully or not." + ) + + sats_received: int = Query( + 0, description="Amount of satoshis received. 0 if the tokens was on error." + ) + + fail_reason: errors.CashuReceiveFailReason = Query( + None, description="Reason why the token could not be received." + ) + + # Message explaining why the token could not be received. + fail_message: str = Query( + None, description="Message explaining why the token could not be received." + ) + + untrusted_mint_url: str = Query(None, description="URL of the untrusted mint.") + untrusted_mint_keyset: str = Query( + None, description="Keyset of the untrusted mint." + ) + + @staticmethod + def from_exception(e: errors.CashuException) -> "CashuReceiveResult": + if isinstance(e, errors.UntrustedMintException): + return CashuReceiveResult( + fail_reason=errors.CashuReceiveFailReason.MINT_UNTRUSTED, + fail_message=e.message, + untrusted_mint_url=e.mint_url, + untrusted_mint_keyset=e.mint_keyset, + ) + + return CashuReceiveResult(fail_reason=e.reason, fail_message=e.message) + + @staticmethod + def success(sats_received: int) -> "CashuReceiveResult": + return CashuReceiveResult(success=True, sats_received=sats_received) + + # Internal only class CashuWallet(Wallet): - initialized: bool = False - def __init__(self, mint: CashuMint, name: str = "no_name") -> None: super().__init__(url=mint.url, db=join(DATA_FOLDER, name), name=name) - async def initialize(self): - if self.initialized: - raise RuntimeError("Cashu wallet is already initialized") - - await migrate_databases(self.db, migrations) - await self.load_proofs() - @property def balance_overview(self) -> CashuWalletBalance: return CashuWalletBalance( @@ -99,33 +141,98 @@ def balance_overview(self) -> CashuWalletBalance: tokens=len([p for p in self.proofs if not p.reserved]), ) - async def receive(self, coin: str, lock: str): - await super().load_mint() - + async def receive(self, token: str, lock: str, trust_mint: bool = False): + # check for P2SH locks script, signature = None, None if lock: # load the script and signature of this address from the database if len(lock.split("P2SH:")) != 2: - raise ce.LockFormatException() + raise errors.CashuException( + message="lock has wrong format. Expected P2SH:
.", + reason=errors.CashuReceiveFailReason.FORMAT_ERROR, + ) address_split = lock.split("P2SH:")[1] - p2sh_scripts = await get_unused_locks(address_split, db=self.db) + p2shscripts = await get_unused_locks(address_split, db=self.database) - if len(p2sh_scripts) == 0: - raise ce.LockNotFoundException() + if len(p2shscripts) != 1: + raise errors.CashuException( + message="lock not found.", + reason=errors.CashuReceiveFailReason.LOCK_ERROR, + ) - script = p2sh_scripts[0].script - signature = p2sh_scripts[0].signature + script, signature = p2shscripts[0].script, p2shscripts[0].signature + + # deserialize token + + # ----- backwards compatibility ----- + + # we support old tokens (< 0.7) without mint information and (W3siaWQ...) + # new tokens (>= 0.7) with multiple mint support (eyJ0b2...) try: - proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(coin))] - _, _ = await self.redeem( - proofs, scnd_script=script, scnd_siganture=signature + # backwards compatibility: tokens without mint information + # supports tokens of the form W3siaWQiOiJH + + # if it's an lnbits https:// link with a token as an argument, special treatment + token, url = token_from_lnbits_link(token) + + # assume W3siaWQiOiJH.. token + # next line trows an error if the deserialization with the old format doesn't + # work and we can assume it's the new format + proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(token))] + + # we take the proofs parsed from the old format token and produce a new format token with it + token = await self._proofs_to_serialized_token_v2(self, proofs, url) + except: + pass + + # ----- receive token ----- + + # deserialize token + dtoken = json.loads(base64.urlsafe_b64decode(token)) + + # backwards compatibility wallet to wallet < 0.8.0: V2 tokens renamed "tokens" field to "proofs" + if "tokens" in dtoken: + dtoken["proofs"] = dtoken.pop("tokens") + + # backwards compatibility wallet to wallet < 0.8.3: V2 tokens got rid of the "MINT_NAME" key in "mints" and renamed "ks" to "ids" + if "mints" in dtoken and isinstance(dtoken["mints"], dict): + dtoken["mints"] = list(dtoken["mints"].values()) + for m in dtoken["mints"]: + m["ids"] = m.pop("ks") + + tokenObj = TokenV2.parse_obj(dtoken) + + if len(tokenObj.proofs) == 0: + raise errors.CashuException( + message="no proofs in token", + reason=errors.CashuReceiveFailReason.PROOF_ERROR, ) - except Exception as e: - if "Mint Error: tokens already spent. Secret:" in e.args[0]: - raise ce.TokensSpentException(secret=e.args[0].split("Secret:")[1]) - raise + includes_mint_info: bool = ( + tokenObj.mints is not None and len(tokenObj.mints) > 0 + ) + + # if there is a `mints` field in the token + # we check whether the token has mints that we don't know yet + # and ask the user if they want to trust the new mints + if includes_mint_info: + # we ask the user to confirm any new mints the tokens may include + await self._verify_mints(tokenObj, trust_mint) + # redeem tokens with new wallet instances + await self._redeem_multimint(tokenObj, script, signature) + # reload main wallet so the balance updates + await self.load_proofs() + + return CashuReceiveResult.success(sum_proofs(tokenObj.proofs)) + + else: + # no mint information present, we extract the proofs and use wallet's default mint + proofs = [Proof(**p) for p in dtoken["proofs"]] + _, _ = await self.redeem(proofs, script, signature) + logger.info(f"Received {sum_proofs(proofs)} sats") + + return CashuReceiveResult.success(sum_proofs(proofs)) def get_wallet_data_for_client( self, include_mint_balances: bool = False @@ -147,3 +254,26 @@ def get_wallet_data_for_client( balance=self.balance_overview, balances_per_mint=balances, ) + + async def _proofs_to_serialized_token_v2(self, proofs: List[Proof], url: str): + """ + Ingests list of proofs and produces a serialized TokenV2 + """ + # and add url and keyset id to token + token: TokenV2 = await self._make_token(proofs, include_mints=False) + token.mints = [] + + # get keysets of proofs + keysets = list(set([p.id for p in proofs if p.id is not None])) + + # check whether we know the mint urls for these proofs + for k in keysets: + ks = await get_keyset(id=k, db=self.database) + url = ks.mint_url if ks and ks.mint_url else "" + + if not url: + Exception("mint url not found") + + token.mints.append(TokenV2Mint(url=url, ids=keysets)) + token_serialized = await self._serialize_token_base64(token) + return token_serialized diff --git a/app/cashu/router.py b/app/cashu/router.py index 0ded561..cb51d5f 100644 --- a/app/cashu/router.py +++ b/app/cashu/router.py @@ -1,15 +1,18 @@ import asyncio from typing import Union -from fastapi import APIRouter, Body, HTTPException, Query, status +from fastapi import APIRouter, Body, HTTPException, Query, Response, status import app.cashu.constants as c import app.cashu.docs as docs +from app.cashu.errors import CashuException, UntrustedMintException from app.cashu.models import ( CashuInfo, CashuMint, CashuMintInput, + CashuMintKeyInput, CashuPayEstimation, + CashuReceiveResult, CashuWalletBalance, CashuWalletData, ) @@ -222,23 +225,39 @@ async def cashu_mint_path( "/receive-tokens", name=f"{_PREFIX}.receive-tokens", summary="Receive Cashu tokens", - response_model=CashuWalletBalance, + response_model=CashuReceiveResult, + responses={ + status.HTTP_400_BAD_REQUEST: { + "description": "An error happened while receiving the tokens. See the error message for details.", + "model": CashuReceiveResult, + }, + status.HTTP_406_NOT_ACCEPTABLE: { + "description": "The mint is not trusted. Use the /update-mint-key endpoint to trust the mint with the given key.", + "model": CashuReceiveResult, + }, + }, # dependencies=[Depends(JWTBearer())], ) async def cashu_receive_path( + response: Response, coin: str = Body(..., description="The coins to receive."), lock: str = Body(None, description="Unlock coins."), - mint_name: Union[None, str] = Body( - None, - description=f"Name of the mint. Will use the pinned mint if empty.", - ), wallet_name: Union[None, str] = Body( None, description=f"Name of the wallet. Will use the pinned wallet if empty.", ), -) -> CashuWalletBalance: + trust_mint: bool = Body( + False, description="Automatically trust the mint if it is not trusted yet." + ), +) -> CashuReceiveResult: try: - return await service.receive(coin, lock, wallet_name, mint_name) + return await service.receive(coin, lock, wallet_name, trust_mint) + except UntrustedMintException as e: + response.status_code = status.HTTP_406_NOT_ACCEPTABLE + return CashuReceiveResult.from_exception(e) + except CashuException as e: + response.status_code = status.HTTP_400_BAD_REQUEST + return CashuReceiveResult.from_exception(e) except HTTPException: raise except NotImplementedError: diff --git a/app/cashu/service.py b/app/cashu/service.py index cc10f36..9d4a4dc 100644 --- a/app/cashu/service.py +++ b/app/cashu/service.py @@ -4,20 +4,24 @@ from os.path import isdir, join from typing import Union -from cashu.core.settings import DEBUG, MINT_URL, VERSION from fastapi import HTTPException +from loguru import logger import app.cashu.constants as c import app.cashu.exceptions as ce +from app.cashu.errors import CashuException from app.cashu.models import ( CashuInfo, CashuMint, CashuMintInput, + CashuMintKeyInput, CashuPayEstimation, + CashuReceiveResult, CashuWallet, CashuWalletBalance, CashuWalletData, ) +from app.external.cashu.core.settings import DEBUG, MINT_URL, VERSION from app.lightning.models import PaymentStatus from app.lightning.service import send_payment @@ -94,6 +98,16 @@ async def add_mint(self, mint_in: CashuMintInput) -> CashuMint: return m + async def update_mint_key(self, i: CashuMintKeyInput) -> bool: + w = self._resolve_wallet(i.wallet_name) + try: + # loading the mint with the updated key + # will also save it to the DB + await w.load_mint(i.key) + return True + except: + raise + async def list_mints(self) -> list[CashuMint]: # pretend doing a DB operation await asyncio.sleep(0.01) @@ -164,11 +178,19 @@ async def mint( self.add_mint(mint_in=CashuMintInput(url=mint_name)) wallet.url = mint_name - await wallet.load_mint() + try: + await wallet.load_mint() + except Exception as e: + logger.error(f"Error while loading mint {e.with_traceback(None)}") + raise HTTPException(status_code=500, detail=f"Error while loading mint {e}") wallet.status() # TODO: remove me, debug only - invoice = await wallet.request_mint(amount) + try: + invoice = await wallet.request_mint(amount) + except Exception as e: + logger.error(f"Error while requesting mint {e}") + raise HTTPException(status_code=500, detail=f"Error while loading mint {e}") res = await send_payment( pay_req=invoice.pr, timeout_seconds=5, fee_limit_msat=8000 @@ -187,24 +209,24 @@ async def mint( except Exception as e: # TODO: cashu wallet lib throws an Exception here => # submit PR with a more specific exception - raise HTTPException(status_code=500, detail="Error while minting {e}") + raise HTTPException(status_code=500, detail=f"Error while minting {e}") async def receive( self, - coin: str, + token: str, lock: str, - mint_name: Union[None, str], wallet_name: Union[None, str], - ) -> CashuWalletBalance: - wallet = self._resolve_wallet(wallet_name) + trust_mint: bool = False, + ) -> CashuReceiveResult: + wallet: CashuWallet = self._resolve_wallet(wallet_name) wallet.status() # TODO: remove me, debug only - await wallet.receive(coin, lock) + res = await wallet.receive(token, lock, trust_mint) wallet.status() # TODO: remove me, debug only - return wallet.balance_overview + return res async def pay( self, @@ -219,7 +241,9 @@ async def pay( res = await self.estimate_pay(invoice, mint_name, wallet_name) - _, send_proofs = await wallet.split_to_send(wallet.proofs, res.amount) + _, send_proofs = await wallet.split_to_send( + wallet.proofs, res.amount + ) # type:ignore await wallet.pay_lightning(send_proofs, invoice) wallet.status() diff --git a/poetry.lock b/poetry.lock index 9145b76..12036a8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -162,6 +162,37 @@ files = [ [package.dependencies] frozenlist = ">=1.1.0" +[[package]] +name = "aiosqlite" +version = "0.18.0" +description = "asyncio bridge to the standard sqlite3 module" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosqlite-0.18.0-py3-none-any.whl", hash = "sha256:c3511b841e3a2c5614900ba1d179f366826857586f78abd75e7cbeb88e75a557"}, + {file = "aiosqlite-0.18.0.tar.gz", hash = "sha256:faa843ef5fb08bafe9a9b3859012d3d9d6f77ce3637899de20606b7fc39aa213"}, +] + +[[package]] +name = "alembic" +version = "1.9.4" +description = "A database migration tool for SQLAlchemy." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "alembic-1.9.4-py3-none-any.whl", hash = "sha256:6f1c2207369bf4f49f952057a33bb017fbe5c148c2a773b46906b806ea6e825f"}, + {file = "alembic-1.9.4.tar.gz", hash = "sha256:4d3bd32ecdbb7bbfb48a9fe9e6d6fd6a831a1b59d03e26e292210237373e7db5"}, +] + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.3.0" + +[package.extras] +tz = ["python-dateutil"] + [[package]] name = "anyio" version = "3.6.2" @@ -218,7 +249,7 @@ tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy name = "bech32" version = "1.2.0" description = "Reference implementation for Bech32 and segwit addresses." -category = "main" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -230,7 +261,7 @@ files = [ name = "bitstring" version = "3.1.9" description = "Simple construction, analysis and modification of binary data." -category = "main" +category = "dev" optional = false python-versions = "*" files = [ @@ -284,68 +315,6 @@ d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] -[[package]] -name = "cashu" -version = "0.9.2" -description = "Ecash wallet and mint for Bitcoin Lightning" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "cashu-0.9.2-py3-none-any.whl", hash = "sha256:197e008d37c7df6cce36b316e4cc718d1821d45bc59955622ed011424ab0eaa3"}, - {file = "cashu-0.9.2.tar.gz", hash = "sha256:4a231022f40da3e5238714a5b4e5e9b915b11003860c50c8aa2bd4df4fbb72bb"}, -] - -[package.dependencies] -anyio = {version = "3.6.2", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -attrs = {version = "22.2.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -bech32 = {version = "1.2.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -bitstring = {version = "3.1.9", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -certifi = {version = "2022.12.7", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -cffi = {version = "1.15.1", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -charset-normalizer = {version = "2.0.12", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -click = {version = "8.0.4", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -colorama = {version = "0.4.6", markers = "python_version >= \"3.7\" and python_version < \"4.0\" and platform_system == \"Windows\" or python_version >= \"3.7\" and python_version < \"4.0\" and sys_platform == \"win32\""} -cryptography = {version = "36.0.2", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -ecdsa = {version = "0.18.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -environs = {version = "9.5.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -exceptiongroup = {version = "1.1.0", markers = "python_version >= \"3.7\" and python_version < \"3.11\""} -fastapi = {version = "0.83.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -h11 = {version = "0.12.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -idna = {version = "3.4", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -importlib-metadata = {version = "5.2.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -iniconfig = {version = "2.0.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -loguru = {version = "0.6.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -marshmallow = {version = "3.19.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -outcome = {version = "1.2.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -packaging = {version = "23.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -pluggy = {version = "1.0.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -pycparser = {version = "2.21", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -pycryptodomex = {version = "3.16.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -pydantic = {version = "1.10.4", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -pysocks = {version = "1.7.1", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -pytest = {version = "7.2.1", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -pytest-asyncio = {version = "0.19.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -python-bitcoinlib = {version = "0.11.2", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -python-dotenv = {version = "0.21.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -represent = {version = "1.6.0.post0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -requests = {version = "2.27.1", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -secp256k1 = {version = "0.14.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -setuptools = {version = "65.7.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -six = {version = "1.16.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -sniffio = {version = "1.3.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -sqlalchemy = {version = "1.3.24", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -sqlalchemy-aio = {version = "0.17.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -starlette = {version = "0.19.1", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -tomli = {version = "2.0.1", markers = "python_version >= \"3.7\" and python_version < \"3.11\""} -typing-extensions = {version = "4.4.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -urllib3 = {version = "1.26.14", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -uvicorn = {version = "0.18.3", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -websocket-client = {version = "1.3.3", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -wheel = {version = "0.38.4", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -win32-setctime = {version = "1.1.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\" and sys_platform == \"win32\""} -zipp = {version = "3.11.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} - [[package]] name = "cchardet" version = "2.1.7" @@ -595,7 +564,7 @@ toml = ["tomli"] name = "cryptography" version = "36.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -684,7 +653,7 @@ files = [ name = "ecdsa" version = "0.18.0" description = "ECDSA cryptographic signature library (pure python)" -category = "main" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -703,7 +672,7 @@ gmpy2 = ["gmpy2"] name = "environs" version = "9.5.0" description = "simplified environment variable parsing" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -725,7 +694,7 @@ tests = ["dj-database-url", "dj-email-url", "django-cache-url", "pytest"] name = "exceptiongroup" version = "1.1.0" description = "Backport of PEP 654 (exception groups)" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -902,6 +871,80 @@ protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.1 || >4.21.1,<4 [package.extras] grpc = ["grpcio (>=1.44.0,<2.0.0dev)"] +[[package]] +name = "greenlet" +version = "2.0.2" +description = "Lightweight in-process concurrent programming" +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +files = [ + {file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"}, + {file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"}, + {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, + {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, + {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, + {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, + {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, + {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, + {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470"}, + {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a"}, + {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, + {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, + {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, + {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, + {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, + {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, + {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19"}, + {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3"}, + {file = "greenlet-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5"}, + {file = "greenlet-2.0.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6"}, + {file = "greenlet-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43"}, + {file = "greenlet-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a"}, + {file = "greenlet-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394"}, + {file = "greenlet-2.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75"}, + {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf"}, + {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292"}, + {file = "greenlet-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9"}, + {file = "greenlet-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f"}, + {file = "greenlet-2.0.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73"}, + {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86"}, + {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33"}, + {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, + {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, + {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857"}, + {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a"}, + {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, + {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, + {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, + {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b"}, + {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8"}, + {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9"}, + {file = "greenlet-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5"}, + {file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"}, + {file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"}, +] + +[package.extras] +docs = ["Sphinx", "docutils (<0.18)"] +test = ["objgraph", "psutil"] + [[package]] name = "grpcio" version = "1.50.0" @@ -1165,7 +1208,7 @@ files = [ name = "importlib-metadata" version = "5.2.0" description = "Read metadata from Python packages" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1185,7 +1228,7 @@ testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packag name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1230,11 +1273,91 @@ win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} [package.extras] dev = ["Sphinx (>=4.1.1)", "black (>=19.10b0)", "colorama (>=0.3.4)", "docutils (==0.16)", "flake8 (>=3.7.7)", "isort (>=5.1.1)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)"] +[[package]] +name = "mako" +version = "1.2.4" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"}, + {file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"}, +] + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua"] +testing = ["pytest"] + +[[package]] +name = "markupsafe" +version = "2.1.2" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, + {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, +] + [[package]] name = "marshmallow" version = "3.19.0" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1377,21 +1500,6 @@ files = [ [package.extras] dev = ["black", "mypy", "pytest"] -[[package]] -name = "outcome" -version = "1.2.0" -description = "Capture the outcome of Python function calls." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "outcome-1.2.0-py2.py3-none-any.whl", hash = "sha256:c4ab89a56575d6d38a05aa16daeaa333109c1f96167aba8901ab18b6b5e0f7f5"}, - {file = "outcome-1.2.0.tar.gz", hash = "sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672"}, -] - -[package.dependencies] -attrs = ">=19.2.0" - [[package]] name = "packaging" version = "23.0" @@ -1436,7 +1544,7 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytes name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1470,26 +1578,25 @@ virtualenv = ">=20.0.8" [[package]] name = "protobuf" -version = "4.21.12" +version = "4.22.0" description = "" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "protobuf-4.21.12-cp310-abi3-win32.whl", hash = "sha256:b135410244ebe777db80298297a97fbb4c862c881b4403b71bac9d4107d61fd1"}, - {file = "protobuf-4.21.12-cp310-abi3-win_amd64.whl", hash = "sha256:89f9149e4a0169cddfc44c74f230d7743002e3aa0b9472d8c28f0388102fc4c2"}, - {file = "protobuf-4.21.12-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:299ea899484ee6f44604deb71f424234f654606b983cb496ea2a53e3c63ab791"}, - {file = "protobuf-4.21.12-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:d1736130bce8cf131ac7957fa26880ca19227d4ad68b4888b3be0dea1f95df97"}, - {file = "protobuf-4.21.12-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:78a28c9fa223998472886c77042e9b9afb6fe4242bd2a2a5aced88e3f4422aa7"}, - {file = "protobuf-4.21.12-cp37-cp37m-win32.whl", hash = "sha256:3d164928ff0727d97022957c2b849250ca0e64777ee31efd7d6de2e07c494717"}, - {file = "protobuf-4.21.12-cp37-cp37m-win_amd64.whl", hash = "sha256:f45460f9ee70a0ec1b6694c6e4e348ad2019275680bd68a1d9314b8c7e01e574"}, - {file = "protobuf-4.21.12-cp38-cp38-win32.whl", hash = "sha256:6ab80df09e3208f742c98443b6166bcb70d65f52cfeb67357d52032ea1ae9bec"}, - {file = "protobuf-4.21.12-cp38-cp38-win_amd64.whl", hash = "sha256:1f22ac0ca65bb70a876060d96d914dae09ac98d114294f77584b0d2644fa9c30"}, - {file = "protobuf-4.21.12-cp39-cp39-win32.whl", hash = "sha256:27f4d15021da6d2b706ddc3860fac0a5ddaba34ab679dc182b60a8bb4e1121cc"}, - {file = "protobuf-4.21.12-cp39-cp39-win_amd64.whl", hash = "sha256:237216c3326d46808a9f7c26fd1bd4b20015fb6867dc5d263a493ef9a539293b"}, - {file = "protobuf-4.21.12-py2.py3-none-any.whl", hash = "sha256:a53fd3f03e578553623272dc46ac2f189de23862e68565e83dde203d41b76fc5"}, - {file = "protobuf-4.21.12-py3-none-any.whl", hash = "sha256:b98d0148f84e3a3c569e19f52103ca1feacdac0d2df8d6533cf983d1fda28462"}, - {file = "protobuf-4.21.12.tar.gz", hash = "sha256:7cd532c4566d0e6feafecc1059d04c7915aec8e182d1cf7adee8b24ef1e2e6ab"}, + {file = "protobuf-4.22.0-cp310-abi3-win32.whl", hash = "sha256:b2fea9dc8e3c0f32c38124790ef16cba2ee0628fe2022a52e435e1117bfef9b1"}, + {file = "protobuf-4.22.0-cp310-abi3-win_amd64.whl", hash = "sha256:a33a273d21852f911b8bda47f39f4383fe7c061eb1814db2c76c9875c89c2491"}, + {file = "protobuf-4.22.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:e894e9ae603e963f0842498c4cd5d39c6a60f0d7e4c103df50ee939564298658"}, + {file = "protobuf-4.22.0-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:7c535d126e7dcc714105ab20b418c4fedbd28f8b8afc42b7350b1e317bbbcc71"}, + {file = "protobuf-4.22.0-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:86c3d20428b007537ba6792b475c0853bba7f66b1f60e610d913b77d94b486e4"}, + {file = "protobuf-4.22.0-cp37-cp37m-win32.whl", hash = "sha256:1669cb7524221a8e2d9008d0842453dbefdd0fcdd64d67672f657244867635fb"}, + {file = "protobuf-4.22.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ab4d043865dd04e6b09386981fe8f80b39a1e46139fb4a3c206229d6b9f36ff6"}, + {file = "protobuf-4.22.0-cp38-cp38-win32.whl", hash = "sha256:29288813aacaa302afa2381db1d6e0482165737b0afdf2811df5fa99185c457b"}, + {file = "protobuf-4.22.0-cp38-cp38-win_amd64.whl", hash = "sha256:e474b63bab0a2ea32a7b26a4d8eec59e33e709321e5e16fb66e766b61b82a95e"}, + {file = "protobuf-4.22.0-cp39-cp39-win32.whl", hash = "sha256:47d31bdf58222dd296976aa1646c68c6ee80b96d22e0a3c336c9174e253fd35e"}, + {file = "protobuf-4.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:c27f371f0159feb70e6ea52ed7e768b3f3a4c5676c1900a7e51a24740381650e"}, + {file = "protobuf-4.22.0-py3-none-any.whl", hash = "sha256:c3325803095fb4c2a48649c321d2fbde59f8fbfcb9bfc7a86df27d112831c571"}, + {file = "protobuf-4.22.0.tar.gz", hash = "sha256:652d8dfece122a24d98eebfef30e31e455d300efa41999d1182e015984ac5930"}, ] [[package]] @@ -1559,7 +1666,7 @@ files = [ name = "pycryptodomex" version = "3.16.0" description = "Cryptographic library for Python" -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1666,7 +1773,7 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] name = "pysocks" version = "1.7.1" description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1679,7 +1786,7 @@ files = [ name = "pytest" version = "7.2.1" description = "pytest: simple powerful testing with Python" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1703,7 +1810,7 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2. name = "pytest-asyncio" version = "0.19.0" description = "Pytest support for asyncio" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1721,7 +1828,7 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy name = "python-bitcoinlib" version = "0.11.2" description = "The Swiss Army Knife of the Bitcoin protocol." -category = "main" +category = "dev" optional = false python-versions = "*" files = [ @@ -1745,7 +1852,7 @@ files = [ name = "python-dotenv" version = "0.21.0" description = "Read key-value pairs from a .env file and set them as environment variables" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1920,24 +2027,6 @@ packaging = ">=20.4" hiredis = ["hiredis (>=1.0.0)"] ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] -[[package]] -name = "represent" -version = "1.6.0.post0" -description = "Create __repr__ automatically or declaratively." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "Represent-1.6.0.post0-py2.py3-none-any.whl", hash = "sha256:99142650756ef1998ce0661568f54a47dac8c638fb27e3816c02536575dbba8c"}, - {file = "Represent-1.6.0.post0.tar.gz", hash = "sha256:026c0de2ee8385d1255b9c2426cd4f03fe9177ac94c09979bc601946c8493aa0"}, -] - -[package.dependencies] -six = ">=1.8.0" - -[package.extras] -test = ["ipython", "mock", "pytest (>=3.0.5)"] - [[package]] name = "requests" version = "2.27.1" @@ -1964,7 +2053,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<5)"] name = "secp256k1" version = "0.14.0" description = "FFI bindings to libsecp256k1" -category = "main" +category = "dev" optional = false python-versions = "*" files = [ @@ -2039,81 +2128,71 @@ files = [ [[package]] name = "sqlalchemy" -version = "1.3.24" +version = "2.0.4" description = "Database Abstraction Library" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-1.3.24-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:87a2725ad7d41cd7376373c15fd8bf674e9c33ca56d0b8036add2d634dba372e"}, - {file = "SQLAlchemy-1.3.24-cp27-cp27m-win32.whl", hash = "sha256:f597a243b8550a3a0b15122b14e49d8a7e622ba1c9d29776af741f1845478d79"}, - {file = "SQLAlchemy-1.3.24-cp27-cp27m-win_amd64.whl", hash = "sha256:fc4cddb0b474b12ed7bdce6be1b9edc65352e8ce66bc10ff8cbbfb3d4047dbf4"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:f1149d6e5c49d069163e58a3196865e4321bad1803d7886e07d8710de392c548"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:14f0eb5db872c231b20c18b1e5806352723a3a89fb4254af3b3e14f22eaaec75"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:e98d09f487267f1e8d1179bf3b9d7709b30a916491997137dd24d6ae44d18d79"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:fc1f2a5a5963e2e73bac4926bdaf7790c4d7d77e8fc0590817880e22dd9d0b8b"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-win32.whl", hash = "sha256:f3c5c52f7cb8b84bfaaf22d82cb9e6e9a8297f7c2ed14d806a0f5e4d22e83fb7"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-win_amd64.whl", hash = "sha256:0352db1befcbed2f9282e72843f1963860bf0e0472a4fa5cf8ee084318e0e6ab"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:2ed6343b625b16bcb63c5b10523fd15ed8934e1ed0f772c534985e9f5e73d894"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:34fcec18f6e4b24b4a5f6185205a04f1eab1e56f8f1d028a2a03694ebcc2ddd4"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e47e257ba5934550d7235665eee6c911dc7178419b614ba9e1fbb1ce6325b14f"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:816de75418ea0953b5eb7b8a74933ee5a46719491cd2b16f718afc4b291a9658"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-win32.whl", hash = "sha256:26155ea7a243cbf23287f390dba13d7927ffa1586d3208e0e8d615d0c506f996"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-win_amd64.whl", hash = "sha256:f03bd97650d2e42710fbe4cf8a59fae657f191df851fc9fc683ecef10746a375"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a006d05d9aa052657ee3e4dc92544faae5fcbaafc6128217310945610d862d39"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1e2f89d2e5e3c7a88e25a3b0e43626dba8db2aa700253023b82e630d12b37109"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0d5d862b1cfbec5028ce1ecac06a3b42bc7703eb80e4b53fceb2738724311443"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:0172423a27fbcae3751ef016663b72e1a516777de324a76e30efa170dbd3dd2d"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-win32.whl", hash = "sha256:d37843fb8df90376e9e91336724d78a32b988d3d20ab6656da4eb8ee3a45b63c"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-win_amd64.whl", hash = "sha256:c10ff6112d119f82b1618b6dc28126798481b9355d8748b64b9b55051eb4f01b"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:861e459b0e97673af6cc5e7f597035c2e3acdfb2608132665406cded25ba64c7"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5de2464c254380d8a6c20a2746614d5a436260be1507491442cf1088e59430d2"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d375d8ccd3cebae8d90270f7aa8532fe05908f79e78ae489068f3b4eee5994e8"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:014ea143572fee1c18322b7908140ad23b3994036ef4c0d630110faf942652f8"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-win32.whl", hash = "sha256:6607ae6cd3a07f8a4c3198ffbf256c261661965742e2b5265a77cd5c679c9bba"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-win_amd64.whl", hash = "sha256:fcb251305fa24a490b6a9ee2180e5f8252915fb778d3dafc70f9cc3f863827b9"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:01aa5f803db724447c1d423ed583e42bf5264c597fd55e4add4301f163b0be48"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4d0e3515ef98aa4f0dc289ff2eebb0ece6260bbf37c2ea2022aad63797eacf60"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:bce28277f308db43a6b4965734366f533b3ff009571ec7ffa583cb77539b84d6"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:8110e6c414d3efc574543109ee618fe2c1f96fa31833a1ff36cc34e968c4f233"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-win32.whl", hash = "sha256:ee5f5188edb20a29c1cc4a039b074fdc5575337c9a68f3063449ab47757bb064"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-win_amd64.whl", hash = "sha256:09083c2487ca3c0865dc588e07aeaa25416da3d95f7482c07e92f47e080aa17b"}, - {file = "SQLAlchemy-1.3.24.tar.gz", hash = "sha256:ebbb777cbf9312359b897bf81ba00dae0f5cb69fba2a18265dcc18a6f5ef7519"}, + {file = "SQLAlchemy-2.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b67d6e626caa571fb53accaac2fba003ef4f7317cb3481e9ab99dad6e89a70d6"}, + {file = "SQLAlchemy-2.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b01dce097cf6f145da131a53d4cce7f42e0bfa9ae161dd171a423f7970d296d0"}, + {file = "SQLAlchemy-2.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6363697c938b9a13e07f1bc2cd433502a7aa07efd55b946b31d25b9449890621"}, + {file = "SQLAlchemy-2.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:011ef3c33f30bae5637c575f30647e0add98686642d237f0c3a1e3d9b35747fa"}, + {file = "SQLAlchemy-2.0.4-cp310-cp310-win32.whl", hash = "sha256:c1e8edc49b32483cd5d2d015f343e16be7dfab89f4aaf66b0fa6827ab356880d"}, + {file = "SQLAlchemy-2.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:77a380bf8721b416782c763e0ff66f80f3b05aee83db33ddfc0eac20bcb6791f"}, + {file = "SQLAlchemy-2.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a2f9120eb32190bdba31d1022181ef08f257aed4f984f3368aa4e838de72bc0"}, + {file = "SQLAlchemy-2.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:679b9bd10bb32b8d3befed4aad4356799b6ec1bdddc0f930a79e41ba5b084124"}, + {file = "SQLAlchemy-2.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c82395e2925639e6d320592943608070678e7157bd1db2672a63be9c7889434"}, + {file = "SQLAlchemy-2.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9946ee503962859f1a9e1ad17dff0859269b0cb453686747fe87f00b0e030b34"}, + {file = "SQLAlchemy-2.0.4-cp311-cp311-win32.whl", hash = "sha256:c621f05859caed5c0aab032888a3d3bde2cae3988ca151113cbecf262adad976"}, + {file = "SQLAlchemy-2.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:662a79e80f3e9fe33b7861c19fedf3d8389fab2413c04bba787e3f1139c22188"}, + {file = "SQLAlchemy-2.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3f927340b37fe65ec42e19af7ce15260a73e11c6b456febb59009bfdfec29a35"}, + {file = "SQLAlchemy-2.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1644c603558590f465b3fa16e4557d87d3962bc2c81fd7ea85b582ecf4676b31"}, + {file = "SQLAlchemy-2.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8a88b32ce5b69d18507ffc9f10401833934ebc353c7b30d1e056023c64f0a736"}, + {file = "SQLAlchemy-2.0.4-cp37-cp37m-win32.whl", hash = "sha256:2267c004e78e291bba0dc766a9711c389649cf3e662cd46eec2bc2c238c637bd"}, + {file = "SQLAlchemy-2.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:59cf0cdb29baec4e074c7520d7226646a8a8f856b87d8300f3e4494901d55235"}, + {file = "SQLAlchemy-2.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dd801375f19a6e1f021dabd8b1714f2fdb91cbc835cd13b5dd0bd7e9860392d7"}, + {file = "SQLAlchemy-2.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8efdda920988bcade542f53a2890751ff680474d548f32df919a35a21404e3f"}, + {file = "SQLAlchemy-2.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d05773d5c79f2d3371d81697d54ee1b2c32085ad434ce9de4482e457ecb018"}, + {file = "SQLAlchemy-2.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ff0a7c669ec7cdb899eae7e622211c2dd8725b82655db2b41740d39e3cda466"}, + {file = "SQLAlchemy-2.0.4-cp38-cp38-win32.whl", hash = "sha256:57dcd9eed52413f7270b22797aa83c71b698db153d1541c1e83d45ecdf8e95e7"}, + {file = "SQLAlchemy-2.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:54aa9f40d88728dd058e951eeb5ecc55241831ba4011e60c641738c1da0146b7"}, + {file = "SQLAlchemy-2.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:817aab80f7e8fe581696dae7aaeb2ceb0b7ea70ad03c95483c9115970d2a9b00"}, + {file = "SQLAlchemy-2.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc7b9f55c2f72c13b2328b8a870ff585c993ba1b5c155ece5c9d3216fa4b18f6"}, + {file = "SQLAlchemy-2.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce54965a94673a0ebda25e7c3a05bf1aa74fd78cc452a1a710b704bf73fb8402"}, + {file = "SQLAlchemy-2.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b5deafb4901618b3f98e8df7099cd11edd0d1e6856912647e28968b803de0dae"}, + {file = "SQLAlchemy-2.0.4-cp39-cp39-win32.whl", hash = "sha256:81f1ea264278fcbe113b9a5840f13a356cb0186e55b52168334124f1cd1bc495"}, + {file = "SQLAlchemy-2.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:954f1ad73b78ea5ba5a35c89c4a5dfd0f3a06c17926503de19510eb9b3857bde"}, + {file = "SQLAlchemy-2.0.4-py3-none-any.whl", hash = "sha256:0adca8a3ca77234a142c5afed29322fb501921f13d1d5e9fa4253450d786c160"}, + {file = "SQLAlchemy-2.0.4.tar.gz", hash = "sha256:95a18e1a6af2114dbd9ee4f168ad33070d6317e11bafa28d983cc7b585fe900b"}, ] +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} +typing-extensions = ">=4.2.0" + [package.extras] +aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] mssql = ["pyodbc"] mssql-pymssql = ["pymssql"] mssql-pyodbc = ["pyodbc"] -mysql = ["mysqlclient"] -oracle = ["cx_oracle"] -postgresql = ["psycopg2"] -postgresql-pg8000 = ["pg8000 (<1.16.6)"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx-oracle (>=7)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] postgresql-psycopg2binary = ["psycopg2-binary"] postgresql-psycopg2cffi = ["psycopg2cffi"] -pymysql = ["pymysql", "pymysql (<1)"] - -[[package]] -name = "sqlalchemy-aio" -version = "0.17.0" -description = "Async support for SQLAlchemy." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "sqlalchemy_aio-0.17.0-py3-none-any.whl", hash = "sha256:3f4aa392c38f032d6734826a4138a0f02ed3122d442ed142be1e5964f2a33b60"}, - {file = "sqlalchemy_aio-0.17.0.tar.gz", hash = "sha256:f531c7982662d71dfc0b117e77bb2ed544e25cd5361e76cf9f5208edcfb71f7b"}, -] - -[package.dependencies] -outcome = "*" -represent = ">=1.4" -sqlalchemy = "<1.4" - -[package.extras] -test = ["pytest (>=5.4)", "pytest-asyncio (>=0.14)", "pytest-trio (>=0.6)"] -test-noextras = ["pytest (>=5.4)", "pytest-asyncio (>=0.14)"] -trio = ["trio (>=0.15)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3-binary"] [[package]] name = "starlette" @@ -2165,7 +2244,7 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2246,7 +2325,7 @@ test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess name = "websocket-client" version = "1.3.3" description = "WebSocket client for Python with low level API options" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2259,21 +2338,6 @@ docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] optional = ["python-socks", "wsaccel"] test = ["websockets"] -[[package]] -name = "wheel" -version = "0.38.4" -description = "A built-package format for Python" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "wheel-0.38.4-py3-none-any.whl", hash = "sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8"}, - {file = "wheel-0.38.4.tar.gz", hash = "sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac"}, -] - -[package.extras] -test = ["pytest (>=3.0.0)"] - [[package]] name = "win32-setctime" version = "1.1.0" @@ -2455,7 +2519,7 @@ multidict = ">=4.0" name = "zipp" version = "3.11.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2470,4 +2534,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "cee2c8ffdbc2c7d3c48596304f26c49d05e3592bc999364bea290fe27fb3d16c" +content-hash = "685f75ca97362c21968beaf678b4b62ae4697e15922e38fd3cd239ef463af8e4" diff --git a/pyproject.toml b/pyproject.toml index 3e8923a..f0746ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,8 +26,24 @@ grpcio-tools = "1.50.0" googleapis-common-protos = "1.57.0" protobuf = "^4.21.9" deepdiff = "5.8.1" -cashu = "^0.9.2" loguru = "^0.6.0" +sqlalchemy = {extras = ["asyncio"], version = "^2.0.4"} +alembic = "^1.9.4" +aiosqlite = "^0.18.0" + +[tool.poetry.group.cashu.dependencies] +bech32 = "1.2.0" +environs = "9.5.0" +ecdsa = "0.18.0" +bitstring = "3.1.9" +secp256k1 = "0.14.0" +python-bitcoinlib = "0.11.2" +h11 = "0.12.0" +PySocks = "1.7.1" +cryptography = "36.0.2" +websocket-client = "1.3.3" +pycryptodomex = "3.16.0" +importlib-metadata = "5.2.0" [tool.poetry.group.dev.dependencies] black = "22.10.0" From 68de726363ed79d9b480ef442cd387b676e984fc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Mar 2023 18:43:21 +0000 Subject: [PATCH 6/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- app/external/cashu/core/bolt11.py | 1 - app/external/cashu/core/split.py | 2 +- app/external/cashu/lightning/lnbits.py | 1 - app/external/cashu/wallet/crud.py | 1 - 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/external/cashu/core/bolt11.py b/app/external/cashu/core/bolt11.py index 890ea7e..b1fecc2 100644 --- a/app/external/cashu/core/bolt11.py +++ b/app/external/cashu/core/bolt11.py @@ -184,7 +184,6 @@ def lnencode(addr, privkey): tags_set = set() for k, v in addr.tags: - # BOLT #11: # # A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields, diff --git a/app/external/cashu/core/split.py b/app/external/cashu/core/split.py index 44b9cf5..b523299 100644 --- a/app/external/cashu/core/split.py +++ b/app/external/cashu/core/split.py @@ -2,7 +2,7 @@ def amount_split(amount): """Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8].""" bits_amt = bin(amount)[::-1][:-2] rv = [] - for (pos, bit) in enumerate(bits_amt): + for pos, bit in enumerate(bits_amt): if bit == "1": rv.append(2**pos) return rv diff --git a/app/external/cashu/lightning/lnbits.py b/app/external/cashu/lightning/lnbits.py index 5ccd0ea..da23b5f 100644 --- a/app/external/cashu/lightning/lnbits.py +++ b/app/external/cashu/lightning/lnbits.py @@ -105,7 +105,6 @@ async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse async def get_invoice_status(self, checking_id: str) -> PaymentStatus: try: - r = self.s.get( url=f"{self.endpoint}/api/v1/payments/{checking_id}", headers=self.key, diff --git a/app/external/cashu/wallet/crud.py b/app/external/cashu/wallet/crud.py index bb72648..bb85e22 100644 --- a/app/external/cashu/wallet/crud.py +++ b/app/external/cashu/wallet/crud.py @@ -138,7 +138,6 @@ async def store_p2sh(db: Database, p2sh: P2SHScript): async def get_unused_locks(db: Database, address: str = None): - clause: List[str] = [] values: dict[str, Any] = {} From e8b4d6c228c2a2f4f593582712fd192d5690b845 Mon Sep 17 00:00:00 2001 From: fusion44 Date: Sat, 4 Mar 2023 07:55:54 +0100 Subject: [PATCH 7/7] [pre-commit.ci] pre-commit autoupdate (#187) (#189) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/psf/black: 22.12.0 → 23.1.0](https://github.com/psf/black/compare/22.12.0...23.1.0) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- app/api/utils.py | 1 - app/apps/impl/raspiblitz.py | 4 ---- app/bitcoind/models.py | 1 - app/external/sse_starlette/sse_starlette.py | 1 - app/lightning/impl/lnd_grpc.py | 3 --- app/lightning/impl/protos/cln/node_pb2.py | 1 - app/lightning/impl/protos/cln/primitives_pb2.py | 1 - app/lightning/impl/protos/lnd/lightning_pb2.py | 1 - app/lightning/impl/protos/lnd/router_pb2.py | 1 - app/lightning/impl/protos/lnd/signer_pb2.py | 1 - app/lightning/impl/protos/lnd/walletunlocker_pb2.py | 1 - app/lightning/models.py | 1 - app/lightning/service.py | 2 -- app/main.py | 3 --- app/setup/impl/raspiblitz/router.py | 6 +----- app/system/impl/raspiblitz.py | 3 --- app/system/models.py | 1 - 18 files changed, 2 insertions(+), 32 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 60f0f4b..ece245d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: debug-statements - id: mixed-line-ending - repo: https://github.com/psf/black - rev: 22.12.0 + rev: 23.1.0 hooks: - id: black - repo: https://github.com/pycqa/isort diff --git a/app/api/utils.py b/app/api/utils.py index 06bc925..af99f75 100644 --- a/app/api/utils.py +++ b/app/api/utils.py @@ -114,7 +114,6 @@ class _PushID(object): ) def __init__(self): - # Timestamp of last push, used to prevent local collisions if you # push twice in one ms. self.last_push_time = 0 diff --git a/app/apps/impl/raspiblitz.py b/app/apps/impl/raspiblitz.py index 2940d5c..32cd1fc 100644 --- a/app/apps/impl/raspiblitz.py +++ b/app/apps/impl/raspiblitz.py @@ -32,7 +32,6 @@ class RaspiBlitzApps(AppsBase): async def get_app_status_single(self, app_id): - if app_id not in available_app_ids: return { "id": f"{app_id}", @@ -126,7 +125,6 @@ async def get_app_status_single(self, app_id): async def get_app_status(self): appStatusList: List = [] for appID in available_app_ids: - # skip app based on node running if node_type == "" or node_type == "none": if appID == "rtl": @@ -200,7 +198,6 @@ async def uninstall_app_sub(self, app_id: str, delete_data: bool): return jsonable_encoder({"id": app_id}) async def run_bonus_script(self, app_id: str, params: str): - # to satisfy CodeQL: test again against predefined array and don't use 'user value' tested_app_id = "" for id in available_app_ids: @@ -275,7 +272,6 @@ async def run_bonus_script(self, app_id: str, params: str): ) # nothing above consider success else: - # check if script was effective updatedAppData = await self.get_app_status_single(app_id) diff --git a/app/bitcoind/models.py b/app/bitcoind/models.py index 806c4c1..87d59ef 100644 --- a/app/bitcoind/models.py +++ b/app/bitcoind/models.py @@ -327,7 +327,6 @@ class BlockchainInfo(BaseModel): @classmethod def from_rpc(cls, r): - # get softfork information if available softforks = [] if "softforks" in r: diff --git a/app/external/sse_starlette/sse_starlette.py b/app/external/sse_starlette/sse_starlette.py index 3cece2a..3514165 100644 --- a/app/external/sse_starlette/sse_starlette.py +++ b/app/external/sse_starlette/sse_starlette.py @@ -208,7 +208,6 @@ async def stream_response(self, send) -> None: async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: async with anyio.create_task_group() as task_group: - # https://trio.readthedocs.io/en/latest/reference-core.html#custom-supervisors async def wrap(func: Callable[[], Coroutine[None, None, None]]) -> None: await func() diff --git a/app/lightning/impl/lnd_grpc.py b/app/lightning/impl/lnd_grpc.py index 94e68c2..d893f87 100644 --- a/app/lightning/impl/lnd_grpc.py +++ b/app/lightning/impl/lnd_grpc.py @@ -788,7 +788,6 @@ async def peer_resolve_alias(self, node_pub: str) -> str: # get fresh list of peers and their aliases try: - request = ln.NodeInfoRequest(pub_key=node_pub, include_channels=False) response = await self._lnd_stub.GetNodeInfo(request) return str(response.node.alias) @@ -802,7 +801,6 @@ async def channel_list(self) -> List[Channel]: logging.debug(f"LND_GRPC: channel_list()") try: - request = ln.ListChannelsRequest() response = await self._lnd_stub.ListChannels(request) @@ -839,7 +837,6 @@ async def channel_close(self, channel_id: int, force_close: bool) -> str: raise ValueError("channel_id must contain : for lnd") try: - funding_txid = channel_id.split(":")[0] output_index = channel_id.split(":")[1] diff --git a/app/lightning/impl/protos/cln/node_pb2.py b/app/lightning/impl/protos/cln/node_pb2.py index 10589ea..a7e32b2 100644 --- a/app/lightning/impl/protos/cln/node_pb2.py +++ b/app/lightning/impl/protos/cln/node_pb2.py @@ -1660,7 +1660,6 @@ _NODE = DESCRIPTOR.services_by_name["Node"] if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None _GETINFOREQUEST._serialized_start = 37 _GETINFOREQUEST._serialized_end = 53 diff --git a/app/lightning/impl/protos/cln/primitives_pb2.py b/app/lightning/impl/protos/cln/primitives_pb2.py index d822744..bb64496 100644 --- a/app/lightning/impl/protos/cln/primitives_pb2.py +++ b/app/lightning/impl/protos/cln/primitives_pb2.py @@ -158,7 +158,6 @@ _sym_db.RegisterMessage(RoutehintList) if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None _CHANNELSIDE._serialized_start = 632 _CHANNELSIDE._serialized_end = 662 diff --git a/app/lightning/impl/protos/lnd/lightning_pb2.py b/app/lightning/impl/protos/lnd/lightning_pb2.py index 9e46939..57c1a5b 100644 --- a/app/lightning/impl/protos/lnd/lightning_pb2.py +++ b/app/lightning/impl/protos/lnd/lightning_pb2.py @@ -19,7 +19,6 @@ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "lightning_pb2", globals()) if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b"Z%github.com/lightningnetwork/lnd/lnrpc" _TRANSACTION.fields_by_name["dest_addresses"]._options = None diff --git a/app/lightning/impl/protos/lnd/router_pb2.py b/app/lightning/impl/protos/lnd/router_pb2.py index 8ee692a..dd6f754 100644 --- a/app/lightning/impl/protos/lnd/router_pb2.py +++ b/app/lightning/impl/protos/lnd/router_pb2.py @@ -21,7 +21,6 @@ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "routerrpc.router_pb2", globals()) if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None DESCRIPTOR._serialized_options = ( b"Z/github.com/lightningnetwork/lnd/lnrpc/routerrpc" diff --git a/app/lightning/impl/protos/lnd/signer_pb2.py b/app/lightning/impl/protos/lnd/signer_pb2.py index 3d046e0..0597a03 100644 --- a/app/lightning/impl/protos/lnd/signer_pb2.py +++ b/app/lightning/impl/protos/lnd/signer_pb2.py @@ -19,7 +19,6 @@ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "signrpc.signer_pb2", globals()) if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b"Z-github.com/lightningnetwork/lnd/lnrpc/signrpc" _SHAREDKEYREQUEST.fields_by_name["key_loc"]._options = None diff --git a/app/lightning/impl/protos/lnd/walletunlocker_pb2.py b/app/lightning/impl/protos/lnd/walletunlocker_pb2.py index b308093..38dcfaf 100644 --- a/app/lightning/impl/protos/lnd/walletunlocker_pb2.py +++ b/app/lightning/impl/protos/lnd/walletunlocker_pb2.py @@ -21,7 +21,6 @@ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "walletunlocker_pb2", globals()) if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b"Z%github.com/lightningnetwork/lnd/lnrpc" _GENSEEDREQUEST._serialized_start = 48 diff --git a/app/lightning/models.py b/app/lightning/models.py index 840d1cb..c08541d 100644 --- a/app/lightning/models.py +++ b/app/lightning/models.py @@ -390,7 +390,6 @@ def from_cln_json(cls, h) -> "RouteHint": class Channel(BaseModel): - channel_id: Optional[str] active: Optional[bool] diff --git a/app/lightning/service.py b/app/lightning/service.py index 8cb6df3..06cbbf5 100644 --- a/app/lightning/service.py +++ b/app/lightning/service.py @@ -139,7 +139,6 @@ async def send_payment( async def channel_open( local_funding_amount: int, node_URI: str, target_confs: int ) -> str: - if local_funding_amount < 1: raise ValueError("funding amount needs to be positive") @@ -192,7 +191,6 @@ async def register_lightning_listener(): """ try: - if ln_node == "none": logger.info( "SKIPPING register_lightning_listener -> no lightning configured" diff --git a/app/main.py b/app/main.py index 5ae7e1d..c6b080d 100644 --- a/app/main.py +++ b/app/main.py @@ -284,10 +284,8 @@ async def warmup_new_connections(): is_ready = api_startup_status.is_fully_initialized() if is_ready: - # when lightning is active if node_type != "" and node_type != "none": - res = await get_full_client_warmup_data() for id in new_connections: await asyncio.gather( @@ -305,7 +303,6 @@ async def warmup_new_connections(): # when its bitcoin only else: - res = await get_full_client_warmup_data_bitcoinonly() for id in new_connections: await asyncio.gather( diff --git a/app/setup/impl/raspiblitz/router.py b/app/setup/impl/raspiblitz/router.py index 08ec9c9..f8a9e93 100644 --- a/app/setup/impl/raspiblitz/router.py +++ b/app/setup/impl/raspiblitz/router.py @@ -15,6 +15,7 @@ setupFilePath = "/var/cache/raspiblitz/temp/raspiblitz.setup" configFilePath = "/mnt/hdd/raspiblitz.conf" + # can always be called without credentials to check if # the system needs or is in setup (setupPhase!="done") # for example in the beginning setupPhase can be (see controlSetupDialog.sh) @@ -56,7 +57,6 @@ async def get_status(): @router.get("/setup-start-info") async def setup_start_info(): - # first check that node is really in setup state setupPhase = await redis_get("setupPhase") state = await redis_get("state") @@ -99,7 +99,6 @@ class StartDoneData(BaseModel): # With all this info the WebUi can run its own runs its dialogs and in the end makes a call to @router.post("/setup-start-done") async def setup_start_done(data: StartDoneData): - # first check that node is really in setup state setupPhase = await redis_get("setupPhase") state = await redis_get("state") @@ -260,7 +259,6 @@ async def setup_final_info(): # When WebUI displayed seed words & user confirmed write the calls: @router.post("/setup-final-done", dependencies=[Depends(JWTBearer())]) async def setup_final_done(): - # first check that node is really in setup state setupPhase = await redis_get("setupPhase") state = await redis_get("state") @@ -274,7 +272,6 @@ async def setup_final_done(): @router.get("/shutdown") async def get_shutdown(): - # only allow unauthorized shutdowns during setup setupPhase = await redis_get("setupPhase") state = await redis_get("state") @@ -293,7 +290,6 @@ async def get_shutdown(): # When WebUI displayed seed words & user confirmed write the calls: @router.post("/setup-sync-info", dependencies=[Depends(JWTBearer())]) async def setup_sync_info(): - # first check that node is really in setup state setupPhase = await redis_get("setupPhase") if setupPhase != "done": diff --git a/app/system/impl/raspiblitz.py b/app/system/impl/raspiblitz.py index 3fea0ec..8a5900e 100644 --- a/app/system/impl/raspiblitz.py +++ b/app/system/impl/raspiblitz.py @@ -44,7 +44,6 @@ def __init__(self) -> None: super().__init__() async def get_system_info(self) -> SystemInfo: - lightning = await redis_get("lightning") if lightning == "" or lightning == "none": data_chain = await redis_get("chain") @@ -111,7 +110,6 @@ async def shutdown(self, reboot: bool) -> bool: return True async def get_connection_info(self) -> ConnectionInfo: - lightning = await redis_get("lightning") # Bitcoin RPC @@ -208,7 +206,6 @@ async def login(self, i: LoginInput) -> Dict[str, str]: ) async def change_password(self, type: str, old_password: str, new_password: str): - # check just allowed type values type = type.lower() if not type in ["a", "b", "c"]: diff --git a/app/system/models.py b/app/system/models.py index ecb14dc..ee7ad4d 100644 --- a/app/system/models.py +++ b/app/system/models.py @@ -81,7 +81,6 @@ class RawDebugLogData(BaseModel): class ConnectionInfo(BaseModel): - lnd_admin_macaroon: str = Query( "", description="lnd macaroon with admin rights in hexstring format" )