mirror of
https://github.com/aljazceru/nutshell.git
synced 2026-01-07 19:04:20 +01:00
Fix duplicate blank outputs during melt (#795)
* wip blank outputs * wip: working * store ids for promises correctly * tests * fix migraiton * revert * fix tests * fix auth server * fix last tests * retroactively change migration, initial and m017_foreign_keys_proof_tables, remove c_b and replace with c_ (same for b_) * fix constraint * oops * msg stack fix * fix test foreign key constraint * fix postgres tests * foreign key constraint test * should fix psql error * foreign key constraint sqlite * rename to update_blinded_message_signature * drop outputs and change columns from melt_quotes table * switch migration order * reorder migrations again * fix migration * add tests * fix postgres migration too * create signed_at column postgres * foreign key constraingt promises table * migration tool * readme
This commit is contained in:
@@ -37,6 +37,8 @@ settings.fakewallet_brr = True
|
||||
settings.fakewallet_delay_outgoing_payment = 0
|
||||
settings.fakewallet_delay_incoming_payment = 1
|
||||
settings.fakewallet_stochastic_invoice = False
|
||||
settings.lightning_fee_percent = 2.0
|
||||
settings.lightning_reserve_fee_min = 2000 # msat
|
||||
assert (
|
||||
settings.mint_test_database != settings.mint_database
|
||||
), "Test database is the same as the main database"
|
||||
|
||||
@@ -1,235 +1,365 @@
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
|
||||
from cashu.core.base import BlindedMessage, Proof, Unit
|
||||
from cashu.core.crypto.b_dhke import step1_alice
|
||||
from cashu.core.helpers import calculate_number_of_blank_outputs
|
||||
from cashu.core.models import PostMintQuoteRequest
|
||||
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):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
assert exc.args[0] == msg, Exception(
|
||||
f"Expected error: {msg}, got: {exc.args[0]}"
|
||||
)
|
||||
|
||||
|
||||
def assert_amt(proofs: List[Proof], expected: int):
|
||||
"""Assert amounts the proofs contain."""
|
||||
assert [p.amount for p in proofs] == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pubkeys(ledger: Ledger):
|
||||
assert ledger.keyset.public_keys
|
||||
assert (
|
||||
ledger.keyset.public_keys[1].serialize().hex()
|
||||
== "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104"
|
||||
)
|
||||
assert (
|
||||
ledger.keyset.public_keys[2 ** (settings.max_order - 1)].serialize().hex()
|
||||
== "023c84c0895cc0e827b348ea0a62951ca489a5e436f3ea7545f3c1d5f1bea1c866"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_privatekeys(ledger: Ledger):
|
||||
assert ledger.keyset.private_keys
|
||||
assert (
|
||||
ledger.keyset.private_keys[1].serialize()
|
||||
== "8300050453f08e6ead1296bb864e905bd46761beed22b81110fae0751d84604d"
|
||||
)
|
||||
assert (
|
||||
ledger.keyset.private_keys[2 ** (settings.max_order - 1)].serialize()
|
||||
== "b0477644cb3d82ffcc170bc0a76e0409727232e87c5ae51d64a259936228c7be"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_keysets(ledger: Ledger):
|
||||
assert len(ledger.keysets)
|
||||
assert len(list(ledger.keysets.keys()))
|
||||
assert ledger.keyset.id == "009a1f293253e41e"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_keyset(ledger: Ledger):
|
||||
keyset = ledger.get_keyset()
|
||||
assert isinstance(keyset, dict)
|
||||
assert len(keyset) == settings.max_order
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint(ledger: Ledger):
|
||||
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat"))
|
||||
await pay_if_regtest(quote.request)
|
||||
blinded_messages_mock = [
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
B_="02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239",
|
||||
id="009a1f293253e41e",
|
||||
)
|
||||
]
|
||||
promises = await ledger.mint(outputs=blinded_messages_mock, quote_id=quote.quote)
|
||||
assert len(promises)
|
||||
assert promises[0].amount == 8
|
||||
assert (
|
||||
promises[0].C_
|
||||
== "031422eeffb25319e519c68de000effb294cb362ef713a7cf4832cea7b0452ba6e"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_invalid_quote(ledger: Ledger):
|
||||
await assert_err(
|
||||
ledger.get_mint_quote(quote_id="invalid_quote_id"),
|
||||
"quote not found",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_melt_invalid_quote(ledger: Ledger):
|
||||
await assert_err(
|
||||
ledger.get_melt_quote(quote_id="invalid_quote_id"),
|
||||
"quote not found",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_invalid_blinded_message(ledger: Ledger):
|
||||
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat"))
|
||||
await pay_if_regtest(quote.request)
|
||||
blinded_messages_mock_invalid_key = [
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
B_="02634a2c2b34bec9e8a4aba4361f6bff02d7fa2365379b0840afe249a7a9d71237",
|
||||
id="009a1f293253e41e",
|
||||
)
|
||||
]
|
||||
await assert_err(
|
||||
ledger.mint(outputs=blinded_messages_mock_invalid_key, quote_id=quote.quote),
|
||||
"invalid public key",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_promises(ledger: Ledger):
|
||||
blinded_messages_mock = [
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
B_="02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239",
|
||||
id="009a1f293253e41e",
|
||||
)
|
||||
]
|
||||
promises = await ledger._generate_promises(blinded_messages_mock)
|
||||
assert (
|
||||
promises[0].C_
|
||||
== "031422eeffb25319e519c68de000effb294cb362ef713a7cf4832cea7b0452ba6e"
|
||||
)
|
||||
assert promises[0].amount == 8
|
||||
assert promises[0].id == "009a1f293253e41e"
|
||||
|
||||
# DLEQ proof present
|
||||
assert promises[0].dleq
|
||||
assert promises[0].dleq.s
|
||||
assert promises[0].dleq.e
|
||||
|
||||
|
||||
@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
|
||||
# token amount works: `n_blank_outputs != n_returned_promises != 4`.
|
||||
# invoice_amount = 100_000
|
||||
fee_reserve = 2_000
|
||||
# total_provided = invoice_amount + fee_reserve
|
||||
actual_fee = 100
|
||||
|
||||
expected_returned_promises = 7 # Amounts = [4, 8, 32, 64, 256, 512, 1024]
|
||||
expected_returned_fees = 1900
|
||||
|
||||
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(),
|
||||
id="009a1f293253e41e",
|
||||
)
|
||||
for b, _ in blinded_msgs
|
||||
]
|
||||
|
||||
promises = await ledger._generate_change_promises(
|
||||
fee_provided=fee_reserve, fee_paid=actual_fee, outputs=outputs
|
||||
)
|
||||
|
||||
assert len(promises) == expected_returned_promises
|
||||
assert sum([promise.amount for promise in promises]) == expected_returned_fees
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_change_promises_legacy_wallet(ledger: Ledger):
|
||||
# Check if mint handles a legacy wallet implementation (always sends 4 blank
|
||||
# outputs) as well.
|
||||
# invoice_amount = 100_000
|
||||
fee_reserve = 2_000
|
||||
# total_provided = invoice_amount + fee_reserve
|
||||
actual_fee = 100
|
||||
|
||||
expected_returned_promises = 4 # Amounts = [64, 256, 512, 1024]
|
||||
expected_returned_fees = 1856
|
||||
|
||||
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(),
|
||||
id="009a1f293253e41e",
|
||||
)
|
||||
for b, _ in blinded_msgs
|
||||
]
|
||||
|
||||
promises = await ledger._generate_change_promises(fee_reserve, actual_fee, outputs)
|
||||
|
||||
assert len(promises) == expected_returned_promises
|
||||
assert sum([promise.amount for promise in promises]) == expected_returned_fees
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_change_promises_returns_empty_if_no_outputs(ledger: Ledger):
|
||||
# invoice_amount = 100_000
|
||||
fee_reserve = 1_000
|
||||
# total_provided = invoice_amount + fee_reserve
|
||||
actual_fee_msat = 100_000
|
||||
outputs = None
|
||||
|
||||
promises = await ledger._generate_change_promises(
|
||||
fee_reserve, actual_fee_msat, outputs
|
||||
)
|
||||
assert len(promises) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_balance(ledger: Ledger):
|
||||
unit = Unit["sat"]
|
||||
balance, fees_paid = await ledger.get_balance(unit)
|
||||
assert balance == 0
|
||||
assert fees_paid == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_maximum_balance(ledger: Ledger):
|
||||
settings.mint_max_balance = 1000
|
||||
await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat"))
|
||||
await assert_err(
|
||||
ledger.mint_quote(PostMintQuoteRequest(amount=8000, unit="sat")),
|
||||
"Mint has reached maximum balance.",
|
||||
)
|
||||
settings.mint_max_balance = 0
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
|
||||
from cashu.core.base import BlindedMessage, Proof, Unit
|
||||
from cashu.core.crypto.b_dhke import step1_alice
|
||||
from cashu.core.helpers import calculate_number_of_blank_outputs
|
||||
from cashu.core.models import PostMeltQuoteRequest, PostMintQuoteRequest
|
||||
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):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
assert exc.args[0] == msg, Exception(
|
||||
f"Expected error: {msg}, got: {exc.args[0]}"
|
||||
)
|
||||
|
||||
|
||||
def assert_amt(proofs: List[Proof], expected: int):
|
||||
"""Assert amounts the proofs contain."""
|
||||
assert [p.amount for p in proofs] == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pubkeys(ledger: Ledger):
|
||||
assert ledger.keyset.public_keys
|
||||
assert (
|
||||
ledger.keyset.public_keys[1].serialize().hex()
|
||||
== "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104"
|
||||
)
|
||||
assert (
|
||||
ledger.keyset.public_keys[2 ** (settings.max_order - 1)].serialize().hex()
|
||||
== "023c84c0895cc0e827b348ea0a62951ca489a5e436f3ea7545f3c1d5f1bea1c866"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_privatekeys(ledger: Ledger):
|
||||
assert ledger.keyset.private_keys
|
||||
assert (
|
||||
ledger.keyset.private_keys[1].serialize()
|
||||
== "8300050453f08e6ead1296bb864e905bd46761beed22b81110fae0751d84604d"
|
||||
)
|
||||
assert (
|
||||
ledger.keyset.private_keys[2 ** (settings.max_order - 1)].serialize()
|
||||
== "b0477644cb3d82ffcc170bc0a76e0409727232e87c5ae51d64a259936228c7be"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_keysets(ledger: Ledger):
|
||||
assert len(ledger.keysets)
|
||||
assert len(list(ledger.keysets.keys()))
|
||||
assert ledger.keyset.id == "009a1f293253e41e"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_keyset(ledger: Ledger):
|
||||
keyset = ledger.get_keyset()
|
||||
assert isinstance(keyset, dict)
|
||||
assert len(keyset) == settings.max_order
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint(ledger: Ledger):
|
||||
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat"))
|
||||
await pay_if_regtest(quote.request)
|
||||
blinded_messages_mock = [
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
B_="02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239",
|
||||
id="009a1f293253e41e",
|
||||
)
|
||||
]
|
||||
promises = await ledger.mint(outputs=blinded_messages_mock, quote_id=quote.quote)
|
||||
assert len(promises)
|
||||
assert promises[0].amount == 8
|
||||
assert (
|
||||
promises[0].C_
|
||||
== "031422eeffb25319e519c68de000effb294cb362ef713a7cf4832cea7b0452ba6e"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_invalid_quote(ledger: Ledger):
|
||||
await assert_err(
|
||||
ledger.get_mint_quote(quote_id="invalid_quote_id"),
|
||||
"quote not found",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_melt_invalid_quote(ledger: Ledger):
|
||||
await assert_err(
|
||||
ledger.get_melt_quote(quote_id="invalid_quote_id"),
|
||||
"quote not found",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_invalid_blinded_message(ledger: Ledger):
|
||||
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat"))
|
||||
await pay_if_regtest(quote.request)
|
||||
blinded_messages_mock_invalid_key = [
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
B_="02634a2c2b34bec9e8a4aba4361f6bff02d7fa2365379b0840afe249a7a9d71237",
|
||||
id="009a1f293253e41e",
|
||||
)
|
||||
]
|
||||
await assert_err(
|
||||
ledger.mint(outputs=blinded_messages_mock_invalid_key, quote_id=quote.quote),
|
||||
"invalid public key",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_promises(ledger: Ledger):
|
||||
blinded_messages_mock = [
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
B_="02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239",
|
||||
id="009a1f293253e41e",
|
||||
)
|
||||
]
|
||||
await ledger._store_blinded_messages(blinded_messages_mock)
|
||||
promises = await ledger._sign_blinded_messages(blinded_messages_mock)
|
||||
assert (
|
||||
promises[0].C_
|
||||
== "031422eeffb25319e519c68de000effb294cb362ef713a7cf4832cea7b0452ba6e"
|
||||
)
|
||||
assert promises[0].amount == 8
|
||||
assert promises[0].id == "009a1f293253e41e"
|
||||
|
||||
# DLEQ proof present
|
||||
assert promises[0].dleq
|
||||
assert promises[0].dleq.s
|
||||
assert promises[0].dleq.e
|
||||
|
||||
|
||||
@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
|
||||
# token amount works: `n_blank_outputs != n_returned_promises != 4`.
|
||||
# invoice_amount = 100_000
|
||||
fee_reserve = 2_000
|
||||
# total_provided = invoice_amount + fee_reserve
|
||||
actual_fee = 100
|
||||
|
||||
expected_returned_promises = 7 # Amounts = [4, 8, 32, 64, 256, 512, 1024]
|
||||
expected_returned_fees = 1900
|
||||
|
||||
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(),
|
||||
id="009a1f293253e41e",
|
||||
)
|
||||
for b, _ in blinded_msgs
|
||||
]
|
||||
await ledger._store_blinded_messages(outputs)
|
||||
promises = await ledger._generate_change_promises(
|
||||
fee_provided=fee_reserve, fee_paid=actual_fee, outputs=outputs
|
||||
)
|
||||
|
||||
assert len(promises) == expected_returned_promises
|
||||
assert sum([promise.amount for promise in promises]) == expected_returned_fees
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_change_promises_legacy_wallet(ledger: Ledger):
|
||||
# Check if mint handles a legacy wallet implementation (always sends 4 blank
|
||||
# outputs) as well.
|
||||
# invoice_amount = 100_000
|
||||
fee_reserve = 2_000
|
||||
# total_provided = invoice_amount + fee_reserve
|
||||
actual_fee = 100
|
||||
|
||||
expected_returned_promises = 4 # Amounts = [64, 256, 512, 1024]
|
||||
expected_returned_fees = 1856
|
||||
|
||||
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(),
|
||||
id="009a1f293253e41e",
|
||||
)
|
||||
for b, _ in blinded_msgs
|
||||
]
|
||||
|
||||
await ledger._store_blinded_messages(outputs)
|
||||
promises = await ledger._generate_change_promises(fee_reserve, actual_fee, outputs)
|
||||
|
||||
assert len(promises) == expected_returned_promises
|
||||
assert sum([promise.amount for promise in promises]) == expected_returned_fees
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_change_promises_returns_empty_if_no_outputs(ledger: Ledger):
|
||||
# invoice_amount = 100_000
|
||||
fee_reserve = 1_000
|
||||
# total_provided = invoice_amount + fee_reserve
|
||||
actual_fee_msat = 100_000
|
||||
outputs = None
|
||||
|
||||
promises = await ledger._generate_change_promises(
|
||||
fee_reserve, actual_fee_msat, outputs
|
||||
)
|
||||
assert len(promises) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_balance(ledger: Ledger):
|
||||
unit = Unit["sat"]
|
||||
balance, fees_paid = await ledger.get_balance(unit)
|
||||
assert balance == 0
|
||||
assert fees_paid == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_maximum_balance(ledger: Ledger):
|
||||
settings.mint_max_balance = 1000
|
||||
await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat"))
|
||||
await assert_err(
|
||||
ledger.mint_quote(PostMintQuoteRequest(amount=8000, unit="sat")),
|
||||
"Mint has reached maximum balance.",
|
||||
)
|
||||
settings.mint_max_balance = 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_change_promises_signs_subset_and_deletes_rest(ledger: Ledger):
|
||||
from cashu.core.base import BlindedMessage
|
||||
from cashu.core.crypto.b_dhke import step1_alice
|
||||
from cashu.core.split import amount_split
|
||||
|
||||
# Create a real melt quote to satisfy FK on promises.melt_quote
|
||||
mint_quote_resp = await ledger.mint_quote(
|
||||
PostMintQuoteRequest(amount=64, unit="sat")
|
||||
)
|
||||
melt_quote_resp = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(request=mint_quote_resp.request, unit="sat")
|
||||
)
|
||||
melt_id = melt_quote_resp.quote
|
||||
fee_provided = 2_000
|
||||
fee_paid = 100
|
||||
overpaid_fee = fee_provided - fee_paid
|
||||
return_amounts = amount_split(overpaid_fee)
|
||||
|
||||
# Store more blank outputs than needed for the change.
|
||||
extra_blanks = 3
|
||||
n_blank = len(return_amounts) + extra_blanks
|
||||
blank_outputs = [
|
||||
BlindedMessage(
|
||||
amount=1,
|
||||
B_=step1_alice(f"change_blank_{i}")[0].serialize().hex(),
|
||||
id=ledger.keyset.id,
|
||||
)
|
||||
for i in range(n_blank)
|
||||
]
|
||||
await ledger._store_blinded_messages(blank_outputs, melt_id=melt_id)
|
||||
|
||||
# Fetch the stored unsigned blanks (same as melt flow) and run change generation.
|
||||
stored_outputs = await ledger.crud.get_blinded_messages_melt_id(
|
||||
db=ledger.db, melt_id=melt_id
|
||||
)
|
||||
assert len(stored_outputs) == n_blank
|
||||
|
||||
promises = await ledger._generate_change_promises(
|
||||
fee_provided=fee_provided,
|
||||
fee_paid=fee_paid,
|
||||
outputs=stored_outputs,
|
||||
melt_id=melt_id,
|
||||
keyset=ledger.keyset,
|
||||
)
|
||||
|
||||
assert len(promises) == len(return_amounts)
|
||||
assert sorted(p.amount for p in promises) == sorted(return_amounts)
|
||||
|
||||
# All unsigned blanks should be deleted after signing the subset.
|
||||
remaining_unsigned = await ledger.crud.get_blinded_messages_melt_id(
|
||||
db=ledger.db, melt_id=melt_id
|
||||
)
|
||||
assert remaining_unsigned == []
|
||||
|
||||
# The signed promises should remain in the DB with c_ set.
|
||||
async with ledger.db.connect() as conn:
|
||||
rows = await conn.fetchall(
|
||||
f"""
|
||||
SELECT amount, c_ FROM {ledger.db.table_with_schema('promises')}
|
||||
WHERE melt_quote = :melt_id
|
||||
""",
|
||||
{"melt_id": melt_id},
|
||||
)
|
||||
assert len(rows) == len(return_amounts)
|
||||
assert all(row["c_"] for row in rows)
|
||||
assert sorted(int(row["amount"]) for row in rows) == sorted(return_amounts)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_change_promises_zero_fee_deletes_all_blanks(ledger: Ledger):
|
||||
from cashu.core.base import BlindedMessage
|
||||
from cashu.core.crypto.b_dhke import step1_alice
|
||||
|
||||
# Create a real melt quote to satisfy FK on promises.melt_quote
|
||||
mint_quote_resp = await ledger.mint_quote(
|
||||
PostMintQuoteRequest(amount=64, unit="sat")
|
||||
)
|
||||
melt_quote_resp = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(request=mint_quote_resp.request, unit="sat")
|
||||
)
|
||||
melt_id = melt_quote_resp.quote
|
||||
fee_provided = 1_000
|
||||
fee_paid = 1_000 # no overpaid fee
|
||||
n_blank = 4
|
||||
blank_outputs = [
|
||||
BlindedMessage(
|
||||
amount=1,
|
||||
B_=step1_alice(f"no_fee_blank_{i}")[0].serialize().hex(),
|
||||
id=ledger.keyset.id,
|
||||
)
|
||||
for i in range(n_blank)
|
||||
]
|
||||
await ledger._store_blinded_messages(blank_outputs, melt_id=melt_id)
|
||||
|
||||
stored_outputs = await ledger.crud.get_blinded_messages_melt_id(
|
||||
db=ledger.db, melt_id=melt_id
|
||||
)
|
||||
assert len(stored_outputs) == n_blank
|
||||
|
||||
promises = await ledger._generate_change_promises(
|
||||
fee_provided=fee_provided,
|
||||
fee_paid=fee_paid,
|
||||
outputs=stored_outputs,
|
||||
melt_id=melt_id,
|
||||
keyset=ledger.keyset,
|
||||
)
|
||||
|
||||
assert promises == []
|
||||
|
||||
remaining_unsigned = await ledger.crud.get_blinded_messages_melt_id(
|
||||
db=ledger.db, melt_id=melt_id
|
||||
)
|
||||
# With zero fee nothing is signed or deleted; blanks stay pending.
|
||||
assert len(remaining_unsigned) == n_blank
|
||||
|
||||
async with ledger.db.connect() as conn:
|
||||
rows = await conn.fetchall(
|
||||
f"""
|
||||
SELECT amount, c_ FROM {ledger.db.table_with_schema('promises')}
|
||||
WHERE melt_quote = :melt_id
|
||||
""",
|
||||
{"melt_id": melt_id},
|
||||
)
|
||||
assert len(rows) == n_blank
|
||||
assert all(row["c_"] is None for row in rows)
|
||||
|
||||
@@ -23,9 +23,7 @@ from tests.helpers import (
|
||||
pay_if_regtest,
|
||||
)
|
||||
|
||||
payment_request = (
|
||||
"lnbc1u1p5qeft3sp5jn5cqclnxvucfqtjm8qnlar2vhevcuudpccv7tsuglruj3qm579spp5ygdhy0t7xu53myke8z3z024xhz4kzgk9fcqk64sp0fyeqzhmaswqdqqcqpjrzjq0euzzxv65mts5ngg8c2t3vzz2aeuevy5845jvyqulqucd8c9kkhzrtp55qq63qqqqqqqqqqqqqzwyqqyg9qxpqysgqscprcpnk8whs3askqhgu6z5a4hupyn8du2aahdcf00s5pxrs4g94sv9f95xdn4tu0wec7kfyzj439wu9z27k6m6e3q4ysjquf5agx7gp0eeye4"
|
||||
)
|
||||
payment_request = "lnbc1u1p5qeft3sp5jn5cqclnxvucfqtjm8qnlar2vhevcuudpccv7tsuglruj3qm579spp5ygdhy0t7xu53myke8z3z024xhz4kzgk9fcqk64sp0fyeqzhmaswqdqqcqpjrzjq0euzzxv65mts5ngg8c2t3vzz2aeuevy5845jvyqulqucd8c9kkhzrtp55qq63qqqqqqqqqqqqqzwyqqyg9qxpqysgqscprcpnk8whs3askqhgu6z5a4hupyn8du2aahdcf00s5pxrs4g94sv9f95xdn4tu0wec7kfyzj439wu9z27k6m6e3q4ysjquf5agx7gp0eeye4"
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
@@ -295,30 +293,51 @@ async def test_db_events_add_client(wallet: Wallet, ledger: Ledger):
|
||||
# remove subscription
|
||||
client.remove_subscription("subId")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_db_update_mint_quote_state(wallet: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet.request_mint(128)
|
||||
await ledger.db_write._update_mint_quote_state(mint_quote.quote, MintQuoteState.paid)
|
||||
|
||||
mint_quote_db = await ledger.crud.get_mint_quote(quote_id=mint_quote.quote, db=ledger.db)
|
||||
await ledger.db_write._update_mint_quote_state(
|
||||
mint_quote.quote, MintQuoteState.paid
|
||||
)
|
||||
|
||||
mint_quote_db = await ledger.crud.get_mint_quote(
|
||||
quote_id=mint_quote.quote, db=ledger.db
|
||||
)
|
||||
assert mint_quote_db
|
||||
assert mint_quote_db.state == MintQuoteState.paid
|
||||
|
||||
# Update it to issued
|
||||
await ledger.db_write._update_mint_quote_state(mint_quote_db.quote, MintQuoteState.issued)
|
||||
await ledger.db_write._update_mint_quote_state(
|
||||
mint_quote_db.quote, MintQuoteState.issued
|
||||
)
|
||||
|
||||
# Try and revert it back to unpaid
|
||||
await assert_err(ledger.db_write._update_mint_quote_state(mint_quote_db.quote, MintQuoteState.unpaid), "Cannot change state of an issued mint quote.")
|
||||
await assert_err(
|
||||
ledger.db_write._update_mint_quote_state(
|
||||
mint_quote_db.quote, MintQuoteState.unpaid
|
||||
),
|
||||
"Cannot change state of an issued mint quote.",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
is_deprecated_api_only,
|
||||
reason=("Deprecated API")
|
||||
)
|
||||
@pytest.mark.skipif(is_deprecated_api_only, reason=("Deprecated API"))
|
||||
async def test_db_update_melt_quote_state(wallet: Wallet, ledger: Ledger):
|
||||
melt_quote = await wallet.melt_quote(payment_request)
|
||||
await ledger.db_write._update_melt_quote_state(melt_quote.quote, MeltQuoteState.paid)
|
||||
await ledger.db_write._update_melt_quote_state(
|
||||
melt_quote.quote, MeltQuoteState.paid
|
||||
)
|
||||
|
||||
melt_quote_db = await ledger.crud.get_melt_quote(quote_id=melt_quote.quote, db=ledger.db)
|
||||
melt_quote_db = await ledger.crud.get_melt_quote(
|
||||
quote_id=melt_quote.quote, db=ledger.db
|
||||
)
|
||||
assert melt_quote_db
|
||||
assert melt_quote_db.state == MeltQuoteState.paid
|
||||
|
||||
await assert_err(ledger.db_write._update_melt_quote_state(melt_quote.quote, MeltQuoteState.unpaid), "Cannot change state of a paid melt quote.")
|
||||
await assert_err(
|
||||
ledger.db_write._update_melt_quote_state(
|
||||
melt_quote.quote, MeltQuoteState.unpaid
|
||||
),
|
||||
"Cannot change state of a paid melt quote.",
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ import pytest_asyncio
|
||||
from cashu.core import db
|
||||
from cashu.core.db import Connection
|
||||
from cashu.core.migrations import backup_database
|
||||
from cashu.core.models import PostMeltQuoteRequest
|
||||
from cashu.core.settings import settings
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.wallet import Wallet
|
||||
@@ -353,3 +354,402 @@ async def test_db_lock_table(wallet: Wallet, ledger: Ledger):
|
||||
),
|
||||
"failed to acquire database lock",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_store_and_sign_blinded_message(ledger: Ledger):
|
||||
# Localized imports to avoid polluting module scope
|
||||
from cashu.core.crypto.b_dhke import step1_alice, step2_bob
|
||||
from cashu.core.crypto.secp import PublicKey
|
||||
|
||||
# Arrange: prepare a blinded message tied to current active keyset
|
||||
amount = 8
|
||||
keyset_id = ledger.keyset.id
|
||||
B_pubkey, _ = step1_alice("test_store_and_sign_blinded_message")
|
||||
B_hex = B_pubkey.serialize().hex()
|
||||
|
||||
# Act: store the blinded message (unsinged promise row)
|
||||
await ledger.crud.store_blinded_message(
|
||||
db=ledger.db,
|
||||
amount=amount,
|
||||
b_=B_hex,
|
||||
id=keyset_id,
|
||||
)
|
||||
|
||||
# Act: compute a valid blind signature for the stored row and persist it
|
||||
private_key_amount = ledger.keyset.private_keys[amount]
|
||||
B_point = PublicKey(bytes.fromhex(B_hex), raw=True)
|
||||
C_point, e, s = step2_bob(B_point, private_key_amount)
|
||||
|
||||
await ledger.crud.update_blinded_message_signature(
|
||||
db=ledger.db,
|
||||
amount=amount,
|
||||
b_=B_hex,
|
||||
c_=C_point.serialize().hex(),
|
||||
e=e.serialize(),
|
||||
s=s.serialize(),
|
||||
)
|
||||
|
||||
# Assert: row is now a full promise and can be read back via get_promise
|
||||
promise = await ledger.crud.get_promise(db=ledger.db, b_=B_hex)
|
||||
assert promise is not None
|
||||
assert promise.amount == amount
|
||||
assert promise.C_ == C_point.serialize().hex()
|
||||
assert promise.id == keyset_id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_blinded_messages_by_melt_id(wallet: Wallet, ledger: Ledger):
|
||||
# Arrange
|
||||
from cashu.core.crypto.b_dhke import step1_alice
|
||||
|
||||
amount = 8
|
||||
keyset_id = ledger.keyset.id
|
||||
# Create a real melt quote to satisfy FK on promises.melt_quote
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(request=mint_quote.request, unit="sat")
|
||||
)
|
||||
melt_id = melt_quote.quote
|
||||
|
||||
# Create two blinded messages
|
||||
B1, _ = step1_alice("get_by_melt_id_1")
|
||||
B2, _ = step1_alice("get_by_melt_id_2")
|
||||
b1_hex = B1.serialize().hex()
|
||||
b2_hex = B2.serialize().hex()
|
||||
|
||||
# Persist as unsigned messages with proper melt_id FK
|
||||
await ledger.crud.store_blinded_message(
|
||||
db=ledger.db, amount=amount, b_=b1_hex, id=keyset_id, melt_id=melt_id
|
||||
)
|
||||
await ledger.crud.store_blinded_message(
|
||||
db=ledger.db, amount=amount, b_=b2_hex, id=keyset_id, melt_id=melt_id
|
||||
)
|
||||
|
||||
# Act
|
||||
rows = await ledger.crud.get_blinded_messages_melt_id(db=ledger.db, melt_id=melt_id)
|
||||
|
||||
# Assert
|
||||
assert len(rows) == 2
|
||||
assert {r.B_ for r in rows} == {b1_hex, b2_hex}
|
||||
assert all(r.id == keyset_id for r in rows)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_blinded_messages_by_melt_id(wallet: Wallet, ledger: Ledger):
|
||||
from cashu.core.crypto.b_dhke import step1_alice
|
||||
|
||||
amount = 4
|
||||
keyset_id = ledger.keyset.id
|
||||
# Create a real melt quote to satisfy FK on promises.melt_quote
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(request=mint_quote.request, unit="sat")
|
||||
)
|
||||
melt_id = melt_quote.quote
|
||||
|
||||
# Create two blinded messages
|
||||
B1, _ = step1_alice("delete_by_melt_id_1")
|
||||
B2, _ = step1_alice("delete_by_melt_id_2")
|
||||
b1_hex = B1.serialize().hex()
|
||||
b2_hex = B2.serialize().hex()
|
||||
|
||||
# Persist as unsigned messages
|
||||
await ledger.crud.store_blinded_message(
|
||||
db=ledger.db, amount=amount, b_=b1_hex, id=keyset_id, melt_id=melt_id
|
||||
)
|
||||
await ledger.crud.store_blinded_message(
|
||||
db=ledger.db, amount=amount, b_=b2_hex, id=keyset_id, melt_id=melt_id
|
||||
)
|
||||
|
||||
rows_before = await ledger.crud.get_blinded_messages_melt_id(
|
||||
db=ledger.db, melt_id=melt_id
|
||||
)
|
||||
assert len(rows_before) == 2
|
||||
|
||||
# Act: delete all unsigned messages for this melt_id
|
||||
await ledger.crud.delete_blinded_messages_melt_id(db=ledger.db, melt_id=melt_id)
|
||||
|
||||
# Assert: now none left for that melt_id
|
||||
rows_after = await ledger.crud.get_blinded_messages_melt_id(
|
||||
db=ledger.db, melt_id=melt_id
|
||||
)
|
||||
assert rows_after == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_blinded_messages_by_melt_id_filters_signed(
|
||||
wallet: Wallet, ledger: Ledger
|
||||
):
|
||||
from cashu.core.crypto.b_dhke import step1_alice, step2_bob
|
||||
from cashu.core.crypto.secp import PublicKey
|
||||
|
||||
amount = 2
|
||||
keyset_id = ledger.keyset.id
|
||||
# Create a real melt quote to satisfy FK on promises.melt_quote
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(request=mint_quote.request, unit="sat")
|
||||
)
|
||||
melt_id = melt_quote.quote
|
||||
|
||||
B1, _ = step1_alice("filter_by_melt_id_1")
|
||||
B2, _ = step1_alice("filter_by_melt_id_2")
|
||||
b1_hex = B1.serialize().hex()
|
||||
b2_hex = B2.serialize().hex()
|
||||
|
||||
# Persist two unsigned messages
|
||||
await ledger.crud.store_blinded_message(
|
||||
db=ledger.db, amount=amount, b_=b1_hex, id=keyset_id, melt_id=melt_id
|
||||
)
|
||||
await ledger.crud.store_blinded_message(
|
||||
db=ledger.db, amount=amount, b_=b2_hex, id=keyset_id, melt_id=melt_id
|
||||
)
|
||||
|
||||
# Sign one of them (it should no longer be returned by get_blinded_messages_melt_id which filters c_ IS NULL)
|
||||
priv = ledger.keyset.private_keys[amount]
|
||||
C_point, e, s = step2_bob(PublicKey(bytes.fromhex(b1_hex), raw=True), priv)
|
||||
await ledger.crud.update_blinded_message_signature(
|
||||
db=ledger.db,
|
||||
amount=amount,
|
||||
b_=b1_hex,
|
||||
c_=C_point.serialize().hex(),
|
||||
e=e.serialize(),
|
||||
s=s.serialize(),
|
||||
)
|
||||
|
||||
# Act
|
||||
rows = await ledger.crud.get_blinded_messages_melt_id(db=ledger.db, melt_id=melt_id)
|
||||
|
||||
# Assert: only the unsigned one remains (b2_hex)
|
||||
assert len(rows) == 1
|
||||
assert rows[0].B_ == b2_hex
|
||||
assert rows[0].id == keyset_id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_store_blinded_message(ledger: Ledger):
|
||||
from cashu.core.crypto.b_dhke import step1_alice
|
||||
|
||||
amount = 8
|
||||
keyset_id = ledger.keyset.id
|
||||
B_pub, _ = step1_alice("test_store_blinded_message")
|
||||
b_hex = B_pub.serialize().hex()
|
||||
|
||||
# Act: store unsigned blinded message
|
||||
await ledger.crud.store_blinded_message(
|
||||
db=ledger.db, amount=amount, b_=b_hex, id=keyset_id
|
||||
)
|
||||
|
||||
# Assert: row exists and is unsigned (c_ IS NULL)
|
||||
async with ledger.db.connect() as conn:
|
||||
row = await conn.fetchone(
|
||||
f"SELECT amount, id, b_, c_, created FROM {ledger.db.table_with_schema('promises')} WHERE b_ = :b_",
|
||||
{"b_": b_hex},
|
||||
)
|
||||
assert row is not None
|
||||
assert int(row["amount"]) == amount
|
||||
assert row["id"] == keyset_id
|
||||
assert row["b_"] == b_hex
|
||||
assert row["c_"] is None
|
||||
assert row["created"] is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_blinded_message_signature_before_store_blinded_message_errors(
|
||||
ledger: Ledger,
|
||||
):
|
||||
from cashu.core.crypto.b_dhke import step1_alice, step2_bob
|
||||
from cashu.core.crypto.secp import PublicKey
|
||||
|
||||
amount = 8
|
||||
# Generate a blinded message that we will NOT store
|
||||
B_pub, _ = step1_alice("test_sign_before_store_blinded_message")
|
||||
b_hex = B_pub.serialize().hex()
|
||||
|
||||
# Create a valid signature tuple for that blinded message
|
||||
priv = ledger.keyset.private_keys[amount]
|
||||
C_point, e, s = step2_bob(PublicKey(bytes.fromhex(b_hex), raw=True), priv)
|
||||
|
||||
# Expect a DB-level error; on SQLite/Postgres this is typically a no-op update, so this test is xfail.
|
||||
await assert_err(
|
||||
ledger.crud.update_blinded_message_signature(
|
||||
db=ledger.db,
|
||||
amount=amount,
|
||||
b_=b_hex,
|
||||
c_=C_point.serialize().hex(),
|
||||
e=e.serialize(),
|
||||
s=s.serialize(),
|
||||
),
|
||||
"blinded message does not exist",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_store_blinded_message_duplicate_b_(ledger: Ledger):
|
||||
from cashu.core.crypto.b_dhke import step1_alice
|
||||
|
||||
amount = 2
|
||||
keyset_id = ledger.keyset.id
|
||||
B_pub, _ = step1_alice("test_duplicate_b_")
|
||||
b_hex = B_pub.serialize().hex()
|
||||
|
||||
# First insert should succeed
|
||||
await ledger.crud.store_blinded_message(
|
||||
db=ledger.db, amount=amount, b_=b_hex, id=keyset_id
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_blind_signatures_by_melt_id_returns_signed(
|
||||
wallet: Wallet, ledger: Ledger
|
||||
):
|
||||
from cashu.core.crypto.b_dhke import step1_alice, step2_bob
|
||||
from cashu.core.crypto.secp import PublicKey
|
||||
|
||||
amount = 4
|
||||
keyset_id = ledger.keyset.id
|
||||
# Create a real melt quote to satisfy FK on promises.melt_quote
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(request=mint_quote.request, unit="sat")
|
||||
)
|
||||
melt_id = melt_quote.quote
|
||||
|
||||
# Prepare two blinded messages under the same melt_id
|
||||
B1, _ = step1_alice("signed_promises_by_melt_id_1")
|
||||
B2, _ = step1_alice("signed_promises_by_melt_id_2")
|
||||
b1_hex = B1.serialize().hex()
|
||||
b2_hex = B2.serialize().hex()
|
||||
|
||||
await ledger.crud.store_blinded_message(
|
||||
db=ledger.db, amount=amount, b_=b1_hex, id=keyset_id, melt_id=melt_id
|
||||
)
|
||||
await ledger.crud.store_blinded_message(
|
||||
db=ledger.db, amount=amount, b_=b2_hex, id=keyset_id, melt_id=melt_id
|
||||
)
|
||||
|
||||
# Sign only one of them -> should be returned by get_blind_signatures_melt_id
|
||||
priv = ledger.keyset.private_keys[amount]
|
||||
C_point, e, s = step2_bob(PublicKey(bytes.fromhex(b1_hex), raw=True), priv)
|
||||
await ledger.crud.update_blinded_message_signature(
|
||||
db=ledger.db,
|
||||
amount=amount,
|
||||
b_=b1_hex,
|
||||
c_=C_point.serialize().hex(),
|
||||
e=e.serialize(),
|
||||
s=s.serialize(),
|
||||
)
|
||||
|
||||
# Act
|
||||
signed = await ledger.crud.get_blind_signatures_melt_id(
|
||||
db=ledger.db, melt_id=melt_id
|
||||
)
|
||||
|
||||
# Assert: only the signed one is returned
|
||||
assert len(signed) == 1
|
||||
assert signed[0].amount == amount
|
||||
assert signed[0].id == keyset_id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_melt_quote_includes_change_signatures(
|
||||
wallet: Wallet, ledger: Ledger
|
||||
):
|
||||
from cashu.core.crypto.b_dhke import step1_alice, step2_bob
|
||||
from cashu.core.crypto.secp import PublicKey
|
||||
|
||||
amount = 8
|
||||
keyset_id = ledger.keyset.id
|
||||
|
||||
# Create melt quote and attach outputs/promises under its melt_id
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(request=mint_quote.request, unit="sat")
|
||||
)
|
||||
|
||||
melt_id = melt_quote.quote
|
||||
|
||||
# Create two blinded messages, sign one -> becomes change
|
||||
B1, _ = step1_alice("melt_quote_change_1")
|
||||
B2, _ = step1_alice("melt_quote_change_2")
|
||||
b1_hex = B1.serialize().hex()
|
||||
b2_hex = B2.serialize().hex()
|
||||
|
||||
await ledger.crud.store_blinded_message(
|
||||
db=ledger.db, amount=amount, b_=b1_hex, id=keyset_id, melt_id=melt_id
|
||||
)
|
||||
await ledger.crud.store_blinded_message(
|
||||
db=ledger.db, amount=amount, b_=b2_hex, id=keyset_id, melt_id=melt_id
|
||||
)
|
||||
|
||||
# Sign one -> should appear in change loaded by get_melt_quote
|
||||
priv = ledger.keyset.private_keys[amount]
|
||||
C_point, e, s = step2_bob(PublicKey(bytes.fromhex(b1_hex), raw=True), priv)
|
||||
await ledger.crud.update_blinded_message_signature(
|
||||
db=ledger.db,
|
||||
amount=amount,
|
||||
b_=b1_hex,
|
||||
c_=C_point.serialize().hex(),
|
||||
e=e.serialize(),
|
||||
s=s.serialize(),
|
||||
)
|
||||
|
||||
# Act
|
||||
quote_db = await ledger.crud.get_melt_quote(quote_id=melt_id, db=ledger.db)
|
||||
|
||||
# Assert: change contains the signed promise(s)
|
||||
assert quote_db is not None
|
||||
assert quote_db.quote == melt_id
|
||||
assert quote_db.change is not None
|
||||
assert len(quote_db.change) == 1
|
||||
assert quote_db.change[0].amount == amount
|
||||
assert quote_db.change[0].id == keyset_id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_promises_fk_constraints_enforced(ledger: Ledger):
|
||||
from cashu.core.crypto.b_dhke import step1_alice
|
||||
|
||||
keyset_id = ledger.keyset.id
|
||||
B1, _ = step1_alice("fk_check_melt")
|
||||
B2, _ = step1_alice("fk_check_mint")
|
||||
b1_hex = B1.serialize().hex()
|
||||
b2_hex = B2.serialize().hex()
|
||||
|
||||
# Use a single connection and enable FK enforcement on SQLite
|
||||
async with ledger.db.connect() as conn:
|
||||
# Fake melt_id should violate FK on promises.melt_quote
|
||||
await assert_err_multiple(
|
||||
ledger.crud.store_blinded_message(
|
||||
db=ledger.db,
|
||||
amount=1,
|
||||
b_=b1_hex,
|
||||
id=keyset_id,
|
||||
melt_id="nonexistent-melt-id",
|
||||
conn=conn,
|
||||
),
|
||||
[
|
||||
"FOREIGN KEY", # SQLite
|
||||
"violates foreign key constraint", # Postgres
|
||||
],
|
||||
)
|
||||
|
||||
async with ledger.db.connect() as conn:
|
||||
# Fake mint_id should violate FK on promises.mint_quote
|
||||
await assert_err_multiple(
|
||||
ledger.crud.store_blinded_message(
|
||||
db=ledger.db,
|
||||
amount=1,
|
||||
b_=b2_hex,
|
||||
id=keyset_id,
|
||||
mint_id="nonexistent-mint-id",
|
||||
conn=conn,
|
||||
),
|
||||
[
|
||||
"FOREIGN KEY", # SQLite
|
||||
"violates foreign key constraint", # Postgres
|
||||
],
|
||||
)
|
||||
|
||||
# Done. This test only checks FK enforcement paths.
|
||||
|
||||
@@ -51,13 +51,13 @@ async def test_wallet_subscription_mint(wallet: Wallet):
|
||||
await asyncio.sleep(wait + 2)
|
||||
|
||||
assert triggered
|
||||
assert len(msg_stack) == 3
|
||||
assert len(msg_stack) >= 3
|
||||
|
||||
assert msg_stack[0].payload["state"] == MintQuoteState.unpaid.value
|
||||
|
||||
assert msg_stack[1].payload["state"] == MintQuoteState.paid.value
|
||||
|
||||
assert msg_stack[2].payload["state"] == MintQuoteState.issued.value
|
||||
assert msg_stack[-1].payload["state"] == MintQuoteState.issued.value
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -133,7 +133,9 @@ async def test_wallet_subscription_multiple_listeners_receive_updates(wallet: Wa
|
||||
from cashu.wallet.subscriptions import SubscriptionManager
|
||||
|
||||
subs = SubscriptionManager(wallet.url)
|
||||
threading.Thread(target=subs.connect, name="SubscriptionManager", daemon=True).start()
|
||||
threading.Thread(
|
||||
target=subs.connect, name="SubscriptionManager", daemon=True
|
||||
).start()
|
||||
|
||||
stack1: list[JSONRPCNotficationParams] = []
|
||||
stack2: list[JSONRPCNotficationParams] = []
|
||||
|
||||
Reference in New Issue
Block a user