Compare commits

...
10 Commits
Author SHA1 Message Date
dni ⚡andGitHub 95071eb87e quick fix languages bundle (#1691) 2023-05-09 09:56:44 +01:00
BitkarrotandGitHub 44c5c87405 Add troubleshooting note for Poetry errors on OSX install (#1641)
* Update installation.md

for poetry errors on OSX

* Update installation.md

* Update installation.md

* Update installation.md

remove trouble shooting message at top
2023-05-09 09:43:19 +01:00
dni ⚡andGitHub 8e8093bef3 de.js i18n updates, add nl and pirate (#1675)
bump service worker
2023-05-09 09:42:27 +01:00
dni ⚡andGitHub bb9ef7f88c FEAT: precommit git hooks, based on fusions #913 (#1645)
* init commit for precommit

* some reformatting precommit does
2023-05-09 09:41:40 +01:00
Vlad StanandGitHub 67d8b67ee5 Create super user account if it does not exist (#1688)
* fix: temp create account for `super_user_id` if missing

* chore: remove dumb import

* refactor: move logic outside `crud`

* feat: add uuid4 conversion

* fix: require valid string in .env file

* fix: update the `settings.super_user` value in case or normalisation for UUID4

* fix: allow long super_user

* chore: code format

* fix: add UI redirect with the normalized user

* fix: normalize `super_user` up one level

* fix: should normalize user in non-ui mode also
2023-05-09 09:22:19 +01:00
Vlad StanandGitHub 132d8a9320 fix: rename bool super_user to is_super_user (#1690) 2023-05-09 09:20:51 +01:00
c0f66989cb Serverside Pagination for payments (#1613)
* initial backend support

* implement payments pagination on frontend

* implement search for payments api

* fix pyright issues

* sqlite support for searching

* backwards compatability

* formatting, small fixes

* small optimization

* fix sorting issue, add error handling

* GET payments test

* filter by dates, use List instead of list

* fix sqlite

* update bundle

* test old payments endpoint aswell

* refactor for easier review

* optimise test

* revert unnecessary change

---------

Co-authored-by: dni  <office@dnilabs.com>
2023-05-09 09:18:53 +01:00
dni ⚡andGitHub 45b199a8ef create datadir on launch (#1674)
* create datadir on launch
* change makedirs to Path.mkdir
2023-05-09 10:04:27 +02:00
dni ⚡andGitHub 159aa24a4e update 0.10.6 (#1689) 2023-05-09 09:48:05 +02:00
62812938bd French language added (#1686)
* French language added
---------
Co-authored-by: Dood597 <66718486+Dood597@users.noreply.github.com>
2023-05-09 09:47:06 +02:00
37 changed files with 2484 additions and 1624 deletions
+5 -4
View File
@@ -1,10 +1,9 @@
--- ---
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: "[BUG]" title: '[BUG]'
labels: bug labels: bug
assignees: '' assignees: ''
--- ---
**Describe the bug** **Describe the bug**
@@ -12,6 +11,7 @@ A clear and concise description of what the bug is.
**To Reproduce** **To Reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'
@@ -24,8 +24,9 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):** **Desktop (please complete the following information):**
- LNbits version: [e.g. 0.9.2 or commit hash]
- Database [e.g. sqlite, postgres] - LNbits version: [e.g. 0.9.2 or commit hash]
- Database [e.g. sqlite, postgres]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.
+1 -2
View File
@@ -1,10 +1,9 @@
--- ---
name: Feature request name: Feature request
about: Suggest an idea for this project about: Suggest an idea for this project
title: "[Feature request]" title: '[Feature request]'
labels: feature request labels: feature request
assignees: '' assignees: ''
--- ---
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem? Please describe.**
-3
View File
@@ -4,7 +4,4 @@ about: Anything else that you need to say
title: '' title: ''
labels: '' labels: ''
assignees: '' assignees: ''
--- ---
+42
View File
@@ -0,0 +1,42 @@
exclude: '^lnbits/static/bundle.*|^docs/.*|^lnbits/static/vendor/.*|^lnbits/extensions/.*|^lnbits/wallets/lnd_grpc_files/.*|^package-lock.json$'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: check-docstring-first
- id: check-json
- id: debug-statements
- id: mixed-line-ending
- id: check-case-conflict
- repo: https://github.com/psf/black
rev: 22.6.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
args: ['--profile', 'black']
- repo: https://github.com/pre-commit/mirrors-prettier
rev: '50c5478ed9e10bf360335449280cf2a67f4edb7a'
hooks:
- id: prettier
args: ['lnbits']
- repo: https://github.com/pycqa/flake8
rev: 6.0.0
hooks:
- id: flake8
entry: poetry run flake8
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.971
hooks:
- id: mypy
name: mypy
entry: poetry run mypy
args: [--ignore-missing-imports]
additional_dependencies: [types-mock, types-protobuf]
+8
View File
@@ -89,3 +89,11 @@ bundle:
# increment serviceworker version # increment serviceworker version
sed -i -e "s/CACHE_VERSION =.*/CACHE_VERSION = $$(awk '/CACHE_VERSION =/ { print 1+$$4 }' lnbits/core/static/js/service-worker.js)/" \ sed -i -e "s/CACHE_VERSION =.*/CACHE_VERSION = $$(awk '/CACHE_VERSION =/ { print 1+$$4 }' lnbits/core/static/js/service-worker.js)/" \
lnbits/core/static/js/service-worker.js lnbits/core/static/js/service-worker.js
install-pre-commit-hook:
@echo "Installing pre-commit hook to git"
@echo "Uninstall the hook with poetry run pre-commit uninstall"
poetry run pre-commit install
pre-commit:
poetry run pre-commit run --all-files
+7 -10
View File
@@ -1,10 +1,8 @@
LNbits # LNbits
======
[![license-badge]](LICENSE) [![license-badge]](LICENSE)
[![docs-badge]][docs] [![docs-badge]][docs]
![Lightning network wallet](https://i.imgur.com/EHvK6Lq.png) ![Lightning network wallet](https://i.imgur.com/EHvK6Lq.png)
# LNbits v0.10 BETA, free and open-source Lightning wallet accounts system # LNbits v0.10 BETA, free and open-source Lightning wallet accounts system
@@ -17,11 +15,11 @@ Use [legend.lnbits.com](https://legend.lnbits.com), or run your own LNbits serve
LNbits is a Python server that sits on top of any funding source. It can be used as: LNbits is a Python server that sits on top of any funding source. It can be used as:
* Accounts system to mitigate the risk of exposing applications to your full balance via unique API keys for each wallet - Accounts system to mitigate the risk of exposing applications to your full balance via unique API keys for each wallet
* Extendable platform for exploring Lightning network functionality via the LNbits extension framework - Extendable platform for exploring Lightning network functionality via the LNbits extension framework
* Part of a development stack via LNbits API - Part of a development stack via LNbits API
* Fallback wallet for the LNURL scheme - Fallback wallet for the LNURL scheme
* Instant wallet for LN demonstrations - Instant wallet for LN demonstrations
LNbits can run on top of any Lightning funding source. It supports LND, CLN, Eclair, Spark, LNpay, OpenNode, LightningTipBot, and with more being added regularly. LNbits can run on top of any Lightning funding source. It supports LND, CLN, Eclair, Spark, LNpay, OpenNode, LightningTipBot, and with more being added regularly.
@@ -60,7 +58,7 @@ Example use would be an ATM, which utilises LNURL, if the user scans the QR with
## LNbits as an instant wallet ## LNbits as an instant wallet
Wallets can be easily generated and given out to people at events. "Go to this website", has a lot less friction than "Download this app". Wallets can be easily generated and given out to people at events. "Go to this website", has a lot less friction than "Download this app".
![lnurl ATM](https://i.imgur.com/xFWDnwy.png) ![lnurl ATM](https://i.imgur.com/xFWDnwy.png)
@@ -68,7 +66,6 @@ Wallets can be easily generated and given out to people at events. "Go to this
If you like this project [send some tip love](https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)! If you like this project [send some tip love](https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
[docs]: https://github.com/lnbits/lnbits/wiki [docs]: https://github.com/lnbits/lnbits/wiki
[docs-badge]: https://img.shields.io/badge/docs-lnbits.org-673ab7.svg [docs-badge]: https://img.shields.io/badge/docs-lnbits.org-673ab7.svg
[github-mypy]: https://github.com/lnbits/lnbits/actions?query=workflow%3Amypy [github-mypy]: https://github.com/lnbits/lnbits/actions?query=workflow%3Amypy
+1 -5
View File
@@ -12,7 +12,7 @@ By default, LNbits will use SQLite as its database. You can also use PostgreSQL
## Option 1 (recommended): poetry ## Option 1 (recommended): poetry
If you have problems installing LNbits using these instructions, please have a look at the [Troubleshooting](#troubleshooting) section. Mininum poetry version has is ^1.2, but it is recommended to use latest poetry. (including OSX)
```sh ```sh
git clone https://github.com/lnbits/lnbits.git git clone https://github.com/lnbits/lnbits.git
@@ -190,10 +190,6 @@ sudo apt install python3.9-dev gcc build-essential
poetry add setuptools wheel poetry add setuptools wheel
``` ```
#### Poetry
If your Poetry version is older than 1.2, for `poetry install`, ignore the `--only main` flag.
### Optional: PostgreSQL database ### Optional: PostgreSQL database
If you want to use LNbits at scale, we recommend using PostgreSQL as the backend database. Install Postgres and setup a database for LNbits: If you want to use LNbits at scale, we recommend using PostgreSQL as the backend database. Install Postgres and setup a database for LNbits:
+66 -33
View File
@@ -2,24 +2,31 @@ import datetime
import json import json
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from urllib.parse import urlparse from urllib.parse import urlparse
from uuid import uuid4 from uuid import UUID, uuid4
import shortuuid import shortuuid
from lnbits import bolt11 from lnbits import bolt11
from lnbits.db import COCKROACH, POSTGRES, Connection, Filters from lnbits.db import Connection, Filters, Page
from lnbits.extension_manager import InstallableExtension from lnbits.extension_manager import InstallableExtension
from lnbits.settings import AdminSettings, EditableSettings, SuperSettings, settings from lnbits.settings import AdminSettings, EditableSettings, SuperSettings, settings
from . import db from . import db
from .models import BalanceCheck, Payment, TinyURL, User, Wallet from .models import BalanceCheck, Payment, PaymentFilters, TinyURL, User, Wallet
# accounts # accounts
# -------- # --------
async def create_account(conn: Optional[Connection] = None) -> User: async def create_account(
user_id = uuid4().hex conn: Optional[Connection] = None, user_id: Optional[str] = None
) -> User:
if user_id:
user_uuid4 = UUID(hex=user_id, version=4)
assert user_uuid4.hex == user_id, "User ID is not valid UUID4 hex string"
else:
user_id = uuid4().hex
await (conn or db).execute("INSERT INTO accounts (id) VALUES (?)", (user_id,)) await (conn or db).execute("INSERT INTO accounts (id) VALUES (?)", (user_id,))
new_account = await get_account(user_id=user_id, conn=conn) new_account = await get_account(user_id=user_id, conn=conn)
@@ -343,7 +350,7 @@ async def get_latest_payments_by_extension(ext_name: str, ext_id: str, limit: in
return rows return rows
async def get_payments( async def get_payments_paginated(
*, *,
wallet_id: Optional[str] = None, wallet_id: Optional[str] = None,
complete: bool = False, complete: bool = False,
@@ -352,28 +359,23 @@ async def get_payments(
incoming: bool = False, incoming: bool = False,
since: Optional[int] = None, since: Optional[int] = None,
exclude_uncheckable: bool = False, exclude_uncheckable: bool = False,
filters: Optional[Filters[Payment]] = None, filters: Optional[Filters[PaymentFilters]] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> List[Payment]: ) -> Page[Payment]:
""" """
Filters payments to be returned by complete | pending | outgoing | incoming. Filters payments to be returned by complete | pending | outgoing | incoming.
""" """
args: List[Any] = [] values: List[Any] = []
clause: List[str] = [] clause: List[str] = []
if since is not None: if since is not None:
if db.type == POSTGRES: clause.append(f"time > {db.timestamp_placeholder}")
clause.append("time > to_timestamp(?)") values.append(since)
elif db.type == COCKROACH:
clause.append("time > cast(? AS timestamp)")
else:
clause.append("time > ?")
args.append(since)
if wallet_id: if wallet_id:
clause.append("wallet = ?") clause.append("wallet = ?")
args.append(wallet_id) values.append(wallet_id)
if complete and pending: if complete and pending:
pass pass
@@ -397,21 +399,54 @@ async def get_payments(
clause.append("checking_id NOT LIKE 'temp_%'") clause.append("checking_id NOT LIKE 'temp_%'")
clause.append("checking_id NOT LIKE 'internal_%'") clause.append("checking_id NOT LIKE 'internal_%'")
if not filters: return await (conn or db).fetch_page(
filters = Filters(limit=None, offset=None) "SELECT * FROM apipayments",
clause,
rows = await (conn or db).fetchall( values,
f""" filters=filters,
SELECT * model=Payment,
FROM apipayments
{filters.where(clause)}
ORDER BY time DESC
{filters.pagination()}
""",
filters.values(args),
) )
return [Payment.from_row(row) for row in rows]
async def get_payments(
*,
wallet_id: Optional[str] = None,
complete: bool = False,
pending: bool = False,
outgoing: bool = False,
incoming: bool = False,
since: Optional[int] = None,
exclude_uncheckable: bool = False,
filters: Optional[Filters[PaymentFilters]] = None,
conn: Optional[Connection] = None,
limit: Optional[int] = None,
offset: Optional[int] = None,
) -> list[Payment]:
"""
Filters payments to be returned by complete | pending | outgoing | incoming.
"""
if not filters:
filters = Filters()
if limit:
filters.limit = limit
if offset:
filters.offset = offset
page = await get_payments_paginated(
wallet_id=wallet_id,
complete=complete,
pending=pending,
outgoing=outgoing,
incoming=incoming,
since=since,
exclude_uncheckable=exclude_uncheckable,
filters=filters,
conn=conn,
)
return page.data
async def delete_expired_invoices( async def delete_expired_invoices(
@@ -454,7 +489,6 @@ async def create_payment(
webhook: Optional[str] = None, webhook: Optional[str] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> Payment: ) -> Payment:
# todo: add this when tests are fixed # todo: add this when tests are fixed
# previous_payment = await get_wallet_payment(wallet_id, payment_hash, conn=conn) # previous_payment = await get_wallet_payment(wallet_id, payment_hash, conn=conn)
# assert previous_payment is None, "Payment already exists" # assert previous_payment is None, "Payment already exists"
@@ -514,7 +548,6 @@ async def update_payment_details(
new_checking_id: Optional[str] = None, new_checking_id: Optional[str] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ) -> None:
set_clause: List[str] = [] set_clause: List[str] = []
set_variables: List[Any] = [] set_variables: List[Any] = []
@@ -702,7 +735,7 @@ async def get_admin_settings(is_super_user: bool = False) -> Optional[AdminSetti
row_dict = dict(sets) row_dict = dict(sets)
row_dict.pop("super_user") row_dict.pop("super_user")
admin_settings = AdminSettings( admin_settings = AdminSettings(
super_user=is_super_user, is_super_user=is_super_user,
lnbits_allowed_funding_sources=settings.lnbits_allowed_funding_sources, lnbits_allowed_funding_sources=settings.lnbits_allowed_funding_sources,
**row_dict, **row_dict,
) )
+12
View File
@@ -1,6 +1,7 @@
import importlib import importlib
import re import re
from typing import Any from typing import Any
from uuid import UUID
import httpx import httpx
from loguru import logger from loguru import logger
@@ -63,3 +64,14 @@ async def stop_extension_background_work(ext_id: str, user: str):
url = f"https://{settings.host}:{settings.port}/{ext_id}/api/v1?usr={user}" url = f"https://{settings.host}:{settings.port}/{ext_id}/api/v1?usr={user}"
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
def to_valid_user_id(user_id: str) -> UUID:
if len(user_id) < 32:
raise ValueError("User ID must have at least 128 bits")
try:
int(user_id, 16)
except:
raise ValueError("Invalid hex string for User ID.")
return UUID(hex=user_id[:32], version=4)
+20 -2
View File
@@ -11,7 +11,7 @@ from lnurl import encode as lnurl_encode
from loguru import logger from loguru import logger
from pydantic import BaseModel from pydantic import BaseModel
from lnbits.db import Connection from lnbits.db import Connection, FilterModel, FromRowModel
from lnbits.helpers import url_for from lnbits.helpers import url_for
from lnbits.settings import get_wallet_class, settings from lnbits.settings import get_wallet_class, settings
from lnbits.wallets.base import PaymentStatus from lnbits.wallets.base import PaymentStatus
@@ -86,7 +86,7 @@ class User(BaseModel):
return False return False
class Payment(BaseModel): class Payment(FromRowModel):
checking_id: str checking_id: str
pending: bool pending: bool
amount: int amount: int
@@ -214,6 +214,24 @@ class Payment(BaseModel):
await delete_payment(self.checking_id, conn=conn) await delete_payment(self.checking_id, conn=conn)
class PaymentFilters(FilterModel):
__search_fields__ = ["memo", "amount"]
checking_id: str
amount: int
fee: int
memo: Optional[str]
time: datetime.datetime
bolt11: str
preimage: str
payment_hash: str
expiry: Optional[datetime.datetime]
extra: Dict = {}
wallet_id: str
webhook: Optional[str]
webhook_status: Optional[int]
class BalanceCheck(BaseModel): class BalanceCheck(BaseModel):
wallet: str wallet: str
service: str service: str
+5 -2
View File
@@ -43,6 +43,7 @@ from .crud import (
update_payment_status, update_payment_status,
update_super_user, update_super_user,
) )
from .helpers import to_valid_user_id
from .models import Payment from .models import Payment
@@ -437,6 +438,9 @@ async def update_wallet_balance(wallet_id: str, amount: int):
async def check_admin_settings(): async def check_admin_settings():
if settings.super_user:
settings.super_user = to_valid_user_id(settings.super_user).hex
if settings.lnbits_admin_ui: if settings.lnbits_admin_ui:
settings_db = await get_super_settings() settings_db = await get_super_settings()
if not settings_db: if not settings_db:
@@ -488,8 +492,7 @@ async def init_admin_settings(super_user: Optional[str] = None) -> SuperSettings
if super_user: if super_user:
account = await get_account(super_user) account = await get_account(super_user)
if not account: if not account:
account = await create_account() account = await create_account(user_id=super_user)
super_user = account.id
if not account.wallets or len(account.wallets) == 0: if not account.wallets or len(account.wallets) == 0:
await create_wallet(user_id=account.id) await create_wallet(user_id=account.id)
+1 -1
View File
@@ -1,6 +1,6 @@
// update cache version every time there is a new deployment // update cache version every time there is a new deployment
// so the service worker reinitializes the cache // so the service worker reinitializes the cache
const CACHE_VERSION = 5 const CACHE_VERSION = 8
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-` const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
const getApiKey = request => { const getApiKey = request => {
+36 -12
View File
@@ -152,14 +152,14 @@ new Vue({
field: 'memo' field: 'memo'
}, },
{ {
name: 'date', name: 'time',
align: 'left', align: 'left',
label: this.$t('date'), label: this.$t('date'),
field: 'date', field: 'date',
sortable: true sortable: true
}, },
{ {
name: 'sat', name: 'amount',
align: 'right', align: 'right',
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')', label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
field: 'sat', field: 'sat',
@@ -173,9 +173,14 @@ new Vue({
} }
], ],
pagination: { pagination: {
rowsPerPage: 10 rowsPerPage: 10,
page: 1,
sortBy: 'time',
descending: true,
rowsNumber: 10
}, },
filter: null filter: null,
loading: false
}, },
paymentsChart: { paymentsChart: {
show: false show: false
@@ -695,16 +700,35 @@ new Vue({
LNbits.href.deleteWallet(walletId, user) LNbits.href.deleteWallet(walletId, user)
}) })
}, },
fetchPayments: function () { fetchPayments: function (props) {
return LNbits.api.getPayments(this.g.wallet).then(response => { // Props are passed by qasar when pagination or sorting changes
this.payments = response.data if (props) {
.map(obj => { this.paymentsTable.pagination = props.pagination
}
let pagination = this.paymentsTable.pagination
this.paymentsTable.loading = true
const query = {
limit: pagination.rowsPerPage,
offset: (pagination.page - 1) * pagination.rowsPerPage,
sortby: pagination.sortBy ?? 'time',
direction: pagination.descending ? 'desc' : 'asc'
}
if (this.paymentsTable.filter) {
query.search = this.paymentsTable.filter
}
return LNbits.api
.getPayments(this.g.wallet, query)
.then(response => {
this.paymentsTable.loading = false
this.paymentsTable.pagination.rowsNumber = response.data.total
this.payments = response.data.data.map(obj => {
return LNbits.map.payment(obj) return LNbits.map.payment(obj)
}) })
.sort((a, b) => { })
return b.time - a.time .catch(err => {
}) this.paymentsTable.loading = false
}) LNbits.utils.notifyApiError(err)
})
}, },
fetchBalance: function () { fetchBalance: function () {
LNbits.api.getWallet(this.g.wallet).then(response => { LNbits.api.getWallet(this.g.wallet).then(response => {
+2 -2
View File
@@ -463,7 +463,7 @@
this.g.user.wallets[0].adminkey this.g.user.wallets[0].adminkey
) )
.then(response => { .then(response => {
this.isSuperUser = response.data.super_user || false this.isSuperUser = response.data.is_super_user || false
this.settings = response.data this.settings = response.data
this.formData = _.clone(this.settings) this.formData = _.clone(this.settings)
this.updateFundingData() this.updateFundingData()
@@ -474,7 +474,7 @@
}, },
updateSettings() { updateSettings() {
let data = _.omit(this.formData, [ let data = _.omit(this.formData, [
'super_user', 'is_super_user',
'lnbits_allowed_funding_sources' 'lnbits_allowed_funding_sources'
]) ])
LNbits.api LNbits.api
+6 -5
View File
@@ -125,7 +125,6 @@
</div> </div>
</div> </div>
<q-input <q-input
v-if="payments.length > 10"
filled filled
dense dense
clearable clearable
@@ -138,12 +137,14 @@
<q-table <q-table
dense dense
flat flat
:data="filteredPayments" :data="payments"
:row-key="paymentTableRowKey" :row-key="paymentTableRowKey"
:columns="paymentsTable.columns" :columns="paymentsTable.columns"
:pagination.sync="paymentsTable.pagination" :pagination.sync="paymentsTable.pagination"
:no-data-label="$t('no_transactions')" :no-data-label="$t('no_transactions')"
:filter="paymentsTable.filter" :filter="paymentsTable.filter"
:loading="paymentsTable.loading"
@request="fetchPayments"
> >
{% raw %} {% raw %}
<template v-slot:header="props"> <template v-slot:header="props">
@@ -192,14 +193,14 @@
</q-badge> </q-badge>
{{ props.row.memo }} {{ props.row.memo }}
</q-td> </q-td>
<q-td auto-width key="date" :props="props"> <q-td auto-width key="time" :props="props">
<q-tooltip>{{ props.row.date }}</q-tooltip> <q-tooltip>{{ props.row.date }}</q-tooltip>
{{ props.row.dateFrom }} {{ props.row.dateFrom }}
</q-td> </q-td>
{% endraw %} {% endraw %}
<q-td <q-td
auto-width auto-width
key="sat" key="amount"
v-if="'{{LNBITS_DENOMINATION}}' != 'sats'" v-if="'{{LNBITS_DENOMINATION}}' != 'sats'"
:props="props" :props="props"
>{% raw %} {{ >{% raw %} {{
@@ -207,7 +208,7 @@
}} }}
</q-td> </q-td>
<q-td auto-width key="sat" v-else :props="props"> <q-td auto-width key="amount" v-else :props="props">
{{ props.row.fsat }} {{ props.row.fsat }}
</q-td> </q-td>
<q-td auto-width key="fee" :props="props"> <q-td auto-width key="fee" :props="props">
+38 -7
View File
@@ -33,8 +33,8 @@ from lnbits.core.helpers import (
migrate_extension_database, migrate_extension_database,
stop_extension_background_work, stop_extension_background_work,
) )
from lnbits.core.models import Payment, User, Wallet from lnbits.core.models import Payment, PaymentFilters, User, Wallet
from lnbits.db import Filters from lnbits.db import Filters, Page
from lnbits.decorators import ( from lnbits.decorators import (
WalletTypeInfo, WalletTypeInfo,
check_admin, check_admin,
@@ -66,6 +66,7 @@ from ..crud import (
delete_tinyurl, delete_tinyurl,
get_dbversions, get_dbversions,
get_payments, get_payments,
get_payments_paginated,
get_standalone_payment, get_standalone_payment,
get_tinyurl, get_tinyurl,
get_tinyurl_by_url, get_tinyurl_by_url,
@@ -122,19 +123,19 @@ async def api_update_wallet(
summary="get list of payments", summary="get list of payments",
response_description="list of payments", response_description="list of payments",
response_model=List[Payment], response_model=List[Payment],
openapi_extra=generate_filter_params_openapi(Payment), openapi_extra=generate_filter_params_openapi(PaymentFilters),
) )
async def api_payments( async def api_payments(
wallet: WalletTypeInfo = Depends(get_key_type), wallet: WalletTypeInfo = Depends(get_key_type),
filters: Filters = Depends(parse_filters(Payment)), filters: Filters = Depends(parse_filters(PaymentFilters)),
): ):
pendingPayments = await get_payments( pending_payments = await get_payments(
wallet_id=wallet.wallet.id, wallet_id=wallet.wallet.id,
pending=True, pending=True,
exclude_uncheckable=True, exclude_uncheckable=True,
filters=filters, filters=filters,
) )
for payment in pendingPayments: for payment in pending_payments:
await check_transaction_status( await check_transaction_status(
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
) )
@@ -146,6 +147,37 @@ async def api_payments(
) )
@core_app.get(
"/api/v1/payments/paginated",
name="Payment List",
summary="get paginated list of payments",
response_description="list of payments",
response_model=Page[Payment],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments_paginated(
wallet: WalletTypeInfo = Depends(get_key_type),
filters: Filters = Depends(parse_filters(PaymentFilters)),
):
pending = await get_payments_paginated(
wallet_id=wallet.wallet.id,
pending=True,
exclude_uncheckable=True,
filters=filters,
)
for payment in pending.data:
await check_transaction_status(
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
)
page = await get_payments_paginated(
wallet_id=wallet.wallet.id,
pending=True,
complete=True,
filters=filters,
)
return page
class CreateInvoiceData(BaseModel): class CreateInvoiceData(BaseModel):
out: Optional[bool] = True out: Optional[bool] = True
amount: float = Query(None, ge=0) amount: float = Query(None, ge=0)
@@ -788,7 +820,6 @@ async def api_install_extension(
@core_app.delete("/api/v1/extension/{ext_id}") @core_app.delete("/api/v1/extension/{ext_id}")
async def api_uninstall_extension(ext_id: str, user: User = Depends(check_admin)): async def api_uninstall_extension(ext_id: str, user: User = Depends(check_admin)):
installable_extensions = await InstallableExtension.get_installable_extensions() installable_extensions = await InstallableExtension.get_installable_extensions()
extensions = [e for e in installable_extensions if e.id == ext_id] extensions = [e for e in installable_extensions if e.id == ext_id]
+10
View File
@@ -11,6 +11,7 @@ from pydantic.types import UUID4
from starlette.responses import HTMLResponse, JSONResponse from starlette.responses import HTMLResponse, JSONResponse
from lnbits.core import db from lnbits.core import db
from lnbits.core.helpers import to_valid_user_id
from lnbits.core.models import User from lnbits.core.models import User
from lnbits.decorators import check_admin, check_user_exists from lnbits.decorators import check_admin, check_user_exists
from lnbits.helpers import template_renderer, url_for from lnbits.helpers import template_renderer, url_for
@@ -414,6 +415,15 @@ async def index(request: Request, user: User = Depends(check_admin)):
) )
@core_html_routes.get("/uuidv4/{hex_value}")
async def hex_to_uuid4(hex_value: str):
try:
user_id = to_valid_user_id(hex_value).hex
return RedirectResponse(url=f"/wallet?usr={user_id}")
except Exception as e:
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
async def toggle_extension(extension_to_enable, extension_to_disable, user_id): async def toggle_extension(extension_to_enable, extension_to_disable, user_id):
if extension_to_enable and extension_to_disable: if extension_to_enable and extension_to_disable:
raise HTTPException( raise HTTPException(
+208 -71
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import asyncio import asyncio
import datetime import datetime
import os import os
@@ -5,7 +7,8 @@ import re
import time import time
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from enum import Enum from enum import Enum
from typing import Any, Generic, List, Optional, Tuple, Type, TypeVar from sqlite3 import Row
from typing import Any, Generic, List, Literal, Optional, Type, TypeVar
from loguru import logger from loguru import logger
from pydantic import BaseModel, ValidationError from pydantic import BaseModel, ValidationError
@@ -19,6 +22,51 @@ POSTGRES = "POSTGRES"
COCKROACH = "COCKROACH" COCKROACH = "COCKROACH"
SQLITE = "SQLITE" SQLITE = "SQLITE"
if settings.lnbits_database_url:
database_uri = settings.lnbits_database_url
if database_uri.startswith("cockroachdb://"):
DB_TYPE = COCKROACH
else:
DB_TYPE = POSTGRES
from psycopg2.extensions import DECIMAL, new_type, register_type
def _parse_timestamp(value, _):
if value is None:
return None
f = "%Y-%m-%d %H:%M:%S.%f"
if "." not in value:
f = "%Y-%m-%d %H:%M:%S"
return time.mktime(datetime.datetime.strptime(value, f).timetuple())
register_type(
new_type(
DECIMAL.values,
"DEC2FLOAT",
lambda value, curs: float(value) if value is not None else None,
)
)
register_type(
new_type(
(1082, 1083, 1266),
"DATE2INT",
lambda value, curs: time.mktime(value.timetuple())
if value is not None
else None,
)
)
register_type(new_type((1184, 1114), "TIMESTAMP2INT", _parse_timestamp))
else:
if os.path.isdir(settings.lnbits_data_folder):
DB_TYPE = SQLITE
else:
raise NotADirectoryError(
f"LNBITS_DATA_FOLDER named {settings.lnbits_data_folder} was not created"
f" - please 'mkdir {settings.lnbits_data_folder}' and try again"
)
class Compat: class Compat:
type: Optional[str] = "<inherited>" type: Optional[str] = "<inherited>"
@@ -68,6 +116,16 @@ class Compat:
return "BIGINT" return "BIGINT"
return "INT" return "INT"
@classmethod
@property
def timestamp_placeholder(cls):
if DB_TYPE == POSTGRES:
return "to_timestamp(?)"
elif DB_TYPE == COCKROACH:
return "cast(? AS timestamp)"
else:
return "?"
class Connection(Compat): class Connection(Compat):
def __init__(self, conn: AsyncConnection, txn, typ, name, schema): def __init__(self, conn: AsyncConnection, txn, typ, name, schema):
@@ -87,17 +145,21 @@ class Connection(Compat):
# strip html # strip html
CLEANR = re.compile("<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});") CLEANR = re.compile("<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});")
def cleanhtml(raw_html):
if isinstance(raw_html, str):
cleantext = re.sub(CLEANR, "", raw_html)
return cleantext
else:
return raw_html
# tuple to list and back to tuple # tuple to list and back to tuple
value_list = [values] if isinstance(values, str) else list(values) raw_values = [values] if isinstance(values, str) else list(values)
values = tuple([cleanhtml(val) for val in value_list]) values = []
return values for raw_value in raw_values:
if isinstance(raw_value, str):
values.append(re.sub(CLEANR, "", raw_value))
elif isinstance(raw_value, datetime.datetime):
ts = raw_value.timestamp()
if self.type == SQLITE:
values.append(int(ts))
else:
values.append(ts)
else:
values.append(raw_value)
return tuple(values)
async def fetchall(self, query: str, values: tuple = ()) -> list: async def fetchall(self, query: str, values: tuple = ()) -> list:
result = await self.conn.execute( result = await self.conn.execute(
@@ -113,6 +175,51 @@ class Connection(Compat):
await result.close() await result.close()
return row return row
async def fetch_page(
self,
query: str,
where: Optional[List[str]] = None,
values: Optional[List[str]] = None,
filters: Optional[Filters] = None,
model: Optional[Type[TRowModel]] = None,
) -> Page[TRowModel]:
if not filters:
filters = Filters()
clause = filters.where(where)
parsed_values = filters.values(values)
rows = await self.fetchall(
f"""
{query}
{clause}
{filters.order_by()}
{filters.pagination()}
""",
parsed_values,
)
if rows:
# no need for extra query if no pagination is specified
if filters.offset or filters.limit:
count = await self.fetchone(
f"""
SELECT COUNT(*) FROM (
{query}
{clause}
) as count
""",
parsed_values,
)
count = int(count[0])
else:
count = len(rows)
else:
count = 0
return Page(
data=[model.from_row(row) for row in rows] if model else rows,
total=count,
)
async def execute(self, query: str, values: tuple = ()): async def execute(self, query: str, values: tuple = ()):
return await self.conn.execute( return await self.conn.execute(
self.rewrite_query(query), self.rewrite_values(values) self.rewrite_query(query), self.rewrite_values(values)
@@ -122,57 +229,17 @@ class Connection(Compat):
class Database(Compat): class Database(Compat):
def __init__(self, db_name: str): def __init__(self, db_name: str):
self.name = db_name self.name = db_name
self.schema = self.name
self.type = DB_TYPE
if settings.lnbits_database_url: if DB_TYPE == SQLITE:
self.path = os.path.join(
settings.lnbits_data_folder, f"{self.name}.sqlite3"
)
database_uri = f"sqlite:///{self.path}"
else:
database_uri = settings.lnbits_database_url database_uri = settings.lnbits_database_url
if database_uri.startswith("cockroachdb://"):
self.type = COCKROACH
else:
self.type = POSTGRES
from psycopg2.extensions import DECIMAL, new_type, register_type
def _parse_timestamp(value, _):
if value is None:
return None
f = "%Y-%m-%d %H:%M:%S.%f"
if "." not in value:
f = "%Y-%m-%d %H:%M:%S"
return time.mktime(datetime.datetime.strptime(value, f).timetuple())
register_type(
new_type(
DECIMAL.values,
"DEC2FLOAT",
lambda value, curs: float(value) if value is not None else None,
)
)
register_type(
new_type(
(1082, 1083, 1266),
"DATE2INT",
lambda value, curs: time.mktime(value.timetuple())
if value is not None
else None,
)
)
register_type(new_type((1184, 1114), "TIMESTAMP2INT", _parse_timestamp))
else:
if os.path.isdir(settings.lnbits_data_folder):
self.path = os.path.join(
settings.lnbits_data_folder, f"{self.name}.sqlite3"
)
database_uri = f"sqlite:///{self.path}"
self.type = SQLITE
else:
raise NotADirectoryError(
f"LNBITS_DATA_FOLDER named {settings.lnbits_data_folder} was not created"
f" - please 'mkdir {settings.lnbits_data_folder}' and try again"
)
logger.trace(f"database {self.type} added for {self.name}")
self.schema = self.name
if self.name.startswith("ext_"): if self.name.startswith("ext_"):
self.schema = self.name[4:] self.schema = self.name[4:]
else: else:
@@ -181,6 +248,8 @@ class Database(Compat):
self.engine = create_engine(database_uri, strategy=ASYNCIO_STRATEGY) self.engine = create_engine(database_uri, strategy=ASYNCIO_STRATEGY)
self.lock = asyncio.Lock() self.lock = asyncio.Lock()
logger.trace(f"database {self.type} added for {self.name}")
@asynccontextmanager @asynccontextmanager
async def connect(self): async def connect(self):
await self.lock.acquire() await self.lock.acquire()
@@ -215,6 +284,17 @@ class Database(Compat):
await result.close() await result.close()
return row return row
async def fetch_page(
self,
query: str,
where: Optional[List[str]] = None,
values: Optional[List[str]] = None,
filters: Optional[Filters] = None,
model: Optional[Type[TRowModel]] = None,
) -> Page[TRowModel]:
async with self.connect() as conn:
return await conn.fetch_page(query, where, values, filters, model)
async def execute(self, query: str, values: tuple = ()): async def execute(self, query: str, values: tuple = ()):
async with self.connect() as conn: async with self.connect() as conn:
return await conn.execute(query, values) return await conn.execute(query, values)
@@ -229,6 +309,8 @@ class Operator(Enum):
LT = "lt" LT = "lt"
EQ = "eq" EQ = "eq"
NE = "ne" NE = "ne"
GE = "ge"
LE = "le"
INCLUDE = "in" INCLUDE = "in"
EXCLUDE = "ex" EXCLUDE = "ex"
@@ -246,21 +328,45 @@ class Operator(Enum):
return ">" return ">"
elif self == Operator.LT: elif self == Operator.LT:
return "<" return "<"
elif self == Operator.GE:
return ">="
elif self == Operator.LE:
return "<="
else: else:
raise ValueError("Unknown SQL Operator") raise ValueError("Unknown SQL Operator")
class FromRowModel(BaseModel):
@classmethod
def from_row(cls, row: Row):
return cls(**dict(row))
class FilterModel(BaseModel):
__search_fields__: List[str] = []
T = TypeVar("T")
TModel = TypeVar("TModel", bound=BaseModel) TModel = TypeVar("TModel", bound=BaseModel)
TRowModel = TypeVar("TRowModel", bound=FromRowModel)
TFilterModel = TypeVar("TFilterModel", bound=FilterModel)
class Filter(BaseModel, Generic[TModel]): class Page(BaseModel, Generic[T]):
data: list[T]
total: int
class Filter(BaseModel, Generic[TFilterModel]):
field: str field: str
nested: Optional[list[str]] nested: Optional[List[str]]
op: Operator = Operator.EQ op: Operator = Operator.EQ
values: list[Any] values: list[Any]
model: Optional[Type[TFilterModel]]
@classmethod @classmethod
def parse_query(cls, key: str, raw_values: list[Any], model: Type[TModel]): def parse_query(cls, key: str, raw_values: list[Any], model: Type[TFilterModel]):
# Key format: # Key format:
# key[operator] # key[operator]
# e.g. name[eq] # e.g. name[eq]
@@ -300,7 +406,7 @@ class Filter(BaseModel, Generic[TModel]):
else: else:
raise ValueError("Unknown filter field") raise ValueError("Unknown filter field")
return cls(field=field, op=op, nested=nested, values=values) return cls(field=field, op=op, nested=nested, values=values, model=model)
@property @property
def statement(self): def statement(self):
@@ -308,18 +414,29 @@ class Filter(BaseModel, Generic[TModel]):
if self.nested: if self.nested:
for name in self.nested: for name in self.nested:
accessor = f"({accessor} ->> '{name}')" accessor = f"({accessor} ->> '{name}')"
if self.model and self.model.__fields__[self.field].type_ == datetime.datetime:
placeholder = Compat.timestamp_placeholder
else:
placeholder = "?"
if self.op in (Operator.INCLUDE, Operator.EXCLUDE): if self.op in (Operator.INCLUDE, Operator.EXCLUDE):
placeholders = ", ".join(["?"] * len(self.values)) placeholders = ", ".join([placeholder] * len(self.values))
stmt = [f"{accessor} {self.op.as_sql} ({placeholders})"] stmt = [f"{accessor} {self.op.as_sql} ({placeholders})"]
else: else:
stmt = [f"{accessor} {self.op.as_sql} ?"] * len(self.values) stmt = [f"{accessor} {self.op.as_sql} {placeholder}"] * len(self.values)
return " OR ".join(stmt) return " OR ".join(stmt)
class Filters(BaseModel, Generic[TModel]): class Filters(BaseModel, Generic[TFilterModel]):
filters: List[Filter[TModel]] = [] filters: List[Filter[TFilterModel]] = []
limit: Optional[int] search: Optional[str] = None
offset: Optional[int]
offset: Optional[int] = None
limit: Optional[int] = None
sortby: Optional[str] = None
direction: Optional[Literal["asc", "desc"]] = None
model: Optional[Type[TFilterModel]] = None
def pagination(self) -> str: def pagination(self) -> str:
stmt = "" stmt = ""
@@ -329,16 +446,36 @@ class Filters(BaseModel, Generic[TModel]):
stmt += f"OFFSET {self.offset}" stmt += f"OFFSET {self.offset}"
return stmt return stmt
def where(self, where_stmts: List[str]) -> str: def where(self, where_stmts: Optional[List[str]] = None) -> str:
if not where_stmts:
where_stmts = []
if self.filters: if self.filters:
for filter in self.filters: for filter in self.filters:
where_stmts.append(filter.statement) where_stmts.append(filter.statement)
if self.search and self.model:
if DB_TYPE == POSTGRES:
where_stmts.append(
f"lower(concat({f', '.join(self.model.__search_fields__)})) LIKE ?"
)
elif DB_TYPE == SQLITE:
where_stmts.append(
f"lower({'||'.join(self.model.__search_fields__)}) LIKE ?"
)
if where_stmts: if where_stmts:
return "WHERE " + " AND ".join(where_stmts) return "WHERE " + " AND ".join(where_stmts)
return "" return ""
def values(self, values: List[str]) -> Tuple: def order_by(self) -> str:
if self.sortby:
return f"ORDER BY {self.sortby} {self.direction or 'asc'}"
return ""
def values(self, values: Optional[List[str]] = None) -> tuple:
if not values:
values = []
if self.filters: if self.filters:
for filter in self.filters: for filter in self.filters:
values.extend(filter.values) values.extend(filter.values)
if self.search and self.model:
values.append(f"%{self.search}%")
return tuple(values) return tuple(values)
+15 -8
View File
@@ -1,16 +1,16 @@
from http import HTTPStatus from http import HTTPStatus
from typing import Optional, Type from typing import Literal, Optional, Type
from fastapi import HTTPException, Request, Security, status from fastapi import Query, Request, Security, status
from fastapi.exceptions import HTTPException
from fastapi.openapi.models import APIKey, APIKeyIn from fastapi.openapi.models import APIKey, APIKeyIn
from fastapi.security import APIKeyHeader, APIKeyQuery from fastapi.security import APIKeyHeader, APIKeyQuery
from fastapi.security.base import SecurityBase from fastapi.security.base import SecurityBase
from pydantic import BaseModel
from pydantic.types import UUID4 from pydantic.types import UUID4
from lnbits.core.crud import get_user, get_wallet_for_key from lnbits.core.crud import get_user, get_wallet_for_key
from lnbits.core.models import User, Wallet from lnbits.core.models import User, Wallet
from lnbits.db import Filter, Filters from lnbits.db import Filter, Filters, TFilterModel
from lnbits.requestvars import g from lnbits.requestvars import g
from lnbits.settings import settings from lnbits.settings import settings
@@ -185,7 +185,6 @@ async def require_admin_key(
api_key_header: str = Security(api_key_header), api_key_header: str = Security(api_key_header),
api_key_query: str = Security(api_key_query), api_key_query: str = Security(api_key_query),
): ):
token = api_key_header or api_key_query token = api_key_header or api_key_query
if not token: if not token:
@@ -211,7 +210,6 @@ async def require_invoice_key(
api_key_header: str = Security(api_key_header), api_key_header: str = Security(api_key_header),
api_key_query: str = Security(api_key_query), api_key_query: str = Security(api_key_query),
): ):
token = api_key_header or api_key_query token = api_key_header or api_key_query
if not token: if not token:
@@ -279,14 +277,19 @@ async def check_super_user(usr: UUID4) -> User:
return user return user
def parse_filters(model: Type[BaseModel]): def parse_filters(model: Type[TFilterModel]):
""" """
Parses the query params as filters. Parses the query params as filters.
:param model: model used for validation of filter values :param model: model used for validation of filter values
""" """
def dependency( def dependency(
request: Request, limit: Optional[int] = None, offset: Optional[int] = None request: Request,
limit: Optional[int] = None,
offset: Optional[int] = None,
sortby: Optional[str] = None,
direction: Optional[Literal["asc", "desc"]] = None,
search: Optional[str] = Query(None, description="Text based search"),
): ):
params = request.query_params params = request.query_params
filters = [] filters = []
@@ -300,6 +303,10 @@ def parse_filters(model: Type[BaseModel]):
filters=filters, filters=filters,
limit=limit, limit=limit,
offset=offset, offset=offset,
sortby=sortby,
direction=direction,
search=search,
model=model,
) )
return dependency return dependency
+3 -3
View File
@@ -180,7 +180,7 @@ class Extension(NamedTuple):
class ExtensionManager: class ExtensionManager:
def __init__(self): def __init__(self):
p = Path(settings.lnbits_path, "extensions") p = Path(settings.lnbits_path, "extensions")
os.makedirs(p, exist_ok=True) Path(p).mkdir(parents=True, exist_ok=True)
self._extension_folders: List[Path] = [f for f in p.iterdir() if f.is_dir()] self._extension_folders: List[Path] = [f for f in p.iterdir() if f.is_dir()]
@property @property
@@ -283,7 +283,7 @@ class InstallableExtension(BaseModel):
@property @property
def zip_path(self) -> Path: def zip_path(self) -> Path:
extensions_data_dir = Path(settings.lnbits_data_folder, "extensions") extensions_data_dir = Path(settings.lnbits_data_folder, "extensions")
os.makedirs(extensions_data_dir, exist_ok=True) Path(extensions_data_dir).mkdir(parents=True, exist_ok=True)
return Path(extensions_data_dir, f"{self.id}.zip") return Path(extensions_data_dir, f"{self.id}.zip")
@property @property
@@ -335,7 +335,7 @@ class InstallableExtension(BaseModel):
def extract_archive(self): def extract_archive(self):
logger.info(f"Extracting extension {self.name}.") logger.info(f"Extracting extension {self.name}.")
os.makedirs(Path("lnbits", "upgrades"), exist_ok=True) Path("lnbits", "upgrades").mkdir(parents=True, exist_ok=True)
shutil.rmtree(self.ext_upgrade_dir, True) shutil.rmtree(self.ext_upgrade_dir, True)
with zipfile.ZipFile(self.zip_path, "r") as zip_ref: with zipfile.ZipFile(self.zip_path, "r") as zip_ref:
zip_ref.extractall(self.ext_upgrade_dir) zip_ref.extractall(self.ext_upgrade_dir)
+7 -3
View File
@@ -4,7 +4,6 @@ from typing import Any, List, Optional, Type
import jinja2 import jinja2
import shortuuid import shortuuid
from pydantic import BaseModel
from pydantic.schema import ( from pydantic.schema import (
field_schema, field_schema,
get_flat_models_from_fields, get_flat_models_from_fields,
@@ -15,6 +14,7 @@ from lnbits.jinja2_templating import Jinja2Templates
from lnbits.requestvars import g from lnbits.requestvars import g
from lnbits.settings import settings from lnbits.settings import settings
from .db import FilterModel
from .extension_manager import get_valid_extensions from .extension_manager import get_valid_extensions
@@ -32,7 +32,6 @@ def url_for(endpoint: str, external: Optional[bool] = False, **params: Any) -> s
def template_renderer(additional_folders: Optional[List] = None) -> Jinja2Templates: def template_renderer(additional_folders: Optional[List] = None) -> Jinja2Templates:
folders = ["lnbits/templates", "lnbits/core/templates"] folders = ["lnbits/templates", "lnbits/core/templates"]
if additional_folders: if additional_folders:
folders.extend(additional_folders) folders.extend(additional_folders)
@@ -96,7 +95,7 @@ def get_current_extension_name() -> str:
return ext_name return ext_name
def generate_filter_params_openapi(model: Type[BaseModel], keep_optional=False): def generate_filter_params_openapi(model: Type[FilterModel], keep_optional=False):
""" """
Generate openapi documentation for Filters. This is intended to be used along parse_filters (see example) Generate openapi documentation for Filters. This is intended to be used along parse_filters (see example)
:param model: Filter model :param model: Filter model
@@ -117,6 +116,11 @@ def generate_filter_params_openapi(model: Type[BaseModel], keep_optional=False):
description = "Supports Filtering" description = "Supports Filtering"
if schema["type"] == "object": if schema["type"] == "object":
description += f". Nested attributes can be filtered too, e.g. `{field.alias}.[additional].[attributes]`" description += f". Nested attributes can be filtered too, e.g. `{field.alias}.[additional].[attributes]`"
if (
hasattr(model, "__search_fields__")
and field.name in model.__search_fields__
):
description += ". Supports Search"
parameter = { parameter = {
"name": field.alias, "name": field.alias,
+4
View File
@@ -4,6 +4,7 @@ uvloop.install()
import multiprocessing as mp import multiprocessing as mp
import time import time
from pathlib import Path
import click import click
import uvicorn import uvicorn
@@ -37,6 +38,9 @@ def main(
): ):
"""Launched with `poetry run lnbits` at root level""" """Launched with `poetry run lnbits` at root level"""
# create data dir if it does not exist
Path(settings.lnbits_data_folder).mkdir(parents=True, exist_ok=True)
set_cli_settings(host=host, port=port, forwarded_allow_ips=forwarded_allow_ips) set_cli_settings(host=host, port=port, forwarded_allow_ips=forwarded_allow_ips)
# this beautiful beast parses all command line arguments and passes them to the uvicorn server # this beautiful beast parses all command line arguments and passes them to the uvicorn server
+1 -1
View File
@@ -310,7 +310,7 @@ class SuperSettings(EditableSettings):
class AdminSettings(EditableSettings): class AdminSettings(EditableSettings):
super_user: bool is_super_user: bool
lnbits_allowed_funding_sources: Optional[List[str]] lnbits_allowed_funding_sources: Optional[List[str]]
+9 -9
View File
File diff suppressed because one or more lines are too long
+29 -30
View File
@@ -9,46 +9,45 @@ window.localisation.de = {
save_tooltip: 'Änderungen speichern', save_tooltip: 'Änderungen speichern',
topup: 'Aufladen', topup: 'Aufladen',
topup_wallet: 'Wallet aufladen', topup_wallet: 'Wallet aufladen',
topup_hint: 'Nutze die Wallet-ID um eine beliebige Wallet aufzuladen', topup_hint: 'Nutze die Wallet-ID, um eine beliebige Wallet aufzuladen',
restart_tooltip: 'Starte den Server neu um die Änderungen zu übernehmen', restart_tooltip: 'Starte den Server neu, um die Änderungen zu übernehmen',
add_funds_tooltip: 'Füge Geld zu einer Wallet hinzu.', add_funds_tooltip: 'Füge Geld zu einer Wallet hinzu.',
reset_defaults: 'Zurücksetzen', reset_defaults: 'Zurücksetzen',
reset_defaults_tooltip: reset_defaults_tooltip:
'Lösche alle Einstellungen und setze auf die Standardeinstellungen zurück.', 'Alle Einstellungen zurücksetzen auf die Standardeinstellungen.',
download_backup: 'Datenbank-Backup herunterladen', download_backup: 'Datenbank-Backup herunterladen',
name_your_wallet: 'Benenne deine %{name} Wallet', name_your_wallet: 'Vergib deiner %{name} Wallet einen Namen',
paste_invoice_label: paste_invoice_label:
'Füge eine Rechnung, Zahlungsanforderung oder lnurl ein *', 'Füge eine Rechnung, Zahlungsanforderung oder lnurl ein *',
lnbits_description: lnbits_description:
'Einfach zu installieren und leichtgewichtig, LNbits kann auf jeder Lightning-Netzwerk-Funding-Quelle laufen, derzeit unterstützt LND, c-lightning, OpenNode, LNPay und sogar LNbits selbst! Du kannst LNbits für dich selbst laufen lassen oder anderen eine kundige Lösung anbieten. Jede Wallet hat ihre eigenen API-Schlüssel und es gibt keine Begrenzung für die Anzahl der Wallets, die du erstellen kannst. Die Möglichkeit, Gelder zu partitionieren, macht LNbits zu einem nützlichen Werkzeug für die Geldverwaltung und als Entwicklungswerkzeug. Erweiterungen fügen der LNbits zusätzliche Funktionalität hinzu, so dass du mit einer Reihe von vorderen Technologien auf dem Lightning-Netzwerk experimentieren kannst. Wir haben es so einfach wie möglich gemacht, Erweiterungen zu entwickeln, und als freies und Open-Source-Projekt ermutigen wir Menschen, sich selbst zu entwickeln und ihre eigenen Beiträge einzureichen.', 'Einfach zu installieren und kompakt, LNbits kann auf jeder Funding-Quelle im Lightning Netzwerk aufsetzen. Derzeit unterstützt: LND, c-lightning, OpenNode, LNPay und sogar LNbits selbst! Du kannst LNbits für dich selbst betreiben oder anderen die Verwaltung durch dich anbieten. Jede Wallet hat ihre eigenen API-Schlüssel und die Anzahl der Wallets ist unbegrenzt. Die Möglichkeit, Gelder auf verschiedene Accounts mit unterschiedlicher Logik aufteilen zu können macht LNbits zu einem nützlichen Werkzeug für deine Buchhaltung - aber auch als Entwicklungswerkzeug. Erweiterungen bereichern LNbits Accounts um zusätzliche Funktionalität, so dass du mit einer Reihe von neuartigen Technologien auf dem Lightning-Netzwerk experimentieren kannst. Wir haben es so einfach wie möglich gemacht, Erweiterungen zu entwickeln, und als freies und Open-Source-Projekt möchten wir Menschen ermutigen, sich selbst hieran zu versuchen und gemeinsam mit uns neue Funktionalitäten zu entwickeln.',
export_to_phone: 'Exportieren Sie auf das Telefon mit QR-Code', export_to_phone: 'Auf dem Telefon öffnen',
export_to_phone_desc: export_to_phone_desc:
'Dieser QR-Code enthält Ihre Wallet-URL mit vollständigem Zugriff. Sie können es von Ihrem Telefon aus scannen, um Ihre Wallet dort zu öffnen.', 'Dieser QR-Code beinhaltet vollständige Rechte auf deine Wallet. Du kannste den QR-Code mit Deinem Telefon scannen, um deine Wallet dort zu öffnen.',
wallets: 'Wallets', wallets: 'Wallets',
add_wallet: 'Füge eine neue Wallet hinzu', add_wallet: 'Wallet hinzufügen',
delete_wallet: 'Wallet löschen', delete_wallet: 'Wallet löschen',
delete_wallet_desc: delete_wallet_desc:
'Diese ganze Wallet wird gelöscht, die Mittel sind UNWIEDERBRINGLICH.', 'Die Wallet wird gelöscht, die hierin beinhalteten Daten hierin oder innerhalb einer Erweiterung sind UNWIEDERBRINGLICH.',
rename_wallet: 'Wallet umbenennen', rename_wallet: 'Wallet umbenennen',
update_name: 'Name aktualisieren', update_name: 'Namen aktualisieren',
press_to_claim: 'Drücken Sie, um Bitcoin zu beanspruchen', press_to_claim: 'Klicken, um Bitcoin einzufordern.',
donate: 'Spenden', donate: 'Spenden',
view_github: 'Auf GitHub anzeigen', view_github: 'Auf GitHub anzeigen',
voidwallet_active: 'VoidWallet ist aktiv! Zahlungen deaktiviert', voidwallet_active: 'VoidWallet ist aktiv! Zahlungen deaktiviert',
use_with_caution: use_with_caution: 'BITTE MIT VORSICHT BENUTZEN- %{name} Wallet ist noch BETA',
'VERWENDEN SIE MIT VORSICHT - %{name} Wallet ist noch in der BETA', toggle_darkmode: 'Auf Dark Mode umschalten',
toggle_darkmode: 'Dark Mode umschalten', view_swagger_docs: 'LNbits Swagger API-Dokumente',
view_swagger_docs: 'LNbits Swagger API-Dokumente anzeigen', api_docs: 'API docs',
api_docs: 'Api docs', commit_version: 'Commit Version',
commit_version: 'Commit version',
runs_on: 'Läuft auf', runs_on: 'Läuft auf',
credit_hint: 'Drücken Sie Enter, um das Konto zu belasten', credit_hint: 'Klicke Enter, um das Konto zu belasten',
credit_label: '%{denomination} zu belasten', credit_label: '%{denomination} zu belasten',
paste_request: 'Anfrage einfügen', paste_request: 'Anfrage einfügen',
create_invoice: 'Rechnung erstellen', create_invoice: 'Rechnung erstellen',
camera_tooltip: camera_tooltip:
'Verwenden Sie die Kamera, um eine Rechnung / QR-Code zu scannen', 'Verwenden Sie die Kamera, um eine Rechnung oder einen QR-Code zu scannen',
export_csv: 'Exportieren Sie nach CSV', export_csv: 'Exportieren als CSV',
transactions: 'Transaktionen', transactions: 'Transaktionen',
chart_tooltip: 'Diagramm anzeigen', chart_tooltip: 'Diagramm anzeigen',
pending: 'Ausstehend', pending: 'Ausstehend',
@@ -60,25 +59,25 @@ window.localisation.de = {
pay: 'Zahlen', pay: 'Zahlen',
memo: 'Memo', memo: 'Memo',
date: 'Datum', date: 'Datum',
processing_payment: 'Zahlung wird bearbeitet ...', processing_payment: 'Zahlung wird verarbeitet ...',
not_enough_funds: 'Nicht genug Geld!', not_enough_funds: 'Geldmittel sind erschöpft!',
search_by_tag_memo_amount: 'Suche nach Tag, Memo, Betrag', search_by_tag_memo_amount: 'Suche nach Tag, Memo, Betrag',
invoice_waiting: 'Rechnung wartet auf Zahlung', invoice_waiting: 'Rechnung wartend auf Zahlung',
payment_received: 'Zahlung erhalten', payment_received: 'Zahlung erhalten',
payment_sent: 'Zahlung gesendet', payment_sent: 'Zahlung gesendet',
outgoing_payment_pending: 'Ausgehende Zahlung ausstehend', outgoing_payment_pending: 'Ausgehende Zahlung wartend',
drain_funds: 'Leeren Sie die Mittel', drain_funds: 'Sats abziehen',
drain_funds_desc: drain_funds_desc:
'Dies ist ein LNURL-withdraw QR-Code zum Absaugen aller Mittel aus dieser Brieftasche. Teilen Sie es nicht mit jemandem. Es ist mit balanceCheck und balanceNotify kompatibel, so dass Ihre Brieftasche die Mittel nach dem ersten Abzug kontinuierlich von hier ziehen kann.', 'LNURL-withdraw QR-Code, der das Abziehen aller Geldmittel aus dieser Wallet erlaubt. Teile ihn mit niemandem! Kompatibel mit balanceCheck und balanceNotify, so dass deine Brieftasche die Sats nach dem ersten Abzug kontinuierlich von hier abziehen kann.',
i_understand: 'Ich verstehe', i_understand: 'Ich verstehe',
copy_wallet_url: 'Wallet-URL kopieren', copy_wallet_url: 'Wallet-URL kopieren',
disclaimer_dialog: disclaimer_dialog:
'Login-Funktionalität wird in einem zukünftigen Update veröffentlicht, für jetzt, stellen Sie sicher, dass Sie diese Seite als Lesezeichen speichern, um zukünftigen Zugriff auf Ihre Wallet zu erhalten! Dieser Service ist in BETA und wir übernehmen keine Verantwortung für Personen, die den Zugriff auf ihre Mittel verlieren.', 'Login-Funktionalität wird in einem zukünftigen Update veröffentlicht. Bis dahin ist die Speicherung der Wallet-URL als Lesezeichen absolut notwendig, um Zugriff auf die Wallet zu erhalten! Dieser Service ist in BETA und wir übernehmen keine Verantwortung für Verluste durch verlorene Zugriffe.',
no_transactions: 'Noch keine Transaktionen', no_transactions: 'Keine Transaktionen',
manage_extensions: 'Erweiterungen verwalten', manage_extensions: 'Erweiterungen verwalten',
manage_server: 'Server verwalten', manage_server: 'Server verwalten',
extensions: 'Erweiterungen', extensions: 'Erweiterungen',
no_extensions: 'Sie haben keine Erweiterungen installiert :(', no_extensions: 'Du hast noch keine Erweiterungen installiert :(',
created: 'Erstellt', created: 'Erstellt',
payment_hash: 'Zahlungs-Hash', payment_hash: 'Zahlungs-Hash',
fee: 'Gebühr', fee: 'Gebühr',
@@ -86,5 +85,5 @@ window.localisation.de = {
description: 'Beschreibung', description: 'Beschreibung',
expiry: 'Ablauf', expiry: 'Ablauf',
webhook: 'Webhook', webhook: 'Webhook',
payment_proof: 'Zahlungsbeleg' payment_proof: 'Beleg'
} }
+92
View File
@@ -0,0 +1,92 @@
window.localisation.fr = {
server: 'Serveur',
theme: 'Thème',
funding: 'Financement',
users: 'Utilisateurs',
restart: 'Redémarrer le serveur',
save: 'Enregistrer',
save_tooltip: 'Enregistrer vos modifications',
topup: 'Renflouer',
topup_wallet: 'Reflouer un portefeuille',
topup_hint:
"Utilisez l'ID du portefeuille pour recharger n'importe quel portefeuille",
restart_tooltip:
'Redémarrez le serveur pour que les changements prennent effet',
add_funds_tooltip: 'Ajouter des fonds à un portefeuille.',
reset_defaults: 'Réinitialiser aux valeurs par défaut',
reset_defaults_tooltip:
'Supprimer tous les paramètres et les réinitialiser aux valeurs par défaut.',
download_backup: 'Télécharger la sauvegarde de la base de données',
name_your_wallet: 'Nommez votre portefeuille %{name}',
paste_invoice_label:
'Coller une facture, une demande de paiement ou un code lnurl *',
lnbits_description:
"Facile à installer et léger, LNbits peut fonctionner sur n'importe quelle source de financement du réseau Lightning, prenant actuellement en charge LND, c-lightning, OpenNode, LNPay et même LNbits lui-même! Vous pouvez exécuter LNbits pour vous-même ou offrir facilement une solution de gardien pour les autres. Chaque portefeuille a ses propres clés API et il n'y a pas de limite au nombre de portefeuilles que vous pouvez créer. La capacité de partitionner les fonds rend LNbits un outil utile pour la gestion de l'argent et comme outil de développement. Les extensions ajoutent une fonctionnalité supplémentaire à LNbits afin que vous puissiez expérimenter une gamme de technologies de pointe sur le réseau Lightning. Nous avons rendu le développement d'extensions aussi simple que possible et, en tant que projet gratuit et open source, nous encourageons les gens à développer et à soumettre les leurs.",
export_to_phone: 'Exporter vers le téléphone avec un code QR',
export_to_phone_desc:
"Ce code QR contient l'URL de votre portefeuille avec un accès complet. Vous pouvez le scanner depuis votre téléphone pour ouvrir votre portefeuille depuis là-bas.",
wallets: 'Portefeuilles',
add_wallet: 'Ajouter un nouveau portefeuille',
delete_wallet: 'Supprimer le portefeuille',
delete_wallet_desc:
'Ce portefeuille entier sera supprimé et les fonds seront IRRECUPERABLES.',
rename_wallet: 'Renommer le portefeuille',
update_name: 'Mettre à jour le nom',
press_to_claim: 'Appuyez pour demander du Bitcoin',
donate: 'Donner',
view_github: 'Voir sur GitHub',
voidwallet_active: 'VoidWallet est actif! Paiements désactivés',
use_with_caution:
'UTILISER AVEC PRUDENCE - Le portefeuille %{name} est toujours en version BETA',
toggle_darkmode: 'Basculer le mode sombre',
view_swagger_docs: "Voir les documents de l'API Swagger de LNbits",
api_docs: "Documents de l'API",
commit_version: 'Version de commit',
lnbits_version: 'Version de LNbits',
runs_on: 'Fonctionne sur',
credit_hint: 'Appuyez sur Entrée pour créditer le compte',
credit_label: '%{denomination} à créditer',
paste_request: 'Coller la requête',
create_invoice: 'Créer une facture',
camera_tooltip: 'Utiliser la caméra pour scanner une facture / un code QR',
export_csv: 'Exporter vers CSV',
transactions: 'Transactions',
chart_tooltip: 'Afficher le graphique',
pending: 'En attente',
copy_invoice: 'Copier la facture',
close: 'Fermer',
cancel: 'Annuler',
scan: 'Scanner',
read: 'Lire',
pay: 'Payer',
memo: 'Mémo',
date: 'Date',
processing_payment: 'Traitement du paiement...',
not_enough_funds: 'Fonds insuffisants !',
search_by_tag_memo_amount: 'Rechercher par tag, mémo, montant',
invoice_waiting: 'Facture en attente de paiement',
payment_received: 'Paiement reçu',
payment_sent: 'Paiement envoyé',
outgoing_payment_pending: 'Paiement sortant en attente',
drain_funds: 'Vider les fonds',
drain_funds_desc:
"Il s'agit d'un code QR LNURL-withdraw pour tout aspirer de ce portefeuille. Ne le partagez avec personne. Il est compatible avec balanceCheck et balanceNotify, de sorte que votre portefeuille peut continuer à retirer les fonds continuellement à partir d'ici après le premier retrait.",
i_understand: "J'ai compris",
copy_wallet_url: "Copier l'URL du portefeuille",
disclaimer_dialog:
"La fonctionnalité de connexion sera publiée dans une future mise à jour, pour l'instant, assurez-vous de mettre cette page en favori pour accéder à votre portefeuille ultérieurement ! Ce service est en BETA, et nous ne sommes pas responsables des personnes qui perdent l'accès à leurs fonds.",
no_transactions: 'Aucune transaction effectuée pour le moment',
manage_extensions: 'Gérer les extensions',
manage_server: 'Gérer le serveur',
extensions: 'Extensions',
no_extensions: "Vous n'avez installé aucune extension :(",
created: 'Créé',
payment_hash: 'Hash de paiement',
fee: 'Frais',
amount: 'Montant',
unit: 'Unité',
description: 'Description',
expiry: 'Expiration',
webhook: 'Webhook',
payment_proof: 'Preuve de paiement'
}
+90
View File
@@ -0,0 +1,90 @@
window.localisation.nl = {
server: 'Server',
theme: 'Thema',
funding: 'Financiering',
users: 'Gebruikers',
restart: 'Server opnieuw opstarten',
save: 'Opslaan',
save_tooltip: 'Sla uw wijzigingen op',
topup: 'Bijvullen',
topup_wallet: 'Een portemonnee bijvullen',
topup_hint: 'Gebruik de portemonnee-ID om elke portemonnee bij te vullen',
restart_tooltip:
'Start de server opnieuw op zodat wijzigingen van kracht worden',
add_funds_tooltip: 'Voeg geld toe aan een portemonnee.',
reset_defaults: 'Standaardinstellingen herstellen',
reset_defaults_tooltip:
'Wis alle instellingen en herstel de standaardinstellingen.',
download_backup: 'Databaseback-up downloaden',
name_your_wallet: 'Geef je %{name} portemonnee een naam',
paste_invoice_label: 'Plak een factuur, betalingsverzoek of lnurl-code*',
lnbits_description:
'Gemakkelijk in te stellen en lichtgewicht, LNbits kan op elke lightning-netwerkfinancieringsbron draaien, ondersteunt op dit moment LND, c-lightning, OpenNode, LNPay en zelfs LNbits zelf! U kunt LNbits voor uzelf laten draaien of gemakkelijk een bewaardersoplossing voor anderen bieden. Elke portemonnee heeft zijn eigen API-sleutels en er is geen limiet aan het aantal portemonnees dat u kunt maken. Het kunnen partitioneren van fondsen maakt LNbits een nuttige tool voor geldbeheer en als ontwikkelingstool. Extensies voegen extra functionaliteit toe aan LNbits, zodat u kunt experimenteren met een reeks toonaangevende technologieën op het bliksemschichtnetwerk. We hebben het ontwikkelen van extensies zo eenvoudig mogelijk gemaakt en als een gratis en opensource-project moedigen we mensen aan om hun eigen ontwikkelingen in te dienen.',
export_to_phone: 'Exporteren naar telefoon met QR-code',
export_to_phone_desc:
'Deze QR-code bevat uw portemonnee-URL met volledige toegang. U kunt het vanaf uw telefoon scannen om uw portemonnee van daaruit te openen.',
wallets: 'Portemonnees',
add_wallet: 'Een nieuwe portemonnee toevoegen',
delete_wallet: 'Portemonnee verwijderen',
delete_wallet_desc:
'Deze hele portemonnee wordt verwijderd, de fondsen worden NIET TERUGGEVONDEN.',
rename_wallet: 'Portemonnee hernoemen',
update_name: 'Naam bijwerken',
press_to_claim: 'Druk om bitcoin te claimen',
donate: 'Doneren',
view_github: 'Bekijken op GitHub',
voidwallet_active: 'VoidWallet is actief! Betalingen uitgeschakeld',
use_with_caution:
'GEBRUIK MET VOORZICHTIGHEID - %{name} portemonnee is nog in BETA',
toggle_darkmode: 'Donkere modus aan/uit',
view_swagger_docs: 'Bekijk LNbits Swagger API-documentatie',
api_docs: 'API-documentatie',
commit_version: 'Commit-versie',
lnbits_version: 'LNbits-versie',
runs_on: 'Draait op',
credit_hint: 'Druk op Enter om de rekening te crediteren',
credit_label: '%{denomination} te crediteren',
paste_request: 'Verzoek plakken',
create_invoice: 'Factuur aanmaken',
camera_tooltip: 'Gebruik de camera om een factuur/QR-code te scannen',
export_csv: 'Exporteer naar CSV',
transactions: 'Transacties',
chart_tooltip: 'Toon grafiek',
pending: 'In behandeling',
copy_invoice: 'Kopieer factuur',
close: 'Sluiten',
cancel: 'Annuleren',
scan: 'Scannen',
read: 'Lezen',
pay: 'Betalen',
memo: 'Memo',
date: 'Datum',
processing_payment: 'Verwerking betaling...',
not_enough_funds: 'Onvoldoende saldo!',
search_by_tag_memo_amount: 'Zoeken op tag, memo, bedrag',
invoice_waiting: 'Factuur wachtend op betaling',
payment_received: 'Betaling ontvangen',
payment_sent: 'Betaling verzonden',
voutgoing_payment_pending: 'Uitgaande betaling in behandeling',
drain_funds: 'Geld opnemen',
drain_funds_desc:
'Dit is een LNURL-withdraw QR-code om alles uit deze portemonnee te halen. Deel deze code niet met anderen. Het is compatibel met balanceCheck en balanceNotify zodat jouw portemonnee continu geld kan blijven opnemen vanaf hier na de eerste opname.',
i_understand: 'Ik begrijp het',
copy_wallet_url: 'Kopieer portemonnee-URL',
disclaimer_dialog:
'Inlogfunctionaliteit wordt uitgebracht in een toekomstige update. Zorg er nu voor dat je deze pagina als favoriet markeert om in de toekomst toegang te krijgen tot je portemonnee! Deze service is in BETA en we zijn niet verantwoordelijk voor mensen die de toegang tot hun fondsen verliezen.',
no_transactions: 'Er zijn nog geen transacties gedaan',
manage_extensions: 'Beheer extensies',
manage_server: 'Beheer server',
extensions: 'Extensies',
no_extensions: 'Je hebt geen extensies geïnstalleerd :(',
created: 'Aangemaakt',
payment_hash: 'Betalings-hash',
fee: 'Kosten',
amount: 'Bedrag',
unit: 'Eenheid',
description: 'Beschrijving',
expiry: 'Vervaldatum',
webhook: 'Webhook',
payment_proof: 'Betalingsbewijs'
}
+91
View File
@@ -0,0 +1,91 @@
window.localisation.pi = {
server: 'Cap`n',
theme: 'Theme',
funding: 'Funding',
users: 'Buccaneers',
restart: 'Arr, restart Cap`n',
save: 'Bury Treasure',
save_tooltip: 'Bury yer changes, matey',
topup: 'Top up the Chest',
topup_wallet: 'Add more doubloons to the chest',
topup_hint: 'Use the chest ID to top up any chest',
restart_tooltip: 'Restart the Cap`n for changes to take effect, arr!',
add_funds_tooltip: 'Add doubloons to a chest and make it heavier',
reset_defaults: 'Reset to Davy Jones Locker',
reset_defaults_tooltip:
'Scuttle all settings and reset to Davy Jones Locker. Aye, start anew!',
download_backup: 'Download database booty',
name_your_wallet: 'Name yer %{name} treasure chest',
paste_invoice_label: 'Paste a booty, payment request or lnurl code, matey!',
lnbits_description:
'Arr, easy to set up and lightweight, LNbits can run on any lightning-network funding source, currently supporting LND, c-lightning, OpenNode, LNPay and even LNbits itself! Ye can run LNbits for yourself, or easily offer a custodian solution for others. Each chest has its own API keys and there be no limit to the number of chests ye can make. Being able to partition booty makes LNbits a useful tool for money management and as a development tool. Arr, extensions add extra functionality to LNbits so ye can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage scallywags to develop and submit their own.',
export_to_phone: 'Export to Phone with QR Code, me hearties',
export_to_phone_desc:
'This QR code contains yer chest URL with full access. Ye can scan it from yer phone to open yer chest from there, arr!',
wallets: 'Treasure Chests',
add_wallet: 'Add a new chest and fill it with doubloons!',
delete_wallet: 'Scuttle the Chest',
delete_wallet_desc:
'This whole chest will be scuttled, the booty will be UNRECOVERABLE. Aye, be warned!',
rename_wallet: 'Rename the Chest, me hearty',
update_name: 'Update name like a captain',
press_to_claim: 'Press to claim gold doubloons, matey!',
donate: 'Donate like a true pirate!',
view_github: 'View on GitHub and find treasures',
voidwallet_active: 'VoidWallet be active! Payments disabled',
use_with_caution:
'USE WITH CAUTION - %{name} chest be still in BETA. Aye, be careful!',
toggle_darkmode: 'Toggle Dark Mode, arr!',
view_swagger_docs: 'View LNbits Swagger API docs and learn the secrets',
api_docs: 'API docs for the scallywags',
commit_version: 'Commit version like a true pirate',
lnbits_version: 'LNbits version, arr!',
runs_on: 'Runs on, matey',
credit_hint: 'Press Enter to credit account and make it richer',
credit_label: '%{denomination} to credit, arr!',
paste_request: 'Paste Request and find treasures',
create_invoice: 'Create Booty Request and get rich, me hearties!',
camera_tooltip: 'Use spyglass to scan a booty/QR, arr!',
export_csv: 'Export to CSV and keep track of the booty',
transactions: 'Pirate Transactions and loot',
chart_tooltip: 'Show ye chart, me hearty',
pending: 'Pendin like a ship at anchor',
copy_invoice: 'Copy booty request, arrr',
close: 'Batten down the hatches, we be closin',
cancel: 'Abandon ship! We be retreatin',
scan: 'Avast! Scan me beauty, arrr',
read: 'Read it, if ye dare',
pay: 'Pay up or walk the plank, ye scallywag',
memo: 'Message in a bottle, argh',
date: 'Date of the map, me matey',
processing_payment: 'Processing yer payment... don´t make me say it again',
not_enough_funds: 'Arrr, ye don´t have enough doubloons! Walk the plank!',
search_by_tag_memo_amount: 'Search by tag, message, or booty amount, savvy',
invoice_waiting: 'Invoice waiting to be plundered, arrr',
payment_received: 'Payment Received like a treasure, argh',
payment_sent: 'Payment Sent, hoist the colors! We´ve got some doubloons!',
outgoing_payment_pending:
'Outgoing payment pending in the port, ye scurvy dog',
drain_funds: 'Plunder all the doubloons, ye buccaneer',
drain_funds_desc:
'This be an LNURL-withdraw QR code for slurpin everything from this wallet. Don`t share with anyone. It be compatible with balanceCheck and balanceNotify so yer wallet may keep pullin` the funds continuously from here after the first withdraw.',
i_understand: 'I understand, yo ho ho and a bottle of rum!',
copy_wallet_url: 'Copy wallet URL like a map, savvy',
disclaimer_dialog:
'Login functionality to be released in a future update, for now, make sure ye bookmark this page for future access to your booty! This service be in BETA, and we hold no responsibility for people losing access to doubloons.',
no_transactions: 'No transactions made yet, me hearties. Belay that!',
manage_extensions: 'Manage Yer Extensions, ye landlubber',
manage_server: 'Manage Yer Server, me hearty',
extensions: 'Yer Extensions, ye scurvy dog',
no_extensions:
"Ye don't have any extensions installed, ye scallywag :(. Where be yer loot?",
created: 'Created like a legend, savvy',
payment_hash: 'Payment Hash like a treasure map, arrr',
fee: 'Fee like a toll to cross a strait, matey',
amount: 'Amount of doubloons, arrr',
unit: 'Unit of measurement like a fathom, ye buccaneer',
description: 'Description like a tale of adventure, arrr',
expiry: 'Expiry like the food on a ship, ye landlubber',
webhook: 'Webhook like a fishing line, arrr',
payment_proof: 'Payment Proof like a seal of authenticity, argh'
}
+8 -3
View File
@@ -67,8 +67,13 @@ window.LNbits = {
getWallet: function (wallet) { getWallet: function (wallet) {
return this.request('get', '/api/v1/wallet', wallet.inkey) return this.request('get', '/api/v1/wallet', wallet.inkey)
}, },
getPayments: function (wallet) { getPayments: function (wallet, query) {
return this.request('get', '/api/v1/payments', wallet.inkey) const params = new URLSearchParams(query)
return this.request(
'get',
'/api/v1/payments/paginated?' + params,
wallet.inkey
)
}, },
getPayment: function (wallet, paymentHash) { getPayment: function (wallet, paymentHash) {
return this.request( return this.request(
@@ -185,7 +190,7 @@ window.LNbits = {
}, },
payment: function (data) { payment: function (data) {
obj = { obj = {
checking_id: data.id, checking_id: data.checking_id,
pending: data.pending, pending: data.pending,
amount: data.amount, amount: data.amount,
fee: data.fee, fee: data.fee,
+3
View File
@@ -17,6 +17,9 @@
"/static/i18n/de.js", "/static/i18n/de.js",
"/static/i18n/es.js", "/static/i18n/es.js",
"/static/i18n/jp.js", "/static/i18n/jp.js",
"/static/i18n/pi.js",
"/static/i18n/fr.js",
"/static/i18n/nl.js",
"/static/js/base.js", "/static/js/base.js",
"/static/js/components.js", "/static/js/components.js",
"/static/js/bolt11-decoder.js" "/static/js/bolt11-decoder.js"
+15
View File
@@ -90,6 +90,21 @@
<q-item-label>JP</q-item-label> <q-item-label>JP</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item clickable v-close-popup @click="changeLanguage('fr')">
<q-item-section>
<q-item-label>FR</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="changeLanguage('pi')">
<q-item-section>
<q-item-label>PI</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="changeLanguage('nl')">
<q-item-section>
<q-item-label>NL</q-item-label>
</q-item-section>
</q-item>
</q-list> </q-list>
</q-btn-dropdown> </q-btn-dropdown>
<q-btn-dropdown <q-btn-dropdown
-1
View File
@@ -104,4 +104,3 @@ in
}; };
}; };
} }
+1328 -1328
View File
File diff suppressed because it is too large Load Diff
+79 -76
View File
@@ -1,79 +1,82 @@
{ {
"name": "lnbits", "name": "lnbits",
"scripts": { "scripts": {
"sass": "./node_modules/.bin/sass ./lnbits/static/scss/base.scss > ./lnbits/static/css/base.css", "sass": "./node_modules/.bin/sass ./lnbits/static/scss/base.scss > ./lnbits/static/css/base.css",
"vendor_copy": "node -e \"require('./package.json').vendor.forEach((file) => require('fs').copyFileSync(file, './lnbits/static/vendor/'+file.split('/').pop()))\"", "vendor_copy": "node -e \"require('./package.json').vendor.forEach((file) => require('fs').copyFileSync(file, './lnbits/static/vendor/'+file.split('/').pop()))\"",
"vendor_json": "node -e \"require('fs').writeFileSync('./lnbits/static/vendor.json', JSON.stringify(require('./package.json').bundle))\"", "vendor_json": "node -e \"require('fs').writeFileSync('./lnbits/static/vendor.json', JSON.stringify(require('./package.json').bundle))\"",
"vendor_bundle_css": "node -e \"require('concat')(require('./package.json').bundle.css.map(a => './lnbits/'+a), './lnbits/static/bundle.css')\"", "vendor_bundle_css": "node -e \"require('concat')(require('./package.json').bundle.css.map(a => './lnbits/'+a), './lnbits/static/bundle.css')\"",
"vendor_bundle_js": "node -e \"require('concat')(require('./package.json').bundle.js.map(a => './lnbits/'+a), './lnbits/static/bundle.js')\"", "vendor_bundle_js": "node -e \"require('concat')(require('./package.json').bundle.js.map(a => './lnbits/'+a), './lnbits/static/bundle.js')\"",
"vendor_minify_css": "./node_modules/.bin/minify ./lnbits/static/bundle.css > ./lnbits/static/bundle.min.css", "vendor_minify_css": "./node_modules/.bin/minify ./lnbits/static/bundle.css > ./lnbits/static/bundle.min.css",
"vendor_minify_js": "./node_modules/.bin/minify ./lnbits/static/bundle.js > ./lnbits/static/bundle.min.js" "vendor_minify_js": "./node_modules/.bin/minify ./lnbits/static/bundle.js > ./lnbits/static/bundle.min.js"
}, },
"devDependencies": { "devDependencies": {
"concat": "^1.0.3", "concat": "^1.0.3",
"minify": "^9.2.0", "minify": "^9.2.0",
"sass": "^1.60.0", "sass": "^1.60.0",
"prettier": "2.8.3", "prettier": "2.8.3",
"pyright": "1.1.289" "pyright": "1.1.289"
}, },
"dependencies": { "dependencies": {
"@chenfengyuan/vue-qrcode": "1.0.2", "@chenfengyuan/vue-qrcode": "1.0.2",
"axios": "^1.3.4", "axios": "^1.3.4",
"chart.js": "2.9", "chart.js": "2.9",
"moment": "^2.29.4", "moment": "^2.29.4",
"quasar": "1.13.2", "quasar": "1.13.2",
"underscore": "^1.13.6", "underscore": "^1.13.6",
"vue": "2.6.12", "vue": "2.6.12",
"vue-i18n": "^8.28.2", "vue-i18n": "^8.28.2",
"vue-qrcode-reader": "2.2", "vue-qrcode-reader": "2.2",
"vue-router": "3.4.3", "vue-router": "3.4.3",
"vuex": "3.5.1" "vuex": "3.5.1"
}, },
"vendor": [ "vendor": [
"./node_modules/moment/moment.js", "./node_modules/moment/moment.js",
"./node_modules/underscore/underscore.js", "./node_modules/underscore/underscore.js",
"./node_modules/axios/dist/axios.js", "./node_modules/axios/dist/axios.js",
"./node_modules/vue/dist/vue.js", "./node_modules/vue/dist/vue.js",
"./node_modules/vue-router/dist/vue-router.js", "./node_modules/vue-router/dist/vue-router.js",
"./node_modules/vue-qrcode-reader/dist/vue-qrcode-reader.browser.js", "./node_modules/vue-qrcode-reader/dist/vue-qrcode-reader.browser.js",
"./node_modules/@chenfengyuan/vue-qrcode/dist/vue-qrcode.js", "./node_modules/@chenfengyuan/vue-qrcode/dist/vue-qrcode.js",
"./node_modules/vuex/dist/vuex.js", "./node_modules/vuex/dist/vuex.js",
"./node_modules/quasar/dist/quasar.ie.polyfills.umd.min.js", "./node_modules/quasar/dist/quasar.ie.polyfills.umd.min.js",
"./node_modules/quasar/dist/quasar.umd.js", "./node_modules/quasar/dist/quasar.umd.js",
"./node_modules/chart.js/dist/Chart.bundle.js", "./node_modules/chart.js/dist/Chart.bundle.js",
"./node_modules/quasar/dist/quasar.css", "./node_modules/quasar/dist/quasar.css",
"./node_modules/chart.js/dist/Chart.css", "./node_modules/chart.js/dist/Chart.css",
"./node_modules/vue-qrcode-reader/dist/vue-qrcode-reader.css", "./node_modules/vue-qrcode-reader/dist/vue-qrcode-reader.css",
"./node_modules/vue-i18n/dist/vue-i18n.js" "./node_modules/vue-i18n/dist/vue-i18n.js"
],
"bundle": {
"js": [
"/static/vendor/moment.js",
"/static/vendor/underscore.js",
"/static/vendor/axios.js",
"/static/vendor/vue.js",
"/static/vendor/vue-router.js",
"/static/vendor/vue-qrcode-reader.browser.js",
"/static/vendor/vue-qrcode.js",
"/static/vendor/vuex.js",
"/static/vendor/quasar.ie.polyfills.umd.min.js",
"/static/vendor/quasar.umd.js",
"/static/vendor/Chart.bundle.js",
"/static/vendor/vue-i18n.js",
"/static/i18n/i18n.js",
"/static/i18n/en.js",
"/static/i18n/de.js",
"/static/i18n/es.js",
"/static/i18n/jp.js",
"/static/i18n/pi.js",
"/static/i18n/fr.js",
"/static/i18n/nl.js",
"/static/js/base.js",
"/static/js/components.js",
"/static/js/bolt11-decoder.js"
], ],
"bundle": { "css": [
"js": [ "/static/vendor/quasar.css",
"/static/vendor/moment.js", "/static/vendor/Chart.css",
"/static/vendor/underscore.js", "/static/vendor/vue-qrcode-reader.css",
"/static/vendor/axios.js", "/static/css/base.css"
"/static/vendor/vue.js", ]
"/static/vendor/vue-router.js", }
"/static/vendor/vue-qrcode-reader.browser.js",
"/static/vendor/vue-qrcode.js",
"/static/vendor/vuex.js",
"/static/vendor/quasar.ie.polyfills.umd.min.js",
"/static/vendor/quasar.umd.js",
"/static/vendor/Chart.bundle.js",
"/static/vendor/vue-i18n.js",
"/static/i18n/i18n.js",
"/static/i18n/en.js",
"/static/i18n/de.js",
"/static/i18n/es.js",
"/static/i18n/jp.js",
"/static/js/base.js",
"/static/js/components.js",
"/static/js/bolt11-decoder.js"
],
"css": [
"/static/vendor/quasar.css",
"/static/vendor/Chart.css",
"/static/vendor/vue-qrcode-reader.css",
"/static/css/base.css"
]
}
} }
Generated
+173 -1
View File
@@ -325,6 +325,18 @@ files = [
[package.dependencies] [package.dependencies]
pycparser = "*" pycparser = "*"
[[package]]
name = "cfgv"
version = "3.3.1"
description = "Validate configuration and produce human readable error messages."
category = "dev"
optional = false
python-versions = ">=3.6.1"
files = [
{file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
{file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
]
[[package]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "2.0.12" version = "2.0.12"
@@ -538,6 +550,18 @@ files = [
[package.extras] [package.extras]
graph = ["objgraph (>=1.7.2)"] graph = ["objgraph (>=1.7.2)"]
[[package]]
name = "distlib"
version = "0.3.6"
description = "Distribution utilities"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
{file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
]
[[package]] [[package]]
name = "ecdsa" name = "ecdsa"
version = "0.18.0" version = "0.18.0"
@@ -627,6 +651,22 @@ dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "passlib[bcrypt]
doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.5.0)"] doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.5.0)"]
test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.3.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "email_validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "orjson (>=3.2.1,<4.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-dataclasses (==0.6.5)", "types-orjson (==3.6.2)", "types-ujson (==4.2.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.3.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "email_validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "orjson (>=3.2.1,<4.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-dataclasses (==0.6.5)", "types-orjson (==3.6.2)", "types-ujson (==4.2.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"]
[[package]]
name = "filelock"
version = "3.12.0"
description = "A platform independent file lock."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "filelock-3.12.0-py3-none-any.whl", hash = "sha256:ad98852315c2ab702aeb628412cbf7e95b7ce8c3bf9565670b4eaecf1db370a9"},
{file = "filelock-3.12.0.tar.gz", hash = "sha256:fc03ae43288c013d2ea83c8597001b1129db351aad9c57fe2409327916b8e718"},
]
[package.extras]
docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
[[package]] [[package]]
name = "flake8" name = "flake8"
version = "6.0.0" version = "6.0.0"
@@ -760,6 +800,21 @@ cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"]
http2 = ["h2 (>=3,<5)"] http2 = ["h2 (>=3,<5)"]
socks = ["socksio (>=1.0.0,<2.0.0)"] socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]]
name = "identify"
version = "2.5.22"
description = "File identification library for Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "identify-2.5.22-py2.py3-none-any.whl", hash = "sha256:f0faad595a4687053669c112004178149f6c326db71ee999ae4636685753ad2f"},
{file = "identify-2.5.22.tar.gz", hash = "sha256:f7a93d6cf98e29bd07663c60728e7a4057615068d7a639d132dc883b2d54d31e"},
]
[package.extras]
license = ["ukkonen"]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.4" version = "3.4"
@@ -1086,6 +1141,21 @@ files = [
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
] ]
[[package]]
name = "nodeenv"
version = "1.7.0"
description = "Node.js virtual environment builder"
category = "dev"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
files = [
{file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
{file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"},
]
[package.dependencies]
setuptools = "*"
[[package]] [[package]]
name = "outcome" name = "outcome"
version = "1.2.0" version = "1.2.0"
@@ -1157,6 +1227,25 @@ files = [
dev = ["pre-commit", "tox"] dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"] testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pre-commit"
version = "3.2.2"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
{file = "pre_commit-3.2.2-py2.py3-none-any.whl", hash = "sha256:0b4210aea813fe81144e87c5a291f09ea66f199f367fa1df41b55e1d26e1e2b4"},
{file = "pre_commit-3.2.2.tar.gz", hash = "sha256:5b808fcbda4afbccf6d6633a56663fed35b6c2bc08096fd3d47ce197ac351d9d"},
]
[package.dependencies]
cfgv = ">=2.0.0"
identify = ">=1.0.0"
nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
virtualenv = ">=20.10.0"
[[package]] [[package]]
name = "protobuf" name = "protobuf"
version = "4.21.12" version = "4.21.12"
@@ -1545,6 +1634,56 @@ files = [
[package.extras] [package.extras]
cli = ["click (>=5.0)"] cli = ["click (>=5.0)"]
[[package]]
name = "pyyaml"
version = "6.0"
description = "YAML parser and emitter for Python"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
{file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
{file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
{file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
{file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
{file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
{file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
{file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
{file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
{file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
{file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
{file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
{file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
{file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
{file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
{file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
{file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
{file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
{file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
{file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
]
[[package]] [[package]]
name = "represent" name = "represent"
version = "1.6.0.post0" version = "1.6.0.post0"
@@ -1824,6 +1963,18 @@ files = [
{file = "tomlkit-0.11.7.tar.gz", hash = "sha256:f392ef70ad87a672f02519f99967d28a4d3047133e2d1df936511465fbb3791d"}, {file = "tomlkit-0.11.7.tar.gz", hash = "sha256:f392ef70ad87a672f02519f99967d28a4d3047133e2d1df936511465fbb3791d"},
] ]
[[package]]
name = "types-mock"
version = "5.0.0.6"
description = "Typing stubs for mock"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "types-mock-5.0.0.6.tar.gz", hash = "sha256:3abdee238948c6678ef5b4fd740bd41d30289a2fe4d91aac4ae9f6869e6c9333"},
{file = "types_mock-5.0.0.6-py3-none-any.whl", hash = "sha256:6946f4b2240160a1740db7d6c8e8736705acd133afd98555897db0116d41a6fc"},
]
[[package]] [[package]]
name = "types-protobuf" name = "types-protobuf"
version = "3.20.4.6" version = "3.20.4.6"
@@ -1915,6 +2066,27 @@ dev = ["Cython (>=0.29.24,<0.30.0)", "Sphinx (>=4.1.2,<4.2.0)", "aiohttp", "flak
docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
test = ["aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=19.0.0,<19.1.0)", "pycodestyle (>=2.7.0,<2.8.0)"] test = ["aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=19.0.0,<19.1.0)", "pycodestyle (>=2.7.0,<2.8.0)"]
[[package]]
name = "virtualenv"
version = "20.22.0"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "virtualenv-20.22.0-py3-none-any.whl", hash = "sha256:48fd3b907b5149c5aab7c23d9790bea4cac6bc6b150af8635febc4cfeab1275a"},
{file = "virtualenv-20.22.0.tar.gz", hash = "sha256:278753c47aaef1a0f14e6db8a4c5e1e040e90aea654d0fc1dc7e0d8a42616cc3"},
]
[package.dependencies]
distlib = ">=0.3.6,<1"
filelock = ">=3.11,<4"
platformdirs = ">=3.2,<4"
[package.extras]
docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"]
[[package]] [[package]]
name = "websocket-client" name = "websocket-client"
version = "1.3.3" version = "1.3.3"
@@ -2101,4 +2273,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10 | ^3.9" python-versions = "^3.10 | ^3.9"
content-hash = "175f26c804fefbac97f2ddaff2132e004bb105fe37761eb52cc9e5ed377eddf3" content-hash = "23981f78fb5f0da54fa4185d4fe7c009eda6b37a9372e7a471b6e10dc1058497"
+4 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "lnbits" name = "lnbits"
version = "0.10.5" version = "0.10.6"
description = "LNbits, free and open-source Lightning wallet and accounts system." description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = ["Alan Bits <alan@lnbits.com>"] authors = ["Alan Bits <alan@lnbits.com>"]
@@ -36,7 +36,7 @@ cashu = "0.9.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
flake8 = "^6.0.0" flake8 = "^6.0.0"
isort = "^5.10.1" isort = "^5.12.0"
pytest = "^7.1.2" pytest = "^7.1.2"
mock = "^4.0.3" mock = "^4.0.3"
black = "^22.6.0" black = "^22.6.0"
@@ -45,6 +45,8 @@ pytest-cov = "^3.0.0"
mypy = "^0.971" mypy = "^0.971"
types-protobuf = "^3.19.22" types-protobuf = "^3.19.22"
pylint = "^2.17.2" pylint = "^2.17.2"
pre-commit = "^3.2.2"
types-mock = "^5.0.0.6"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]
+65
View File
@@ -1,10 +1,15 @@
import asyncio
import hashlib import hashlib
from time import time
import pytest import pytest
from lnbits import bolt11 from lnbits import bolt11
from lnbits.core.models import Payment
from lnbits.core.views.api import api_payment from lnbits.core.views.api import api_payment
from lnbits.db import DB_TYPE, SQLITE
from lnbits.settings import get_wallet_class from lnbits.settings import get_wallet_class
from tests.conftest import CreateInvoiceData, api_payments_create_invoice
from ...helpers import get_random_invoice_data, is_fake from ...helpers import get_random_invoice_data, is_fake
@@ -181,6 +186,66 @@ async def test_pay_invoice_adminkey(client, invoice, adminkey_headers_from):
assert response.status_code > 300 # should fail assert response.status_code > 300 # should fail
@pytest.mark.asyncio
async def test_get_payments(client, from_wallet, adminkey_headers_from):
# Because sqlite only stores timestamps with milliseconds we have to wait a second to ensure
# a different timestamp than previous invoices
# due to this limitation both payments (normal and paginated) are tested at the same time as they are almost
# identical anyways
if DB_TYPE == SQLITE:
await asyncio.sleep(1)
ts = time()
fake_data = [
CreateInvoiceData(amount=10, memo="aaaa"),
CreateInvoiceData(amount=100, memo="bbbb"),
CreateInvoiceData(amount=1000, memo="aabb"),
]
for invoice in fake_data:
await api_payments_create_invoice(invoice, from_wallet)
async def get_payments(params: dict):
params["time[ge]"] = ts
response = await client.get(
"/api/v1/payments",
params=params,
headers=adminkey_headers_from,
)
assert response.status_code == 200
return [Payment(**payment) for payment in response.json()]
payments = await get_payments({"sortby": "amount", "direction": "desc", "limit": 2})
assert payments[-1].amount < payments[0].amount
assert len(payments) == 2
payments = await get_payments({"offset": 2, "limit": 2})
assert len(payments) == 1
payments = await get_payments({"sortby": "amount", "direction": "asc"})
assert payments[-1].amount > payments[0].amount
payments = await get_payments({"search": "aaa"})
assert len(payments) == 1
payments = await get_payments({"search": "aa"})
assert len(payments) == 2
# amount is in msat
payments = await get_payments({"amount[gt]": 10000})
assert len(payments) == 2
response = await client.get(
"/api/v1/payments/paginated",
params={"limit": 2, "time[ge]": ts},
headers=adminkey_headers_from,
)
assert response.status_code == 200
paginated = response.json()
assert len(paginated["data"]) == 2
assert paginated["total"] == len(fake_data)
# check POST /api/v1/payments/decode # check POST /api/v1/payments/decode
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_decode_invoice(client, invoice): async def test_decode_invoice(client, invoice):