diff --git a/crud.py b/crud.py index 19d9c19..3c3b895 100644 --- a/crud.py +++ b/crud.py @@ -11,21 +11,15 @@ async def create_lnpos(data: CreateLnpos) -> Lnpos: - if data.device == "pos" or data.device == "atm": - lnpos_id = shortuuid.uuid()[:5] - else: - lnpos_id = urlsafe_short_hash() - + lnpos_id = shortuuid.uuid()[:5] lnpos_key = urlsafe_short_hash() - device = Lnpos( id=lnpos_id, key=lnpos_key, title=data.title, wallet=data.wallet, - profit=data.profit, - currency=data.currency, - device=data.device, + profit=data.profit or 0.0, + currency=data.currency or "sat", ) await db.insert("lnpos.lnpos", device) @@ -80,6 +74,7 @@ async def get_lnpos_payment( return await db.fetchone( "SELECT * FROM lnpos.lnpos_payment WHERE id = :id", {"id": lnpos_payment_id}, + LnposPayment, ) @@ -104,6 +99,7 @@ async def get_lnpos_payment_by_payhash( return await db.fetchone( "SELECT * FROM lnpos.lnpos_payment WHERE payhash = :payhash", {"payhash": payhash}, + LnposPayment, ) diff --git a/migrations.py b/migrations.py index ca11cb4..f4543bd 100644 --- a/migrations.py +++ b/migrations.py @@ -15,7 +15,6 @@ async def m001_initial(db): title TEXT NOT NULL, wallet TEXT NOT NULL, currency TEXT NOT NULL, - device TEXT NOT NULL, profit FLOAT NOT NULL, timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} ); diff --git a/models.py b/models.py index 385b137..cd6cbc3 100644 --- a/models.py +++ b/models.py @@ -1,4 +1,5 @@ import json +from typing import Optional from lnurl.types import LnurlPayMetadata from pydantic import BaseModel @@ -7,9 +8,8 @@ class CreateLnpos(BaseModel): title: str wallet: str - currency: str - device: str - profit: float + currency: Optional[str] = "sat" + profit: Optional[float] = None class Lnpos(BaseModel): @@ -19,7 +19,6 @@ class Lnpos(BaseModel): wallet: str profit: float currency: str - device: str @property def lnurlpay_metadata(self) -> LnurlPayMetadata: @@ -33,7 +32,3 @@ class LnposPayment(BaseModel): payload: str pin: int sats: int - - -class Lnurlencode(BaseModel): - url: str diff --git a/static/js/index.js b/static/js/index.js index ad71493..1f2fcac 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -7,10 +7,8 @@ window.app = Vue.createApp({ location: window.location.hostname, filter: '', currency: 'USD', - lnurlValue: '', + deviceString: '', lnposs: [], - atmLinks: [], - lnpossObj: [], lnposTable: { columns: [ { @@ -48,7 +46,6 @@ window.app = Vue.createApp({ rowsPerPage: 10 } }, - passedlnurldevice: {}, settingsDialog: { show: false, data: {} @@ -56,26 +53,13 @@ window.app = Vue.createApp({ formDialog: { show: false, data: {} - }, - formDialog: { - show: false, - data: { - lnurl_toggle: false, - show_message: false, - show_ack: false, - show_price: 'None', - device: 'pos', - profit: 1, - amount: 1, - title: '' - } } } }, methods: { cancelLnpos() { - self.formDialog.show = false - self.clearFormDialog() + this.formDialog.show = false + this.clearFormDialog() }, closeFormDialog() { this.clearFormDialog() @@ -84,17 +68,17 @@ window.app = Vue.createApp({ } }, sendFormData() { - if (!self.formDialog.data.profit) { - self.formDialog.data.profit = 0 + if (!this.formDialog.data.profit) { + this.formDialog.data.profit = 0 } - if (self.formDialog.data.id) { - this.updateLnpos(self.g.user.wallets[0].adminkey, self.formDialog.data) + if (this.formDialog.data.id) { + this.updateLnpos(this.g.user.wallets[0].adminkey, this.formDialog.data) } else { - this.createLnpos(self.g.user.wallets[0].adminkey, self.formDialog.data) + this.createLnpos(this.g.user.wallets[0].adminkey, this.formDialog.data) } }, - createLnpos: function (wallet, data) { + createLnpos(wallet, data) { const updatedData = {} for (const property in data) { if (data[property]) { @@ -102,88 +86,71 @@ window.app = Vue.createApp({ } } LNbits.api - .request('POST', '/lnurldevice/api/v1/lnurlpos', wallet, updatedData) - .then(function (response) { - self.lnposs.push(response.data) - self.formDialog.show = false - self.clearFormDialog() - }) - .catch(function (error) { - LNbits.utils.notifyApiError(error) + .request('POST', '/lnpos/api/v1', wallet, updatedData) + .then(response => { + this.lnposs.push(response.data) + this.formDialog.show = false + this.clearFormDialog() }) + .catch(LNbits.utils.notifyApiError) }, getLnposs() { LNbits.api - .request( - 'GET', - '/lnurldevice/api/v1/lnurlpos', - self.g.user.wallets[0].adminkey - ) - .then(function (response) { + .request('GET', '/lnpos/api/v1', this.g.user.wallets[0].adminkey) + .then(response => { if (response.data) { - self.lnposs = response.data.map(maplnurldevice) + this.lnposs = response.data } }) - .catch(function (error) { - LNbits.utils.notifyApiError(error) - }) + .catch(LNbits.utils.notifyApiError) }, - getLnpos: function (lnurldevice_id) { + getLnpos: lnpos_id => { LNbits.api .request( 'GET', - '/lnurldevice/api/v1/lnurlpos/' + lnurldevice_id, - self.g.user.wallets[0].adminkey + '/lnpos/api/v1/' + lnpos_id, + this.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) + .then(response => { + localStorage.setItem('lnpos', JSON.stringify(response.data)) + localStorage.setItem('inkey', this.g.user.wallets[0].inkey) }) + .catch(LNbits.utils.notifyApiError) }, - deleteLnpos: function (lnurldeviceId) { - const link = _.findWhere(this.lnposs, {id: lnurldeviceId}) + deleteLnpos(lnposId) { LNbits.utils .confirmDialog('Are you sure you want to delete this pay link?') - .onOk(function () { + .onOk(() => { LNbits.api .request( 'DELETE', - '/lnurldevice/api/v1/lnurlpos/' + lnurldeviceId, - self.g.user.wallets[0].adminkey + '/lnpos/api/v1/' + lnposId, + this.g.user.wallets[0].adminkey ) - .then(function (response) { - self.lnposs = _.reject(self.lnposs, function (obj) { - return obj.id === lnurldeviceId + .then(response => { + this.lnposs = _.reject(this.lnposs, obj => { + return obj.id === lnposId }) }) - .catch(function (error) { - LNbits.utils.notifyApiError(error) - }) + .catch(LNbits.utils.notifyApiError) }) }, - openUpdateLnpos: function (lnurldeviceId) { - const lnurldevice = _.findWhere(this.lnposs, { - id: lnurldeviceId + openUpdateLnpos(lnposId) { + const lnpos = _.findWhere(this.lnposs, { + id: lnposId }) - self.formDialog.data = _.clone(lnurldevice._data) - if (lnurldevice.device == 'atm' && lnurldevice.extra == 'boltz') { - self.boltzToggleState = true - } else { - self.boltzToggleState = false - } - self.formDialog.show = true + this.formDialog.data = _.clone(lnpos) + this.formDialog.show = true }, - openSettings: function (lnurldeviceId) { - const lnurldevice = _.findWhere(this.lnposs, { - id: lnurldeviceId + openSettings(lnposId) { + const lnpos = _.findWhere(this.lnposs, { + id: lnposId }) - self.settingsDialog.data = _.clone(lnurldevice._data) - self.settingsDialog.show = true + this.deviceString = `${this.protocol}//${this.location}/lnpos/api/v1/lnurl/${lnpos.id},${lnpos.key},${lnpos.currency}` + this.settingsDialog.data = _.clone(lnpos) + this.settingsDialog.show = true }, - updateLnpos: function (wallet, data) { + updateLnpos(wallet, data) { const updatedData = {} for (const property in data) { if (data[property]) { @@ -192,23 +159,16 @@ window.app = Vue.createApp({ } LNbits.api - .request( - 'PUT', - '/lnurldevice/api/v1/lnurlpos/' + updatedData.id, - wallet, - updatedData - ) - .then(function (response) { - self.lnposs = _.reject(self.lnposs, function (obj) { + .request('PUT', '/lnpos/api/v1/' + updatedData.id, wallet, updatedData) + .then(response => { + this.lnposs = _.reject(this.lnposs, obj => { return obj.id === updatedData.id }) - self.lnposs.push(response.data) - self.formDialog.show = false - self.clearFormDialog() - }) - .catch(function (error) { - LNbits.utils.notifyApiError(error) + this.lnposs.push(response.data) + this.formDialog.show = false + this.clearFormDialog() }) + .catch(LNbits.utils.notifyApiError) }, clearFormDialog() { this.formDialog.data = { @@ -220,7 +180,7 @@ window.app = Vue.createApp({ } }, exportCSV() { - LNbits.utils.exportCSV(self.lnposTable.columns, this.lnposs) + LNbits.utils.exportCSV(this.lnposTable.columns, this.lnposs) } }, created() { @@ -228,10 +188,8 @@ window.app = Vue.createApp({ LNbits.api .request('GET', '/api/v1/currencies') .then(response => { - this.currency = ['sat', 'USD', ...response.data] - }) - .catch(err => { - LNbits.utils.notifyApiError(err) + this.currency = response.data }) + .catch(LNbits.utils.notifyApiError) } }) diff --git a/templates/lnpos/error.html b/templates/lnpos/error.html index ca5b6db..a50094d 100644 --- a/templates/lnpos/error.html +++ b/templates/lnpos/error.html @@ -17,15 +17,12 @@

LNURL-pay not paid

- - {% endblock %} {% block scripts %} - - - - {% endblock %} +{% endblock %} {% block scripts %} + +{% endblock %} diff --git a/templates/lnpos/index.html b/templates/lnpos/index.html index 6729ede..8bd5343 100644 --- a/templates/lnpos/index.html +++ b/templates/lnpos/index.html @@ -128,6 +128,27 @@
{{SITE_TITLE}} LNPoS Extension
+ + +
Device string
+ + + Click to copy URL + +
+
+ diff --git a/templates/lnpos/paid.html b/templates/lnpos/paid.html index c185ecc..e8991c5 100644 --- a/templates/lnpos/paid.html +++ b/templates/lnpos/paid.html @@ -10,18 +10,12 @@

{{ pin }}

- - {% endblock %} {% block scripts %} - - - - {% endblock %} +{% endblock %} {% block scripts %} + +{% endblock %} diff --git a/views_api.py b/views_api.py index 38eb106..da9229b 100644 --- a/views_api.py +++ b/views_api.py @@ -1,52 +1,31 @@ from http import HTTPStatus -import bolt11 -import httpx from fastapi import APIRouter, Depends, HTTPException -from lnbits.core.crud import get_user, get_wallet +from lnbits.core.crud import get_user 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, require_admin_key, require_invoice_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_lnpos, - delete_atm_payment_link, delete_lnpos, get_lnpos, - get_lnpos_payment, - get_lnpos_payments, get_lnposs, update_lnpos, - update_lnpos_payment, ) -from .helpers import register_atm_payment -from .models import CreateLnpos, Lnurlencode +from .models import CreateLnpos lnpos_api_router = APIRouter() -@lnpos_api_router.get("/api/v1/currencies") -async def api_list_currencies_available(): - return list(currencies.keys()) - - -@lnpos_api_router.post("/api/v1/lnurlpos", dependencies=[Depends(require_admin_key)]) +@lnpos_api_router.post("/api/v1", dependencies=[Depends(require_admin_key)]) async def api_lnpos_create(data: CreateLnpos): return await create_lnpos(data) -@lnpos_api_router.put( - "/api/v1/lnurlpos/{lnpos_id}", dependencies=[Depends(require_admin_key)] -) +@lnpos_api_router.put("/api/v1/{lnpos_id}", dependencies=[Depends(require_admin_key)]) async def api_lnpos_update(data: CreateLnpos, lnpos_id: str): lnpos = await get_lnpos(lnpos_id) if not lnpos: @@ -58,241 +37,31 @@ async def api_lnpos_update(data: CreateLnpos, lnpos_id: str): return await update_lnpos(lnpos) -@lnpos_api_router.get("/api/v1/lnurlpos") -async def api_lnposs_retrieve(wallet: WalletTypeInfo = Depends(require_invoice_key)): +@lnpos_api_router.get("/api/v1") +async def api_lnposs_get(wallet: WalletTypeInfo = Depends(require_invoice_key)): user = await get_user(wallet.wallet.user) assert user, "Lnpos cannot retrieve user" return await get_lnposs(user.wallet_ids) -@lnpos_api_router.get( - "/api/v1/lnurlpos/{lnpos_id}", dependencies=[Depends(require_invoice_key)] -) -async def api_lnpos_retrieve(lnpos_id: str): +@lnpos_api_router.get("/api/v1/{lnpos_id}", dependencies=[Depends(require_invoice_key)]) +async def api_lnpos_get(lnpos_id: str): lnpos = await get_lnpos(lnpos_id) if not lnpos: raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="lnpos does not exist" + status_code=HTTPStatus.NOT_FOUND, detail="LNPoS does not exist" ) return lnpos @lnpos_api_router.delete( - "/api/v1/lnurlpos/{lnpos_id}", dependencies=[Depends(require_admin_key)] + "/api/v1/{lnpos_id}", dependencies=[Depends(require_admin_key)] ) async def api_lnpos_delete(lnpos_id: str): lnpos = await get_lnpos(lnpos_id) if not lnpos: raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Lnpos does not exist." + status_code=HTTPStatus.NOT_FOUND, detail="LNPoS does not exist." ) await delete_lnpos(lnpos_id) - - -#########ATM API######### - - -@lnpos_api_router.get("/api/v1/atm") -async def api_atm_payments_retrieve( - wallet: WalletTypeInfo = Depends(require_invoice_key), -): - user = await get_user(wallet.wallet.user) - assert user, "Lnpos cannot retrieve user" - lnposs = await get_lnposs(user.wallet_ids) - deviceids = [] - for lnpos in lnposs: - if lnpos.device == "atm": - deviceids.append(lnpos.id) - return await get_lnpos_payments(deviceids) - - -@lnpos_api_router.post( - "/api/v1/lnurlencode", dependencies=[Depends(require_invoice_key)] -) -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 - - -@lnpos_api_router.delete( - "/api/v1/atm/{atm_id}", dependencies=[Depends(require_admin_key)] -) -async def api_atm_payment_delete(atm_id: str): - lnpos = await get_lnpos_payment(atm_id) - if not lnpos: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="ATM payment does not exist." - ) - - await delete_atm_payment_link(atm_id) - - -@lnpos_api_router.get("/api/v1/ln/{lnpos_id}/{p}/{ln}") -async def get_lnpos_payment_lightning(lnpos_id: str, p: str, ln: str) -> str: - """ - Handle Lightning payments for atms via invoice, lnaddress, lnurlp. - """ - ln = ln.strip().lower() - - lnpos = await get_lnpos(lnpos_id) - if not lnpos: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="lnpos does not exist" - ) - - wallet = await get_wallet(lnpos.wallet) - if not wallet: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, - detail="Wallet does not exist connected to atm, payment could not be made", - ) - lnpos_payment, price_msat = await register_atm_payment(lnpos, p) - if not lnpos_payment: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Payment already claimed." - ) - - # 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={lnpos_payment.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 just an invoice - elif ln[:4] == "lnbc": - ln = ln - - # 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 - """, - ) - - # If its an invoice check its a legit invoice - if ln[:4] == "lnbc": - invoice = bolt11.decode(ln) - assert invoice.amount_msat, "Amountless invoices are not allowed" - if not invoice.payment_hash: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, detail="Not valid payment request" - ) - if not invoice.payment_hash: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, detail="Not valid payment request" - ) - if int(invoice.amount_msat / 1000) != lnpos_payment.sats: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, - detail="Request is not the same as withdraw amount", - ) - - # Finally log the payment and make the payment - try: - lnpos_payment, price_msat = await register_atm_payment(lnpos, p) - assert lnpos_payment - lnpos_payment.payment_hash = lnpos_payment.payload - await update_lnpos_payment(lnpos_payment) - if ln[:4] == "lnbc": - await pay_invoice( - wallet_id=lnpos.wallet, - payment_request=ln, - max_sat=price_msat, - extra={"tag": "lnpos", "id": lnpos_payment.id}, - ) - except Exception as exc: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, detail=f"{exc!s}" - ) from exc - - return lnpos_payment.id - - -@lnpos_api_router.get("/api/v1/boltz/{lnpos_id}/{payload}/{onchain_liquid}/{address}") -async def get_lnpos_payment_boltz( - lnpos_id: str, payload: str, onchain_liquid: str, address: str -): - """ - Handle Boltz payments for atms. - """ - lnpos = await get_lnpos(lnpos_id) - if not lnpos: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="lnpos does not exist" - ) - - lnpos_payment, _ = await register_atm_payment(lnpos, payload) - assert lnpos_payment - if lnpos_payment == "ERROR": - return lnpos_payment - if lnpos_payment.payload == lnpos_payment.payment_hash: - return {"status": "ERROR", "reason": "Payment already claimed."} - if lnpos_payment.payment_hash == "pending": - return { - "status": "ERROR", - "reason": "Pending. If you are unable to withdraw contact vendor", - } - wallet = await get_wallet(lnpos.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": lnpos.wallet, - "asset": onchain_liquid.replace("temp", "/"), - "amount": lnpos_payment.sats, - "direction": "send", - "instant_settlement": True, - "onchain_address": address, - "feerate": False, - "feerate_value": 0, - } - - try: - lnpos_payment.payload = payload - lnpos_payment.payment_hash = "pending" - lnpos_payment_updated = await update_lnpos_payment(lnpos_payment) - assert lnpos_payment_updated - 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}, - json=data, - ) - lnpos_payment.payment_hash = lnpos_payment.payload - lnpos_payment_updated = await update_lnpos_payment(lnpos_payment) - assert lnpos_payment_updated - resp = response.json() - return resp - except Exception as exc: - lnpos_payment.payment_hash = "payment_hash" - lnpos_payment_updated = await update_lnpos_payment(lnpos_payment) - assert lnpos_payment_updated - return {"status": "ERROR", "reason": str(exc)}