Browse Source

refactor: "payments" is the name, and API updates

fee_issues
Eneko Illarramendi 5 years ago
parent
commit
d862b16ee6
  1. 94
      lnbits/__init__.py
  2. 52
      lnbits/core/crud.py
  3. 38
      lnbits/core/models.py
  4. 166
      lnbits/core/static/js/wallet.js
  5. 31
      lnbits/core/templates/core/wallet.html
  6. 8
      lnbits/core/views.py
  7. 100
      lnbits/core/views_api.py
  8. 3
      lnbits/helpers.py
  9. 53
      lnbits/static/js/base.js
  10. 1
      lnbits/static/vendor/moment@2.24.0/moment.min.js
  11. 1
      lnbits/templates/macros.jinja
  12. 15
      lnbits/wallets/base.py
  13. 14
      lnbits/wallets/lnd.py
  14. 18
      lnbits/wallets/lnpay.py
  15. 25
      lnbits/wallets/lntxbot.py
  16. 14
      lnbits/wallets/opennode.py

94
lnbits/__init__.py

@ -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()

52
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 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,))

38
lnbits/core/models.py

@ -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)
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"]: def get_payments(self, *, include_all_pending: bool = False) -> List["Payment"]:
from .crud import get_wallet_transactions 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 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_payment_status(self.payhash, pending)
def delete(self) -> None:
from .crud import delete_payment
update_transaction_status(self.payhash, pending) delete_payment(self.payhash)

166
lnbits/core/static/js/wallet.js

@ -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();
} }
}); });

31
lnbits/core/templates/core/wallet.html

@ -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>

8
lnbits/core/views.py

@ -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"))

100
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.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

3
lnbits/helpers.py

@ -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):

53
lnbits/static/js/base.js

@ -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
});
},
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) { getPayment: function (wallet, payhash) {
return this.request('get', '/api/v1/transactions', wallet.inkey); 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

File diff suppressed because one or more lines are too long

1
lnbits/templates/macros.jinja

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

15
lnbits/wallets/base.py

@ -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

14
lnbits/wallets/lnd.py

@ -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)

18
lnbits/wallets/lnpay.py

@ -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:
r = get(url=f"{self.endpoint}/user/lntx/{payment_hash}", headers=self.auth_api) return self.get_payment_status(payment_hash)
if not r.ok:
return TxStatus(r, None)
statuses = {0: None, 1: True, -1: False}
return TxStatus(r, statuses[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}/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"]])

25
lnbits/wallets/lntxbot.py

@ -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")])

14
lnbits/wallets/opennode.py

@ -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…
Cancel
Save