diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c7e6d92..6cd1c63 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,10 @@ jobs: python-version: ["3.9"] poetry-version: ["1.3.1"] steps: - - uses: actions/checkout@v2 + - name: Checkout repository and submodules + uses: actions/checkout@v2 + with: + submodules: recursive - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: @@ -29,14 +32,15 @@ jobs: LIGHTNING: False MINT_PRIVATE_KEY: "testingkey" MINT_SERVER_HOST: 0.0.0.0 - MINT_SERVER_PORT: 3338 + MINT_SERVER_PORT: 3337 run: | nohup poetry run mint & - name: Run tests env: LIGHTNING: False + WALLET_NAME: test_wallet MINT_HOST: localhost - MINT_PORT: 3338 + MINT_PORT: 3337 TOR: False run: | poetry run pytest tests --cov-report xml --cov cashu diff --git a/cashu/mint/main.py b/cashu/mint/main.py index 5cce3a1..47c86ae 100644 --- a/cashu/mint/main.py +++ b/cashu/mint/main.py @@ -1,5 +1,8 @@ +from typing import Optional + import click import uvicorn +from click import Context from cashu.core.settings import MINT_SERVER_HOST, MINT_SERVER_PORT @@ -16,11 +19,11 @@ from cashu.core.settings import MINT_SERVER_HOST, MINT_SERVER_PORT @click.option("--ssl-certfile", default=None, help="Path to SSL certificate") @click.pass_context def main( - ctx, + ctx: Context, port: int = MINT_SERVER_PORT, host: str = MINT_SERVER_HOST, - ssl_keyfile: str = None, - ssl_certfile: str = None, + ssl_keyfile: Optional[str] = None, + ssl_certfile: Optional[str] = None, ): """This routine starts the uvicorn server if the Cashu mint is launched with `poetry run mint` at root level""" diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index d3acf17..1573c5f 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -16,6 +16,7 @@ from os.path import isdir, join from typing import Dict, List import click +from click import Context from loguru import logger from cashu.core.base import Proof, TokenV2 @@ -80,7 +81,7 @@ class NaturalOrderGroup(click.Group): help="Wallet name (default: wallet).", ) @click.pass_context -def cli(ctx, host: str, walletname: str): +def cli(ctx: Context, host: str, walletname: str): if TOR and not TorProxy().check_platform(): error_str = "Your settings say TOR=true but the built-in Tor bundle is not supported on your system. You have two options: Either install Tor manually and set TOR=FALSE and SOCKS_HOST=localhost and SOCKS_PORT=9050 in your Cashu config (recommended). Or turn off Tor by setting TOR=false (not recommended). Cashu will not work until you edit your config file accordingly." error_str += "\n\n" @@ -124,7 +125,7 @@ def coro(f): ) @click.pass_context @coro -async def pay(ctx, invoice: str, yes: bool): +async def pay(ctx: Context, invoice: str, yes: bool): wallet: Wallet = ctx.obj["WALLET"] await wallet.load_mint() wallet.status() @@ -151,7 +152,7 @@ async def pay(ctx, invoice: str, yes: bool): @click.option("--hash", default="", help="Hash of the paid invoice.", type=str) @click.pass_context @coro -async def invoice(ctx, amount: int, hash: str): +async def invoice(ctx: Context, amount: int, hash: str): wallet: Wallet = ctx.obj["WALLET"] await wallet.load_mint() wallet.status() @@ -203,7 +204,7 @@ async def invoice(ctx, amount: int, hash: str): ) @click.pass_context @coro -async def balance(ctx, verbose): +async def balance(ctx: Context, verbose): wallet: Wallet = ctx.obj["WALLET"] if verbose: # show balances per keyset @@ -227,7 +228,7 @@ async def balance(ctx, verbose): print(f"Balance: {wallet.available_balance} sat") -async def nostr_send(ctx, amount: int, pubkey: str, verbose: bool, yes: bool): +async def nostr_send(ctx: Context, amount: int, pubkey: str, verbose: bool, yes: bool): """ Sends tokens via nostr. """ @@ -259,7 +260,7 @@ async def nostr_send(ctx, amount: int, pubkey: str, verbose: bool, yes: bool): client.close() -async def send(ctx, amount: int, lock: str, legacy: bool): +async def send(ctx: Context, amount: int, lock: str, legacy: bool): """ Prints token to send to stdout. """ @@ -342,7 +343,7 @@ async def send_command( await nostr_send(ctx, amount, nostr, verbose, yes) -async def receive(ctx, token: str, lock: str): +async def receive(ctx: Context, token: str, lock: str): wallet: Wallet = ctx.obj["WALLET"] await wallet.load_mint() @@ -419,7 +420,7 @@ async def receive(ctx, token: str, lock: str): wallet.status() -async def receive_nostr(ctx, verbose: bool): +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." @@ -469,7 +470,7 @@ async def receive_nostr(ctx, verbose: bool): ) @click.pass_context @coro -async def receive_cli(ctx, token: str, lock: str, nostr: bool, verbose: bool): +async def receive_cli(ctx: Context, token: str, lock: str, nostr: bool, verbose: bool): wallet: Wallet = ctx.obj["WALLET"] wallet.status() if token: @@ -488,7 +489,7 @@ async def receive_cli(ctx, token: str, lock: str, nostr: bool, verbose: bool): ) @click.pass_context @coro -async def burn(ctx, token: str, all: bool, force: bool): +async def burn(ctx: Context, token: str, all: bool, force: bool): wallet: Wallet = ctx.obj["WALLET"] await wallet.load_mint() if not (all or token or force) or (token and all): @@ -521,7 +522,7 @@ async def burn(ctx, token: str, all: bool, force: bool): ) @click.pass_context @coro -async def pending(ctx, legacy): +async def pending(ctx: Context, legacy): wallet: Wallet = ctx.obj["WALLET"] reserved_proofs = await get_reserved_proofs(wallet.db) if len(reserved_proofs): @@ -657,7 +658,7 @@ async def wallets(ctx): @cli.command("info", help="Information about Cashu wallet.") @click.pass_context @coro -async def info(ctx): +async def info(ctx: Context): print(f"Version: {VERSION}") print(f"Wallet: {ctx.obj['WALLET_NAME']}") if DEBUG: diff --git a/cashu/wallet/clihelpers.py b/cashu/wallet/clihelpers.py index b31e367..b7626bf 100644 --- a/cashu/wallet/clihelpers.py +++ b/cashu/wallet/clihelpers.py @@ -3,6 +3,7 @@ import urllib.parse from typing import List import click +from click import Context from loguru import logger from cashu.core.base import Proof, TokenV2, TokenV2Mint, WalletKeyset @@ -11,7 +12,7 @@ from cashu.wallet.crud import get_keyset from cashu.wallet.wallet import Wallet as Wallet -async def verify_mints(ctx, token: TokenV2): +async def verify_mints(ctx: Context, token: TokenV2): """ A helper function that iterates through all mints in the token and if it has not been encountered before, asks the user to confirm. @@ -60,7 +61,7 @@ async def verify_mints(ctx, token: TokenV2): assert trust_token_mints, Exception("Aborted!") -async def redeem_multimint(ctx, token: TokenV2, script, signature): +async def redeem_multimint(ctx: Context, 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. @@ -88,7 +89,7 @@ async def redeem_multimint(ctx, token: TokenV2, script, signature): ) -async def print_mint_balances(ctx, wallet, show_mints=False): +async def print_mint_balances(ctx: Context, wallet, show_mints=False): """ Helper function that prints the balances for each mint URL that we have tokens from. """ @@ -114,7 +115,7 @@ async def print_mint_balances(ctx, wallet, show_mints=False): print("") -async def get_mint_wallet(ctx): +async def get_mint_wallet(ctx: Context): """ 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. diff --git a/docs/specs/00.md b/docs/specs/00.md index 7963abe..aec033b 100644 --- a/docs/specs/00.md +++ b/docs/specs/00.md @@ -140,7 +140,6 @@ This token format includes information about the mint as well. The field `proofs } ] } - ``` When serialized, this becomes: diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..969292e --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,42 @@ +import multiprocessing +import time + +import pytest +import pytest_asyncio +import uvicorn +from uvicorn import Config, Server + +from cashu.core.migrations import migrate_databases +from cashu.wallet import migrations +from cashu.wallet.wallet import Wallet + +SERVER_ENDPOINT = "http://localhost:3337" + + +class UvicornServer(multiprocessing.Process): + def __init__(self, config: Config): + super().__init__() + self.server = Server(config=config) + self.config = config + + def stop(self): + self.terminate() + + def run(self, *args, **kwargs): + self.server.run() + + +@pytest.fixture(autouse=True, scope="session") +def mint(): + + config = uvicorn.Config( + "cashu.mint.app:app", + port=3337, + host="127.0.0.1", + ) + + server = UvicornServer(config=config) + server.start() + time.sleep(1) + yield server + server.stop() diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..92e1557 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,117 @@ +import asyncio + +import click +import pytest +from click.testing import CliRunner + +from cashu.core.migrations import migrate_databases +from cashu.core.settings import VERSION +from cashu.wallet import migrations +from cashu.wallet.cli import cli +from cashu.wallet.wallet import Wallet +from tests.conftest import SERVER_ENDPOINT, mint + +cli_prefix = ["--wallet", "test_wallet", "--host", SERVER_ENDPOINT] + + +async def init_wallet(): + wallet = Wallet(SERVER_ENDPOINT, "data/test_wallet", "wallet") + await migrate_databases(wallet.db, migrations) + await wallet.load_proofs() + return wallet + + +@pytest.mark.asyncio +def test_info(): + runner = CliRunner() + result = runner.invoke( + cli, + [*cli_prefix, "info"], + ) + print("INFO") + print(result.output) + result.output.startswith(f"Version: {VERSION}") + assert result.exit_code == 0 + + +@pytest.mark.asyncio +def test_balance(): + runner = CliRunner() + result = runner.invoke( + cli, + [*cli_prefix, "balance"], + ) + print("------ BALANCE ------") + print(result.output) + wallet = asyncio.run(init_wallet()) + assert f"Balance: {wallet.available_balance} sat" in result.output + assert result.exit_code == 0 + + +@pytest.mark.asyncio +def test_wallets(): + runner = CliRunner() + result = runner.invoke( + cli, + [*cli_prefix, "wallets"], + ) + print("WALLETS") + # on github this is empty + if len(result.output): + assert "test_wallet" in result.output + assert result.exit_code == 0 + + +@pytest.mark.asyncio +def test_invoice(): + runner = CliRunner() + result = runner.invoke( + cli, + [*cli_prefix, "invoice", "1000"], + ) + print("INVOICE") + print(result.output) + wallet = asyncio.run(init_wallet()) + assert f"Balance: {wallet.available_balance} sat" in result.output + assert result.exit_code == 0 + + +@pytest.mark.asyncio +def test_send(mint): + runner = CliRunner() + result = runner.invoke( + cli, + [*cli_prefix, "send", "10"], + ) + print("SEND") + print(result.output) + + token = [l for l in result.output.split("\n") if l.startswith("ey")][0] + print("TOKEN") + print(token) + + +@pytest.mark.asyncio +def test_receive_tokenv2(mint): + runner = CliRunner() + token = "eyJwcm9vZnMiOiBbeyJpZCI6ICJEU0FsOW52dnlmdmEiLCAiYW1vdW50IjogMiwgInNlY3JldCI6ICJ3MEs4dE9OcFJOdVFvUzQ1Y2g1NkJ3IiwgIkMiOiAiMDI3NzcxODY4NWQ0MDgxNmQ0MTdmZGE1NWUzN2YxOTFkN2E5ODA0N2QyYWE2YzFlNDRhMWZjNTM1ZmViZDdjZDQ5In0sIHsiaWQiOiAiRFNBbDludnZ5ZnZhIiwgImFtb3VudCI6IDgsICJzZWNyZXQiOiAiX2J4cDVHeG1JQUVaRFB5Sm5qaFUxdyIsICJDIjogIjAzZTY2M2UzOWYyNTZlZTAzOTBiNGFiMThkZDA2OTc0NjRjZjIzYTM4OTc1MDlmZDFlYzQ1MzMxMTRlMTcwMDQ2NCJ9XSwgIm1pbnRzIjogW3sidXJsIjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzNyIsICJpZHMiOiBbIkRTQWw5bnZ2eWZ2YSJdfV19" + result = runner.invoke( + cli, + [*cli_prefix, "receive", token], + ) + + print("RECEIVE") + print(result.output) + + +@pytest.mark.asyncio +def test_receive_tokenv1(mint): + runner = CliRunner() + token = "3siaWQiOiAiRFNBbDludnZ5ZnZhIiwgImFtb3VudCI6IDIsICJzZWNyZXQiOiAiX3VOV1ZNeDRhQndieWszRDZoLWREZyIsICJDIjogIjAyMmEzMzRmZTIzYTA1OTJhZmM3OTk3OWQyZDJmMmUwOTgxMGNkZTRlNDY5ZGYwYzZhMGE4ZDg0ZmY1MmIxOTZhNyJ9LCB7ImlkIjogIkRTQWw5bnZ2eWZ2YSIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIk9VUUxnRE90WXhHOXJUMzZKdHFwbWciLCAiQyI6ICIwMzVmMGM2NTNhNTEzMGY4ZmQwNjY5NDg5YzEwMDY3N2Q5NGU0MGFlZjhkYWE0OWZiZDIyZTgzZjhjNThkZjczMTUifV0" + result = runner.invoke( + cli, + [*cli_prefix, "receive", token], + ) + + print("RECEIVE") + print(result.output) diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 4156cd2..c6dabdb 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -12,8 +12,7 @@ from cashu.wallet import migrations from cashu.wallet.wallet import Wallet from cashu.wallet.wallet import Wallet as Wallet1 from cashu.wallet.wallet import Wallet as Wallet2 - -SERVER_ENDPOINT = "http://localhost:3338" +from tests.conftest import SERVER_ENDPOINT, mint async def assert_err(f, msg): @@ -32,7 +31,7 @@ def assert_amt(proofs: List[Proof], expected: int): @pytest_asyncio.fixture(scope="function") -async def wallet1(): +async def wallet1(mint): wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet1", "wallet1") await migrate_databases(wallet1.db, migrations) await wallet1.load_mint() @@ -41,7 +40,7 @@ async def wallet1(): @pytest_asyncio.fixture(scope="function") -async def wallet2(): +async def wallet2(mint): wallet2 = Wallet2(SERVER_ENDPOINT, "data/wallet2", "wallet2") await migrate_databases(wallet2.db, migrations) await wallet2.load_mint() @@ -53,6 +52,7 @@ async def wallet2(): async def test_get_keys(wallet1: Wallet): assert len(wallet1.keys) == MAX_ORDER keyset = await wallet1._get_keys(wallet1.url) + assert keyset.id is not None assert type(keyset.id) == str assert len(keyset.id) > 0 @@ -63,7 +63,10 @@ async def test_get_keyset(wallet1: Wallet): # ket's get the keys first so we can get a keyset ID that we use later keys1 = await wallet1._get_keys(wallet1.url) # gets the keys of a specific keyset + assert keys1.id is not None + assert keys1.public_keys is not None keys2 = await wallet1._get_keyset(wallet1.url, keys1.id) + assert keys2.public_keys is not None assert len(keys1.public_keys) == len(keys2.public_keys) @@ -156,7 +159,7 @@ async def test_duplicate_proofs_double_spent(wallet1: Wallet): @pytest.mark.asyncio async def test_send_and_redeem(wallet1: Wallet, wallet2: Wallet): await wallet1.mint(64) - _, spendable_proofs = await wallet1.split_to_send( + _, spendable_proofs = await wallet1.split_to_send( # type: ignore wallet1.proofs, 32, set_reserved=True ) await wallet2.redeem(spendable_proofs) @@ -229,7 +232,7 @@ async def test_p2sh_receive_wrong_script(wallet1: Wallet, wallet2: Wallet): p2shscript = await wallet1.create_p2sh_lock() txin_p2sh_address = p2shscript.address lock = f"P2SH:{txin_p2sh_address}" - _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, lock) + _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, lock) # type: ignore wrong_script = "asad" + p2shscript.script @@ -249,7 +252,7 @@ async def test_p2sh_receive_wrong_signature(wallet1: Wallet, wallet2: Wallet): p2shscript = await wallet1.create_p2sh_lock() txin_p2sh_address = p2shscript.address lock = f"P2SH:{txin_p2sh_address}" - _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, lock) + _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, lock) # type: ignore wrong_signature = "asda" + p2shscript.signature