mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 12:28:22 +00:00
160 lines
5.6 KiB
Python
160 lines
5.6 KiB
Python
from contextlib import asynccontextmanager
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, Mock, patch
|
|
|
|
import pytest
|
|
from cashu.core.base import BlindedSignature, Proof, Unit
|
|
from cashu.core.crypto import b_dhke
|
|
from cashu.core.models import PostMeltQuoteResponse
|
|
from cashu.wallet.v1_api import LedgerAPI
|
|
from cashu.wallet.wallet import Wallet as CashuWallet
|
|
|
|
from routstr.core.settings import settings
|
|
from routstr.mint import MintRateGuard
|
|
from routstr.wallet import _payout_mint_and_unit
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("unit,scale", [("sat", 1), ("msat", 1000)])
|
|
@pytest.mark.parametrize(
|
|
"liability,input_fee,reserve,actual_fee",
|
|
[(0, 0, 0, 0), (0, 7, 10, 3), (300000, 7, 10, 3)],
|
|
)
|
|
async def test_capped_payout_recovers_all_change_with_real_cashu_sdk(
|
|
unit, scale, liability, input_fee, reserve, actual_fee
|
|
):
|
|
MintRateGuard._guards.clear()
|
|
private_key = b_dhke.PrivateKey()
|
|
proof = Proof(
|
|
id="00",
|
|
amount=524288 * scale,
|
|
secret="input-proof",
|
|
C=private_key.public_key.format().hex(),
|
|
)
|
|
w = CashuWallet.__new__(CashuWallet)
|
|
w.url = "https://mint.test"
|
|
w.unit = Unit[unit]
|
|
w.keyset_id = "00"
|
|
w.keysets = {
|
|
"00": SimpleNamespace(
|
|
public_keys={2**i: private_key.public_key for i in range(40)}
|
|
)
|
|
}
|
|
w.proofs = [proof]
|
|
w.db = Mock()
|
|
w.get_fees_for_proofs = Mock(return_value=input_fee * scale)
|
|
w.set_reserved_for_send = AsyncMock()
|
|
w.set_reserved_for_melt = AsyncMock()
|
|
w.sign_proofs_inplace_melt = Mock(side_effect=lambda ps, outputs, quote: ps)
|
|
w._store_proofs = AsyncMock()
|
|
|
|
async def invalidate(ps):
|
|
w.proofs = [p for p in w.proofs if p not in ps]
|
|
|
|
w.invalidate = AsyncMock(side_effect=invalidate)
|
|
w.generate_n_secrets = AsyncMock(
|
|
side_effect=lambda n: (
|
|
[f"change-{i}" for i in range(n)],
|
|
[],
|
|
[f"path-{i}" for i in range(n)],
|
|
)
|
|
)
|
|
quotes = {}
|
|
|
|
async def quote(invoice):
|
|
amount_msat = int(invoice)
|
|
amount = amount_msat // 1000 if unit == "sat" else amount_msat
|
|
q = PostMeltQuoteResponse(
|
|
quote=str(amount),
|
|
amount=amount,
|
|
unit=unit,
|
|
request=invoice,
|
|
fee_reserve=reserve * scale,
|
|
state="UNPAID",
|
|
expiry=None,
|
|
)
|
|
quotes[q.quote] = q
|
|
return q
|
|
|
|
w.melt_quote = AsyncMock(side_effect=quote)
|
|
selected_total = 0
|
|
returned_change = 0
|
|
blank_count = 0
|
|
paid_amount = 0
|
|
|
|
async def mint_melt(quote_id, inputs, outputs):
|
|
nonlocal selected_total, returned_change, blank_count, paid_amount
|
|
q = quotes[quote_id]
|
|
selected_total = sum(p.amount for p in inputs)
|
|
paid_amount = q.amount
|
|
blank_count = len(outputs)
|
|
assert q.fee_reserve == reserve * scale
|
|
change = selected_total - q.amount - (input_fee + actual_fee) * scale
|
|
amounts = [2**i for i in range(change.bit_length()) if change & (2**i)]
|
|
signatures = []
|
|
for amount, output in zip(amounts, outputs):
|
|
blinded, _, _ = b_dhke.step2_bob(
|
|
b_dhke.PublicKey(bytes.fromhex(output.B_)), private_key
|
|
)
|
|
signatures.append(
|
|
BlindedSignature(id="00", amount=amount, C_=blinded.format().hex())
|
|
)
|
|
returned_change = sum(s.amount for s in signatures)
|
|
assert returned_change == change
|
|
return q.model_copy(update={"state": "PAID", "change": signatures})
|
|
|
|
@asynccontextmanager
|
|
async def session():
|
|
yield Mock()
|
|
|
|
with (
|
|
patch.object(settings, "max_payout_sat", 250000),
|
|
patch.object(settings, "min_payout_sat", 210),
|
|
patch("routstr.wallet.get_wallet", AsyncMock(return_value=w)),
|
|
patch("routstr.wallet.get_proofs_per_mint_and_unit", return_value=[proof]),
|
|
patch(
|
|
"routstr.wallet.slow_filter_spend_proofs", AsyncMock(return_value=[proof])
|
|
),
|
|
patch("routstr.wallet.asyncio.sleep", AsyncMock()),
|
|
patch("routstr.wallet.db.create_session", session),
|
|
patch(
|
|
"routstr.wallet.db.total_user_liability",
|
|
AsyncMock(return_value=liability * 1000),
|
|
),
|
|
patch(
|
|
"routstr.payment.lnurl.get_lnurl_data",
|
|
AsyncMock(
|
|
return_value={
|
|
"callback_url": "https://ln.test/cb",
|
|
"min_sendable": 1000,
|
|
"max_sendable": 10**12,
|
|
}
|
|
),
|
|
),
|
|
patch(
|
|
"routstr.payment.lnurl.get_lnurl_invoice",
|
|
AsyncMock(side_effect=lambda callback, amount: (str(amount), {})),
|
|
),
|
|
patch.object(LedgerAPI, "melt", AsyncMock(side_effect=mint_melt)) as transport,
|
|
patch("cashu.wallet.wallet.update_bolt11_melt_quote", AsyncMock()),
|
|
):
|
|
await _payout_mint_and_unit(w.url, unit)
|
|
|
|
transport.assert_awaited_once()
|
|
assert selected_total == 524288 * scale
|
|
assert blank_count > 0
|
|
assert sum(p.amount for p in w.proofs) == returned_change
|
|
assert all(
|
|
b_dhke.verify(private_key, b_dhke.PublicKey(bytes.fromhex(p.C)), p.secret)
|
|
for p in w.proofs
|
|
)
|
|
net_debit = selected_total - returned_change
|
|
assert net_debit == paid_amount + (input_fee + actual_fee) * scale
|
|
assert net_debit <= min(250000, 524288 - liability) * scale
|
|
assert returned_change >= liability * scale
|
|
if liability == input_fee == reserve == actual_fee == 0:
|
|
assert returned_change == 274288 * scale
|
|
assert net_debit == 250000 * scale
|
|
w._store_proofs.assert_awaited_once()
|
|
MintRateGuard._guards.clear()
|