Refactor: melt quote unit validation (#489)

* refactor: mint quote validation

* convert amount in Lightning backend for later validation

* fix blink amount return tests

* retry tests

* fix conftest fakewallet

* fix start
This commit is contained in:
callebtc
2024-03-23 01:16:28 +01:00
committed by GitHub
parent 3ba1e81fcb
commit b288a6d50e
12 changed files with 50 additions and 55 deletions

View File

@@ -58,6 +58,8 @@ MINT_DATABASE=data/mint
# Funding source backends
# Supported: FakeWallet, LndRestWallet, CoreLightningRestWallet, BlinkWallet, LNbitsWallet, StrikeUSDWallet
MINT_BACKEND_BOLT11_SAT=FakeWallet
# Only works if a usd derivation path is set
# MINT_BACKEND_BOLT11_SAT=FakeWallet
# for use with LNbitsWallet
MINT_LNBITS_ENDPOINT=https://legend.lnbits.com

View File

@@ -63,7 +63,7 @@ jobs:
MINT_PORT: 3337
MINT_TEST_DATABASE: ${{ inputs.mint-database }}
TOR: false
MINT_LIGHTNING_BACKEND: ${{ inputs.backend-wallet-class }}
MINT_BACKEND_BOLT11_SAT: ${{ inputs.backend-wallet-class }}
MINT_LNBITS_ENDPOINT: http://localhost:5001
MINT_LNBITS_KEY: d08a3313322a4514af75d488bcc27eee
MINT_LND_REST_ENDPOINT: https://localhost:8081/

View File

@@ -444,7 +444,11 @@ class BlinkWallet(LightningBackend):
fees = Amount(unit=Unit.msat, amount=fees_msat)
amount = Amount(unit=Unit.msat, amount=amount_msat)
return PaymentQuoteResponse(checking_id=bolt11, fee=fees, amount=amount)
return PaymentQuoteResponse(
checking_id=bolt11,
fee=fees.to(self.unit, round="up"),
amount=amount.to(self.unit, round="up"),
)
async def main():

View File

@@ -320,5 +320,7 @@ class CoreLightningRestWallet(LightningBackend):
fees = Amount(unit=Unit.msat, amount=fees_msat)
amount = Amount(unit=Unit.msat, amount=amount_msat)
return PaymentQuoteResponse(
checking_id=invoice_obj.payment_hash, fee=fees, amount=amount
checking_id=invoice_obj.payment_hash,
fee=fees.to(self.unit, round="up"),
amount=amount.to(self.unit, round="up"),
)

View File

@@ -169,5 +169,7 @@ class FakeWallet(LightningBackend):
raise NotImplementedError()
return PaymentQuoteResponse(
checking_id=invoice_obj.payment_hash, fee=fees, amount=amount
checking_id=invoice_obj.payment_hash,
fee=fees.to(self.unit, round="up"),
amount=amount.to(self.unit, round="up"),
)

View File

@@ -165,5 +165,7 @@ class LNbitsWallet(LightningBackend):
fees = Amount(unit=Unit.msat, amount=fees_msat)
amount = Amount(unit=Unit.msat, amount=amount_msat)
return PaymentQuoteResponse(
checking_id=invoice_obj.payment_hash, fee=fees, amount=amount
checking_id=invoice_obj.payment_hash,
fee=fees.to(self.unit, round="up"),
amount=amount.to(self.unit, round="up"),
)

View File

@@ -272,5 +272,7 @@ class LndRestWallet(LightningBackend):
fees = Amount(unit=Unit.msat, amount=fees_msat)
amount = Amount(unit=Unit.msat, amount=amount_msat)
return PaymentQuoteResponse(
checking_id=invoice_obj.payment_hash, fee=fees, amount=amount
checking_id=invoice_obj.payment_hash,
fee=fees.to(self.unit, round="up"),
amount=amount.to(self.unit, round="up"),
)

View File

@@ -195,27 +195,3 @@ class StrikeUSDWallet(LightningBackend):
fee_msat=data["details"]["fee"],
preimage=data["preimage"],
)
# async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
# url = f"{self.endpoint}/api/v1/payments/sse"
# while True:
# try:
# async with requests.stream("GET", url) as r:
# async for line in r.aiter_lines():
# if line.startswith("data:"):
# try:
# data = json.loads(line[5:])
# except json.decoder.JSONDecodeError:
# continue
# if type(data) is not dict:
# continue
# yield data["payment_hash"] # payment_hash
# except:
# pass
# print("lost connection to lnbits /payments/sse, retrying in 5 seconds")
# await asyncio.sleep(5)

View File

@@ -480,22 +480,14 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
# NOTE: we normalize the request to lowercase to avoid case sensitivity
# This works with Lightning but might not work with other methods
request = melt_quote.request.lower()
invoice_obj = bolt11.decode(melt_quote.request)
assert invoice_obj.amount_msat, "invoice has no amount."
# check if there is a mint quote with the same payment request
# so that we can handle the transaction internally without lightning
# and respond with zero fees
# so that we would be able to handle the transaction internally
# and therefore respond with internal transaction fees (0 for now)
mint_quote = await self.crud.get_mint_quote_by_request(
request=request, db=self.db
)
if mint_quote:
# internal transaction, validate and return amount from
# associated mint quote and demand zero fees
# assert (
# Amount(unit, mint_quote.amount).to(Unit.msat).amount
# == invoice_obj.amount_msat
# ), "amounts do not match"
assert request == mint_quote.request, "bolt11 requests do not match"
assert mint_quote.unit == melt_quote.unit, "units do not match"
assert mint_quote.method == method.name, "methods do not match"
@@ -512,10 +504,23 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
f" {mint_quote.quote} ({mint_quote.amount} {mint_quote.unit})"
)
else:
# not internal, get quote by backend
# not internal, get payment quote by backend
payment_quote = await self.backends[method][unit].get_payment_quote(request)
assert payment_quote.checking_id, "quote has no checking id"
# make sure the backend returned the amount with a correct unit
assert (
payment_quote.amount.unit == unit
), "payment quote amount units do not match"
# fee from the backend must be in the same unit as the amount
assert (
payment_quote.fee.unit == unit
), "payment quote fee units do not match"
# We assume that the request is a bolt11 invoice, this works since we
# support only the bol11 method for now.
invoice_obj = bolt11.decode(melt_quote.request)
assert invoice_obj.amount_msat, "invoice has no amount."
# we set the expiry of this quote to the expiry of the bolt11 invoice
expiry = None
if invoice_obj.expiry is not None:
expiry = invoice_obj.date + invoice_obj.expiry

View File

@@ -31,7 +31,7 @@ settings.mint_listen_port = SERVER_PORT
settings.mint_url = SERVER_ENDPOINT
settings.tor = False
settings.wallet_unit = "sat"
settings.mint_lightning_backend = settings.mint_lightning_backend or "FakeWallet"
settings.mint_backend_bolt11_sat = settings.mint_backend_bolt11_sat or "FakeWallet"
settings.fakewallet_brr = True
settings.fakewallet_delay_payment = False
settings.fakewallet_stochastic_invoice = False
@@ -111,7 +111,7 @@ async def ledger():
await conn.execute("CREATE SCHEMA public;")
wallets_module = importlib.import_module("cashu.lightning")
lightning_backend = getattr(wallets_module, settings.mint_lightning_backend)()
lightning_backend = getattr(wallets_module, settings.mint_backend_bolt11_sat)()
backends = {
Method.bolt11: {Unit.sat: lightning_backend},
}

View File

@@ -7,7 +7,7 @@ from cashu.core.settings import settings
from cashu.lightning.blink import MINIMUM_FEE_MSAT, BlinkWallet
settings.mint_blink_key = "123"
blink = BlinkWallet()
blink = BlinkWallet(unit=Unit.sat)
payment_request = (
"lnbc10u1pjap7phpp50s9lzr3477j0tvacpfy2ucrs4q0q6cvn232ex7nt2zqxxxj8gxrsdpv2phhwetjv4jzqcneypqyc6t8dp6xu6twva2xjuzzda6qcqzzsxqrrsss"
"p575z0n39w2j7zgnpqtdlrgz9rycner4eptjm3lz363dzylnrm3h4s9qyyssqfz8jglcshnlcf0zkw4qu8fyr564lg59x5al724kms3h6gpuhx9xrfv27tgx3l3u3cyf6"
@@ -194,32 +194,32 @@ async def test_blink_get_payment_quote():
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
quote = await blink.get_payment_quote(payment_request)
assert quote.checking_id == payment_request
assert quote.amount == Amount(Unit.msat, 1000000) # msat
assert quote.fee == Amount(Unit.msat, 5000) # msat
assert quote.amount == Amount(Unit.sat, 1000) # sat
assert quote.fee == Amount(Unit.sat, 5) # sat
# response says 10 sat fees but invoice (1000 sat) * 0.5% is 5 sat so we expect 10 sat
mock_response = {"data": {"lnInvoiceFeeProbe": {"amount": 10}}}
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
quote = await blink.get_payment_quote(payment_request)
assert quote.checking_id == payment_request
assert quote.amount == Amount(Unit.msat, 1000000) # msat
assert quote.fee == Amount(Unit.msat, 10000) # msat
assert quote.amount == Amount(Unit.sat, 1000) # sat
assert quote.fee == Amount(Unit.sat, 10) # sat
# response says 10 sat fees but invoice (4973 sat) * 0.5% is 24.865 sat so we expect 25 sat
mock_response = {"data": {"lnInvoiceFeeProbe": {"amount": 10}}}
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
quote = await blink.get_payment_quote(payment_request_4973)
assert quote.checking_id == payment_request_4973
assert quote.amount == Amount(Unit.msat, 4973000) # msat
assert quote.fee == Amount(Unit.msat, 25000) # msat
assert quote.amount == Amount(Unit.sat, 4973) # sat
assert quote.fee == Amount(Unit.sat, 25) # sat
# response says 0 sat fees but invoice (1 sat) * 0.5% is 0.005 sat so we expect MINIMUM_FEE_MSAT/1000 sat
mock_response = {"data": {"lnInvoiceFeeProbe": {"amount": 0}}}
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
quote = await blink.get_payment_quote(payment_request_1)
assert quote.checking_id == payment_request_1
assert quote.amount == Amount(Unit.msat, 1000) # msat
assert quote.fee == Amount(Unit.msat, MINIMUM_FEE_MSAT) # msat
assert quote.amount == Amount(Unit.sat, 1) # sat
assert quote.fee == Amount(Unit.sat, MINIMUM_FEE_MSAT // 1000) # msat
@respx.mock
@@ -230,5 +230,5 @@ async def test_blink_get_payment_quote_backend_error():
respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response))
quote = await blink.get_payment_quote(payment_request)
assert quote.checking_id == payment_request
assert quote.amount == Amount(Unit.msat, 1000000) # msat
assert quote.fee == Amount(Unit.msat, 5000) # msat
assert quote.amount == Amount(Unit.sat, 1000) # sat
assert quote.fee == Amount(Unit.sat, 5) # sat

View File

@@ -118,7 +118,7 @@ async def test_mint_external(wallet1: Wallet, ledger: Ledger):
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=128, unit="sat"))
mint_quote = await ledger.get_mint_quote(quote.quote)
assert not mint_quote.paid, "mint quote not should be paid"
assert not mint_quote.paid, "mint quote already paid"
await assert_err(
wallet1.mint(128, id=quote.quote),