feat: add linters and ci (#28)
* feat: introduce linting and ci * add locks * prettier * black and sorting * f405 missing imports * E902 * mypy * renderer * circular imports * check comment * add exports * add lnurlerrorhandler only on lnurl routes * add test case
This commit is contained in:
parent
b5b5abd776
commit
a44820f61f
23 changed files with 2934 additions and 145 deletions
10
.github/workflows/lint.yml
vendored
Normal file
10
.github/workflows/lint.yml
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
name: lint
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
uses: lnbits/lnbits/.github/workflows/lint.yml@dev
|
||||
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
|
|
@ -1,10 +1,9 @@
|
|||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
|
||||
jobs:
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
|
@ -34,12 +33,12 @@ jobs:
|
|||
- name: Create pull request in extensions repo
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.EXT_GITHUB }}
|
||||
repo_name: "${{ github.event.repository.name }}"
|
||||
tag: "${{ github.ref_name }}"
|
||||
branch: "update-${{ github.event.repository.name }}-${{ github.ref_name }}"
|
||||
title: "[UPDATE] ${{ github.event.repository.name }} to ${{ github.ref_name }}"
|
||||
body: "https://github.com/lnbits/${{ github.event.repository.name }}/releases/${{ github.ref_name }}"
|
||||
archive: "https://github.com/lnbits/${{ github.event.repository.name }}/archive/refs/tags/${{ github.ref_name }}.zip"
|
||||
repo_name: '${{ github.event.repository.name }}'
|
||||
tag: '${{ github.ref_name }}'
|
||||
branch: 'update-${{ github.event.repository.name }}-${{ github.ref_name }}'
|
||||
title: '[UPDATE] ${{ github.event.repository.name }} to ${{ github.ref_name }}'
|
||||
body: 'https://github.com/lnbits/${{ github.event.repository.name }}/releases/${{ github.ref_name }}'
|
||||
archive: 'https://github.com/lnbits/${{ github.event.repository.name }}/archive/refs/tags/${{ github.ref_name }}.zip'
|
||||
run: |
|
||||
cd lnbits-extensions
|
||||
git checkout -b $branch
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1 +1,4 @@
|
|||
__pycache__
|
||||
node_modules
|
||||
.mypy_cache
|
||||
.venv
|
||||
|
|
|
|||
27
.pre-commit-config.yaml
Normal file
27
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
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: 24.2.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.3.2
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: 'v4.0.0-alpha.8'
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [css, javascript, html, json]
|
||||
12
.prettierrc
Normal file
12
.prettierrc
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"semi": false,
|
||||
"arrowParens": "avoid",
|
||||
"insertPragma": false,
|
||||
"printWidth": 80,
|
||||
"proseWrap": "preserve",
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"useTabs": false,
|
||||
"bracketSameLine": false,
|
||||
"bracketSpacing": false
|
||||
}
|
||||
47
Makefile
Normal file
47
Makefile
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
all: format check
|
||||
|
||||
format: prettier black ruff
|
||||
|
||||
check: mypy pyright checkblack checkruff checkprettier
|
||||
|
||||
prettier:
|
||||
poetry run ./node_modules/.bin/prettier --write .
|
||||
pyright:
|
||||
poetry run ./node_modules/.bin/pyright
|
||||
|
||||
mypy:
|
||||
poetry run mypy .
|
||||
|
||||
black:
|
||||
poetry run black .
|
||||
|
||||
ruff:
|
||||
poetry run ruff check . --fix
|
||||
|
||||
checkruff:
|
||||
poetry run ruff check .
|
||||
|
||||
checkprettier:
|
||||
poetry run ./node_modules/.bin/prettier --check .
|
||||
|
||||
checkblack:
|
||||
poetry run black --check .
|
||||
|
||||
checkeditorconfig:
|
||||
editorconfig-checker
|
||||
|
||||
test:
|
||||
PYTHONUNBUFFERED=1 \
|
||||
DEBUG=true \
|
||||
poetry run pytest
|
||||
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
|
||||
|
||||
|
||||
checkbundle:
|
||||
@echo "skipping checkbundle"
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
# LNURLw - <small>[LNbits](https://github.com/lnbits/lnbits) extension</small>
|
||||
|
||||
<small>For more about LNBits extension check [this tutorial](https://github.com/lnbits/lnbits/wiki/LNbits-Extensions)</small>
|
||||
|
||||
## Create a static QR code people can use to withdraw funds from a Lightning Network wallet
|
||||
|
|
|
|||
49
__init__.py
49
__init__.py
|
|
@ -1,13 +1,9 @@
|
|||
from fastapi import APIRouter, Request, Response
|
||||
from fastapi.routing import APIRoute
|
||||
from fastapi import APIRouter
|
||||
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from lnbits.db import Database
|
||||
from lnbits.helpers import template_renderer
|
||||
from typing import Callable
|
||||
|
||||
db = Database("ext_withdraw")
|
||||
from .crud import db
|
||||
from .views import withdraw_ext_generic
|
||||
from .views_api import withdraw_ext_api
|
||||
from .views_lnurl import withdraw_ext_lnurl
|
||||
|
||||
withdraw_static_files = [
|
||||
{
|
||||
|
|
@ -16,36 +12,9 @@ withdraw_static_files = [
|
|||
}
|
||||
]
|
||||
|
||||
|
||||
class LNURLErrorResponseHandler(APIRoute):
|
||||
def get_route_handler(self) -> Callable:
|
||||
original_route_handler = super().get_route_handler()
|
||||
|
||||
async def custom_route_handler(request: Request) -> Response:
|
||||
try:
|
||||
response = await original_route_handler(request)
|
||||
except HTTPException as exc:
|
||||
logger.debug(f"HTTPException: {exc}")
|
||||
response = JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"status": "ERROR", "reason": f"{exc.detail}"},
|
||||
)
|
||||
except Exception as exc:
|
||||
raise exc
|
||||
|
||||
return response
|
||||
|
||||
return custom_route_handler
|
||||
|
||||
|
||||
withdraw_ext: APIRouter = APIRouter(prefix="/withdraw", tags=["withdraw"])
|
||||
withdraw_ext.route_class = LNURLErrorResponseHandler
|
||||
withdraw_ext.include_router(withdraw_ext_generic)
|
||||
withdraw_ext.include_router(withdraw_ext_api)
|
||||
withdraw_ext.include_router(withdraw_ext_lnurl)
|
||||
|
||||
|
||||
def withdraw_renderer():
|
||||
return template_renderer(["withdraw/templates"])
|
||||
|
||||
|
||||
from .lnurl import * # noqa: F401,F403
|
||||
from .views import * # noqa: F401,F403
|
||||
from .views_api import * # noqa: F401,F403
|
||||
__all__ = ["withdraw_ext", "withdraw_static_files", "db"]
|
||||
|
|
|
|||
17
crud.py
17
crud.py
|
|
@ -2,12 +2,13 @@ from datetime import datetime
|
|||
from typing import List, Optional, Union
|
||||
|
||||
import shortuuid
|
||||
|
||||
from lnbits.db import Database
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from . import db
|
||||
from .models import CreateWithdrawData, HashCheck, WithdrawLink
|
||||
|
||||
db = Database("ext_withdraw")
|
||||
|
||||
|
||||
async def create_withdraw_link(
|
||||
data: CreateWithdrawData, wallet_id: str
|
||||
|
|
@ -92,7 +93,11 @@ async def get_withdraw_links(wallet_ids: Union[str, List[str]]) -> List[Withdraw
|
|||
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = await db.fetchall(
|
||||
f"SELECT * FROM withdraw.withdraw_link WHERE wallet IN ({q}) ORDER BY open_time DESC", (*wallet_ids,)
|
||||
f"""
|
||||
SELECT * FROM withdraw.withdraw_link
|
||||
WHERE wallet IN ({q}) ORDER BY open_time DESC
|
||||
""",
|
||||
(*wallet_ids,),
|
||||
)
|
||||
return [WithdrawLink(**row) for row in rows]
|
||||
|
||||
|
|
@ -116,6 +121,7 @@ async def increment_withdraw_link(link: WithdrawLink) -> None:
|
|||
open_time=link.wait_time + int(datetime.now().timestamp()),
|
||||
)
|
||||
|
||||
|
||||
async def update_withdraw_link(link_id: str, **kwargs) -> Optional[WithdrawLink]:
|
||||
if "is_unique" in kwargs:
|
||||
kwargs["is_unique"] = int(kwargs["is_unique"])
|
||||
|
|
@ -150,8 +156,8 @@ async def create_hash_check(the_hash: str, lnurl_id: str) -> HashCheck:
|
|||
""",
|
||||
(the_hash, lnurl_id),
|
||||
)
|
||||
hashCheck = await get_hash_check(the_hash, lnurl_id)
|
||||
return hashCheck
|
||||
hash_check = await get_hash_check(the_hash, lnurl_id)
|
||||
return hash_check
|
||||
|
||||
|
||||
async def get_hash_check(the_hash: str, lnurl_id: str) -> HashCheck:
|
||||
|
|
@ -171,5 +177,6 @@ async def get_hash_check(the_hash: str, lnurl_id: str) -> HashCheck:
|
|||
else:
|
||||
return HashCheck(lnurl=True, hash=True)
|
||||
|
||||
|
||||
async def delete_hash_check(the_hash: str) -> None:
|
||||
await db.execute("DELETE FROM withdraw.hash_check WHERE id = ?", (the_hash,))
|
||||
|
|
|
|||
|
|
@ -4,4 +4,4 @@ LNURL is a range of lightning-network standards that allow us to use lightning-n
|
|||
|
||||
The most common use case for an LNURL withdraw is a faucet, although it is a very powerful technology, with much further reaching implications. For example, an LNURL withdraw could be minted to pay for a subscription service. Or you can have a LNURLw as an offline Lightning wallet (a pre paid "card"), you use to pay for something without having to even reach your smartphone.
|
||||
|
||||
LNURL withdraw is a very powerful tool and should not have his use limited to just faucet applications. With LNURL withdraw, you have the ability to give someone the right to spend a range, once or multiple times. This functionality has not existed in money before.
|
||||
LNURL withdraw is a very powerful tool and should not have his use limited to just faucet applications. With LNURL withdraw, you have the ability to give someone the right to spend a range, once or multiple times. This functionality has not existed in money before.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"repos": [
|
||||
{
|
||||
"id": "withdraw",
|
||||
"organisation": "lnbits",
|
||||
"repository": "withdraw"
|
||||
}
|
||||
]
|
||||
"repos": [
|
||||
{
|
||||
"id": "withdraw",
|
||||
"organisation": "lnbits",
|
||||
"repository": "withdraw"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
29
models.py
29
models.py
|
|
@ -1,10 +1,9 @@
|
|||
import shortuuid
|
||||
from fastapi import Query
|
||||
from fastapi import Query, Request
|
||||
from lnurl import Lnurl, LnurlWithdrawResponse
|
||||
from lnurl import encode as lnurl_encode
|
||||
from lnurl.models import ClearnetUrl, MilliSatoshi
|
||||
from lnurl.types import ClearnetUrl, MilliSatoshi
|
||||
from pydantic import BaseModel
|
||||
from starlette.requests import Request
|
||||
|
||||
|
||||
class CreateWithdrawData(BaseModel):
|
||||
|
|
@ -49,24 +48,24 @@ class WithdrawLink(BaseModel):
|
|||
usescssv = self.usescsv.split(",")
|
||||
tohash = self.id + self.unique_hash + usescssv[self.number]
|
||||
multihash = shortuuid.uuid(name=tohash)
|
||||
url = str(req.url_for(
|
||||
"withdraw.api_lnurl_multi_response",
|
||||
unique_hash=self.unique_hash,
|
||||
id_unique_hash=multihash,
|
||||
))
|
||||
url = str(
|
||||
req.url_for(
|
||||
"withdraw.api_lnurl_multi_response",
|
||||
unique_hash=self.unique_hash,
|
||||
id_unique_hash=multihash,
|
||||
)
|
||||
)
|
||||
else:
|
||||
url = str(req.url_for(
|
||||
"withdraw.api_lnurl_response", unique_hash=self.unique_hash
|
||||
))
|
||||
url = str(
|
||||
req.url_for("withdraw.api_lnurl_response", unique_hash=self.unique_hash)
|
||||
)
|
||||
|
||||
return lnurl_encode(url)
|
||||
|
||||
def lnurl_response(self, req: Request) -> LnurlWithdrawResponse:
|
||||
url = str(req.url_for(
|
||||
name="withdraw.api_lnurl_callback", unique_hash=self.unique_hash
|
||||
))
|
||||
url = req.url_for("withdraw.api_lnurl_callback", unique_hash=self.unique_hash)
|
||||
return LnurlWithdrawResponse(
|
||||
callback=ClearnetUrl(url, scheme="https"),
|
||||
callback=ClearnetUrl(url, scheme="https"), # type: ignore
|
||||
k1=self.k1,
|
||||
minWithdrawable=MilliSatoshi(self.min_withdrawable * 1000),
|
||||
maxWithdrawable=MilliSatoshi(self.max_withdrawable * 1000),
|
||||
|
|
|
|||
59
package-lock.json
generated
Normal file
59
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"name": "withdraw",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "withdraw",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"prettier": "^3.2.5",
|
||||
"pyright": "^1.1.358"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
|
||||
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pyright": {
|
||||
"version": "1.1.359",
|
||||
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.359.tgz",
|
||||
"integrity": "sha512-rtdQDlVfZy10MUDuTlY75wKaQt4hbd/kSAKHIJqaStZs4UPQMVrhpZBEDf1NQGAiSGCuKQn0qVpNNuGUEicqlQ==",
|
||||
"bin": {
|
||||
"pyright": "index.js",
|
||||
"pyright-langserver": "langserver.index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
package.json
Normal file
15
package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "withdraw",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"prettier": "^3.2.5",
|
||||
"pyright": "^1.1.358"
|
||||
}
|
||||
}
|
||||
2494
poetry.lock
generated
Normal file
2494
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
91
pyproject.toml
Normal file
91
pyproject.toml
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
[tool.poetry]
|
||||
name = "lnbits-withdraw"
|
||||
version = "0.0.0"
|
||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||
authors = ["Alan Bits <alan@lnbits.com>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10 | ^3.9"
|
||||
lnbits = "*"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^24.3.0"
|
||||
pytest-asyncio = "^0.21.0"
|
||||
pytest = "^7.3.2"
|
||||
mypy = "^1.5.1"
|
||||
pre-commit = "^3.2.2"
|
||||
ruff = "^0.3.2"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = [
|
||||
"lnbits.*",
|
||||
"lnurl.*",
|
||||
"loguru.*",
|
||||
"fastapi.*",
|
||||
"pydantic.*",
|
||||
"pyqrcode.*",
|
||||
"shortuuid.*",
|
||||
"httpx.*",
|
||||
]
|
||||
ignore_missing_imports = "True"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
log_cli = false
|
||||
testpaths = [
|
||||
"tests"
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
|
||||
[tool.ruff]
|
||||
# Same as Black. + 10% rule of black
|
||||
line-length = 88
|
||||
|
||||
[tool.ruff.lint]
|
||||
# Enable:
|
||||
# F - pyflakes
|
||||
# E - pycodestyle errors
|
||||
# W - pycodestyle warnings
|
||||
# I - isort
|
||||
# A - flake8-builtins
|
||||
# C - mccabe
|
||||
# N - naming
|
||||
# UP - pyupgrade
|
||||
# RUF - ruff
|
||||
# B - bugbear
|
||||
select = ["F", "E", "W", "I", "A", "C", "N", "UP", "RUF", "B"]
|
||||
# UP007: pyupgrade: use X | Y instead of Optional. (python3.10)
|
||||
# C901 `api_link_create_or_update` is too complex (15 > 10)
|
||||
ignore = ["UP007", "C901"]
|
||||
|
||||
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
# needed for pydantic
|
||||
[tool.ruff.lint.pep8-naming]
|
||||
classmethod-decorators = [
|
||||
"root_validator",
|
||||
]
|
||||
|
||||
# Ignore unused imports in __init__.py files.
|
||||
# [tool.ruff.lint.extend-per-file-ignores]
|
||||
# "__init__.py" = ["F401", "F403"]
|
||||
|
||||
# [tool.ruff.lint.mccabe]
|
||||
# max-complexity = 10
|
||||
|
||||
[tool.ruff.lint.flake8-bugbear]
|
||||
# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`.
|
||||
extend-immutable-calls = [
|
||||
"fastapi.Depends",
|
||||
"fastapi.Query",
|
||||
]
|
||||
|
|
@ -198,7 +198,7 @@ new Vue({
|
|||
},
|
||||
updateWithdrawLink: function (wallet, data) {
|
||||
var self = this
|
||||
|
||||
|
||||
// Remove webhook info if toggle is set to false
|
||||
if (!data.has_webhook) {
|
||||
data.webhook_url = null
|
||||
|
|
@ -213,7 +213,7 @@ new Vue({
|
|||
wallet.adminkey,
|
||||
data
|
||||
)
|
||||
.then((response) => {
|
||||
.then(response => {
|
||||
self.withdrawLinks = _.reject(self.withdrawLinks, function (obj) {
|
||||
return obj.id === data.id
|
||||
})
|
||||
|
|
@ -230,7 +230,7 @@ new Vue({
|
|||
|
||||
LNbits.api
|
||||
.request('POST', '/withdraw/api/v1/links', wallet.adminkey, data)
|
||||
.then((response) => {
|
||||
.then(response => {
|
||||
self.withdrawLinks.push(mapWithdrawLink(response.data))
|
||||
self.formDialog.show = false
|
||||
self.simpleformDialog.show = false
|
||||
|
|
@ -305,7 +305,7 @@ new Vue({
|
|||
this.withdrawLinks,
|
||||
'withdraw-links'
|
||||
)
|
||||
},
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
|
|
|
|||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
11
tests/test_init.py
Normal file
11
tests/test_init.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import pytest
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .. import withdraw_ext
|
||||
|
||||
|
||||
# just import router and add it to a test router
|
||||
@pytest.mark.asyncio
|
||||
async def test_router():
|
||||
router = APIRouter()
|
||||
router.include_router(withdraw_ext)
|
||||
9
toc.md
9
toc.md
|
|
@ -1,22 +1,29 @@
|
|||
# Terms and Conditions for LNbits Extension
|
||||
|
||||
## 1. Acceptance of Terms
|
||||
|
||||
By installing and using the LNbits extension ("Extension"), you agree to be bound by these terms and conditions ("Terms"). If you do not agree to these Terms, do not use the Extension.
|
||||
|
||||
## 2. License
|
||||
|
||||
The Extension is free and open-source software, released under [specify the FOSS license here, e.g., GPL-3.0, MIT, etc.]. You are permitted to use, copy, modify, and distribute the Extension under the terms of that license.
|
||||
|
||||
## 3. No Warranty
|
||||
|
||||
The Extension is provided "as is" and with all faults, and the developer expressly disclaims all warranties of any kind, whether express, implied, statutory, or otherwise, including but not limited to warranties of merchantability, fitness for a particular purpose, non-infringement, and any warranties arising out of course of dealing or usage of trade. No advice or information, whether oral or written, obtained from the developer or elsewhere will create any warranty not expressly stated in this Terms.
|
||||
|
||||
## 4. Limitation of Liability
|
||||
|
||||
In no event will the developer be liable to you or any third party for any direct, indirect, incidental, special, consequential, or punitive damages, including lost profit, lost revenue, loss of data, or other damages arising out of or in connection with your use of the Extension, even if the developer has been advised of the possibility of such damages. The foregoing limitation of liability shall apply to the fullest extent permitted by law in the applicable jurisdiction.
|
||||
|
||||
## 5. Modification of Terms
|
||||
|
||||
The developer reserves the right to modify these Terms at any time. You are advised to review these Terms periodically for any changes. Changes to these Terms are effective when they are posted on the appropriate location within or associated with the Extension.
|
||||
|
||||
## 6. General Provisions
|
||||
|
||||
If any provision of these Terms is held to be invalid or unenforceable, that provision will be enforced to the maximum extent permissible, and the other provisions of these Terms will remain in full force and effect. These Terms constitute the entire agreement between you and the developer regarding the use of the Extension.
|
||||
|
||||
## 7. Contact Information
|
||||
If you have any questions about these Terms, please contact the developer at [developer's contact information].
|
||||
|
||||
If you have any questions about these Terms, please contact the developer at [developer's contact information].
|
||||
|
|
|
|||
28
views.py
28
views.py
|
|
@ -2,27 +2,29 @@ from http import HTTPStatus
|
|||
from io import BytesIO
|
||||
|
||||
import pyqrcode
|
||||
from fastapi import Depends, HTTPException, Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from starlette.responses import HTMLResponse, StreamingResponse
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse, StreamingResponse
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_user_exists
|
||||
from lnbits.helpers import template_renderer
|
||||
|
||||
from . import withdraw_ext, withdraw_renderer
|
||||
from .crud import chunks, get_withdraw_link
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
withdraw_ext_generic = APIRouter()
|
||||
|
||||
|
||||
@withdraw_ext.get("/", response_class=HTMLResponse)
|
||||
def withdraw_renderer():
|
||||
return template_renderer(["withdraw/templates"])
|
||||
|
||||
|
||||
@withdraw_ext_generic.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
return withdraw_renderer().TemplateResponse(
|
||||
"withdraw/index.html", {"request": request, "user": user.dict()}
|
||||
)
|
||||
|
||||
|
||||
@withdraw_ext.get("/{link_id}", response_class=HTMLResponse)
|
||||
@withdraw_ext_generic.get("/{link_id}", response_class=HTMLResponse)
|
||||
async def display(request: Request, link_id):
|
||||
link = await get_withdraw_link(link_id, 0)
|
||||
|
||||
|
|
@ -41,7 +43,7 @@ async def display(request: Request, link_id):
|
|||
)
|
||||
|
||||
|
||||
@withdraw_ext.get("/img/{link_id}", response_class=StreamingResponse)
|
||||
@withdraw_ext_generic.get("/img/{link_id}", response_class=StreamingResponse)
|
||||
async def img(request: Request, link_id):
|
||||
link = await get_withdraw_link(link_id, 0)
|
||||
if not link:
|
||||
|
|
@ -67,7 +69,7 @@ async def img(request: Request, link_id):
|
|||
)
|
||||
|
||||
|
||||
@withdraw_ext.get("/print/{link_id}", response_class=HTMLResponse)
|
||||
@withdraw_ext_generic.get("/print/{link_id}", response_class=HTMLResponse)
|
||||
async def print_qr(request: Request, link_id):
|
||||
link = await get_withdraw_link(link_id)
|
||||
if not link:
|
||||
|
|
@ -86,7 +88,7 @@ async def print_qr(request: Request, link_id):
|
|||
links = []
|
||||
count = 0
|
||||
|
||||
for x in link.usescsv.split(","):
|
||||
for _ in link.usescsv.split(","):
|
||||
linkk = await get_withdraw_link(link_id, count)
|
||||
if not linkk:
|
||||
raise HTTPException(
|
||||
|
|
@ -114,7 +116,7 @@ async def print_qr(request: Request, link_id):
|
|||
)
|
||||
|
||||
|
||||
@withdraw_ext.get("/csv/{link_id}", response_class=HTMLResponse)
|
||||
@withdraw_ext_generic.get("/csv/{link_id}", response_class=HTMLResponse)
|
||||
async def csv(request: Request, link_id):
|
||||
link = await get_withdraw_link(link_id)
|
||||
if not link:
|
||||
|
|
@ -133,7 +135,7 @@ async def csv(request: Request, link_id):
|
|||
links = []
|
||||
count = 0
|
||||
|
||||
for x in link.usescsv.split(","):
|
||||
for _ in link.usescsv.split(","):
|
||||
linkk = await get_withdraw_link(link_id, count)
|
||||
if not linkk:
|
||||
raise HTTPException(
|
||||
|
|
|
|||
60
views_api.py
60
views_api.py
|
|
@ -1,14 +1,12 @@
|
|||
import json
|
||||
from http import HTTPStatus
|
||||
from typing import Optional
|
||||
import json
|
||||
|
||||
from fastapi import Depends, HTTPException, Query, Request
|
||||
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl
|
||||
|
||||
from . import withdraw_ext
|
||||
from .crud import (
|
||||
create_withdraw_link,
|
||||
delete_withdraw_link,
|
||||
|
|
@ -19,8 +17,10 @@ from .crud import (
|
|||
)
|
||||
from .models import CreateWithdrawData
|
||||
|
||||
withdraw_ext_api = APIRouter(prefix="/api/v1")
|
||||
|
||||
@withdraw_ext.get("/api/v1/links", status_code=HTTPStatus.OK)
|
||||
|
||||
@withdraw_ext_api.get("/links", status_code=HTTPStatus.OK)
|
||||
async def api_links(
|
||||
req: Request,
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
|
|
@ -38,14 +38,17 @@ async def api_links(
|
|||
for link in await get_withdraw_links(wallet_ids)
|
||||
]
|
||||
|
||||
except LnurlInvalidUrl:
|
||||
except LnurlInvalidUrl as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.UPGRADE_REQUIRED,
|
||||
detail="LNURLs need to be delivered over a publically accessible `https` domain or Tor.",
|
||||
)
|
||||
detail="""
|
||||
LNURLs need to be delivered over a publically
|
||||
accessible `https` domain or Tor.
|
||||
""",
|
||||
) from exc
|
||||
|
||||
|
||||
@withdraw_ext.get("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
|
||||
@withdraw_ext_api.get("/links/{link_id}", status_code=HTTPStatus.OK)
|
||||
async def api_link_retrieve(
|
||||
link_id: str, request: Request, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
):
|
||||
|
|
@ -63,8 +66,8 @@ async def api_link_retrieve(
|
|||
return {**link.dict(), **{"lnurl": link.lnurl(request)}}
|
||||
|
||||
|
||||
@withdraw_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED)
|
||||
@withdraw_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
|
||||
@withdraw_ext_api.post("/links", status_code=HTTPStatus.CREATED)
|
||||
@withdraw_ext_api.put("/links/{link_id}", status_code=HTTPStatus.OK)
|
||||
async def api_link_create_or_update(
|
||||
req: Request,
|
||||
data: CreateWithdrawData,
|
||||
|
|
@ -88,20 +91,20 @@ async def api_link_create_or_update(
|
|||
if data.webhook_body:
|
||||
try:
|
||||
json.loads(data.webhook_body)
|
||||
except:
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
detail="`webhook_body` can not parse JSON.",
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
)
|
||||
) from exc
|
||||
|
||||
if data.webhook_headers:
|
||||
try:
|
||||
json.loads(data.webhook_headers)
|
||||
except:
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
detail="`webhook_headers` can not parse JSON.",
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
)
|
||||
) from exc
|
||||
|
||||
if link_id:
|
||||
link = await get_withdraw_link(link_id, 0)
|
||||
|
|
@ -113,32 +116,33 @@ async def api_link_create_or_update(
|
|||
raise HTTPException(
|
||||
detail="Not your withdraw link.", status_code=HTTPStatus.FORBIDDEN
|
||||
)
|
||||
|
||||
data_dict = data.dict()
|
||||
|
||||
data_dict = data.dict()
|
||||
if link.uses > data.uses:
|
||||
if data.uses - link.used <= 0:
|
||||
raise HTTPException(
|
||||
detail="Cannot reduce uses below current used.", status_code=HTTPStatus.BAD_REQUEST
|
||||
detail="Cannot reduce uses below current used.",
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
)
|
||||
numbers = link.usescsv.split(",")
|
||||
usescsv = ",".join(numbers[:data.uses - link.used])
|
||||
usescsv = ",".join(numbers[: data.uses - link.used])
|
||||
data_dict["usescsv"] = usescsv
|
||||
|
||||
if link.uses < data.uses:
|
||||
numbers = link.usescsv.split(",")
|
||||
|
||||
|
||||
if numbers[-1] == "":
|
||||
current_number = int(link.uses)
|
||||
numbers[-1] = str(link.uses)
|
||||
else:
|
||||
current_number = int(numbers[-1])
|
||||
|
||||
|
||||
while len(numbers) < (data.uses - link.used):
|
||||
current_number += 1
|
||||
numbers.append(str(current_number))
|
||||
usescsv = ",".join(numbers)
|
||||
data_dict["usescsv"] = usescsv
|
||||
|
||||
|
||||
link = await update_withdraw_link(link_id, **data_dict)
|
||||
else:
|
||||
link = await create_withdraw_link(wallet_id=wallet.wallet.id, data=data)
|
||||
|
|
@ -146,7 +150,7 @@ async def api_link_create_or_update(
|
|||
return {**link.dict(), **{"lnurl": link.lnurl(req)}}
|
||||
|
||||
|
||||
@withdraw_ext.delete("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
|
||||
@withdraw_ext_api.delete("/links/{link_id}", status_code=HTTPStatus.OK)
|
||||
async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(require_admin_key)):
|
||||
link = await get_withdraw_link(link_id)
|
||||
|
||||
|
|
@ -164,11 +168,11 @@ async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(require_admi
|
|||
return {"success": True}
|
||||
|
||||
|
||||
@withdraw_ext.get(
|
||||
"/api/v1/links/{the_hash}/{lnurl_id}",
|
||||
@withdraw_ext_api.get(
|
||||
"/links/{the_hash}/{lnurl_id}",
|
||||
status_code=HTTPStatus.OK,
|
||||
dependencies=[Depends(get_key_type)],
|
||||
)
|
||||
async def api_hash_retrieve(the_hash, lnurl_id):
|
||||
hashCheck = await get_hash_check(the_hash, lnurl_id)
|
||||
return hashCheck
|
||||
hash_check = await get_hash_check(the_hash, lnurl_id)
|
||||
return hash_check
|
||||
|
|
|
|||
|
|
@ -1,30 +1,55 @@
|
|||
import json
|
||||
from datetime import datetime
|
||||
from http import HTTPStatus
|
||||
from typing import Callable
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
import shortuuid
|
||||
from fastapi import HTTPException, Query, Request
|
||||
from fastapi import APIRouter, HTTPException, Query, Request, Response
|
||||
from fastapi.responses import JSONResponse
|
||||
from loguru import logger
|
||||
|
||||
from fastapi.routing import APIRoute
|
||||
from lnbits.core.crud import update_payment_extra
|
||||
from lnbits.core.services import pay_invoice
|
||||
from loguru import logger
|
||||
|
||||
from . import withdraw_ext
|
||||
from .crud import (
|
||||
create_hash_check,
|
||||
delete_hash_check,
|
||||
get_withdraw_link_by_hash,
|
||||
increment_withdraw_link,
|
||||
remove_unique_withdraw_link,
|
||||
delete_hash_check,
|
||||
create_hash_check
|
||||
)
|
||||
from .models import WithdrawLink
|
||||
|
||||
|
||||
@withdraw_ext.get(
|
||||
"/api/v1/lnurl/{unique_hash}",
|
||||
class LNURLErrorResponseHandler(APIRoute):
|
||||
def get_route_handler(self) -> Callable:
|
||||
original_route_handler = super().get_route_handler()
|
||||
|
||||
async def custom_route_handler(request: Request) -> Response:
|
||||
try:
|
||||
response = await original_route_handler(request)
|
||||
except HTTPException as exc:
|
||||
logger.debug(f"HTTPException: {exc}")
|
||||
response = JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"status": "ERROR", "reason": f"{exc.detail}"},
|
||||
)
|
||||
except Exception as exc:
|
||||
raise exc
|
||||
|
||||
return response
|
||||
|
||||
return custom_route_handler
|
||||
|
||||
|
||||
withdraw_ext_lnurl = APIRouter(prefix="/api/v1/lnurl")
|
||||
withdraw_ext_lnurl.route_class = LNURLErrorResponseHandler
|
||||
|
||||
|
||||
@withdraw_ext_lnurl.get(
|
||||
"/{unique_hash}",
|
||||
response_class=JSONResponse,
|
||||
name="withdraw.api_lnurl_response",
|
||||
)
|
||||
|
|
@ -40,7 +65,9 @@ async def api_lnurl_response(request: Request, unique_hash: str):
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Withdraw is spent."
|
||||
)
|
||||
url = str(request.url_for("withdraw.api_lnurl_callback", unique_hash=link.unique_hash))
|
||||
url = str(
|
||||
request.url_for("withdraw.api_lnurl_callback", unique_hash=link.unique_hash)
|
||||
)
|
||||
|
||||
# Check if url is .onion and change to http
|
||||
if urlparse(url).netloc.endswith(".onion"):
|
||||
|
|
@ -60,8 +87,8 @@ async def api_lnurl_response(request: Request, unique_hash: str):
|
|||
}
|
||||
|
||||
|
||||
@withdraw_ext.get(
|
||||
"/api/v1/lnurl/cb/{unique_hash}",
|
||||
@withdraw_ext_lnurl.get(
|
||||
"/cb/{unique_hash}",
|
||||
name="withdraw.api_lnurl_callback",
|
||||
summary="lnurl withdraw callback",
|
||||
description="""
|
||||
|
|
@ -106,7 +133,6 @@ async def api_lnurl_callback(
|
|||
detail=f"wait link open_time {link.open_time - now} seconds.",
|
||||
)
|
||||
|
||||
|
||||
if id_unique_hash:
|
||||
if check_unique_link(link, id_unique_hash):
|
||||
await remove_unique_withdraw_link(link, id_unique_hash)
|
||||
|
|
@ -133,7 +159,8 @@ async def api_lnurl_callback(
|
|||
)
|
||||
await increment_withdraw_link(link)
|
||||
# If the payment succeeds, delete the record with the unique_hash.
|
||||
# If it has unique_hash, do not delete to prevent the same LNURL from being processed twice.
|
||||
# TODO: we delete this now: "If it has unique_hash, do not delete to prevent
|
||||
# the same LNURL from being processed twice."
|
||||
await delete_hash_check(id_unique_hash or unique_hash)
|
||||
|
||||
if link.webhook_url:
|
||||
|
|
@ -143,7 +170,7 @@ async def api_lnurl_callback(
|
|||
# If payment fails, delete the hash stored so another attempt can be made.
|
||||
await delete_hash_check(id_unique_hash or unique_hash)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail=f"withdraw not working. {str(exc)}"
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail=f"withdraw not working. {exc!s}"
|
||||
) from exc
|
||||
|
||||
|
||||
|
|
@ -167,9 +194,9 @@ async def dispatch_webhook(
|
|||
"lnurlw": link.id,
|
||||
"body": json.loads(link.webhook_body) if link.webhook_body else "",
|
||||
},
|
||||
headers=json.loads(link.webhook_headers)
|
||||
if link.webhook_headers
|
||||
else None,
|
||||
headers=(
|
||||
json.loads(link.webhook_headers) if link.webhook_headers else None
|
||||
),
|
||||
timeout=40,
|
||||
)
|
||||
await update_payment_extra(
|
||||
|
|
@ -182,8 +209,9 @@ async def dispatch_webhook(
|
|||
outgoing=True,
|
||||
)
|
||||
except Exception as exc:
|
||||
# webhook fails shouldn't cause the lnurlw to fail since invoice is already paid
|
||||
logger.error(f"Caught exception when dispatching webhook url: {str(exc)}")
|
||||
# webhook fails shouldn't cause the lnurlw to fail
|
||||
# since invoice is already paid
|
||||
logger.error(f"Caught exception when dispatching webhook url: {exc!s}")
|
||||
await update_payment_extra(
|
||||
payment_hash=payment_hash,
|
||||
extra={"wh_success": False, "wh_message": str(exc)},
|
||||
|
|
@ -192,12 +220,14 @@ async def dispatch_webhook(
|
|||
|
||||
|
||||
# FOR LNURLs WHICH ARE UNIQUE
|
||||
@withdraw_ext.get(
|
||||
"/api/v1/lnurl/{unique_hash}/{id_unique_hash}",
|
||||
@withdraw_ext_lnurl.get(
|
||||
"/{unique_hash}/{id_unique_hash}",
|
||||
response_class=JSONResponse,
|
||||
name="withdraw.api_lnurl_multi_response",
|
||||
)
|
||||
async def api_lnurl_multi_response(request: Request, unique_hash: str, id_unique_hash: str):
|
||||
async def api_lnurl_multi_response(
|
||||
request: Request, unique_hash: str, id_unique_hash: str
|
||||
):
|
||||
link = await get_withdraw_link_by_hash(unique_hash)
|
||||
|
||||
if not link:
|
||||
|
|
@ -215,7 +245,9 @@ async def api_lnurl_multi_response(request: Request, unique_hash: str, id_unique
|
|||
status_code=HTTPStatus.NOT_FOUND, detail="LNURL-withdraw not found."
|
||||
)
|
||||
|
||||
url = str(request.url_for("withdraw.api_lnurl_callback", unique_hash=link.unique_hash))
|
||||
url = str(
|
||||
request.url_for("withdraw.api_lnurl_callback", unique_hash=link.unique_hash)
|
||||
)
|
||||
|
||||
# Check if url is .onion and change to http
|
||||
if urlparse(url).netloc.endswith(".onion"):
|
||||
Loading…
Add table
Add a link
Reference in a new issue