LNURLs handled internally
Has bugs
This commit is contained in:
parent
4e68c114fd
commit
56c9234aff
7 changed files with 312 additions and 133 deletions
|
|
@ -20,6 +20,6 @@ boltcards_ext: APIRouter = APIRouter(prefix="/boltcards", tags=["boltcards"])
|
||||||
def boltcards_renderer():
|
def boltcards_renderer():
|
||||||
return template_renderer(["lnbits/extensions/boltcards/templates"])
|
return template_renderer(["lnbits/extensions/boltcards/templates"])
|
||||||
|
|
||||||
|
from .lnurl import * # noqa
|
||||||
from .views import * # noqa
|
from .views import * # noqa
|
||||||
from .views_api import * # noqa
|
from .views_api import * # noqa
|
||||||
|
|
|
||||||
197
lnbits/extensions/boltcards/lnurl.py
Normal file
197
lnbits/extensions/boltcards/lnurl.py
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
from http import HTTPStatus
|
||||||
|
from io import BytesIO
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from embit import bech32, compact
|
||||||
|
from fastapi import Request
|
||||||
|
from fastapi.param_functions import Query
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
|
from lnbits.core.services import create_invoice
|
||||||
|
from lnbits.core.views.api import pay_invoice
|
||||||
|
|
||||||
|
from lnurl import Lnurl, LnurlWithdrawResponse
|
||||||
|
from lnurl import encode as lnurl_encode # type: ignore
|
||||||
|
from lnurl.types import LnurlPayMetadata # type: ignore
|
||||||
|
|
||||||
|
from . import boltcards_ext
|
||||||
|
from .crud import (
|
||||||
|
create_hit,
|
||||||
|
get_card,
|
||||||
|
get_card_by_otp,
|
||||||
|
get_card_by_uid,
|
||||||
|
get_hit,
|
||||||
|
update_card,
|
||||||
|
update_card_counter,
|
||||||
|
update_card_otp,
|
||||||
|
)
|
||||||
|
from .models import CreateCardData
|
||||||
|
from .nxp424 import decryptSUN, getSunMAC
|
||||||
|
|
||||||
|
# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
|
||||||
|
@boltcards_ext.get("/api/v1/scan/{card_uid}")
|
||||||
|
async def api_scan(p, c, request: Request, card_uid: str = None):
|
||||||
|
# some wallets send everything as lower case, no bueno
|
||||||
|
p = p.upper()
|
||||||
|
c = c.upper()
|
||||||
|
card = None
|
||||||
|
counter = b""
|
||||||
|
try:
|
||||||
|
card = await get_card_by_uid(card_uid)
|
||||||
|
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."}
|
||||||
|
except:
|
||||||
|
return {"status": "ERROR", "reason": "Error decrypting card."}
|
||||||
|
|
||||||
|
if card == None:
|
||||||
|
return {"status": "ERROR", "reason": "Unknown card."}
|
||||||
|
|
||||||
|
if c != getSunMAC(card_uid, counter, bytes.fromhex(card.k2)).hex().upper():
|
||||||
|
return {"status": "ERROR", "reason": "CMAC does not check."}
|
||||||
|
|
||||||
|
ctr_int = int.from_bytes(counter, "little")
|
||||||
|
|
||||||
|
if ctr_int <= card.counter:
|
||||||
|
return {"status": "ERROR", "reason": "This link is already used."}
|
||||||
|
|
||||||
|
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)
|
||||||
|
int hits_amount = 0
|
||||||
|
for hit in todays_hits:
|
||||||
|
hits_amount = hits_amount + hit.amount
|
||||||
|
if (hits_amount + card.tx_limit) > card.daily_limit:
|
||||||
|
return {"status": "ERROR", "reason": "Max daily liit spent."}
|
||||||
|
hit = await create_hit(card.id, ip, agent, card.counter, ctr_int)
|
||||||
|
|
||||||
|
# link = await get_withdraw_link(card.withdraw, 0)
|
||||||
|
return link.lnurl_response(request)
|
||||||
|
return {
|
||||||
|
"tag": "withdrawRequest",
|
||||||
|
"callback": request.url_for(
|
||||||
|
"boltcards.lnurl_callback"
|
||||||
|
),
|
||||||
|
"k1": hit.id,
|
||||||
|
"minWithdrawable": 1 * 1000,
|
||||||
|
"maxWithdrawable": card.tx_limit * 1000,
|
||||||
|
"defaultDescription": f"Boltcard (Refunds address {lnurl_encode(req.url_for("boltcards.lnurlp_response", hit_id=hit.id))})",
|
||||||
|
}
|
||||||
|
|
||||||
|
@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),
|
||||||
|
):
|
||||||
|
hit = await get_hit(k1)
|
||||||
|
card = await get_card(hit.id)
|
||||||
|
if not hit:
|
||||||
|
return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
|
||||||
|
|
||||||
|
if pr:
|
||||||
|
if hit.id != k1:
|
||||||
|
return {"status": "ERROR", "reason": "Bad K1"}
|
||||||
|
if hit.spent:
|
||||||
|
return {"status": "ERROR", "reason": f"Payment already claimed"}
|
||||||
|
hit = await spend_hit(hit.id)
|
||||||
|
if not hit:
|
||||||
|
return {"status": "ERROR", "reason": f"Payment failed"}
|
||||||
|
await pay_invoice(
|
||||||
|
wallet_id=card.wallet,
|
||||||
|
payment_request=pr,
|
||||||
|
max_sat=card.tx_limit / 1000,
|
||||||
|
extra={"tag": "boltcard"},
|
||||||
|
)
|
||||||
|
return {"status": "OK"}
|
||||||
|
else:
|
||||||
|
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)
|
||||||
|
print(card.otp)
|
||||||
|
print(new_otp)
|
||||||
|
await update_card_otp(new_otp, card.id)
|
||||||
|
|
||||||
|
response = {"k0": card.k0, "k1": card.k1, "k2": card.k2}
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
###############LNURLPAY REFUNDS#################
|
||||||
|
|
||||||
|
@satsdice_ext.get(
|
||||||
|
"/api/v1/lnurlp/{hit_id}",
|
||||||
|
response_class=HTMLResponse,
|
||||||
|
name="boltcards.lnurlp_response",
|
||||||
|
)
|
||||||
|
async def api_lnurlp_response(req: Request, hit_id: str = Query(None)):
|
||||||
|
hit = await get_hit(hit_id)
|
||||||
|
if not hit:
|
||||||
|
return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
|
||||||
|
payResponse = {
|
||||||
|
"tag": "payRequest",
|
||||||
|
"callback": req.url_for("boltcards.lnurlp_callback", hit_id=hit_id),
|
||||||
|
"metadata": LnurlPayMetadata(json.dumps([["text/plain", "Refund"]])),
|
||||||
|
"minSendable": math.ceil(link.min_bet * 1) * 1000,
|
||||||
|
"maxSendable": round(link.max_bet * 1) * 1000,
|
||||||
|
}
|
||||||
|
return json.dumps(payResponse)
|
||||||
|
|
||||||
|
|
||||||
|
@satsdice_ext.get(
|
||||||
|
"/api/v1/lnurlp/cb/{hit_id}",
|
||||||
|
response_class=HTMLResponse,
|
||||||
|
name="boltcards.lnurlp_callback",
|
||||||
|
)
|
||||||
|
async def api_lnurlp_callback(
|
||||||
|
req: Request, hit_id: str = Query(None), amount: str = Query(None)
|
||||||
|
):
|
||||||
|
hit = await get_hit(hit_id)
|
||||||
|
if not hit:
|
||||||
|
return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
|
||||||
|
|
||||||
|
payment_hash, payment_request = await create_invoice(
|
||||||
|
wallet_id=link.wallet,
|
||||||
|
amount=int(amount / 1000),
|
||||||
|
memo=f"Refund {hit_id}",
|
||||||
|
unhashed_description=LnurlPayMetadata(json.dumps([["text/plain", hit_id]])).encode("utf-8"),
|
||||||
|
extra={"refund": hit_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
payResponse = {"pr": payment_request, "successAction": success_action, "routes": []}
|
||||||
|
|
||||||
|
return json.dumps(payResponse)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -10,7 +10,8 @@ async def m001_initial(db):
|
||||||
card_name TEXT NOT NULL,
|
card_name TEXT NOT NULL,
|
||||||
uid TEXT NOT NULL,
|
uid TEXT NOT NULL,
|
||||||
counter INT NOT NULL DEFAULT 0,
|
counter INT NOT NULL DEFAULT 0,
|
||||||
withdraw TEXT NOT NULL,
|
tx_limit TEXT NOT NULL,
|
||||||
|
daily_limit TEXT NOT NULL,
|
||||||
k0 TEXT NOT NULL DEFAULT '00000000000000000000000000000000',
|
k0 TEXT NOT NULL DEFAULT '00000000000000000000000000000000',
|
||||||
k1 TEXT NOT NULL DEFAULT '00000000000000000000000000000000',
|
k1 TEXT NOT NULL DEFAULT '00000000000000000000000000000000',
|
||||||
k2 TEXT NOT NULL DEFAULT '00000000000000000000000000000000',
|
k2 TEXT NOT NULL DEFAULT '00000000000000000000000000000000',
|
||||||
|
|
@ -31,9 +32,24 @@ async def m001_initial(db):
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
card_id TEXT NOT NULL,
|
card_id TEXT NOT NULL,
|
||||||
ip TEXT NOT NULL,
|
ip TEXT NOT NULL,
|
||||||
|
spent BOOL NOT NULL DEFAULT True,
|
||||||
useragent TEXT,
|
useragent TEXT,
|
||||||
old_ctr INT NOT NULL DEFAULT 0,
|
old_ctr INT NOT NULL DEFAULT 0,
|
||||||
new_ctr INT NOT NULL DEFAULT 0,
|
new_ctr INT NOT NULL DEFAULT 0,
|
||||||
|
amount INT NOT NULL,
|
||||||
|
time TIMESTAMP NOT NULL DEFAULT """
|
||||||
|
+ db.timestamp_now
|
||||||
|
+ """
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE boltcards.refunds (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
hit_id TEXT NOT NULL,
|
||||||
|
refund_amount INT NOT NULL,
|
||||||
time TIMESTAMP NOT NULL DEFAULT """
|
time TIMESTAMP NOT NULL DEFAULT """
|
||||||
+ db.timestamp_now
|
+ db.timestamp_now
|
||||||
+ """
|
+ """
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ class Card(BaseModel):
|
||||||
card_name: str
|
card_name: str
|
||||||
uid: str
|
uid: str
|
||||||
counter: int
|
counter: int
|
||||||
withdraw: str
|
tx_limit: int
|
||||||
|
daily_limit: int
|
||||||
k0: str
|
k0: str
|
||||||
k1: str
|
k1: str
|
||||||
k2: str
|
k2: str
|
||||||
|
|
@ -20,12 +21,24 @@ class Card(BaseModel):
|
||||||
otp: str
|
otp: str
|
||||||
time: int
|
time: int
|
||||||
|
|
||||||
|
def from_row(cls, row: Row) -> "Card":
|
||||||
|
return cls(**dict(row))
|
||||||
|
|
||||||
|
def lnurl(self, req: Request) -> Lnurl:
|
||||||
|
url = req.url_for(
|
||||||
|
"boltcard.lnurl_response", device_id=self.id, _external=True
|
||||||
|
)
|
||||||
|
return lnurl_encode(url)
|
||||||
|
|
||||||
|
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
||||||
|
return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
|
||||||
|
|
||||||
class CreateCardData(BaseModel):
|
class CreateCardData(BaseModel):
|
||||||
card_name: str = Query(...)
|
card_name: str = Query(...)
|
||||||
uid: str = Query(...)
|
uid: str = Query(...)
|
||||||
counter: int = Query(0)
|
counter: int = Query(0)
|
||||||
withdraw: str = Query(...)
|
tx_limit: int = Query(0)
|
||||||
|
daily_limit: int = Query(0)
|
||||||
k0: str = Query(ZERO_KEY)
|
k0: str = Query(ZERO_KEY)
|
||||||
k1: str = Query(ZERO_KEY)
|
k1: str = Query(ZERO_KEY)
|
||||||
k2: str = Query(ZERO_KEY)
|
k2: str = Query(ZERO_KEY)
|
||||||
|
|
@ -33,12 +46,18 @@ class CreateCardData(BaseModel):
|
||||||
prev_k1: str = Query(ZERO_KEY)
|
prev_k1: str = Query(ZERO_KEY)
|
||||||
prev_k2: str = Query(ZERO_KEY)
|
prev_k2: str = Query(ZERO_KEY)
|
||||||
|
|
||||||
|
|
||||||
class Hit(BaseModel):
|
class Hit(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
card_id: str
|
card_id: str
|
||||||
ip: str
|
ip: str
|
||||||
|
spent: bool
|
||||||
useragent: str
|
useragent: str
|
||||||
old_ctr: int
|
old_ctr: int
|
||||||
new_ctr: int
|
new_ctr: int
|
||||||
time: int
|
time: int
|
||||||
|
|
||||||
|
class Refund(BaseModel):
|
||||||
|
id: str
|
||||||
|
hit_id: str
|
||||||
|
refund_amount: int
|
||||||
|
time: int
|
||||||
|
|
@ -17,10 +17,14 @@ new Vue({
|
||||||
toggleAdvanced: false,
|
toggleAdvanced: false,
|
||||||
cards: [],
|
cards: [],
|
||||||
hits: [],
|
hits: [],
|
||||||
withdrawsOptions: [],
|
|
||||||
cardDialog: {
|
cardDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
data: {counter:1},
|
data: {
|
||||||
|
counter:1,
|
||||||
|
k0: '',
|
||||||
|
k1: '',
|
||||||
|
k2: '',
|
||||||
|
card_name:''},
|
||||||
temp: {}
|
temp: {}
|
||||||
},
|
},
|
||||||
cardsTable: {
|
cardsTable: {
|
||||||
|
|
@ -133,25 +137,6 @@ new Vue({
|
||||||
console.log(self.hits)
|
console.log(self.hits)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getWithdraws: function () {
|
|
||||||
var self = this
|
|
||||||
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'GET',
|
|
||||||
'/withdraw/api/v1/links?all_wallets=true',
|
|
||||||
this.g.user.wallets[0].inkey
|
|
||||||
)
|
|
||||||
.then(function (response) {
|
|
||||||
self.withdrawsOptions = response.data.map(function (obj) {
|
|
||||||
return {
|
|
||||||
label: [obj.title, ' - ', obj.id].join(''),
|
|
||||||
value: obj.id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
console.log(self.withdraws)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
openQrCodeDialog(cardId) {
|
openQrCodeDialog(cardId) {
|
||||||
var card = _.findWhere(this.cards, {id: cardId})
|
var card = _.findWhere(this.cards, {id: cardId})
|
||||||
|
|
||||||
|
|
@ -166,6 +151,7 @@ new Vue({
|
||||||
this.qrCodeDialog.show = true
|
this.qrCodeDialog.show = true
|
||||||
},
|
},
|
||||||
generateKeys: function () {
|
generateKeys: function () {
|
||||||
|
this.cardDialog.show = true
|
||||||
const genRanHex = size =>
|
const genRanHex = size =>
|
||||||
[...Array(size)]
|
[...Array(size)]
|
||||||
.map(() => Math.floor(Math.random() * 16).toString(16))
|
.map(() => Math.floor(Math.random() * 16).toString(16))
|
||||||
|
|
@ -194,7 +180,6 @@ new Vue({
|
||||||
this.cardDialog.data = {}
|
this.cardDialog.data = {}
|
||||||
},
|
},
|
||||||
sendFormData: function () {
|
sendFormData: function () {
|
||||||
this.generateKeys()
|
|
||||||
let wallet = _.findWhere(this.g.user.wallets, {
|
let wallet = _.findWhere(this.g.user.wallets, {
|
||||||
id: this.cardDialog.data.wallet
|
id: this.cardDialog.data.wallet
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-btn unelevated color="primary" @click="cardDialog.show = true"
|
<q-btn unelevated color="primary" v-on:click="generateKeys"
|
||||||
>Add Card</q-btn
|
>Add Card</q-btn
|
||||||
>
|
>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
@ -122,6 +122,45 @@
|
||||||
</q-table>
|
</q-table>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col">
|
||||||
|
<h5 class="text-subtitle1 q-my-none">Refunds</h5>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<q-btn flat color="grey" @click="exportRefundsCSV"
|
||||||
|
>Export to CSV</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-table
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
:data="refunds"
|
||||||
|
row-key="id"
|
||||||
|
:columns="refundsTable.columns"
|
||||||
|
:pagination.sync="refundsTable.pagination"
|
||||||
|
>
|
||||||
|
{% raw %}
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
|
{{ col.label }}
|
||||||
|
</q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
|
{{ col.value }}
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
{% endraw %}
|
||||||
|
</q-table>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
||||||
<q-card>
|
<q-card>
|
||||||
|
|
@ -148,15 +187,32 @@
|
||||||
label="Wallet *"
|
label="Wallet *"
|
||||||
>
|
>
|
||||||
</q-select>
|
</q-select>
|
||||||
<q-select
|
|
||||||
filled
|
<div class="row">
|
||||||
dense
|
<div class="col">
|
||||||
emit-value
|
<q-input
|
||||||
v-model="cardDialog.data.withdraw"
|
filled
|
||||||
:options="withdrawsOptions"
|
dense
|
||||||
label="Withdraw link *"
|
emit-value
|
||||||
>
|
v-model.trim="cardDialog.data.trans_limit"
|
||||||
</q-select>
|
type="number"
|
||||||
|
label="Max transaction (sats)"
|
||||||
|
class="q-pr-sm"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model.trim="cardDialog.data.daily_limit"
|
||||||
|
type="number"
|
||||||
|
label="Daily limit (sats)"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
|
|
@ -174,6 +230,7 @@
|
||||||
><q-tooltip>From the NFC 424 ntag card that will be loaded</q-tooltip>
|
><q-tooltip>From the NFC 424 ntag card that will be loaded</q-tooltip>
|
||||||
</q-input>
|
</q-input>
|
||||||
<q-toggle
|
<q-toggle
|
||||||
|
@click="toggleKeys"
|
||||||
v-model="toggleAdvanced"
|
v-model="toggleAdvanced"
|
||||||
label="Show advanced options"
|
label="Show advanced options"
|
||||||
></q-toggle>
|
></q-toggle>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,3 @@
|
||||||
# views_api.py is for you API endpoints that could be hit by another service
|
|
||||||
|
|
||||||
# add your dependencies here
|
|
||||||
|
|
||||||
# import httpx
|
|
||||||
# (use httpx just like requests, except instead of response.ok there's only the
|
|
||||||
# response.is_error that is its inverse)
|
|
||||||
|
|
||||||
import secrets
|
import secrets
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
|
@ -15,7 +7,6 @@ from starlette.requests import Request
|
||||||
|
|
||||||
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.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||||
from lnbits.extensions.withdraw import get_withdraw_link
|
|
||||||
|
|
||||||
from . import boltcards_ext
|
from . import boltcards_ext
|
||||||
from .crud import (
|
from .crud import (
|
||||||
|
|
@ -127,90 +118,4 @@ async def api_hits(
|
||||||
for card in cards:
|
for card in cards:
|
||||||
cards_ids.append(card.id)
|
cards_ids.append(card.id)
|
||||||
|
|
||||||
return [hit.dict() for hit in await get_hits(cards_ids)]
|
return [hit.dict() for hit in await get_hits(cards_ids)]
|
||||||
|
|
||||||
|
|
||||||
# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
|
|
||||||
@boltcards_ext.get("/api/v1/scan")
|
|
||||||
@boltcards_ext.get("/api/v1/scan/{card_uid}")
|
|
||||||
async def api_scan(p, c, request: Request, card_uid: str = None):
|
|
||||||
# some wallets send everything as lower case, no bueno
|
|
||||||
p = p.upper()
|
|
||||||
c = c.upper()
|
|
||||||
card = None
|
|
||||||
counter = b""
|
|
||||||
|
|
||||||
if not card_uid:
|
|
||||||
# since this route is common to all cards I don't know whitch 'meta key' to use
|
|
||||||
# so I try one by one until decrypted uid matches
|
|
||||||
for cand in await get_all_cards():
|
|
||||||
if cand.k1:
|
|
||||||
try:
|
|
||||||
card_uid, counter = decryptSUN(
|
|
||||||
bytes.fromhex(p), bytes.fromhex(cand.k1)
|
|
||||||
)
|
|
||||||
|
|
||||||
if card_uid.hex().upper() == cand.uid.upper():
|
|
||||||
card = cand
|
|
||||||
break
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
card = await get_card_by_uid(card_uid)
|
|
||||||
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."}
|
|
||||||
except:
|
|
||||||
return {"status": "ERROR", "reason": "Error decrypting card."}
|
|
||||||
|
|
||||||
if card == None:
|
|
||||||
return {"status": "ERROR", "reason": "Unknown card."}
|
|
||||||
|
|
||||||
if c != getSunMAC(card_uid, counter, bytes.fromhex(card.k2)).hex().upper():
|
|
||||||
return {"status": "ERROR", "reason": "CMAC does not check."}
|
|
||||||
|
|
||||||
ctr_int = int.from_bytes(counter, "little")
|
|
||||||
if ctr_int <= card.counter:
|
|
||||||
return {"status": "ERROR", "reason": "This link is already used."}
|
|
||||||
|
|
||||||
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 ""
|
|
||||||
|
|
||||||
await create_hit(card.id, ip, agent, card.counter, ctr_int)
|
|
||||||
|
|
||||||
link = await get_withdraw_link(card.withdraw, 0)
|
|
||||||
return link.lnurl_response(request)
|
|
||||||
|
|
||||||
|
|
||||||
# /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)
|
|
||||||
print(card.otp)
|
|
||||||
print(new_otp)
|
|
||||||
await update_card_otp(new_otp, card.id)
|
|
||||||
|
|
||||||
response = {"k0": card.k0, "k1": card.k1, "k2": card.k2}
|
|
||||||
|
|
||||||
return response
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue