@ -0,0 +1,180 @@ |
|||||
|
from uuid import uuid4 |
||||
|
|
||||
|
from lnbits.db import open_db |
||||
|
from lnbits.settings import DEFAULT_USER_WALLET_NAME, FEE_RESERVE |
||||
|
from typing import List, Optional |
||||
|
|
||||
|
from .models import User, Transaction, Wallet |
||||
|
|
||||
|
|
||||
|
# accounts |
||||
|
# -------- |
||||
|
|
||||
|
|
||||
|
def create_account() -> User: |
||||
|
with open_db() as db: |
||||
|
user_id = uuid4().hex |
||||
|
db.execute("INSERT INTO accounts (id) VALUES (?)", (user_id,)) |
||||
|
|
||||
|
return get_account(user_id=user_id) |
||||
|
|
||||
|
|
||||
|
def get_account(user_id: str) -> Optional[User]: |
||||
|
with open_db() as db: |
||||
|
row = db.fetchone("SELECT id, email, pass as password FROM accounts WHERE id = ?", (user_id,)) |
||||
|
|
||||
|
return User(**row) if row else None |
||||
|
|
||||
|
|
||||
|
def get_user(user_id: str) -> Optional[User]: |
||||
|
with open_db() as db: |
||||
|
user = db.fetchone("SELECT id, email FROM accounts WHERE id = ?", (user_id,)) |
||||
|
|
||||
|
if user: |
||||
|
extensions = db.fetchall("SELECT extension FROM extensions WHERE user = ? AND active = 1", (user_id,)) |
||||
|
wallets = db.fetchall( |
||||
|
""" |
||||
|
SELECT *, COALESCE((SELECT balance/1000 FROM balances WHERE wallet = wallets.id), 0) * ? AS balance |
||||
|
FROM wallets |
||||
|
WHERE user = ? |
||||
|
""", |
||||
|
(1 - FEE_RESERVE, user_id), |
||||
|
) |
||||
|
|
||||
|
return ( |
||||
|
User(**{**user, **{"extensions": [e[0] for e in extensions], "wallets": [Wallet(**w) for w in wallets]}}) |
||||
|
if user |
||||
|
else None |
||||
|
) |
||||
|
|
||||
|
|
||||
|
def update_user_extension(*, user_id: str, extension: str, active: int) -> None: |
||||
|
with open_db() as db: |
||||
|
db.execute( |
||||
|
""" |
||||
|
INSERT OR REPLACE INTO extensions (user, extension, active) |
||||
|
VALUES (?, ?, ?) |
||||
|
""", |
||||
|
(user_id, extension, active), |
||||
|
) |
||||
|
|
||||
|
|
||||
|
# wallets |
||||
|
# ------- |
||||
|
|
||||
|
|
||||
|
def create_wallet(*, user_id: str, wallet_name: Optional[str]) -> Wallet: |
||||
|
with open_db() as db: |
||||
|
wallet_id = uuid4().hex |
||||
|
db.execute( |
||||
|
""" |
||||
|
INSERT INTO wallets (id, name, user, adminkey, inkey) |
||||
|
VALUES (?, ?, ?, ?, ?) |
||||
|
""", |
||||
|
(wallet_id, wallet_name or DEFAULT_USER_WALLET_NAME, user_id, uuid4().hex, uuid4().hex), |
||||
|
) |
||||
|
|
||||
|
return get_wallet(wallet_id=wallet_id) |
||||
|
|
||||
|
|
||||
|
def delete_wallet(*, user_id: str, wallet_id: str) -> None: |
||||
|
with open_db() as db: |
||||
|
db.execute( |
||||
|
""" |
||||
|
UPDATE wallets AS w |
||||
|
SET |
||||
|
user = 'del:' || w.user, |
||||
|
adminkey = 'del:' || w.adminkey, |
||||
|
inkey = 'del:' || w.inkey |
||||
|
WHERE id = ? AND user = ? |
||||
|
""", |
||||
|
(wallet_id, user_id), |
||||
|
) |
||||
|
|
||||
|
|
||||
|
def get_wallet(wallet_id: str) -> Optional[Wallet]: |
||||
|
with open_db() as db: |
||||
|
row = db.fetchone( |
||||
|
""" |
||||
|
SELECT *, COALESCE((SELECT balance/1000 FROM balances WHERE wallet = wallets.id), 0) * ? AS balance |
||||
|
FROM wallets |
||||
|
WHERE id = ? |
||||
|
""", |
||||
|
(1 - FEE_RESERVE, wallet_id), |
||||
|
) |
||||
|
|
||||
|
return Wallet(**row) if row else None |
||||
|
|
||||
|
|
||||
|
def get_wallet_for_key(key: str, key_type: str = "invoice") -> Optional[Wallet]: |
||||
|
with open_db() as db: |
||||
|
check_field = "adminkey" if key_type == "admin" else "inkey" |
||||
|
row = db.fetchone( |
||||
|
f""" |
||||
|
SELECT *, COALESCE((SELECT balance/1000 FROM balances WHERE wallet = wallets.id), 0) * ? AS balance |
||||
|
FROM wallets |
||||
|
WHERE {check_field} = ? |
||||
|
""", |
||||
|
(1 - FEE_RESERVE, key), |
||||
|
) |
||||
|
|
||||
|
return Wallet(**row) if row else None |
||||
|
|
||||
|
|
||||
|
# wallet transactions |
||||
|
# ------------------- |
||||
|
|
||||
|
|
||||
|
def get_wallet_transaction(wallet_id: str, payhash: str) -> Optional[Transaction]: |
||||
|
with open_db() as db: |
||||
|
row = db.fetchone( |
||||
|
""" |
||||
|
SELECT payhash, amount, fee, pending, memo, time |
||||
|
FROM apipayments |
||||
|
WHERE wallet = ? AND payhash = ? |
||||
|
""", |
||||
|
(wallet_id, payhash), |
||||
|
) |
||||
|
|
||||
|
return Transaction(**row) if row else None |
||||
|
|
||||
|
|
||||
|
def get_wallet_transactions(wallet_id: str, *, pending: bool = False) -> List[Transaction]: |
||||
|
with open_db() as db: |
||||
|
rows = db.fetchall( |
||||
|
""" |
||||
|
SELECT payhash, amount, fee, pending, memo, time |
||||
|
FROM apipayments |
||||
|
WHERE wallet = ? AND pending = ? |
||||
|
ORDER BY time DESC |
||||
|
""", |
||||
|
(wallet_id, int(pending)), |
||||
|
) |
||||
|
|
||||
|
return [Transaction(**row) for row in rows] |
||||
|
|
||||
|
|
||||
|
# transactions |
||||
|
# ------------ |
||||
|
|
||||
|
|
||||
|
def create_transaction(*, wallet_id: str, payhash: str, amount: str, memo: str) -> Transaction: |
||||
|
with open_db() as db: |
||||
|
db.execute( |
||||
|
""" |
||||
|
INSERT INTO apipayments (wallet, payhash, amount, pending, memo) |
||||
|
VALUES (?, ?, ?, ?, ?) |
||||
|
""", |
||||
|
(wallet_id, payhash, amount, 1, memo), |
||||
|
) |
||||
|
|
||||
|
return get_wallet_transaction(wallet_id, payhash) |
||||
|
|
||||
|
|
||||
|
def update_transaction_status(payhash: str, pending: bool) -> None: |
||||
|
with open_db() as db: |
||||
|
db.execute("UPDATE apipayments SET pending = ? WHERE payhash = ?", (int(pending), payhash,)) |
||||
|
|
||||
|
|
||||
|
def check_pending_transactions(wallet_id: str) -> None: |
||||
|
pass |
@ -0,0 +1,62 @@ |
|||||
|
from decimal import Decimal |
||||
|
from typing import List, NamedTuple, Optional |
||||
|
|
||||
|
|
||||
|
class User(NamedTuple): |
||||
|
id: str |
||||
|
email: str |
||||
|
extensions: Optional[List[str]] = [] |
||||
|
wallets: Optional[List["Wallet"]] = [] |
||||
|
password: Optional[str] = None |
||||
|
|
||||
|
@property |
||||
|
def wallet_ids(self) -> List[str]: |
||||
|
return [wallet.id for wallet in self.wallets] |
||||
|
|
||||
|
def get_wallet(self, wallet_id: str) -> Optional["Wallet"]: |
||||
|
w = [wallet for wallet in self.wallets if wallet.id == wallet_id] |
||||
|
return w[0] if w else None |
||||
|
|
||||
|
|
||||
|
class Wallet(NamedTuple): |
||||
|
id: str |
||||
|
name: str |
||||
|
user: str |
||||
|
adminkey: str |
||||
|
inkey: str |
||||
|
balance: Decimal |
||||
|
|
||||
|
def get_transaction(self, payhash: str) -> "Transaction": |
||||
|
from .crud import get_wallet_transaction |
||||
|
|
||||
|
return get_wallet_transaction(self.id, payhash) |
||||
|
|
||||
|
def get_transactions(self) -> List["Transaction"]: |
||||
|
from .crud import get_wallet_transactions |
||||
|
|
||||
|
return get_wallet_transactions(self.id) |
||||
|
|
||||
|
|
||||
|
class Transaction(NamedTuple): |
||||
|
payhash: str |
||||
|
pending: bool |
||||
|
amount: int |
||||
|
fee: int |
||||
|
memo: str |
||||
|
time: int |
||||
|
|
||||
|
@property |
||||
|
def msat(self) -> int: |
||||
|
return self.amount |
||||
|
|
||||
|
@property |
||||
|
def sat(self) -> int: |
||||
|
return self.amount / 1000 |
||||
|
|
||||
|
@property |
||||
|
def tx_type(self) -> str: |
||||
|
return "payment" if self.amount < 0 else "invoice" |
||||
|
|
||||
|
def set_pending(self, pending: bool) -> None: |
||||
|
from .crud import update_transaction_status |
||||
|
update_transaction_status(self.payhash, pending) |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 18 KiB |
@ -0,0 +1,4 @@ |
|||||
|
new Vue({ |
||||
|
el: '#vue', |
||||
|
mixins: [windowMixin] |
||||
|
}); |
@ -0,0 +1,14 @@ |
|||||
|
new Vue({ |
||||
|
el: '#vue', |
||||
|
mixins: [windowMixin], |
||||
|
data: function () { |
||||
|
return { |
||||
|
walletName: '' |
||||
|
}; |
||||
|
}, |
||||
|
methods: { |
||||
|
createWallet: function () { |
||||
|
LNbits.href.createWallet(this.walletName); |
||||
|
} |
||||
|
} |
||||
|
}); |
@ -0,0 +1,137 @@ |
|||||
|
Vue.component(VueQrcode.name, VueQrcode); |
||||
|
|
||||
|
new Vue({ |
||||
|
el: '#vue', |
||||
|
mixins: [windowMixin], |
||||
|
data: function () { |
||||
|
return { |
||||
|
txUpdate: null, |
||||
|
receive: { |
||||
|
show: false, |
||||
|
status: 'pending', |
||||
|
paymentReq: null, |
||||
|
data: { |
||||
|
amount: null, |
||||
|
memo: '' |
||||
|
} |
||||
|
}, |
||||
|
send: { |
||||
|
show: false, |
||||
|
invoice: null, |
||||
|
data: { |
||||
|
bolt11: '' |
||||
|
} |
||||
|
}, |
||||
|
transactionsTable: { |
||||
|
columns: [ |
||||
|
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'}, |
||||
|
{name: 'date', align: 'left', label: 'Date', field: 'date', sortable: true}, |
||||
|
{name: 'sat', align: 'right', label: 'Amount (sat)', field: 'sat', sortable: true} |
||||
|
], |
||||
|
pagination: { |
||||
|
rowsPerPage: 10 |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
}, |
||||
|
computed: { |
||||
|
canPay: function () { |
||||
|
if (!this.send.invoice) return false; |
||||
|
return this.send.invoice.sat < this.w.wallet.balance; |
||||
|
}, |
||||
|
transactions: function () { |
||||
|
var data = (this.txUpdate) ? this.txUpdate : this.w.transactions; |
||||
|
return data.sort(function (a, b) { |
||||
|
return b.time - a.time; |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
openReceiveDialog: function () { |
||||
|
this.receive = { |
||||
|
show: true, |
||||
|
status: 'pending', |
||||
|
paymentReq: null, |
||||
|
data: { |
||||
|
amount: null, |
||||
|
memo: '' |
||||
|
} |
||||
|
}; |
||||
|
}, |
||||
|
openSendDialog: function () { |
||||
|
this.send = { |
||||
|
show: true, |
||||
|
invoice: null, |
||||
|
data: { |
||||
|
bolt11: '' |
||||
|
} |
||||
|
}; |
||||
|
}, |
||||
|
createInvoice: function () { |
||||
|
var self = this; |
||||
|
this.receive.status = 'loading'; |
||||
|
LNbits.api.createInvoice(this.w.wallet, this.receive.data.amount, this.receive.data.memo) |
||||
|
.then(function (response) { |
||||
|
self.receive.status = 'success'; |
||||
|
self.receive.paymentReq = response.data.payment_request; |
||||
|
|
||||
|
var check_invoice = setInterval(function () { |
||||
|
LNbits.api.getInvoice(self.w.wallet, response.data.payment_hash).then(function (response) { |
||||
|
if (response.data.paid) { |
||||
|
self.refreshTransactions(); |
||||
|
self.receive.show = false; |
||||
|
clearInterval(check_invoice); |
||||
|
} |
||||
|
}); |
||||
|
}, 3000); |
||||
|
|
||||
|
}).catch(function (error) { |
||||
|
LNbits.utils.notifyApiError(error); |
||||
|
self.receive.status = 'pending'; |
||||
|
}); |
||||
|
}, |
||||
|
decodeInvoice: function () { |
||||
|
try { |
||||
|
var invoice = decode(this.send.data.bolt11); |
||||
|
} catch (err) { |
||||
|
this.$q.notify({type: 'warning', message: err}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var cleanInvoice = { |
||||
|
msat: invoice.human_readable_part.amount, |
||||
|
sat: invoice.human_readable_part.amount / 1000, |
||||
|
fsat: LNbits.utils.formatSat(invoice.human_readable_part.amount / 1000) |
||||
|
}; |
||||
|
|
||||
|
_.each(invoice.data.tags, function (tag) { |
||||
|
if (_.isObject(tag) && _.has(tag, 'description')) { |
||||
|
if (tag.description == 'payment_hash') { cleanInvoice.hash = tag.value; } |
||||
|
else if (tag.description == 'description') { cleanInvoice.description = tag.value; } |
||||
|
else if (tag.description == 'expiry') { |
||||
|
var expireDate = new Date((invoice.data.time_stamp + tag.value) * 1000); |
||||
|
cleanInvoice.expireDate = Quasar.utils.date.formatDate(expireDate, 'YYYY-MM-DDTHH:mm:ss.SSSZ'); |
||||
|
cleanInvoice.expired = false; // TODO
|
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
this.send.invoice = Object.freeze(cleanInvoice); |
||||
|
}, |
||||
|
payInvoice: function () { |
||||
|
alert('pay!'); |
||||
|
}, |
||||
|
deleteWallet: function (walletId, user) { |
||||
|
LNbits.href.deleteWallet(walletId, user); |
||||
|
}, |
||||
|
refreshTransactions: function (notify) { |
||||
|
var self = this; |
||||
|
|
||||
|
LNbits.api.getTransactions(this.w.wallet).then(function (response) { |
||||
|
self.txUpdate = response.data.map(function (obj) { |
||||
|
return LNbits.map.transaction(obj); |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
}); |
@ -0,0 +1,42 @@ |
|||||
|
{% extends "base.html" %} |
||||
|
|
||||
|
{% from "macros.jinja" import window_vars with context %} |
||||
|
|
||||
|
|
||||
|
{% block scripts %} |
||||
|
{{ window_vars(user) }} |
||||
|
{% assets filters='rjsmin', output='__bundle__/core/extensions.js', |
||||
|
'core/js/extensions.js' %} |
||||
|
<script type="text/javascript" src="{{ ASSET_URL }}"></script> |
||||
|
{% endassets %} |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block page %} |
||||
|
<div class="row q-col-gutter-md"> |
||||
|
<div class="col-6 col-md-4 col-lg-3" v-for="extension in w.extensions" :key="extension.code"> |
||||
|
<q-card> |
||||
|
<q-card-section> |
||||
|
<q-icon :name="extension.icon" color="grey-5" style="font-size: 4rem;"></q-icon> |
||||
|
{% raw %} |
||||
|
<h5 class="q-mt-lg q-mb-xs">{{ extension.name }}</h5> |
||||
|
{{ extension.shortDescription }} |
||||
|
{% endraw %} |
||||
|
</q-card-section> |
||||
|
<q-separator></q-separator> |
||||
|
<q-card-actions> |
||||
|
<div v-if="extension.isEnabled"> |
||||
|
<q-btn flat color="deep-purple" |
||||
|
type="a" :href="[extension.url, '?usr=', w.user.id].join('')">Open</q-btn> |
||||
|
<q-btn flat color="grey-5" |
||||
|
type="a" |
||||
|
:href="['{{ url_for('core.extensions') }}', '?usr=', w.user.id, '&disable=', extension.code].join('')"> Disable</q-btn> |
||||
|
</div> |
||||
|
<q-btn v-else flat color="deep-purple" |
||||
|
type="a" |
||||
|
:href="['{{ url_for('core.extensions') }}', '?usr=', w.user.id, '&enable=', extension.code].join('')"> |
||||
|
Enable</q-btn> |
||||
|
</q-card-actions> |
||||
|
</q-card> |
||||
|
</div> |
||||
|
</div> |
||||
|
{% endblock %} |
@ -0,0 +1,87 @@ |
|||||
|
{% extends "base.html" %} |
||||
|
|
||||
|
|
||||
|
{% block scripts %} |
||||
|
{% assets filters='rjsmin', output='__bundle__/core/index.js', |
||||
|
'core/js/index.js' %} |
||||
|
<script type="text/javascript" src="{{ ASSET_URL }}"></script> |
||||
|
{% endassets %} |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block drawer %} |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block page %} |
||||
|
<div class="row q-col-gutter-md justify-between"> |
||||
|
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md"> |
||||
|
|
||||
|
<q-card> |
||||
|
<q-card-section> |
||||
|
<q-form class="q-gutter-md"> |
||||
|
<q-input filled dense |
||||
|
v-model="walletName" |
||||
|
label="Name your LNbits wallet *" |
||||
|
></q-input> |
||||
|
<q-btn unelevated |
||||
|
color="deep-purple" |
||||
|
:disable="walletName == ''" |
||||
|
@click="createWallet">Add a new wallet</q-btn> |
||||
|
</q-form> |
||||
|
</q-card-section> |
||||
|
</q-card> |
||||
|
|
||||
|
<q-card> |
||||
|
<q-card-section> |
||||
|
<h3 class="q-my-none"><strong>LN</strong>bits</h3> |
||||
|
<h5 class="q-my-md">Free and open-source lightning wallet</h5> |
||||
|
<p>LNbits is a simple, free and open-source lightning-network |
||||
|
wallet for bits and bobs. You can run it on your own server, |
||||
|
or use it at lnbits.com.</p> |
||||
|
<p>The wallet can be used in a variety of ways: an instant wallet for |
||||
|
LN demonstrations, a fallback wallet for the LNURL scheme, an |
||||
|
accounts system to mitigate the risk of exposing applications to |
||||
|
your full balance.</p> |
||||
|
<p>The wallet can run on top of LND, LNPay, @lntxbot or OpenNode.</p> |
||||
|
<p>Please note that although one of the aims of this wallet is to |
||||
|
mitigate exposure of all your funds, it’s still very BETA and may, |
||||
|
in fact, do the opposite!</p> |
||||
|
</q-card-section> |
||||
|
<q-card-actions align="right"> |
||||
|
<q-btn flat |
||||
|
color="deep-purple" |
||||
|
type="a" href="https://github.com/arcbtc/lnbits" target="_blank" rel="noopener">View project in GitHub</q-btn> |
||||
|
<q-btn flat |
||||
|
color="deep-purple" |
||||
|
type="a" href="https://paywall.link/to/f4e4e" target="_blank" rel="noopener">Donate</q-btn> |
||||
|
</q-card-actions> |
||||
|
</q-card> |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
<!-- Ads --> |
||||
|
<div class="col-12 col-md-3 col-lg-3 q-gutter-y-md"> |
||||
|
<q-btn flat color="deep-purple" label="Advertise here!" type="a" href="mailto:ben@arc.wales" class="full-width"></q-btn> |
||||
|
<div> |
||||
|
<a href="https://where39.com/"> |
||||
|
<q-img :ratio="16/9" src="{{ url_for('static', filename='images/where39.png') }}" class="rounded-borders"> |
||||
|
<div class="absolute-top text-center">Where39 anon locations</div> |
||||
|
</q-img> |
||||
|
</a> |
||||
|
</div> |
||||
|
<div> |
||||
|
<a href="https://github.com/arcbtc/Quickening"> |
||||
|
<q-img :ratio="16/9" src="{{ url_for('static', filename='images/quick.gif') }}" class="rounded-borders"> |
||||
|
<div class="absolute-top text-center">The Quickening <$8 PoS</div> |
||||
|
</q-img> |
||||
|
</a> |
||||
|
</div> |
||||
|
<div> |
||||
|
<a href="http://jigawatt.co/"> |
||||
|
<q-img :ratio="16/9" src="{{ url_for('static', filename='images/stamps.jpg') }}" class="rounded-borders"> |
||||
|
<div class="absolute-top text-center">Buy BTC stamps + electronics</div> |
||||
|
</q-img> |
||||
|
</a> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
{% endblock %} |
@ -0,0 +1,252 @@ |
|||||
|
{% extends "base.html" %} |
||||
|
|
||||
|
{% from "macros.jinja" import window_vars with context %} |
||||
|
|
||||
|
|
||||
|
{% block scripts %} |
||||
|
{{ window_vars(user, wallet) }} |
||||
|
{% assets filters='rjsmin', output='__bundle__/core/wallet.js', |
||||
|
'vendor/bolt11/utils.js', |
||||
|
'vendor/bolt11/decoder.js', |
||||
|
'vendor/vue-qrcode@1.0.2/vue-qrcode.min.js', |
||||
|
'core/js/wallet.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-8 col-lg-7 q-gutter-y-md"> |
||||
|
<q-card> |
||||
|
<q-card-section> |
||||
|
<h4 class="q-my-none"><strong>{% raw %}{{ w.wallet.fsat }}{% endraw %}</strong> sat</h4> |
||||
|
</q-card-section> |
||||
|
<div class="row q-pb-md q-px-md q-col-gutter-md"> |
||||
|
<div class="col"> |
||||
|
<q-btn unelevated |
||||
|
color="purple" |
||||
|
class="full-width" |
||||
|
@click="openSendDialog">Send</q-btn> |
||||
|
</div> |
||||
|
<div class="col"> |
||||
|
<q-btn unelevated |
||||
|
color="deep-purple" |
||||
|
class="full-width" |
||||
|
@click="openReceiveDialog">Receive</q-btn> |
||||
|
</div> |
||||
|
</div> |
||||
|
</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">Transactions</h5> |
||||
|
</div> |
||||
|
<div class="col-auto"> |
||||
|
<q-btn flat color="grey" onclick="exportbut()">Export to CSV</q-btn> |
||||
|
</div> |
||||
|
</div> |
||||
|
<q-table dense flat |
||||
|
:data="transactions" |
||||
|
row-key="payhash" |
||||
|
:columns="transactionsTable.columns" |
||||
|
:pagination.sync="transactionsTable.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-tr> |
||||
|
</template> |
||||
|
<template v-slot:body="props"> |
||||
|
<q-tr :props="props"> |
||||
|
<q-td auto-width class="lnbits__q-table__icon-td"> |
||||
|
<q-icon size="14px" |
||||
|
:name="(props.row.sat < 0) ? 'call_made' : 'call_received'" |
||||
|
:color="(props.row.sat < 0) ? 'purple-5' : 'green'"></q-icon> |
||||
|
</q-td> |
||||
|
<q-td key="memo" :props="props"> |
||||
|
{{ props.row.memo }} |
||||
|
</q-td> |
||||
|
<q-td auto-width key="date" :props="props"> |
||||
|
{{ props.row.date }} |
||||
|
</q-td> |
||||
|
<q-td auto-width key="sat" :props="props"> |
||||
|
{{ props.row.fsat }} |
||||
|
</q-td> |
||||
|
</q-tr> |
||||
|
</template> |
||||
|
{% endraw %} |
||||
|
</q-table> |
||||
|
</q-card-section> |
||||
|
</q-card> |
||||
|
|
||||
|
<q-card> |
||||
|
<q-card-section> |
||||
|
<div id="satschart"></div> |
||||
|
</q-card-section> |
||||
|
</q-card> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md"> |
||||
|
<q-card> |
||||
|
<q-card-section> |
||||
|
<strong>Wallet name: </strong><em>{{ wallet.name }}</em><br> |
||||
|
<strong>Wallet ID: </strong><em>{{ wallet.id }}</em><br> |
||||
|
<strong>Admin key: </strong><em>{{ wallet.adminkey }}</em><br> |
||||
|
<strong>Invoice/read key: </strong><em>{{ wallet.inkey }}</em> |
||||
|
</q-card-section> |
||||
|
<q-card-section class="q-pa-none"> |
||||
|
<q-separator></q-separator> |
||||
|
<q-list> |
||||
|
<q-expansion-item |
||||
|
group="extras" |
||||
|
icon="swap_vertical_circle" |
||||
|
label="API info" |
||||
|
:content-inset-level="0.5" |
||||
|
> |
||||
|
<q-expansion-item group="api" expand-separator label="Create an invoice"> |
||||
|
<q-card> |
||||
|
<q-card-section> |
||||
|
Generate an invoice:<br /><code>POST /api/v1/invoices</code |
||||
|
><br />Header |
||||
|
<code |
||||
|
>{"Grpc-Metadata-macaroon": "<i>{{ wallet.inkey }}</i |
||||
|
>"}</code |
||||
|
><br /> |
||||
|
Body <code>{"value": "200","memo": "beer"} </code><br /> |
||||
|
Returns |
||||
|
<code>{"pay_req": string,"pay_id": string} </code><br /> |
||||
|
*payment will not register in the wallet until the "check |
||||
|
invoice" endpoint is used<br /><br /> |
||||
|
|
||||
|
Check invoice:<br /> |
||||
|
Check an invoice:<br /><code |
||||
|
>GET /api/v1/invoice/*payment_hash*</code |
||||
|
><br />Header |
||||
|
<code |
||||
|
>{"Grpc-Metadata-macaroon": "<i>{{ wallet.inkey }}</i |
||||
|
>"}</code |
||||
|
><br /> |
||||
|
|
||||
|
Returns |
||||
|
<code>{"PAID": "TRUE"}/{"PAID": "FALSE"} </code><br /> |
||||
|
*if using LNTXBOT return will hang until paid<br /><br /> |
||||
|
</q-card-section> |
||||
|
</q-card> |
||||
|
</q-expansion-item> |
||||
|
<q-expansion-item group="api" expand-separator label="Get an invoice"> |
||||
|
<q-card> |
||||
|
<q-card-section> |
||||
|
This whole wallet will be deleted, the funds will be <strong>UNRECOVERABLE</strong>. |
||||
|
</q-card-section> |
||||
|
</q-card> |
||||
|
</q-expansion-item> |
||||
|
</q-expansion-item> |
||||
|
<q-separator></q-separator> |
||||
|
<q-expansion-item |
||||
|
group="extras" |
||||
|
icon="remove_circle" |
||||
|
label="Delete wallet"> |
||||
|
<q-card> |
||||
|
<q-card-section> |
||||
|
<p>This whole wallet will be deleted, the funds will be <strong>UNRECOVERABLE</strong>.</p> |
||||
|
<q-btn unelevated |
||||
|
color="red-10" |
||||
|
@click="deleteWallet('{{ wallet.id }}', '{{ user.id }}')">Delete wallet</q-btn> |
||||
|
</q-card-section> |
||||
|
</q-card> |
||||
|
</q-expansion-item> |
||||
|
</q-list> |
||||
|
</q-card-section> |
||||
|
</q-card> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<q-dialog v-model="receive.show" :position="($q.screen.gt.sm) ? 'standard' : 'top'"> |
||||
|
<q-card class="q-pa-md" style="width: 500px"> |
||||
|
<q-form v-if="!receive.paymentReq" class="q-gutter-md"> |
||||
|
<q-input filled dense |
||||
|
v-model.number="receive.data.amount" |
||||
|
type="number" |
||||
|
label="Amount *"></q-input> |
||||
|
<q-input filled dense |
||||
|
v-model="receive.data.memo" |
||||
|
label="Memo" |
||||
|
placeholder="LNbits invoice"></q-input> |
||||
|
<div v-if="receive.status == 'pending'" class="row justify-between"> |
||||
|
<q-btn unelevated |
||||
|
color="deep-purple" |
||||
|
:disable="receive.data.amount == null || receive.data.amount <= 0" |
||||
|
@click="createInvoice">Create invoice</q-btn> |
||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn> |
||||
|
</div> |
||||
|
<q-spinner v-if="receive.status == 'loading'" color="deep-purple" size="2.55em"></q-spinner> |
||||
|
</q-form> |
||||
|
<div v-else> |
||||
|
<div class="text-center q-mb-md"> |
||||
|
<a :href="'lightning:' + receive.paymentReq"> |
||||
|
<qrcode :value="receive.paymentReq" :options="{width: 340}"></qrcode> |
||||
|
</a> |
||||
|
</div> |
||||
|
<!--<q-separator class="q-my-md"></q-separator> |
||||
|
<p class="text-caption" style="word-break: break-all"> |
||||
|
{% raw %}{{ receive.paymentReq }}{% endraw %} |
||||
|
</p>--> |
||||
|
<div class="row justify-between"> |
||||
|
<q-btn flat color="grey" @click="copyText(receive.paymentReq)">Copy invoice</q-btn> |
||||
|
<q-btn v-close-popup flat color="grey">Close</q-btn> |
||||
|
</div> |
||||
|
</div> |
||||
|
</q-card> |
||||
|
</q-dialog> |
||||
|
|
||||
|
<q-dialog v-model="send.show" :position="($q.screen.gt.sm) ? 'standard' : 'top'"> |
||||
|
<q-card class="q-pa-md" style="width: 500px"> |
||||
|
<q-form v-if="!send.invoice" class="q-gutter-md"> |
||||
|
<q-input filled dense |
||||
|
v-model="send.data.bolt11" |
||||
|
type="textarea" |
||||
|
label="Paste an invoice *" |
||||
|
> |
||||
|
<template v-slot:after> |
||||
|
<q-btn round dense flat icon="photo_camera"> |
||||
|
<q-tooltip>Use camera to scan an invoice</q-tooltip> |
||||
|
</q-btn> |
||||
|
</template> |
||||
|
</q-input> |
||||
|
<div class="row justify-between"> |
||||
|
<q-btn unelevated |
||||
|
color="deep-purple" |
||||
|
:disable="send.data.bolt11 == ''" |
||||
|
@click="decodeInvoice">Read invoice</q-btn> |
||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn> |
||||
|
</div> |
||||
|
</q-form> |
||||
|
<div v-else> |
||||
|
{% raw %} |
||||
|
<h6 class="q-my-none">{{ send.invoice.fsat }} sat</h6> |
||||
|
<p style="word-break: break-all"> |
||||
|
<strong>Memo:</strong> {{ send.invoice.description }}<br> |
||||
|
<strong>Expire date:</strong> {{ send.invoice.expireDate }}<br> |
||||
|
<strong>Hash:</strong> {{ send.invoice.hash }} |
||||
|
</p> |
||||
|
{% endraw %} |
||||
|
<div v-if="canPay" class="row justify-between"> |
||||
|
<q-btn unelevated |
||||
|
color="deep-purple" |
||||
|
@click="payInvoice">Send satoshis</q-btn> |
||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn> |
||||
|
</div> |
||||
|
<div v-else class="row justify-between"> |
||||
|
<q-btn unelevated disabled color="yellow" text-color="black">Not enough funds!</q-btn> |
||||
|
<q-btn v-close-popup flat color="grey">Cancel</q-btn> |
||||
|
</div> |
||||
|
</div> |
||||
|
</q-card> |
||||
|
</q-dialog> |
||||
|
{% endblock %} |
@ -1,98 +0,0 @@ |
|||||
<!-- @format --> |
|
||||
|
|
||||
{% extends "base.html" %} {% block menuitems %} |
|
||||
|
|
||||
<li><a href="https://where39.com/"><p>Where39 anon locations</p><img src="static/where39.png" style="width:170px"></a></li> |
|
||||
<li><a href="https://github.com/arcbtc/Quickening"><p>The Quickening <$8 PoS</p><img src="static/quick.gif" style="width:170px"></a></li> |
|
||||
<li><a href="http://jigawatt.co/"><p>Buy BTC stamps + electronics</p><img src="static/stamps.jpg" style="width:170px"></a></li> |
|
||||
<li><a href="mailto:ben@arc.wales"><h3>Advertise here!</h3></a></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"> |
|
||||
<ol class="breadcrumb"> |
|
||||
<li> |
|
||||
<a href="{{ url_for('core.home') }}"><i class="fa fa-dashboard"></i> Home</a> |
|
||||
</li> |
|
||||
</ol> |
|
||||
<br /><br /> |
|
||||
<div class="row"> |
|
||||
<div class="col-md-6"> |
|
||||
<div class="alert alert-danger alert-dismissable"> |
|
||||
<h4> |
|
||||
TESTING ONLY - wallet is still in BETA and very unstable |
|
||||
</h4> |
|
||||
</div></div></div> |
|
||||
</section> |
|
||||
|
|
||||
<!-- Main content --> |
|
||||
<section class="content"> |
|
||||
<div class="row"> |
|
||||
<div class="col-md-3"> |
|
||||
<!-- Default box --> |
|
||||
<div class="box"> |
|
||||
<div class="box-header"> |
|
||||
{% block call_to_action %} |
|
||||
<h1> |
|
||||
<small>Make a wallet</small> |
|
||||
</h1> |
|
||||
<div class="form-group"> |
|
||||
<input |
|
||||
type="text" |
|
||||
class="form-control" |
|
||||
id="walname" |
|
||||
placeholder="Name your LNBits wallet" |
|
||||
required |
|
||||
/> |
|
||||
</div> |
|
||||
<button type="button" class="btn btn-primary" onclick="newwallet()"> |
|
||||
Submit |
|
||||
</button> |
|
||||
{% endblock %} |
|
||||
</div> |
|
||||
<!-- /.box-body --> |
|
||||
</div> |
|
||||
<!-- /.box --> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div class="row"> |
|
||||
<div class="col-md-6"> |
|
||||
<!-- Default box --> |
|
||||
<div class="box"> |
|
||||
<div class="box-header"> |
|
||||
<h1> |
|
||||
<a href="index.html" class="logo"><b>LN</b>bits</a> |
|
||||
<small>free and open-source lightning wallet</small> |
|
||||
</h1> |
|
||||
<p> |
|
||||
LNbits is a simple, free and open-source lightning-network wallet |
|
||||
for bits and bobs. You can run it on your own server, or use this |
|
||||
one. |
|
||||
<br /><br /> |
|
||||
The wallet can be used in a variety of ways, an instant wallet for |
|
||||
LN demonstrations, a fallback wallet for the LNURL scheme, an |
|
||||
accounts system to mitigate the risk of exposing applications to |
|
||||
your full balance. |
|
||||
<br /><br /> |
|
||||
The wallet can run on top of LND, lntxbot, paywall, opennode |
|
||||
<br /><br /> |
|
||||
Please note that although one of the aims of this wallet is to |
|
||||
mitigate exposure of all your funds, it’s still very BETA and may |
|
||||
in fact do the opposite! |
|
||||
<br /> |
|
||||
<a href="https://github.com/arcbtc/lnbits" |
|
||||
>https://github.com/arcbtc/lnbits</a |
|
||||
> |
|
||||
</p> |
|
||||
</div> |
|
||||
<!-- /.box-body --> |
|
||||
</div> |
|
||||
<!-- /.box --> |
|
||||
</div> |
|
||||
</div> |
|
||||
</section> |
|
||||
<!-- /.content --> |
|
||||
</div> |
|
||||
<!-- /.content-wrapper --> |
|
||||
{% endblock %} |
|
@ -0,0 +1,62 @@ |
|||||
|
from flask import g, jsonify |
||||
|
|
||||
|
from lnbits.core import core_app |
||||
|
from lnbits.decorators import api_check_wallet_macaroon, api_validate_post_request |
||||
|
from lnbits.helpers import Status |
||||
|
from lnbits.settings import WALLET |
||||
|
|
||||
|
from .crud import create_transaction |
||||
|
|
||||
|
|
||||
|
@core_app.route("/api/v1/invoices", methods=["POST"]) |
||||
|
@api_validate_post_request(required_params=["amount", "memo"]) |
||||
|
@api_check_wallet_macaroon(key_type="invoice") |
||||
|
def api_invoices(): |
||||
|
if not isinstance(g.data["amount"], int) or g.data["amount"] < 1: |
||||
|
return jsonify({"message": "`amount` needs to be a positive integer."}), Status.BAD_REQUEST |
||||
|
|
||||
|
if not isinstance(g.data["memo"], str) or not g.data["memo"].strip(): |
||||
|
return jsonify({"message": "`memo` needs to be a valid string."}), Status.BAD_REQUEST |
||||
|
|
||||
|
try: |
||||
|
r, payhash, payment_request = WALLET.create_invoice(g.data["amount"], g.data["memo"]) |
||||
|
server_error = not r.ok or "message" in r.json() |
||||
|
except Exception: |
||||
|
server_error = True |
||||
|
|
||||
|
if server_error: |
||||
|
return jsonify({"message": "Unexpected backend error. Try again later."}), 500 |
||||
|
|
||||
|
amount_msat = g.data["amount"] * 1000 |
||||
|
create_transaction(wallet_id=g.wallet.id, payhash=payhash, amount=amount_msat, memo=g.data["memo"]) |
||||
|
|
||||
|
return jsonify({"payment_request": payment_request, "payment_hash": payhash}), Status.CREATED |
||||
|
|
||||
|
|
||||
|
@core_app.route("/api/v1/invoices/<payhash>", defaults={"incoming": True}, methods=["GET"]) |
||||
|
@core_app.route("/api/v1/payments/<payhash>", defaults={"incoming": False}, methods=["GET"]) |
||||
|
@api_check_wallet_macaroon(key_type="invoice") |
||||
|
def api_transaction(payhash, incoming): |
||||
|
tx = g.wallet.get_transaction(payhash) |
||||
|
|
||||
|
if not tx: |
||||
|
return jsonify({"message": "Transaction does not exist."}), Status.NOT_FOUND |
||||
|
elif not tx.pending: |
||||
|
return jsonify({"paid": True}), Status.OK |
||||
|
|
||||
|
try: |
||||
|
is_settled = WALLET.get_invoice_status(payhash).settled |
||||
|
except Exception: |
||||
|
return jsonify({"paid": False}), Status.OK |
||||
|
|
||||
|
if is_settled is True: |
||||
|
tx.set_pending(False) |
||||
|
return jsonify({"paid": True}), Status.OK |
||||
|
|
||||
|
return jsonify({"paid": False}), Status.OK |
||||
|
|
||||
|
|
||||
|
@core_app.route("/api/v1/transactions", methods=["GET"]) |
||||
|
@api_check_wallet_macaroon(key_type="invoice") |
||||
|
def api_transactions(): |
||||
|
return jsonify(g.wallet.get_transactions()), Status.OK |
@ -0,0 +1,2 @@ |
|||||
|
* |
||||
|
!.gitignore |
@ -0,0 +1,81 @@ |
|||||
|
from flask import g, abort, jsonify, request |
||||
|
from functools import wraps |
||||
|
from typing import List, Union |
||||
|
from uuid import UUID |
||||
|
|
||||
|
from lnbits.core.crud import get_user, get_wallet_for_key |
||||
|
from .helpers import Status |
||||
|
|
||||
|
|
||||
|
def api_check_wallet_macaroon(*, key_type: str = "invoice"): |
||||
|
def wrap(view): |
||||
|
@wraps(view) |
||||
|
def wrapped_view(**kwargs): |
||||
|
try: |
||||
|
g.wallet = get_wallet_for_key(request.headers["Grpc-Metadata-macaroon"], key_type) |
||||
|
except KeyError: |
||||
|
return jsonify({"message": "`Grpc-Metadata-macaroon` header missing."}), Status.BAD_REQUEST |
||||
|
|
||||
|
if not g.wallet: |
||||
|
return jsonify({"message": "Wrong keys."}), Status.UNAUTHORIZED |
||||
|
|
||||
|
return view(**kwargs) |
||||
|
|
||||
|
return wrapped_view |
||||
|
|
||||
|
return wrap |
||||
|
|
||||
|
|
||||
|
def api_validate_post_request(*, required_params: List[str] = []): |
||||
|
def wrap(view): |
||||
|
@wraps(view) |
||||
|
def wrapped_view(**kwargs): |
||||
|
if "application/json" not in request.headers["Content-Type"]: |
||||
|
return jsonify({"message": "Content-Type must be `application/json`."}), Status.BAD_REQUEST |
||||
|
|
||||
|
g.data = request.json |
||||
|
|
||||
|
for param in required_params: |
||||
|
if param not in g.data: |
||||
|
return jsonify({"message": f"`{param}` is required."}), Status.BAD_REQUEST |
||||
|
|
||||
|
return view(**kwargs) |
||||
|
|
||||
|
return wrapped_view |
||||
|
|
||||
|
return wrap |
||||
|
|
||||
|
|
||||
|
def check_user_exists(param: str = "usr"): |
||||
|
def wrap(view): |
||||
|
@wraps(view) |
||||
|
def wrapped_view(**kwargs): |
||||
|
g.user = get_user(request.args.get(param, type=str)) or abort(Status.NOT_FOUND, "User not found.") |
||||
|
return view(**kwargs) |
||||
|
|
||||
|
return wrapped_view |
||||
|
|
||||
|
return wrap |
||||
|
|
||||
|
|
||||
|
def validate_uuids(params: List[str], *, required: Union[bool, List[str]] = False, version: int = 4): |
||||
|
def wrap(view): |
||||
|
@wraps(view) |
||||
|
def wrapped_view(**kwargs): |
||||
|
query_params = {param: request.args.get(param, type=str) for param in params} |
||||
|
|
||||
|
for param, value in query_params.items(): |
||||
|
if not value and (required is True or (required and param in required)): |
||||
|
abort(Status.BAD_REQUEST, f"`{param}` is required.") |
||||
|
|
||||
|
if value: |
||||
|
try: |
||||
|
UUID(value, version=version) |
||||
|
except ValueError: |
||||
|
abort(Status.BAD_REQUEST, f"`{param}` is not a valid UUID.") |
||||
|
|
||||
|
return view(**kwargs) |
||||
|
|
||||
|
return wrapped_view |
||||
|
|
||||
|
return wrap |
@ -1,5 +1,6 @@ |
|||||
{ |
{ |
||||
"name": "LNEVENTS", |
"name": "Events", |
||||
"short_description": "LN tickets for events", |
"short_description": "LN tickets for events.", |
||||
"ion_icon": "calendar" |
"icon": "local_activity", |
||||
|
"contributors": ["arcbtc"] |
||||
} |
} |
||||
|
@ -1,5 +1,6 @@ |
|||||
{ |
{ |
||||
"name": "SHORT-NAME-FOR-EXTENSIONS-PAGE", |
"name": "SHORT-NAME-FOR-EXTENSIONS-PAGE", |
||||
"short_description": "BLah blah blah.", |
"short_description": "BLah blah blah.", |
||||
"ion_icon": "calendar" |
"icon": "calendar", |
||||
|
"contributors": ["github_username"] |
||||
} |
} |
||||
|
@ -1,5 +1,6 @@ |
|||||
{ |
{ |
||||
"name": "TPOS", |
"name": "TPOS", |
||||
"short_description": "A shareable POS!", |
"short_description": "A shareable POS!", |
||||
"ion_icon": "calculator" |
"icon": "dialpad", |
||||
|
"contributors": ["talvasconcelos", "arcbtc"] |
||||
} |
} |
||||
|
@ -1,5 +1,6 @@ |
|||||
{ |
{ |
||||
"name": "LNURLw", |
"name": "LNURLw", |
||||
"short_description": "Make LNURL withdraw links", |
"short_description": "Make LNURL withdraw links.", |
||||
"ion_icon": "beer" |
"icon": "crop_free", |
||||
|
"contributors": ["arcbtc"] |
||||
} |
} |
||||
|
@ -0,0 +1,33 @@ |
|||||
|
[v-cloak] { |
||||
|
display: none; } |
||||
|
|
||||
|
.bg-lnbits-dark { |
||||
|
background-color: #1f2234; } |
||||
|
|
||||
|
body.body--dark, body.body--dark .q-drawer--dark, body.body--dark .q-menu--dark { |
||||
|
background: #1f2234; } |
||||
|
|
||||
|
body.body--dark .q-card--dark { |
||||
|
background: #333646; } |
||||
|
|
||||
|
body.body--dark .q-table--dark { |
||||
|
background: transparent; } |
||||
|
|
||||
|
body.body--light, body.body--light .q-drawer { |
||||
|
background: whitesmoke; } |
||||
|
|
||||
|
body.body--dark .q-field--error .text-negative, |
||||
|
body.body--dark .q-field--error .q-field__messages { |
||||
|
color: yellow !important; } |
||||
|
|
||||
|
.lnbits__q-table__icon-td { |
||||
|
padding-left: 5px !important; } |
||||
|
|
||||
|
.lnbits-drawer__q-list .q-item { |
||||
|
padding-top: 5px !important; |
||||
|
padding-bottom: 5px !important; |
||||
|
border-top-right-radius: 3px; |
||||
|
border-bottom-right-radius: 3px; } |
||||
|
.lnbits-drawer__q-list .q-item.q-item--active { |
||||
|
color: inherit; |
||||
|
font-weight: bold; } |
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 230 KiB |
@ -0,0 +1,146 @@ |
|||||
|
var LOCALE = 'en' |
||||
|
|
||||
|
var LNbits = { |
||||
|
api: { |
||||
|
request: function (method, url, macaroon, data) { |
||||
|
return axios({ |
||||
|
method: method, |
||||
|
url: url, |
||||
|
headers: { |
||||
|
'Grpc-Metadata-macaroon': macaroon |
||||
|
}, |
||||
|
data: data |
||||
|
}); |
||||
|
}, |
||||
|
createInvoice: function (wallet, amount, memo) { |
||||
|
return this.request('post', '/api/v1/invoices', wallet.inkey, { |
||||
|
amount: amount, |
||||
|
memo: memo |
||||
|
}); |
||||
|
}, |
||||
|
getInvoice: function (wallet, payhash) { |
||||
|
return this.request('get', '/api/v1/invoices/' + payhash, wallet.inkey); |
||||
|
}, |
||||
|
getTransactions: function (wallet) { |
||||
|
return this.request('get', '/api/v1/transactions', wallet.inkey); |
||||
|
} |
||||
|
}, |
||||
|
href: { |
||||
|
openWallet: function (wallet) { |
||||
|
window.location.href = '/wallet?usr=' + wallet.user + '&wal=' + wallet.id; |
||||
|
}, |
||||
|
createWallet: function (walletName, userId) { |
||||
|
window.location.href = '/wallet?' + (userId ? 'usr=' + userId + '&' : '') + 'nme=' + walletName; |
||||
|
}, |
||||
|
deleteWallet: function (walletId, userId) { |
||||
|
window.location.href = '/deletewallet?usr=' + userId + '&wal=' + walletId; |
||||
|
} |
||||
|
}, |
||||
|
map: { |
||||
|
extension: function (data) { |
||||
|
var obj = _.object(['code', 'isValid', 'name', 'shortDescription', 'icon'], data); |
||||
|
obj.url = ['/', obj.code, '/'].join(''); |
||||
|
return obj; |
||||
|
}, |
||||
|
transaction: function (data) { |
||||
|
var obj = _.object(['payhash', 'pending', 'amount', 'fee', 'memo', 'time'], data); |
||||
|
obj.date = Quasar.utils.date.formatDate(new Date(obj.time * 1000), 'YYYY-MM-DD HH:mm') |
||||
|
obj.msat = obj.amount; |
||||
|
obj.sat = obj.msat / 1000; |
||||
|
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat); |
||||
|
return obj; |
||||
|
}, |
||||
|
user: function (data) { |
||||
|
var obj = _.object(['id', 'email', 'extensions', 'wallets'], data); |
||||
|
var mapWallet = this.wallet; |
||||
|
obj.wallets = obj.wallets.map(function (obj) { |
||||
|
return mapWallet(obj); |
||||
|
}).sort(function (a, b) { |
||||
|
return a.name > b.name; |
||||
|
}); |
||||
|
return obj; |
||||
|
}, |
||||
|
wallet: function (data) { |
||||
|
var obj = _.object(['id', 'name', 'user', 'adminkey', 'inkey', 'balance'], data); |
||||
|
obj.sat = Math.round(obj.balance); |
||||
|
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat); |
||||
|
obj.url = ['/wallet?usr=', obj.user, '&wal=', obj.id].join(''); |
||||
|
return obj; |
||||
|
} |
||||
|
}, |
||||
|
utils: { |
||||
|
formatSat: function (value) { |
||||
|
return new Intl.NumberFormat(LOCALE).format(value); |
||||
|
}, |
||||
|
notifyApiError: function (error) { |
||||
|
var types = { |
||||
|
400: 'warning', |
||||
|
401: 'warning', |
||||
|
500: 'negative' |
||||
|
} |
||||
|
Quasar.plugins.Notify.create({ |
||||
|
progress: true, |
||||
|
timeout: 3000, |
||||
|
type: types[error.response.status] || 'warning', |
||||
|
message: error.response.data.message || null, |
||||
|
caption: [error.response.status, ' ', error.response.statusText].join('') || null, |
||||
|
icon: null |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
var windowMixin = { |
||||
|
data: function () { |
||||
|
return { |
||||
|
w: { |
||||
|
visibleDrawer: false, |
||||
|
extensions: [], |
||||
|
user: null, |
||||
|
wallet: null, |
||||
|
transactions: [], |
||||
|
} |
||||
|
}; |
||||
|
}, |
||||
|
methods: { |
||||
|
toggleDarkMode: function () { |
||||
|
this.$q.dark.toggle(); |
||||
|
this.$q.localStorage.set('lnbits.darkMode', this.$q.dark.isActive); |
||||
|
}, |
||||
|
copyText: function (text, message) { |
||||
|
var notify = this.$q.notify; |
||||
|
Quasar.utils.copyToClipboard(text).then(function () { |
||||
|
notify({message: 'Copied to clipboard!'}); |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
created: function () { |
||||
|
this.$q.dark.set(this.$q.localStorage.getItem('lnbits.darkMode')); |
||||
|
if (window.user) { |
||||
|
this.w.user = Object.freeze(LNbits.map.user(window.user)); |
||||
|
} |
||||
|
if (window.wallet) { |
||||
|
this.w.wallet = Object.freeze(LNbits.map.wallet(window.wallet)); |
||||
|
} |
||||
|
if (window.transactions) { |
||||
|
this.w.transactions = window.transactions.map(function (data) { |
||||
|
return LNbits.map.transaction(data); |
||||
|
}); |
||||
|
} |
||||
|
if (window.extensions) { |
||||
|
var user = this.w.user; |
||||
|
this.w.extensions = Object.freeze(window.extensions.map(function (data) { |
||||
|
return LNbits.map.extension(data); |
||||
|
}).map(function (obj) { |
||||
|
if (user) { |
||||
|
obj.isEnabled = user.extensions.indexOf(obj.code) != -1; |
||||
|
} else { |
||||
|
obj.isEnabled = false; |
||||
|
} |
||||
|
return obj; |
||||
|
}).sort(function (a, b) { |
||||
|
return a.name > b.name; |
||||
|
})); |
||||
|
} |
||||
|
} |
||||
|
}; |
@ -0,0 +1,122 @@ |
|||||
|
Vue.component('lnbits-wallet-list', { |
||||
|
data: function () { |
||||
|
return { |
||||
|
user: null, |
||||
|
activeWallet: null, |
||||
|
showForm: false, |
||||
|
walletName: '' |
||||
|
} |
||||
|
}, |
||||
|
template: ` |
||||
|
<q-list v-if="user && user.wallets.length" dense class="lnbits-drawer__q-list"> |
||||
|
<q-item-label header>Wallets</q-item-label> |
||||
|
<q-item v-for="wallet in user.wallets" :key="wallet.id" |
||||
|
clickable |
||||
|
:active="activeWallet && activeWallet.id == wallet.id" |
||||
|
tag="a" :href="wallet.url"> |
||||
|
<q-item-section side> |
||||
|
<q-avatar size="md" |
||||
|
:color="(activeWallet && activeWallet.id == wallet.id) |
||||
|
? (($q.dark.isActive) ? 'deep-purple-5' : 'deep-purple') |
||||
|
: 'grey-5'"> |
||||
|
<q-icon name="flash_on" :size="($q.dark.isActive) ? '21px' : '20px'" |
||||
|
:color="($q.dark.isActive) ? 'blue-grey-10' : 'grey-3'"></q-icon> |
||||
|
</q-avatar> |
||||
|
</q-item-section> |
||||
|
<q-item-section> |
||||
|
<q-item-label lines="1">{{ wallet.name }}</q-item-label> |
||||
|
<q-item-label caption>{{ wallet.fsat }} sat</q-item-label> |
||||
|
</q-item-section> |
||||
|
<q-item-section side v-show="activeWallet && activeWallet.id == wallet.id"> |
||||
|
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon> |
||||
|
</q-item-section> |
||||
|
</q-item> |
||||
|
<q-item clickable @click="showForm = !showForm"> |
||||
|
<q-item-section side> |
||||
|
<q-icon :name="(showForm) ? 'remove' : 'add'" color="grey-5" size="md"></q-icon> |
||||
|
</q-item-section> |
||||
|
<q-item-section> |
||||
|
<q-item-label lines="1" class="text-caption">Add a wallet</q-item-label> |
||||
|
</q-item-section> |
||||
|
</q-item> |
||||
|
<q-item v-if="showForm"> |
||||
|
<q-item-section> |
||||
|
<q-form> |
||||
|
<q-input filled dense v-model="walletName" label="Name wallet *"> |
||||
|
<template v-slot:append> |
||||
|
<q-btn round dense flat icon="send" size="sm" @click="createWallet" :disable="walletName == ''"></q-btn> |
||||
|
</template> |
||||
|
</q-input> |
||||
|
</q-form> |
||||
|
</q-item-section> |
||||
|
</q-item> |
||||
|
</q-list> |
||||
|
`,
|
||||
|
methods: { |
||||
|
createWallet: function () { |
||||
|
LNbits.href.createWallet(this.walletName, this.user.id); |
||||
|
} |
||||
|
}, |
||||
|
created: function () { |
||||
|
if (window.user) { |
||||
|
this.user = LNbits.map.user(window.user); |
||||
|
} |
||||
|
if (window.wallet) { |
||||
|
this.activeWallet = LNbits.map.wallet(window.wallet); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
Vue.component('lnbits-extension-list', { |
||||
|
data: function () { |
||||
|
return { |
||||
|
extensions: [], |
||||
|
user: null |
||||
|
} |
||||
|
}, |
||||
|
template: ` |
||||
|
<q-list v-if="user" dense class="lnbits-drawer__q-list"> |
||||
|
<q-item-label header>Extensions</q-item-label> |
||||
|
<q-item v-for="extension in userExtensions" :key="extension.code" |
||||
|
clickable |
||||
|
tag="a" :href="[extension.url, '?usr=', user.id].join('')"> |
||||
|
<q-item-section side> |
||||
|
<q-avatar size="md" color="grey-5"> |
||||
|
<q-icon :name="extension.icon" :size="($q.dark.isActive) ? '21px' : '20px'" |
||||
|
:color="($q.dark.isActive) ? 'blue-grey-10' : 'grey-3'"></q-icon> |
||||
|
</q-avatar> |
||||
|
</q-item-section> |
||||
|
<q-item-section> |
||||
|
<q-item-label lines="1">{{ extension.name }}</q-item-label> |
||||
|
</q-item-section> |
||||
|
</q-item> |
||||
|
<q-item clickable tag="a" :href="['/extensions?usr=', user.id].join('')"> |
||||
|
<q-item-section side> |
||||
|
<q-icon name="clear_all" color="grey-5" size="md"></q-icon> |
||||
|
</q-item-section> |
||||
|
<q-item-section> |
||||
|
<q-item-label lines="1" class="text-caption">Manage extensions</q-item-label> |
||||
|
</q-item-section> |
||||
|
</q-item> |
||||
|
</q-list> |
||||
|
`,
|
||||
|
computed: { |
||||
|
userExtensions: function () { |
||||
|
if (!this.user) return []; |
||||
|
var userExtensions = this.user.extensions; |
||||
|
return this.extensions.filter(function (obj) { |
||||
|
return userExtensions.indexOf(obj.code) !== -1; |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
created: function () { |
||||
|
this.extensions = window.extensions.map(function (data) { |
||||
|
return LNbits.map.extension(data); |
||||
|
}).sort(function (a, b) { |
||||
|
return a.name > b.name; |
||||
|
}); |
||||
|
if (window.user) { |
||||
|
this.user = LNbits.map.user(window.user); |
||||
|
} |
||||
|
} |
||||
|
}); |
Before Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 1.2 MiB |
@ -0,0 +1,53 @@ |
|||||
|
$dark-background: #1f2234; |
||||
|
$dark-card-background: #333646; |
||||
|
|
||||
|
|
||||
|
[v-cloak] { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.bg-lnbits-dark { |
||||
|
background-color: $dark-background; |
||||
|
} |
||||
|
|
||||
|
body.body--dark, |
||||
|
body.body--dark .q-drawer--dark, |
||||
|
body.body--dark .q-menu--dark { |
||||
|
background: $dark-background; |
||||
|
} |
||||
|
|
||||
|
body.body--dark .q-card--dark { |
||||
|
background: $dark-card-background; |
||||
|
} |
||||
|
|
||||
|
body.body--dark .q-table--dark { |
||||
|
background: transparent; |
||||
|
} |
||||
|
|
||||
|
body.body--light, |
||||
|
body.body--light .q-drawer { |
||||
|
background: whitesmoke; |
||||
|
} |
||||
|
|
||||
|
body.body--dark .q-field--error { |
||||
|
.text-negative, |
||||
|
.q-field__messages { |
||||
|
color: yellow !important; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.lnbits__q-table__icon-td { |
||||
|
padding-left: 5px !important; |
||||
|
} |
||||
|
|
||||
|
.lnbits-drawer__q-list .q-item { |
||||
|
padding-top: 5px !important; |
||||
|
padding-bottom: 5px !important; |
||||
|
border-top-right-radius: 3px; |
||||
|
border-bottom-right-radius: 3px; |
||||
|
|
||||
|
&.q-item--active { |
||||
|
color: inherit; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
} |
Before Width: | Height: | Size: 14 KiB |
@ -0,0 +1,236 @@ |
|||||
|
//TODO - A reader MUST check that the signature is valid (see the n tagged field)
|
||||
|
//TODO - Tagged part of type f: the fallback on-chain address should be decoded into an address format
|
||||
|
//TODO - A reader MUST check that the SHA-2 256 in the h field exactly matches the hashed description.
|
||||
|
//TODO - A reader MUST use the n field to validate the signature instead of performing signature recovery if a valid n field is provided.
|
||||
|
|
||||
|
function decode(paymentRequest) { |
||||
|
let input = paymentRequest.toLowerCase(); |
||||
|
let splitPosition = input.lastIndexOf('1'); |
||||
|
let humanReadablePart = input.substring(0, splitPosition); |
||||
|
let data = input.substring(splitPosition + 1, input.length - 6); |
||||
|
let checksum = input.substring(input.length - 6, input.length); |
||||
|
if (!verify_checksum(humanReadablePart, bech32ToFiveBitArray(data + checksum))) { |
||||
|
throw 'Malformed request: checksum is incorrect'; // A reader MUST fail if the checksum is incorrect.
|
||||
|
} |
||||
|
return { |
||||
|
'human_readable_part': decodeHumanReadablePart(humanReadablePart), |
||||
|
'data': decodeData(data, humanReadablePart), |
||||
|
'checksum': checksum |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function decodeHumanReadablePart(humanReadablePart) { |
||||
|
let prefixes = ['lnbc', 'lntb', 'lnbcrt', 'lnsb']; |
||||
|
let prefix; |
||||
|
prefixes.forEach(value => { |
||||
|
if (humanReadablePart.substring(0, value.length) === value) { |
||||
|
prefix = value; |
||||
|
} |
||||
|
}); |
||||
|
if (prefix == null) throw 'Malformed request: unknown prefix'; // A reader MUST fail if it does not understand the prefix.
|
||||
|
let amount = decodeAmount(humanReadablePart.substring(prefix.length, humanReadablePart.length)); |
||||
|
return { |
||||
|
'prefix': prefix, |
||||
|
'amount': amount |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function decodeData(data, humanReadablePart) { |
||||
|
let date32 = data.substring(0, 7); |
||||
|
let dateEpoch = bech32ToInt(date32); |
||||
|
let signature = data.substring(data.length - 104, data.length); |
||||
|
let tagData = data.substring(7, data.length - 104); |
||||
|
let decodedTags = decodeTags(tagData); |
||||
|
let value = bech32ToFiveBitArray(date32 + tagData); |
||||
|
value = fiveBitArrayTo8BitArray(value, true); |
||||
|
value = textToHexString(humanReadablePart).concat(byteArrayToHexString(value)); |
||||
|
return { |
||||
|
'time_stamp': dateEpoch, |
||||
|
'tags': decodedTags, |
||||
|
'signature': decodeSignature(signature), |
||||
|
'signing_data': value |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function decodeSignature(signature) { |
||||
|
let data = fiveBitArrayTo8BitArray(bech32ToFiveBitArray(signature)); |
||||
|
let recoveryFlag = data[data.length - 1]; |
||||
|
let r = byteArrayToHexString(data.slice(0, 32)); |
||||
|
let s = byteArrayToHexString(data.slice(32, data.length - 1)); |
||||
|
return { |
||||
|
'r': r, |
||||
|
's': s, |
||||
|
'recovery_flag': recoveryFlag |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function decodeAmount(str) { |
||||
|
let multiplier = str.charAt(str.length - 1); |
||||
|
let amount = str.substring(0, str.length - 1); |
||||
|
if (amount.substring(0, 1) === '0') { |
||||
|
throw 'Malformed request: amount cannot contain leading zeros'; |
||||
|
} |
||||
|
amount = Number(amount); |
||||
|
if (amount < 0 || !Number.isInteger(amount)) { |
||||
|
throw 'Malformed request: amount must be a positive decimal integer'; // A reader SHOULD fail if amount contains a non-digit
|
||||
|
} |
||||
|
|
||||
|
switch (multiplier) { |
||||
|
case '': |
||||
|
return 'Any amount'; // A reader SHOULD indicate if amount is unspecified
|
||||
|
case 'p': |
||||
|
return amount / 10; |
||||
|
case 'n': |
||||
|
return amount * 100; |
||||
|
case 'u': |
||||
|
return amount * 100000; |
||||
|
case 'm': |
||||
|
return amount * 100000000; |
||||
|
default: |
||||
|
// A reader SHOULD fail if amount is followed by anything except a defined multiplier.
|
||||
|
throw 'Malformed request: undefined amount multiplier'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function decodeTags(tagData) { |
||||
|
let tags = extractTags(tagData); |
||||
|
let decodedTags = []; |
||||
|
tags.forEach(value => decodedTags.push(decodeTag(value.type, value.length, value.data))); |
||||
|
return decodedTags; |
||||
|
} |
||||
|
|
||||
|
function extractTags(str) { |
||||
|
let tags = []; |
||||
|
while (str.length > 0) { |
||||
|
let type = str.charAt(0); |
||||
|
let dataLength = bech32ToInt(str.substring(1, 3)); |
||||
|
let data = str.substring(3, dataLength + 3); |
||||
|
tags.push({ |
||||
|
'type': type, |
||||
|
'length': dataLength, |
||||
|
'data': data |
||||
|
}); |
||||
|
str = str.substring(3 + dataLength, str.length); |
||||
|
} |
||||
|
return tags; |
||||
|
} |
||||
|
|
||||
|
function decodeTag(type, length, data) { |
||||
|
switch (type) { |
||||
|
case 'p': |
||||
|
if (length !== 52) break; // A reader MUST skip over a 'p' field that does not have data_length 52
|
||||
|
return { |
||||
|
'type': type, |
||||
|
'length': length, |
||||
|
'description': 'payment_hash', |
||||
|
'value': byteArrayToHexString(fiveBitArrayTo8BitArray(bech32ToFiveBitArray(data))) |
||||
|
}; |
||||
|
case 'd': |
||||
|
return { |
||||
|
'type': type, |
||||
|
'length': length, |
||||
|
'description': 'description', |
||||
|
'value': bech32ToUTF8String(data) |
||||
|
}; |
||||
|
case 'n': |
||||
|
if (length !== 53) break; // A reader MUST skip over a 'n' field that does not have data_length 53
|
||||
|
return { |
||||
|
'type': type, |
||||
|
'length': length, |
||||
|
'description': 'payee_public_key', |
||||
|
'value': byteArrayToHexString(fiveBitArrayTo8BitArray(bech32ToFiveBitArray(data))) |
||||
|
}; |
||||
|
case 'h': |
||||
|
if (length !== 52) break; // A reader MUST skip over a 'h' field that does not have data_length 52
|
||||
|
return { |
||||
|
'type': type, |
||||
|
'length': length, |
||||
|
'description': 'description_hash', |
||||
|
'value': data |
||||
|
}; |
||||
|
case 'x': |
||||
|
return { |
||||
|
'type': type, |
||||
|
'length': length, |
||||
|
'description': 'expiry', |
||||
|
'value': bech32ToInt(data) |
||||
|
}; |
||||
|
case 'c': |
||||
|
return { |
||||
|
'type': type, |
||||
|
'length': length, |
||||
|
'description': 'min_final_cltv_expiry', |
||||
|
'value': bech32ToInt(data) |
||||
|
}; |
||||
|
case 'f': |
||||
|
let version = bech32ToFiveBitArray(data.charAt(0))[0]; |
||||
|
if (version < 0 || version > 18) break; // a reader MUST skip over an f field with unknown version.
|
||||
|
data = data.substring(1, data.length); |
||||
|
return { |
||||
|
'type': type, |
||||
|
'length': length, |
||||
|
'description': 'fallback_address', |
||||
|
'value': { |
||||
|
'version': version, |
||||
|
'fallback_address': data |
||||
|
} |
||||
|
}; |
||||
|
case 'r': |
||||
|
data = fiveBitArrayTo8BitArray(bech32ToFiveBitArray(data)); |
||||
|
let pubkey = data.slice(0, 33); |
||||
|
let shortChannelId = data.slice(33, 41); |
||||
|
let feeBaseMsat = data.slice(41, 45); |
||||
|
let feeProportionalMillionths = data.slice(45, 49); |
||||
|
let cltvExpiryDelta = data.slice(49, 51); |
||||
|
return { |
||||
|
'type': type, |
||||
|
'length': length, |
||||
|
'description': 'routing_information', |
||||
|
'value': { |
||||
|
'public_key': byteArrayToHexString(pubkey), |
||||
|
'short_channel_id': byteArrayToHexString(shortChannelId), |
||||
|
'fee_base_msat': byteArrayToInt(feeBaseMsat), |
||||
|
'fee_proportional_millionths': byteArrayToInt(feeProportionalMillionths), |
||||
|
'cltv_expiry_delta': byteArrayToInt(cltvExpiryDelta) |
||||
|
} |
||||
|
}; |
||||
|
default: |
||||
|
// reader MUST skip over unknown fields
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function polymod(values) { |
||||
|
let GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; |
||||
|
let chk = 1; |
||||
|
values.forEach((value) => { |
||||
|
let b = (chk >> 25); |
||||
|
chk = (chk & 0x1ffffff) << 5 ^ value; |
||||
|
for (let i = 0; i < 5; i++) { |
||||
|
if (((b >> i) & 1) === 1) { |
||||
|
chk ^= GEN[i]; |
||||
|
} else { |
||||
|
chk ^= 0; |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
return chk; |
||||
|
} |
||||
|
|
||||
|
function expand(str) { |
||||
|
let array = []; |
||||
|
for (let i = 0; i < str.length; i++) { |
||||
|
array.push(str.charCodeAt(i) >> 5); |
||||
|
} |
||||
|
array.push(0); |
||||
|
for (let i = 0; i < str.length; i++) { |
||||
|
array.push(str.charCodeAt(i) & 31); |
||||
|
} |
||||
|
return array; |
||||
|
} |
||||
|
|
||||
|
function verify_checksum(hrp, data) { |
||||
|
hrp = expand(hrp); |
||||
|
let all = hrp.concat(data); |
||||
|
let bool = polymod(all); |
||||
|
return bool === 1; |
||||
|
} |
@ -0,0 +1,96 @@ |
|||||
|
const bech32CharValues = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'; |
||||
|
|
||||
|
function byteArrayToInt(byteArray) { |
||||
|
let value = 0; |
||||
|
for (let i = 0; i < byteArray.length; ++i) { |
||||
|
value = (value << 8) + byteArray[i]; |
||||
|
} |
||||
|
return value; |
||||
|
} |
||||
|
|
||||
|
function bech32ToInt(str) { |
||||
|
let sum = 0; |
||||
|
for (let i = 0; i < str.length; i++) { |
||||
|
sum = sum * 32; |
||||
|
sum = sum + bech32CharValues.indexOf(str.charAt(i)); |
||||
|
} |
||||
|
return sum; |
||||
|
} |
||||
|
|
||||
|
function bech32ToFiveBitArray(str) { |
||||
|
let array = []; |
||||
|
for (let i = 0; i < str.length; i++) { |
||||
|
array.push(bech32CharValues.indexOf(str.charAt(i))); |
||||
|
} |
||||
|
return array; |
||||
|
} |
||||
|
|
||||
|
function fiveBitArrayTo8BitArray(int5Array, includeOverflow) { |
||||
|
let count = 0; |
||||
|
let buffer = 0; |
||||
|
let byteArray = []; |
||||
|
int5Array.forEach((value) => { |
||||
|
buffer = (buffer << 5) + value; |
||||
|
count += 5; |
||||
|
if (count >= 8) { |
||||
|
byteArray.push(buffer >> (count - 8) & 255); |
||||
|
count -= 8; |
||||
|
} |
||||
|
}); |
||||
|
if (includeOverflow && count > 0) { |
||||
|
byteArray.push(buffer << (8 - count) & 255); |
||||
|
} |
||||
|
return byteArray; |
||||
|
} |
||||
|
|
||||
|
function bech32ToUTF8String(str) { |
||||
|
let int5Array = bech32ToFiveBitArray(str); |
||||
|
let byteArray = fiveBitArrayTo8BitArray(int5Array); |
||||
|
|
||||
|
let utf8String = ''; |
||||
|
for (let i = 0; i < byteArray.length; i++) { |
||||
|
utf8String += '%' + ('0' + byteArray[i].toString(16)).slice(-2); |
||||
|
} |
||||
|
return decodeURIComponent(utf8String); |
||||
|
} |
||||
|
|
||||
|
function byteArrayToHexString(byteArray) { |
||||
|
return Array.prototype.map.call(byteArray, function (byte) { |
||||
|
return ('0' + (byte & 0xFF).toString(16)).slice(-2); |
||||
|
}).join(''); |
||||
|
} |
||||
|
|
||||
|
function textToHexString(text) { |
||||
|
let hexString = ''; |
||||
|
for (let i = 0; i < text.length; i++) { |
||||
|
hexString += text.charCodeAt(i).toString(16); |
||||
|
} |
||||
|
return hexString; |
||||
|
} |
||||
|
|
||||
|
function epochToDate(int) { |
||||
|
let date = new Date(int * 1000); |
||||
|
return date.toUTCString(); |
||||
|
} |
||||
|
|
||||
|
function isEmptyOrSpaces(str){ |
||||
|
return str === null || str.match(/^ *$/) !== null; |
||||
|
} |
||||
|
|
||||
|
function toFixed(x) { |
||||
|
if (Math.abs(x) < 1.0) { |
||||
|
var e = parseInt(x.toString().split('e-')[1]); |
||||
|
if (e) { |
||||
|
x *= Math.pow(10,e-1); |
||||
|
x = '0.' + (new Array(e)).join('0') + x.toString().substring(2); |
||||
|
} |
||||
|
} else { |
||||
|
var e = parseInt(x.toString().split('+')[1]); |
||||
|
if (e > 20) { |
||||
|
e -= 20; |
||||
|
x /= Math.pow(10,e); |
||||
|
x += (new Array(e+1)).join('0'); |
||||
|
} |
||||
|
} |
||||
|
return x; |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
@keyframes chartjs-render-animation{from{opacity:.99}to{opacity:1}}.chartjs-render-monitor{animation:chartjs-render-animation 1ms}.chartjs-size-monitor,.chartjs-size-monitor-expand,.chartjs-size-monitor-shrink{position:absolute;direction:ltr;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1}.chartjs-size-monitor-expand>div{position:absolute;width:1000000px;height:1000000px;left:0;top:0}.chartjs-size-monitor-shrink>div{position:absolute;width:200%;height:200%;left:0;top:0} |
Before Width: | Height: | Size: 59 KiB |
@ -1,477 +1,86 @@ |
|||||
<!-- @format --> |
<!doctype html> |
||||
|
|
||||
<!DOCTYPE html> |
<html lang="en"> |
||||
<html> |
|
||||
<head> |
<head> |
||||
<meta charset="UTF-8" /> |
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Material+Icons" type="text/css"> |
||||
<title>LNBits Wallet</title> |
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='vendor/quasar@1.9.7/quasar.min.css') }}"> |
||||
<meta |
{% assets 'base_css' %} |
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" |
<link rel="stylesheet" type="text/css" href="{{ ASSET_URL }}"> |
||||
name="viewport" |
{% endassets %} |
||||
/> |
{% block styles %}{% endblock %} |
||||
<!-- Date picker --> |
<title>{% block title %}LNbits{% endblock %}</title> |
||||
<link |
<meta charset="utf-8"> |
||||
rel="stylesheet" |
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> |
||||
media="screen" |
{% block head_scripts %}{% endblock %} |
||||
href="{{ url_for('static', filename='bootstrap/css/datepicker.min.css') }}" |
|
||||
/> |
|
||||
|
|
||||
<!-- 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> |
|
||||
<!-- Datepicker 3.3.2 JS --> |
|
||||
<script |
|
||||
src="{{ url_for('static', filename='bootstrap/js/datepicker.min.js') }}" |
|
||||
type="text/javascript" |
|
||||
></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; |
|
||||
|
|
||||
} |
|
||||
|
|
||||
.h1 .small, .h1 small, .h2 .small, .h2 small, .h3 .small, .h3 small, .h4 .small, .h4 small, .h5 .small, .h5 small, .h6 .small, .h6 small, h1 .small, h1 small, h2 .small, h2 small, h3 .small, h3 small, h4 .small, h4 small, h5 .small, h5 small, h6 .small, h6 small { |
|
||||
font-weight: 400; |
|
||||
line-height: 1; |
|
||||
color: |
|
||||
#fff; |
|
||||
} |
|
||||
|
|
||||
body { |
|
||||
|
|
||||
background-color: #1f2234; |
|
||||
} |
|
||||
|
|
||||
|
|
||||
</style> |
|
||||
</head> |
</head> |
||||
<body class="skin-blue"> |
|
||||
<div class="wrapper"> |
|
||||
<header class="main-header"> |
|
||||
<!-- Logo --> |
|
||||
<a href="{{ url_for('core.home') }}" class="logo"><b style="color: #8964a9;">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> |
|
||||
</nav> |
|
||||
</header> |
|
||||
|
|
||||
<!-- Left side column. contains the logo and sidebar --> |
<body> |
||||
<aside class="main-sidebar"> |
<q-layout id="vue" view="hHh lpR lfr" v-cloak> |
||||
<!-- sidebar: style can be found in sidebar.less --> |
|
||||
<section class="sidebar"> |
<q-header bordered class="bg-lnbits-dark"> |
||||
<!-- Sidebar user panel --> |
<q-toolbar> |
||||
|
<q-btn dense flat round icon="menu" @click="w.visibleDrawer = !w.visibleDrawer"></q-btn> |
||||
<!-- /.search form --> |
<q-toolbar-title><strong>LN</strong>bits</q-toolbar-title> |
||||
<!-- sidebar menu: : style can be found in sidebar.less --> |
<q-badge color="yellow" text-color="black"> |
||||
<ul class="sidebar-menu"> |
<span><span v-show="$q.screen.gt.sm">USE WITH CAUTION - LNbits wallet is still in </span>BETA</span> |
||||
<li class="header">MENU</li> |
</q-badge> |
||||
{% block menuitems %}{% endblock %} |
<q-btn dense flat round @click="toggleDarkMode" :icon="($q.dark.isActive) ? 'brightness_3' : 'wb_sunny'" class="q-ml-lg" size="sm"> |
||||
</ul> |
<q-tooltip>Toggle Dark Mode</q-tooltip> |
||||
</section> |
</q-btn> |
||||
<!-- /.sidebar --> |
</q-toolbar> |
||||
</aside> |
</q-header> |
||||
|
|
||||
{% block body %}{% endblock %} |
<q-drawer v-model="w.visibleDrawer" side="left" :width="($q.screen.lt.md) ? 260 : 230" show-if-above :elevated="$q.screen.lt.md"> |
||||
</div> |
<lnbits-wallet-list></lnbits-wallet-list> |
||||
|
<lnbits-extension-list class="q-pb-xl"></lnbits-extension-list> |
||||
<footer class="main-footer"> |
</q-drawer> |
||||
<div class="pull-right hidden-xs"> |
|
||||
<b>BETA</b> |
<q-page-container> |
||||
</div> |
<q-page class="q-px-md q-py-lg" :class="{'q-px-lg': $q.screen.gt.xs}"> |
||||
<strong |
{% block page %}{% endblock %} |
||||
>Learn more about LNbits |
</q-page> |
||||
<a href="https://github.com/arcbtc/lnbits" |
</q-page-container> |
||||
>https://github.com/arcbtc/lnbits</a |
|
||||
></strong |
<q-footer class="bg-transparent q-px-lg q-py-md" :class="{'text-dark': !$q.dark.isActive}"> |
||||
> |
<q-toolbar> |
||||
</footer> |
<q-toolbar-title class="text-caption"> |
||||
|
<strong>LN</strong>bits, free and open-source lightning wallet |
||||
|
</q-toolbar-title> |
||||
|
<q-space></q-space> |
||||
|
<q-btn flat dense :color="($q.dark.isActive) ? 'white' : 'deep-purple'" icon="code" type="a" href="https://github.com/arcbtc/lnbits" target="_blank" rel="noopener"> |
||||
|
<q-tooltip>View project in GitHub</q-tooltip> |
||||
|
</q-btn> |
||||
|
</q-toolbar> |
||||
|
</q-footer> |
||||
|
|
||||
|
</q-layout> |
||||
|
|
||||
|
{% block vue_templates %}{% endblock %} |
||||
|
|
||||
|
{% if DEBUG %} |
||||
|
<script src="{{ url_for('static', filename='vendor/vue@2.6.11/vue.js') }}"></script> |
||||
|
<script src="{{ url_for('static', filename='vendor/vue-router@3.1.6/vue-router.js') }}"></script> |
||||
|
<script src="{{ url_for('static', filename='vendor/vuex@3.1.2/vuex.js') }}"></script> |
||||
|
<script src="{{ url_for('static', filename='vendor/quasar@1.9.7/quasar.umd.js') }}"></script> |
||||
|
{% else %} |
||||
|
{% assets output='__bundle__/vue.js', |
||||
|
'vendor/quasar@1.9.7/quasar.ie.polyfills.umd.min.js', |
||||
|
'vendor/vue@2.6.11/vue.min.js', |
||||
|
'vendor/vue-router@3.1.6/vue-router.min.js', |
||||
|
'vendor/vuex@3.1.2/vuex.min.js', |
||||
|
'vendor/quasar@1.9.7/quasar.umd.min.js' %} |
||||
|
<script type="text/javascript" src="{{ ASSET_URL }}"></script> |
||||
|
{% endassets %} |
||||
|
{% endif %} |
||||
|
|
||||
|
{% assets filters='rjsmin', output='__bundle__/base.js', |
||||
|
'vendor/axios@0.19.2/axios.min.js', |
||||
|
'vendor/underscore@1.9.2/underscore.min.js', |
||||
|
'js/base.js', |
||||
|
'js/components.js' %} |
||||
|
<script type="text/javascript" src="{{ ASSET_URL }}"></script> |
||||
|
{% endassets %} |
||||
|
|
||||
|
{% block scripts %}{% endblock %} |
||||
</body> |
</body> |
||||
|
|
||||
<script |
|
||||
src="{{ url_for('static', filename='app.js') }}" |
|
||||
type="text/javascript" |
|
||||
></script> |
|
||||
</html> |
</html> |
||||
|
@ -1,114 +0,0 @@ |
|||||
<!-- @format --> |
|
||||
|
|
||||
{% extends "base.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('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"> |
|
||||
<div id="wonga"></div> |
|
||||
<h1>Wallet <small>Control panel</small></h1> |
|
||||
<ol class="breadcrumb"> |
|
||||
<li><a href="#"><i class="fa fa-dashboard"></i> Home</a></li> |
|
||||
<li class="active">Extensions</li> |
|
||||
</ol> |
|
||||
|
|
||||
<br /> |
|
||||
<br /> |
|
||||
|
|
||||
<div class="alert alert-danger alert-dismissable"> |
|
||||
<h4>Bookmark to save your wallet. Wallet is in BETA, use with caution.</h4> |
|
||||
</div> |
|
||||
</section> |
|
||||
|
|
||||
<!-- Main content --> |
|
||||
<section class="content"> |
|
||||
<!-- Small boxes (Stat box) --> |
|
||||
<div class="row"> |
|
||||
|
|
||||
{% for extension in EXTENSIONS %} |
|
||||
<div class="col-lg-3 col-xs-6"> |
|
||||
<!-- small box --> |
|
||||
<div class="small-box bg-blue"> |
|
||||
<div class="inner"> |
|
||||
{% if extension.code in user_ext %} |
|
||||
<a href="{{ url_for(extension.code + '.index') }}?usr={{ user }}" style="color: inherit"> |
|
||||
{% endif %} |
|
||||
<h3>{{ extension.name }}</h3> |
|
||||
<p>{{ extension.short_description }}</p> |
|
||||
{% if extension.code in user_ext %} |
|
||||
</a> |
|
||||
{% endif %} |
|
||||
</div> |
|
||||
<div class="icon"> |
|
||||
<i class="ion ion-{{ extension.ion_icon }}"></i> |
|
||||
</div> |
|
||||
{% if extension.code in user_ext %} |
|
||||
<a href="{{ url_for('extensions') }}?usr={{user}}&disable={{ extension.code }}" class="small-box-footer">Disable <i class="fa fa-arrow-circle-right"></i></a> |
|
||||
{% else %} |
|
||||
<a href="{{ url_for('extensions') }}?usr={{user}}&enable={{ extension.code }}" class="small-box-footer">Enable <i class="fa fa-arrow-circle-right"></i></a> |
|
||||
{% endif %} |
|
||||
</div> |
|
||||
</div> |
|
||||
{% endfor %} |
|
||||
|
|
||||
</div> |
|
||||
<!-- /.content --> |
|
||||
</section> |
|
||||
</div> |
|
||||
|
|
||||
<script> |
|
||||
window.user = {{ user | megajson | safe }} |
|
||||
window.user_wallets = {{ user_wallets | megajson | safe }} |
|
||||
window.user_ext = {{ user_ext | megajson | safe }} |
|
||||
</script> |
|
||||
{% endblock %} |
|
@ -0,0 +1,477 @@ |
|||||
|
<!-- @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" |
||||
|
/> |
||||
|
<!-- Date picker --> |
||||
|
<link |
||||
|
rel="stylesheet" |
||||
|
media="screen" |
||||
|
href="{{ url_for('static', filename='bootstrap/css/datepicker.min.css') }}" |
||||
|
/> |
||||
|
|
||||
|
<!-- 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> |
||||
|
<!-- Datepicker 3.3.2 JS --> |
||||
|
<script |
||||
|
src="{{ url_for('static', filename='bootstrap/js/datepicker.min.js') }}" |
||||
|
type="text/javascript" |
||||
|
></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; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
.h1 .small, .h1 small, .h2 .small, .h2 small, .h3 .small, .h3 small, .h4 .small, .h4 small, .h5 .small, .h5 small, .h6 .small, .h6 small, h1 .small, h1 small, h2 .small, h2 small, h3 .small, h3 small, h4 .small, h4 small, h5 .small, h5 small, h6 .small, h6 small { |
||||
|
font-weight: 400; |
||||
|
line-height: 1; |
||||
|
color: |
||||
|
#fff; |
||||
|
} |
||||
|
|
||||
|
body { |
||||
|
|
||||
|
background-color: #1f2234; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
</style> |
||||
|
</head> |
||||
|
<body class="skin-blue"> |
||||
|
<div class="wrapper"> |
||||
|
<header class="main-header"> |
||||
|
<!-- Logo --> |
||||
|
<a href="{{ url_for('core.home') }}" class="logo"><b style="color: #8964a9;">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> |
||||
|
</nav> |
||||
|
</header> |
||||
|
|
||||
|
<!-- Left side column. contains the logo and sidebar --> |
||||
|
<aside class="main-sidebar"> |
||||
|
<!-- sidebar: style can be found in sidebar.less --> |
||||
|
<section class="sidebar"> |
||||
|
<!-- Sidebar user panel --> |
||||
|
|
||||
|
<!-- /.search form --> |
||||
|
<!-- sidebar menu: : style can be found in sidebar.less --> |
||||
|
<ul class="sidebar-menu"> |
||||
|
<li class="header">MENU</li> |
||||
|
{% block menuitems %}{% endblock %} |
||||
|
</ul> |
||||
|
</section> |
||||
|
<!-- /.sidebar --> |
||||
|
</aside> |
||||
|
|
||||
|
{% block body %}{% endblock %} |
||||
|
</div> |
||||
|
|
||||
|
<footer class="main-footer"> |
||||
|
<div class="pull-right hidden-xs"> |
||||
|
<b>BETA</b> |
||||
|
</div> |
||||
|
<strong |
||||
|
>Learn more about LNbits |
||||
|
<a href="https://github.com/arcbtc/lnbits" |
||||
|
>https://github.com/arcbtc/lnbits</a |
||||
|
></strong |
||||
|
> |
||||
|
</footer> |
||||
|
</body> |
||||
|
|
||||
|
<script |
||||
|
src="{{ url_for('static', filename='app.js') }}" |
||||
|
type="text/javascript" |
||||
|
></script> |
||||
|
</html> |
@ -0,0 +1,12 @@ |
|||||
|
{% macro window_vars(user, wallet) -%} |
||||
|
<script> |
||||
|
window.extensions = {{ EXTENSIONS | tojson | safe }}; |
||||
|
{% if user %} |
||||
|
window.user = {{ user | tojson | safe }}; |
||||
|
{% endif %} |
||||
|
{% if wallet %} |
||||
|
window.wallet = {{ wallet | tojson | safe }}; |
||||
|
window.transactions = {{ wallet.get_transactions() | tojson | safe }}; |
||||
|
{% endif %} |
||||
|
</script> |
||||
|
{%- endmacro %} |
Before Width: | Height: | Size: 79 KiB |