mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-19 10:04:19 +01:00
* split wallet test from mint test pipeline * regtest mint and wallet * fix * fix * move mint tests * real invoice in regtest mpp
453 lines
17 KiB
Python
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
|