diff --git a/lnbits/settings.py b/lnbits/settings.py index 976718f37..b977904a5 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -210,6 +210,7 @@ class ZBDFundingSource(LNbitsSettings): zbd_api_endpoint: Optional[str] = Field(default="https://api.zebedee.io/v0/") zbd_api_key: Optional[str] = Field(default=None) + class AlbyFundingSource(LNbitsSettings): alby_api_endpoint: Optional[str] = Field(default="https://api.getalby.com/") alby_access_token: Optional[str] = Field(default=None) @@ -249,6 +250,7 @@ class FundingSourcesSettings( LndGrpcFundingSource, LnPayFundingSource, AlbyFundingSource, + ZBDFundingSource, OpenNodeFundingSource, SparkFundingSource, LnTipsFundingSource, @@ -403,6 +405,7 @@ class SuperUserSettings(LNbitsSettings): "LnTipsWallet", "LNPayWallet", "AlbyWallet", + "ZBDWallet", "LNbitsWallet", "OpenNodeWallet", ] diff --git a/lnbits/static/js/components/lnbits-funding-sources.js b/lnbits/static/js/components/lnbits-funding-sources.js index 668518a30..b83679bc4 100644 --- a/lnbits/static/js/components/lnbits-funding-sources.js +++ b/lnbits/static/js/components/lnbits-funding-sources.js @@ -110,7 +110,7 @@ Vue.component('lnbits-funding-sources', { 'ZBD', { zbd_api_endpoint: 'Endpoint', - zbd_access_token: 'Key' + zbd_api_key: 'Key' } ], [ diff --git a/lnbits/wallets/__init__.py b/lnbits/wallets/__init__.py index 3ac7d8491..6a4bbe102 100644 --- a/lnbits/wallets/__init__.py +++ b/lnbits/wallets/__init__.py @@ -8,7 +8,6 @@ from lnbits.settings import settings from lnbits.wallets.base import Wallet from .alby import AlbyWallet -from .zbd import ZBDWallet from .cliche import ClicheWallet from .corelightning import CoreLightningWallet @@ -26,6 +25,7 @@ from .lntips import LnTipsWallet from .opennode import OpenNodeWallet from .spark import SparkWallet from .void import VoidWallet +from .zbd import ZBDWallet def set_wallet_class(class_name: Optional[str] = None): diff --git a/lnbits/wallets/zbd.py b/lnbits/wallets/zbd.py index 2e108762a..ba1dfafb5 100644 --- a/lnbits/wallets/zbd.py +++ b/lnbits/wallets/zbd.py @@ -1,17 +1,18 @@ import asyncio -import hashlib from typing import AsyncGenerator, Dict, Optional import httpx from loguru import logger from lnbits.settings import settings +from lnbits.wallets.base import PaymentStatus from .base import ( InvoiceResponse, PaymentResponse, PaymentStatus, StatusResponse, + Unsupported, Wallet, ) @@ -27,7 +28,7 @@ class ZBDWallet(Wallet): self.endpoint = self.normalize_endpoint(settings.zbd_api_endpoint) self.auth = { - "Authorization": "Bearer " + settings.zbd_api_key, + "apikey": settings.zbd_api_key, "User-Agent": settings.user_agent, } self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.auth) @@ -40,16 +41,18 @@ class ZBDWallet(Wallet): async def status(self) -> StatusResponse: try: - r = await self.client.get("/balance", timeout=10) + r = await self.client.get("wallet", timeout=10) except (httpx.ConnectError, httpx.RequestError): return StatusResponse(f"Unable to connect to '{self.endpoint}'", 0) if r.is_error: error_message = r.json()["message"] return StatusResponse(error_message, 0) - data = r.json()["balance"] - # if no error, multiply balance by 1000 for msats representation in lnbits - return StatusResponse(None, data * 1000) + + data = int(r.json()["data"]["balance"]) + # ZBD returns everything as a str not int + # balance is returned in msats already in ZBD + return StatusResponse(None, data) async def create_invoice( self, @@ -60,16 +63,20 @@ class ZBDWallet(Wallet): **kwargs, ) -> InvoiceResponse: # https://api.zebedee.io/v0/charges - data: Dict = {"amount": f"{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 description_hash or unhashed_description: + raise Unsupported("description_hash") + + msats_amount = amount * 1000 + data: Dict = { + "amount": f"{msats_amount}", + "description": memo, + "expiresIn": 3600, + "callbackUrl": "", + "internalId": "", + } r = await self.client.post( - "/invoices", + "charges", json=data, timeout=40, ) @@ -78,16 +85,22 @@ class ZBDWallet(Wallet): error_message = r.json()["message"] return InvoiceResponse(False, None, None, error_message) - data = r.json() - checking_id = data["payment_hash"] - payment_request = data["payment_request"] + data = r.json()["data"] + checking_id = data["id"] # this is a zbd id + payment_request = data["invoice"]["request"] return InvoiceResponse(True, checking_id, payment_request, None) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: # https://api.zebedee.io/v0/payments r = await self.client.post( - "/payments/bolt11", - json={"invoice": bolt11}, # assume never need amount in body + "payments", + json={ + "invoice": bolt11, + "description": "", + "amount": "", + "internalId": "", + "callbackUrl": "", + }, timeout=None, ) @@ -96,28 +109,57 @@ class ZBDWallet(Wallet): return PaymentResponse(False, None, None, None, error_message) data = r.json() - checking_id = data["payment_hash"] - fee_msat = -data["fee"] - preimage = data["payment_preimage"] + + # get the payment hash from the zbd api + decoded_request = await self.client.post( + "decode-invoice", + json={"invoice": bolt11}, + timeout=40, + ) + if decoded_request.is_error: + error_message = decoded_request.json()["message"] + return InvoiceResponse(False, None, None, error_message) + + decoded_data = decoded_request.json() + + checking_id = decoded_data["data"]["paymentHash"] + fee_msat = -int(data["data"]["fee"]) + preimage = data["data"]["preimage"] return PaymentResponse(True, checking_id, fee_msat, preimage, None) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: - return await self.get_payment_status(checking_id) + r = await self.client.get(f"charges/{checking_id}") + if r.is_error: + return PaymentStatus(None) + data = r.json()["data"] + + statuses = { + "pending": None, + "paid": True, + "unpaid": None, + "expired": False, + "completed": True, + } + return PaymentStatus(statuses[data.get("status")]) async def get_payment_status(self, checking_id: str) -> PaymentStatus: - r = await self.client.get(f"/invoices/{checking_id}") - + r = await self.client.get(f"payments/{checking_id}") if r.is_error: return PaymentStatus(None) - data = r.json() + data = r.json()["data"] statuses = { - "CREATED": None, - "SETTLED": True, + "initial": None, + "pending": None, + "completed": True, + "error": None, + "expired": False, + "failed": False, } - return PaymentStatus(statuses[data.get("state")], fee_msat=None, preimage=None) + + return PaymentStatus(statuses[data.get("status")], fee_msat=None, preimage=None) async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: self.queue: asyncio.Queue = asyncio.Queue(0)