diff --git a/.env.example b/.env.example index daad78fd..55e0d1ae 100644 --- a/.env.example +++ b/.env.example @@ -44,13 +44,18 @@ MINT_DERIVATION_PATH="0/0/0/0" MINT_DATABASE=data/mint # Lightning -# Supported: LNbitsWallet, FakeWallet +# Supported: LndRestWallet, LNbitsWallet, FakeWallet MINT_LIGHTNING_BACKEND=LNbitsWallet # for use with LNbitsWallet MINT_LNBITS_ENDPOINT=https://legend.lnbits.com MINT_LNBITS_KEY=yourkeyasdasdasd +# LndRestWallet +MINT_LND_REST_ENDPOINT=https://127.0.0.1:8086 +MINT_LND_REST_CERT="/home/lnd/.lnd/tls.cert" +MINT_LND_REST_MACAROON="/home/lnd/.lnd/data/chain/bitcoin/regtest/admin.macaroon" + # fee to reserve in percent of the amount LIGHTNING_FEE_PERCENT=1.0 # minimum fee to reserve diff --git a/.github/actions/prepare/action.yml b/.github/actions/prepare/action.yml new file mode 100644 index 00000000..85232716 --- /dev/null +++ b/.github/actions/prepare/action.yml @@ -0,0 +1,27 @@ +name: prepare + +inputs: + python-version: + description: "Python Version" + required: true + default: "3.10" + poetry-version: + description: "Poetry Version" + default: "1.5.1" + +runs: + using: "composite" + steps: + - name: Set up Poetry ${{ inputs.poetry-version }} + uses: abatilo/actions-poetry@v2 + with: + poetry-version: ${{ inputs.poetry-version }} + - name: Set up Python ${{ inputs.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ inputs.python-version }} + cache: "poetry" + - name: Install dependencies + run: | + poetry install --extras pgsql + shell: bash diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 429f2ede..a537475b 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -1,24 +1,28 @@ name: checks -on: [push, pull_request] +on: + workflow_call: + inputs: + python-version: + default: "3.10.4" + type: string + poetry-version: + default: "1.5.1" + type: string jobs: formatting: runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.10.4"] - poetry-version: ["1.5.1"] steps: - uses: actions/checkout@v2 - - name: Set up Poetry ${{ matrix.poetry-version }} + - name: Set up Poetry ${{ inputs.poetry-version }} uses: abatilo/actions-poetry@v2 with: - poetry-version: ${{ matrix.poetry-version }} - - name: Set up Python ${{ matrix.python-version }} + poetry-version: ${{ inputs.poetry-version }} + - name: Set up Python ${{ inputs.python-version }} uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python-version }} + python-version: ${{ inputs.python-version }} cache: "poetry" - name: Install packages run: poetry install @@ -26,20 +30,16 @@ jobs: run: make black-check mypy: runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.10.4"] - poetry-version: ["1.5.1"] steps: - uses: actions/checkout@v2 - - name: Set up Poetry ${{ matrix.poetry-version }} + - name: Set up Poetry ${{ inputs.poetry-version }} uses: abatilo/actions-poetry@v2 with: - poetry-version: ${{ matrix.poetry-version }} - - name: Set up Python ${{ matrix.python-version }} + poetry-version: ${{ inputs.poetry-version }} + - name: Set up Python ${{ inputs.python-version }} uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python-version }} + python-version: ${{ inputs.python-version }} cache: "poetry" - name: Install packages run: poetry install diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..d4237433 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: Nutshell CI +on: + push: + branches: + - main + pull_request: + +jobs: + checks: + uses: ./.github/workflows/checks.yml + tests: + strategy: + matrix: + os: [ubuntu-latest] + python-version: ["3.9", "3.10"] + poetry-version: ["1.5.1"] + # db-url: ["", "postgres://cashu:cashu@localhost:5432/test"] # TODO: Postgres test not working + db-url: [""] + backend-wallet-class: ["FakeWallet"] + uses: ./.github/workflows/tests.yml + with: + python-version: ${{ matrix.python-version }} + poetry-version: ${{ matrix.poetry-version }} + regtest: + uses: ./.github/workflows/regtest.yml + strategy: + matrix: + python-version: ["3.10"] + poetry-version: ["1.5.1"] + backend-wallet-class: ["LndRestWallet", "LNbitsWallet"] + with: + python-version: ${{ matrix.python-version }} + backend-wallet-class: ${{ matrix.backend-wallet-class }} diff --git a/.github/workflows/regtest.yml b/.github/workflows/regtest.yml new file mode 100644 index 00000000..9eeda033 --- /dev/null +++ b/.github/workflows/regtest.yml @@ -0,0 +1,74 @@ +name: regtest + +on: + workflow_call: + inputs: + python-version: + default: "3.10.4" + type: string + poetry-version: + default: "1.5.1" + type: string + os-version: + default: "ubuntu-latest" + type: string + db-url: + default: "" + type: string + backend-wallet-class: + required: true + type: string + +jobs: + regtest: + runs-on: ${{ inputs.os-version }} + timeout-minutes: 10 + steps: + - uses: actions/checkout@v3 + + - uses: ./.github/actions/prepare + with: + python-version: ${{ inputs.python-version }} + poetry-version: ${{ inputs.poetry-version }} + + - name: Setup Regtest + run: | + git clone https://github.com/callebtc/cashu-regtest-enviroment.git regtest + cd regtest + chmod -R 777 . + bash ./start.sh + + - name: Create fake admin + if: ${{ inputs.backend-wallet-class == 'LNbitsWallet' }} + run: docker exec cashu-lnbits-1 poetry run python tools/create_fake_admin.py + + - name: Run Tests + env: + WALLET_NAME: test_wallet + MINT_HOST: localhost + MINT_PORT: 3337 + MINT_DATABASE: ${{ inputs.db-url }} + TOR: false + MINT_LIGHTNING_BACKEND: ${{ inputs.backend-wallet-class }} + MINT_LNBITS_ENDPOINT: http://localhost:5001 + MINT_LNBITS_KEY: d08a3313322a4514af75d488bcc27eee + MINT_LND_REST_ENDPOINT: https://localhost:8081/ + MINT_LND_REST_CERT: ./regtest/data/lnd-3/tls.cert + MINT_LND_REST_MACAROON: ./regtest/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon + # LND_GRPC_ENDPOINT: localhost + # LND_GRPC_PORT: 10009 + # LND_GRPC_CERT: docker/data/lnd-3/tls.cert + # LND_GRPC_MACAROON: docker/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon + # CORELIGHTNING_RPC: ./docker/data/clightning-1/regtest/lightning-rpc + # CORELIGHTNING_REST_URL: https://localhost:3001 + # CORELIGHTNING_REST_MACAROON: ./docker/data/clightning-2-rest/access.macaroon + # CORELIGHTNING_REST_CERT: ./docker/data/clightning-2-rest/certificate.pem + run: | + sudo chmod -R 777 . + make test + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 986e58e7..2889b52e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,10 +1,24 @@ name: tests -on: [push, pull_request] +on: + workflow_call: + inputs: + python-version: + default: "3.10.4" + type: string + poetry-version: + default: "1.5.1" + type: string + db-url: + default: "" + type: string + os: + default: "ubuntu-latest" + type: string jobs: poetry: - runs-on: ${{ matrix.os }} + runs-on: ${{ inputs.os }} services: postgres: image: postgres:latest @@ -19,34 +33,16 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 - strategy: - matrix: - os: [ubuntu-latest] - python-version: ["3.10.4"] - poetry-version: ["1.5.1"] - # db-url: ["", "postgres://cashu:cashu@localhost:5432/test"] # TODO: Postgres test not working - db-url: [""] steps: - name: Checkout repository and submodules uses: actions/checkout@v2 + - uses: ./.github/actions/prepare with: - submodules: recursive - - name: Set up Poetry ${{ matrix.poetry-version }} - uses: abatilo/actions-poetry@v2 - with: - poetry-version: ${{ matrix.poetry-version }} - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - cache: "poetry" - - name: Install dependencies - run: | - poetry install --extras pgsql - shell: bash + python-version: ${{ inputs.python-version }} + poetry-version: ${{ inputs.poetry-version }} - name: Run tests env: - LIGHTNING: false + MINT_LIGHTNING_BACKEND: FakeWallet WALLET_NAME: test_wallet MINT_HOST: localhost MINT_PORT: 3337 diff --git a/Makefile b/Makefile index 92596a45..93964999 100644 --- a/Makefile +++ b/Makefile @@ -29,8 +29,19 @@ package: python setup.py sdist bdist_wheel test: + PYTHONUNBUFFERED=1 \ + DEBUG=true \ poetry run pytest tests --cov-report xml --cov cashu +test-lndrest: + PYTHONUNBUFFERED=1 \ + DEBUG=true \ + MINT_LIGHTNING_BACKEND=LndRestWallet \ + MINT_LND_REST_ENDPOINT=https://localhost:8081/ \ + MINT_LND_REST_CERT=../cashu-regtest-enviroment/data/lnd-3/tls.cert \ + MINT_LND_REST_MACAROON=../cashu-regtest-enviroment/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon \ + poetry run pytest tests/test_cli.py --cov-report xml --cov cashu + install: make clean python setup.py sdist bdist_wheel diff --git a/cashu/core/settings.py b/cashu/core/settings.py index f52c3eba..39922029 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -1,7 +1,7 @@ import os import sys from pathlib import Path -from typing import List +from typing import List, Optional from environs import Env # type: ignore from pydantic import BaseSettings, Extra, Field @@ -99,8 +99,17 @@ class WalletSettings(CashuSettings): locktime_delta_seconds: int = Field(default=86400) # 1 day +class LndRestFundingSource(MintSettings): + mint_lnd_rest_endpoint: Optional[str] = Field(default=None) + mint_lnd_rest_cert: Optional[str] = Field(default=None) + mint_lnd_rest_macaroon: Optional[str] = Field(default=None) + mint_lnd_rest_admin_macaroon: Optional[str] = Field(default=None) + mint_lnd_rest_invoice_macaroon: Optional[str] = Field(default=None) + + class Settings( EnvSettings, + LndRestFundingSource, MintSettings, MintInformation, WalletSettings, diff --git a/cashu/lightning/__init__.py b/cashu/lightning/__init__.py index 8e72fd44..146c3616 100644 --- a/cashu/lightning/__init__.py +++ b/cashu/lightning/__init__.py @@ -1,3 +1,8 @@ # type: ignore +from ..core.settings import settings from .fake import FakeWallet # noqa: F401 from .lnbits import LNbitsWallet # noqa: F401 +from .lndrest import LndRestWallet # noqa: F401 + +if settings.mint_lightning_backend is None: + raise Exception("MINT_LIGHTNING_BACKEND not configured") diff --git a/cashu/lightning/lndrest.py b/cashu/lightning/lndrest.py new file mode 100644 index 00000000..33c44808 --- /dev/null +++ b/cashu/lightning/lndrest.py @@ -0,0 +1,271 @@ +import asyncio +import base64 +import hashlib +import json +from typing import AsyncGenerator, Dict, Optional + +import httpx +from loguru import logger + +from ..core.settings import settings +from .base import ( + InvoiceResponse, + PaymentResponse, + PaymentStatus, + StatusResponse, + Wallet, +) + + +def load_macaroon(macaroon: str) -> str: + """Returns hex version of a macaroon encoded in base64 or the file path. + + :param macaroon: Macaroon encoded in base64 or file path. + :type macaroon: str + :return: Hex version of macaroon. + :rtype: str + """ + + # if the macaroon is a file path, load it and return hex version + if macaroon.split(".")[-1] == "macaroon": + with open(macaroon, "rb") as f: + macaroon_bytes = f.read() + return macaroon_bytes.hex() + else: + # if macaroon is a provided string + # check if it is hex, if so, return + try: + bytes.fromhex(macaroon) + return macaroon + except ValueError: + pass + # convert the bas64 macaroon to hex + try: + macaroon = base64.b64decode(macaroon).hex() + except Exception: + pass + return macaroon + + +class LndRestWallet(Wallet): + """https://api.lightning.community/rest/index.html#lnd-rest-api-reference""" + + def __init__(self): + endpoint = settings.mint_lnd_rest_endpoint + cert = settings.mint_lnd_rest_cert + + macaroon = ( + settings.mint_lnd_rest_macaroon + or settings.mint_lnd_rest_admin_macaroon + or settings.mint_lnd_rest_invoice_macaroon + ) + + if not endpoint: + raise Exception("cannot initialize lndrest: no endpoint") + + if not macaroon: + raise Exception("cannot initialize lndrest: no macaroon") + + if not cert: + logger.warning( + "no certificate for lndrest provided, this only works if you have a" + " publicly issued certificate" + ) + + endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint + endpoint = ( + f"https://{endpoint}" if not endpoint.startswith("http") else endpoint + ) + self.endpoint = endpoint + self.macaroon = load_macaroon(macaroon) + + # if no cert provided it should be public so we set verify to True + # and it will still check for validity of certificate and fail if its not valid + # even on startup + self.cert = cert or True + + self.auth = {"Grpc-Metadata-macaroon": self.macaroon} + self.client = httpx.AsyncClient( + base_url=self.endpoint, headers=self.auth, verify=self.cert + ) + + async def status(self) -> StatusResponse: + try: + r = await self.client.get("/v1/balance/channels") + r.raise_for_status() + except (httpx.ConnectError, httpx.RequestError) as exc: + return StatusResponse( + error_message=f"Unable to connect to {self.endpoint}. {exc}", + balance_msat=0, + ) + + try: + data = r.json() + if r.is_error: + raise Exception + except Exception: + return StatusResponse(error_message=r.text[:200], balance_msat=0) + + return StatusResponse( + error_message=None, balance_msat=int(data["balance"]) * 1000 + ) + + async def create_invoice( + self, + amount: int, + memo: Optional[str] = None, + description_hash: Optional[bytes] = None, + unhashed_description: Optional[bytes] = None, + **kwargs, + ) -> InvoiceResponse: + data: Dict = {"value": amount, "private": True, "memo": memo or ""} + if kwargs.get("expiry"): + data["expiry"] = kwargs["expiry"] + if description_hash: + data["description_hash"] = base64.b64encode(description_hash).decode( + "ascii" + ) + elif unhashed_description: + data["description_hash"] = base64.b64encode( + hashlib.sha256(unhashed_description).digest() + ).decode("ascii") + + r = await self.client.post(url="/v1/invoices", json=data) + + if r.is_error: + error_message = r.text + try: + error_message = r.json()["error"] + except Exception: + pass + return InvoiceResponse( + ok=False, + checking_id=None, + payment_request=None, + error_message=error_message, + ) + + data = r.json() + payment_request = data["payment_request"] + payment_hash = base64.b64decode(data["r_hash"]).hex() + checking_id = payment_hash + + return InvoiceResponse( + ok=True, + checking_id=checking_id, + payment_request=payment_request, + error_message=None, + ) + + async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: + # set the fee limit for the payment + lnrpcFeeLimit = dict() + lnrpcFeeLimit["fixed_msat"] = f"{fee_limit_msat}" + + r = await self.client.post( + url="/v1/channels/transactions", + json={"payment_request": bolt11, "fee_limit": lnrpcFeeLimit}, + timeout=None, + ) + + if r.is_error or r.json().get("payment_error"): + error_message = r.json().get("payment_error") or r.text + return PaymentResponse( + ok=False, + checking_id=None, + fee_msat=None, + preimage=None, + error_message=error_message, + ) + + data = r.json() + checking_id = base64.b64decode(data["payment_hash"]).hex() + fee_msat = int(data["payment_route"]["total_fees_msat"]) + preimage = base64.b64decode(data["payment_preimage"]).hex() + return PaymentResponse( + ok=True, + checking_id=checking_id, + fee_msat=fee_msat, + preimage=preimage, + error_message=None, + ) + + async def get_invoice_status(self, checking_id: str) -> PaymentStatus: + r = await self.client.get(url=f"/v1/invoice/{checking_id}") + + if r.is_error or not r.json().get("settled"): + # this must also work when checking_id is not a hex recognizable by lnd + # it will return an error and no "settled" attribute on the object + return PaymentStatus(paid=None) + + return PaymentStatus(paid=True) + + async def get_payment_status(self, checking_id: str) -> PaymentStatus: + """ + This routine checks the payment status using routerpc.TrackPaymentV2. + """ + # convert checking_id from hex to base64 and some LND magic + try: + checking_id = base64.urlsafe_b64encode(bytes.fromhex(checking_id)).decode( + "ascii" + ) + except ValueError: + return PaymentStatus(paid=None) + + url = f"/v2/router/track/{checking_id}" + + # check payment.status: + # https://api.lightning.community/?python=#paymentpaymentstatus + statuses = { + "UNKNOWN": None, + "IN_FLIGHT": None, + "SUCCEEDED": True, + "FAILED": False, + } + + async with self.client.stream("GET", url, timeout=None) as r: + async for json_line in r.aiter_lines(): + try: + line = json.loads(json_line) + if line.get("error"): + logger.error( + line["error"]["message"] + if "message" in line["error"] + else line["error"] + ) + return PaymentStatus(paid=None) + payment = line.get("result") + if payment is not None and payment.get("status"): + return PaymentStatus( + paid=statuses[payment["status"]], + fee_msat=payment.get("fee_msat"), + preimage=payment.get("payment_preimage"), + ) + else: + return PaymentStatus(paid=None) + except Exception: + continue + + return PaymentStatus(paid=None) + + async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: + while True: + try: + url = "/v1/invoices/subscribe" + async with self.client.stream("GET", url, timeout=None) as r: + async for line in r.aiter_lines(): + try: + inv = json.loads(line)["result"] + if not inv["settled"]: + continue + except Exception: + continue + + payment_hash = base64.b64decode(inv["r_hash"]).hex() + yield payment_hash + except Exception as exc: + logger.error( + f"lost connection to lnd invoices stream: '{exc}', retrying in 5" + " seconds" + ) + await asyncio.sleep(5) diff --git a/cashu/wallet/cli/cli.py b/cashu/wallet/cli/cli.py index 4be2cbea..eceb4fe5 100644 --- a/cashu/wallet/cli/cli.py +++ b/cashu/wallet/cli/cli.py @@ -210,9 +210,17 @@ async def pay(ctx: Context, invoice: str, yes: bool): help="Split minted tokens with a specific amount.", type=int, ) +@click.option( + "--no-check", + "-n", + default=False, + is_flag=True, + help="Do not check if invoice is paid.", + type=bool, +) @click.pass_context @coro -async def invoice(ctx: Context, amount: int, id: str, split: int): +async def invoice(ctx: Context, amount: int, id: str, split: int, no_check: bool): wallet: Wallet = ctx.obj["WALLET"] await wallet.load_mint() wallet.status() @@ -236,9 +244,11 @@ async def invoice(ctx: Context, amount: int, id: str, split: int): print(f"Invoice: {invoice.bolt11}") print("") print( - "If you abort this you can use this command to recheck the" - f" invoice:\ncashu invoice {amount} --id {invoice.id}" + "You can use this command to check the invoice: cashu invoice" + f" {amount} --id {invoice.id}" ) + if no_check: + return check_until = time.time() + 5 * 60 # check for five minutes print("") print( diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/conftest.py b/tests/conftest.py index 39914079..9906b832 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,7 +28,7 @@ settings.mint_url = SERVER_ENDPOINT settings.lightning = True settings.tor = False -settings.mint_lightning_backend = "FakeWallet" +settings.mint_lightning_backend = settings.mint_lightning_backend or "FakeWallet" settings.mint_database = "./test_data/test_mint" settings.mint_derivation_path = "0/0/0/0" settings.mint_private_key = "TEST_PRIVATE_KEY" diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 00000000..e6d084a6 --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,184 @@ +import hashlib +import importlib +import json +import os +import random +import string +import time +from subprocess import PIPE, Popen, TimeoutExpired +from typing import Tuple + +from loguru import logger + +from cashu.core.settings import settings + + +def get_random_string(N: int = 10): + return "".join( + random.SystemRandom().choice(string.ascii_uppercase + string.digits) + for _ in range(N) + ) + + +async def get_random_invoice_data(): + return {"out": False, "amount": 10, "memo": f"test_memo_{get_random_string(10)}"} + + +wallets_module = importlib.import_module("cashu.lightning") +wallet_class = getattr(wallets_module, settings.mint_lightning_backend) +WALLET = wallet_class() +is_fake: bool = WALLET.__class__.__name__ == "FakeWallet" +is_regtest: bool = not is_fake + + +docker_lightning_cli = [ + "docker", + "exec", + "cashu-lnd-1-1", + "lncli", + "--network", + "regtest", + "--rpcserver=lnd-1", +] + +docker_bitcoin_cli = [ + "docker", + "exec", + "cashu-bitcoind-1-1bitcoin-cli", + "-rpcuser=lnbits", + "-rpcpassword=lnbits", + "-regtest", +] + + +docker_lightning_unconnected_cli = [ + "docker", + "exec", + "cashu-lnd-2-1", + "lncli", + "--network", + "regtest", + "--rpcserver=lnd-2", +] + + +def run_cmd(cmd: list) -> str: + timeout = 20 + process = Popen(cmd, stdout=PIPE, stderr=PIPE) + + def process_communication(comm): + stdout, stderr = comm + output = stdout.decode("utf-8").strip() + error = stderr.decode("utf-8").strip() + return output, error + + try: + now = time.time() + output, error = process_communication(process.communicate(timeout=timeout)) + took = time.time() - now + logger.debug(f"ran command output: {output}, error: {error}, took: {took}s") + return output + except TimeoutExpired: + process.kill() + output, error = process_communication(process.communicate()) + logger.error(f"timeout command: {cmd}, output: {output}, error: {error}") + raise + + +def run_cmd_json(cmd: list) -> dict: + output = run_cmd(cmd) + try: + return json.loads(output) if output else {} + except json.decoder.JSONDecodeError: + logger.error(f"failed to decode json from cmd `{cmd}`: {output}") + raise + + +def get_hold_invoice(sats: int) -> Tuple[str, dict]: + preimage = os.urandom(32) + preimage_hash = hashlib.sha256(preimage).hexdigest() + cmd = docker_lightning_cli.copy() + cmd.extend(["addholdinvoice", preimage_hash, str(sats)]) + json = run_cmd_json(cmd) + return preimage.hex(), json + + +def settle_invoice(preimage: str) -> str: + cmd = docker_lightning_cli.copy() + cmd.extend(["settleinvoice", preimage]) + return run_cmd(cmd) + + +def cancel_invoice(preimage_hash: str) -> str: + cmd = docker_lightning_cli.copy() + cmd.extend(["cancelinvoice", preimage_hash]) + return run_cmd(cmd) + + +def get_real_invoice(sats: int) -> dict: + cmd = docker_lightning_cli.copy() + cmd.extend(["addinvoice", str(sats)]) + return run_cmd_json(cmd) + + +def pay_real_invoice(invoice: str) -> str: + cmd = docker_lightning_cli.copy() + cmd.extend(["payinvoice", "--force", invoice]) + return run_cmd(cmd) + + +def mine_blocks(blocks: int = 1) -> str: + cmd = docker_bitcoin_cli.copy() + cmd.extend(["-generate", str(blocks)]) + return run_cmd(cmd) + + +def get_unconnected_node_uri() -> str: + cmd = docker_lightning_unconnected_cli.copy() + cmd.append("getinfo") + info = run_cmd_json(cmd) + pubkey = info["identity_pubkey"] + return f"{pubkey}@lnd-2:9735" + + +def create_onchain_address(address_type: str = "bech32") -> str: + cmd = docker_bitcoin_cli.copy() + cmd.extend(["getnewaddress", address_type]) + return run_cmd(cmd) + + +def pay_onchain(address: str, sats: int) -> str: + btc = sats * 0.00000001 + cmd = docker_bitcoin_cli.copy() + cmd.extend(["sendtoaddress", address, str(btc)]) + return run_cmd(cmd) + + +# def clean_database(settings): +# if DB_TYPE == POSTGRES: +# db_url = make_url(settings.lnbits_database_url) + +# conn = psycopg2.connect(settings.lnbits_database_url) +# conn.autocommit = True +# with conn.cursor() as cur: +# try: +# cur.execute("DROP DATABASE lnbits_test") +# except psycopg2.errors.InvalidCatalogName: +# pass +# cur.execute("CREATE DATABASE lnbits_test") + +# db_url.database = "lnbits_test" +# settings.lnbits_database_url = str(db_url) + +# core.db.__init__("database") + +# conn.close() +# else: +# # FIXME: do this once mock data is removed from test data folder +# # os.remove(settings.lnbits_data_folder + "/database.sqlite3") +# pass + + +def pay_if_regtest(bolt11: str): + if is_regtest: + pay_real_invoice(bolt11) diff --git a/tests/test_cli.py b/tests/test_cli.py index 9bb1f86b..3ec460e3 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -7,6 +7,7 @@ from cashu.core.settings import settings from cashu.wallet.cli.cli import cli from cashu.wallet.wallet import Wallet +from tests.helpers import is_fake, pay_if_regtest @pytest.fixture(autouse=True, scope="session") @@ -14,6 +15,16 @@ def cli_prefix(): yield ["--wallet", "test_cli_wallet", "--host", settings.mint_url, "--tests"] +def get_bolt11_and_invoice_id_from_invoice_command(output: str) -> (str, str): + invoice = [ + line.split(" ")[1] for line in output.split("\n") if line.startswith("Invoice") + ][0] + invoice_id = [ + line.split(" ")[-1] for line in output.split("\n") if line.startswith("You can") + ][0] + return invoice, invoice_id + + async def init_wallet(): wallet = await Wallet.with_db( url=settings.mint_host, @@ -77,7 +88,8 @@ def test_balance(cli_prefix): assert result.exit_code == 0 -def test_invoice(mint, cli_prefix): +@pytest.mark.skipif(not is_fake, reason="only on fakewallet") +def test_invoice_automatic_fakewallet(mint, cli_prefix): runner = CliRunner() result = runner.invoke( cli, @@ -87,20 +99,60 @@ def test_invoice(mint, cli_prefix): print("INVOICE") print(result.output) wallet = asyncio.run(init_wallet()) - # assert wallet.available_balance >= 1000 + assert wallet.available_balance >= 1000 assert f"Balance: {wallet.available_balance} sat" in result.output assert result.exit_code == 0 +def test_invoice(mint, cli_prefix): + runner = CliRunner() + result = runner.invoke( + cli, + [*cli_prefix, "invoice", "-n", "1000"], + ) + + assert result.exception is None + + invoice, invoice_id = get_bolt11_and_invoice_id_from_invoice_command(result.output) + pay_if_regtest(invoice) + + result = runner.invoke( + cli, + [*cli_prefix, "invoice", "1000", "--id", invoice_id], + ) + assert result.exception is None + + wallet = asyncio.run(init_wallet()) + assert wallet.available_balance >= 1000 + assert result.exit_code == 0 + + def test_invoice_with_split(mint, cli_prefix): runner = CliRunner() result = runner.invoke( cli, - [*cli_prefix, "invoice", "10", "-s", "1"], + [ + *cli_prefix, + "invoice", + "10", + "-s", + "1", + "-n", + ], ) assert result.exception is None - # wallet = asyncio.run(init_wallet()) - # assert wallet.proof_amounts.count(1) >= 10 + + invoice, invoice_id = get_bolt11_and_invoice_id_from_invoice_command(result.output) + pay_if_regtest(invoice) + result = runner.invoke( + cli, + [*cli_prefix, "invoice", "10", "-s", "1", "--id", invoice_id], + ) + assert result.exception is None + + assert result.exception is None + wallet = asyncio.run(init_wallet()) + assert wallet.proof_amounts.count(1) >= 10 def test_wallets(cli_prefix): diff --git a/tests/test_mint_operations.py b/tests/test_mint_operations.py index 35e8c479..e4e9e739 100644 --- a/tests/test_mint_operations.py +++ b/tests/test_mint_operations.py @@ -5,6 +5,7 @@ from cashu.wallet.wallet import Wallet from cashu.wallet.wallet import Wallet as Wallet1 from tests.conftest import SERVER_ENDPOINT +from tests.helpers import pay_if_regtest @pytest_asyncio.fixture(scope="function") @@ -23,8 +24,10 @@ async def wallet1(mint): async def test_melt(wallet1: Wallet, ledger: Ledger): # mint twice so we have enough to pay the second invoice back invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) assert wallet1.balance == 128 total_amount, fee_reserve_sat = await wallet1.get_pay_amount_with_fees( @@ -41,6 +44,7 @@ async def test_melt(wallet1: Wallet, ledger: Ledger): @pytest.mark.asyncio async def test_split(wallet1: Wallet, ledger: Ledger): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10) @@ -57,6 +61,7 @@ async def test_split(wallet1: Wallet, ledger: Ledger): @pytest.mark.asyncio async def test_check_proof_state(wallet1: Wallet, ledger: Ledger): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10) diff --git a/tests/test_wallet.py b/tests/test_wallet.py index e2bab85c..6433009c 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -14,6 +14,7 @@ from cashu.wallet.wallet import Wallet as Wallet1 from cashu.wallet.wallet import Wallet as Wallet2 from tests.conftest import SERVER_ENDPOINT +from tests.helpers import get_real_invoice, is_regtest, pay_if_regtest async def assert_err(f, msg: Union[str, CashuError]): @@ -138,6 +139,7 @@ async def test_get_keyset_ids(wallet1: Wallet): @pytest.mark.asyncio async def test_mint(wallet1: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) assert wallet1.balance == 64 @@ -158,6 +160,7 @@ async def test_mint(wallet1: Wallet): async def test_mint_amounts(wallet1: Wallet): """Mint predefined amounts""" invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) amts = [1, 1, 1, 2, 2, 4, 16] await wallet1.mint(amount=sum(amts), split=amts, id=invoice.id) assert wallet1.balance == 27 @@ -187,6 +190,7 @@ async def test_mint_amounts_wrong_order(wallet1: Wallet): @pytest.mark.asyncio async def test_split(wallet1: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) assert wallet1.balance == 64 p1, p2 = await wallet1.split(wallet1.proofs, 20) @@ -202,6 +206,7 @@ async def test_split(wallet1: Wallet): @pytest.mark.asyncio async def test_split_to_send(wallet1: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) keep_proofs, spendable_proofs = await wallet1.split_to_send( wallet1.proofs, 32, set_reserved=True @@ -217,6 +222,7 @@ async def test_split_to_send(wallet1: Wallet): @pytest.mark.asyncio async def test_split_more_than_balance(wallet1: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) await assert_err( wallet1.split(wallet1.proofs, 128), @@ -230,8 +236,10 @@ async def test_split_more_than_balance(wallet1: Wallet): async def test_melt(wallet1: Wallet): # mint twice so we have enough to pay the second invoice back invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) assert wallet1.balance == 128 @@ -243,38 +251,46 @@ async def test_melt(wallet1: Wallet): assert fee_reserve_sat == 2 _, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount) + invoice_to_pay = invoice.bolt11 + invoice_payment_hash = str(invoice.payment_hash) + if is_regtest: + invoice_dict = get_real_invoice(64) + invoice_to_pay = invoice_dict["payment_request"] + invoice_payment_hash = str(invoice_dict["r_hash"]) + melt_response = await wallet1.pay_lightning( - send_proofs, invoice=invoice.bolt11, fee_reserve_sat=fee_reserve_sat + send_proofs, invoice=invoice_to_pay, fee_reserve_sat=fee_reserve_sat ) - assert melt_response.change - assert len(melt_response.change) == 1 + assert melt_response.change, "No change returned" + assert len(melt_response.change) == 1, "More than one change returned" # NOTE: we assume that we will get a token back from the same keyset as the ones we melted # this could be wrong if we melted tokens from an old keyset but the returned ones are # from a newer one. - assert melt_response.change[0].id == send_proofs[0].id + assert melt_response.change[0].id == send_proofs[0].id, "Wrong keyset returned" # verify that proofs in proofs_used db have the same melt_id as the invoice in the db - assert invoice.payment_hash + assert invoice.payment_hash, "No payment hash in invoice" invoice_db = await get_lightning_invoice( - db=wallet1.db, payment_hash=invoice.payment_hash, out=True + db=wallet1.db, payment_hash=invoice_payment_hash, out=True ) - assert invoice_db + assert invoice_db, "No invoice in db" proofs_used = await get_proofs( db=wallet1.db, melt_id=invoice_db.id, table="proofs_used" ) - assert len(proofs_used) == len(send_proofs) - assert all([p.melt_id == invoice_db.id for p in proofs_used]) + assert len(proofs_used) == len(send_proofs), "Not all proofs used" + assert all([p.melt_id == invoice_db.id for p in proofs_used]), "Wrong melt_id" # the payment was without fees so we need to remove it from the total amount - assert wallet1.balance == 128 - (total_amount - fee_reserve_sat) - assert wallet1.balance == 64 + assert wallet1.balance == 128 - (total_amount - fee_reserve_sat), "Wrong balance" + assert wallet1.balance == 64, "Wrong balance" @pytest.mark.asyncio async def test_split_to_send_more_than_balance(wallet1: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) await assert_err( wallet1.split_to_send(wallet1.proofs, 128, set_reserved=True), @@ -287,6 +303,7 @@ async def test_split_to_send_more_than_balance(wallet1: Wallet): @pytest.mark.asyncio async def test_double_spend(wallet1: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) doublespend = await wallet1.mint(64, id=invoice.id) await wallet1.split(wallet1.proofs, 20) await assert_err( @@ -300,6 +317,7 @@ async def test_double_spend(wallet1: Wallet): @pytest.mark.asyncio async def test_duplicate_proofs_double_spent(wallet1: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) doublespend = await wallet1.mint(64, id=invoice.id) await assert_err( wallet1.split(wallet1.proofs + doublespend, 20), @@ -312,6 +330,7 @@ async def test_duplicate_proofs_double_spent(wallet1: Wallet): @pytest.mark.asyncio async def test_send_and_redeem(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) _, spendable_proofs = await wallet1.split_to_send( wallet1.proofs, 32, set_reserved=True @@ -330,6 +349,7 @@ async def test_send_and_redeem(wallet1: Wallet, wallet2: Wallet): async def test_invalidate_unspent_proofs(wallet1: Wallet): """Try to invalidate proofs that have not been spent yet. Should not work!""" invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) await wallet1.invalidate(wallet1.proofs) assert wallet1.balance == 64 @@ -339,6 +359,7 @@ async def test_invalidate_unspent_proofs(wallet1: Wallet): async def test_invalidate_unspent_proofs_without_checking(wallet1: Wallet): """Try to invalidate proofs that have not been spent yet but force no check.""" invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) await wallet1.invalidate(wallet1.proofs, check_spendable=False) assert wallet1.balance == 0 @@ -347,6 +368,7 @@ async def test_invalidate_unspent_proofs_without_checking(wallet1: Wallet): @pytest.mark.asyncio async def test_split_invalid_amount(wallet1: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) await assert_err( wallet1.split(wallet1.proofs, -1), @@ -357,6 +379,7 @@ async def test_split_invalid_amount(wallet1: Wallet): @pytest.mark.asyncio async def test_token_state(wallet1: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) assert wallet1.balance == 64 resp = await wallet1.check_proof_state(wallet1.proofs) diff --git a/tests/test_wallet_api.py b/tests/test_wallet_api.py index d07fa388..4bd66369 100644 --- a/tests/test_wallet_api.py +++ b/tests/test_wallet_api.py @@ -8,6 +8,7 @@ from cashu.wallet.api.app import app from cashu.wallet.wallet import Wallet from tests.conftest import SERVER_ENDPOINT +from tests.helpers import is_regtest @pytest_asyncio.fixture(scope="function") @@ -22,6 +23,7 @@ async def wallet(mint): yield wallet +@pytest.mark.skipif(is_regtest, reason="regtest") @pytest.mark.asyncio async def test_invoice(wallet: Wallet): with TestClient(app) as client: @@ -40,6 +42,7 @@ async def test_invoice(wallet: Wallet): print("paid") +@pytest.mark.skipif(is_regtest, reason="regtest") @pytest.mark.asyncio async def test_balance(): with TestClient(app) as client: @@ -50,6 +53,7 @@ async def test_balance(): assert response.json()["mints"] +@pytest.mark.skipif(is_regtest, reason="regtest") @pytest.mark.asyncio async def test_send(wallet: Wallet): with TestClient(app) as client: @@ -58,6 +62,7 @@ async def test_send(wallet: Wallet): assert response.json()["balance"] +@pytest.mark.skipif(is_regtest, reason="regtest") @pytest.mark.asyncio async def test_send_without_split(wallet: Wallet): with TestClient(app) as client: @@ -66,6 +71,7 @@ async def test_send_without_split(wallet: Wallet): assert response.json()["balance"] +@pytest.mark.skipif(is_regtest, reason="regtest") @pytest.mark.asyncio async def test_send_without_split_but_wrong_amount(wallet: Wallet): with TestClient(app) as client: @@ -73,6 +79,7 @@ async def test_send_without_split_but_wrong_amount(wallet: Wallet): assert response.status_code == 400 +@pytest.mark.skipif(is_regtest, reason="regtest") @pytest.mark.asyncio async def test_pending(): with TestClient(app) as client: @@ -80,6 +87,7 @@ async def test_pending(): assert response.status_code == 200 +@pytest.mark.skipif(is_regtest, reason="regtest") @pytest.mark.asyncio async def test_receive_all(wallet: Wallet): with TestClient(app) as client: @@ -89,6 +97,7 @@ async def test_receive_all(wallet: Wallet): assert response.json()["balance"] +@pytest.mark.skipif(is_regtest, reason="regtest") @pytest.mark.asyncio async def test_burn_all(wallet: Wallet): with TestClient(app) as client: @@ -99,6 +108,7 @@ async def test_burn_all(wallet: Wallet): assert response.json()["balance"] +@pytest.mark.skipif(is_regtest, reason="regtest") @pytest.mark.asyncio async def test_pay(): with TestClient(app) as client: @@ -113,6 +123,7 @@ async def test_pay(): assert response.status_code == 200 +@pytest.mark.skipif(is_regtest, reason="regtest") @pytest.mark.asyncio async def test_lock(): with TestClient(app) as client: @@ -120,6 +131,7 @@ async def test_lock(): assert response.status_code == 200 +@pytest.mark.skipif(is_regtest, reason="regtest") @pytest.mark.asyncio async def test_locks(): with TestClient(app) as client: @@ -127,6 +139,7 @@ async def test_locks(): assert response.status_code == 200 +@pytest.mark.skipif(is_regtest, reason="regtest") @pytest.mark.asyncio async def test_invoices(): with TestClient(app) as client: @@ -134,6 +147,7 @@ async def test_invoices(): assert response.status_code == 200 +@pytest.mark.skipif(is_regtest, reason="regtest") @pytest.mark.asyncio async def test_wallets(): with TestClient(app) as client: @@ -141,6 +155,7 @@ async def test_wallets(): assert response.status_code == 200 +@pytest.mark.skipif(is_regtest, reason="regtest") @pytest.mark.asyncio async def test_info(): with TestClient(app) as client: @@ -149,6 +164,7 @@ async def test_info(): assert response.json()["version"] +@pytest.mark.skipif(is_regtest, reason="regtest") @pytest.mark.asyncio async def test_flow(wallet: Wallet): with TestClient(app) as client: diff --git a/tests/test_wallet_htlc.py b/tests/test_wallet_htlc.py index 2bd0ce16..b64557f8 100644 --- a/tests/test_wallet_htlc.py +++ b/tests/test_wallet_htlc.py @@ -15,6 +15,7 @@ from cashu.wallet.wallet import Wallet as Wallet1 from cashu.wallet.wallet import Wallet as Wallet2 from tests.conftest import SERVER_ENDPOINT +from tests.helpers import pay_if_regtest async def assert_err(f, msg): @@ -59,6 +60,7 @@ async def wallet2(mint): @pytest.mark.asyncio async def test_create_htlc_secret(wallet1: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) preimage = "00000000000000000000000000000000" preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() @@ -69,6 +71,7 @@ async def test_create_htlc_secret(wallet1: Wallet): @pytest.mark.asyncio async def test_htlc_split(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) preimage = "00000000000000000000000000000000" preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() @@ -81,6 +84,7 @@ async def test_htlc_split(wallet1: Wallet, wallet2: Wallet): @pytest.mark.asyncio async def test_htlc_redeem_with_preimage(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) preimage = "00000000000000000000000000000000" # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() @@ -94,6 +98,7 @@ async def test_htlc_redeem_with_preimage(wallet1: Wallet, wallet2: Wallet): @pytest.mark.asyncio async def test_htlc_redeem_with_wrong_preimage(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) preimage = "00000000000000000000000000000000" # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() @@ -111,6 +116,7 @@ async def test_htlc_redeem_with_wrong_preimage(wallet1: Wallet, wallet2: Wallet) @pytest.mark.asyncio async def test_htlc_redeem_with_no_signature(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) preimage = "00000000000000000000000000000000" pubkey_wallet1 = await wallet1.create_p2pk_pubkey() @@ -130,6 +136,7 @@ async def test_htlc_redeem_with_no_signature(wallet1: Wallet, wallet2: Wallet): @pytest.mark.asyncio async def test_htlc_redeem_with_wrong_signature(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) preimage = "00000000000000000000000000000000" pubkey_wallet1 = await wallet1.create_p2pk_pubkey() @@ -153,6 +160,7 @@ async def test_htlc_redeem_with_wrong_signature(wallet1: Wallet, wallet2: Wallet @pytest.mark.asyncio async def test_htlc_redeem_with_correct_signature(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) preimage = "00000000000000000000000000000000" pubkey_wallet1 = await wallet1.create_p2pk_pubkey() @@ -174,6 +182,7 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_correct_signature( wallet1: Wallet, wallet2: Wallet ): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) preimage = "00000000000000000000000000000000" pubkey_wallet1 = await wallet1.create_p2pk_pubkey() @@ -207,6 +216,7 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_wrong_signature( wallet1: Wallet, wallet2: Wallet ): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) preimage = "00000000000000000000000000000000" pubkey_wallet1 = await wallet1.create_p2pk_pubkey() diff --git a/tests/test_wallet_p2pk.py b/tests/test_wallet_p2pk.py index 2dbd5bfb..bb6771c4 100644 --- a/tests/test_wallet_p2pk.py +++ b/tests/test_wallet_p2pk.py @@ -16,6 +16,7 @@ from cashu.wallet.wallet import Wallet as Wallet1 from cashu.wallet.wallet import Wallet as Wallet2 from tests.conftest import SERVER_ENDPOINT +from tests.helpers import pay_if_regtest async def assert_err(f, msg): @@ -60,6 +61,7 @@ async def wallet2(mint): @pytest.mark.asyncio async def test_create_p2pk_pubkey(wallet1: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey = await wallet1.create_p2pk_pubkey() PublicKey(bytes.fromhex(pubkey), raw=True) @@ -68,6 +70,7 @@ async def test_create_p2pk_pubkey(wallet1: Wallet): @pytest.mark.asyncio async def test_p2pk(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # p2pk test @@ -81,6 +84,7 @@ async def test_p2pk(wallet1: Wallet, wallet2: Wallet): @pytest.mark.asyncio async def test_p2pk_sig_all(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # p2pk test @@ -96,6 +100,7 @@ async def test_p2pk_sig_all(wallet1: Wallet, wallet2: Wallet): @pytest.mark.asyncio async def test_p2pk_receive_with_wrong_private_key(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side # sender side @@ -116,6 +121,7 @@ async def test_p2pk_short_locktime_receive_with_wrong_private_key( wallet1: Wallet, wallet2: Wallet ): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side # sender side @@ -141,6 +147,7 @@ async def test_p2pk_short_locktime_receive_with_wrong_private_key( @pytest.mark.asyncio async def test_p2pk_locktime_with_refund_pubkey(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side # sender side @@ -169,6 +176,7 @@ async def test_p2pk_locktime_with_refund_pubkey(wallet1: Wallet, wallet2: Wallet @pytest.mark.asyncio async def test_p2pk_locktime_with_wrong_refund_pubkey(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) await wallet2.create_p2pk_pubkey() # receiver side # sender side @@ -204,6 +212,7 @@ async def test_p2pk_locktime_with_second_refund_pubkey( wallet1: Wallet, wallet2: Wallet ): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet1 = await wallet1.create_p2pk_pubkey() # receiver side pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side @@ -235,6 +244,7 @@ async def test_p2pk_locktime_with_second_refund_pubkey( @pytest.mark.asyncio async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey() @@ -256,6 +266,7 @@ async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet): @pytest.mark.asyncio async def test_p2pk_multisig_duplicate_signature(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey() @@ -279,6 +290,7 @@ async def test_p2pk_multisig_duplicate_signature(wallet1: Wallet, wallet2: Walle @pytest.mark.asyncio async def test_p2pk_multisig_quorum_not_met_1_of_2(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey() @@ -299,6 +311,7 @@ async def test_p2pk_multisig_quorum_not_met_1_of_2(wallet1: Wallet, wallet2: Wal @pytest.mark.asyncio async def test_p2pk_multisig_quorum_not_met_2_of_3(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey() @@ -323,6 +336,7 @@ async def test_p2pk_multisig_quorum_not_met_2_of_3(wallet1: Wallet, wallet2: Wal @pytest.mark.asyncio async def test_p2pk_multisig_with_duplicate_publickey(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # p2pk test @@ -340,6 +354,7 @@ async def test_p2pk_multisig_with_wrong_first_private_key( wallet1: Wallet, wallet2: Wallet ): invoice = await wallet1.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet1.mint(64, id=invoice.id) await wallet1.create_p2pk_pubkey() pubkey_wallet2 = await wallet2.create_p2pk_pubkey() diff --git a/tests/test_wallet_restore.py b/tests/test_wallet_restore.py index ebb439ba..1e9b213a 100644 --- a/tests/test_wallet_restore.py +++ b/tests/test_wallet_restore.py @@ -12,6 +12,7 @@ from cashu.wallet.wallet import Wallet as Wallet1 from cashu.wallet.wallet import Wallet as Wallet2 from tests.conftest import SERVER_ENDPOINT +from tests.helpers import pay_if_regtest async def assert_err(f, msg: Union[str, CashuError]): @@ -147,6 +148,7 @@ async def test_generate_secrets_from_to(wallet3: Wallet): async def test_restore_wallet_after_mint(wallet3: Wallet): await reset_wallet_db(wallet3) invoice = await wallet3.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet3.mint(64, id=invoice.id) assert wallet3.balance == 64 await reset_wallet_db(wallet3) @@ -177,6 +179,7 @@ async def test_restore_wallet_after_split_to_send(wallet3: Wallet): await reset_wallet_db(wallet3) invoice = await wallet3.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet3.mint(64, id=invoice.id) assert wallet3.balance == 64 @@ -199,6 +202,7 @@ async def test_restore_wallet_after_send_and_receive(wallet3: Wallet, wallet2: W ) await reset_wallet_db(wallet3) invoice = await wallet3.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet3.mint(64, id=invoice.id) assert wallet3.balance == 64 @@ -239,6 +243,7 @@ async def test_restore_wallet_after_send_and_self_receive(wallet3: Wallet): await reset_wallet_db(wallet3) invoice = await wallet3.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet3.mint(64, id=invoice.id) assert wallet3.balance == 64 @@ -265,6 +270,7 @@ async def test_restore_wallet_after_send_twice( await reset_wallet_db(wallet3) invoice = await wallet3.request_mint(2) + pay_if_regtest(invoice.bolt11) await wallet3.mint(2, id=invoice.id) box.add(wallet3.proofs) assert wallet3.balance == 2 @@ -319,6 +325,7 @@ async def test_restore_wallet_after_send_and_self_receive_nonquadratic_value( await reset_wallet_db(wallet3) invoice = await wallet3.request_mint(64) + pay_if_regtest(invoice.bolt11) await wallet3.mint(64, id=invoice.id) box.add(wallet3.proofs) assert wallet3.balance == 64