feat: code quality (#39)
This commit is contained in:
parent
c10bb032fd
commit
7213d5ca93
23 changed files with 2902 additions and 159 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
|
||||
|
|
|
|||
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"
|
||||
21
README.md
21
README.md
|
|
@ -1,27 +1,24 @@
|
|||
# Bolt Cards - <small>[LNbits](https://github.com/lnbits/lnbits) extension</small>
|
||||
<small>For more about LNBits extensions check [this tutorial](https://youtu.be/_sW7miqaXJc)</small>
|
||||
|
||||
<small>For more about LNBits extensions check [this tutorial](https://youtu.be/_sW7miqaXJc)</small>
|
||||
|
||||
This extension allows you to link your [Bolt Card](https://github.com/boltcard) on a NXP NTAG424 DNA tag with a LNbits hub that generated new links on each tab which allows a better privacy and security than a static LNURLw that you can also write to a NFC tag (fromon NTAG 213) in the withdraw-extension e.g. for one-time usage as a gift-card.
|
||||
|
||||
<a class="text-secondary" href="https://youtu.be/_sW7miqaXJc">Video Tutorial</a>
|
||||
|
||||
|
||||
**Disclaimer:** ***Use this only if you either know what you are doing or are a reckless lightning pioneer.
|
||||
Only you are responsible for all your sats, cards and other devices. Always backup all your card keys!***
|
||||
|
||||
**Disclaimer:** **_Use this only if you either know what you are doing or are a reckless lightning pioneer.
|
||||
Only you are responsible for all your sats, cards and other devices. Always backup all your card keys!_**
|
||||
|
||||
For the easy way you need:
|
||||
|
||||
* an LNbits instance in clearnet
|
||||
* opened on Android in Chrome browser
|
||||
* Boltcard extension installed for your LNbits wallet
|
||||
* [Boltcard NFC Card Creator App](https://github.com/boltcard/bolt-nfc-android-app) from the [Apple-](https://apps.apple.com/us/app/boltcard-nfc-programmer/id6450968873) or [Play-Store](https://play.google.com/store/search?q=bolt+card+nfc+card+creator&c=apps) to write your keys to the tags once they were generated on LNbits
|
||||
- an LNbits instance in clearnet
|
||||
- opened on Android in Chrome browser
|
||||
- Boltcard extension installed for your LNbits wallet
|
||||
- [Boltcard NFC Card Creator App](https://github.com/boltcard/bolt-nfc-android-app) from the [Apple-](https://apps.apple.com/us/app/boltcard-nfc-programmer/id6450968873) or [Play-Store](https://play.google.com/store/search?q=bolt+card+nfc+card+creator&c=apps) to write your keys to the tags once they were generated on LNbits
|
||||
|
||||
If you want to gift a Boltcard, make sure to [include the following data](https://www.figma.com/proto/OH6aGCxH45vNpKsZ2nD96S/Untitled?node-id=6%3A37&scaling=min-zoom&page-id=0%3A1) in your present, so that the user is able to make full use of it.
|
||||
|
||||
***Always backup all keys that you're trying to write on the card. Without them you may not be able to change them in the future!***
|
||||
|
||||
**_Always backup all keys that you're trying to write on the card. Without them you may not be able to change them in the future!_**
|
||||
|
||||
## Setting the card - Boltcard NFC Card Creator (easy way)
|
||||
|
||||
|
|
@ -43,6 +40,7 @@ If you want to gift a Boltcard, make sure to [include the following data](https:
|
|||
- Click WRITE CARD NOW and approach the NFC card to set it up. DO NOT REMOVE THE CARD PREMATURELY!
|
||||
|
||||
## Erasing the card - Boltcard NFC Card Creator
|
||||
|
||||
Updated for v0.1.9
|
||||
|
||||
Since v0.1.2 of Boltcard NFC Card Creator it is possible not only to reset the keys but also to disable the SUN function and do the complete erase so the card can be used again as a static tag (or set as a new Bolt Card, ofc).
|
||||
|
|
@ -57,7 +55,6 @@ Since v0.1.2 of Boltcard NFC Card Creator it is possible not only to reset the k
|
|||
|
||||
If you somehow find yourself in some non-standard state (for instance only k3 and k4 remains filled after previous unsuccessful reset), then you need to edit the key fields manually (for instance leave k0-k2 to zeroes and provide the right k3 and k4).
|
||||
|
||||
|
||||
## Setting the card (advanced)
|
||||
|
||||
A technology called [Secure Unique NFC](https://web.archive.org/web/20220706134959/https://mishka-scan.com/blog/secure-unique-nfc) is utilized in this workflow.
|
||||
|
|
|
|||
33
__init__.py
33
__init__.py
|
|
@ -1,12 +1,13 @@
|
|||
import asyncio
|
||||
from loguru import logger
|
||||
|
||||
from fastapi import APIRouter
|
||||
from lnbits.db import Database
|
||||
from lnbits.helpers import template_renderer
|
||||
from lnbits.tasks import create_permanent_unique_task
|
||||
from loguru import logger
|
||||
|
||||
db = Database("ext_boltcards")
|
||||
from .crud import db
|
||||
from .tasks import wait_for_paid_invoices
|
||||
from .views import boltcards_generic_router
|
||||
from .views_api import boltcards_api_router
|
||||
from .views_lnurl import boltcards_lnurl_router
|
||||
|
||||
boltcards_static_files = [
|
||||
{
|
||||
|
|
@ -16,14 +17,9 @@ boltcards_static_files = [
|
|||
]
|
||||
|
||||
boltcards_ext: APIRouter = APIRouter(prefix="/boltcards", tags=["boltcards"])
|
||||
|
||||
|
||||
def boltcards_renderer():
|
||||
return template_renderer(["boltcards/templates"])
|
||||
|
||||
|
||||
from .lnurl import * # noqa: F401,F403
|
||||
from .tasks import * # noqa: F401,F403
|
||||
boltcards_ext.include_router(boltcards_generic_router)
|
||||
boltcards_ext.include_router(boltcards_api_router)
|
||||
boltcards_ext.include_router(boltcards_lnurl_router)
|
||||
|
||||
scheduled_tasks: list[asyncio.Task] = []
|
||||
|
||||
|
|
@ -37,9 +33,16 @@ def boltcards_stop():
|
|||
|
||||
|
||||
def boltcards_start():
|
||||
from lnbits.tasks import create_permanent_unique_task
|
||||
|
||||
task = create_permanent_unique_task("ext_boltcards", wait_for_paid_invoices)
|
||||
scheduled_tasks.append(task)
|
||||
|
||||
|
||||
from .views import * # noqa: F401,F403
|
||||
from .views_api import * # noqa: F401,F403
|
||||
__all__ = [
|
||||
"db",
|
||||
"boltcards_ext",
|
||||
"boltcards_static_files",
|
||||
"boltcards_start",
|
||||
"boltcards_stop",
|
||||
]
|
||||
|
|
|
|||
24
crud.py
24
crud.py
|
|
@ -2,11 +2,13 @@ import secrets
|
|||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from lnbits.db import Database
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from . import db
|
||||
from .models import Card, CreateCardData, Hit, Refund
|
||||
|
||||
db = Database("ext_boltcards")
|
||||
|
||||
|
||||
async def create_card(data: CreateCardData, wallet_id: str) -> Card:
|
||||
card_id = urlsafe_short_hash().upper()
|
||||
|
|
@ -137,25 +139,25 @@ async def delete_card(card_id: str) -> None:
|
|||
)
|
||||
|
||||
|
||||
async def update_card_counter(counter: int, id: str):
|
||||
async def update_card_counter(counter: int, card_id: str):
|
||||
await db.execute(
|
||||
"UPDATE boltcards.cards SET counter = ? WHERE id = ?",
|
||||
(counter, id),
|
||||
(counter, card_id),
|
||||
)
|
||||
|
||||
|
||||
async def enable_disable_card(enable: bool, id: str) -> Optional[Card]:
|
||||
async def enable_disable_card(enable: bool, card_id: str) -> Optional[Card]:
|
||||
await db.execute(
|
||||
"UPDATE boltcards.cards SET enable = ? WHERE id = ?",
|
||||
(enable, id),
|
||||
(enable, card_id),
|
||||
)
|
||||
return await get_card(id)
|
||||
return await get_card(card_id)
|
||||
|
||||
|
||||
async def update_card_otp(otp: str, id: str):
|
||||
async def update_card_otp(otp: str, card_id: str):
|
||||
await db.execute(
|
||||
"UPDATE boltcards.cards SET otp = ? WHERE id = ?",
|
||||
(otp, id),
|
||||
(otp, card_id),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -194,12 +196,12 @@ async def get_hits_today(card_id: str) -> List[Hit]:
|
|||
return [Hit(**row) for row in updatedrow]
|
||||
|
||||
|
||||
async def spend_hit(id: str, amount: int):
|
||||
async def spend_hit(card_id: str, amount: int):
|
||||
await db.execute(
|
||||
"UPDATE boltcards.hits SET spent = ?, amount = ? WHERE id = ?",
|
||||
(True, amount, id),
|
||||
(True, amount, card_id),
|
||||
)
|
||||
return await get_hit(id)
|
||||
return await get_hit(card_id)
|
||||
|
||||
|
||||
async def create_hit(card_id, ip, useragent, old_ctr, new_ctr) -> Hit:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ This extension enables you to link your Bolt Card to an NXP NTAG424 DNA tag via
|
|||
|
||||
For a simpler setup, you will need:
|
||||
|
||||
* An LNbits instance accessible over clearnet.
|
||||
* Google Chrome browser opened on Android.
|
||||
* Boltcard extension installed on your LNbits wallet.
|
||||
* Boltcard NFC Card Creator App, available in the Apple or Play Store, to write your keys to the tags after they have been generated on LNbits.
|
||||
- An LNbits instance accessible over clearnet.
|
||||
- Google Chrome browser opened on Android.
|
||||
- Boltcard extension installed on your LNbits wallet.
|
||||
- Boltcard NFC Card Creator App, available in the Apple or Play Store, to write your keys to the tags after they have been generated on LNbits.
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ class Card(BaseModel):
|
|||
return cls(**dict(row))
|
||||
|
||||
def lnurl(self, req: Request) -> Lnurl:
|
||||
url = str(req.url_for("boltcard.lnurl_response", device_id=self.id, _external=True))
|
||||
url = str(
|
||||
req.url_for("boltcard.lnurl_response", device_id=self.id, _external=True)
|
||||
)
|
||||
return lnurl_encode(url)
|
||||
|
||||
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
||||
|
|
|
|||
21
nxp424.py
21
nxp424.py
|
|
@ -1,5 +1,4 @@
|
|||
# https://www.nxp.com/docs/en/application-note/AN12196.pdf
|
||||
from typing import Tuple
|
||||
|
||||
from Cryptodome.Cipher import AES
|
||||
from Cryptodome.Hash import CMAC
|
||||
|
|
@ -7,30 +6,30 @@ from Cryptodome.Hash import CMAC
|
|||
SV2 = "3CC300010080"
|
||||
|
||||
|
||||
def myCMAC(key: bytes, msg: bytes = b"") -> bytes:
|
||||
def my_cmac(key: bytes, msg: bytes = b"") -> bytes:
|
||||
cobj = CMAC.new(key, ciphermod=AES)
|
||||
if msg != b"":
|
||||
cobj.update(msg)
|
||||
return cobj.digest()
|
||||
|
||||
|
||||
def decryptSUN(sun: bytes, key: bytes) -> Tuple[bytes, bytes]:
|
||||
IVbytes = b"\x00" * 16
|
||||
def decrypt_sun(sun: bytes, key: bytes) -> tuple[bytes, bytes]:
|
||||
ivbytes = b"\x00" * 16
|
||||
|
||||
cipher = AES.new(key, AES.MODE_CBC, IVbytes)
|
||||
cipher = AES.new(key, AES.MODE_CBC, ivbytes)
|
||||
sun_plain = cipher.decrypt(sun)
|
||||
|
||||
UID = sun_plain[1:8]
|
||||
uid = sun_plain[1:8]
|
||||
counter = sun_plain[8:11]
|
||||
|
||||
return UID, counter
|
||||
return uid, counter
|
||||
|
||||
|
||||
def getSunMAC(UID: bytes, counter: bytes, key: bytes) -> bytes:
|
||||
def get_sun_mac(uid: bytes, counter: bytes, key: bytes) -> bytes:
|
||||
sv2prefix = bytes.fromhex(SV2)
|
||||
sv2bytes = sv2prefix + UID + counter
|
||||
sv2bytes = sv2prefix + uid + counter
|
||||
|
||||
mac1 = myCMAC(key, sv2bytes)
|
||||
mac2 = myCMAC(mac1)
|
||||
mac1 = my_cmac(key, sv2bytes)
|
||||
mac2 = my_cmac(mac1)
|
||||
|
||||
return mac2[1::2]
|
||||
|
|
|
|||
59
package-lock.json
generated
Normal file
59
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"name": "boltcards",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "boltcards",
|
||||
"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.3.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
|
||||
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pyright": {
|
||||
"version": "1.1.374",
|
||||
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.374.tgz",
|
||||
"integrity": "sha512-ISbC1YnYDYrEatoKKjfaA5uFIp0ddC/xw9aSlN/EkmwupXUMVn41Jl+G6wHEjRhC+n4abHZeGpEvxCUus/K9dA==",
|
||||
"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": "boltcards",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
2492
poetry.lock
generated
Normal file
2492
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
94
pyproject.toml
Normal file
94
pyproject.toml
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
[tool.poetry]
|
||||
name = "lnbits-boltcards"
|
||||
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]
|
||||
exclude = "(nostr/*)"
|
||||
[[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
|
||||
exclude = [
|
||||
"nostr",
|
||||
]
|
||||
|
||||
[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"]
|
||||
ignore = ["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",
|
||||
]
|
||||
19
tasks.py
19
tasks.py
|
|
@ -1,7 +1,6 @@
|
|||
import asyncio
|
||||
import json
|
||||
|
||||
from lnbits.core import db as core_db
|
||||
from lnbits.core.crud import update_payment_extra
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
|
@ -31,17 +30,5 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||
|
||||
if hit:
|
||||
await create_refund(hit_id=hit.id, refund_amount=(payment.amount / 1000))
|
||||
await mark_webhook_sent(payment, 1)
|
||||
|
||||
|
||||
async def mark_webhook_sent(payment: Payment, status: int) -> None:
|
||||
|
||||
payment.extra["wh_status"] = status
|
||||
|
||||
await core_db.execute(
|
||||
"""
|
||||
UPDATE apipayments SET extra = ?
|
||||
WHERE hash = ?
|
||||
""",
|
||||
(json.dumps(payment.extra), payment.payment_hash),
|
||||
)
|
||||
payment.extra["wh_status"] = 1
|
||||
await update_payment_extra(payment.payment_hash, payment.extra)
|
||||
|
|
|
|||
|
|
@ -10,9 +10,7 @@
|
|||
<p>
|
||||
Manage your Bolt Cards self custodian way<br />
|
||||
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://github.com/lnbits/boltcards"
|
||||
<a class="text-secondary" href="https://github.com/lnbits/boltcards"
|
||||
>More details</a
|
||||
>
|
||||
<br />
|
||||
|
|
|
|||
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 boltcards_ext
|
||||
|
||||
|
||||
# just import router and add it to a test router
|
||||
@pytest.mark.asyncio
|
||||
async def test_router():
|
||||
router = APIRouter()
|
||||
router.include_router(boltcards_ext)
|
||||
7
toc.md
7
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].
|
||||
24
views.py
24
views.py
|
|
@ -1,27 +1,31 @@
|
|||
from http import HTTPStatus
|
||||
|
||||
from fastapi import Depends, Request
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_user_exists
|
||||
from lnbits.helpers import template_renderer
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_user_exists
|
||||
|
||||
from . import boltcards_ext, boltcards_renderer
|
||||
from .crud import get_card_by_external_id, get_hits, get_refunds
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
boltcards_generic_router = APIRouter()
|
||||
|
||||
|
||||
@boltcards_ext.get("/", response_class=HTMLResponse)
|
||||
def boltcards_renderer():
|
||||
return template_renderer(["boltcards/templates"])
|
||||
|
||||
|
||||
@boltcards_generic_router.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
return boltcards_renderer().TemplateResponse(
|
||||
"boltcards/index.html", {"request": request, "user": user.dict()}
|
||||
)
|
||||
|
||||
|
||||
@boltcards_ext.get("/{card_id}", response_class=HTMLResponse)
|
||||
@boltcards_generic_router.get("/{card_id}", response_class=HTMLResponse)
|
||||
async def display(request: Request, card_id: str):
|
||||
card = await get_card_by_external_id(card_id)
|
||||
if not card:
|
||||
|
|
@ -32,11 +36,11 @@ async def display(request: Request, card_id: str):
|
|||
refunds = [
|
||||
refund.hit_id for refund in await get_refunds([hit["id"] for hit in hits])
|
||||
]
|
||||
card = card.dict()
|
||||
card_dict = card.dict()
|
||||
# Remove wallet id from card dict
|
||||
del card["wallet"]
|
||||
del card_dict["wallet"]
|
||||
|
||||
return boltcards_renderer().TemplateResponse(
|
||||
"boltcards/display.html",
|
||||
{"request": request, "card": card, "hits": hits, "refunds": refunds},
|
||||
{"request": request, "card": card_dict, "hits": hits, "refunds": refunds},
|
||||
)
|
||||
|
|
|
|||
49
views_api.py
49
views_api.py
|
|
@ -1,12 +1,10 @@
|
|||
from http import HTTPStatus
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Depends, HTTPException, Query
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||
from lnbits.core.models import WalletTypeInfo
|
||||
from lnbits.decorators import get_key_type, require_admin_key
|
||||
|
||||
from . import boltcards_ext
|
||||
from .crud import (
|
||||
create_card,
|
||||
delete_card,
|
||||
|
|
@ -20,8 +18,10 @@ from .crud import (
|
|||
)
|
||||
from .models import Card, CreateCardData
|
||||
|
||||
boltcards_api_router = APIRouter()
|
||||
|
||||
@boltcards_ext.get("/api/v1/cards")
|
||||
|
||||
@boltcards_api_router.get("/api/v1/cards")
|
||||
async def api_cards(
|
||||
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = False
|
||||
):
|
||||
|
|
@ -55,18 +55,17 @@ def validate_card(data: CreateCardData):
|
|||
raise HTTPException(
|
||||
detail="Invalid bytes for k2.", status_code=HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
except Exception:
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
detail="Invalid byte data provided.", status_code=HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
) from exc
|
||||
|
||||
|
||||
@boltcards_ext.put(
|
||||
@boltcards_api_router.put(
|
||||
"/api/v1/cards/{card_id}",
|
||||
status_code=HTTPStatus.OK,
|
||||
dependencies=[Depends(validate_card)]
|
||||
dependencies=[Depends(validate_card)],
|
||||
)
|
||||
|
||||
async def api_card_update(
|
||||
data: CreateCardData,
|
||||
card_id: str,
|
||||
|
|
@ -79,11 +78,9 @@ async def api_card_update(
|
|||
detail="Card does not exist.", status_code=HTTPStatus.NOT_FOUND
|
||||
)
|
||||
if card.wallet != wallet.wallet.id:
|
||||
raise HTTPException(
|
||||
detail="Not your card.", status_code=HTTPStatus.FORBIDDEN
|
||||
)
|
||||
checkUid = await get_card_by_uid(data.uid)
|
||||
if checkUid and checkUid.id != card_id:
|
||||
raise HTTPException(detail="Not your card.", status_code=HTTPStatus.FORBIDDEN)
|
||||
check_uid = await get_card_by_uid(data.uid)
|
||||
if check_uid and check_uid.id != card_id:
|
||||
raise HTTPException(
|
||||
detail="UID already registered. Delete registered card and try again.",
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
|
|
@ -93,17 +90,17 @@ async def api_card_update(
|
|||
return card
|
||||
|
||||
|
||||
@boltcards_ext.post(
|
||||
@boltcards_api_router.post(
|
||||
"/api/v1/cards",
|
||||
status_code=HTTPStatus.CREATED,
|
||||
dependencies=[Depends(validate_card)]
|
||||
dependencies=[Depends(validate_card)],
|
||||
)
|
||||
async def api_card_create(
|
||||
data: CreateCardData,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> Card:
|
||||
checkUid = await get_card_by_uid(data.uid)
|
||||
if checkUid:
|
||||
check_uid = await get_card_by_uid(data.uid)
|
||||
if check_uid:
|
||||
raise HTTPException(
|
||||
detail="UID already registered. Delete registered card and try again.",
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
|
|
@ -113,7 +110,9 @@ async def api_card_create(
|
|||
return card
|
||||
|
||||
|
||||
@boltcards_ext.get("/api/v1/cards/enable/{card_id}/{enable}", status_code=HTTPStatus.OK)
|
||||
@boltcards_api_router.get(
|
||||
"/api/v1/cards/enable/{card_id}/{enable}", status_code=HTTPStatus.OK
|
||||
)
|
||||
async def enable_card(
|
||||
card_id,
|
||||
enable,
|
||||
|
|
@ -124,12 +123,12 @@ async def enable_card(
|
|||
raise HTTPException(detail="No card found.", status_code=HTTPStatus.NOT_FOUND)
|
||||
if card.wallet != wallet.wallet.id:
|
||||
raise HTTPException(detail="Not your card.", status_code=HTTPStatus.FORBIDDEN)
|
||||
card = await enable_disable_card(enable=enable, id=card_id)
|
||||
card = await enable_disable_card(enable=enable, card_id=card_id)
|
||||
assert card
|
||||
return card.dict()
|
||||
|
||||
|
||||
@boltcards_ext.delete("/api/v1/cards/{card_id}")
|
||||
@boltcards_api_router.delete("/api/v1/cards/{card_id}")
|
||||
async def api_card_delete(card_id, wallet: WalletTypeInfo = Depends(require_admin_key)):
|
||||
card = await get_card(card_id)
|
||||
|
||||
|
|
@ -145,7 +144,7 @@ async def api_card_delete(card_id, wallet: WalletTypeInfo = Depends(require_admi
|
|||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
@boltcards_ext.get("/api/v1/hits")
|
||||
@boltcards_api_router.get("/api/v1/hits")
|
||||
async def api_hits(
|
||||
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
||||
):
|
||||
|
|
@ -163,7 +162,7 @@ async def api_hits(
|
|||
return [hit.dict() for hit in await get_hits(cards_ids)]
|
||||
|
||||
|
||||
@boltcards_ext.get("/api/v1/refunds")
|
||||
@boltcards_api_router.get("/api/v1/refunds")
|
||||
async def api_refunds(
|
||||
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
||||
):
|
||||
|
|
|
|||
|
|
@ -3,16 +3,14 @@ import secrets
|
|||
from http import HTTPStatus
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from fastapi import HTTPException, Query, Request
|
||||
import bolt11
|
||||
from fastapi import APIRouter, HTTPException, Query, Request
|
||||
from lnbits.core.services import create_invoice, pay_invoice
|
||||
from lnurl import encode as lnurl_encode
|
||||
from lnurl.types import LnurlPayMetadata
|
||||
from loguru import logger
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.core.services import create_invoice
|
||||
from lnbits.core.views.api import pay_invoice
|
||||
|
||||
from . import boltcards_ext
|
||||
from .crud import (
|
||||
create_hit,
|
||||
get_card,
|
||||
|
|
@ -24,13 +22,13 @@ from .crud import (
|
|||
update_card_counter,
|
||||
update_card_otp,
|
||||
)
|
||||
from .nxp424 import decryptSUN, getSunMAC
|
||||
from .nxp424 import decrypt_sun, get_sun_mac
|
||||
|
||||
###############LNURLWITHDRAW#################
|
||||
boltcards_lnurl_router = APIRouter()
|
||||
|
||||
|
||||
# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
|
||||
@boltcards_ext.get("/api/v1/scan/{external_id}")
|
||||
@boltcards_lnurl_router.get("/api/v1/scan/{external_id}")
|
||||
async def api_scan(p, c, request: Request, external_id: str):
|
||||
# some wallets send everything as lower case, no bueno
|
||||
p = p.upper()
|
||||
|
|
@ -43,12 +41,12 @@ async def api_scan(p, c, request: Request, external_id: str):
|
|||
if not card.enable:
|
||||
return {"status": "ERROR", "reason": "Card is disabled."}
|
||||
try:
|
||||
card_uid, counter = decryptSUN(bytes.fromhex(p), bytes.fromhex(card.k1))
|
||||
card_uid, counter = decrypt_sun(bytes.fromhex(p), bytes.fromhex(card.k1))
|
||||
if card.uid.upper() != card_uid.hex().upper():
|
||||
return {"status": "ERROR", "reason": "Card UID mis-match."}
|
||||
if c != getSunMAC(card_uid, counter, bytes.fromhex(card.k2)).hex().upper():
|
||||
if c != get_sun_mac(card_uid, counter, bytes.fromhex(card.k2)).hex().upper():
|
||||
return {"status": "ERROR", "reason": "CMAC does not check."}
|
||||
except:
|
||||
except Exception:
|
||||
return {"status": "ERROR", "reason": "Error decrypting card."}
|
||||
|
||||
ctr_int = int.from_bytes(counter, "little")
|
||||
|
|
@ -80,8 +78,10 @@ async def api_scan(p, c, request: Request, external_id: str):
|
|||
lnurlpay_raw = str(request.url_for("boltcards.lnurlp_response", hit_id=hit.id))
|
||||
# bech32 encoded lnurl
|
||||
lnurlpay_bech32 = lnurl_encode(lnurlpay_raw)
|
||||
# create a lud17 lnurlp to support lud19, add to payLink field of the withdrawRequest
|
||||
lnurlpay_nonbech32_lud17 = lnurlpay_raw.replace("https://", "lnurlp://").replace("http://","lnurlp://")
|
||||
# create a lud17 lnurlp to support lud19, add payLink field of the withdrawRequest
|
||||
lnurlpay_nonbech32_lud17 = lnurlpay_raw.replace("https://", "lnurlp://").replace(
|
||||
"http://", "lnurlp://"
|
||||
)
|
||||
|
||||
return {
|
||||
"tag": "withdrawRequest",
|
||||
|
|
@ -94,7 +94,7 @@ async def api_scan(p, c, request: Request, external_id: str):
|
|||
}
|
||||
|
||||
|
||||
@boltcards_ext.get(
|
||||
@boltcards_lnurl_router.get(
|
||||
"/api/v1/lnurl/cb/{hit_id}",
|
||||
status_code=HTTPStatus.OK,
|
||||
name="boltcards.lnurl_callback",
|
||||
|
|
@ -104,6 +104,8 @@ async def lnurl_callback(
|
|||
k1: str = Query(None),
|
||||
pr: str = Query(None),
|
||||
):
|
||||
# TODO: why no hit_id? its not used why is it passed by url?
|
||||
logger.debug(f"TODO: why no hit_id? {hit_id}")
|
||||
if not k1:
|
||||
return {"status": "ERROR", "reason": "Missing K1 token"}
|
||||
|
||||
|
|
@ -121,12 +123,13 @@ async def lnurl_callback(
|
|||
|
||||
try:
|
||||
invoice = bolt11.decode(pr)
|
||||
except:
|
||||
except bolt11.Bolt11Exception:
|
||||
return {"status": "ERROR", "reason": "Failed to decode payment request"}
|
||||
|
||||
card = await get_card(hit.card_id)
|
||||
assert card
|
||||
hit = await spend_hit(id=hit.id, amount=int(invoice.amount_msat / 1000))
|
||||
assert invoice.amount_msat, "Invoice amount is missing"
|
||||
hit = await spend_hit(card_id=hit.id, amount=int(invoice.amount_msat / 1000))
|
||||
assert hit
|
||||
try:
|
||||
await pay_invoice(
|
||||
|
|
@ -141,7 +144,7 @@ async def lnurl_callback(
|
|||
|
||||
|
||||
# /boltcards/api/v1/auth?a=00000000000000000000000000000000
|
||||
@boltcards_ext.get("/api/v1/auth")
|
||||
@boltcards_lnurl_router.get("/api/v1/auth")
|
||||
async def api_auth(a, request: Request):
|
||||
if a == "00000000000000000000000000000000":
|
||||
response = {"k0": "0" * 32, "k1": "1" * 32, "k2": "2" * 32}
|
||||
|
|
@ -179,7 +182,7 @@ async def api_auth(a, request: Request):
|
|||
###############LNURLPAY REFUNDS#################
|
||||
|
||||
|
||||
@boltcards_ext.get(
|
||||
@boltcards_lnurl_router.get(
|
||||
"/api/v1/lnurlp/{hit_id}",
|
||||
response_class=HTMLResponse,
|
||||
name="boltcards.lnurlp_response",
|
||||
|
|
@ -193,17 +196,17 @@ async def lnurlp_response(req: Request, hit_id: str):
|
|||
return {"status": "ERROR", "reason": "LNURL-pay record not found."}
|
||||
if not card.enable:
|
||||
return {"status": "ERROR", "reason": "Card is disabled."}
|
||||
payResponse = {
|
||||
pay_response = {
|
||||
"tag": "payRequest",
|
||||
"callback": str(req.url_for("boltcards.lnurlp_callback", hit_id=hit_id)),
|
||||
"metadata": LnurlPayMetadata(json.dumps([["text/plain", "Refund"]])),
|
||||
"minSendable": 1 * 1000,
|
||||
"maxSendable": card.tx_limit * 1000,
|
||||
}
|
||||
return json.dumps(payResponse)
|
||||
return json.dumps(pay_response)
|
||||
|
||||
|
||||
@boltcards_ext.get(
|
||||
@boltcards_lnurl_router.get(
|
||||
"/api/v1/lnurlp/cb/{hit_id}",
|
||||
response_class=HTMLResponse,
|
||||
name="boltcards.lnurlp_callback",
|
||||
|
|
@ -226,6 +229,6 @@ async def lnurlp_callback(hit_id: str, amount: str = Query(None)):
|
|||
extra={"refund": hit_id},
|
||||
)
|
||||
|
||||
payResponse = {"pr": payment_request, "routes": []}
|
||||
pay_response = {"pr": payment_request, "routes": []}
|
||||
|
||||
return json.dumps(payResponse)
|
||||
return json.dumps(pay_response)
|
||||
Loading…
Add table
Add a link
Reference in a new issue