@kcFNBMPc1&MrcVU_5x}Q_}MZZ7Xq@wS3-aYu1yrENw%ex88 z|FQx%${95uyFNwo`0+-o6HQZsudwiGau=-#@VlQPId`eQX=)|6V5&viKgTxSX{#Sp z`b=mK?+=r6y4tXFQ)qt#*!H+L>k7V9Z-mb`gz(sGv (`z8lm7a;ddnFV58dV)mlB1aY45cC#r^;8yV=`LKCYE{q(03!V%o#h zyBnXLR$e83ZKbiyRoh^TyP`V04(s-GC`Ng8Gj;yfRr1);#l-7lA$>qC?6LP8?`e$> zE UDPWn#35 |{N=_Ko+Y*2o+U?NX{tm^9C2Fn6l*{cg4#w^fnzkS4^Yh$U;;yIUYMVY+q z-al8!B`f3n<(uy$rtj(RnEt_O`vxz2S!G_KnjJ+?7BS^skV{)H>zy0&YLdjY!maD4 zai=d7n)9VLD_g(jSdir5j)|IaZ@+2B%bQz0bXi`ub%T)^tJiJjD&=i!&rg}hzVi8X z$8(nx@5<>-l(Y>_63lFz{#bFzoZ~hLiSBE@l-mAqsalw}`fTpCdwsn310$Br&XxSs zt{2chFJ-1~Q!i7ihQH(E7dB$|CdtNx&KLW;O!M23ogNpKNAs>}TqnY_b>=ZvRZ*kz zTpb48qyTHLhZFZ+`sinB7Nz!9uVPE%?v$f4Tf{RWcTJo=v9?)Q-s92+>lZmK^6K9H zZ5$O_mwC *pd0Pv~#!opf4u zx>35bN=p6KQb#$NOHHd?{WRS6-u63Rx$xT!w-w2WyC=R$O=Ol|xo^VNibvIsbFDNi z=KK>WlfPTx{Wa<2<14}Jo^Sqo-9EH{gH`s !|9QelCf|I4(CTs-yb zz0?$MbNlz55t>)#yYp<*YBuZQjN>eSE%wIq{%o4Ub!L{Pa}<}OlBASy1#6mvOYOlm z={X8IM(2tf*X-SLKKJZ8xhr!b`c_HEpIf$=<(XgAb4!osF$ ^SiVERUBKr>&$e~FWU~-h2CGi=ib|`5^GF!wGTBs`KEby z{&CJVPP6ZJ|Ngk+n964JpZnRi-tZ0I8vj5$cE@C)& 6X`5=6wpDvg+;{Eq~p*cfK!{NR)MmKIZWJX0!Za zo> bF`4Hf-XZw@>o%#*>?_{=03OsW@wm-XTZB z!xo|?jjv75^)dUdog(t>+?L2asfsK&o|?s1=^fb;nR%z}X=jj#-TaL*B8IO$l`5G6 zwyxZfX-P=X>IbGBEvSVIacBIDDzSJ$uZTT&qQzW`nv;&`q>^baT)^SPft>Ogs`}^;$ zIXeBV!~Ol4rN?%Df7<7{ Cku znZAqL^1Yz5)c@kDblc!B4=YXx<||y9&wX2c#& lI3-<;_#L^f;4Y ziJ@1)a{1f8Tn;*aUuNxmizDoP?XNFs^Y2|>W|n>L^~UAhC(GCF?8>!xX}76U!XPlq zd{yF+cPbf@R Nb0 55q zW0bgCUkmD7{5HFwcBb=ZkEAQxx_!;ttmgfh)2FMHCuVQ=o_pW?t% a3+Y4*nv81o l}_IC+9ym-onVdOxGv)o%-V2 z|1KVO{`}rwKAu^YHSF2P`PSEG|9_r(u>94Hw>R>SR9L;Wedo~bZT;Z6qSLF5hmS5x z*D<_y IZe)r I78XnLp3F+j9Fthm-l$ z+e&73x1V8EvtE~-$1KceJH^NJ{gDjYz?^OY$0LerQqgWp-+S#nHa{(M&Aj|4_gDSN z7tgkHeam R`wY;v>HC>WydR@ zy}8khqsw^Bj4-3GQ-3Z07P@bfck})2>s^0uUfieoNzzxgd|TTAMa4QBnbM1o(`;KU zMd!b*Jh~+1$5-vkOZU#Z*q``cPwZ)VYV}!TOO?&fruJSjd8rsXLzUHb{~C==c2PTC z-8}dE-s1b)>wai|Hi&pHv&$~}>!%m@Ex#l?-<_%J8h3WBq l~O3U}l&8NS#i}`YJW_{_OUQP4-ufO?ges14(XHPw| z@Wy;~qpoeL@ lcuiLn| fh6i8m`%xt8Ut!0Sykn)!Th(=iism10%`2btr!f2a!h7Z? zL#p1TtWtjU`NI^(){nYei{C0ANL0~wnzi7I#{U_S^Y<3M{(m^*@7vZ*uRfgr&lK7G z`j++kXN?RDj4heY&H w_Kg z@88+JyT|CX_l(KvY?%usw|!I}C8_P!Vv7@W;%5_ZiAbAfsP5@mA`}1p!^f|B$2MEd z|6Sa2b c`mEnQMWH?#R }RdIzUSVo-xjqDm#&0 XM6e0dv^90 zMKz4cUl%Yi2q =!bMQ(wwFWmUNVrVEN=jZBIBo^o! z>KW+g=ISHsC@yg=E5Wa$IHa;5RX-@TIKQ+gIn}i+HLpY&*@%>6xc-9Dq8zYyQj+yk zb5e6t^Gb^K4fPCBtaSJE4S?$a1wnddZUI v#GOwAJwOp{Y}6D`w>bWKu?4Rn)? z3@vqy6BEr0Qxh#sO%2nKjPlGYE=kNwP6ZiNkz1gbnVDi`oMvQcm}F_Jo0gK4q-$c4 zXsT;zmYAk%lw@XVVq~6ZW^QDHWQ2cFW_o5`Vh*yaKt`oxrdXLKo2HprTAJ&oq!}ma znwVQA=~^Tvn&}#uq^6`88z)*CrkH_^N=deI%P-1JEU{I}%uP&B)i20P2TOng+{!V) z(^kny&j2A3kds)FmS2=>tK^fJS6YFP2+7P%4K7Ipg{GmofuV(^xuL0 G_o=<3o$YTF^o;M4GgRdkksd=WTsUT zQf&vy95#?B_sA?R$uFt|3qf*JaB3lh2jS*qf>bLgD1bACRbn#43&n|LsVQJ@z@?Hi z67$kiQ*4!>$rYw06H8J^GD *j)HN|qvDCFNOHR@?FgG_bO))Yvw@8He z3T}FFep*R+Vo|DNdTL&Yt&)3YZUH!~6g0pQrHSg6@{CkaEEpIW=^7g88XJZfSXvod zS{WHY15BUxUQA6kH8x61G1E;;Of=UuNlHo7O-waO(KSm>HZV*vF*7i EkDFjGTJes;jgNvjPAW89P>Y`e3aUuHssd*{3O65xS zcGIS}zF}ZsU`z6LcVYMsf(!O8pUl9(z**oCS J zhmT7{S2rbichoz$vPO%3RAH0e%I=zDu*~wylw)C6!lZ;ZL_`O#Ick4p*_9by!M!K# z_8 ga=iS}qf6V9G*XxCH zPWU4 -H?yvcI$Xl@2?dO}Dn*|okI5kn(-9oOvc-xt?XFtv~PFHZyRQvDG z>Gi}fV2^nD^K-I(OM~{rTzY9zy*G{{V8u!Q<(GAu&Ykn)2+(j{eRb3E$AZ&p($YL9 z-FW|f^Tmu7hAcg+xqZ$H1+-mNgf!hayVQTKkK3EHw9`c?CN`Fr@k&Tmc6Mb+$&}AW zm;2BE@#5lQ0SDca^JmR^RAX#x9IPWGQub-_MUC|$u1b!HY;|8>h0dEj`|(t@nLeNH z?k-n!*f)8;k6NVelFONS`T6Y(KQ;8GdxxI tmx`Kn*( {7~ Q}x9o;`zSX-(~j} z^~SI&D6Lzw=E=W*b%}*`m7iKnrF=8F6AUDNyu7^pkljybXXhLPiDz4tof^)xO!44! zS||{$GwqVdfdqpOA3ki@a85=-B4Kab{)Fg7EGO1j@~|*jt~L8tU3ikqJK)C2=_;Lf z^S&>7+bN j*&?FXIP`93~*l5%g-`Y8;HR6C9))%^PM@{!ns2M3$~{QX_7 zC~IbIecC+l&WTmAOIZ|@_LaO8I-X>hDD0-K;8-V>m6atltET<%#M0YdTn;XK>i^d* z6pGnXv2h1W!OoaNc~d)Gn*8TjD6Xzu$||7CX#Dx{@&0b-hHcx-5_gvzJAS `FZ#5U8hG;nv5MC)>c+6o(6`72YY*aUx^&Z+R8Qi zfv4K!KMxKvCyMW!#3UeSJ=bsE+_|Ec%s*DdY+>0sf62amaW}-)rWl=^t{*R!_0x;n z!6j$=?N+aXH#ZDRIWF9~rIqt?iqOgMZiYpw9?LJ+{QmZKs_25PTSIT?%{_VYWWDYq z4L3%Y{>ecrMRv!m`@_B9V#bcWdt)O&`67C4*wMPzt2`N$k|v)#^7!%NL+@DP)|*ei z>+wnZ=ij ^4C#!fWt-n6|x9owWjS4?m-6|_B@9rw?&M}M5 zW3rIpd-dv-(KH$eDZcZPsACsudP}5>{*(YzW(yIwzj438DwQ;uUxs(@ HY;> -J>}Q%`U2+21Vs(|u=~UEG|0{-1Kg`RB^>ON<(&)f2 zGpM7 !)2dQL49XTi#u* z)#q;9knr{Q*S_^LG&FQe)z?>R`u~4>E4{b=zuoJ{r%#>Ikm}W|oO8i_0UHzJ&Hk>n z>(=S*bU!L2s4m1AY1n!=@uA@ZTuwj<0tbt%TIWuw$3aTVjDEeBUgDZ)G&4n)bFxb3 zuHCy|9`Bd$Dve1^RSgXdJ-N!*%F3&`xw%WN YWS?^2dJivXXy4P-&t;ucLK6V7DX+Q! literal 0 HcmV?d00001 diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..214bcd1 --- /dev/null +++ b/tasks.py @@ -0,0 +1,59 @@ +import asyncio + +from loguru import logger + +from lnbits.core.models import Payment +from lnbits.core.services import create_invoice, pay_invoice, websocketUpdater +from lnbits.helpers import get_current_extension_name +from lnbits.tasks import register_invoice_listener + +from .crud import get_temp + + +####################################### +########## RUN YOU TASKS HERE ######### +####################################### + + +# the usual task is to listen to invoices related to this extension + +async def wait_for_paid_invoices(): + invoice_queue = asyncio.Queue() + register_invoice_listener(invoice_queue, get_current_extension_name()) + + while True: + payment = await invoice_queue.get() + await on_invoice_paid(payment) + + +# do somethhing when an invoice related top this extension is paid + +async def on_invoice_paid(payment: Payment) -> None: + if payment.extra.get("tag") != "temp": + return + + temp_id = payment.extra.get("tempId") + assert temp_id + + temp = await get_temp(temp_id) + assert temp + + # update something + + data_to_update = { + "total" temp.total + payment.amount + } + + await update_temp(temp_id=temp_id, **data_to_update.dict()) + + + # here we could send some data to a websocket on wss:// /api/v1/ws/ + + some_payment_data = { + "name": temp.name, + "amount": payment.amount, + "fee": payment.fee, + "checking_id": payment.checking_id + } + + await websocketUpdater(temp_id, str(some_payment_data)) diff --git a/templates/tpos/_api_docs.html b/templates/tpos/_api_docs.html new file mode 100644 index 0000000..abe4fc0 --- /dev/null +++ b/templates/tpos/_api_docs.html @@ -0,0 +1,79 @@ + + diff --git a/templates/tpos/_tpos.html b/templates/tpos/_tpos.html new file mode 100644 index 0000000..9157728 --- /dev/null +++ b/templates/tpos/_tpos.html @@ -0,0 +1,21 @@ ++ + ++ ++ +GET /temp/api/v1/temps+Headers
+{"X-Api-Key": <invoice_key>}
+Body (application/json)
++ Returns 200 OK (application/json) +
+[<temp_object>, ...]+Curl example
+curl -X GET {{ request.base_url }}temp/api/v1/temps -H "X-Api-Key: + <invoice_key>" +++ + ++ ++ +POST /temp/api/v1/temps+Headers
+{"X-Api-Key": <invoice_key>}
+Body (application/json)
+{"name": <string>, "currency": <string*ie USD*>}++ Returns 201 CREATED (application/json) +
+{"currency": <string>, "id": <string>, "name": + <string>, "wallet": <string>}+Curl example
+curl -X POST {{ request.base_url }}temp/api/v1/temps -d '{"name": + <string>, "currency": <string>}' -H "Content-type: + application/json" -H "X-Api-Key: <admin_key>" +++ ++ ++ +DELETE + /temp/api/v1/temps/<temp_id>+Headers
+{"X-Api-Key": <admin_key>}
+Returns 204 NO CONTENT
++Curl example
+curl -X DELETE {{ request.base_url + }}temp/api/v1/temps/<temp_id> -H "X-Api-Key: <admin_key>" +++ diff --git a/templates/tpos/index.html b/templates/tpos/index.html new file mode 100644 index 0000000..ffde185 --- /dev/null +++ b/templates/tpos/index.html @@ -0,0 +1,1150 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} ++ ++ ++ Thiago's Point of Sale is a secure, mobile-ready, instant and shareable + point of sale terminal (PoS) for merchants. The PoS is linked to your + LNbits wallet but completely air-gapped so users can ONLY create + invoices. To share the Temp hit the hash on the terminal. +
+ Created by + Tiago Vasconcelos. +++{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} diff --git a/templates/tpos/tpos.html b/templates/tpos/tpos.html new file mode 100644 index 0000000..8c4a56f --- /dev/null +++ b/templates/tpos/tpos.html @@ -0,0 +1,1205 @@ +{% extends "public.html" %} {% block toolbar_title %} {{ temp.name }} +++ ++ + ++ +New Temp ++ ++ +++++Temp
+++Export to CSV ++ ++ + ++ ++ + + ${ col.label } + +Tip Wallet +Tip Options % +Withdraw PIN +Withdraw Limit +Withdraw Premium ++ + + ++ ++ ++ + ++ PoS QR + Open PoS + + ++ Click to copy ${props.row.id.substring(0,6)}...+ ${ (col.name == 'tip_options' && col.value ? + JSON.parse(col.value).join(", ") : col.value) } + ++ ++ Click to copy ${props.row.tip_wallet.substring(0,6)}...N/A ++ ${props.row.tip_options} + +N/A ++ ${props.row.withdrawpin} + +N/A ++ ${props.row.withdrawlimit} + +N/A ++ ${props.row.withdrawpremium}% + +0 ++ ++ + ++ + ++ ++++++ ++ + ++ +++++ +++Add Item +Delete All ++ ++ ++ + + ++ +Import +Import a JSON file ++ ++ +Export +Export a JSON file ++++++ ++ ++ ++ ++ + + ++ + ${props.row.id.split(':')[1]} + ++ ${props.row.title} + ++ ${itemFormatPrice(props.row.price, + props.row.id)} + ++ ${props.row.disabled} + +++ ++ ++ +{{SITE_TITLE}} Temp extension
++ ++ + {% include "temp/_api_docs.html" %} + ++ {% include "temp/_temp.html" %} + + ++ ++ ++ + + +++++ +++ + ++ + Hit enter to add values ++ You can leave this blank. A default rounding option is available + (round amount to a value) + ++ + ++ + + + ++Update Temp +Create Temp +Cancel ++ ++ ++ ++ + + + + + ++${itemDialog.data.id ? 'Update Item' : 'Create Item'} +Cancel ++ ++ ++ ++ +++ ${ urlDialog.data.name }
+
${ + urlDialog.data.shareUrl } +++Copy URL +Close ++ ++ ++ ++ Importing ${fileDataDialog?.count} items +
++ ++ ++ + ++ ++
+ + +${item.title} +${item.description} ++ ++ ++Import +Close ++ +{% endblock %} {% block footer %}{% endblock %} {% block page_container %} + + +{% endblock %} {% block styles %} + +{% endblock %} {% block scripts %} + + +{% endblock %} diff --git a/views.py b/views.py new file mode 100644 index 0000000..8cd1fb7 --- /dev/null +++ b/views.py @@ -0,0 +1,88 @@ +from http import HTTPStatus + +from fastapi import Depends, Request +from fastapi.templating import Jinja2Temps +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse + +from lnbits.core.models import User +from lnbits.decorators import check_user_exists +from lnbits.settings import settings + +from . import temp_ext, temp_renderer +from .crud import get_temp + +temps = Jinja2Temps(directory="temps") + + +####################################### +##### ADD YOUR PAGE ENDPOINTS HERE #### +####################################### + + +# Backend admin page + +@temp_ext.get("/", response_class=HTMLResponse) +async def index(request: Request, user: User = Depends(check_user_exists)): + return temp_renderer().TempResponse( + "temp/index.html", {"request": request, "user": user.dict()} + ) + + +# Frontend shareable page + +@temp_ext.get("/{temp_id}") +async def temp(request: Request, temp_id): + temp = await get_temp(temp_id) + if not temp: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Temp does not exist." + ) + return temp_renderer().TempResponse( + "temp/temp.html", + { + "request": request, + "temp": temp, + "withdrawamtemps": temp.withdrawamtemps, + "web_manifest": f"/temp/manifest/{temp_id}.webmanifest", + }, + ) + + +# Customise a manifest, or remove manifest completely + +@temp_ext.get("/manifest/{temp_id}.webmanifest") +async def manifest(temp_id: str): + remp= await get_temp(temp_id) + if not temp: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Temp does not exist." + ) + + return { + "short_name": settings.lnbits_site_title, + "name": temp.name + " - " + settings.lnbits_site_title, + "icons": [ + { + "src": settings.lnbits_custom_logo + if settings.lnbits_custom_logo + else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png", + "type": "image/png", + "sizes": "900x900", + } + ], + "start_url": "/temp/" + temp_id, + "background_color": "#1F2234", + "description": "Minimal extension to build on", + "display": "standalone", + "scope": "/temp/" + temp_id, + "theme_color": "#1F2234", + "shortcuts": [ + { + "name": temp.name + " - " + settings.lnbits_site_title, + "short_name": temp.name, + "description": temp.name + " - " + settings.lnbits_site_title, + "url": "/temp/" + temp_id, + } + ], + } diff --git a/views_api.py b/views_api.py new file mode 100644 index 0000000..6dc1bc6 --- /dev/null +++ b/views_api.py @@ -0,0 +1,136 @@ +from http import HTTPStatus +import json + +import httpx +from fastapi import Depends, Query, Request +from lnurl import decode as decode_lnurl +from loguru import logger +from starlette.exceptions import HTTPException + +from lnbits.core.crud import get_user +from lnbits.core.models import Payment +from lnbits.core.services import create_invoice +from lnbits.core.views.api import api_payment +from lnbits.decorators import ( + WalletTypeInfo, + check_admin, + get_key_type, + require_admin_key, + require_invoice_key, +) + +from . import temp_ext +from .crud import ( + create_temp, + update_temp, + delete_temp, + get_temp, + get_temps +) +from .models import CreateTempData, PayLnurlWData, LNURLCharge, CreateUpdateItemData + + +####################################### +##### ADD YOUR API ENDPOINTS HERE ##### +####################################### + + +# TYPICAL ENDPOINTS + +# get all the records belonging to the user + +@temp_ext.get("/api/v1/temps", status_code=HTTPStatus.OK) +async def api_temps( + all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type) +): + wallet_ids = [wallet.wallet.id] + if all_wallets: + user = await get_user(wallet.wallet.user) + wallet_ids = user.wallet_ids if user else [] + return [temp.dict() for temp in await get_temps(wallet_ids)] + + +# get a specific record belonging to a user + +@temp_ext.put("/api/v1/temps/{temp_id}") +async def api_temp_update( + data: CreateTempData, + temp_id: str, + wallet: WalletTypeInfo = Depends(require_admin_key), +): + if not temp_id: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Temp does not exist." + ) + temp = await get_temp(temp_id) + assert temp, "Temp couldn't be retrieved" + + if wallet.wallet.id != temp.wallet: + raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your Temp.") + temp = await update_temp(temp_id=temp_id, **data.dict()) + return temp.dict() + + +# Create a new record + +@temp_ext.post("/api/v1/temps", status_code=HTTPStatus.CREATED) +async def api_temp_create( + data: CreateTempData, wallet: WalletTypeInfo = Depends(get_key_type) +): + temp = await create_temp(wallet_id=wallet.wallet.id, data=data) + return temp.dict() + + +# Delete a record + +@temp_ext.delete("/api/v1/temps/{temp_id}") +async def api_temp_delete( + temp_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) +): + temp = await get_temp(temp_id) + + if not temp: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Temp does not exist." + ) + + if temp.wallet != wallet.wallet.id: + raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your Temp.") + + await delete_temp(temp_id) + return "", HTTPStatus.NO_CONTENT + + +# ANY OTHER ENDPOINTS YOU NEED + +# This endpoint creates a payment + +@tpos_ext.post("/api/v1/temps/payment/{temp_id}", status_code=HTTPStatus.CREATED) +async def api_tpos_create_invoice( + temp_id: str, amount: int = Query(..., ge=1), memo: str = "" +) -> dict: + temp = await get_temp(temp_id) + + if not temp: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Temp does not exist." + ) + + # we create a payment and add some tags, so tasks.py can grab the payment once its paid + + try: + payment_hash, payment_request = await create_invoice( + wallet_id=temp.wallet, + amount=amount, + memo=f"{memo} to {temp.name}" if memo else f"{temp.name}", + extra={ + "tag": "temp", + "tipAmount": tipAmount, + "tempId": tempId, + "amount": amount, + }, + ) + except Exception as e: + raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) + + return {"payment_hash": payment_hash, "payment_request": payment_request} \ No newline at end of file+ ++ +++++${ amountFormatted }
+${ fsat } sat
++++ Total: ${totalFormatted}
+
${totalfsat} sat ++ ++ +++++++1 +2 +3 +4 +5 +6 +7 +8 +9 +0.0 ? cancelAddAmount() : stack = []" + size="xl" + :outline="!($q.dark.isActive)" + rounded + :color="monochrome ? 'secondary' : 'negative'" + >C +0 +⬅ ++ +Ok ++ ++ ++ + ++ + ++ + ++ +++ +++${totalFormatted}
+${totalfsat} sat
++++
++ + ++ + + + + + + ++ ++++ + ${item.quantity} +++ + ${item.formattedPrice} + ++ ++ +++++ +++ +++++ ++ +++++ ++ +
+ + +${item.title}+${item.formattedPrice}++ ${item.description} +++ +Add ++ ++ +${cartDrawer ? 'Hide Cart' :' Open Cart'} EXIT ATM MODE ++ + ++ ++ ++ ++${ amountWithTipFormatted }
++ ${ fsat } + sat + ( + ${ tipAmountFormatted } tip) +
++ ++Copy invoice +Pay in wallet +Close ++ + ++ ++ Would you like to leave a tip? ++++${ tip }% ++ +++ +Ok +++No, thanks +Close ++ + ++ ++ ++ +++ {{ temp.name }}
+
{{ request.url }} +++Copy URL +Close ++ + ++ + + ++ ++ ++ + + ++ ++ +No paid invoices ++ ++ +${payment.amount / 1000} sats +Hash: ${payment.checking_id.slice(0, 30)}... ++ +${payment.dateFrom} ++ + ++ ++ +Withdraw PIN++ ++ ++ + +++