mirror of
https://github.com/aljazceru/nutshell.git
synced 2026-02-01 15:04:18 +01:00
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:
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.",
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user