diff --git a/lnbits/__init__.py b/lnbits/__init__.py
index 2f59c57..d3179cc 100644
--- a/lnbits/__init__.py
+++ b/lnbits/__init__.py
@@ -3,18 +3,16 @@ import json
import requests
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_compress import Compress
from flask_talisman import Talisman
from lnurl import Lnurl, LnurlWithdrawResponse
-from . import bolt11
from .core import core_app
-from .decorators import api_validate_post_request
from .db import init_databases, open_db
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__)
@@ -147,93 +145,5 @@ def lnurlwallet():
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__':
app.run()
diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py
index 6fd7ba1..51431c8 100644
--- a/lnbits/core/crud.py
+++ b/lnbits/core/crud.py
@@ -4,7 +4,7 @@ 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
+from .models import User, Wallet, Payment
# 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,))
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
WHERE user = ?
""",
@@ -96,7 +96,7 @@ 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
+ SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) * ? AS balance_msat
FROM wallets
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"
row = db.fetchone(
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
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
-# 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:
row = db.fetchone(
"""
@@ -136,45 +136,51 @@ def get_wallet_transaction(wallet_id: str, payhash: str) -> Optional[Transaction
(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:
+ if include_all_pending:
+ clause = "pending = 1"
+ else:
+ clause = "((amount > 0 AND pending = 0) OR amount < 0)"
+
rows = db.fetchall(
- """
+ f"""
SELECT payhash, amount, fee, pending, memo, time
FROM apipayments
- WHERE wallet = ? AND pending = ?
+ WHERE wallet = ? AND {clause}
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:
db.execute(
"""
- INSERT INTO apipayments (wallet, payhash, amount, pending, memo)
- VALUES (?, ?, ?, ?, ?)
+ INSERT INTO apipayments (wallet, payhash, amount, pending, memo, fee)
+ 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:
db.execute("UPDATE apipayments SET pending = ? WHERE payhash = ?", (int(pending), payhash,))
-def check_pending_transactions(wallet_id: str) -> None:
- pass
+def delete_payment(payhash: str) -> None:
+ with open_db() as db:
+ db.execute("DELETE FROM apipayments WHERE payhash = ?", (payhash,))
diff --git a/lnbits/core/models.py b/lnbits/core/models.py
index e1f112d..120ecc3 100644
--- a/lnbits/core/models.py
+++ b/lnbits/core/models.py
@@ -1,4 +1,3 @@
-from decimal import Decimal
from typing import List, NamedTuple, Optional
@@ -24,20 +23,24 @@ class Wallet(NamedTuple):
user: str
adminkey: str
inkey: str
- balance: Decimal
+ balance_msat: int
- def get_transaction(self, payhash: str) -> "Transaction":
- from .crud import get_wallet_transaction
+ @property
+ def balance(self) -> int:
+ return int(self.balance / 1000)
+
+ def get_payment(self, payhash: str) -> "Payment":
+ from .crud import get_wallet_payment
- return get_wallet_transaction(self.id, payhash)
+ return get_wallet_payment(self.id, payhash)
- def get_transactions(self) -> List["Transaction"]:
- from .crud import get_wallet_transactions
+ def get_payments(self, *, include_all_pending: bool = False) -> List["Payment"]:
+ from .crud import get_wallet_payments
- return get_wallet_transactions(self.id)
+ return get_wallet_payments(self.id, include_all_pending=include_all_pending)
-class Transaction(NamedTuple):
+class Payment(NamedTuple):
payhash: str
pending: bool
amount: int
@@ -54,10 +57,19 @@ class Transaction(NamedTuple):
return self.amount / 1000
@property
- def tx_type(self) -> str:
- return "payment" if self.amount < 0 else "invoice"
+ def is_in(self) -> bool:
+ return self.amount > 0
+
+ @property
+ def is_out(self) -> bool:
+ return self.amount < 0
def set_pending(self, pending: bool) -> None:
- from .crud import update_transaction_status
+ from .crud import update_payment_status
+
+ update_payment_status(self.payhash, pending)
+
+ def delete(self) -> None:
+ from .crud import delete_payment
- update_transaction_status(self.payhash, pending)
+ delete_payment(self.payhash)
diff --git a/lnbits/core/static/js/wallet.js b/lnbits/core/static/js/wallet.js
index 8cbbdf5..0bd86bf 100644
--- a/lnbits/core/static/js/wallet.js
+++ b/lnbits/core/static/js/wallet.js
@@ -1,29 +1,36 @@
Vue.component(VueQrcode.name, VueQrcode);
-function generateChart(canvas, transactions) {
+function generateChart(canvas, payments) {
var txs = [];
var n = 0;
var data = {
labels: [],
- sats: [],
+ income: [],
+ outcome: [],
cumulative: []
};
- _.each(transactions.sort(function (a, b) {
+ _.each(payments.slice(0).sort(function (a, b) {
return a.time - b.time;
}), function (tx) {
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,
});
});
- _.each(_.groupBy(txs, 'day'), function (value, day) {
- var sat = _.reduce(value, function(memo, tx) { return memo + tx.sat; }, 0);
- n = n + sat;
+ _.each(_.groupBy(txs, 'hour'), function (value, day) {
+ var income = _.reduce(value, function(memo, tx) {
+ 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.sats.push(sat);
+ data.income.push(income);
+ data.outcome.push(outcome);
data.cumulative.push(n);
});
@@ -36,19 +43,25 @@ function generateChart(canvas, transactions) {
data: data.cumulative,
type: 'line',
label: 'balance',
- borderColor: '#673ab7', // deep-purple
+ backgroundColor: '#673ab7', // deep-purple
+ borderColor: '#673ab7',
borderWidth: 4,
pointRadius: 3,
fill: false
},
{
- data: data.sats,
+ data: data.income,
type: 'bar',
- label: 'tx',
- backgroundColor: function (ctx) {
- var value = ctx.dataset.data[ctx.dataIndex];
- return (value < 0) ? '#e91e63' : '#4caf50'; // pink : green
- }
+ label: 'in',
+ barPercentage: 0.75,
+ backgroundColor: window.Color('rgb(76,175,80)').alpha(0.5).rgbString() // 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: [{
type: 'time',
display: true,
+ offset: true,
time: {
minUnit: 'hour',
stepSize: 3
}
}],
},
+ // performance tweaks
+ animation: {
+ duration: 0
+ },
+ elements: {
+ line: {
+ tension: 0
+ }
+ }
}
});
}
@@ -80,7 +103,6 @@ new Vue({
mixins: [windowMixin],
data: function () {
return {
- txUpdate: null,
receive: {
show: false,
status: 'pending',
@@ -97,7 +119,8 @@ new Vue({
bolt11: ''
}
},
- transactionsTable: {
+ payments: [],
+ paymentsTable: {
columns: [
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'},
{name: 'date', align: 'left', label: 'Date', field: 'date', sortable: true},
@@ -107,24 +130,38 @@ new Vue({
rowsPerPage: 10
}
},
- transactionsChart: {
+ paymentsChart: {
show: false
}
};
},
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 () {
if (!this.send.invoice) return false;
- return this.send.invoice.sat < this.w.wallet.balance;
+ return this.send.invoice.sat < this.balance;
},
- transactions: function () {
- var data = (this.txUpdate) ? this.txUpdate : this.w.transactions;
- return data.sort(function (a, b) {
- return b.time - a.time;
- });
+ pendingPaymentsExist: function () {
+ return (this.payments)
+ ? _.where(this.payments, {pending: 1}).length > 0
+ : false;
}
},
methods: {
+ showChart: function () {
+ this.paymentsChart.show = true;
+ this.$nextTick(function () {
+ generateChart(this.$refs.canvas, this.payments);
+ });
+ },
showReceiveDialog: function () {
this.receive = {
show: true,
@@ -133,7 +170,8 @@ new Vue({
data: {
amount: null,
memo: ''
- }
+ },
+ paymentChecker: null
};
},
showSendDialog: function () {
@@ -145,11 +183,11 @@ new Vue({
}
};
},
- showChart: function () {
- this.transactionsChart.show = true;
- this.$nextTick(function () {
- generateChart(this.$refs.canvas, this.transactions);
- });
+ closeReceiveDialog: function () {
+ var checker = this.receive.paymentChecker;
+ setTimeout(function () {
+ clearInterval(checker);
+ }, 10000);
},
createInvoice: function () {
var self = this;
@@ -159,15 +197,15 @@ new Vue({
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) {
+ self.receive.paymentChecker = setInterval(function () {
+ LNbits.api.getPayment(self.w.wallet, response.data.payment_hash).then(function (response) {
if (response.data.paid) {
- self.refreshTransactions();
+ self.fetchPayments();
self.receive.show = false;
- clearInterval(check_invoice);
+ clearInterval(self.receive.paymentChecker);
}
});
- }, 3000);
+ }, 2000);
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
@@ -177,8 +215,14 @@ new Vue({
decodeInvoice: function () {
try {
var invoice = decode(this.send.data.bolt11);
- } catch (err) {
- this.$q.notify({type: 'warning', message: err});
+ } catch (error) {
+ this.$q.notify({
+ timeout: 3000,
+ type: 'warning',
+ message: error + '.',
+ caption: '400 BAD REQUEST',
+ icon: null
+ });
return;
}
@@ -203,19 +247,57 @@ new Vue({
this.send.invoice = Object.freeze(cleanInvoice);
},
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) {
LNbits.href.deleteWallet(walletId, user);
},
- refreshTransactions: function (notify) {
+ fetchPayments: function (checkPending) {
var self = this;
- LNbits.api.getTransactions(this.w.wallet).then(function (response) {
- self.txUpdate = response.data.map(function (obj) {
- return LNbits.map.transaction(obj);
+ return LNbits.api.getPayments(this.w.wallet, checkPending).then(function (response) {
+ self.payments = response.data.map(function (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();
}
});
diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html
index 0565279..f729e3e 100644
--- a/lnbits/core/templates/core/wallet.html
+++ b/lnbits/core/templates/core/wallet.html
@@ -4,9 +4,9 @@
{% block scripts %}
-
{{ window_vars(user, wallet) }}
{% 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' %}
{% endassets %}
@@ -24,7 +24,7 @@
- {% raw %}{{ w.wallet.fsat }}{% endraw %} sat
+ {% raw %}{{ fbalance }}{% endraw %} sat
@@ -50,16 +50,19 @@
Export to CSV
+
Show chart
+ :columns="paymentsTable.columns"
+ :pagination.sync="paymentsTable.pagination">
{% raw %}
@@ -71,11 +74,14 @@
-
+
-
+
+ Pending
+
{{ props.row.memo }}
@@ -169,8 +175,8 @@
-
-
+
+
-
+
{% raw %}
{{ send.invoice.fsat }} sat
+
Memo: {{ send.invoice.description }}
Expire date: {{ send.invoice.expireDate }}
@@ -252,8 +259,8 @@
-
-
+
+
diff --git a/lnbits/core/views.py b/lnbits/core/views.py
index 428cda3..a742772 100644
--- a/lnbits/core/views.py
+++ b/lnbits/core/views.py
@@ -79,13 +79,15 @@ def wallet():
@check_user_exists()
def deletewallet():
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.")
else:
delete_wallet(user_id=g.user.id, wallet_id=wallet_id)
+ user_wallet_ids.remove(wallet_id)
- if g.user.wallets:
- return redirect(url_for("core.wallet", usr=g.user.id, wal=g.user.wallets[0].id))
+ if user_wallet_ids:
+ return redirect(url_for("core.wallet", usr=g.user.id, wal=user_wallet_ids[0]))
return redirect(url_for("core.home"))
diff --git a/lnbits/core/views_api.py b/lnbits/core/views_api.py
index 025cfcb..eb0e044 100644
--- a/lnbits/core/views_api.py
+++ b/lnbits/core/views_api.py
@@ -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.decorators import api_check_wallet_macaroon, api_validate_post_request
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"])
-@api_validate_post_request(required_params=["amount", "memo"])
+@core_app.route("/api/v1/payments", methods=["GET"])
@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:
return jsonify({"message": "`amount` needs to be a positive integer."}), Status.BAD_REQUEST
@@ -25,38 +38,77 @@ def api_invoices():
server_error = True
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
- 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
-@core_app.route("/api/v1/invoices/", defaults={"incoming": True}, methods=["GET"])
-@core_app.route("/api/v1/payments/", defaults={"incoming": False}, methods=["GET"])
@api_check_wallet_macaroon(key_type="invoice")
-def api_transaction(payhash, incoming):
- tx = g.wallet.get_transaction(payhash)
+@api_validate_post_request(required_params=["bolt11"])
+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:
- return jsonify({"message": "Transaction does not exist."}), Status.NOT_FOUND
- elif not tx.pending:
+ try:
+ invoice = bolt11.decode(g.data["bolt11"])
+
+ 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/", 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
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:
return jsonify({"paid": False}), Status.OK
- if is_settled is True:
- tx.set_pending(False)
+ if is_paid is True:
+ payment.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
diff --git a/lnbits/helpers.py b/lnbits/helpers.py
index 329594f..9a7775f 100644
--- a/lnbits/helpers.py
+++ b/lnbits/helpers.py
@@ -47,8 +47,9 @@ class Status:
PAYMENT_REQUIRED = 402
FORBIDDEN = 403
NOT_FOUND = 404
- TOO_MANY_REQUESTS = 429
METHOD_NOT_ALLOWED = 405
+ TOO_MANY_REQUESTS = 429
+ INTERNAL_SERVER_ERROR = 500
class MegaEncoder(json.JSONEncoder):
diff --git a/lnbits/static/js/base.js b/lnbits/static/js/base.js
index 5ed0f39..b9e1351 100644
--- a/lnbits/static/js/base.js
+++ b/lnbits/static/js/base.js
@@ -13,22 +13,27 @@ var LNbits = {
});
},
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,
memo: memo
});
},
- getInvoice: function (wallet, payhash) {
- return this.request('get', '/api/v1/invoices/' + payhash, wallet.inkey);
+ payInvoice: function (wallet, bolt11) {
+ return this.request('post', '/api/v1/payments', wallet.inkey, {
+ out: true,
+ bolt11: bolt11
+ });
+ },
+ getPayments: function (wallet, checkPending) {
+ var query_param = (checkPending) ? '?check_pending' : '';
+ return this.request('get', ['/api/v1/payments', query_param].join(''), wallet.inkey);
},
- getTransactions: function (wallet) {
- return this.request('get', '/api/v1/transactions', wallet.inkey);
+ getPayment: function (wallet, payhash) {
+ return this.request('get', '/api/v1/payments/' + payhash, 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;
},
@@ -42,14 +47,6 @@ var LNbits = {
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;
@@ -62,10 +59,22 @@ var LNbits = {
},
wallet: function (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.url = ['/wallet?usr=', obj.user, '&wal=', obj.id].join('');
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: {
@@ -79,11 +88,10 @@ var LNbits = {
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,
+ caption: [error.response.status, ' ', error.response.statusText].join('').toUpperCase() || null,
icon: null
});
}
@@ -98,7 +106,7 @@ var windowMixin = {
extensions: [],
user: null,
wallet: null,
- transactions: [],
+ payments: [],
}
};
},
@@ -122,11 +130,6 @@ var windowMixin = {
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) {
diff --git a/lnbits/static/vendor/moment@2.24.0/moment.min.js b/lnbits/static/vendor/moment@2.24.0/moment.min.js
new file mode 100644
index 0000000..5787a40
--- /dev/null
+++ b/lnbits/static/vendor/moment@2.24.0/moment.min.js
@@ -0,0 +1 @@
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.moment=t()}(this,function(){"use strict";var e,i;function c(){return e.apply(null,arguments)}function o(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function u(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function l(e){return void 0===e}function h(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function d(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function f(e,t){var n,s=[];for(n=0;n>>0,s=0;sSe(e)?(r=e+1,o-Se(e)):(r=e,o),{year:r,dayOfYear:a}}function Ie(e,t,n){var s,i,r=Ve(e.year(),t,n),a=Math.floor((e.dayOfYear()-r-1)/7)+1;return a<1?s=a+Ae(i=e.year()-1,t,n):a>Ae(e.year(),t,n)?(s=a-Ae(e.year(),t,n),i=e.year()+1):(i=e.year(),s=a),{week:s,year:i}}function Ae(e,t,n){var s=Ve(e,t,n),i=Ve(e+1,t,n);return(Se(e)-s+i)/7}I("w",["ww",2],"wo","week"),I("W",["WW",2],"Wo","isoWeek"),C("week","w"),C("isoWeek","W"),F("week",5),F("isoWeek",5),ue("w",B),ue("ww",B,z),ue("W",B),ue("WW",B,z),fe(["w","ww","W","WW"],function(e,t,n,s){t[s.substr(0,1)]=D(e)});function je(e,t){return e.slice(t,7).concat(e.slice(0,t))}I("d",0,"do","day"),I("dd",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),I("ddd",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),I("dddd",0,0,function(e){return this.localeData().weekdays(this,e)}),I("e",0,0,"weekday"),I("E",0,0,"isoWeekday"),C("day","d"),C("weekday","e"),C("isoWeekday","E"),F("day",11),F("weekday",11),F("isoWeekday",11),ue("d",B),ue("e",B),ue("E",B),ue("dd",function(e,t){return t.weekdaysMinRegex(e)}),ue("ddd",function(e,t){return t.weekdaysShortRegex(e)}),ue("dddd",function(e,t){return t.weekdaysRegex(e)}),fe(["dd","ddd","dddd"],function(e,t,n,s){var i=n._locale.weekdaysParse(e,s,n._strict);null!=i?t.d=i:g(n).invalidWeekday=e}),fe(["d","e","E"],function(e,t,n,s){t[s]=D(e)});var Ze="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_");var ze="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_");var $e="Su_Mo_Tu_We_Th_Fr_Sa".split("_");var qe=ae;var Je=ae;var Be=ae;function Qe(){function e(e,t){return t.length-e.length}var t,n,s,i,r,a=[],o=[],u=[],l=[];for(t=0;t<7;t++)n=y([2e3,1]).day(t),s=this.weekdaysMin(n,""),i=this.weekdaysShort(n,""),r=this.weekdays(n,""),a.push(s),o.push(i),u.push(r),l.push(s),l.push(i),l.push(r);for(a.sort(e),o.sort(e),u.sort(e),l.sort(e),t=0;t<7;t++)o[t]=he(o[t]),u[t]=he(u[t]),l[t]=he(l[t]);this._weekdaysRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+o.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+a.join("|")+")","i")}function Xe(){return this.hours()%12||12}function Ke(e,t){I(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)})}function et(e,t){return t._meridiemParse}I("H",["HH",2],0,"hour"),I("h",["hh",2],0,Xe),I("k",["kk",2],0,function(){return this.hours()||24}),I("hmm",0,0,function(){return""+Xe.apply(this)+L(this.minutes(),2)}),I("hmmss",0,0,function(){return""+Xe.apply(this)+L(this.minutes(),2)+L(this.seconds(),2)}),I("Hmm",0,0,function(){return""+this.hours()+L(this.minutes(),2)}),I("Hmmss",0,0,function(){return""+this.hours()+L(this.minutes(),2)+L(this.seconds(),2)}),Ke("a",!0),Ke("A",!1),C("hour","h"),F("hour",13),ue("a",et),ue("A",et),ue("H",B),ue("h",B),ue("k",B),ue("HH",B,z),ue("hh",B,z),ue("kk",B,z),ue("hmm",Q),ue("hmmss",X),ue("Hmm",Q),ue("Hmmss",X),ce(["H","HH"],ge),ce(["k","kk"],function(e,t,n){var s=D(e);t[ge]=24===s?0:s}),ce(["a","A"],function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e}),ce(["h","hh"],function(e,t,n){t[ge]=D(e),g(n).bigHour=!0}),ce("hmm",function(e,t,n){var s=e.length-2;t[ge]=D(e.substr(0,s)),t[ve]=D(e.substr(s)),g(n).bigHour=!0}),ce("hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[ge]=D(e.substr(0,s)),t[ve]=D(e.substr(s,2)),t[pe]=D(e.substr(i)),g(n).bigHour=!0}),ce("Hmm",function(e,t,n){var s=e.length-2;t[ge]=D(e.substr(0,s)),t[ve]=D(e.substr(s))}),ce("Hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[ge]=D(e.substr(0,s)),t[ve]=D(e.substr(s,2)),t[pe]=D(e.substr(i))});var tt,nt=Te("Hours",!0),st={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Ce,monthsShort:He,week:{dow:0,doy:6},weekdays:Ze,weekdaysMin:$e,weekdaysShort:ze,meridiemParse:/[ap]\.?m?\.?/i},it={},rt={};function at(e){return e?e.toLowerCase().replace("_","-"):e}function ot(e){var t=null;if(!it[e]&&"undefined"!=typeof module&&module&&module.exports)try{t=tt._abbr,require("./locale/"+e),ut(t)}catch(e){}return it[e]}function ut(e,t){var n;return e&&((n=l(t)?ht(e):lt(e,t))?tt=n:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+e+" not found. Did you forget to load it?")),tt._abbr}function lt(e,t){if(null===t)return delete it[e],null;var n,s=st;if(t.abbr=e,null!=it[e])T("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),s=it[e]._config;else if(null!=t.parentLocale)if(null!=it[t.parentLocale])s=it[t.parentLocale]._config;else{if(null==(n=ot(t.parentLocale)))return rt[t.parentLocale]||(rt[t.parentLocale]=[]),rt[t.parentLocale].push({name:e,config:t}),null;s=n._config}return it[e]=new P(x(s,t)),rt[e]&&rt[e].forEach(function(e){lt(e.name,e.config)}),ut(e),it[e]}function ht(e){var t;if(e&&e._locale&&e._locale._abbr&&(e=e._locale._abbr),!e)return tt;if(!o(e)){if(t=ot(e))return t;e=[e]}return function(e){for(var t,n,s,i,r=0;r=t&&a(i,n,!0)>=t-1)break;t--}r++}return tt}(e)}function dt(e){var t,n=e._a;return n&&-2===g(e).overflow&&(t=n[_e]<0||11Pe(n[me],n[_e])?ye:n[ge]<0||24Ae(n,r,a)?g(e)._overflowWeeks=!0:null!=u?g(e)._overflowWeekday=!0:(o=Ee(n,s,i,r,a),e._a[me]=o.year,e._dayOfYear=o.dayOfYear)}(e),null!=e._dayOfYear&&(r=ct(e._a[me],s[me]),(e._dayOfYear>Se(r)||0===e._dayOfYear)&&(g(e)._overflowDayOfYear=!0),n=Ge(r,0,e._dayOfYear),e._a[_e]=n.getUTCMonth(),e._a[ye]=n.getUTCDate()),t=0;t<3&&null==e._a[t];++t)e._a[t]=a[t]=s[t];for(;t<7;t++)e._a[t]=a[t]=null==e._a[t]?2===t?1:0:e._a[t];24===e._a[ge]&&0===e._a[ve]&&0===e._a[pe]&&0===e._a[we]&&(e._nextDay=!0,e._a[ge]=0),e._d=(e._useUTC?Ge:function(e,t,n,s,i,r,a){var o;return e<100&&0<=e?(o=new Date(e+400,t,n,s,i,r,a),isFinite(o.getFullYear())&&o.setFullYear(e)):o=new Date(e,t,n,s,i,r,a),o}).apply(null,a),i=e._useUTC?e._d.getUTCDay():e._d.getDay(),null!=e._tzm&&e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),e._nextDay&&(e._a[ge]=24),e._w&&void 0!==e._w.d&&e._w.d!==i&&(g(e).weekdayMismatch=!0)}}var mt=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,_t=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,yt=/Z|[+-]\d\d(?::?\d\d)?/,gt=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],vt=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],pt=/^\/?Date\((\-?\d+)/i;function wt(e){var t,n,s,i,r,a,o=e._i,u=mt.exec(o)||_t.exec(o);if(u){for(g(e).iso=!0,t=0,n=gt.length;tn.valueOf():n.valueOf()this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},mn.isLocal=function(){return!!this.isValid()&&!this._isUTC},mn.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},mn.isUtc=Et,mn.isUTC=Et,mn.zoneAbbr=function(){return this._isUTC?"UTC":""},mn.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},mn.dates=n("dates accessor is deprecated. Use date instead.",un),mn.months=n("months accessor is deprecated. Use month instead",Ue),mn.years=n("years accessor is deprecated. Use year instead",Oe),mn.zone=n("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(e,t){return null!=e?("string"!=typeof e&&(e=-e),this.utcOffset(e,t),this):-this.utcOffset()}),mn.isDSTShifted=n("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){if(!l(this._isDSTShifted))return this._isDSTShifted;var e={};if(w(e,this),(e=Ot(e))._a){var t=e._isUTC?y(e._a):bt(e._a);this._isDSTShifted=this.isValid()&&0
{%- endmacro %}
diff --git a/lnbits/wallets/base.py b/lnbits/wallets/base.py
index 0aef512..11bfdd7 100644
--- a/lnbits/wallets/base.py
+++ b/lnbits/wallets/base.py
@@ -13,11 +13,16 @@ class PaymentResponse(NamedTuple):
raw_response: Response
failed: bool = False
fee_msat: int = 0
+ error_message: Optional[str] = None
-class TxStatus(NamedTuple):
+class PaymentStatus(NamedTuple):
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):
@@ -26,13 +31,13 @@ class Wallet(ABC):
pass
@abstractmethod
- def pay_invoice(self, bolt11: str) -> Response:
+ def pay_invoice(self, bolt11: str) -> PaymentResponse:
pass
@abstractmethod
- def get_invoice_status(self, payment_hash: str) -> TxStatus:
+ def get_invoice_status(self, payment_hash: str) -> PaymentStatus:
pass
@abstractmethod
- def get_payment_status(self, payment_hash: str) -> TxStatus:
+ def get_payment_status(self, payment_hash: str) -> PaymentStatus:
pass
diff --git a/lnbits/wallets/lnd.py b/lnbits/wallets/lnd.py
index d8cd1b4..7359367 100644
--- a/lnbits/wallets/lnd.py
+++ b/lnbits/wallets/lnd.py
@@ -1,5 +1,5 @@
from requests import get, post
-from .base import InvoiceResponse, PaymentResponse, TxStatus, Wallet
+from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
class LndWallet(Wallet):
@@ -41,15 +41,15 @@ class LndWallet(Wallet):
)
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)
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(
url=f"{self.endpoint}/v1/payments",
headers=self.auth_admin,
@@ -58,11 +58,11 @@ class LndWallet(Wallet):
)
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]
payment = payments[0] if payments else None
# check payment.status: https://api.lightning.community/rest/index.html?python#peersynctype
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)
diff --git a/lnbits/wallets/lnpay.py b/lnbits/wallets/lnpay.py
index cedc39b..d9aaf3e 100644
--- a/lnbits/wallets/lnpay.py
+++ b/lnbits/wallets/lnpay.py
@@ -1,6 +1,6 @@
from requests import get, post
-from .base import InvoiceResponse, PaymentResponse, TxStatus, Wallet
+from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
class LNPayWallet(Wallet):
@@ -37,20 +37,14 @@ class LNPayWallet(Wallet):
return PaymentResponse(r, not r.ok)
- def get_invoice_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"]])
+ def get_invoice_status(self, payment_hash: str) -> PaymentStatus:
+ return self.get_payment_status(payment_hash)
- def get_payment_status(self, payment_hash: str) -> TxStatus:
+ def get_payment_status(self, payment_hash: str) -> PaymentStatus:
r = get(url=f"{self.endpoint}/user/lntx/{payment_hash}", headers=self.auth_api)
if not r.ok:
- return TxStatus(r, None)
+ return PaymentStatus(r, None)
statuses = {0: None, 1: True, -1: False}
- return TxStatus(r, statuses[r.json()["settled"]])
+ return PaymentStatus(r, statuses[r.json()["settled"]])
diff --git a/lnbits/wallets/lntxbot.py b/lnbits/wallets/lntxbot.py
index 037148e..63e2ab8 100644
--- a/lnbits/wallets/lntxbot.py
+++ b/lnbits/wallets/lntxbot.py
@@ -1,6 +1,6 @@
from requests import post
-from .base import InvoiceResponse, PaymentResponse, TxStatus, Wallet
+from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
class LntxbotWallet(Wallet):
@@ -23,38 +23,39 @@ class LntxbotWallet(Wallet):
def pay_invoice(self, bolt11: str) -> PaymentResponse:
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:
data = r.json()
if "error" in data and data["error"]:
failed = True
+ error_message = data["message"]
elif "fee_msat" in data:
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)
if not r.ok:
- return TxStatus(r, None)
+ return PaymentStatus(r, None)
data = r.json()
if "error" in data:
- return TxStatus(r, None)
+ return PaymentStatus(r, None)
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)
if not r.ok or "error" in r.json():
- return TxStatus(r, None)
+ return PaymentStatus(r, None)
- statuses = {"complete": True, "failed": False, "unknown": None}
- return TxStatus(r, statuses[r.json().get("status", "unknown")])
+ statuses = {"complete": True, "failed": False, "pending": None, "unknown": None}
+ return PaymentStatus(r, statuses[r.json().get("status", "unknown")])
diff --git a/lnbits/wallets/opennode.py b/lnbits/wallets/opennode.py
index 50443e6..54930a4 100644
--- a/lnbits/wallets/opennode.py
+++ b/lnbits/wallets/opennode.py
@@ -1,6 +1,6 @@
from requests import get, post
-from .base import InvoiceResponse, PaymentResponse, TxStatus, Wallet
+from .base import InvoiceResponse, PaymentResponse, PaymentStatus, 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})
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)
if not r.ok:
- return TxStatus(r, None)
+ return PaymentStatus(r, None)
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)
if not r.ok:
- return TxStatus(r, None)
+ return PaymentStatus(r, None)
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"]])