From cf94a35eab91ecf7c640072c17071d7eff4ad869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Mon, 16 Dec 2024 10:34:16 +0100 Subject: [PATCH] fix: atm page (#1) --- __init__.py | 2 + static/js/index.js | 33 +++-- templates/fossa/atm.html | 23 ++- templates/fossa/atm_receipt.html | 2 +- templates/fossa/index.html | 4 +- views_api.py | 244 ++----------------------------- views_api_atm.py | 237 ++++++++++++++++++++++++++++++ 7 files changed, 283 insertions(+), 262 deletions(-) create mode 100644 views_api_atm.py diff --git a/__init__.py b/__init__.py index 06a34ae..26f50d8 100644 --- a/__init__.py +++ b/__init__.py @@ -3,12 +3,14 @@ from .crud import db from .views import fossa_generic_router from .views_api import fossa_api_router +from .views_api_atm import fossa_api_atm_router from .views_lnurl import fossa_lnurl_router fossa_ext: APIRouter = APIRouter(prefix="/fossa", tags=["fossa"]) fossa_ext.include_router(fossa_generic_router) fossa_ext.include_router(fossa_api_router) fossa_ext.include_router(fossa_lnurl_router) +fossa_ext.include_router(fossa_api_atm_router) fossa_static_files = [ { diff --git a/static/js/index.js b/static/js/index.js index be994d8..2bb80a7 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -114,7 +114,7 @@ window.app = Vue.createApp({ } } LNbits.api - .request('POST', '/fossa/api/v1', wallet, updatedData) + .request('POST', '/fossa/api/v1/fossa', wallet, updatedData) .then(response => { this.fossa.push(response.data) this.formDialog.show = false @@ -124,7 +124,7 @@ window.app = Vue.createApp({ }, getFossa() { LNbits.api - .request('GET', '/fossa/api/v1', this.g.user.wallets[0].adminkey) + .request('GET', '/fossa/api/v1/fossa', this.g.user.wallets[0].adminkey) .then(response => { if (response.data) { this.fossa = response.data @@ -135,9 +135,9 @@ window.app = Vue.createApp({ getAtmPayments() { LNbits.api .request('GET', '/fossa/api/v1/atm', this.g.user.wallets[0].adminkey) - .then(function (response) { + .then(response => { if (response.data) { - this.atmLinks = response.data.map(mapatmpayments) + this.atmLinks = response.data } }) .catch(LNbits.utils.notifyApiError) @@ -160,17 +160,17 @@ window.app = Vue.createApp({ .catch(LNbits.utils.notifyApiError) }) }, - deleteATMLink(atmId) { + deleteAtmLink(atmId) { LNbits.utils .confirmDialog('Are you sure you want to delete this atm link?') - .onOk(function () { + .onOk(() => { LNbits.api .request( 'DELETE', '/fossa/api/v1/atm/' + atmId, this.g.user.wallets[0].adminkey ) - .then(function (response) { + .then(() => { this.atmLinks = _.reject(this.atmLinks, function (obj) { return obj.id === atmId }) @@ -214,7 +214,12 @@ window.app = Vue.createApp({ } LNbits.api - .request('PUT', '/fossa/api/v1/' + updatedData.id, wallet, updatedData) + .request( + 'PUT', + '/fossa/api/v1/fossa/' + updatedData.id, + wallet, + updatedData + ) .then(response => { this.fossa = _.reject(this.fossa, obj => { return obj.id === updatedData.id @@ -240,18 +245,14 @@ window.app = Vue.createApp({ exportAtmCSV() { LNbits.utils.exportCSV(this.atmTable.columns, this.atmLinks) }, - openATMLink(deviceid, p) { - const url = - this.location + '/fossa/api/v1/lnurl/' + deviceid + '?atm=1&p=' + p - data = { - url: url - } + openAtmLink(deviceid, p) { + const url = `${this.protocol}//${this.location}/fossa/api/v1/lnurl/${deviceid}?atm=1&p=${p}` LNbits.api .request( 'POST', '/fossa/api/v1/lnurlencode', this.g.user.wallets[0].adminkey, - data + {url: url} ) .then(response => { window.open('/fossa/atm?lightning=' + response.data) @@ -261,7 +262,7 @@ window.app = Vue.createApp({ }, created() { this.getFossa() - // this.getatmpayments() + this.getAtmPayments() LNbits.api .request('GET', '/api/v1/currencies') .then(response => { diff --git a/templates/fossa/atm.html b/templates/fossa/atm.html index 64d2584..10416ce 100644 --- a/templates/fossa/atm.html +++ b/templates/fossa/atm.html @@ -24,7 +24,7 @@ @@ -61,13 +61,10 @@
LNURL withdraw
- - - +
@@ -175,7 +172,7 @@

unelevated type="a" target="_blank" - :href="'/lnurldevice/print/' + recentpay" + :href="'/fossa/print/' + recentpay" > @@ -188,7 +185,7 @@

el: '#vue', mixins: [windowMixin], delimiters: ['${', '}'], - data: function () { + data() { return { device_id: '{{device_id}}', qr_value: '{{lnurl}}', @@ -210,7 +207,7 @@

try { const response = await LNbits.api.request( 'GET', - `/lnurldevice/api/v1/ln/${this.device_id}/${this.p}/${this.ln}`, + `/fossa/api/v1/ln/${this.device_id}/${this.p}/${this.ln}`, '' ) console.log(response.data) @@ -241,7 +238,7 @@

try { const response = await LNbits.api.request( 'GET', - `/lnurldevice/api/v1/boltz/${this.device_id}/${this.p}/${this.onchain_liquid}/${this.address}`, + `/fossa/api/v1/boltz/${this.device_id}/${this.p}/${this.onchain_liquid}/${this.address}`, '' ) if (response.data) { @@ -294,7 +291,7 @@

} }, watch: { - tab: function (newVal, oldVal) { + tab(newVal, oldVal) { if (newVal === 'ln') { this.launchFunction() } diff --git a/templates/fossa/atm_receipt.html b/templates/fossa/atm_receipt.html index cfd0786..7c03689 100644 --- a/templates/fossa/atm_receipt.html +++ b/templates/fossa/atm_receipt.html @@ -134,7 +134,7 @@

ATM receipt for: "{{devicename}}"

el: '#vue', mixins: [windowMixin], delimiters: ['${', '}'], - data: function () { + data() { return { theurl: location.protocol + '//' + location.host, printDialog: { diff --git a/templates/fossa/index.html b/templates/fossa/index.html index 2e68cc6..bc8d2f0 100644 --- a/templates/fossa/index.html +++ b/templates/fossa/index.html @@ -170,7 +170,7 @@
ATM Payments
flat dense size="xs" - @click="openATMLink(props.row.deviceid, props.row.payload)" + @click="openAtmLink(props.row.deviceid, props.row.payload)" icon="link" color="grey" > @@ -182,7 +182,7 @@
ATM Payments
flat dense size="xs" - @click="deleteATMLink(props.row.id)" + @click="deleteAtmLink(props.row.id)" icon="cancel" color="pink" > diff --git a/views_api.py b/views_api.py index 3cdfc2b..4d1bdc5 100644 --- a/views_api.py +++ b/views_api.py @@ -1,39 +1,26 @@ 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 lnurl import encode as lnurl_encode -from loguru import logger from .crud import ( create_fossa, - delete_atm_payment_link, delete_fossa, get_fossa, - get_fossa_payment, - get_fossa_payments, get_fossas, update_fossa, - update_fossa_payment, ) -from .helpers import register_atm_payment -from .models import CreateFossa, Fossa, FossaPayment, Lnurlencode +from .models import CreateFossa, Fossa -fossa_api_router = APIRouter(prefix="/api/v1") +fossa_api_router = APIRouter() -@fossa_api_router.get("") +@fossa_api_router.get("/api/v1/fossa") async def api_fossas_retrieve( key_info: WalletTypeInfo = Depends(require_invoice_key), ) -> list[Fossa]: @@ -42,7 +29,9 @@ async def api_fossas_retrieve( return await get_fossas(user.wallet_ids) -@fossa_api_router.get("/{fossa_id}", dependencies=[Depends(require_invoice_key)]) +@fossa_api_router.get( + "/api/v1/fossa/{fossa_id}", dependencies=[Depends(require_invoice_key)] +) async def api_fossa_retrieve(fossa_id: str) -> Fossa: fossa = await get_fossa(fossa_id) if not fossa: @@ -52,12 +41,14 @@ async def api_fossa_retrieve(fossa_id: str) -> Fossa: return fossa -@fossa_api_router.post("", dependencies=[Depends(require_admin_key)]) +@fossa_api_router.post("/api/v1/fossa", dependencies=[Depends(require_admin_key)]) async def api_fossa_create(data: CreateFossa) -> Fossa: return await create_fossa(data) -@fossa_api_router.put("/{fossa_id}", dependencies=[Depends(require_admin_key)]) +@fossa_api_router.put( + "/api/v1/fossa/{fossa_id}", dependencies=[Depends(require_admin_key)] +) async def api_fossa_update(data: CreateFossa, fossa_id: str) -> Fossa: fossa = await get_fossa(fossa_id) if not fossa: @@ -69,7 +60,9 @@ async def api_fossa_update(data: CreateFossa, fossa_id: str) -> Fossa: return await update_fossa(fossa) -@fossa_api_router.delete("/{fossa_id}", dependencies=[Depends(require_admin_key)]) +@fossa_api_router.delete( + "/api/v1/fossa/{fossa_id}", dependencies=[Depends(require_admin_key)] +) async def api_fossa_delete(fossa_id: str): fossa = await get_fossa(fossa_id) if not fossa: @@ -78,212 +71,3 @@ async def api_fossa_delete(fossa_id: str): ) await delete_fossa(fossa_id) - - -#########ATM API######### - - -@fossa_api_router.get("/api/v1/atm") -async def api_atm_payments_retrieve( - wallet: WalletTypeInfo = Depends(require_invoice_key), -) -> list[FossaPayment]: - user = await get_user(wallet.wallet.user) - assert user, "Fossa cannot retrieve user" - fossas = await get_fossas(user.wallet_ids) - deviceids = [] - for fossa in fossas: - deviceids.append(fossa.id) - return await get_fossa_payments(deviceids) - - -@fossa_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 - - -@fossa_api_router.delete( - "/api/v1/atm/{atm_id}", dependencies=[Depends(require_admin_key)] -) -async def api_atm_payment_delete(atm_id: str): - fossa = await get_fossa_payment(atm_id) - if not fossa: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="ATM payment does not exist." - ) - - await delete_atm_payment_link(atm_id) - - -@fossa_api_router.get("/api/v1/ln/{fossa_id}/{p}/{ln}") -async def get_fossa_payment_lightning(fossa_id: str, p: str, ln: str) -> str: - """ - Handle Lightning payments for atms via invoice, lnaddress, lnurlp. - """ - ln = ln.strip().lower() - - fossa = await get_fossa(fossa_id) - if not fossa: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="fossa does not exist" - ) - - wallet = await get_wallet(fossa.wallet) - if not wallet: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, - detail="Wallet does not exist connected to atm, payment could not be made", - ) - fossa_payment, price_msat = await register_atm_payment(fossa, p) - if not fossa_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={fossa_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) - 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 ( - not invoice.amount_msat - or int(invoice.amount_msat / 1000) != fossa_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: - fossa_payment, price_msat = await register_atm_payment(fossa, p) - assert fossa_payment - fossa_payment.payhash = fossa_payment.payload - await update_fossa_payment(fossa_payment) - if ln[:4] == "lnbc": - await pay_invoice( - wallet_id=fossa.wallet, - payment_request=ln, - max_sat=price_msat, - extra={"tag": "fossa", "id": fossa_payment.id}, - ) - except Exception as exc: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, detail=f"{exc!s}" - ) from exc - - return fossa_payment.id - - -@fossa_api_router.get("/api/v1/boltz/{fossa_id}/{payload}/{onchain_liquid}/{address}") -async def get_fossa_payment_boltz( - fossa_id: str, payload: str, onchain_liquid: str, address: str -): - """ - Handle Boltz payments for atms. - """ - fossa = await get_fossa(fossa_id) - if not fossa: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="fossa does not exist" - ) - - fossa_payment, _ = await register_atm_payment(fossa, payload) - assert fossa_payment - if fossa_payment == "ERROR": - return fossa_payment - if fossa_payment.payload == fossa_payment.payhash: - return {"status": "ERROR", "reason": "Payment already claimed."} - if fossa_payment.payhash == "pending": - return { - "status": "ERROR", - "reason": "Pending. If you are unable to withdraw contact vendor", - } - wallet = await get_wallet(fossa.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": fossa.wallet, - "asset": onchain_liquid.replace("temp", "/"), - "amount": fossa_payment.sats, - "direction": "send", - "instant_settlement": True, - "onchain_address": address, - "feerate": False, - "feerate_value": 0, - } - - try: - fossa_payment.payload = payload - fossa_payment.payhash = "pending" - fossa_payment_updated = await update_fossa_payment(fossa_payment) - assert fossa_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, - ) - fossa_payment.payhash = fossa_payment.payload - fossa_payment_updated = await update_fossa_payment(fossa_payment) - assert fossa_payment_updated - resp = response.json() - return resp - except Exception as exc: - fossa_payment.payhash = "payment_hash" - fossa_payment_updated = await update_fossa_payment(fossa_payment) - assert fossa_payment_updated - return {"status": "ERROR", "reason": str(exc)} diff --git a/views_api_atm.py b/views_api_atm.py new file mode 100644 index 0000000..f07b12f --- /dev/null +++ b/views_api_atm.py @@ -0,0 +1,237 @@ +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.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 lnurl import encode as lnurl_encode +from loguru import logger + +from .crud import ( + delete_atm_payment_link, + get_fossa, + get_fossa_payment, + get_fossa_payments, + get_fossas, + update_fossa_payment, +) +from .helpers import register_atm_payment +from .models import FossaPayment, Lnurlencode + +fossa_api_atm_router = APIRouter() + + +@fossa_api_atm_router.post( + "/api/v1/lnurlencode", dependencies=[Depends(require_invoice_key)] +) +async def api_lnurlencode(data: Lnurlencode) -> str: + lnurl = lnurl_encode(data.url) + if not lnurl: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Lnurl could not be encoded." + ) + return str(lnurl) + + +@fossa_api_atm_router.get("/api/v1/atm") +async def api_atm_payments_retrieve( + wallet: WalletTypeInfo = Depends(require_invoice_key), +) -> list[FossaPayment]: + user = await get_user(wallet.wallet.user) + assert user, "Fossa cannot retrieve user" + fossas = await get_fossas(user.wallet_ids) + ids = [] + for fossa in fossas: + ids.append(fossa.id) + return await get_fossa_payments(ids) + + +@fossa_api_atm_router.delete( + "/api/v1/atm/{atm_id}", dependencies=[Depends(require_admin_key)] +) +async def api_atm_payment_delete(atm_id: str): + fossa = await get_fossa_payment(atm_id) + if not fossa: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="ATM payment does not exist." + ) + + await delete_atm_payment_link(atm_id) + + +@fossa_api_atm_router.get("/api/v1/ln/{fossa_id}/{p}/{ln}") +async def get_fossa_payment_lightning(fossa_id: str, p: str, ln: str) -> str: + """ + Handle Lightning payments for atms via invoice, lnaddress, lnurlp. + """ + ln = ln.strip().lower() + + fossa = await get_fossa(fossa_id) + if not fossa: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="fossa does not exist" + ) + + wallet = await get_wallet(fossa.wallet) + if not wallet: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Wallet does not exist connected to atm, payment could not be made", + ) + fossa_payment, price_msat = await register_atm_payment(fossa, p) + if not fossa_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={fossa_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) + 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 ( + not invoice.amount_msat + or int(invoice.amount_msat / 1000) != fossa_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: + fossa_payment, price_msat = await register_atm_payment(fossa, p) + assert fossa_payment + fossa_payment.payhash = fossa_payment.payload + await update_fossa_payment(fossa_payment) + if ln[:4] == "lnbc": + await pay_invoice( + wallet_id=fossa.wallet, + payment_request=ln, + max_sat=price_msat, + extra={"tag": "fossa", "id": fossa_payment.id}, + ) + except Exception as exc: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, detail=f"{exc!s}" + ) from exc + + return fossa_payment.id + + +@fossa_api_atm_router.get( + "/api/v1/boltz/{fossa_id}/{payload}/{onchain_liquid}/{address}" +) +async def get_fossa_payment_boltz( + fossa_id: str, payload: str, onchain_liquid: str, address: str +): + """ + Handle Boltz payments for atms. + """ + fossa = await get_fossa(fossa_id) + if not fossa: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="fossa does not exist" + ) + + fossa_payment, _ = await register_atm_payment(fossa, payload) + assert fossa_payment + if fossa_payment == "ERROR": + return fossa_payment + if fossa_payment.payload == fossa_payment.payhash: + return {"status": "ERROR", "reason": "Payment already claimed."} + if fossa_payment.payhash == "pending": + return { + "status": "ERROR", + "reason": "Pending. If you are unable to withdraw contact vendor", + } + wallet = await get_wallet(fossa.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": fossa.wallet, + "asset": onchain_liquid.replace("temp", "/"), + "amount": fossa_payment.sats, + "direction": "send", + "instant_settlement": True, + "onchain_address": address, + "feerate": False, + "feerate_value": 0, + } + + try: + fossa_payment.payload = payload + fossa_payment.payhash = "pending" + fossa_payment_updated = await update_fossa_payment(fossa_payment) + assert fossa_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, + ) + fossa_payment.payhash = fossa_payment.payload + fossa_payment_updated = await update_fossa_payment(fossa_payment) + assert fossa_payment_updated + resp = response.json() + return resp + except Exception as exc: + fossa_payment.payhash = "payment_hash" + fossa_payment_updated = await update_fossa_payment(fossa_payment) + assert fossa_payment_updated + return {"status": "ERROR", "reason": str(exc)}