feat: code quality (#39)

This commit is contained in:
dni ⚡ 2024-08-29 12:24:15 +02:00 committed by GitHub
commit 7213d5ca93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 2902 additions and 159 deletions

10
.github/workflows/lint.yml vendored Normal file
View file

@ -0,0 +1,10 @@
name: lint
on:
push:
branches:
- main
pull_request:
jobs:
lint:
uses: lnbits/lnbits/.github/workflows/lint.yml@dev

View file

@ -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
View file

@ -1 +1,4 @@
__pycache__
node_modules
.mypy_cache
.venv

12
.prettierrc Normal file
View 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
View 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"

View file

@ -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.

View file

@ -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
View file

@ -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:

View file

@ -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.

View file

@ -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:

View file

@ -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
View 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
View 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

File diff suppressed because it is too large Load diff

94
pyproject.toml Normal file
View 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",
]

View file

@ -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)

View file

@ -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
View file

11
tests/test_init.py Normal file
View 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
View file

@ -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].

View file

@ -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},
)

View file

@ -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)
):

View file

@ -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)