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:
calle
2022-08-30 13:28:58 +02:00
committed by GitHub
parent 78a98ca97d
commit 2ee10e28c5
18 changed files with 384 additions and 194 deletions
+65 -44
View File
@@ -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)