refactor: decorators, models and more broken bits
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
core_app = Blueprint("core", __name__, template_folder="templates")
|
||||
core_app = Blueprint("core", __name__, template_folder="templates", static_folder="static")
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,47 @@
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT,
|
||||
pass TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS extensions (
|
||||
user TEXT NOT NULL,
|
||||
extension TEXT NOT NULL,
|
||||
active BOOLEAN DEFAULT 0,
|
||||
|
||||
UNIQUE (user, extension)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wallets (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
user TEXT NOT NULL,
|
||||
adminkey TEXT NOT NULL,
|
||||
inkey TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS apipayments (
|
||||
payhash TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
fee INTEGER NOT NULL DEFAULT 0,
|
||||
wallet TEXT NOT NULL,
|
||||
pending BOOLEAN NOT NULL,
|
||||
memo TEXT,
|
||||
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
|
||||
UNIQUE (wallet, payhash)
|
||||
);
|
||||
|
||||
CREATE VIEW IF NOT EXISTS balances AS
|
||||
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
|
||||
SELECT wallet, SUM(amount) AS s -- incoming
|
||||
FROM apipayments
|
||||
WHERE amount > 0 AND pending = 0 -- don't sum pending
|
||||
GROUP BY wallet
|
||||
UNION ALL
|
||||
SELECT wallet, SUM(amount + fee) AS s -- outgoing, sum fees
|
||||
FROM apipayments
|
||||
WHERE amount < 0 -- do sum pending
|
||||
GROUP BY wallet
|
||||
)
|
||||
GROUP BY wallet;
|
||||
Binary file not shown.
|
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 %}
|
||||
+79
-2
@@ -1,7 +1,17 @@
|
||||
from flask import render_template, send_from_directory
|
||||
from flask import g, abort, redirect, request, render_template, send_from_directory, url_for
|
||||
from os import path
|
||||
|
||||
from lnbits.core import core_app
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.helpers import Status
|
||||
|
||||
from .crud import (
|
||||
create_account,
|
||||
get_user,
|
||||
update_user_extension,
|
||||
create_wallet,
|
||||
delete_wallet,
|
||||
)
|
||||
|
||||
|
||||
@core_app.route("/favicon.ico")
|
||||
@@ -11,4 +21,71 @@ def favicon():
|
||||
|
||||
@core_app.route("/")
|
||||
def home():
|
||||
return render_template("index.html")
|
||||
return render_template("core/index.html")
|
||||
|
||||
|
||||
@core_app.route("/extensions")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
def extensions():
|
||||
extension_to_enable = request.args.get("enable", type=str)
|
||||
extension_to_disable = request.args.get("disable", type=str)
|
||||
|
||||
if extension_to_enable and extension_to_disable:
|
||||
abort(Status.BAD_REQUEST, "You can either `enable` or `disable` an extension.")
|
||||
|
||||
if extension_to_enable:
|
||||
update_user_extension(user_id=g.user.id, extension=extension_to_enable, active=1)
|
||||
elif extension_to_disable:
|
||||
update_user_extension(user_id=g.user.id, extension=extension_to_disable, active=0)
|
||||
|
||||
return render_template("core/extensions.html", user=get_user(g.user.id))
|
||||
|
||||
|
||||
@core_app.route("/wallet")
|
||||
@validate_uuids(["usr", "wal"])
|
||||
def wallet():
|
||||
user_id = request.args.get("usr", type=str)
|
||||
wallet_id = request.args.get("wal", type=str)
|
||||
wallet_name = request.args.get("nme", type=str)
|
||||
|
||||
# just wallet_name: create a new user, then create a new wallet for user with wallet_name
|
||||
# just user_id: return the first user wallet or create one if none found (with default wallet_name)
|
||||
# user_id and wallet_name: create a new wallet for user with wallet_name
|
||||
# user_id and wallet_id: return that wallet if user is the owner
|
||||
# nothing: create everything
|
||||
|
||||
if not user_id:
|
||||
user = get_user(create_account().id)
|
||||
else:
|
||||
user = get_user(user_id) or abort(Status.NOT_FOUND, "User does not exist.")
|
||||
|
||||
if not wallet_id:
|
||||
if user.wallets and not wallet_name:
|
||||
wallet = user.wallets[0]
|
||||
else:
|
||||
wallet = create_wallet(user_id=user.id, wallet_name=wallet_name)
|
||||
|
||||
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))
|
||||
|
||||
if wallet_id not in user.wallet_ids:
|
||||
abort(Status.FORBIDDEN, "Not your wallet.")
|
||||
|
||||
return render_template("core/wallet.html", user=user, wallet=user.get_wallet(wallet_id))
|
||||
|
||||
|
||||
@core_app.route("/deletewallet")
|
||||
@validate_uuids(["usr", "wal"], required=True)
|
||||
@check_user_exists()
|
||||
def deletewallet():
|
||||
wallet_id = request.args.get("wal", type=str)
|
||||
|
||||
if wallet_id not in g.user.wallet_ids:
|
||||
abort(Status.FORBIDDEN, "Not your wallet.")
|
||||
else:
|
||||
delete_wallet(user_id=g.user.id, wallet_id=wallet_id)
|
||||
|
||||
if g.user.wallets:
|
||||
return redirect(url_for("core.wallet", usr=g.user.id, wal=g.user.wallets[0].id))
|
||||
|
||||
return redirect(url_for("core.home"))
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user