Merge branch 'main' into SCRUB
This commit is contained in:
commit
184da9958c
3
.github/workflows/mypy.yml
vendored
3
.github/workflows/mypy.yml
vendored
|
@ -5,10 +5,9 @@ on: [push, pull_request]
|
|||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ 'false' == 'true' }} # skip mypy for now
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: jpetrucciani/mypy-check@master
|
||||
with:
|
||||
mypy_flags: '--install-types --non-interactive'
|
||||
path: lnbits
|
||||
path: 'lnbits'
|
||||
|
|
8
.github/workflows/regtest.yml
vendored
8
.github/workflows/regtest.yml
vendored
|
@ -16,7 +16,6 @@ jobs:
|
|||
python-version: ${{ matrix.python-version }}
|
||||
- name: Setup Regtest
|
||||
run: |
|
||||
docker build -t lnbits-legend .
|
||||
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
||||
cd docker
|
||||
chmod +x ./tests
|
||||
|
@ -39,8 +38,8 @@ jobs:
|
|||
LNBITS_DATA_FOLDER: ./data
|
||||
LNBITS_BACKEND_WALLET_CLASS: LndRestWallet
|
||||
LND_REST_ENDPOINT: https://localhost:8081/
|
||||
LND_REST_CERT: docker/data/lnd-1/tls.cert
|
||||
LND_REST_MACAROON: docker/data/lnd-1/data/chain/bitcoin/regtest/admin.macaroon
|
||||
LND_REST_CERT: ./docker/data/lnd-1/tls.cert
|
||||
LND_REST_MACAROON: ./docker/data/lnd-1/data/chain/bitcoin/regtest/admin.macaroon
|
||||
run: |
|
||||
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
|
||||
make test-real-wallet
|
||||
|
@ -57,7 +56,6 @@ jobs:
|
|||
python-version: ${{ matrix.python-version }}
|
||||
- name: Setup Regtest
|
||||
run: |
|
||||
docker build -t lnbits-legend .
|
||||
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
||||
cd docker
|
||||
chmod +x ./tests
|
||||
|
@ -79,7 +77,7 @@ jobs:
|
|||
PORT: 5123
|
||||
LNBITS_DATA_FOLDER: ./data
|
||||
LNBITS_BACKEND_WALLET_CLASS: CLightningWallet
|
||||
CLIGHTNING_RPC: docker/data/clightning-1/regtest/lightning-rpc
|
||||
CLIGHTNING_RPC: ./docker/data/clightning-1/regtest/lightning-rpc
|
||||
run: |
|
||||
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
|
||||
make test-real-wallet
|
||||
|
|
59
.github/workflows/tests.yml
vendored
59
.github/workflows/tests.yml
vendored
|
@ -68,11 +68,11 @@ jobs:
|
|||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
pipenv-sqlite:
|
||||
poetry-sqlite:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.7, 3.8, 3.9]
|
||||
python-version: [3.9]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
|
@ -80,9 +80,56 @@ jobs:
|
|||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
env:
|
||||
VIRTUAL_ENV: ./venv
|
||||
PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
|
||||
run: |
|
||||
pip install pipenv
|
||||
pipenv install --dev
|
||||
pipenv install importlib-metadata
|
||||
python -m venv ${{ env.VIRTUAL_ENV }}
|
||||
./venv/bin/python -m pip install --upgrade pip
|
||||
./venv/bin/pip install -r requirements.txt
|
||||
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
|
||||
- name: Run tests
|
||||
run: make test-pipenv
|
||||
run: make test
|
||||
poetry-postgres:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: postgres
|
||||
ports:
|
||||
# maps tcp port 5432 on service container to the host
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.9]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
env:
|
||||
VIRTUAL_ENV: ./venv
|
||||
PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
|
||||
run: |
|
||||
python -m venv ${{ env.VIRTUAL_ENV }}
|
||||
./venv/bin/python -m pip install --upgrade pip
|
||||
./venv/bin/pip install -r requirements.txt
|
||||
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
|
||||
- name: Run tests
|
||||
env:
|
||||
LNBITS_DATABASE_URL: postgres://postgres:postgres@0.0.0.0:5432/postgres
|
||||
run: make test
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage.xml
|
44
Pipfile
44
Pipfile
|
@ -1,44 +0,0 @@
|
|||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[requires]
|
||||
python_version = "3.8"
|
||||
|
||||
[packages]
|
||||
bitstring = "*"
|
||||
cerberus = "*"
|
||||
ecdsa = "*"
|
||||
environs = "*"
|
||||
lnurl = "==0.3.6"
|
||||
loguru = "*"
|
||||
pyscss = "*"
|
||||
shortuuid = "*"
|
||||
typing-extensions = "*"
|
||||
httpx = "*"
|
||||
sqlalchemy-aio = "*"
|
||||
embit = "*"
|
||||
pyqrcode = "*"
|
||||
pypng = "*"
|
||||
sqlalchemy = "==1.3.23"
|
||||
psycopg2-binary = "*"
|
||||
aiofiles = "*"
|
||||
asyncio = "*"
|
||||
fastapi = "*"
|
||||
uvicorn = {extras = ["standard"], version = "*"}
|
||||
sse-starlette = "*"
|
||||
jinja2 = "==3.0.1"
|
||||
pyngrok = "*"
|
||||
secp256k1 = "==0.14.0"
|
||||
cffi = "==1.15.0"
|
||||
pycryptodomex = "*"
|
||||
|
||||
[dev-packages]
|
||||
black = "==20.8b1"
|
||||
pytest = "*"
|
||||
pytest-cov = "*"
|
||||
mypy = "*"
|
||||
pytest-asyncio = "*"
|
||||
requests = "*"
|
||||
mock = "*"
|
1157
Pipfile.lock
generated
1157
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
|
@ -7,7 +7,7 @@ LNbits
|
|||
|
||||
![Lightning network wallet](https://i.imgur.com/EHvK6Lq.png)
|
||||
|
||||
# LNbits v0.3 BETA, free and open-source lightning-network wallet/accounts system
|
||||
# LNbits v0.9 BETA, free and open-source lightning-network wallet/accounts system
|
||||
|
||||
(Join us on [https://t.me/lnbits](https://t.me/lnbits))
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ nav_order: 1
|
|||
# Installation
|
||||
|
||||
This guide has been moved to the [installation guide](../guide/installation.md).
|
||||
To install the developer packages, use `pipenv install --dev`.
|
||||
To install the developer packages for running tests etc before pr'ing, use `./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock black mypy isort`.
|
||||
|
||||
## Notes:
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ nav_order: 2
|
|||
|
||||
# Basic installation
|
||||
|
||||
You can choose between four package managers, `poetry`, `pipenv`, `venv` and `nix`.
|
||||
You can choose between four package managers, `poetry`, `nix` and `venv`.
|
||||
|
||||
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).
|
||||
|
||||
|
@ -33,35 +33,25 @@ poetry run lnbits
|
|||
# To change port/host pass 'poetry run lnbits --port 9000 --host 0.0.0.0'
|
||||
```
|
||||
|
||||
## Option 2: pipenv
|
||||
## Option 2: Nix
|
||||
|
||||
```sh
|
||||
git clone https://github.com/lnbits/lnbits-legend.git
|
||||
cd lnbits-legend/
|
||||
# Modern debian distros usually include Nix, however you can install with:
|
||||
# 'sh <(curl -L https://nixos.org/nix/install) --daemon', or use setup here https://nixos.org/download.html#nix-verify-installation
|
||||
|
||||
sudo apt update && sudo apt install -y pipenv
|
||||
pipenv install --dev
|
||||
# pipenv --python 3.9 install --dev (if you wish to use a version of Python higher than 3.7)
|
||||
pipenv shell
|
||||
# pipenv --python 3.9 shell (if you wish to use a version of Python higher than 3.7)
|
||||
nix build .#lnbits
|
||||
mkdir data
|
||||
|
||||
# If any of the modules fails to install, try checking and upgrading your setupTool module
|
||||
# pip install -U setuptools wheel
|
||||
|
||||
# install libffi/libpq in case "pipenv install" fails
|
||||
# sudo apt-get install -y libffi-dev libpq-dev
|
||||
|
||||
mkdir data && cp .env.example .env
|
||||
```
|
||||
|
||||
#### Running the server
|
||||
|
||||
```sh
|
||||
pipenv run python -m uvicorn lnbits.__main__:app --port 5000 --host 0.0.0.0
|
||||
```
|
||||
|
||||
Add the flag `--reload` for development (includes hot-reload).
|
||||
#### 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
|
||||
```
|
||||
|
||||
## Option 3: venv
|
||||
|
||||
|
@ -84,26 +74,6 @@ mkdir data && cp .env.example .env
|
|||
|
||||
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
|
||||
|
||||
Problems installing? These commands have helped us install LNbits.
|
||||
|
@ -112,10 +82,10 @@ Problems installing? These commands have helped us install LNbits.
|
|||
sudo apt install pkg-config libffi-dev libpq-dev
|
||||
|
||||
# if the secp256k1 build fails:
|
||||
# if you used pipenv (option 1)
|
||||
pipenv install setuptools wheel
|
||||
# if you used venv (option 2)
|
||||
# if you used venv
|
||||
./venv/bin/pip install setuptools wheel
|
||||
# if you used poetry
|
||||
poetry add setuptools wheel
|
||||
# build essentials for debian/ubuntu
|
||||
sudo apt install python3-dev gcc build-essential
|
||||
```
|
||||
|
|
|
@ -17,7 +17,6 @@ from loguru import logger
|
|||
import lnbits.settings
|
||||
from lnbits.core.tasks import register_task_listeners
|
||||
|
||||
from .commands import db_migrate, handle_assets
|
||||
from .core import core_app
|
||||
from .core.views.generic import core_html_routes
|
||||
from .helpers import (
|
||||
|
@ -93,7 +92,6 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
|
|||
check_funding_source(app)
|
||||
register_assets(app)
|
||||
register_routes(app)
|
||||
# register_commands(app)
|
||||
register_async_tasks(app)
|
||||
register_exception_handlers(app)
|
||||
|
||||
|
@ -146,12 +144,6 @@ def register_routes(app: FastAPI) -> None:
|
|||
)
|
||||
|
||||
|
||||
def register_commands(app: FastAPI):
|
||||
"""Register Click commands."""
|
||||
app.cli.add_command(db_migrate)
|
||||
app.cli.add_command(handle_assets)
|
||||
|
||||
|
||||
def register_assets(app: FastAPI):
|
||||
"""Serve each vendored asset separately or a bundle."""
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ def decode(pr: str) -> Invoice:
|
|||
invoice = Invoice()
|
||||
|
||||
# decode the amount from the hrp
|
||||
m = re.search("[^\d]+", hrp[2:])
|
||||
m = re.search(r"[^\d]+", hrp[2:])
|
||||
if m:
|
||||
amountstr = hrp[2 + m.end() :]
|
||||
if amountstr != "":
|
||||
|
@ -296,7 +296,7 @@ def _unshorten_amount(amount: str) -> int:
|
|||
# BOLT #11:
|
||||
# A reader SHOULD fail if `amount` contains a non-digit, or is followed by
|
||||
# anything except a `multiplier` in the table above.
|
||||
if not re.fullmatch("\d+[pnum]?", str(amount)):
|
||||
if not re.fullmatch(r"\d+[pnum]?", str(amount)):
|
||||
raise ValueError("Invalid amount '{}'".format(amount))
|
||||
|
||||
if unit in units:
|
||||
|
|
|
@ -113,7 +113,7 @@ async def create_wallet(
|
|||
async def update_wallet(
|
||||
wallet_id: str, new_name: str, conn: Optional[Connection] = None
|
||||
) -> Optional[Wallet]:
|
||||
await (conn or db).execute(
|
||||
return await (conn or db).execute(
|
||||
"""
|
||||
UPDATE wallets SET
|
||||
name = ?
|
||||
|
|
|
@ -106,6 +106,8 @@ class Payment(BaseModel):
|
|||
|
||||
@property
|
||||
def tag(self) -> Optional[str]:
|
||||
if self.extra is None:
|
||||
return ""
|
||||
return self.extra.get("tag")
|
||||
|
||||
@property
|
||||
|
|
|
@ -109,18 +109,15 @@ async def pay_invoice(
|
|||
raise ValueError("Amount in invoice is too high.")
|
||||
|
||||
# put all parameters that don't change here
|
||||
PaymentKwargs = TypedDict(
|
||||
"PaymentKwargs",
|
||||
{
|
||||
"wallet_id": str,
|
||||
"payment_request": str,
|
||||
"payment_hash": str,
|
||||
"amount": int,
|
||||
"memo": str,
|
||||
"extra": Optional[Dict],
|
||||
},
|
||||
)
|
||||
payment_kwargs: PaymentKwargs = dict(
|
||||
class PaymentKwargs(TypedDict):
|
||||
wallet_id: str
|
||||
payment_request: str
|
||||
payment_hash: str
|
||||
amount: int
|
||||
memo: str
|
||||
extra: Optional[Dict]
|
||||
|
||||
payment_kwargs: PaymentKwargs = PaymentKwargs(
|
||||
wallet_id=wallet_id,
|
||||
payment_request=payment_request,
|
||||
payment_hash=invoice.payment_hash,
|
||||
|
@ -272,6 +269,7 @@ async def perform_lnurlauth(
|
|||
cb = urlparse(callback)
|
||||
|
||||
k1 = unhexlify(parse_qs(cb.query)["k1"][0])
|
||||
|
||||
key = wallet.wallet.lnurlauth_key(cb.netloc)
|
||||
|
||||
def int_to_bytes_suitable_der(x: int) -> bytes:
|
||||
|
|
|
@ -55,7 +55,7 @@ async def dispatch_webhook(payment: Payment):
|
|||
data = payment.dict()
|
||||
try:
|
||||
logger.debug("sending webhook", payment.webhook)
|
||||
r = await client.post(payment.webhook, json=data, timeout=40)
|
||||
r = await client.post(payment.webhook, json=data, timeout=40) # type: ignore
|
||||
await mark_webhook_sent(payment, r.status_code)
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
await mark_webhook_sent(payment, -1)
|
||||
|
|
|
@ -3,10 +3,12 @@ import hashlib
|
|||
import json
|
||||
from binascii import unhexlify
|
||||
from http import HTTPStatus
|
||||
from typing import Dict, List, Optional, Union
|
||||
from io import BytesIO
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
||||
|
||||
import httpx
|
||||
import pyqrcode
|
||||
from fastapi import Depends, Header, Query, Request
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.params import Body
|
||||
|
@ -14,6 +16,7 @@ from loguru import logger
|
|||
from pydantic import BaseModel
|
||||
from pydantic.fields import Field
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
from starlette.responses import HTMLResponse, StreamingResponse
|
||||
|
||||
from lnbits import bolt11, lnurl
|
||||
from lnbits.core.models import Payment, Wallet
|
||||
|
@ -185,7 +188,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
|||
assert (
|
||||
data.lnurl_balance_check is not None
|
||||
), "lnurl_balance_check is required"
|
||||
save_balance_check(wallet.id, data.lnurl_balance_check)
|
||||
await save_balance_check(wallet.id, data.lnurl_balance_check)
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
|
@ -248,7 +251,7 @@ async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
|
|||
)
|
||||
async def api_payments_create(
|
||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
invoiceData: CreateInvoiceData = Body(...),
|
||||
invoiceData: CreateInvoiceData = Body(...), # type: ignore
|
||||
):
|
||||
if invoiceData.out is True and wallet.wallet_type == 0:
|
||||
if not invoiceData.bolt11:
|
||||
|
@ -291,7 +294,7 @@ async def api_payments_pay_lnurl(
|
|||
timeout=40,
|
||||
)
|
||||
if r.is_error:
|
||||
raise httpx.ConnectError
|
||||
raise httpx.ConnectError("LNURL callback connection error")
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
|
@ -354,7 +357,7 @@ async def subscribe(request: Request, wallet: Wallet):
|
|||
logger.debug("adding sse listener", payment_queue)
|
||||
api_invoice_listeners.append(payment_queue)
|
||||
|
||||
send_queue: asyncio.Queue[tuple[str, Payment]] = asyncio.Queue(0)
|
||||
send_queue: asyncio.Queue[Tuple[str, Payment]] = asyncio.Queue(0)
|
||||
|
||||
async def payment_received() -> None:
|
||||
while True:
|
||||
|
@ -393,16 +396,13 @@ async def api_payments_sse(
|
|||
async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
|
||||
# We use X_Api_Key here because we want this call to work with and without keys
|
||||
# If a valid key is given, we also return the field "details", otherwise not
|
||||
wallet = None
|
||||
try:
|
||||
if X_Api_Key.extra:
|
||||
logger.warning("No key")
|
||||
except:
|
||||
wallet = await get_wallet_for_key(X_Api_Key)
|
||||
wallet = await get_wallet_for_key(X_Api_Key) if type(X_Api_Key) == str else None
|
||||
|
||||
# we have to specify the wallet id here, because postgres and sqlite return internal payments in different order
|
||||
# and get_standalone_payment otherwise just fetches the first one, causing unpredictable results
|
||||
payment = await get_standalone_payment(
|
||||
payment_hash, wallet_id=wallet.id if wallet else None
|
||||
) # we have to specify the wallet id here, because postgres and sqlite return internal payments in different order
|
||||
# and get_standalone_payment otherwise just fetches the first one, causing unpredictable results
|
||||
)
|
||||
if payment is None:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
|
||||
|
@ -488,7 +488,8 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
|
|||
)
|
||||
|
||||
try:
|
||||
tag = data["tag"]
|
||||
tag: str = data.get("tag")
|
||||
params.update(**data)
|
||||
if tag == "channelRequest":
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
|
@ -498,10 +499,7 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
|
|||
"message": "unsupported",
|
||||
},
|
||||
)
|
||||
|
||||
params.update(**data)
|
||||
|
||||
if tag == "withdrawRequest":
|
||||
elif tag == "withdrawRequest":
|
||||
params.update(kind="withdraw")
|
||||
params.update(fixed=data["minWithdrawable"] == data["maxWithdrawable"])
|
||||
|
||||
|
@ -519,8 +517,7 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
|
|||
query=urlencode(qs, doseq=True)
|
||||
)
|
||||
params.update(callback=urlunparse(parsed_callback))
|
||||
|
||||
if tag == "payRequest":
|
||||
elif tag == "payRequest":
|
||||
params.update(kind="pay")
|
||||
params.update(fixed=data["minSendable"] == data["maxSendable"])
|
||||
|
||||
|
@ -538,8 +535,8 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
|
|||
params.update(image=data_uri)
|
||||
if k == "text/email" or k == "text/identifier":
|
||||
params.update(targetUser=v)
|
||||
|
||||
params.update(commentAllowed=data.get("commentAllowed", 0))
|
||||
|
||||
except KeyError as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
||||
|
@ -612,8 +609,8 @@ class ConversionData(BaseModel):
|
|||
async def api_fiat_as_sats(data: ConversionData):
|
||||
output = {}
|
||||
if data.from_ == "sat":
|
||||
output["sats"] = int(data.amount)
|
||||
output["BTC"] = data.amount / 100000000
|
||||
output["sats"] = int(data.amount)
|
||||
for currency in data.to.split(","):
|
||||
output[currency.strip().upper()] = await satoshis_amount_as_fiat(
|
||||
data.amount, currency.strip()
|
||||
|
@ -624,3 +621,24 @@ async def api_fiat_as_sats(data: ConversionData):
|
|||
output["sats"] = await fiat_amount_as_satoshis(data.amount, data.from_)
|
||||
output["BTC"] = output["sats"] / 100000000
|
||||
return output
|
||||
|
||||
|
||||
@core_app.get("/api/v1/qrcode/{data}", response_class=StreamingResponse)
|
||||
async def img(request: Request, data):
|
||||
qr = pyqrcode.create(data)
|
||||
stream = BytesIO()
|
||||
qr.svg(stream, scale=3)
|
||||
stream.seek(0)
|
||||
|
||||
async def _generator(stream: BytesIO):
|
||||
yield stream.getvalue()
|
||||
|
||||
return StreamingResponse(
|
||||
_generator(stream),
|
||||
headers={
|
||||
"Content-Type": "image/svg+xml",
|
||||
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||
"Pragma": "no-cache",
|
||||
"Expires": "0",
|
||||
},
|
||||
)
|
||||
|
|
|
@ -55,9 +55,9 @@ async def home(request: Request, lightning: str = None):
|
|||
)
|
||||
async def extensions(
|
||||
request: Request,
|
||||
user: User = Depends(check_user_exists),
|
||||
enable: str = Query(None),
|
||||
disable: str = Query(None),
|
||||
user: User = Depends(check_user_exists), # type: ignore
|
||||
enable: str = Query(None), # type: ignore
|
||||
disable: str = Query(None), # type: ignore
|
||||
):
|
||||
extension_to_enable = enable
|
||||
extension_to_disable = disable
|
||||
|
@ -88,7 +88,7 @@ async def extensions(
|
|||
|
||||
# Update user as his extensions have been updated
|
||||
if extension_to_enable or extension_to_disable:
|
||||
user = await get_user(user.id)
|
||||
user = await get_user(user.id) # type: ignore
|
||||
|
||||
return template_renderer().TemplateResponse(
|
||||
"core/extensions.html", {"request": request, "user": user.dict()}
|
||||
|
@ -109,10 +109,10 @@ nothing: create everything<br>
|
|||
""",
|
||||
)
|
||||
async def wallet(
|
||||
request: Request = Query(None),
|
||||
nme: Optional[str] = Query(None),
|
||||
usr: Optional[UUID4] = Query(None),
|
||||
wal: Optional[UUID4] = Query(None),
|
||||
request: Request = Query(None), # type: ignore
|
||||
nme: Optional[str] = Query(None), # type: ignore
|
||||
usr: Optional[UUID4] = Query(None), # type: ignore
|
||||
wal: Optional[UUID4] = Query(None), # type: ignore
|
||||
):
|
||||
user_id = usr.hex if usr else None
|
||||
wallet_id = wal.hex if wal else None
|
||||
|
@ -121,7 +121,7 @@ async def wallet(
|
|||
|
||||
if not user_id:
|
||||
user = await get_user((await create_account()).id)
|
||||
logger.info(f"Create user {user.id}")
|
||||
logger.info(f"Create user {user.id}") # type: ignore
|
||||
else:
|
||||
user = await get_user(user_id)
|
||||
if not user:
|
||||
|
@ -135,22 +135,22 @@ async def wallet(
|
|||
if LNBITS_ADMIN_USERS and user_id in LNBITS_ADMIN_USERS:
|
||||
user.admin = True
|
||||
if not wallet_id:
|
||||
if user.wallets and not wallet_name:
|
||||
wallet = user.wallets[0]
|
||||
if user.wallets and not wallet_name: # type: ignore
|
||||
wallet = user.wallets[0] # type: ignore
|
||||
else:
|
||||
wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name)
|
||||
wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name) # type: ignore
|
||||
logger.info(
|
||||
f"Created new wallet {wallet_name if wallet_name else '(no name)'} for user {user.id}"
|
||||
f"Created new wallet {wallet_name if wallet_name else '(no name)'} for user {user.id}" # type: ignore
|
||||
)
|
||||
|
||||
return RedirectResponse(
|
||||
f"/wallet?usr={user.id}&wal={wallet.id}",
|
||||
f"/wallet?usr={user.id}&wal={wallet.id}", # type: ignore
|
||||
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
|
||||
)
|
||||
|
||||
logger.debug(f"Access wallet {wallet_name}{'of user '+ user.id if user else ''}")
|
||||
wallet = user.get_wallet(wallet_id)
|
||||
if not wallet:
|
||||
userwallet = user.get_wallet(wallet_id) # type: ignore
|
||||
if not userwallet:
|
||||
return template_renderer().TemplateResponse(
|
||||
"error.html", {"request": request, "err": "Wallet not found"}
|
||||
)
|
||||
|
@ -159,10 +159,10 @@ async def wallet(
|
|||
"core/wallet.html",
|
||||
{
|
||||
"request": request,
|
||||
"user": user.dict(),
|
||||
"wallet": wallet.dict(),
|
||||
"user": user.dict(), # type: ignore
|
||||
"wallet": userwallet.dict(),
|
||||
"service_fee": service_fee,
|
||||
"web_manifest": f"/manifest/{user.id}.webmanifest",
|
||||
"web_manifest": f"/manifest/{user.id}.webmanifest", # type: ignore
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -216,20 +216,20 @@ async def lnurl_full_withdraw_callback(request: Request):
|
|||
|
||||
|
||||
@core_html_routes.get("/deletewallet", response_class=RedirectResponse)
|
||||
async def deletewallet(request: Request, wal: str = Query(...), usr: str = Query(...)):
|
||||
async def deletewallet(request: Request, wal: str = Query(...), usr: str = Query(...)): # type: ignore
|
||||
user = await get_user(usr)
|
||||
user_wallet_ids = [u.id for u in user.wallets]
|
||||
user_wallet_ids = [u.id for u in user.wallets] # type: ignore
|
||||
|
||||
if wal not in user_wallet_ids:
|
||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Not your wallet.")
|
||||
else:
|
||||
await delete_wallet(user_id=user.id, wallet_id=wal)
|
||||
await delete_wallet(user_id=user.id, wallet_id=wal) # type: ignore
|
||||
user_wallet_ids.remove(wal)
|
||||
logger.debug("Deleted wallet {wal} of user {user.id}")
|
||||
|
||||
if user_wallet_ids:
|
||||
return RedirectResponse(
|
||||
url_for("/wallet", usr=user.id, wal=user_wallet_ids[0]),
|
||||
url_for("/wallet", usr=user.id, wal=user_wallet_ids[0]), # type: ignore
|
||||
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
|
||||
)
|
||||
|
||||
|
@ -242,7 +242,7 @@ async def deletewallet(request: Request, wal: str = Query(...), usr: str = Query
|
|||
async def lnurl_balance_notify(request: Request, service: str):
|
||||
bc = await get_balance_check(request.query_params.get("wal"), service)
|
||||
if bc:
|
||||
redeem_lnurl_withdraw(bc.wallet, bc.url)
|
||||
await redeem_lnurl_withdraw(bc.wallet, bc.url)
|
||||
|
||||
|
||||
@core_html_routes.get(
|
||||
|
@ -252,7 +252,7 @@ async def lnurlwallet(request: Request):
|
|||
async with db.connect() as conn:
|
||||
account = await create_account(conn=conn)
|
||||
user = await get_user(account.id, conn=conn)
|
||||
wallet = await create_wallet(user_id=user.id, conn=conn)
|
||||
wallet = await create_wallet(user_id=user.id, conn=conn) # type: ignore
|
||||
|
||||
asyncio.create_task(
|
||||
redeem_lnurl_withdraw(
|
||||
|
@ -265,7 +265,7 @@ async def lnurlwallet(request: Request):
|
|||
)
|
||||
|
||||
return RedirectResponse(
|
||||
f"/wallet?usr={user.id}&wal={wallet.id}",
|
||||
f"/wallet?usr={user.id}&wal={wallet.id}", # type: ignore
|
||||
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
|
||||
)
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from http import HTTPStatus
|
||||
from typing import Union
|
||||
|
||||
from cerberus import Validator # type: ignore
|
||||
from fastapi import status
|
||||
|
@ -29,20 +30,21 @@ class KeyChecker(SecurityBase):
|
|||
self._key_type = "invoice"
|
||||
self._api_key = api_key
|
||||
if api_key:
|
||||
self.model: APIKey = APIKey(
|
||||
key = APIKey(
|
||||
**{"in": APIKeyIn.query},
|
||||
name="X-API-KEY",
|
||||
description="Wallet API Key - QUERY",
|
||||
)
|
||||
else:
|
||||
self.model: APIKey = APIKey(
|
||||
key = APIKey(
|
||||
**{"in": APIKeyIn.header},
|
||||
name="X-API-KEY",
|
||||
description="Wallet API Key - HEADER",
|
||||
)
|
||||
self.wallet = None
|
||||
self.wallet = None # type: ignore
|
||||
self.model: APIKey = key
|
||||
|
||||
async def __call__(self, request: Request) -> Wallet:
|
||||
async def __call__(self, request: Request):
|
||||
try:
|
||||
key_value = (
|
||||
self._api_key
|
||||
|
@ -52,7 +54,7 @@ class KeyChecker(SecurityBase):
|
|||
# FIXME: Find another way to validate the key. A fetch from DB should be avoided here.
|
||||
# Also, we should not return the wallet here - thats silly.
|
||||
# Possibly store it in a Redis DB
|
||||
self.wallet = await get_wallet_for_key(key_value, self._key_type)
|
||||
self.wallet = await get_wallet_for_key(key_value, self._key_type) # type: ignore
|
||||
if not self.wallet:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.UNAUTHORIZED,
|
||||
|
@ -120,8 +122,8 @@ api_key_query = APIKeyQuery(
|
|||
|
||||
async def get_key_type(
|
||||
r: Request,
|
||||
api_key_header: str = Security(api_key_header),
|
||||
api_key_query: str = Security(api_key_query),
|
||||
api_key_header: str = Security(api_key_header), # type: ignore
|
||||
api_key_query: str = Security(api_key_query), # type: ignore
|
||||
) -> WalletTypeInfo:
|
||||
# 0: admin
|
||||
# 1: invoice
|
||||
|
@ -134,9 +136,9 @@ async def get_key_type(
|
|||
token = api_key_header if api_key_header else api_key_query
|
||||
|
||||
try:
|
||||
checker = WalletAdminKeyChecker(api_key=token)
|
||||
await checker.__call__(r)
|
||||
wallet = WalletTypeInfo(0, checker.wallet)
|
||||
admin_checker = WalletAdminKeyChecker(api_key=token)
|
||||
await admin_checker.__call__(r)
|
||||
wallet = WalletTypeInfo(0, admin_checker.wallet) # type: ignore
|
||||
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (
|
||||
LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS
|
||||
):
|
||||
|
@ -153,9 +155,9 @@ async def get_key_type(
|
|||
raise
|
||||
|
||||
try:
|
||||
checker = WalletInvoiceKeyChecker(api_key=token)
|
||||
await checker.__call__(r)
|
||||
wallet = WalletTypeInfo(1, checker.wallet)
|
||||
invoice_checker = WalletInvoiceKeyChecker(api_key=token)
|
||||
await invoice_checker.__call__(r)
|
||||
wallet = WalletTypeInfo(1, invoice_checker.wallet) # type: ignore
|
||||
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (
|
||||
LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS
|
||||
):
|
||||
|
@ -167,15 +169,16 @@ async def get_key_type(
|
|||
if e.status_code == HTTPStatus.BAD_REQUEST:
|
||||
raise
|
||||
if e.status_code == HTTPStatus.UNAUTHORIZED:
|
||||
return WalletTypeInfo(2, None)
|
||||
return WalletTypeInfo(2, None) # type: ignore
|
||||
except:
|
||||
raise
|
||||
return wallet
|
||||
|
||||
|
||||
async def require_admin_key(
|
||||
r: Request,
|
||||
api_key_header: str = Security(api_key_header),
|
||||
api_key_query: str = Security(api_key_query),
|
||||
api_key_header: str = Security(api_key_header), # type: ignore
|
||||
api_key_query: str = Security(api_key_query), # type: ignore
|
||||
):
|
||||
token = api_key_header if api_key_header else api_key_query
|
||||
|
||||
|
@ -193,8 +196,8 @@ async def require_admin_key(
|
|||
|
||||
async def require_invoice_key(
|
||||
r: Request,
|
||||
api_key_header: str = Security(api_key_header),
|
||||
api_key_query: str = Security(api_key_query),
|
||||
api_key_header: str = Security(api_key_header), # type: ignore
|
||||
api_key_query: str = Security(api_key_query), # type: ignore
|
||||
):
|
||||
token = api_key_header if api_key_header else api_key_query
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ class ExtensionManager:
|
|||
|
||||
@property
|
||||
def extensions(self) -> List[Extension]:
|
||||
output = []
|
||||
output: List[Extension] = []
|
||||
|
||||
if "all" in self._disabled:
|
||||
return output
|
||||
|
|
|
@ -21,7 +21,7 @@ class Jinja2Templates(templating.Jinja2Templates):
|
|||
self.env = self.get_environment(loader)
|
||||
|
||||
def get_environment(self, loader: "jinja2.BaseLoader") -> "jinja2.Environment":
|
||||
@jinja2.contextfunction
|
||||
@jinja2.pass_context
|
||||
def url_for(context: dict, name: str, **path_params: typing.Any) -> str:
|
||||
request: Request = context["request"]
|
||||
return request.app.url_path_for(name, **path_params)
|
||||
|
|
|
@ -66,7 +66,7 @@ async def webhook_handler():
|
|||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
|
||||
|
||||
internal_invoice_queue = asyncio.Queue(0)
|
||||
internal_invoice_queue: asyncio.Queue = asyncio.Queue(0)
|
||||
|
||||
|
||||
async def internal_invoice_listener():
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import asyncio
|
||||
from typing import Callable, NamedTuple
|
||||
from typing import Callable, List, NamedTuple
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
|
@ -227,10 +227,10 @@ async def btc_price(currency: str) -> float:
|
|||
"TO": currency.upper(),
|
||||
"to": currency.lower(),
|
||||
}
|
||||
rates = []
|
||||
tasks = []
|
||||
rates: List[float] = []
|
||||
tasks: List[asyncio.Task] = []
|
||||
|
||||
send_channel = asyncio.Queue()
|
||||
send_channel: asyncio.Queue = asyncio.Queue()
|
||||
|
||||
async def controller():
|
||||
failures = 0
|
||||
|
|
|
@ -7,7 +7,10 @@ from typing import AsyncGenerator, Dict, Optional
|
|||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
from websockets import connect
|
||||
|
||||
# TODO: https://github.com/lnbits/lnbits-legend/issues/764
|
||||
# mypy https://github.com/aaugustin/websockets/issues/940
|
||||
from websockets import connect # type: ignore
|
||||
from websockets.exceptions import (
|
||||
ConnectionClosed,
|
||||
ConnectionClosedError,
|
||||
|
|
|
@ -28,7 +28,7 @@ class FakeWallet(Wallet):
|
|||
logger.info(
|
||||
"FakeWallet funding source is for using LNbits as a centralised, stand-alone payment system with brrrrrr."
|
||||
)
|
||||
return StatusResponse(None, float("inf"))
|
||||
return StatusResponse(None, 1000000000)
|
||||
|
||||
async def create_invoice(
|
||||
self,
|
||||
|
@ -82,7 +82,7 @@ class FakeWallet(Wallet):
|
|||
invoice = decode(bolt11)
|
||||
if (
|
||||
hasattr(invoice, "checking_id")
|
||||
and invoice.checking_id[6:] == data["privkey"][:6]
|
||||
and invoice.checking_id[6:] == data["privkey"][:6] # type: ignore
|
||||
):
|
||||
return PaymentResponse(True, invoice.payment_hash, 0)
|
||||
else:
|
||||
|
@ -97,7 +97,7 @@ class FakeWallet(Wallet):
|
|||
return PaymentStatus(None)
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
self.queue = asyncio.Queue(0)
|
||||
self.queue: asyncio.Queue = asyncio.Queue(0)
|
||||
while True:
|
||||
value = await self.queue.get()
|
||||
yield value
|
||||
|
|
|
@ -119,7 +119,7 @@ class LNPayWallet(Wallet):
|
|||
return PaymentStatus(statuses[r.json()["settled"]])
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
self.queue = asyncio.Queue(0)
|
||||
self.queue: asyncio.Queue = asyncio.Queue(0)
|
||||
while True:
|
||||
value = await self.queue.get()
|
||||
yield value
|
||||
|
|
|
@ -73,10 +73,10 @@ class AESCipher(object):
|
|||
final_key += key
|
||||
return final_key[:output]
|
||||
|
||||
def decrypt(self, encrypted: str) -> str:
|
||||
def decrypt(self, encrypted: str) -> str: # type: ignore
|
||||
"""Decrypts a string using AES-256-CBC."""
|
||||
passphrase = self.passphrase
|
||||
encrypted = base64.b64decode(encrypted)
|
||||
encrypted = base64.b64decode(encrypted) # type: ignore
|
||||
assert encrypted[0:8] == b"Salted__"
|
||||
salt = encrypted[8:16]
|
||||
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
||||
|
@ -84,7 +84,7 @@ class AESCipher(object):
|
|||
iv = key_iv[32:]
|
||||
aes = AES.new(key, AES.MODE_CBC, iv)
|
||||
try:
|
||||
return self.unpad(aes.decrypt(encrypted[16:])).decode()
|
||||
return self.unpad(aes.decrypt(encrypted[16:])).decode() # type: ignore
|
||||
except UnicodeDecodeError:
|
||||
raise ValueError("Wrong passphrase")
|
||||
|
||||
|
|
|
@ -127,7 +127,7 @@ class OpenNodeWallet(Wallet):
|
|||
return PaymentStatus(statuses[r.json()["data"]["status"]])
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
self.queue = asyncio.Queue(0)
|
||||
self.queue: asyncio.Queue = asyncio.Queue(0)
|
||||
while True:
|
||||
value = await self.queue.get()
|
||||
yield value
|
||||
|
|
7
mypy.ini
7
mypy.ini
|
@ -1,7 +1,8 @@
|
|||
[mypy]
|
||||
ignore_missing_imports = True
|
||||
exclude = lnbits/wallets/lnd_grpc_files/
|
||||
exclude = lnbits/extensions/
|
||||
|
||||
exclude = (?x)(
|
||||
^lnbits/extensions.
|
||||
| ^lnbits/wallets/lnd_grpc_files.
|
||||
)
|
||||
[mypy-lnbits.wallets.lnd_grpc_files.*]
|
||||
follow_imports = skip
|
||||
|
|
1
result
Symbolic link
1
result
Symbolic link
|
@ -0,0 +1 @@
|
|||
/nix/store/ds9c48q7hnkdmpzy3aq14kc1x9wrrszd-python3.9-lnbits-0.1.0
|
|
@ -1,6 +1,7 @@
|
|||
import pytest
|
||||
import pytest_asyncio
|
||||
from lnbits.core.crud import get_wallet
|
||||
from lnbits.core.views.api import api_payment
|
||||
|
||||
from ...helpers import get_random_invoice_data
|
||||
|
||||
|
@ -155,3 +156,26 @@ async def test_decode_invoice(client, invoice):
|
|||
)
|
||||
assert response.status_code < 300
|
||||
assert response.json()["payment_hash"] == invoice["payment_hash"]
|
||||
|
||||
|
||||
# check api_payment() internal function call (NOT API): payment status
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_payment_without_key(invoice):
|
||||
# check the payment status
|
||||
response = await api_payment(invoice["payment_hash"])
|
||||
assert type(response) == dict
|
||||
assert response["paid"] == True
|
||||
# no key, that's why no "details"
|
||||
assert "details" not in response
|
||||
|
||||
|
||||
# check api_payment() internal function call (NOT API): payment status
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_payment_with_key(invoice, inkey_headers_from):
|
||||
# check the payment status
|
||||
response = await api_payment(
|
||||
invoice["payment_hash"], inkey_headers_from["X-Api-Key"]
|
||||
)
|
||||
assert type(response) == dict
|
||||
assert response["paid"] == True
|
||||
assert "details" in response
|
||||
|
|
Loading…
Reference in New Issue
Block a user