Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79c3d7526e |
@@ -10,16 +10,15 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.head_ref || github.event.pull_request.head.sha }}
|
ref: ${{ github.head_ref }}
|
||||||
- uses: lnbits/lnbits/.github/actions/prepare@dev
|
- uses: lnbits/lnbits/.github/actions/prepare@dev
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
node-version: "24.x"
|
node-version: "24.x"
|
||||||
npm: true
|
npm: true
|
||||||
- name: Build and commit bundle (same-repo PR)
|
- run: make bundle
|
||||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
- name: Commit and push bundle changes
|
||||||
run: |
|
run: |
|
||||||
make bundle
|
|
||||||
git config user.name "alan"
|
git config user.name "alan"
|
||||||
git config user.email "alan@lnbits.com"
|
git config user.email "alan@lnbits.com"
|
||||||
git add lnbits/static
|
git add lnbits/static
|
||||||
@@ -28,6 +27,3 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
git commit -m "chore: make bundle [skip ci]"
|
git commit -m "chore: make bundle [skip ci]"
|
||||||
git push
|
git push
|
||||||
- name: Check bundle is up-to-date (fork PR)
|
|
||||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
|
||||||
run: make checkbundle
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ nav_order: 1
|
|||||||
sudo apt-get install jq libfuse2
|
sudo apt-get install jq libfuse2
|
||||||
wget $(curl -s https://api.github.com/repos/lnbits/lnbits/releases/latest | jq -r '.assets[] | select(.name | endswith(".AppImage")) | .browser_download_url') -O LNbits-latest.AppImage
|
wget $(curl -s https://api.github.com/repos/lnbits/lnbits/releases/latest | jq -r '.assets[] | select(.name | endswith(".AppImage")) | .browser_download_url') -O LNbits-latest.AppImage
|
||||||
chmod +x LNbits-latest.AppImage
|
chmod +x LNbits-latest.AppImage
|
||||||
LNBITS_ADMIN_UI=true HOST=0.0.0.0 PORT=5000 AUTH_HTTPS_ONLY=false ./LNbits-latest.AppImage # most system settings are now in the admin UI, but pass additional .env variables here
|
LNBITS_ADMIN_UI=true HOST=0.0.0.0 PORT=5000 ./LNbits-latest.AppImage # most system settings are now in the admin UI, but pass additional .env variables here
|
||||||
```
|
```
|
||||||
|
|
||||||
- LNbits will create a folder for DB and extension files **in the same directory** as the AppImage.
|
- LNbits will create a folder for DB and extension files **in the same directory** as the AppImage.
|
||||||
@@ -285,7 +285,10 @@ but you can also set the env variables or pass command line arguments:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
# .env variables are currently passed when running, but LNbits can be managed with the admin UI.
|
# .env variables are currently passed when running, but LNbits can be managed with the admin UI.
|
||||||
LNBITS_ADMIN_UI=true AUTH_HTTPS_ONLY=false ./result/bin/lnbits --port 9000 --host 0.0.0.0
|
LNBITS_ADMIN_UI=true ./result/bin/lnbits --port 9000 --host 0.0.0.0
|
||||||
|
|
||||||
|
# Once you have created a user, you can set as the super_user
|
||||||
|
SUPER_USER=be54db7f245346c8833eaa430e1e0405 LNBITS_ADMIN_UI=true ./result/bin/lnbits --port 9000
|
||||||
```
|
```
|
||||||
|
|
||||||
> 
|
> 
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import httpx
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.crud import get_wallet
|
from lnbits.core.crud import get_wallet
|
||||||
from lnbits.core.crud.payments import create_payment, update_payment
|
from lnbits.core.crud.payments import create_payment
|
||||||
from lnbits.core.models import CreatePayment, Payment, PaymentState
|
from lnbits.core.models import CreatePayment, Payment, PaymentState
|
||||||
from lnbits.core.models.misc import SimpleStatus
|
from lnbits.core.models.misc import SimpleStatus
|
||||||
from lnbits.db import Connection
|
from lnbits.db import Connection
|
||||||
@@ -57,8 +57,6 @@ async def check_fiat_status(payment: Payment) -> FiatPaymentStatus:
|
|||||||
fiat_status = await fiat_provider.get_invoice_status(checking_id)
|
fiat_status = await fiat_provider.get_invoice_status(checking_id)
|
||||||
|
|
||||||
if fiat_status.success:
|
if fiat_status.success:
|
||||||
payment.status = PaymentState.SUCCESS.value
|
|
||||||
await update_payment(payment)
|
|
||||||
await handle_fiat_payment_confirmation(payment)
|
await handle_fiat_payment_confirmation(payment)
|
||||||
|
|
||||||
# notify receivers asynchronously
|
# notify receivers asynchronously
|
||||||
|
|||||||
@@ -129,6 +129,15 @@ async def create_fiat_invoice(
|
|||||||
if not fiat_provider_name:
|
if not fiat_provider_name:
|
||||||
raise ValueError("Fiat provider is required for fiat invoices.")
|
raise ValueError("Fiat provider is required for fiat invoices.")
|
||||||
if not settings.is_fiat_provider_enabled(fiat_provider_name):
|
if not settings.is_fiat_provider_enabled(fiat_provider_name):
|
||||||
|
funding_source = get_funding_source()
|
||||||
|
if funding_source.__class__.__name__ == "LNbitsWallet":
|
||||||
|
fiat_invoice = await create_wallet_invoice(wallet_id, invoice_data)
|
||||||
|
fiat_invoice.fiat_provider = fiat_provider_name
|
||||||
|
fiat_invoice.extra["fiat_checking_id"] = fiat_invoice.checking_id
|
||||||
|
# TODO: move to payment
|
||||||
|
fiat_invoice.extra["fiat_payment_request"] = fiat_invoice.payment_request
|
||||||
|
|
||||||
|
return fiat_invoice
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Fiat provider '{fiat_provider_name}' is not enabled.",
|
f"Fiat provider '{fiat_provider_name}' is not enabled.",
|
||||||
)
|
)
|
||||||
@@ -219,6 +228,7 @@ async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
|
|||||||
payment_hash=data.payment_hash,
|
payment_hash=data.payment_hash,
|
||||||
labels=data.labels,
|
labels=data.labels,
|
||||||
external_id=data.external_id,
|
external_id=data.external_id,
|
||||||
|
fiat_provider=data.fiat_provider,
|
||||||
conn=conn,
|
conn=conn,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -261,6 +271,7 @@ async def create_invoice(
|
|||||||
payment_hash: str | None = None,
|
payment_hash: str | None = None,
|
||||||
labels: list[str] | None = None,
|
labels: list[str] | None = None,
|
||||||
external_id: str | None = None,
|
external_id: str | None = None,
|
||||||
|
fiat_provider: str | None = None,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
if not amount > 0:
|
if not amount > 0:
|
||||||
@@ -321,6 +332,9 @@ async def create_invoice(
|
|||||||
description_hash=description_hash,
|
description_hash=description_hash,
|
||||||
unhashed_description=unhashed_description,
|
unhashed_description=unhashed_description,
|
||||||
expiry=expiry or settings.lightning_invoice_expiry,
|
expiry=expiry or settings.lightning_invoice_expiry,
|
||||||
|
fiat_provider=fiat_provider,
|
||||||
|
currency=currency if currency != "sat" else None,
|
||||||
|
fiat_amount=amount if currency != "sat" else None,
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
not invoice_response.ok
|
not invoice_response.ok
|
||||||
@@ -332,6 +346,8 @@ async def create_invoice(
|
|||||||
status="pending",
|
status="pending",
|
||||||
)
|
)
|
||||||
invoice = bolt11_decode(invoice_response.payment_request)
|
invoice = bolt11_decode(invoice_response.payment_request)
|
||||||
|
extra["fiat_provider"] = fiat_provider
|
||||||
|
extra["fiat_payment_request"] = invoice_response.fiat_payment_request
|
||||||
|
|
||||||
create_payment_model = CreatePayment(
|
create_payment_model = CreatePayment(
|
||||||
wallet_id=user_wallet.source_wallet_id,
|
wallet_id=user_wallet.source_wallet_id,
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+10
-10
File diff suppressed because one or more lines are too long
@@ -27,14 +27,6 @@ const DynamicComponent = {
|
|||||||
name: r.name,
|
name: r.name,
|
||||||
component: async () => {
|
component: async () => {
|
||||||
await LNbits.utils.loadTemplate(r.template)
|
await LNbits.utils.loadTemplate(r.template)
|
||||||
if (r.i18n) {
|
|
||||||
const locale =
|
|
||||||
window.i18n?.global?.locale?.value ??
|
|
||||||
window.i18n?.global?.locale ??
|
|
||||||
window.g.locale ??
|
|
||||||
'en'
|
|
||||||
await LNbits.utils.loadExtI18n(r.i18n, locale)
|
|
||||||
}
|
|
||||||
await LNbits.utils.loadScript(r.component)
|
await LNbits.utils.loadScript(r.component)
|
||||||
return window[r.name]
|
return window[r.name]
|
||||||
}
|
}
|
||||||
@@ -159,30 +151,6 @@ window.i18n = new VueI18n.createI18n({
|
|||||||
fallbackLocale: 'en',
|
fallbackLocale: 'en',
|
||||||
messages: window.localisation
|
messages: window.localisation
|
||||||
})
|
})
|
||||||
;(function () {
|
|
||||||
let _applying = false
|
|
||||||
let _target = null
|
|
||||||
Vue.watch(
|
|
||||||
() => window.i18n.global.locale,
|
|
||||||
async (locale, prevLocale) => {
|
|
||||||
if (_applying || !LNbits.utils._extI18nDirs.size) return
|
|
||||||
_target = locale
|
|
||||||
_applying = true
|
|
||||||
window.i18n.global.locale = prevLocale
|
|
||||||
_applying = false
|
|
||||||
await Promise.all(
|
|
||||||
[...LNbits.utils._extI18nDirs].map(dir =>
|
|
||||||
LNbits.utils.loadExtI18n(dir, locale)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (_target !== locale) return
|
|
||||||
_applying = true
|
|
||||||
window.i18n.global.locale = locale
|
|
||||||
_applying = false
|
|
||||||
},
|
|
||||||
{flush: 'sync'}
|
|
||||||
)
|
|
||||||
})()
|
|
||||||
|
|
||||||
window.app.mixin({
|
window.app.mixin({
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@@ -323,20 +323,6 @@ window._lnbitsUtils = {
|
|||||||
converter.setOption('simpleLineBreaks', true)
|
converter.setOption('simpleLineBreaks', true)
|
||||||
return converter.makeHtml(text)
|
return converter.makeHtml(text)
|
||||||
},
|
},
|
||||||
_extI18nDirs: new Set(),
|
|
||||||
_extI18nLoaded: {},
|
|
||||||
loadExtI18n(dir, locale) {
|
|
||||||
this._extI18nDirs.add(dir)
|
|
||||||
const loaded = (this._extI18nLoaded[dir] ??= {})
|
|
||||||
if (loaded[locale]) return loaded[locale]
|
|
||||||
loaded[locale] = this.loadScript(`${dir}/${locale}.js`).catch(() => {
|
|
||||||
if (locale !== 'en') {
|
|
||||||
loaded['en'] ??= this.loadScript(`${dir}/en.js`).catch(() => {})
|
|
||||||
return loaded['en']
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return loaded[locale]
|
|
||||||
},
|
|
||||||
async decryptLnurlPayAES(success_action, preimage) {
|
async decryptLnurlPayAES(success_action, preimage) {
|
||||||
let keyb = new Uint8Array(
|
let keyb = new Uint8Array(
|
||||||
preimage.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16))
|
preimage.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16))
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class InvoiceResponse(NamedTuple):
|
|||||||
ok: bool
|
ok: bool
|
||||||
checking_id: str | None = None # payment_hash, rpc_id
|
checking_id: str | None = None # payment_hash, rpc_id
|
||||||
payment_request: str | None = None
|
payment_request: str | None = None
|
||||||
|
fiat_payment_request: str | None = None
|
||||||
error_message: str | None = None
|
error_message: str | None = None
|
||||||
preimage: str | None = None
|
preimage: str | None = None
|
||||||
fee_msat: int | None = None
|
fee_msat: int | None = None
|
||||||
|
|||||||
@@ -78,17 +78,28 @@ class LNbitsWallet(Wallet):
|
|||||||
data: dict = {"out": False, "amount": amount, "memo": memo or ""}
|
data: dict = {"out": False, "amount": amount, "memo": memo or ""}
|
||||||
if kwargs.get("expiry"):
|
if kwargs.get("expiry"):
|
||||||
data["expiry"] = kwargs["expiry"]
|
data["expiry"] = kwargs["expiry"]
|
||||||
|
if kwargs.get("fiat_provider"):
|
||||||
|
if "currency" not in kwargs or "fiat_amount" not in kwargs:
|
||||||
|
return InvoiceResponse(
|
||||||
|
ok=False,
|
||||||
|
error_message="Fiat provider requires "
|
||||||
|
"'currency' and 'fiat_amount' parameters.",
|
||||||
|
)
|
||||||
|
data["amount"] = kwargs["fiat_amount"]
|
||||||
|
data["unit"] = kwargs["currency"]
|
||||||
|
data["fiat_provider"] = kwargs["fiat_provider"]
|
||||||
|
|
||||||
if description_hash:
|
if description_hash:
|
||||||
data["description_hash"] = description_hash.hex()
|
data["description_hash"] = description_hash.hex()
|
||||||
if unhashed_description:
|
if unhashed_description:
|
||||||
data["unhashed_description"] = unhashed_description.hex()
|
data["unhashed_description"] = unhashed_description.hex()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = await self.client.post(url="/api/v1/payments", json=data)
|
print(f"Creating invoice with data: {data}")
|
||||||
|
r = await self.client.post(url="/api/v1/payments", json=data, timeout=30)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
# Backwards compatibility for pre-v1 which used the key "payment_request"
|
|
||||||
payment_str = data.get("bolt11") or data.get("payment_request")
|
payment_str = data.get("bolt11") or data.get("payment_request")
|
||||||
if r.is_error or not payment_str:
|
if r.is_error or not payment_str:
|
||||||
error_message = data["detail"] if "detail" in data else r.text
|
error_message = data["detail"] if "detail" in data else r.text
|
||||||
@@ -100,6 +111,7 @@ class LNbitsWallet(Wallet):
|
|||||||
ok=True,
|
ok=True,
|
||||||
checking_id=data["checking_id"],
|
checking_id=data["checking_id"],
|
||||||
payment_request=payment_str,
|
payment_request=payment_str,
|
||||||
|
fiat_payment_request=data.get("payment_request"),
|
||||||
preimage=data.get("preimage"),
|
preimage=data.get("preimage"),
|
||||||
)
|
)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
@@ -111,6 +123,11 @@ class LNbitsWallet(Wallet):
|
|||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
ok=False, error_message="Server error: 'missing required fields'"
|
ok=False, error_message="Server error: 'missing required fields'"
|
||||||
)
|
)
|
||||||
|
except httpx.HTTPStatusError as exc:
|
||||||
|
logger.warning(exc.response.text)
|
||||||
|
return InvoiceResponse(
|
||||||
|
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
@@ -213,6 +230,7 @@ class LNbitsWallet(Wallet):
|
|||||||
logger.info("connected to LNbits fundingsource websocket.")
|
logger.info("connected to LNbits fundingsource websocket.")
|
||||||
while settings.lnbits_running:
|
while settings.lnbits_running:
|
||||||
message = await ws.recv()
|
message = await ws.recv()
|
||||||
|
print(f"### Received message from websocket: {message}")
|
||||||
message_dict = json.loads(message)
|
message_dict = json.loads(message)
|
||||||
if (
|
if (
|
||||||
message_dict
|
message_dict
|
||||||
|
|||||||
@@ -117,12 +117,11 @@ class PhoenixdWallet(Wallet):
|
|||||||
# PhoenixD description limited to 128 characters
|
# PhoenixD description limited to 128 characters
|
||||||
if description_hash:
|
if description_hash:
|
||||||
data["descriptionHash"] = description_hash.hex()
|
data["descriptionHash"] = description_hash.hex()
|
||||||
elif unhashed_description:
|
|
||||||
data["descriptionHash"] = hashlib.sha256(
|
|
||||||
unhashed_description
|
|
||||||
).hexdigest()
|
|
||||||
else:
|
else:
|
||||||
desc = memo or ""
|
desc = memo
|
||||||
|
if desc is None and unhashed_description:
|
||||||
|
desc = unhashed_description.decode()
|
||||||
|
desc = desc or ""
|
||||||
if len(desc) > 128:
|
if len(desc) > 128:
|
||||||
data["descriptionHash"] = hashlib.sha256(desc.encode()).hexdigest()
|
data["descriptionHash"] = hashlib.sha256(desc.encode()).hexdigest()
|
||||||
else:
|
else:
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "lnbits"
|
name = "lnbits"
|
||||||
version = "1.5.5"
|
version = "1.5.5-rc2"
|
||||||
requires-python = ">=3.10,<3.13"
|
requires-python = ">=3.10,<3.13"
|
||||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||||
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from unittest.mock import AsyncMock
|
|||||||
import pytest
|
import pytest
|
||||||
from pytest_mock.plugin import MockerFixture
|
from pytest_mock.plugin import MockerFixture
|
||||||
|
|
||||||
from lnbits.core.crud.payments import get_payment, get_payments
|
from lnbits.core.crud.payments import get_payments
|
||||||
from lnbits.core.crud.users import get_user
|
from lnbits.core.crud.users import get_user
|
||||||
from lnbits.core.crud.wallets import create_wallet
|
from lnbits.core.crud.wallets import create_wallet
|
||||||
from lnbits.core.models.payments import CreateInvoice, Payment, PaymentState
|
from lnbits.core.models.payments import CreateInvoice, Payment, PaymentState
|
||||||
@@ -1752,51 +1752,6 @@ async def test_check_fiat_status_handles_internal_states(mocker: MockerFixture):
|
|||||||
assert queue_put.await_count == 1
|
assert queue_put.await_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_check_fiat_status_persists_successful_payment(
|
|
||||||
to_wallet: Wallet, settings: Settings, mocker: MockerFixture
|
|
||||||
):
|
|
||||||
settings.stripe_enabled = True
|
|
||||||
settings.stripe_api_secret_key = "mock_sk_test_4eC39HqLyjWDarjtT1zdp7dc"
|
|
||||||
settings.stripe_limits.service_min_amount_sats = 0
|
|
||||||
settings.stripe_limits.service_max_amount_sats = 0
|
|
||||||
settings.stripe_limits.service_fee_wallet_id = None
|
|
||||||
settings.stripe_limits.service_faucet_wallet_id = None
|
|
||||||
|
|
||||||
invoice_data = CreateInvoice(
|
|
||||||
unit="USD", amount=1.0, memo="Test", fiat_provider="stripe"
|
|
||||||
)
|
|
||||||
fiat_mock_response = FiatInvoiceResponse(
|
|
||||||
ok=True,
|
|
||||||
checking_id=f"session_paid_{get_random_string(10)}",
|
|
||||||
payment_request="https://stripe.com/pay/session_paid",
|
|
||||||
)
|
|
||||||
mocker.patch(
|
|
||||||
"lnbits.fiat.StripeWallet.create_invoice",
|
|
||||||
AsyncMock(return_value=fiat_mock_response),
|
|
||||||
)
|
|
||||||
mocker.patch(
|
|
||||||
"lnbits.utils.exchange_rates.get_fiat_rate_satoshis",
|
|
||||||
AsyncMock(return_value=1000),
|
|
||||||
)
|
|
||||||
payment = await payments.create_fiat_invoice(to_wallet.id, invoice_data)
|
|
||||||
assert payment.status == PaymentState.PENDING
|
|
||||||
|
|
||||||
mocker.patch(
|
|
||||||
"lnbits.fiat.StripeWallet.get_invoice_status",
|
|
||||||
AsyncMock(return_value=FiatPaymentStatus(paid=True)),
|
|
||||||
)
|
|
||||||
queue_put = mocker.patch("lnbits.tasks.internal_invoice_queue.put", AsyncMock())
|
|
||||||
|
|
||||||
status = await check_fiat_status(payment)
|
|
||||||
|
|
||||||
assert status.success is True
|
|
||||||
assert payment.status == PaymentState.SUCCESS
|
|
||||||
updated_payment = await get_payment(payment.checking_id)
|
|
||||||
assert updated_payment.status == PaymentState.SUCCESS
|
|
||||||
queue_put.assert_awaited_once_with(payment.checking_id)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_verify_paypal_webhook_requires_configuration(settings: Settings):
|
async def test_verify_paypal_webhook_requires_configuration(settings: Settings):
|
||||||
settings.paypal_webhook_id = None
|
settings.paypal_webhook_id = None
|
||||||
|
|||||||
Reference in New Issue
Block a user