feat: add Breez SDK wallet (#1897)

* add Breez SDK wallet
* use more description status classes
* fix: add try-except

---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
Co-authored-by: dni  <office@dnilabs.com>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
This commit is contained in:
callebtc
2024-08-06 10:06:21 +02:00
committed by GitHub
co-authored by Pavol Rusnak dni ⚡ Vlad Stan
parent 235f8a6c19
commit 0015314e11
14 changed files with 390 additions and 4 deletions
+26
View File
@@ -484,6 +484,32 @@
</a>
</div>
</div>
<div class="row">
<div class="col">
<a
href="https://breez.technology/sdk/"
target="_blank"
rel="noopener noreferrer"
>
<q-img
contain
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/breez.png') }}' : '{{ static_url_for('static', 'images/breezl.png') }}'"
></q-img>
</a>
</div>
<div class="col q-pl-md">
<a
href="https://blockstream.com/lightning/greenlight/"
target="_blank"
rel="noopener noreferrer"
>
<q-img
contain
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/greenlight.png') }}' : '{{ static_url_for('static', 'images/greenlightl.png') }}'"
></q-img>
</a>
</div>
</div>
<div class="row">
<div class="col">
<a
+10
View File
@@ -258,6 +258,14 @@ class LnTipsFundingSource(LNbitsSettings):
lntips_invoice_key: Optional[str] = Field(default=None)
class BreezSdkFundingSource(LNbitsSettings):
breez_api_key: Optional[str] = Field(default=None)
breez_greenlight_seed: Optional[str] = Field(default=None)
breez_greenlight_invite_code: Optional[str] = Field(default=None)
breez_greenlight_device_key: Optional[str] = Field(default=None)
breez_greenlight_device_cert: Optional[str] = Field(default=None)
class LightningSettings(LNbitsSettings):
lightning_invoice_expiry: int = Field(default=3600)
@@ -279,6 +287,7 @@ class FundingSourcesSettings(
OpenNodeFundingSource,
SparkFundingSource,
LnTipsFundingSource,
BreezSdkFundingSource,
):
lnbits_backend_wallet_class: str = Field(default="VoidWallet")
@@ -423,6 +432,7 @@ class SuperUserSettings(LNbitsSettings):
default=[
"AlbyWallet",
"BlinkWallet",
"BreezSdkWallet",
"CoreLightningRestWallet",
"CoreLightningWallet",
"EclairWallet",
+1 -1
View File
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 469 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -165,6 +165,17 @@ Vue.component('lnbits-funding-sources', {
spark_url: 'Endpoint',
spark_token: 'Token'
}
],
[
'BreezSdkWallet',
'Breez SDK',
{
breez_api_key: 'Breez API Key',
breez_greenlight_seed: 'Greenlight Seed',
breez_greenlight_device_key: 'Greenlight Device Key',
breez_greenlight_device_cert: 'Greenlight Device Cert',
breez_greenlight_invite_code: 'Greenlight Invite Code'
}
]
]
}
+1
View File
@@ -9,6 +9,7 @@ from lnbits.wallets.base import Wallet
from .alby import AlbyWallet
from .blink import BlinkWallet
from .breez import BreezSdkWallet
from .cliche import ClicheWallet
from .corelightning import CoreLightningWallet
+281
View File
@@ -0,0 +1,281 @@
import base64
try:
import breez_sdk # type: ignore
BREEZ_SDK_INSTALLED = True
except ImportError:
BREEZ_SDK_INSTALLED = False
if not BREEZ_SDK_INSTALLED:
class BreezSdkWallet: # pyright: ignore
def __init__(self):
raise RuntimeError(
"Breez SDK is not installed. "
"Ask admin to run `poetry install -E breez` to install it."
)
else:
import asyncio
from pathlib import Path
from typing import AsyncGenerator, Optional
from loguru import logger
from lnbits import bolt11 as lnbits_bolt11
from lnbits.settings import settings
from .base import (
InvoiceResponse,
PaymentFailedStatus,
PaymentPendingStatus,
PaymentResponse,
PaymentStatus,
PaymentSuccessStatus,
StatusResponse,
UnsupportedError,
Wallet,
)
breez_event_queue: asyncio.Queue = asyncio.Queue()
def load_bytes(source: str, extension: str) -> Optional[bytes]:
# first check if it can be read from a file
if source.split(".")[-1] == extension:
with open(source, "rb") as f:
source_bytes = f.read()
return source_bytes
else:
# else check the source string can be converted from hex
try:
return bytes.fromhex(source)
except ValueError:
pass
# else convert from base64
try:
return base64.b64decode(source)
except Exception:
pass
return None
def load_greenlight_credentials() -> (
Optional[
breez_sdk.GreenlightCredentials # pyright: ignore[reportUnboundVariable]
]
):
if (
settings.breez_greenlight_device_key
and settings.breez_greenlight_device_cert
):
device_key_bytes = load_bytes(settings.breez_greenlight_device_key, "pem")
device_cert_bytes = load_bytes(settings.breez_greenlight_device_cert, "crt")
if not device_key_bytes or not device_cert_bytes:
raise ValueError(
"cannot initialize BreezSdkWallet: "
"cannot decode breez_greenlight_device_key "
"or breez_greenlight_device_cert"
)
return breez_sdk.GreenlightCredentials( # pyright: ignore[reportUnboundVariable]
developer_key=list(device_key_bytes),
developer_cert=list(device_cert_bytes),
)
return None
class SDKListener(
breez_sdk.EventListener # pyright: ignore[reportUnboundVariable]
):
def on_event(self, event):
logger.debug(event)
breez_event_queue.put_nowait(event)
class BreezSdkWallet(Wallet): # type: ignore[no-redef]
def __init__(self):
if not settings.breez_greenlight_seed:
raise ValueError(
"cannot initialize BreezSdkWallet: missing breez_greenlight_seed"
)
if not settings.breez_api_key:
raise ValueError(
"cannot initialize BreezSdkWallet: missing breez_api_key"
)
if (
settings.breez_greenlight_device_key
and not settings.breez_greenlight_device_cert
):
raise ValueError(
"cannot initialize BreezSdkWallet: "
"missing breez_greenlight_device_cert"
)
if (
settings.breez_greenlight_device_cert
and not settings.breez_greenlight_device_key
):
raise ValueError(
"cannot initialize BreezSdkWallet: "
"missing breez_greenlight_device_key"
)
self.config = breez_sdk.default_config(
breez_sdk.EnvironmentType.PRODUCTION,
settings.breez_api_key,
breez_sdk.NodeConfig.GREENLIGHT(
config=breez_sdk.GreenlightNodeConfig(
partner_credentials=load_greenlight_credentials(),
invite_code=settings.breez_greenlight_invite_code,
)
),
)
breez_sdk_working_dir = Path(settings.lnbits_data_folder, "breez-sdk")
breez_sdk_working_dir.mkdir(parents=True, exist_ok=True)
self.config.working_dir = breez_sdk_working_dir.absolute().as_posix()
try:
seed = breez_sdk.mnemonic_to_seed(settings.breez_greenlight_seed)
connect_request = breez_sdk.ConnectRequest(self.config, seed)
self.sdk_services = breez_sdk.connect(connect_request, SDKListener())
except Exception as exc:
logger.warning(exc)
raise ValueError(f"cannot initialize BreezSdkWallet: {exc!s}") from exc
async def cleanup(self):
self.sdk_services.disconnect()
async def status(self) -> StatusResponse:
try:
node_info: breez_sdk.NodeState = self.sdk_services.node_info()
except Exception as exc:
return StatusResponse(f"Failed to connect to breez, got: '{exc}...'", 0)
return StatusResponse(None, int(node_info.channels_balance_msat))
async def create_invoice(
self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
unhashed_description: Optional[bytes] = None,
**kwargs,
) -> InvoiceResponse:
# if description_hash or unhashed_description:
# raise UnsupportedError("description_hash and unhashed_description")
try:
if description_hash and not unhashed_description:
raise UnsupportedError(
"'description_hash' unsupported by Greenlight, provide"
" 'unhashed_description'"
)
breez_invoice: breez_sdk.ReceivePaymentResponse = (
self.sdk_services.receive_payment(
breez_sdk.ReceivePaymentRequest(
amount * 1000, # breez uses msat
(
unhashed_description.decode()
if unhashed_description
else memo
),
preimage=kwargs.get("preimage"),
opening_fee_params=None,
use_description_hash=True if unhashed_description else None,
)
)
)
return InvoiceResponse(
True,
breez_invoice.ln_invoice.payment_hash,
breez_invoice.ln_invoice.bolt11,
None,
)
except Exception as e:
logger.warning(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)
try:
send_payment_request = breez_sdk.SendPaymentRequest(bolt11=bolt11)
send_payment_response: breez_sdk.SendPaymentResponse = (
self.sdk_services.send_payment(send_payment_request)
)
payment: breez_sdk.Payment = send_payment_response.payment
except Exception as exc:
logger.warning(exc)
try:
# try to report issue to Breez to improve LSP routing
self.sdk_services.report_issue(
breez_sdk.ReportIssueRequest.PAYMENT_FAILURE(
breez_sdk.ReportPaymentFailureDetails(invoice.payment_hash)
)
)
except Exception as ex:
logger.info(ex)
# assume that payment failed?
return PaymentResponse(
False, None, None, None, f"payment failed: {exc}"
)
if payment.status != breez_sdk.PaymentStatus.COMPLETE:
return PaymentResponse(False, None, None, None, "payment is pending")
# let's use the payment_hash as the checking_id
checking_id = invoice.payment_hash
return PaymentResponse(
True,
checking_id,
payment.fee_msat,
payment.details.data.payment_preimage,
None,
)
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
try:
payment: breez_sdk.Payment = self.sdk_services.payment_by_hash(
checking_id
)
if payment is None:
return PaymentPendingStatus()
if payment.payment_type != breez_sdk.PaymentType.RECEIVED:
logger.warning(f"unexpected payment type: {payment.status}")
return PaymentPendingStatus()
if payment.status == breez_sdk.PaymentStatus.FAILED:
return PaymentFailedStatus()
if payment.status == breez_sdk.PaymentStatus.COMPLETE:
return PaymentSuccessStatus()
return PaymentPendingStatus()
except Exception as exc:
logger.warning(exc)
return PaymentPendingStatus()
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
try:
payment: breez_sdk.Payment = self.sdk_services.payment_by_hash(
checking_id
)
if payment is None:
return PaymentPendingStatus()
if payment.payment_type != breez_sdk.PaymentType.SENT:
logger.warning(f"unexpected payment type: {payment.status}")
return PaymentPendingStatus()
if payment.status == breez_sdk.PaymentStatus.COMPLETE:
return PaymentSuccessStatus(
fee_msat=payment.fee_msat,
preimage=payment.details.data.payment_preimage,
)
if payment.status == breez_sdk.PaymentStatus.FAILED:
return PaymentFailedStatus()
return PaymentPendingStatus()
except Exception as exc:
logger.warning(exc)
return PaymentPendingStatus()
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
while True:
event = await breez_event_queue.get()
if event.is_invoice_paid():
yield event.details.payment_hash