Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3372f7463a | ||
|
|
3f4fd93858 |
@@ -23,10 +23,3 @@ mypy.ini
|
||||
package-lock.json
|
||||
package.json
|
||||
pytest.ini
|
||||
|
||||
.mypy_cache
|
||||
.github
|
||||
.pytest_cache
|
||||
.vscode
|
||||
bin
|
||||
dist
|
||||
|
||||
@@ -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 #######
|
||||
######################################
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
+1
-1
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'
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user