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