Wallets refactor (#1729)
* feat: cleanup function for wallet * update eclair implementation * update lnd implementation * update lnbits implementation * update lnpay implementation * update lnbits implementation * update opennode implementation * update spark implementation * use base_url for clients * fix lnpay * fix opennode * fix lntips * test real invoice creation * add small delay to test * test paid invoice stream * fix lnbits * fix lndrest * fix spark fix spark * check node balance in test * increase balance check delay * check balance in pay test aswell * make sure get_payment_status is called * fix lndrest * revert unnecessary changes
This commit is contained in:
parent
49411e58cc
commit
e6499104c0
|
@ -46,7 +46,6 @@ from .tasks import (
|
|||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
|
||||
configure_logger()
|
||||
|
||||
app = FastAPI(
|
||||
|
@ -82,6 +81,7 @@ def create_app() -> FastAPI:
|
|||
register_routes(app)
|
||||
register_async_tasks(app)
|
||||
register_exception_handlers(app)
|
||||
register_shutdown(app)
|
||||
|
||||
# Allow registering new extensions routes without direct access to the `app` object
|
||||
setattr(core_app_extra, "register_new_ext_routes", register_new_ext_routes(app))
|
||||
|
@ -90,7 +90,6 @@ def create_app() -> FastAPI:
|
|||
|
||||
|
||||
async def check_funding_source() -> None:
|
||||
|
||||
original_sigint_handler = signal.getsignal(signal.SIGINT)
|
||||
|
||||
def signal_handler(signal, frame):
|
||||
|
@ -279,7 +278,6 @@ def register_ext_routes(app: FastAPI, ext: Extension) -> None:
|
|||
def register_startup(app: FastAPI):
|
||||
@app.on_event("startup")
|
||||
async def lnbits_startup():
|
||||
|
||||
try:
|
||||
# wait till migration is done
|
||||
await migrate_databases()
|
||||
|
@ -303,6 +301,13 @@ def register_startup(app: FastAPI):
|
|||
raise ImportError("Failed to run 'startup' event.")
|
||||
|
||||
|
||||
def register_shutdown(app: FastAPI):
|
||||
@app.on_event("shutdown")
|
||||
async def on_shutdown():
|
||||
WALLET = get_wallet_class()
|
||||
await WALLET.cleanup()
|
||||
|
||||
|
||||
def log_server_info():
|
||||
logger.info("Starting LNbits")
|
||||
logger.info(f"Version: {settings.version}")
|
||||
|
|
|
@ -48,6 +48,9 @@ class PaymentStatus(NamedTuple):
|
|||
|
||||
|
||||
class Wallet(ABC):
|
||||
async def cleanup(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def status(self) -> Coroutine[None, None, StatusResponse]:
|
||||
pass
|
||||
|
|
|
@ -41,12 +41,13 @@ class EclairWallet(Wallet):
|
|||
encodedAuth = base64.b64encode(f":{passw}".encode())
|
||||
auth = str(encodedAuth, "utf-8")
|
||||
self.auth = {"Authorization": f"Basic {auth}"}
|
||||
self.client = httpx.AsyncClient(base_url=self.url, headers=self.auth)
|
||||
|
||||
async def cleanup(self):
|
||||
await self.client.aclose()
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.url}/globalbalance", headers=self.auth, timeout=5
|
||||
)
|
||||
r = await self.client.post("/globalbalance", timeout=5)
|
||||
try:
|
||||
data = r.json()
|
||||
except:
|
||||
|
@ -69,7 +70,6 @@ class EclairWallet(Wallet):
|
|||
unhashed_description: Optional[bytes] = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
|
||||
data: Dict[str, Any] = {
|
||||
"amountMsat": amount * 1000,
|
||||
}
|
||||
|
@ -84,10 +84,7 @@ class EclairWallet(Wallet):
|
|||
else:
|
||||
data["description"] = memo
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.url}/createinvoice", headers=self.auth, data=data, timeout=40
|
||||
)
|
||||
r = await self.client.post("/createinvoice", data=data, timeout=40)
|
||||
|
||||
if r.is_error:
|
||||
try:
|
||||
|
@ -102,13 +99,11 @@ class EclairWallet(Wallet):
|
|||
return InvoiceResponse(True, data["paymentHash"], data["serialized"], None)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.url}/payinvoice",
|
||||
headers=self.auth,
|
||||
data={"invoice": bolt11, "blocking": True},
|
||||
timeout=None,
|
||||
)
|
||||
r = await self.client.post(
|
||||
"/payinvoice",
|
||||
data={"invoice": bolt11, "blocking": True},
|
||||
timeout=None,
|
||||
)
|
||||
|
||||
if "error" in r.json():
|
||||
try:
|
||||
|
@ -128,13 +123,11 @@ class EclairWallet(Wallet):
|
|||
|
||||
# We do all this again to get the fee:
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.url}/getsentinfo",
|
||||
headers=self.auth,
|
||||
data={"paymentHash": checking_id},
|
||||
timeout=40,
|
||||
)
|
||||
r = await self.client.post(
|
||||
"/getsentinfo",
|
||||
data={"paymentHash": checking_id},
|
||||
timeout=40,
|
||||
)
|
||||
|
||||
if "error" in r.json():
|
||||
try:
|
||||
|
@ -162,12 +155,10 @@ class EclairWallet(Wallet):
|
|||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.url}/getreceivedinfo",
|
||||
headers=self.auth,
|
||||
data={"paymentHash": checking_id},
|
||||
)
|
||||
r = await self.client.post(
|
||||
"/getreceivedinfo",
|
||||
data={"paymentHash": checking_id},
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
|
@ -186,13 +177,11 @@ class EclairWallet(Wallet):
|
|||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.url}/getsentinfo",
|
||||
headers=self.auth,
|
||||
data={"paymentHash": checking_id},
|
||||
timeout=40,
|
||||
)
|
||||
r = await self.client.post(
|
||||
"/getsentinfo",
|
||||
data={"paymentHash": checking_id},
|
||||
timeout=40,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
|
|
|
@ -29,17 +29,18 @@ class LNbitsWallet(Wallet):
|
|||
if not self.endpoint or not key:
|
||||
raise Exception("cannot initialize lnbits wallet")
|
||||
self.key = {"X-Api-Key": key}
|
||||
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.key)
|
||||
|
||||
async def cleanup(self):
|
||||
await self.client.aclose()
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
r = await client.get(
|
||||
url=f"{self.endpoint}/api/v1/wallet", headers=self.key, timeout=15
|
||||
)
|
||||
except Exception as exc:
|
||||
return StatusResponse(
|
||||
f"Failed to connect to {self.endpoint} due to: {exc}", 0
|
||||
)
|
||||
try:
|
||||
r = await self.client.get(url="/api/v1/wallet", timeout=15)
|
||||
except Exception as exc:
|
||||
return StatusResponse(
|
||||
f"Failed to connect to {self.endpoint} due to: {exc}", 0
|
||||
)
|
||||
|
||||
try:
|
||||
data = r.json()
|
||||
|
@ -69,10 +70,7 @@ class LNbitsWallet(Wallet):
|
|||
if unhashed_description:
|
||||
data["unhashed_description"] = unhashed_description.hex()
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
url=f"{self.endpoint}/api/v1/payments", headers=self.key, json=data
|
||||
)
|
||||
r = await self.client.post(url="/api/v1/payments", json=data)
|
||||
ok, checking_id, payment_request, error_message = (
|
||||
not r.is_error,
|
||||
None,
|
||||
|
@ -89,20 +87,12 @@ class LNbitsWallet(Wallet):
|
|||
return InvoiceResponse(ok, checking_id, payment_request, error_message)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
url=f"{self.endpoint}/api/v1/payments",
|
||||
headers=self.key,
|
||||
json={"out": True, "bolt11": bolt11},
|
||||
timeout=None,
|
||||
)
|
||||
ok, checking_id, _, _, error_message = (
|
||||
not r.is_error,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
r = await self.client.post(
|
||||
url="/api/v1/payments",
|
||||
json={"out": True, "bolt11": bolt11},
|
||||
timeout=None,
|
||||
)
|
||||
ok = not r.is_error
|
||||
|
||||
if r.is_error:
|
||||
error_message = r.json()["detail"]
|
||||
|
@ -118,11 +108,9 @@ class LNbitsWallet(Wallet):
|
|||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
url=f"{self.endpoint}/api/v1/payments/{checking_id}",
|
||||
headers=self.key,
|
||||
)
|
||||
r = await self.client.get(
|
||||
url=f"/api/v1/payments/{checking_id}",
|
||||
)
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
return PaymentStatus(r.json()["paid"])
|
||||
|
@ -130,10 +118,7 @@ class LNbitsWallet(Wallet):
|
|||
return PaymentStatus(None)
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
url=f"{self.endpoint}/api/v1/payments/{checking_id}", headers=self.key
|
||||
)
|
||||
r = await self.client.get(url=f"/api/v1/payments/{checking_id}")
|
||||
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
|
|
|
@ -64,14 +64,17 @@ class LndRestWallet(Wallet):
|
|||
self.cert = cert or True
|
||||
|
||||
self.auth = {"Grpc-Metadata-macaroon": self.macaroon}
|
||||
self.client = httpx.AsyncClient(
|
||||
base_url=self.endpoint, headers=self.auth, verify=self.cert
|
||||
)
|
||||
|
||||
async def cleanup(self):
|
||||
await self.client.aclose()
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
try:
|
||||
async with httpx.AsyncClient(verify=self.cert) as client:
|
||||
r = await client.get(
|
||||
f"{self.endpoint}/v1/balance/channels", headers=self.auth
|
||||
)
|
||||
r.raise_for_status()
|
||||
r = await self.client.get("/v1/balance/channels")
|
||||
r.raise_for_status()
|
||||
except (httpx.ConnectError, httpx.RequestError) as exc:
|
||||
return StatusResponse(f"Unable to connect to {self.endpoint}. {exc}", 0)
|
||||
|
||||
|
@ -104,10 +107,7 @@ class LndRestWallet(Wallet):
|
|||
hashlib.sha256(unhashed_description).digest()
|
||||
).decode("ascii")
|
||||
|
||||
async with httpx.AsyncClient(verify=self.cert) as client:
|
||||
r = await client.post(
|
||||
url=f"{self.endpoint}/v1/invoices", headers=self.auth, json=data
|
||||
)
|
||||
r = await self.client.post(url="/v1/invoices", json=data)
|
||||
|
||||
if r.is_error:
|
||||
error_message = r.text
|
||||
|
@ -125,17 +125,15 @@ class LndRestWallet(Wallet):
|
|||
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
async with httpx.AsyncClient(verify=self.cert) as client:
|
||||
# set the fee limit for the payment
|
||||
lnrpcFeeLimit = dict()
|
||||
lnrpcFeeLimit["fixed_msat"] = f"{fee_limit_msat}"
|
||||
# set the fee limit for the payment
|
||||
lnrpcFeeLimit = dict()
|
||||
lnrpcFeeLimit["fixed_msat"] = f"{fee_limit_msat}"
|
||||
|
||||
r = await client.post(
|
||||
url=f"{self.endpoint}/v1/channels/transactions",
|
||||
headers=self.auth,
|
||||
json={"payment_request": bolt11, "fee_limit": lnrpcFeeLimit},
|
||||
timeout=None,
|
||||
)
|
||||
r = await self.client.post(
|
||||
url="/v1/channels/transactions",
|
||||
json={"payment_request": bolt11, "fee_limit": lnrpcFeeLimit},
|
||||
timeout=None,
|
||||
)
|
||||
|
||||
if r.is_error or r.json().get("payment_error"):
|
||||
error_message = r.json().get("payment_error") or r.text
|
||||
|
@ -148,10 +146,7 @@ class LndRestWallet(Wallet):
|
|||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
async with httpx.AsyncClient(verify=self.cert) as client:
|
||||
r = await client.get(
|
||||
url=f"{self.endpoint}/v1/invoice/{checking_id}", headers=self.auth
|
||||
)
|
||||
r = await self.client.get(url=f"/v1/invoice/{checking_id}")
|
||||
|
||||
if r.is_error or not r.json().get("settled"):
|
||||
# this must also work when checking_id is not a hex recognizable by lnd
|
||||
|
@ -172,7 +167,7 @@ class LndRestWallet(Wallet):
|
|||
except ValueError:
|
||||
return PaymentStatus(None)
|
||||
|
||||
url = f"{self.endpoint}/v2/router/track/{checking_id}"
|
||||
url = f"/v2/router/track/{checking_id}"
|
||||
|
||||
# check payment.status:
|
||||
# https://api.lightning.community/?python=#paymentpaymentstatus
|
||||
|
@ -183,52 +178,46 @@ class LndRestWallet(Wallet):
|
|||
"FAILED": False,
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
timeout=None, headers=self.auth, verify=self.cert
|
||||
) as client:
|
||||
async with client.stream("GET", url) as r:
|
||||
async for json_line in r.aiter_lines():
|
||||
try:
|
||||
line = json.loads(json_line)
|
||||
if line.get("error"):
|
||||
logger.error(
|
||||
line["error"]["message"]
|
||||
if "message" in line["error"]
|
||||
else line["error"]
|
||||
)
|
||||
return PaymentStatus(None)
|
||||
payment = line.get("result")
|
||||
if payment is not None and payment.get("status"):
|
||||
return PaymentStatus(
|
||||
paid=statuses[payment["status"]],
|
||||
fee_msat=payment.get("fee_msat"),
|
||||
preimage=payment.get("payment_preimage"),
|
||||
)
|
||||
else:
|
||||
return PaymentStatus(None)
|
||||
except:
|
||||
continue
|
||||
async with self.client.stream("GET", url, timeout=None) as r:
|
||||
async for json_line in r.aiter_lines():
|
||||
try:
|
||||
line = json.loads(json_line)
|
||||
if line.get("error"):
|
||||
logger.error(
|
||||
line["error"]["message"]
|
||||
if "message" in line["error"]
|
||||
else line["error"]
|
||||
)
|
||||
return PaymentStatus(None)
|
||||
payment = line.get("result")
|
||||
if payment is not None and payment.get("status"):
|
||||
return PaymentStatus(
|
||||
paid=statuses[payment["status"]],
|
||||
fee_msat=payment.get("fee_msat"),
|
||||
preimage=payment.get("payment_preimage"),
|
||||
)
|
||||
else:
|
||||
return PaymentStatus(None)
|
||||
except:
|
||||
continue
|
||||
|
||||
return PaymentStatus(None)
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
while True:
|
||||
try:
|
||||
url = self.endpoint + "/v1/invoices/subscribe"
|
||||
async with httpx.AsyncClient(
|
||||
timeout=None, headers=self.auth, verify=self.cert
|
||||
) as client:
|
||||
async with client.stream("GET", url) as r:
|
||||
async for line in r.aiter_lines():
|
||||
try:
|
||||
inv = json.loads(line)["result"]
|
||||
if not inv["settled"]:
|
||||
continue
|
||||
except:
|
||||
url = "/v1/invoices/subscribe"
|
||||
async with self.client.stream("GET", url, timeout=None) as r:
|
||||
async for line in r.aiter_lines():
|
||||
try:
|
||||
inv = json.loads(line)["result"]
|
||||
if not inv["settled"]:
|
||||
continue
|
||||
except:
|
||||
continue
|
||||
|
||||
payment_hash = base64.b64decode(inv["r_hash"]).hex()
|
||||
yield payment_hash
|
||||
payment_hash = base64.b64decode(inv["r_hash"]).hex()
|
||||
yield payment_hash
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
f"lost connection to lnd invoices stream: '{exc}', retrying in 5 seconds"
|
||||
|
|
|
@ -32,12 +32,15 @@ class LNPayWallet(Wallet):
|
|||
self.wallet_key = wallet_key
|
||||
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
||||
self.auth = {"X-Api-Key": settings.lnpay_api_key}
|
||||
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.auth)
|
||||
|
||||
async def cleanup(self):
|
||||
await self.client.aclose()
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
url = f"{self.endpoint}/wallet/{self.wallet_key}"
|
||||
url = f"/wallet/{self.wallet_key}"
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(url, headers=self.auth, timeout=60)
|
||||
r = await self.client.get(url, timeout=60)
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
return StatusResponse(f"Unable to connect to '{url}'", 0)
|
||||
|
||||
|
@ -69,13 +72,11 @@ class LNPayWallet(Wallet):
|
|||
else:
|
||||
data["memo"] = memo or ""
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.endpoint}/wallet/{self.wallet_key}/invoice",
|
||||
headers=self.auth,
|
||||
json=data,
|
||||
timeout=60,
|
||||
)
|
||||
r = await self.client.post(
|
||||
f"/wallet/{self.wallet_key}/invoice",
|
||||
json=data,
|
||||
timeout=60,
|
||||
)
|
||||
ok, checking_id, payment_request, error_message = (
|
||||
r.status_code == 201,
|
||||
None,
|
||||
|
@ -90,13 +91,11 @@ class LNPayWallet(Wallet):
|
|||
return InvoiceResponse(ok, checking_id, payment_request, error_message)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.endpoint}/wallet/{self.wallet_key}/withdraw",
|
||||
headers=self.auth,
|
||||
json={"payment_request": bolt11},
|
||||
timeout=None,
|
||||
)
|
||||
r = await self.client.post(
|
||||
f"/wallet/{self.wallet_key}/withdraw",
|
||||
json={"payment_request": bolt11},
|
||||
timeout=None,
|
||||
)
|
||||
|
||||
try:
|
||||
data = r.json()
|
||||
|
@ -117,11 +116,9 @@ class LNPayWallet(Wallet):
|
|||
return await self.get_payment_status(checking_id)
|
||||
|
||||
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}",
|
||||
headers=self.auth,
|
||||
)
|
||||
r = await self.client.get(
|
||||
url=f"/lntx/{checking_id}",
|
||||
)
|
||||
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
|
@ -155,12 +152,9 @@ class LNPayWallet(Wallet):
|
|||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
|
||||
lntx_id = data["data"]["wtx"]["lnTx"]["id"]
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
f"{self.endpoint}/lntx/{lntx_id}?fields=settled", headers=self.auth
|
||||
)
|
||||
data = r.json()
|
||||
if data["settled"]:
|
||||
await self.queue.put(lntx_id)
|
||||
r = await self.client.get(f"/lntx/{lntx_id}?fields=settled")
|
||||
data = r.json()
|
||||
if data["settled"]:
|
||||
await self.queue.put(lntx_id)
|
||||
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
|
|
|
@ -30,12 +30,13 @@ class LnTipsWallet(Wallet):
|
|||
raise Exception("cannot initialize lntxbod")
|
||||
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
||||
self.auth = {"Authorization": f"Basic {key}"}
|
||||
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.auth)
|
||||
|
||||
async def cleanup(self):
|
||||
await self.client.aclose()
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
f"{self.endpoint}/api/v1/balance", headers=self.auth, timeout=40
|
||||
)
|
||||
r = await self.client.get("/api/v1/balance", timeout=40)
|
||||
try:
|
||||
data = r.json()
|
||||
except:
|
||||
|
@ -62,13 +63,11 @@ class LnTipsWallet(Wallet):
|
|||
elif unhashed_description:
|
||||
data["description_hash"] = hashlib.sha256(unhashed_description).hexdigest()
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.endpoint}/api/v1/createinvoice",
|
||||
headers=self.auth,
|
||||
json=data,
|
||||
timeout=40,
|
||||
)
|
||||
r = await self.client.post(
|
||||
"/api/v1/createinvoice",
|
||||
json=data,
|
||||
timeout=40,
|
||||
)
|
||||
|
||||
if r.is_error:
|
||||
try:
|
||||
|
@ -85,13 +84,11 @@ class LnTipsWallet(Wallet):
|
|||
)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.endpoint}/api/v1/payinvoice",
|
||||
headers=self.auth,
|
||||
json={"pay_req": bolt11},
|
||||
timeout=None,
|
||||
)
|
||||
r = await self.client.post(
|
||||
"/api/v1/payinvoice",
|
||||
json={"pay_req": bolt11},
|
||||
timeout=None,
|
||||
)
|
||||
if r.is_error:
|
||||
return PaymentResponse(False, None, 0, None, r.text)
|
||||
|
||||
|
@ -111,11 +108,9 @@ class LnTipsWallet(Wallet):
|
|||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.endpoint}/api/v1/invoicestatus/{checking_id}",
|
||||
headers=self.auth,
|
||||
)
|
||||
r = await self.client.post(
|
||||
f"/api/v1/invoicestatus/{checking_id}",
|
||||
)
|
||||
|
||||
if r.is_error or len(r.text) == 0:
|
||||
raise Exception
|
||||
|
@ -127,11 +122,9 @@ class LnTipsWallet(Wallet):
|
|||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
url=f"{self.endpoint}/api/v1/paymentstatus/{checking_id}",
|
||||
headers=self.auth,
|
||||
)
|
||||
r = await self.client.post(
|
||||
url=f"/api/v1/paymentstatus/{checking_id}",
|
||||
)
|
||||
|
||||
if r.is_error:
|
||||
raise Exception
|
||||
|
@ -145,23 +138,22 @@ class LnTipsWallet(Wallet):
|
|||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
last_connected = None
|
||||
while True:
|
||||
url = f"{self.endpoint}/api/v1/invoicestream"
|
||||
url = "/api/v1/invoicestream"
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=None, headers=self.auth) as client:
|
||||
last_connected = time.time()
|
||||
async with client.stream("GET", url) as r:
|
||||
async for line in r.aiter_lines():
|
||||
try:
|
||||
prefix = "data: "
|
||||
if not line.startswith(prefix):
|
||||
continue
|
||||
data = line[len(prefix) :] # sse parsing
|
||||
inv = json.loads(data)
|
||||
if not inv.get("payment_hash"):
|
||||
continue
|
||||
except:
|
||||
last_connected = time.time()
|
||||
async with self.client.stream("GET", url) as r:
|
||||
async for line in r.aiter_lines():
|
||||
try:
|
||||
prefix = "data: "
|
||||
if not line.startswith(prefix):
|
||||
continue
|
||||
yield inv["payment_hash"]
|
||||
data = line[len(prefix) :] # sse parsing
|
||||
inv = json.loads(data)
|
||||
if not inv.get("payment_hash"):
|
||||
continue
|
||||
except:
|
||||
continue
|
||||
yield inv["payment_hash"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
|
@ -34,13 +34,14 @@ class OpenNodeWallet(Wallet):
|
|||
|
||||
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
||||
self.auth = {"Authorization": key}
|
||||
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.auth)
|
||||
|
||||
async def cleanup(self):
|
||||
await self.client.aclose()
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
f"{self.endpoint}/v1/account/balance", headers=self.auth, timeout=40
|
||||
)
|
||||
r = await self.client.get("/v1/account/balance", timeout=40)
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
return StatusResponse(f"Unable to connect to '{self.endpoint}'", 0)
|
||||
|
||||
|
@ -61,17 +62,15 @@ class OpenNodeWallet(Wallet):
|
|||
if description_hash or unhashed_description:
|
||||
raise Unsupported("description_hash")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.endpoint}/v1/charges",
|
||||
headers=self.auth,
|
||||
json={
|
||||
"amount": amount,
|
||||
"description": memo or "",
|
||||
# "callback_url": url_for("/webhook_listener", _external=True),
|
||||
},
|
||||
timeout=40,
|
||||
)
|
||||
r = await self.client.post(
|
||||
"/v1/charges",
|
||||
json={
|
||||
"amount": amount,
|
||||
"description": memo or "",
|
||||
# "callback_url": url_for("/webhook_listener", _external=True),
|
||||
},
|
||||
timeout=40,
|
||||
)
|
||||
|
||||
if r.is_error:
|
||||
error_message = r.json()["message"]
|
||||
|
@ -83,13 +82,11 @@ class OpenNodeWallet(Wallet):
|
|||
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.endpoint}/v2/withdrawals",
|
||||
headers=self.auth,
|
||||
json={"type": "ln", "address": bolt11},
|
||||
timeout=None,
|
||||
)
|
||||
r = await self.client.post(
|
||||
"/v2/withdrawals",
|
||||
json={"type": "ln", "address": bolt11},
|
||||
timeout=None,
|
||||
)
|
||||
|
||||
if r.is_error:
|
||||
error_message = r.json()["message"]
|
||||
|
@ -105,10 +102,7 @@ class OpenNodeWallet(Wallet):
|
|||
return PaymentResponse(True, checking_id, fee_msat, None, None)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
f"{self.endpoint}/v1/charge/{checking_id}", headers=self.auth
|
||||
)
|
||||
r = await self.client.get(f"/v1/charge/{checking_id}")
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
data = r.json()["data"]
|
||||
|
@ -116,10 +110,7 @@ class OpenNodeWallet(Wallet):
|
|||
return PaymentStatus(statuses[data.get("status")])
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
f"{self.endpoint}/v1/withdrawal/{checking_id}", headers=self.auth
|
||||
)
|
||||
r = await self.client.get(f"/v1/withdrawal/{checking_id}")
|
||||
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
|
|
|
@ -31,6 +31,13 @@ class SparkWallet(Wallet):
|
|||
assert settings.spark_url, "spark url does not exist"
|
||||
self.url = settings.spark_url.replace("/rpc", "")
|
||||
self.token = settings.spark_token
|
||||
assert self.token, "spark wallet token does not exist"
|
||||
self.client = httpx.AsyncClient(
|
||||
base_url=self.url, headers={"X-Access": self.token}
|
||||
)
|
||||
|
||||
async def cleanup(self):
|
||||
await self.client.aclose()
|
||||
|
||||
def __getattr__(self, key):
|
||||
async def call(*args, **kwargs):
|
||||
|
@ -46,15 +53,12 @@ class SparkWallet(Wallet):
|
|||
params = {}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
assert self.token, "spark wallet token does not exist"
|
||||
r = await client.post(
|
||||
self.url + "/rpc",
|
||||
headers={"X-Access": self.token},
|
||||
json={"method": key, "params": params},
|
||||
timeout=60 * 60 * 24,
|
||||
)
|
||||
r.raise_for_status()
|
||||
r = await self.client.post(
|
||||
"/rpc",
|
||||
json={"method": key, "params": params},
|
||||
timeout=60 * 60 * 24,
|
||||
)
|
||||
r.raise_for_status()
|
||||
except (
|
||||
OSError,
|
||||
httpx.ConnectError,
|
||||
|
@ -224,17 +228,16 @@ class SparkWallet(Wallet):
|
|||
raise KeyError("supplied an invalid checking_id")
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
url = f"{self.url}/stream?access-key={self.token}"
|
||||
url = f"/stream?access-key={self.token}"
|
||||
|
||||
while True:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=None) as client:
|
||||
async with client.stream("GET", url) as r:
|
||||
async for line in r.aiter_lines():
|
||||
if line.startswith("data:"):
|
||||
data = json.loads(line[5:])
|
||||
if "pay_index" in data and data.get("status") == "paid":
|
||||
yield data["label"]
|
||||
async with self.client.stream("GET", url, timeout=None) as r:
|
||||
async for line in r.aiter_lines():
|
||||
if line.startswith("data:"):
|
||||
data = json.loads(line[5:])
|
||||
if "pay_index" in data and data.get("status") == "paid":
|
||||
yield data["label"]
|
||||
except (
|
||||
OSError,
|
||||
httpx.ReadError,
|
||||
|
|
|
@ -6,12 +6,12 @@ import pytest
|
|||
|
||||
from lnbits import bolt11
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.views.api import api_payment
|
||||
from lnbits.core.views.api import api_auditor, api_payment
|
||||
from lnbits.db import DB_TYPE, SQLITE
|
||||
from lnbits.settings import get_wallet_class
|
||||
from tests.conftest import CreateInvoiceData, api_payments_create_invoice
|
||||
|
||||
from ...helpers import get_random_invoice_data, is_fake
|
||||
from ...helpers import get_random_invoice_data, is_fake, pay_real_invoice
|
||||
|
||||
WALLET = get_wallet_class()
|
||||
|
||||
|
@ -320,11 +320,17 @@ async def test_create_invoice_with_unhashed_description(client, inkey_headers_to
|
|||
return invoice
|
||||
|
||||
|
||||
async def get_node_balance_sats():
|
||||
audit = await api_auditor()
|
||||
return audit["node_balance_msats"] / 1000
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="this only works in regtest")
|
||||
async def test_pay_real_invoice(
|
||||
client, real_invoice, adminkey_headers_from, inkey_headers_from
|
||||
):
|
||||
prev_balance = await get_node_balance_sats()
|
||||
response = await client.post(
|
||||
"/api/v1/payments", json=real_invoice, headers=adminkey_headers_from
|
||||
)
|
||||
|
@ -337,5 +343,46 @@ async def test_pay_real_invoice(
|
|||
response = await api_payment(
|
||||
invoice["payment_hash"], inkey_headers_from["X-Api-Key"]
|
||||
)
|
||||
assert type(response) == dict
|
||||
assert response["paid"] is True
|
||||
assert response["paid"]
|
||||
|
||||
status = await WALLET.get_payment_status(invoice["payment_hash"])
|
||||
assert status.paid
|
||||
|
||||
await asyncio.sleep(0.3)
|
||||
balance = await get_node_balance_sats()
|
||||
assert prev_balance - balance == 100
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="this only works in regtest")
|
||||
async def test_create_real_invoice(client, adminkey_headers_from, inkey_headers_from):
|
||||
prev_balance = await get_node_balance_sats()
|
||||
create_invoice = CreateInvoiceData(out=False, amount=1000, memo="test")
|
||||
response = await client.post(
|
||||
"/api/v1/payments",
|
||||
json=create_invoice.dict(),
|
||||
headers=adminkey_headers_from,
|
||||
)
|
||||
assert response.status_code < 300
|
||||
invoice = response.json()
|
||||
response = await api_payment(
|
||||
invoice["payment_hash"], inkey_headers_from["X-Api-Key"]
|
||||
)
|
||||
assert not response["paid"]
|
||||
|
||||
async def listen():
|
||||
async for payment_hash in get_wallet_class().paid_invoices_stream():
|
||||
assert payment_hash == invoice["payment_hash"]
|
||||
return
|
||||
|
||||
task = asyncio.create_task(listen())
|
||||
pay_real_invoice(invoice["payment_request"])
|
||||
await asyncio.wait_for(task, timeout=3)
|
||||
response = await api_payment(
|
||||
invoice["payment_hash"], inkey_headers_from["X-Api-Key"]
|
||||
)
|
||||
assert response["paid"]
|
||||
|
||||
await asyncio.sleep(0.3)
|
||||
balance = await get_node_balance_sats()
|
||||
assert balance - prev_balance == create_invoice.amount
|
||||
|
|
|
@ -63,13 +63,12 @@ def run_cmd_json(cmd: str) -> dict:
|
|||
|
||||
|
||||
def get_real_invoice(sats: int) -> dict:
|
||||
msats = sats * 1000
|
||||
return run_cmd_json(f"{docker_lightning_cli} addinvoice {msats}")
|
||||
return run_cmd_json(f"{docker_lightning_cli} addinvoice {sats}")
|
||||
|
||||
|
||||
def pay_real_invoice(invoice: str) -> Popen:
|
||||
return Popen(
|
||||
f"{docker_lightning_cli} payinvoice {invoice}",
|
||||
f"{docker_lightning_cli} payinvoice --force {invoice}",
|
||||
shell=True,
|
||||
stdin=PIPE,
|
||||
stdout=PIPE,
|
||||
|
|
Loading…
Reference in New Issue
Block a user