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}'",
|
||||
RuntimeWarning,
|
||||
)
|
||||
logger.info("Retrying connection to backend in 5 seconds...")
|
||||
await asyncio.sleep(5)
|
||||
except:
|
||||
pass
|
||||
logger.info("Retrying connection to backend in 5 seconds...")
|
||||
await asyncio.sleep(5)
|
||||
signal.signal(signal.SIGINT, original_sigint_handler)
|
||||
logger.info(
|
||||
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,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> 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(
|
||||
"""
|
||||
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:
|
||||
await (conn or db).execute(
|
||||
"DELETE FROM apipayments WHERE checking_id = ?", (checking_id,)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from pydantic import BaseModel
|
|||
|
||||
from lnbits.helpers import url_for
|
||||
from lnbits.settings import WALLET
|
||||
from lnbits.wallets.base import PaymentStatus
|
||||
|
||||
|
||||
class Wallet(BaseModel):
|
||||
|
|
@ -128,8 +129,16 @@ class Payment(BaseModel):
|
|||
|
||||
@property
|
||||
def is_uncheckable(self) -> bool:
|
||||
return self.checking_id.startswith("temp_") or self.checking_id.startswith(
|
||||
"internal_"
|
||||
return self.checking_id.startswith("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:
|
||||
|
|
@ -137,9 +146,9 @@ class Payment(BaseModel):
|
|||
|
||||
await update_payment_status(self.checking_id, pending)
|
||||
|
||||
async def check_pending(self) -> None:
|
||||
async def check_status(self) -> PaymentStatus:
|
||||
if self.is_uncheckable:
|
||||
return
|
||||
return PaymentStatus(None)
|
||||
|
||||
logger.debug(
|
||||
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}")
|
||||
|
||||
if self.is_out and status.failed:
|
||||
logger.info(
|
||||
logger.warning(
|
||||
f"Deleting outgoing failed payment {self.checking_id}: {status}"
|
||||
)
|
||||
await self.delete()
|
||||
|
|
@ -161,7 +170,8 @@ class Payment(BaseModel):
|
|||
logger.info(
|
||||
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:
|
||||
from .crud import delete_payment
|
||||
|
|
|
|||
|
|
@ -31,8 +31,10 @@ from .crud import (
|
|||
delete_payment,
|
||||
get_wallet,
|
||||
get_wallet_payment,
|
||||
update_payment_details,
|
||||
update_payment_status,
|
||||
)
|
||||
from .models import Payment
|
||||
|
||||
try:
|
||||
from typing import TypedDict # type: ignore
|
||||
|
|
@ -101,11 +103,20 @@ async def pay_invoice(
|
|||
description: str = "",
|
||||
conn: Optional[Connection] = None,
|
||||
) -> 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)
|
||||
fee_reserve_msat = fee_reserve(invoice.amount_msat)
|
||||
async with (db.reuse_conn(conn) if conn else db.connect()) as conn:
|
||||
temp_id = f"temp_{urlsafe_short_hash()}"
|
||||
internal_id = f"internal_{urlsafe_short_hash()}"
|
||||
temp_id = invoice.payment_hash
|
||||
internal_id = f"internal_{invoice.payment_hash}"
|
||||
|
||||
if invoice.amount_msat == 0:
|
||||
raise ValueError("Amountless invoices not supported.")
|
||||
|
|
@ -185,30 +196,41 @@ async def pay_invoice(
|
|||
payment: PaymentResponse = await WALLET.pay_invoice(
|
||||
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}")
|
||||
if payment.ok and payment.checking_id:
|
||||
logger.debug(f"creating final payment {payment.checking_id}")
|
||||
if payment.checking_id and payment.ok != False:
|
||||
# payment.ok can be True (paid) or None (pending)!
|
||||
logger.debug(f"updating payment {temp_id}")
|
||||
async with db.connect() as conn:
|
||||
await create_payment(
|
||||
checking_id=payment.checking_id,
|
||||
await update_payment_details(
|
||||
checking_id=temp_id,
|
||||
pending=payment.ok != True,
|
||||
fee=payment.fee_msat,
|
||||
preimage=payment.preimage,
|
||||
pending=payment.ok == None,
|
||||
new_checking_id=payment.checking_id,
|
||||
conn=conn,
|
||||
**payment_kwargs,
|
||||
)
|
||||
logger.debug(f"deleting temporary payment {temp_id}")
|
||||
await delete_payment(temp_id, conn=conn)
|
||||
else:
|
||||
logger.debug(f"backend payment failed")
|
||||
logger.debug(f"payment successful {payment.checking_id}")
|
||||
elif payment.checking_id is None and payment.ok == False:
|
||||
# payment failed
|
||||
logger.warning(f"backend sent payment failure")
|
||||
async with db.connect() as conn:
|
||||
logger.debug(f"deleting temporary payment {temp_id}")
|
||||
await delete_payment(temp_id, conn=conn)
|
||||
raise PaymentFailure(
|
||||
payment.error_message
|
||||
or "Payment failed, but backend didn't give us an error message."
|
||||
f"payment failed: {payment.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
|
||||
|
||||
|
||||
|
|
@ -344,23 +366,16 @@ async def perform_lnurlauth(
|
|||
async def check_transaction_status(
|
||||
wallet_id: str, payment_hash: str, conn: Optional[Connection] = None
|
||||
) -> 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:
|
||||
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:
|
||||
return status
|
||||
if payment.is_out and status.failed:
|
||||
logger.info(f"deleting outgoing failed payment {payment.checking_id}: {status}")
|
||||
await payment.delete()
|
||||
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)
|
||||
# note: before, we still checked the status of the payment again
|
||||
return PaymentStatus(True)
|
||||
|
||||
status: PaymentStatus = await payment.check_status()
|
||||
return status
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -402,6 +402,10 @@ async def subscribe(request: Request, wallet: Wallet):
|
|||
async def api_payments_sse(
|
||||
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(
|
||||
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}
|
||||
|
||||
try:
|
||||
await payment.check_pending()
|
||||
await payment.check_status()
|
||||
except Exception:
|
||||
if wallet and wallet.id == payment.wallet_id:
|
||||
return {"paid": False, "details": payment}
|
||||
|
|
|
|||
|
|
@ -130,9 +130,8 @@ async def lndhub_gettxs(
|
|||
offset=offset,
|
||||
exclude_uncheckable=True,
|
||||
):
|
||||
await payment.set_pending(
|
||||
(await WALLET.get_payment_status(payment.checking_id)).pending
|
||||
)
|
||||
await payment.check_status()
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -86,6 +86,9 @@ async def check_pending_payments():
|
|||
incoming = 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(
|
||||
since=(int(time.time()) - 60 * 60 * 24 * 15), # 15 days ago
|
||||
complete=False,
|
||||
|
|
@ -94,11 +97,14 @@ async def check_pending_payments():
|
|||
incoming=incoming,
|
||||
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
|
||||
if incoming:
|
||||
logger.debug("Task: deleting all expired invoices")
|
||||
await delete_expired_invoices()
|
||||
logger.debug("Task: expired invoice deletion finished")
|
||||
|
||||
# after the first check we will only check outgoing, not incoming
|
||||
# that will be handled by the global invoice listeners, hopefully
|
||||
incoming = False
|
||||
|
|
|
|||
|
|
@ -18,13 +18,15 @@ class PaymentResponse(NamedTuple):
|
|||
# when ok is None it means we don't know if this succeeded
|
||||
ok: Optional[bool] = None
|
||||
checking_id: Optional[str] = None # payment_hash, rcp_id
|
||||
fee_msat: int = 0
|
||||
fee_msat: Optional[int] = None
|
||||
preimage: Optional[str] = None
|
||||
error_message: Optional[str] = None
|
||||
|
||||
|
||||
class PaymentStatus(NamedTuple):
|
||||
paid: Optional[bool] = None
|
||||
fee_msat: Optional[int] = None
|
||||
preimage: Optional[str] = None
|
||||
|
||||
@property
|
||||
def pending(self) -> bool:
|
||||
|
|
|
|||
|
|
@ -81,31 +81,41 @@ class ClicheWallet(Wallet):
|
|||
data["result"]["invoice"],
|
||||
)
|
||||
else:
|
||||
return InvoiceResponse(
|
||||
False, checking_id, payment_request, "Could not get payment hash"
|
||||
)
|
||||
return InvoiceResponse(False, None, None, "Could not get payment hash")
|
||||
|
||||
return InvoiceResponse(True, checking_id, payment_request, error_message)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
ws = create_connection(self.endpoint)
|
||||
ws.send(f"pay-invoice --invoice {bolt11}")
|
||||
r = ws.recv()
|
||||
data = json.loads(r)
|
||||
checking_id = None
|
||||
error_message = None
|
||||
for _ in range(2):
|
||||
r = ws.recv()
|
||||
data = json.loads(r)
|
||||
checking_id, fee_msat, preimage, error_message, payment_ok = (
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
|
||||
if data.get("error") is not None and data["error"].get("message"):
|
||||
logger.error(data["error"]["message"])
|
||||
error_message = data["error"]["message"]
|
||||
return PaymentResponse(False, None, 0, error_message)
|
||||
if data.get("error") is not None:
|
||||
error_message = data["error"].get("message")
|
||||
return PaymentResponse(False, None, None, None, error_message)
|
||||
|
||||
if data.get("result") is not None and data["result"].get("payment_hash"):
|
||||
checking_id = data["result"]["payment_hash"]
|
||||
else:
|
||||
return PaymentResponse(False, checking_id, 0, "Could not get payment hash")
|
||||
if data.get("method") == "payment_succeeded":
|
||||
payment_ok = True
|
||||
checking_id = data["params"]["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:
|
||||
ws = create_connection(self.endpoint)
|
||||
|
|
@ -129,22 +139,30 @@ class ClicheWallet(Wallet):
|
|||
if data.get("error") is not None and data["error"].get("message"):
|
||||
logger.error(data["error"]["message"])
|
||||
return PaymentStatus(None)
|
||||
|
||||
payment = data["result"]
|
||||
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]:
|
||||
try:
|
||||
ws = await create_connection(self.endpoint)
|
||||
while True:
|
||||
r = await ws.recv()
|
||||
data = json.loads(r)
|
||||
try:
|
||||
if data["result"]["status"]:
|
||||
yield data["result"]["payment_hash"]
|
||||
except:
|
||||
continue
|
||||
except:
|
||||
pass
|
||||
logger.error("lost connection to cliche's websocket, retrying in 5 seconds")
|
||||
await asyncio.sleep(5)
|
||||
while True:
|
||||
try:
|
||||
ws = await create_connection(self.endpoint)
|
||||
while True:
|
||||
r = await ws.recv()
|
||||
data = json.loads(r)
|
||||
print(data)
|
||||
try:
|
||||
if data["result"]["status"]:
|
||||
yield data["result"]["payment_hash"]
|
||||
except:
|
||||
continue
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
f"lost connection to cliche's invoices stream: '{exc}', retrying in 5 seconds"
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -110,29 +110,38 @@ class CoreLightningWallet(Wallet):
|
|||
|
||||
return InvoiceResponse(True, r["payment_hash"], r["bolt11"], "")
|
||||
except RpcError as exc:
|
||||
error_message = f"lightningd '{exc.method}' failed with '{exc.error}'."
|
||||
logger.error("RPC error:", error_message)
|
||||
error_message = f"CLN method '{exc.method}' failed with '{exc.error.get('message') or exc.error}'."
|
||||
return InvoiceResponse(False, None, None, error_message)
|
||||
except Exception as e:
|
||||
logger.error("error:", e)
|
||||
return InvoiceResponse(False, None, None, str(e))
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
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
|
||||
|
||||
payload = {
|
||||
"bolt11": bolt11,
|
||||
"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:
|
||||
wrapped = async_wrap(_pay_invoice)
|
||||
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:
|
||||
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(
|
||||
True, r["payment_hash"], fee_msat, r["payment_preimage"], None
|
||||
)
|
||||
|
|
@ -144,9 +153,16 @@ class CoreLightningWallet(Wallet):
|
|||
return PaymentStatus(None)
|
||||
if not r["invoices"]:
|
||||
return PaymentStatus(None)
|
||||
if r["invoices"][0]["payment_hash"] == checking_id:
|
||||
return PaymentStatus(r["invoices"][0]["status"] == "paid")
|
||||
raise KeyError("supplied an invalid checking_id")
|
||||
|
||||
invoice_resp = r["invoices"][-1]
|
||||
|
||||
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:
|
||||
try:
|
||||
|
|
@ -155,14 +171,21 @@ class CoreLightningWallet(Wallet):
|
|||
return PaymentStatus(None)
|
||||
if not r["pays"]:
|
||||
return PaymentStatus(None)
|
||||
if r["pays"][0]["payment_hash"] == checking_id:
|
||||
status = r["pays"][0]["status"]
|
||||
payment_resp = r["pays"][-1]
|
||||
|
||||
if payment_resp["payment_hash"] == checking_id:
|
||||
status = payment_resp["status"]
|
||||
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":
|
||||
return PaymentStatus(False)
|
||||
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]:
|
||||
while True:
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class EclairWallet(Wallet):
|
|||
async def status(self) -> StatusResponse:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.url}/usablebalances", headers=self.auth, timeout=40
|
||||
f"{self.url}/globalbalance", headers=self.auth, timeout=5
|
||||
)
|
||||
try:
|
||||
data = r.json()
|
||||
|
|
@ -60,9 +60,11 @@ class EclairWallet(Wallet):
|
|||
)
|
||||
|
||||
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(
|
||||
self,
|
||||
|
|
@ -114,13 +116,18 @@ class EclairWallet(Wallet):
|
|||
except:
|
||||
error_message = r.text
|
||||
pass
|
||||
return PaymentResponse(False, None, 0, None, error_message)
|
||||
return PaymentResponse(False, None, None, None, error_message)
|
||||
|
||||
data = r.json()
|
||||
|
||||
if data["type"] == "payment-failed":
|
||||
return PaymentResponse(False, None, None, None, "payment failed")
|
||||
|
||||
checking_id = data["paymentHash"]
|
||||
preimage = data["paymentPreimage"]
|
||||
|
||||
# We do all this again to get the fee:
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.url}/getsentinfo",
|
||||
|
|
@ -136,15 +143,22 @@ class EclairWallet(Wallet):
|
|||
except:
|
||||
error_message = r.text
|
||||
pass
|
||||
return PaymentResponse(
|
||||
True, checking_id, 0, preimage, error_message
|
||||
) ## ?? is this ok ??
|
||||
return PaymentResponse(None, checking_id, None, preimage, error_message)
|
||||
|
||||
data = r.json()
|
||||
fees = [i["status"] for i in data]
|
||||
fee_msat = sum([i["feesPaid"] for i in fees])
|
||||
statuses = {
|
||||
"sent": True,
|
||||
"failed": False,
|
||||
"pending": None,
|
||||
}
|
||||
|
||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
||||
data = r.json()[-1]
|
||||
if data["status"]["type"] == "sent":
|
||||
fee_msat = -data["status"]["feesPaid"]
|
||||
preimage = data["status"]["paymentPreimage"]
|
||||
|
||||
return PaymentResponse(
|
||||
statuses[data["status"]["type"]], checking_id, fee_msat, preimage, None
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
async with httpx.AsyncClient() as client:
|
||||
|
|
@ -155,54 +169,61 @@ class EclairWallet(Wallet):
|
|||
)
|
||||
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)
|
||||
|
||||
if data["status"]["type"] != "received":
|
||||
return PaymentStatus(False)
|
||||
|
||||
return PaymentStatus(True)
|
||||
statuses = {
|
||||
"received": True,
|
||||
"expired": False,
|
||||
"pending": None,
|
||||
}
|
||||
return PaymentStatus(statuses.get(data["status"]["type"]))
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
url=f"{self.url}/getsentinfo",
|
||||
f"{self.url}/getsentinfo",
|
||||
headers=self.auth,
|
||||
data={"paymentHash": checking_id},
|
||||
timeout=40,
|
||||
)
|
||||
|
||||
data = r.json()[0]
|
||||
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
|
||||
if data["status"]["type"] != "sent":
|
||||
return PaymentStatus(False)
|
||||
data = r.json()[-1]
|
||||
|
||||
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]:
|
||||
while True:
|
||||
try:
|
||||
async with connect(
|
||||
self.ws_url,
|
||||
extra_headers=[("Authorization", self.auth["Authorization"])],
|
||||
) as ws:
|
||||
while True:
|
||||
message = await ws.recv()
|
||||
message = json.loads(message)
|
||||
|
||||
try:
|
||||
async with connect(
|
||||
self.ws_url,
|
||||
extra_headers=[("Authorization", self.auth["Authorization"])],
|
||||
) as ws:
|
||||
while True:
|
||||
message = await ws.recv()
|
||||
message = json.loads(message)
|
||||
if message and message["type"] == "payment-received":
|
||||
yield message["paymentHash"]
|
||||
|
||||
if message and message["type"] == "payment-received":
|
||||
yield message["paymentHash"]
|
||||
|
||||
except (
|
||||
OSError,
|
||||
ConnectionClosedOK,
|
||||
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)
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
f"lost connection to eclair invoices stream: '{exc}', retrying in 5 seconds"
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
|
|
|
|||
|
|
@ -62,10 +62,10 @@ class LNbitsWallet(Wallet):
|
|||
data: Dict = {"out": False, "amount": amount}
|
||||
if description_hash:
|
||||
data["description_hash"] = description_hash.hex()
|
||||
elif unhashed_description:
|
||||
data["description_hash"] = hashlib.sha256(unhashed_description).hexdigest()
|
||||
else:
|
||||
data["memo"] = memo or ""
|
||||
if unhashed_description:
|
||||
data["unhashed_description"] = unhashed_description.hex()
|
||||
|
||||
data["memo"] = memo or ""
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
|
|
@ -94,15 +94,25 @@ class LNbitsWallet(Wallet):
|
|||
json={"out": True, "bolt11": bolt11},
|
||||
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:
|
||||
error_message = r.json()["detail"]
|
||||
return PaymentResponse(None, None, None, None, error_message)
|
||||
else:
|
||||
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:
|
||||
try:
|
||||
|
|
@ -125,8 +135,11 @@ class LNbitsWallet(Wallet):
|
|||
|
||||
if r.is_error:
|
||||
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]:
|
||||
url = f"{self.endpoint}/api/v1/payments/sse"
|
||||
|
|
|
|||
|
|
@ -65,14 +65,32 @@ def get_ssl_context(cert_path: str):
|
|||
return context
|
||||
|
||||
|
||||
def parse_checking_id(checking_id: str) -> bytes:
|
||||
def b64_to_bytes(checking_id: str) -> bytes:
|
||||
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("/", "_")
|
||||
|
||||
|
||||
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
|
||||
# we need to use that cipher suite otherwise there will be a handhsake
|
||||
# error when we communicate with the lnd rpc server.
|
||||
|
|
@ -153,7 +171,7 @@ class LndWallet(Wallet):
|
|||
error_message = str(exc)
|
||||
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)
|
||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||
|
||||
|
|
@ -168,9 +186,9 @@ class LndWallet(Wallet):
|
|||
try:
|
||||
resp = await self.routerpc.SendPaymentV2(req).read()
|
||||
except RpcError as exc:
|
||||
return PaymentResponse(False, "", 0, None, exc._details)
|
||||
return PaymentResponse(False, None, None, None, exc._details)
|
||||
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
|
||||
statuses = {
|
||||
|
|
@ -180,29 +198,31 @@ class LndWallet(Wallet):
|
|||
3: False, # FAILED
|
||||
}
|
||||
|
||||
if resp.status in [0, 1, 3]:
|
||||
fee_msat = 0
|
||||
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
|
||||
fee_msat = None
|
||||
preimage = None
|
||||
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(
|
||||
statuses[resp.status], checking_id, fee_msat, preimage, None
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
r_hash = parse_checking_id(checking_id)
|
||||
r_hash = hex_to_bytes(checking_id)
|
||||
if len(r_hash) != 32:
|
||||
raise binascii.Error
|
||||
except binascii.Error:
|
||||
# this may happen if we switch between backend wallets
|
||||
# that use different checking_id formats
|
||||
return PaymentStatus(None)
|
||||
|
||||
resp = await self.rpc.LookupInvoice(ln.PaymentHash(r_hash=r_hash))
|
||||
try:
|
||||
resp = await self.rpc.LookupInvoice(ln.PaymentHash(r_hash=r_hash))
|
||||
except RpcError as exc:
|
||||
return PaymentStatus(None)
|
||||
if resp.settled:
|
||||
return PaymentStatus(True)
|
||||
|
||||
|
|
@ -213,7 +233,7 @@ class LndWallet(Wallet):
|
|||
This routine checks the payment status using routerpc.TrackPaymentV2.
|
||||
"""
|
||||
try:
|
||||
r_hash = parse_checking_id(checking_id)
|
||||
r_hash = hex_to_bytes(checking_id)
|
||||
if len(r_hash) != 32:
|
||||
raise binascii.Error
|
||||
except binascii.Error:
|
||||
|
|
@ -221,11 +241,6 @@ class LndWallet(Wallet):
|
|||
# that use different checking_id formats
|
||||
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(
|
||||
router.TrackPaymentRequest(payment_hash=r_hash)
|
||||
)
|
||||
|
|
@ -240,6 +255,12 @@ class LndWallet(Wallet):
|
|||
|
||||
try:
|
||||
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])
|
||||
except: # most likely the payment wasn't found
|
||||
return PaymentStatus(None)
|
||||
|
|
@ -248,13 +269,13 @@ class LndWallet(Wallet):
|
|||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
while True:
|
||||
request = ln.InvoiceSubscription()
|
||||
try:
|
||||
request = ln.InvoiceSubscription()
|
||||
async for i in self.rpc.SubscribeInvoices(request):
|
||||
if not i.settled:
|
||||
continue
|
||||
|
||||
checking_id = stringify_checking_id(i.r_hash)
|
||||
checking_id = bytes_to_hex(i.r_hash)
|
||||
yield checking_id
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
|
|
|
|||
|
|
@ -123,18 +123,15 @@ class LndRestWallet(Wallet):
|
|||
|
||||
if r.is_error or r.json().get("payment_error"):
|
||||
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()
|
||||
payment_hash = data["payment_hash"]
|
||||
checking_id = payment_hash
|
||||
checking_id = base64.b64decode(data["payment_hash"]).hex()
|
||||
fee_msat = int(data["payment_route"]["total_fees_msat"])
|
||||
preimage = base64.b64decode(data["payment_preimage"]).hex()
|
||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
checking_id = checking_id.replace("_", "/")
|
||||
|
||||
async with httpx.AsyncClient(verify=self.cert) as client:
|
||||
r = await client.get(
|
||||
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.
|
||||
"""
|
||||
# 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}"
|
||||
|
||||
# check payment.status:
|
||||
# https://api.lightning.community/rest/index.html?python#peersynctype
|
||||
# https://api.lightning.community/?python=#paymentpaymentstatus
|
||||
statuses = {
|
||||
"UNKNOWN": None,
|
||||
"IN_FLIGHT": None,
|
||||
|
|
@ -178,7 +183,11 @@ class LndRestWallet(Wallet):
|
|||
return PaymentStatus(None)
|
||||
payment = line.get("result")
|
||||
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:
|
||||
return PaymentStatus(None)
|
||||
except:
|
||||
|
|
@ -187,10 +196,9 @@ class LndRestWallet(Wallet):
|
|||
return PaymentStatus(None)
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
url = self.endpoint + "/v1/invoices/subscribe"
|
||||
|
||||
while True:
|
||||
try:
|
||||
url = self.endpoint + "/v1/invoices/subscribe"
|
||||
async with httpx.AsyncClient(
|
||||
timeout=None, headers=self.auth, verify=self.cert
|
||||
) as client:
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ class LNPayWallet(Wallet):
|
|||
)
|
||||
|
||||
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"]
|
||||
fee_msat = 0
|
||||
|
|
@ -113,15 +113,18 @@ class LNPayWallet(Wallet):
|
|||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
url=f"{self.endpoint}/lntx/{checking_id}?fields=settled",
|
||||
url=f"{self.endpoint}/lntx/{checking_id}",
|
||||
headers=self.auth,
|
||||
)
|
||||
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
|
||||
data = r.json()
|
||||
preimage = data["payment_preimage"]
|
||||
fee_msat = data["fee_msat"]
|
||||
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]:
|
||||
self.queue: asyncio.Queue = asyncio.Queue(0)
|
||||
|
|
|
|||
|
|
@ -97,10 +97,11 @@ class LntxbotWallet(Wallet):
|
|||
except:
|
||||
error_message = r.text
|
||||
pass
|
||||
|
||||
return PaymentResponse(False, None, 0, None, error_message)
|
||||
return PaymentResponse(False, None, None, None, error_message)
|
||||
|
||||
data = r.json()
|
||||
if data.get("type") != "paid_invoice":
|
||||
return PaymentResponse(None)
|
||||
checking_id = data["payment_hash"]
|
||||
fee_msat = -data["fee_msat"]
|
||||
preimage = data["payment_preimage"]
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class OpenNodeWallet(Wallet):
|
|||
if r.is_error:
|
||||
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(
|
||||
self,
|
||||
|
|
@ -92,11 +92,15 @@ class OpenNodeWallet(Wallet):
|
|||
|
||||
if r.is_error:
|
||||
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"]
|
||||
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)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
|
|
@ -106,9 +110,9 @@ class OpenNodeWallet(Wallet):
|
|||
)
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
|
||||
statuses = {"processing": None, "paid": True, "unpaid": False}
|
||||
return PaymentStatus(statuses[r.json()["data"]["status"]])
|
||||
data = r.json()["data"]
|
||||
statuses = {"processing": None, "paid": True, "unpaid": None}
|
||||
return PaymentStatus(statuses[data.get("status")])
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
async with httpx.AsyncClient() as client:
|
||||
|
|
@ -119,14 +123,16 @@ class OpenNodeWallet(Wallet):
|
|||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
|
||||
data = r.json()["data"]
|
||||
statuses = {
|
||||
"initial": None,
|
||||
"pending": None,
|
||||
"confirmed": True,
|
||||
"error": False,
|
||||
"error": None,
|
||||
"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]:
|
||||
self.queue: asyncio.Queue = asyncio.Queue(0)
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ class SparkWallet(Wallet):
|
|||
pays = listpays["pays"]
|
||||
|
||||
if len(pays) == 0:
|
||||
return PaymentResponse(False, None, 0, None, str(exc))
|
||||
return PaymentResponse(False, None, None, None, str(exc))
|
||||
|
||||
pay = pays[0]
|
||||
payment_hash = pay["payment_hash"]
|
||||
|
|
@ -148,11 +148,9 @@ class SparkWallet(Wallet):
|
|||
)
|
||||
|
||||
if pay["status"] == "failed":
|
||||
return PaymentResponse(False, None, 0, None, str(exc))
|
||||
return PaymentResponse(False, None, None, None, str(exc))
|
||||
elif pay["status"] == "pending":
|
||||
return PaymentResponse(
|
||||
None, payment_hash, fee_limit_msat, None, None
|
||||
)
|
||||
return PaymentResponse(None, payment_hash, None, None, None)
|
||||
elif pay["status"] == "complete":
|
||||
r = pay
|
||||
r["payment_preimage"] = pay["preimage"]
|
||||
|
|
@ -163,7 +161,7 @@ class SparkWallet(Wallet):
|
|||
# this is good
|
||||
pass
|
||||
|
||||
fee_msat = r["msatoshi_sent"] - r["msatoshi"]
|
||||
fee_msat = -int(r["msatoshi_sent"] - r["msatoshi"])
|
||||
preimage = r["payment_preimage"]
|
||||
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:
|
||||
status = r["pays"][0]["status"]
|
||||
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":
|
||||
return PaymentStatus(False)
|
||||
return PaymentStatus(None)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue