diff --git a/crud.py b/crud.py
index ed46061..202f0c8 100644
--- a/crud.py
+++ b/crud.py
@@ -4,7 +4,7 @@
import shortuuid
from fastapi import Request
from lnbits.db import Database
-from lnbits.helpers import urlsafe_short_hash
+from lnbits.helpers import update_query, urlsafe_short_hash
from lnurl import encode as lnurl_encode
from .models import CreateLnurldevice, Lnurldevice, LnurldevicePayment
@@ -19,24 +19,24 @@ async def create_lnurldevice(data: CreateLnurldevice, req: Request) -> Lnurldevi
lnurldevice_id = urlsafe_short_hash()
lnurldevice_key = urlsafe_short_hash()
- if data.switches:
+ if isinstance(data.extra, list):
url = req.url_for("lnurldevice.lnurl_v2_params", device_id=lnurldevice_id)
- for _switch in data.switches:
- _switch.lnurl = lnurl_encode(
+ for _extra in data.extra:
+ _extra.lnurl = lnurl_encode(
str(url)
- + f"?pin={_switch.pin}"
- + f"&amount={_switch.amount}"
- + f"&duration={_switch.duration}"
- + f"&variable={_switch.variable}"
- + f"&comment={_switch.comment}"
+ + f"?pin={_extra.pin}"
+ + f"&amount={_extra.amount}"
+ + f"&duration={_extra.duration}"
+ + f"&variable={_extra.variable}"
+ + f"&comment={_extra.comment}"
+ "&disabletime=0"
)
await db.execute(
"""
INSERT INTO lnurldevice.lnurldevice
- (id, key, title, wallet, profit, currency, device, switches)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ (id, key, title, wallet, profit, currency, device, extra) VALUES
+ (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
lnurldevice_id,
@@ -46,7 +46,11 @@ async def create_lnurldevice(data: CreateLnurldevice, req: Request) -> Lnurldevi
data.profit,
data.currency,
data.device,
- json.dumps(data.switches, default=lambda x: x.dict()),
+ (
+ json.dumps(data.extra, default=lambda x: x.dict())
+ if data.extra != "boltz"
+ else data.extra
+ ),
),
)
@@ -58,17 +62,16 @@ async def create_lnurldevice(data: CreateLnurldevice, req: Request) -> Lnurldevi
async def update_lnurldevice(
lnurldevice_id: str, data: CreateLnurldevice, req: Request
) -> Lnurldevice:
-
- if data.switches:
+ if isinstance(data.extra, list):
url = req.url_for("lnurldevice.lnurl_v2_params", device_id=lnurldevice_id)
- for _switch in data.switches:
- _switch.lnurl = lnurl_encode(
+ for _extra in data.extra:
+ _extra.lnurl = lnurl_encode(
str(url)
- + f"?pin={_switch.pin}"
- + f"&amount={_switch.amount}"
- + f"&duration={_switch.duration}"
- + f"&variable={_switch.variable}"
- + f"&comment={_switch.comment}"
+ + f"?pin={_extra.pin}"
+ + f"&amount={_extra.amount}"
+ + f"&duration={_extra.duration}"
+ + f"&variable={_extra.variable}"
+ + f"&comment={_extra.comment}"
)
await db.execute(
@@ -79,7 +82,7 @@ async def update_lnurldevice(
profit = ?,
currency = ?,
device = ?,
- switches = ?
+ extra = ?
WHERE id = ?
""",
(
@@ -88,7 +91,11 @@ async def update_lnurldevice(
data.profit,
data.currency,
data.device,
- json.dumps(data.switches, default=lambda x: x.dict()),
+ (
+ json.dumps(data.extra, default=lambda x: x.dict())
+ if data.extra != "boltz"
+ else data.extra
+ ),
lnurldevice_id,
),
)
@@ -106,18 +113,17 @@ async def get_lnurldevice(lnurldevice_id: str, req: Request) -> Optional[Lnurlde
device = Lnurldevice(**row)
- # this is needed for backwards compabtibility,
- # before the LNURL were cached inside db
- if device.switches:
+ # this is needed for backward compatibility, before the LNURL were cached inside db
+ if isinstance(device.extra, list):
url = req.url_for("lnurldevice.lnurl_v2_params", device_id=device.id)
- for _switch in device.switches:
- _switch.lnurl = lnurl_encode(
+ for _extra in device.extra:
+ _extra.lnurl = lnurl_encode(
str(url)
- + f"?pin={_switch.pin}"
- + f"&amount={_switch.amount}"
- + f"&duration={_switch.duration}"
- + f"&variable={_switch.variable}"
- + f"&comment={_switch.comment}"
+ + f"?pin={_extra.pin}"
+ + f"&amount={_extra.amount}"
+ + f"&duration={_extra.duration}"
+ + f"&variable={_extra.variable}"
+ + f"&comment={_extra.comment}"
)
return device
@@ -139,16 +145,16 @@ async def get_lnurldevices(wallet_ids: List[str], req: Request) -> List[Lnurldev
devices = [Lnurldevice(**row) for row in rows]
for device in devices:
- if device.switches:
+ if isinstance(device.extra, list):
url = req.url_for("lnurldevice.lnurl_v2_params", device_id=device.id)
- for _switch in device.switches:
- _switch.lnurl = lnurl_encode(
+ for _extra in device.extra:
+ _extra.lnurl = lnurl_encode(
str(url)
- + f"?pin={_switch.pin}"
- + f"&amount={_switch.amount}"
- + f"&duration={_switch.duration}"
- + f"&variable={_switch.variable}"
- + f"&comment={_switch.comment}"
+ + f"?pin={_extra.pin}"
+ + f"&amount={_extra.amount}"
+ + f"&duration={_extra.duration}"
+ + f"&variable={_extra.variable}"
+ + f"&comment={_extra.comment}"
)
return devices
@@ -168,11 +174,6 @@ async def create_lnurldevicepayment(
sats: Optional[int] = 0,
) -> LnurldevicePayment:
- # TODO: ben what is this for?
- # if device.device == "atm":
- # lnurldevicepayment_id = shortuuid.uuid(name=payload)
- # else:
-
lnurldevicepayment_id = urlsafe_short_hash()
await db.execute(
"""
@@ -194,16 +195,13 @@ async def create_lnurldevicepayment(
async def update_lnurldevicepayment(
- lnurldevicepayment_id: str, **kwargs
+ lnurldevicepayment: LnurldevicePayment,
) -> 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),
+ update_query("lnurldevice.lnurldevicepayment", lnurldevicepayment),
+ (*lnurldevicepayment.dict().values(), lnurldevicepayment.id),
)
- dpayment = await get_lnurldevicepayment(lnurldevicepayment_id)
- assert dpayment, "Couldnt retrieve update LnurldevicePayment"
- return dpayment
+ return lnurldevicepayment
async def get_lnurldevicepayment(
@@ -216,6 +214,20 @@ async def get_lnurldevicepayment(
return LnurldevicePayment(**row) if row else None
+async def get_lnurldevicepayments(
+ lnurldevice_ids: List[str],
+) -> List[LnurldevicePayment]:
+ q = ",".join(["?"] * len(lnurldevice_ids))
+ rows = await db.fetchall(
+ f"""
+ SELECT * FROM lnurldevice.lnurldevicepayment WHERE deviceid IN ({q})
+ ORDER BY id
+ """,
+ (*lnurldevice_ids,),
+ )
+ return [LnurldevicePayment(**row) for row in rows]
+
+
async def get_lnurldevicepayment_by_p(
p: str,
) -> Optional[LnurldevicePayment]:
@@ -234,3 +246,20 @@ async def get_lnurlpayload(
(lnurldevicepayment_payload,),
)
return LnurldevicePayment(**row) if row else None
+
+
+async def get_recent_lnurldevicepayment(payload: str) -> Optional[LnurldevicePayment]:
+ row = await db.fetchone(
+ """
+ SELECT * FROM lnurldevice.lnurldevicepayment
+ WHERE payload = ? ORDER BY timestamp DESC LIMIT 1
+ """,
+ (payload,),
+ )
+ return LnurldevicePayment(**row) if row else None
+
+
+async def delete_atm_payment_link(atm_id: str) -> None:
+ await db.execute(
+ "DELETE FROM lnurldevice.lnurldevicepayment WHERE id = ?", (atm_id,)
+ )
diff --git a/helpers.py b/helpers.py
new file mode 100644
index 0000000..31eb92f
--- /dev/null
+++ b/helpers.py
@@ -0,0 +1,83 @@
+import base64
+import hmac
+from io import BytesIO
+from typing import Optional
+
+from embit import compact
+from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
+
+from .crud import create_lnurldevicepayment, get_recent_lnurldevicepayment
+from .models import Lnurldevice, LnurldevicePayment
+
+
+async def register_atm_payment(
+ device: Lnurldevice, payload: str
+) -> tuple[Optional[LnurldevicePayment], Optional[int]]:
+ """
+ Register an ATM payment to avoid double pull.
+ """
+ lnurldevicepayment = await get_recent_lnurldevicepayment(payload)
+ # If the payment is already registered and been paid, return None
+ if lnurldevicepayment and lnurldevicepayment.payload == lnurldevicepayment.payhash:
+ return None, None
+ # If the payment is already registered and not been paid, return lnurlpayment record
+ elif (
+ lnurldevicepayment and lnurldevicepayment.payload != lnurldevicepayment.payhash
+ ):
+ return lnurldevicepayment, None
+
+ # else create a new lnurlpayment record
+ data = base64.urlsafe_b64decode(payload)
+ decrypted = xor_decrypt(device.key.encode(), data)
+ price_msat = (
+ await fiat_amount_as_satoshis(float(decrypted[1]) / 100, device.currency) * 1000
+ if device.currency != "sat"
+ else decrypted[1] * 1000
+ )
+ price_msat = int(price_msat * ((device.profit / 100) + 1))
+ lnurldevicepayment = await create_lnurldevicepayment(
+ deviceid=device.id,
+ payload=payload,
+ sats=int(price_msat / 1000),
+ pin=decrypted[0],
+ payhash="payment_hash",
+ )
+ return lnurldevicepayment, price_msat
+
+
+def xor_decrypt(key, blob):
+ s = BytesIO(blob)
+ variant = s.read(1)[0]
+ if variant != 1:
+ raise RuntimeError("Not implemented")
+ # reading nonce
+ nonce_len = s.read(1)[0]
+ nonce = s.read(nonce_len)
+ if len(nonce) != nonce_len:
+ raise RuntimeError("Missing nonce bytes")
+ if nonce_len < 8:
+ raise RuntimeError("Nonce is too short")
+
+ # reading payload
+ payload_len = s.read(1)[0]
+ payload = s.read(payload_len)
+ if len(payload) > 32:
+ raise RuntimeError("Payload is too long for this encryption method")
+ if len(payload) != payload_len:
+ 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 str(pin), amount_in_cent
diff --git a/migrations.py b/migrations.py
index 7ac9cf5..e6e4a12 100644
--- a/migrations.py
+++ b/migrations.py
@@ -212,3 +212,10 @@ async def m005_redux(db):
# drop old table columns
await db.execute(f"DROP TABLE {old_db}")
+
+
+async def m006_redux(db):
+ # Rename switches so we can also use for atm
+ await db.execute(
+ "ALTER TABLE lnurldevice.lnurldevice RENAME COLUMN switches TO extra"
+ )
diff --git a/models.py b/models.py
index a454220..ff6d6ab 100644
--- a/models.py
+++ b/models.py
@@ -1,12 +1,11 @@
import json
-from sqlite3 import Row
-from typing import List, Optional
+from typing import List, Optional, Union
from lnurl.types import LnurlPayMetadata
from pydantic import BaseModel, Json
-class LnurldeviceSwitch(BaseModel):
+class LnurldeviceExtra(BaseModel):
amount: float = 0.0
duration: int = 0
pin: int = 0
@@ -21,7 +20,7 @@ class CreateLnurldevice(BaseModel):
currency: str
device: str
profit: float
- switches: Optional[List[LnurldeviceSwitch]]
+ extra: Optional[Union[List[LnurldeviceExtra], str]]
class Lnurldevice(BaseModel):
@@ -32,13 +31,9 @@ class Lnurldevice(BaseModel):
profit: float
currency: str
device: str
- switches: Optional[Json[List[LnurldeviceSwitch]]]
+ extra: Optional[Union[Json[List[LnurldeviceExtra]], str]]
timestamp: str
- @classmethod
- def from_row(cls, row: Row) -> "Lnurldevice":
- return cls(**dict(row))
-
@property
def lnurlpay_metadata(self) -> LnurlPayMetadata:
return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
@@ -53,6 +48,6 @@ class LnurldevicePayment(BaseModel):
sats: int
timestamp: str
- @classmethod
- def from_row(cls, row: Row) -> "LnurldevicePayment":
- return cls(**dict(row))
+
+class Lnurlencode(BaseModel):
+ url: str
diff --git a/static/js/index.js b/static/js/index.js
new file mode 100644
index 0000000..ac897dc
--- /dev/null
+++ b/static/js/index.js
@@ -0,0 +1,481 @@
+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
+}
+var mapatmpayments = obj => {
+ obj._data = _.clone(obj)
+ obj.time = obj.timestamp * 60 - (Date.now() / 1000 - obj.timestamp)
+ return obj
+}
+
+new Vue({
+ el: '#vue',
+ mixins: [windowMixin],
+ data: function () {
+ return {
+ tab: 'mails',
+ protocol: window.location.protocol,
+ location: window.location.hostname,
+ wslocation: window.location.hostname,
+ filter: '',
+ currency: 'USD',
+ lnurlValue: '',
+ websocketMessage: '',
+ lnurldeviceLinks: [],
+ atmLinks: [],
+ lnurldeviceLinksObj: [],
+ boltzToggleState: false,
+ devices: [
+ {
+ label: 'PoS',
+ value: 'pos'
+ },
+ {
+ label: 'ATM',
+ value: 'atm'
+ },
+ {
+ label: 'Switch',
+ value: 'switch'
+ }
+ ],
+ 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: 'currency',
+ align: 'left',
+ label: 'currency',
+ field: 'currency'
+ }
+ ],
+ pagination: {
+ rowsPerPage: 10
+ }
+ },
+ atmTable: {
+ columns: [
+ {
+ name: 'id',
+ align: 'left',
+ label: 'ID',
+ field: 'id'
+ },
+ {
+ name: 'deviceid',
+ align: 'left',
+ label: 'Device ID',
+ field: 'deviceid'
+ },
+ {
+ name: 'sats',
+ align: 'left',
+ label: 'Sats',
+ field: 'sats'
+ },
+ {
+ name: 'time',
+ align: 'left',
+ label: 'Date',
+ field: 'time'
+ }
+ ],
+ pagination: {
+ rowsPerPage: 10
+ }
+ },
+ passedlnurldevice: {},
+ settingsDialog: {
+ show: false,
+ data: {}
+ },
+ formDialog: {
+ show: false,
+ data: {}
+ },
+ formDialoglnurldevice: {
+ show: false,
+ data: {
+ extra: [],
+ lnurl_toggle: false,
+ show_message: false,
+ show_ack: false,
+ show_price: 'None',
+ device: 'pos',
+ profit: 1,
+ amount: 1,
+ title: ''
+ }
+ },
+ qrCodeDialog: {
+ show: false,
+ data: null
+ }
+ }
+ },
+ computed: {
+ wsMessage: function () {
+ return this.websocketMessage
+ }
+ },
+ methods: {
+ openQrCodeDialog: function (lnurldevice_id) {
+ var lnurldevice = _.findWhere(this.lnurldeviceLinks, {
+ id: lnurldevice_id
+ })
+ this.qrCodeDialog.data = _.clone(lnurldevice)
+ this.qrCodeDialog.data.url =
+ window.location.protocol + '//' + window.location.host
+ this.lnurlValue = this.qrCodeDialog.data.extra[0].lnurl
+ this.websocketConnector(
+ 'wss://' + window.location.host + '/api/v1/ws/' + lnurldevice_id
+ )
+ this.qrCodeDialog.show = true
+ },
+ addSwitch: function () {
+ if (!this.formDialoglnurldevice.data.extra) {
+ this.formDialoglnurldevice.data.extra = []
+ }
+ this.formDialoglnurldevice.data.extra.push({
+ amount: 10,
+ pin: 0,
+ duration: 1000,
+ variable: false,
+ comment: false
+ })
+ },
+ removeSwitch: function () {
+ this.formDialoglnurldevice.data.extra.pop()
+ },
+
+ 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.profit) {
+ self.formDialoglnurldevice.data.profit = 0
+ }
+ 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)
+ })
+ },
+ getatmpayments: function () {
+ var self = this
+ LNbits.api
+ .request(
+ 'GET',
+ '/lnurldevice/api/v1/atm',
+ self.g.user.wallets[0].adminkey
+ )
+ .then(function (response) {
+ if (response.data) {
+ self.atmLinks = response.data.map(mapatmpayments)
+ }
+ })
+ .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)
+ })
+ })
+ },
+ deleteATMLink: function (atmId) {
+ var self = this
+ var link = _.findWhere(this.atmLinks, {id: atmId})
+ LNbits.utils
+ .confirmDialog('Are you sure you want to delete this atm link?')
+ .onOk(function () {
+ LNbits.api
+ .request(
+ 'DELETE',
+ '/lnurldevice/api/v1/atm/' + atmId,
+ self.g.user.wallets[0].adminkey
+ )
+ .then(function (response) {
+ self.atmLinks = _.reject(self.atmLinks, function (obj) {
+ return obj.id === atmId
+ })
+ })
+ .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)
+ if (lnurldevice.device == 'atm' && lnurldevice.extra == 'boltz') {
+ self.boltzToggleState = true
+ } else {
+ self.boltzToggleState = false
+ }
+ 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
+ },
+ handleBoltzToggleChange(val) {
+ if (val) {
+ this.formDialoglnurldevice.data.extra = 'boltz'
+ } else {
+ this.formDialoglnurldevice.data.extra = ''
+ }
+ },
+ 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)
+ })
+ },
+ websocketConnector: function (websocketUrl) {
+ if ('WebSocket' in window) {
+ self = this
+ var ws = new WebSocket(websocketUrl)
+ self.updateWsMessage('Websocket connected')
+ ws.onmessage = function (evt) {
+ var received_msg = evt.data
+ self.updateWsMessage('Message received: ' + received_msg)
+ }
+ ws.onclose = function () {
+ self.updateWsMessage('Connection closed')
+ }
+ } else {
+ self.updateWsMessage('WebSocket NOT supported by your Browser!')
+ }
+ },
+ updateWsMessage: function (message) {
+ this.websocketMessage = message
+ },
+ 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
+ )
+ },
+ exportATMCSV: function () {
+ var self = this
+ LNbits.utils.exportCSV(self.atmTable.columns, this.atmLinks)
+ },
+ openATMLink: function (deviceid, p) {
+ var self = this
+ var url =
+ this.location +
+ '/lnurldevice/api/v1/lnurl/' +
+ deviceid +
+ '?atm=1&p=' +
+ p
+ data = {
+ url: url
+ }
+ LNbits.api
+ .request(
+ 'POST',
+ '/lnurldevice/api/v1/lnurlencode',
+ self.g.user.wallets[0].adminkey,
+ data
+ )
+ .then(function (response) {
+ window.open('/lnurldevice/atm?lightning=' + response.data)
+ })
+ .catch(function (error) {
+ LNbits.utils.notifyApiError(error)
+ })
+ }
+ },
+ created: function () {
+ var self = this
+ var getlnurldevices = this.getlnurldevices
+ getlnurldevices()
+ var getatmpayments = this.getatmpayments
+ getatmpayments()
+ self.location = [window.location.protocol, '//', window.location.host].join(
+ ''
+ )
+ self.wslocation = ['ws://', window.location.host].join('')
+ LNbits.api
+ .request('GET', '/api/v1/currencies')
+ .then(response => {
+ this.currency = ['sat', 'USD', ...response.data]
+ })
+ .catch(err => {
+ LNbits.utils.notifyApiError(err)
+ })
+ }
+})
diff --git a/tasks.py b/tasks.py
index 75e129b..3cfad76 100644
--- a/tasks.py
+++ b/tasks.py
@@ -26,17 +26,15 @@ async def on_invoice_paid(payment: Payment) -> None:
return
if lnurldevicepayment.payhash == "used":
return
-
- lnurldevicepayment = await update_lnurldevicepayment(
- lnurldevicepayment_id=payment.extra["id"], payhash="used"
- )
+ lnurldevicepayment.payhash = lnurldevicepayment.payload
+ lnurldevicepayment = await update_lnurldevicepayment(lnurldevicepayment)
comment = payment.extra["comment"]
variable = False
if payment.extra["variable"] == "True":
variable = True
payload = lnurldevicepayment.payload
if variable:
- payload = str(
+ payload = int(
(int(payload) / int(lnurldevicepayment.sats))
* int(payment.extra["amount"])
)
diff --git a/templates/lnurldevice/atm.html b/templates/lnurldevice/atm.html
new file mode 100644
index 0000000..a0e80b1
--- /dev/null
+++ b/templates/lnurldevice/atm.html
@@ -0,0 +1,307 @@
+
+
+
+
+{% extends "public.html" %} {% block page %}
+
+
+
+
+
+
+ This ATM has been used.
+
+
+
+
+
+
+
+
+
+
+
+ Amount is too small to send over onchain, needs to be 10000+
+ sats
+ Onchain not available
+
+
+
+ Amount is too small to send over liquid, needs to be 2000+
+ sats
+ Onchain not available
+
+
+
+
+
+
+ LNURL withdraw
+
+
+
+ Copy LNURL
+
+
+
+
+ Lightning / LNaddress / LNURL-pay
+
+
+
+
+
+
+
+
+
+
+ Onchain
+
+
+
+
+
+
+
+
+
+
+ Liquid
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ You can withdraw:
+
+ You can withdraw:
+
+ ${amount} SATS/BTC
+ ${(amount / 100000000).toFixed(8)} BTC
+
+
+
+
+
+
+
+
+
+{% endblock %} {% block scripts %}
+
+{% endblock %}
diff --git a/templates/lnurldevice/atm_receipt.html b/templates/lnurldevice/atm_receipt.html
new file mode 100644
index 0000000..4548811
--- /dev/null
+++ b/templates/lnurldevice/atm_receipt.html
@@ -0,0 +1,160 @@
+{% extends "print.html" %} {% block page %}
+
+
+
+
+
+
ATM receipt for: "{{devicename}}"
+
{{ amt }} sats
+
+
+ Payment ID |
+ {{id}} |
+
+
+
+ Amount |
+ {{sats/1000}} Sats |
+
+
+ Device |
+ {{devicename}} ({{deviceid}}) |
+
+
+ Withdraw attempt |
+ ${newtimestamp} |
+
+
+ Claimed |
+ {{ "False" if payhash == "payment_hash" else "True" }} |
+
+
+
+
+
+ (scan for this page)
+
+
+
+
+
+ (scan for claim page)
+
+
+
+
+
+
+
+
+
+{% endblock %} {% block styles %}
+
+
+{% endblock %} {% block scripts %}
+
+{% endblock %}
diff --git a/templates/lnurldevice/index.html b/templates/lnurldevice/index.html
index 71ba260..79ba722 100644
--- a/templates/lnurldevice/index.html
+++ b/templates/lnurldevice/index.html
@@ -132,6 +132,95 @@ LNURLdevice
+
+
+
+
+
+
+
+
+
ATM Payments
+
+
+
+
+
+
+
+
+ Export to CSV
+
+
+
+
+
+
+
+
+
+ {{ col.label }}
+
+
+
+
+
+
+
+
+
+ Open Claim page
+
+
+
+
+ Delete ATM Claim
+
+
+
+
+ {{ col.value }}
+
+
+
{% endraw %}
@@ -205,7 +294,8 @@
color="primary"
size="md"
@click="copyText(wslocation + '/api/v1/ws/' + settingsDialog.data.id, 'Link copied to clipboard!')"
- >{% raw %}{{wslocation}}/api/v1/ws/{{settingsDialog.data.id}}{% endraw
+ >
+ {% raw %}{{wslocation}}/api/v1/ws/{{settingsDialog.data.id}}{% endraw
%} Click to copy URL
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
+ @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
- %} Click to copy URL
+ %}
+ Click to copy URL
@@ -240,7 +331,6 @@
v-html="formDialoglnurldevice.data.device"
v-if="formDialoglnurldevice.data.id"
>
-
type="text"
label="Title"
>
-
:options="g.user.walletOptions"
label="Wallet *"
>
-
:options="devices"
color="primary"
label="Type of device"
+ @input="formDialoglnurldevice.data.extra = []"
>
size="sm"
icon="add"
@click="addSwitch"
- v-model="formDialoglnurldevice.data.switches"
+ v-model="formDialoglnurldevice.data.extra"
color="primary"
>
size="sm"
icon="remove"
@click="removeSwitch"
- v-model="formDialoglnurldevice.data.switches"
+ v-model="formDialoglnurldevice.data.extra"
color="primary"
>
-
+
label="Comment"
size="xs"
dense
- >Enable LNURLp comments with payments
+ Enable LNURLp comments with payments
+
+
Update lnurldevice
@@ -372,8 +465,7 @@
v-else
unelevated
color="primary"
- :disable="
- formDialoglnurldevice.data.title == ''"
+ :disable="formDialoglnurldevice.data.title == ''"
type="submit"
>Create lnurldevice
@@ -406,390 +498,27 @@
color="red"
text-color="white"
icon="error"
- >{% raw %}{{ wsMessage }}{% endraw %}
+ {% raw %}{{ wsMessage }}{% endraw %}
+
{% raw %}{{ wsMessage }}{% endraw %}
+ Close
{% endblock %} {% block scripts %} {{ window_vars(user) }}
-
-
+
{% endblock %}
diff --git a/views.py b/views.py
index 56360f8..7564fc4 100644
--- a/views.py
+++ b/views.py
@@ -1,16 +1,25 @@
+import base64
from http import HTTPStatus
+from urllib.parse import parse_qs, urlparse
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import HTMLResponse
-from fastapi.templating import Jinja2Templates
-from lnbits.core.crud import get_standalone_payment
+from lnbits.core.crud import (
+ get_installed_extensions,
+ get_standalone_payment,
+ get_wallet,
+)
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from lnbits.helpers import template_renderer
+from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
+from lnurl import decode as lnurl_decode
+from lnurl import encode as lnurl_encode
+from loguru import logger
-from .crud import get_lnurldevice, get_lnurldevicepayment
+from .crud import get_lnurldevice, get_lnurldevicepayment, get_recent_lnurldevicepayment
+from .helpers import xor_decrypt
-templates = Jinja2Templates(directory="templates")
lnurldevice_generic_router = APIRouter()
@@ -26,6 +35,91 @@ async def index(request: Request, user: User = Depends(check_user_exists)):
)
+@lnurldevice_generic_router.get("/atm", response_class=HTMLResponse)
+async def atmpage(request: Request, lightning: str):
+ # Debug log for the incoming lightning request
+ logger.debug(lightning)
+
+ # Decode the lightning URL
+ url = str(lnurl_decode(lightning))
+ if not url:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND, detail="Unable to decode lnurl."
+ )
+
+ # Parse the URL to extract device ID and query parameters
+ parsed_url = urlparse(url)
+ device_id = parsed_url.path.split("/")[-1]
+ device = await get_lnurldevice(device_id, request)
+ if not device:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND, detail="Unable to find device."
+ )
+
+ # Extract and validate the 'p' parameter
+ p = parse_qs(parsed_url.query).get("p", [None])[0]
+ if p is None:
+ raise HTTPException(
+ status_code=HTTPStatus.BAD_REQUEST, detail="Missing 'p' parameter."
+ )
+ # Adjust for base64 padding if necessary
+ p += "=" * (-len(p) % 4)
+
+ # Decode and decrypt the 'p' parameter
+ try:
+ data = base64.urlsafe_b64decode(p)
+ decrypted = xor_decrypt(device.key.encode(), data)
+ except Exception as exc:
+ raise HTTPException(
+ status_code=HTTPStatus.BAD_REQUEST, detail=f"{exc!s}"
+ ) from exc
+
+ # Determine the price in msat
+ if device.currency != "sat":
+ price_msat = await fiat_amount_as_satoshis(decrypted[1] / 100, device.currency)
+ else:
+ price_msat = decrypted[1]
+
+ # Check wallet and user access
+ wallet = await get_wallet(device.wallet)
+ if not wallet:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found."
+ )
+
+ # check if boltz payouts is enabled but also check the boltz extension is enabled
+ access = False
+ if device.extra == "boltz":
+ installed_extensions = await get_installed_extensions(active=True)
+ for extension in installed_extensions:
+ if extension.id == "boltz" and extension.active:
+ access = True
+ logger.debug(access)
+
+ # Attempt to get recent payment information
+ recent_pay_attempt = await get_recent_lnurldevicepayment(p)
+
+ # Render the response template
+ return lnurldevice_renderer().TemplateResponse(
+ "lnurldevice/atm.html",
+ {
+ "request": request,
+ "lnurl": lightning,
+ "amount": int(((int(price_msat) / 100) * device.profit) + int(price_msat)),
+ "device_id": device.id,
+ "boltz": True if access else False,
+ "p": p,
+ "recentpay": recent_pay_attempt.id if recent_pay_attempt else False,
+ "used": (
+ True
+ if recent_pay_attempt
+ and recent_pay_attempt.payload == recent_pay_attempt.payhash
+ else False
+ ),
+ },
+ )
+
+
@lnurldevice_generic_router.get(
"/{paymentid}", name="lnurldevice.displaypin", response_class=HTMLResponse
)
@@ -54,3 +148,42 @@ async def displaypin(request: Request, paymentid: str):
"lnurldevice/error.html",
{"request": request, "pin": "filler", "not_paid": True},
)
+
+
+@lnurldevice_generic_router.get("/print/{payment_id}", response_class=HTMLResponse)
+async def print_receipt(request: Request, payment_id):
+ lnurldevicepayment = await get_lnurldevicepayment(payment_id)
+ if not lnurldevicepayment:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND, detail="Payment link does not exist."
+ )
+ device = await get_lnurldevice(lnurldevicepayment.deviceid, request)
+ if not device:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND, detail="Unable to find device."
+ )
+
+ lnurl = lnurl_encode(
+ str(
+ request.url_for(
+ "lnurldevice.lnurl_v1_params", device_id=lnurldevicepayment.deviceid
+ )
+ )
+ + "?atm=1&p="
+ + lnurldevicepayment.payload
+ )
+ logger.debug(lnurl)
+ return lnurldevice_renderer().TemplateResponse(
+ "lnurldevice/atm_receipt.html",
+ {
+ "request": request,
+ "id": lnurldevicepayment.id,
+ "deviceid": lnurldevicepayment.deviceid,
+ "devicename": device.title,
+ "payhash": lnurldevicepayment.payhash,
+ "payload": lnurldevicepayment.payload,
+ "sats": lnurldevicepayment.sats,
+ "timestamp": lnurldevicepayment.timestamp,
+ "lnurl": lnurl,
+ },
+ )
diff --git a/views_api.py b/views_api.py
index d096abb..38e1c68 100644
--- a/views_api.py
+++ b/views_api.py
@@ -1,22 +1,34 @@
from http import HTTPStatus
+import httpx
from fastapi import APIRouter, Depends, HTTPException, Request
-from lnbits.core.crud import get_user
+from lnbits.core.crud import get_user, get_wallet
from lnbits.core.models import WalletTypeInfo
+from lnbits.core.services import pay_invoice
+from lnbits.core.views.api import api_lnurlscan
from lnbits.decorators import (
+ check_user_extension_access,
get_key_type,
require_admin_key,
)
+from lnbits.settings import settings
from lnbits.utils.exchange_rates import currencies
+from lnurl import encode as lnurl_encode
+from loguru import logger
from .crud import (
create_lnurldevice,
+ delete_atm_payment_link,
delete_lnurldevice,
get_lnurldevice,
+ get_lnurldevicepayment,
+ get_lnurldevicepayments,
get_lnurldevices,
update_lnurldevice,
+ update_lnurldevicepayment,
)
-from .models import CreateLnurldevice
+from .helpers import register_atm_payment
+from .models import CreateLnurldevice, Lnurlencode
lnurldevice_api_router = APIRouter()
@@ -74,3 +86,180 @@ async def api_lnurldevice_delete(req: Request, lnurldevice_id: str):
)
await delete_lnurldevice(lnurldevice_id)
+
+
+#########ATM API#########
+
+
+@lnurldevice_api_router.get("/api/v1/atm")
+async def api_atm_payments_retrieve(
+ req: Request, wallet: WalletTypeInfo = Depends(get_key_type)
+):
+ user = await get_user(wallet.wallet.user)
+ assert user, "Lnurldevice cannot retrieve user"
+ lnurldevices = await get_lnurldevices(user.wallet_ids, req)
+ deviceids = []
+ for lnurldevice in lnurldevices:
+ if lnurldevice.device == "atm":
+ deviceids.append(lnurldevice.id)
+ return await get_lnurldevicepayments(deviceids)
+
+
+@lnurldevice_api_router.post(
+ "/api/v1/lnurlencode", dependencies=[Depends(get_key_type)]
+)
+async def api_lnurlencode(data: Lnurlencode):
+ lnurl = lnurl_encode(data.url)
+ logger.debug(lnurl)
+ if not lnurl:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND, detail="Lnurl could not be encoded."
+ )
+ return lnurl
+
+
+@lnurldevice_api_router.delete(
+ "/api/v1/atm/{atm_id}", dependencies=[Depends(require_admin_key)]
+)
+async def api_atm_payment_delete(atm_id: str):
+ lnurldevice = await get_lnurldevicepayment(atm_id)
+ if not lnurldevice:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND, detail="ATM payment does not exist."
+ )
+
+ await delete_atm_payment_link(atm_id)
+
+
+@lnurldevice_api_router.get("/api/v1/ln/{lnurldevice_id}/{p}/{ln}")
+async def get_lnurldevice_payment_lightning(
+ req: Request, lnurldevice_id: str, p: str, ln: str
+) -> str:
+ """
+ Handle Lightning payments for atms via invoice, lnaddress, lnurlp.
+ """
+ ln = ln.strip().lower()
+
+ lnurldevice = await get_lnurldevice(lnurldevice_id, req)
+ if not lnurldevice:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND, detail="lnurldevice does not exist"
+ )
+
+ wallet = await get_wallet(lnurldevice.wallet)
+ if not wallet:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND,
+ detail="Wallet does not exist connected to atm, payment could not be made",
+ )
+ lnurldevicepayment, price_msat = await register_atm_payment(lnurldevice, p)
+ if not lnurldevicepayment:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND, detail="Payment already claimed."
+ )
+
+ # If its an invoice check its a legit invoice
+ if ln[:4] == "lnbc":
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND, detail="Invoices not supported."
+ )
+
+ # If its an lnaddress or lnurlp get the request from callback
+ elif ln[:5] == "lnurl" or "@" in ln and "." in ln.split("@")[-1]:
+ data = await api_lnurlscan(ln)
+ logger.debug(data)
+ if data.get("status") == "ERROR":
+ raise HTTPException(
+ status_code=HTTPStatus.BAD_REQUEST, detail=data.get("reason")
+ )
+ async with httpx.AsyncClient() as client:
+ response = await client.get(
+ url=f"{data['callback']}?amount={lnurldevicepayment.sats * 1000}"
+ )
+ if response.status_code != 200:
+ raise HTTPException(
+ status_code=HTTPStatus.BAD_REQUEST,
+ detail="Could not get callback from lnurl",
+ )
+ ln = response.json()["pr"]
+
+ # If ln is gibberish, return an error
+ else:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND,
+ detail="""
+ Wrong format for payment, could not be made.
+ Use LNaddress or LNURLp
+ """,
+ )
+
+ # Finally log the payment and make the payment
+ try:
+ lnurldevicepayment, price_msat = await register_atm_payment(lnurldevice, p)
+ assert lnurldevicepayment
+ lnurldevicepayment.payhash = lnurldevicepayment.payload
+ await update_lnurldevicepayment(lnurldevicepayment)
+ if ln[:4] == "lnbc":
+ await pay_invoice(
+ wallet_id=lnurldevice.wallet,
+ payment_request=ln,
+ max_sat=price_msat,
+ extra={"tag": "lnurldevice", "id": lnurldevicepayment.id},
+ )
+ except Exception as exc:
+ raise HTTPException(
+ status_code=HTTPStatus.BAD_REQUEST, detail=f"{exc!s}"
+ ) from exc
+
+ return lnurldevicepayment.id
+
+
+@lnurldevice_api_router.get(
+ "'/api/v1/boltz/{lnurldevice_id}/{payload}/{onchain_liquid}/{address}"
+)
+async def get_lnurldevice_payment_boltz(
+ req: Request, lnurldevice_id: str, payload: str, onchain_liquid: str, address: str
+):
+ """
+ Handle Boltz payments for atms.
+ """
+ lnurldevice = await get_lnurldevice(lnurldevice_id, req)
+ if not lnurldevice:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND, detail="lnurldevice does not exist"
+ )
+
+ lnurldevicepayment, price_msat = await register_atm_payment(lnurldevice, payload)
+ assert lnurldevicepayment
+ if lnurldevicepayment == "ERROR":
+ return lnurldevicepayment
+
+ wallet = await get_wallet(lnurldevice.wallet)
+ if not wallet:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND,
+ detail="Wallet does not exist connected to atm, payment could not be made",
+ )
+ access = await check_user_extension_access(wallet.user, "boltz")
+ if not access.success:
+ return {"status": "ERROR", "reason": "Boltz not enabled"}
+
+ data = {
+ "wallet": lnurldevice.wallet,
+ "asset": onchain_liquid.replace("-", "/"),
+ "amount": price_msat,
+ "instant_settlement": True,
+ "onchain_address": address,
+ }
+ try:
+ lnurldevicepayment.payload = payload
+ await update_lnurldevicepayment(lnurldevicepayment)
+ async with httpx.AsyncClient() as client:
+ response = await client.post(
+ url=f"http://{settings.host}:{settings.port}/boltz/api/v1/swap/reverse",
+ headers={"X-API-KEY": wallet.adminkey},
+ data=data,
+ )
+ return response.json()
+ except Exception as exc:
+ return {"status": "ERROR", "reason": str(exc)}
diff --git a/views_lnurl.py b/views_lnurl.py
index ae1a011..3453983 100644
--- a/views_lnurl.py
+++ b/views_lnurl.py
@@ -1,63 +1,24 @@
import base64
-import hmac
from http import HTTPStatus
-from io import BytesIO
import bolt11
-from embit import compact
from fastapi import APIRouter, HTTPException, Query, Request
-from lnbits.core.services import create_invoice, pay_invoice
+from lnbits.core.crud import get_wallet
+from lnbits.core.services import create_invoice
+from lnbits.core.views.api import pay_invoice
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
from .crud import (
create_lnurldevicepayment,
get_lnurldevice,
get_lnurldevicepayment,
- get_lnurldevicepayment_by_p,
update_lnurldevicepayment,
)
+from .helpers import register_atm_payment, xor_decrypt
lnurldevice_lnurl_router = APIRouter()
-def xor_decrypt(key, blob):
- s = BytesIO(blob)
- variant = s.read(1)[0]
- if variant != 1:
- raise RuntimeError("Not implemented")
- # reading nonce
- val = s.read(1)[0]
- nonce = s.read(val)
- if len(nonce) != val:
- raise RuntimeError("Missing nonce bytes")
- if val < 8:
- raise RuntimeError("Nonce is too short")
-
- # reading payload
- val = s.read(1)[0]
- payload = s.read(val)
- if len(payload) > 32:
- raise RuntimeError("Payload is too long for this encryption method")
- if len(payload) != val:
- 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 str(pin), amount_in_cent
-
-
@lnurldevice_lnurl_router.get(
"/api/v1/lnurl/{device_id}",
status_code=HTTPStatus.OK,
@@ -126,18 +87,18 @@ async def lnurl_params(
# Check they're not trying to trick the switch!
check = False
- if device.switches:
- for switch in device.switches:
+ if device.extra and "atm" not in device.extra:
+ for extra in device.extra:
if (
- switch.pin == int(pin)
- and switch.duration == int(duration)
- and bool(switch.variable) == bool(variable)
- and bool(switch.comment) == bool(comment)
+ extra.pin == int(pin)
+ and extra.duration == int(duration)
+ and bool(extra.variable) == bool(variable)
+ and bool(extra.comment) == bool(comment)
):
check = True
continue
if not check:
- return {"status": "ERROR", "reason": "Switch params wrong"}
+ return {"status": "ERROR", "reason": "Extra params wrong"}
lnurldevicepayment = await create_lnurldevicepayment(
deviceid=device.id,
@@ -154,9 +115,9 @@ async def lnurl_params(
request.url_for(
"lnurldevice.lnurl_callback",
paymentid=lnurldevicepayment.id,
- variable=variable,
)
- ),
+ )
+ + f"?variable={variable}",
"minSendable": price_msat,
"maxSendable": price_msat,
"metadata": device.lnurlpay_metadata,
@@ -180,43 +141,25 @@ async def lnurl_params(
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)
- new_lnurldevicepayment = await get_lnurldevicepayment_by_p(p)
- if new_lnurldevicepayment:
- if new_lnurldevicepayment.payload == new_lnurldevicepayment.payhash:
- return {"status": "ERROR", "reason": "Payment already claimed"}
- try:
- lnurldevicepayment = await create_lnurldevicepayment(
- deviceid=device.id,
- payload=p,
- sats=price_msat * 1000,
- pin=pin,
- payhash="payment_hash",
- )
- except Exception:
- return {"status": "ERROR", "reason": "Could not create ATM payment."}
+ lnurldevicepayment, price_msat = await register_atm_payment(device, p)
if not lnurldevicepayment:
return {"status": "ERROR", "reason": "Could not create ATM payment."}
return {
"tag": "withdrawRequest",
"callback": str(
request.url_for(
- "lnurldevice.lnurl_callback",
- paymentid=lnurldevicepayment.id,
- variable=None,
+ "lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id
)
),
"k1": p,
- "minWithdrawable": price_msat * 1000,
- "maxWithdrawable": price_msat * 1000,
- "defaultDescription": f"{device.title} - pin: {lnurldevicepayment.pin}",
+ "minWithdrawable": price_msat,
+ "maxWithdrawable": price_msat,
+ "defaultDescription": f"{device.title} ID: {lnurldevicepayment.id}",
}
- price_msat = int(price_msat * ((device.profit / 100) + 1) / 1000)
+ price_msat = int(price_msat * ((device.profit / 100) + 1))
lnurldevicepayment = await create_lnurldevicepayment(
deviceid=device.id,
@@ -231,9 +174,7 @@ async def lnurl_params(
"tag": "payRequest",
"callback": str(
request.url_for(
- "lnurldevice.lnurl_callback",
- paymentid=lnurldevicepayment.id,
- variable=None,
+ "lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id
)
),
"minSendable": price_msat * 1000,
@@ -243,14 +184,14 @@ async def lnurl_params(
@lnurldevice_lnurl_router.get(
- "/api/v1/lnurl/cb/{paymentid}/{variable}",
+ "/api/v1/lnurl/cb/{paymentid}",
status_code=HTTPStatus.OK,
name="lnurldevice.lnurl_callback",
)
async def lnurl_callback(
request: Request,
paymentid: str,
- variable: str,
+ variable: str = Query(None),
amount: int = Query(None),
comment: str = Query(None),
pr: str = Query(None),
@@ -278,14 +219,21 @@ async def lnurl_callback(
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not valid payment request"
)
+ wallet = await get_wallet(device.wallet)
+ assert wallet
+ if wallet.balance_msat < (int(lnurldevicepayment.sats / 1000) + 100):
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN, detail="Not enough funds"
+ )
else:
if lnurldevicepayment.payload != k1:
return {"status": "ERROR", "reason": "Bad K1"}
if lnurldevicepayment.payhash != "payment_hash":
return {"status": "ERROR", "reason": "Payment already claimed"}
try:
+ lnurldevicepayment.payhash = lnurldevicepayment.payload
lnurldevicepayment_updated = await update_lnurldevicepayment(
- lnurldevicepayment_id=paymentid, payhash=lnurldevicepayment.payload
+ lnurldevicepayment
)
assert lnurldevicepayment_updated
await pay_invoice(
@@ -294,11 +242,15 @@ async def lnurl_callback(
max_sat=int(lnurldevicepayment_updated.sats / 1000),
extra={"tag": "withdraw"},
)
- except Exception:
- return {
- "status": "ERROR",
- "reason": "Payment failed, use a different wallet.",
- }
+ except Exception as exc:
+ lnurldevicepayment.payhash = "payment_hash"
+ lnurldevicepayment_updated = await update_lnurldevicepayment(
+ lnurldevicepayment
+ )
+ assert lnurldevicepayment_updated
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN, detail="Failed to make payment"
+ ) from exc
return {"status": "OK"}
if device.device == "switch":
if not amount:
@@ -307,10 +259,7 @@ async def lnurl_callback(
payment_hash, payment_request = await create_invoice(
wallet_id=device.wallet,
amount=int(amount / 1000),
- memo=(
- f"{device.id} pin {lnurldevicepayment.pin} "
- f"({lnurldevicepayment.payload} ms)"
- ),
+ memo=f"{device.title} ({lnurldevicepayment.payload} ms)",
unhashed_description=device.lnurlpay_metadata.encode(),
extra={
"tag": "Switch",
@@ -322,9 +271,7 @@ async def lnurl_callback(
},
)
- lnurldevicepayment = await update_lnurldevicepayment(
- lnurldevicepayment_id=paymentid, payhash=payment_hash
- )
+ lnurldevicepayment = await update_lnurldevicepayment(lnurldevicepayment)
resp = {
"pr": payment_request,
"successAction": {
@@ -343,9 +290,8 @@ async def lnurl_callback(
unhashed_description=device.lnurlpay_metadata.encode(),
extra={"tag": "PoS"},
)
- lnurldevicepayment = await update_lnurldevicepayment(
- lnurldevicepayment_id=paymentid, payhash=payment_hash
- )
+ lnurldevicepayment.payhash = payment_hash
+ lnurldevicepayment = await update_lnurldevicepayment(lnurldevicepayment)
return {
"pr": payment_request,