mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 10:34:20 +01:00
* 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
373 lines
13 KiB
Python
373 lines
13 KiB
Python
import shutil
|
|
from pathlib import Path
|
|
from typing import Dict, List, Union
|
|
|
|
import pytest
|
|
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
|
|
from tests.conftest import SERVER_ENDPOINT
|
|
from tests.helpers import 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: Wallet):
|
|
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 wallet1():
|
|
wallet1 = await Wallet1.with_db(
|
|
url=SERVER_ENDPOINT,
|
|
db="test_data/wallet1",
|
|
name="wallet1",
|
|
)
|
|
await wallet1.load_mint()
|
|
yield wallet1
|
|
|
|
|
|
@pytest_asyncio.fixture(scope="function")
|
|
async def wallet2():
|
|
wallet2 = await Wallet2.with_db(
|
|
url=SERVER_ENDPOINT,
|
|
db="test_data/wallet2",
|
|
name="wallet2",
|
|
)
|
|
await wallet2.load_mint()
|
|
yield wallet2
|
|
|
|
|
|
@pytest_asyncio.fixture(scope="function")
|
|
async def wallet3():
|
|
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()
|
|
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"
|
|
" humble"
|
|
)
|
|
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 == "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 == [
|
|
"485875df74771877439ac06339e284c3acfcd9be7abf3bc20b516faeadfe77ae",
|
|
"8f2b39e8e594a4056eb1e6dbb4b0c38ef13b1b2c751f64f810ec04ee35b77270",
|
|
"bc628c79accd2364fd31511216a0fab62afd4a18ff77a20deded7b858c9860c8",
|
|
"59284fd1650ea9fa17db2b3acf59ecd0f2d52ec3261dd4152785813ff27a33bf",
|
|
"576c23393a8b31cc8da6688d9c9a96394ec74b40fdaf1f693a6bb84284334ea0",
|
|
]
|
|
for d in derivation_paths1:
|
|
print('"' + d + '",')
|
|
assert derivation_paths1 == [
|
|
"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'",
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bump_secret_derivation_two_steps(wallet3: Wallet):
|
|
await wallet3._init_private_key(
|
|
"half depart obvious quality work element tank gorilla view sugar picture"
|
|
" humble"
|
|
)
|
|
secrets1_1, rs1_1, derivation_paths1 = await wallet3.generate_n_secrets(2)
|
|
secrets1_2, rs1_2, derivation_paths2 = await wallet3.generate_n_secrets(3)
|
|
secrets1 = secrets1_1 + secrets1_2
|
|
rs1 = rs1_1 + rs1_2
|
|
secrets2, rs2, derivation_paths = await wallet3.generate_secrets_from_to(0, 4)
|
|
assert secrets1 == secrets2
|
|
assert [r.private_key for r in rs1] == [r.private_key for r in rs2]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generate_secrets_from_to(wallet3: Wallet):
|
|
await wallet3._init_private_key(
|
|
"half depart obvious quality work element tank gorilla view sugar picture"
|
|
" humble"
|
|
)
|
|
secrets1, rs1, derivation_paths1 = await wallet3.generate_secrets_from_to(0, 4)
|
|
assert len(secrets1) == 5
|
|
secrets2, rs2, derivation_paths2 = await wallet3.generate_secrets_from_to(2, 4)
|
|
assert len(secrets2) == 3
|
|
assert secrets1[2:] == secrets2
|
|
assert [r.private_key for r in rs1[2:]] == [r.private_key for r in rs2]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_restore_wallet_after_mint(wallet3: Wallet):
|
|
await reset_wallet_db(wallet3)
|
|
invoice = await wallet3.request_mint(64)
|
|
pay_if_regtest(invoice.bolt11)
|
|
await wallet3.mint(64, id=invoice.id)
|
|
assert wallet3.balance == 64
|
|
await reset_wallet_db(wallet3)
|
|
await wallet3.load_proofs()
|
|
wallet3.proofs = []
|
|
assert wallet3.balance == 0
|
|
await wallet3.restore_promises_from_to(0, 20)
|
|
assert wallet3.balance == 64
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_restore_wallet_with_invalid_mnemonic(wallet3: Wallet):
|
|
await assert_err(
|
|
wallet3._init_private_key(
|
|
"half depart obvious quality work element tank gorilla view sugar picture"
|
|
" picture"
|
|
),
|
|
"Invalid mnemonic",
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_restore_wallet_after_split_to_send(wallet3: Wallet):
|
|
await wallet3._init_private_key(
|
|
"half depart obvious quality work element tank gorilla view sugar picture"
|
|
" humble"
|
|
)
|
|
await reset_wallet_db(wallet3)
|
|
|
|
invoice = await wallet3.request_mint(64)
|
|
pay_if_regtest(invoice.bolt11)
|
|
await wallet3.mint(64, id=invoice.id)
|
|
assert wallet3.balance == 64
|
|
|
|
_, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 32, set_reserved=True) # type: ignore
|
|
|
|
await reset_wallet_db(wallet3)
|
|
await wallet3.load_proofs()
|
|
wallet3.proofs = []
|
|
assert wallet3.balance == 0
|
|
await wallet3.restore_promises_from_to(0, 100)
|
|
assert wallet3.balance == 64 * 2
|
|
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
|
|
assert wallet3.balance == 64
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_restore_wallet_after_send_and_receive(wallet3: Wallet, wallet2: Wallet):
|
|
await wallet3._init_private_key(
|
|
"hello rug want adapt talent together lunar method bean expose beef position"
|
|
)
|
|
await reset_wallet_db(wallet3)
|
|
invoice = await wallet3.request_mint(64)
|
|
pay_if_regtest(invoice.bolt11)
|
|
await wallet3.mint(64, id=invoice.id)
|
|
assert wallet3.balance == 64
|
|
|
|
_, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 32, set_reserved=True) # type: ignore
|
|
|
|
await wallet2.redeem(spendable_proofs)
|
|
|
|
await reset_wallet_db(wallet3)
|
|
await wallet3.load_proofs(reload=True)
|
|
assert wallet3.proofs == []
|
|
assert wallet3.balance == 0
|
|
await wallet3.restore_promises_from_to(0, 100)
|
|
assert wallet3.balance == 64 + 2 * 32
|
|
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
|
|
assert wallet3.balance == 32
|
|
|
|
|
|
class ProofBox:
|
|
proofs: Dict[str, Proof] = {}
|
|
|
|
def add(self, proofs: List[Proof]) -> None:
|
|
for proof in proofs:
|
|
if proof.secret in self.proofs:
|
|
if self.proofs[proof.secret].C != proof.C:
|
|
print("Proofs are not equal")
|
|
print(self.proofs[proof.secret])
|
|
print(proof)
|
|
else:
|
|
self.proofs[proof.secret] = proof
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_restore_wallet_after_send_and_self_receive(wallet3: Wallet):
|
|
await wallet3._init_private_key(
|
|
"lucky broken tell exhibit shuffle tomato ethics virus rabbit spread measure"
|
|
" text"
|
|
)
|
|
await reset_wallet_db(wallet3)
|
|
|
|
invoice = await wallet3.request_mint(64)
|
|
pay_if_regtest(invoice.bolt11)
|
|
await wallet3.mint(64, id=invoice.id)
|
|
assert wallet3.balance == 64
|
|
|
|
_, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 32, set_reserved=True) # type: ignore
|
|
|
|
await wallet3.redeem(spendable_proofs)
|
|
|
|
await reset_wallet_db(wallet3)
|
|
await wallet3.load_proofs(reload=True)
|
|
assert wallet3.proofs == []
|
|
assert wallet3.balance == 0
|
|
await wallet3.restore_promises_from_to(0, 100)
|
|
assert wallet3.balance == 64 + 2 * 32 + 32
|
|
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
|
|
assert wallet3.balance == 64
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_restore_wallet_after_send_twice(
|
|
wallet3: Wallet,
|
|
):
|
|
box = ProofBox()
|
|
wallet3.private_key = PrivateKey()
|
|
await reset_wallet_db(wallet3)
|
|
|
|
invoice = await wallet3.request_mint(2)
|
|
pay_if_regtest(invoice.bolt11)
|
|
await wallet3.mint(2, id=invoice.id)
|
|
box.add(wallet3.proofs)
|
|
assert wallet3.balance == 2
|
|
|
|
keep_proofs, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 1, set_reserved=True) # type: ignore
|
|
box.add(wallet3.proofs)
|
|
assert wallet3.available_balance == 1
|
|
await wallet3.redeem(spendable_proofs)
|
|
box.add(wallet3.proofs)
|
|
assert wallet3.available_balance == 2
|
|
assert wallet3.balance == 2
|
|
|
|
await reset_wallet_db(wallet3)
|
|
await wallet3.load_proofs(reload=True)
|
|
assert wallet3.proofs == []
|
|
assert wallet3.balance == 0
|
|
await wallet3.restore_promises_from_to(0, 10)
|
|
box.add(wallet3.proofs)
|
|
assert wallet3.balance == 5
|
|
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
|
|
assert wallet3.balance == 2
|
|
|
|
# again
|
|
|
|
_, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 1, set_reserved=True) # type: ignore
|
|
box.add(wallet3.proofs)
|
|
|
|
assert wallet3.available_balance == 1
|
|
await wallet3.redeem(spendable_proofs)
|
|
box.add(wallet3.proofs)
|
|
assert wallet3.available_balance == 2
|
|
|
|
await reset_wallet_db(wallet3)
|
|
await wallet3.load_proofs(reload=True)
|
|
assert wallet3.proofs == []
|
|
assert wallet3.balance == 0
|
|
await wallet3.restore_promises_from_to(0, 15)
|
|
box.add(wallet3.proofs)
|
|
assert wallet3.balance == 7
|
|
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
|
|
assert wallet3.balance == 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_restore_wallet_after_send_and_self_receive_nonquadratic_value(
|
|
wallet3: Wallet,
|
|
):
|
|
box = ProofBox()
|
|
await wallet3._init_private_key(
|
|
"casual demise flight cradle feature hub link slim remember anger front asthma"
|
|
)
|
|
await reset_wallet_db(wallet3)
|
|
|
|
invoice = await wallet3.request_mint(64)
|
|
pay_if_regtest(invoice.bolt11)
|
|
await wallet3.mint(64, id=invoice.id)
|
|
box.add(wallet3.proofs)
|
|
assert wallet3.balance == 64
|
|
|
|
keep_proofs, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 10, set_reserved=True) # type: ignore
|
|
box.add(wallet3.proofs)
|
|
|
|
assert wallet3.available_balance == 64 - 10
|
|
await wallet3.redeem(spendable_proofs)
|
|
box.add(wallet3.proofs)
|
|
assert wallet3.available_balance == 64
|
|
|
|
await reset_wallet_db(wallet3)
|
|
await wallet3.load_proofs(reload=True)
|
|
assert wallet3.proofs == []
|
|
assert wallet3.balance == 0
|
|
await wallet3.restore_promises_from_to(0, 20)
|
|
box.add(wallet3.proofs)
|
|
assert wallet3.balance == 138
|
|
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
|
|
assert wallet3.balance == 64
|
|
|
|
# again
|
|
|
|
_, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 12, set_reserved=True) # type: ignore
|
|
|
|
assert wallet3.available_balance == 64 - 12
|
|
await wallet3.redeem(spendable_proofs)
|
|
assert wallet3.available_balance == 64
|
|
|
|
await reset_wallet_db(wallet3)
|
|
await wallet3.load_proofs(reload=True)
|
|
assert wallet3.proofs == []
|
|
assert wallet3.balance == 0
|
|
await wallet3.restore_promises_from_to(0, 50)
|
|
assert wallet3.balance == 182
|
|
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
|
|
assert wallet3.balance == 64
|