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 +
+
+ + + + {% 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
+ 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,