[PATCH] LND use_mission_control + exclude failing channels (#738)

* lnd_grpc multinut patch

* lndrest multinut patch

* mypy fixes

* fixes non escaped double quotes in error messages formats

* fix

* fix debug log with correct hops number

* correctly escape "hops"

* remove `ignored_pairs` constraint

* Apply suggestions from code review

change some error logs to debug

* add tests and some cleanup

---------

Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
This commit is contained in:
lollerfirst
2025-05-10 15:45:15 +02:00
committed by GitHub
parent 3d21443c3c
commit 619d06f0ab
4 changed files with 240 additions and 78 deletions

View File

@@ -2,17 +2,21 @@ import asyncio
import threading
from typing import List
import bolt11
import pytest
import pytest_asyncio
from cashu.core.base import Method, Proof
from cashu.core.base import MeltQuote, MeltQuoteState, Method, Proof
from cashu.lightning.base import PaymentResponse
from cashu.lightning.clnrest import CLNRestWallet
from cashu.mint.ledger import Ledger
from cashu.wallet.wallet import Wallet
from tests.conftest import SERVER_ENDPOINT
from tests.helpers import (
SLEEP_TIME,
assert_err,
get_real_invoice,
cancel_invoice,
get_hold_invoice,
is_fake,
partial_pay_real_invoice,
pay_if_regtest,
@@ -47,12 +51,12 @@ async def test_regtest_pay_mpp(wallet: Wallet, ledger: Ledger):
assert wallet.balance == 128
# this is the invoice we want to pay in two parts
invoice_dict = get_real_invoice(64)
invoice_payment_request = invoice_dict["payment_request"]
preimage, invoice_dict = get_hold_invoice(64)
invoice_payment_request = str(invoice_dict["payment_request"])
async def _mint_pay_mpp(invoice: str, amount: int, proofs: List[Proof]):
# wallet pays 32 sat of the invoice
quote = await wallet.melt_quote(invoice, amount_msat=amount*1000)
quote = await wallet.melt_quote(invoice, amount_msat=amount * 1000)
assert quote.amount == amount
await wallet.melt(
proofs,
@@ -112,13 +116,15 @@ async def test_regtest_pay_mpp_incomplete_payment(wallet: Wallet, ledger: Ledger
assert wallet.balance == 384
# this is the invoice we want to pay in two parts
invoice_dict = get_real_invoice(64)
invoice_payment_request = invoice_dict["payment_request"]
preimage, invoice_dict = get_hold_invoice(64)
invoice_payment_request = str(invoice_dict["payment_request"])
async def pay_mpp(amount: int, proofs: List[Proof], delay: float = 0.0):
await asyncio.sleep(delay)
# wallet pays 32 sat of the invoice
quote = await wallet.melt_quote(invoice_payment_request, amount_msat=amount*1000)
quote = await wallet.melt_quote(
invoice_payment_request, amount_msat=amount * 1000
)
assert quote.amount == amount
await wallet.melt(
proofs,
@@ -154,5 +160,112 @@ async def test_regtest_internal_mpp_melt_quotes(wallet: Wallet, ledger: Ledger):
# try and create a multi-part melt quote
await assert_err(
wallet.melt_quote(mint_quote.request, 100*1000), "internal mpp not allowed"
wallet.melt_quote(mint_quote.request, 100 * 1000), "internal mpp not allowed"
)
@pytest.mark.asyncio
@pytest.mark.skipif(is_fake, reason="only regtest")
async def test_regtest_pay_mpp_cancel_payment(wallet: Wallet, ledger: Ledger):
# make sure that mpp is supported by the bolt11-sat backend
if not ledger.backends[Method["bolt11"]][wallet.unit].supports_mpp:
pytest.skip("backend does not support mpp")
# make sure wallet knows the backend supports mpp
assert wallet.mint_info.supports_mpp("bolt11", wallet.unit)
# top up wallet so we have enough for the payment
topup_mint_quote = await wallet.request_mint(128)
await pay_if_regtest(topup_mint_quote.request)
proofs1 = await wallet.mint(128, quote_id=topup_mint_quote.quote)
assert wallet.balance == 128
# create a hold invoice that we can cancel
preimage, invoice_dict = get_hold_invoice(64)
invoice_payment_request = str(invoice_dict.get("payment_request", ""))
invoice_obj = bolt11.decode(invoice_payment_request)
payment_hash = invoice_obj.payment_hash
async def _mint_pay_mpp(invoice: str, amount: int, proofs: List[Proof]):
# wallet pays 32 sat of the invoice
quote = await wallet.melt_quote(invoice, amount_msat=amount * 1000)
assert quote.amount == amount
await wallet.melt(
proofs,
invoice,
fee_reserve_sat=quote.fee_reserve,
quote_id=quote.quote,
)
def mint_pay_mpp(invoice: str, amount: int, proofs: List[Proof]):
asyncio.run(_mint_pay_mpp(invoice, amount, proofs))
# start the MPP payment
t1 = threading.Thread(
target=mint_pay_mpp, args=(invoice_payment_request, 32, proofs1)
)
t1.start()
await asyncio.sleep(SLEEP_TIME)
# cancel the invoice
cancel_invoice(payment_hash)
await asyncio.sleep(SLEEP_TIME)
# check the payment status
status = await ledger.backends[Method["bolt11"]][wallet.unit].get_payment_status(
payment_hash
)
assert status.failed # some backends return unknown instead of failed
assert not status.preimage # no preimage since payment failed
# check that the proofs are unspent since payment failed
states = await wallet.check_proof_state(proofs1)
assert all([s.unspent for s in states.states])
@pytest.mark.asyncio
@pytest.mark.skipif(is_fake, reason="only regtest")
async def test_regtest_pay_mpp_cancel_payment_pay_partial_invoice(
wallet: Wallet, ledger: Ledger
):
# create a hold invoice that we can cancel
preimage, invoice_dict = get_hold_invoice(64)
invoice_payment_request = str(invoice_dict.get("payment_request", ""))
invoice_obj = bolt11.decode(invoice_payment_request)
payment_hash = invoice_obj.payment_hash
# Use a shared container to store the result
result_container = []
async def _mint_pay_mpp(invoice: str, amount: int) -> PaymentResponse:
ret = await ledger.backends[Method["bolt11"]][wallet.unit].pay_invoice(
MeltQuote(
request=invoice,
amount=amount,
fee_reserve=0,
quote="",
method="bolt11",
checking_id="",
unit=wallet.unit.name,
state=MeltQuoteState.pending,
),
0,
)
return ret
# Create a wrapper function that will store the result
def thread_func():
result = asyncio.run(_mint_pay_mpp(invoice_payment_request, 32))
result_container.append(result)
t1 = threading.Thread(target=thread_func)
t1.start()
await asyncio.sleep(SLEEP_TIME)
# cancel the invoice
cancel_invoice(payment_hash)
await asyncio.sleep(SLEEP_TIME)
t1.join()
# Get the result from the container
assert result_container[0].failed