Browse Source

refactor(withdraw): migrate extension to Vue

fee_issues
Eneko Illarramendi 5 years ago
parent
commit
a834a64319
  1. 2
      lnbits/extensions/withdraw/config.json
  2. 94
      lnbits/extensions/withdraw/crud.py
  3. 65
      lnbits/extensions/withdraw/migrations.py
  4. 46
      lnbits/extensions/withdraw/models.py
  5. 169
      lnbits/extensions/withdraw/static/js/index.js
  6. 35
      lnbits/extensions/withdraw/templates/withdraw/_api_docs.html
  7. 10
      lnbits/extensions/withdraw/templates/withdraw/_lnurl.html
  8. 576
      lnbits/extensions/withdraw/templates/withdraw/display.html
  9. 670
      lnbits/extensions/withdraw/templates/withdraw/index.html
  10. 291
      lnbits/extensions/withdraw/templates/withdraw/print.html
  11. 66
      lnbits/extensions/withdraw/templates/withdraw/print_qr.html
  12. 163
      lnbits/extensions/withdraw/views.py
  13. 331
      lnbits/extensions/withdraw/views_api.py
  14. 48
      lnbits/templates/print.html
  15. 6
      lnbits/templates/public.html

2
lnbits/extensions/withdraw/config.json

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

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

65
lnbits/extensions/withdraw/migrations.py

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

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

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

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

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

576
lnbits/extensions/withdraw/templates/withdraw/display.html

@ -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>
<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>
</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>
{% extends "public.html" %}
{% 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>
</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>
</q-card-section>
</q-card>
</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
}
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] }}")
<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>
Vue.component(VueQrcode.name, VueQrcode);
new Vue({
el: '#vue',
mixins: [windowMixin]
});
</script>
</html>
{% endblock %}

670
lnbits/extensions/withdraw/templates/withdraw/index.html

@ -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>
{% 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 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" >
</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>
{% 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 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>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Withdraw links</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div>
</div>
<q-table dense flat
:data="sortedWithdrawLinks"
row-key="id"
:columns="withdrawLinksTable.columns"
:pagination.sync="withdrawLinksTable.pagination">
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-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-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 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>
<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>
<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>
<!-- /.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 -->
<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>
<!-- /.box -->
</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>
</div>
</q-card>
</q-dialog>
</div>
{% endblock %}

291
lnbits/extensions/withdraw/templates/withdraw/print.html

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

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

163
lnbits/extensions/withdraw/views.py

@ -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."""
return render_template("withdraw/index.html", user=g.user)
usr = request.args.get("usr")
if usr:
if not len(usr) > 20:
return redirect(url_for("home"))
@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.")
# 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]
return render_template("withdraw/display.html", link=link)
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,))
@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.")
return render_template(
"withdraw/index.html", user_wallets=user_wallets, user=usr, user_ext=user_ext, user_fau=user_fau
)
@withdraw_ext.route("/create", methods=["GET", "POST"])
def create():
"""."""
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
)
@withdraw_ext.route("/display", methods=["GET", "POST"])
def display():
"""Simple shareable link."""
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,))
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)

331
lnbits/extensions/withdraw/views_api.py

@ -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/links", methods=["GET"])
@api_check_wallet_macaroon(key_type="invoice")
def api_links():
wallet_ids = [g.wallet.id]
if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids
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/lnurlencode/<urlstr>/<parstr>", methods=["GET"])
def api_lnurlencode(urlstr, parstr):
"""Returns encoded LNURL if web url and parameter gieven."""
if not urlstr:
return jsonify({"status": "FALSE"}), 200
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]
else:
rand = randar[0]
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
@withdraw_ext.route("/api/v1/lnurlfetch/<urlstr>/<parstr>/<rand>", methods=["GET"])
def api_lnurlfetch(parstr, urlstr, rand):
"""Returns LNURL json."""
if not parstr:
return jsonify({"status": "FALSE", "ERROR": "NO WALL ID"}), 200
if not urlstr:
return jsonify({"status": "FALSE", "ERROR": "NO URL"}), 200
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)
@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:
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
@withdraw_ext.route("/api/v1/lnurlwithdraw/<rand>/", methods=["GET"])
def api_lnurlwithdraw(rand):
"""Pays invoice if passed k1 invoice and rand."""
k1 = request.args.get("k1")
pr = request.args.get("pr")
if not k1:
return jsonify({"status": "FALSE", "ERROR": "NO k1"}), 200
if not pr:
return jsonify({"status": "FALSE", "ERROR": "NO PR"}), 200
with open_ext_db("withdraw") as withdraw_ext_db:
user_fau = withdraw_ext_db.fetchall("SELECT * FROM withdraws WHERE withdrawals = ?", (k1,))
if not user_fau:
return jsonify({"status": "ERROR", "reason": "NO AUTH"}), 400
if user_fau[0][10] < 1:
return jsonify({"status": "ERROR", "reason": "withdraw SPENT"}), 400
# 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
link = create_withdraw_link(wallet_id=g.wallet.id, **g.data)
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/lnurl/<unique_hash>", methods=["GET"])
def api_lnurl_response(unique_hash):
link = get_withdraw_link_by_hash(unique_hash)
if not link:
return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), Status.OK
link = update_withdraw_link(link.id, k1=urlsafe_short_hash())
return jsonify(link.lnurl_response.dict()), Status.OK
@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())
if not link:
return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), Status.OK
if link.is_spent:
return jsonify({"status": "ERROR", "reason": "Withdraw is spent."}), Status.OK
if link.k1 != k1:
return jsonify({"status": "ERROR", "reason": "Bad request."}), Status.OK
if now < link.open_time:
return jsonify({"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."}), Status.OK
try:
pay_invoice(wallet=get_wallet(link.wallet), bolt11=payment_request, max_sat=link.max_withdrawable)
changes = {
"used": link.used + 1,
"open_time": link.wait_time + now,
}
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

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

@ -0,0 +1,6 @@
{% extends "base.html" %}
{% block beta %}{% endblock %}
{% block drawer_toggle %}{% endblock %}
{% block drawer %}{% endblock %}
Loading…
Cancel
Save