mirror of https://github.com/lukechilds/lnbits.git
15 changed files with 915 additions and 1657 deletions
@ -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,)) |
@ -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", |
||||
|
) |
@ -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);*/ |
||||
|
} |
||||
|
} |
||||
|
}); |
@ -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> |
@ -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 --> |
{% extends "public.html" %} |
||||
|
|
||||
<!DOCTYPE html> |
|
||||
<html> |
{% block page %} |
||||
<head> |
<div class="row q-col-gutter-md justify-center"> |
||||
<meta charset="UTF-8" /> |
<div class="col-12 col-sm-6 col-md-4"> |
||||
<title>LNBits Wallet</title> |
<q-card class="q-pa-lg"> |
||||
<meta |
<q-card-section class="q-pa-none"> |
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" |
<div class="text-center q-mb-md"> |
||||
name="viewport" |
{% if link.is_spent %} |
||||
/> |
<q-badge color="red" class="q-mb-md">Withdraw is spent.</q-badge> |
||||
<!-- Bootstrap 3.3.2 --> |
{% endif %} |
||||
<link |
<a href="lightning:{{ link.lnurl }}"> |
||||
rel="stylesheet" |
<q-responsive :ratio="1" class="q-mx-md"> |
||||
media="screen" |
<qrcode value="{{ link.lnurl }}" :options="{width: 800}" class="rounded-borders"></qrcode> |
||||
href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}" |
</q-responsive> |
||||
/> |
</a> |
||||
<!-- 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> |
|
||||
</div> |
</div> |
||||
</nav> |
<div class="row justify-between"> |
||||
</header> |
<q-btn flat color="grey" @click="copyText('{{ link.lnurl }}')">Copy LNURL</q-btn> |
||||
|
</div> |
||||
<aside class="main-sidebar"> |
</q-card-section> |
||||
<!-- sidebar: style can be found in sidebar.less --> |
</q-card> |
||||
<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> |
</div> |
||||
</body> |
<div class="col-12 col-sm-6 col-md-4 q-gutter-y-md"> |
||||
|
<q-card> |
||||
<script> |
<q-card-section> |
||||
function getAjax(url, thekey, success) { |
<h6 class="text-subtitle1 q-mb-sm q-mt-none">LNbits LNURL-withdraw link</h6> |
||||
var xhr = window.XMLHttpRequest |
<p class="q-my-none">Use a LNURL compatible bitcoin wallet to claim the sats.</p> |
||||
? new XMLHttpRequest() |
</q-card-section> |
||||
: new ActiveXObject('Microsoft.XMLHTTP') |
<q-card-section class="q-pa-none"> |
||||
xhr.open('GET', url, true) |
<q-separator></q-separator> |
||||
xhr.onreadystatechange = function() { |
<q-list> |
||||
if (xhr.readyState > 3 && xhr.status == 200) { |
{% include "withdraw/_lnurl.html" %} |
||||
success(xhr.responseText) |
</q-list> |
||||
} |
</q-card-section> |
||||
} |
</q-card> |
||||
xhr.setRequestHeader('Grpc-Metadata-macaroon', thekey) |
</div> |
||||
xhr.setRequestHeader('Content-Type', 'application/json') |
</div> |
||||
|
{% endblock %} |
||||
xhr.send() |
|
||||
return xhr |
{% 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); |
||||
function drawwithdraw(data) { |
|
||||
|
new Vue({ |
||||
|
el: '#vue', |
||||
console.log(data) |
mixins: [windowMixin] |
||||
|
}); |
||||
|
|
||||
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] }}") |
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
</script> |
</script> |
||||
</html> |
{% endblock %} |
||||
|
@ -1,507 +1,183 @@ |
|||||
<!-- @format --> |
{% extends "base.html" %} |
||||
|
|
||||
{% extends "legacy.html" %} {% block messages %} |
{% from "macros.jinja" import window_vars with context %} |
||||
<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> |
|
||||
|
|
||||
|
|
||||
|
{% 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-12 col-md-5 q-gutter-y-md"> |
||||
|
<q-card> |
||||
<div class="col-md-6"> |
<q-card-section> |
||||
<!-- general form elements --> |
<h6 class="text-subtitle1 q-my-none">LNbits LNURL-withdraw extension</h6> |
||||
<div class="box box-primary"> |
</q-card-section> |
||||
<div class="box-header"> |
<q-card-section class="q-pa-none"> |
||||
<h3 class="box-title">Select a link</h3> |
<q-separator></q-separator> |
||||
</div><!-- /.box-header --> |
<q-list> |
||||
<form role="form"> |
{% include "withdraw/_api_docs.html" %} |
||||
<div class="box-body"> |
<q-separator></q-separator> |
||||
<div class="form-group"> |
{% include "withdraw/_lnurl.html" %} |
||||
|
</q-list> |
||||
<select class="form-control" id="fauselect" onchange="drawwithdraw()"> |
</q-card-section> |
||||
<option></option> |
</q-card> |
||||
{% 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> |
||||
|
|
||||
<div class="row"> |
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog"> |
||||
<div class="col-md-6"> |
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> |
||||
<div class="box"> |
<q-form @submit="sendFormData" class="q-gutter-md"> |
||||
<div class="box-header"> |
<q-select filled dense emit-value v-model="formDialog.data.wallet" :options="g.user.walletOptions" label="Wallet *"> |
||||
<h3 class="box-title">Withdraw links <b id="withdraws"></b></h3> |
</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> |
</div> |
||||
<!-- /.box-header --> |
<q-list> |
||||
<div class="box-body no-padding"> |
<q-item tag="label" class="rounded-borders"> |
||||
<table id="pagnation" class="table table-bswearing anchorordered table-striped"> |
<q-item-section avatar> |
||||
<tr> |
<q-checkbox v-model="formDialog.data.is_unique" color="deep-purple"></q-checkbox> |
||||
<th>Title</th> |
</q-item-section> |
||||
<th style="width:15%">Link/ID</th> |
<q-item-section> |
||||
<th style="width:15%">Max Withdraw</th> |
<q-item-label>Use unique withdraw QR codes to reduce `assmilking`</q-item-label> |
||||
<th style="width:15%">No. uses</th> |
<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> |
||||
<th style="width:15%">Wait</th> |
</q-item-section> |
||||
<th style="width:15%">Wallet</th> |
</q-item> |
||||
<th style="width:10%">Edit</th> |
</q-list> |
||||
<th style="width:10%">Del</th> |
<q-btn v-if="formDialog.data.id" unelevated color="deep-purple" type="submit">Update withdraw link</q-btn> |
||||
</tr> |
<q-btn v-else unelevated |
||||
<tbody id="transactions"></tbody> |
color="deep-purple" |
||||
</table> |
:disable=" |
||||
</div> |
formDialog.data.wallet == null || |
||||
<!-- /.box-body --> |
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> |
</div> |
||||
<!-- /.box --> |
</q-card> |
||||
</div> |
</q-dialog> |
||||
</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> |
|
||||
{% endblock %} |
{% 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> |
|
@ -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 lnbits.decorators import check_user_exists, validate_uuids |
||||
from lnurl import encode as lnurl_encode |
from lnbits.helpers import Status |
||||
from datetime import datetime |
|
||||
|
|
||||
from lnbits.db import open_db, open_ext_db |
|
||||
from lnbits.extensions.withdraw import withdraw_ext |
from lnbits.extensions.withdraw import withdraw_ext |
||||
|
from .crud import get_withdraw_link |
||||
|
|
||||
|
|
||||
@withdraw_ext.route("/") |
@withdraw_ext.route("/") |
||||
|
@validate_uuids(["usr"], required=True) |
||||
|
@check_user_exists() |
||||
def index(): |
def index(): |
||||
"""Main withdraw link page.""" |
return render_template("withdraw/index.html", user=g.user) |
||||
|
|
||||
usr = request.args.get("usr") |
|
||||
|
|
||||
if usr: |
@withdraw_ext.route("/<link_id>") |
||||
if not len(usr) > 20: |
def display(link_id): |
||||
return redirect(url_for("home")) |
link = get_withdraw_link(link_id) or abort(Status.NOT_FOUND, "Withdraw link does not exist.") |
||||
|
|
||||
# Get all the data |
return render_template("withdraw/display.html", link=link) |
||||
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 |
@withdraw_ext.route("/print/<link_id>") |
||||
faudel = request.args.get("del") |
def print_qr(link_id): |
||||
if faudel: |
link = get_withdraw_link(link_id) or abort(Status.NOT_FOUND, "Withdraw link does not exist.") |
||||
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( |
return render_template("withdraw/print_qr.html", link=link) |
||||
"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],) |
|
||||
|
@ -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 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 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"]) |
@withdraw_ext.route("/api/v1/links", methods=["POST"]) |
||||
def api_lnurlencode(urlstr, parstr): |
@withdraw_ext.route("/api/v1/links/<link_id>", methods=["PUT"]) |
||||
"""Returns encoded LNURL if web url and parameter gieven.""" |
@api_check_wallet_macaroon(key_type="invoice") |
||||
|
@api_validate_post_request( |
||||
if not urlstr: |
schema={ |
||||
return jsonify({"status": "FALSE"}), 200 |
"title": {"type": "string", "empty": False, "required": True}, |
||||
|
"min_withdrawable": {"type": "integer", "min": 1, "required": True}, |
||||
with open_ext_db("withdraw") as withdraw_ext_db: |
"max_withdrawable": {"type": "integer", "min": 1, "required": True}, |
||||
user_fau = withdraw_ext_db.fetchall("SELECT * FROM withdraws WHERE uni = ?", (parstr,)) |
"uses": {"type": "integer", "min": 1, "required": True}, |
||||
randar = user_fau[0][15].split(",") |
"wait_time": {"type": "integer", "min": 1, "required": True}, |
||||
print(randar) |
"is_unique": {"type": "boolean", "required": True}, |
||||
# randar = randar[:-1] |
} |
||||
# If "Unique links" selected get correct rand, if not there is only one rand |
) |
||||
if user_fau[0][12] > 0: |
def api_link_create(link_id=None): |
||||
rand = randar[user_fau[0][10] - 1] |
if g.data["max_withdrawable"] < g.data["min_withdrawable"]: |
||||
else: |
return jsonify({"message": "`max_withdrawable` needs to be at least `min_withdrawable`."}), Status.BAD_REQUEST |
||||
rand = randar[0] |
|
||||
|
if (g.data["max_withdrawable"] * g.data["uses"] * 1000) > g.wallet.balance_msat: |
||||
url = url_for("withdraw.api_lnurlfetch", _external=True, urlstr=urlstr, parstr=parstr, rand=rand) |
return jsonify({"message": "Insufficient balance."}), Status.FORBIDDEN |
||||
|
|
||||
if "onion" in url: |
if link_id: |
||||
return jsonify({"status": "TRUE", "lnurl": lnurl_encode(url)}), 200 |
link = get_withdraw_link(link_id) |
||||
print(url) |
|
||||
|
if not link: |
||||
return jsonify({"status": "TRUE", "lnurl": lnurl_encode(url.replace("http://", "https://"))}), 200 |
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 |
||||
|
|
||||
@withdraw_ext.route("/api/v1/lnurlfetch/<urlstr>/<parstr>/<rand>", methods=["GET"]) |
link = update_withdraw_link(link_id, **g.data) |
||||
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) |
|
||||
else: |
else: |
||||
precallback = url_for("withdraw.api_lnurlwithdraw", _external=True, rand=rand).replace("http://", "https://") |
link = create_withdraw_link(wallet_id=g.wallet.id, **g.data) |
||||
|
|
||||
res = LnurlWithdrawResponse( |
return jsonify({**link._asdict(), **{"lnurl": link.lnurl}}), Status.OK if link_id else Status.CREATED |
||||
callback=precallback, |
|
||||
k1=k1str, |
|
||||
min_withdrawable=user_fau[0][8] * 1000, |
@withdraw_ext.route("/api/v1/links/<link_id>", methods=["DELETE"]) |
||||
max_withdrawable=user_fau[0][7] * 1000, |
@api_check_wallet_macaroon(key_type="invoice") |
||||
default_description="LNbits LNURL withdraw", |
def api_link_delete(link_id): |
||||
) |
link = get_withdraw_link(link_id) |
||||
|
|
||||
return res.json(), 200 |
if not link: |
||||
|
return jsonify({"message": "Withdraw link does not exist."}), Status.NOT_FOUND |
||||
|
|
||||
@withdraw_ext.route("/api/v1/lnurlwithdraw/<rand>/", methods=["GET"]) |
if link.wallet != g.wallet.id: |
||||
def api_lnurlwithdraw(rand): |
return jsonify({"message": "Not your withdraw link."}), Status.FORBIDDEN |
||||
"""Pays invoice if passed k1 invoice and rand.""" |
|
||||
|
delete_withdraw_link(link_id) |
||||
k1 = request.args.get("k1") |
|
||||
pr = request.args.get("pr") |
return "", Status.NO_CONTENT |
||||
|
|
||||
if not k1: |
|
||||
return jsonify({"status": "FALSE", "ERROR": "NO k1"}), 200 |
@withdraw_ext.route("/api/v1/lnurl/<unique_hash>", methods=["GET"]) |
||||
|
def api_lnurl_response(unique_hash): |
||||
if not pr: |
link = get_withdraw_link_by_hash(unique_hash) |
||||
return jsonify({"status": "FALSE", "ERROR": "NO PR"}), 200 |
|
||||
|
if not link: |
||||
with open_ext_db("withdraw") as withdraw_ext_db: |
return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), Status.OK |
||||
user_fau = withdraw_ext_db.fetchall("SELECT * FROM withdraws WHERE withdrawals = ?", (k1,)) |
|
||||
|
link = update_withdraw_link(link.id, k1=urlsafe_short_hash()) |
||||
if not user_fau: |
|
||||
return jsonify({"status": "ERROR", "reason": "NO AUTH"}), 400 |
return jsonify(link.lnurl_response.dict()), Status.OK |
||||
|
|
||||
if user_fau[0][10] < 1: |
|
||||
return jsonify({"status": "ERROR", "reason": "withdraw SPENT"}), 400 |
@withdraw_ext.route("/api/v1/lnurl/cb/<unique_hash>", methods=["GET"]) |
||||
|
def api_lnurl_callback(unique_hash): |
||||
# Check withdraw time |
link = get_withdraw_link_by_hash(unique_hash) |
||||
dt = datetime.now() |
k1 = request.args.get("k1", type=str) |
||||
seconds = dt.timestamp() |
payment_request = request.args.get("pr", type=str) |
||||
secspast = seconds - user_fau[0][14] |
now = int(datetime.now().timestamp()) |
||||
|
|
||||
if secspast < user_fau[0][11]: |
if not link: |
||||
return jsonify({"status": "ERROR", "reason": "WAIT " + str(int(user_fau[0][11] - secspast)) + "s"}), 400 |
return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), Status.OK |
||||
|
|
||||
randar = user_fau[0][15].split(",") |
if link.is_spent: |
||||
if rand not in randar: |
return jsonify({"status": "ERROR", "reason": "Withdraw is spent."}), Status.OK |
||||
print("huhhh") |
|
||||
return jsonify({"status": "ERROR", "reason": "BAD AUTH"}), 400 |
if link.k1 != k1: |
||||
if len(randar) > 2: |
return jsonify({"status": "ERROR", "reason": "Bad request."}), Status.OK |
||||
randar.remove(rand) |
|
||||
randstr = ",".join(randar) |
if now < link.open_time: |
||||
|
return jsonify({"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."}), Status.OK |
||||
# Update time and increments |
|
||||
upinc = int(user_fau[0][10]) - 1 |
try: |
||||
withdraw_ext_db.execute( |
pay_invoice(wallet=get_wallet(link.wallet), bolt11=payment_request, max_sat=link.max_withdrawable) |
||||
"UPDATE withdraws SET inc = ?, rand = ?, tmestmp = ? WHERE withdrawals = ?", (upinc, randstr, seconds, k1,) |
|
||||
) |
changes = { |
||||
|
"used": link.used + 1, |
||||
header = {"Content-Type": "application/json", "Grpc-Metadata-macaroon": str(user_fau[0][4])} |
"open_time": link.wait_time + now, |
||||
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)) |
if link.is_unique: |
||||
r = requests.post(url=url_for("api_transactions", _external=True), headers=header, data=json.dumps(data)) |
changes["unique_hash"] = urlsafe_short_hash() |
||||
r_json = r.json() |
|
||||
|
update_withdraw_link(link.id, **changes) |
||||
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 |
|
||||
|
|
||||
|
|
||||
|
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 |
||||
|
@ -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> |
@ -0,0 +1,6 @@ |
|||||
|
{% extends "base.html" %} |
||||
|
|
||||
|
|
||||
|
{% block beta %}{% endblock %} |
||||
|
{% block drawer_toggle %}{% endblock %} |
||||
|
{% block drawer %}{% endblock %} |
Loading…
Reference in new issue