remove withdraw (#1517)

This commit is contained in:
dni ⚡ 2023-02-17 15:19:39 +01:00 committed by GitHub
parent 967f46f82a
commit 1eb3df60fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 0 additions and 2238 deletions

View file

@ -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)

View file

@ -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

View file

@ -1,6 +0,0 @@
{
"name": "LNURLw",
"short_description": "Make LNURL withdraw links",
"tile": "/withdraw/static/image/lnurl-withdraw.png",
"contributors": ["arcbtc", "eillarra"]
}

View file

@ -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)

View file

@ -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)

View file

@ -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;")

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View file

@ -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)
}
}
})

View file

@ -1,204 +0,0 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-btn flat label="Swagger API" type="a" href="../docs#/withdraw"></q-btn>
<q-expansion-item
group="api"
dense
expand-separator
label="List withdraw links"
>
<q-card>
<q-card-section>
<code><span class="text-blue">GET</span> /withdraw/api/v1/links</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;withdraw_link_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}withdraw/api/v1/links -H
"X-Api-Key: {{ user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Get a withdraw link"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/withdraw/api/v1/links/&lt;withdraw_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url
}}withdraw/api/v1/links/&lt;withdraw_id&gt; -H "X-Api-Key: {{
user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Create a withdraw link"
>
<q-card>
<q-card-section>
<code><span class="text-green">POST</span> /withdraw/api/v1/links</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"title": &lt;string&gt;, "min_withdrawable": &lt;integer&gt;,
"max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;,
"webhook_url": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url }}withdraw/api/v1/links -d
'{"title": &lt;string&gt;, "min_withdrawable": &lt;integer&gt;,
"max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;,
"webhook_url": &lt;string&gt;}' -H "Content-type: application/json" -H
"X-Api-Key: {{ user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Update a withdraw link"
>
<q-card>
<q-card-section>
<code
><span class="text-green">PUT</span>
/withdraw/api/v1/links/&lt;withdraw_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"title": &lt;string&gt;, "min_withdrawable": &lt;integer&gt;,
"max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X PUT {{ request.base_url
}}withdraw/api/v1/links/&lt;withdraw_id&gt; -d '{"title":
&lt;string&gt;, "min_withdrawable": &lt;integer&gt;,
"max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}' -H
"Content-type: application/json" -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Delete a withdraw link"
>
<q-card>
<q-card-section>
<code
><span class="text-pink">DELETE</span>
/withdraw/api/v1/links/&lt;withdraw_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
<code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.base_url
}}withdraw/api/v1/links/&lt;withdraw_id&gt; -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Get hash check (for captchas to prevent milking)"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/withdraw/api/v1/links/&lt;the_hash&gt;/&lt;lnurl_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>{"status": &lt;bool&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url
}}withdraw/api/v1/links/&lt;the_hash&gt;/&lt;lnurl_id&gt; -H
"X-Api-Key: {{ user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Get image to embed"
class="q-pb-md"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/withdraw/img/&lt;lnurl_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}withdraw/img/&lt;lnurl_id&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>

View file

@ -1,32 +0,0 @@
<q-expansion-item group="extras" icon="info" label="Powered by LNURL">
<q-card>
<q-card-section>
<p>
<b>WARNING: LNURL must be used over https or TOR</b><br />
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.
</p>
<p>
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.
</p>
<small
>Check
<a
class="text-secondary"
href="https://github.com/fiatjaf/awesome-lnurl"
target="_blank"
>Awesome LNURL</a
>
for further information.</small
>
</q-card-section>
</q-card>
</q-expansion-item>

View file

@ -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 %}
<script>
new Vue({
el: '#vue',
data: function () {
return {}
}
})
</script>
{% endblock %}

View file

@ -1,68 +0,0 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-sm-6 col-md-5 col-lg-4">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<div class="text-center">
{% if link.is_spent %}
<q-badge color="red" class="q-mb-md">Withdraw is spent.</q-badge>
{% endif %}
<a class="text-secondary" href="lightning:{{ lnurl }}">
<q-responsive :ratio="1" class="q-mx-md">
<qrcode
:value="this.here + '/?lightning={{lnurl }}'"
:options="{width: 800}"
class="rounded-borders"
>
</qrcode>
</q-responsive>
</a>
</div>
<div class="row q-mt-lg q-gutter-sm">
<q-btn outline color="grey" @click="copyText('{{ lnurl }}')"
>Copy LNURL</q-btn
>
<q-btn
outline
color="grey"
icon="nfc"
@click="writeNfcTag(' {{ lnurl }} ')"
:disable="nfcTagWriting"
></q-btn>
</div>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-sm-6 col-md-5 col-lg-4 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-mb-sm q-mt-none">
LNbits LNURL-withdraw link
</h6>
<p class="q-my-none">
Use a LNURL compatible bitcoin wallet to claim the sats.
</p>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "withdraw/_lnurl.html" %} </q-list>
</q-card-section>
</q-card>
</div>
</div>
{% endblock %} {% block scripts %}
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
here: location.protocol + '//' + location.host,
nfcTagWriting: false
}
}
})
</script>
{% endblock %}

View file

@ -1,471 +0,0 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block scripts %} {{ window_vars(user) }}
<script src="/withdraw/static/js/index.js"></script>
{% endblock %} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="primary" @click="simpleformDialog.show = true"
>Quick vouchers</q-btn
>
<q-btn unelevated color="primary" @click="formDialog.show = true"
>Advanced withdraw link(s)</q-btn
>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Withdraw links</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div>
</div>
<q-table
dense
flat
:data="sortedWithdrawLinks"
row-key="id"
:columns="withdrawLinksTable.columns"
:pagination.sync="withdrawLinksTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="launch"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.withdraw_url"
target="_blank"
>
<q-tooltip> shareable link </q-tooltip></q-btn
>
<q-btn
unelevated
dense
size="xs"
icon="web_asset"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="'/withdraw/img/' + props.row.id"
target="_blank"
><q-tooltip> embeddable image </q-tooltip></q-btn
>
<q-btn
unelevated
dense
size="xs"
icon="reorder"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="'/withdraw/csv/' + props.row.id"
target="_blank"
><q-tooltip> csv list </q-tooltip></q-btn
>
<q-btn
unelevated
dense
size="xs"
icon="visibility"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openQrCodeDialog(props.row.id)"
><q-tooltip> view LNURL </q-tooltip></q-btn
>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td>
<q-icon v-if="props.row.webhook_url" size="14px" name="http">
<q-tooltip>Webhook to {{ props.row.webhook_url}}</q-tooltip>
</q-icon>
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="openUpdateDialog(props.row.id)"
icon="edit"
color="light-blue"
></q-btn>
<q-btn
flat
dense
size="xs"
@click="deleteWithdrawLink(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} LNURL-withdraw extension
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "withdraw/_api_docs.html" %}
<q-separator></q-separator>
{% include "withdraw/_lnurl.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendFormData" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="formDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
>
</q-select>
<q-input
filled
dense
v-model.trim="formDialog.data.title"
type="text"
label="Link title *"
></q-input>
<q-input
filled
dense
v-model.number="formDialog.data.min_withdrawable"
type="number"
min="10"
label="Min withdrawable (sat, at least 10) *"
></q-input>
<q-input
filled
dense
v-model.number="formDialog.data.max_withdrawable"
type="number"
min="10"
label="Max withdrawable (sat, at least 10) *"
></q-input>
<q-input
filled
dense
v-model.number="formDialog.data.uses"
type="number"
max="250"
:default="1"
label="Amount of uses *"
></q-input>
<div class="row q-col-gutter-none">
<div class="col-8">
<q-input
filled
dense
v-model.number="formDialog.data.wait_time"
type="number"
:default="1"
label="Time between withdrawals *"
>
</q-input>
</div>
<div class="col-4 q-pl-xs">
<q-select
filled
dense
v-model="formDialog.secondMultiplier"
:options="formDialog.secondMultiplierOptions"
>
</q-select>
</div>
</div>
<q-toggle
label="Webhook"
color="secodary"
v-model="formDialog.data.has_webhook"
></q-toggle>
<q-input
v-if="formDialog.data.has_webhook"
filled
dense
v-model="formDialog.data.webhook_url"
type="text"
label="Webhook URL (optional)"
hint="A URL to be called whenever this link gets used."
></q-input>
<q-input
v-if="formDialog.data.has_webhook"
filled
dense
v-model="formDialog.data.webhook_headers"
type="text"
label="Webhook Headers (optional)"
hint="Custom data as JSON string, send headers along with the webhook."
></q-input>
<q-input
v-if="formDialog.data.has_webhook"
filled
dense
v-model="formDialog.data.webhook_body"
type="text"
label="Webhook custom data (optional)"
hint="Custom data as JSON string, will get posted along with webhook 'body' field."
></q-input>
<q-list>
<q-item tag="label" class="rounded-borders">
<q-item-section avatar>
<q-checkbox
v-model="formDialog.data.use_custom"
color="primary"
></q-checkbox>
</q-item-section>
<q-item-section>
<q-item-label>Use a custom voucher design </q-item-label>
<q-item-label caption
>You can use an LNbits voucher design or a custom
one</q-item-label
>
</q-item-section>
</q-item>
</q-list>
<q-input
v-if="formDialog.data.use_custom"
filled
dense
v-model="formDialog.data.custom_url"
type="text"
label="Custom design .png (optional)"
hint="Enter a URL if you want to use a custom design or leave blank for LNbits designed vouchers!"
></q-input>
<q-list>
<q-item tag="label" class="rounded-borders">
<q-item-section avatar>
<q-checkbox
v-model="formDialog.data.is_unique"
color="primary"
></q-checkbox>
</q-item-section>
<q-item-section>
<q-item-label
>Use unique withdraw QR codes to reduce `assmilking`
</q-item-label>
<q-item-label caption
>This is recommended if you are sharing the links on social
media or print QR codes.</q-item-label
>
</q-item-section>
</q-item>
</q-list>
<div class="row q-mt-lg">
<q-btn
v-if="formDialog.data.id"
unelevated
color="primary"
type="submit"
>Update withdraw link</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="
formDialog.data.wallet == null ||
formDialog.data.title == null ||
(formDialog.data.min_withdrawable == null || formDialog.data.min_withdrawable < 1) ||
(
formDialog.data.max_withdrawable == null ||
formDialog.data.max_withdrawable < 1 ||
formDialog.data.max_withdrawable < formDialog.data.min_withdrawable
) ||
formDialog.data.uses == null ||
formDialog.data.wait_time == null"
type="submit"
>Create withdraw link</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
<q-dialog
v-model="simpleformDialog.show"
position="top"
@hide="simplecloseFormDialog"
>
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="simplesendFormData" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="simpleformDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
>
</q-select>
<q-input
filled
dense
v-model.number="simpleformDialog.data.max_withdrawable"
type="number"
min="10"
label="Withdraw amount per voucher (sat, at least 10)"
></q-input>
<q-input
filled
dense
v-model.number="simpleformDialog.data.uses"
type="number"
max="250"
:default="1"
label="Number of vouchers"
></q-input>
<q-list>
<q-item tag="label" class="rounded-borders">
<q-item-section avatar>
<q-checkbox
v-model="simpleformDialog.data.use_custom"
color="primary"
></q-checkbox>
</q-item-section>
<q-item-section>
<q-item-label>Use a custom voucher design </q-item-label>
<q-item-label caption
>You can use an LNbits voucher design or a custom
one</q-item-label
>
</q-item-section>
</q-item>
</q-list>
<q-input
v-if="simpleformDialog.data.use_custom"
filled
dense
v-model="simpleformDialog.data.custom_url"
type="text"
label="Custom design .png (optional)"
hint="Enter a URL if you want to use a custom design or leave blank for LNbits designed vouchers!"
></q-input>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="
simpleformDialog.data.wallet == null ||
simpleformDialog.data.max_withdrawable == null ||
simpleformDialog.data.max_withdrawable < 1 ||
simpleformDialog.data.uses == null"
type="submit"
>Create vouchers</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="qrCodeDialog.show" position="top">
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode
:value="qrCodeDialog.data.url + '/?lightning=' + qrCodeDialog.data.lnurl"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
{% raw %}
</q-responsive>
<p style="word-break: break-all">
<strong>ID:</strong> {{ qrCodeDialog.data.id }}<br />
<strong>Unique:</strong> {{ qrCodeDialog.data.is_unique }}<span
v-if="qrCodeDialog.data.is_unique"
class="text-deep-purple"
>
(QR code will change after each withdrawal)</span
><br />
<strong>Max. withdrawable:</strong> {{
qrCodeDialog.data.max_withdrawable }} sat<br />
<strong>Wait time:</strong> {{ qrCodeDialog.data.wait_time }} seconds<br />
<strong>Withdraws:</strong> {{ qrCodeDialog.data.used }} / {{
qrCodeDialog.data.uses }}
<q-linear-progress
:value="qrCodeDialog.data.used / qrCodeDialog.data.uses"
color="primary"
class="q-mt-sm"
></q-linear-progress>
</p>
{% endraw %}
<div class="row q-mt-lg q-gutter-sm">
<q-btn
outline
color="grey"
@click="copyText(qrCodeDialog.data.lnurl, 'LNURL copied to clipboard!')"
class="q-ml-sm"
>Copy LNURL</q-btn
>
<q-btn
outline
color="grey"
icon="link"
@click="copyText(qrCodeDialog.data.withdraw_url, 'Link copied to clipboard!')"
><q-tooltip>Copy sharable link</q-tooltip>
</q-btn>
<q-btn
outline
color="grey"
icon="nfc"
@click="writeNfcTag(qrCodeDialog.data.lnurl)"
:disable="nfcTagWriting"
><q-tooltip>Write to NFC</q-tooltip></q-btn
>
<q-btn
outline
color="grey"
icon="print"
type="a"
:href="qrCodeDialog.data.print_url"
target="_blank"
><q-tooltip>Print</q-tooltip></q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
</div>
{% endblock %}

View file

@ -1,71 +0,0 @@
{% extends "print.html" %} {% block page %}
<div class="row">
<div class="" id="vue">
{% for page in link %}
<page size="A4" id="pdfprint">
<table style="width: 100%">
{% for threes in page %}
<tr style="height: 59.4mm">
{% for one in threes %}
<td style="width: 105mm">
<center>
<qrcode
:value="theurl + '/?lightning={{one}}'"
:options="{width: 150}"
></qrcode>
</center>
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
</page>
{% endfor %}
</div>
</div>
{% endblock %} {% block styles %}
<style>
body {
background: rgb(204, 204, 204);
}
page {
background: white;
display: block;
margin: 0 auto;
margin-bottom: 0.5cm;
box-shadow: 0 0 0.5cm rgba(0, 0, 0, 0.5);
}
page[size='A4'] {
width: 21cm;
height: 29.7cm;
}
@media print {
body,
page {
margin: 0px !important;
box-shadow: none !important;
}
.q-page {
padding: 0px !important;
}
}
</style>
{% endblock %} {% block scripts %}
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
data: function () {
return {
theurl: location.protocol + '//' + location.host,
printDialog: {
show: true,
data: null
}
}
}
})
</script>
{% endblock %}

View file

@ -1,113 +0,0 @@
{% extends "print.html" %} {% block page %}
<div class="row">
<div class="" id="vue">
{% for page in link %}
<page size="A4" id="pdfprint">
{% for one in page %}
<div class="wrapper">
<img src="{{custom_url}}" alt="..." />
<span>{{ amt }} sats</span>
<div class="lnurlw">
<qrcode
:value="theurl + '/?lightning={{one}}'"
:options="{width: 95, margin: 1}"
></qrcode>
</div>
</div>
{% endfor %}
</page>
{% endfor %}
</div>
</div>
{% endblock %} {% block styles %}
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400');
body {
background: rgb(204, 204, 204);
}
page {
background: white;
display: block;
margin: 0 auto;
margin-bottom: 0.5cm;
box-shadow: 0 0 0.5cm rgba(0, 0, 0, 0.5);
}
page[size='A4'] {
width: 21cm;
height: 29.7cm;
}
.wrapper {
position: relative;
margin-bottom: 1rem;
padding: 1rem;
width: fit-content;
}
.wrapper span {
display: block;
position: absolute;
font-family: 'Inter';
font-size: 0.75rem;
color: #fff;
top: calc(3.2mm + 1rem);
right: calc(4mm + 1rem);
}
.wrapper img {
display: block;
width: 187mm;
height: auto;
}
.wrapper .lnurlw {
display: block;
position: absolute;
top: calc(7.3mm + 1rem);
left: calc(7.5mm + 1rem);
transform: rotate(45deg);
}
@media print {
body,
page {
margin: 0px !important;
box-shadow: none !important;
}
.q-page,
.wrapper {
padding: 0px !important;
}
.wrapper span {
top: 3mm;
right: 4mm;
}
.wrapper .lnurlw {
display: block;
position: absolute;
top: 7.3mm;
left: 7.5mm;
transform: rotate(45deg);
}
}
</style>
{% endblock %} {% block scripts %}
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
data: function () {
return {
theurl: location.protocol + '//' + location.host,
printDialog: {
show: true,
data: null
},
links: []
}
},
created() {
this.links = '{{ link | tojson }}'
}
})
</script>
{% endblock %}

View file

@ -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}
)

View file

@ -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