mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-21 11:04:19 +01:00
[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:
@@ -46,6 +46,8 @@ INVOICE_RESULT_MAP = {
|
|||||||
lnrpc.Invoice.InvoiceState.ACCEPTED: PaymentResult.PENDING,
|
lnrpc.Invoice.InvoiceState.ACCEPTED: PaymentResult.PENDING,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MAX_ROUTE_RETRIES = 50
|
||||||
|
|
||||||
|
|
||||||
class LndRPCWallet(LightningBackend):
|
class LndRPCWallet(LightningBackend):
|
||||||
supports_mpp = settings.mint_lnd_enable_mpp
|
supports_mpp = settings.mint_lnd_enable_mpp
|
||||||
@@ -206,6 +208,8 @@ class LndRPCWallet(LightningBackend):
|
|||||||
async def pay_partial_invoice(
|
async def pay_partial_invoice(
|
||||||
self, quote: MeltQuote, amount: Amount, fee_limit_msat: int
|
self, quote: MeltQuote, amount: Amount, fee_limit_msat: int
|
||||||
) -> PaymentResponse:
|
) -> PaymentResponse:
|
||||||
|
total_attempts = 0
|
||||||
|
|
||||||
# set the fee limit for the payment
|
# set the fee limit for the payment
|
||||||
feelimit = lnrpc.FeeLimit(fixed_msat=fee_limit_msat)
|
feelimit = lnrpc.FeeLimit(fixed_msat=fee_limit_msat)
|
||||||
invoice = bolt11.decode(quote.request)
|
invoice = bolt11.decode(quote.request)
|
||||||
@@ -223,46 +227,58 @@ class LndRPCWallet(LightningBackend):
|
|||||||
payer_addr = str(payer_addr_tag.data)
|
payer_addr = str(payer_addr_tag.data)
|
||||||
|
|
||||||
# get the route
|
# get the route
|
||||||
r = None
|
response: Optional[lnrpc.HTLCAttempt] = None # type: ignore
|
||||||
|
route: Optional[lnrpc.Rou] = None # type: ignore
|
||||||
try:
|
try:
|
||||||
async with grpc.aio.secure_channel(
|
async with grpc.aio.secure_channel(
|
||||||
self.endpoint, self.combined_creds
|
self.endpoint, self.combined_creds
|
||||||
) as channel:
|
) as channel:
|
||||||
lnstub = lightningstub.LightningStub(channel)
|
lnstub = lightningstub.LightningStub(channel)
|
||||||
router_stub = routerstub.RouterStub(channel)
|
router_stub = routerstub.RouterStub(channel)
|
||||||
r = await lnstub.QueryRoutes(
|
|
||||||
lnrpc.QueryRoutesRequest(
|
for attempt in range(MAX_ROUTE_RETRIES):
|
||||||
pub_key=pubkey,
|
total_attempts += 1
|
||||||
amt=amount.to(Unit.sat).amount,
|
route = await lnstub.QueryRoutes(
|
||||||
fee_limit=feelimit,
|
lnrpc.QueryRoutesRequest(
|
||||||
|
pub_key=pubkey,
|
||||||
|
amt=amount.to(Unit.sat).amount,
|
||||||
|
fee_limit=feelimit,
|
||||||
|
use_mission_control=True,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
assert route
|
||||||
"""
|
logger.debug(
|
||||||
# We need to set the mpp_record for a partial payment
|
f"QueryRoutes returned {len(route.routes)} available routes."
|
||||||
mpp_record = lnrpc.MPPRecord(
|
)
|
||||||
payment_addr=bytes.fromhex(payer_addr),
|
route.routes[0].hops[-1].mpp_record.payment_addr = bytes.fromhex( # type: ignore
|
||||||
total_amt_msat=total_amount_msat,
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
# modify the mpp_record in the last hop
|
|
||||||
for route_nr in range(len(r.routes)):
|
|
||||||
r.routes[route_nr].hops[-1].mpp_record.payment_addr = bytes.fromhex( # type: ignore
|
|
||||||
payer_addr
|
payer_addr
|
||||||
)
|
)
|
||||||
r.routes[route_nr].hops[ # type: ignore
|
route.routes[0].hops[ # type: ignore
|
||||||
-1
|
-1
|
||||||
].mpp_record.total_amt_msat = total_amount_msat
|
].mpp_record.total_amt_msat = total_amount_msat
|
||||||
|
|
||||||
# Send to route request
|
# Send to route request
|
||||||
r = await router_stub.SendToRouteV2(
|
response = await router_stub.SendToRouteV2(
|
||||||
routerrpc.SendToRouteRequest(
|
routerrpc.SendToRouteRequest(
|
||||||
payment_hash=bytes.fromhex(invoice.payment_hash),
|
payment_hash=bytes.fromhex(invoice.payment_hash),
|
||||||
route=r.routes[route_nr], # type: ignore
|
route=route.routes[0], # type: ignore
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if r.status == lnrpc.HTLCAttempt.HTLCStatus.FAILED:
|
assert route and response
|
||||||
if r.failure.code == lnrpc.Failure.FailureCode.TEMPORARY_CHANNEL_FAILURE:
|
if response.status == lnrpc.HTLCAttempt.HTLCStatus.FAILED:
|
||||||
# Try a different route
|
if (
|
||||||
|
response.failure.code
|
||||||
|
== lnrpc.Failure.FailureCode.TEMPORARY_CHANNEL_FAILURE
|
||||||
|
):
|
||||||
|
# Add the channels that failed to the excluded channels
|
||||||
|
failure_index = response.failure.failure_source_index
|
||||||
|
failed_source = (
|
||||||
|
route.routes[0].hops[failure_index - 1].pub_key
|
||||||
|
)
|
||||||
|
failed_dest = route.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
|
continue
|
||||||
break
|
break
|
||||||
except AioRpcError as e:
|
except AioRpcError as e:
|
||||||
@@ -272,25 +288,29 @@ class LndRPCWallet(LightningBackend):
|
|||||||
error_message=str(e),
|
error_message=str(e),
|
||||||
)
|
)
|
||||||
|
|
||||||
if r.status == lnrpc.HTLCAttempt.HTLCStatus.FAILED:
|
assert route and response
|
||||||
error_message = f"Sending to route failed with code {r.failure.code}"
|
if response.status == lnrpc.HTLCAttempt.HTLCStatus.FAILED:
|
||||||
logger.error(error_message)
|
error_message = f"Sending to route failed with code {response.failure.code} after {total_attempts} different tries."
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
result=PaymentResult.FAILED,
|
result=PaymentResult.FAILED,
|
||||||
error_message=error_message,
|
error_message=error_message,
|
||||||
)
|
)
|
||||||
|
|
||||||
result = PaymentResult.UNKNOWN
|
if response.status == lnrpc.HTLCAttempt.HTLCStatus.SUCCEEDED:
|
||||||
if r.status == lnrpc.HTLCAttempt.HTLCStatus.SUCCEEDED:
|
|
||||||
result = PaymentResult.SETTLED
|
result = PaymentResult.SETTLED
|
||||||
elif r.status == lnrpc.HTLCAttempt.HTLCStatus.IN_FLIGHT:
|
elif response.status == lnrpc.HTLCAttempt.HTLCStatus.IN_FLIGHT:
|
||||||
result = PaymentResult.PENDING
|
result = PaymentResult.PENDING
|
||||||
else:
|
else:
|
||||||
result = PaymentResult.FAILED
|
result = PaymentResult.UNKNOWN
|
||||||
|
|
||||||
checking_id = invoice.payment_hash
|
checking_id = invoice.payment_hash
|
||||||
fee_msat = r.route.total_fees_msat
|
fee_msat = response.route.total_fees_msat
|
||||||
preimage = r.preimage.hex()
|
preimage = response.preimage.hex()
|
||||||
|
|
||||||
|
logger.debug(f"Partial payment succeeded after {total_attempts} tries!")
|
||||||
|
logger.debug(
|
||||||
|
f"Partial payment route length was {len(route.routes[0].hops)} hops."
|
||||||
|
)
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
result=result,
|
result=result,
|
||||||
checking_id=checking_id,
|
checking_id=checking_id,
|
||||||
@@ -376,11 +396,7 @@ class LndRPCWallet(LightningBackend):
|
|||||||
self, melt_quote: PostMeltQuoteRequest
|
self, melt_quote: PostMeltQuoteRequest
|
||||||
) -> PaymentQuoteResponse:
|
) -> PaymentQuoteResponse:
|
||||||
# get amount from melt_quote or from bolt11
|
# get amount from melt_quote or from bolt11
|
||||||
amount_msat = (
|
amount_msat = melt_quote.mpp_amount if melt_quote.is_mpp else None
|
||||||
melt_quote.mpp_amount
|
|
||||||
if melt_quote.is_mpp
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
invoice_obj = bolt11.decode(melt_quote.request)
|
invoice_obj = bolt11.decode(melt_quote.request)
|
||||||
assert invoice_obj.amount_msat, "invoice has no amount."
|
assert invoice_obj.amount_msat, "invoice has no amount."
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ INVOICE_RESULT_MAP = {
|
|||||||
"ACCEPTED": PaymentResult.PENDING,
|
"ACCEPTED": PaymentResult.PENDING,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MAX_ROUTE_RETRIES = 50
|
||||||
|
TEMPORARY_CHANNEL_FAILURE_ERROR = "TEMPORARY_CHANNEL_FAILURE"
|
||||||
|
|
||||||
|
|
||||||
class LndRestWallet(LightningBackend):
|
class LndRestWallet(LightningBackend):
|
||||||
"""https://api.lightning.community/rest/index.html#lnd-rest-api-reference"""
|
"""https://api.lightning.community/rest/index.html#lnd-rest-api-reference"""
|
||||||
@@ -219,6 +222,8 @@ class LndRestWallet(LightningBackend):
|
|||||||
async def pay_partial_invoice(
|
async def pay_partial_invoice(
|
||||||
self, quote: MeltQuote, amount: Amount, fee_limit_msat: int
|
self, quote: MeltQuote, amount: Amount, fee_limit_msat: int
|
||||||
) -> PaymentResponse:
|
) -> PaymentResponse:
|
||||||
|
attempts = 0
|
||||||
|
|
||||||
# set the fee limit for the payment
|
# set the fee limit for the payment
|
||||||
lnrpcFeeLimit = dict()
|
lnrpcFeeLimit = dict()
|
||||||
lnrpcFeeLimit["fixed_msat"] = f"{fee_limit_msat}"
|
lnrpcFeeLimit["fixed_msat"] = f"{fee_limit_msat}"
|
||||||
@@ -236,56 +241,78 @@ class LndRestWallet(LightningBackend):
|
|||||||
assert payer_addr_tag
|
assert payer_addr_tag
|
||||||
payer_addr = str(payer_addr_tag.data)
|
payer_addr = str(payer_addr_tag.data)
|
||||||
|
|
||||||
# get the route
|
# add the mpp_record to the last hop
|
||||||
r = await self.client.post(
|
response: Optional[httpx.Response] = None
|
||||||
url=f"/v1/graph/routes/{pubkey}/{amount.to(Unit.sat).amount}",
|
route: Optional[httpx.Response] = None
|
||||||
json={"fee_limit": lnrpcFeeLimit},
|
|
||||||
timeout=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
data = r.json()
|
for attempt in range(MAX_ROUTE_RETRIES):
|
||||||
if r.is_error or data.get("message"):
|
attempts += 1
|
||||||
error_message = data.get("message") or r.text
|
# get the route
|
||||||
return PaymentResponse(
|
route = await self.client.post(
|
||||||
result=PaymentResult.FAILED, error_message=error_message
|
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
|
assert route
|
||||||
mpp_record = {
|
|
||||||
"mpp_record": {
|
|
||||||
"payment_addr": base64.b64encode(bytes.fromhex(payer_addr)).decode(),
|
|
||||||
"total_amt_msat": total_amount_msat,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# add the mpp_record to the last hop
|
route_data = route.json()
|
||||||
r = None # type: ignore
|
if route.is_error or route_data.get("message"):
|
||||||
for route_nr in range(len(data["routes"])):
|
error_message = route_data.get("message") or route.text
|
||||||
logger.debug(f"Trying to pay partial amount with route number {route_nr+1}")
|
return PaymentResponse(
|
||||||
data["routes"][route_nr]["hops"][-1].update(mpp_record)
|
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
|
# send to route
|
||||||
r = await self.client.post(
|
response = await self.client.post(
|
||||||
url="/v2/router/route/send",
|
url="/v2/router/route/send",
|
||||||
json={
|
json={
|
||||||
"payment_hash": base64.b64encode(
|
"payment_hash": base64.b64encode(
|
||||||
bytes.fromhex(invoice.payment_hash)
|
bytes.fromhex(invoice.payment_hash)
|
||||||
).decode(),
|
).decode(),
|
||||||
"route": data["routes"][route_nr],
|
"route": route_data["routes"][0],
|
||||||
},
|
},
|
||||||
timeout=None,
|
timeout=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
data = r.json()
|
assert response
|
||||||
failure = data.get("failure")
|
|
||||||
if failure:
|
response_data = response.json()
|
||||||
if failure["code"] == 15:
|
if response_data.get("status") == "FAILED":
|
||||||
# Try with a different route
|
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
|
continue
|
||||||
break
|
break
|
||||||
|
|
||||||
if r.is_error or data.get("message"):
|
assert response and route
|
||||||
error_message = data.get("message") or r.text
|
|
||||||
|
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(
|
return PaymentResponse(
|
||||||
result=PaymentResult.FAILED, error_message=error_message
|
result=PaymentResult.FAILED, error_message=error_message
|
||||||
)
|
)
|
||||||
@@ -296,6 +323,11 @@ class LndRestWallet(LightningBackend):
|
|||||||
preimage = (
|
preimage = (
|
||||||
base64.b64decode(data["preimage"]).hex() if data.get("preimage") else None
|
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(
|
return PaymentResponse(
|
||||||
result=result,
|
result=result,
|
||||||
checking_id=checking_id,
|
checking_id=checking_id,
|
||||||
|
|||||||
@@ -545,6 +545,7 @@ class Ledger(
|
|||||||
melt_quote.is_mpp
|
melt_quote.is_mpp
|
||||||
and melt_quote.mpp_amount != payment_quote.amount.to(Unit.msat).amount
|
and melt_quote.mpp_amount != payment_quote.amount.to(Unit.msat).amount
|
||||||
):
|
):
|
||||||
|
logger.error(f"expected {payment_quote.amount.to(Unit.msat).amount} msat but got {melt_quote.mpp_amount}")
|
||||||
raise TransactionError("quote amount not as requested")
|
raise TransactionError("quote amount not as requested")
|
||||||
# make sure the backend returned the amount with a correct unit
|
# make sure the backend returned the amount with a correct unit
|
||||||
if not payment_quote.amount.unit == unit:
|
if not payment_quote.amount.unit == unit:
|
||||||
|
|||||||
@@ -2,17 +2,21 @@ import asyncio
|
|||||||
import threading
|
import threading
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
import bolt11
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
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.lightning.clnrest import CLNRestWallet
|
||||||
from cashu.mint.ledger import Ledger
|
from cashu.mint.ledger import Ledger
|
||||||
from cashu.wallet.wallet import Wallet
|
from cashu.wallet.wallet import Wallet
|
||||||
from tests.conftest import SERVER_ENDPOINT
|
from tests.conftest import SERVER_ENDPOINT
|
||||||
from tests.helpers import (
|
from tests.helpers import (
|
||||||
|
SLEEP_TIME,
|
||||||
assert_err,
|
assert_err,
|
||||||
get_real_invoice,
|
cancel_invoice,
|
||||||
|
get_hold_invoice,
|
||||||
is_fake,
|
is_fake,
|
||||||
partial_pay_real_invoice,
|
partial_pay_real_invoice,
|
||||||
pay_if_regtest,
|
pay_if_regtest,
|
||||||
@@ -47,12 +51,12 @@ async def test_regtest_pay_mpp(wallet: Wallet, ledger: Ledger):
|
|||||||
assert wallet.balance == 128
|
assert wallet.balance == 128
|
||||||
|
|
||||||
# this is the invoice we want to pay in two parts
|
# this is the invoice we want to pay in two parts
|
||||||
invoice_dict = get_real_invoice(64)
|
preimage, invoice_dict = get_hold_invoice(64)
|
||||||
invoice_payment_request = invoice_dict["payment_request"]
|
invoice_payment_request = str(invoice_dict["payment_request"])
|
||||||
|
|
||||||
async def _mint_pay_mpp(invoice: str, amount: int, proofs: List[Proof]):
|
async def _mint_pay_mpp(invoice: str, amount: int, proofs: List[Proof]):
|
||||||
# wallet pays 32 sat of the invoice
|
# 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
|
assert quote.amount == amount
|
||||||
await wallet.melt(
|
await wallet.melt(
|
||||||
proofs,
|
proofs,
|
||||||
@@ -112,13 +116,15 @@ async def test_regtest_pay_mpp_incomplete_payment(wallet: Wallet, ledger: Ledger
|
|||||||
assert wallet.balance == 384
|
assert wallet.balance == 384
|
||||||
|
|
||||||
# this is the invoice we want to pay in two parts
|
# this is the invoice we want to pay in two parts
|
||||||
invoice_dict = get_real_invoice(64)
|
preimage, invoice_dict = get_hold_invoice(64)
|
||||||
invoice_payment_request = invoice_dict["payment_request"]
|
invoice_payment_request = str(invoice_dict["payment_request"])
|
||||||
|
|
||||||
async def pay_mpp(amount: int, proofs: List[Proof], delay: float = 0.0):
|
async def pay_mpp(amount: int, proofs: List[Proof], delay: float = 0.0):
|
||||||
await asyncio.sleep(delay)
|
await asyncio.sleep(delay)
|
||||||
# wallet pays 32 sat of the invoice
|
# 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
|
assert quote.amount == amount
|
||||||
await wallet.melt(
|
await wallet.melt(
|
||||||
proofs,
|
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
|
# try and create a multi-part melt quote
|
||||||
await assert_err(
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user