Merge branch 'main' into fix/mypy

This commit is contained in:
ben 2022-07-27 19:57:23 +01:00
commit 817472c036
61 changed files with 1824 additions and 102 deletions

3
.gitignore vendored
View file

@ -35,3 +35,6 @@ coverage.xml
node_modules node_modules
lnbits/static/bundle.* lnbits/static/bundle.*
docker docker
# Nix
*result*

110
build.py Normal file
View file

@ -0,0 +1,110 @@
import warnings
import subprocess
import glob
import os
from os import path
from typing import Any, List, NamedTuple, Optional
from pathlib import Path
LNBITS_PATH = path.dirname(path.realpath(__file__)) + "/lnbits"
def get_js_vendored(prefer_minified: bool = False) -> List[str]:
paths = get_vendored(".js", prefer_minified)
def sorter(key: str):
if "moment@" in key:
return 1
if "vue@" in key:
return 2
if "vue-router@" in key:
return 3
if "polyfills" in key:
return 4
return 9
return sorted(paths, key=sorter)
def get_css_vendored(prefer_minified: bool = False) -> List[str]:
paths = get_vendored(".css", prefer_minified)
def sorter(key: str):
if "quasar@" in key:
return 1
if "vue@" in key:
return 2
if "chart.js@" in key:
return 100
return 9
return sorted(paths, key=sorter)
def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]:
paths: List[str] = []
for path in glob.glob(
os.path.join(LNBITS_PATH, "static/vendor/**"), recursive=True
):
if path.endswith(".min" + ext):
# path is minified
unminified = path.replace(".min" + ext, ext)
if prefer_minified:
paths.append(path)
if unminified in paths:
paths.remove(unminified)
elif unminified not in paths:
paths.append(path)
elif path.endswith(ext):
# path is not minified
minified = path.replace(ext, ".min" + ext)
if not prefer_minified:
paths.append(path)
if minified in paths:
paths.remove(minified)
elif minified not in paths:
paths.append(path)
return sorted(paths)
def url_for_vendored(abspath: str) -> str:
return "/" + os.path.relpath(abspath, LNBITS_PATH)
def transpile_scss():
with warnings.catch_warnings():
warnings.simplefilter("ignore")
from scss.compiler import compile_string # type: ignore
with open(os.path.join(LNBITS_PATH, "static/scss/base.scss")) as scss:
with open(os.path.join(LNBITS_PATH, "static/css/base.css"), "w") as css:
css.write(compile_string(scss.read()))
def bundle_vendored():
for getfiles, outputpath in [
(get_js_vendored, os.path.join(LNBITS_PATH, "static/bundle.js")),
(get_css_vendored, os.path.join(LNBITS_PATH, "static/bundle.css")),
]:
output = ""
for path in getfiles():
with open(path) as f:
output += "/* " + url_for_vendored(path) + " */\n" + f.read() + ";\n"
with open(outputpath, "w") as f:
f.write(output)
def build():
transpile_scss()
bundle_vendored()
# root = Path("lnbits/static/foo")
# root.mkdir(parents=True)
# root.joinpath("example.css").write_text("")
if __name__ == "__main__":
build()
#def build(setup_kwargs):
# """Build """
# transpile_scss()
# bundle_vendored()
# subprocess.run(["ls", "-la", "./lnbits/static"])

View file

@ -8,13 +8,32 @@ nav_order: 2
# Basic installation # Basic installation
You can choose between two python package managers, `venv` and `pipenv`. Both are fine but if you don't know what you're doing, just go for the first option. You can choose between four package managers, `poetry`, `pipenv`, `venv` and `nix`.
By default, LNbits will use SQLite as its database. You can also use PostgreSQL which is recommended for applications with a high load (see guide below). By default, LNbits will use SQLite as its database. You can also use PostgreSQL which is recommended for applications with a high load (see guide below).
## Option 1: pipenv ## Option 1: poetry
You can also use Pipenv to manage your python packages. ```sh
git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/
curl -sSL https://install.python-poetry.org | python3 -
poetry install
# You may need to install python 3.9, update your python following this guide https://linuxize.com/post/how-to-install-python-3-9-on-ubuntu-20-04/
mkdir data && cp .env.example .env
```
#### Running the server
```sh
poetry run lnbits
# To change port/host pass 'poetry run lnbits --port 9000 --host 0.0.0.0'
```
## Option 2: pipenv
```sh ```sh
git clone https://github.com/lnbits/lnbits-legend.git git clone https://github.com/lnbits/lnbits-legend.git
@ -44,9 +63,7 @@ pipenv run python -m uvicorn lnbits.__main__:app --port 5000 --host 0.0.0.0
Add the flag `--reload` for development (includes hot-reload). Add the flag `--reload` for development (includes hot-reload).
## Option 2: venv ## Option 3: venv
Download this repo and install the dependencies:
```sh ```sh
git clone https://github.com/lnbits/lnbits-legend.git git clone https://github.com/lnbits/lnbits-legend.git
@ -67,6 +84,26 @@ mkdir data && cp .env.example .env
If you want to host LNbits on the internet, run with the option `--host 0.0.0.0`. If you want to host LNbits on the internet, run with the option `--host 0.0.0.0`.
## Option 4: Nix
```sh
git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/
# Install nix, modern debian distros usually already include
sh <(curl -L https://nixos.org/nix/install) --daemon
nix build .#lnbits
mkdir data
```
#### Running the server
```sh
# .env variables are currently passed when running
LNBITS_DATA_FOLDER=data LNBITS_BACKEND_WALLET_CLASS=LNbitsWallet LNBITS_ENDPOINT=https://legend.lnbits.com LNBITS_KEY=7b1a78d6c78f48b09a202f2dcb2d22eb ./result/bin/lnbits --port 9000
```
### Troubleshooting ### Troubleshooting
Problems installing? These commands have helped us install LNbits. Problems installing? These commands have helped us install LNbits.

View file

@ -17,7 +17,6 @@ A backend wallet can be configured using the following LNbits environment variab
### CLightning ### CLightning
Using this wallet requires the installation of the `pylightning` Python package. Using this wallet requires the installation of the `pylightning` Python package.
If you want to use LNURLp you should use SparkWallet because of an issue with description_hash and CLightning.
- `LNBITS_BACKEND_WALLET_CLASS`: **CLightningWallet** - `LNBITS_BACKEND_WALLET_CLASS`: **CLightningWallet**
- `CLIGHTNING_RPC`: /file/path/lightning-rpc - `CLIGHTNING_RPC`: /file/path/lightning-rpc

77
flake.lock generated Normal file
View file

@ -0,0 +1,77 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1656928814,
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1657114324,
"narHash": "sha256-fWuaUNXrHcz/ciHRHlcSO92dvV3EVS0GJQUSBO5JIB4=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "a5c867d9fe9e4380452628e8f171c26b69fa9d3d",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1657261001,
"narHash": "sha256-sUZeuRYfhG59uD6xafM07bc7bAIkpcGq84Vj4B+cyms=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0be91cefefde5701f8fa957904618a13e3bb51d8",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"poetry2nix": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1657149754,
"narHash": "sha256-iSnZoqwNDDVoO175whSuvl4sS9lAb/2zZ3Sa4ywo970=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "fc1930e011dea149db81863aac22fe701f36f1b5",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "poetry2nix",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"poetry2nix": "poetry2nix"
}
}
},
"root": "root",
"version": 7
}

55
flake.nix Normal file
View file

@ -0,0 +1,55 @@
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
poetry2nix.url = "github:nix-community/poetry2nix";
};
outputs = { self, nixpkgs, poetry2nix }@inputs:
let
supportedSystems = [ "x86_64-linux" "aarch64-linux" ];
forSystems = systems: f:
nixpkgs.lib.genAttrs systems
(system: f system (import nixpkgs { inherit system; overlays = [ poetry2nix.overlay self.overlays.default ]; }));
forAllSystems = forSystems supportedSystems;
projectName = "lnbits";
in
{
devShells = forAllSystems (system: pkgs: {
default = pkgs.mkShell {
buildInputs = with pkgs; [
nodePackages.prettier
];
};
});
overlays = {
default = final: prev: {
${projectName} = self.packages.${final.hostPlatform.system}.${projectName};
};
};
packages = forAllSystems (system: pkgs: {
default = self.packages.${system}.${projectName};
${projectName} = pkgs.poetry2nix.mkPoetryApplication {
projectDir = ./.;
python = pkgs.python39;
};
});
nixosModules = {
default = { pkgs, lib, config, ... }: {
imports = [ "${./nix/modules/${projectName}-service.nix}" ];
nixpkgs.overlays = [ self.overlays.default ];
};
};
checks = forAllSystems (system: pkgs:
let
vmTests = import ./nix/tests {
makeTest = (import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; }).makeTest;
inherit inputs pkgs;
};
in
pkgs.lib.optionalAttrs pkgs.stdenv.isLinux vmTests # vmTests can only be ran on Linux, so append them only if on Linux.
//
{
# Other checks here...
}
);
};
}

View file

@ -4,7 +4,7 @@ import uvloop
from loguru import logger from loguru import logger
from starlette.requests import Request from starlette.requests import Request
from .commands import bundle_vendored, migrate_databases, transpile_scss from .commands import migrate_databases
from .settings import ( from .settings import (
DEBUG, DEBUG,
HOST, HOST,
@ -19,8 +19,6 @@ from .settings import (
uvloop.install() uvloop.install()
asyncio.create_task(migrate_databases()) asyncio.create_task(migrate_databases())
transpile_scss()
bundle_vendored()
from .app import create_app from .app import create_app

View file

@ -44,10 +44,19 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
""" """
configure_logger() configure_logger()
app = FastAPI() app = FastAPI(
app.mount("/static", StaticFiles(directory="lnbits/static"), name="static") title="LNbits API",
description="API for LNbits, the free and open source bitcoin wallet and accounts system with plugins.",
license_info={
"name": "MIT License",
"url": "https://raw.githubusercontent.com/lnbits/lnbits-legend/main/LICENSE",
},
)
app.mount("/static", StaticFiles(packages=[("lnbits", "static")]), name="static")
app.mount( app.mount(
"/core/static", StaticFiles(directory="lnbits/core/static"), name="core_static" "/core/static",
StaticFiles(packages=[("lnbits.core", "static")]),
name="core_static",
) )
origins = ["*"] origins = ["*"]

View file

@ -9,7 +9,7 @@ db = Database("ext_bleskomat")
bleskomat_static_files = [ bleskomat_static_files = [
{ {
"path": "/bleskomat/static", "path": "/bleskomat/static",
"app": StaticFiles(directory="lnbits/extensions/bleskomat/static"), "app": StaticFiles(packages=[("lnbits", "extensions/bleskomat/static")]),
"name": "bleskomat_static", "name": "bleskomat_static",
} }
] ]

View file

@ -62,4 +62,5 @@
</p> </p>
</q-card-section> </q-card-section>
</q-card> </q-card>
<q-btn flat label="Swagger API" type="a" href="../docs#/Bleskomat"></q-btn>
</q-expansion-item> </q-expansion-item>

View file

@ -12,7 +12,7 @@ db = Database("ext_copilot")
copilot_static_files = [ copilot_static_files = [
{ {
"path": "/copilot/static", "path": "/copilot/static",
"app": StaticFiles(directory="lnbits/extensions/copilot/static"), "app": StaticFiles(packages=[("lnbits", "extensions/copilot/static")]),
"name": "copilot_static", "name": "copilot_static",
} }
] ]

View file

@ -14,6 +14,7 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn flat label="Swagger API" type="a" href="../docs#/copilot"></q-btn>
<q-expansion-item group="api" dense expand-separator label="Create copilot"> <q-expansion-item group="api" dense expand-separator label="Create copilot">
<q-card> <q-card>
<q-card-section> <q-card-section>

View file

@ -9,7 +9,7 @@ db = Database("ext_discordbot")
discordbot_static_files = [ discordbot_static_files = [
{ {
"path": "/discordbot/static", "path": "/discordbot/static",
"app": StaticFiles(directory="lnbits/extensions/discordbot/static"), "app": StaticFiles(packages=[("lnbits", "extensions/discordbot/static")]),
"name": "discordbot_static", "name": "discordbot_static",
} }
] ]

View file

@ -34,6 +34,7 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn flat label="Swagger API" type="a" href="../docs#/discordbot"></q-btn>
<q-expansion-item group="api" dense expand-separator label="GET users"> <q-expansion-item group="api" dense expand-separator label="GET users">
<q-card> <q-card>
<q-card-section> <q-card-section>

View file

@ -16,7 +16,7 @@ async def create_ticket(
INSERT INTO events.ticket (id, wallet, event, name, email, registered, paid) INSERT INTO events.ticket (id, wallet, event, name, email, registered, paid)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
""", """,
(payment_hash, wallet, event, name, email, False, False), (payment_hash, wallet, event, name, email, False, True),
) )
ticket = await get_ticket(payment_hash) ticket = await get_ticket(payment_hash)

View file

@ -20,4 +20,5 @@
</p> </p>
</q-card-section> </q-card-section>
</q-card> </q-card>
<q-btn flat label="Swagger API" type="a" href="../docs#/events"></q-btn>
</q-expansion-item> </q-expansion-item>

View file

@ -135,15 +135,7 @@
var self = this var self = this
axios axios
.post( .get('/events/api/v1/tickets/' + '{{ event_id }}')
'/events/api/v1/tickets/' + '{{ event_id }}/{{ event_price }}',
{
event: '{{ event_id }}',
event_name: '{{ event_name }}',
name: self.formDialog.data.name,
email: self.formDialog.data.email
}
)
.then(function (response) { .then(function (response) {
self.paymentReq = response.data.payment_request self.paymentReq = response.data.payment_request
self.paymentCheck = response.data.payment_hash self.paymentCheck = response.data.payment_hash
@ -161,7 +153,17 @@
paymentChecker = setInterval(function () { paymentChecker = setInterval(function () {
axios axios
.get('/events/api/v1/tickets/' + self.paymentCheck) .post(
'/events/api/v1/tickets/' +
'{{ event_id }}/' +
self.paymentCheck,
{
event: '{{ event_id }}',
event_name: '{{ event_name }}',
name: self.formDialog.data.name,
email: self.formDialog.data.email
}
)
.then(function (res) { .then(function (res) {
if (res.data.paid) { if (res.data.paid) {
clearInterval(paymentChecker) clearInterval(paymentChecker)

View file

@ -133,7 +133,10 @@
var self = this var self = this
LNbits.api LNbits.api
.request('GET', '/events/api/v1/register/ticket/' + res) .request(
'GET',
'/events/api/v1/register/ticket/' + res.split('//')[1]
)
.then(function (response) { .then(function (response) {
self.$q.notify({ self.$q.notify({
type: 'positive', type: 'positive',

View file

@ -13,9 +13,8 @@
<br /> <br />
<qrcode <qrcode
:value="'{{ ticket_id }}'" :value="'ticket://{{ ticket_id }}'"
:options="{width: 340}" :options="{width: 500}"
class="rounded-borders"
></qrcode> ></qrcode>
<br /> <br />
<q-btn @click="printWindow" color="grey" class="q-ml-auto"> <q-btn @click="printWindow" color="grey" class="q-ml-auto">

View file

@ -97,8 +97,8 @@ async def api_tickets(
return [ticket.dict() for ticket in await get_tickets(wallet_ids)] return [ticket.dict() for ticket in await get_tickets(wallet_ids)]
@events_ext.post("/api/v1/tickets/{event_id}/{sats}") @events_ext.get("/api/v1/tickets/{event_id}")
async def api_ticket_make_ticket(event_id, sats, data: CreateTicket): async def api_ticket_make_ticket(event_id):
event = await get_event(event_id) event = await get_event(event_id)
if not event: if not event:
raise HTTPException( raise HTTPException(
@ -107,37 +107,35 @@ async def api_ticket_make_ticket(event_id, sats, data: CreateTicket):
try: try:
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=event.wallet, wallet_id=event.wallet,
amount=int(sats), amount=event.price_per_ticket,
memo=f"{event_id}", memo=f"{event_id}",
extra={"tag": "events"}, extra={"tag": "events"},
) )
except Exception as e: except Exception as e:
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
ticket = await create_ticket(
payment_hash=payment_hash,
wallet=event.wallet,
event=event_id,
name=data.name,
email=data.email,
)
if not ticket:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail=f"Event could not be fetched."
)
return {"payment_hash": payment_hash, "payment_request": payment_request} return {"payment_hash": payment_hash, "payment_request": payment_request}
@events_ext.get("/api/v1/tickets/{payment_hash}") @events_ext.post("/api/v1/tickets/{event_id}/{payment_hash}")
async def api_ticket_send_ticket(payment_hash): async def api_ticket_send_ticket(event_id, payment_hash, data: CreateTicket):
ticket = await get_ticket(payment_hash) event = await get_event(event_id)
try: try:
status = await api_payment(payment_hash) status = await api_payment(payment_hash)
if status["paid"]: if status["paid"]:
await set_ticket_paid(payment_hash=payment_hash) ticket = await create_ticket(
payment_hash=payment_hash,
wallet=event.wallet,
event=event_id,
name=data.name,
email=data.email,
)
if not ticket:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail=f"Event could not be fetched."
)
return {"paid": True, "ticket_id": ticket.id} return {"paid": True, "ticket_id": ticket.id}
except Exception: except Exception:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Not paid") raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Not paid")

View file

@ -12,7 +12,7 @@ db = Database("ext_jukebox")
jukebox_static_files = [ jukebox_static_files = [
{ {
"path": "/jukebox/static", "path": "/jukebox/static",
"app": StaticFiles(directory="lnbits/extensions/jukebox/static"), "app": StaticFiles(packages=[("lnbits", "extensions/jukebox/static")]),
"name": "jukebox_static", "name": "jukebox_static",
} }
] ]

View file

@ -24,6 +24,8 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn flat label="Swagger API" type="a" href="../docs#/jukebox"></q-btn>
<q-expansion-item group="api" dense expand-separator label="List jukeboxes"> <q-expansion-item group="api" dense expand-separator label="List jukeboxes">
<q-card> <q-card>
<q-card-section> <q-card-section>

View file

@ -12,7 +12,7 @@ db = Database("ext_livestream")
livestream_static_files = [ livestream_static_files = [
{ {
"path": "/livestream/static", "path": "/livestream/static",
"app": StaticFiles(directory="lnbits/extensions/livestream/static"), "app": StaticFiles(packages=[("lnbits", "extensions/livestream/static")]),
"name": "livestream_static", "name": "livestream_static",
} }
] ]

View file

@ -17,6 +17,8 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn flat label="Swagger API" type="a" href="../docs#/livestream"></q-btn>
<q-expansion-item <q-expansion-item
group="api" group="api"
dense dense

View file

@ -31,6 +31,7 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn flat label="Swagger API" type="a" href="../docs#/lnaddress"></q-btn>
<q-expansion-item group="api" dense expand-separator label="GET domains"> <q-expansion-item group="api" dense expand-separator label="GET domains">
<q-card> <q-card>
<q-card-section> <q-card-section>

View file

@ -31,5 +31,6 @@
</li> </li>
</ul> </ul>
</q-card-section> </q-card-section>
<q-btn flat label="Swagger API" type="a" href="../docs#/lndhub"></q-btn>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>

View file

@ -19,4 +19,5 @@
</p> </p>
</q-card-section> </q-card-section>
</q-card> </q-card>
<q-btn flat label="Swagger API" type="a" href="../docs#/lnticket"></q-btn>
</q-expansion-item> </q-expansion-item>

View file

@ -17,6 +17,12 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn
flat
label="Swagger API"
type="a"
href="../docs#/lnurldevice"
></q-btn>
<q-expansion-item <q-expansion-item
group="api" group="api"
dense dense

View file

@ -12,7 +12,7 @@ db = Database("ext_lnurlp")
lnurlp_static_files = [ lnurlp_static_files = [
{ {
"path": "/lnurlp/static", "path": "/lnurlp/static",
"app": StaticFiles(directory="lnbits/extensions/lnurlp/static"), "app": StaticFiles(packages=[("lnbits", "extensions/lnurlp/static")]),
"name": "lnurlp_static", "name": "lnurlp_static",
} }
] ]

View file

@ -4,6 +4,7 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn flat label="Swagger API" type="a" href="../docs#/lnurlp"></q-btn>
<q-expansion-item group="api" dense expand-separator label="List pay links"> <q-expansion-item group="api" dense expand-separator label="List pay links">
<q-card> <q-card>
<q-card-section> <q-card-section>
@ -51,6 +52,7 @@
expand-separator expand-separator
label="Create a pay link" label="Create a pay link"
> >
<q-btn flat label="Swagger API" type="a" href="../docs#/lnurlp"></q-btn>
<q-card> <q-card>
<q-card-section> <q-card-section>
<code><span class="text-green">POST</span> /lnurlp/api/v1/links</code> <code><span class="text-green">POST</span> /lnurlp/api/v1/links</code>

View file

@ -2,5 +2,5 @@
"name": "LNURLPayout", "name": "LNURLPayout",
"short_description": "Autodump wallet funds to LNURLpay", "short_description": "Autodump wallet funds to LNURLpay",
"icon": "exit_to_app", "icon": "exit_to_app",
"contributors": ["arcbtc"] "contributors": ["arcbtc","talvasconcelos"]
} }

View file

@ -4,6 +4,12 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn
flat
label="Swagger API"
type="a"
href="../docs#/lnurlpayout"
></q-btn>
<q-expansion-item group="api" dense expand-separator label="List lnurlpayout"> <q-expansion-item group="api" dense expand-separator label="List lnurlpayout">
<q-card> <q-card>
<q-card-section> <q-card-section>
@ -32,6 +38,7 @@
expand-separator expand-separator
label="Create a lnurlpayout" label="Create a lnurlpayout"
> >
<q-card> <q-card>
<q-card-section> <q-card-section>
<code <code

View file

@ -9,7 +9,7 @@ db = Database("ext_offlineshop")
offlineshop_static_files = [ offlineshop_static_files = [
{ {
"path": "/offlineshop/static", "path": "/offlineshop/static",
"app": StaticFiles(directory="lnbits/extensions/offlineshop/static"), "app": StaticFiles(packages=[("lnbits", "extensions/offlineshop/static")]),
"name": "offlineshop_static", "name": "offlineshop_static",
} }
] ]

View file

@ -47,6 +47,7 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn flat label="Swagger API" type="a" href="../docs#/offlineshop"></q-btn>
<q-expansion-item <q-expansion-item
group="api" group="api"
dense dense

View file

@ -4,6 +4,7 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn flat label="Swagger API" type="a" href="../docs#/paywall"></q-btn>
<q-expansion-item group="api" dense expand-separator label="List paywalls"> <q-expansion-item group="api" dense expand-separator label="List paywalls">
<q-card> <q-card>
<q-card-section> <q-card-section>

View file

@ -4,6 +4,7 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn flat label="Swagger API" type="a" href="../docs#/satsdice"></q-btn>
<q-expansion-item group="api" dense expand-separator label="List satsdices"> <q-expansion-item group="api" dense expand-separator label="List satsdices">
<q-card> <q-card>
<q-card-section> <q-card-section>

View file

@ -15,6 +15,7 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn flat label="Swagger API" type="a" href="../docs#/satspay"></q-btn>
<q-expansion-item group="api" dense expand-separator label="Create charge"> <q-expansion-item group="api" dense expand-separator label="Create charge">
<q-card> <q-card>
<q-card-section> <q-card-section>

View file

@ -12,7 +12,7 @@ db = Database("ext_splitpayments")
splitpayments_static_files = [ splitpayments_static_files = [
{ {
"path": "/splitpayments/static", "path": "/splitpayments/static",
"app": StaticFiles(directory="lnbits/extensions/splitpayments/static"), "app": StaticFiles(packages=[("lnbits", "extensions/splitpayments/static")]),
"name": "splitpayments_static", "name": "splitpayments_static",
} }
] ]

View file

@ -28,6 +28,12 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn
flat
label="Swagger API"
type="a"
href="../docs#/splitpayments"
></q-btn>
<q-expansion-item <q-expansion-item
group="api" group="api"
dense dense

View file

@ -15,4 +15,5 @@
> >
</p> </p>
</q-card-section> </q-card-section>
<q-btn flat label="Swagger API" type="a" href="../docs#/streamalerts"></q-btn>
</q-card> </q-card>

View file

@ -22,5 +22,6 @@
> >
</p> </p>
</q-card-section> </q-card-section>
<q-btn flat label="Swagger API" type="a" href="../docs#/subdomains"></q-btn>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>

View file

@ -12,4 +12,5 @@
> >
</p> </p>
</q-card-section> </q-card-section>
<q-btn flat label="Swagger API" type="a" href="../docs#/tipjar"></q-btn>
</q-card> </q-card>

View file

@ -4,6 +4,7 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn flat label="Swagger API" type="a" href="../docs#/tpos"></q-btn>
<q-expansion-item group="api" dense expand-separator label="List TPoS"> <q-expansion-item group="api" dense expand-separator label="List TPoS">
<q-card> <q-card>
<q-card-section> <q-card-section>

View file

@ -308,7 +308,9 @@
}, },
sat: function () { sat: function () {
if (!this.exchangeRate) return 0 if (!this.exchangeRate) return 0
return Math.ceil((this.amount / this.exchangeRate) * 100000000) return Math.ceil(
((this.amount - this.tipAmount) / this.exchangeRate) * 100000000
)
}, },
tipAmountSat: function () { tipAmountSat: function () {
if (!this.exchangeRate) return 0 if (!this.exchangeRate) return 0
@ -423,10 +425,9 @@
'{{ tpos.tip_options | tojson }}' == 'null' '{{ tpos.tip_options | tojson }}' == 'null'
? null ? null
: JSON.parse('{{ tpos.tip_options }}') : JSON.parse('{{ tpos.tip_options }}')
console.log(typeof this.tip_options, this.tip_options)
setInterval(function () { setInterval(function () {
getRates() getRates()
}, 20000) }, 120000)
} }
}) })
</script> </script>

View file

@ -28,6 +28,7 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn flat label="Swagger API" type="a" href="../docs#/usermanager"></q-btn>
<q-expansion-item group="api" dense expand-separator label="GET users"> <q-expansion-item group="api" dense expand-separator label="GET users">
<q-card> <q-card>
<q-card-section> <q-card-section>

View file

@ -20,6 +20,7 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn flat label="Swagger API" type="a" href="../docs#/watchonly"></q-btn>
<q-expansion-item group="api" dense expand-separator label="List wallets"> <q-expansion-item group="api" dense expand-separator label="List wallets">
<q-card> <q-card>
<q-card-section> <q-card-section>

View file

@ -9,7 +9,7 @@ db = Database("ext_withdraw")
withdraw_static_files = [ withdraw_static_files = [
{ {
"path": "/withdraw/static", "path": "/withdraw/static",
"app": StaticFiles(directory="lnbits/extensions/withdraw/static"), "app": StaticFiles(packages=[("lnbits", "extensions/withdraw/static")]),
"name": "withdraw_static", "name": "withdraw_static",
} }
] ]

View file

@ -4,6 +4,7 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn flat label="Swagger API" type="a" href="../docs#/withdraw"></q-btn>
<q-expansion-item <q-expansion-item
group="api" group="api"
dense dense

18
lnbits/server.py Normal file
View file

@ -0,0 +1,18 @@
import click
import uvicorn
@click.command()
@click.option("--port", default="5000", help="Port to run LNBits on")
@click.option("--host", default="127.0.0.1", help="Host to run LNBits on")
def main(port, host):
"""Launched with `poetry run lnbits` at root level"""
uvicorn.run("lnbits.__main__:app", port=port, host=host)
if __name__ == "__main__":
main()
# def main():
# """Launched with `poetry run start` at root level"""
# uvicorn.run("lnbits.__main__:app")

View file

@ -100,7 +100,7 @@ class EclairWallet(Wallet):
f"{self.url}/payinvoice", f"{self.url}/payinvoice",
headers=self.auth, headers=self.auth,
data={"invoice": bolt11, "blocking": True}, data={"invoice": bolt11, "blocking": True},
timeout=40, timeout=None,
) )
if "error" in r.json(): if "error" in r.json():

View file

@ -88,7 +88,7 @@ class LNbitsWallet(Wallet):
url=f"{self.endpoint}/api/v1/payments", url=f"{self.endpoint}/api/v1/payments",
headers=self.key, headers=self.key,
json={"out": True, "bolt11": bolt11}, json={"out": True, "bolt11": bolt11},
timeout=100, timeout=None,
) )
ok, checking_id, fee_msat, error_message = not r.is_error, None, 0, None ok, checking_id, fee_msat, error_message = not r.is_error, None, 0, None

View file

@ -111,7 +111,7 @@ class LndRestWallet(Wallet):
url=f"{self.endpoint}/v1/channels/transactions", url=f"{self.endpoint}/v1/channels/transactions",
headers=self.auth, headers=self.auth,
json={"payment_request": bolt11, "fee_limit": lnrpcFeeLimit}, json={"payment_request": bolt11, "fee_limit": lnrpcFeeLimit},
timeout=180, timeout=None,
) )
if r.is_error or r.json().get("payment_error"): if r.is_error or r.json().get("payment_error"):

View file

@ -84,7 +84,7 @@ class LNPayWallet(Wallet):
f"{self.endpoint}/wallet/{self.wallet_key}/withdraw", f"{self.endpoint}/wallet/{self.wallet_key}/withdraw",
headers=self.auth, headers=self.auth,
json={"payment_request": bolt11}, json={"payment_request": bolt11},
timeout=180, timeout=None,
) )
try: try:

View file

@ -82,7 +82,7 @@ class LntxbotWallet(Wallet):
f"{self.endpoint}/payinvoice", f"{self.endpoint}/payinvoice",
headers=self.auth, headers=self.auth,
json={"invoice": bolt11}, json={"invoice": bolt11},
timeout=100, timeout=None,
) )
if "error" in r.json(): if "error" in r.json():

View file

@ -85,7 +85,7 @@ class OpenNodeWallet(Wallet):
f"{self.endpoint}/v2/withdrawals", f"{self.endpoint}/v2/withdrawals",
headers=self.auth, headers=self.auth,
json={"type": "ln", "address": bolt11}, json={"type": "ln", "address": bolt11},
timeout=180, timeout=None,
) )
if r.is_error: if r.is_error:

View file

@ -0,0 +1,106 @@
{ config, pkgs, lib, ... }:
let
defaultUser = "lnbits";
cfg = config.services.lnbits;
inherit (lib) mkOption mkIf types optionalAttrs;
in
{
options = {
services.lnbits = {
enable = mkOption {
default = false;
type = types.bool;
description = ''
Whether to enable the lnbits service
'';
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Whether to open the ports used by lnbits in the firewall for the server
'';
};
package = mkOption {
type = types.package;
default = pkgs.lnbits;
description = ''
The lnbits package to use.
'';
};
stateDir = mkOption {
type = types.path;
default = "/var/lib/lnbits";
description = ''
The lnbits state directory which LNBITS_DATA_FOLDER will be set to
'';
};
host = mkOption {
type = types.str;
default = "127.0.0.1";
description = ''
The host to bind to
'';
};
port = mkOption {
type = types.port;
default = 8231;
description = ''
The port to run on
'';
};
user = mkOption {
type = types.str;
default = "lnbits";
description = "user to run lnbits as";
};
group = mkOption {
type = types.str;
default = "lnbits";
description = "group to run lnbits as";
};
};
};
config = mkIf cfg.enable {
users.users = optionalAttrs (cfg.user == defaultUser) {
${defaultUser} = {
isSystemUser = true;
group = defaultUser;
};
};
users.groups = optionalAttrs (cfg.group == defaultUser) {
${defaultUser} = { };
};
systemd.tmpfiles.rules = [
"d ${cfg.stateDir} 0700 ${cfg.user} ${cfg.group} - -"
];
systemd.services.lnbits = {
enable = true;
description = "lnbits";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
environment = {
LNBITS_DATA_FOLDER = "${cfg.stateDir}";
};
serviceConfig = {
User = cfg.user;
Group = cfg.group;
WorkingDirectory = "${cfg.package.src}";
StateDirectory = "${cfg.stateDir}";
ExecStart = "${lib.getExe cfg.package} --port ${toString cfg.port} --host ${cfg.host}";
Restart = "always";
PrivateTmp = true;
};
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.port ];
};
};
}

4
nix/tests/default.nix Normal file
View file

@ -0,0 +1,4 @@
{ pkgs, makeTest, inputs }:
{
vmTest = import ./nixos-module { inherit pkgs makeTest inputs; };
}

View file

@ -0,0 +1,25 @@
{ pkgs, makeTest, inputs }:
makeTest {
nodes = {
client = { config, pkgs, ... }: {
environment.systemPackages = [ pkgs.curl ];
};
lnbits = { ... }: {
imports = [ inputs.self.nixosModules.default ];
services.lnbits = {
enable = true;
openFirewall = true;
host = "0.0.0.0";
};
};
};
testScript = { nodes, ... }: ''
start_all()
lnbits.wait_for_open_port(${toString nodes.lnbits.config.services.lnbits.port})
client.wait_for_unit("multi-user.target")
with subtest("Check that the lnbits webserver can be reached."):
assert "<title>LNbits</title>" in client.succeed(
"curl -sSf http:/lnbits:8231/ | grep title"
)
'';
}

1165
poetry.lock generated Normal file

File diff suppressed because it is too large Load diff

70
pyproject.toml Normal file
View file

@ -0,0 +1,70 @@
[tool.poetry]
name = "lnbits"
version = "0.1.0"
description = ""
authors = ["matthewcroughan <matt@croughan.sh>"]
[tool.poetry.build]
generate-setup-file = false
script = "build.py"
[tool.poetry.dependencies]
python = "^3.9"
aiofiles = "0.7.0"
asgiref = "3.4.1"
attrs = "21.2.0"
bech32 = "1.2.0"
bitstring = "3.1.9"
cerberus = "1.3.4"
certifi = "2021.5.30"
charset-normalizer = "2.0.6"
click = "8.0.1"
ecdsa = "0.17.0"
embit = "0.4.9"
environs = "9.3.3"
fastapi = "0.78.0"
h11 = "0.12.0"
httpcore = "0.13.7"
httptools = "0.2.0"
httpx = "0.19.0"
idna = "3.2"
importlib-metadata = "4.8.1"
jinja2 = "3.0.1"
lnurl = "0.3.6"
markupsafe = "2.0.1"
marshmallow = "3.13.0"
outcome = "1.1.0"
psycopg2-binary = "2.9.1"
pycryptodomex = "3.14.1"
pydantic = "1.8.2"
pypng = "0.0.21"
pyqrcode = "1.2.1"
pyscss = "1.3.7"
python-dotenv = "0.19.0"
pyyaml = "5.4.1"
represent = "1.6.0.post0"
rfc3986 = "1.5.0"
secp256k1 = "0.14.0"
shortuuid = "1.0.1"
six = "1.16.0"
sniffio = "1.2.0"
sqlalchemy = "1.3.23"
sqlalchemy-aio = "0.16.0"
sse-starlette = "0.6.2"
typing-extensions = "3.10.0.2"
uvicorn = "0.18.1"
uvloop = "0.16.0"
watchgod = "0.7"
websockets = "10.0"
zipp = "3.5.0"
loguru = "0.5.3"
cffi = "1.15.0"
[tool.poetry.dev-dependencies]
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
lnbits = "lnbits.server:main"

View file

@ -1,52 +1,52 @@
aiofiles==0.7.0 aiofiles==0.8.0
anyio==3.3.1 anyio==3.6.1
asgiref==3.4.1
asyncio==3.4.3 asyncio==3.4.3
attrs==21.2.0 attrs==21.4.0
bech32==1.2.0 bech32==1.2.0
bitstring==3.1.9 bitstring==3.1.9
cerberus==1.3.4 cerberus==1.3.4
certifi==2021.5.30 certifi==2022.6.15
charset-normalizer==2.0.6 cffi==1.15.0
click==8.0.1 click==8.1.3
ecdsa==0.17.0 ecdsa==0.18.0
embit==0.4.9 embit==0.5.0
environs==9.3.3 environs==9.5.0
fastapi==0.68.1 fastapi==0.79.0
h11==0.12.0 h11==0.12.0
httpcore==0.15.0 httpcore==0.15.0
httptools==0.2.0 httptools==0.4.0
httpx==0.23.0 httpx==0.23.0
idna==3.2 idna==3.3
importlib-metadata==4.8.1
jinja2==3.0.1 jinja2==3.0.1
lnurl==0.3.6 lnurl==0.3.6
loguru==0.6.0 loguru==0.6.0
markupsafe==2.0.1 markupsafe==2.1.1
marshmallow==3.13.0 marshmallow==3.17.0
outcome==1.1.0 outcome==1.2.0
psycopg2-binary==2.9.1 packaging==21.3
pycryptodomex==3.14.1 psycopg2-binary==2.9.3
pydantic==1.8.2 pycparser==2.21
pypng==0.0.21 pycryptodomex==3.15.0
pydantic==1.9.1
pyngrok==5.1.0
pyparsing==3.0.9
pypng==0.20220715.0
pyqrcode==1.2.1 pyqrcode==1.2.1
pyscss==1.3.7 pyscss==1.4.0
python-dotenv==0.19.0 python-dotenv==0.20.0
pyyaml==5.4.1 pyyaml==6.0
represent==1.6.0.post0 represent==1.6.0.post0
rfc3986==1.5.0 rfc3986==1.5.0
secp256k1==0.14.0 secp256k1==0.14.0
cffi==1.15.0 shortuuid==1.0.9
shortuuid==1.0.1
six==1.16.0 six==1.16.0
sniffio==1.2.0 sniffio==1.2.0
sqlalchemy-aio==0.17.0
sqlalchemy==1.3.23 sqlalchemy==1.3.23
sqlalchemy-aio==0.16.0 sse-starlette==0.10.3
sse-starlette==0.6.2 starlette==0.19.1
starlette==0.14.2 typing-extensions==4.3.0
typing-extensions==3.10.0.2 uvicorn==0.18.2
uvicorn==0.15.0
uvloop==0.16.0 uvloop==0.16.0
watchgod==0.7 watchfiles==0.16.0
websockets==10.0 websockets==10.3
zipp==3.5.0