Compare commits

..
Author SHA1 Message Date
Arc 3372f7463a defaults 2025-12-17 00:45:33 +00:00
Arc 3f4fd93858 boltz funding source seed fix
Users should be able to have a seed created or replace with their own
2025-12-17 00:42:39 +00:00
26 changed files with 129 additions and 366 deletions
-7
View File
@@ -23,10 +23,3 @@ mypy.ini
package-lock.json
package.json
pytest.ini
.mypy_cache
.github
.pytest_cache
.vscode
bin
dist
+3
View File
@@ -18,6 +18,7 @@ ENABLE_LOG_TO_FILE=true
# https://loguru.readthedocs.io/en/stable/api/logger.html#file
LOG_ROTATION="100 MB"
LOG_RETENTION="3 months"
# for database cleanup commands
# CLEANUP_WALLETS_DAYS=90
@@ -186,6 +187,7 @@ BOLTZ_CLIENT_ENDPOINT=127.0.0.1:9002
BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroons/admin.macaroon"
# HEXSTRING instead of path also possible
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert"
BOLTZ_CLIENT_WALLET="lnbits"
# StrikeWallet
STRIKE_API_ENDPOINT=https://api.strike.me/v1
@@ -331,3 +333,4 @@ LNBITS_RESERVE_FEE_PERCENT=1.0
######################################
###### Logging and Development #######
######################################
+1 -8
View File
@@ -75,14 +75,7 @@ jobs:
strategy:
matrix:
python-version: ["3.10"]
backend-wallet-class:
- BoltzWallet
- LndRestWallet
- LndWallet
- CoreLightningWallet
- CoreLightningRestWallet
- LNbitsWallet
- EclairWallet
backend-wallet-class: ["LndRestWallet", "LndWallet", "CoreLightningWallet", "CoreLightningRestWallet", "LNbitsWallet", "EclairWallet"]
with:
custom-pytest: "uv run pytest tests/regtest"
python-version: ${{ matrix.python-version }}
-1
View File
@@ -62,4 +62,3 @@ jobs:
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
build-args: LNBITS_TAG=${{ inputs.tag }}
+2 -4
View File
@@ -40,8 +40,8 @@ jobs:
run: |
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
cd docker
chmod +x ./start-regtest
./start-regtest
chmod +x ./tests
./tests
sudo chmod -R a+rwx .
- name: Run pytest
@@ -63,8 +63,6 @@ jobs:
LNBITS_ENDPOINT: http://localhost:5001
LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee"
ECLAIR_URL: http://127.0.0.1:8082
BOLTZ_CLIENT_ENDPOINT: 127.0.0.1:9002
BOLTZ_MNEMONIC: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
LNBITS_MAX_OUTGOING_PAYMENT_AMOUNT_SATS: 1000000000
LNBITS_MAX_INCOMING_PAYMENT_AMOUNT_SATS: 1000000000
ECLAIR_PASS: lnbits
-30
View File
@@ -10,29 +10,7 @@ permissions:
jobs:
release:
runs-on: ubuntu-24.04
outputs:
upload_url: ${{ steps.get_upload_url.outputs.upload_url }}
steps:
- uses: actions/checkout@v4
- name: Create github pre-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }}
run: |
gh release create "$tag" --prerelease --generate-notes --draft
- id: get_upload_url
name: Get upload url of Github release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }}
run: |
upload_url=$(gh release view "$tag" --json uploadUrl -q ".uploadUrl")
echo "upload_url=$upload_url" >> "$GITHUB_OUTPUT"
docker:
if: github.repository == 'lnbits/lnbits'
uses: ./.github/workflows/docker.yml
with:
tag: ${{ github.ref_name }}
@@ -41,7 +19,6 @@ jobs:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
pypi:
if: github.repository == 'lnbits/lnbits'
runs-on: ubuntu-24.04
steps:
- name: Install dependencies for building secp256k1
@@ -53,10 +30,3 @@ jobs:
uses: JRubics/poetry-publish@v1.15
with:
pypi_token: ${{ secrets.PYPI_API_KEY }}
appimage:
needs: [ release ]
uses: ./.github/workflows/appimage.yml
with:
tag_name: ${{ github.ref_name }}
upload_url: ${{ needs.release.outputs.upload_url }}
+2 -8
View File
@@ -14,7 +14,7 @@ jobs:
release:
runs-on: ubuntu-24.04
outputs:
upload_url: ${{ steps.get_upload_url.outputs.upload_url }}
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- uses: actions/checkout@v4
- name: Create github release
@@ -23,13 +23,7 @@ jobs:
tag: ${{ github.ref_name }}
run: |
gh release create "$tag" --generate-notes --draft
- id: get_upload_url
name: Get upload url of Github release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }}
run: |
upload_url=$(gh release view "$tag" --json uploadUrl -q ".uploadUrl")
upload_url=$(gh release view "$tag" --repo "${{ github.repository }}" --json uploadUrl -q ".uploadUrl")
echo "upload_url=$upload_url" >> "$GITHUB_OUTPUT"
docker:
+2 -3
View File
@@ -1,7 +1,6 @@
ARG LNBITS_TAG=latest
FROM boltz/boltz-client:latest AS boltz
FROM lnbits/lnbits:${LNBITS_TAG}
FROM lnbits/lnbits:latest
COPY --from=boltz /bin/boltzd /bin/boltzcli /usr/local/bin/
RUN ls -l /usr/local/bin/boltzd
+1 -1
View File
@@ -16,7 +16,7 @@ async def create_user_asset(user_id: str, file: UploadFile, is_public: bool) ->
if file.content_type.lower() not in settings.lnbits_assets_allowed_mime_types:
raise ValueError(f"File type '{file.content_type}' not allowed.")
if not settings.is_unlimited_assets_user(user_id):
if user_id not in settings.lnbits_assets_no_limit_users:
user_assets_count = await get_user_assets_count(user_id)
if user_assets_count >= settings.lnbits_max_assets_per_user:
raise ValueError(
+2 -2
View File
@@ -337,7 +337,7 @@ async def api_payment(payment_hash, x_api_key: str | None = Header(None)):
return {"paid": False, "status": "failed"}
try:
payment = await update_pending_payment(payment)
status = await payment.check_status()
except Exception:
if wallet and wallet.id == payment.wallet_id:
return {"paid": False, "details": payment}
@@ -346,7 +346,7 @@ async def api_payment(payment_hash, x_api_key: str | None = Header(None)):
if wallet and wallet.id == payment.wallet_id:
return {
"paid": payment.success,
"status": f"{payment.status!s}",
"status": f"{status!s}",
"preimage": payment.preimage,
"details": payment,
}
+1 -1
View File
@@ -249,7 +249,7 @@ async def check_account_id_exists(
account_id = cache.get(cache_key)
if account_id:
r.scope["user_id"] = account_id.id
await _check_user_access(r, account_id.id, conn=conn)
await _check_user_access(r, account_id, conn=conn)
return account_id
account = await _check_account_exists(r, access_token, usr, conn=conn)
-21
View File
@@ -35,7 +35,6 @@ class StripeTerminalOptions(BaseModel):
capture_method: Literal["automatic", "manual"] = "automatic"
metadata: dict[str, str] = Field(default_factory=dict)
reader_id: str | None = None
class StripeCheckoutOptions(BaseModel):
@@ -296,15 +295,6 @@ class StripeWallet(FiatProvider):
r.raise_for_status()
return r.json()
async def _process_terminal_payment_intent(
self, reader_id: str, payment_intent_id: str
) -> None:
data = {"payment_intent": payment_intent_id}
r = await self.client.post(
f"/v1/terminal/readers/{reader_id}/process_payment_intent", data=data
)
r.raise_for_status()
async def _create_checkout_invoice(
self,
amount_cents: int,
@@ -388,17 +378,6 @@ class StripeWallet(FiatProvider):
ok=False,
error_message="Error: missing PaymentIntent or client_secret",
)
if term.reader_id:
try:
await self._process_terminal_payment_intent(term.reader_id, pi_id)
except Exception as exc:
logger.warning(exc)
return FiatInvoiceResponse(
ok=False,
error_message=(
"Error: unable to process PaymentIntent on reader"
),
)
return FiatInvoiceResponse(
ok=True, checking_id=pi_id, payment_request=client_secret
)
+20 -7
View File
@@ -6,6 +6,7 @@ import inspect
import json
import os
import re
import secrets
from datetime import datetime, timezone
from enum import Enum
from os import path
@@ -18,6 +19,21 @@ from loguru import logger
from pydantic import BaseModel, BaseSettings, Extra, Field, validator
def generate_default_boltz_mnemonic() -> str | None:
"""
Generate a BIP39 mnemonic using the bundled embit dependency.
If embit is unavailable at runtime, return None to avoid blocking startup.
"""
try:
from embit import bip39
except Exception as exc: # pragma: no cover - dependency missing at runtime
logger.warning(f"Unable to generate Boltz mnemonic (embit missing): {exc}")
return None
entropy = secrets.token_bytes(16) # 128 bits -> 12-word mnemonic
return bip39.mnemonic_from_bytes(entropy)
def list_parse_fallback(v: str):
v = v.replace(" ", "")
if len(v) > 0:
@@ -308,12 +324,6 @@ class AssetSettings(LNbitsSettings):
lnbits_max_assets_per_user: int = Field(default=1, ge=0)
lnbits_assets_no_limit_users: list[str] = Field(default=[])
def is_unlimited_assets_user(self, user_id: str) -> bool:
return (
settings.is_admin_user(user_id)
or user_id in self.lnbits_assets_no_limit_users
)
class FeeSettings(LNbitsSettings):
lnbits_reserve_fee_min: int = Field(default=2000, ge=0)
@@ -607,9 +617,12 @@ class BreezLiquidSdkFundingSource(LNbitsSettings):
class BoltzFundingSource(LNbitsSettings):
boltz_client_endpoint: str | None = Field(default="127.0.0.1:9002")
boltz_client_macaroon: str | None = Field(default=None)
boltz_client_wallet: str | None = Field(default="lnbits")
boltz_client_password: str = Field(default="")
boltz_client_cert: str | None = Field(default=None)
boltz_mnemonic: str | None = Field(default=None)
boltz_mnemonic: str | None = Field(
default_factory=generate_default_boltz_mnemonic
)
class StrikeFundingSource(LNbitsSettings):
File diff suppressed because one or more lines are too long
@@ -89,14 +89,8 @@ window.app.component('lnbits-admin-funding-sources', {
lnd_rest_cert: 'Certificate',
lnd_rest_macaroon: 'Macaroon',
lnd_rest_macaroon_encrypted: 'Encrypted Macaroon',
lnd_rest_route_hints: {
advanced: true,
label: 'Enable Route Hints'
},
lnd_rest_allow_self_payment: {
advanced: true,
label: 'Allow Self Payment'
}
lnd_rest_route_hints: 'Enable Route Hints',
lnd_rest_allow_self_payment: 'Allow Self Payment'
}
],
[
@@ -166,27 +160,22 @@ window.app.component('lnbits-admin-funding-sources', {
'BoltzWallet',
'Boltz',
{
boltz_client_endpoint: {
label: 'Boltz client endpoint',
value: '127.0.0.1:9002'
},
boltz_client_endpoint: 'Endpoint',
boltz_client_macaroon: {
label: 'Admin Macaroon path or hex',
value: '/home/ubuntu/.boltz/macaroons/admin.macaroon'
default: '/home/ubuntu/.boltz/macaroons/admin.macaroon'
},
boltz_client_cert: {
label: 'Certificate path or hex',
value: '/home/ubuntu/.boltz/tls.cert'
default: '/home/ben/.boltz/tls.cert'
},
boltz_client_wallet: 'Wallet Name',
boltz_client_password: 'Wallet Password (can be empty)',
boltz_mnemonic: {
label: 'Liquid seed phrase',
hint: 'Boltz will fetch once connected, but you can change later (can be opened in a liquid wallet) ',
label: 'Liquid mnemonic (copy into greenwallet)',
copy: true,
qrcode: true
},
boltz_client_password: {
label: 'Wallet Password (optional)',
advanced: true
qrcode: true,
hint: 'Paste or type your liquid mnemonic'
}
}
],
+1 -2
View File
@@ -86,7 +86,6 @@ window.PageExtensions = {
version: release.version
})
.then(response => {
this.selectedExtension.inProgress = false
const extension = this.extensions.find(
ext => ext.id === this.selectedExtension.id
)
@@ -101,7 +100,7 @@ window.PageExtensions = {
})
.catch(err => {
console.warn(err)
this.selectedExtension.inProgress = false
extension.inProgress = false
LNbits.utils.notifyApiError(err)
})
},
@@ -35,9 +35,7 @@
>
<div
class="row"
v-for="([key, prop], i) in Object.entries(
fundingSources.get(fund)
).filter(([, p]) => !p.advanced)"
v-for="([key, prop], i) in Object.entries(fundingSources.get(fund))"
:key="i"
>
<div class="col-12">
@@ -48,7 +46,7 @@
:type="hideInput ? 'password' : 'text'"
:label="prop.label"
:hint="prop.hint"
:value="prop.value"
:readonly="prop.readonly || false"
>
<q-btn
v-if="prop.copy"
@@ -71,55 +69,6 @@
</q-input>
</div>
</div>
<q-expansion-item
v-if="
Object.entries(fundingSources.get(fund)).some(([, p]) => p.advanced)
"
dense
expand-separator
icon="tune"
label="Advanced"
class="q-mt-sm"
>
<div
class="row"
v-for="([key, prop], i) in Object.entries(
fundingSources.get(fund)
).filter(([, p]) => p.advanced)"
:key="`adv-${i}`"
>
<div class="col-12">
<q-input
v-model="formData[key]"
filled
class="q-mt-sm"
:type="hideInput ? 'password' : 'text'"
:label="prop.label"
:hint="prop.hint"
:readonly="prop.readonly || false"
>
<q-btn
v-if="prop.copy"
@click="utils.copyText(formData[key])"
icon="content_copy"
class="cursor-pointer"
color="grey"
flat
dense
></q-btn>
<q-btn
v-if="prop.qrcode"
@click="showQRValue(formData[key])"
icon="qr_code"
class="cursor-pointer"
color="grey"
flat
dense
></q-btn>
</q-input>
</div>
</div>
</q-expansion-item>
</div>
</q-list>
<q-dialog v-model="showQRDialog">
+59 -137
View File
@@ -5,7 +5,7 @@ from bolt11.decode import decode
from grpc.aio import AioRpcError
from loguru import logger
from lnbits.helpers import normalize_endpoint, sha256s
from lnbits.helpers import normalize_endpoint
from lnbits.settings import settings
from lnbits.wallets.boltz_grpc_files import boltzrpc_pb2, boltzrpc_pb2_grpc
from lnbits.wallets.lnd_grpc_files.lightning_pb2_grpc import grpc
@@ -38,18 +38,21 @@ class BoltzWallet(Wallet):
raise ValueError(
"cannot initialize BoltzWallet: missing boltz_client_endpoint"
)
if not settings.boltz_client_wallet:
raise ValueError(
"cannot initialize BoltzWallet: missing boltz_client_wallet"
)
self.endpoint = normalize_endpoint(
settings.boltz_client_endpoint, add_proto=True
)
self.metadata = None
if settings.boltz_client_macaroon:
try:
macaroon = load_macaroon(settings.boltz_client_macaroon)
self.metadata = [("macaroon", macaroon)]
except Exception as e:
logger.error(f"BoltzWallet failed to load macaroon: {e}")
self.metadata = [
("macaroon", load_macaroon(settings.boltz_client_macaroon))
]
else:
self.metadata = None
if settings.boltz_client_cert:
cert = open(settings.boltz_client_cert, "rb").read()
@@ -60,21 +63,31 @@ class BoltzWallet(Wallet):
self.rpc = boltzrpc_pb2_grpc.BoltzStub(channel)
self.wallet_id = 0
self.wallet_name = "lnbits"
self.wallet_ready = False
# Auto-create wallet if running in Docker mode
async def _init_boltz_wallet():
try:
wallet_name = settings.boltz_client_wallet or "lnbits"
mnemonic = await self._fetch_wallet(wallet_name)
if mnemonic:
logger.info(
"✅ Mnemonic found for Boltz wallet, saving to settings"
)
settings.boltz_mnemonic = mnemonic
from lnbits.core.crud.settings import set_settings_field
await set_settings_field("boltz_mnemonic", mnemonic)
else:
logger.warning("⚠️ No mnemonic returned from Boltz")
except Exception as e:
logger.error(f"❌ Failed to auto-create Boltz wallet: {e}")
self._init_wallet_task = asyncio.create_task(_init_boltz_wallet())
async def status(self) -> StatusResponse:
if self.wallet_ready is False:
self.wallet_ready = True
if settings.boltz_mnemonic: # restore wallet from mnemonic
await self._restore_boltz_wallet(
settings.boltz_mnemonic,
settings.boltz_client_password,
)
else: # create new wallet
await self._create_boltz_wallet()
try:
request = boltzrpc_pb2.GetWalletRequest(name=self.wallet_name)
request = boltzrpc_pb2.GetWalletRequest(name=settings.boltz_client_wallet)
response: boltzrpc_pb2.Wallet = await self.rpc.GetWallet(
request, metadata=self.metadata
)
@@ -113,18 +126,12 @@ class BoltzWallet(Wallet):
try:
response = await self.rpc.CreateReverseSwap(request, metadata=self.metadata)
except AioRpcError as exc:
logger.warning(exc)
return InvoiceResponse(ok=False, error_message=exc.details())
fee_msat = response.routing_fee_milli_sat
return InvoiceResponse(
ok=True,
checking_id=response.id,
payment_request=response.invoice,
fee_msat=fee_msat,
ok=True, checking_id=response.id, payment_request=response.invoice
)
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
pair = boltzrpc_pb2.Pair(**{"from": boltzrpc_pb2.LBTC})
try:
pair_info: boltzrpc_pb2.PairInfo
@@ -136,9 +143,8 @@ class BoltzWallet(Wallet):
if not invoice.amount_msat:
raise ValueError("amountless invoice")
service_fee: float = invoice.amount_msat * pair_info.fees.percentage / 100
estimate = int(service_fee + pair_info.fees.miner_fees * 1000)
estimate = service_fee + pair_info.fees.miner_fees * 1000
if estimate > fee_limit_msat:
error = f"fee of {estimate} msat exceeds limit of {fee_limit_msat} msat"
@@ -160,12 +166,11 @@ class BoltzWallet(Wallet):
if response.id == "":
# note that there is no way to provide a checking id here,
# but there is no need since it immediately is considered as successfull
logger.warning(
"Boltz invoice paid directly on liquid network using magic routing"
return PaymentResponse(
ok=True,
checking_id=response.id,
)
return PaymentResponse(ok=True, checking_id=invoice.payment_hash)
except AioRpcError as exc:
logger.warning(exc)
return PaymentResponse(ok=False, error_message=exc.details())
try:
@@ -175,15 +180,10 @@ class BoltzWallet(Wallet):
info_request, metadata=self.metadata
):
if info.swap.state == boltzrpc_pb2.SUCCESSFUL:
fee_msat = (info.swap.onchain_fee + info.swap.service_fee) * 1000
logger.debug(
f"Boltz swap successful, status: {info.swap.status}"
f"fee_msat: {fee_msat}"
)
return PaymentResponse(
ok=True,
checking_id=invoice.payment_hash,
fee_msat=fee_msat,
checking_id=response.id,
fee_msat=(info.swap.onchain_fee + info.swap.service_fee) * 1000,
preimage=info.swap.preimage,
)
elif info.swap.error != "":
@@ -192,29 +192,21 @@ class BoltzWallet(Wallet):
ok=False, error_message="stream stopped unexpectedly"
)
except AioRpcError as exc:
logger.warning(exc)
return PaymentResponse(ok=False, error_message=exc.details())
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
try:
request = boltzrpc_pb2.GetSwapInfoRequest(id=checking_id)
response: boltzrpc_pb2.GetSwapInfoResponse = await self.rpc.GetSwapInfo(
request,
metadata=self.metadata,
boltzrpc_pb2.GetSwapInfoRequest(id=checking_id), metadata=self.metadata
)
swap = response.reverse_swap
except AioRpcError as exc:
logger.warning(exc)
except AioRpcError:
return PaymentPendingStatus()
if swap.state == boltzrpc_pb2.SwapState.SUCCESSFUL:
fee_msat = (
swap.service_fee + swap.onchain_fee
) * 1000 + swap.routing_fee_msat
logger.debug(
f"Boltz swap successful, status: {swap.status}, fee_msat: {fee_msat}"
)
return PaymentSuccessStatus(
fee_msat=fee_msat,
fee_msat=(
(swap.service_fee + swap.onchain_fee) * 1000 + swap.routing_fee_msat
),
preimage=swap.preimage,
)
elif swap.state == boltzrpc_pb2.SwapState.PENDING:
@@ -224,23 +216,18 @@ class BoltzWallet(Wallet):
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
try:
checking_id_bytes = bytes.fromhex(checking_id)
request = boltzrpc_pb2.GetSwapInfoRequest(payment_hash=checking_id_bytes)
response: boltzrpc_pb2.GetSwapInfoResponse = await self.rpc.GetSwapInfo(
request,
boltzrpc_pb2.GetSwapInfoRequest(
payment_hash=bytes.fromhex(checking_id)
),
metadata=self.metadata,
)
swap = response.swap
except AioRpcError as exc:
logger.warning(exc)
except AioRpcError:
return PaymentPendingStatus()
if swap.state == boltzrpc_pb2.SwapState.SUCCESSFUL:
fee_msat = (swap.service_fee + swap.onchain_fee) * 1000
logger.debug(
f"Boltz swap successful, status: {swap.status}, fee_msat: {fee_msat}"
)
return PaymentSuccessStatus(
fee_msat=fee_msat,
fee_msat=(swap.service_fee + swap.onchain_fee) * 1000,
preimage=swap.preimage,
)
elif swap.state == boltzrpc_pb2.SwapState.PENDING:
@@ -257,16 +244,7 @@ class BoltzWallet(Wallet):
request, metadata=self.metadata
):
reverse = info.reverse_swap
if (
reverse
and reverse.state == boltzrpc_pb2.SUCCESSFUL
and reverse.status == "invoice.settled"
):
fee_msat = ((reverse.service_fee + reverse.onchain_fee) * 1000,)
logger.debug(
f"Boltz reverse swap settled: {reverse.id}, "
f"fee_msat: {fee_msat}"
)
if reverse and reverse.state == boltzrpc_pb2.SUCCESSFUL:
yield reverse.id
except Exception as exc:
logger.error(
@@ -275,80 +253,24 @@ class BoltzWallet(Wallet):
)
await asyncio.sleep(5)
async def _check_wallet_exists(
self, wallet_name: str
) -> boltzrpc_pb2.Wallet | None:
async def _fetch_wallet(self, wallet_name: str) -> str | None:
try:
request = boltzrpc_pb2.GetWalletRequest(name=wallet_name)
response = await self.rpc.GetWallet(request, metadata=self.metadata)
return response
except AioRpcError:
return None
logger.info(f"Wallet '{wallet_name}' already exists with ID {response.id}")
return settings.boltz_mnemonic
except AioRpcError as exc:
details = exc.details() or "unknown error"
if exc.code() != grpc.StatusCode.NOT_FOUND:
logger.error(f"Error checking wallet existence: {details}")
raise
async def _delete_wallet(self, wallet_id: int) -> None:
logger.info(f"Deleting wallet '{wallet_id}'")
delete_request = boltzrpc_pb2.RemoveWalletRequest(id=wallet_id)
await self.rpc.RemoveWallet(delete_request, metadata=self.metadata)
async def _restore_wallet(self, wallet_name: str, mnemonic: str, password: str):
logger.info(f"Restoring wallet '{wallet_name}' from mnemonic")
credentials = boltzrpc_pb2.WalletCredentials(mnemonic=mnemonic)
params = boltzrpc_pb2.WalletParams(
name=wallet_name,
currency=boltzrpc_pb2.LBTC,
password=password,
)
restore_request = boltzrpc_pb2.ImportWalletRequest(
credentials=credentials, params=params
)
response = await self.rpc.ImportWallet(restore_request, metadata=self.metadata)
return response
async def _create_wallet(self, wallet_name: str, password: str) -> str:
logger.info(f"Creating new wallet '{wallet_name}'")
params = boltzrpc_pb2.WalletParams(
name=wallet_name,
currency=boltzrpc_pb2.LBTC,
password=password,
password=settings.boltz_client_password,
)
create_request = boltzrpc_pb2.CreateWalletRequest(params=params)
response = await self.rpc.CreateWallet(create_request, metadata=self.metadata)
return response.mnemonic
async def _restore_boltz_wallet(self, mnemonic: str, password: str):
try:
# delete the wallet with to name without hashing first if it exists
wallet = await self._check_wallet_exists(self.wallet_name)
if wallet:
await self._delete_wallet(wallet.id)
# recreate the wallet with the hashed name
self.wallet_name = sha256s(mnemonic + password)
wallet = await self._check_wallet_exists(self.wallet_name)
if wallet:
logger.success("✅ Boltz wallet exists.")
return
await self._restore_wallet(self.wallet_name, mnemonic, password)
logger.success("✅ Boltz wallet restored from existing mnemonic")
except Exception as e:
logger.error(f"❌ Failed to restore Boltz wallet: {e}")
async def _create_boltz_wallet(self):
try:
# delete the wallet with to name without hashing first if it exists
wallet = await self._check_wallet_exists(self.wallet_name)
if wallet:
await self._delete_wallet(wallet.id)
logger.info("No Mnemonic found for Boltz wallet, creating wallet...")
mnemonic = await self._create_wallet(
self.wallet_name, settings.boltz_client_password
)
logger.success("✅ Boltz wallet created successfully")
settings.boltz_mnemonic = mnemonic
from lnbits.core.crud.settings import set_settings_field
await set_settings_field("boltz_mnemonic", mnemonic)
except Exception as e:
logger.error(f"❌ Failed to create Boltz wallet: {e}")
Generated
+9 -4
View File
@@ -1333,16 +1333,21 @@ pyjwt = ">=2.10.1,<3.0.0"
[[package]]
name = "filelock"
version = "3.20.1"
version = "3.18.0"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.10"
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a"},
{file = "filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c"},
{file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"},
{file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"},
]
[package.extras]
docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"]
typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""]
[[package]]
name = "filetype"
version = "1.2.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "lnbits"
version = "1.4.0"
version = "1.4.0-rc2"
requires-python = ">=3.10,<3.13"
description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
+2 -2
View File
@@ -331,8 +331,8 @@ def _settings_cleanup(settings: Settings):
settings.lnbits_allowed_users = []
settings.auth_allowed_methods = AuthMethods.all()
settings.auth_credetials_update_threshold = 120
settings.lnbits_reserve_fee_percent = 2
settings.lnbits_reserve_fee_min = 20000
settings.lnbits_reserve_fee_percent = 1
settings.lnbits_reserve_fee_min = 2000
settings.lnbits_service_fee = 0
settings.lnbits_reserve_fee_percent = 0
settings.lnbits_wallet_limit_daily_max_withdraw = 0
+1 -22
View File
@@ -6,11 +6,6 @@ from subprocess import PIPE, Popen, TimeoutExpired
from loguru import logger
from lnbits.wallets import get_funding_source
funding_source = get_funding_source()
is_boltz_wallet = funding_source.__class__.__name__ == "BoltzWallet"
docker_lightning_cli = [
"docker",
"exec",
@@ -32,16 +27,6 @@ docker_bitcoin_cli = [
]
docker_elements_cli = [
"docker",
"exec",
"lnbits-elementsd-1",
"elements-cli",
"-rpcport=18884",
"-chain=liquidregtest",
]
docker_lightning_unconnected_cli = [
"docker",
"exec",
@@ -65,7 +50,7 @@ docker_lightning_noroute_cli = [
def run_cmd(cmd: list) -> str:
timeout = 30
timeout = 10
process = Popen(cmd, stdout=PIPE, stderr=PIPE)
logger.debug(f"running command: {cmd}")
@@ -143,12 +128,6 @@ def mine_blocks(blocks: int = 1) -> str:
return run_cmd(cmd)
def mine_blocks_liquid(blocks: int = 1) -> str:
cmd = docker_elements_cli.copy()
cmd.extend(["-generate", str(blocks)])
return run_cmd(cmd)
def get_unconnected_node_uri() -> str:
cmd = docker_lightning_unconnected_cli.copy()
cmd.append("getinfo")
+4 -12
View File
@@ -18,7 +18,6 @@ from ..helpers import FakeError, is_fake, is_regtest
from .helpers import (
cancel_invoice,
get_real_invoice,
mine_blocks_liquid,
pay_real_invoice,
settle_invoice,
)
@@ -67,7 +66,7 @@ async def test_pay_real_invoice(
await asyncio.sleep(1)
balance = await get_node_balance_sats()
assert prev_balance - balance == 100 + abs(payment.fee // 1000)
assert prev_balance - balance == 100
@pytest.mark.anyio
@@ -154,8 +153,7 @@ async def test_create_real_invoice(client, adminkey_headers_from, inkey_headers_
async def on_paid(payment: Payment):
assert payment.payment_hash == invoice["payment_hash"]
assert payment.checking_id == invoice["checking_id"]
assert payment.checking_id == invoice["payment_hash"]
response = await client.get(
f'/api/v1/payments/{invoice["payment_hash"]}', headers=inkey_headers_from
@@ -166,8 +164,7 @@ async def test_create_real_invoice(client, adminkey_headers_from, inkey_headers_
await asyncio.sleep(1)
balance = await get_node_balance_sats()
fee = abs(payment_status.get("details", {}).get("fee", 0) // 1000)
assert balance - prev_balance == create_invoice.amount - fee
assert balance - prev_balance == create_invoice.amount
assert payment_status.get("preimage") is not None
@@ -203,8 +200,6 @@ async def test_pay_real_invoice_set_pending_and_check_state(
assert len(invoice["payment_hash"]) == 64
assert len(invoice["checking_id"]) > 0
mine_blocks_liquid(1)
await asyncio.sleep(1)
# check the payment status
response = await client.get(
f'/api/v1/payments/{invoice["payment_hash"]}', headers=inkey_headers_from
@@ -346,7 +341,6 @@ async def test_pay_hold_invoice_check_pending_and_fail_cancel_payment_task_in_me
assert preimage_hash == invoice_obj.payment_hash
cancel_invoice(preimage_hash)
mine_blocks_liquid(1)
# check if paid
await asyncio.sleep(1)
@@ -388,9 +382,7 @@ async def test_receive_real_invoice_set_pending_and_check_state(
assert not payment_status["paid"]
async def on_paid(payment: Payment):
assert payment.payment_hash == invoice["payment_hash"]
assert payment.checking_id == invoice["checking_id"]
assert payment.checking_id == invoice["payment_hash"]
response = await client.get(
f'/api/v1/payments/{invoice["payment_hash"]}', headers=inkey_headers_from
+2 -10
View File
@@ -7,8 +7,6 @@ from lnbits.core.services import (
from lnbits.wallets import get_funding_source
from lnbits.wallets.base import PaymentStatus
from .helpers import is_boltz_wallet
description = "test create invoice"
@@ -19,10 +17,7 @@ async def test_create_invoice(from_wallet):
amount=1000,
memo=description,
)
# we cannot know the preimage of the swap yet
if not is_boltz_wallet:
assert payment.preimage
assert payment.preimage
invoice = decode(payment.bolt11)
assert invoice.payment_hash == payment.payment_hash
@@ -40,10 +35,7 @@ async def test_create_internal_invoice(from_wallet):
payment = await create_invoice(
wallet_id=from_wallet.id, amount=1000, memo=description, internal=True
)
# we cannot know the preimage of the swap yet
if not is_boltz_wallet:
assert payment.preimage
assert payment.preimage
invoice = decode(payment.bolt11)
assert invoice.payment_hash == payment.payment_hash
+2 -7
View File
@@ -6,8 +6,6 @@ from lnbits.core.services import (
)
from lnbits.exceptions import PaymentError
from .helpers import is_boltz_wallet
description = "test pay invoice"
@@ -19,12 +17,9 @@ async def test_services_pay_invoice(to_wallet, real_invoice):
description=description,
)
assert payment
assert payment.status == PaymentState.SUCCESS
assert payment.memo == description
if not is_boltz_wallet:
assert payment.status == PaymentState.SUCCESS
assert payment.preimage
else:
assert payment.status == PaymentState.PENDING
assert payment.preimage
@pytest.mark.anyio
Generated
+1 -1
View File
@@ -1247,7 +1247,7 @@ wheels = [
[[package]]
name = "lnbits"
version = "1.4.0"
version = "1.4.0rc2"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },