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