Skip to content

Commit d90e9d9

Browse files
Feat: add fiat denominated withdraw links (#71)
1 parent 2e52400 commit d90e9d9

9 files changed

Lines changed: 1294 additions & 1101 deletions

File tree

crud.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ async def create_withdraw_link(
2222
created_at=datetime.now(),
2323
open_time=int(datetime.now().timestamp()) + data.wait_time,
2424
title=data.title,
25-
min_withdrawable=data.min_withdrawable,
26-
max_withdrawable=data.max_withdrawable,
25+
currency=data.currency,
26+
min_withdrawable=int(data.min_withdrawable),
27+
max_withdrawable=int(data.max_withdrawable),
2728
uses=data.uses,
2829
wait_time=data.wait_time,
2930
is_unique=data.is_unique,
@@ -85,12 +86,10 @@ async def get_withdraw_links(
8586
query_params,
8687
WithdrawLink,
8788
)
88-
result = await db.execute(
89-
f"""
89+
result = await db.execute(f"""
9090
SELECT COUNT(*) as total FROM withdraw.withdraw_link
9191
WHERE wallet IN ({q})
92-
"""
93-
)
92+
""")
9493
result2 = result.mappings().first()
9594

9695
return PaginatedWithdraws(data=links, total=int(result2.total))
@@ -141,7 +140,6 @@ async def create_hash_check(the_hash: str, lnurl_id: str) -> HashCheck:
141140

142141

143142
async def get_hash_check(the_hash: str, lnurl_id: str) -> HashCheck:
144-
145143
hash_check = await db.fetchone(
146144
"""
147145
SELECT id as hash, lnurl_id as lnurl

helpers.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from fastapi import Request
2+
from lnbits.utils.exchange_rates import get_fiat_rate_satoshis
23
from lnurl import Lnurl
34
from lnurl import encode as lnurl_encode
45
from shortuuid import uuid
@@ -26,3 +27,15 @@ def create_lnurl(link: WithdrawLink, req: Request) -> Lnurl:
2627
f"Error creating LNURL with url: `{url!s}`, "
2728
"check your webserver proxy configuration."
2829
) from e
30+
31+
32+
async def min_max_withdrawable(link: WithdrawLink) -> tuple[int, int]:
33+
min_withdrawable = link.min_withdrawable
34+
max_withdrawable = link.max_withdrawable
35+
36+
if link.currency:
37+
rate = await get_fiat_rate_satoshis(link.currency)
38+
min_withdrawable = round(min_withdrawable / 100 * rate)
39+
max_withdrawable = round(max_withdrawable / 100 * rate)
40+
41+
return min_withdrawable, max_withdrawable

migrations.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ async def m001_initial(db):
22
"""
33
Creates an improved withdraw table and migrates the existing data.
44
"""
5-
await db.execute(
6-
f"""
5+
await db.execute(f"""
76
CREATE TABLE withdraw.withdraw_links (
87
id TEXT PRIMARY KEY,
98
wallet TEXT,
@@ -19,16 +18,14 @@ async def m001_initial(db):
1918
used INTEGER DEFAULT 0,
2019
usescsv TEXT
2120
);
22-
"""
23-
)
21+
""")
2422

2523

2624
async def m002_change_withdraw_table(db):
2725
"""
2826
Creates an improved withdraw table and migrates the existing data.
2927
"""
30-
await db.execute(
31-
f"""
28+
await db.execute(f"""
3229
CREATE TABLE withdraw.withdraw_link (
3330
id TEXT PRIMARY KEY,
3431
wallet TEXT,
@@ -44,8 +41,7 @@ async def m002_change_withdraw_table(db):
4441
used INTEGER DEFAULT 0,
4542
usescsv TEXT
4643
);
47-
"""
48-
)
44+
""")
4945

5046
for row in [
5147
list(row) for row in await db.fetchall("SELECT * FROM withdraw.withdraw_links")
@@ -100,14 +96,12 @@ async def m003_make_hash_check(db):
10096
"""
10197
Creates a hash check table.
10298
"""
103-
await db.execute(
104-
"""
99+
await db.execute("""
105100
CREATE TABLE withdraw.hash_check (
106101
id TEXT PRIMARY KEY,
107102
lnurl_id TEXT
108103
);
109-
"""
110-
)
104+
""")
111105

112106

113107
async def m004_webhook_url(db):
@@ -145,3 +139,7 @@ async def m008_add_enabled_column(db):
145139
await db.execute(
146140
"ALTER TABLE withdraw.withdraw_link ADD COLUMN enabled BOOLEAN DEFAULT true;"
147141
)
142+
143+
144+
async def m009_add_currency(db):
145+
await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN currency TEXT;")

models.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77
class CreateWithdrawData(BaseModel):
88
title: str = Query(...)
9-
min_withdrawable: int = Query(..., ge=1)
10-
max_withdrawable: int = Query(..., ge=1)
9+
min_withdrawable: float = Query(..., ge=0.01)
10+
max_withdrawable: float = Query(..., ge=0.01)
1111
uses: int = Query(..., ge=1)
1212
wait_time: int = Query(..., ge=1)
1313
is_unique: bool
@@ -16,6 +16,7 @@ class CreateWithdrawData(BaseModel):
1616
webhook_body: str = Query(None)
1717
custom_url: str = Query(None)
1818
enabled: bool = Query(True)
19+
currency: str = Query(None)
1920

2021

2122
class WithdrawLink(BaseModel):
@@ -39,6 +40,7 @@ class WithdrawLink(BaseModel):
3940
custom_url: str = Query(None)
4041
created_at: datetime
4142
enabled: bool = Query(True)
43+
currency: str = Query(None)
4244
lnurl: str | None = Field(
4345
default=None,
4446
no_database=True,

static/js/index.js

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ const mapWithdrawLink = function (obj) {
22
obj._data = _.clone(obj)
33
obj.uses_left = obj.uses - obj.used
44
obj._data.use_custom = Boolean(obj.custom_url)
5+
if (obj.currency) {
6+
obj.min_withdrawable = obj.min_withdrawable / 100
7+
obj.max_withdrawable = obj.max_withdrawable / 100
8+
}
59
return obj
610
}
711

@@ -14,6 +18,7 @@ window.app = Vue.createApp({
1418
return {
1519
checker: null,
1620
withdrawLinks: [],
21+
currencyOptions: [],
1722
lnurl: '',
1823
withdrawLinksTable: {
1924
columns: [
@@ -46,12 +51,24 @@ window.app = Vue.createApp({
4651
label: 'Uses left',
4752
field: 'uses_left'
4853
},
54+
{
55+
name: 'currency',
56+
align: 'right',
57+
label: 'Currency',
58+
field: 'currency',
59+
format: function (val) {
60+
return val ? val.toUpperCase() : 'sat'
61+
}
62+
},
4963
{
5064
name: 'max_withdrawable',
5165
align: 'right',
52-
label: 'Max (sat)',
66+
label: `Max`,
5367
field: 'max_withdrawable',
54-
format: LNbits.utils.formatSat
68+
format: (val, row) =>
69+
row.currency
70+
? LNbits.utils.formatCurrency(val, row.currency)
71+
: val
5572
}
5673
],
5774
pagination: {
@@ -94,6 +111,24 @@ window.app = Vue.createApp({
94111
return this.withdrawLinks.sort(function (a, b) {
95112
return b.uses_left - a.uses_left
96113
})
114+
},
115+
assertMinimumWithdrawable() {
116+
const dialog = this.formDialog.show
117+
? this.formDialog
118+
: this.simpleformDialog
119+
return dialog.data.currency
120+
? dialog.data.min_withdrawable >= 0.01
121+
: dialog.data.min_withdrawable >= 1
122+
},
123+
assertMaximumWithdrawable() {
124+
const dialog = this.formDialog.show
125+
? this.formDialog
126+
: this.simpleformDialog
127+
return dialog.data.currency
128+
? dialog.data.max_withdrawable >= 0.01 &&
129+
dialog.data.max_withdrawable >= dialog.data.min_withdrawable
130+
: dialog.data.max_withdrawable >= 1 &&
131+
dialog.data.max_withdrawable >= dialog.data.min_withdrawable
97132
}
98133
},
99134
methods: {
@@ -164,6 +199,11 @@ window.app = Vue.createApp({
164199
data.custom_url = CUSTOM_URL
165200
}
166201

202+
if (data.currency) {
203+
data.min_withdrawable = data.min_withdrawable * 100
204+
data.max_withdrawable = data.max_withdrawable * 100
205+
}
206+
167207
data.wait_time =
168208
data.wait_time *
169209
{
@@ -197,6 +237,11 @@ window.app = Vue.createApp({
197237
data.custom_url = '/static/images/default_voucher.png'
198238
}
199239

240+
if (data.currency) {
241+
data.min_withdrawable = data.min_withdrawable * 100
242+
data.max_withdrawable = data.max_withdrawable * 100
243+
}
244+
200245
if (data.id) {
201246
this.updateWithdrawLink(wallet, data)
202247
} else {
@@ -231,6 +276,7 @@ window.app = Vue.createApp({
231276
})
232277
},
233278
createWithdrawLink(wallet, data) {
279+
console.log(data)
234280
LNbits.api
235281
.request('POST', '/withdraw/api/v1/links', wallet.adminkey, data)
236282
.then(response => {
@@ -314,5 +360,6 @@ window.app = Vue.createApp({
314360
this.getWithdrawLinks()
315361
this.checker = setInterval(this.getWithdrawLinks, 300000)
316362
}
363+
this.currencyOptions = this.g.allowedCurrencies
317364
}
318365
})

templates/withdraw/index.html

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -172,21 +172,33 @@ <h6 class="text-subtitle1 q-my-none">
172172
type="text"
173173
label="Link title *"
174174
></q-input>
175+
<q-select
176+
filled
177+
dense
178+
clearable
179+
v-model="formDialog.data.currency"
180+
:options="currencyOptions"
181+
label="Currency (optional)"
182+
hint="Leave blank for sats"
183+
>
184+
</q-select>
175185
<q-input
176186
filled
177187
dense
178188
v-model.number="formDialog.data.min_withdrawable"
179189
type="number"
180-
min="10"
181-
label="Min withdrawable (sat, at least 10) *"
190+
:min="formDialog.data.currency ? 0.01 : 10"
191+
:step="formDialog.data.currency ? 0.01 : 1"
192+
:label="formDialog.data.currency ? 'Min withdrawable (' + formDialog.data.currency + ', at least 0.01) *' : 'Min withdrawable (sat, at least 10) *'"
182193
></q-input>
183194
<q-input
184195
filled
185196
dense
186197
v-model.number="formDialog.data.max_withdrawable"
187198
type="number"
188-
min="10"
189-
label="Max withdrawable (sat, at least 10) *"
199+
:min="formDialog.data.currency ? 0.01 : 10"
200+
:step="formDialog.data.currency ? 0.01 : 1"
201+
:label="formDialog.data.currency ? 'Max withdrawable (' + formDialog.data.currency + ', at least 0.01) *' : 'Max withdrawable (sat, at least 10) *'"
190202
></q-input>
191203
<q-input
192204
filled
@@ -325,12 +337,8 @@ <h6 class="text-subtitle1 q-my-none">
325337
:disable="
326338
formDialog.data.wallet == null ||
327339
formDialog.data.title == null ||
328-
(formDialog.data.min_withdrawable == null || formDialog.data.min_withdrawable < 1) ||
329-
(
330-
formDialog.data.max_withdrawable == null ||
331-
formDialog.data.max_withdrawable < 1 ||
332-
formDialog.data.max_withdrawable < formDialog.data.min_withdrawable
333-
) ||
340+
!assertMinimumWithdrawable ||
341+
!assertMaximumWithdrawable ||
334342
formDialog.data.uses == null ||
335343
formDialog.data.wait_time == null"
336344
type="submit"
@@ -360,13 +368,24 @@ <h6 class="text-subtitle1 q-my-none">
360368
label="Wallet *"
361369
>
362370
</q-select>
371+
<q-select
372+
filled
373+
dense
374+
clearable
375+
v-model="simpleformDialog.data.currency"
376+
:options="currencyOptions"
377+
label="Currency (optional)"
378+
hint="Leave blank for sats"
379+
>
380+
</q-select>
363381
<q-input
364382
filled
365383
dense
366384
v-model.number="simpleformDialog.data.max_withdrawable"
367385
type="number"
368-
min="10"
369-
label="Withdraw amount per voucher (sat, at least 10)"
386+
:min="simpleformDialog.data.currency ? 0.01 : 10"
387+
:step="simpleformDialog.data.currency ? 0.01 : 1"
388+
:label="simpleformDialog.data.currency ? 'Withdraw amount per voucher (' + simpleformDialog.data.currency + ', at least 0.01)' : 'Withdraw amount per voucher (sat, at least 10)'"
370389
></q-input>
371390
<q-input
372391
filled
@@ -424,9 +443,8 @@ <h6 class="text-subtitle1 q-my-none">
424443
color="primary"
425444
:disable="
426445
simpleformDialog.data.wallet == null ||
427-
428-
simpleformDialog.data.max_withdrawable == null ||
429-
simpleformDialog.data.max_withdrawable < 1 ||
446+
simpleformDialog.data.max_withdrawable == null ||
447+
!assertMaximumWithdrawable ||
430448
simpleformDialog.data.uses == null"
431449
type="submit"
432450
>Create vouchers</q-btn

0 commit comments

Comments
 (0)