Files
nutshell/tests/mint/test_mint_operations.py
callebtc 38bdb9ce76 Tests: split wallet test from mint test pipeline (#748)
* split wallet test from mint test pipeline

* regtest mint and wallet

* fix

* fix

* move mint tests

* real invoice in regtest mpp
2025-05-11 14:14:49 +02:00

453 lines
17 KiB
Python

import pytest
import pytest_asyncio
from cashu.core.base import MeltQuoteState, MintQuoteState
from cashu.core.helpers import sum_proofs
from cashu.core.models import PostMeltQuoteRequest, PostMintQuoteRequest
from cashu.core.nuts import nut20
from cashu.core.settings import settings
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 get_real_invoice, is_fake, is_regtest, pay_if_regtest
async def assert_err(f, msg):
"""Compute f() and expect an error message 'msg'."""
try:
await f
except Exception as exc:
if msg not in str(exc.args[0]):
raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
return
raise Exception(f"Expected error: {msg}, got no error")
@pytest_asyncio.fixture(scope="function")
async def wallet1(ledger: Ledger):
wallet1 = await Wallet1.with_db(
url=SERVER_ENDPOINT,
db="test_data/wallet1",
name="wallet1",
)
await wallet1.load_mint()
yield wallet1
@pytest.mark.asyncio
@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
mint_quote = await wallet1.request_mint(128)
await ledger.get_mint_quote(mint_quote.quote)
await wallet1.mint(128, quote_id=mint_quote.quote)
assert wallet1.balance == 128
# create a mint quote so that we can melt to it internally
mint_quote_to_pay = await wallet1.request_mint(64)
invoice_payment_request = mint_quote_to_pay.request
melt_quote = await ledger.melt_quote(
PostMeltQuoteRequest(request=invoice_payment_request, unit="sat")
)
assert not melt_quote.paid
assert melt_quote.state == MeltQuoteState.unpaid.value
assert melt_quote.amount == 64
assert melt_quote.fee_reserve == 0
if not settings.debug_mint_only_deprecated:
melt_quote_response_pre_payment = await wallet1.get_melt_quote(melt_quote.quote)
assert melt_quote_response_pre_payment
assert (
not melt_quote_response_pre_payment.state == MeltQuoteState.paid
), "melt quote should not be paid"
assert melt_quote_response_pre_payment.amount == 64
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 melt_quote_pre_payment.unpaid
keep_proofs, send_proofs = await wallet1.swap_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"
assert melt_quote_post_payment.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
mint_quote = await wallet1.request_mint(128)
await pay_if_regtest(mint_quote.request)
await wallet1.mint(128, quote_id=mint_quote.quote)
assert wallet1.balance == 128
invoice_dict = get_real_invoice(64)
invoice_payment_request = invoice_dict["payment_request"]
melt_quote = await wallet1.melt_quote(invoice_payment_request)
assert not melt_quote.paid, "mint quote should not be paid"
assert melt_quote.state == MeltQuoteState.unpaid
total_amount = melt_quote.amount + melt_quote.fee_reserve
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, total_amount)
melt_quote = await ledger.melt_quote(
PostMeltQuoteRequest(request=invoice_payment_request, unit="sat")
)
if not settings.debug_mint_only_deprecated:
melt_quote_response_pre_payment = await wallet1.get_melt_quote(melt_quote.quote)
assert melt_quote_response_pre_payment
assert (
melt_quote_response_pre_payment.state == MeltQuoteState.unpaid
), "melt quote should not be paid"
assert melt_quote_response_pre_payment.amount == melt_quote.amount
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 melt_quote_pre_payment.unpaid
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"
assert melt_quote_post_payment.paid
@pytest.mark.asyncio
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
async def test_mint_internal(wallet1: Wallet, ledger: Ledger):
wallet_mint_quote = await wallet1.request_mint(128)
await ledger.get_mint_quote(wallet_mint_quote.quote)
mint_quote = await ledger.get_mint_quote(wallet_mint_quote.quote)
assert mint_quote.paid, "mint quote should be paid"
if not settings.debug_mint_only_deprecated:
mint_quote = await wallet1.get_mint_quote(mint_quote.quote)
assert mint_quote.state == MintQuoteState.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)
assert wallet_mint_quote.privkey
signature = nut20.sign_mint_quote(
mint_quote.quote, outputs, wallet_mint_quote.privkey
)
await ledger.mint(outputs=outputs, quote_id=mint_quote.quote, signature=signature)
await assert_err(
ledger.mint(outputs=outputs, quote_id=mint_quote.quote),
"outputs have already been signed before.",
)
mint_quote_after_payment = await ledger.get_mint_quote(mint_quote.quote)
assert mint_quote_after_payment.issued, "mint quote should be issued"
assert mint_quote_after_payment.issued
@pytest.mark.asyncio
@pytest.mark.skipif(is_fake, reason="only works with Regtest")
async def test_mint_external(wallet1: Wallet, ledger: Ledger):
quote = await wallet1.request_mint(128)
mint_quote = await ledger.get_mint_quote(quote.quote)
assert not mint_quote.paid, "mint quote already paid"
assert mint_quote.unpaid
if not settings.debug_mint_only_deprecated:
mint_quote = await wallet1.get_mint_quote(quote.quote)
assert not mint_quote.paid, "mint quote should not be paid"
await assert_err(
wallet1.mint(128, quote_id=quote.quote),
"quote not paid",
)
await pay_if_regtest(quote.request)
mint_quote = await ledger.get_mint_quote(quote.quote)
assert mint_quote.paid, "mint quote should be paid"
assert mint_quote.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)
assert quote.privkey
signature = nut20.sign_mint_quote(quote.quote, outputs, quote.privkey)
await ledger.mint(outputs=outputs, quote_id=quote.quote, signature=signature)
mint_quote_after_payment = await ledger.get_mint_quote(quote.quote)
assert mint_quote_after_payment.issued, "mint quote should be issued"
@pytest.mark.asyncio
async def test_split(wallet1: Wallet, ledger: Ledger):
mint_quote = await wallet1.request_mint(64)
await pay_if_regtest(mint_quote.request)
await wallet1.mint(64, quote_id=mint_quote.quote)
keep_proofs, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 10)
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(len(send_proofs))
outputs, rs = wallet1._construct_outputs(
[p.amount for p in send_proofs], secrets, rs
)
promises = await ledger.swap(proofs=send_proofs, outputs=outputs)
assert len(promises) == len(outputs)
assert [p.amount for p in promises] == [p.amount for p in outputs]
@pytest.mark.asyncio
async def test_split_with_no_outputs(wallet1: Wallet, ledger: Ledger):
mint_quote = await wallet1.request_mint(64)
await pay_if_regtest(mint_quote.request)
await wallet1.mint(64, quote_id=mint_quote.quote)
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 10, set_reserved=False)
await assert_err(
ledger.swap(proofs=send_proofs, outputs=[]),
"no outputs provided",
)
@pytest.mark.asyncio
async def test_split_with_input_less_than_outputs(wallet1: Wallet, ledger: Ledger):
mint_quote = await wallet1.request_mint(64)
await pay_if_regtest(mint_quote.request)
await wallet1.mint(64, quote_id=mint_quote.quote)
keep_proofs, send_proofs = await wallet1.swap_to_send(
wallet1.proofs, 10, set_reserved=False
)
too_many_proofs = send_proofs + send_proofs
# generate more outputs than inputs
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
len(too_many_proofs)
)
outputs, rs = wallet1._construct_outputs(
[p.amount for p in too_many_proofs], secrets, rs
)
await assert_err(
ledger.swap(proofs=send_proofs, outputs=outputs),
"are not balanced",
)
# make sure we can still spend our tokens
keep_proofs, send_proofs = await wallet1.split(wallet1.proofs, 10)
@pytest.mark.asyncio
async def test_split_with_input_more_than_outputs(wallet1: Wallet, ledger: Ledger):
mint_quote = await wallet1.request_mint(128)
await pay_if_regtest(mint_quote.request)
await wallet1.mint(128, quote_id=mint_quote.quote)
inputs = wallet1.proofs
# less outputs than inputs
output_amounts = [8]
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
len(output_amounts)
)
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
await assert_err(
ledger.swap(proofs=inputs, outputs=outputs),
"are not balanced",
)
# make sure we can still spend our tokens
keep_proofs, send_proofs = await wallet1.split(inputs, 10)
@pytest.mark.asyncio
async def test_split_twice_with_same_outputs(wallet1: Wallet, ledger: Ledger):
mint_quote = await wallet1.request_mint(128)
await pay_if_regtest(mint_quote.request)
await wallet1.mint(128, split=[64, 64], quote_id=mint_quote.quote)
inputs1 = wallet1.proofs[:1]
inputs2 = wallet1.proofs[1:]
assert inputs1[0].amount == 64
assert inputs2[0].amount == 64
output_amounts = [64]
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
len(output_amounts)
)
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
await ledger.swap(proofs=inputs1, outputs=outputs)
# try to spend other proofs with the same outputs again
await assert_err(
ledger.swap(proofs=inputs2, outputs=outputs),
"outputs have already been signed before.",
)
# try to spend inputs2 again with new outputs
output_amounts = [64]
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
len(output_amounts)
)
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
await ledger.swap(proofs=inputs2, outputs=outputs)
@pytest.mark.asyncio
async def test_mint_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger):
mint_quote = await wallet1.request_mint(128)
await pay_if_regtest(mint_quote.request)
output_amounts = [128]
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
len(output_amounts)
)
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
assert mint_quote.privkey
signature = nut20.sign_mint_quote(mint_quote.quote, outputs, mint_quote.privkey)
await ledger.mint(outputs=outputs, quote_id=mint_quote.quote, signature=signature)
# now try to mint with the same outputs again
mint_quote_2 = await wallet1.request_mint(128)
await pay_if_regtest(mint_quote_2.request)
assert mint_quote_2.privkey
signature = nut20.sign_mint_quote(mint_quote_2.quote, outputs, mint_quote_2.privkey)
await assert_err(
ledger.mint(outputs=outputs, quote_id=mint_quote_2.quote, signature=signature),
"outputs have already been signed before.",
)
@pytest.mark.asyncio
async def test_melt_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger):
mint_quote = await wallet1.request_mint(130)
await pay_if_regtest(mint_quote.request)
await wallet1.mint(130, quote_id=mint_quote.quote)
output_amounts = [128]
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
len(output_amounts)
)
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
# we use the outputs once for minting
mint_quote_2 = await wallet1.request_mint(128)
await pay_if_regtest(mint_quote_2.request)
assert mint_quote_2.privkey
signature = nut20.sign_mint_quote(mint_quote_2.quote, outputs, mint_quote_2.privkey)
await ledger.mint(outputs=outputs, quote_id=mint_quote_2.quote, signature=signature)
# use the same outputs for melting
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(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):
mint_quote = await wallet1.request_mint(32)
await pay_if_regtest(mint_quote.request)
await wallet1.mint(32, quote_id=mint_quote.quote)
# 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):
mint_quote = await wallet1.request_mint(130)
await pay_if_regtest(mint_quote.request)
await wallet1.mint(130, split=[64, 64, 2], quote_id=mint_quote.quote)
# 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
melt_resp = await ledger.melt(
proofs=wallet1.proofs, quote=melt_quote.quote, outputs=outputs
)
# we get 2 sats back because we overpaid
assert melt_resp.change
assert sum([o.amount for o in melt_resp.change]) == 2
@pytest.mark.asyncio
async def test_check_proof_state(wallet1: Wallet, ledger: Ledger):
mint_quote = await wallet1.request_mint(64)
await pay_if_regtest(mint_quote.request)
await wallet1.mint(64, quote_id=mint_quote.quote)
keep_proofs, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 10)
proof_states = await ledger.db_read.get_proofs_states(Ys=[p.Y for p in send_proofs])
assert all([p.state.value == "UNSPENT" for p in proof_states])
# TODO: test keeps running forever, needs to be fixed
# @pytest.mark.asyncio
# async def test_websocket_quote_updates(wallet1: Wallet, ledger: Ledger):
# mint_quote = await wallet1.request_mint(64)
# ws = websocket.create_connection(
# f"ws://localhost:{SERVER_PORT}/v1/quote/{invoice.id}"
# )
# await asyncio.sleep(0.1)
# await pay_if_regtest(mint_quote.request)
# await wallet1.mint(64, quote_id=mint_quote.quote)
# await asyncio.sleep(0.1)
# data = str(ws.recv())
# ws.close()
# n_lines = len(data.split("\n"))
# assert n_lines == 1