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:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
@ -34,12 +33,12 @@ jobs:
|
||||||
- name: Create pull request in extensions repo
|
- name: Create pull request in extensions repo
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.EXT_GITHUB }}
|
GH_TOKEN: ${{ secrets.EXT_GITHUB }}
|
||||||
repo_name: "${{ github.event.repository.name }}"
|
repo_name: '${{ github.event.repository.name }}'
|
||||||
tag: "${{ github.ref_name }}"
|
tag: '${{ github.ref_name }}'
|
||||||
branch: "update-${{ github.event.repository.name }}-${{ github.ref_name }}"
|
branch: 'update-${{ github.event.repository.name }}-${{ github.ref_name }}'
|
||||||
title: "[UPDATE] ${{ github.event.repository.name }} to ${{ 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 }}"
|
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"
|
archive: 'https://github.com/lnbits/${{ github.event.repository.name }}/archive/refs/tags/${{ github.ref_name }}.zip'
|
||||||
run: |
|
run: |
|
||||||
cd lnbits-extensions
|
cd lnbits-extensions
|
||||||
git checkout -b $branch
|
git checkout -b $branch
|
||||||
|
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1 +1,4 @@
|
||||||
__pycache__
|
__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"
|
||||||
57
README.md
57
README.md
|
|
@ -1,48 +1,46 @@
|
||||||
# Bolt Cards - <small>[LNbits](https://github.com/lnbits/lnbits) extension</small>
|
# 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.
|
||||||
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>
|
<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.
|
||||||
**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!_**
|
||||||
Only you are responsible for all your sats, cards and other devices. Always backup all your card keys!***
|
|
||||||
|
|
||||||
|
|
||||||
For the easy way you need:
|
For the easy way you need:
|
||||||
|
|
||||||
* an LNbits instance in clearnet
|
- an LNbits instance in clearnet
|
||||||
* opened on Android in Chrome browser
|
- opened on Android in Chrome browser
|
||||||
* Boltcard extension installed for your LNbits wallet
|
- 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
|
- [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.
|
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)
|
## Setting the card - Boltcard NFC Card Creator (easy way)
|
||||||
|
|
||||||
- Add new card in the extension.
|
- Add new card in the extension.
|
||||||
- Set a max sats per transaction. Any transaction greater than this amount will be rejected. This is usually set higher than the funds in the wallet are to prevent accidential withdraws.
|
- Set a max sats per transaction. Any transaction greater than this amount will be rejected. This is usually set higher than the funds in the wallet are to prevent accidential withdraws.
|
||||||
- Set a max sats per day. After the card spends this amount of sats in a day, additional transactions will be rejected.
|
- Set a max sats per day. After the card spends this amount of sats in a day, additional transactions will be rejected.
|
||||||
- Set a card name. This is just for your reference inside LNbits.
|
- Set a card name. This is just for your reference inside LNbits.
|
||||||
- Set the card UID. This is the unique identifier of your NFC card and is 7 bytes.
|
- Set the card UID. This is the unique identifier of your NFC card and is 7 bytes.
|
||||||
- If on an Android device with a newish version of Chrome, you can click the icon next to the input and tap your card to autofill this field.
|
- If on an Android device with a newish version of Chrome, you can click the icon next to the input and tap your card to autofill this field.
|
||||||
- Otherwise read it with the Bolt-Card app (Read NFC) and paste it to the field.
|
- Otherwise read it with the Bolt-Card app (Read NFC) and paste it to the field.
|
||||||
- Advanced Options
|
- Advanced Options
|
||||||
- Card Keys (k0, k1, k2) will be automatically generated if not explicitly set.
|
- Card Keys (k0, k1, k2) will be automatically generated if not explicitly set.
|
||||||
- Set to 16 bytes of 0s (00000000000000000000000000000000) to leave the keys in default (empty) state (this is unsecure).
|
- Set to 16 bytes of 0s (00000000000000000000000000000000) to leave the keys in default (empty) state (this is unsecure).
|
||||||
- GENERATE KEY button fill the keys randomly.
|
- GENERATE KEY button fill the keys randomly.
|
||||||
- Click CREATE CARD button
|
- Click CREATE CARD button
|
||||||
- Click the QR code button next to a card to view its details. Backup the keys now! They'll be comfortable in your password manager.
|
- Click the QR code button next to a card to view its details. Backup the keys now! They'll be comfortable in your password manager.
|
||||||
- Now you can scan the QR code with the Boltcard app (Create Bolt Card -> SCAN QR CODE).
|
- Now you can scan the QR code with the Boltcard app (Create Bolt Card -> SCAN QR CODE).
|
||||||
- Or the "KEYS / AUTH LINK" button to copy the auth URL to the clipboard. Then paste it into the Android app (Create Bolt Card -> PASTE AUTH URL).
|
- Or the "KEYS / AUTH LINK" button to copy the auth URL to the clipboard. Then paste it into the Android app (Create Bolt Card -> PASTE AUTH URL).
|
||||||
- Click WRITE CARD NOW and approach the NFC card to set it up. DO NOT REMOVE THE CARD PREMATURELY!
|
- 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
|
## Erasing the card - Boltcard NFC Card Creator
|
||||||
|
|
||||||
Updated for v0.1.9
|
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).
|
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).
|
||||||
|
|
@ -50,14 +48,13 @@ Since v0.1.2 of Boltcard NFC Card Creator it is possible not only to reset the k
|
||||||
- In the Boltcard extension click the QR code button next to a card to view its details and select WIPE
|
- In the Boltcard extension click the QR code button next to a card to view its details and select WIPE
|
||||||
- OR click the red cross icon on the right side to reach the same
|
- OR click the red cross icon on the right side to reach the same
|
||||||
- In the Boltcard app (Reset Keys)
|
- In the Boltcard app (Reset Keys)
|
||||||
- Click SCAN QR CODE to scan the QR
|
- Click SCAN QR CODE to scan the QR
|
||||||
- Or click WIPE DATA in LNbits to copy and paste in to the app (PASTE KEY JSON)
|
- Or click WIPE DATA in LNbits to copy and paste in to the app (PASTE KEY JSON)
|
||||||
- Click RESET CARD NOW and approach the NFC card to erase it. DO NOT REMOVE THE CARD PREMATURELY!
|
- Click RESET CARD NOW and approach the NFC card to erase it. DO NOT REMOVE THE CARD PREMATURELY!
|
||||||
- Now if all is successful the card can be safely deleted from LNbits (but keep the keys backuped anyway; batter safe than brick).
|
- Now if all is successful the card can be safely deleted from LNbits (but keep the keys backuped anyway; batter safe than brick).
|
||||||
|
|
||||||
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).
|
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)
|
## 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.
|
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.
|
||||||
|
|
@ -74,8 +71,8 @@ The key #00, K0 (also know as auth key) is used as authentification key. It is n
|
||||||
|
|
||||||
### The writing process
|
### The writing process
|
||||||
|
|
||||||
There's also a more [advanced guide](https://www.whitewolftech.com/articles/payment-card/) to set cards up manually with a card reader connected to your computer.
|
There's also a more [advanced guide](https://www.whitewolftech.com/articles/payment-card/) to set cards up manually with a card reader connected to your computer.
|
||||||
Writing can also be done (without setting the keys) via the [TagWriter app by NXP](https://play.google.com/store/apps/details?id=com.nxp.nfc.tagwriter) on Android.
|
Writing can also be done (without setting the keys) via the [TagWriter app by NXP](https://play.google.com/store/apps/details?id=com.nxp.nfc.tagwriter) on Android.
|
||||||
|
|
||||||
The URI should be `lnurlw://YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan/{YOUR_card_external_id}?p=00000000000000000000000000000000&c=0000000000000000`
|
The URI should be `lnurlw://YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan/{YOUR_card_external_id}?p=00000000000000000000000000000000&c=0000000000000000`
|
||||||
|
|
||||||
|
|
|
||||||
33
__init__.py
33
__init__.py
|
|
@ -1,12 +1,13 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from lnbits.db import Database
|
from loguru import logger
|
||||||
from lnbits.helpers import template_renderer
|
|
||||||
from lnbits.tasks import create_permanent_unique_task
|
|
||||||
|
|
||||||
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 = [
|
boltcards_static_files = [
|
||||||
{
|
{
|
||||||
|
|
@ -16,14 +17,9 @@ boltcards_static_files = [
|
||||||
]
|
]
|
||||||
|
|
||||||
boltcards_ext: APIRouter = APIRouter(prefix="/boltcards", tags=["boltcards"])
|
boltcards_ext: APIRouter = APIRouter(prefix="/boltcards", tags=["boltcards"])
|
||||||
|
boltcards_ext.include_router(boltcards_generic_router)
|
||||||
|
boltcards_ext.include_router(boltcards_api_router)
|
||||||
def boltcards_renderer():
|
boltcards_ext.include_router(boltcards_lnurl_router)
|
||||||
return template_renderer(["boltcards/templates"])
|
|
||||||
|
|
||||||
|
|
||||||
from .lnurl import * # noqa: F401,F403
|
|
||||||
from .tasks import * # noqa: F401,F403
|
|
||||||
|
|
||||||
scheduled_tasks: list[asyncio.Task] = []
|
scheduled_tasks: list[asyncio.Task] = []
|
||||||
|
|
||||||
|
|
@ -37,9 +33,16 @@ def boltcards_stop():
|
||||||
|
|
||||||
|
|
||||||
def boltcards_start():
|
def boltcards_start():
|
||||||
|
from lnbits.tasks import create_permanent_unique_task
|
||||||
|
|
||||||
task = create_permanent_unique_task("ext_boltcards", wait_for_paid_invoices)
|
task = create_permanent_unique_task("ext_boltcards", wait_for_paid_invoices)
|
||||||
scheduled_tasks.append(task)
|
scheduled_tasks.append(task)
|
||||||
|
|
||||||
|
|
||||||
from .views import * # noqa: F401,F403
|
__all__ = [
|
||||||
from .views_api import * # noqa: F401,F403
|
"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 datetime import datetime
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from lnbits.db import Database
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
from . import db
|
|
||||||
from .models import Card, CreateCardData, Hit, Refund
|
from .models import Card, CreateCardData, Hit, Refund
|
||||||
|
|
||||||
|
db = Database("ext_boltcards")
|
||||||
|
|
||||||
|
|
||||||
async def create_card(data: CreateCardData, wallet_id: str) -> Card:
|
async def create_card(data: CreateCardData, wallet_id: str) -> Card:
|
||||||
card_id = urlsafe_short_hash().upper()
|
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(
|
await db.execute(
|
||||||
"UPDATE boltcards.cards SET counter = ? WHERE id = ?",
|
"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(
|
await db.execute(
|
||||||
"UPDATE boltcards.cards SET enable = ? WHERE id = ?",
|
"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(
|
await db.execute(
|
||||||
"UPDATE boltcards.cards SET otp = ? WHERE id = ?",
|
"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]
|
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(
|
await db.execute(
|
||||||
"UPDATE boltcards.hits SET spent = ?, amount = ? WHERE id = ?",
|
"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:
|
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:
|
For a simpler setup, you will need:
|
||||||
|
|
||||||
* An LNbits instance accessible over clearnet.
|
- An LNbits instance accessible over clearnet.
|
||||||
* Google Chrome browser opened on Android.
|
- Google Chrome browser opened on Android.
|
||||||
* Boltcard extension installed on your LNbits wallet.
|
- 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.
|
- 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))
|
return cls(**dict(row))
|
||||||
|
|
||||||
def lnurl(self, req: Request) -> Lnurl:
|
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)
|
return lnurl_encode(url)
|
||||||
|
|
||||||
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
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
|
# https://www.nxp.com/docs/en/application-note/AN12196.pdf
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
from Cryptodome.Cipher import AES
|
from Cryptodome.Cipher import AES
|
||||||
from Cryptodome.Hash import CMAC
|
from Cryptodome.Hash import CMAC
|
||||||
|
|
@ -7,30 +6,30 @@ from Cryptodome.Hash import CMAC
|
||||||
SV2 = "3CC300010080"
|
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)
|
cobj = CMAC.new(key, ciphermod=AES)
|
||||||
if msg != b"":
|
if msg != b"":
|
||||||
cobj.update(msg)
|
cobj.update(msg)
|
||||||
return cobj.digest()
|
return cobj.digest()
|
||||||
|
|
||||||
|
|
||||||
def decryptSUN(sun: bytes, key: bytes) -> Tuple[bytes, bytes]:
|
def decrypt_sun(sun: bytes, key: bytes) -> tuple[bytes, bytes]:
|
||||||
IVbytes = b"\x00" * 16
|
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)
|
sun_plain = cipher.decrypt(sun)
|
||||||
|
|
||||||
UID = sun_plain[1:8]
|
uid = sun_plain[1:8]
|
||||||
counter = sun_plain[8:11]
|
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)
|
sv2prefix = bytes.fromhex(SV2)
|
||||||
sv2bytes = sv2prefix + UID + counter
|
sv2bytes = sv2prefix + uid + counter
|
||||||
|
|
||||||
mac1 = myCMAC(key, sv2bytes)
|
mac1 = my_cmac(key, sv2bytes)
|
||||||
mac2 = myCMAC(mac1)
|
mac2 = my_cmac(mac1)
|
||||||
|
|
||||||
return mac2[1::2]
|
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 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.core.models import Payment
|
||||||
from lnbits.helpers import get_current_extension_name
|
from lnbits.helpers import get_current_extension_name
|
||||||
from lnbits.tasks import register_invoice_listener
|
from lnbits.tasks import register_invoice_listener
|
||||||
|
|
@ -31,17 +30,5 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||||
|
|
||||||
if hit:
|
if hit:
|
||||||
await create_refund(hit_id=hit.id, refund_amount=(payment.amount / 1000))
|
await create_refund(hit_id=hit.id, refund_amount=(payment.amount / 1000))
|
||||||
await mark_webhook_sent(payment, 1)
|
payment.extra["wh_status"] = 1
|
||||||
|
await update_payment_extra(payment.payment_hash, payment.extra)
|
||||||
|
|
||||||
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),
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,7 @@
|
||||||
<p>
|
<p>
|
||||||
Manage your Bolt Cards self custodian way<br />
|
Manage your Bolt Cards self custodian way<br />
|
||||||
|
|
||||||
<a
|
<a class="text-secondary" href="https://github.com/lnbits/boltcards"
|
||||||
class="text-secondary"
|
|
||||||
href="https://github.com/lnbits/boltcards"
|
|
||||||
>More details</a
|
>More details</a
|
||||||
>
|
>
|
||||||
<br />
|
<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)
|
||||||
9
toc.md
9
toc.md
|
|
@ -1,22 +1,29 @@
|
||||||
# Terms and Conditions for LNbits Extension
|
# Terms and Conditions for LNbits Extension
|
||||||
|
|
||||||
## 1. Acceptance of Terms
|
## 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.
|
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
|
## 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.
|
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
|
## 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.
|
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
|
## 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.
|
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
|
## 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.
|
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
|
## 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.
|
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
|
## 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].
|
||||||
|
|
|
||||||
24
views.py
24
views.py
|
|
@ -1,27 +1,31 @@
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from fastapi import Depends, Request
|
from fastapi import APIRouter, Depends, Request
|
||||||
from fastapi.templating import Jinja2Templates
|
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.exceptions import HTTPException
|
||||||
from starlette.responses import HTMLResponse
|
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
|
from .crud import get_card_by_external_id, get_hits, get_refunds
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
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)):
|
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||||
return boltcards_renderer().TemplateResponse(
|
return boltcards_renderer().TemplateResponse(
|
||||||
"boltcards/index.html", {"request": request, "user": user.dict()}
|
"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):
|
async def display(request: Request, card_id: str):
|
||||||
card = await get_card_by_external_id(card_id)
|
card = await get_card_by_external_id(card_id)
|
||||||
if not card:
|
if not card:
|
||||||
|
|
@ -32,11 +36,11 @@ async def display(request: Request, card_id: str):
|
||||||
refunds = [
|
refunds = [
|
||||||
refund.hit_id for refund in await get_refunds([hit["id"] for hit in hits])
|
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
|
# Remove wallet id from card dict
|
||||||
del card["wallet"]
|
del card_dict["wallet"]
|
||||||
|
|
||||||
return boltcards_renderer().TemplateResponse(
|
return boltcards_renderer().TemplateResponse(
|
||||||
"boltcards/display.html",
|
"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 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.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 (
|
from .crud import (
|
||||||
create_card,
|
create_card,
|
||||||
delete_card,
|
delete_card,
|
||||||
|
|
@ -20,8 +18,10 @@ from .crud import (
|
||||||
)
|
)
|
||||||
from .models import Card, CreateCardData
|
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(
|
async def api_cards(
|
||||||
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = False
|
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = False
|
||||||
):
|
):
|
||||||
|
|
@ -55,18 +55,17 @@ def validate_card(data: CreateCardData):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
detail="Invalid bytes for k2.", status_code=HTTPStatus.BAD_REQUEST
|
detail="Invalid bytes for k2.", status_code=HTTPStatus.BAD_REQUEST
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
detail="Invalid byte data provided.", status_code=HTTPStatus.BAD_REQUEST
|
detail="Invalid byte data provided.", status_code=HTTPStatus.BAD_REQUEST
|
||||||
)
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
@boltcards_ext.put(
|
@boltcards_api_router.put(
|
||||||
"/api/v1/cards/{card_id}",
|
"/api/v1/cards/{card_id}",
|
||||||
status_code=HTTPStatus.OK,
|
status_code=HTTPStatus.OK,
|
||||||
dependencies=[Depends(validate_card)]
|
dependencies=[Depends(validate_card)],
|
||||||
)
|
)
|
||||||
|
|
||||||
async def api_card_update(
|
async def api_card_update(
|
||||||
data: CreateCardData,
|
data: CreateCardData,
|
||||||
card_id: str,
|
card_id: str,
|
||||||
|
|
@ -79,11 +78,9 @@ async def api_card_update(
|
||||||
detail="Card does not exist.", status_code=HTTPStatus.NOT_FOUND
|
detail="Card does not exist.", status_code=HTTPStatus.NOT_FOUND
|
||||||
)
|
)
|
||||||
if card.wallet != wallet.wallet.id:
|
if card.wallet != wallet.wallet.id:
|
||||||
raise HTTPException(
|
raise HTTPException(detail="Not your card.", status_code=HTTPStatus.FORBIDDEN)
|
||||||
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:
|
||||||
checkUid = await get_card_by_uid(data.uid)
|
|
||||||
if checkUid and checkUid.id != card_id:
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
detail="UID already registered. Delete registered card and try again.",
|
detail="UID already registered. Delete registered card and try again.",
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
|
@ -93,17 +90,17 @@ async def api_card_update(
|
||||||
return card
|
return card
|
||||||
|
|
||||||
|
|
||||||
@boltcards_ext.post(
|
@boltcards_api_router.post(
|
||||||
"/api/v1/cards",
|
"/api/v1/cards",
|
||||||
status_code=HTTPStatus.CREATED,
|
status_code=HTTPStatus.CREATED,
|
||||||
dependencies=[Depends(validate_card)]
|
dependencies=[Depends(validate_card)],
|
||||||
)
|
)
|
||||||
async def api_card_create(
|
async def api_card_create(
|
||||||
data: CreateCardData,
|
data: CreateCardData,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
) -> Card:
|
) -> Card:
|
||||||
checkUid = await get_card_by_uid(data.uid)
|
check_uid = await get_card_by_uid(data.uid)
|
||||||
if checkUid:
|
if check_uid:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
detail="UID already registered. Delete registered card and try again.",
|
detail="UID already registered. Delete registered card and try again.",
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
|
@ -113,7 +110,9 @@ async def api_card_create(
|
||||||
return card
|
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(
|
async def enable_card(
|
||||||
card_id,
|
card_id,
|
||||||
enable,
|
enable,
|
||||||
|
|
@ -124,12 +123,12 @@ async def enable_card(
|
||||||
raise HTTPException(detail="No card found.", status_code=HTTPStatus.NOT_FOUND)
|
raise HTTPException(detail="No card found.", status_code=HTTPStatus.NOT_FOUND)
|
||||||
if card.wallet != wallet.wallet.id:
|
if card.wallet != wallet.wallet.id:
|
||||||
raise HTTPException(detail="Not your card.", status_code=HTTPStatus.FORBIDDEN)
|
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
|
assert card
|
||||||
return card.dict()
|
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)):
|
async def api_card_delete(card_id, wallet: WalletTypeInfo = Depends(require_admin_key)):
|
||||||
card = await get_card(card_id)
|
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
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
@boltcards_ext.get("/api/v1/hits")
|
@boltcards_api_router.get("/api/v1/hits")
|
||||||
async def api_hits(
|
async def api_hits(
|
||||||
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
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)]
|
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(
|
async def api_refunds(
|
||||||
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
||||||
):
|
):
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,14 @@ import secrets
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from urllib.parse import urlparse
|
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 import encode as lnurl_encode
|
||||||
from lnurl.types import LnurlPayMetadata
|
from lnurl.types import LnurlPayMetadata
|
||||||
|
from loguru import logger
|
||||||
from starlette.responses import HTMLResponse
|
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 (
|
from .crud import (
|
||||||
create_hit,
|
create_hit,
|
||||||
get_card,
|
get_card,
|
||||||
|
|
@ -24,13 +22,13 @@ from .crud import (
|
||||||
update_card_counter,
|
update_card_counter,
|
||||||
update_card_otp,
|
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/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):
|
async def api_scan(p, c, request: Request, external_id: str):
|
||||||
# some wallets send everything as lower case, no bueno
|
# some wallets send everything as lower case, no bueno
|
||||||
p = p.upper()
|
p = p.upper()
|
||||||
|
|
@ -43,12 +41,12 @@ async def api_scan(p, c, request: Request, external_id: str):
|
||||||
if not card.enable:
|
if not card.enable:
|
||||||
return {"status": "ERROR", "reason": "Card is disabled."}
|
return {"status": "ERROR", "reason": "Card is disabled."}
|
||||||
try:
|
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():
|
if card.uid.upper() != card_uid.hex().upper():
|
||||||
return {"status": "ERROR", "reason": "Card UID mis-match."}
|
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."}
|
return {"status": "ERROR", "reason": "CMAC does not check."}
|
||||||
except:
|
except Exception:
|
||||||
return {"status": "ERROR", "reason": "Error decrypting card."}
|
return {"status": "ERROR", "reason": "Error decrypting card."}
|
||||||
|
|
||||||
ctr_int = int.from_bytes(counter, "little")
|
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))
|
lnurlpay_raw = str(request.url_for("boltcards.lnurlp_response", hit_id=hit.id))
|
||||||
# bech32 encoded lnurl
|
# bech32 encoded lnurl
|
||||||
lnurlpay_bech32 = lnurl_encode(lnurlpay_raw)
|
lnurlpay_bech32 = lnurl_encode(lnurlpay_raw)
|
||||||
# create a lud17 lnurlp to support lud19, add to payLink field of the withdrawRequest
|
# create a lud17 lnurlp to support lud19, add payLink field of the withdrawRequest
|
||||||
lnurlpay_nonbech32_lud17 = lnurlpay_raw.replace("https://", "lnurlp://").replace("http://","lnurlp://")
|
lnurlpay_nonbech32_lud17 = lnurlpay_raw.replace("https://", "lnurlp://").replace(
|
||||||
|
"http://", "lnurlp://"
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"tag": "withdrawRequest",
|
"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}",
|
"/api/v1/lnurl/cb/{hit_id}",
|
||||||
status_code=HTTPStatus.OK,
|
status_code=HTTPStatus.OK,
|
||||||
name="boltcards.lnurl_callback",
|
name="boltcards.lnurl_callback",
|
||||||
|
|
@ -104,6 +104,8 @@ async def lnurl_callback(
|
||||||
k1: str = Query(None),
|
k1: str = Query(None),
|
||||||
pr: 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:
|
if not k1:
|
||||||
return {"status": "ERROR", "reason": "Missing K1 token"}
|
return {"status": "ERROR", "reason": "Missing K1 token"}
|
||||||
|
|
||||||
|
|
@ -121,12 +123,13 @@ async def lnurl_callback(
|
||||||
|
|
||||||
try:
|
try:
|
||||||
invoice = bolt11.decode(pr)
|
invoice = bolt11.decode(pr)
|
||||||
except:
|
except bolt11.Bolt11Exception:
|
||||||
return {"status": "ERROR", "reason": "Failed to decode payment request"}
|
return {"status": "ERROR", "reason": "Failed to decode payment request"}
|
||||||
|
|
||||||
card = await get_card(hit.card_id)
|
card = await get_card(hit.card_id)
|
||||||
assert card
|
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
|
assert hit
|
||||||
try:
|
try:
|
||||||
await pay_invoice(
|
await pay_invoice(
|
||||||
|
|
@ -141,7 +144,7 @@ async def lnurl_callback(
|
||||||
|
|
||||||
|
|
||||||
# /boltcards/api/v1/auth?a=00000000000000000000000000000000
|
# /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):
|
async def api_auth(a, request: Request):
|
||||||
if a == "00000000000000000000000000000000":
|
if a == "00000000000000000000000000000000":
|
||||||
response = {"k0": "0" * 32, "k1": "1" * 32, "k2": "2" * 32}
|
response = {"k0": "0" * 32, "k1": "1" * 32, "k2": "2" * 32}
|
||||||
|
|
@ -179,7 +182,7 @@ async def api_auth(a, request: Request):
|
||||||
###############LNURLPAY REFUNDS#################
|
###############LNURLPAY REFUNDS#################
|
||||||
|
|
||||||
|
|
||||||
@boltcards_ext.get(
|
@boltcards_lnurl_router.get(
|
||||||
"/api/v1/lnurlp/{hit_id}",
|
"/api/v1/lnurlp/{hit_id}",
|
||||||
response_class=HTMLResponse,
|
response_class=HTMLResponse,
|
||||||
name="boltcards.lnurlp_response",
|
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."}
|
return {"status": "ERROR", "reason": "LNURL-pay record not found."}
|
||||||
if not card.enable:
|
if not card.enable:
|
||||||
return {"status": "ERROR", "reason": "Card is disabled."}
|
return {"status": "ERROR", "reason": "Card is disabled."}
|
||||||
payResponse = {
|
pay_response = {
|
||||||
"tag": "payRequest",
|
"tag": "payRequest",
|
||||||
"callback": str(req.url_for("boltcards.lnurlp_callback", hit_id=hit_id)),
|
"callback": str(req.url_for("boltcards.lnurlp_callback", hit_id=hit_id)),
|
||||||
"metadata": LnurlPayMetadata(json.dumps([["text/plain", "Refund"]])),
|
"metadata": LnurlPayMetadata(json.dumps([["text/plain", "Refund"]])),
|
||||||
"minSendable": 1 * 1000,
|
"minSendable": 1 * 1000,
|
||||||
"maxSendable": card.tx_limit * 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}",
|
"/api/v1/lnurlp/cb/{hit_id}",
|
||||||
response_class=HTMLResponse,
|
response_class=HTMLResponse,
|
||||||
name="boltcards.lnurlp_callback",
|
name="boltcards.lnurlp_callback",
|
||||||
|
|
@ -226,6 +229,6 @@ async def lnurlp_callback(hit_id: str, amount: str = Query(None)):
|
||||||
extra={"refund": hit_id},
|
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