Backend: Unstuck outgoing payments (#857)
* first attempts * lndrest works * fix details * optional fee update * use base64.urlsafe_b64encode * return paymentstatus * CLN: return pending for pending invoices * grpc wip * lndgrpc works * cln: return pending for pending invoices really this time * retry wallet out of exception * wip eclair * take all routines into try except * cliche: return error * rename payment.check_pending() to payment.check_status() * rename payment.check_pending() to payment.check_status() * eclair: works * eclair: better error check * opennode: works * check payment.checking_id istead of payment.ok * payment.ok check as well * cln: works? * cln: works * lntxbot: works * lnbits/wallets/lnpay.py * cln: error handling * make format * lndhub full detail update * spark: wip * error to False * wallets: return clean PaymentResponse * opennode: strict error * cliche: works * lnbits: works * cln: dont throw error * preimage not error * fix cln * do not add duplicate payments * revert cln * extra safety for cln * undo crud changes until tests work * tasks: better logging and 0.5s sleep for regular status check * 0.1 s * check if wallet exists * lnbits unhashed description * remove sleep * revert app.py * cleanup * add check * clean error * readd app.py * fix eclaid
This commit is contained in:
parent
78a98ca97d
commit
2ee10e28c5
18 changed files with 384 additions and 194 deletions
|
|
@ -122,10 +122,10 @@ def check_funding_source(app: FastAPI) -> None:
|
||||||
f"The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'",
|
f"The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'",
|
||||||
RuntimeWarning,
|
RuntimeWarning,
|
||||||
)
|
)
|
||||||
logger.info("Retrying connection to backend in 5 seconds...")
|
|
||||||
await asyncio.sleep(5)
|
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
logger.info("Retrying connection to backend in 5 seconds...")
|
||||||
|
await asyncio.sleep(5)
|
||||||
signal.signal(signal.SIGINT, original_sigint_handler)
|
signal.signal(signal.SIGINT, original_sigint_handler)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"✔️ Backend {WALLET.__class__.__name__} connected and with a balance of {balance} msat."
|
f"✔️ Backend {WALLET.__class__.__name__} connected and with a balance of {balance} msat."
|
||||||
|
|
|
||||||
|
|
@ -365,6 +365,11 @@ async def create_payment(
|
||||||
webhook: Optional[str] = None,
|
webhook: Optional[str] = None,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
|
|
||||||
|
# todo: add this when tests are fixed
|
||||||
|
# previous_payment = await get_wallet_payment(wallet_id, payment_hash, conn=conn)
|
||||||
|
# assert previous_payment is None, "Payment already exists"
|
||||||
|
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO apipayments
|
INSERT INTO apipayments
|
||||||
|
|
@ -404,6 +409,40 @@ async def update_payment_status(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_payment_details(
|
||||||
|
checking_id: str,
|
||||||
|
pending: Optional[bool] = None,
|
||||||
|
fee: Optional[int] = None,
|
||||||
|
preimage: Optional[str] = None,
|
||||||
|
new_checking_id: Optional[str] = None,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
set_clause: List[str] = []
|
||||||
|
set_variables: List[Any] = []
|
||||||
|
|
||||||
|
if new_checking_id is not None:
|
||||||
|
set_clause.append("checking_id = ?")
|
||||||
|
set_variables.append(new_checking_id)
|
||||||
|
if pending is not None:
|
||||||
|
set_clause.append("pending = ?")
|
||||||
|
set_variables.append(pending)
|
||||||
|
if fee is not None:
|
||||||
|
set_clause.append("fee = ?")
|
||||||
|
set_variables.append(fee)
|
||||||
|
if preimage is not None:
|
||||||
|
set_clause.append("preimage = ?")
|
||||||
|
set_variables.append(preimage)
|
||||||
|
|
||||||
|
set_variables.append(checking_id)
|
||||||
|
|
||||||
|
await (conn or db).execute(
|
||||||
|
f"UPDATE apipayments SET {', '.join(set_clause)} WHERE checking_id = ?",
|
||||||
|
tuple(set_variables),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
async def delete_payment(checking_id: str, conn: Optional[Connection] = None) -> None:
|
async def delete_payment(checking_id: str, conn: Optional[Connection] = None) -> None:
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"DELETE FROM apipayments WHERE checking_id = ?", (checking_id,)
|
"DELETE FROM apipayments WHERE checking_id = ?", (checking_id,)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ from pydantic import BaseModel
|
||||||
|
|
||||||
from lnbits.helpers import url_for
|
from lnbits.helpers import url_for
|
||||||
from lnbits.settings import WALLET
|
from lnbits.settings import WALLET
|
||||||
|
from lnbits.wallets.base import PaymentStatus
|
||||||
|
|
||||||
|
|
||||||
class Wallet(BaseModel):
|
class Wallet(BaseModel):
|
||||||
|
|
@ -128,8 +129,16 @@ class Payment(BaseModel):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_uncheckable(self) -> bool:
|
def is_uncheckable(self) -> bool:
|
||||||
return self.checking_id.startswith("temp_") or self.checking_id.startswith(
|
return self.checking_id.startswith("internal_")
|
||||||
"internal_"
|
|
||||||
|
async def update_status(self, status: PaymentStatus) -> None:
|
||||||
|
from .crud import update_payment_details
|
||||||
|
|
||||||
|
await update_payment_details(
|
||||||
|
checking_id=self.checking_id,
|
||||||
|
pending=status.pending,
|
||||||
|
fee=status.fee_msat,
|
||||||
|
preimage=status.preimage,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def set_pending(self, pending: bool) -> None:
|
async def set_pending(self, pending: bool) -> None:
|
||||||
|
|
@ -137,9 +146,9 @@ class Payment(BaseModel):
|
||||||
|
|
||||||
await update_payment_status(self.checking_id, pending)
|
await update_payment_status(self.checking_id, pending)
|
||||||
|
|
||||||
async def check_pending(self) -> None:
|
async def check_status(self) -> PaymentStatus:
|
||||||
if self.is_uncheckable:
|
if self.is_uncheckable:
|
||||||
return
|
return PaymentStatus(None)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Checking {'outgoing' if self.is_out else 'incoming'} pending payment {self.checking_id}"
|
f"Checking {'outgoing' if self.is_out else 'incoming'} pending payment {self.checking_id}"
|
||||||
|
|
@ -153,7 +162,7 @@ class Payment(BaseModel):
|
||||||
logger.debug(f"Status: {status}")
|
logger.debug(f"Status: {status}")
|
||||||
|
|
||||||
if self.is_out and status.failed:
|
if self.is_out and status.failed:
|
||||||
logger.info(
|
logger.warning(
|
||||||
f"Deleting outgoing failed payment {self.checking_id}: {status}"
|
f"Deleting outgoing failed payment {self.checking_id}: {status}"
|
||||||
)
|
)
|
||||||
await self.delete()
|
await self.delete()
|
||||||
|
|
@ -161,7 +170,8 @@ class Payment(BaseModel):
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Marking '{'in' if self.is_in else 'out'}' {self.checking_id} as not pending anymore: {status}"
|
f"Marking '{'in' if self.is_in else 'out'}' {self.checking_id} as not pending anymore: {status}"
|
||||||
)
|
)
|
||||||
await self.set_pending(status.pending)
|
await self.update_status(status)
|
||||||
|
return status
|
||||||
|
|
||||||
async def delete(self) -> None:
|
async def delete(self) -> None:
|
||||||
from .crud import delete_payment
|
from .crud import delete_payment
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,10 @@ from .crud import (
|
||||||
delete_payment,
|
delete_payment,
|
||||||
get_wallet,
|
get_wallet,
|
||||||
get_wallet_payment,
|
get_wallet_payment,
|
||||||
|
update_payment_details,
|
||||||
update_payment_status,
|
update_payment_status,
|
||||||
)
|
)
|
||||||
|
from .models import Payment
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from typing import TypedDict # type: ignore
|
from typing import TypedDict # type: ignore
|
||||||
|
|
@ -101,11 +103,20 @@ async def pay_invoice(
|
||||||
description: str = "",
|
description: str = "",
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
|
"""
|
||||||
|
Pay a Lightning invoice.
|
||||||
|
First, we create a temporary payment in the database with fees set to the reserve fee.
|
||||||
|
We then check whether the balance of the payer would go negative.
|
||||||
|
We then attempt to pay the invoice through the backend.
|
||||||
|
If the payment is successful, we update the payment in the database with the payment details.
|
||||||
|
If the payment is unsuccessful, we delete the temporary payment.
|
||||||
|
If the payment is still in flight, we hope that some other process will regularly check for the payment.
|
||||||
|
"""
|
||||||
invoice = bolt11.decode(payment_request)
|
invoice = bolt11.decode(payment_request)
|
||||||
fee_reserve_msat = fee_reserve(invoice.amount_msat)
|
fee_reserve_msat = fee_reserve(invoice.amount_msat)
|
||||||
async with (db.reuse_conn(conn) if conn else db.connect()) as conn:
|
async with (db.reuse_conn(conn) if conn else db.connect()) as conn:
|
||||||
temp_id = f"temp_{urlsafe_short_hash()}"
|
temp_id = invoice.payment_hash
|
||||||
internal_id = f"internal_{urlsafe_short_hash()}"
|
internal_id = f"internal_{invoice.payment_hash}"
|
||||||
|
|
||||||
if invoice.amount_msat == 0:
|
if invoice.amount_msat == 0:
|
||||||
raise ValueError("Amountless invoices not supported.")
|
raise ValueError("Amountless invoices not supported.")
|
||||||
|
|
@ -185,30 +196,41 @@ async def pay_invoice(
|
||||||
payment: PaymentResponse = await WALLET.pay_invoice(
|
payment: PaymentResponse = await WALLET.pay_invoice(
|
||||||
payment_request, fee_reserve_msat
|
payment_request, fee_reserve_msat
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if payment.checking_id and payment.checking_id != temp_id:
|
||||||
|
logger.warning(
|
||||||
|
f"backend sent unexpected checking_id (expected: {temp_id} got: {payment.checking_id})"
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug(f"backend: pay_invoice finished {temp_id}")
|
logger.debug(f"backend: pay_invoice finished {temp_id}")
|
||||||
if payment.ok and payment.checking_id:
|
if payment.checking_id and payment.ok != False:
|
||||||
logger.debug(f"creating final payment {payment.checking_id}")
|
# payment.ok can be True (paid) or None (pending)!
|
||||||
|
logger.debug(f"updating payment {temp_id}")
|
||||||
async with db.connect() as conn:
|
async with db.connect() as conn:
|
||||||
await create_payment(
|
await update_payment_details(
|
||||||
checking_id=payment.checking_id,
|
checking_id=temp_id,
|
||||||
|
pending=payment.ok != True,
|
||||||
fee=payment.fee_msat,
|
fee=payment.fee_msat,
|
||||||
preimage=payment.preimage,
|
preimage=payment.preimage,
|
||||||
pending=payment.ok == None,
|
new_checking_id=payment.checking_id,
|
||||||
conn=conn,
|
conn=conn,
|
||||||
**payment_kwargs,
|
|
||||||
)
|
)
|
||||||
logger.debug(f"deleting temporary payment {temp_id}")
|
logger.debug(f"payment successful {payment.checking_id}")
|
||||||
await delete_payment(temp_id, conn=conn)
|
elif payment.checking_id is None and payment.ok == False:
|
||||||
else:
|
# payment failed
|
||||||
logger.debug(f"backend payment failed")
|
logger.warning(f"backend sent payment failure")
|
||||||
async with db.connect() as conn:
|
async with db.connect() as conn:
|
||||||
logger.debug(f"deleting temporary payment {temp_id}")
|
logger.debug(f"deleting temporary payment {temp_id}")
|
||||||
await delete_payment(temp_id, conn=conn)
|
await delete_payment(temp_id, conn=conn)
|
||||||
raise PaymentFailure(
|
raise PaymentFailure(
|
||||||
payment.error_message
|
f"payment failed: {payment.error_message}"
|
||||||
or "Payment failed, but backend didn't give us an error message."
|
or "payment failed, but backend didn't give us an error message"
|
||||||
)
|
)
|
||||||
logger.debug(f"payment successful {payment.checking_id}")
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"didn't receive checking_id from backend, payment may be stuck in database: {temp_id}"
|
||||||
|
)
|
||||||
|
|
||||||
return invoice.payment_hash
|
return invoice.payment_hash
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -344,23 +366,16 @@ async def perform_lnurlauth(
|
||||||
async def check_transaction_status(
|
async def check_transaction_status(
|
||||||
wallet_id: str, payment_hash: str, conn: Optional[Connection] = None
|
wallet_id: str, payment_hash: str, conn: Optional[Connection] = None
|
||||||
) -> PaymentStatus:
|
) -> PaymentStatus:
|
||||||
payment = await get_wallet_payment(wallet_id, payment_hash, conn=conn)
|
payment: Optional[Payment] = await get_wallet_payment(
|
||||||
|
wallet_id, payment_hash, conn=conn
|
||||||
|
)
|
||||||
if not payment:
|
if not payment:
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
if payment.is_out:
|
|
||||||
status = await WALLET.get_payment_status(payment.checking_id)
|
|
||||||
else:
|
|
||||||
status = await WALLET.get_invoice_status(payment.checking_id)
|
|
||||||
if not payment.pending:
|
if not payment.pending:
|
||||||
return status
|
# note: before, we still checked the status of the payment again
|
||||||
if payment.is_out and status.failed:
|
return PaymentStatus(True)
|
||||||
logger.info(f"deleting outgoing failed payment {payment.checking_id}: {status}")
|
|
||||||
await payment.delete()
|
status: PaymentStatus = await payment.check_status()
|
||||||
elif not status.pending:
|
|
||||||
logger.info(
|
|
||||||
f"marking '{'in' if payment.is_in else 'out'}' {payment.checking_id} as not pending anymore: {status}"
|
|
||||||
)
|
|
||||||
await payment.set_pending(status.pending)
|
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -402,6 +402,10 @@ async def subscribe(request: Request, wallet: Wallet):
|
||||||
async def api_payments_sse(
|
async def api_payments_sse(
|
||||||
request: Request, wallet: WalletTypeInfo = Depends(get_key_type)
|
request: Request, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||||
):
|
):
|
||||||
|
if wallet is None or wallet.wallet is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Wallet does not exist."
|
||||||
|
)
|
||||||
return EventSourceResponse(
|
return EventSourceResponse(
|
||||||
subscribe(request, wallet.wallet), ping=20, media_type="text/event-stream"
|
subscribe(request, wallet.wallet), ping=20, media_type="text/event-stream"
|
||||||
)
|
)
|
||||||
|
|
@ -436,7 +440,7 @@ async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
|
||||||
return {"paid": True, "preimage": payment.preimage}
|
return {"paid": True, "preimage": payment.preimage}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await payment.check_pending()
|
await payment.check_status()
|
||||||
except Exception:
|
except Exception:
|
||||||
if wallet and wallet.id == payment.wallet_id:
|
if wallet and wallet.id == payment.wallet_id:
|
||||||
return {"paid": False, "details": payment}
|
return {"paid": False, "details": payment}
|
||||||
|
|
|
||||||
|
|
@ -130,9 +130,8 @@ async def lndhub_gettxs(
|
||||||
offset=offset,
|
offset=offset,
|
||||||
exclude_uncheckable=True,
|
exclude_uncheckable=True,
|
||||||
):
|
):
|
||||||
await payment.set_pending(
|
await payment.check_status()
|
||||||
(await WALLET.get_payment_status(payment.checking_id)).pending
|
|
||||||
)
|
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,9 @@ async def check_pending_payments():
|
||||||
incoming = True
|
incoming = True
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
logger.debug(
|
||||||
|
f"Task: checking all pending payments (incoming={incoming}, outgoing={outgoing}) of last 15 days"
|
||||||
|
)
|
||||||
for payment in await get_payments(
|
for payment in await get_payments(
|
||||||
since=(int(time.time()) - 60 * 60 * 24 * 15), # 15 days ago
|
since=(int(time.time()) - 60 * 60 * 24 * 15), # 15 days ago
|
||||||
complete=False,
|
complete=False,
|
||||||
|
|
@ -94,11 +97,14 @@ async def check_pending_payments():
|
||||||
incoming=incoming,
|
incoming=incoming,
|
||||||
exclude_uncheckable=True,
|
exclude_uncheckable=True,
|
||||||
):
|
):
|
||||||
await payment.check_pending()
|
await payment.check_status()
|
||||||
|
logger.debug("Task: pending payments check finished")
|
||||||
# we delete expired invoices once upon the first pending check
|
# we delete expired invoices once upon the first pending check
|
||||||
if incoming:
|
if incoming:
|
||||||
|
logger.debug("Task: deleting all expired invoices")
|
||||||
await delete_expired_invoices()
|
await delete_expired_invoices()
|
||||||
|
logger.debug("Task: expired invoice deletion finished")
|
||||||
|
|
||||||
# after the first check we will only check outgoing, not incoming
|
# after the first check we will only check outgoing, not incoming
|
||||||
# that will be handled by the global invoice listeners, hopefully
|
# that will be handled by the global invoice listeners, hopefully
|
||||||
incoming = False
|
incoming = False
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,15 @@ class PaymentResponse(NamedTuple):
|
||||||
# when ok is None it means we don't know if this succeeded
|
# when ok is None it means we don't know if this succeeded
|
||||||
ok: Optional[bool] = None
|
ok: Optional[bool] = None
|
||||||
checking_id: Optional[str] = None # payment_hash, rcp_id
|
checking_id: Optional[str] = None # payment_hash, rcp_id
|
||||||
fee_msat: int = 0
|
fee_msat: Optional[int] = None
|
||||||
preimage: Optional[str] = None
|
preimage: Optional[str] = None
|
||||||
error_message: Optional[str] = None
|
error_message: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class PaymentStatus(NamedTuple):
|
class PaymentStatus(NamedTuple):
|
||||||
paid: Optional[bool] = None
|
paid: Optional[bool] = None
|
||||||
|
fee_msat: Optional[int] = None
|
||||||
|
preimage: Optional[str] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pending(self) -> bool:
|
def pending(self) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -81,31 +81,41 @@ class ClicheWallet(Wallet):
|
||||||
data["result"]["invoice"],
|
data["result"]["invoice"],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(False, None, None, "Could not get payment hash")
|
||||||
False, checking_id, payment_request, "Could not get payment hash"
|
|
||||||
)
|
|
||||||
|
|
||||||
return InvoiceResponse(True, checking_id, payment_request, error_message)
|
return InvoiceResponse(True, checking_id, payment_request, error_message)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
ws = create_connection(self.endpoint)
|
ws = create_connection(self.endpoint)
|
||||||
ws.send(f"pay-invoice --invoice {bolt11}")
|
ws.send(f"pay-invoice --invoice {bolt11}")
|
||||||
|
for _ in range(2):
|
||||||
r = ws.recv()
|
r = ws.recv()
|
||||||
data = json.loads(r)
|
data = json.loads(r)
|
||||||
checking_id = None
|
checking_id, fee_msat, preimage, error_message, payment_ok = (
|
||||||
error_message = None
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
if data.get("error") is not None and data["error"].get("message"):
|
if data.get("error") is not None:
|
||||||
logger.error(data["error"]["message"])
|
error_message = data["error"].get("message")
|
||||||
error_message = data["error"]["message"]
|
return PaymentResponse(False, None, None, None, error_message)
|
||||||
return PaymentResponse(False, None, 0, error_message)
|
|
||||||
|
|
||||||
if data.get("result") is not None and data["result"].get("payment_hash"):
|
if data.get("method") == "payment_succeeded":
|
||||||
checking_id = data["result"]["payment_hash"]
|
payment_ok = True
|
||||||
else:
|
checking_id = data["params"]["payment_hash"]
|
||||||
return PaymentResponse(False, checking_id, 0, "Could not get payment hash")
|
fee_msat = data["params"]["fee_msatoshi"]
|
||||||
|
preimage = data["params"]["preimage"]
|
||||||
|
continue
|
||||||
|
|
||||||
return PaymentResponse(True, checking_id, 0, error_message)
|
if data.get("result") is None:
|
||||||
|
return PaymentResponse(None)
|
||||||
|
|
||||||
|
return PaymentResponse(
|
||||||
|
payment_ok, checking_id, fee_msat, preimage, error_message
|
||||||
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
ws = create_connection(self.endpoint)
|
ws = create_connection(self.endpoint)
|
||||||
|
|
@ -129,22 +139,30 @@ class ClicheWallet(Wallet):
|
||||||
if data.get("error") is not None and data["error"].get("message"):
|
if data.get("error") is not None and data["error"].get("message"):
|
||||||
logger.error(data["error"]["message"])
|
logger.error(data["error"]["message"])
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
|
payment = data["result"]
|
||||||
statuses = {"pending": None, "complete": True, "failed": False}
|
statuses = {"pending": None, "complete": True, "failed": False}
|
||||||
return PaymentStatus(statuses[data["result"]["status"]])
|
return PaymentStatus(
|
||||||
|
statuses[payment["status"]],
|
||||||
|
payment.get("fee_msatoshi"),
|
||||||
|
payment.get("preimage"),
|
||||||
|
)
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
|
while True:
|
||||||
try:
|
try:
|
||||||
ws = await create_connection(self.endpoint)
|
ws = await create_connection(self.endpoint)
|
||||||
while True:
|
while True:
|
||||||
r = await ws.recv()
|
r = await ws.recv()
|
||||||
data = json.loads(r)
|
data = json.loads(r)
|
||||||
|
print(data)
|
||||||
try:
|
try:
|
||||||
if data["result"]["status"]:
|
if data["result"]["status"]:
|
||||||
yield data["result"]["payment_hash"]
|
yield data["result"]["payment_hash"]
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
except:
|
except Exception as exc:
|
||||||
pass
|
logger.error(
|
||||||
logger.error("lost connection to cliche's websocket, retrying in 5 seconds")
|
f"lost connection to cliche's invoices stream: '{exc}', retrying in 5 seconds"
|
||||||
|
)
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
continue
|
||||||
|
|
|
||||||
|
|
@ -110,29 +110,38 @@ class CoreLightningWallet(Wallet):
|
||||||
|
|
||||||
return InvoiceResponse(True, r["payment_hash"], r["bolt11"], "")
|
return InvoiceResponse(True, r["payment_hash"], r["bolt11"], "")
|
||||||
except RpcError as exc:
|
except RpcError as exc:
|
||||||
error_message = f"lightningd '{exc.method}' failed with '{exc.error}'."
|
error_message = f"CLN method '{exc.method}' failed with '{exc.error.get('message') or exc.error}'."
|
||||||
logger.error("RPC error:", error_message)
|
|
||||||
return InvoiceResponse(False, None, None, error_message)
|
return InvoiceResponse(False, None, None, error_message)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("error:", e)
|
|
||||||
return InvoiceResponse(False, None, None, str(e))
|
return InvoiceResponse(False, None, None, str(e))
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
invoice = lnbits_bolt11.decode(bolt11)
|
invoice = lnbits_bolt11.decode(bolt11)
|
||||||
|
|
||||||
|
previous_payment = await self.get_payment_status(invoice.payment_hash)
|
||||||
|
if previous_payment.paid:
|
||||||
|
return PaymentResponse(False, None, None, None, "invoice already paid")
|
||||||
|
|
||||||
fee_limit_percent = fee_limit_msat / invoice.amount_msat * 100
|
fee_limit_percent = fee_limit_msat / invoice.amount_msat * 100
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"bolt11": bolt11,
|
"bolt11": bolt11,
|
||||||
"maxfeepercent": "{:.11}".format(fee_limit_percent),
|
"maxfeepercent": "{:.11}".format(fee_limit_percent),
|
||||||
"exemptfee": 0, # so fee_limit_percent is applied even on payments with fee under 5000 millisatoshi (which is default value of exemptfee)
|
"exemptfee": 0, # so fee_limit_percent is applied even on payments with fee < 5000 millisatoshi (which is default value of exemptfee)
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
wrapped = async_wrap(_pay_invoice)
|
wrapped = async_wrap(_pay_invoice)
|
||||||
r = await wrapped(self.ln, payload)
|
r = await wrapped(self.ln, payload)
|
||||||
|
except RpcError as exc:
|
||||||
|
try:
|
||||||
|
error_message = exc.error["attempts"][-1]["fail_reason"]
|
||||||
|
except:
|
||||||
|
error_message = f"CLN method '{exc.method}' failed with '{exc.error.get('message') or exc.error}'."
|
||||||
|
return PaymentResponse(False, None, None, None, error_message)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return PaymentResponse(False, None, 0, None, str(exc))
|
return PaymentResponse(False, None, None, None, str(exc))
|
||||||
|
|
||||||
fee_msat = r["msatoshi_sent"] - r["msatoshi"]
|
fee_msat = -int(r["msatoshi_sent"] - r["msatoshi"])
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
True, r["payment_hash"], fee_msat, r["payment_preimage"], None
|
True, r["payment_hash"], fee_msat, r["payment_preimage"], None
|
||||||
)
|
)
|
||||||
|
|
@ -144,9 +153,16 @@ class CoreLightningWallet(Wallet):
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
if not r["invoices"]:
|
if not r["invoices"]:
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
if r["invoices"][0]["payment_hash"] == checking_id:
|
|
||||||
return PaymentStatus(r["invoices"][0]["status"] == "paid")
|
invoice_resp = r["invoices"][-1]
|
||||||
raise KeyError("supplied an invalid checking_id")
|
|
||||||
|
if invoice_resp["payment_hash"] == checking_id:
|
||||||
|
if invoice_resp["status"] == "paid":
|
||||||
|
return PaymentStatus(True)
|
||||||
|
elif invoice_resp["status"] == "unpaid":
|
||||||
|
return PaymentStatus(None)
|
||||||
|
logger.warning(f"supplied an invalid checking_id: {checking_id}")
|
||||||
|
return PaymentStatus(None)
|
||||||
|
|
||||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
try:
|
try:
|
||||||
|
|
@ -155,14 +171,21 @@ class CoreLightningWallet(Wallet):
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
if not r["pays"]:
|
if not r["pays"]:
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
if r["pays"][0]["payment_hash"] == checking_id:
|
payment_resp = r["pays"][-1]
|
||||||
status = r["pays"][0]["status"]
|
|
||||||
|
if payment_resp["payment_hash"] == checking_id:
|
||||||
|
status = payment_resp["status"]
|
||||||
if status == "complete":
|
if status == "complete":
|
||||||
return PaymentStatus(True)
|
fee_msat = -int(
|
||||||
|
payment_resp["amount_sent_msat"] - payment_resp["amount_msat"]
|
||||||
|
)
|
||||||
|
|
||||||
|
return PaymentStatus(True, fee_msat, payment_resp["preimage"])
|
||||||
elif status == "failed":
|
elif status == "failed":
|
||||||
return PaymentStatus(False)
|
return PaymentStatus(False)
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
raise KeyError("supplied an invalid checking_id")
|
logger.warning(f"supplied an invalid checking_id: {checking_id}")
|
||||||
|
return PaymentStatus(None)
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
while True:
|
while True:
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ class EclairWallet(Wallet):
|
||||||
async def status(self) -> StatusResponse:
|
async def status(self) -> StatusResponse:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
r = await client.post(
|
r = await client.post(
|
||||||
f"{self.url}/usablebalances", headers=self.auth, timeout=40
|
f"{self.url}/globalbalance", headers=self.auth, timeout=5
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
@ -60,9 +60,11 @@ class EclairWallet(Wallet):
|
||||||
)
|
)
|
||||||
|
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
return StatusResponse(data["error"], 0)
|
return StatusResponse(data.get("error") or "undefined error", 0)
|
||||||
|
if len(data) == 0:
|
||||||
|
return StatusResponse("no data", 0)
|
||||||
|
|
||||||
return StatusResponse(None, data[0]["canSend"] * 1000)
|
return StatusResponse(None, int(data.get("total") * 100_000_000_000))
|
||||||
|
|
||||||
async def create_invoice(
|
async def create_invoice(
|
||||||
self,
|
self,
|
||||||
|
|
@ -114,13 +116,18 @@ class EclairWallet(Wallet):
|
||||||
except:
|
except:
|
||||||
error_message = r.text
|
error_message = r.text
|
||||||
pass
|
pass
|
||||||
return PaymentResponse(False, None, 0, None, error_message)
|
return PaymentResponse(False, None, None, None, error_message)
|
||||||
|
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
|
if data["type"] == "payment-failed":
|
||||||
|
return PaymentResponse(False, None, None, None, "payment failed")
|
||||||
|
|
||||||
checking_id = data["paymentHash"]
|
checking_id = data["paymentHash"]
|
||||||
preimage = data["paymentPreimage"]
|
preimage = data["paymentPreimage"]
|
||||||
|
|
||||||
|
# We do all this again to get the fee:
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
r = await client.post(
|
r = await client.post(
|
||||||
f"{self.url}/getsentinfo",
|
f"{self.url}/getsentinfo",
|
||||||
|
|
@ -136,15 +143,22 @@ class EclairWallet(Wallet):
|
||||||
except:
|
except:
|
||||||
error_message = r.text
|
error_message = r.text
|
||||||
pass
|
pass
|
||||||
|
return PaymentResponse(None, checking_id, None, preimage, error_message)
|
||||||
|
|
||||||
|
statuses = {
|
||||||
|
"sent": True,
|
||||||
|
"failed": False,
|
||||||
|
"pending": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
data = r.json()[-1]
|
||||||
|
if data["status"]["type"] == "sent":
|
||||||
|
fee_msat = -data["status"]["feesPaid"]
|
||||||
|
preimage = data["status"]["paymentPreimage"]
|
||||||
|
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
True, checking_id, 0, preimage, error_message
|
statuses[data["status"]["type"]], checking_id, fee_msat, preimage, None
|
||||||
) ## ?? is this ok ??
|
)
|
||||||
|
|
||||||
data = r.json()
|
|
||||||
fees = [i["status"] for i in data]
|
|
||||||
fee_msat = sum([i["feesPaid"] for i in fees])
|
|
||||||
|
|
||||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
|
|
@ -155,34 +169,47 @@ class EclairWallet(Wallet):
|
||||||
)
|
)
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
if r.is_error or "error" in data:
|
if r.is_error or "error" in data or data.get("status") is None:
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
|
|
||||||
if data["status"]["type"] != "received":
|
statuses = {
|
||||||
return PaymentStatus(False)
|
"received": True,
|
||||||
|
"expired": False,
|
||||||
return PaymentStatus(True)
|
"pending": None,
|
||||||
|
}
|
||||||
|
return PaymentStatus(statuses.get(data["status"]["type"]))
|
||||||
|
|
||||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
r = await client.post(
|
r = await client.post(
|
||||||
url=f"{self.url}/getsentinfo",
|
f"{self.url}/getsentinfo",
|
||||||
headers=self.auth,
|
headers=self.auth,
|
||||||
data={"paymentHash": checking_id},
|
data={"paymentHash": checking_id},
|
||||||
|
timeout=40,
|
||||||
)
|
)
|
||||||
|
|
||||||
data = r.json()[0]
|
|
||||||
|
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
|
|
||||||
if data["status"]["type"] != "sent":
|
data = r.json()[-1]
|
||||||
return PaymentStatus(False)
|
|
||||||
|
|
||||||
return PaymentStatus(True)
|
if r.is_error or "error" in data or data.get("status") is None:
|
||||||
|
return PaymentStatus(None)
|
||||||
|
|
||||||
|
fee_msat, preimage = None, None
|
||||||
|
if data["status"]["type"] == "sent":
|
||||||
|
fee_msat = -data["status"]["feesPaid"]
|
||||||
|
preimage = data["status"]["paymentPreimage"]
|
||||||
|
|
||||||
|
statuses = {
|
||||||
|
"sent": True,
|
||||||
|
"failed": False,
|
||||||
|
"pending": None,
|
||||||
|
}
|
||||||
|
return PaymentStatus(statuses.get(data["status"]["type"]), fee_msat, preimage)
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
|
while True:
|
||||||
try:
|
try:
|
||||||
async with connect(
|
async with connect(
|
||||||
self.ws_url,
|
self.ws_url,
|
||||||
|
|
@ -195,14 +222,8 @@ class EclairWallet(Wallet):
|
||||||
if message and message["type"] == "payment-received":
|
if message and message["type"] == "payment-received":
|
||||||
yield message["paymentHash"]
|
yield message["paymentHash"]
|
||||||
|
|
||||||
except (
|
except Exception as exc:
|
||||||
OSError,
|
logger.error(
|
||||||
ConnectionClosedOK,
|
f"lost connection to eclair invoices stream: '{exc}', retrying in 5 seconds"
|
||||||
ConnectionClosedError,
|
)
|
||||||
ConnectionClosed,
|
|
||||||
) as ose:
|
|
||||||
logger.error("OSE", ose)
|
|
||||||
pass
|
|
||||||
|
|
||||||
logger.error("lost connection to eclair's websocket, retrying in 5 seconds")
|
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,9 @@ class LNbitsWallet(Wallet):
|
||||||
data: Dict = {"out": False, "amount": amount}
|
data: Dict = {"out": False, "amount": amount}
|
||||||
if description_hash:
|
if description_hash:
|
||||||
data["description_hash"] = description_hash.hex()
|
data["description_hash"] = description_hash.hex()
|
||||||
elif unhashed_description:
|
if unhashed_description:
|
||||||
data["description_hash"] = hashlib.sha256(unhashed_description).hexdigest()
|
data["unhashed_description"] = unhashed_description.hex()
|
||||||
else:
|
|
||||||
data["memo"] = memo or ""
|
data["memo"] = memo or ""
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
|
|
@ -94,15 +94,25 @@ class LNbitsWallet(Wallet):
|
||||||
json={"out": True, "bolt11": bolt11},
|
json={"out": True, "bolt11": bolt11},
|
||||||
timeout=None,
|
timeout=None,
|
||||||
)
|
)
|
||||||
ok, checking_id, fee_msat, error_message = not r.is_error, None, 0, None
|
ok, checking_id, fee_msat, preimage, error_message = (
|
||||||
|
not r.is_error,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
error_message = r.json()["detail"]
|
error_message = r.json()["detail"]
|
||||||
|
return PaymentResponse(None, None, None, None, error_message)
|
||||||
else:
|
else:
|
||||||
data = r.json()
|
data = r.json()
|
||||||
checking_id = data["checking_id"]
|
checking_id = data["payment_hash"]
|
||||||
|
|
||||||
return PaymentResponse(ok, checking_id, fee_msat, error_message)
|
# we do this to get the fee and preimage
|
||||||
|
payment: PaymentStatus = await self.get_payment_status(checking_id)
|
||||||
|
|
||||||
|
return PaymentResponse(ok, checking_id, payment.fee_msat, payment.preimage)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
try:
|
try:
|
||||||
|
|
@ -125,8 +135,11 @@ class LNbitsWallet(Wallet):
|
||||||
|
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
|
data = r.json()
|
||||||
|
if "paid" not in data and "details" not in data:
|
||||||
|
return PaymentStatus(None)
|
||||||
|
|
||||||
return PaymentStatus(r.json()["paid"])
|
return PaymentStatus(data["paid"], data["details"]["fee"], data["preimage"])
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
url = f"{self.endpoint}/api/v1/payments/sse"
|
url = f"{self.endpoint}/api/v1/payments/sse"
|
||||||
|
|
|
||||||
|
|
@ -65,14 +65,32 @@ def get_ssl_context(cert_path: str):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
def parse_checking_id(checking_id: str) -> bytes:
|
def b64_to_bytes(checking_id: str) -> bytes:
|
||||||
return base64.b64decode(checking_id.replace("_", "/"))
|
return base64.b64decode(checking_id.replace("_", "/"))
|
||||||
|
|
||||||
|
|
||||||
def stringify_checking_id(r_hash: bytes) -> str:
|
def bytes_to_b64(r_hash: bytes) -> str:
|
||||||
return base64.b64encode(r_hash).decode("utf-8").replace("/", "_")
|
return base64.b64encode(r_hash).decode("utf-8").replace("/", "_")
|
||||||
|
|
||||||
|
|
||||||
|
def hex_to_b64(hex_str: str) -> str:
|
||||||
|
try:
|
||||||
|
return base64.b64encode(bytes.fromhex(hex_str)).decode()
|
||||||
|
except ValueError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def hex_to_bytes(hex_str: str) -> bytes:
|
||||||
|
try:
|
||||||
|
return bytes.fromhex(hex_str)
|
||||||
|
except:
|
||||||
|
return b""
|
||||||
|
|
||||||
|
|
||||||
|
def bytes_to_hex(b: bytes) -> str:
|
||||||
|
return b.hex()
|
||||||
|
|
||||||
|
|
||||||
# Due to updated ECDSA generated tls.cert we need to let gprc know that
|
# Due to updated ECDSA generated tls.cert we need to let gprc know that
|
||||||
# we need to use that cipher suite otherwise there will be a handhsake
|
# we need to use that cipher suite otherwise there will be a handhsake
|
||||||
# error when we communicate with the lnd rpc server.
|
# error when we communicate with the lnd rpc server.
|
||||||
|
|
@ -153,7 +171,7 @@ class LndWallet(Wallet):
|
||||||
error_message = str(exc)
|
error_message = str(exc)
|
||||||
return InvoiceResponse(False, None, None, error_message)
|
return InvoiceResponse(False, None, None, error_message)
|
||||||
|
|
||||||
checking_id = stringify_checking_id(resp.r_hash)
|
checking_id = bytes_to_hex(resp.r_hash)
|
||||||
payment_request = str(resp.payment_request)
|
payment_request = str(resp.payment_request)
|
||||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||||
|
|
||||||
|
|
@ -168,9 +186,9 @@ class LndWallet(Wallet):
|
||||||
try:
|
try:
|
||||||
resp = await self.routerpc.SendPaymentV2(req).read()
|
resp = await self.routerpc.SendPaymentV2(req).read()
|
||||||
except RpcError as exc:
|
except RpcError as exc:
|
||||||
return PaymentResponse(False, "", 0, None, exc._details)
|
return PaymentResponse(False, None, None, None, exc._details)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return PaymentResponse(False, "", 0, None, str(exc))
|
return PaymentResponse(False, None, None, None, str(exc))
|
||||||
|
|
||||||
# PaymentStatus from https://github.com/lightningnetwork/lnd/blob/master/channeldb/payments.go#L178
|
# PaymentStatus from https://github.com/lightningnetwork/lnd/blob/master/channeldb/payments.go#L178
|
||||||
statuses = {
|
statuses = {
|
||||||
|
|
@ -180,29 +198,31 @@ class LndWallet(Wallet):
|
||||||
3: False, # FAILED
|
3: False, # FAILED
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.status in [0, 1, 3]:
|
fee_msat = None
|
||||||
fee_msat = 0
|
preimage = None
|
||||||
preimage = ""
|
|
||||||
checking_id = ""
|
|
||||||
elif resp.status == 2: # SUCCEEDED
|
|
||||||
fee_msat = resp.htlcs[-1].route.total_fees_msat
|
|
||||||
preimage = resp.payment_preimage
|
|
||||||
checking_id = resp.payment_hash
|
checking_id = resp.payment_hash
|
||||||
|
|
||||||
|
if resp.status: # SUCCEEDED
|
||||||
|
fee_msat = -resp.htlcs[-1].route.total_fees_msat
|
||||||
|
preimage = bytes_to_hex(resp.payment_preimage)
|
||||||
|
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
statuses[resp.status], checking_id, fee_msat, preimage, None
|
statuses[resp.status], checking_id, fee_msat, preimage, None
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
try:
|
try:
|
||||||
r_hash = parse_checking_id(checking_id)
|
r_hash = hex_to_bytes(checking_id)
|
||||||
if len(r_hash) != 32:
|
if len(r_hash) != 32:
|
||||||
raise binascii.Error
|
raise binascii.Error
|
||||||
except binascii.Error:
|
except binascii.Error:
|
||||||
# this may happen if we switch between backend wallets
|
# this may happen if we switch between backend wallets
|
||||||
# that use different checking_id formats
|
# that use different checking_id formats
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
|
try:
|
||||||
resp = await self.rpc.LookupInvoice(ln.PaymentHash(r_hash=r_hash))
|
resp = await self.rpc.LookupInvoice(ln.PaymentHash(r_hash=r_hash))
|
||||||
|
except RpcError as exc:
|
||||||
|
return PaymentStatus(None)
|
||||||
if resp.settled:
|
if resp.settled:
|
||||||
return PaymentStatus(True)
|
return PaymentStatus(True)
|
||||||
|
|
||||||
|
|
@ -213,7 +233,7 @@ class LndWallet(Wallet):
|
||||||
This routine checks the payment status using routerpc.TrackPaymentV2.
|
This routine checks the payment status using routerpc.TrackPaymentV2.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
r_hash = parse_checking_id(checking_id)
|
r_hash = hex_to_bytes(checking_id)
|
||||||
if len(r_hash) != 32:
|
if len(r_hash) != 32:
|
||||||
raise binascii.Error
|
raise binascii.Error
|
||||||
except binascii.Error:
|
except binascii.Error:
|
||||||
|
|
@ -221,11 +241,6 @@ class LndWallet(Wallet):
|
||||||
# that use different checking_id formats
|
# that use different checking_id formats
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
|
|
||||||
# for some reason our checking_ids are in base64 but the payment hashes
|
|
||||||
# returned here are in hex, lnd is weird
|
|
||||||
checking_id = checking_id.replace("_", "/")
|
|
||||||
checking_id = base64.b64decode(checking_id).hex()
|
|
||||||
|
|
||||||
resp = self.routerpc.TrackPaymentV2(
|
resp = self.routerpc.TrackPaymentV2(
|
||||||
router.TrackPaymentRequest(payment_hash=r_hash)
|
router.TrackPaymentRequest(payment_hash=r_hash)
|
||||||
)
|
)
|
||||||
|
|
@ -240,6 +255,12 @@ class LndWallet(Wallet):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async for payment in resp:
|
async for payment in resp:
|
||||||
|
if statuses[payment.htlcs[-1].status]:
|
||||||
|
return PaymentStatus(
|
||||||
|
True,
|
||||||
|
-payment.htlcs[-1].route.total_fees_msat,
|
||||||
|
bytes_to_hex(payment.htlcs[-1].preimage),
|
||||||
|
)
|
||||||
return PaymentStatus(statuses[payment.htlcs[-1].status])
|
return PaymentStatus(statuses[payment.htlcs[-1].status])
|
||||||
except: # most likely the payment wasn't found
|
except: # most likely the payment wasn't found
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
|
|
@ -248,13 +269,13 @@ class LndWallet(Wallet):
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
while True:
|
while True:
|
||||||
request = ln.InvoiceSubscription()
|
|
||||||
try:
|
try:
|
||||||
|
request = ln.InvoiceSubscription()
|
||||||
async for i in self.rpc.SubscribeInvoices(request):
|
async for i in self.rpc.SubscribeInvoices(request):
|
||||||
if not i.settled:
|
if not i.settled:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
checking_id = stringify_checking_id(i.r_hash)
|
checking_id = bytes_to_hex(i.r_hash)
|
||||||
yield checking_id
|
yield checking_id
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error(
|
logger.error(
|
||||||
|
|
|
||||||
|
|
@ -123,18 +123,15 @@ class LndRestWallet(Wallet):
|
||||||
|
|
||||||
if r.is_error or r.json().get("payment_error"):
|
if r.is_error or r.json().get("payment_error"):
|
||||||
error_message = r.json().get("payment_error") or r.text
|
error_message = r.json().get("payment_error") or r.text
|
||||||
return PaymentResponse(False, None, 0, None, error_message)
|
return PaymentResponse(False, None, None, None, error_message)
|
||||||
|
|
||||||
data = r.json()
|
data = r.json()
|
||||||
payment_hash = data["payment_hash"]
|
checking_id = base64.b64decode(data["payment_hash"]).hex()
|
||||||
checking_id = payment_hash
|
|
||||||
fee_msat = int(data["payment_route"]["total_fees_msat"])
|
fee_msat = int(data["payment_route"]["total_fees_msat"])
|
||||||
preimage = base64.b64decode(data["payment_preimage"]).hex()
|
preimage = base64.b64decode(data["payment_preimage"]).hex()
|
||||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
checking_id = checking_id.replace("_", "/")
|
|
||||||
|
|
||||||
async with httpx.AsyncClient(verify=self.cert) as client:
|
async with httpx.AsyncClient(verify=self.cert) as client:
|
||||||
r = await client.get(
|
r = await client.get(
|
||||||
url=f"{self.endpoint}/v1/invoice/{checking_id}", headers=self.auth
|
url=f"{self.endpoint}/v1/invoice/{checking_id}", headers=self.auth
|
||||||
|
|
@ -151,10 +148,18 @@ class LndRestWallet(Wallet):
|
||||||
"""
|
"""
|
||||||
This routine checks the payment status using routerpc.TrackPaymentV2.
|
This routine checks the payment status using routerpc.TrackPaymentV2.
|
||||||
"""
|
"""
|
||||||
|
# convert checking_id from hex to base64 and some LND magic
|
||||||
|
try:
|
||||||
|
checking_id = base64.urlsafe_b64encode(bytes.fromhex(checking_id)).decode(
|
||||||
|
"ascii"
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
return PaymentStatus(None)
|
||||||
|
|
||||||
url = f"{self.endpoint}/v2/router/track/{checking_id}"
|
url = f"{self.endpoint}/v2/router/track/{checking_id}"
|
||||||
|
|
||||||
# check payment.status:
|
# check payment.status:
|
||||||
# https://api.lightning.community/rest/index.html?python#peersynctype
|
# https://api.lightning.community/?python=#paymentpaymentstatus
|
||||||
statuses = {
|
statuses = {
|
||||||
"UNKNOWN": None,
|
"UNKNOWN": None,
|
||||||
"IN_FLIGHT": None,
|
"IN_FLIGHT": None,
|
||||||
|
|
@ -178,7 +183,11 @@ class LndRestWallet(Wallet):
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
payment = line.get("result")
|
payment = line.get("result")
|
||||||
if payment is not None and payment.get("status"):
|
if payment is not None and payment.get("status"):
|
||||||
return PaymentStatus(statuses[payment["status"]])
|
return PaymentStatus(
|
||||||
|
paid=statuses[payment["status"]],
|
||||||
|
fee_msat=payment.get("fee_msat"),
|
||||||
|
preimage=payment.get("payment_preimage"),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
except:
|
except:
|
||||||
|
|
@ -187,10 +196,9 @@ class LndRestWallet(Wallet):
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
url = self.endpoint + "/v1/invoices/subscribe"
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
url = self.endpoint + "/v1/invoices/subscribe"
|
||||||
async with httpx.AsyncClient(
|
async with httpx.AsyncClient(
|
||||||
timeout=None, headers=self.auth, verify=self.cert
|
timeout=None, headers=self.auth, verify=self.cert
|
||||||
) as client:
|
) as client:
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ class LNPayWallet(Wallet):
|
||||||
)
|
)
|
||||||
|
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
return PaymentResponse(False, None, 0, None, data["message"])
|
return PaymentResponse(False, None, None, None, data["message"])
|
||||||
|
|
||||||
checking_id = data["lnTx"]["id"]
|
checking_id = data["lnTx"]["id"]
|
||||||
fee_msat = 0
|
fee_msat = 0
|
||||||
|
|
@ -113,15 +113,18 @@ class LNPayWallet(Wallet):
|
||||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
r = await client.get(
|
r = await client.get(
|
||||||
url=f"{self.endpoint}/lntx/{checking_id}?fields=settled",
|
url=f"{self.endpoint}/lntx/{checking_id}",
|
||||||
headers=self.auth,
|
headers=self.auth,
|
||||||
)
|
)
|
||||||
|
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
|
|
||||||
|
data = r.json()
|
||||||
|
preimage = data["payment_preimage"]
|
||||||
|
fee_msat = data["fee_msat"]
|
||||||
statuses = {0: None, 1: True, -1: False}
|
statuses = {0: None, 1: True, -1: False}
|
||||||
return PaymentStatus(statuses[r.json()["settled"]])
|
return PaymentStatus(statuses[data["settled"]], fee_msat, preimage)
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
self.queue: asyncio.Queue = asyncio.Queue(0)
|
self.queue: asyncio.Queue = asyncio.Queue(0)
|
||||||
|
|
|
||||||
|
|
@ -97,10 +97,11 @@ class LntxbotWallet(Wallet):
|
||||||
except:
|
except:
|
||||||
error_message = r.text
|
error_message = r.text
|
||||||
pass
|
pass
|
||||||
|
return PaymentResponse(False, None, None, None, error_message)
|
||||||
return PaymentResponse(False, None, 0, None, error_message)
|
|
||||||
|
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
if data.get("type") != "paid_invoice":
|
||||||
|
return PaymentResponse(None)
|
||||||
checking_id = data["payment_hash"]
|
checking_id = data["payment_hash"]
|
||||||
fee_msat = -data["fee_msat"]
|
fee_msat = -data["fee_msat"]
|
||||||
preimage = data["payment_preimage"]
|
preimage = data["payment_preimage"]
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ class OpenNodeWallet(Wallet):
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
return StatusResponse(data["message"], 0)
|
return StatusResponse(data["message"], 0)
|
||||||
|
|
||||||
return StatusResponse(None, data["balance"]["BTC"] / 100_000_000_000)
|
return StatusResponse(None, data["balance"]["BTC"] * 1000)
|
||||||
|
|
||||||
async def create_invoice(
|
async def create_invoice(
|
||||||
self,
|
self,
|
||||||
|
|
@ -92,11 +92,15 @@ class OpenNodeWallet(Wallet):
|
||||||
|
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
error_message = r.json()["message"]
|
error_message = r.json()["message"]
|
||||||
return PaymentResponse(False, None, 0, None, error_message)
|
return PaymentResponse(False, None, None, None, error_message)
|
||||||
|
|
||||||
data = r.json()["data"]
|
data = r.json()["data"]
|
||||||
checking_id = data["id"]
|
checking_id = data["id"]
|
||||||
fee_msat = data["fee"] * 1000
|
fee_msat = -data["fee"] * 1000
|
||||||
|
|
||||||
|
if data["status"] != "paid":
|
||||||
|
return PaymentResponse(None, checking_id, fee_msat, None, "payment failed")
|
||||||
|
|
||||||
return PaymentResponse(True, checking_id, fee_msat, None, None)
|
return PaymentResponse(True, checking_id, fee_msat, None, None)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
|
|
@ -106,9 +110,9 @@ class OpenNodeWallet(Wallet):
|
||||||
)
|
)
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
|
data = r.json()["data"]
|
||||||
statuses = {"processing": None, "paid": True, "unpaid": False}
|
statuses = {"processing": None, "paid": True, "unpaid": None}
|
||||||
return PaymentStatus(statuses[r.json()["data"]["status"]])
|
return PaymentStatus(statuses[data.get("status")])
|
||||||
|
|
||||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
|
|
@ -119,14 +123,16 @@ class OpenNodeWallet(Wallet):
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
|
|
||||||
|
data = r.json()["data"]
|
||||||
statuses = {
|
statuses = {
|
||||||
"initial": None,
|
"initial": None,
|
||||||
"pending": None,
|
"pending": None,
|
||||||
"confirmed": True,
|
"confirmed": True,
|
||||||
"error": False,
|
"error": None,
|
||||||
"failed": False,
|
"failed": False,
|
||||||
}
|
}
|
||||||
return PaymentStatus(statuses[r.json()["data"]["status"]])
|
fee_msat = -data.get("fee") * 1000
|
||||||
|
return PaymentStatus(statuses[data.get("status")], fee_msat)
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
self.queue: asyncio.Queue = asyncio.Queue(0)
|
self.queue: asyncio.Queue = asyncio.Queue(0)
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ class SparkWallet(Wallet):
|
||||||
pays = listpays["pays"]
|
pays = listpays["pays"]
|
||||||
|
|
||||||
if len(pays) == 0:
|
if len(pays) == 0:
|
||||||
return PaymentResponse(False, None, 0, None, str(exc))
|
return PaymentResponse(False, None, None, None, str(exc))
|
||||||
|
|
||||||
pay = pays[0]
|
pay = pays[0]
|
||||||
payment_hash = pay["payment_hash"]
|
payment_hash = pay["payment_hash"]
|
||||||
|
|
@ -148,11 +148,9 @@ class SparkWallet(Wallet):
|
||||||
)
|
)
|
||||||
|
|
||||||
if pay["status"] == "failed":
|
if pay["status"] == "failed":
|
||||||
return PaymentResponse(False, None, 0, None, str(exc))
|
return PaymentResponse(False, None, None, None, str(exc))
|
||||||
elif pay["status"] == "pending":
|
elif pay["status"] == "pending":
|
||||||
return PaymentResponse(
|
return PaymentResponse(None, payment_hash, None, None, None)
|
||||||
None, payment_hash, fee_limit_msat, None, None
|
|
||||||
)
|
|
||||||
elif pay["status"] == "complete":
|
elif pay["status"] == "complete":
|
||||||
r = pay
|
r = pay
|
||||||
r["payment_preimage"] = pay["preimage"]
|
r["payment_preimage"] = pay["preimage"]
|
||||||
|
|
@ -163,7 +161,7 @@ class SparkWallet(Wallet):
|
||||||
# this is good
|
# this is good
|
||||||
pass
|
pass
|
||||||
|
|
||||||
fee_msat = r["msatoshi_sent"] - r["msatoshi"]
|
fee_msat = -int(r["msatoshi_sent"] - r["msatoshi"])
|
||||||
preimage = r["payment_preimage"]
|
preimage = r["payment_preimage"]
|
||||||
return PaymentResponse(True, r["payment_hash"], fee_msat, preimage, None)
|
return PaymentResponse(True, r["payment_hash"], fee_msat, preimage, None)
|
||||||
|
|
||||||
|
|
@ -201,7 +199,10 @@ class SparkWallet(Wallet):
|
||||||
if r["pays"][0]["payment_hash"] == checking_id:
|
if r["pays"][0]["payment_hash"] == checking_id:
|
||||||
status = r["pays"][0]["status"]
|
status = r["pays"][0]["status"]
|
||||||
if status == "complete":
|
if status == "complete":
|
||||||
return PaymentStatus(True)
|
fee_msat = -int(
|
||||||
|
r["pays"][0]["amount_sent_msat"] - r["pays"][0]["amount_msat"]
|
||||||
|
)
|
||||||
|
return PaymentStatus(True, fee_msat, r["pays"][0]["preimage"])
|
||||||
elif status == "failed":
|
elif status == "failed":
|
||||||
return PaymentStatus(False)
|
return PaymentStatus(False)
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue