Manage your Bolt Cards self custodian way
- More details
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_init.py b/tests/test_init.py
new file mode 100644
index 0000000..d2dbf0d
--- /dev/null
+++ b/tests/test_init.py
@@ -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)
diff --git a/toc.md b/toc.md
index 544be53..baa0342 100644
--- a/toc.md
+++ b/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].
\ No newline at end of file
+
+If you have any questions about these Terms, please contact the developer at [developer's contact information].
diff --git a/views.py b/views.py
index d328bef..96c87d3 100644
--- a/views.py
+++ b/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 starlette.exceptions import HTTPException
-from starlette.responses import HTMLResponse
-
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 . 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()
+
+
+def boltcards_renderer():
+ return template_renderer(["boltcards/templates"])
-@boltcards_ext.get("/", response_class=HTMLResponse)
+@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},
)
diff --git a/views_api.py b/views_api.py
index b710518..fddc343 100644
--- a/views_api.py
+++ b/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 .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)
):
diff --git a/lnurl.py b/views_lnurl.py
similarity index 83%
rename from lnurl.py
rename to views_lnurl.py
index 6ebea46..9994127 100644
--- a/lnurl.py
+++ b/views_lnurl.py
@@ -3,16 +3,14 @@
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 @@
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)