mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-03-03 17:37:06 +01:00
refactor(withdraw): migrate extension to Vue
This commit is contained in:
parent
dd23b20090
commit
a834a64319
15 changed files with 896 additions and 1638 deletions
|
@ -2,5 +2,5 @@
|
|||
"name": "LNURLw",
|
||||
"short_description": "Make LNURL withdraw links.",
|
||||
"icon": "crop_free",
|
||||
"contributors": ["arcbtc"]
|
||||
"contributors": ["arcbtc", "eillarra"]
|
||||
}
|
||||
|
|
94
lnbits/extensions/withdraw/crud.py
Normal file
94
lnbits/extensions/withdraw/crud.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
from datetime import datetime
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits.db import open_ext_db
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from .models import WithdrawLink
|
||||
|
||||
|
||||
def create_withdraw_link(
|
||||
*,
|
||||
wallet_id: str,
|
||||
title: str,
|
||||
min_withdrawable: int,
|
||||
max_withdrawable: int,
|
||||
uses: int,
|
||||
wait_time: int,
|
||||
is_unique: bool,
|
||||
) -> WithdrawLink:
|
||||
with open_ext_db("withdraw") as db:
|
||||
link_id = urlsafe_short_hash()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO withdraw_links (
|
||||
id,
|
||||
wallet,
|
||||
title,
|
||||
min_withdrawable,
|
||||
max_withdrawable,
|
||||
uses,
|
||||
wait_time,
|
||||
is_unique,
|
||||
unique_hash,
|
||||
k1,
|
||||
open_time
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
link_id,
|
||||
wallet_id,
|
||||
title,
|
||||
min_withdrawable,
|
||||
max_withdrawable,
|
||||
uses,
|
||||
wait_time,
|
||||
int(is_unique),
|
||||
urlsafe_short_hash(),
|
||||
urlsafe_short_hash(),
|
||||
int(datetime.now().timestamp()) + wait_time,
|
||||
),
|
||||
)
|
||||
|
||||
return get_withdraw_link(link_id)
|
||||
|
||||
|
||||
def get_withdraw_link(link_id: str) -> Optional[WithdrawLink]:
|
||||
with open_ext_db("withdraw") as db:
|
||||
row = db.fetchone("SELECT * FROM withdraw_links WHERE id = ?", (link_id,))
|
||||
|
||||
return WithdrawLink(**row) if row else None
|
||||
|
||||
|
||||
def get_withdraw_link_by_hash(unique_hash: str) -> Optional[WithdrawLink]:
|
||||
with open_ext_db("withdraw") as db:
|
||||
row = db.fetchone("SELECT * FROM withdraw_links WHERE unique_hash = ?", (unique_hash,))
|
||||
|
||||
return WithdrawLink(**row) if row else None
|
||||
|
||||
|
||||
def get_withdraw_links(wallet_ids: Union[str, List[str]]) -> List[WithdrawLink]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
with open_ext_db("withdraw") as db:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM withdraw_links WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
|
||||
return [WithdrawLink(**row) for row in rows]
|
||||
|
||||
|
||||
def update_withdraw_link(link_id: str, **kwargs) -> Optional[WithdrawLink]:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
|
||||
with open_ext_db("withdraw") as db:
|
||||
db.execute(f"UPDATE withdraw_links SET {q} WHERE id = ?", (*kwargs.values(), link_id))
|
||||
row = db.fetchone("SELECT * FROM withdraw_links WHERE id = ?", (link_id,))
|
||||
|
||||
return WithdrawLink(**row) if row else None
|
||||
|
||||
|
||||
def delete_withdraw_link(link_id: str) -> None:
|
||||
with open_ext_db("withdraw") as db:
|
||||
db.execute("DELETE FROM withdraw_links WHERE id = ?", (link_id,))
|
|
@ -1,7 +1,7 @@
|
|||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from lnbits.db import open_ext_db
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
|
||||
def m001_initial(db):
|
||||
|
@ -32,6 +32,69 @@ def m001_initial(db):
|
|||
)
|
||||
|
||||
|
||||
def m002_change_withdraw_table(db):
|
||||
"""
|
||||
Creates an improved withdraw table and migrates the existing data.
|
||||
"""
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS withdraw_links (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT,
|
||||
title TEXT,
|
||||
min_withdrawable INTEGER DEFAULT 1,
|
||||
max_withdrawable INTEGER 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
|
||||
);
|
||||
"""
|
||||
)
|
||||
db.execute("CREATE INDEX IF NOT EXISTS wallet_idx ON withdraw_links (wallet)")
|
||||
db.execute("CREATE UNIQUE INDEX IF NOT EXISTS unique_hash_idx ON withdraw_links (unique_hash)")
|
||||
|
||||
for row in [list(row) for row in db.fetchall("SELECT * FROM withdraws")]:
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO withdraw_links (
|
||||
id,
|
||||
wallet,
|
||||
title,
|
||||
min_withdrawable,
|
||||
max_withdrawable,
|
||||
uses,
|
||||
wait_time,
|
||||
is_unique,
|
||||
unique_hash,
|
||||
k1,
|
||||
open_time,
|
||||
used
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
row[5], # uni
|
||||
row[2], # wal
|
||||
row[6], # tit
|
||||
row[8], # minamt
|
||||
row[7], # maxamt
|
||||
row[10], # inc
|
||||
row[11], # tme
|
||||
row[12], # uniq
|
||||
urlsafe_short_hash(),
|
||||
urlsafe_short_hash(),
|
||||
int(datetime.now().timestamp()) + row[11],
|
||||
row[9], # spent
|
||||
),
|
||||
)
|
||||
db.execute("DROP TABLE withdraws")
|
||||
|
||||
|
||||
def migrate():
|
||||
with open_ext_db("withdraw") as db:
|
||||
m001_initial(db)
|
||||
m002_change_withdraw_table(db)
|
||||
|
|
46
lnbits/extensions/withdraw/models.py
Normal file
46
lnbits/extensions/withdraw/models.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
from flask import url_for
|
||||
from lnurl import Lnurl, LnurlWithdrawResponse, encode as lnurl_encode
|
||||
from os import getenv
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class WithdrawLink(NamedTuple):
|
||||
id: str
|
||||
wallet: str
|
||||
title: str
|
||||
min_withdrawable: int
|
||||
max_withdrawable: int
|
||||
uses: int
|
||||
wait_time: int
|
||||
is_unique: bool
|
||||
unique_hash: str
|
||||
k1: str
|
||||
open_time: int
|
||||
used: int
|
||||
|
||||
@property
|
||||
def is_spent(self) -> bool:
|
||||
return self.used >= self.uses
|
||||
|
||||
@property
|
||||
def is_onion(self) -> bool:
|
||||
return getenv("LNBITS_WITH_ONION", 1) == 1
|
||||
|
||||
@property
|
||||
def lnurl(self) -> Lnurl:
|
||||
scheme = None if self.is_onion else "https"
|
||||
url = url_for("withdraw.api_lnurl_response", unique_hash=self.unique_hash, _external=True, _scheme=scheme)
|
||||
return lnurl_encode(url)
|
||||
|
||||
@property
|
||||
def lnurl_response(self) -> LnurlWithdrawResponse:
|
||||
scheme = None if self.is_onion else "https"
|
||||
url = url_for("withdraw.api_lnurl_callback", unique_hash=self.unique_hash, _external=True, _scheme=scheme)
|
||||
|
||||
return LnurlWithdrawResponse(
|
||||
callback=url,
|
||||
k1=self.k1,
|
||||
min_withdrawable=self.min_withdrawable * 1000,
|
||||
max_withdrawable=self.max_withdrawable * 1000,
|
||||
default_description="LNbits LNURL withdraw",
|
||||
)
|
169
lnbits/extensions/withdraw/static/js/index.js
Normal file
169
lnbits/extensions/withdraw/static/js/index.js
Normal file
|
@ -0,0 +1,169 @@
|
|||
Vue.component(VueQrcode.name, VueQrcode);
|
||||
|
||||
var locationPath = [window.location.protocol, '//', window.location.hostname, window.location.pathname].join('');
|
||||
|
||||
var mapWithdrawLink = function (obj) {
|
||||
obj.is_unique = obj.is_unique == 1;
|
||||
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('');
|
||||
return obj;
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
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
|
||||
}
|
||||
},
|
||||
formDialog: {
|
||||
show: false,
|
||||
secondMultiplier: 'seconds',
|
||||
secondMultiplierOptions: ['seconds', 'minutes', 'hours'],
|
||||
data: {
|
||||
is_unique: false
|
||||
}
|
||||
},
|
||||
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',
|
||||
this.g.user.wallets[0].inkey
|
||||
).then(function (response) {
|
||||
self.withdrawLinks = response.data.map(function (obj) {
|
||||
return mapWithdrawLink(obj);
|
||||
});
|
||||
});
|
||||
},
|
||||
closeFormDialog: function () {
|
||||
this.formDialog.data = {
|
||||
is_unique: false
|
||||
};
|
||||
},
|
||||
openQrCodeDialog: function (linkId) {
|
||||
var link = _.findWhere(this.withdrawLinks, {id: linkId});
|
||||
this.qrCodeDialog.data = _.clone(link);
|
||||
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');
|
||||
|
||||
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); }
|
||||
},
|
||||
updateWithdrawLink: function (wallet, data) {
|
||||
var self = this;
|
||||
|
||||
LNbits.api.request(
|
||||
'PUT',
|
||||
'/withdraw/api/v1/links/' + data.id,
|
||||
wallet.inkey,
|
||||
_.pick(data, 'title', 'min_withdrawable', 'max_withdrawable', 'uses', 'wait_time', 'is_unique')
|
||||
).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.inkey,
|
||||
data
|
||||
).then(function (response) {
|
||||
self.withdrawLinks.push(mapWithdrawLink(response.data));
|
||||
self.formDialog.show = false;
|
||||
}).catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
});
|
||||
},
|
||||
deleteWithdrawLink: function (linkId) {
|
||||
var self = this;
|
||||
var link = _.findWhere(this.withdrawLinks, {id: linkId});
|
||||
|
||||
this.$q.dialog({
|
||||
message: 'Are you sure you want to delete this withdraw link?',
|
||||
ok: {
|
||||
flat: true,
|
||||
color: 'orange'
|
||||
},
|
||||
cancel: {
|
||||
flat: true,
|
||||
color: 'grey'
|
||||
}
|
||||
}).onOk(function () {
|
||||
LNbits.api.request(
|
||||
'DELETE',
|
||||
'/withdraw/api/v1/links/' + linkId,
|
||||
_.findWhere(self.g.user.wallets, {id: link.wallet}).inkey
|
||||
).then(function (response) {
|
||||
self.withdrawLinks = _.reject(self.withdrawLinks, function (obj) { return obj.id == linkId; });
|
||||
}).catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
exportCSV: function () {
|
||||
LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls);
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
var getWithdrawLinks = this.getWithdrawLinks;
|
||||
getWithdrawLinks();
|
||||
/*setInterval(function(){
|
||||
getWithdrawLinks();
|
||||
}, 20000);*/
|
||||
}
|
||||
}
|
||||
});
|
35
lnbits/extensions/withdraw/templates/withdraw/_api_docs.html
Normal file
35
lnbits/extensions/withdraw/templates/withdraw/_api_docs.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-expansion-item group="api" dense expand-separator label="List withdraw links">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
|
||||
</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>
|
||||
|
||||
</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>
|
||||
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Delete a withdraw link" class="q-pb-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-expansion-item>
|
10
lnbits/extensions/withdraw/templates/withdraw/_lnurl.html
Normal file
10
lnbits/extensions/withdraw/templates/withdraw/_lnurl.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="info"
|
||||
label="Powered by LNURL">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
LNURL-withdraw info.
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
|
@ -1,530 +1,52 @@
|
|||
<!-- @format -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>LNBits Wallet</title>
|
||||
<meta
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
|
||||
name="viewport"
|
||||
/>
|
||||
<!-- Bootstrap 3.3.2 -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}"
|
||||
/>
|
||||
<!-- FontAwesome 4.3.0 -->
|
||||
<link
|
||||
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<!-- Ionicons 2.0.0 -->
|
||||
<link
|
||||
href="https://code.ionicframework.com/ionicons/2.0.0/css/ionicons.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
|
||||
<!-- Theme style -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='dist/css/AdminLTE.min.css') }}"
|
||||
/>
|
||||
<!-- AdminLTE Skins. Choose a skin from the css/skins
|
||||
folder instead of downloading all of them to reduce the load. -->
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='dist/css/skins/_all-skins.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- Morris chart -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/morris/morris.css') }}"
|
||||
/>
|
||||
|
||||
<!-- jvectormap -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.css') }}"
|
||||
/>
|
||||
|
||||
<!-- bootstrap wysihtml5 - text editor -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
||||
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<style>
|
||||
.small-box > .small-box-footer {
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#loadingMessage {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#output {
|
||||
margin-top: 20px;
|
||||
background: #eee;
|
||||
padding: 10px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#output div {
|
||||
padding-bottom: 10px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#noQRFound {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- jQuery 2.1.3 -->
|
||||
<script src="{{ url_for('static', filename='plugins/jQuery/jQuery-2.1.3.min.js') }}"></script>
|
||||
<!-- jQuery UI 1.11.2 -->
|
||||
<script
|
||||
src="https://code.jquery.com/ui/1.11.2/jquery-ui.min.js"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Resolve conflict in jQuery UI tooltip with Bootstrap tooltip -->
|
||||
<script>
|
||||
$.widget.bridge('uibutton', $.ui.button)
|
||||
</script>
|
||||
<!-- Bootstrap 3.3.2 JS -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='bootstrap/js/bootstrap.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Morris.js charts -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/morris/morris.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Sparkline -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/sparkline/jquery.sparkline.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- jvectormap -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-world-mill-en.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- jQuery Knob Chart -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/knob/jquery.knob.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Bootstrap WYSIHTML5 -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Slimscroll -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/slimScroll/jquery.slimscroll.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- FastClick -->
|
||||
<script src="{{ url_for('static', filename='plugins/fastclick/fastclick.min.js') }}"></script>
|
||||
<!-- AdminLTE App -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/app.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- AdminLTE dashboard demo (This is only for demo purposes) -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/pages/dashboard.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- AdminLTE for demo purposes -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/demo.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/datatables/jquery.dataTables.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css"
|
||||
/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js"></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jscam/JS.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jscam/qrcode.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bolt11/decoder.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bolt11/utils.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
{% extends "public.html" %}
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
//GOOFY CSS HACK TO GO DARK
|
||||
|
||||
.skin-blue .wrapper {
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li.active > a {
|
||||
color: #fff;
|
||||
background:#1f2234;
|
||||
border-left-color:#8964a9;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar {
|
||||
background-color:
|
||||
#2e507d;
|
||||
}
|
||||
|
||||
.content-wrapper, .right-side {
|
||||
background-color:
|
||||
#1f2234;
|
||||
}
|
||||
.skin-blue .main-header .logo {
|
||||
background-color:
|
||||
#1f2234;
|
||||
color:
|
||||
#fff;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li.header {
|
||||
color:
|
||||
#4b646f;
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
.skin-blue .wrapper, .skin-blue .main-sidebar, .skin-blue .left-side {
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > .treeview-menu {
|
||||
margin: 0 1px;
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > a {
|
||||
border-left: 3px solid
|
||||
transparent;
|
||||
margin-right: 1px;
|
||||
}
|
||||
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
|
||||
|
||||
color: #fff;
|
||||
background:#3e355a;
|
||||
border-left-color:#8964a9;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.skin-blue .main-header .logo:hover {
|
||||
background:
|
||||
#3e355a;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar .sidebar-toggle:hover {
|
||||
background-color:
|
||||
#3e355a;
|
||||
}
|
||||
.main-footer {
|
||||
background-color: #1f2234;
|
||||
padding: 15px;
|
||||
color: #fff;
|
||||
border-top: 0px;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar {
|
||||
background-color: #1f2234;
|
||||
}
|
||||
|
||||
.bg-red, .callout.callout-danger, .alert-danger, .alert-error, .label-danger, .modal-danger .modal-body {
|
||||
background-color:
|
||||
#1f2234 !important;
|
||||
}
|
||||
.alert-danger, .alert-error {
|
||||
|
||||
border-color: #fff;
|
||||
border: 1px solid
|
||||
|
||||
#fff;
|
||||
border-radius: 7px;
|
||||
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar .nav > li > a:hover, .skin-blue .main-header .navbar .nav > li > a:active, .skin-blue .main-header .navbar .nav > li > a:focus, .skin-blue .main-header .navbar .nav .open > a, .skin-blue .main-header .navbar .nav .open > a:hover, .skin-blue .main-header .navbar .nav .open > a:focus {
|
||||
color:
|
||||
#f6f6f6;
|
||||
background-color: #3e355a;
|
||||
}
|
||||
.bg-aqua, .callout.callout-info, .alert-info, .label-info, .modal-info .modal-body {
|
||||
background-color:
|
||||
#3e355a !important;
|
||||
}
|
||||
|
||||
.box {
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
background-color: #333646;
|
||||
border-top: 3px solid #8964a9;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
.table-striped > tbody > tr:nth-of-type(2n+1) {
|
||||
background-color:
|
||||
#333646;
|
||||
}
|
||||
|
||||
.box-header {
|
||||
color: #fff;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.box.box-danger {
|
||||
border-top-color: #8964a9;
|
||||
}
|
||||
.box.box-primary {
|
||||
border-top-color: #8964a9;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #8964a9;
|
||||
}
|
||||
.box-header.with-border {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
a:hover, a:active, a:focus {
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
}
|
||||
// .modal.in .modal-dialog{
|
||||
// color:#000;
|
||||
// }
|
||||
|
||||
.form-control {
|
||||
|
||||
background-color:#333646;
|
||||
color: #fff;
|
||||
|
||||
}
|
||||
.box-footer {
|
||||
|
||||
border-top: none;
|
||||
|
||||
background-color:
|
||||
#333646;
|
||||
}
|
||||
.modal-footer {
|
||||
|
||||
border-top: none;
|
||||
|
||||
}
|
||||
.modal-content {
|
||||
|
||||
background-color:
|
||||
#333646;
|
||||
}
|
||||
.modal.in .modal-dialog {
|
||||
|
||||
background-color: #333646;
|
||||
|
||||
}
|
||||
|
||||
.layout-boxed {
|
||||
background: none;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
background-color:
|
||||
#3e355a;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
|
||||
|
||||
background: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
</head>
|
||||
<body class="skin-blue layout-boxed sidebar-collapse sidebar-open">
|
||||
<div class="wrapper">
|
||||
<header class="main-header">
|
||||
<!-- Logo -->
|
||||
<a href="{{ url_for('core.home') }}" class="logo"><b>LN</b>bits</a>
|
||||
<!-- Header Navbar: style can be found in header.less -->
|
||||
<nav class="navbar navbar-static-top" role="navigation">
|
||||
<!-- Sidebar toggle button-->
|
||||
<a
|
||||
href="#"
|
||||
class="sidebar-toggle"
|
||||
data-toggle="offcanvas"
|
||||
role="button"
|
||||
>
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
{% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-sm-6 col-md-4">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<div class="text-center q-mb-md">
|
||||
{% if link.is_spent %}
|
||||
<q-badge color="red" class="q-mb-md">Withdraw is spent.</q-badge>
|
||||
{% endif %}
|
||||
<a href="lightning:{{ link.lnurl }}">
|
||||
<q-responsive :ratio="1" class="q-mx-md">
|
||||
<qrcode value="{{ link.lnurl }}" :options="{width: 800}" class="rounded-borders"></qrcode>
|
||||
</q-responsive>
|
||||
</a>
|
||||
<div class="navbar-custom-menu">
|
||||
<ul class="nav navbar-nav">
|
||||
<!-- Messages: style can be found in dropdown.less-->
|
||||
<li class="dropdown messages-menu">
|
||||
|
||||
{% block messages %}{% endblock %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<aside class="main-sidebar">
|
||||
<!-- sidebar: style can be found in sidebar.less -->
|
||||
<section class="sidebar" style="height: auto;">
|
||||
<!-- Sidebar user panel -->
|
||||
|
||||
<!-- /.search form -->
|
||||
<!-- sidebar menu: : style can be found in sidebar.less -->
|
||||
<ul class="sidebar-menu">
|
||||
<li><br/><br/><a href="https://where39.com/"><p>Where39 anon locations</p><img src="../static/where39.png" style="width:170px"></a></li>
|
||||
<li><br/><a href="https://github.com/arcbtc/Quickening"><p>The Quickening <$8 PoS</p><img src="../static/quick.gif" style="width:170px"></a></li>
|
||||
<li><br/><a href="https://jigawatt.co/"><p>Buy BTC stamps + electronics</p><img src="../static/stamps.jpg" style="width:170px"></a></li>
|
||||
<li><br/><a href="mailto:ben@arc.wales"><h3>Advertise here!</h3></a></li>
|
||||
|
||||
</ul>
|
||||
</section>
|
||||
<!-- /.sidebar -->
|
||||
</aside>
|
||||
|
||||
<!-- Right side column. Contains the navbar and content of the page -->
|
||||
<div class="content-wrapper">
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
LNURL Withdraw Link
|
||||
<small>Use LNURL compatible bitcoin wallet</small>
|
||||
</h1>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content"><br/><br/>
|
||||
<center><h1 style="font-size:500%">Withdraw Link: {{ user_fau[0][6] }}</h1></center>
|
||||
|
||||
<center><br/><br/> <div id="qrcode" style="width: 340px;"></div><br/><br/>
|
||||
<div style="width:55%;word-wrap: break-word;" id="qrcodetxt"></div> <br/></center>
|
||||
|
||||
</section><!-- /.content -->
|
||||
</div><!-- /.content-wrapper -->
|
||||
<div class="row justify-between">
|
||||
<q-btn flat color="grey" @click="copyText('{{ link.lnurl }}')">Copy LNURL</q-btn>
|
||||
</div>
|
||||
</body>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-md-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 src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<script>
|
||||
function getAjax(url, thekey, success) {
|
||||
var xhr = window.XMLHttpRequest
|
||||
? new XMLHttpRequest()
|
||||
: new ActiveXObject('Microsoft.XMLHTTP')
|
||||
xhr.open('GET', url, true)
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState > 3 && xhr.status == 200) {
|
||||
success(xhr.responseText)
|
||||
}
|
||||
}
|
||||
xhr.setRequestHeader('Grpc-Metadata-macaroon', thekey)
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
|
||||
xhr.send()
|
||||
return xhr
|
||||
}
|
||||
|
||||
|
||||
function drawwithdraw(data) {
|
||||
|
||||
|
||||
console.log(data)
|
||||
|
||||
|
||||
getAjax('/withdraw/api/v1/lnurlencode/'+ window.location.hostname + "/" + data, "filla", function(datab) {
|
||||
if (JSON.parse(datab).status == 'TRUE') {
|
||||
console.log(JSON.parse(datab).status)
|
||||
lnurlfau = (JSON.parse(datab).lnurl)
|
||||
|
||||
|
||||
new QRCode(document.getElementById('qrcode'), {
|
||||
text: lnurlfau,
|
||||
width: 300,
|
||||
height: 300,
|
||||
colorDark: '#000000',
|
||||
colorLight: '#ffffff',
|
||||
correctLevel: QRCode.CorrectLevel.M
|
||||
})
|
||||
document.getElementById("qrcode").style.backgroundColor = "white";
|
||||
document.getElementById("qrcode").style.padding = "20px";
|
||||
|
||||
document.getElementById('qrcodetxt').innerHTML = lnurlfau + "<br/><br/>"
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
data = "Failed to build LNURL"
|
||||
}
|
||||
})
|
||||
}
|
||||
drawwithdraw("{{ user_fau[0][5] }}")
|
||||
|
||||
|
||||
|
||||
Vue.component(VueQrcode.name, VueQrcode);
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin]
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,507 +1,183 @@
|
|||
<!-- @format -->
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% extends "legacy.html" %} {% block messages %}
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-bell-o"></i>
|
||||
<span class="label label-danger">!</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="header"><b>Instant wallet, bookmark to save</b></li>
|
||||
<li></li>
|
||||
</ul>
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
{{ window_vars(user) }}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
{% assets filters='rjsmin', output='__bundle__/withdraw/index.js',
|
||||
'withdraw/js/index.js' %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
{% endblock %}
|
||||
|
||||
{% block menuitems %}
|
||||
<li class="treeview">
|
||||
<a href="#">
|
||||
<i class="fa fa-bitcoin"></i> <span>Wallets</span>
|
||||
<i class="fa fa-angle-left pull-right"></i>
|
||||
</a>
|
||||
<ul class="treeview-menu">
|
||||
{% for w in user_wallets %}
|
||||
<li>
|
||||
<a href="{{ url_for('wallet') }}?wal={{ w.id }}&usr={{ w.user }}"><i class="fa fa-bolt"></i> {{ w.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li><a onclick="sidebarmake()">Add a wallet +</a></li>
|
||||
<div id="sidebarmake"></div>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="active treeview">
|
||||
<a href="#">
|
||||
<i class="fa fa-th"></i> <span>Extensions</span>
|
||||
<i class="fa fa-angle-left pull-right"></i>
|
||||
</a>
|
||||
<ul class="treeview-menu">
|
||||
{% for extension in EXTENSIONS %}
|
||||
{% if extension.code in user_ext %}
|
||||
<li>
|
||||
<a href="{{ url_for(extension.code + '.index') }}?usr={{ user }}"><i class="fa fa-plus"></i> {{ extension.name }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<li>
|
||||
<a href="{{ url_for('core.extensions') }}?usr={{ user }}">Manager</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% 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="deep-purple" @click="formDialog.show = true">New withdraw link</q-btn>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
{% block body %}
|
||||
<!-- Right side column. Contains the navbar and content of the page -->
|
||||
<div class="content-wrapper">
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
Withdraw link maker
|
||||
<small>powered by LNURL</small>
|
||||
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li>
|
||||
<a href="{{ url_for('wallet') }}?usr={{ user }}"><i class="fa fa-dashboard"></i> Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('core.extensions') }}?usr={{ user }}"><li class="fa fa-dashboard">Extensions</li></a>
|
||||
</li>
|
||||
<li>
|
||||
<i class="active" class="fa fa-dashboard">Withdraw link maker</i>
|
||||
</li>
|
||||
</ol>
|
||||
<br /><br />
|
||||
</section>
|
||||
<div class='row'><div class='col-md-6'><div id="erralert"></div></div></div>
|
||||
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<!-- Small boxes (Stat box) -->
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-6">
|
||||
<!-- general form elements -->
|
||||
<div class="box box-primary">
|
||||
<div class="box-header">
|
||||
<h3 class="box-title"> Make a link</h3>
|
||||
</div><!-- /.box-header -->
|
||||
<!-- form start -->
|
||||
<form role="form">
|
||||
<div class="box-body">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="exampleInputEmail1">Link title</label>
|
||||
<input id="tit" type="text" pattern="^[A-Za-z]+$" class="form-control" >
|
||||
<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>
|
||||
<!-- select -->
|
||||
<div class="form-group">
|
||||
<label>Select a wallet</label>
|
||||
<select id="wal" class="form-control">
|
||||
<option></option>
|
||||
{% for w in user_wallets %}
|
||||
<option>{{w.name}}-{{w.id}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label for="exampleInputEmail1">Max withdraw:</label>
|
||||
<input id="maxamt" type="number" class="form-control" placeholder="1"></input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="exampleInputEmail1">Min withdraw:</label>
|
||||
<input id="minamt" type="number" class="form-control" placeholder="1"></input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="exampleInputPassword1">Amount of uses:</label>
|
||||
<input id="amt" type="number" class="form-control" placeholder="1"></input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="exampleInputPassword1">Time between withdrawals:</label>
|
||||
<input id="tme" type="number" class="form-control" placeholder="0" max="86400"></input>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input id="uniq" type="checkbox"> Unique links
|
||||
</label>
|
||||
</div>
|
||||
</div><!-- /.box-body -->
|
||||
|
||||
<div class="box-footer">
|
||||
|
||||
<button onclick="postfau()" type="button" class="btn btn-info">Create link(s)</button><p style="color:red;" id="error"></p>
|
||||
</div>
|
||||
</form>
|
||||
</div><!-- /.box -->
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="col-md-6">
|
||||
<!-- general form elements -->
|
||||
<div class="box box-primary">
|
||||
<div class="box-header">
|
||||
<h3 class="box-title">Select a link</h3>
|
||||
</div><!-- /.box-header -->
|
||||
<form role="form">
|
||||
<div class="box-body">
|
||||
<div class="form-group">
|
||||
|
||||
<select class="form-control" id="fauselect" onchange="drawwithdraw()">
|
||||
<option></option>
|
||||
{% for w in user_fau %}
|
||||
<option id="{{w.uni}}" >{{w.tit}}-{{w.uni}}-{{w.inc}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<center> <br/><div id="qrcode" style="width:340px" ></div><br/><div style="width:75%;word-wrap: break-word;" id="qrcodetxt" ></div></center>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div><!-- /.box -->
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
<h3 class="box-title">Withdraw links <b id="withdraws"></b></h3>
|
||||
</div>
|
||||
<!-- /.box-header -->
|
||||
<div class="box-body no-padding">
|
||||
<table id="pagnation" class="table table-bswearing anchorordered table-striped">
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th style="width:15%">Link/ID</th>
|
||||
<th style="width:15%">Max Withdraw</th>
|
||||
<th style="width:15%">No. uses</th>
|
||||
<th style="width:15%">Wait</th>
|
||||
<th style="width:15%">Wallet</th>
|
||||
<th style="width:10%">Edit</th>
|
||||
<th style="width:10%">Del</th>
|
||||
</tr>
|
||||
<tbody id="transactions"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- /.box-body -->
|
||||
</div>
|
||||
<!-- /.box -->
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div id="editlink"></div>
|
||||
|
||||
<!-- /.content -->
|
||||
</section>
|
||||
|
||||
<script>
|
||||
window.user = {{ user | megajson | safe }}
|
||||
window.user_wallets = {{ user_wallets | megajson | safe }}
|
||||
window.user_ext = {{ user_ext | megajson | safe }}
|
||||
window.user_fau = {{ user_fau | megajson | safe }}
|
||||
|
||||
const user_fau = window.user_fau
|
||||
console.log(user_fau)
|
||||
|
||||
function erralert(){
|
||||
|
||||
var myUrlPattern = '.onion';
|
||||
if (window.location.hostname.indexOf("168") > 3 || location.hostname === "localhost" || location.hostname === "127.0.0.1" || window.location.hostname.indexOf(myUrlPattern) >= 0){
|
||||
document.getElementById("erralert").innerHTML = "<div class='alert alert-danger alert-dismissable'>"+
|
||||
"<h4>*Running LNURLw locally will likely need an SSH tunnel, or DNS magic."+
|
||||
"</h4></div>";
|
||||
}
|
||||
|
||||
}
|
||||
erralert();
|
||||
|
||||
function drawChart(user_fau) {
|
||||
var transactionsHTML = ''
|
||||
|
||||
for (var i = 0; i < user_fau.length; i++) {
|
||||
var tx = user_fau[i]
|
||||
console.log(tx.nme)
|
||||
// make the transactions table
|
||||
transactionsHTML =
|
||||
"<tr><td style='width: 50%'>" +
|
||||
tx.tit +
|
||||
'</td><td >' +
|
||||
"<a href='" + "{{ url_for('withdraw.display') }}?id=" + tx.uni + "'>" + tx.uni.substring(0, 4) + "...</a>" +
|
||||
'</td><td>' +
|
||||
tx.maxamt +
|
||||
'</td><td>' +
|
||||
tx.inc +
|
||||
'</td><td>' +
|
||||
tx.tme +
|
||||
'</td><td>' +
|
||||
"<a href='{{ url_for('wallet') }}?usr="+ user +"'>" + tx.uni.substring(0, 4) + "...</a>" +
|
||||
'</td><td>' +
|
||||
"<i onclick='editlink("+ i +")'' class='fa fa-edit'></i>" +
|
||||
'</td><td>' +
|
||||
"<b><a style='color:red;' href='" + "{{ url_for('withdraw.index') }}?del=" + tx.uni + "&usr=" + user +"'>" + "<i class='fa fa-trash'></i>" + "</a></b>" +
|
||||
'</td></tr>' +
|
||||
transactionsHTML
|
||||
document.getElementById('transactions').innerHTML = transactionsHTML
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (user_fau.length) {
|
||||
drawChart(user_fau)
|
||||
}
|
||||
|
||||
|
||||
//draws withdraw QR code
|
||||
function drawwithdraw() {
|
||||
|
||||
|
||||
walname = document.getElementById("fauselect").value
|
||||
|
||||
thewithdraw = walname.split("-");
|
||||
console.log(window.location.hostname + "-" + thewithdraw[1])
|
||||
|
||||
getAjax("/withdraw/api/v1/lnurlencode/"+ window.location.hostname + "/" + thewithdraw[1], "filla", function(datab) {
|
||||
if (JSON.parse(datab).status == 'TRUE') {
|
||||
lnurlfau = (JSON.parse(datab).lnurl)
|
||||
|
||||
|
||||
new QRCode(document.getElementById('qrcode'), {
|
||||
text: lnurlfau,
|
||||
width: 300,
|
||||
height: 300,
|
||||
colorDark: '#000000',
|
||||
colorLight: '#ffffff',
|
||||
correctLevel: QRCode.CorrectLevel.M
|
||||
})
|
||||
|
||||
if (thewithdraw[2] > 0){
|
||||
document.getElementById('qrcodetxt').innerHTML = lnurlfau
|
||||
+
|
||||
"<a href='{{ url_for('withdraw.display') }}?id=" + thewithdraw[1] + "'><h4>Shareable link</h4></a>" +
|
||||
"<a href='/withdraw/print/" + window.location.hostname + "/?id=" + thewithdraw[1] + "'><h4>Print all withdraws</h4></a>"
|
||||
document.getElementById("qrcode").style.backgroundColor = "white";
|
||||
document.getElementById("qrcode").style.padding = "20px";
|
||||
}
|
||||
else{
|
||||
document.getElementById('qrcode').innerHTML = ""
|
||||
document.getElementById('qrcodetxt').innerHTML = "<h1>No more uses left in link!</h1><br/><br/>"
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
thewithdraw[1] = "Failed to build LNURL"
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
function postfau(){
|
||||
|
||||
wal = document.getElementById('wal').value
|
||||
tit = document.getElementById('tit').value
|
||||
amt = document.getElementById('amt').value
|
||||
maxamt = document.getElementById('maxamt').value
|
||||
minamt = document.getElementById('minamt').value
|
||||
tme = document.getElementById('tme').value
|
||||
uniq = document.getElementById('uniq').checked
|
||||
|
||||
|
||||
if (tit == "") {
|
||||
document.getElementById("error").innerHTML = "Only use letters in title"
|
||||
return amt
|
||||
}
|
||||
if (wal == "") {
|
||||
document.getElementById("error").innerHTML = "No wallet selected"
|
||||
return amt
|
||||
}
|
||||
|
||||
if (isNaN(maxamt) || maxamt < 10) {
|
||||
document.getElementById("error").innerHTML = "Max 15 - 1000000 and must be higher than min"
|
||||
return amt
|
||||
}
|
||||
if (isNaN(minamt) || minamt < 1 || minamt > maxamt) {
|
||||
document.getElementById("error").innerHTML = "Min 1 - 1000000 and must be lower than max"
|
||||
return amt
|
||||
}
|
||||
|
||||
if (isNaN(amt) || amt < 1 || amt > 1000) {
|
||||
document.getElementById("error").innerHTML = "Amount of uses must be between 1 - 1000"
|
||||
return amt
|
||||
}
|
||||
|
||||
if (isNaN(tme) || tme < 1 || tme > 86400) {
|
||||
document.getElementById("error").innerHTML = "Max waiting time 1 day (86400 secs)"
|
||||
return amt
|
||||
}
|
||||
|
||||
|
||||
postAjax(
|
||||
"{{ url_for('withdraw.create') }}",
|
||||
JSON.stringify({"tit": tit, "amt": amt, "maxamt": maxamt, "minamt": minamt, "tme": tme, "wal": wal, "usr": user, "uniq": uniq}),
|
||||
"filla",
|
||||
|
||||
function(data) { location.replace("{{ url_for('withdraw.index') }}?usr=" + user)
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
function editlink(linknum){
|
||||
|
||||
faudetails = user_fau[linknum]
|
||||
|
||||
console.log(faudetails)
|
||||
wallpick = ""
|
||||
|
||||
checkbox = ""
|
||||
if (faudetails.uniq == 1){
|
||||
checkbox = "checked"}
|
||||
|
||||
document.getElementById('editlink').innerHTML = "<div class='row'>"+
|
||||
"<div class='col-md-6'>"+
|
||||
" <!-- general form elements -->"+
|
||||
"<div class='box box-primary'>"+
|
||||
"<div class='box-header'>"+
|
||||
"<h3 class='box-title'> Edit: <i id='unid'>" + faudetails.tit + "-" + faudetails.uni + "</i> </h3>"+
|
||||
"<div class='box-tools pull-right'>" +
|
||||
"<button class='btn btn-box-tool' data-widget='remove'><i class='fa fa-times'></i></button>" +
|
||||
"</div>" +
|
||||
" </div><!-- /.box-header -->"+
|
||||
" <!-- form start -->"+
|
||||
"<form role='form'>"+
|
||||
"<div class='box-body'>"+
|
||||
"<div class='col-sm-3 col-md-4'>"+
|
||||
"<div class='form-group'>"+
|
||||
"<label for='exampleInputEmail1'>Link title</label>"+
|
||||
"<input id='edittit' type='text' class='form-control' value='"+
|
||||
faudetails.tit +
|
||||
"'></input> </div>"+
|
||||
" </div>"+
|
||||
" <div class='col-sm-4 col-md-4'>"+
|
||||
" <!-- select -->"+
|
||||
" <div class='form-group'>"+
|
||||
" <label>Select a wallet</label>"+
|
||||
"<select id='editwal' class='form-control'>"+
|
||||
" <option>" + faudetails.walnme + "-" + faudetails.wal + "</option>"+
|
||||
" {% for w in user_wallets %}"+
|
||||
|
||||
" <option>{{w.name}}-{{w.id}}</option>"+
|
||||
|
||||
" {% endfor %}"+
|
||||
" </select>"+
|
||||
" </div>"+
|
||||
" </div>"+
|
||||
" <div class='col-sm-3 col-md-4'>"+
|
||||
"<div class='form-group'>"+
|
||||
" <label for='exampleInputPassword1'>Time between withdrawals:</label>"+
|
||||
" <input id='edittme' type='number' class='form-control' placeholder='0' max='86400' value='"+
|
||||
faudetails.tme +
|
||||
"'></input>"+
|
||||
"</div> </div>"+
|
||||
" <div class='col-sm-3 col-md-4'>"+
|
||||
"<div class='form-group'>"+
|
||||
"<label for='exampleInputEmail1'>Max withdraw:</label>"+
|
||||
" <input id='editmaxamt' type='number' class='form-control' placeholder='1' value='"+
|
||||
faudetails.maxamt +
|
||||
"'></input>"+
|
||||
" </div></div>"+
|
||||
" <div class='col-sm-3 col-md-4'>"+
|
||||
" <div class='form-group'>"+
|
||||
" <label for='exampleInputEmail1'>Min withdraw:</label>"+
|
||||
" <input id='editminamt' type='number' class='form-control' placeholder='1' value='"+
|
||||
faudetails.minamt +
|
||||
"'></input>"+
|
||||
" </div></div>"+
|
||||
" <div class='col-sm-3 col-md-4'>"+
|
||||
"<div class='form-group'>"+
|
||||
" <label for='exampleInputPassword1'>Amount of uses:</label>"+
|
||||
" <input id='editamt' type='number' class='form-control' placeholder='1' value='"+
|
||||
faudetails.inc +
|
||||
"'></input>"+
|
||||
" </div> </div>"+
|
||||
" <div class='col-sm-3 col-md-4'>"+
|
||||
" <div class='checkbox'>"+
|
||||
"<label data-toggle='tooltip' title='Some tooltip text!'><input id='edituniq' type='checkbox' "+
|
||||
checkbox +
|
||||
">"+
|
||||
"Unique links</label>"+
|
||||
"</div></div><!-- /.box-body -->"+
|
||||
" </div><br/>"+
|
||||
" <div class='box-footer'>"+
|
||||
" <button onclick='editlinkcont()' type='button' class='btn btn-info'>Edit link(s)</button><p style='color:red;' id='error2'></p>"+
|
||||
" </div></form></div><!-- /.box --></div></div>"
|
||||
|
||||
|
||||
}
|
||||
|
||||
function editlinkcont(){
|
||||
|
||||
unid = document.getElementById('unid').innerHTML
|
||||
wal = document.getElementById('editwal').value
|
||||
tit = document.getElementById('edittit').value
|
||||
amt = document.getElementById('editamt').value
|
||||
maxamt = document.getElementById('editmaxamt').value
|
||||
minamt = document.getElementById('editminamt').value
|
||||
tme = document.getElementById('edittme').value
|
||||
uniq = document.getElementById('edituniq').checked
|
||||
|
||||
|
||||
|
||||
if (tit == "") {
|
||||
document.getElementById("error2").innerHTML = "Only use letters in title"
|
||||
return amt
|
||||
}
|
||||
if (wal == "") {
|
||||
document.getElementById("error2").innerHTML = "No wallet selected"
|
||||
return amt
|
||||
}
|
||||
|
||||
if (isNaN(maxamt) || maxamt < 10) {
|
||||
document.getElementById("error2").innerHTML = "Max 10 - 1000000 and must be higher than min"
|
||||
return amt
|
||||
}
|
||||
if (isNaN(minamt) || minamt < 1 || minamt > maxamt) {
|
||||
document.getElementById("error2").innerHTML = "Min 1 - 1000000 and must be lower than max"
|
||||
return amt
|
||||
}
|
||||
|
||||
if (isNaN(amt) || amt < 1 || amt > 1000) {
|
||||
document.getElementById("error2").innerHTML = "Amount of uses must be between 1 - 1000"
|
||||
return amt
|
||||
}
|
||||
|
||||
if (isNaN(tme) || tme < 1 || tme > 86400) {
|
||||
document.getElementById("error2").innerHTML = "Max waiting time 1 day (86400 secs)"
|
||||
return amt
|
||||
}
|
||||
|
||||
|
||||
postAjax(
|
||||
"{{ url_for('withdraw.create') }}",
|
||||
JSON.stringify({"id": unid, "tit": tit, "amt": amt, "maxamt": maxamt, "minamt": minamt, "tme": tme, "wal": wal, "usr": user, "uniq": uniq}),
|
||||
"filla",
|
||||
|
||||
function(data) { location.replace("{{ url_for('withdraw.index') }}?usr=" + user)
|
||||
})
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
<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-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-btn>
|
||||
<q-btn unelevated dense size="xs" icon="crop_free" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" @click="openQrCodeDialog(props.row.id)"></q-btn>
|
||||
</q-td>
|
||||
<q-td
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
>
|
||||
{{ col.value }}
|
||||
</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">LNbits 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"
|
||||
label="Min withdrawable (sat) *"></q-input>
|
||||
<q-input filled dense
|
||||
v-model.number="formDialog.data.max_withdrawable"
|
||||
type="number"
|
||||
label="Max withdrawable (sat) *"></q-input>
|
||||
<q-input filled dense
|
||||
v-model.number="formDialog.data.uses"
|
||||
type="number"
|
||||
: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-list>
|
||||
<q-item tag="label" class="rounded-borders">
|
||||
<q-item-section avatar>
|
||||
<q-checkbox v-model="formDialog.data.is_unique" color="deep-purple"></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. NOT if you plan to print QR codes.</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<q-btn v-if="formDialog.data.id" unelevated color="deep-purple" type="submit">Update withdraw link</q-btn>
|
||||
<q-btn v-else unelevated
|
||||
color="deep-purple"
|
||||
: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>
|
||||
</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">
|
||||
{% raw %}
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||
<qrcode :value="qrCodeDialog.data.lnurl" :options="{width: 800}" class="rounded-borders"></qrcode>
|
||||
</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="deep-purple" class="q-mt-sm"></q-linear-progress>
|
||||
</p>
|
||||
{% endraw %}
|
||||
<div class="row q-mt-md">
|
||||
<q-btn flat color="grey" @click="copyText(qrCodeDialog.data.withdraw_url)">Copy link</q-btn>
|
||||
<q-btn v-if="!qrCodeDialog.data.is_unique" flat color="grey" type="a" :href="qrCodeDialog.data.print_url" target="_blank">Print QR codes</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,291 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html style="background-color:grey;">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>LNBits Wallet</title>
|
||||
<meta
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
|
||||
name="viewport"
|
||||
/>
|
||||
<!-- Bootstrap 3.3.2 -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}"
|
||||
/>
|
||||
<!-- FontAwesome 4.3.0 -->
|
||||
<link
|
||||
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<!-- Ionicons 2.0.0 -->
|
||||
<link
|
||||
href="https://code.ionicframework.com/ionicons/2.0.0/css/ionicons.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
|
||||
<!-- Theme style -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='dist/css/AdminLTE.min.css') }}"
|
||||
/>
|
||||
<!-- AdminLTE Skins. Choose a skin from the css/skins
|
||||
folder instead of downloading all of them to reduce the load. -->
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='dist/css/skins/_all-skins.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- Morris chart -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/morris/morris.css') }}"
|
||||
/>
|
||||
|
||||
<!-- jvectormap -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.css') }}"
|
||||
/>
|
||||
|
||||
<!-- bootstrap wysihtml5 - text editor -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
||||
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<style>
|
||||
.small-box > .small-box-footer {
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#loadingMessage {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#output {
|
||||
margin-top: 20px;
|
||||
background: #eee;
|
||||
padding: 10px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#output div {
|
||||
padding-bottom: 10px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#noQRFound {
|
||||
text-align: center;
|
||||
}
|
||||
.layout-boxed {
|
||||
background: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- jQuery 2.1.3 -->
|
||||
<script src="{{ url_for('static', filename='plugins/jQuery/jQuery-2.1.3.min.js') }}"></script>
|
||||
<!-- jQuery UI 1.11.2 -->
|
||||
<script
|
||||
src="https://code.jquery.com/ui/1.11.2/jquery-ui.min.js"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Resolve conflict in jQuery UI tooltip with Bootstrap tooltip -->
|
||||
<script>
|
||||
$.widget.bridge('uibutton', $.ui.button)
|
||||
</script>
|
||||
<!-- Bootstrap 3.3.2 JS -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='bootstrap/js/bootstrap.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Morris.js charts -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/morris/morris.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Sparkline -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/sparkline/jquery.sparkline.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- jvectormap -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-world-mill-en.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- jQuery Knob Chart -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/knob/jquery.knob.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Bootstrap WYSIHTML5 -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Slimscroll -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/slimScroll/jquery.slimscroll.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- FastClick -->
|
||||
<script src="{{ url_for('static', filename='plugins/fastclick/fastclick.min.js') }}"></script>
|
||||
<!-- AdminLTE App -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/app.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- AdminLTE dashboard demo (This is only for demo purposes) -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/pages/dashboard.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- AdminLTE for demo purposes -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/demo.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/datatables/jquery.dataTables.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css"
|
||||
/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js"></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jscam/JS.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jscam/qrcode.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bolt11/decoder.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bolt11/utils.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
</head>
|
||||
<body class="skin-white layout-boxed sidebar-collapse sidebar-open">
|
||||
<div class="wrapper">
|
||||
|
||||
<style>
|
||||
body {
|
||||
width: 210mm;
|
||||
/* to centre page on screen*/
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color:white;
|
||||
}
|
||||
.layout-boxed .wrapper {
|
||||
box-shadow: none;
|
||||
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="allqrs"></div>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
<script>
|
||||
function getAjax(url, thekey, success) {
|
||||
var xhr = window.XMLHttpRequest
|
||||
? new XMLHttpRequest()
|
||||
: new ActiveXObject('Microsoft.XMLHTTP')
|
||||
xhr.open('GET', url, true)
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState > 3 && xhr.status == 200) {
|
||||
success(xhr.responseText)
|
||||
}
|
||||
}
|
||||
xhr.setRequestHeader('Grpc-Metadata-macaroon', thekey)
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
|
||||
xhr.send()
|
||||
return xhr
|
||||
}
|
||||
|
||||
window.user_fau = {{ user_fau | megajson | safe }}
|
||||
|
||||
|
||||
function drawwithdraw(data, id) {
|
||||
|
||||
new QRCode(document.getElementById(id), {
|
||||
text: data,
|
||||
width: 120,
|
||||
height: 120,
|
||||
colorDark: '#000000',
|
||||
colorLight: '#ffffff',
|
||||
correctLevel: QRCode.CorrectLevel.M
|
||||
} )
|
||||
|
||||
|
||||
}
|
||||
lnurlar = {{ lnurlar|tojson }}
|
||||
lnurlamt = user_fau["inc"]
|
||||
console.log(user_fau)
|
||||
allqr = ""
|
||||
|
||||
for (i = 0; i < lnurlamt; i++) {
|
||||
allqr += "<div style='float:left;padding:20px; background-image: url(/static/noted.jpg); width: 500px;height: 248px;'><div style='width:120px;float:right;margin-top:-16px;margin-right:-19px;background-color: white;'><div id='qrcode" + i + "'></div><center><p>{{user_fau[7]}} FREE SATS! <br/> <small style='font-size: 52%;'>SCAN AND FOLLOW LINK OR<br/>USE LN BITCOIN WALLET</small></p></center></div></div>"
|
||||
}
|
||||
|
||||
document.getElementById("allqrs").innerHTML = allqr
|
||||
|
||||
if (typeof lnurlar[1] != 'undefined'){
|
||||
for (i = 0; i < lnurlamt; i++) {
|
||||
drawwithdraw(lnurlar[i], "qrcode" + i)
|
||||
}
|
||||
window.print()
|
||||
}
|
||||
else{
|
||||
for (i = 0; i < lnurlamt; i++) {
|
||||
drawwithdraw(lnurlar[0], "qrcode" + i)
|
||||
}
|
||||
window.print()
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
</html>
|
66
lnbits/extensions/withdraw/templates/withdraw/print_qr.html
Normal file
66
lnbits/extensions/withdraw/templates/withdraw/print_qr.html
Normal file
|
@ -0,0 +1,66 @@
|
|||
{% extends "print.html" %}
|
||||
|
||||
|
||||
{% block page %}
|
||||
<div class="row justify-center">
|
||||
<div class="col-12 col-sm-8 col-lg-6 text-center">
|
||||
{% for i in range(link.uses) %}
|
||||
<div class="zimbabwe">
|
||||
<div class="qr">
|
||||
<qrcode value="{{ link.lnurl }}" :options="{width: 150}"></qrcode>
|
||||
<br><br>
|
||||
<strong>{{ SITE_TITLE }}</strong><br>
|
||||
<strong>{{ link.max_withdrawable }} FREE SATS</strong><br>
|
||||
<small>Scan and follow link<br>or use Lightning wallet</small>
|
||||
</div>
|
||||
<img src="{{ url_for('static', filename='images/note.jpg') }}">
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
<style>
|
||||
.zimbabwe {
|
||||
page-break-inside: avoid;
|
||||
height: 7cm;
|
||||
width: 16cm;
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.zimbabwe img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.zimbabwe .qr {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
background: rgb(255, 255, 255, 0.7);
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
line-height: 1.1;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode);
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
created: function () {
|
||||
window.print();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -1,161 +1,28 @@
|
|||
import uuid
|
||||
from flask import g, abort, render_template
|
||||
|
||||
from flask import jsonify, render_template, request, redirect, url_for
|
||||
from lnurl import encode as lnurl_encode
|
||||
from datetime import datetime
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.helpers import Status
|
||||
|
||||
from lnbits.db import open_db, open_ext_db
|
||||
from lnbits.extensions.withdraw import withdraw_ext
|
||||
from .crud import get_withdraw_link
|
||||
|
||||
|
||||
@withdraw_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
def index():
|
||||
"""Main withdraw link page."""
|
||||
|
||||
usr = request.args.get("usr")
|
||||
|
||||
if usr:
|
||||
if not len(usr) > 20:
|
||||
return redirect(url_for("home"))
|
||||
|
||||
# Get all the data
|
||||
with open_db() as db:
|
||||
user_wallets = db.fetchall("SELECT * FROM wallets WHERE user = ?", (usr,))
|
||||
user_ext = db.fetchall("SELECT extension FROM extensions WHERE user = ? AND active = 1", (usr,))
|
||||
user_ext = [v[0] for v in user_ext]
|
||||
|
||||
with open_ext_db("withdraw") as withdraw_ext_db:
|
||||
user_fau = withdraw_ext_db.fetchall("SELECT * FROM withdraws WHERE usr = ?", (usr,))
|
||||
|
||||
# If del is selected by user from withdraw page, the withdraw link is to be deleted
|
||||
faudel = request.args.get("del")
|
||||
if faudel:
|
||||
withdraw_ext_db.execute("DELETE FROM withdraws WHERE uni = ?", (faudel,))
|
||||
user_fau = withdraw_ext_db.fetchall("SELECT * FROM withdraws WHERE usr = ?", (usr,))
|
||||
|
||||
return render_template(
|
||||
"withdraw/index.html", user_wallets=user_wallets, user=usr, user_ext=user_ext, user_fau=user_fau
|
||||
)
|
||||
return render_template("withdraw/index.html", user=g.user)
|
||||
|
||||
|
||||
@withdraw_ext.route("/create", methods=["GET", "POST"])
|
||||
def create():
|
||||
"""."""
|
||||
@withdraw_ext.route("/<link_id>")
|
||||
def display(link_id):
|
||||
link = get_withdraw_link(link_id) or abort(Status.NOT_FOUND, "Withdraw link does not exist.")
|
||||
|
||||
data = request.json
|
||||
amt = data["amt"]
|
||||
tit = data["tit"]
|
||||
wal = data["wal"]
|
||||
minamt = data["minamt"]
|
||||
maxamt = data["maxamt"]
|
||||
tme = data["tme"]
|
||||
uniq = data["uniq"]
|
||||
usr = data["usr"]
|
||||
wall = wal.split("-")
|
||||
|
||||
# Form validation
|
||||
if (
|
||||
int(amt) < 0
|
||||
or not tit.replace(" ", "").isalnum()
|
||||
or wal == ""
|
||||
or int(minamt) < 0
|
||||
or int(maxamt) < 0
|
||||
or int(minamt) > int(maxamt)
|
||||
or int(tme) < 0
|
||||
):
|
||||
return jsonify({"ERROR": "FORM ERROR"}), 401
|
||||
|
||||
# If id that means its a link being edited, delet the record first
|
||||
if "id" in data:
|
||||
unid = data["id"].split("-")
|
||||
uni = unid[1]
|
||||
with open_ext_db("withdraw") as withdraw_ext_db:
|
||||
withdraw_ext_db.execute("DELETE FROM withdraws WHERE uni = ?", (unid[1],))
|
||||
else:
|
||||
uni = uuid.uuid4().hex
|
||||
|
||||
# Randomiser for random QR option
|
||||
rand = ""
|
||||
if uniq > 0:
|
||||
for x in range(0, int(amt)):
|
||||
rand += uuid.uuid4().hex[0:5] + ","
|
||||
else:
|
||||
rand = uuid.uuid4().hex[0:5] + ","
|
||||
|
||||
with open_db() as dbb:
|
||||
user_wallets = dbb.fetchall("SELECT * FROM wallets WHERE user = ? AND id = ?", (usr, wall[1],))
|
||||
if not user_wallets:
|
||||
return jsonify({"ERROR": "NO WALLET USER"}), 401
|
||||
|
||||
# Get time
|
||||
dt = datetime.now()
|
||||
seconds = dt.timestamp()
|
||||
|
||||
with open_db() as db:
|
||||
user_ext = db.fetchall("SELECT * FROM extensions WHERE user = ?", (usr,))
|
||||
user_ext = [v[0] for v in user_ext]
|
||||
|
||||
# Add to DB
|
||||
with open_ext_db("withdraw") as withdraw_ext_db:
|
||||
withdraw_ext_db.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO withdraws
|
||||
(usr, wal, walnme, adm, uni, tit, maxamt, minamt, spent, inc, tme, uniq, withdrawals, tmestmp, rand)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
usr,
|
||||
wall[1],
|
||||
user_wallets[0][1],
|
||||
user_wallets[0][3],
|
||||
uni,
|
||||
tit,
|
||||
maxamt,
|
||||
minamt,
|
||||
0,
|
||||
amt,
|
||||
tme,
|
||||
uniq,
|
||||
0,
|
||||
seconds,
|
||||
rand,
|
||||
),
|
||||
)
|
||||
|
||||
user_fau = withdraw_ext_db.fetchall("SELECT * FROM withdraws WHERE usr = ?", (usr,))
|
||||
|
||||
if not user_fau:
|
||||
return jsonify({"ERROR": "NO WALLET USER"}), 401
|
||||
|
||||
return render_template(
|
||||
"withdraw/index.html", user_wallets=user_wallets, user=usr, user_ext=user_ext, user_fau=user_fau
|
||||
)
|
||||
return render_template("withdraw/display.html", link=link)
|
||||
|
||||
|
||||
@withdraw_ext.route("/display", methods=["GET", "POST"])
|
||||
def display():
|
||||
"""Simple shareable link."""
|
||||
fauid = request.args.get("id")
|
||||
@withdraw_ext.route("/print/<link_id>")
|
||||
def print_qr(link_id):
|
||||
link = get_withdraw_link(link_id) or abort(Status.NOT_FOUND, "Withdraw link does not exist.")
|
||||
|
||||
with open_ext_db("withdraw") as withdraw_ext_db:
|
||||
user_fau = withdraw_ext_db.fetchall("SELECT * FROM withdraws WHERE uni = ?", (fauid,))
|
||||
|
||||
return render_template("withdraw/display.html", user_fau=user_fau,)
|
||||
|
||||
|
||||
@withdraw_ext.route("/print/<urlstr>/", methods=["GET", "POST"])
|
||||
def print_qr(urlstr):
|
||||
"""Simple printable page of links."""
|
||||
fauid = request.args.get("id")
|
||||
|
||||
with open_ext_db("withdraw") as withdraw_ext_db:
|
||||
user_fau = withdraw_ext_db.fetchall("SELECT * FROM withdraws WHERE uni = ?", (fauid,))
|
||||
randar = user_fau[0][15].split(",")
|
||||
randar = randar[:-1]
|
||||
lnurlar = []
|
||||
|
||||
for d in range(len(randar)):
|
||||
url = url_for("withdraw.api_lnurlfetch", _external=True, urlstr=urlstr, parstr=fauid, rand=randar[d])
|
||||
lnurlar.append(lnurl_encode(url.replace("http://", "https://")))
|
||||
|
||||
return render_template("withdraw/print.html", lnurlar=lnurlar, user_fau=user_fau[0],)
|
||||
return render_template("withdraw/print_qr.html", link=link)
|
||||
|
|
|
@ -1,201 +1,148 @@
|
|||
import uuid
|
||||
import json
|
||||
import requests
|
||||
|
||||
from flask import jsonify, request, url_for
|
||||
from lnurl import LnurlWithdrawResponse, encode as lnurl_encode
|
||||
from datetime import datetime
|
||||
from flask import g, jsonify, request
|
||||
|
||||
from lnbits.core.crud import get_user, get_wallet
|
||||
from lnbits.core.services import pay_invoice
|
||||
from lnbits.decorators import api_check_wallet_macaroon, api_validate_post_request
|
||||
from lnbits.helpers import urlsafe_short_hash, Status
|
||||
|
||||
from lnbits.db import open_ext_db, open_db
|
||||
from lnbits.extensions.withdraw import withdraw_ext
|
||||
from .crud import (
|
||||
create_withdraw_link,
|
||||
get_withdraw_link,
|
||||
get_withdraw_link_by_hash,
|
||||
get_withdraw_links,
|
||||
update_withdraw_link,
|
||||
delete_withdraw_link,
|
||||
)
|
||||
|
||||
|
||||
@withdraw_ext.route("/api/v1/lnurlencode/<urlstr>/<parstr>", methods=["GET"])
|
||||
def api_lnurlencode(urlstr, parstr):
|
||||
"""Returns encoded LNURL if web url and parameter gieven."""
|
||||
@withdraw_ext.route("/api/v1/links", methods=["GET"])
|
||||
@api_check_wallet_macaroon(key_type="invoice")
|
||||
def api_links():
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if not urlstr:
|
||||
return jsonify({"status": "FALSE"}), 200
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
with open_ext_db("withdraw") as withdraw_ext_db:
|
||||
user_fau = withdraw_ext_db.fetchall("SELECT * FROM withdraws WHERE uni = ?", (parstr,))
|
||||
randar = user_fau[0][15].split(",")
|
||||
print(randar)
|
||||
# randar = randar[:-1]
|
||||
# If "Unique links" selected get correct rand, if not there is only one rand
|
||||
if user_fau[0][12] > 0:
|
||||
rand = randar[user_fau[0][10] - 1]
|
||||
return jsonify([{**link._asdict(), **{"lnurl": link.lnurl}} for link in get_withdraw_links(wallet_ids)]), Status.OK
|
||||
|
||||
|
||||
@withdraw_ext.route("/api/v1/links/<link_id>", methods=["GET"])
|
||||
@api_check_wallet_macaroon(key_type="invoice")
|
||||
def api_link_retrieve(link_id):
|
||||
link = get_withdraw_link(link_id)
|
||||
|
||||
if not link:
|
||||
return jsonify({"message": "Withdraw link does not exist."}), Status.NOT_FOUND
|
||||
|
||||
if link.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your withdraw link."}), Status.FORBIDDEN
|
||||
|
||||
return jsonify({**link._asdict(), **{"lnurl": link.lnurl}}), Status.OK
|
||||
|
||||
|
||||
@withdraw_ext.route("/api/v1/links", methods=["POST"])
|
||||
@withdraw_ext.route("/api/v1/links/<link_id>", methods=["PUT"])
|
||||
@api_check_wallet_macaroon(key_type="invoice")
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"title": {"type": "string", "empty": False, "required": True},
|
||||
"min_withdrawable": {"type": "integer", "min": 1, "required": True},
|
||||
"max_withdrawable": {"type": "integer", "min": 1, "required": True},
|
||||
"uses": {"type": "integer", "min": 1, "required": True},
|
||||
"wait_time": {"type": "integer", "min": 1, "required": True},
|
||||
"is_unique": {"type": "boolean", "required": True},
|
||||
}
|
||||
)
|
||||
def api_link_create(link_id=None):
|
||||
if g.data["max_withdrawable"] < g.data["min_withdrawable"]:
|
||||
return jsonify({"message": "`max_withdrawable` needs to be at least `min_withdrawable`."}), Status.BAD_REQUEST
|
||||
|
||||
if (g.data["max_withdrawable"] * g.data["uses"] * 1000) > g.wallet.balance_msat:
|
||||
return jsonify({"message": "Insufficient balance."}), Status.FORBIDDEN
|
||||
|
||||
if link_id:
|
||||
link = get_withdraw_link(link_id)
|
||||
|
||||
if not link:
|
||||
return jsonify({"message": "Withdraw link does not exist."}), Status.NOT_FOUND
|
||||
|
||||
if link.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your withdraw link."}), Status.FORBIDDEN
|
||||
|
||||
link = update_withdraw_link(link_id, **g.data)
|
||||
else:
|
||||
rand = randar[0]
|
||||
link = create_withdraw_link(wallet_id=g.wallet.id, **g.data)
|
||||
|
||||
url = url_for("withdraw.api_lnurlfetch", _external=True, urlstr=urlstr, parstr=parstr, rand=rand)
|
||||
|
||||
if "onion" in url:
|
||||
return jsonify({"status": "TRUE", "lnurl": lnurl_encode(url)}), 200
|
||||
print(url)
|
||||
|
||||
return jsonify({"status": "TRUE", "lnurl": lnurl_encode(url.replace("http://", "https://"))}), 200
|
||||
return jsonify({**link._asdict(), **{"lnurl": link.lnurl}}), Status.OK if link_id else Status.CREATED
|
||||
|
||||
|
||||
@withdraw_ext.route("/api/v1/links/<link_id>", methods=["DELETE"])
|
||||
@api_check_wallet_macaroon(key_type="invoice")
|
||||
def api_link_delete(link_id):
|
||||
link = get_withdraw_link(link_id)
|
||||
|
||||
if not link:
|
||||
return jsonify({"message": "Withdraw link does not exist."}), Status.NOT_FOUND
|
||||
|
||||
if link.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your withdraw link."}), Status.FORBIDDEN
|
||||
|
||||
delete_withdraw_link(link_id)
|
||||
|
||||
return "", Status.NO_CONTENT
|
||||
|
||||
|
||||
@withdraw_ext.route("/api/v1/lnurlfetch/<urlstr>/<parstr>/<rand>", methods=["GET"])
|
||||
def api_lnurlfetch(parstr, urlstr, rand):
|
||||
"""Returns LNURL json."""
|
||||
@withdraw_ext.route("/api/v1/lnurl/<unique_hash>", methods=["GET"])
|
||||
def api_lnurl_response(unique_hash):
|
||||
link = get_withdraw_link_by_hash(unique_hash)
|
||||
|
||||
if not parstr:
|
||||
return jsonify({"status": "FALSE", "ERROR": "NO WALL ID"}), 200
|
||||
if not link:
|
||||
return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), Status.OK
|
||||
|
||||
if not urlstr:
|
||||
return jsonify({"status": "FALSE", "ERROR": "NO URL"}), 200
|
||||
link = update_withdraw_link(link.id, k1=urlsafe_short_hash())
|
||||
|
||||
with open_ext_db("withdraw") as withdraw_ext_db:
|
||||
user_fau = withdraw_ext_db.fetchall("SELECT * FROM withdraws WHERE uni = ?", (parstr,))
|
||||
k1str = uuid.uuid4().hex
|
||||
withdraw_ext_db.execute("UPDATE withdraws SET withdrawals = ? WHERE uni = ?", (k1str, parstr,))
|
||||
|
||||
precallback = url_for("withdraw.api_lnurlwithdraw", _external=True, rand=rand)
|
||||
|
||||
if "onion" in precallback:
|
||||
print(precallback)
|
||||
else:
|
||||
precallback = url_for("withdraw.api_lnurlwithdraw", _external=True, rand=rand).replace("http://", "https://")
|
||||
|
||||
res = LnurlWithdrawResponse(
|
||||
callback=precallback,
|
||||
k1=k1str,
|
||||
min_withdrawable=user_fau[0][8] * 1000,
|
||||
max_withdrawable=user_fau[0][7] * 1000,
|
||||
default_description="LNbits LNURL withdraw",
|
||||
)
|
||||
|
||||
return res.json(), 200
|
||||
return jsonify(link.lnurl_response.dict()), Status.OK
|
||||
|
||||
|
||||
@withdraw_ext.route("/api/v1/lnurlwithdraw/<rand>/", methods=["GET"])
|
||||
def api_lnurlwithdraw(rand):
|
||||
"""Pays invoice if passed k1 invoice and rand."""
|
||||
@withdraw_ext.route("/api/v1/lnurl/cb/<unique_hash>", methods=["GET"])
|
||||
def api_lnurl_callback(unique_hash):
|
||||
link = get_withdraw_link_by_hash(unique_hash)
|
||||
k1 = request.args.get("k1", type=str)
|
||||
payment_request = request.args.get("pr", type=str)
|
||||
now = int(datetime.now().timestamp())
|
||||
|
||||
k1 = request.args.get("k1")
|
||||
pr = request.args.get("pr")
|
||||
if not link:
|
||||
return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), Status.OK
|
||||
|
||||
if not k1:
|
||||
return jsonify({"status": "FALSE", "ERROR": "NO k1"}), 200
|
||||
if link.is_spent:
|
||||
return jsonify({"status": "ERROR", "reason": "Withdraw is spent."}), Status.OK
|
||||
|
||||
if not pr:
|
||||
return jsonify({"status": "FALSE", "ERROR": "NO PR"}), 200
|
||||
if link.k1 != k1:
|
||||
return jsonify({"status": "ERROR", "reason": "Bad request."}), Status.OK
|
||||
|
||||
with open_ext_db("withdraw") as withdraw_ext_db:
|
||||
user_fau = withdraw_ext_db.fetchall("SELECT * FROM withdraws WHERE withdrawals = ?", (k1,))
|
||||
if now < link.open_time:
|
||||
return jsonify({"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."}), Status.OK
|
||||
|
||||
if not user_fau:
|
||||
return jsonify({"status": "ERROR", "reason": "NO AUTH"}), 400
|
||||
try:
|
||||
pay_invoice(wallet=get_wallet(link.wallet), bolt11=payment_request, max_sat=link.max_withdrawable)
|
||||
|
||||
if user_fau[0][10] < 1:
|
||||
return jsonify({"status": "ERROR", "reason": "withdraw SPENT"}), 400
|
||||
changes = {
|
||||
"used": link.used + 1,
|
||||
"open_time": link.wait_time + now,
|
||||
}
|
||||
|
||||
# Check withdraw time
|
||||
dt = datetime.now()
|
||||
seconds = dt.timestamp()
|
||||
secspast = seconds - user_fau[0][14]
|
||||
|
||||
if secspast < user_fau[0][11]:
|
||||
return jsonify({"status": "ERROR", "reason": "WAIT " + str(int(user_fau[0][11] - secspast)) + "s"}), 400
|
||||
|
||||
randar = user_fau[0][15].split(",")
|
||||
if rand not in randar:
|
||||
print("huhhh")
|
||||
return jsonify({"status": "ERROR", "reason": "BAD AUTH"}), 400
|
||||
if len(randar) > 2:
|
||||
randar.remove(rand)
|
||||
randstr = ",".join(randar)
|
||||
|
||||
# Update time and increments
|
||||
upinc = int(user_fau[0][10]) - 1
|
||||
withdraw_ext_db.execute(
|
||||
"UPDATE withdraws SET inc = ?, rand = ?, tmestmp = ? WHERE withdrawals = ?", (upinc, randstr, seconds, k1,)
|
||||
)
|
||||
|
||||
header = {"Content-Type": "application/json", "Grpc-Metadata-macaroon": str(user_fau[0][4])}
|
||||
data = {"payment_request": pr}
|
||||
# this works locally but not being served over host, bug, needs fixing
|
||||
# r = requests.post(url="https://lnbits.com/api/v1/channels/transactions", headers=header, data=json.dumps(data))
|
||||
r = requests.post(url=url_for("api_transactions", _external=True), headers=header, data=json.dumps(data))
|
||||
r_json = r.json()
|
||||
|
||||
if "ERROR" in r_json:
|
||||
return jsonify({"status": "ERROR", "reason": r_json["ERROR"]}), 400
|
||||
|
||||
with open_ext_db("withdraw") as withdraw_ext_db:
|
||||
user_fau = withdraw_ext_db.fetchall("SELECT * FROM withdraws WHERE withdrawals = ?", (k1,))
|
||||
|
||||
return jsonify({"status": "OK"}), 200
|
||||
|
||||
@withdraw_ext.route("/api/v1/lnurlmaker", methods=["GET","POST"])
|
||||
def api_lnurlmaker():
|
||||
|
||||
if request.headers["Content-Type"] != "application/json":
|
||||
return jsonify({"ERROR": "MUST BE JSON"}), 400
|
||||
|
||||
with open_db() as db:
|
||||
wallet = db.fetchall(
|
||||
"SELECT * FROM wallets WHERE adminkey = ?",
|
||||
(request.headers["Grpc-Metadata-macaroon"],),
|
||||
)
|
||||
if not wallet:
|
||||
return jsonify({"ERROR": "NO KEY"}), 200
|
||||
|
||||
balance = db.fetchone("SELECT balance/1000 FROM balances WHERE wallet = ?", (wallet[0][0],))[0]
|
||||
print(balance)
|
||||
|
||||
postedjson = request.json
|
||||
print(postedjson["amount"])
|
||||
|
||||
if balance < int(postedjson["amount"]):
|
||||
return jsonify({"ERROR": "NOT ENOUGH FUNDS"}), 200
|
||||
|
||||
uni = uuid.uuid4().hex
|
||||
rand = uuid.uuid4().hex[0:5]
|
||||
|
||||
with open_ext_db("withdraw") as withdraw_ext_db:
|
||||
withdraw_ext_db.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO withdraws
|
||||
(usr, wal, walnme, adm, uni, tit, maxamt, minamt, spent, inc, tme, uniq, withdrawals, tmestmp, rand)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
wallet[0][2],
|
||||
wallet[0][0],
|
||||
wallet[0][1],
|
||||
wallet[0][3],
|
||||
uni,
|
||||
postedjson["memo"],
|
||||
postedjson["amount"],
|
||||
postedjson["amount"],
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
rand,
|
||||
),
|
||||
)
|
||||
|
||||
user_fau = withdraw_ext_db.fetchone("SELECT * FROM withdraws WHERE uni = ?", (uni,))
|
||||
|
||||
if not user_fau:
|
||||
return jsonify({"ERROR": "WITHDRAW NOT MADE"}), 401
|
||||
|
||||
url = url_for("withdraw.api_lnurlfetch", _external=True, urlstr=request.host, parstr=uni, rand=rand)
|
||||
|
||||
if "onion" in url:
|
||||
return jsonify({"status": "TRUE", "lnurl": lnurl_encode(url)}), 200
|
||||
print(url)
|
||||
|
||||
return jsonify({"status": "TRUE", "lnurl": lnurl_encode(url.replace("http://", "https://"))}), 200
|
||||
if link.is_unique:
|
||||
changes["unique_hash"] = urlsafe_short_hash()
|
||||
|
||||
update_withdraw_link(link.id, **changes)
|
||||
|
||||
except ValueError as e:
|
||||
return jsonify({"status": "ERROR", "reason": str(e)}), Status.OK
|
||||
except PermissionError:
|
||||
return jsonify({"status": "ERROR", "reason": "Withdraw link is empty."}), Status.OK
|
||||
except Exception as e:
|
||||
return jsonify({"status": "ERROR", "reason": str(e)}), Status.OK
|
||||
|
||||
return jsonify({"status": "OK"}), Status.OK
|
||||
|
|
48
lnbits/templates/print.html
Normal file
48
lnbits/templates/print.html
Normal file
|
@ -0,0 +1,48 @@
|
|||
<!doctype html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Material+Icons" type="text/css">
|
||||
<style>
|
||||
@page {
|
||||
size: A4 portrait;
|
||||
}
|
||||
body {
|
||||
font-family: Roboto,-apple-system,Helvetica Neue,Helvetica,Arial,sans-serif;
|
||||
}
|
||||
</style>
|
||||
{% block styles %}{% endblock %}
|
||||
<title>
|
||||
{% block title %}
|
||||
{% if SITE_TITLE != 'LNbits' %}{{ SITE_TITLE }}{% else %}LNbits{% endif %}
|
||||
{% endblock %}
|
||||
</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
{% block head_scripts %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<q-layout id="vue" view="hHh lpR lfr" v-cloak>
|
||||
<q-page-container>
|
||||
<q-page class="q-px-md q-py-lg" :class="{'q-px-lg': $q.screen.gt.xs}">
|
||||
{% block page %}{% endblock %}
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
|
||||
{% if DEBUG %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue@2.6.11/vue.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='vendor/quasar@1.9.12/quasar.umd.js') }}"></script>
|
||||
{% else %}
|
||||
{% assets output='__bundle__/vue-print.js',
|
||||
'vendor/quasar@1.9.12/quasar.ie.polyfills.umd.min.js',
|
||||
'vendor/vue@2.6.11/vue.min.js',
|
||||
'vendor/quasar@1.9.12/quasar.umd.min.js' %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
{% endif %}
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
6
lnbits/templates/public.html
Normal file
6
lnbits/templates/public.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
|
||||
{% block beta %}{% endblock %}
|
||||
{% block drawer_toggle %}{% endblock %}
|
||||
{% block drawer %}{% endblock %}
|
Loading…
Add table
Reference in a new issue