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
parent 235f8a6c19
commit 0015314e11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 390 additions and 4 deletions

View file

@ -28,7 +28,7 @@ PORT=5000
######################################
# which fundingsources are allowed in the admin ui
LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet"
LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet, BreezSdkWallet"
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
@ -117,6 +117,14 @@ ECLAIR_PASS=eclairpw
LNTIPS_API_KEY=LNTIPS_ADMIN_KEY
LNTIPS_API_ENDPOINT=https://ln.tips
# BreezSdkWallet
BREEZ_API_KEY=KEY
BREEZ_GREENLIGHT_SEED=SEED
# A Greenlight invite code or Greenlight partner certificate/key can be used
BREEZ_GREENLIGHT_INVITE_CODE=CODE
BREEZ_GREENLIGHT_DEVICE_KEY="/path/to/breezsdk/device.pem" # or BASE64/HEXSTRING
BREEZ_GREENLIGHT_DEVICE_CERT="/path/to/breezsdk/device.crt" # or BASE64/HEXSTRING
######################################
####### Auth Configurations ##########
######################################

View file

@ -110,6 +110,17 @@ For the invoice to work you must have a publicly accessible URL in your LNbits.
- `PHOENIXD_API_ENDPOINT`: http://localhost:9740/
- `PHOENIXD_API_PASSWORD`: PhoenixdApiPassword
### Breez SDK
A Greenlight invite code or Greenlight partner certificate/key can be used to register a new node with Greenlight. If the Greenlight node already exists, neither are required.
- `LNBITS_BACKEND_WALLET_CLASS`: **BreezSdkWallet**
- `BREEZ_API_KEY`: ...
- `BREEZ_GREENLIGHT_SEED`: ...
- `BREEZ_GREENLIGHT_INVITE_CODE`: ...
- `BREEZ_GREENLIGHT_DEVICE_KEY`: /path/to/breezsdk/device.pem or Base64/Hex
- `BREEZ_GREENLIGHT_DEVICE_CERT`: /path/to/breezsdk/device.crt or Base64/Hex
### Cliche Wallet
- `CLICHE_ENDPOINT`: ws://127.0.0.1:12000

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

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",

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

View file

@ -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'
}
]
]
}

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
lnbits/wallets/breez.py Normal file
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

39
poetry.lock generated
View file

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "anyio"
@ -329,6 +329,40 @@ click = "*"
ecdsa = "*"
secp256k1 = "*"
[[package]]
name = "breez-sdk"
version = "0.5.0"
description = "Python language bindings for the Breez SDK"
optional = true
python-versions = "*"
files = [
{file = "breez_sdk-0.5.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6a73f8b8c2b74798278b8d4ca7757738d7425d216b0edbe281d4260e6a7a331d"},
{file = "breez_sdk-0.5.0-cp310-cp310-manylinux_2_31_aarch64.whl", hash = "sha256:c46bf3ebd3f184d1c9d72c81eeb3b134ecae87551b979a3bc4379d70216451f6"},
{file = "breez_sdk-0.5.0-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:fd8b285c855f3d3c464962b8a3add5ccd01f57a7f37ddab36de36481903aa63a"},
{file = "breez_sdk-0.5.0-cp310-cp310-win32.whl", hash = "sha256:bff9ea24736e847922e008ad83799d388f2112e701376decb56a9023402ce5ce"},
{file = "breez_sdk-0.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:ba94423afe27eec0e35191a4f288011c1e8b3174d029154fc87f5dc1cd9afdad"},
{file = "breez_sdk-0.5.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a76bdd69b144f796c299342f1989c3f73e05c969c8698d10f002465033ed8d3a"},
{file = "breez_sdk-0.5.0-cp311-cp311-manylinux_2_31_aarch64.whl", hash = "sha256:62c70ebc7c77cb948beb680c8f506b0dff6588826b0f3a07eb219fa0c45e5595"},
{file = "breez_sdk-0.5.0-cp311-cp311-manylinux_2_31_x86_64.whl", hash = "sha256:b48fa4e5ae6b5299fc7dc865a52a14ad2eb95736840e0eb12d6136a3aeeb1763"},
{file = "breez_sdk-0.5.0-cp311-cp311-win32.whl", hash = "sha256:2bd289fbf7da5249022060fd3f4c53b29fa25aefbde7c4fc4c0557fef6eef00a"},
{file = "breez_sdk-0.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:987f52476cf7726720535b5d04c3b86f469094284298fef3404dafad5c854342"},
{file = "breez_sdk-0.5.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:9c87c691c6fee3d87a12bac1a03e4370ba97142f9e8e74af900f27b98704efa5"},
{file = "breez_sdk-0.5.0-cp312-cp312-manylinux_2_31_aarch64.whl", hash = "sha256:1e5283bb734c814097b62c681341aff687dc5e894ace2e9845e977e04dba6dd3"},
{file = "breez_sdk-0.5.0-cp312-cp312-manylinux_2_31_x86_64.whl", hash = "sha256:d9950e90710a04167c5da6990d84d4212361fc0119b5bdfcfd2ba8bb14511327"},
{file = "breez_sdk-0.5.0-cp312-cp312-win32.whl", hash = "sha256:39549ee886dc5f50c0e7961d3789705de1795d1551f397f74b5c6e5c82be09ed"},
{file = "breez_sdk-0.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:b2bac63004779e41d3c89c26d89162e16b37969725aa87b64287163d79a86f03"},
{file = "breez_sdk-0.5.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:f7470682dc6eec94bd82e968b124a9e261bd286bf0c97880ded3f53609b570c5"},
{file = "breez_sdk-0.5.0-cp38-cp38-manylinux_2_31_aarch64.whl", hash = "sha256:fefde0e66380489f4cde08d6dbcd1fdacd68adc229c1bf6b60e1d6c18c342cb6"},
{file = "breez_sdk-0.5.0-cp38-cp38-manylinux_2_31_x86_64.whl", hash = "sha256:fac91a5afafaffe2de6586b0e2a65253e1c01ad882aebc27335d7ef6486b3400"},
{file = "breez_sdk-0.5.0-cp38-cp38-win32.whl", hash = "sha256:fd901b0576db9e20e923086e2009264a1230d0a1eaeb8ca1e197fa1dd1f8b30c"},
{file = "breez_sdk-0.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:577648a87a5a552199a0422174aea47ba8a5ed9d18d8afd249e88e0fa0a61309"},
{file = "breez_sdk-0.5.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:a643641f7c3586bd13e491e5a3778b5b2dc222ebb072b3fd9084366176948846"},
{file = "breez_sdk-0.5.0-cp39-cp39-manylinux_2_31_aarch64.whl", hash = "sha256:5367652c5389c5fcc65a779ebc52322327af3b4ac343448f3ac9254f4a305f43"},
{file = "breez_sdk-0.5.0-cp39-cp39-manylinux_2_31_x86_64.whl", hash = "sha256:40aead32105c42ebb0c7389718d33bd7384eab633fde46a47034b71fa28493cb"},
{file = "breez_sdk-0.5.0-cp39-cp39-win32.whl", hash = "sha256:636804daa36264317a45f905f0c7d881ace7259d84654577f969aedfc27c8de8"},
{file = "breez_sdk-0.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:9fb8335ae6c8d71a47350d53965194cf803e73bd4cba1f658fe0ab556a9d8471"},
]
[[package]]
name = "certifi"
version = "2024.7.4"
@ -2995,9 +3029,10 @@ doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linke
test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
[extras]
breez = ["breez-sdk"]
liquid = ["wallycore"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10 | ^3.9"
content-hash = "3795e179851835839e5f89f51704309942915386a67a00b7c9408ff78b4fea41"
content-hash = "6890197bd92c72c54aec6b498ddd6b6df51b95884f6ad29dd3655d8e4c9cfd00"

View file

@ -57,8 +57,11 @@ environs = "9.5.0"
python-crontab = "3.0.0"
# needed for liquid support boltz
wallycore = {version = "^1.0.0", optional = true}
# needed for breez funding source
breez-sdk = {version = "0.5.0", optional = true}
[tool.poetry.extras]
breez = ["breez-sdk"]
liquid = ["wallycore"]
[tool.poetry.group.dev.dependencies]