diff --git a/.gitignore b/.gitignore
index 0152b6e..8d6aa84 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,6 @@ __pycache__
node_modules
.mypy_cache
.venv
+
+# test data
+data/
diff --git a/README.md b/README.md
index a1b5bc6..cc983f3 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,7 @@ LNbits Quick Vouchers allows you to easily create a batch of LNURLw's QR codes t
- 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_
+ - optionally set an expiration time in days, weeks, or months. The expiration starts from the date the vouchers are created. Months are counted as 30 days
2. You can now print, share, display your LNURLw links or QR codes\

- on details you can print the vouchers\
@@ -39,6 +40,7 @@ LNbits Quick Vouchers allows you to easily create a batch of LNURLw's QR codes t
- 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
+ - optionally set an expiration time in days, weeks, or months. The expiration starts from the date the LNURLw is created. Months are counted as 30 days
- 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
@@ -47,4 +49,6 @@ LNbits Quick Vouchers allows you to easily create a batch of LNURLw's QR codes t
**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!
+If no expiration time is set, the LNURLw never expires. Existing LNURLw links and vouchers keep working without an expiration unless one is added later.
+

diff --git a/crud.py b/crud.py
index bca32d8..3531106 100644
--- a/crud.py
+++ b/crud.py
@@ -33,6 +33,8 @@ async def create_withdraw_link(
webhook_headers=data.webhook_headers,
webhook_body=data.webhook_body,
custom_url=data.custom_url,
+ enabled=data.enabled,
+ validity_seconds=data.validity_seconds,
number=0,
)
await db.insert("withdraw.withdraw_link", withdraw_link)
diff --git a/migrations.py b/migrations.py
index dccccde..2a802e4 100644
--- a/migrations.py
+++ b/migrations.py
@@ -143,3 +143,10 @@ async def m008_add_enabled_column(db):
async def m009_add_currency(db):
await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN currency TEXT;")
+
+
+async def m010_add_validity_seconds(db):
+ await db.execute(
+ "ALTER TABLE withdraw.withdraw_link "
+ "ADD COLUMN validity_seconds INTEGER NOT NULL DEFAULT 0;"
+ )
diff --git a/models.py b/models.py
index 9c5cc86..1bf87d2 100644
--- a/models.py
+++ b/models.py
@@ -1,4 +1,4 @@
-from datetime import datetime
+from datetime import datetime, timedelta
from fastapi import Query
from pydantic import BaseModel, Field
@@ -17,6 +17,7 @@ class CreateWithdrawData(BaseModel):
custom_url: str = Query(None)
enabled: bool = Query(True)
currency: str = Query(None)
+ validity_seconds: int = Query(0, ge=0)
class WithdrawLink(BaseModel):
@@ -41,6 +42,7 @@ class WithdrawLink(BaseModel):
created_at: datetime
enabled: bool = Query(True)
currency: str = Query(None)
+ validity_seconds: int = Query(0)
lnurl: str | None = Field(
default=None,
no_database=True,
@@ -61,6 +63,17 @@ class WithdrawLink(BaseModel):
def is_spent(self) -> bool:
return self.used >= self.uses
+ @property
+ def expires_at(self) -> datetime | None:
+ if self.validity_seconds <= 0:
+ return None
+ return self.created_at + timedelta(seconds=self.validity_seconds)
+
+ @property
+ def is_expired(self) -> bool:
+ expires_at = self.expires_at
+ return bool(expires_at and datetime.now(expires_at.tzinfo) > expires_at)
+
class HashCheck(BaseModel):
hash: bool
diff --git a/static/js/index.js b/static/js/index.js
index c19c145..57e78ff 100644
--- a/static/js/index.js
+++ b/static/js/index.js
@@ -2,6 +2,27 @@ const mapWithdrawLink = function (obj) {
obj._data = _.clone(obj)
obj.uses_left = obj.uses - obj.used
obj._data.use_custom = Boolean(obj.custom_url)
+ obj.expires_at = obj.validity_seconds
+ ? new Date(
+ new Date(obj.created_at).getTime() + obj.validity_seconds * 1000
+ ).toISOString()
+ : null
+ obj.is_expired = obj.expires_at
+ ? new Date(obj.expires_at) < new Date()
+ : false
+ obj._data.validity_multiplier = 'days'
+ obj._data.validity_amount = null
+ if (obj.validity_seconds) {
+ if (obj.validity_seconds % 2592000 === 0) {
+ obj._data.validity_multiplier = 'months'
+ obj._data.validity_amount = obj.validity_seconds / 2592000
+ } else if (obj.validity_seconds % 604800 === 0) {
+ obj._data.validity_multiplier = 'weeks'
+ obj._data.validity_amount = obj.validity_seconds / 604800
+ } else {
+ obj._data.validity_amount = obj.validity_seconds / 86400
+ }
+ }
if (obj.currency) {
obj.min_withdrawable = obj.min_withdrawable / 100
obj.max_withdrawable = obj.max_withdrawable / 100
@@ -41,6 +62,15 @@ window.app = Vue.createApp({
label: 'Wait',
field: 'wait_time'
},
+ {
+ name: 'expires_at',
+ align: 'left',
+ label: 'Expires',
+ field: 'expires_at',
+ format: function (val) {
+ return val ? new Date(val).toLocaleString() : 'Never'
+ }
+ },
{
name: 'uses',
align: 'right',
@@ -84,6 +114,8 @@ window.app = Vue.createApp({
show: false,
secondMultiplier: 'seconds',
secondMultiplierOptions: ['seconds', 'minutes', 'hours'],
+ validityMultiplier: 'days',
+ validityMultiplierOptions: ['days', 'weeks', 'months'],
data: {
is_unique: false,
use_custom: false,
@@ -93,6 +125,8 @@ window.app = Vue.createApp({
},
simpleformDialog: {
show: false,
+ validityMultiplier: 'days',
+ validityMultiplierOptions: ['days', 'weeks', 'months'],
data: {
is_unique: true,
use_custom: false,
@@ -161,18 +195,22 @@ window.app = Vue.createApp({
})
},
closeFormDialog() {
+ this.formDialog.validityMultiplier = 'days'
this.formDialog.data = {
is_unique: false,
use_custom: false,
has_webhook: false,
- enabled: true
+ enabled: true,
+ validity_seconds: 0
}
},
simplecloseFormDialog() {
+ this.simpleformDialog.validityMultiplier = 'days'
this.simpleformDialog.data = {
is_unique: false,
use_custom: false,
- enabled: true
+ enabled: true,
+ validity_seconds: 0
}
},
openQrCodeDialog(linkId) {
@@ -184,6 +222,7 @@ window.app = Vue.createApp({
openUpdateDialog(linkId) {
let link = _.findWhere(this.withdrawLinks, {id: linkId})
link._data.has_webhook = link._data.webhook_url ? true : false
+ this.formDialog.validityMultiplier = link._data.validity_multiplier
this.formDialog.data = _.clone(link._data)
this.formDialog.show = true
},
@@ -208,6 +247,12 @@ window.app = Vue.createApp({
minutes: 60,
hours: 3600
}[this.formDialog.secondMultiplier]
+ data.validity_seconds = this.validitySeconds(
+ data.validity_amount,
+ this.formDialog.validityMultiplier
+ )
+ delete data.validity_amount
+ delete data.validity_multiplier
if (data.id) {
this.updateWithdrawLink(wallet, data)
@@ -225,6 +270,12 @@ window.app = Vue.createApp({
data.min_withdrawable = data.max_withdrawable
data.title = 'vouchers'
data.is_unique = true
+ data.validity_seconds = this.validitySeconds(
+ data.validity_amount,
+ this.simpleformDialog.validityMultiplier
+ )
+ delete data.validity_amount
+ delete data.validity_multiplier
if (!data.use_custom) {
data.custom_url = null
@@ -267,6 +318,16 @@ window.app = Vue.createApp({
LNbits.utils.notifyApiError(error)
})
},
+ validitySeconds(amount, unit) {
+ return (
+ (Number(amount) || 0) *
+ {
+ days: 86400,
+ weeks: 604800,
+ months: 2592000
+ }[unit]
+ )
+ },
createWithdrawLink(wallet, data) {
console.log(data)
LNbits.api
diff --git a/templates/withdraw/display.html b/templates/withdraw/display.html
index 812c95f..1077857 100644
--- a/templates/withdraw/display.html
+++ b/templates/withdraw/display.html
@@ -7,8 +7,8 @@