Nutshell cleanup wishlist (#332)

* fix keys

* fix tests

* backwards compatible api upgrade

* upgrade seems to work

* fix tests

* add deprecated api functions

* add more tests of backwards compat

* add test serialization for nut00

* remove a redundant test

* move mint and melt to new api

* mypy works

* CI: mypy --check-untyped-defs

* add deprecated router

* add hints and remove logs

* fix tests

* cleanup

* use new mint and melt endpoints

* tests passing?

* fix mypy

* make format

* make format

* make format

* commit

* errors gone

* save

* adjust the API

* store quotes in db

* make mypy happy

* add fakewallet settings

* remove LIGHTNING=True and pass quote id for melt

* format

* tests passing

* add CoreLightningRestWallet

* add macaroon loader

* add correct config

* preimage -> proof

* move wallet.status() to cli.helpers.print_status()

* remove statuses from tests

* remove

* make format

* Use httpx in deprecated wallet

* fix cln interface

* create invoice before quote

* internal transactions and deprecated api testing

* fix tests

* add deprecated API tests

* fastapi type hints break things

* fix duplicate wallet error

* make format

* update poetry in CI to 1.7.1

* precommit restore

* remove bolt11

* oops

* default poetry

* store fee reserve for melt quotes and refactor melt()

* works?

* make format

* test

* finally

* fix deprecated models

* rename v1 endpoints to bolt11

* raise restore and check to v1, bump version to 0.15.0

* add version byte to keyset id

* remove redundant fields in json

* checks

* generate bip32 keyset wip

* migrate old keysets

* load duplicate keys

* duplicate old keysets

* revert router changes

* add deprecated /check and /restore endpoints

* try except invalidate

* parse unit from derivation path, adjust keyset id calculation with bytes

* remove keyest id from functions again and rely on self.keyset_id

* mosts tests work

* mint loads multiple derivation paths

* make format

* properly print units

* fix tests

* wallet works with multiple units

* add strike wallet and choose backend dynamically

* fix mypy

* add get_payment_quote to lightning backends

* make format

* fix startup

* fix lnbitswallet

* fix tests

* LightningWallet -> LightningBackend

* remove comments

* make format

* remove msat conversion

* add Amount type

* fix regtest

* use melt_quote as argument for pay_invoice

* test old api

* fees in sats

* fix deprecated fees

* fixes

* print balance correctly

* internally index keyset response by int

* add pydantic validation to input models

* add timestamps to mint db

* store timestamps for invoices, promises, proofs_used

* fix wallet migration

* rotate keys correctly for testing

* remove print

* update latest keyset

* fix tests

* fix test

* make format

* make format with correct black version

* remove nsat and cheese

* test against deprecated mint

* fix tests?

* actually use env var

* mint run with env vars

* moar test

* cleanup

* simplify tests, load all keys

* try out testing with internal invoices

* fix internal melt test

* fix test

* deprecated checkfees expects appropriate fees

* adjust comment

* drop lightning table

* split migration for testing for now, remove it later

* remove unused lightning table

* skip_private_key -> skip_db_read

* throw error on migration error

* reorder

* fix migrations

* fix lnbits fee return value negative

* fix typo

* comments

* add type

* make format

* split must use correct amount

* fix tests

* test deprecated api with internal/external melts

* do not split if not necessary

* refactor

* fix test

* make format with new black

* cleanup and add comments

* add quote state check endpoints

* fix deprecated wallet response

* split -> swap endpoint

* make format

* add expiry to quotes, get quote endpoints, and adjust to nut review comments

* allow overpayment of melt

* add lightning wallet tests

* commiting to save

* fix tests a bit

* make format

* remove comments

* get mint info

* check_spendable default False, and return payment quote checking id

* make format

* bump version in pyproject

* update to /v1/checkstate

* make format

* fix mint api checks

* return witness on /v1/checkstate

* no failfast

* try fail-fast: false in ci.yaml

* fix db lookup

* clean up literals
This commit is contained in:
callebtc
2024-01-08 00:57:15 +01:00
committed by GitHub
parent 375b27833a
commit a518274f7e
64 changed files with 5362 additions and 2046 deletions

View File

@@ -1,3 +1,4 @@
import asyncio
import multiprocessing
import os
import shutil
@@ -9,35 +10,50 @@ import pytest_asyncio
import uvicorn
from uvicorn import Config, Server
from cashu.core.base import Method, Unit
from cashu.core.db import Database
from cashu.core.migrations import migrate_databases
from cashu.core.settings import settings
from cashu.lightning.fake import FakeWallet
from cashu.mint import migrations as migrations_mint
from cashu.mint.crud import LedgerCrud
from cashu.mint.crud import LedgerCrudSqlite
from cashu.mint.ledger import Ledger
SERVER_PORT = 3337
SERVER_ENDPOINT = f"http://localhost:{SERVER_PORT}"
settings.debug = True
settings.debug = False
settings.cashu_dir = "./test_data/"
settings.mint_host = "localhost"
settings.mint_port = SERVER_PORT
settings.mint_host = "0.0.0.0"
settings.mint_listen_port = SERVER_PORT
settings.mint_url = SERVER_ENDPOINT
settings.lightning = True
settings.tor = False
settings.wallet_unit = "sat"
settings.mint_lightning_backend = settings.mint_lightning_backend or "FakeWallet"
settings.fakewallet_brr = True
settings.fakewallet_delay_payment = False
settings.fakewallet_stochastic_invoice = False
settings.mint_database = "./test_data/test_mint"
settings.mint_derivation_path = "0/0/0/0"
settings.mint_derivation_path = "m/0'/0'/0'"
settings.mint_derivation_path_list = []
settings.mint_private_key = "TEST_PRIVATE_KEY"
settings.mint_max_balance = 0
assert "test" in settings.cashu_dir
shutil.rmtree(settings.cashu_dir, ignore_errors=True)
Path(settings.cashu_dir).mkdir(parents=True, exist_ok=True)
from cashu.mint.startup import lightning_backend # noqa
@pytest.fixture(scope="session")
def event_loop():
policy = asyncio.get_event_loop_policy()
loop = policy.new_event_loop()
yield loop
loop.close()
class UvicornServer(multiprocessing.Process):
def __init__(self, config: Config):
@@ -52,33 +68,7 @@ class UvicornServer(multiprocessing.Process):
self.server.run()
@pytest_asyncio.fixture(scope="function")
async def ledger():
async def start_mint_init(ledger: Ledger):
await migrate_databases(ledger.db, migrations_mint)
if settings.mint_cache_secrets:
await ledger.load_used_proofs()
await ledger.init_keysets()
database_name = "mint"
if not settings.mint_database.startswith("postgres"):
# clear sqlite database
db_file = os.path.join(settings.mint_database, database_name + ".sqlite3")
if os.path.exists(db_file):
os.remove(db_file)
ledger = Ledger(
db=Database(database_name, settings.mint_database),
seed=settings.mint_private_key,
derivation_path=settings.mint_derivation_path,
lightning=FakeWallet(),
crud=LedgerCrud(),
)
await start_mint_init(ledger)
yield ledger
# # This fixture is used for tests that require API access to the mint
@pytest.fixture(autouse=True, scope="session")
def mint():
config = uvicorn.Config(
@@ -92,3 +82,33 @@ def mint():
time.sleep(1)
yield server
server.stop()
# This fixture is used for all other tests
@pytest_asyncio.fixture(scope="function")
async def ledger():
async def start_mint_init(ledger: Ledger):
await migrate_databases(ledger.db, migrations_mint)
if settings.mint_cache_secrets:
await ledger.load_used_proofs()
await ledger.init_keysets()
if not settings.mint_database.startswith("postgres"):
# clear sqlite database
db_file = os.path.join(settings.mint_database, "mint.sqlite3")
if os.path.exists(db_file):
os.remove(db_file)
backends = {
Method.bolt11: {Unit.sat: lightning_backend},
}
ledger = Ledger(
db=Database("mint", settings.mint_database),
seed=settings.mint_private_key,
derivation_path=settings.mint_derivation_path,
backends=backends,
crud=LedgerCrudSqlite(),
)
await start_mint_init(ledger)
yield ledger
print("teardown")

View File

@@ -29,7 +29,7 @@ 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
is_deprecated_api_only = settings.debug_mint_only_deprecated
docker_lightning_cli = [
"docker",

View File

@@ -1,4 +1,6 @@
import asyncio
import base64
import json
from typing import Tuple
import pytest
@@ -27,8 +29,9 @@ def get_bolt11_and_invoice_id_from_invoice_command(output: str) -> Tuple[str, st
async def init_wallet():
settings.debug = False
wallet = await Wallet.with_db(
url=settings.mint_host,
url=settings.mint_url,
db="test_data/test_cli_wallet",
name="wallet",
)
@@ -56,7 +59,7 @@ def test_info_with_mint(cli_prefix):
[*cli_prefix, "info", "--mint"],
)
assert result.exception is None
print("INFO -M")
print("INFO --MINT")
print(result.output)
assert "Mint name" in result.output
assert result.exit_code == 0
@@ -69,7 +72,7 @@ def test_info_with_mnemonic(cli_prefix):
[*cli_prefix, "info", "--mnemonic"],
)
assert result.exception is None
print("INFO -M")
print("INFO --MNEMONIC")
print(result.output)
assert "Mnemonic" in result.output
assert result.exit_code == 0
@@ -177,7 +180,7 @@ def test_send(mint, cli_prefix):
[*cli_prefix, "send", "10"],
)
assert result.exception is None
print(result.output)
print("test_send", result.output)
token_str = result.output.split("\n")[0]
assert "cashuA" in token_str, "output does not have a token"
token = TokenV3.deserialize(token_str)
@@ -191,7 +194,7 @@ def test_send_with_dleq(mint, cli_prefix):
[*cli_prefix, "send", "10", "--dleq"],
)
assert result.exception is None
print(result.output)
print("test_send_with_dleq", result.output)
token_str = result.output.split("\n")[0]
assert "cashuA" in token_str, "output does not have a token"
token = TokenV3.deserialize(token_str)
@@ -205,7 +208,7 @@ def test_send_legacy(mint, cli_prefix):
[*cli_prefix, "send", "10", "--legacy"],
)
assert result.exception is None
print(result.output)
print("test_send_legacy", result.output)
# this is the legacy token in the output
token_str = result.output.split("\n")[4]
assert token_str.startswith("eyJwcm9v"), "output is not as expected"
@@ -219,7 +222,7 @@ def test_send_without_split(mint, cli_prefix):
)
assert result.exception is None
print("SEND")
print(result.output)
print("test_send_without_split", result.output)
assert "cashuA" in result.output, "output does not have a token"
@@ -234,12 +237,7 @@ def test_send_without_split_but_wrong_amount(mint, cli_prefix):
def test_receive_tokenv3(mint, cli_prefix):
runner = CliRunner()
token = (
"cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIld6TEF2VW53SDlRaFYwQU1rMy1oYWciLC"
"AiQyI6ICIwMmZlMzUxYjAyN2FlMGY1ZDkyN2U2ZjFjMTljMjNjNTc3NzRhZTI2M2UyOGExN2E2MTUxNjY1ZjU3NWNhNjMyNWMifSwgeyJpZCI6ICIxY0NOSUFaMlgvdzEiLCAiYW"
"1vdW50IjogOCwgInNlY3JldCI6ICJDamFTeTcyR2dVOGwzMGV6bE5zZnVBIiwgIkMiOiAiMDNjMzM0OTJlM2ZlNjI4NzFhMWEzMDhiNWUyYjVhZjBkNWI1Mjk5YzI0YmVkNDI2Zj"
"Q1YzZmNDg5N2QzZjc4NGQ5In1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzcifV19"
)
token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjAwOWExZjI5MzI1M2U0MWUiLCAiYW1vdW50IjogMiwgInNlY3JldCI6ICI0NzlkY2E0MzUzNzU4MTM4N2Q1ODllMDU1MGY0Y2Q2MjFmNjE0MDM1MGY5M2Q4ZmI1OTA2YjJlMGRiNmRjYmI3IiwgIkMiOiAiMDM1MGQ0ZmI0YzdiYTMzNDRjMWRjYWU1ZDExZjNlNTIzZGVkOThmNGY4ODdkNTQwZmYyMDRmNmVlOWJjMjkyZjQ1In0sIHsiaWQiOiAiMDA5YTFmMjkzMjUzZTQxZSIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIjZjNjAzNDgwOGQyNDY5N2IyN2YxZTEyMDllNjdjNjVjNmE2MmM2Zjc3NGI4NWVjMGQ5Y2Y3MjE0M2U0NWZmMDEiLCAiQyI6ICIwMjZkNDlhYTE0MmFlNjM1NWViZTJjZGQzYjFhOTdmMjE1MDk2NTlkMDE3YWU0N2FjNDY3OGE4NWVkY2E4MGMxYmQifV0sICJtaW50IjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzNyJ9XX0=" # noqa
result = runner.invoke(
cli,
[
@@ -258,12 +256,29 @@ def test_receive_tokenv3_no_mint(mint, cli_prefix):
# where the mint URL is not in the token therefore, we need to know the mint keyset
# already and have the mint URL in the db
runner = CliRunner()
token = (
"cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIi1oM0ZXMFFoX1FYLW9ac1V2c0RuNlEiLC"
"AiQyI6ICIwMzY5Mzc4MzdlYjg5ZWI4NjMyNWYwOWUyOTIxMWQxYTI4OTRlMzQ2YmM1YzQwZTZhMThlNTk5ZmVjNjEwOGRmMGIifSwgeyJpZCI6ICIxY0NOSUFaMlgvdzEiLCAiYW"
"1vdW50IjogOCwgInNlY3JldCI6ICI3d0VhNUgzZGhSRGRNZl94c1k3c3JnIiwgIkMiOiAiMDJiZmZkM2NlZDkxNjUyMzcxMDg2NjQxMzJiMjgxYjBhZjY1ZTNlZWVkNTY3MmFkZj"
"M0Y2VhNzE5ODhhZWM1NWI1In1dfV19"
)
token_dict = {
"token": [
{
"proofs": [
{
"id": "009a1f293253e41e",
"amount": 2,
"secret": "ea3420987e1ecd71de58e4ff00e8a94d1f1f9333dad98e923e3083d21bf314e2",
"C": "0204eb99cf27105b4de4029478376d6f71e9e3d5af1cc28a652c028d1bcd6537cc",
},
{
"id": "009a1f293253e41e",
"amount": 8,
"secret": "3447975db92f43b269290e05b91805df7aa733f622e55d885a2cab78e02d4a72",
"C": "0286c78750d414bc067178cbac0f3551093cea47d213ebf356899c972448ee6255",
},
]
}
]
}
token = "cashuA" + base64.b64encode(json.dumps(token_dict).encode()).decode()
print("RECEIVE")
print(token)
result = runner.invoke(
cli,
[
@@ -273,18 +288,37 @@ def test_receive_tokenv3_no_mint(mint, cli_prefix):
],
)
assert result.exception is None
print("RECEIVE")
print(result.output)
def test_receive_tokenv2(mint, cli_prefix):
runner = CliRunner()
token = (
"eyJwcm9vZnMiOiBbeyJpZCI6ICIxY0NOSUFaMlgvdzEiLCAiYW1vdW50IjogMiwgInNlY3JldCI6ICJhUmREbzlFdW9yZUVfOW90enRNVVpnIiwgIkMiOiAiMDNhMzY5ZmUy"
"N2IxYmVmOTg4MzA3NDQyN2RjMzc1NmU0NThlMmMwYjQ1NWMwYmVmZGM4ZjVmNTA3YmM5MGQxNmU3In0sIHsiaWQiOiAiMWNDTklBWjJYL3cxIiwgImFtb3VudCI6IDgsICJzZWNy"
"ZXQiOiAiTEZQbFp6Ui1MWHFfYXFDMGhUeDQyZyIsICJDIjogIjAzNGNiYzQxYWY0ODIxMGFmNjVmYjVjOWIzOTNkMjhmMmQ5ZDZhOWE5MzI2YmI3MzQ2YzVkZmRmMTU5MDk1MzI2"
"YyJ9XSwgIm1pbnRzIjogW3sidXJsIjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzNyIsICJpZHMiOiBbIjFjQ05JQVoyWC93MSJdfV19"
)
token_dict = {
"proofs": [
{
"id": "009a1f293253e41e",
"amount": 2,
"secret": (
"a1efb610726b342aec209375397fee86a0b88732779ce218e99132f9a975db2a"
),
"C": (
"03057e5fe352bac785468ffa51a1ecf0f75af24d2d27ab1fd00164672a417d9523"
),
},
{
"id": "009a1f293253e41e",
"amount": 8,
"secret": (
"b065a17938bc79d6224dc381873b8b7f3a46267e8b00d9ce59530354d9d81ae4"
),
"C": (
"021e83773f5eb66f837a5721a067caaa8d7018ef0745b4302f4e2c6cac8806dc69"
),
},
],
"mints": [{"url": "http://localhost:3337", "ids": ["009a1f293253e41e"]}],
}
token = base64.b64encode(json.dumps(token_dict).encode()).decode()
result = runner.invoke(
cli,
[*cli_prefix, "receive", token],
@@ -296,11 +330,25 @@ def test_receive_tokenv2(mint, cli_prefix):
def test_receive_tokenv1(mint, cli_prefix):
runner = CliRunner()
token = (
"W3siaWQiOiAiMWNDTklBWjJYL3cxIiwgImFtb3VudCI6IDIsICJzZWNyZXQiOiAiRnVsc2dzMktQV1FMcUlLX200SzgwQSIsICJDIjogIjAzNTc4OThlYzlhMjIxN2VhYWIx"
"ZDc3YmM1Mzc2OTUwMjJlMjU2YTljMmMwNjc0ZDJlM2FiM2JiNGI0ZDMzMWZiMSJ9LCB7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogInJlRDBD"
"azVNS2xBTUQ0dWk2OEtfbEEiLCAiQyI6ICIwMjNkODNkNDE0MDU0NWQ1NTg4NjUyMzU5YjJhMjFhODljODY1ZGIzMzAyZTkzMTZkYTM5NjA0YTA2ZDYwYWQzOGYifV0="
)
token_dict = [
{
"id": "009a1f293253e41e",
"amount": 2,
"secret": (
"bc0360c041117969ef7b8add48d0981c669619aa5743cccce13d4a771c9e164d"
),
"C": "026fd492f933e9240f36fb2559a7327f47b3441b895a5f8f0b1d6825fee73438f0",
},
{
"id": "009a1f293253e41e",
"amount": 8,
"secret": (
"cf83bd8df35bb104d3818511c1653e9ebeb2b645a36fd071b2229aa2c3044acd"
),
"C": "0279606f3dfd7784757c6320b17e1bf2211f284318814c12bfaa40680e017abd34",
},
]
token = base64.b64encode(json.dumps(token_dict).encode()).decode()
result = runner.invoke(
cli,
[*cli_prefix, "receive", token],

View File

@@ -9,7 +9,7 @@ def test_get_output_split():
assert amount_split(13) == [1, 4, 8]
def test_tokenv3_get_amount():
def test_tokenv3_deserialize_get_attributes():
token_str = (
"cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIkplaFpMVTZuQ3BSZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIjBFN2lDazRkVmxSZjVQRjFnNFpWMnci"
"LCAiQyI6ICIwM2FiNTgwYWQ5NTc3OGVkNTI5NmY4YmVlNjU1ZGJkN2Q2NDJmNWQzMmRlOGUyNDg0NzdlMGI0ZDZhYTg2M2ZjZDUifSwgeyJpZCI6ICJKZWhaTFU2bkNwUmQiLCAiYW"
@@ -18,16 +18,6 @@ def test_tokenv3_get_amount():
)
token = TokenV3.deserialize(token_str)
assert token.get_amount() == 10
def test_tokenv3_get_proofs():
token_str = (
"cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIkplaFpMVTZuQ3BSZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIjBFN2lDazRkVmxSZjVQRjFnNFpWMnci"
"LCAiQyI6ICIwM2FiNTgwYWQ5NTc3OGVkNTI5NmY4YmVlNjU1ZGJkN2Q2NDJmNWQzMmRlOGUyNDg0NzdlMGI0ZDZhYTg2M2ZjZDUifSwgeyJpZCI6ICJKZWhaTFU2bkNwUmQiLCAiYW"
"1vdW50IjogOCwgInNlY3JldCI6ICJzNklwZXh3SGNxcXVLZDZYbW9qTDJnIiwgIkMiOiAiMDIyZDAwNGY5ZWMxNmE1OGFkOTAxNGMyNTliNmQ2MTRlZDM2ODgyOWYwMmMzODc3M2M0"
"NzIyMWY0OTYxY2UzZjIzIn1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzgifV19"
)
token = TokenV3.deserialize(token_str)
assert len(token.get_proofs()) == 2
@@ -117,6 +107,43 @@ def test_tokenv3_deserialize_with_memo():
assert token.memo == "Test memo"
def test_serialize_example_token_nut00():
token_dict = {
"token": [
{
"mint": "https://8333.space:3338",
"proofs": [
{
"id": "9bb9d58392cd823e",
"amount": 2,
"secret": "EhpennC9qB3iFlW8FZ_pZw",
"C": "02c020067db727d586bc3183aecf97fcb800c3f4cc4759f69c626c9db5d8f5b5d4",
},
{
"id": "9bb9d58392cd823e",
"amount": 8,
"secret": "TmS6Cv0YT5PU_5ATVKnukw",
"C": "02ac910bef28cbe5d7325415d5c263026f15f9b967a079ca9779ab6e5c2db133a7",
},
],
}
],
"memo": "Thank you.",
}
tokenObj = TokenV3.parse_obj(token_dict)
assert (
tokenObj.serialize()
== "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjliYjlkNTgzOTJjZDg"
"yM2UiLCAiYW1vdW50IjogMiwgInNlY3JldCI6ICJFaHBlbm5DOXFCM2lGbFc4Rlpf"
"cFp3IiwgIkMiOiAiMDJjMDIwMDY3ZGI3MjdkNTg2YmMzMTgzYWVjZjk3ZmNiODAwY"
"zNmNGNjNDc1OWY2OWM2MjZjOWRiNWQ4ZjViNWQ0In0sIHsiaWQiOiAiOWJiOWQ1OD"
"M5MmNkODIzZSIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIlRtUzZDdjBZVDVQVV8"
"1QVRWS251a3ciLCAiQyI6ICIwMmFjOTEwYmVmMjhjYmU1ZDczMjU0MTVkNWMyNjMw"
"MjZmMTVmOWI5NjdhMDc5Y2E5Nzc5YWI2ZTVjMmRiMTMzYTcifV0sICJtaW50IjogI"
"mh0dHBzOi8vODMzMy5zcGFjZTozMzM4In1dLCAibWVtbyI6ICJUaGFuayB5b3UuIn0="
)
def test_calculate_number_of_blank_outputs():
# Example from NUT-08 specification.
fee_reserve_sat = 1000

View File

@@ -2,11 +2,12 @@ from typing import List
import pytest
from cashu.core.base import BlindedMessage, Proof
from cashu.core.base import BlindedMessage, PostMintQuoteRequest, Proof
from cashu.core.crypto.b_dhke import step1_alice
from cashu.core.helpers import calculate_number_of_blank_outputs
from cashu.core.settings import settings
from cashu.mint.ledger import Ledger
from tests.helpers import pay_if_regtest
async def assert_err(f, msg):
@@ -29,11 +30,11 @@ async def test_pubkeys(ledger: Ledger):
assert ledger.keyset.public_keys
assert (
ledger.keyset.public_keys[1].serialize().hex()
== "03190ebc0c3e2726a5349904f572a2853ea021b0128b269b8b6906501d262edaa8"
== "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104"
)
assert (
ledger.keyset.public_keys[2 ** (settings.max_order - 1)].serialize().hex()
== "032dc008b88b85fdc2301a499bfaaef774c191a6307d8c9434838fc2eaa2e48d51"
== "023c84c0895cc0e827b348ea0a62951ca489a5e436f3ea7545f3c1d5f1bea1c866"
)
@@ -42,19 +43,30 @@ async def test_privatekeys(ledger: Ledger):
assert ledger.keyset.private_keys
assert (
ledger.keyset.private_keys[1].serialize()
== "67de62e1bf8b5ccf88dbad6768b7d13fa0f41433b0a89caf915039505f2e00a7"
== "8300050453f08e6ead1296bb864e905bd46761beed22b81110fae0751d84604d"
)
assert (
ledger.keyset.private_keys[2 ** (settings.max_order - 1)].serialize()
== "3b1340c703b02028a11025302d2d9e68d2a6dd721ab1a2770f0942d15eacb8d0"
== "b0477644cb3d82ffcc170bc0a76e0409727232e87c5ae51d64a259936228c7be"
)
@pytest.mark.asyncio
async def test_keysets(ledger: Ledger):
assert len(ledger.keysets.keysets)
assert len(ledger.keysets.get_ids())
assert ledger.keyset.id == "1cCNIAZ2X/w1"
assert len(ledger.keysets)
assert len(list(ledger.keysets.keys()))
assert ledger.keyset.id == "009a1f293253e41e"
@pytest.mark.asyncio
async def test_keysets_backwards_compatibility_pre_v0_15(ledger: Ledger):
"""Backwards compatibility test for keysets pre v0.15.0
We expect two instances of the same keyset but with different IDs.
First one is the new hex ID, second one is the old base64 ID.
"""
assert len(ledger.keysets) == 2
assert list(ledger.keysets.keys()) == ["009a1f293253e41e", "eGnEWtdJ0PIM"]
assert ledger.keyset.id == "009a1f293253e41e"
@pytest.mark.asyncio
@@ -66,33 +78,37 @@ async def test_get_keyset(ledger: Ledger):
@pytest.mark.asyncio
async def test_mint(ledger: Ledger):
invoice, id = await ledger.request_mint(8)
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat"))
pay_if_regtest(quote.request)
blinded_messages_mock = [
BlindedMessage(
amount=8,
B_="02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239",
id="009a1f293253e41e",
)
]
promises = await ledger.mint(blinded_messages_mock, id=id)
promises = await ledger.mint(outputs=blinded_messages_mock, quote_id=quote.quote)
assert len(promises)
assert promises[0].amount == 8
assert (
promises[0].C_
== "037074c4f53e326ee14ed67125f387d160e0e729351471b69ad41f7d5d21071e15"
== "031422eeffb25319e519c68de000effb294cb362ef713a7cf4832cea7b0452ba6e"
)
@pytest.mark.asyncio
async def test_mint_invalid_blinded_message(ledger: Ledger):
invoice, id = await ledger.request_mint(8)
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat"))
pay_if_regtest(quote.request)
blinded_messages_mock_invalid_key = [
BlindedMessage(
amount=8,
B_="02634a2c2b34bec9e8a4aba4361f6bff02d7fa2365379b0840afe249a7a9d71237",
id="009a1f293253e41e",
)
]
await assert_err(
ledger.mint(blinded_messages_mock_invalid_key, id=id),
ledger.mint(outputs=blinded_messages_mock_invalid_key, quote_id=quote.quote),
"invalid public key",
)
@@ -103,14 +119,16 @@ async def test_generate_promises(ledger: Ledger):
BlindedMessage(
amount=8,
B_="02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239",
id="009a1f293253e41e",
)
]
promises = await ledger._generate_promises(blinded_messages_mock)
assert (
promises[0].C_
== "037074c4f53e326ee14ed67125f387d160e0e729351471b69ad41f7d5d21071e15"
== "031422eeffb25319e519c68de000effb294cb362ef713a7cf4832cea7b0452ba6e"
)
assert promises[0].amount == 8
assert promises[0].id == "009a1f293253e41e"
# DLEQ proof present
assert promises[0].dleq
@@ -118,6 +136,55 @@ async def test_generate_promises(ledger: Ledger):
assert promises[0].dleq.e
@pytest.mark.asyncio
async def test_generate_promises_deprecated_keyset_id(ledger: Ledger):
blinded_messages_mock = [
BlindedMessage(
amount=8,
B_="02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239",
id="eGnEWtdJ0PIM",
)
]
promises = await ledger._generate_promises(blinded_messages_mock)
assert (
promises[0].C_
== "031422eeffb25319e519c68de000effb294cb362ef713a7cf4832cea7b0452ba6e"
)
assert promises[0].amount == 8
assert promises[0].id == "eGnEWtdJ0PIM"
# DLEQ proof present
assert promises[0].dleq
assert promises[0].dleq.s
assert promises[0].dleq.e
@pytest.mark.asyncio
async def test_generate_promises_keyset_backwards_compatibility_pre_v0_15(
ledger: Ledger,
):
"""Backwards compatibility test for keysets pre v0.15.0
We want to generate promises using the old keyset ID.
We expect the promise to have the old base64 ID.
"""
blinded_messages_mock = [
BlindedMessage(
amount=8,
B_="02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239",
id="eGnEWtdJ0PIM",
)
]
promises = await ledger._generate_promises(
blinded_messages_mock, keyset=ledger.keysets["eGnEWtdJ0PIM"]
)
assert (
promises[0].C_
== "031422eeffb25319e519c68de000effb294cb362ef713a7cf4832cea7b0452ba6e"
)
assert promises[0].amount == 8
assert promises[0].id == "eGnEWtdJ0PIM"
@pytest.mark.asyncio
async def test_generate_change_promises(ledger: Ledger):
# Example slightly adapted from NUT-08 because we want to ensure the dynamic change
@@ -125,7 +192,7 @@ async def test_generate_change_promises(ledger: Ledger):
invoice_amount = 100_000
fee_reserve = 2_000
total_provided = invoice_amount + fee_reserve
actual_fee_msat = 100_000
actual_fee = 100
expected_returned_promises = 7 # Amounts = [4, 8, 32, 64, 256, 512, 1024]
expected_returned_fees = 1900
@@ -133,11 +200,16 @@ async def test_generate_change_promises(ledger: Ledger):
n_blank_outputs = calculate_number_of_blank_outputs(fee_reserve)
blinded_msgs = [step1_alice(str(n)) for n in range(n_blank_outputs)]
outputs = [
BlindedMessage(amount=1, B_=b.serialize().hex()) for b, _ in blinded_msgs
BlindedMessage(
amount=1,
B_=b.serialize().hex(),
id="009a1f293253e41e",
)
for b, _ in blinded_msgs
]
promises = await ledger._generate_change_promises(
total_provided, invoice_amount, actual_fee_msat, outputs
total_provided, invoice_amount, actual_fee, outputs
)
assert len(promises) == expected_returned_promises
@@ -151,7 +223,7 @@ async def test_generate_change_promises_legacy_wallet(ledger: Ledger):
invoice_amount = 100_000
fee_reserve = 2_000
total_provided = invoice_amount + fee_reserve
actual_fee_msat = 100_000
actual_fee = 100
expected_returned_promises = 4 # Amounts = [64, 256, 512, 1024]
expected_returned_fees = 1856
@@ -159,11 +231,16 @@ async def test_generate_change_promises_legacy_wallet(ledger: Ledger):
n_blank_outputs = 4
blinded_msgs = [step1_alice(str(n)) for n in range(n_blank_outputs)]
outputs = [
BlindedMessage(amount=1, B_=b.serialize().hex()) for b, _ in blinded_msgs
BlindedMessage(
amount=1,
B_=b.serialize().hex(),
id="009a1f293253e41e",
)
for b, _ in blinded_msgs
]
promises = await ledger._generate_change_promises(
total_provided, invoice_amount, actual_fee_msat, outputs
total_provided, invoice_amount, actual_fee, outputs
)
assert len(promises) == expected_returned_promises
@@ -193,9 +270,9 @@ async def test_get_balance(ledger: Ledger):
@pytest.mark.asyncio
async def test_maximum_balance(ledger: Ledger):
settings.mint_max_balance = 1000
invoice, id = await ledger.request_mint(8)
await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat"))
await assert_err(
ledger.request_mint(8000),
ledger.mint_quote(PostMintQuoteRequest(amount=8000, unit="sat")),
"Mint has reached maximum balance.",
)
settings.mint_max_balance = 0

View File

@@ -1,71 +1,371 @@
import bolt11
import httpx
import pytest
import pytest_asyncio
from cashu.core.base import CheckSpendableRequest, CheckSpendableResponse, Proof
from cashu.core.base import (
PostCheckStateRequest,
PostCheckStateResponse,
SpentState,
)
from cashu.core.settings import settings
from cashu.mint.ledger import Ledger
from cashu.wallet.wallet import Wallet
from tests.helpers import get_real_invoice, is_fake, is_regtest, pay_if_regtest
BASE_URL = "http://localhost:3337"
@pytest_asyncio.fixture(scope="function")
async def wallet(ledger: Ledger):
wallet1 = await Wallet.with_db(
url=BASE_URL,
db="test_data/wallet_mint_api",
name="wallet_mint_api",
)
await wallet1.load_mint()
yield wallet1
@pytest.mark.asyncio
async def test_info(ledger):
response = httpx.get(f"{BASE_URL}/info")
@pytest.mark.skipif(
settings.debug_mint_only_deprecated,
reason="settings.debug_mint_only_deprecated is set",
)
async def test_info(ledger: Ledger):
response = httpx.get(f"{BASE_URL}/v1/info")
assert response.status_code == 200, f"{response.url} {response.status_code}"
assert ledger.pubkey
assert response.json()["pubkey"] == ledger.pubkey.serialize().hex()
@pytest.mark.asyncio
async def test_api_keys(ledger):
response = httpx.get(f"{BASE_URL}/keys")
@pytest.mark.skipif(
settings.debug_mint_only_deprecated,
reason="settings.debug_mint_only_deprecated is set",
)
async def test_api_keys(ledger: Ledger):
response = httpx.get(f"{BASE_URL}/v1/keys")
assert response.status_code == 200, f"{response.url} {response.status_code}"
assert response.json() == {
str(k): v.serialize().hex() for k, v in ledger.keyset.public_keys.items()
assert ledger.keyset.public_keys
expected = {
"keysets": [
{
"id": keyset.id,
"unit": keyset.unit.name,
"keys": {
str(k): v.serialize().hex() for k, v in keyset.public_keys.items() # type: ignore
},
}
for keyset in ledger.keysets.values()
]
}
assert response.json() == expected
@pytest.mark.asyncio
async def test_api_keysets(ledger):
response = httpx.get(f"{BASE_URL}/keysets")
@pytest.mark.skipif(
settings.debug_mint_only_deprecated,
reason="settings.debug_mint_only_deprecated is set",
)
async def test_api_keysets(ledger: Ledger):
response = httpx.get(f"{BASE_URL}/v1/keysets")
assert response.status_code == 200, f"{response.url} {response.status_code}"
assert response.json()["keysets"] == list(ledger.keysets.keysets.keys())
expected = {
"keysets": [
{
"id": "009a1f293253e41e",
"unit": "sat",
"active": True,
},
# for backwards compatibility of the new keyset ID format,
# we also return the same keyset with the old base64 ID
{
"id": "eGnEWtdJ0PIM",
"unit": "sat",
"active": True,
},
]
}
assert response.json() == expected
@pytest.mark.asyncio
async def test_api_keyset_keys(ledger):
response = httpx.get(
f"{BASE_URL}/keys/{'1cCNIAZ2X/w1'.replace('/', '_').replace('+', '-')}"
@pytest.mark.skipif(
settings.debug_mint_only_deprecated,
reason="settings.debug_mint_only_deprecated is set",
)
async def test_api_keyset_keys(ledger: Ledger):
response = httpx.get(f"{BASE_URL}/v1/keys/009a1f293253e41e")
assert response.status_code == 200, f"{response.url} {response.status_code}"
assert ledger.keyset.public_keys
expected = {
"keysets": [
{
"id": "009a1f293253e41e",
"unit": "sat",
"keys": {
str(k): v.serialize().hex()
for k, v in ledger.keysets["009a1f293253e41e"].public_keys.items() # type: ignore
},
}
]
}
assert response.json() == expected
@pytest.mark.asyncio
@pytest.mark.skipif(
settings.debug_mint_only_deprecated,
reason="settings.debug_mint_only_deprecated is set",
)
async def test_api_keyset_keys_old_keyset_id(ledger: Ledger):
response = httpx.get(f"{BASE_URL}/v1/keys/eGnEWtdJ0PIM")
assert response.status_code == 200, f"{response.url} {response.status_code}"
assert ledger.keyset.public_keys
expected = {
"keysets": [
{
"id": "eGnEWtdJ0PIM",
"unit": "sat",
"keys": {
str(k): v.serialize().hex()
for k, v in ledger.keysets["eGnEWtdJ0PIM"].public_keys.items() # type: ignore
},
}
]
}
assert response.json() == expected
@pytest.mark.asyncio
@pytest.mark.skipif(
settings.debug_mint_only_deprecated,
reason="settings.debug_mint_only_deprecated is set",
)
async def test_split(ledger: Ledger, wallet: Wallet):
invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64
secrets, rs, derivation_paths = await wallet.generate_n_secrets(2)
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
# outputs = wallet._construct_outputs([32, 32], ["a", "b"], ["c", "d"])
inputs_payload = [p.to_dict() for p in wallet.proofs]
outputs_payload = [o.dict() for o in outputs]
payload = {"inputs": inputs_payload, "outputs": outputs_payload}
response = httpx.post(f"{BASE_URL}/v1/swap", json=payload)
assert response.status_code == 200, f"{response.url} {response.status_code}"
result = response.json()
assert len(result["signatures"]) == 2
assert result["signatures"][0]["amount"] == 32
assert result["signatures"][1]["amount"] == 32
assert result["signatures"][0]["id"] == "009a1f293253e41e"
assert result["signatures"][0]["dleq"]
assert "e" in result["signatures"][0]["dleq"]
assert "s" in result["signatures"][0]["dleq"]
@pytest.mark.asyncio
@pytest.mark.skipif(
settings.debug_mint_only_deprecated,
reason="settings.debug_mint_only_deprecated is set",
)
async def test_mint_quote(ledger: Ledger):
response = httpx.post(
f"{BASE_URL}/v1/mint/quote/bolt11",
json={"unit": "sat", "amount": 100},
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
assert response.json() == {
str(k): v.serialize().hex() for k, v in ledger.keyset.public_keys.items()
}
result = response.json()
assert result["quote"]
assert result["request"]
invoice = bolt11.decode(result["request"])
assert invoice.amount_msat == 100 * 1000
@pytest.mark.asyncio
async def test_api_mint_validation(ledger):
response = httpx.get(f"{BASE_URL}/mint?amount=-21")
assert "detail" in response.json()
response = httpx.get(f"{BASE_URL}/mint?amount=0")
assert "detail" in response.json()
response = httpx.get(f"{BASE_URL}/mint?amount=2100000000000001")
assert "detail" in response.json()
response = httpx.get(f"{BASE_URL}/mint?amount=1")
assert "detail" not in response.json()
@pytest.mark.asyncio
async def test_api_check_state(ledger):
proofs = [
Proof(id="1234", amount=0, secret="asdasdasd", C="asdasdasd"),
Proof(id="1234", amount=0, secret="asdasdasd1", C="asdasdasd1"),
]
payload = CheckSpendableRequest(proofs=proofs)
@pytest.mark.skipif(
settings.debug_mint_only_deprecated,
reason="settings.debug_mint_only_deprecated is set",
)
async def test_mint(ledger: Ledger, wallet: Wallet):
invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11)
quote_id = invoice.id
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001)
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
outputs_payload = [o.dict() for o in outputs]
response = httpx.post(
f"{BASE_URL}/check",
f"{BASE_URL}/v1/mint/bolt11",
json={"quote": quote_id, "outputs": outputs_payload},
timeout=None,
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
result = response.json()
assert len(result["signatures"]) == 2
assert result["signatures"][0]["amount"] == 32
assert result["signatures"][1]["amount"] == 32
assert result["signatures"][0]["id"] == "009a1f293253e41e"
assert result["signatures"][0]["dleq"]
assert "e" in result["signatures"][0]["dleq"]
assert "s" in result["signatures"][0]["dleq"]
@pytest.mark.asyncio
@pytest.mark.skipif(
settings.debug_mint_only_deprecated,
reason="settings.debug_mint_only_deprecated is set",
)
@pytest.mark.skipif(
is_regtest,
reason="regtest",
)
async def test_melt_quote_internal(ledger: Ledger, wallet: Wallet):
# internal invoice
invoice = await wallet.request_mint(64)
request = invoice.bolt11
response = httpx.post(
f"{BASE_URL}/v1/melt/quote/bolt11",
json={"unit": "sat", "request": request},
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
result = response.json()
assert result["quote"]
assert result["amount"] == 64
# TODO: internal invoice, fee should be 0
assert result["fee_reserve"] == 0
@pytest.mark.asyncio
@pytest.mark.skipif(
settings.debug_mint_only_deprecated,
reason="settings.debug_mint_only_deprecated is set",
)
@pytest.mark.skipif(
is_fake,
reason="only works on regtest",
)
async def test_melt_quote_external(ledger: Ledger, wallet: Wallet):
# internal invoice
invoice_dict = get_real_invoice(64)
request = invoice_dict["payment_request"]
response = httpx.post(
f"{BASE_URL}/v1/melt/quote/bolt11",
json={"unit": "sat", "request": request},
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
result = response.json()
assert result["quote"]
assert result["amount"] == 64
# external invoice, fee should be 2
assert result["fee_reserve"] == 2
@pytest.mark.asyncio
@pytest.mark.skipif(
settings.debug_mint_only_deprecated,
reason="settings.debug_mint_only_deprecated is set",
)
async def test_melt_internal(ledger: Ledger, wallet: Wallet):
# internal invoice
invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64
# create invoice to melt to
invoice = await wallet.request_mint(64)
invoice_payment_request = invoice.bolt11
quote = await wallet.melt_quote(invoice_payment_request)
assert quote.amount == 64
assert quote.fee_reserve == 0
inputs_payload = [p.to_dict() for p in wallet.proofs]
# outputs for change
secrets, rs, derivation_paths = await wallet.generate_n_secrets(1)
outputs, rs = wallet._construct_outputs([2], secrets, rs)
outputs_payload = [o.dict() for o in outputs]
response = httpx.post(
f"{BASE_URL}/v1/melt/bolt11",
json={
"quote": quote.quote,
"inputs": inputs_payload,
"outputs": outputs_payload,
},
timeout=None,
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
result = response.json()
assert result.get("payment_preimage") is not None
assert result["paid"] is True
@pytest.mark.asyncio
@pytest.mark.skipif(
settings.debug_mint_only_deprecated,
reason="settings.debug_mint_only_deprecated is set",
)
@pytest.mark.skipif(
is_fake,
reason="only works on regtest",
)
async def test_melt_external(ledger: Ledger, wallet: Wallet):
# internal invoice
invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64
invoice_dict = get_real_invoice(62)
invoice_payment_request = invoice_dict["payment_request"]
quote = await wallet.melt_quote(invoice_payment_request)
assert quote.amount == 62
assert quote.fee_reserve == 2
keep, send = await wallet.split_to_send(wallet.proofs, 64)
inputs_payload = [p.to_dict() for p in send]
# outputs for change
secrets, rs, derivation_paths = await wallet.generate_n_secrets(1)
outputs, rs = wallet._construct_outputs([2], secrets, rs)
outputs_payload = [o.dict() for o in outputs]
response = httpx.post(
f"{BASE_URL}/v1/melt/bolt11",
json={
"quote": quote.quote,
"inputs": inputs_payload,
"outputs": outputs_payload,
},
timeout=None,
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
result = response.json()
assert result.get("payment_preimage") is not None
assert result["paid"] is True
assert result["change"]
# we get back 2 sats because Lightning was free to pay on regtest
assert result["change"][0]["amount"] == 2
@pytest.mark.asyncio
@pytest.mark.skipif(
settings.debug_mint_only_deprecated,
reason="settings.debug_mint_only_deprecated is set",
)
async def test_api_check_state(ledger: Ledger):
payload = PostCheckStateRequest(secrets=["asdasdasd", "asdasdasd1"])
response = httpx.post(
f"{BASE_URL}/v1/checkstate",
json=payload.dict(),
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
states = CheckSpendableResponse.parse_obj(response.json())
assert states.spendable
assert len(states.spendable) == 2
assert states.pending
assert len(states.pending) == 2
response = PostCheckStateResponse.parse_obj(response.json())
assert response
assert len(response.states) == 2
assert response.states[0].state == SpentState.unspent

View File

@@ -0,0 +1,289 @@
import httpx
import pytest
import pytest_asyncio
from cashu.core.base import (
CheckSpendableRequest_deprecated,
CheckSpendableResponse_deprecated,
Proof,
)
from cashu.core.settings import settings
from cashu.mint.ledger import Ledger
from cashu.wallet.wallet import Wallet
from tests.helpers import get_real_invoice, is_fake, is_regtest, pay_if_regtest
BASE_URL = "http://localhost:3337"
@pytest_asyncio.fixture(scope="function")
async def wallet(ledger: Ledger):
wallet1 = await Wallet.with_db(
url=BASE_URL,
db="test_data/wallet_mint_api_deprecated",
name="wallet_mint_api_deprecated",
)
await wallet1.load_mint()
yield wallet1
@pytest.mark.asyncio
async def test_info(ledger: Ledger):
response = httpx.get(f"{BASE_URL}/info")
assert response.status_code == 200, f"{response.url} {response.status_code}"
assert ledger.pubkey
assert response.json()["pubkey"] == ledger.pubkey.serialize().hex()
@pytest.mark.asyncio
async def test_api_keys(ledger: Ledger):
response = httpx.get(f"{BASE_URL}/keys")
assert response.status_code == 200, f"{response.url} {response.status_code}"
assert ledger.keyset.public_keys
assert response.json() == {
str(k): v.serialize().hex() for k, v in ledger.keyset.public_keys.items()
}
@pytest.mark.asyncio
async def test_api_keysets(ledger: Ledger):
response = httpx.get(f"{BASE_URL}/keysets")
assert response.status_code == 200, f"{response.url} {response.status_code}"
assert ledger.keyset.public_keys
assert response.json()["keysets"] == list(ledger.keysets.keys())
@pytest.mark.asyncio
async def test_api_keyset_keys(ledger: Ledger):
response = httpx.get(f"{BASE_URL}/keys/009a1f293253e41e")
assert response.status_code == 200, f"{response.url} {response.status_code}"
assert ledger.keyset.public_keys
assert response.json() == {
str(k): v.serialize().hex() for k, v in ledger.keyset.public_keys.items()
}
@pytest.mark.asyncio
async def test_split(ledger: Ledger, wallet: Wallet):
invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(20000, 20001)
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
# outputs = wallet._construct_outputs([32, 32], ["a", "b"], ["c", "d"])
inputs_payload = [p.to_dict() for p in wallet.proofs]
outputs_payload = [o.dict() for o in outputs]
# strip "id" from outputs_payload, which is not used in the deprecated split endpoint
for o in outputs_payload:
o.pop("id")
payload = {"proofs": inputs_payload, "outputs": outputs_payload}
response = httpx.post(f"{BASE_URL}/split", json=payload, timeout=None)
assert response.status_code == 200, f"{response.url} {response.status_code}"
result = response.json()
assert result["promises"]
@pytest.mark.asyncio
async def test_split_deprecated_with_amount(ledger: Ledger, wallet: Wallet):
invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(80000, 80001)
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
# outputs = wallet._construct_outputs([32, 32], ["a", "b"], ["c", "d"])
inputs_payload = [p.to_dict() for p in wallet.proofs]
outputs_payload = [o.dict() for o in outputs]
# strip "id" from outputs_payload, which is not used in the deprecated split endpoint
for o in outputs_payload:
o.pop("id")
# we supply an amount here, which should cause the very old deprecated split endpoint to be used
payload = {"proofs": inputs_payload, "outputs": outputs_payload, "amount": 32}
response = httpx.post(f"{BASE_URL}/split", json=payload, timeout=None)
assert response.status_code == 200, f"{response.url} {response.status_code}"
result = response.json()
# old deprecated output format
assert result["fst"]
assert result["snd"]
@pytest.mark.asyncio
async def test_api_mint_validation(ledger):
response = httpx.get(f"{BASE_URL}/mint?amount=-21")
assert "detail" in response.json()
response = httpx.get(f"{BASE_URL}/mint?amount=0")
assert "detail" in response.json()
response = httpx.get(f"{BASE_URL}/mint?amount=2100000000000001")
assert "detail" in response.json()
response = httpx.get(f"{BASE_URL}/mint?amount=1")
assert "detail" not in response.json()
@pytest.mark.asyncio
async def test_mint(ledger: Ledger, wallet: Wallet):
invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11)
quote_id = invoice.id
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001)
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
outputs_payload = [o.dict() for o in outputs]
response = httpx.post(
f"{BASE_URL}/mint",
json={"outputs": outputs_payload},
params={"hash": quote_id},
timeout=None,
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
result = response.json()
assert len(result["promises"]) == 2
assert result["promises"][0]["amount"] == 32
assert result["promises"][1]["amount"] == 32
if settings.debug_mint_only_deprecated:
assert result["promises"][0]["id"] == "eGnEWtdJ0PIM"
else:
assert result["promises"][0]["id"] == "009a1f293253e41e"
assert result["promises"][0]["dleq"]
assert "e" in result["promises"][0]["dleq"]
assert "s" in result["promises"][0]["dleq"]
@pytest.mark.asyncio
async def test_melt_internal(ledger: Ledger, wallet: Wallet):
# internal invoice
invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64
# create invoice to melt to
invoice = await wallet.request_mint(64)
invoice_payment_request = invoice.bolt11
quote = await wallet.melt_quote(invoice_payment_request)
assert quote.amount == 64
assert quote.fee_reserve == 0
inputs_payload = [p.to_dict() for p in wallet.proofs]
# outputs for change
secrets, rs, derivation_paths = await wallet.generate_n_secrets(1)
outputs, rs = wallet._construct_outputs([2], secrets, rs)
outputs_payload = [o.dict() for o in outputs]
response = httpx.post(
f"{BASE_URL}/melt",
json={
"pr": invoice_payment_request,
"proofs": inputs_payload,
"outputs": outputs_payload,
},
timeout=None,
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
result = response.json()
assert result.get("preimage") is not None
assert result["paid"] is True
@pytest.mark.asyncio
@pytest.mark.skipif(
is_fake,
reason="only works on regtest",
)
async def test_melt_external(ledger: Ledger, wallet: Wallet):
# internal invoice
invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64
# create invoice to melt to
# use 2 sat less because we need to pay the fee
invoice_dict = get_real_invoice(62)
invoice_payment_request = invoice_dict["payment_request"]
quote = await wallet.melt_quote(invoice_payment_request)
assert quote.amount == 62
assert quote.fee_reserve == 2
inputs_payload = [p.to_dict() for p in wallet.proofs]
# outputs for change
secrets, rs, derivation_paths = await wallet.generate_n_secrets(1)
outputs, rs = wallet._construct_outputs([2], secrets, rs)
outputs_payload = [o.dict() for o in outputs]
response = httpx.post(
f"{BASE_URL}/melt",
json={
"pr": invoice_payment_request,
"proofs": inputs_payload,
"outputs": outputs_payload,
},
timeout=None,
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
result = response.json()
assert result.get("preimage") is not None
assert result["paid"] is True
assert result["change"]
# we get back 2 sats because Lightning was free to pay on regtest
assert result["change"][0]["amount"] == 2
@pytest.mark.asyncio
async def test_checkfees(ledger: Ledger, wallet: Wallet):
# internal invoice
invoice = await wallet.request_mint(64)
response = httpx.post(
f"{BASE_URL}/checkfees",
json={
"pr": invoice.bolt11,
},
timeout=None,
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
result = response.json()
# internal invoice, so no fee
assert result["fee"] == 0
@pytest.mark.asyncio
@pytest.mark.skipif(not is_regtest, reason="only works on regtest")
async def test_checkfees_external(ledger: Ledger, wallet: Wallet):
# external invoice
invoice_dict = get_real_invoice(62)
invoice_payment_request = invoice_dict["payment_request"]
response = httpx.post(
f"{BASE_URL}/checkfees",
json={"pr": invoice_payment_request},
timeout=None,
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
result = response.json()
# external invoice, so fee
assert result["fee"] == 2
@pytest.mark.asyncio
@pytest.mark.skipif(
settings.debug_mint_only_deprecated,
reason="settings.debug_mint_only_deprecated is set",
)
async def test_api_check_state(ledger: Ledger):
proofs = [
Proof(id="1234", amount=0, secret="asdasdasd", C="asdasdasd"),
Proof(id="1234", amount=0, secret="asdasdasd1", C="asdasdasd1"),
]
payload = CheckSpendableRequest_deprecated(proofs=proofs)
response = httpx.post(
f"{BASE_URL}/check",
json=payload.dict(),
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
states = CheckSpendableResponse_deprecated.parse_obj(response.json())
assert states.spendable
assert len(states.spendable) == 2
assert states.pending
assert len(states.pending) == 2

View File

@@ -1,11 +1,13 @@
import pytest
import pytest_asyncio
from cashu.core.base import PostMeltQuoteRequest, PostMintQuoteRequest
from cashu.core.helpers import sum_proofs
from cashu.mint.ledger import Ledger
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
from tests.helpers import get_real_invoice, is_fake, is_regtest, pay_if_regtest
async def assert_err(f, msg):
@@ -20,36 +22,120 @@ async def assert_err(f, msg):
@pytest_asyncio.fixture(scope="function")
async def wallet1(mint):
async def wallet1(ledger: Ledger):
wallet1 = await Wallet1.with_db(
url=SERVER_ENDPOINT,
db="test_data/wallet1",
name="wallet1",
)
await wallet1.load_mint()
wallet1.status()
yield wallet1
@pytest.mark.asyncio
async def test_melt(wallet1: Wallet, ledger: Ledger):
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
async def test_melt_internal(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)
invoice2 = await wallet1.request_mint(64)
pay_if_regtest(invoice2.bolt11)
await wallet1.mint(64, id=invoice2.id)
invoice = await wallet1.request_mint(128)
await wallet1.mint(128, id=invoice.id)
assert wallet1.balance == 128
total_amount, fee_reserve_sat = await wallet1.get_pay_amount_with_fees(
invoice2.bolt11
# create a mint quote so that we can melt to it internally
invoice_to_pay = await wallet1.request_mint(64)
invoice_payment_request = invoice_to_pay.bolt11
melt_quote = await ledger.melt_quote(
PostMeltQuoteRequest(request=invoice_payment_request, unit="sat")
)
melt_fees = await ledger.get_melt_fees(invoice2.bolt11)
assert melt_fees == fee_reserve_sat
assert not melt_quote.paid
assert melt_quote.amount == 64
assert melt_quote.fee_reserve == 0
melt_quote_pre_payment = await ledger.get_melt_quote(melt_quote.quote)
assert not melt_quote_pre_payment.paid, "melt quote should not be paid"
keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 64)
await ledger.melt(proofs=send_proofs, quote=melt_quote.quote)
melt_quote_post_payment = await ledger.get_melt_quote(melt_quote.quote)
assert melt_quote_post_payment.paid, "melt quote should be paid"
@pytest.mark.asyncio
@pytest.mark.skipif(is_fake, reason="only works with Regtest")
async def test_melt_external(wallet1: Wallet, ledger: Ledger):
# mint twice so we have enough to pay the second invoice back
invoice = await wallet1.request_mint(128)
pay_if_regtest(invoice.bolt11)
await wallet1.mint(128, id=invoice.id)
assert wallet1.balance == 128
invoice_dict = get_real_invoice(64)
invoice_payment_request = invoice_dict["payment_request"]
mint_quote = await wallet1.get_pay_amount_with_fees(invoice_payment_request)
total_amount = mint_quote.amount + mint_quote.fee_reserve
keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount)
melt_quote = await ledger.melt_quote(
PostMeltQuoteRequest(request=invoice_payment_request, unit="sat")
)
await ledger.melt(send_proofs, invoice2.bolt11, outputs=None)
melt_quote_pre_payment = await ledger.get_melt_quote(melt_quote.quote)
assert not melt_quote_pre_payment.paid, "melt quote should not be paid"
assert not melt_quote.paid, "melt quote should not be paid"
await ledger.melt(proofs=send_proofs, quote=melt_quote.quote)
melt_quote_post_payment = await ledger.get_melt_quote(melt_quote.quote)
assert melt_quote_post_payment.paid, "melt quote should be paid"
@pytest.mark.asyncio
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
async def test_mint_internal(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(128)
mint_quote = await ledger.get_mint_quote(invoice.id)
assert mint_quote.paid, "mint quote should be paid"
output_amounts = [128]
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
len(output_amounts)
)
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
await ledger.mint(outputs=outputs, quote_id=invoice.id)
await assert_err(
ledger.mint(outputs=outputs, quote_id=invoice.id),
"outputs have already been signed before.",
)
@pytest.mark.asyncio
@pytest.mark.skipif(is_fake, reason="only works with Regtest")
async def test_mint_external(wallet1: Wallet, ledger: Ledger):
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=128, unit="sat"))
mint_quote = await ledger.get_mint_quote(quote.quote)
assert not mint_quote.paid, "mint quote not should be paid"
await assert_err(
wallet1.mint(128, id=quote.quote),
"quote not paid",
)
pay_if_regtest(quote.request)
mint_quote = await ledger.get_mint_quote(quote.quote)
assert mint_quote.paid, "mint quote should be paid"
output_amounts = [128]
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
len(output_amounts)
)
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
await ledger.mint(outputs=outputs, quote_id=quote.quote)
@pytest.mark.asyncio
@@ -120,14 +206,13 @@ async def test_split_with_input_more_than_outputs(wallet1: Wallet, ledger: Ledge
# make sure we can still spend our tokens
keep_proofs, send_proofs = await wallet1.split(inputs, 10)
print(keep_proofs, send_proofs)
@pytest.mark.asyncio
async def test_split_twice_with_same_outputs(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(128)
pay_if_regtest(invoice.bolt11)
await wallet1.mint(128, [64, 64], id=invoice.id)
await wallet1.mint(128, split=[64, 64], id=invoice.id)
inputs1 = wallet1.proofs[:1]
inputs2 = wallet1.proofs[1:]
@@ -164,14 +249,14 @@ async def test_mint_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger):
len(output_amounts)
)
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
await ledger.mint(outputs, id=invoice.id)
await ledger.mint(outputs=outputs, quote_id=invoice.id)
# now try to mint with the same outputs again
invoice2 = await wallet1.request_mint(128)
pay_if_regtest(invoice2.bolt11)
await assert_err(
ledger.mint(outputs, id=invoice2.id),
ledger.mint(outputs=outputs, quote_id=invoice2.id),
"outputs have already been signed before.",
)
@@ -191,16 +276,79 @@ async def test_melt_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger):
# we use the outputs once for minting
invoice2 = await wallet1.request_mint(128)
pay_if_regtest(invoice2.bolt11)
await ledger.mint(outputs, id=invoice2.id)
await ledger.mint(outputs=outputs, quote_id=invoice2.id)
# use the same outputs for melting
invoice3 = await wallet1.request_mint(128)
mint_quote = await ledger.mint_quote(PostMintQuoteRequest(unit="sat", amount=128))
melt_quote = await ledger.melt_quote(
PostMeltQuoteRequest(unit="sat", request=mint_quote.request)
)
await assert_err(
ledger.melt(wallet1.proofs, invoice3.bolt11, outputs=outputs),
ledger.melt(proofs=wallet1.proofs, quote=melt_quote.quote, outputs=outputs),
"outputs have already been signed before.",
)
@pytest.mark.asyncio
async def test_melt_with_less_inputs_than_invoice(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(32)
pay_if_regtest(invoice.bolt11)
await wallet1.mint(32, id=invoice.id)
# outputs for fee return
output_amounts = [1, 1, 1, 1]
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
len(output_amounts)
)
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
# create a mint quote to pay
mint_quote = await ledger.mint_quote(PostMintQuoteRequest(unit="sat", amount=128))
# prepare melt quote
melt_quote = await ledger.melt_quote(
PostMeltQuoteRequest(unit="sat", request=mint_quote.request)
)
assert melt_quote.amount + melt_quote.fee_reserve > sum_proofs(wallet1.proofs)
# try to pay with not enough inputs
await assert_err(
ledger.melt(proofs=wallet1.proofs, quote=melt_quote.quote, outputs=outputs),
"not enough inputs provided for melt",
)
@pytest.mark.asyncio
async def test_melt_with_more_inputs_than_invoice(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(130)
pay_if_regtest(invoice.bolt11)
await wallet1.mint(130, split=[64, 64, 2], id=invoice.id)
# outputs for fee return
output_amounts = [1, 1, 1, 1]
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
len(output_amounts)
)
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
# create a mint quote to pay
mint_quote = await ledger.mint_quote(PostMintQuoteRequest(unit="sat", amount=128))
# prepare melt quote
melt_quote = await ledger.melt_quote(
PostMeltQuoteRequest(unit="sat", request=mint_quote.request)
)
# fees are 0 because it's internal
assert melt_quote.fee_reserve == 0
# make sure we have more inputs than the melt quote needs
assert sum_proofs(wallet1.proofs) >= melt_quote.amount + melt_quote.fee_reserve
payment_proof, return_outputs = await ledger.melt(
proofs=wallet1.proofs, quote=melt_quote.quote, outputs=outputs
)
# we get 2 sats back because we overpaid
assert sum([o.amount for o in return_outputs]) == 2
@pytest.mark.asyncio
async def test_check_proof_state(wallet1: Wallet, ledger: Ledger):
invoice = await wallet1.request_mint(64)
@@ -209,6 +357,7 @@ async def test_check_proof_state(wallet1: Wallet, ledger: Ledger):
keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10)
spendable, pending = await ledger.check_proof_state(proofs=send_proofs)
assert sum(spendable) == len(send_proofs)
assert sum(pending) == 0
proof_states = await ledger.check_proofs_state(
secrets=[p.secret for p in send_proofs]
)
assert all([p.state.value == "UNSPENT" for p in proof_states])

View File

@@ -1,6 +1,4 @@
import copy
import shutil
from pathlib import Path
from typing import List, Union
import pytest
@@ -10,12 +8,12 @@ from cashu.core.base import Proof
from cashu.core.errors import CashuError, KeysetNotFoundError
from cashu.core.helpers import sum_proofs
from cashu.core.settings import settings
from cashu.wallet.crud import get_keyset, get_lightning_invoice, get_proofs
from cashu.wallet.crud import get_keysets, get_lightning_invoice, get_proofs
from cashu.wallet.wallet import Wallet
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
from tests.helpers import get_real_invoice, is_fake, is_regtest, pay_if_regtest
async def assert_err(f, msg: Union[str, CashuError]):
@@ -56,47 +54,32 @@ async def wallet1(mint):
name="wallet1",
)
await wallet1.load_mint()
wallet1.status()
yield wallet1
@pytest_asyncio.fixture(scope="function")
async def wallet2(mint):
async def wallet2():
wallet2 = await Wallet2.with_db(
url=SERVER_ENDPOINT,
db="test_data/wallet2",
name="wallet2",
)
await wallet2.load_mint()
wallet2.status()
yield wallet2
@pytest_asyncio.fixture(scope="function")
async def wallet3(mint):
dirpath = Path("test_data/wallet3")
if dirpath.exists() and dirpath.is_dir():
shutil.rmtree(dirpath)
wallet3 = await Wallet1.with_db(
url=SERVER_ENDPOINT,
db="test_data/wallet3",
name="wallet3",
)
await wallet3.db.execute("DELETE FROM proofs")
await wallet3.db.execute("DELETE FROM proofs_used")
await wallet3.load_mint()
wallet3.status()
yield wallet3
@pytest.mark.asyncio
async def test_get_keys(wallet1: Wallet):
assert wallet1.keysets[wallet1.keyset_id].public_keys
assert len(wallet1.keysets[wallet1.keyset_id].public_keys) == settings.max_order
keyset = await wallet1._get_keys(wallet1.url)
keysets = await wallet1._get_keys()
keyset = keysets[0]
assert keyset.id is not None
assert keyset.id == "1cCNIAZ2X/w1"
# assert keyset.id_deprecated == "eGnEWtdJ0PIM"
if settings.debug_mint_only_deprecated:
assert keyset.id == "eGnEWtdJ0PIM"
else:
assert keyset.id == "009a1f293253e41e"
assert isinstance(keyset.id, str)
assert len(keyset.id) > 0
@@ -106,13 +89,14 @@ async def test_get_keyset(wallet1: Wallet):
assert wallet1.keysets[wallet1.keyset_id].public_keys
assert len(wallet1.keysets[wallet1.keyset_id].public_keys) == settings.max_order
# let's get the keys first so we can get a keyset ID that we use later
keys1 = await wallet1._get_keys(wallet1.url)
keysets = await wallet1._get_keys()
keyset = keysets[0]
# gets the keys of a specific keyset
assert keys1.id is not None
assert keys1.public_keys is not None
keys2 = await wallet1._get_keys_of_keyset(wallet1.url, keys1.id)
assert keyset.id is not None
assert keyset.public_keys is not None
keys2 = await wallet1._get_keys_of_keyset(keyset.id)
assert keys2.public_keys is not None
assert len(keys1.public_keys) == len(keys2.public_keys)
assert len(keyset.public_keys) == len(keys2.public_keys)
@pytest.mark.asyncio
@@ -130,32 +114,39 @@ async def test_get_keyset_from_db(wallet1: Wallet):
assert keyset1.id == keyset2.id
# load it directly from the db
keyset3 = await get_keyset(db=wallet1.db, id=keyset1.id)
assert keyset3
keysets_local = await get_keysets(db=wallet1.db, id=keyset1.id)
assert keysets_local[0]
keyset3 = keysets_local[0]
assert keyset1.public_keys == keyset3.public_keys
assert keyset1.id == keyset3.id
@pytest.mark.asyncio
async def test_get_info(wallet1: Wallet):
info = await wallet1._get_info(wallet1.url)
info = await wallet1._get_info()
assert info.name
@pytest.mark.asyncio
async def test_get_nonexistent_keyset(wallet1: Wallet):
await assert_err(
wallet1._get_keys_of_keyset(wallet1.url, "nonexistent"),
wallet1._get_keys_of_keyset("nonexistent"),
KeysetNotFoundError(),
)
@pytest.mark.asyncio
async def test_get_keyset_ids(wallet1: Wallet):
keyset = await wallet1._get_keyset_ids(wallet1.url)
assert isinstance(keyset, list)
assert len(keyset) > 0
assert keyset[-1] == wallet1.keyset_id
keysets = await wallet1._get_keyset_ids()
assert isinstance(keysets, list)
assert len(keysets) > 0
assert wallet1.keyset_id in keysets
@pytest.mark.asyncio
async def test_request_mint(wallet1: Wallet):
invoice = await wallet1.request_mint(64)
assert invoice.payment_hash
@pytest.mark.asyncio
@@ -181,9 +172,9 @@ async def test_mint(wallet1: Wallet):
@pytest.mark.asyncio
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]
invoice = await wallet1.request_mint(sum(amts))
pay_if_regtest(invoice.bolt11)
await wallet1.mint(amount=sum(amts), split=amts, id=invoice.id)
assert wallet1.balance == 27
assert wallet1.proof_amounts == amts
@@ -192,9 +183,11 @@ async def test_mint_amounts(wallet1: Wallet):
@pytest.mark.asyncio
async def test_mint_amounts_wrong_sum(wallet1: Wallet):
"""Mint predefined amounts"""
amts = [1, 1, 1, 2, 2, 4, 16]
invoice = await wallet1.request_mint(sum(amts))
await assert_err(
wallet1.mint(amount=sum(amts) + 1, split=amts),
wallet1.mint(amount=sum(amts) + 1, split=amts, id=invoice.id),
"split must sum to amount",
)
@@ -203,8 +196,9 @@ async def test_mint_amounts_wrong_sum(wallet1: Wallet):
async def test_mint_amounts_wrong_order(wallet1: Wallet):
"""Mint amount that is not part in 2^n"""
amts = [1, 2, 3]
invoice = await wallet1.request_mint(sum(amts))
await assert_err(
wallet1.mint(amount=sum(amts), split=[1, 2, 3]),
wallet1.mint(amount=sum(amts), split=[1, 2, 3], id=invoice.id),
f"Can only mint amounts with 2^n up to {2**settings.max_order}.",
)
@@ -257,42 +251,54 @@ async def test_split_more_than_balance(wallet1: Wallet):
@pytest.mark.asyncio
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)
topup_invoice = await wallet1.request_mint(128)
pay_if_regtest(topup_invoice.bolt11)
await wallet1.mint(128, id=topup_invoice.id)
assert wallet1.balance == 128
total_amount, fee_reserve_sat = await wallet1.get_pay_amount_with_fees(
invoice.bolt11
)
assert total_amount == 66
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)
invoice_payment_request = ""
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"])
invoice_payment_request = invoice_dict["payment_request"]
if is_fake:
invoice = await wallet1.request_mint(64)
invoice_payment_hash = str(invoice.payment_hash)
invoice_payment_request = invoice.bolt11
quote = await wallet1.get_pay_amount_with_fees(invoice_payment_request)
total_amount = quote.amount + quote.fee_reserve
if is_regtest:
# we expect a fee reserve of 2 sat for regtest
assert total_amount == 66
assert quote.fee_reserve == 2
if is_fake:
# we expect a fee reserve of 0 sat for fake
assert total_amount == 64
assert quote.fee_reserve == 0
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount)
melt_response = await wallet1.pay_lightning(
send_proofs, invoice=invoice_to_pay, fee_reserve_sat=fee_reserve_sat
proofs=send_proofs,
invoice=invoice_payment_request,
fee_reserve_sat=quote.fee_reserve,
quote_id=quote.quote,
)
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, "Wrong keyset returned"
if is_regtest:
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, "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, "No payment hash in invoice"
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
)
@@ -305,7 +311,7 @@ async def test_melt(wallet1: Wallet):
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), "Wrong balance"
assert wallet1.balance == 128 - (total_amount - quote.fee_reserve), "Wrong balance"
assert wallet1.balance == 64, "Wrong balance"
@@ -368,23 +374,23 @@ async def test_send_and_redeem(wallet1: Wallet, wallet2: Wallet):
@pytest.mark.asyncio
async def test_invalidate_unspent_proofs(wallet1: Wallet):
async def test_invalidate_all_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
assert wallet1.balance == 0
@pytest.mark.asyncio
async def test_invalidate_unspent_proofs_without_checking(wallet1: Wallet):
async def test_invalidate_unspent_proofs_with_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
await wallet1.invalidate(wallet1.proofs, check_spendable=True)
assert wallet1.balance == 64
@pytest.mark.asyncio
@@ -405,5 +411,21 @@ async def test_token_state(wallet1: Wallet):
await wallet1.mint(64, id=invoice.id)
assert wallet1.balance == 64
resp = await wallet1.check_proof_state(wallet1.proofs)
assert resp.dict()["spendable"]
assert resp.dict()["pending"]
assert resp.states[0].state.value == "UNSPENT"
@pytest.mark.asyncio
async def test_load_mint_keys_specific_keyset(wallet1: Wallet):
await wallet1._load_mint_keys()
if settings.debug_mint_only_deprecated:
assert list(wallet1.keysets.keys()) == ["eGnEWtdJ0PIM"]
else:
assert list(wallet1.keysets.keys()) == ["009a1f293253e41e", "eGnEWtdJ0PIM"]
await wallet1._load_mint_keys(keyset_id=wallet1.keyset_id)
await wallet1._load_mint_keys(keyset_id="009a1f293253e41e")
# expect deprecated keyset id to be present
await wallet1._load_mint_keys(keyset_id="eGnEWtdJ0PIM")
await assert_err(
wallet1._load_mint_keys(keyset_id="nonexistent"),
KeysetNotFoundError(),
)

View File

@@ -12,14 +12,13 @@ from tests.helpers import is_regtest
@pytest_asyncio.fixture(scope="function")
async def wallet(mint):
async def wallet():
wallet = await Wallet.with_db(
url=SERVER_ENDPOINT,
db="test_data/wallet",
name="wallet",
)
await wallet.load_mint()
wallet.status()
yield wallet

View File

@@ -35,25 +35,23 @@ def assert_amt(proofs: List[Proof], expected: int):
@pytest_asyncio.fixture(scope="function")
async def wallet1(mint):
async def wallet1():
wallet1 = await Wallet1.with_db(
SERVER_ENDPOINT, "test_data/wallet_p2pk_1", "wallet1"
)
await migrate_databases(wallet1.db, migrations)
await wallet1.load_mint()
wallet1.status()
yield wallet1
@pytest_asyncio.fixture(scope="function")
async def wallet2(mint):
async def wallet2():
wallet2 = await Wallet2.with_db(
SERVER_ENDPOINT, "test_data/wallet_p2pk_2", "wallet2"
)
await migrate_databases(wallet2.db, migrations)
wallet2.private_key = PrivateKey(secrets.token_bytes(32), raw=True)
await wallet2.load_mint()
wallet2.status()
yield wallet2

View File

@@ -0,0 +1,130 @@
from typing import List, Union
import pytest
import pytest_asyncio
from cashu.core.base import Proof
from cashu.core.errors import CashuError
from cashu.wallet.lightning import LightningWallet
from tests.conftest import SERVER_ENDPOINT
from tests.helpers import get_real_invoice, is_fake, is_regtest, pay_if_regtest
async def assert_err(f, msg: Union[str, CashuError]):
"""Compute f() and expect an error message 'msg'."""
try:
await f
except Exception as exc:
error_message: str = str(exc.args[0])
if isinstance(msg, CashuError):
if msg.detail not in error_message:
raise Exception(
f"CashuError. Expected error: {msg.detail}, got: {error_message}"
)
return
if msg not in error_message:
raise Exception(f"Expected error: {msg}, got: {error_message}")
return
raise Exception(f"Expected error: {msg}, got no error")
def assert_amt(proofs: List[Proof], expected: int):
"""Assert amounts the proofs contain."""
assert [p.amount for p in proofs] == expected
async def reset_wallet_db(wallet: LightningWallet):
await wallet.db.execute("DELETE FROM proofs")
await wallet.db.execute("DELETE FROM proofs_used")
await wallet.db.execute("DELETE FROM keysets")
await wallet._load_mint()
@pytest_asyncio.fixture(scope="function")
async def wallet():
wallet = await LightningWallet.with_db(
url=SERVER_ENDPOINT,
db="test_data/wallet1",
name="wallet1",
)
await wallet.async_init()
yield wallet
@pytest.mark.asyncio
async def test_create_invoice(wallet: LightningWallet):
invoice = await wallet.create_invoice(64)
assert invoice.payment_request
assert invoice.payment_request.startswith("ln")
@pytest.mark.asyncio
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
async def test_check_invoice_internal(wallet: LightningWallet):
# fill wallet
invoice = await wallet.create_invoice(64)
assert invoice.payment_request
assert invoice.checking_id
status = await wallet.get_invoice_status(invoice.checking_id)
assert status.paid
@pytest.mark.asyncio
@pytest.mark.skipif(is_fake, reason="only works with Regtest")
async def test_check_invoice_external(wallet: LightningWallet):
# fill wallet
invoice = await wallet.create_invoice(64)
assert invoice.payment_request
assert invoice.checking_id
status = await wallet.get_invoice_status(invoice.checking_id)
assert not status.paid
pay_if_regtest(invoice.payment_request)
status = await wallet.get_invoice_status(invoice.checking_id)
assert status.paid
@pytest.mark.asyncio
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
async def test_pay_invoice_internal(wallet: LightningWallet):
# fill wallet
invoice = await wallet.create_invoice(64)
assert invoice.payment_request
assert invoice.checking_id
await wallet.get_invoice_status(invoice.checking_id)
assert wallet.available_balance >= 64
# pay invoice
invoice2 = await wallet.create_invoice(16)
assert invoice2.payment_request
status = await wallet.pay_invoice(invoice2.payment_request)
assert status.ok
# check payment
assert invoice2.checking_id
status = await wallet.get_payment_status(invoice2.checking_id)
assert status.paid
@pytest.mark.asyncio
@pytest.mark.skipif(is_fake, reason="only works with Regtest")
async def test_pay_invoice_external(wallet: LightningWallet):
# fill wallet
invoice = await wallet.create_invoice(64)
assert invoice.payment_request
assert invoice.checking_id
pay_if_regtest(invoice.payment_request)
status = await wallet.get_invoice_status(invoice.checking_id)
assert status.paid
assert wallet.available_balance >= 64
# pay invoice
invoice_real = get_real_invoice(16)
status = await wallet.pay_invoice(invoice_real["payment_request"])
assert status.ok
# check payment
assert status.checking_id
status = await wallet.get_payment_status(status.checking_id)
assert status.paid

View File

@@ -1,12 +1,13 @@
import asyncio
import copy
import json
import secrets
from typing import List
import pytest
import pytest_asyncio
from cashu.core.base import Proof
from cashu.core.base import Proof, SpentState
from cashu.core.crypto.secp import PrivateKey, PublicKey
from cashu.core.migrations import migrate_databases
from cashu.core.p2pk import SigFlags
@@ -16,7 +17,7 @@ from cashu.wallet.wallet import Wallet
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
from tests.helpers import is_deprecated_api_only, pay_if_regtest
async def assert_err(f, msg):
@@ -36,25 +37,23 @@ def assert_amt(proofs: List[Proof], expected: int):
@pytest_asyncio.fixture(scope="function")
async def wallet1(mint):
async def wallet1():
wallet1 = await Wallet1.with_db(
SERVER_ENDPOINT, "test_data/wallet_p2pk_1", "wallet1"
)
await migrate_databases(wallet1.db, migrations)
await wallet1.load_mint()
wallet1.status()
yield wallet1
@pytest_asyncio.fixture(scope="function")
async def wallet2(mint):
async def wallet2():
wallet2 = await Wallet2.with_db(
SERVER_ENDPOINT, "test_data/wallet_p2pk_2", "wallet2"
)
await migrate_databases(wallet2.db, migrations)
wallet2.private_key = PrivateKey(secrets.token_bytes(32), raw=True)
await wallet2.load_mint()
wallet2.status()
yield wallet2
@@ -80,6 +79,16 @@ async def test_p2pk(wallet1: Wallet, wallet2: Wallet):
)
await wallet2.redeem(send_proofs)
proof_states = await wallet2.check_proof_state(send_proofs)
assert all([p.state == SpentState.spent for p in proof_states.states])
if not is_deprecated_api_only:
for state in proof_states.states:
assert state.witness is not None
witness_obj = json.loads(state.witness)
assert len(witness_obj["signatures"]) == 1
assert len(witness_obj["signatures"][0]) == 128
@pytest.mark.asyncio
async def test_p2pk_sig_all(wallet1: Wallet, wallet2: Wallet):
@@ -222,9 +231,9 @@ async def test_p2pk_locktime_with_second_refund_pubkey(
secret_lock = await wallet1.create_p2pk_lock(
garbage_pubkey.serialize().hex(), # create lock to unspendable pubkey
locktime_seconds=2, # locktime
tags=Tags(
[["refund", pubkey_wallet2, pubkey_wallet1]]
), # multiple refund pubkeys
tags=Tags([
["refund", pubkey_wallet2, pubkey_wallet1]
]), # multiple refund pubkeys
) # sender side
_, send_proofs = await wallet1.split_to_send(
wallet1.proofs, 8, secret_lock=secret_lock
@@ -379,9 +388,9 @@ async def test_p2pk_multisig_with_wrong_first_private_key(
def test_tags():
tags = Tags(
[["key1", "value1"], ["key2", "value2", "value2_1"], ["key2", "value3"]]
)
tags = Tags([
["key1", "value1"], ["key2", "value2", "value2_1"], ["key2", "value3"]
])
assert tags.get_tag("key1") == "value1"
assert tags["key1"] == "value1"
assert tags.get_tag("key2") == "value2"

View File

@@ -8,6 +8,7 @@ import pytest_asyncio
from cashu.core.base import Proof
from cashu.core.crypto.secp import PrivateKey
from cashu.core.errors import CashuError
from cashu.core.settings import settings
from cashu.wallet.wallet import Wallet
from cashu.wallet.wallet import Wallet as Wallet1
from cashu.wallet.wallet import Wallet as Wallet2
@@ -46,31 +47,29 @@ async def reset_wallet_db(wallet: Wallet):
@pytest_asyncio.fixture(scope="function")
async def wallet1(mint):
async def wallet1():
wallet1 = await Wallet1.with_db(
url=SERVER_ENDPOINT,
db="test_data/wallet1",
name="wallet1",
)
await wallet1.load_mint()
wallet1.status()
yield wallet1
@pytest_asyncio.fixture(scope="function")
async def wallet2(mint):
async def wallet2():
wallet2 = await Wallet2.with_db(
url=SERVER_ENDPOINT,
db="test_data/wallet2",
name="wallet2",
)
await wallet2.load_mint()
wallet2.status()
yield wallet2
@pytest_asyncio.fixture(scope="function")
async def wallet3(mint):
async def wallet3():
dirpath = Path("test_data/wallet3")
if dirpath.exists() and dirpath.is_dir():
shutil.rmtree(dirpath)
@@ -83,11 +82,14 @@ async def wallet3(mint):
await wallet3.db.execute("DELETE FROM proofs")
await wallet3.db.execute("DELETE FROM proofs_used")
await wallet3.load_mint()
wallet3.status()
yield wallet3
@pytest.mark.asyncio
@pytest.mark.skipif(
settings.debug_mint_only_deprecated,
reason="settings.debug_mint_only_deprecated is set",
)
async def test_bump_secret_derivation(wallet3: Wallet):
await wallet3._init_private_key(
"half depart obvious quality work element tank gorilla view sugar picture"
@@ -95,23 +97,27 @@ async def test_bump_secret_derivation(wallet3: Wallet):
)
secrets1, rs1, derivation_paths1 = await wallet3.generate_n_secrets(5)
secrets2, rs2, derivation_paths2 = await wallet3.generate_secrets_from_to(0, 4)
assert wallet3.keyset_id == "1cCNIAZ2X/w1"
assert wallet3.keyset_id == "009a1f293253e41e"
assert secrets1 == secrets2
assert [r.private_key for r in rs1] == [r.private_key for r in rs2]
assert derivation_paths1 == derivation_paths2
for s in secrets1:
print('"' + s + '",')
assert secrets1 == [
"9d32fc57e6fa2942d05ee475d28ba6a56839b8cb8a3f174b05ed0ed9d3a420f6",
"1c0f2c32e7438e7cc992612049e9dfcdbffd454ea460901f24cc429921437802",
"327c606b761af03cbe26fa13c4b34a6183b868c52cda059fe57fdddcb4e1e1e7",
"53476919560398b56c0fdc5dd92cf8628b1e06de6f2652b0f7d6e8ac319de3b7",
"b2f5d632229378a716be6752fc79ac8c2b43323b820859a7956f2dfe5432b7b4",
"485875df74771877439ac06339e284c3acfcd9be7abf3bc20b516faeadfe77ae",
"8f2b39e8e594a4056eb1e6dbb4b0c38ef13b1b2c751f64f810ec04ee35b77270",
"bc628c79accd2364fd31511216a0fab62afd4a18ff77a20deded7b858c9860c8",
"59284fd1650ea9fa17db2b3acf59ecd0f2d52ec3261dd4152785813ff27a33bf",
"576c23393a8b31cc8da6688d9c9a96394ec74b40fdaf1f693a6bb84284334ea0",
]
for d in derivation_paths1:
print('"' + d + '",')
assert derivation_paths1 == [
"m/129372'/0'/2004500376'/0'",
"m/129372'/0'/2004500376'/1'",
"m/129372'/0'/2004500376'/2'",
"m/129372'/0'/2004500376'/3'",
"m/129372'/0'/2004500376'/4'",
"m/129372'/0'/864559728'/0'",
"m/129372'/0'/864559728'/1'",
"m/129372'/0'/864559728'/2'",
"m/129372'/0'/864559728'/3'",
"m/129372'/0'/864559728'/4'",
]
@@ -191,7 +197,7 @@ async def test_restore_wallet_after_split_to_send(wallet3: Wallet):
assert wallet3.balance == 0
await wallet3.restore_promises_from_to(0, 100)
assert wallet3.balance == 64 * 2
await wallet3.invalidate(wallet3.proofs)
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
assert wallet3.balance == 64
@@ -216,7 +222,7 @@ async def test_restore_wallet_after_send_and_receive(wallet3: Wallet, wallet2: W
assert wallet3.balance == 0
await wallet3.restore_promises_from_to(0, 100)
assert wallet3.balance == 64 + 2 * 32
await wallet3.invalidate(wallet3.proofs)
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
assert wallet3.balance == 32
@@ -257,7 +263,7 @@ async def test_restore_wallet_after_send_and_self_receive(wallet3: Wallet):
assert wallet3.balance == 0
await wallet3.restore_promises_from_to(0, 100)
assert wallet3.balance == 64 + 2 * 32 + 32
await wallet3.invalidate(wallet3.proofs)
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
assert wallet3.balance == 64
@@ -290,7 +296,7 @@ async def test_restore_wallet_after_send_twice(
await wallet3.restore_promises_from_to(0, 10)
box.add(wallet3.proofs)
assert wallet3.balance == 5
await wallet3.invalidate(wallet3.proofs)
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
assert wallet3.balance == 2
# again
@@ -310,7 +316,7 @@ async def test_restore_wallet_after_send_twice(
await wallet3.restore_promises_from_to(0, 15)
box.add(wallet3.proofs)
assert wallet3.balance == 7
await wallet3.invalidate(wallet3.proofs)
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
assert wallet3.balance == 2
@@ -345,7 +351,7 @@ async def test_restore_wallet_after_send_and_self_receive_nonquadratic_value(
await wallet3.restore_promises_from_to(0, 20)
box.add(wallet3.proofs)
assert wallet3.balance == 138
await wallet3.invalidate(wallet3.proofs)
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
assert wallet3.balance == 64
# again
@@ -362,5 +368,5 @@ async def test_restore_wallet_after_send_and_self_receive_nonquadratic_value(
assert wallet3.balance == 0
await wallet3.restore_promises_from_to(0, 50)
assert wallet3.balance == 182
await wallet3.invalidate(wallet3.proofs)
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
assert wallet3.balance == 64