Fix blind message already signed error (#828)

* get unsigned blinded messages for output duplicate check

* bm regression tests (#827)

* fix last entry

* test for error from error struct

* rename tests, fix second regression test, add descriptive comments.

* check for error message

* one more test

---------

Co-authored-by: lollerfirst <43107113+lollerfirst@users.noreply.github.com>
Co-authored-by: lollerfirst <lollerfirst@gmail.com>
This commit is contained in:
callebtc
2025-11-19 12:21:07 +01:00
committed by GitHub
parent 0041e3a6f2
commit 37446fbeea
8 changed files with 281 additions and 81 deletions

View File

@@ -20,7 +20,7 @@ class NotAllowedError(CashuError):
class OutputsAlreadySignedError(CashuError):
detail = "outputs have already been signed before."
detail = "outputs have already been signed before or are pending."
code = 10002
def __init__(self, detail: Optional[str] = None, code: Optional[int] = None):

View File

@@ -3,6 +3,7 @@ from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional
from ...core.base import (
BlindedMessage,
BlindedSignature,
MeltQuote,
MintKeyset,
@@ -129,7 +130,7 @@ class AuthLedgerCrud(ABC):
) -> None: ...
@abstractmethod
async def get_promise(
async def get_blind_signature(
self,
*,
db: Database,
@@ -138,13 +139,13 @@ class AuthLedgerCrud(ABC):
) -> Optional[BlindedSignature]: ...
@abstractmethod
async def get_promises(
async def get_outputs(
self,
*,
db: Database,
b_s: List[str],
conn: Optional[Connection] = None,
) -> List[BlindedSignature]: ...
) -> List[BlindedMessage]: ...
class AuthLedgerCrudSqlite(AuthLedgerCrud):
@@ -234,7 +235,7 @@ class AuthLedgerCrudSqlite(AuthLedgerCrud):
},
)
async def get_promise(
async def get_blind_signature(
self,
*,
db: Database,
@@ -248,15 +249,15 @@ class AuthLedgerCrudSqlite(AuthLedgerCrud):
""",
{"b_": str(b_)},
)
return BlindedSignature.from_row(row) if row else None
return BlindedSignature.from_row(row) if row else None # type: ignore
async def get_promises(
async def get_outputs(
self,
*,
db: Database,
b_s: List[str],
conn: Optional[Connection] = None,
) -> List[BlindedSignature]:
) -> List[BlindedMessage]:
rows = await (conn or db).fetchall(
f"""
SELECT * from {db.table_with_schema('promises')}
@@ -264,7 +265,7 @@ class AuthLedgerCrudSqlite(AuthLedgerCrud):
""",
{f"b_{i}": b_s[i] for i in range(len(b_s))},
)
return [BlindedSignature.from_row(r) for r in rows] if rows else []
return [BlindedMessage.from_row(r) for r in rows] if rows else []
async def invalidate_proof(
self,
@@ -303,7 +304,7 @@ class AuthLedgerCrudSqlite(AuthLedgerCrud):
SELECT * from {db.table_with_schema('melt_quotes')} WHERE quote in (SELECT DISTINCT melt_quote FROM {db.table_with_schema('proofs_pending')})
"""
)
return [MeltQuote.from_row(r) for r in rows]
return [MeltQuote.from_row(r) for r in rows] if rows else [] # type: ignore
async def get_pending_proofs_for_quote(
self,
@@ -441,7 +442,7 @@ class AuthLedgerCrudSqlite(AuthLedgerCrud):
)
if row is None:
return None
return MintQuote.from_row(row) if row else None
return MintQuote.from_row(row) if row else None # type: ignore
async def get_mint_quote_by_request(
self,
@@ -457,7 +458,7 @@ class AuthLedgerCrudSqlite(AuthLedgerCrud):
""",
{"request": request},
)
return MintQuote.from_row(row) if row else None
return MintQuote.from_row(row) if row else None # type: ignore
async def update_mint_quote(
self,
@@ -549,7 +550,7 @@ class AuthLedgerCrudSqlite(AuthLedgerCrud):
)
if row is None:
return None
return MeltQuote.from_row(row) if row else None
return MeltQuote.from_row(row) if row else None # type: ignore
async def update_melt_quote(
self,

View File

@@ -206,7 +206,7 @@ class LedgerCrud(ABC):
) -> List[BlindedSignature]: ...
@abstractmethod
async def get_promise(
async def get_blind_signature(
self,
*,
db: Database,
@@ -215,13 +215,13 @@ class LedgerCrud(ABC):
) -> Optional[BlindedSignature]: ...
@abstractmethod
async def get_promises(
async def get_outputs(
self,
*,
db: Database,
b_s: List[str],
conn: Optional[Connection] = None,
) -> List[BlindedSignature]: ...
) -> List[BlindedMessage]: ...
@abstractmethod
async def store_mint_quote(
@@ -451,7 +451,7 @@ class LedgerCrudSqlite(LedgerCrud):
},
)
async def get_promise(
async def get_blind_signature(
self,
*,
db: Database,
@@ -467,21 +467,22 @@ class LedgerCrudSqlite(LedgerCrud):
)
return BlindedSignature.from_row(row) if row else None # type: ignore
async def get_promises(
async def get_outputs(
self,
*,
db: Database,
b_s: List[str],
conn: Optional[Connection] = None,
) -> List[BlindedSignature]:
) -> List[BlindedMessage]:
rows = await (conn or db).fetchall(
f"""
SELECT * from {db.table_with_schema('promises')}
WHERE b_ IN ({','.join([f":b_{i}" for i in range(len(b_s))])}) AND c_ IS NOT NULL
WHERE b_ IN ({','.join([f":b_{i}" for i in range(len(b_s))])})
""",
{f"b_{i}": b_s[i] for i in range(len(b_s))},
)
return [BlindedSignature.from_row(r) for r in rows] if rows else [] # type: ignore
# could be unsigned (BlindedMessage) or signed (BlindedSignature), but BlindedMessage is a subclass of BlindedSignature
return [BlindedMessage.from_row(r) for r in rows] if rows else [] # type: ignore
async def invalidate_proof(
self,
@@ -1037,7 +1038,7 @@ class LedgerCrudSqlite(LedgerCrud):
)
return MintBalanceLogEntry.from_row(row) if row else None
async def get_melt_quotes_by_checking_id(
self,
*,
@@ -1050,6 +1051,6 @@ class LedgerCrudSqlite(LedgerCrud):
SELECT * FROM {db.table_with_schema('melt_quotes')}
WHERE checking_id = :checking_id
""",
{"checking_id": checking_id}
{"checking_id": checking_id},
)
return [MeltQuote.from_row(row) for row in results] # type: ignore
return [MeltQuote.from_row(row) for row in results] # type: ignore

View File

@@ -1086,7 +1086,7 @@ class Ledger(
async with self.db.get_connection() as conn:
for output in outputs:
logger.trace(f"looking for promise: {output}")
promise = await self.crud.get_promise(
promise = await self.crud.get_blind_signature(
b_=output.B_, db=self.db, conn=conn
)
if promise is not None:

View File

@@ -133,17 +133,19 @@ class LedgerVerification(
if not self._verify_no_duplicate_outputs(outputs):
raise TransactionDuplicateOutputsError()
# verify that outputs have not been signed previously
signed_before = await self._check_outputs_issued_before(outputs, conn)
signed_before = await self._check_outputs_pending_or_issued_before(
outputs, conn
)
if any(signed_before):
raise OutputsAlreadySignedError()
logger.trace(f"Verified {len(outputs)} outputs.")
async def _check_outputs_issued_before(
async def _check_outputs_pending_or_issued_before(
self,
outputs: List[BlindedMessage],
conn: Optional[Connection] = None,
) -> List[bool]:
"""Checks whether the provided outputs have previously been signed by the mint
"""Checks whether the provided outputs have previously stored (as blinded messages) been signed (as blind signatures) by the mint
(which would lead to a duplication error later when trying to store these outputs again).
Args:
@@ -153,7 +155,7 @@ class LedgerVerification(
result (List[bool]): Whether outputs are already present in the database.
"""
async with self.db.get_connection(conn) as conn:
promises = await self.crud.get_promises(
promises = await self.crud.get_outputs(
b_s=[output.B_ for output in outputs], db=self.db, conn=conn
)
return [True if promise else False for promise in promises]

View File

@@ -390,8 +390,8 @@ async def test_store_and_sign_blinded_message(ledger: Ledger):
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: row is now a full promise and can be read back via get_blind_signature
promise = await ledger.crud.get_blind_signature(db=ledger.db, b_=B_hex)
assert promise is not None
assert promise.amount == amount
assert promise.C_ == C_point.serialize().hex()
@@ -760,9 +760,9 @@ async def test_promises_fk_constraints_enforced(ledger: Ledger):
async def test_concurrent_set_melt_quote_pending_same_checking_id(ledger: Ledger):
"""Test that concurrent attempts to set quotes with same checking_id as pending are handled correctly."""
from cashu.core.base import MeltQuote, MeltQuoteState
checking_id = "test_checking_id_concurrent"
# Create two quotes with the same checking_id
quote1 = MeltQuote(
quote="quote_id_conc_1",
@@ -784,24 +784,24 @@ async def test_concurrent_set_melt_quote_pending_same_checking_id(ledger: Ledger
fee_reserve=2,
state=MeltQuoteState.unpaid,
)
await ledger.crud.store_melt_quote(quote=quote1, db=ledger.db)
await ledger.crud.store_melt_quote(quote=quote2, db=ledger.db)
# Try to set both as pending concurrently
results = await asyncio.gather(
ledger.db_write._set_melt_quote_pending(quote=quote1),
ledger.db_write._set_melt_quote_pending(quote=quote2),
return_exceptions=True
return_exceptions=True,
)
# One should succeed, one should fail
success_count = sum(1 for r in results if isinstance(r, MeltQuote))
error_count = sum(1 for r in results if isinstance(r, Exception))
assert success_count == 1, "Exactly one quote should be set as pending"
assert error_count == 1, "Exactly one should fail"
# The error should be about the quote already being pending
error = next(r for r in results if isinstance(r, Exception))
assert "Melt quote already paid or pending." in str(error)

View File

@@ -4,7 +4,7 @@ import pytest
import pytest_asyncio
from cashu.core.base import MeltQuote, MeltQuoteState, Proof
from cashu.core.errors import LightningPaymentFailedError
from cashu.core.errors import LightningPaymentFailedError, OutputsAlreadySignedError
from cashu.core.models import PostMeltQuoteRequest, PostMintQuoteRequest
from cashu.core.settings import settings
from cashu.lightning.base import PaymentResult
@@ -13,6 +13,7 @@ from cashu.wallet.wallet import Wallet
from tests.conftest import SERVER_ENDPOINT
from tests.helpers import (
get_real_invoice,
is_deprecated_api_only,
is_fake,
is_regtest,
pay_if_regtest,
@@ -85,6 +86,174 @@ async def create_pending_melts(
return pending_proof, quote
@pytest.mark.asyncio
@pytest.mark.skipif(
not is_fake or is_deprecated_api_only,
reason="only fakewallet and non-deprecated api",
)
async def test_pending_melt_quote_outputs_registration_regression(
wallet, ledger: Ledger
):
"""When paying a request results in a PENDING melt quote,
the change outputs should be registered properly
and further requests with the same outputs should result in an expected error.
"""
settings.fakewallet_payment_state = PaymentResult.PENDING.name
settings.fakewallet_pay_invoice_state = PaymentResult.PENDING.name
mint_quote1 = await wallet.request_mint(100)
mint_quote2 = await wallet.request_mint(100)
# await pay_if_regtest(mint_quote1.request)
# await pay_if_regtest(mint_quote2.request)
proofs1 = await wallet.mint(amount=100, quote_id=mint_quote1.quote)
proofs2 = await wallet.mint(amount=100, quote_id=mint_quote2.quote)
invoice_64_sat = "lnbcrt640n1pn0r3tfpp5e30xac756gvd26cn3tgsh8ug6ct555zrvl7vsnma5cwp4g7auq5qdqqcqzzsxqyz5vqsp5xfhtzg0y3mekv6nsdnj43c346smh036t4f8gcfa2zwpxzwcryqvs9qxpqysgqw5juev8y3zxpdu0mvdrced5c6a852f9x7uh57g6fgjgcg5muqzd5474d7xgh770frazel67eejfwelnyr507q46hxqehala880rhlqspw07ta0"
invoice_62_sat = "lnbcrt620n1pn0r3vepp5zljn7g09fsyeahl4rnhuy0xax2puhua5r3gspt7ttlfrley6valqdqqcqzzsxqyz5vqsp577h763sel3q06tfnfe75kvwn5pxn344sd5vnays65f9wfgx4fpzq9qxpqysgqg3re9afz9rwwalytec04pdhf9mvh3e2k4r877tw7dr4g0fvzf9sny5nlfggdy6nduy2dytn06w50ls34qfldgsj37x0ymxam0a687mspp0ytr8"
# Get two melt quotes
melt_quote1 = await wallet.melt_quote(invoice_64_sat)
melt_quote2 = await wallet.melt_quote(invoice_62_sat)
n_change_outputs = 7
(
change_secrets,
change_rs,
change_derivation_paths,
) = await wallet.generate_n_secrets(n_change_outputs, skip_bump=True)
change_outputs, change_rs = wallet._construct_outputs(
n_change_outputs * [1], change_secrets, change_rs
)
response1 = await ledger.melt(
proofs=proofs1, quote=melt_quote1.quote, outputs=change_outputs
)
assert response1.state == "PENDING"
await assert_err(
ledger.melt(
proofs=proofs2,
quote=melt_quote2.quote,
outputs=change_outputs,
),
OutputsAlreadySignedError.detail,
)
# use get_melt_quote to verify that the quote state is updated
melt_quote1_updated = await ledger.get_melt_quote(melt_quote1.quote)
assert melt_quote1_updated.state == MeltQuoteState.pending
melt_quote2_updated = await ledger.get_melt_quote(melt_quote2.quote)
assert melt_quote2_updated.state == MeltQuoteState.unpaid
@pytest.mark.asyncio
@pytest.mark.skipif(
not is_fake or is_deprecated_api_only,
reason="only fakewallet and non-deprecated api",
)
async def test_settled_melt_quote_outputs_registration_regression(
wallet, ledger: Ledger
):
"""Verify that if one melt request fails, we can still use the same outputs in another request"""
settings.fakewallet_payment_state = PaymentResult.FAILED.name
settings.fakewallet_pay_invoice_state = PaymentResult.FAILED.name
mint_quote1 = await wallet.request_mint(100)
mint_quote2 = await wallet.request_mint(100)
# await pay_if_regtest(mint_quote1.request)
# await pay_if_regtest(mint_quote2.request)
proofs1 = await wallet.mint(amount=100, quote_id=mint_quote1.quote)
proofs2 = await wallet.mint(amount=100, quote_id=mint_quote2.quote)
invoice_64_sat = "lnbcrt640n1pn0r3tfpp5e30xac756gvd26cn3tgsh8ug6ct555zrvl7vsnma5cwp4g7auq5qdqqcqzzsxqyz5vqsp5xfhtzg0y3mekv6nsdnj43c346smh036t4f8gcfa2zwpxzwcryqvs9qxpqysgqw5juev8y3zxpdu0mvdrced5c6a852f9x7uh57g6fgjgcg5muqzd5474d7xgh770frazel67eejfwelnyr507q46hxqehala880rhlqspw07ta0"
invoice_62_sat = "lnbcrt620n1pn0r3vepp5zljn7g09fsyeahl4rnhuy0xax2puhua5r3gspt7ttlfrley6valqdqqcqzzsxqyz5vqsp577h763sel3q06tfnfe75kvwn5pxn344sd5vnays65f9wfgx4fpzq9qxpqysgqg3re9afz9rwwalytec04pdhf9mvh3e2k4r877tw7dr4g0fvzf9sny5nlfggdy6nduy2dytn06w50ls34qfldgsj37x0ymxam0a687mspp0ytr8"
# Get two melt quotes
melt_quote1 = await wallet.melt_quote(invoice_64_sat)
melt_quote2 = await wallet.melt_quote(invoice_62_sat)
n_change_outputs = 7
(
change_secrets,
change_rs,
change_derivation_paths,
) = await wallet.generate_n_secrets(n_change_outputs, skip_bump=True)
change_outputs, change_rs = wallet._construct_outputs(
n_change_outputs * [1], change_secrets, change_rs
)
await assert_err(
ledger.melt(proofs=proofs1, quote=melt_quote1.quote, outputs=change_outputs),
"Lightning payment failed.",
)
settings.fakewallet_payment_state = PaymentResult.SETTLED.name
settings.fakewallet_pay_invoice_state = PaymentResult.SETTLED.name
response2 = await ledger.melt(
proofs=proofs2,
quote=melt_quote2.quote,
outputs=change_outputs,
)
assert response2.state == "PAID"
# use get_melt_quote to verify that the quote state is updated
melt_quote2_updated = await ledger.get_melt_quote(melt_quote2.quote)
assert melt_quote2_updated.state == MeltQuoteState.paid
@pytest.mark.asyncio
@pytest.mark.skipif(
not is_fake or is_deprecated_api_only,
reason="only fakewallet and non-deprecated api",
)
async def test_melt_quote_reuse_same_outputs(wallet, ledger: Ledger):
"""Verify that if the same outputs are used in two melt requests,
the second one fails.
"""
settings.fakewallet_payment_state = PaymentResult.SETTLED.name
settings.fakewallet_pay_invoice_state = PaymentResult.SETTLED.name
mint_quote1 = await wallet.request_mint(100)
mint_quote2 = await wallet.request_mint(100)
# await pay_if_regtest(mint_quote1.request)
# await pay_if_regtest(mint_quote2.request)
proofs1 = await wallet.mint(amount=100, quote_id=mint_quote1.quote)
proofs2 = await wallet.mint(amount=100, quote_id=mint_quote2.quote)
invoice_64_sat = "lnbcrt640n1pn0r3tfpp5e30xac756gvd26cn3tgsh8ug6ct555zrvl7vsnma5cwp4g7auq5qdqqcqzzsxqyz5vqsp5xfhtzg0y3mekv6nsdnj43c346smh036t4f8gcfa2zwpxzwcryqvs9qxpqysgqw5juev8y3zxpdu0mvdrced5c6a852f9x7uh57g6fgjgcg5muqzd5474d7xgh770frazel67eejfwelnyr507q46hxqehala880rhlqspw07ta0"
invoice_62_sat = "lnbcrt620n1pn0r3vepp5zljn7g09fsyeahl4rnhuy0xax2puhua5r3gspt7ttlfrley6valqdqqcqzzsxqyz5vqsp577h763sel3q06tfnfe75kvwn5pxn344sd5vnays65f9wfgx4fpzq9qxpqysgqg3re9afz9rwwalytec04pdhf9mvh3e2k4r877tw7dr4g0fvzf9sny5nlfggdy6nduy2dytn06w50ls34qfldgsj37x0ymxam0a687mspp0ytr8"
# Get two melt quotes
melt_quote1 = await wallet.melt_quote(invoice_64_sat)
melt_quote2 = await wallet.melt_quote(invoice_62_sat)
n_change_outputs = 7
(
change_secrets,
change_rs,
change_derivation_paths,
) = await wallet.generate_n_secrets(n_change_outputs, skip_bump=True)
change_outputs, change_rs = wallet._construct_outputs(
n_change_outputs * [1], change_secrets, change_rs
)
(ledger.melt(proofs=proofs1, quote=melt_quote1.quote, outputs=change_outputs),)
await assert_err(
ledger.melt(
proofs=proofs2,
quote=melt_quote2.quote,
outputs=change_outputs,
),
OutputsAlreadySignedError.detail,
)
@pytest.mark.asyncio
@pytest.mark.skipif(is_regtest, reason="only fake wallet")
async def test_fakewallet_pending_quote_get_melt_quote_success(ledger: Ledger):
@@ -379,7 +548,7 @@ async def test_mint_melt_different_units(ledger: Ledger, wallet: Wallet):
async def test_set_melt_quote_pending_without_checking_id(ledger: Ledger):
"""Test that setting a melt quote as pending without a checking_id raises an error."""
from cashu.core.errors import TransactionError
quote = MeltQuote(
quote="quote_id_no_checking",
method="bolt11",
@@ -391,10 +560,10 @@ async def test_set_melt_quote_pending_without_checking_id(ledger: Ledger):
state=MeltQuoteState.unpaid,
)
await ledger.crud.store_melt_quote(quote=quote, db=ledger.db)
# Set checking_id to empty to simulate the error condition
quote.checking_id = ""
try:
await ledger.db_write._set_melt_quote_pending(quote=quote)
raise AssertionError("Expected TransactionError")
@@ -406,9 +575,9 @@ async def test_set_melt_quote_pending_without_checking_id(ledger: Ledger):
async def test_set_melt_quote_pending_prevents_duplicate_checking_id(ledger: Ledger):
"""Test that setting a melt quote as pending fails if another quote with same checking_id is already pending."""
from cashu.core.errors import TransactionError
checking_id = "test_checking_id_duplicate"
quote1 = MeltQuote(
quote="quote_id_dup_first",
method="bolt11",
@@ -429,26 +598,30 @@ async def test_set_melt_quote_pending_prevents_duplicate_checking_id(ledger: Led
fee_reserve=2,
state=MeltQuoteState.unpaid,
)
await ledger.crud.store_melt_quote(quote=quote1, db=ledger.db)
await ledger.crud.store_melt_quote(quote=quote2, db=ledger.db)
# Set the first quote as pending
await ledger.db_write._set_melt_quote_pending(quote=quote1)
# Verify the first quote is pending
quote1_db = await ledger.crud.get_melt_quote(quote_id="quote_id_dup_first", db=ledger.db)
quote1_db = await ledger.crud.get_melt_quote(
quote_id="quote_id_dup_first", db=ledger.db
)
assert quote1_db.state == MeltQuoteState.pending
# Attempt to set the second quote as pending should fail
try:
await ledger.db_write._set_melt_quote_pending(quote=quote2)
raise AssertionError("Expected TransactionError")
except TransactionError as e:
assert "Melt quote already paid or pending." in str(e)
# Verify the second quote is still unpaid
quote2_db = await ledger.crud.get_melt_quote(quote_id="quote_id_dup_second", db=ledger.db)
quote2_db = await ledger.crud.get_melt_quote(
quote_id="quote_id_dup_second", db=ledger.db
)
assert quote2_db.state == MeltQuoteState.unpaid
@@ -457,7 +630,7 @@ async def test_set_melt_quote_pending_allows_different_checking_id(ledger: Ledge
"""Test that setting melt quotes as pending succeeds when they have different checking_ids."""
checking_id_1 = "test_checking_id_allow_1"
checking_id_2 = "test_checking_id_allow_2"
quote1 = MeltQuote(
quote="quote_id_allow_1",
method="bolt11",
@@ -478,17 +651,21 @@ async def test_set_melt_quote_pending_allows_different_checking_id(ledger: Ledge
fee_reserve=2,
state=MeltQuoteState.unpaid,
)
await ledger.crud.store_melt_quote(quote=quote1, db=ledger.db)
await ledger.crud.store_melt_quote(quote=quote2, db=ledger.db)
# Set both quotes as pending - should succeed
await ledger.db_write._set_melt_quote_pending(quote=quote1)
await ledger.db_write._set_melt_quote_pending(quote=quote2)
# Verify both quotes are pending
quote1_db = await ledger.crud.get_melt_quote(quote_id="quote_id_allow_1", db=ledger.db)
quote2_db = await ledger.crud.get_melt_quote(quote_id="quote_id_allow_2", db=ledger.db)
quote1_db = await ledger.crud.get_melt_quote(
quote_id="quote_id_allow_1", db=ledger.db
)
quote2_db = await ledger.crud.get_melt_quote(
quote_id="quote_id_allow_2", db=ledger.db
)
assert quote1_db.state == MeltQuoteState.pending
assert quote2_db.state == MeltQuoteState.pending
@@ -497,7 +674,7 @@ async def test_set_melt_quote_pending_allows_different_checking_id(ledger: Ledge
async def test_set_melt_quote_pending_after_unset(ledger: Ledger):
"""Test that a quote can be set as pending again after being unset."""
checking_id = "test_checking_id_unset_test"
quote1 = MeltQuote(
quote="quote_id_unset_first",
method="bolt11",
@@ -518,28 +695,38 @@ async def test_set_melt_quote_pending_after_unset(ledger: Ledger):
fee_reserve=2,
state=MeltQuoteState.unpaid,
)
await ledger.crud.store_melt_quote(quote=quote1, db=ledger.db)
await ledger.crud.store_melt_quote(quote=quote2, db=ledger.db)
# Set the first quote as pending
quote1_pending = await ledger.db_write._set_melt_quote_pending(quote=quote1)
assert quote1_pending.state == MeltQuoteState.pending
# Unset the first quote (mark as paid)
await ledger.db_write._unset_melt_quote_pending(quote=quote1_pending, state=MeltQuoteState.paid)
await ledger.db_write._unset_melt_quote_pending(
quote=quote1_pending, state=MeltQuoteState.paid
)
# Verify the first quote is no longer pending
quote1_db = await ledger.crud.get_melt_quote(quote_id="quote_id_unset_first", db=ledger.db)
quote1_db = await ledger.crud.get_melt_quote(
quote_id="quote_id_unset_first", db=ledger.db
)
assert quote1_db.state == MeltQuoteState.paid
# Now the second quote should still
assert_err(ledger.db_write._set_melt_quote_pending(quote=quote2), "Melt quote already paid or pending.")
await assert_err(
ledger.db_write._set_melt_quote_pending(quote=quote2),
"Melt quote already paid or pending.",
)
# Verify the second quote is unpaid
quote2_db = await ledger.crud.get_melt_quote(quote_id="quote_id_unset_second", db=ledger.db)
quote2_db = await ledger.crud.get_melt_quote(
quote_id="quote_id_unset_second", db=ledger.db
)
assert quote2_db.state == MeltQuoteState.unpaid
@pytest.mark.asyncio
@pytest.mark.skipif(is_fake, reason="only regtest")
async def test_mint_pay_with_duplicate_checking_id(wallet):
@@ -551,18 +738,26 @@ async def test_mint_pay_with_duplicate_checking_id(wallet):
proofs1 = await wallet.mint(amount=1024, quote_id=mint_quote1.quote)
proofs2 = await wallet.mint(amount=1024, quote_id=mint_quote2.quote)
invoice = get_real_invoice(64)['payment_request']
invoice = get_real_invoice(64)["payment_request"]
# Get two melt quotes for the same invoice
melt_quote1 = await wallet.melt_quote(invoice)
melt_quote2 = await wallet.melt_quote(invoice)
response1 = await wallet.melt(
proofs=proofs1, invoice=invoice, fee_reserve_sat=melt_quote1.fee_reserve, quote_id=melt_quote1.quote
)
assert response1.state == 'PAID'
proofs=proofs1,
invoice=invoice,
fee_reserve_sat=melt_quote1.fee_reserve,
quote_id=melt_quote1.quote,
)
assert response1.state == "PAID"
assert_err(wallet.melt(
proofs=proofs2, invoice=invoice, fee_reserve_sat=melt_quote2.fee_reserve, quote_id=melt_quote2.quote
), "Melt quote already paid or pending.")
assert_err(
wallet.melt(
proofs=proofs2,
invoice=invoice,
fee_reserve_sat=melt_quote2.fee_reserve,
quote_id=melt_quote2.quote,
),
"Melt quote already paid or pending.",
)

View File

@@ -2,6 +2,7 @@ import pytest
import pytest_asyncio
from cashu.core.base import MeltQuoteState, MintQuoteState
from cashu.core.errors import OutputsAlreadySignedError
from cashu.core.helpers import sum_proofs
from cashu.core.models import PostMeltQuoteRequest, PostMintQuoteRequest
from cashu.core.nuts import nut20
@@ -145,7 +146,7 @@ async def test_mint_internal(wallet1: Wallet, ledger: Ledger):
await assert_err(
ledger.mint(outputs=outputs, quote_id=mint_quote.quote),
"outputs have already been signed before.",
OutputsAlreadySignedError.detail,
)
mint_quote_after_payment = await ledger.get_mint_quote(mint_quote.quote)
@@ -294,7 +295,7 @@ async def test_split_twice_with_same_outputs(wallet1: Wallet, ledger: Ledger):
# 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.",
OutputsAlreadySignedError.detail,
)
# try to spend inputs2 again with new outputs
@@ -328,7 +329,7 @@ async def test_mint_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger):
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.",
OutputsAlreadySignedError.detail,
)
@@ -358,7 +359,7 @@ async def test_melt_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger):
)
await assert_err(
ledger.melt(proofs=wallet1.proofs, quote=melt_quote.quote, outputs=outputs),
"outputs have already been signed before.",
OutputsAlreadySignedError.detail,
)