[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

@@ -41,6 +41,9 @@ INVOICE_RESULT_MAP = {
"ACCEPTED": PaymentResult.PENDING,
}
MAX_ROUTE_RETRIES = 50
TEMPORARY_CHANNEL_FAILURE_ERROR = "TEMPORARY_CHANNEL_FAILURE"
class LndRestWallet(LightningBackend):
"""https://api.lightning.community/rest/index.html#lnd-rest-api-reference"""
@@ -219,6 +222,8 @@ class LndRestWallet(LightningBackend):
async def pay_partial_invoice(
self, quote: MeltQuote, amount: Amount, fee_limit_msat: int
) -> PaymentResponse:
attempts = 0
# set the fee limit for the payment
lnrpcFeeLimit = dict()
lnrpcFeeLimit["fixed_msat"] = f"{fee_limit_msat}"
@@ -236,56 +241,78 @@ class LndRestWallet(LightningBackend):
assert payer_addr_tag
payer_addr = str(payer_addr_tag.data)
# get the route
r = await self.client.post(
url=f"/v1/graph/routes/{pubkey}/{amount.to(Unit.sat).amount}",
json={"fee_limit": lnrpcFeeLimit},
timeout=None,
)
# add the mpp_record to the last hop
response: Optional[httpx.Response] = None
route: Optional[httpx.Response] = None
data = r.json()
if r.is_error or data.get("message"):
error_message = data.get("message") or r.text
return PaymentResponse(
result=PaymentResult.FAILED, error_message=error_message
for attempt in range(MAX_ROUTE_RETRIES):
attempts += 1
# get the route
route = await self.client.post(
url=f"/v1/graph/routes/{pubkey}/{amount.to(Unit.sat).amount}",
json={
"fee_limit": lnrpcFeeLimit,
"use_mission_control": True,
},
timeout=None,
)
# We need to set the mpp_record for a partial payment
mpp_record = {
"mpp_record": {
"payment_addr": base64.b64encode(bytes.fromhex(payer_addr)).decode(),
"total_amt_msat": total_amount_msat,
}
}
assert route
# add the mpp_record to the last hop
r = None # type: ignore
for route_nr in range(len(data["routes"])):
logger.debug(f"Trying to pay partial amount with route number {route_nr+1}")
data["routes"][route_nr]["hops"][-1].update(mpp_record)
route_data = route.json()
if route.is_error or route_data.get("message"):
error_message = route_data.get("message") or route.text
return PaymentResponse(
result=PaymentResult.FAILED, error_message=error_message
)
# We need to set the mpp_record for a partial payment
mpp_record = {
"mpp_record": {
"payment_addr": base64.b64encode(
bytes.fromhex(payer_addr)
).decode(),
"total_amt_msat": total_amount_msat,
}
}
route_data["routes"][0]["hops"][-1].update(mpp_record)
# send to route
r = await self.client.post(
response = await self.client.post(
url="/v2/router/route/send",
json={
"payment_hash": base64.b64encode(
bytes.fromhex(invoice.payment_hash)
).decode(),
"route": data["routes"][route_nr],
"route": route_data["routes"][0],
},
timeout=None,
)
data = r.json()
failure = data.get("failure")
if failure:
if failure["code"] == 15:
# Try with a different route
assert response
response_data = response.json()
if response_data.get("status") == "FAILED":
if response_data["failure"]["code"] == TEMPORARY_CHANNEL_FAILURE_ERROR:
# Add the channels that failed to the excluded channels
failure_index = response_data["failure"]["failure_source_index"]
failed_source = route_data["routes"][0]["hops"][failure_index - 1][
"pub_key"
]
failed_dest = route_data["routes"][0]["hops"][failure_index][
"pub_key"
]
logger.debug(
f"Partial payment failed from {failed_source} to {failed_dest} at index {failure_index-1} of the route"
)
continue
break
if r.is_error or data.get("message"):
error_message = data.get("message") or r.text
assert response and route
data = response.json()
if response.is_error or data.get("message") or data.get("status") == "FAILED":
error_message = f"Sending to route failed with code {data.get('failure').get('code')} after {attempts} tries."
return PaymentResponse(
result=PaymentResult.FAILED, error_message=error_message
)
@@ -296,6 +323,11 @@ class LndRestWallet(LightningBackend):
preimage = (
base64.b64decode(data["preimage"]).hex() if data.get("preimage") else None
)
logger.debug(f"Partial payment succeeded after {attempts} different tries!")
logger.debug(
f"Partial payment route length was {len(route.json().get('routes'))} hops."
)
return PaymentResponse(
result=result,
checking_id=checking_id,