Swaps lnurlpos for lnurldevice

This commit is contained in:
benarc
2022-01-21 21:23:32 +00:00
parent 9e13b8eb70
commit 57eb27fcee
21 changed files with 784 additions and 599 deletions
+3
View File
@@ -0,0 +1,3 @@
# LNURLDevice
For offline LNURL devices
+17
View File
@@ -0,0 +1,17 @@
from fastapi import APIRouter
from lnbits.db import Database
from lnbits.helpers import template_renderer
db = Database("ext_lnurldevice")
lnurldevice_ext: APIRouter = APIRouter(prefix="/lnurldevice", tags=["lnurldevice"])
def lnurldevice_renderer():
return template_renderer(["lnbits/extensions/lnurldevice/templates"])
from .lnurl import * # noqa
from .views import * # noqa
from .views_api import * # noqa
@@ -0,0 +1,6 @@
{
"name": "LNURLDevice",
"short_description": "For offline LNURL devices",
"icon": "point_of_sale",
"contributors": ["arcbtc"]
}
+120
View File
@@ -0,0 +1,120 @@
from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import createLnurldevice, lnurldevicepayment, lnurldevices
###############lnurldeviceS##########################
async def create_lnurldevice(data: createLnurldevice,) -> lnurldevices:
lnurldevice_id = urlsafe_short_hash()
lnurldevice_key = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO lnurldevice.lnurldevices (
id,
key,
title,
wallet,
currency,
device,
profit
)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(lnurldevice_id, lnurldevice_key, data.title, data.wallet, data.currency, data.device, data.profit,),
)
return await get_lnurldevice(lnurldevice_id)
async def update_lnurldevice(lnurldevice_id: str, **kwargs) -> Optional[lnurldevices]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE lnurldevice.lnurldevices SET {q} WHERE id = ?",
(*kwargs.values(), lnurldevice_id),
)
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)
)
return lnurldevices(**row) if row else None
async def get_lnurldevice(lnurldevice_id: str) -> lnurldevices:
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)
)
return lnurldevices(**row) if row else None
async def get_lnurldevices(wallet_ids: Union[str, List[str]]) -> List[lnurldevices]:
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids[0]))
rows = await db.fetchall(
f"""
SELECT * FROM lnurldevice.lnurldevices WHERE wallet IN ({q})
ORDER BY id
""",
(*wallet_ids,),
)
return [lnurldevices(**row) if row else None for row in rows]
async def delete_lnurldevice(lnurldevice_id: str) -> None:
await db.execute("DELETE FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,))
########################lnuldevice payments###########################
async def create_lnurldevicepayment(
deviceid: str,
payload: Optional[str] = None,
pin: Optional[str] = None,
payhash: Optional[str] = None,
sats: Optional[int] = 0,
) -> lnurldevicepayment:
lnurldevicepayment_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO lnurldevice.lnurldevicepayment (
id,
deviceid,
payload,
pin,
payhash,
sats
)
VALUES (?, ?, ?, ?, ?, ?)
""",
(lnurldevicepayment_id, deviceid, payload, pin, payhash, sats),
)
return await get_lnurldevicepayment(lnurldevicepayment_id)
async def update_lnurldevicepayment(
lnurldevicepayment_id: str, **kwargs
) -> Optional[lnurldevicepayment]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE lnurldevice.lnurldevicepayment SET {q} WHERE id = ?",
(*kwargs.values(), lnurldevicepayment_id),
)
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE id = ?", (lnurldevicepayment_id,)
)
return lnurldevicepayment(**row) if row else None
async def get_lnurldevicepayment(lnurldevicepayment_id: str) -> lnurldevicepayment:
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE id = ?", (lnurldevicepayment_id,)
)
return lnurldevicepayment(**row) if row else None
async def get_lnurlpayload(lnurldevicepayment_payload: str) -> lnurldevicepayment:
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE payload = ?", (lnurldevicepayment_payload,)
)
return lnurldevicepayment(**row) if row else None
+233
View File
@@ -0,0 +1,233 @@
import base64
import hashlib
from http import HTTPStatus
from typing import Optional
from embit import bech32
from embit import compact
import base64
from io import BytesIO
import hmac
from fastapi import Request
from fastapi.param_functions import Query
from starlette.exceptions import HTTPException
from lnbits.core.services import create_invoice
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
from lnbits.core.views.api import pay_invoice
from . import lnurldevice_ext
from .crud import (
create_lnurldevicepayment,
get_lnurldevice,
get_lnurldevicepayment,
update_lnurldevicepayment,
get_lnurlpayload,
)
def bech32_decode(bech):
"""tweaked version of bech32_decode that ignores length limitations"""
if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or (
bech.lower() != bech and bech.upper() != bech
):
return
bech = bech.lower()
device = bech.rfind("1")
if device < 1 or device + 7 > len(bech):
return
if not all(x in bech32.CHARSET for x in bech[device + 1 :]):
return
hrp = bech[:device]
data = [bech32.CHARSET.find(x) for x in bech[device + 1 :]]
encoding = bech32.bech32_verify_checksum(hrp, data)
if encoding is None:
return
return bytes(bech32.convertbits(data[:-6], 5, 8, False))
def xor_decrypt(key, blob):
s = BytesIO(blob)
variant = s.read(1)[0]
if variant != 1:
raise RuntimeError("Not implemented")
# reading nonce
l = s.read(1)[0]
nonce = s.read(l)
if len(nonce) != l:
raise RuntimeError("Missing nonce bytes")
if l < 8:
raise RuntimeError("Nonce is too short")
# reading payload
l = s.read(1)[0]
payload = s.read(l)
if len(payload) > 32:
raise RuntimeError("Payload is too long for this encryption method")
if len(payload) != l:
raise RuntimeError("Missing payload bytes")
hmacval = s.read()
expected = hmac.new(
key, b"Data:" + blob[: -len(hmacval)], digestmod="sha256"
).digest()
if len(hmacval) < 8:
raise RuntimeError("HMAC is too short")
if hmacval != expected[: len(hmacval)]:
raise RuntimeError("HMAC is invalid")
secret = hmac.new(key, b"Round secret:" + nonce, digestmod="sha256").digest()
payload = bytearray(payload)
for i in range(len(payload)):
payload[i] = payload[i] ^ secret[i]
s = BytesIO(payload)
pin = compact.read_from(s)
amount_in_cent = compact.read_from(s)
return pin, amount_in_cent
@lnurldevice_ext.get(
"/api/v1/lnurl/{device_id}",
status_code=HTTPStatus.OK,
name="lnurldevice.lnurl_v1_params",
)
async def lnurl_v1_params(
request: Request,
device_id: str = Query(None),
p: str = Query(None),
atm: str = Query(None),
):
device = await get_lnurldevice(device_id)
if not device:
return {
"status": "ERROR",
"reason": f"lnurldevice {device_id} not found on this server",
}
paymentcheck = await get_lnurlpayload(p)
if device.device == "atm":
if paymentcheck:
return {
"status": "ERROR",
"reason": f"Payment already claimed",
}
if len(p) % 4 > 0:
p += "=" * (4 - (len(p) % 4))
data = base64.urlsafe_b64decode(p)
pin = 0
amount_in_cent = 0
try:
result = xor_decrypt(device.key.encode(), data)
pin = result[0]
amount_in_cent = result[1]
except Exception as exc:
return {"status": "ERROR", "reason": str(exc)}
price_msat = (
await fiat_amount_as_satoshis(float(amount_in_cent) / 100, device.currency)
if device.currency != "sat"
else amount_in_cent
) * 1000
if atm:
if device.device != "atm":
return {"status": "ERROR", "reason": "Not ATM device."}
price_msat = int(price_msat * (1 - (device.profit / 100)) / 1000)
lnurldevicepayment = await create_lnurldevicepayment(
deviceid=device.id,
payload=p,
sats=price_msat * 1000,
pin=pin,
payhash="payment_hash",
)
if not lnurldevicepayment:
return {"status": "ERROR", "reason": "Could not create payment."}
return {
"tag": "withdrawRequest",
"callback": request.url_for(
"lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id
),
"k1": lnurldevicepayment.id,
"minWithdrawable": price_msat * 1000,
"maxWithdrawable": price_msat * 1000,
"defaultDescription": device.title,
}
price_msat = int(price_msat * ((device.profit / 100) + 1) / 1000)
print(price_msat)
lnurldevicepayment = await create_lnurldevicepayment(
deviceid=device.id,
payload=p,
sats=price_msat * 1000,
pin=pin,
payhash="payment_hash",
)
if not lnurldevicepayment:
return {"status": "ERROR", "reason": "Could not create payment."}
return {
"tag": "payRequest",
"callback": request.url_for(
"lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id
),
"minSendable": price_msat * 1000,
"maxSendable": price_msat * 1000,
"metadata": await device.lnurlpay_metadata(),
}
@lnurldevice_ext.get(
"/api/v1/lnurl/cb/{paymentid}",
status_code=HTTPStatus.OK,
name="lnurldevice.lnurl_callback",
)
async def lnurl_callback(request: Request, paymentid: str = Query(None), pr: str = Query(None), k1: str = Query(None)):
lnurldevicepayment = await get_lnurldevicepayment(paymentid)
device = await get_lnurldevice(lnurldevicepayment.deviceid)
if not device:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="lnurldevice not found."
)
if pr:
if lnurldevicepayment.id != k1:
return {"status": "ERROR", "reason": "Bad K1"}
if lnurldevicepayment.payhash != "payment_hash":
return {
"status": "ERROR",
"reason": f"Payment already claimed",
}
lnurldevicepayment = await update_lnurldevicepayment(
lnurldevicepayment_id=paymentid, payhash=lnurldevicepayment.payload
)
await pay_invoice(
wallet_id=device.wallet,
payment_request=pr,
max_sat=lnurldevicepayment.sats / 1000,
extra={"tag": "withdraw"},
)
return {"status": "OK"}
print(lnurldevicepayment.sats)
payment_hash, payment_request = await create_invoice(
wallet_id=device.wallet,
amount=lnurldevicepayment.sats / 1000,
memo=device.title,
description_hash=hashlib.sha256(
(await device.lnurlpay_metadata()).encode("utf-8")
).digest(),
extra={"tag": "PoS"},
)
lnurldevicepayment = await update_lnurldevicepayment(
lnurldevicepayment_id=paymentid, payhash=payment_hash
)
return {
"pr": payment_request,
"successAction": {
"tag": "url",
"description": "Check the attached link",
"url": request.url_for("lnurldevice.displaypin", paymentid=paymentid),
},
"routes": [],
}
return resp.dict()
@@ -0,0 +1,76 @@
from lnbits.db import Database
db2 = Database("ext_lnurlpos")
async def m001_initial(db):
"""
Initial lnurldevice table.
"""
await db.execute(
f"""
CREATE TABLE lnurldevice.lnurldevices (
id TEXT NOT NULL PRIMARY KEY,
key TEXT NOT NULL,
title TEXT NOT NULL,
wallet TEXT NOT NULL,
currency TEXT NOT NULL,
device TEXT NOT NULL,
profit FLOAT NOT NULL,
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
await db.execute(
f"""
CREATE TABLE lnurldevice.lnurldevicepayment (
id TEXT NOT NULL PRIMARY KEY,
deviceid TEXT NOT NULL,
payhash TEXT,
payload TEXT NOT NULL,
pin INT,
sats INT,
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
async def m002_redux(db):
"""
Moves everything from lnurlpos to lnurldevices
"""
for row in [
list(row) for row in await db2.fetchall("SELECT * FROM lnurlpos.lnurlposs")
]:
await db.execute(
"""
INSERT INTO lnurldevice.lnurldevices (
id,
key,
title,
wallet,
currency,
device,
profit
)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(row[0], row[1], row[2], row[3], row[4], "pos", 0),
)
for row in [
list(row) for row in await db2.fetchall("SELECT * FROM lnurlpos.lnurlpospayment")
]:
await db.execute(
"""
INSERT INTO lnurldevice.lnurldevicepayment (
id,
deviceid,
payhash,
payload,
pin,
sats
)
VALUES (?, ?, ?, ?, ?, ?)
""",
(row[0], row[1], row[3], row[4], row[5], row[6]),
)
+53
View File
@@ -0,0 +1,53 @@
import json
from sqlite3 import Row
from typing import Optional
from fastapi import Request
from lnurl import Lnurl
from lnurl import encode as lnurl_encode # type: ignore
from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
from lnurl.types import LnurlPayMetadata # type: ignore
from pydantic import BaseModel
from pydantic.main import BaseModel
class createLnurldevice(BaseModel):
title: str
wallet: str
currency: str
device: str
profit: float
class lnurldevices(BaseModel):
id: str
key: str
title: str
wallet: str
currency: str
device: str
profit: float
timestamp: str
def from_row(cls, row: Row) -> "lnurldevices":
return cls(**dict(row))
def lnurl(self, req: Request) -> Lnurl:
url = req.url_for("lnurldevice.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 lnurldevicepayment(BaseModel):
id: str
deviceid: str
payhash: str
payload: str
pin: int
sats: int
timestamp: str
@classmethod
def from_row(cls, row: Row) -> "lnurldevicepayment":
return cls(**dict(row))
@@ -0,0 +1,169 @@
<q-card>
<q-card-section>
<p>
Register LNURLDevice devices to receive payments in your LNbits wallet.<br />
Build your own here
<a href="https://github.com/arcbtc/bitcoinpos"
>https://github.com/arcbtc/bitcoinpos</a
><br />
<small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
>
</p>
</q-card-section>
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item
group="api"
dense
expand-separator
label="Create lnurldevice"
>
<q-card>
<q-card-section>
<code
><span class="text-blue"></span> /lnurldevice/api/v1/lnurlpos</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">
Body (application/json)
</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;lnurldevice_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url }}api/v1/lnurldevice -d '{"title":
&lt;string&gt;, "message":&lt;string&gt;, "currency":
&lt;integer&gt;}' -H "Content-type: application/json" -H "X-Api-Key:
{{user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Update lnurldevice"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">PUT</span>
/lnurldevice/api/v1/lnurlpos/&lt;lnurldevice_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">
Body (application/json)
</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;lnurldevice_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url
}}api/v1/lnurlpos/&lt;lnurldevice_id&gt; -d ''{"title":
&lt;string&gt;, "message":&lt;string&gt;, "currency":
&lt;integer&gt;} -H "Content-type: application/json" -H "X-Api-Key:
{{user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Get lnurldevice"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/lnurldevice/api/v1/lnurlpos/&lt;lnurldevice_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">
Body (application/json)
</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;lnurldevice_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url
}}api/v1/lnurlpos/&lt;lnurldevice_id&gt; -H "X-Api-Key: {{
user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Get lnurldevices"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/lnurldevice/api/v1/lnurlposs</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">
Body (application/json)
</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;lnurldevice_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}api/v1/lnurldevices -H
"X-Api-Key: {{ user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Delete a pay link"
class="q-pb-md"
>
<q-card>
<q-card-section>
<code
><span class="text-pink">DELETE</span>
/lnurldevice/api/v1/lnurlpos/&lt;lnurldevice_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
<code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.base_url
}}api/v1/lnurlpos/&lt;lnurldevice_id&gt; -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>
</q-card>
@@ -0,0 +1,34 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<center>
<h3 class="q-my-none">LNURL-pay not paid</h3>
<br />
<q-icon
name="warning"
class="text-grey"
style="font-size: 20rem"
></q-icon>
<br />
</center>
</q-card-section>
</q-card>
</div>
{% endblock %} {% block scripts %}
<script>
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {}
}
})
</script>
{% endblock %}
</div>
@@ -0,0 +1,534 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-7 q-gutter-y-md">
<q-card>
<q-card-section>
{% raw %}
<q-btn
unelevated
color="primary"
@click="formDialoglnurldevice.show = true"
>New LNURLDevice instance
</q-btn>
</q-card-section>
</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">lNURLdevice</h5>
</div>
<div class="col-auto">
<q-input
borderless
dense
debounce="300"
v-model="filter"
placeholder="Search"
>
<template v-slot:append>
<q-icon name="search"></q-icon>
</template>
</q-input>
<q-btn flat color="grey" @click="exportlnurldeviceCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
flat
dense
:data="lnurldeviceLinks"
row-key="id"
:columns="lnurldevicesTable.columns"
:pagination.sync="lnurldevicesTable.pagination"
:filter="filter"
>
<template v-slot:header="props">
<q-tr :props="props">
<q-th style="width: 5%"></q-th>
<q-th style="width: 5%"></q-th>
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
auto-width
>
<div v-if="col.name == 'id'"></div>
<div v-else>{{ col.label }}</div>
</q-th>
<!-- <q-th auto-width></q-th> -->
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td>
<q-btn
flat
dense
size="xs"
@click="deletelnurldeviceLink(props.row.id)"
icon="cancel"
color="pink"
>
<q-tooltip> Delete LNURLDevice </q-tooltip>
</q-btn>
</q-td>
<q-td>
<q-btn
flat
dense
size="xs"
@click="openlnurldeviceSettings(props.row.id)"
icon="perm_data_setting"
color="primary"
>
<q-tooltip> LNURLDevice Settings </q-tooltip>
</q-btn>
</q-td>
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
auto-width
>
<div v-if="col.name == 'id'"></div>
<div v-else>{{ col.value }}</div>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} LNURLDevice Extension
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "lnurldevice/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
<q-dialog
v-model="settingsDialog.show"
deviceition="top"
@hide="closeFormDialog"
>
<q-card
style="width: 700px; max-width: 80vw"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<div class="text-h6">LNURLDevice device string</div>
<q-btn
dense
outline
unelevated
color="primary"
size="md"
@click="copyText(location + '/lnurldevice/api/v1/lnurl/' + settingsDialog.data.id + ',' +
settingsDialog.data.key + ',' + settingsDialog.data.currency, 'Link copied to clipboard!')"
>{% raw
%}{{location}}/lnurldevice/api/v1/lnurl/{{settingsDialog.data.id}},
{{settingsDialog.data.key}}, {{settingsDialog.data.currency}}{% endraw
%}<q-tooltip> Click to copy URL </q-tooltip>
</q-btn>
<div class="text-subtitle2">
<small> </small>
</div>
</q-card>
</q-dialog>
<q-dialog
v-model="formDialoglnurldevice.show"
deviceition="top"
@hide="closeFormDialog"
>
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendFormDatalnurldevice" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="formDialoglnurldevice.data.title"
type="text"
label="Title"
></q-input>
<q-select
filled
dense
emit-value
v-model="formDialoglnurldevice.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
></q-select>
<q-select
filled
dense
v-model.trim="formDialoglnurldevice.data.currency"
type="text"
label="Fiat currency for device"
:options="currency"
></q-select>
<q-option-group
v-model.trim="formDialoglnurldevice.data.device"
:options="devices"
color="primary"
label="Type of device"
></q-option-group>
<q-input
filled
dense
v-model.trim="formDialoglnurldevice.data.profit"
type="number"
max="90"
label="Profit margin (% added to invoices/deducted from faucets)"
></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="formDialoglnurldevice.data.id"
unelevated
color="primary"
:disable="
formDialoglnurldevice.data.title == ''"
type="submit"
>Update lnurldevice</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="
formDialoglnurldevice.data.title == ''"
type="submit"
>Create lnurldevice</q-btn
>
<q-btn @click="cancellnurldevice" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
Vue.component(VueQrcode.name, VueQrcode)
var maplnurldevice = obj => {
obj._data = _.clone(obj)
obj.theTime = obj.time * 60 - (Date.now() / 1000 - obj.timestamp)
obj.time = obj.time + 'mins'
if (obj.time_elapsed) {
obj.date = 'Time elapsed'
} else {
obj.date = Quasar.utils.date.formatDate(
new Date((obj.theTime - 3600) * 1000),
'HH:mm:ss'
)
}
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
location: window.location.hostname,
filter: '',
currency: 'USD',
lnurldeviceLinks: [],
lnurldeviceLinksObj: [],
devices: [
{
label: 'PoS',
value: 'pos'
},
{
label: 'ATM',
value: 'atm'
}
],
lnurldevicesTable: {
columns: [
{
name: 'title',
align: 'left',
label: 'title',
field: 'title'
},
{
name: 'theId',
align: 'left',
label: 'id',
field: 'id'
},
{
name: 'key',
align: 'left',
label: 'key',
field: 'key'
},
{
name: 'wallet',
align: 'left',
label: 'wallet',
field: 'wallet'
},
{
name: 'device',
align: 'left',
label: 'device',
field: 'device'
},
{
name: 'profit',
align: 'left',
label: 'profit',
field: 'profit'
},
{
name: 'currency',
align: 'left',
label: 'currency',
field: 'currency'
}
],
pagination: {
rowsPerPage: 10
}
},
passedlnurldevice: {},
settingsDialog: {
show: false,
data: {}
},
formDialog: {
show: false,
data: {}
},
formDialoglnurldevice: {
show: false,
data: {
lnurl_toggle: false,
show_message: false,
show_ack: false,
show_price: 'None',
device: 'pos',
profit: 2,
title: ''
}
},
qrCodeDialog: {
show: false,
data: null
}
}
},
methods: {
cancellnurldevice: function (data) {
var self = this
self.formDialoglnurldevice.show = false
self.clearFormDialoglnurldevice()
},
closeFormDialog: function () {
this.clearFormDialoglnurldevice()
this.formDialog.data = {
is_unique: false
}
},
sendFormDatalnurldevice: function () {
var self = this
if (self.formDialoglnurldevice.data.id) {
this.updatelnurldevice(
self.g.user.wallets[0].adminkey,
self.formDialoglnurldevice.data
)
} else {
this.createlnurldevice(
self.g.user.wallets[0].adminkey,
self.formDialoglnurldevice.data
)
}
},
createlnurldevice: function (wallet, data) {
var self = this
var updatedData = {}
for (const property in data) {
if (data[property]) {
updatedData[property] = data[property]
}
}
LNbits.api
.request('POST', '/lnurldevice/api/v1/lnurlpos', wallet, updatedData)
.then(function (response) {
self.lnurldeviceLinks.push(maplnurldevice(response.data))
self.formDialoglnurldevice.show = false
self.clearFormDialoglnurldevice()
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getlnurldevices: function () {
var self = this
LNbits.api
.request(
'GET',
'/lnurldevice/api/v1/lnurlpos',
self.g.user.wallets[0].adminkey
)
.then(function (response) {
if (response.data) {
self.lnurldeviceLinks = response.data.map(maplnurldevice)
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getlnurldevice: function (lnurldevice_id) {
var self = this
LNbits.api
.request(
'GET',
'/lnurldevice/api/v1/lnurlpos/' + lnurldevice_id,
self.g.user.wallets[0].adminkey
)
.then(function (response) {
localStorage.setItem('lnurldevice', JSON.stringify(response.data))
localStorage.setItem('inkey', self.g.user.wallets[0].inkey)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deletelnurldeviceLink: function (lnurldeviceId) {
var self = this
var link = _.findWhere(this.lnurldeviceLinks, {id: lnurldeviceId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this pay link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/lnurldevice/api/v1/lnurlpos/' + lnurldeviceId,
self.g.user.wallets[0].adminkey
)
.then(function (response) {
self.lnurldeviceLinks = _.reject(
self.lnurldeviceLinks,
function (obj) {
return obj.id === lnurldeviceId
}
)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
openUpdatelnurldeviceLink: function (lnurldeviceId) {
var self = this
var lnurldevice = _.findWhere(this.lnurldeviceLinks, {
id: lnurldeviceId
})
self.formDialoglnurldevice.data = _.clone(lnurldevice._data)
self.formDialoglnurldevice.show = true
},
openlnurldeviceSettings: function (lnurldeviceId) {
var self = this
var lnurldevice = _.findWhere(this.lnurldeviceLinks, {
id: lnurldeviceId
})
self.settingsDialog.data = _.clone(lnurldevice._data)
self.settingsDialog.show = true
},
updatelnurldevice: function (wallet, data) {
var self = this
var updatedData = {}
for (const property in data) {
if (data[property]) {
updatedData[property] = data[property]
}
}
LNbits.api
.request(
'PUT',
'/lnurldevice/api/v1/lnurlpos/' + updatedData.id,
wallet,
updatedData
)
.then(function (response) {
self.lnurldeviceLinks = _.reject(
self.lnurldeviceLinks,
function (obj) {
return obj.id === updatedData.id
}
)
self.lnurldeviceLinks.push(maplnurldevice(response.data))
self.formDialoglnurldevice.show = false
self.clearFormDialoglnurldevice()
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
clearFormDialoglnurldevice() {
this.formDialoglnurldevice.data = {
lnurl_toggle: false,
show_message: false,
show_ack: false,
show_price: 'None',
title: ''
}
},
exportlnurldeviceCSV: function () {
var self = this
LNbits.utils.exportCSV(
self.lnurldevicesTable.columns,
this.lnurldeviceLinks
)
}
},
created: function () {
var self = this
var getlnurldevices = this.getlnurldevices
getlnurldevices()
self.location = [
window.location.protocol,
'//',
window.location.host
].join('')
LNbits.api
.request('GET', '/api/v1/currencies')
.then(response => {
this.currency = ['USD', ...response.data]
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
}
})
</script>
{% endblock %}
@@ -0,0 +1,27 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<center>
<h3 class="q-my-none">{{ pin }}</h3>
<br />
</center>
</q-card-section>
</q-card>
</div>
{% endblock %} {% block scripts %}
<script>
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {}
}
})
</script>
{% endblock %}
</div>
+50
View File
@@ -0,0 +1,50 @@
from http import HTTPStatus
from fastapi import Request
from fastapi.param_functions import Query
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from lnbits.core.crud import update_payment_status
from lnbits.core.models import User
from lnbits.core.views.api import api_payment
from lnbits.decorators import check_user_exists
from . import lnurldevice_ext, lnurldevice_renderer
from .crud import get_lnurldevice, get_lnurldevicepayment
templates = Jinja2Templates(directory="templates")
@lnurldevice_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return lnurldevice_renderer().TemplateResponse(
"lnurldevice/index.html", {"request": request, "user": user.dict()}
)
@lnurldevice_ext.get(
"/{paymentid}", name="lnurldevice.displaypin", response_class=HTMLResponse
)
async def displaypin(request: Request, paymentid: str = Query(None)):
lnurldevicepayment = await get_lnurldevicepayment(paymentid)
if not lnurldevicepayment:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="No lmurldevice payment"
)
device = await get_lnurldevice(lnurldevicepayment.deviceid)
if not device:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="lnurldevice not found."
)
status = await api_payment(lnurldevicepayment.payhash)
if status["paid"]:
await update_payment_status(checking_id=lnurldevicepayment.payhash, pending=True)
return lnurldevice_renderer().TemplateResponse(
"lnurldevice/paid.html", {"request": request, "pin": lnurldevicepayment.pin}
)
return lnurldevice_renderer().TemplateResponse(
"lnurldevice/error.html", {"request": request, "pin": "filler", "not_paid": True}
)
@@ -0,0 +1,85 @@
from http import HTTPStatus
from fastapi import Request
from fastapi.param_functions import Query
from fastapi.params import Depends
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from lnbits.extensions.lnurldevice import lnurldevice_ext
from lnbits.utils.exchange_rates import currencies
from . import lnurldevice_ext
from .crud import (
create_lnurldevice,
delete_lnurldevice,
get_lnurldevice,
get_lnurldevices,
update_lnurldevice,
)
from .models import createLnurldevice
@lnurldevice_ext.get("/api/v1/currencies")
async def api_list_currencies_available():
return list(currencies.keys())
#######################lnurldevice##########################
@lnurldevice_ext.post("/api/v1/lnurlpos")
@lnurldevice_ext.put("/api/v1/lnurlpos/{lnurldevice_id}")
async def api_lnurldevice_create_or_update(
data: createLnurldevice,
wallet: WalletTypeInfo = Depends(require_admin_key),
lnurldevice_id: str = Query(None),
):
if not lnurldevice_id:
lnurldevice = await create_lnurldevice(data)
return lnurldevice.dict()
else:
lnurldevice = await update_lnurldevice(data, lnurldevice_id=lnurldevice_id)
return lnurldevice.dict()
@lnurldevice_ext.get("/api/v1/lnurlpos")
async def api_lnurldevices_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
try:
return [{**lnurldevice.dict()} for lnurldevice in await get_lnurldevices(wallet_ids)]
except:
return ""
@lnurldevice_ext.get("/api/v1/lnurlpos/{lnurldevice_id}")
async def api_lnurldevice_retrieve(
request: Request,
wallet: WalletTypeInfo = Depends(get_key_type),
lnurldevice_id: str = Query(None),
):
lnurldevice = await get_lnurldevice(lnurldevice_id)
if not lnurldevice:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="lnurldevice does not exist"
)
if not lnurldevice.lnurl_toggle:
return {**lnurldevice.dict()}
return {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(request=request)}}
@lnurldevice_ext.delete("/api/v1/lnurlpos/{lnurldevice_id}")
async def api_lnurldevice_delete(
wallet: WalletTypeInfo = Depends(require_admin_key), lnurldevice_id: str = Query(None)
):
lnurldevice = await get_lnurldevice(lnurldevice_id)
if not lnurldevice:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Wallet link does not exist."
)
await delete_lnurldevice(lnurldevice_id)
return "", HTTPStatus.NO_CONTENT