refactor: "payments" is the name, and API updates
This commit is contained in:
parent
6e26e06aea
commit
d862b16ee6
16 changed files with 355 additions and 280 deletions
|
|
@ -3,18 +3,16 @@ import json
|
||||||
import requests
|
import requests
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from flask import g, Flask, jsonify, redirect, render_template, request, url_for
|
from flask import Flask, redirect, render_template, request, url_for
|
||||||
from flask_assets import Environment, Bundle
|
from flask_assets import Environment, Bundle
|
||||||
from flask_compress import Compress
|
from flask_compress import Compress
|
||||||
from flask_talisman import Talisman
|
from flask_talisman import Talisman
|
||||||
from lnurl import Lnurl, LnurlWithdrawResponse
|
from lnurl import Lnurl, LnurlWithdrawResponse
|
||||||
|
|
||||||
from . import bolt11
|
|
||||||
from .core import core_app
|
from .core import core_app
|
||||||
from .decorators import api_validate_post_request
|
|
||||||
from .db import init_databases, open_db
|
from .db import init_databases, open_db
|
||||||
from .helpers import ExtensionManager, megajson
|
from .helpers import ExtensionManager, megajson
|
||||||
from .settings import WALLET, DEFAULT_USER_WALLET_NAME, FEE_RESERVE
|
from .settings import WALLET, DEFAULT_USER_WALLET_NAME
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
@ -147,93 +145,5 @@ def lnurlwallet():
|
||||||
return redirect(url_for("wallet", usr=user_id, wal=wallet_id))
|
return redirect(url_for("wallet", usr=user_id, wal=wallet_id))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/v1/channels/transactions", methods=["GET", "POST"])
|
|
||||||
@api_validate_post_request(required_params=["payment_request"])
|
|
||||||
def api_transactions():
|
|
||||||
|
|
||||||
with open_db() as db:
|
|
||||||
wallet = db.fetchone("SELECT id FROM wallets WHERE adminkey = ?", (request.headers["Grpc-Metadata-macaroon"],))
|
|
||||||
|
|
||||||
if not wallet:
|
|
||||||
return jsonify({"message": "BAD AUTH"}), 401
|
|
||||||
|
|
||||||
# decode the invoice
|
|
||||||
invoice = bolt11.decode(g.data["payment_request"])
|
|
||||||
if invoice.amount_msat == 0:
|
|
||||||
return jsonify({"message": "AMOUNTLESS INVOICES NOT SUPPORTED"}), 400
|
|
||||||
|
|
||||||
# insert the payment
|
|
||||||
db.execute(
|
|
||||||
"INSERT OR IGNORE INTO apipayments (payhash, amount, fee, wallet, pending, memo) VALUES (?, ?, ?, ?, 1, ?)",
|
|
||||||
(
|
|
||||||
invoice.payment_hash,
|
|
||||||
-int(invoice.amount_msat),
|
|
||||||
-int(invoice.amount_msat) * FEE_RESERVE,
|
|
||||||
wallet["id"],
|
|
||||||
invoice.description,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# check balance
|
|
||||||
balance = db.fetchone("SELECT balance/1000 FROM balances WHERE wallet = ?", (wallet["id"],))[0]
|
|
||||||
if balance < 0:
|
|
||||||
db.execute("DELETE FROM apipayments WHERE payhash = ? AND wallet = ?", (invoice.payment_hash, wallet["id"]))
|
|
||||||
return jsonify({"message": "INSUFFICIENT BALANCE"}), 403
|
|
||||||
|
|
||||||
# check if the invoice is an internal one
|
|
||||||
if db.fetchone("SELECT count(*) FROM apipayments WHERE payhash = ?", (invoice.payment_hash,))[0] == 2:
|
|
||||||
# internal. mark both sides as fulfilled.
|
|
||||||
db.execute("UPDATE apipayments SET pending = 0, fee = 0 WHERE payhash = ?", (invoice.payment_hash,))
|
|
||||||
else:
|
|
||||||
# actually send the payment
|
|
||||||
r = WALLET.pay_invoice(g.data["payment_request"])
|
|
||||||
|
|
||||||
if not r.raw_response.ok or r.failed:
|
|
||||||
return jsonify({"message": "UNEXPECTED PAYMENT ERROR"}), 500
|
|
||||||
|
|
||||||
# payment went through, not pending anymore, save actual fees
|
|
||||||
db.execute(
|
|
||||||
"UPDATE apipayments SET pending = 0, fee = ? WHERE payhash = ? AND wallet = ?",
|
|
||||||
(r.fee_msat, invoice.payment_hash, wallet["id"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
return jsonify({"PAID": "TRUE", "payment_hash": invoice.payment_hash}), 200
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/v1/checkpending", methods=["POST"])
|
|
||||||
def api_checkpending():
|
|
||||||
with open_db() as db:
|
|
||||||
for pendingtx in db.fetchall(
|
|
||||||
"""
|
|
||||||
SELECT
|
|
||||||
payhash,
|
|
||||||
CASE
|
|
||||||
WHEN amount < 0 THEN 'send'
|
|
||||||
ELSE 'recv'
|
|
||||||
END AS kind
|
|
||||||
FROM apipayments
|
|
||||||
INNER JOIN wallets ON apipayments.wallet = wallets.id
|
|
||||||
WHERE time > strftime('%s', 'now') - 86400
|
|
||||||
AND pending = 1
|
|
||||||
AND (adminkey = ? OR inkey = ?)
|
|
||||||
""",
|
|
||||||
(request.headers["Grpc-Metadata-macaroon"], request.headers["Grpc-Metadata-macaroon"]),
|
|
||||||
):
|
|
||||||
payhash = pendingtx["payhash"]
|
|
||||||
kind = pendingtx["kind"]
|
|
||||||
|
|
||||||
if kind == "send":
|
|
||||||
payment_complete = WALLET.get_payment_status(payhash).settled
|
|
||||||
if payment_complete:
|
|
||||||
db.execute("UPDATE apipayments SET pending = 0 WHERE payhash = ?", (payhash,))
|
|
||||||
elif payment_complete is False:
|
|
||||||
db.execute("DELETE FROM apipayments WHERE payhash = ?", (payhash,))
|
|
||||||
|
|
||||||
elif kind == "recv" and WALLET.get_invoice_status(payhash).settled:
|
|
||||||
db.execute("UPDATE apipayments SET pending = 0 WHERE payhash = ?", (payhash,))
|
|
||||||
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run()
|
app.run()
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from lnbits.db import open_db
|
||||||
from lnbits.settings import DEFAULT_USER_WALLET_NAME, FEE_RESERVE
|
from lnbits.settings import DEFAULT_USER_WALLET_NAME, FEE_RESERVE
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from .models import User, Transaction, Wallet
|
from .models import User, Wallet, Payment
|
||||||
|
|
||||||
|
|
||||||
# accounts
|
# accounts
|
||||||
|
|
@ -34,7 +34,7 @@ def get_user(user_id: str) -> Optional[User]:
|
||||||
extensions = db.fetchall("SELECT extension FROM extensions WHERE user = ? AND active = 1", (user_id,))
|
extensions = db.fetchall("SELECT extension FROM extensions WHERE user = ? AND active = 1", (user_id,))
|
||||||
wallets = db.fetchall(
|
wallets = db.fetchall(
|
||||||
"""
|
"""
|
||||||
SELECT *, COALESCE((SELECT balance/1000 FROM balances WHERE wallet = wallets.id), 0) * ? AS balance
|
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) * ? AS balance_msat
|
||||||
FROM wallets
|
FROM wallets
|
||||||
WHERE user = ?
|
WHERE user = ?
|
||||||
""",
|
""",
|
||||||
|
|
@ -96,7 +96,7 @@ def get_wallet(wallet_id: str) -> Optional[Wallet]:
|
||||||
with open_db() as db:
|
with open_db() as db:
|
||||||
row = db.fetchone(
|
row = db.fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT *, COALESCE((SELECT balance/1000 FROM balances WHERE wallet = wallets.id), 0) * ? AS balance
|
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) * ? AS balance_msat
|
||||||
FROM wallets
|
FROM wallets
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
""",
|
""",
|
||||||
|
|
@ -111,7 +111,7 @@ def get_wallet_for_key(key: str, key_type: str = "invoice") -> Optional[Wallet]:
|
||||||
check_field = "adminkey" if key_type == "admin" else "inkey"
|
check_field = "adminkey" if key_type == "admin" else "inkey"
|
||||||
row = db.fetchone(
|
row = db.fetchone(
|
||||||
f"""
|
f"""
|
||||||
SELECT *, COALESCE((SELECT balance/1000 FROM balances WHERE wallet = wallets.id), 0) * ? AS balance
|
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) * ? AS balance_msat
|
||||||
FROM wallets
|
FROM wallets
|
||||||
WHERE {check_field} = ?
|
WHERE {check_field} = ?
|
||||||
""",
|
""",
|
||||||
|
|
@ -121,11 +121,11 @@ def get_wallet_for_key(key: str, key_type: str = "invoice") -> Optional[Wallet]:
|
||||||
return Wallet(**row) if row else None
|
return Wallet(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
# wallet transactions
|
# wallet payments
|
||||||
# -------------------
|
# ---------------
|
||||||
|
|
||||||
|
|
||||||
def get_wallet_transaction(wallet_id: str, payhash: str) -> Optional[Transaction]:
|
def get_wallet_payment(wallet_id: str, payhash: str) -> Optional[Payment]:
|
||||||
with open_db() as db:
|
with open_db() as db:
|
||||||
row = db.fetchone(
|
row = db.fetchone(
|
||||||
"""
|
"""
|
||||||
|
|
@ -136,45 +136,51 @@ def get_wallet_transaction(wallet_id: str, payhash: str) -> Optional[Transaction
|
||||||
(wallet_id, payhash),
|
(wallet_id, payhash),
|
||||||
)
|
)
|
||||||
|
|
||||||
return Transaction(**row) if row else None
|
return Payment(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
def get_wallet_transactions(wallet_id: str, *, pending: bool = False) -> List[Transaction]:
|
def get_wallet_payments(wallet_id: str, *, include_all_pending: bool = False) -> List[Payment]:
|
||||||
with open_db() as db:
|
with open_db() as db:
|
||||||
|
if include_all_pending:
|
||||||
|
clause = "pending = 1"
|
||||||
|
else:
|
||||||
|
clause = "((amount > 0 AND pending = 0) OR amount < 0)"
|
||||||
|
|
||||||
rows = db.fetchall(
|
rows = db.fetchall(
|
||||||
"""
|
f"""
|
||||||
SELECT payhash, amount, fee, pending, memo, time
|
SELECT payhash, amount, fee, pending, memo, time
|
||||||
FROM apipayments
|
FROM apipayments
|
||||||
WHERE wallet = ? AND pending = ?
|
WHERE wallet = ? AND {clause}
|
||||||
ORDER BY time DESC
|
ORDER BY time DESC
|
||||||
""",
|
""",
|
||||||
(wallet_id, int(pending)),
|
(wallet_id,),
|
||||||
)
|
)
|
||||||
|
|
||||||
return [Transaction(**row) for row in rows]
|
return [Payment(**row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
# transactions
|
# payments
|
||||||
# ------------
|
# --------
|
||||||
|
|
||||||
|
|
||||||
def create_transaction(*, wallet_id: str, payhash: str, amount: str, memo: str) -> Transaction:
|
def create_payment(*, wallet_id: str, payhash: str, amount: str, memo: str, fee: int = 0) -> Payment:
|
||||||
with open_db() as db:
|
with open_db() as db:
|
||||||
db.execute(
|
db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO apipayments (wallet, payhash, amount, pending, memo)
|
INSERT INTO apipayments (wallet, payhash, amount, pending, memo, fee)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(wallet_id, payhash, amount, 1, memo),
|
(wallet_id, payhash, amount, 1, memo, fee),
|
||||||
)
|
)
|
||||||
|
|
||||||
return get_wallet_transaction(wallet_id, payhash)
|
return get_wallet_payment(wallet_id, payhash)
|
||||||
|
|
||||||
|
|
||||||
def update_transaction_status(payhash: str, pending: bool) -> None:
|
def update_payment_status(payhash: str, pending: bool) -> None:
|
||||||
with open_db() as db:
|
with open_db() as db:
|
||||||
db.execute("UPDATE apipayments SET pending = ? WHERE payhash = ?", (int(pending), payhash,))
|
db.execute("UPDATE apipayments SET pending = ? WHERE payhash = ?", (int(pending), payhash,))
|
||||||
|
|
||||||
|
|
||||||
def check_pending_transactions(wallet_id: str) -> None:
|
def delete_payment(payhash: str) -> None:
|
||||||
pass
|
with open_db() as db:
|
||||||
|
db.execute("DELETE FROM apipayments WHERE payhash = ?", (payhash,))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
from decimal import Decimal
|
|
||||||
from typing import List, NamedTuple, Optional
|
from typing import List, NamedTuple, Optional
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -24,20 +23,24 @@ class Wallet(NamedTuple):
|
||||||
user: str
|
user: str
|
||||||
adminkey: str
|
adminkey: str
|
||||||
inkey: str
|
inkey: str
|
||||||
balance: Decimal
|
balance_msat: int
|
||||||
|
|
||||||
def get_transaction(self, payhash: str) -> "Transaction":
|
@property
|
||||||
from .crud import get_wallet_transaction
|
def balance(self) -> int:
|
||||||
|
return int(self.balance / 1000)
|
||||||
|
|
||||||
return get_wallet_transaction(self.id, payhash)
|
def get_payment(self, payhash: str) -> "Payment":
|
||||||
|
from .crud import get_wallet_payment
|
||||||
|
|
||||||
def get_transactions(self) -> List["Transaction"]:
|
return get_wallet_payment(self.id, payhash)
|
||||||
from .crud import get_wallet_transactions
|
|
||||||
|
|
||||||
return get_wallet_transactions(self.id)
|
def get_payments(self, *, include_all_pending: bool = False) -> List["Payment"]:
|
||||||
|
from .crud import get_wallet_payments
|
||||||
|
|
||||||
|
return get_wallet_payments(self.id, include_all_pending=include_all_pending)
|
||||||
|
|
||||||
|
|
||||||
class Transaction(NamedTuple):
|
class Payment(NamedTuple):
|
||||||
payhash: str
|
payhash: str
|
||||||
pending: bool
|
pending: bool
|
||||||
amount: int
|
amount: int
|
||||||
|
|
@ -54,10 +57,19 @@ class Transaction(NamedTuple):
|
||||||
return self.amount / 1000
|
return self.amount / 1000
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tx_type(self) -> str:
|
def is_in(self) -> bool:
|
||||||
return "payment" if self.amount < 0 else "invoice"
|
return self.amount > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_out(self) -> bool:
|
||||||
|
return self.amount < 0
|
||||||
|
|
||||||
def set_pending(self, pending: bool) -> None:
|
def set_pending(self, pending: bool) -> None:
|
||||||
from .crud import update_transaction_status
|
from .crud import update_payment_status
|
||||||
|
|
||||||
update_transaction_status(self.payhash, pending)
|
update_payment_status(self.payhash, pending)
|
||||||
|
|
||||||
|
def delete(self) -> None:
|
||||||
|
from .crud import delete_payment
|
||||||
|
|
||||||
|
delete_payment(self.payhash)
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,36 @@
|
||||||
Vue.component(VueQrcode.name, VueQrcode);
|
Vue.component(VueQrcode.name, VueQrcode);
|
||||||
|
|
||||||
|
|
||||||
function generateChart(canvas, transactions) {
|
function generateChart(canvas, payments) {
|
||||||
var txs = [];
|
var txs = [];
|
||||||
var n = 0;
|
var n = 0;
|
||||||
var data = {
|
var data = {
|
||||||
labels: [],
|
labels: [],
|
||||||
sats: [],
|
income: [],
|
||||||
|
outcome: [],
|
||||||
cumulative: []
|
cumulative: []
|
||||||
};
|
};
|
||||||
|
|
||||||
_.each(transactions.sort(function (a, b) {
|
_.each(payments.slice(0).sort(function (a, b) {
|
||||||
return a.time - b.time;
|
return a.time - b.time;
|
||||||
}), function (tx) {
|
}), function (tx) {
|
||||||
txs.push({
|
txs.push({
|
||||||
day: Quasar.utils.date.formatDate(tx.date, 'YYYY-MM-DDTHH:00'),
|
hour: Quasar.utils.date.formatDate(tx.date, 'YYYY-MM-DDTHH:00'),
|
||||||
sat: tx.sat,
|
sat: tx.sat,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
_.each(_.groupBy(txs, 'day'), function (value, day) {
|
_.each(_.groupBy(txs, 'hour'), function (value, day) {
|
||||||
var sat = _.reduce(value, function(memo, tx) { return memo + tx.sat; }, 0);
|
var income = _.reduce(value, function(memo, tx) {
|
||||||
n = n + sat;
|
return (tx.sat >= 0) ? memo + tx.sat : memo;
|
||||||
|
}, 0);
|
||||||
|
var outcome = _.reduce(value, function(memo, tx) {
|
||||||
|
return (tx.sat < 0) ? memo + Math.abs(tx.sat) : memo;
|
||||||
|
}, 0);
|
||||||
|
n = n + income - outcome;
|
||||||
data.labels.push(day);
|
data.labels.push(day);
|
||||||
data.sats.push(sat);
|
data.income.push(income);
|
||||||
|
data.outcome.push(outcome);
|
||||||
data.cumulative.push(n);
|
data.cumulative.push(n);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -36,19 +43,25 @@ function generateChart(canvas, transactions) {
|
||||||
data: data.cumulative,
|
data: data.cumulative,
|
||||||
type: 'line',
|
type: 'line',
|
||||||
label: 'balance',
|
label: 'balance',
|
||||||
borderColor: '#673ab7', // deep-purple
|
backgroundColor: '#673ab7', // deep-purple
|
||||||
|
borderColor: '#673ab7',
|
||||||
borderWidth: 4,
|
borderWidth: 4,
|
||||||
pointRadius: 3,
|
pointRadius: 3,
|
||||||
fill: false
|
fill: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: data.sats,
|
data: data.income,
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
label: 'tx',
|
label: 'in',
|
||||||
backgroundColor: function (ctx) {
|
barPercentage: 0.75,
|
||||||
var value = ctx.dataset.data[ctx.dataIndex];
|
backgroundColor: window.Color('rgb(76,175,80)').alpha(0.5).rgbString() // green
|
||||||
return (value < 0) ? '#e91e63' : '#4caf50'; // pink : green
|
},
|
||||||
}
|
{
|
||||||
|
data: data.outcome,
|
||||||
|
type: 'bar',
|
||||||
|
label: 'out',
|
||||||
|
barPercentage: 0.75,
|
||||||
|
backgroundColor: window.Color('rgb(233,30,99)').alpha(0.5).rgbString() // pink
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -64,12 +77,22 @@ function generateChart(canvas, transactions) {
|
||||||
xAxes: [{
|
xAxes: [{
|
||||||
type: 'time',
|
type: 'time',
|
||||||
display: true,
|
display: true,
|
||||||
|
offset: true,
|
||||||
time: {
|
time: {
|
||||||
minUnit: 'hour',
|
minUnit: 'hour',
|
||||||
stepSize: 3
|
stepSize: 3
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
|
// performance tweaks
|
||||||
|
animation: {
|
||||||
|
duration: 0
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
line: {
|
||||||
|
tension: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +103,6 @@ new Vue({
|
||||||
mixins: [windowMixin],
|
mixins: [windowMixin],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
txUpdate: null,
|
|
||||||
receive: {
|
receive: {
|
||||||
show: false,
|
show: false,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
|
|
@ -97,7 +119,8 @@ new Vue({
|
||||||
bolt11: ''
|
bolt11: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
transactionsTable: {
|
payments: [],
|
||||||
|
paymentsTable: {
|
||||||
columns: [
|
columns: [
|
||||||
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'},
|
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'},
|
||||||
{name: 'date', align: 'left', label: 'Date', field: 'date', sortable: true},
|
{name: 'date', align: 'left', label: 'Date', field: 'date', sortable: true},
|
||||||
|
|
@ -107,24 +130,38 @@ new Vue({
|
||||||
rowsPerPage: 10
|
rowsPerPage: 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
transactionsChart: {
|
paymentsChart: {
|
||||||
show: false
|
show: false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
balance: function () {
|
||||||
|
if (this.payments.length) {
|
||||||
|
return _.pluck(this.payments, 'amount').reduce(function (a, b) { return a + b; }, 0) / 1000;
|
||||||
|
}
|
||||||
|
return this.w.wallet.sat;
|
||||||
|
},
|
||||||
|
fbalance: function () {
|
||||||
|
return LNbits.utils.formatSat(this.balance)
|
||||||
|
},
|
||||||
canPay: function () {
|
canPay: function () {
|
||||||
if (!this.send.invoice) return false;
|
if (!this.send.invoice) return false;
|
||||||
return this.send.invoice.sat < this.w.wallet.balance;
|
return this.send.invoice.sat < this.balance;
|
||||||
},
|
},
|
||||||
transactions: function () {
|
pendingPaymentsExist: function () {
|
||||||
var data = (this.txUpdate) ? this.txUpdate : this.w.transactions;
|
return (this.payments)
|
||||||
return data.sort(function (a, b) {
|
? _.where(this.payments, {pending: 1}).length > 0
|
||||||
return b.time - a.time;
|
: false;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
showChart: function () {
|
||||||
|
this.paymentsChart.show = true;
|
||||||
|
this.$nextTick(function () {
|
||||||
|
generateChart(this.$refs.canvas, this.payments);
|
||||||
|
});
|
||||||
|
},
|
||||||
showReceiveDialog: function () {
|
showReceiveDialog: function () {
|
||||||
this.receive = {
|
this.receive = {
|
||||||
show: true,
|
show: true,
|
||||||
|
|
@ -133,7 +170,8 @@ new Vue({
|
||||||
data: {
|
data: {
|
||||||
amount: null,
|
amount: null,
|
||||||
memo: ''
|
memo: ''
|
||||||
}
|
},
|
||||||
|
paymentChecker: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
showSendDialog: function () {
|
showSendDialog: function () {
|
||||||
|
|
@ -145,11 +183,11 @@ new Vue({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
showChart: function () {
|
closeReceiveDialog: function () {
|
||||||
this.transactionsChart.show = true;
|
var checker = this.receive.paymentChecker;
|
||||||
this.$nextTick(function () {
|
setTimeout(function () {
|
||||||
generateChart(this.$refs.canvas, this.transactions);
|
clearInterval(checker);
|
||||||
});
|
}, 10000);
|
||||||
},
|
},
|
||||||
createInvoice: function () {
|
createInvoice: function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
@ -159,15 +197,15 @@ new Vue({
|
||||||
self.receive.status = 'success';
|
self.receive.status = 'success';
|
||||||
self.receive.paymentReq = response.data.payment_request;
|
self.receive.paymentReq = response.data.payment_request;
|
||||||
|
|
||||||
var check_invoice = setInterval(function () {
|
self.receive.paymentChecker = setInterval(function () {
|
||||||
LNbits.api.getInvoice(self.w.wallet, response.data.payment_hash).then(function (response) {
|
LNbits.api.getPayment(self.w.wallet, response.data.payment_hash).then(function (response) {
|
||||||
if (response.data.paid) {
|
if (response.data.paid) {
|
||||||
self.refreshTransactions();
|
self.fetchPayments();
|
||||||
self.receive.show = false;
|
self.receive.show = false;
|
||||||
clearInterval(check_invoice);
|
clearInterval(self.receive.paymentChecker);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, 3000);
|
}, 2000);
|
||||||
|
|
||||||
}).catch(function (error) {
|
}).catch(function (error) {
|
||||||
LNbits.utils.notifyApiError(error);
|
LNbits.utils.notifyApiError(error);
|
||||||
|
|
@ -177,8 +215,14 @@ new Vue({
|
||||||
decodeInvoice: function () {
|
decodeInvoice: function () {
|
||||||
try {
|
try {
|
||||||
var invoice = decode(this.send.data.bolt11);
|
var invoice = decode(this.send.data.bolt11);
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
this.$q.notify({type: 'warning', message: err});
|
this.$q.notify({
|
||||||
|
timeout: 3000,
|
||||||
|
type: 'warning',
|
||||||
|
message: error + '.',
|
||||||
|
caption: '400 BAD REQUEST',
|
||||||
|
icon: null
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,19 +247,57 @@ new Vue({
|
||||||
this.send.invoice = Object.freeze(cleanInvoice);
|
this.send.invoice = Object.freeze(cleanInvoice);
|
||||||
},
|
},
|
||||||
payInvoice: function () {
|
payInvoice: function () {
|
||||||
alert('pay!');
|
var self = this;
|
||||||
|
|
||||||
|
dismissPaymentMsg = this.$q.notify({
|
||||||
|
timeout: 0,
|
||||||
|
message: 'Processing payment...',
|
||||||
|
icon: null
|
||||||
|
});
|
||||||
|
|
||||||
|
LNbits.api.payInvoice(this.w.wallet, this.send.data.bolt11).catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
paymentChecker = setInterval(function () {
|
||||||
|
LNbits.api.getPayment(self.w.wallet, self.send.invoice.hash).then(function (response) {
|
||||||
|
if (response.data.paid) {
|
||||||
|
this.send.show = false;
|
||||||
|
clearInterval(paymentChecker);
|
||||||
|
dismissPaymentMsg();
|
||||||
|
self.fetchPayments();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
},
|
},
|
||||||
deleteWallet: function (walletId, user) {
|
deleteWallet: function (walletId, user) {
|
||||||
LNbits.href.deleteWallet(walletId, user);
|
LNbits.href.deleteWallet(walletId, user);
|
||||||
},
|
},
|
||||||
refreshTransactions: function (notify) {
|
fetchPayments: function (checkPending) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
LNbits.api.getTransactions(this.w.wallet).then(function (response) {
|
return LNbits.api.getPayments(this.w.wallet, checkPending).then(function (response) {
|
||||||
self.txUpdate = response.data.map(function (obj) {
|
self.payments = response.data.map(function (obj) {
|
||||||
return LNbits.map.transaction(obj);
|
return LNbits.map.payment(obj);
|
||||||
|
}).sort(function (a, b) {
|
||||||
|
return b.time - a.time;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
checkPendingPayments: function () {
|
||||||
|
var dismissMsg = this.$q.notify({
|
||||||
|
timeout: 0,
|
||||||
|
message: 'Checking pending transactions...',
|
||||||
|
icon: null
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fetchPayments(true).then(function () {
|
||||||
|
dismissMsg();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
this.fetchPayments();
|
||||||
|
this.checkPendingPayments();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
|
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.13.0/moment.min.js"></script>
|
|
||||||
{{ window_vars(user, wallet) }}
|
{{ window_vars(user, wallet) }}
|
||||||
{% assets filters='rjsmin', output='__bundle__/core/chart.js',
|
{% assets filters='rjsmin', output='__bundle__/core/chart.js',
|
||||||
|
'vendor/moment@2.24.0/moment.min.js',
|
||||||
'vendor/chart.js@2.9.3/chart.min.js' %}
|
'vendor/chart.js@2.9.3/chart.min.js' %}
|
||||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||||
{% endassets %}
|
{% endassets %}
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<h4 class="q-my-none"><strong>{% raw %}{{ w.wallet.fsat }}{% endraw %}</strong> sat</h4>
|
<h3 class="q-my-none"><strong>{% raw %}{{ fbalance }}{% endraw %}</strong> sat</h3>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<div class="row q-pb-md q-px-md q-col-gutter-md">
|
<div class="row q-pb-md q-px-md q-col-gutter-md">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
|
@ -50,16 +50,19 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<q-btn flat color="grey" onclick="exportbut()">Export to CSV</q-btn>
|
<q-btn flat color="grey" onclick="exportbut()">Export to CSV</q-btn>
|
||||||
|
<!--<q-btn v-if="pendingPaymentsExist" dense flat round icon="update" color="grey" @click="checkPendingPayments">
|
||||||
|
<q-tooltip>Check pending</q-tooltip>
|
||||||
|
</q-btn>-->
|
||||||
<q-btn dense flat round icon="show_chart" color="grey" @click="showChart">
|
<q-btn dense flat round icon="show_chart" color="grey" @click="showChart">
|
||||||
<q-tooltip>Show chart</q-tooltip>
|
<q-tooltip>Show chart</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<q-table dense flat
|
<q-table dense flat
|
||||||
:data="transactions"
|
:data="payments"
|
||||||
row-key="payhash"
|
row-key="payhash"
|
||||||
:columns="transactionsTable.columns"
|
:columns="paymentsTable.columns"
|
||||||
:pagination.sync="transactionsTable.pagination">
|
:pagination.sync="paymentsTable.pagination">
|
||||||
{% raw %}
|
{% raw %}
|
||||||
<template v-slot:header="props">
|
<template v-slot:header="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
|
|
@ -71,11 +74,14 @@
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:body="props">
|
<template v-slot:body="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props" v-if="props.row.isPaid">
|
||||||
<q-td auto-width class="lnbits__q-table__icon-td">
|
<q-td auto-width class="lnbits__q-table__icon-td">
|
||||||
<q-icon size="14px"
|
<q-icon v-if="props.row.isPaid" size="14px"
|
||||||
:name="(props.row.sat < 0) ? 'call_made' : 'call_received'"
|
:name="(props.row.sat < 0) ? 'call_made' : 'call_received'"
|
||||||
:color="(props.row.sat < 0) ? 'pink' : 'green'"></q-icon>
|
:color="(props.row.sat < 0) ? 'pink' : 'green'"></q-icon>
|
||||||
|
<q-icon v-else name="settings_ethernet" color="grey">
|
||||||
|
<q-tooltip>Pending</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td key="memo" :props="props">
|
<q-td key="memo" :props="props">
|
||||||
{{ props.row.memo }}
|
{{ props.row.memo }}
|
||||||
|
|
@ -169,8 +175,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-dialog v-model="receive.show" position="top">
|
<q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog">
|
||||||
<q-card class="q-pa-md" style="width: 500px">
|
<q-card class="q-pa-lg" style="width: 500px">
|
||||||
<q-form v-if="!receive.paymentReq" class="q-gutter-md">
|
<q-form v-if="!receive.paymentReq" class="q-gutter-md">
|
||||||
<q-input filled dense
|
<q-input filled dense
|
||||||
v-model.number="receive.data.amount"
|
v-model.number="receive.data.amount"
|
||||||
|
|
@ -208,7 +214,7 @@
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
<q-dialog v-model="send.show" position="top">
|
<q-dialog v-model="send.show" position="top">
|
||||||
<q-card class="q-pa-md" style="width: 500px">
|
<q-card class="q-pa-lg" style="width: 500px">
|
||||||
<q-form v-if="!send.invoice" class="q-gutter-md">
|
<q-form v-if="!send.invoice" class="q-gutter-md">
|
||||||
<q-input filled dense
|
<q-input filled dense
|
||||||
v-model="send.data.bolt11"
|
v-model="send.data.bolt11"
|
||||||
|
|
@ -232,6 +238,7 @@
|
||||||
<div v-else>
|
<div v-else>
|
||||||
{% raw %}
|
{% raw %}
|
||||||
<h6 class="q-my-none">{{ send.invoice.fsat }} sat</h6>
|
<h6 class="q-my-none">{{ send.invoice.fsat }} sat</h6>
|
||||||
|
<q-separator class="q-my-sm"></q-separator>
|
||||||
<p style="word-break: break-all">
|
<p style="word-break: break-all">
|
||||||
<strong>Memo:</strong> {{ send.invoice.description }}<br>
|
<strong>Memo:</strong> {{ send.invoice.description }}<br>
|
||||||
<strong>Expire date:</strong> {{ send.invoice.expireDate }}<br>
|
<strong>Expire date:</strong> {{ send.invoice.expireDate }}<br>
|
||||||
|
|
@ -252,8 +259,8 @@
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
<q-dialog v-model="transactionsChart.show" position="top">
|
<q-dialog v-model="paymentsChart.show" position="top">
|
||||||
<q-card class="q-pa-md" style="width: 800px; max-width: unset">
|
<q-card class="q-pa-sm" style="width: 800px; max-width: unset">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<canvas ref="canvas" width="600" height="400"></canvas>
|
<canvas ref="canvas" width="600" height="400"></canvas>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
|
||||||
|
|
@ -79,13 +79,15 @@ def wallet():
|
||||||
@check_user_exists()
|
@check_user_exists()
|
||||||
def deletewallet():
|
def deletewallet():
|
||||||
wallet_id = request.args.get("wal", type=str)
|
wallet_id = request.args.get("wal", type=str)
|
||||||
|
user_wallet_ids = g.user.wallet_ids
|
||||||
|
|
||||||
if wallet_id not in g.user.wallet_ids:
|
if wallet_id not in user_wallet_ids:
|
||||||
abort(Status.FORBIDDEN, "Not your wallet.")
|
abort(Status.FORBIDDEN, "Not your wallet.")
|
||||||
else:
|
else:
|
||||||
delete_wallet(user_id=g.user.id, wallet_id=wallet_id)
|
delete_wallet(user_id=g.user.id, wallet_id=wallet_id)
|
||||||
|
user_wallet_ids.remove(wallet_id)
|
||||||
|
|
||||||
if g.user.wallets:
|
if user_wallet_ids:
|
||||||
return redirect(url_for("core.wallet", usr=g.user.id, wal=g.user.wallets[0].id))
|
return redirect(url_for("core.wallet", usr=g.user.id, wal=user_wallet_ids[0]))
|
||||||
|
|
||||||
return redirect(url_for("core.home"))
|
return redirect(url_for("core.home"))
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,30 @@
|
||||||
from flask import g, jsonify
|
from flask import g, jsonify, request
|
||||||
|
|
||||||
|
from lnbits import bolt11
|
||||||
from lnbits.core import core_app
|
from lnbits.core import core_app
|
||||||
from lnbits.decorators import api_check_wallet_macaroon, api_validate_post_request
|
from lnbits.decorators import api_check_wallet_macaroon, api_validate_post_request
|
||||||
from lnbits.helpers import Status
|
from lnbits.helpers import Status
|
||||||
from lnbits.settings import WALLET
|
from lnbits.settings import FEE_RESERVE, WALLET
|
||||||
|
|
||||||
from .crud import create_transaction
|
from .crud import create_payment
|
||||||
|
|
||||||
|
|
||||||
@core_app.route("/api/v1/invoices", methods=["POST"])
|
@core_app.route("/api/v1/payments", methods=["GET"])
|
||||||
@api_validate_post_request(required_params=["amount", "memo"])
|
|
||||||
@api_check_wallet_macaroon(key_type="invoice")
|
@api_check_wallet_macaroon(key_type="invoice")
|
||||||
def api_invoices():
|
def api_payments():
|
||||||
|
if "check_pending" in request.args:
|
||||||
|
for payment in g.wallet.get_payments(include_all_pending=True):
|
||||||
|
if payment.is_out:
|
||||||
|
payment.set_pending(WALLET.get_payment_status(payment.payhash).pending)
|
||||||
|
elif payment.is_in:
|
||||||
|
payment.set_pending(WALLET.get_invoice_status(payment.payhash).pending)
|
||||||
|
|
||||||
|
return jsonify(g.wallet.get_payments()), Status.OK
|
||||||
|
|
||||||
|
|
||||||
|
@api_check_wallet_macaroon(key_type="invoice")
|
||||||
|
@api_validate_post_request(required_params=["amount", "memo"])
|
||||||
|
def api_payments_create_invoice():
|
||||||
if not isinstance(g.data["amount"], int) or g.data["amount"] < 1:
|
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
|
return jsonify({"message": "`amount` needs to be a positive integer."}), Status.BAD_REQUEST
|
||||||
|
|
||||||
|
|
@ -25,38 +38,77 @@ def api_invoices():
|
||||||
server_error = True
|
server_error = True
|
||||||
|
|
||||||
if server_error:
|
if server_error:
|
||||||
return jsonify({"message": "Unexpected backend error. Try again later."}), 500
|
return jsonify({"message": "Unexpected backend error. Try again later."}), Status.INTERNAL_SERVER_ERROR
|
||||||
|
|
||||||
amount_msat = g.data["amount"] * 1000
|
amount_msat = g.data["amount"] * 1000
|
||||||
create_transaction(wallet_id=g.wallet.id, payhash=payhash, amount=amount_msat, memo=g.data["memo"])
|
create_payment(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
|
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")
|
@api_check_wallet_macaroon(key_type="invoice")
|
||||||
def api_transaction(payhash, incoming):
|
@api_validate_post_request(required_params=["bolt11"])
|
||||||
tx = g.wallet.get_transaction(payhash)
|
def api_payments_pay_invoice():
|
||||||
|
if not isinstance(g.data["bolt11"], str) or not g.data["bolt11"].strip():
|
||||||
|
return jsonify({"message": "`bolt11` needs to be a valid string."}), Status.BAD_REQUEST
|
||||||
|
|
||||||
if not tx:
|
try:
|
||||||
return jsonify({"message": "Transaction does not exist."}), Status.NOT_FOUND
|
invoice = bolt11.decode(g.data["bolt11"])
|
||||||
elif not tx.pending:
|
|
||||||
|
if invoice.amount_msat == 0:
|
||||||
|
return jsonify({"message": "Amountless invoices not supported."}), Status.BAD_REQUEST
|
||||||
|
|
||||||
|
if invoice.amount_msat > g.wallet.balance_msat:
|
||||||
|
return jsonify({"message": "Insufficient balance."}), Status.FORBIDDEN
|
||||||
|
|
||||||
|
create_payment(
|
||||||
|
wallet_id=g.wallet.id,
|
||||||
|
payhash=invoice.payment_hash,
|
||||||
|
amount=-invoice.amount_msat,
|
||||||
|
memo=invoice.description,
|
||||||
|
fee=-invoice.amount_msat * FEE_RESERVE,
|
||||||
|
)
|
||||||
|
|
||||||
|
r, server_error, fee_msat, error_message = WALLET.pay_invoice(g.data["bolt11"])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
server_error = True
|
||||||
|
error_message = str(e)
|
||||||
|
|
||||||
|
if server_error:
|
||||||
|
return jsonify({"message": error_message}), Status.INTERNAL_SERVER_ERROR
|
||||||
|
|
||||||
|
return jsonify({"payment_hash": invoice.payment_hash}), Status.CREATED
|
||||||
|
|
||||||
|
|
||||||
|
@core_app.route("/api/v1/payments", methods=["POST"])
|
||||||
|
@api_validate_post_request(required_params=["out"])
|
||||||
|
def api_payments_create():
|
||||||
|
if g.data["out"] is True:
|
||||||
|
return api_payments_pay_invoice()
|
||||||
|
return api_payments_create_invoice()
|
||||||
|
|
||||||
|
|
||||||
|
@core_app.route("/api/v1/payments/<payhash>", methods=["GET"])
|
||||||
|
@api_check_wallet_macaroon(key_type="invoice")
|
||||||
|
def api_payment(payhash):
|
||||||
|
payment = g.wallet.get_payment(payhash)
|
||||||
|
|
||||||
|
if not payment:
|
||||||
|
return jsonify({"message": "Payment does not exist."}), Status.NOT_FOUND
|
||||||
|
elif not payment.pending:
|
||||||
return jsonify({"paid": True}), Status.OK
|
return jsonify({"paid": True}), Status.OK
|
||||||
|
|
||||||
try:
|
try:
|
||||||
is_settled = WALLET.get_invoice_status(payhash).settled
|
if payment.is_out:
|
||||||
|
is_paid = WALLET.get_payment_status(payhash).paid
|
||||||
|
elif payment.is_in:
|
||||||
|
is_paid = WALLET.get_invoice_status(payhash).paid
|
||||||
except Exception:
|
except Exception:
|
||||||
return jsonify({"paid": False}), Status.OK
|
return jsonify({"paid": False}), Status.OK
|
||||||
|
|
||||||
if is_settled is True:
|
if is_paid is True:
|
||||||
tx.set_pending(False)
|
payment.set_pending(False)
|
||||||
return jsonify({"paid": True}), Status.OK
|
return jsonify({"paid": True}), Status.OK
|
||||||
|
|
||||||
return jsonify({"paid": False}), 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
|
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,9 @@ class Status:
|
||||||
PAYMENT_REQUIRED = 402
|
PAYMENT_REQUIRED = 402
|
||||||
FORBIDDEN = 403
|
FORBIDDEN = 403
|
||||||
NOT_FOUND = 404
|
NOT_FOUND = 404
|
||||||
TOO_MANY_REQUESTS = 429
|
|
||||||
METHOD_NOT_ALLOWED = 405
|
METHOD_NOT_ALLOWED = 405
|
||||||
|
TOO_MANY_REQUESTS = 429
|
||||||
|
INTERNAL_SERVER_ERROR = 500
|
||||||
|
|
||||||
|
|
||||||
class MegaEncoder(json.JSONEncoder):
|
class MegaEncoder(json.JSONEncoder):
|
||||||
|
|
|
||||||
|
|
@ -13,22 +13,27 @@ var LNbits = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
createInvoice: function (wallet, amount, memo) {
|
createInvoice: function (wallet, amount, memo) {
|
||||||
return this.request('post', '/api/v1/invoices', wallet.inkey, {
|
return this.request('post', '/api/v1/payments', wallet.inkey, {
|
||||||
|
out: false,
|
||||||
amount: amount,
|
amount: amount,
|
||||||
memo: memo
|
memo: memo
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getInvoice: function (wallet, payhash) {
|
payInvoice: function (wallet, bolt11) {
|
||||||
return this.request('get', '/api/v1/invoices/' + payhash, wallet.inkey);
|
return this.request('post', '/api/v1/payments', wallet.inkey, {
|
||||||
|
out: true,
|
||||||
|
bolt11: bolt11
|
||||||
|
});
|
||||||
},
|
},
|
||||||
getTransactions: function (wallet) {
|
getPayments: function (wallet, checkPending) {
|
||||||
return this.request('get', '/api/v1/transactions', wallet.inkey);
|
var query_param = (checkPending) ? '?check_pending' : '';
|
||||||
|
return this.request('get', ['/api/v1/payments', query_param].join(''), wallet.inkey);
|
||||||
|
},
|
||||||
|
getPayment: function (wallet, payhash) {
|
||||||
|
return this.request('get', '/api/v1/payments/' + payhash, wallet.inkey);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
href: {
|
href: {
|
||||||
openWallet: function (wallet) {
|
|
||||||
window.location.href = '/wallet?usr=' + wallet.user + '&wal=' + wallet.id;
|
|
||||||
},
|
|
||||||
createWallet: function (walletName, userId) {
|
createWallet: function (walletName, userId) {
|
||||||
window.location.href = '/wallet?' + (userId ? 'usr=' + userId + '&' : '') + 'nme=' + walletName;
|
window.location.href = '/wallet?' + (userId ? 'usr=' + userId + '&' : '') + 'nme=' + walletName;
|
||||||
},
|
},
|
||||||
|
|
@ -42,14 +47,6 @@ var LNbits = {
|
||||||
obj.url = ['/', obj.code, '/'].join('');
|
obj.url = ['/', obj.code, '/'].join('');
|
||||||
return obj;
|
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) {
|
user: function (data) {
|
||||||
var obj = _.object(['id', 'email', 'extensions', 'wallets'], data);
|
var obj = _.object(['id', 'email', 'extensions', 'wallets'], data);
|
||||||
var mapWallet = this.wallet;
|
var mapWallet = this.wallet;
|
||||||
|
|
@ -62,10 +59,22 @@ var LNbits = {
|
||||||
},
|
},
|
||||||
wallet: function (data) {
|
wallet: function (data) {
|
||||||
var obj = _.object(['id', 'name', 'user', 'adminkey', 'inkey', 'balance'], data);
|
var obj = _.object(['id', 'name', 'user', 'adminkey', 'inkey', 'balance'], data);
|
||||||
obj.sat = Math.round(obj.balance);
|
obj.msat = obj.balance;
|
||||||
|
obj.sat = Math.round(obj.balance / 1000);
|
||||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat);
|
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat);
|
||||||
obj.url = ['/wallet?usr=', obj.user, '&wal=', obj.id].join('');
|
obj.url = ['/wallet?usr=', obj.user, '&wal=', obj.id].join('');
|
||||||
return obj;
|
return obj;
|
||||||
|
},
|
||||||
|
payment: 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);
|
||||||
|
obj.isIn = obj.amount > 0;
|
||||||
|
obj.isOut = obj.amount < 0;
|
||||||
|
obj.isPaid = obj.pending == 0;
|
||||||
|
return obj;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
utils: {
|
utils: {
|
||||||
|
|
@ -79,11 +88,10 @@ var LNbits = {
|
||||||
500: 'negative'
|
500: 'negative'
|
||||||
}
|
}
|
||||||
Quasar.plugins.Notify.create({
|
Quasar.plugins.Notify.create({
|
||||||
progress: true,
|
|
||||||
timeout: 3000,
|
timeout: 3000,
|
||||||
type: types[error.response.status] || 'warning',
|
type: types[error.response.status] || 'warning',
|
||||||
message: error.response.data.message || null,
|
message: error.response.data.message || null,
|
||||||
caption: [error.response.status, ' ', error.response.statusText].join('') || null,
|
caption: [error.response.status, ' ', error.response.statusText].join('').toUpperCase() || null,
|
||||||
icon: null
|
icon: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -98,7 +106,7 @@ var windowMixin = {
|
||||||
extensions: [],
|
extensions: [],
|
||||||
user: null,
|
user: null,
|
||||||
wallet: null,
|
wallet: null,
|
||||||
transactions: [],
|
payments: [],
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
@ -122,11 +130,6 @@ var windowMixin = {
|
||||||
if (window.wallet) {
|
if (window.wallet) {
|
||||||
this.w.wallet = Object.freeze(LNbits.map.wallet(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) {
|
if (window.extensions) {
|
||||||
var user = this.w.user;
|
var user = this.w.user;
|
||||||
this.w.extensions = Object.freeze(window.extensions.map(function (data) {
|
this.w.extensions = Object.freeze(window.extensions.map(function (data) {
|
||||||
|
|
|
||||||
1
lnbits/static/vendor/moment@2.24.0/moment.min.js
vendored
Normal file
1
lnbits/static/vendor/moment@2.24.0/moment.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -6,7 +6,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if wallet %}
|
{% if wallet %}
|
||||||
window.wallet = {{ wallet | tojson | safe }};
|
window.wallet = {{ wallet | tojson | safe }};
|
||||||
window.transactions = {{ wallet.get_transactions() | tojson | safe }};
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,16 @@ class PaymentResponse(NamedTuple):
|
||||||
raw_response: Response
|
raw_response: Response
|
||||||
failed: bool = False
|
failed: bool = False
|
||||||
fee_msat: int = 0
|
fee_msat: int = 0
|
||||||
|
error_message: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class TxStatus(NamedTuple):
|
class PaymentStatus(NamedTuple):
|
||||||
raw_response: Response
|
raw_response: Response
|
||||||
settled: Optional[bool] = None
|
paid: Optional[bool] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pending(self) -> bool:
|
||||||
|
return self.paid is not True
|
||||||
|
|
||||||
|
|
||||||
class Wallet(ABC):
|
class Wallet(ABC):
|
||||||
|
|
@ -26,13 +31,13 @@ class Wallet(ABC):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def pay_invoice(self, bolt11: str) -> Response:
|
def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_invoice_status(self, payment_hash: str) -> TxStatus:
|
def get_invoice_status(self, payment_hash: str) -> PaymentStatus:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_payment_status(self, payment_hash: str) -> TxStatus:
|
def get_payment_status(self, payment_hash: str) -> PaymentStatus:
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from requests import get, post
|
from requests import get, post
|
||||||
from .base import InvoiceResponse, PaymentResponse, TxStatus, Wallet
|
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
|
||||||
|
|
||||||
|
|
||||||
class LndWallet(Wallet):
|
class LndWallet(Wallet):
|
||||||
|
|
@ -41,15 +41,15 @@ class LndWallet(Wallet):
|
||||||
)
|
)
|
||||||
return PaymentResponse(r, not r.ok)
|
return PaymentResponse(r, not r.ok)
|
||||||
|
|
||||||
def get_invoice_status(self, payment_hash: str) -> TxStatus:
|
def get_invoice_status(self, payment_hash: str) -> PaymentStatus:
|
||||||
r = get(url=f"{self.endpoint}/v1/invoice/{payment_hash}", headers=self.auth_read, verify=False)
|
r = get(url=f"{self.endpoint}/v1/invoice/{payment_hash}", headers=self.auth_read, verify=False)
|
||||||
|
|
||||||
if not r.ok or "settled" not in r.json():
|
if not r.ok or "settled" not in r.json():
|
||||||
return TxStatus(r, None)
|
return PaymentStatus(r, None)
|
||||||
|
|
||||||
return TxStatus(r, r.json()["settled"])
|
return PaymentStatus(r, r.json()["settled"])
|
||||||
|
|
||||||
def get_payment_status(self, payment_hash: str) -> TxStatus:
|
def get_payment_status(self, payment_hash: str) -> PaymentStatus:
|
||||||
r = get(
|
r = get(
|
||||||
url=f"{self.endpoint}/v1/payments",
|
url=f"{self.endpoint}/v1/payments",
|
||||||
headers=self.auth_admin,
|
headers=self.auth_admin,
|
||||||
|
|
@ -58,11 +58,11 @@ class LndWallet(Wallet):
|
||||||
)
|
)
|
||||||
|
|
||||||
if not r.ok:
|
if not r.ok:
|
||||||
return TxStatus(r, None)
|
return PaymentStatus(r, None)
|
||||||
|
|
||||||
payments = [p for p in r.json()["payments"] if p["payment_hash"] == payment_hash]
|
payments = [p for p in r.json()["payments"] if p["payment_hash"] == payment_hash]
|
||||||
payment = payments[0] if payments else None
|
payment = payments[0] if payments else None
|
||||||
|
|
||||||
# check payment.status: https://api.lightning.community/rest/index.html?python#peersynctype
|
# check payment.status: https://api.lightning.community/rest/index.html?python#peersynctype
|
||||||
statuses = {"UNKNOWN": None, "IN_FLIGHT": None, "SUCCEEDED": True, "FAILED": False}
|
statuses = {"UNKNOWN": None, "IN_FLIGHT": None, "SUCCEEDED": True, "FAILED": False}
|
||||||
return TxStatus(r, statuses[payment["status"]] if payment else None)
|
return PaymentStatus(r, statuses[payment["status"]] if payment else None)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from requests import get, post
|
from requests import get, post
|
||||||
|
|
||||||
from .base import InvoiceResponse, PaymentResponse, TxStatus, Wallet
|
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
|
||||||
|
|
||||||
|
|
||||||
class LNPayWallet(Wallet):
|
class LNPayWallet(Wallet):
|
||||||
|
|
@ -37,20 +37,14 @@ class LNPayWallet(Wallet):
|
||||||
|
|
||||||
return PaymentResponse(r, not r.ok)
|
return PaymentResponse(r, not r.ok)
|
||||||
|
|
||||||
def get_invoice_status(self, payment_hash: str) -> TxStatus:
|
def get_invoice_status(self, payment_hash: str) -> PaymentStatus:
|
||||||
|
return self.get_payment_status(payment_hash)
|
||||||
|
|
||||||
|
def get_payment_status(self, payment_hash: str) -> PaymentStatus:
|
||||||
r = get(url=f"{self.endpoint}/user/lntx/{payment_hash}", headers=self.auth_api)
|
r = get(url=f"{self.endpoint}/user/lntx/{payment_hash}", headers=self.auth_api)
|
||||||
|
|
||||||
if not r.ok:
|
if not r.ok:
|
||||||
return TxStatus(r, None)
|
return PaymentStatus(r, None)
|
||||||
|
|
||||||
statuses = {0: None, 1: True, -1: False}
|
statuses = {0: None, 1: True, -1: False}
|
||||||
return TxStatus(r, statuses[r.json()["settled"]])
|
return PaymentStatus(r, statuses[r.json()["settled"]])
|
||||||
|
|
||||||
def get_payment_status(self, payment_hash: str) -> TxStatus:
|
|
||||||
r = get(url=f"{self.endpoint}/user/lntx/{payment_hash}", headers=self.auth_api)
|
|
||||||
|
|
||||||
if not r.ok:
|
|
||||||
return TxStatus(r, None)
|
|
||||||
|
|
||||||
statuses = {0: None, 1: True, -1: False}
|
|
||||||
return TxStatus(r, statuses[r.json()["settled"]])
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from requests import post
|
from requests import post
|
||||||
|
|
||||||
from .base import InvoiceResponse, PaymentResponse, TxStatus, Wallet
|
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
|
||||||
|
|
||||||
|
|
||||||
class LntxbotWallet(Wallet):
|
class LntxbotWallet(Wallet):
|
||||||
|
|
@ -23,38 +23,39 @@ class LntxbotWallet(Wallet):
|
||||||
|
|
||||||
def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
||||||
r = post(url=f"{self.endpoint}/payinvoice", headers=self.auth_admin, json={"invoice": bolt11})
|
r = post(url=f"{self.endpoint}/payinvoice", headers=self.auth_admin, json={"invoice": bolt11})
|
||||||
failed, fee_msat = not r.ok, 0
|
failed, fee_msat, error_message = not r.ok, 0, None
|
||||||
|
|
||||||
if r.ok:
|
if r.ok:
|
||||||
data = r.json()
|
data = r.json()
|
||||||
if "error" in data and data["error"]:
|
if "error" in data and data["error"]:
|
||||||
failed = True
|
failed = True
|
||||||
|
error_message = data["message"]
|
||||||
elif "fee_msat" in data:
|
elif "fee_msat" in data:
|
||||||
fee_msat = data["fee_msat"]
|
fee_msat = data["fee_msat"]
|
||||||
|
|
||||||
return PaymentResponse(r, failed, fee_msat)
|
return PaymentResponse(r, failed, fee_msat, error_message)
|
||||||
|
|
||||||
def get_invoice_status(self, payment_hash: str) -> TxStatus:
|
def get_invoice_status(self, payment_hash: str) -> PaymentStatus:
|
||||||
r = post(url=f"{self.endpoint}/invoicestatus/{payment_hash}?wait=false", headers=self.auth_invoice)
|
r = post(url=f"{self.endpoint}/invoicestatus/{payment_hash}?wait=false", headers=self.auth_invoice)
|
||||||
|
|
||||||
if not r.ok:
|
if not r.ok:
|
||||||
return TxStatus(r, None)
|
return PaymentStatus(r, None)
|
||||||
|
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
if "error" in data:
|
if "error" in data:
|
||||||
return TxStatus(r, None)
|
return PaymentStatus(r, None)
|
||||||
|
|
||||||
if "preimage" not in data or not data["preimage"]:
|
if "preimage" not in data or not data["preimage"]:
|
||||||
return TxStatus(r, False)
|
return PaymentStatus(r, False)
|
||||||
|
|
||||||
return TxStatus(r, True)
|
return PaymentStatus(r, True)
|
||||||
|
|
||||||
def get_payment_status(self, payment_hash: str) -> TxStatus:
|
def get_payment_status(self, payment_hash: str) -> PaymentStatus:
|
||||||
r = post(url=f"{self.endpoint}/paymentstatus/{payment_hash}", headers=self.auth_invoice)
|
r = post(url=f"{self.endpoint}/paymentstatus/{payment_hash}", headers=self.auth_invoice)
|
||||||
|
|
||||||
if not r.ok or "error" in r.json():
|
if not r.ok or "error" in r.json():
|
||||||
return TxStatus(r, None)
|
return PaymentStatus(r, None)
|
||||||
|
|
||||||
statuses = {"complete": True, "failed": False, "unknown": None}
|
statuses = {"complete": True, "failed": False, "pending": None, "unknown": None}
|
||||||
return TxStatus(r, statuses[r.json().get("status", "unknown")])
|
return PaymentStatus(r, statuses[r.json().get("status", "unknown")])
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from requests import get, post
|
from requests import get, post
|
||||||
|
|
||||||
from .base import InvoiceResponse, PaymentResponse, TxStatus, Wallet
|
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
|
||||||
|
|
||||||
|
|
||||||
class OpenNodeWallet(Wallet):
|
class OpenNodeWallet(Wallet):
|
||||||
|
|
@ -28,20 +28,20 @@ class OpenNodeWallet(Wallet):
|
||||||
r = post(url=f"{self.endpoint}/v2/withdrawals", headers=self.auth_admin, json={"type": "ln", "address": bolt11})
|
r = post(url=f"{self.endpoint}/v2/withdrawals", headers=self.auth_admin, json={"type": "ln", "address": bolt11})
|
||||||
return PaymentResponse(r, not r.ok)
|
return PaymentResponse(r, not r.ok)
|
||||||
|
|
||||||
def get_invoice_status(self, payment_hash: str) -> TxStatus:
|
def get_invoice_status(self, payment_hash: str) -> PaymentStatus:
|
||||||
r = get(url=f"{self.endpoint}/v1/charge/{payment_hash}", headers=self.auth_invoice)
|
r = get(url=f"{self.endpoint}/v1/charge/{payment_hash}", headers=self.auth_invoice)
|
||||||
|
|
||||||
if not r.ok:
|
if not r.ok:
|
||||||
return TxStatus(r, None)
|
return PaymentStatus(r, None)
|
||||||
|
|
||||||
statuses = {"processing": None, "paid": True, "unpaid": False}
|
statuses = {"processing": None, "paid": True, "unpaid": False}
|
||||||
return TxStatus(r, statuses[r.json()["data"]["status"]])
|
return PaymentStatus(r, statuses[r.json()["data"]["status"]])
|
||||||
|
|
||||||
def get_payment_status(self, payment_hash: str) -> TxStatus:
|
def get_payment_status(self, payment_hash: str) -> PaymentStatus:
|
||||||
r = get(url=f"{self.endpoint}/v1/withdrawal/{payment_hash}", headers=self.auth_admin)
|
r = get(url=f"{self.endpoint}/v1/withdrawal/{payment_hash}", headers=self.auth_admin)
|
||||||
|
|
||||||
if not r.ok:
|
if not r.ok:
|
||||||
return TxStatus(r, None)
|
return PaymentStatus(r, None)
|
||||||
|
|
||||||
statuses = {"pending": None, "confirmed": True, "error": False, "failed": False}
|
statuses = {"pending": None, "confirmed": True, "error": False, "failed": False}
|
||||||
return TxStatus(r, statuses[r.json()["data"]["status"]])
|
return PaymentStatus(r, statuses[r.json()["data"]["status"]])
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue