diff --git a/lnbits/extensions/withdraw/README.md b/lnbits/extensions/withdraw/README.md deleted file mode 100644 index fce2c6e5..00000000 --- a/lnbits/extensions/withdraw/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# LNURLw - -## Create a static QR code people can use to withdraw funds from a Lightning Network wallet - -LNURL is a range of lightning-network standards that allow us to use lightning-network differently. An LNURL withdraw is the permission for someone to pull a certain amount of funds from a lightning wallet. - -The most common use case for an LNURL withdraw is a faucet, although it is a very powerful technology, with much further reaching implications. For example, an LNURL withdraw could be minted to pay for a subscription service. Or you can have a LNURLw as an offline Lightning wallet (a pre paid "card"), you use to pay for something without having to even reach your smartphone. - -LNURL withdraw is a **very powerful tool** and should not have his use limited to just faucet applications. With LNURL withdraw, you have the ability to give someone the right to spend a range, once or multiple times. **This functionality has not existed in money before**. - -[**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets) - -## Usage - -#### Quick Vouchers - -LNbits Quick Vouchers allows you to easily create a batch of LNURLw's QR codes that you can print and distribute as rewards, onboarding people into Lightning Network, gifts, etc... - -1. Create Quick Vouchers\ - ![quick vouchers](https://i.imgur.com/IUfwdQz.jpg) - - select wallet - - set the amount each voucher will allow someone to withdraw - - set the amount of vouchers you want to create - _have in mind you need to have a balance on the wallet that supports the amount \* number of vouchers_ -2. You can now print, share, display your LNURLw links or QR codes\ - ![lnurlw created](https://i.imgur.com/X00twiX.jpg) - - on details you can print the vouchers\ - ![printable vouchers](https://i.imgur.com/2xLHbob.jpg) - - every printed LNURLw QR code is unique, it can only be used once -3. Bonus: you can use an LNbits themed voucher, or use a custom one. There's a _template.svg_ file in `static/images` folder if you want to create your own.\ - ![voucher](https://i.imgur.com/qyQoHi3.jpg) - -#### Advanced - -1. Create the Advanced LNURLw\ - ![create advanced lnurlw](https://i.imgur.com/OR0f885.jpg) - - set the wallet - - set a title for the LNURLw (it will show up in users wallet) - - define the minimum and maximum a user can withdraw, if you want a fixed amount set them both to an equal value - - set how many times can the LNURLw be scanned, if it's a one time use or it can be scanned 100 times - - LNbits has the "_Time between withdraws_" setting, you can define how long the LNURLw will be unavailable between scans - - you can set the time in _seconds, minutes or hours_ - - the "_Use unique withdraw QR..._" reduces the chance of your LNURL withdraw being exploited and depleted by one person, by generating a new QR code every time it's scanned -2. Print, share or display your LNURLw link or it's QR code\ - ![lnurlw created](https://i.imgur.com/X00twiX.jpg) - -**LNbits bonus:** If a user doesn't have a Lightning Network wallet and scans the LNURLw QR code with their smartphone camera, or a QR scanner app, they can follow the link provided to claim their satoshis and get an instant LNbits wallet! - -![](https://i.imgur.com/2zZ7mi8.jpg) diff --git a/lnbits/extensions/withdraw/__init__.py b/lnbits/extensions/withdraw/__init__.py deleted file mode 100644 index cb5eb9c4..00000000 --- a/lnbits/extensions/withdraw/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -from fastapi import APIRouter -from fastapi.staticfiles import StaticFiles - -from lnbits.db import Database -from lnbits.helpers import template_renderer - -db = Database("ext_withdraw") - -withdraw_static_files = [ - { - "path": "/withdraw/static", - "app": StaticFiles(packages=[("lnbits", "extensions/withdraw/static")]), - "name": "withdraw_static", - } -] - - -withdraw_ext: APIRouter = APIRouter(prefix="/withdraw", tags=["withdraw"]) - - -def withdraw_renderer(): - return template_renderer(["lnbits/extensions/withdraw/templates"]) - - -from .lnurl import * # noqa: F401,F403 -from .views import * # noqa: F401,F403 -from .views_api import * # noqa: F401,F403 diff --git a/lnbits/extensions/withdraw/config.json b/lnbits/extensions/withdraw/config.json deleted file mode 100644 index c22d69c8..00000000 --- a/lnbits/extensions/withdraw/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "LNURLw", - "short_description": "Make LNURL withdraw links", - "tile": "/withdraw/static/image/lnurl-withdraw.png", - "contributors": ["arcbtc", "eillarra"] -} diff --git a/lnbits/extensions/withdraw/crud.py b/lnbits/extensions/withdraw/crud.py deleted file mode 100644 index 83dd0593..00000000 --- a/lnbits/extensions/withdraw/crud.py +++ /dev/null @@ -1,173 +0,0 @@ -from datetime import datetime -from typing import List, Optional, Union - -import shortuuid - -from lnbits.helpers import urlsafe_short_hash - -from . import db -from .models import CreateWithdrawData, HashCheck, WithdrawLink - - -async def create_withdraw_link( - data: CreateWithdrawData, wallet_id: str -) -> WithdrawLink: - link_id = urlsafe_short_hash()[:6] - available_links = ",".join([str(i) for i in range(data.uses)]) - await db.execute( - """ - INSERT INTO withdraw.withdraw_link ( - id, - wallet, - title, - min_withdrawable, - max_withdrawable, - uses, - wait_time, - is_unique, - unique_hash, - k1, - open_time, - usescsv, - webhook_url, - webhook_headers, - webhook_body, - custom_url - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - link_id, - wallet_id, - data.title, - data.min_withdrawable, - data.max_withdrawable, - data.uses, - data.wait_time, - int(data.is_unique), - urlsafe_short_hash(), - urlsafe_short_hash(), - int(datetime.now().timestamp()) + data.wait_time, - available_links, - data.webhook_url, - data.webhook_headers, - data.webhook_body, - data.custom_url, - ), - ) - link = await get_withdraw_link(link_id, 0) - assert link, "Newly created link couldn't be retrieved" - return link - - -async def get_withdraw_link(link_id: str, num=0) -> Optional[WithdrawLink]: - row = await db.fetchone( - "SELECT * FROM withdraw.withdraw_link WHERE id = ?", (link_id,) - ) - if not row: - return None - - link = dict(**row) - link["number"] = num - - return WithdrawLink.parse_obj(link) - - -async def get_withdraw_link_by_hash(unique_hash: str, num=0) -> Optional[WithdrawLink]: - row = await db.fetchone( - "SELECT * FROM withdraw.withdraw_link WHERE unique_hash = ?", (unique_hash,) - ) - if not row: - return None - - link = dict(**row) - link["number"] = num - - return WithdrawLink.parse_obj(link) - - -async def get_withdraw_links(wallet_ids: Union[str, List[str]]) -> List[WithdrawLink]: - if isinstance(wallet_ids, str): - wallet_ids = [wallet_ids] - - q = ",".join(["?"] * len(wallet_ids)) - rows = await db.fetchall( - f"SELECT * FROM withdraw.withdraw_link WHERE wallet IN ({q})", (*wallet_ids,) - ) - return [WithdrawLink(**row) for row in rows] - - -async def remove_unique_withdraw_link(link: WithdrawLink, unique_hash: str) -> None: - unique_links = [ - x.strip() - for x in link.usescsv.split(",") - if unique_hash != shortuuid.uuid(name=link.id + link.unique_hash + x.strip()) - ] - await update_withdraw_link( - link.id, - usescsv=",".join(unique_links), - ) - - -async def increment_withdraw_link(link: WithdrawLink) -> None: - await update_withdraw_link( - link.id, - used=link.used + 1, - open_time=link.wait_time + int(datetime.now().timestamp()), - ) - - -async def update_withdraw_link(link_id: str, **kwargs) -> Optional[WithdrawLink]: - if "is_unique" in kwargs: - kwargs["is_unique"] = int(kwargs["is_unique"]) - q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) - await db.execute( - f"UPDATE withdraw.withdraw_link SET {q} WHERE id = ?", - (*kwargs.values(), link_id), - ) - row = await db.fetchone( - "SELECT * FROM withdraw.withdraw_link WHERE id = ?", (link_id,) - ) - return WithdrawLink(**row) if row else None - - -async def delete_withdraw_link(link_id: str) -> None: - await db.execute("DELETE FROM withdraw.withdraw_link WHERE id = ?", (link_id,)) - - -def chunks(lst, n): - for i in range(0, len(lst), n): - yield lst[i : i + n] - - -async def create_hash_check(the_hash: str, lnurl_id: str) -> HashCheck: - await db.execute( - """ - INSERT INTO withdraw.hash_check ( - id, - lnurl_id - ) - VALUES (?, ?) - """, - (the_hash, lnurl_id), - ) - hashCheck = await get_hash_check(the_hash, lnurl_id) - return hashCheck - - -async def get_hash_check(the_hash: str, lnurl_id: str) -> HashCheck: - rowid = await db.fetchone( - "SELECT * FROM withdraw.hash_check WHERE id = ?", (the_hash,) - ) - rowlnurl = await db.fetchone( - "SELECT * FROM withdraw.hash_check WHERE lnurl_id = ?", (lnurl_id,) - ) - if not rowlnurl: - await create_hash_check(the_hash, lnurl_id) - return HashCheck(lnurl=True, hash=False) - else: - if not rowid: - await create_hash_check(the_hash, lnurl_id) - return HashCheck(lnurl=True, hash=False) - else: - return HashCheck(lnurl=True, hash=True) diff --git a/lnbits/extensions/withdraw/lnurl.py b/lnbits/extensions/withdraw/lnurl.py deleted file mode 100644 index 5ef521fa..00000000 --- a/lnbits/extensions/withdraw/lnurl.py +++ /dev/null @@ -1,200 +0,0 @@ -import json -from datetime import datetime -from http import HTTPStatus - -import httpx -import shortuuid -from fastapi import HTTPException, Query, Request, Response -from loguru import logger - -from lnbits.core.crud import update_payment_extra -from lnbits.core.services import pay_invoice - -from . import withdraw_ext -from .crud import ( - get_withdraw_link_by_hash, - increment_withdraw_link, - remove_unique_withdraw_link, -) -from .models import WithdrawLink - - -@withdraw_ext.get( - "/api/v1/lnurl/{unique_hash}", - response_class=Response, - name="withdraw.api_lnurl_response", -) -async def api_lnurl_response(request: Request, unique_hash): - link = await get_withdraw_link_by_hash(unique_hash) - - if not link: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Withdraw link does not exist." - ) - - if link.is_spent: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Withdraw is spent." - ) - url = request.url_for("withdraw.api_lnurl_callback", unique_hash=link.unique_hash) - withdrawResponse = { - "tag": "withdrawRequest", - "callback": url, - "k1": link.k1, - "minWithdrawable": link.min_withdrawable * 1000, - "maxWithdrawable": link.max_withdrawable * 1000, - "defaultDescription": link.title, - "webhook_url": link.webhook_url, - "webhook_headers": link.webhook_headers, - "webhook_body": link.webhook_body, - } - - return json.dumps(withdrawResponse) - - -@withdraw_ext.get( - "/api/v1/lnurl/cb/{unique_hash}", - name="withdraw.api_lnurl_callback", - summary="lnurl withdraw callback", - description=""" - This endpoints allows you to put unique_hash, k1 - and a payment_request to get your payment_request paid. - """, - response_description="JSON with status", - responses={ - 200: {"description": "status: OK"}, - 400: {"description": "k1 is wrong or link open time or withdraw not working."}, - 404: {"description": "withdraw link not found."}, - 405: {"description": "withdraw link is spent."}, - }, -) -async def api_lnurl_callback( - unique_hash, - k1: str = Query(...), - pr: str = Query(...), - id_unique_hash=None, -): - link = await get_withdraw_link_by_hash(unique_hash) - now = int(datetime.now().timestamp()) - if not link: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="withdraw not found." - ) - - if link.is_spent: - raise HTTPException( - status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail="withdraw is spent." - ) - - if link.k1 != k1: - raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="k1 is wrong.") - - if now < link.open_time: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, - detail=f"wait link open_time {link.open_time - now} seconds.", - ) - - if id_unique_hash: - if check_unique_link(link, id_unique_hash): - await remove_unique_withdraw_link(link, id_unique_hash) - else: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="withdraw not found." - ) - - try: - payment_hash = await pay_invoice( - wallet_id=link.wallet, - payment_request=pr, - max_sat=link.max_withdrawable, - extra={"tag": "withdraw"}, - ) - await increment_withdraw_link(link) - if link.webhook_url: - await dispatch_webhook(link, payment_hash, pr) - return {"status": "OK"} - except Exception as e: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, detail=f"withdraw not working. {str(e)}" - ) - - -def check_unique_link(link: WithdrawLink, unique_hash: str) -> bool: - return any( - unique_hash == shortuuid.uuid(name=link.id + link.unique_hash + x.strip()) - for x in link.usescsv.split(",") - ) - - -async def dispatch_webhook( - link: WithdrawLink, payment_hash: str, payment_request: str -) -> None: - async with httpx.AsyncClient() as client: - try: - r: httpx.Response = await client.post( - link.webhook_url, - json={ - "payment_hash": payment_hash, - "payment_request": payment_request, - "lnurlw": link.id, - "body": json.loads(link.webhook_body) if link.webhook_body else "", - }, - headers=json.loads(link.webhook_headers) - if link.webhook_headers - else None, - timeout=40, - ) - await update_payment_extra( - payment_hash=payment_hash, - extra={ - "wh_success": r.is_success, - "wh_message": r.reason_phrase, - "wh_response": r.text, - }, - outgoing=True, - ) - except Exception as exc: - # webhook fails shouldn't cause the lnurlw to fail since invoice is already paid - logger.error("Caught exception when dispatching webhook url: " + str(exc)) - await update_payment_extra( - payment_hash=payment_hash, - extra={"wh_success": False, "wh_message": str(exc)}, - outgoing=True, - ) - - -# FOR LNURLs WHICH ARE UNIQUE -@withdraw_ext.get( - "/api/v1/lnurl/{unique_hash}/{id_unique_hash}", - response_class=Response, - name="withdraw.api_lnurl_multi_response", -) -async def api_lnurl_multi_response(request: Request, unique_hash, id_unique_hash): - link = await get_withdraw_link_by_hash(unique_hash) - - if not link: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="LNURL-withdraw not found." - ) - - if link.is_spent: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Withdraw is spent." - ) - - if not check_unique_link(link, id_unique_hash): - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="LNURL-withdraw not found." - ) - - url = request.url_for("withdraw.api_lnurl_callback", unique_hash=link.unique_hash) - withdrawResponse = { - "tag": "withdrawRequest", - "callback": url + "?id_unique_hash=" + id_unique_hash, - "k1": link.k1, - "minWithdrawable": link.min_withdrawable * 1000, - "maxWithdrawable": link.max_withdrawable * 1000, - "defaultDescription": link.title, - } - return json.dumps(withdrawResponse) diff --git a/lnbits/extensions/withdraw/migrations.py b/lnbits/extensions/withdraw/migrations.py deleted file mode 100644 index 95805ae7..00000000 --- a/lnbits/extensions/withdraw/migrations.py +++ /dev/null @@ -1,134 +0,0 @@ -async def m001_initial(db): - """ - Creates an improved withdraw table and migrates the existing data. - """ - await db.execute( - f""" - CREATE TABLE withdraw.withdraw_links ( - id TEXT PRIMARY KEY, - wallet TEXT, - title TEXT, - min_withdrawable {db.big_int} DEFAULT 1, - max_withdrawable {db.big_int} DEFAULT 1, - uses INTEGER DEFAULT 1, - wait_time INTEGER, - is_unique INTEGER DEFAULT 0, - unique_hash TEXT UNIQUE, - k1 TEXT, - open_time INTEGER, - used INTEGER DEFAULT 0, - usescsv TEXT - ); - """ - ) - - -async def m002_change_withdraw_table(db): - """ - Creates an improved withdraw table and migrates the existing data. - """ - await db.execute( - f""" - CREATE TABLE withdraw.withdraw_link ( - id TEXT PRIMARY KEY, - wallet TEXT, - title TEXT, - min_withdrawable {db.big_int} DEFAULT 1, - max_withdrawable {db.big_int} DEFAULT 1, - uses INTEGER DEFAULT 1, - wait_time INTEGER, - is_unique INTEGER DEFAULT 0, - unique_hash TEXT UNIQUE, - k1 TEXT, - open_time INTEGER, - used INTEGER DEFAULT 0, - usescsv TEXT - ); - """ - ) - - for row in [ - list(row) for row in await db.fetchall("SELECT * FROM withdraw.withdraw_links") - ]: - usescsv = "" - - for i in range(row[5]): - if row[7]: - usescsv += "," + str(i + 1) - else: - usescsv += "," + str(1) - usescsv = usescsv[1:] - await db.execute( - """ - INSERT INTO withdraw.withdraw_link ( - id, - wallet, - title, - min_withdrawable, - max_withdrawable, - uses, - wait_time, - is_unique, - unique_hash, - k1, - open_time, - used, - usescsv - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - row[0], - row[1], - row[2], - row[3], - row[4], - row[5], - row[6], - row[7], - row[8], - row[9], - row[10], - row[11], - usescsv, - ), - ) - await db.execute("DROP TABLE withdraw.withdraw_links") - - -async def m003_make_hash_check(db): - """ - Creates a hash check table. - """ - await db.execute( - """ - CREATE TABLE withdraw.hash_check ( - id TEXT PRIMARY KEY, - lnurl_id TEXT - ); - """ - ) - - -async def m004_webhook_url(db): - """ - Adds webhook_url - """ - await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN webhook_url TEXT;") - - -async def m005_add_custom_print_design(db): - """ - Adds custom print design - """ - await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN custom_url TEXT;") - - -async def m006_webhook_headers_and_body(db): - """ - Add headers and body to webhooks - """ - await db.execute( - "ALTER TABLE withdraw.withdraw_link ADD COLUMN webhook_headers TEXT;" - ) - await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN webhook_body TEXT;") diff --git a/lnbits/extensions/withdraw/models.py b/lnbits/extensions/withdraw/models.py deleted file mode 100644 index 49421a79..00000000 --- a/lnbits/extensions/withdraw/models.py +++ /dev/null @@ -1,79 +0,0 @@ -import shortuuid -from fastapi import Query -from lnurl import Lnurl, LnurlWithdrawResponse -from lnurl import encode as lnurl_encode -from lnurl.models import ClearnetUrl, MilliSatoshi -from pydantic import BaseModel -from starlette.requests import Request - - -class CreateWithdrawData(BaseModel): - title: str = Query(...) - min_withdrawable: int = Query(..., ge=1) - max_withdrawable: int = Query(..., ge=1) - uses: int = Query(..., ge=1) - wait_time: int = Query(..., ge=1) - is_unique: bool - webhook_url: str = Query(None) - webhook_headers: str = Query(None) - webhook_body: str = Query(None) - custom_url: str = Query(None) - - -class WithdrawLink(BaseModel): - id: str - wallet: str = Query(None) - title: str = Query(None) - min_withdrawable: int = Query(0) - max_withdrawable: int = Query(0) - uses: int = Query(0) - wait_time: int = Query(0) - is_unique: bool = Query(False) - unique_hash: str = Query(0) - k1: str = Query(None) - open_time: int = Query(0) - used: int = Query(0) - usescsv: str = Query(None) - number: int = Query(0) - webhook_url: str = Query(None) - webhook_headers: str = Query(None) - webhook_body: str = Query(None) - custom_url: str = Query(None) - - @property - def is_spent(self) -> bool: - return self.used >= self.uses - - def lnurl(self, req: Request) -> Lnurl: - if self.is_unique: - usescssv = self.usescsv.split(",") - tohash = self.id + self.unique_hash + usescssv[self.number] - multihash = shortuuid.uuid(name=tohash) - url = req.url_for( - "withdraw.api_lnurl_multi_response", - unique_hash=self.unique_hash, - id_unique_hash=multihash, - ) - else: - url = req.url_for( - "withdraw.api_lnurl_response", unique_hash=self.unique_hash - ) - - return lnurl_encode(url) - - def lnurl_response(self, req: Request) -> LnurlWithdrawResponse: - url = req.url_for( - name="withdraw.api_lnurl_callback", unique_hash=self.unique_hash - ) - return LnurlWithdrawResponse( - callback=ClearnetUrl(url, scheme="https"), - k1=self.k1, - minWithdrawable=MilliSatoshi(self.min_withdrawable * 1000), - maxWithdrawable=MilliSatoshi(self.max_withdrawable * 1000), - defaultDescription=self.title, - ) - - -class HashCheck(BaseModel): - hash: bool - lnurl: bool diff --git a/lnbits/extensions/withdraw/static/image/lnurl-withdraw.png b/lnbits/extensions/withdraw/static/image/lnurl-withdraw.png deleted file mode 100644 index 4f036423..00000000 Binary files a/lnbits/extensions/withdraw/static/image/lnurl-withdraw.png and /dev/null differ diff --git a/lnbits/extensions/withdraw/static/js/index.js b/lnbits/extensions/withdraw/static/js/index.js deleted file mode 100644 index ced78439..00000000 --- a/lnbits/extensions/withdraw/static/js/index.js +++ /dev/null @@ -1,323 +0,0 @@ -/* global Vue, VueQrcode, _, Quasar, LOCALE, windowMixin, LNbits */ - -Vue.component(VueQrcode.name, VueQrcode) - -var locationPath = [ - window.location.protocol, - '//', - window.location.host, - window.location.pathname -].join('') - -var mapWithdrawLink = function (obj) { - obj._data = _.clone(obj) - obj.date = Quasar.utils.date.formatDate( - new Date(obj.time * 1000), - 'YYYY-MM-DD HH:mm' - ) - obj.min_fsat = new Intl.NumberFormat(LOCALE).format(obj.min_withdrawable) - obj.max_fsat = new Intl.NumberFormat(LOCALE).format(obj.max_withdrawable) - obj.uses_left = obj.uses - obj.used - obj.print_url = [locationPath, 'print/', obj.id].join('') - obj.withdraw_url = [locationPath, obj.id].join('') - obj._data.use_custom = Boolean(obj.custom_url) - return obj -} - -const CUSTOM_URL = '/static/images/default_voucher.png' - -new Vue({ - el: '#vue', - mixins: [windowMixin], - data: function () { - return { - checker: null, - withdrawLinks: [], - withdrawLinksTable: { - columns: [ - {name: 'id', align: 'left', label: 'ID', field: 'id'}, - {name: 'title', align: 'left', label: 'Title', field: 'title'}, - { - name: 'wait_time', - align: 'right', - label: 'Wait', - field: 'wait_time' - }, - { - name: 'uses_left', - align: 'right', - label: 'Uses left', - field: 'uses_left' - }, - {name: 'min', align: 'right', label: 'Min (sat)', field: 'min_fsat'}, - {name: 'max', align: 'right', label: 'Max (sat)', field: 'max_fsat'} - ], - pagination: { - rowsPerPage: 10 - } - }, - nfcTagWriting: false, - formDialog: { - show: false, - secondMultiplier: 'seconds', - secondMultiplierOptions: ['seconds', 'minutes', 'hours'], - data: { - is_unique: false, - use_custom: false, - has_webhook: false - } - }, - simpleformDialog: { - show: false, - data: { - is_unique: true, - use_custom: false, - title: 'Vouchers', - min_withdrawable: 0, - wait_time: 1 - } - }, - qrCodeDialog: { - show: false, - data: null - } - } - }, - computed: { - sortedWithdrawLinks: function () { - return this.withdrawLinks.sort(function (a, b) { - return b.uses_left - a.uses_left - }) - } - }, - methods: { - getWithdrawLinks: function () { - var self = this - - LNbits.api - .request( - 'GET', - '/withdraw/api/v1/links?all_wallets=true', - this.g.user.wallets[0].inkey - ) - .then(function (response) { - self.withdrawLinks = response.data.map(function (obj) { - return mapWithdrawLink(obj) - }) - }) - .catch(function (error) { - clearInterval(self.checker) - LNbits.utils.notifyApiError(error) - }) - }, - closeFormDialog: function () { - this.formDialog.data = { - is_unique: false, - use_custom: false - } - }, - simplecloseFormDialog: function () { - this.simpleformDialog.data = { - is_unique: false, - use_custom: false - } - }, - openQrCodeDialog: function (linkId) { - var link = _.findWhere(this.withdrawLinks, {id: linkId}) - - this.qrCodeDialog.data = _.clone(link) - this.qrCodeDialog.data.url = - window.location.protocol + '//' + window.location.host - this.qrCodeDialog.show = true - }, - openUpdateDialog: function (linkId) { - var link = _.findWhere(this.withdrawLinks, {id: linkId}) - this.formDialog.data = _.clone(link._data) - this.formDialog.show = true - }, - sendFormData: function () { - var wallet = _.findWhere(this.g.user.wallets, { - id: this.formDialog.data.wallet - }) - var data = _.omit(this.formDialog.data, 'wallet') - - if (!data.use_custom) { - data.custom_url = null - } - - if (data.use_custom && !data?.custom_url) { - data.custom_url = CUSTOM_URL - } - - data.wait_time = - data.wait_time * - { - seconds: 1, - minutes: 60, - hours: 3600 - }[this.formDialog.secondMultiplier] - if (data.id) { - this.updateWithdrawLink(wallet, data) - } else { - this.createWithdrawLink(wallet, data) - } - }, - simplesendFormData: function () { - var wallet = _.findWhere(this.g.user.wallets, { - id: this.simpleformDialog.data.wallet - }) - var data = _.omit(this.simpleformDialog.data, 'wallet') - - data.wait_time = 1 - data.min_withdrawable = data.max_withdrawable - data.title = 'vouchers' - data.is_unique = true - - if (!data.use_custom) { - data.custom_url = null - } - - if (data.use_custom && !data?.custom_url) { - data.custom_url = '/static/images/default_voucher.png' - } - - if (data.id) { - this.updateWithdrawLink(wallet, data) - } else { - this.createWithdrawLink(wallet, data) - } - }, - updateWithdrawLink: function (wallet, data) { - var self = this - const body = _.pick( - data, - 'title', - 'min_withdrawable', - 'max_withdrawable', - 'uses', - 'wait_time', - 'is_unique', - 'webhook_url', - 'webhook_headers', - 'webhook_body', - 'custom_url' - ) - - if (data.has_webhook) { - body = { - ...body, - webhook_url: data.webhook_url, - webhook_headers: data.webhook_headers, - webhook_body: data.webhook_body - } - } - - LNbits.api - .request( - 'PUT', - '/withdraw/api/v1/links/' + data.id, - wallet.adminkey, - body - ) - .then(function (response) { - self.withdrawLinks = _.reject(self.withdrawLinks, function (obj) { - return obj.id === data.id - }) - self.withdrawLinks.push(mapWithdrawLink(response.data)) - self.formDialog.show = false - }) - .catch(function (error) { - LNbits.utils.notifyApiError(error) - }) - }, - createWithdrawLink: function (wallet, data) { - var self = this - - LNbits.api - .request('POST', '/withdraw/api/v1/links', wallet.adminkey, data) - .then(function (response) { - self.withdrawLinks.push(mapWithdrawLink(response.data)) - self.formDialog.show = false - self.simpleformDialog.show = false - }) - .catch(function (error) { - LNbits.utils.notifyApiError(error) - }) - }, - deleteWithdrawLink: function (linkId) { - var self = this - var link = _.findWhere(this.withdrawLinks, {id: linkId}) - - LNbits.utils - .confirmDialog('Are you sure you want to delete this withdraw link?') - .onOk(function () { - LNbits.api - .request( - 'DELETE', - '/withdraw/api/v1/links/' + linkId, - _.findWhere(self.g.user.wallets, {id: link.wallet}).adminkey - ) - .then(function (response) { - self.withdrawLinks = _.reject(self.withdrawLinks, function (obj) { - return obj.id === linkId - }) - }) - .catch(function (error) { - LNbits.utils.notifyApiError(error) - }) - }) - }, - writeNfcTag: async function (lnurl) { - try { - if (typeof NDEFReader == 'undefined') { - throw { - toString: function () { - return 'NFC not supported on this device or browser.' - } - } - } - - const ndef = new NDEFReader() - - this.nfcTagWriting = true - this.$q.notify({ - message: 'Tap your NFC tag to write the LNURL-withdraw link to it.' - }) - - await ndef.write({ - records: [{recordType: 'url', data: 'lightning:' + lnurl, lang: 'en'}] - }) - - this.nfcTagWriting = false - this.$q.notify({ - type: 'positive', - message: 'NFC tag written successfully.' - }) - } catch (error) { - this.nfcTagWriting = false - this.$q.notify({ - type: 'negative', - message: error - ? error.toString() - : 'An unexpected error has occurred.' - }) - } - }, - exportCSV() { - LNbits.utils.exportCSV( - this.withdrawLinksTable.columns, - this.withdrawLinks, - 'withdraw-links' - ) - } - }, - created: function () { - if (this.g.user.wallets.length) { - var getWithdrawLinks = this.getWithdrawLinks - getWithdrawLinks() - this.checker = setInterval(function () { - getWithdrawLinks() - }, 300000) - } - } -}) diff --git a/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html b/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html deleted file mode 100644 index ff88189d..00000000 --- a/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html +++ /dev/null @@ -1,204 +0,0 @@ - - - - - - GET /withdraw/api/v1/links -
Headers
- {"X-Api-Key": <invoice_key>}
-
Body (application/json)
-
- Returns 200 OK (application/json) -
- [<withdraw_link_object>, ...] -
Curl example
- curl -X GET {{ request.base_url }}withdraw/api/v1/links -H - "X-Api-Key: {{ user.wallets[0].inkey }}" - -
-
-
- - - - GET - /withdraw/api/v1/links/<withdraw_id> -
Headers
- {"X-Api-Key": <invoice_key>}
-
Body (application/json)
-
- Returns 201 CREATED (application/json) -
- {"lnurl": <string>} -
Curl example
- curl -X GET {{ request.base_url - }}withdraw/api/v1/links/<withdraw_id> -H "X-Api-Key: {{ - user.wallets[0].inkey }}" - -
-
-
- - - - POST /withdraw/api/v1/links -
Headers
- {"X-Api-Key": <admin_key>}
-
Body (application/json)
- {"title": <string>, "min_withdrawable": <integer>, - "max_withdrawable": <integer>, "uses": <integer>, - "wait_time": <integer>, "is_unique": <boolean>, - "webhook_url": <string>} -
- Returns 201 CREATED (application/json) -
- {"lnurl": <string>} -
Curl example
- curl -X POST {{ request.base_url }}withdraw/api/v1/links -d - '{"title": <string>, "min_withdrawable": <integer>, - "max_withdrawable": <integer>, "uses": <integer>, - "wait_time": <integer>, "is_unique": <boolean>, - "webhook_url": <string>}' -H "Content-type: application/json" -H - "X-Api-Key: {{ user.wallets[0].adminkey }}" - -
-
-
- - - - PUT - /withdraw/api/v1/links/<withdraw_id> -
Headers
- {"X-Api-Key": <admin_key>}
-
Body (application/json)
- {"title": <string>, "min_withdrawable": <integer>, - "max_withdrawable": <integer>, "uses": <integer>, - "wait_time": <integer>, "is_unique": <boolean>} -
- Returns 200 OK (application/json) -
- {"lnurl": <string>} -
Curl example
- curl -X PUT {{ request.base_url - }}withdraw/api/v1/links/<withdraw_id> -d '{"title": - <string>, "min_withdrawable": <integer>, - "max_withdrawable": <integer>, "uses": <integer>, - "wait_time": <integer>, "is_unique": <boolean>}' -H - "Content-type: application/json" -H "X-Api-Key: {{ - user.wallets[0].adminkey }}" - -
-
-
- - - - DELETE - /withdraw/api/v1/links/<withdraw_id> -
Headers
- {"X-Api-Key": <admin_key>}
-
Returns 204 NO CONTENT
- -
Curl example
- curl -X DELETE {{ request.base_url - }}withdraw/api/v1/links/<withdraw_id> -H "X-Api-Key: {{ - user.wallets[0].adminkey }}" - -
-
-
- - - - GET - /withdraw/api/v1/links/<the_hash>/<lnurl_id> -
Headers
- {"X-Api-Key": <invoice_key>}
-
Body (application/json)
-
- Returns 201 CREATED (application/json) -
- {"status": <bool>} -
Curl example
- curl -X GET {{ request.base_url - }}withdraw/api/v1/links/<the_hash>/<lnurl_id> -H - "X-Api-Key: {{ user.wallets[0].inkey }}" - -
-
-
- - - - GET - /withdraw/img/<lnurl_id> -
Curl example
- curl -X GET {{ request.base_url }}withdraw/img/<lnurl_id>" - -
-
-
-
diff --git a/lnbits/extensions/withdraw/templates/withdraw/_lnurl.html b/lnbits/extensions/withdraw/templates/withdraw/_lnurl.html deleted file mode 100644 index f6b52050..00000000 --- a/lnbits/extensions/withdraw/templates/withdraw/_lnurl.html +++ /dev/null @@ -1,32 +0,0 @@ - - - -

- WARNING: LNURL must be used over https or TOR
- LNURL is a range of lightning-network standards that allow us to use - lightning-network differently. An LNURL withdraw is the permission for - someone to pull a certain amount of funds from a lightning wallet. In - this extension time is also added - an amount can be withdraw over a - period of time. A typical use case for an LNURL withdraw is a faucet, - although it is a very powerful technology, with much further reaching - implications. For example, an LNURL withdraw could be minted to pay for - a subscription service. -

-

- Exploring LNURL and finding use cases, is really helping inform - lightning protocol development, rather than the protocol dictating how - lightning-network should be engaged with. -

- Check - Awesome LNURL - for further information. -
-
-
diff --git a/lnbits/extensions/withdraw/templates/withdraw/csv.html b/lnbits/extensions/withdraw/templates/withdraw/csv.html deleted file mode 100644 index 62902905..00000000 --- a/lnbits/extensions/withdraw/templates/withdraw/csv.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "print.html" %} {% block page %} {% for page in link %} {% for threes -in page %} {% for one in threes %} {{one}}, {% endfor %} {% endfor %} {% endfor -%} {% endblock %} {% block scripts %} - -{% endblock %} diff --git a/lnbits/extensions/withdraw/templates/withdraw/display.html b/lnbits/extensions/withdraw/templates/withdraw/display.html deleted file mode 100644 index 3ef545c3..00000000 --- a/lnbits/extensions/withdraw/templates/withdraw/display.html +++ /dev/null @@ -1,68 +0,0 @@ -{% extends "public.html" %} {% block page %} -
-
- - -
- {% if link.is_spent %} - Withdraw is spent. - {% endif %} - - - - - - -
-
- Copy LNURL - -
-
-
-
-
- - -
- LNbits LNURL-withdraw link -
-

- Use a LNURL compatible bitcoin wallet to claim the sats. -

-
- - - {% include "withdraw/_lnurl.html" %} - -
-
-
-{% endblock %} {% block scripts %} - -{% endblock %} diff --git a/lnbits/extensions/withdraw/templates/withdraw/index.html b/lnbits/extensions/withdraw/templates/withdraw/index.html deleted file mode 100644 index 3ae244e6..00000000 --- a/lnbits/extensions/withdraw/templates/withdraw/index.html +++ /dev/null @@ -1,471 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} {% block page %} -
-
- - - Quick vouchers - Advanced withdraw link(s) - - - - - -
-
-
Withdraw links
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
-
- -
- - -
- {{SITE_TITLE}} LNURL-withdraw extension -
-
- - - - {% include "withdraw/_api_docs.html" %} - - {% include "withdraw/_lnurl.html" %} - - -
-
- - - - - - - - - - -
-
- - -
-
- - -
-
- - - - - - - - - - - Use a custom voucher design - You can use an LNbits voucher design or a custom - one - - - - - - - - - - - Use unique withdraw QR codes to reduce `assmilking` - - This is recommended if you are sharing the links on social - media or print QR codes. - - - -
- Update withdraw link - Create withdraw link - Cancel -
-
-
-
- - - - - - - - - - - - - - - Use a custom voucher design - You can use an LNbits voucher design or a custom - one - - - - - -
- Create vouchers - Cancel -
-
-
-
- - - - - - {% raw %} - -

- ID: {{ qrCodeDialog.data.id }}
- Unique: {{ qrCodeDialog.data.is_unique }} - (QR code will change after each withdrawal)
- Max. withdrawable: {{ - qrCodeDialog.data.max_withdrawable }} sat
- Wait time: {{ qrCodeDialog.data.wait_time }} seconds
- Withdraws: {{ qrCodeDialog.data.used }} / {{ - qrCodeDialog.data.uses }} - -

- {% endraw %} -
- Copy LNURL - Copy sharable link - - Write to NFC - Print - Close -
-
-
-
-{% endblock %} diff --git a/lnbits/extensions/withdraw/templates/withdraw/print_qr.html b/lnbits/extensions/withdraw/templates/withdraw/print_qr.html deleted file mode 100644 index df4ca7d7..00000000 --- a/lnbits/extensions/withdraw/templates/withdraw/print_qr.html +++ /dev/null @@ -1,71 +0,0 @@ -{% extends "print.html" %} {% block page %} - -
-
- {% for page in link %} - - - {% for threes in page %} - - {% for one in threes %} - - {% endfor %} - - {% endfor %} -
-
- -
-
-
- {% endfor %} -
-
-{% endblock %} {% block styles %} - -{% endblock %} {% block scripts %} - -{% endblock %} diff --git a/lnbits/extensions/withdraw/templates/withdraw/print_qr_custom.html b/lnbits/extensions/withdraw/templates/withdraw/print_qr_custom.html deleted file mode 100644 index ca47cec4..00000000 --- a/lnbits/extensions/withdraw/templates/withdraw/print_qr_custom.html +++ /dev/null @@ -1,113 +0,0 @@ -{% extends "print.html" %} {% block page %} - -
-
- {% for page in link %} - - {% for one in page %} -
- ... - {{ amt }} sats -
- -
-
- {% endfor %} -
- {% endfor %} -
-
-{% endblock %} {% block styles %} - -{% endblock %} {% block scripts %} - -{% endblock %} diff --git a/lnbits/extensions/withdraw/views.py b/lnbits/extensions/withdraw/views.py deleted file mode 100644 index e8e5719a..00000000 --- a/lnbits/extensions/withdraw/views.py +++ /dev/null @@ -1,149 +0,0 @@ -from http import HTTPStatus -from io import BytesIO - -import pyqrcode -from fastapi import Depends, HTTPException, Request -from fastapi.templating import Jinja2Templates -from starlette.responses import HTMLResponse, StreamingResponse - -from lnbits.core.models import User -from lnbits.decorators import check_user_exists - -from . import withdraw_ext, withdraw_renderer -from .crud import chunks, get_withdraw_link - -templates = Jinja2Templates(directory="templates") - - -@withdraw_ext.get("/", response_class=HTMLResponse) -async def index(request: Request, user: User = Depends(check_user_exists)): - return withdraw_renderer().TemplateResponse( - "withdraw/index.html", {"request": request, "user": user.dict()} - ) - - -@withdraw_ext.get("/{link_id}", response_class=HTMLResponse) -async def display(request: Request, link_id): - link = await get_withdraw_link(link_id, 0) - - if not link: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Withdraw link does not exist." - ) - return withdraw_renderer().TemplateResponse( - "withdraw/display.html", - { - "request": request, - "link": link.dict(), - "lnurl": link.lnurl(req=request), - "unique": True, - }, - ) - - -@withdraw_ext.get("/img/{link_id}", response_class=StreamingResponse) -async def img(request: Request, link_id): - link = await get_withdraw_link(link_id, 0) - if not link: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Withdraw link does not exist." - ) - qr = pyqrcode.create(link.lnurl(request)) - stream = BytesIO() - qr.svg(stream, scale=3) - stream.seek(0) - - async def _generator(stream: BytesIO): - yield stream.getvalue() - - return StreamingResponse( - _generator(stream), - headers={ - "Content-Type": "image/svg+xml", - "Cache-Control": "no-cache, no-store, must-revalidate", - "Pragma": "no-cache", - "Expires": "0", - }, - ) - - -@withdraw_ext.get("/print/{link_id}", response_class=HTMLResponse) -async def print_qr(request: Request, link_id): - link = await get_withdraw_link(link_id) - if not link: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Withdraw link does not exist." - ) - # response.status_code = HTTPStatus.NOT_FOUND - # return "Withdraw link does not exist." - - if link.uses == 0: - - return withdraw_renderer().TemplateResponse( - "withdraw/print_qr.html", - {"request": request, "link": link.dict(), "unique": False}, - ) - links = [] - count = 0 - - for x in link.usescsv.split(","): - linkk = await get_withdraw_link(link_id, count) - if not linkk: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Withdraw link does not exist." - ) - links.append(str(linkk.lnurl(request))) - count = count + 1 - page_link = list(chunks(links, 2)) - linked = list(chunks(page_link, 5)) - - if link.custom_url: - return withdraw_renderer().TemplateResponse( - "withdraw/print_qr_custom.html", - { - "request": request, - "link": page_link, - "unique": True, - "custom_url": link.custom_url, - "amt": link.max_withdrawable, - }, - ) - - return withdraw_renderer().TemplateResponse( - "withdraw/print_qr.html", {"request": request, "link": linked, "unique": True} - ) - - -@withdraw_ext.get("/csv/{link_id}", response_class=HTMLResponse) -async def csv(request: Request, link_id): - link = await get_withdraw_link(link_id) - if not link: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Withdraw link does not exist." - ) - # response.status_code = HTTPStatus.NOT_FOUND - # return "Withdraw link does not exist." - - if link.uses == 0: - - return withdraw_renderer().TemplateResponse( - "withdraw/csv.html", - {"request": request, "link": link.dict(), "unique": False}, - ) - links = [] - count = 0 - - for x in link.usescsv.split(","): - linkk = await get_withdraw_link(link_id, count) - if not linkk: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Withdraw link does not exist." - ) - links.append(str(linkk.lnurl(request))) - count = count + 1 - page_link = list(chunks(links, 2)) - linked = list(chunks(page_link, 5)) - - return withdraw_renderer().TemplateResponse( - "withdraw/csv.html", {"request": request, "link": linked, "unique": True} - ) diff --git a/lnbits/extensions/withdraw/views_api.py b/lnbits/extensions/withdraw/views_api.py deleted file mode 100644 index 525796c9..00000000 --- a/lnbits/extensions/withdraw/views_api.py +++ /dev/null @@ -1,128 +0,0 @@ -from http import HTTPStatus - -from fastapi import Depends, HTTPException, Query, Request -from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl - -from lnbits.core.crud import get_user -from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key - -from . import withdraw_ext -from .crud import ( - create_withdraw_link, - delete_withdraw_link, - get_hash_check, - get_withdraw_link, - get_withdraw_links, - update_withdraw_link, -) -from .models import CreateWithdrawData - - -@withdraw_ext.get("/api/v1/links", status_code=HTTPStatus.OK) -async def api_links( - req: Request, - wallet: WalletTypeInfo = Depends(get_key_type), - all_wallets: bool = Query(False), -): - wallet_ids = [wallet.wallet.id] - - if all_wallets: - user = await get_user(wallet.wallet.user) - wallet_ids = user.wallet_ids if user else [] - - try: - return [ - {**link.dict(), **{"lnurl": link.lnurl(req)}} - for link in await get_withdraw_links(wallet_ids) - ] - - except LnurlInvalidUrl: - raise HTTPException( - status_code=HTTPStatus.UPGRADE_REQUIRED, - detail="LNURLs need to be delivered over a publically accessible `https` domain or Tor.", - ) - - -@withdraw_ext.get("/api/v1/links/{link_id}", status_code=HTTPStatus.OK) -async def api_link_retrieve( - link_id: str, request: Request, wallet: WalletTypeInfo = Depends(get_key_type) -): - link = await get_withdraw_link(link_id, 0) - - if not link: - raise HTTPException( - detail="Withdraw link does not exist.", status_code=HTTPStatus.NOT_FOUND - ) - - if link.wallet != wallet.wallet.id: - raise HTTPException( - detail="Not your withdraw link.", status_code=HTTPStatus.FORBIDDEN - ) - return {**link.dict(), **{"lnurl": link.lnurl(request)}} - - -@withdraw_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED) -@withdraw_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK) -async def api_link_create_or_update( - req: Request, - data: CreateWithdrawData, - link_id: str = Query(None), - wallet: WalletTypeInfo = Depends(require_admin_key), -): - if data.uses > 250: - raise HTTPException(detail="250 uses max.", status_code=HTTPStatus.BAD_REQUEST) - - if data.min_withdrawable < 1: - raise HTTPException( - detail="Min must be more than 1.", status_code=HTTPStatus.BAD_REQUEST - ) - - if data.max_withdrawable < data.min_withdrawable: - raise HTTPException( - detail="`max_withdrawable` needs to be at least `min_withdrawable`.", - status_code=HTTPStatus.BAD_REQUEST, - ) - - if link_id: - link = await get_withdraw_link(link_id, 0) - if not link: - raise HTTPException( - detail="Withdraw link does not exist.", status_code=HTTPStatus.NOT_FOUND - ) - if link.wallet != wallet.wallet.id: - raise HTTPException( - detail="Not your withdraw link.", status_code=HTTPStatus.FORBIDDEN - ) - link = await update_withdraw_link(link_id, **data.dict()) - else: - link = await create_withdraw_link(wallet_id=wallet.wallet.id, data=data) - assert link - return {**link.dict(), **{"lnurl": link.lnurl(req)}} - - -@withdraw_ext.delete("/api/v1/links/{link_id}", status_code=HTTPStatus.OK) -async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(require_admin_key)): - link = await get_withdraw_link(link_id) - - if not link: - raise HTTPException( - detail="Withdraw link does not exist.", status_code=HTTPStatus.NOT_FOUND - ) - - if link.wallet != wallet.wallet.id: - raise HTTPException( - detail="Not your withdraw link.", status_code=HTTPStatus.FORBIDDEN - ) - - await delete_withdraw_link(link_id) - return {"success": True} - - -@withdraw_ext.get( - "/api/v1/links/{the_hash}/{lnurl_id}", - status_code=HTTPStatus.OK, - dependencies=[Depends(get_key_type)], -) -async def api_hash_retrieve(the_hash, lnurl_id): - hashCheck = await get_hash_check(the_hash, lnurl_id) - return hashCheck