lnbits-legend/lnbits/extensions/boltcards/lnurl.py

220 lines
6.8 KiB
Python
Raw Normal View History

2022-08-22 22:33:20 +01:00
import base64
import hashlib
import hmac
2022-08-26 19:22:03 +01:00
import json
2022-08-29 08:51:32 -06:00
import secrets
2022-08-22 22:33:20 +01:00
from http import HTTPStatus
from io import BytesIO
from typing import Optional
from urllib.parse import urlparse
2022-08-22 22:33:20 +01:00
from embit import bech32, compact
from fastapi import Request
from fastapi.param_functions import Query
2022-08-22 23:29:42 +01:00
from fastapi.params import Depends, Query
2022-08-29 08:51:32 -06:00
from lnurl import Lnurl, LnurlWithdrawResponse
from lnurl import encode as lnurl_encode # type: ignore
from lnurl.types import LnurlPayMetadata # type: ignore
from loguru import logger
2022-08-22 23:29:42 +01:00
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import HTMLResponse
2022-08-31 18:32:13 +01:00
from lnbits import bolt11
2022-08-22 22:33:20 +01:00
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,
2022-08-29 14:51:18 -06:00
get_card_by_external_id,
2022-08-22 22:33:20 +01:00
get_card_by_otp,
get_hit,
2022-08-22 23:29:42 +01:00
get_hits_today,
2022-08-26 19:22:03 +01:00
spend_hit,
2022-08-22 22:33:20 +01:00
update_card,
update_card_counter,
update_card_otp,
)
from .models import CreateCardData
from .nxp424 import decryptSUN, getSunMAC
2022-08-22 23:29:42 +01:00
###############LNURLWITHDRAW#################
2022-08-22 22:33:20 +01:00
# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
2022-08-29 14:51:18 -06:00
@boltcards_ext.get("/api/v1/scan/{external_id}")
async def api_scan(p, c, request: Request, external_id: str = None):
2022-08-22 22:33:20 +01:00
# some wallets send everything as lower case, no bueno
p = p.upper()
c = c.upper()
card = None
counter = b""
2022-08-29 14:51:18 -06:00
card = await get_card_by_external_id(external_id)
2022-08-28 11:59:21 +01:00
if not card:
return {"status": "ERROR", "reason": "No card."}
if not card.enable:
2022-08-29 14:18:18 +01:00
return {"status": "ERROR", "reason": "Card is disabled."}
2022-08-22 22:33:20 +01:00
try:
card_uid, counter = decryptSUN(bytes.fromhex(p), bytes.fromhex(card.k1))
if card.uid.upper() != card_uid.hex().upper():
return {"status": "ERROR", "reason": "Card UID mis-match."}
2022-08-28 11:59:21 +01:00
if c != getSunMAC(card_uid, counter, bytes.fromhex(card.k2)).hex().upper():
return {"status": "ERROR", "reason": "CMAC does not check."}
2022-08-22 22:33:20 +01:00
except:
return {"status": "ERROR", "reason": "Error decrypting card."}
ctr_int = int.from_bytes(counter, "little")
2022-08-29 14:18:18 +01:00
2022-08-28 11:59:21 +01:00
if ctr_int <= card.counter:
return {"status": "ERROR", "reason": "This link is already used."}
2022-08-22 22:33:20 +01:00
await update_card_counter(ctr_int, card.id)
# gathering some info for hit record
ip = request.client.host
if "x-real-ip" in request.headers:
ip = request.headers["x-real-ip"]
elif "x-forwarded-for" in request.headers:
ip = request.headers["x-forwarded-for"]
agent = request.headers["user-agent"] if "user-agent" in request.headers else ""
todays_hits = await get_hits_today(card.id)
2022-08-22 23:29:42 +01:00
hits_amount = 0
2022-08-22 22:33:20 +01:00
for hit in todays_hits:
hits_amount = hits_amount + hit.amount
if (hits_amount + card.tx_limit) > card.daily_limit:
2022-08-26 19:22:03 +01:00
return {"status": "ERROR", "reason": "Max daily limit spent."}
2022-08-22 22:33:20 +01:00
hit = await create_hit(card.id, ip, agent, card.counter, ctr_int)
2022-08-22 23:29:42 +01:00
lnurlpay = lnurl_encode(request.url_for("boltcards.lnurlp_response", hit_id=hit.id))
2022-08-22 22:33:20 +01:00
return {
"tag": "withdrawRequest",
2022-08-29 14:18:18 +01:00
"callback": request.url_for("boltcards.lnurl_callback", hitid=hit.id),
2022-08-22 22:33:20 +01:00
"k1": hit.id,
"minWithdrawable": 1 * 1000,
"maxWithdrawable": card.tx_limit * 1000,
2022-08-29 15:51:22 +01:00
"defaultDescription": f"Boltcard (refund address lnurl://{lnurlpay})",
2022-08-22 22:33:20 +01:00
}
@boltcards_ext.get(
"/api/v1/lnurl/cb/{hitid}",
status_code=HTTPStatus.OK,
name="boltcards.lnurl_callback",
)
async def lnurl_callback(
request: Request,
pr: str = Query(None),
k1: str = Query(None),
):
2022-08-29 14:18:18 +01:00
hit = await get_hit(k1)
card = await get_card(hit.card_id)
2022-08-22 22:33:20 +01:00
if not hit:
return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
2022-08-31 18:32:13 +01:00
if hit.id != k1:
return {"status": "ERROR", "reason": "Bad K1"}
if hit.spent:
return {"status": "ERROR", "reason": f"Payment already claimed"}
invoice = bolt11.decode(pr)
hit = await spend_hit(id=hit.id, amount=int(invoice.amount_msat / 1000))
2022-08-28 11:59:21 +01:00
try:
2022-08-22 22:33:20 +01:00
await pay_invoice(
wallet_id=card.wallet,
payment_request=pr,
2022-08-28 11:59:21 +01:00
max_sat=card.tx_limit,
2022-08-31 18:32:13 +01:00
extra={"tag": "boltcard", "tag": hit.id},
2022-08-22 22:33:20 +01:00
)
return {"status": "OK"}
2022-08-28 11:59:21 +01:00
except:
2022-08-22 22:33:20 +01:00
return {"status": "ERROR", "reason": f"Payment failed"}
# /boltcards/api/v1/auth?a=00000000000000000000000000000000
@boltcards_ext.get("/api/v1/auth")
async def api_auth(a, request: Request):
if a == "00000000000000000000000000000000":
response = {"k0": "0" * 32, "k1": "1" * 32, "k2": "2" * 32}
return response
card = await get_card_by_otp(a)
if not card:
raise HTTPException(
detail="Card does not exist.", status_code=HTTPStatus.NOT_FOUND
)
new_otp = secrets.token_hex(16)
await update_card_otp(new_otp, card.id)
lnurlw_base = (
f"{urlparse(str(request.url)).netloc}/boltcards/api/v1/scan/{card.external_id}"
)
response = {
2022-08-31 20:27:42 +01:00
"card_name": card.name,
"id": 1,
"k0": card.k0,
"k1": card.k1,
"k2": card.k2,
2022-08-31 20:27:42 +01:00
"k3": card.k1,
"k4": card.k2,
"lnurlw_base": lnurlw_base,
2022-08-31 20:27:42 +01:00
"protocol_name": "new_bolt_card_response",
"protocol_version": 1
}
2022-08-31 20:27:42 +01:00
2022-08-22 22:33:20 +01:00
return response
2022-08-29 14:18:18 +01:00
2022-08-22 22:33:20 +01:00
###############LNURLPAY REFUNDS#################
2022-08-29 14:18:18 +01:00
2022-08-22 23:29:42 +01:00
@boltcards_ext.get(
2022-08-22 22:33:20 +01:00
"/api/v1/lnurlp/{hit_id}",
response_class=HTMLResponse,
name="boltcards.lnurlp_response",
)
2022-08-22 23:29:42 +01:00
async def lnurlp_response(req: Request, hit_id: str = Query(None)):
2022-08-29 14:18:18 +01:00
hit = await get_hit(hit_id)
card = await get_card(hit.card_id)
2022-08-22 22:33:20 +01:00
if not hit:
return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
2022-08-28 10:58:17 +01:00
if not card.enable:
return {"status": "ERROR", "reason": "Card is disabled."}
2022-08-22 22:33:20 +01:00
payResponse = {
"tag": "payRequest",
"callback": req.url_for("boltcards.lnurlp_callback", hit_id=hit_id),
"metadata": LnurlPayMetadata(json.dumps([["text/plain", "Refund"]])),
2022-08-26 19:22:03 +01:00
"minSendable": 1 * 1000,
"maxSendable": card.tx_limit * 1000,
2022-08-22 22:33:20 +01:00
}
return json.dumps(payResponse)
2022-08-22 23:29:42 +01:00
@boltcards_ext.get(
2022-08-22 22:33:20 +01:00
"/api/v1/lnurlp/cb/{hit_id}",
response_class=HTMLResponse,
name="boltcards.lnurlp_callback",
)
2022-08-22 23:29:42 +01:00
async def lnurlp_callback(
2022-08-22 22:33:20 +01:00
req: Request, hit_id: str = Query(None), amount: str = Query(None)
):
2022-08-29 14:18:18 +01:00
hit = await get_hit(hit_id)
card = await get_card(hit.card_id)
2022-08-22 22:33:20 +01:00
if not hit:
return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
payment_hash, payment_request = await create_invoice(
2022-08-26 19:22:03 +01:00
wallet_id=card.wallet,
amount=int(amount) / 1000,
2022-08-22 22:33:20 +01:00
memo=f"Refund {hit_id}",
2022-08-29 14:18:18 +01:00
unhashed_description=LnurlPayMetadata(
json.dumps([["text/plain", "Refund"]])
).encode("utf-8"),
2022-08-22 22:33:20 +01:00
extra={"refund": hit_id},
)
payResponse = {"pr": payment_request, "routes": []}
2022-08-22 22:33:20 +01:00
return json.dumps(payResponse)