From c96b22664e61051a31cbc4471289cb57357811b2 Mon Sep 17 00:00:00 2001 From: arcbtc Date: Wed, 19 Aug 2020 17:53:27 +0100 Subject: [PATCH 1/9] internal payments. --- README.md | 2 +- lnbits/__init__.py | 1 + lnbits/core/crud.py | 31 +++++++++++------- lnbits/core/migrations.py | 67 +++++++++++++++++++++++++++++++++++++++ lnbits/core/services.py | 34 ++++++++++++++++---- 5 files changed, 117 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index ed9fe35..dc70c3b 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ See [lnbits.org](https://lnbits.org) for more detailed documentation. Checkout the LNbits [YouTube](https://www.youtube.com/playlist?list=PLPj3KCksGbSYG0ciIQUWJru1dWstPHshe) video series. -LNbits is inspired by all the great work of [opennode.com](https://www.opennode.com/), and in particular [lnpay.co](https://lnpay.co/). Both work as excellent funding sources for LNbits! +LNbits is inspired by all the great work of [opennode.com](https://www.opennode.com/), and in particular [lnpay.co](https://lnpay.co/). Both work as excellent funding sources for LNbits. ## LNbits as an account system diff --git a/lnbits/__init__.py b/lnbits/__init__.py index b243376..479338e 100644 --- a/lnbits/__init__.py +++ b/lnbits/__init__.py @@ -85,3 +85,4 @@ def migrate_databases(): if __name__ == "__main__": app.run() + diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 1b73786..17b46a9 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -140,9 +140,9 @@ def get_wallet_payment(wallet_id: str, checking_id: str) -> Optional[Payment]: with open_db() as db: row = db.fetchone( """ - SELECT payhash as checking_id, amount, fee, pending, memo, time - FROM apipayments - WHERE wallet = ? AND payhash = ? + SELECT id as checking_id, amount, fee, pending, memo, time + FROM apipayment + WHERE wallet = ? AND id = ? """, (wallet_id, checking_id), ) @@ -179,7 +179,7 @@ def get_wallet_payments( with open_db() as db: rows = db.fetchall( f""" - SELECT payhash as checking_id, amount, fee, pending, memo, time + SELECT id as checking_id, amount, fee, pending, memo, time FROM apipayments WHERE wallet = ? {clause} ORDER BY time DESC @@ -195,7 +195,7 @@ def delete_wallet_payments_expired(wallet_id: str, *, seconds: int = 86400) -> N db.execute( """ DELETE - FROM apipayments WHERE wallet = ? AND pending = 1 AND time < strftime('%s', 'now') - ? + FROM apipayment WHERE wallet = ? AND pending = 1 AND time < strftime('%s', 'now') - ? """, (wallet_id, seconds), ) @@ -206,15 +206,15 @@ def delete_wallet_payments_expired(wallet_id: str, *, seconds: int = 86400) -> N def create_payment( - *, wallet_id: str, checking_id: str, amount: int, memo: str, fee: int = 0, pending: bool = True + *, wallet_id: str, checking_id: str, payment_hash: str, amount: int, memo: str, fee: int = 0, pending: bool = True ) -> Payment: with open_db() as db: db.execute( """ - INSERT INTO apipayments (wallet, payhash, amount, pending, memo, fee) - VALUES (?, ?, ?, ?, ?, ?) + INSERT INTO apipayment (wallet, id, payment_hash, amount, pending, memo, fee) + VALUES (?, ?, ?, ?, ?, ?, ?) """, - (wallet_id, checking_id, amount, int(pending), memo, fee), + (wallet_id, checking_id, payment_hash, amount, int(pending), memo, fee), ) new_payment = get_wallet_payment(wallet_id, checking_id) @@ -225,9 +225,18 @@ def create_payment( def update_payment_status(checking_id: str, pending: bool) -> None: with open_db() as db: - db.execute("UPDATE apipayments SET pending = ? WHERE payhash = ?", (int(pending), checking_id,)) + db.execute("UPDATE apipayment SET pending = ? WHERE id = ?", (int(pending), checking_id,)) def delete_payment(checking_id: str) -> None: with open_db() as db: - db.execute("DELETE FROM apipayments WHERE payhash = ?", (checking_id,)) + db.execute("DELETE FROM apipayment WHERE id = ?", (checking_id,)) + + +def check_internal(payment_hash: str) -> None: + with open_db() as db: + row = db.fetchone("SELECT * FROM apipayment WHERE payment_hash = ?", (payment_hash,)) + if not row: + return False + else: + return row['id'] diff --git a/lnbits/core/migrations.py b/lnbits/core/migrations.py index a844300..29b0371 100644 --- a/lnbits/core/migrations.py +++ b/lnbits/core/migrations.py @@ -51,6 +51,7 @@ def m001_initial(db): ); """ ) + db.execute( """ CREATE VIEW IF NOT EXISTS balances AS @@ -68,8 +69,74 @@ def m001_initial(db): GROUP BY wallet; """ ) + db.execute("DROP VIEW balances") + db.execute( + """ + CREATE VIEW IF NOT EXISTS balances AS + SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM ( + SELECT wallet, SUM(amount) AS s -- incoming + FROM apipayment + WHERE amount > 0 AND pending = 0 -- don't sum pending + GROUP BY wallet + UNION ALL + SELECT wallet, SUM(amount + fee) AS s -- outgoing, sum fees + FROM apipayment + WHERE amount < 0 -- do sum pending + GROUP BY wallet + ) + GROUP BY wallet; + """ + ) +def m002_changed(db): + + db.execute( + """ + CREATE TABLE IF NOT EXISTS apipayment ( + id TEXT NOT NULL, + payment_hash TEXT NOT NULL, + amount INTEGER NOT NULL, + fee INTEGER NOT NULL DEFAULT 0, + wallet TEXT NOT NULL, + pending BOOLEAN NOT NULL, + memo TEXT, + time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')), + + UNIQUE (wallet, id) + ); + """ + ) + + + for row in [list(row) for row in db.fetchall("SELECT * FROM apipayments")]: + db.execute( + """ + INSERT INTO apipayment ( + id, + payment_hash, + amount, + fee, + wallet, + pending, + memo, + time + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + row[0], + "oldinvoice", + row[1], + row[2], + row[3], + row[4], + row[5], + row[6], + ), + ) + db.execute("DROP TABLE apipayments") def migrate(): with open_db() as db: m001_initial(db) + m002_changed(db) diff --git a/lnbits/core/services.py b/lnbits/core/services.py index 717cfc1..3ff3b32 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -4,7 +4,7 @@ from lnbits.bolt11 import decode as bolt11_decode # type: ignore from lnbits.helpers import urlsafe_short_hash from lnbits.settings import WALLET -from .crud import get_wallet, create_payment, delete_payment +from .crud import get_wallet, create_payment, delete_payment, check_internal, update_payment_status def create_invoice(*, wallet_id: str, amount: int, memo: str, description_hash: bytes = None) -> Tuple[str, str]: @@ -18,9 +18,10 @@ def create_invoice(*, wallet_id: str, amount: int, memo: str, description_hash: if not ok: raise Exception(error_message or "Unexpected backend error.") + invoice = bolt11_decode(payment_request) amount_msat = amount * 1000 - create_payment(wallet_id=wallet_id, checking_id=checking_id, amount=amount_msat, memo=memo) + create_payment(wallet_id=wallet_id, checking_id=checking_id, payment_hash=invoice.payment_hash, amount=amount_msat, memo=memo) return checking_id, payment_request @@ -29,6 +30,7 @@ def pay_invoice(*, wallet_id: str, bolt11: str, max_sat: Optional[int] = None) - temp_id = f"temp_{urlsafe_short_hash()}" try: invoice = bolt11_decode(bolt11) + internal = check_internal(invoice.payment_hash) if invoice.amount_msat == 0: raise ValueError("Amountless invoices not supported.") @@ -37,21 +39,41 @@ def pay_invoice(*, wallet_id: str, bolt11: str, max_sat: Optional[int] = None) - raise ValueError("Amount in invoice is too high.") fee_reserve = max(1000, int(invoice.amount_msat * 0.01)) - create_payment( - wallet_id=wallet_id, checking_id=temp_id, amount=-invoice.amount_msat, fee=-fee_reserve, memo=temp_id, - ) + + if not internal: + create_payment( + wallet_id=wallet_id, + checking_id=temp_id, + payment_hash=invoice.payment_hash, + amount=-invoice.amount_msat, + fee=-fee_reserve, + memo=temp_id, + ) wallet = get_wallet(wallet_id) assert wallet, "invalid wallet id" if wallet.balance_msat < 0: raise PermissionError("Insufficient balance.") - ok, checking_id, fee_msat, error_message = WALLET.pay_invoice(bolt11) + if internal: + create_payment( + wallet_id=wallet_id, + checking_id=temp_id, + payment_hash=invoice.payment_hash, + amount=-invoice.amount_msat, + fee=0, + pending=False, + memo=invoice.description, + ) + update_payment_status(checking_id=internal, pending=False) + return temp_id + ok, checking_id, fee_msat, error_message = WALLET.pay_invoice(bolt11) if ok: create_payment( wallet_id=wallet_id, checking_id=checking_id, + payment_hash=invoice.payment_hash, amount=-invoice.amount_msat, fee=fee_msat, memo=invoice.description, From bf3c44b3c4b3cf5735b94e9722698f21b8257d66 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 31 Aug 2020 22:12:46 -0300 Subject: [PATCH 2/9] extra fields on apipayments + index payments by payment_hash --- lnbits/core/crud.py | 50 +++++--- lnbits/core/migrations.py | 74 ++--------- lnbits/core/models.py | 31 ++++- lnbits/core/services.py | 118 ++++++++++-------- lnbits/core/static/js/wallet.js | 112 +++++++++-------- lnbits/core/templates/core/_api_docs.html | 12 +- lnbits/core/templates/core/wallet.html | 2 +- lnbits/core/views/api.py | 42 +++++-- lnbits/core/views/generic.py | 2 +- lnbits/extensions/amilk/views_api.py | 34 ++--- lnbits/extensions/events/crud.py | 24 ++-- .../events/templates/events/display.html | 2 +- lnbits/extensions/events/views_api.py | 25 ++-- lnbits/extensions/lnticket/crud.py | 16 +-- .../lnticket/templates/lnticket/display.html | 2 +- lnbits/extensions/lnticket/views_api.py | 25 ++-- .../paywall/templates/paywall/_api_docs.html | 6 +- .../paywall/templates/paywall/display.html | 2 +- lnbits/extensions/paywall/views_api.py | 13 +- .../extensions/tpos/templates/tpos/tpos.html | 2 +- lnbits/extensions/tpos/views_api.py | 18 +-- .../templates/usermanager/_api_docs.html | 8 +- lnbits/extensions/withdraw/views_api.py | 3 +- lnbits/static/js/base.js | 86 +++++++------ 24 files changed, 360 insertions(+), 349 deletions(-) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 17b46a9..2c3ae7a 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Optional, Dict from uuid import uuid4 from lnbits.db import open_db @@ -136,18 +136,18 @@ def get_wallet_for_key(key: str, key_type: str = "invoice") -> Optional[Wallet]: # --------------- -def get_wallet_payment(wallet_id: str, checking_id: str) -> Optional[Payment]: +def get_wallet_payment(wallet_id: str, payment_hash: str) -> Optional[Payment]: with open_db() as db: row = db.fetchone( """ - SELECT id as checking_id, amount, fee, pending, memo, time - FROM apipayment - WHERE wallet = ? AND id = ? + SELECT * + FROM apipayments + WHERE wallet = ? AND hash = ? """, - (wallet_id, checking_id), + (wallet_id, payment_hash), ) - return Payment(**row) if row else None + return Payment.from_row(row) if row else None def get_wallet_payments( @@ -179,7 +179,7 @@ def get_wallet_payments( with open_db() as db: rows = db.fetchall( f""" - SELECT id as checking_id, amount, fee, pending, memo, time + SELECT * FROM apipayments WHERE wallet = ? {clause} ORDER BY time DESC @@ -187,7 +187,7 @@ def get_wallet_payments( (wallet_id,), ) - return [Payment(**row) for row in rows] + return [Payment.from_row(row) for row in rows] def delete_wallet_payments_expired(wallet_id: str, *, seconds: int = 86400) -> None: @@ -195,7 +195,7 @@ def delete_wallet_payments_expired(wallet_id: str, *, seconds: int = 86400) -> N db.execute( """ DELETE - FROM apipayment WHERE wallet = ? AND pending = 1 AND time < strftime('%s', 'now') - ? + FROM apipayments WHERE wallet = ? AND pending = 1 AND time < strftime('%s', 'now') - ? """, (wallet_id, seconds), ) @@ -206,18 +206,30 @@ def delete_wallet_payments_expired(wallet_id: str, *, seconds: int = 86400) -> N def create_payment( - *, wallet_id: str, checking_id: str, payment_hash: str, amount: int, memo: str, fee: int = 0, pending: bool = True + *, + wallet_id: str, + checking_id: str, + payment_request: str, + payment_hash: str, + amount: int, + memo: str, + fee: int = 0, + preimage: Optional[str] = None, + pending: bool = True, + extra: Optional[Dict] = None, ) -> Payment: with open_db() as db: db.execute( """ - INSERT INTO apipayment (wallet, id, payment_hash, amount, pending, memo, fee) - VALUES (?, ?, ?, ?, ?, ?, ?) + INSERT INTO apipayments + (wallet, checking_id, bolt11, hash, preimage, + amount, pending, memo, fee, extra) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, - (wallet_id, checking_id, payment_hash, amount, int(pending), memo, fee), + (wallet_id, checking_id, payment_request, payment_hash, preimage, amount, int(pending), memo, fee, extra), ) - new_payment = get_wallet_payment(wallet_id, checking_id) + new_payment = get_wallet_payment(wallet_id, payment_hash) assert new_payment, "Newly created payment couldn't be retrieved" return new_payment @@ -225,18 +237,18 @@ def create_payment( def update_payment_status(checking_id: str, pending: bool) -> None: with open_db() as db: - db.execute("UPDATE apipayment SET pending = ? WHERE id = ?", (int(pending), checking_id,)) + db.execute("UPDATE apipayments SET pending = ? WHERE checking_id = ?", (int(pending), checking_id,)) def delete_payment(checking_id: str) -> None: with open_db() as db: - db.execute("DELETE FROM apipayment WHERE id = ?", (checking_id,)) + db.execute("DELETE FROM apipayments WHERE checking_id = ?", (checking_id,)) def check_internal(payment_hash: str) -> None: with open_db() as db: - row = db.fetchone("SELECT * FROM apipayment WHERE payment_hash = ?", (payment_hash,)) + row = db.fetchone("SELECT checking_id FROM apipayments WHERE hash = ?", (payment_hash,)) if not row: return False else: - return row['id'] + return row["checking_id"] diff --git a/lnbits/core/migrations.py b/lnbits/core/migrations.py index 29b0371..826b217 100644 --- a/lnbits/core/migrations.py +++ b/lnbits/core/migrations.py @@ -69,74 +69,22 @@ def m001_initial(db): GROUP BY wallet; """ ) - db.execute("DROP VIEW balances") - db.execute( - """ - CREATE VIEW IF NOT EXISTS balances AS - SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM ( - SELECT wallet, SUM(amount) AS s -- incoming - FROM apipayment - WHERE amount > 0 AND pending = 0 -- don't sum pending - GROUP BY wallet - UNION ALL - SELECT wallet, SUM(amount + fee) AS s -- outgoing, sum fees - FROM apipayment - WHERE amount < 0 -- do sum pending - GROUP BY wallet - ) - GROUP BY wallet; - """ - ) -def m002_changed(db): - db.execute( - """ - CREATE TABLE IF NOT EXISTS apipayment ( - id TEXT NOT NULL, - payment_hash TEXT NOT NULL, - amount INTEGER NOT NULL, - fee INTEGER NOT NULL DEFAULT 0, - wallet TEXT NOT NULL, - pending BOOLEAN NOT NULL, - memo TEXT, - time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')), - - UNIQUE (wallet, id) - ); +def m002_add_fields_to_apipayments(db): """ - ) - + Adding fields to apipayments for better accounting, + and renaming payhash to checking_id since that is what it really is. + """ + db.execute("ALTER TABLE apipayments RENAME COLUMN payhash TO checking_id") + db.execute("ALTER TABLE apipayments ADD COLUMN hash TEXT") + db.execute("CREATE INDEX by_hash ON apipayments (hash)") + db.execute("ALTER TABLE apipayments ADD COLUMN preimage TEXT") + db.execute("ALTER TABLE apipayments ADD COLUMN bolt11 TEXT") + db.execute("ALTER TABLE apipayments ADD COLUMN extra TEXT") - for row in [list(row) for row in db.fetchall("SELECT * FROM apipayments")]: - db.execute( - """ - INSERT INTO apipayment ( - id, - payment_hash, - amount, - fee, - wallet, - pending, - memo, - time - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - row[0], - "oldinvoice", - row[1], - row[2], - row[3], - row[4], - row[5], - row[6], - ), - ) - db.execute("DROP TABLE apipayments") def migrate(): with open_db() as db: m001_initial(db) - m002_changed(db) + m002_add_fields_to_apipayments(db) diff --git a/lnbits/core/models.py b/lnbits/core/models.py index 10a87ad..9dec751 100644 --- a/lnbits/core/models.py +++ b/lnbits/core/models.py @@ -1,4 +1,6 @@ -from typing import List, NamedTuple, Optional +import json +from typing import List, NamedTuple, Optional, Dict +from sqlite3 import Row class User(NamedTuple): @@ -29,10 +31,10 @@ class Wallet(NamedTuple): def balance(self) -> int: return self.balance_msat // 1000 - def get_payment(self, checking_id: str) -> Optional["Payment"]: + def get_payment(self, payment_hash: str) -> Optional["Payment"]: from .crud import get_wallet_payment - return get_wallet_payment(self.id, checking_id) + return get_wallet_payment(self.id, payment_hash) def get_payments( self, *, complete: bool = True, pending: bool = False, outgoing: bool = True, incoming: bool = True @@ -54,6 +56,29 @@ class Payment(NamedTuple): fee: int memo: str time: int + bolt11: str + preimage: str + payment_hash: str + extra: Dict + + @classmethod + def from_row(cls, row: Row): + return cls( + checking_id=row["checking_id"], + payment_hash=row["hash"], + bolt11=row["bolt11"], + preimage=row["preimage"], + extra=json.loads(row["extra"] or "{}"), + pending=row["pending"], + amount=row["amount"], + fee=row["fee"], + memo=row["memo"], + time=row["time"], + ) + + @property + def tag(self) -> Optional[str]: + return self.extra.get("tag") @property def msat(self) -> int: diff --git a/lnbits/core/services.py b/lnbits/core/services.py index 3ff3b32..e074a34 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -1,14 +1,20 @@ -from typing import Optional, Tuple +from typing import Optional, Tuple, Dict -from lnbits.bolt11 import decode as bolt11_decode # type: ignore +from lnbits import bolt11 from lnbits.helpers import urlsafe_short_hash from lnbits.settings import WALLET -from .crud import get_wallet, create_payment, delete_payment, check_internal, update_payment_status +from .crud import get_wallet, create_payment, delete_payment, check_internal, update_payment_status, get_wallet_payment -def create_invoice(*, wallet_id: str, amount: int, memo: str, description_hash: bytes = None) -> Tuple[str, str]: - +def create_invoice( + *, + wallet_id: str, + amount: int, + memo: Optional[str] = None, + description_hash: Optional[bytes] = None, + extra: Optional[Dict] = None, +) -> Tuple[str, str]: try: ok, checking_id, payment_request, error_message = WALLET.create_invoice( amount=amount, memo=memo, description_hash=description_hash @@ -18,77 +24,81 @@ def create_invoice(*, wallet_id: str, amount: int, memo: str, description_hash: if not ok: raise Exception(error_message or "Unexpected backend error.") - invoice = bolt11_decode(payment_request) - - amount_msat = amount * 1000 - create_payment(wallet_id=wallet_id, checking_id=checking_id, payment_hash=invoice.payment_hash, amount=amount_msat, memo=memo) - - return checking_id, payment_request + invoice = bolt11.decode(payment_request) -def pay_invoice(*, wallet_id: str, bolt11: str, max_sat: Optional[int] = None) -> str: + amount_msat = amount * 1000 + create_payment( + wallet_id=wallet_id, + checking_id=checking_id, + payment_request=payment_request, + payment_hash=invoice.payment_hash, + amount=amount_msat, + memo=memo, + extra=extra, + ) + + return invoice.payment_hash, payment_request + + +def pay_invoice( + *, wallet_id: str, payment_request: str, max_sat: Optional[int] = None, extra: Optional[Dict] = None +) -> str: temp_id = f"temp_{urlsafe_short_hash()}" try: - invoice = bolt11_decode(bolt11) - internal = check_internal(invoice.payment_hash) - + invoice = bolt11.decode(payment_request) if invoice.amount_msat == 0: raise ValueError("Amountless invoices not supported.") - if max_sat and invoice.amount_msat > max_sat * 1000: raise ValueError("Amount in invoice is too high.") - fee_reserve = max(1000, int(invoice.amount_msat * 0.01)) - - if not internal: - create_payment( - wallet_id=wallet_id, - checking_id=temp_id, - payment_hash=invoice.payment_hash, - amount=-invoice.amount_msat, - fee=-fee_reserve, - memo=temp_id, - ) + # put all parameters that don't change here + payment_kwargs = dict( + wallet_id=wallet_id, + payment_request=payment_request, + payment_hash=invoice.payment_hash, + amount=-invoice.amount_msat, + memo=invoice.description, + extra=extra, + ) + # check_internal() returns the checking_id of the invoice we're waiting for + internal = check_internal(invoice.payment_hash) + if internal: + # create a new payment from this wallet + create_payment(checking_id=temp_id, fee=0, pending=False, **payment_kwargs) + else: + # create a temporary payment here so we can check if + # the balance is enough in the next step + fee_reserve = max(1000, int(invoice.amount_msat * 0.01)) + create_payment(checking_id=temp_id, fee=-fee_reserve, **payment_kwargs) + + # do the balance check wallet = get_wallet(wallet_id) assert wallet, "invalid wallet id" if wallet.balance_msat < 0: raise PermissionError("Insufficient balance.") if internal: - create_payment( - wallet_id=wallet_id, - checking_id=temp_id, - payment_hash=invoice.payment_hash, - amount=-invoice.amount_msat, - fee=0, - pending=False, - memo=invoice.description, - ) + # mark the invoice from the other side as not pending anymore + # so the other side only has access to his new money when we are sure + # the payer has enough to deduct from update_payment_status(checking_id=internal, pending=False) - return temp_id - - ok, checking_id, fee_msat, error_message = WALLET.pay_invoice(bolt11) - if ok: - create_payment( - wallet_id=wallet_id, - checking_id=checking_id, - payment_hash=invoice.payment_hash, - amount=-invoice.amount_msat, - fee=fee_msat, - memo=invoice.description, - ) + else: + # actually pay the external invoice + ok, checking_id, fee_msat, error_message = WALLET.pay_invoice(payment_request) + if ok: + create_payment(checking_id=checking_id, fee=fee_msat, **payment_kwargs) + delete_payment(temp_id) except Exception as e: ok, error_message = False, str(e) - - delete_payment(temp_id) - if not ok: raise Exception(error_message or "Unexpected backend error.") - return checking_id + return invoice.payment_hash -def check_payment(*, checking_id: str) -> str: - pass +def check_invoice_status(wallet_id: str, payment_hash: str) -> str: + payment = get_wallet_payment(wallet_id, payment_hash) + return WALLET.get_invoice_status(payment.checking_id) diff --git a/lnbits/core/static/js/wallet.js b/lnbits/core/static/js/wallet.js index 20e7778..ae01162 100644 --- a/lnbits/core/static/js/wallet.js +++ b/lnbits/core/static/js/wallet.js @@ -1,3 +1,5 @@ +/* globals Vue, VueQrcodeReader, VueQrcode, Quasar, LNbits, _ */ + Vue.component(VueQrcode.name, VueQrcode) Vue.use(VueQrcodeReader) @@ -12,10 +14,10 @@ function generateChart(canvas, payments) { } _.each( - payments.slice(0).sort(function (a, b) { + payments.slice(0).sort(function(a, b) { return a.time - b.time }), - function (tx) { + function(tx) { txs.push({ hour: Quasar.utils.date.formatDate(tx.date, 'YYYY-MM-DDTHH:00'), sat: tx.sat @@ -23,17 +25,17 @@ function generateChart(canvas, payments) { } ) - _.each(_.groupBy(txs, 'hour'), function (value, day) { + _.each(_.groupBy(txs, 'hour'), function(value, day) { var income = _.reduce( value, - function (memo, tx) { + function(memo, tx) { return tx.sat >= 0 ? memo + tx.sat : memo }, 0 ) var outcome = _.reduce( value, - function (memo, tx) { + function(memo, tx) { return tx.sat < 0 ? memo + Math.abs(tx.sat) : memo }, 0 @@ -65,14 +67,20 @@ function generateChart(canvas, payments) { type: 'bar', label: 'in', barPercentage: 0.75, - backgroundColor: window.Color('rgb(76,175,80)').alpha(0.5).rgbString() // green + 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 + backgroundColor: window + .Color('rgb(233,30,99)') + .alpha(0.5) + .rgbString() // pink } ] }, @@ -113,7 +121,7 @@ function generateChart(canvas, payments) { new Vue({ el: '#vue', mixins: [windowMixin], - data: function () { + data: function() { return { receive: { show: false, @@ -169,49 +177,49 @@ new Vue({ } }, computed: { - filteredPayments: function () { + filteredPayments: function() { var q = this.paymentsTable.filter if (!q || q == '') return this.payments return LNbits.utils.search(this.payments, q) }, - balance: function () { + balance: function() { if (this.payments.length) { return ( - _.pluck(this.payments, 'amount').reduce(function (a, b) { + _.pluck(this.payments, 'amount').reduce(function(a, b) { return a + b }, 0) / 1000 ) } return this.g.wallet.sat }, - fbalance: function () { + fbalance: function() { return LNbits.utils.formatSat(this.balance) }, - canPay: function () { + canPay: function() { if (!this.send.invoice) return false return this.send.invoice.sat <= this.balance }, - pendingPaymentsExist: function () { + pendingPaymentsExist: function() { return this.payments ? _.where(this.payments, {pending: 1}).length > 0 : false } }, methods: { - closeCamera: function () { + closeCamera: function() { this.sendCamera.show = false }, - showCamera: function () { + showCamera: function() { this.sendCamera.show = true }, - showChart: function () { + showChart: function() { this.paymentsChart.show = true - this.$nextTick(function () { + this.$nextTick(function() { generateChart(this.$refs.canvas, this.payments) }) }, - showReceiveDialog: function () { + showReceiveDialog: function() { this.receive = { show: true, status: 'pending', @@ -223,7 +231,7 @@ new Vue({ paymentChecker: null } }, - showSendDialog: function () { + showSendDialog: function() { this.send = { show: true, invoice: null, @@ -233,20 +241,20 @@ new Vue({ paymentChecker: null } }, - closeReceiveDialog: function () { + closeReceiveDialog: function() { var checker = this.receive.paymentChecker - setTimeout(function () { + setTimeout(function() { clearInterval(checker) }, 10000) }, - closeSendDialog: function () { + closeSendDialog: function() { this.sendCamera.show = false var checker = this.send.paymentChecker - setTimeout(function () { + setTimeout(function() { clearInterval(checker) }, 1000) }, - createInvoice: function () { + createInvoice: function() { var self = this this.receive.status = 'loading' LNbits.api @@ -255,14 +263,14 @@ new Vue({ this.receive.data.amount, this.receive.data.memo ) - .then(function (response) { + .then(function(response) { self.receive.status = 'success' self.receive.paymentReq = response.data.payment_request - self.receive.paymentChecker = setInterval(function () { + self.receive.paymentChecker = setInterval(function() { LNbits.api - .getPayment(self.g.wallet, response.data.checking_id) - .then(function (response) { + .getPayment(self.g.wallet, response.data.payment_hash) + .then(function(response) { if (response.data.paid) { self.fetchPayments() self.receive.show = false @@ -271,17 +279,17 @@ new Vue({ }) }, 2000) }) - .catch(function (error) { + .catch(function(error) { LNbits.utils.notifyApiError(error) self.receive.status = 'pending' }) }, - decodeQR: function (res) { + decodeQR: function(res) { this.send.data.bolt11 = res this.decodeInvoice() this.sendCamera.show = false }, - decodeInvoice: function () { + decodeInvoice: function() { if (this.send.data.bolt11.startsWith('lightning:')) { this.send.data.bolt11 = this.send.data.bolt11.slice(10) } @@ -306,7 +314,7 @@ new Vue({ fsat: LNbits.utils.formatSat(invoice.human_readable_part.amount / 1000) } - _.each(invoice.data.tags, function (tag) { + _.each(invoice.data.tags, function(tag) { if (_.isObject(tag) && _.has(tag, 'description')) { if (tag.description == 'payment_hash') { cleanInvoice.hash = tag.value @@ -327,10 +335,10 @@ new Vue({ this.send.invoice = Object.freeze(cleanInvoice) }, - payInvoice: function () { + payInvoice: function() { var self = this - dismissPaymentMsg = this.$q.notify({ + let dismissPaymentMsg = this.$q.notify({ timeout: 0, message: 'Processing payment...', icon: null @@ -338,11 +346,11 @@ new Vue({ LNbits.api .payInvoice(this.g.wallet, this.send.data.bolt11) - .then(function (response) { - self.send.paymentChecker = setInterval(function () { + .then(function(response) { + self.send.paymentChecker = setInterval(function() { LNbits.api - .getPayment(self.g.wallet, response.data.checking_id) - .then(function (res) { + .getPayment(self.g.wallet, response.data.payment_hash) + .then(function(res) { if (res.data.paid) { self.send.show = false clearInterval(self.send.paymentChecker) @@ -352,58 +360,58 @@ new Vue({ }) }, 2000) }) - .catch(function (error) { + .catch(function(error) { dismissPaymentMsg() LNbits.utils.notifyApiError(error) }) }, - deleteWallet: function (walletId, user) { + deleteWallet: function(walletId, user) { LNbits.utils .confirmDialog('Are you sure you want to delete this wallet?') - .onOk(function () { + .onOk(function() { LNbits.href.deleteWallet(walletId, user) }) }, - fetchPayments: function (checkPending) { + fetchPayments: function(checkPending) { var self = this return LNbits.api .getPayments(this.g.wallet, checkPending) - .then(function (response) { + .then(function(response) { self.payments = response.data - .map(function (obj) { + .map(function(obj) { return LNbits.map.payment(obj) }) - .sort(function (a, b) { + .sort(function(a, b) { return b.time - a.time }) }) }, - checkPendingPayments: function () { + checkPendingPayments: function() { var dismissMsg = this.$q.notify({ timeout: 0, message: 'Checking pending transactions...', icon: null }) - this.fetchPayments(true).then(function () { + this.fetchPayments(true).then(function() { dismissMsg() }) }, - exportCSV: function () { + exportCSV: function() { LNbits.utils.exportCSV(this.paymentsTable.columns, this.payments) } }, watch: { - payments: function () { + payments: function() { EventHub.$emit('update-wallet-balance', [this.g.wallet.id, this.balance]) } }, - created: function () { + created: function() { this.fetchPayments() setTimeout(this.checkPendingPayments(), 1200) }, - mounted: function () { + mounted: function() { if ( this.$refs.disclaimer && !this.$q.localStorage.getItem('lnbits.disclaimerShown') diff --git a/lnbits/core/templates/core/_api_docs.html b/lnbits/core/templates/core/_api_docs.html index 38c3568..f1fddd5 100644 --- a/lnbits/core/templates/core/_api_docs.html +++ b/lnbits/core/templates/core/_api_docs.html @@ -23,7 +23,7 @@ Returns 201 CREATED (application/json) {"checking_id": <string>, "payment_request": + >{"payment_hash": <string>, "payment_request": <string>}
Curl example
@@ -51,7 +51,7 @@
Returns 201 CREATED (application/json)
- {"checking_id": <string>} + {"payment_hash": <string>}
Curl example
curl -X POST {{ request.url_root }}api/v1/payments -d '{"out": true, @@ -73,7 +73,7 @@ GET - /api/v1/payments/<checking_id>
Headers
{"X-Api-Key": "{{ wallet.inkey }}"} @@ -83,9 +83,9 @@ {"paid": <bool>}
Curl example
curl -X GET {{ request.url_root }}api/v1/payments/<checking_id> - -H "X-Api-Key: {{ wallet.inkey }}" -H "Content-type: - application/json"curl -X GET {{ request.url_root + }}api/v1/payments/<payment_hash> -H "X-Api-Key: + {{ wallet.inkey }}" -H "Content-type: application/json"
diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html index 6d19bca..bc24876 100644 --- a/lnbits/core/templates/core/wallet.html +++ b/lnbits/core/templates/core/wallet.html @@ -84,7 +84,7 @@ dense flat :data="filteredPayments" - row-key="payhash" + row-key="checking_id" :columns="paymentsTable.columns" :pagination.sync="paymentsTable.pagination" > diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 0567031..4018fbd 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -2,12 +2,12 @@ from flask import g, jsonify, request from http import HTTPStatus from binascii import unhexlify +from lnbits import bolt11 from lnbits.core import core_app +from lnbits.core.services import create_invoice, pay_invoice from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.settings import WALLET -from ..services import create_invoice, pay_invoice - @core_app.route("/api/v1/payments", methods=["GET"]) @api_check_wallet_key("invoice") @@ -41,20 +41,31 @@ def api_payments_create_invoice(): memo = g.data["memo"] try: - checking_id, payment_request = create_invoice( + payment_hash, payment_request = create_invoice( wallet_id=g.wallet.id, amount=g.data["amount"], memo=memo, description_hash=description_hash ) except Exception as e: return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR - return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.CREATED + invoice = bolt11.decode(payment_request) + return ( + jsonify( + { + "payment_hash": invoice.payment_hash, + "payment_request": payment_request, + # maintain backwards compatibility with API clients: + "checking_id": invoice.payment_hash, + } + ), + HTTPStatus.CREATED, + ) @api_check_wallet_key("admin") @api_validate_post_request(schema={"bolt11": {"type": "string", "empty": False, "required": True}}) def api_payments_pay_invoice(): try: - checking_id = pay_invoice(wallet_id=g.wallet.id, bolt11=g.data["bolt11"]) + payment_hash = pay_invoice(wallet_id=g.wallet.id, payment_request=g.data["bolt11"]) except ValueError as e: return jsonify({"message": str(e)}), HTTPStatus.BAD_REQUEST except PermissionError as e: @@ -62,7 +73,16 @@ def api_payments_pay_invoice(): except Exception as e: return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR - return jsonify({"checking_id": checking_id}), HTTPStatus.CREATED + return ( + jsonify( + { + "payment_hash": payment_hash, + # maintain backwards compatibility with API clients: + "checking_id": payment_hash, + } + ), + HTTPStatus.CREATED, + ) @core_app.route("/api/v1/payments", methods=["POST"]) @@ -73,10 +93,10 @@ def api_payments_create(): return api_payments_create_invoice() -@core_app.route("/api/v1/payments/", methods=["GET"]) +@core_app.route("/api/v1/payments/", methods=["GET"]) @api_check_wallet_key("invoice") -def api_payment(checking_id): - payment = g.wallet.get_payment(checking_id) +def api_payment(payment_hash): + payment = g.wallet.get_payment(payment_hash) if not payment: return jsonify({"message": "Payment does not exist."}), HTTPStatus.NOT_FOUND @@ -85,9 +105,9 @@ def api_payment(checking_id): try: if payment.is_out: - is_paid = not WALLET.get_payment_status(checking_id).pending + is_paid = not WALLET.get_payment_status(payment.checking_id).pending elif payment.is_in: - is_paid = not WALLET.get_invoice_status(checking_id).pending + is_paid = not WALLET.get_invoice_status(payment.checking_id).pending except Exception: return jsonify({"paid": False}), HTTPStatus.OK diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index f712cb4..62b194e 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -64,7 +64,7 @@ def wallet(): allowed_users = getenv("LNBITS_ALLOWED_USERS", "all") if allowed_users != "all" and user_id not in allowed_users.split(","): - abort(HTTPStatus.UNAUTHORIZED, f"User not authorized.") + abort(HTTPStatus.UNAUTHORIZED, "User not authorized.") if not wallet_id: if user.wallets and not wallet_name: diff --git a/lnbits/extensions/amilk/views_api.py b/lnbits/extensions/amilk/views_api.py index 6d51da0..dccad42 100644 --- a/lnbits/extensions/amilk/views_api.py +++ b/lnbits/extensions/amilk/views_api.py @@ -1,19 +1,16 @@ -from flask import g, jsonify, request +import requests +from flask import g, jsonify, request, abort from http import HTTPStatus +from lnurl import LnurlWithdrawResponse, handle as handle_lnurl +from lnurl.exceptions import LnurlException +from time import sleep from lnbits.core.crud import get_user from lnbits.decorators import api_check_wallet_key, api_validate_post_request +from lnbits.core.services import create_invoice, check_invoice_status from lnbits.extensions.amilk import amilk_ext from .crud import create_amilk, get_amilk, get_amilks, delete_amilk -from lnbits.core.services import create_invoice - -from flask import abort, redirect, request, url_for -from lnurl import LnurlWithdrawResponse, handle as handle_lnurl -from lnurl.exceptions import LnurlException -from time import sleep -import requests -from lnbits.settings import WALLET @amilk_ext.route("/api/v1/amilk", methods=["GET"]) @@ -36,13 +33,8 @@ def api_amilkit(amilk_id): withdraw_res = handle_lnurl(milk.lnurl, response_class=LnurlWithdrawResponse) except LnurlException: abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.") - print(withdraw_res.max_sats) - try: - checking_id, payment_request = create_invoice(wallet_id=milk.wallet, amount=withdraw_res.max_sats, memo=memo) - # print(payment_request) - except Exception as e: - error_message = False, str(e) + payment_hash, payment_request = create_invoice(wallet_id=milk.wallet, amount=withdraw_res.max_sats, memo=memo) r = requests.get( withdraw_res.callback.base, @@ -50,19 +42,17 @@ def api_amilkit(amilk_id): ) if not r.ok: - abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.") for i in range(10): - invoice_status = WALLET.get_invoice_status(checking_id) sleep(i) - if not invoice_status.paid: - continue + invoice_status = check_invoice_status(milk.wallet, payment_hash) + if invoice_status.paid: + return jsonify({"paid": True}), HTTPStatus.OK else: - return jsonify({"paid": False}), HTTPStatus.OK - break + continue - return jsonify({"paid": True}), HTTPStatus.OK + return jsonify({"paid": False}), HTTPStatus.OK @amilk_ext.route("/api/v1/amilk", methods=["POST"]) diff --git a/lnbits/extensions/events/crud.py b/lnbits/extensions/events/crud.py index 1c8c968..b7219f2 100644 --- a/lnbits/extensions/events/crud.py +++ b/lnbits/extensions/events/crud.py @@ -9,31 +9,31 @@ from .models import Tickets, Events #######TICKETS######## -def create_ticket(checking_id: str, wallet: str, event: str, name: str, email: str) -> Tickets: +def create_ticket(payment_hash: str, wallet: str, event: str, name: str, email: str) -> Tickets: with open_ext_db("events") as db: db.execute( """ INSERT INTO ticket (id, wallet, event, name, email, registered, paid) VALUES (?, ?, ?, ?, ?, ?, ?) """, - (checking_id, wallet, event, name, email, False, False), + (payment_hash, wallet, event, name, email, False, False), ) - return get_ticket(checking_id) + return get_ticket(payment_hash) -def update_ticket(paid: bool, checking_id: str) -> Tickets: +def update_ticket(paid: bool, payment_hash: str) -> Tickets: with open_ext_db("events") as db: - row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (checking_id,)) + row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (payment_hash,)) if row[6] == True: - return get_ticket(checking_id) + return get_ticket(payment_hash) db.execute( """ UPDATE ticket SET paid = ? WHERE id = ? """, - (paid, checking_id), + (paid, payment_hash), ) eventdata = get_event(row[2]) @@ -47,12 +47,12 @@ def update_ticket(paid: bool, checking_id: str) -> Tickets: """, (sold, amount_tickets, row[2]), ) - return get_ticket(checking_id) + return get_ticket(payment_hash) -def get_ticket(checking_id: str) -> Optional[Tickets]: +def get_ticket(payment_hash: str) -> Optional[Tickets]: with open_ext_db("events") as db: - row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (checking_id,)) + row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (payment_hash,)) return Tickets(**row) if row else None @@ -68,9 +68,9 @@ def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]: return [Tickets(**row) for row in rows] -def delete_ticket(checking_id: str) -> None: +def delete_ticket(payment_hash: str) -> None: with open_ext_db("events") as db: - db.execute("DELETE FROM ticket WHERE id = ?", (checking_id,)) + db.execute("DELETE FROM ticket WHERE id = ?", (payment_hash,)) ########EVENTS######### diff --git a/lnbits/extensions/events/templates/events/display.html b/lnbits/extensions/events/templates/events/display.html index 59a06ef..542feb9 100644 --- a/lnbits/extensions/events/templates/events/display.html +++ b/lnbits/extensions/events/templates/events/display.html @@ -144,7 +144,7 @@ ) .then(function (response) { self.paymentReq = response.data.payment_request - self.paymentCheck = response.data.checking_id + self.paymentCheck = response.data.payment_hash dismissMsg = self.$q.notify({ timeout: 0, diff --git a/lnbits/extensions/events/views_api.py b/lnbits/extensions/events/views_api.py index cfc67ff..0680cbe 100644 --- a/lnbits/extensions/events/views_api.py +++ b/lnbits/extensions/events/views_api.py @@ -2,9 +2,8 @@ from flask import g, jsonify, request from http import HTTPStatus from lnbits.core.crud import get_user, get_wallet -from lnbits.core.services import create_invoice +from lnbits.core.services import create_invoice, check_invoice_status from lnbits.decorators import api_check_wallet_key, api_validate_post_request -from lnbits.settings import WALLET from lnbits.extensions.events import events_ext from .crud import ( @@ -108,39 +107,37 @@ def api_tickets(): } ) def api_ticket_make_ticket(event_id, sats): - event = get_event(event_id) - if not event: return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND try: - checking_id, payment_request = create_invoice( + payment_hash, payment_request = create_invoice( wallet_id=event.wallet, amount=int(sats), memo=f"#lnticket {event_id}" ) except Exception as e: return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR - ticket = create_ticket(checking_id=checking_id, wallet=event.wallet, event=event_id, **g.data) + ticket = create_ticket(payment_hash=payment_hash, wallet=event.wallet, event=event_id, **g.data) if not ticket: return jsonify({"message": "LNTicket could not be fetched."}), HTTPStatus.NOT_FOUND - return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.OK + return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.OK -@events_ext.route("/api/v1/tickets/", methods=["GET"]) -def api_ticket_send_ticket(checking_id): - theticket = get_ticket(checking_id) +@events_ext.route("/api/v1/tickets/", methods=["GET"]) +def api_ticket_send_ticket(payment_hash): + ticket = get_ticket(payment_hash) try: - is_paid = not WALLET.get_invoice_status(checking_id).pending + is_paid = not check_invoice_status(ticket.wallet, payment_hash).pending except Exception: return jsonify({"message": "Not paid."}), HTTPStatus.NOT_FOUND if is_paid: - wallet = get_wallet(theticket.wallet) - payment = wallet.get_payment(checking_id) + wallet = get_wallet(ticket.wallet) + payment = wallet.get_payment(payment_hash) payment.set_pending(False) - ticket = update_ticket(paid=True, checking_id=checking_id) + ticket = update_ticket(paid=True, payment_hash=payment_hash) return jsonify({"paid": True, "ticket_id": ticket.id}), HTTPStatus.OK diff --git a/lnbits/extensions/lnticket/crud.py b/lnbits/extensions/lnticket/crud.py index 5a532fc..561137d 100644 --- a/lnbits/extensions/lnticket/crud.py +++ b/lnbits/extensions/lnticket/crud.py @@ -9,31 +9,31 @@ from .models import Tickets, Forms #######TICKETS######## -def create_ticket(checking_id: str, wallet: str, form: str, name: str, email: str, ltext: str, sats: int) -> Tickets: +def create_ticket(payment_hash: str, wallet: str, form: str, name: str, email: str, ltext: str, sats: int) -> Tickets: with open_ext_db("lnticket") as db: db.execute( """ INSERT INTO ticket (id, form, email, ltext, name, wallet, sats, paid) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, - (checking_id, form, email, ltext, name, wallet, sats, False), + (payment_hash, form, email, ltext, name, wallet, sats, False), ) - return get_ticket(checking_id) + return get_ticket(payment_hash) -def update_ticket(paid: bool, checking_id: str) -> Tickets: +def update_ticket(paid: bool, payment_hash: str) -> Tickets: with open_ext_db("lnticket") as db: - row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (checking_id,)) + row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (payment_hash,)) if row[7] == True: - return get_ticket(checking_id) + return get_ticket(payment_hash) db.execute( """ UPDATE ticket SET paid = ? WHERE id = ? """, - (paid, checking_id), + (paid, payment_hash), ) formdata = get_form(row[1]) @@ -46,7 +46,7 @@ def update_ticket(paid: bool, checking_id: str) -> Tickets: """, (amount, row[1]), ) - return get_ticket(checking_id) + return get_ticket(payment_hash) def get_ticket(ticket_id: str) -> Optional[Tickets]: diff --git a/lnbits/extensions/lnticket/templates/lnticket/display.html b/lnbits/extensions/lnticket/templates/lnticket/display.html index 1b72615..9001519 100644 --- a/lnbits/extensions/lnticket/templates/lnticket/display.html +++ b/lnbits/extensions/lnticket/templates/lnticket/display.html @@ -152,7 +152,7 @@ ) .then(function (response) { self.paymentReq = response.data.payment_request - self.paymentCheck = response.data.checking_id + self.paymentCheck = response.data.payment_hash dismissMsg = self.$q.notify({ timeout: 0, diff --git a/lnbits/extensions/lnticket/views_api.py b/lnbits/extensions/lnticket/views_api.py index 69a2ac8..be5cb8a 100644 --- a/lnbits/extensions/lnticket/views_api.py +++ b/lnbits/extensions/lnticket/views_api.py @@ -2,9 +2,8 @@ from flask import g, jsonify, request from http import HTTPStatus from lnbits.core.crud import get_user, get_wallet -from lnbits.core.services import create_invoice +from lnbits.core.services import create_invoice, check_invoice_status from lnbits.decorators import api_check_wallet_key, api_validate_post_request -from lnbits.settings import WALLET from lnbits.extensions.lnticket import lnticket_ext from .crud import ( @@ -104,40 +103,38 @@ def api_tickets(): } ) def api_ticket_make_ticket(form_id, sats): - event = get_form(form_id) if not event: return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND try: - checking_id, payment_request = create_invoice( + payment_hash, payment_request = create_invoice( wallet_id=event.wallet, amount=int(sats), memo=f"#lnticket {form_id}" ) except Exception as e: return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR - ticket = create_ticket(checking_id=checking_id, wallet=event.wallet, **g.data) + ticket = create_ticket(payment_hash=payment_hash, wallet=event.wallet, **g.data) if not ticket: return jsonify({"message": "LNTicket could not be fetched."}), HTTPStatus.NOT_FOUND - return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.OK + return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.OK -@lnticket_ext.route("/api/v1/tickets/", methods=["GET"]) -def api_ticket_send_ticket(checking_id): - theticket = get_ticket(checking_id) +@lnticket_ext.route("/api/v1/tickets/", methods=["GET"]) +def api_ticket_send_ticket(payment_hash): + ticket = get_ticket(payment_hash) try: - is_paid = not WALLET.get_invoice_status(checking_id).pending + is_paid = not check_invoice_status(ticket.wallet, payment_hash).pending except Exception: return jsonify({"message": "Not paid."}), HTTPStatus.NOT_FOUND if is_paid: - wallet = get_wallet(theticket.wallet) - payment = wallet.get_payment(checking_id) + wallet = get_wallet(ticket.wallet) + payment = wallet.get_payment(payment_hash) payment.set_pending(False) - ticket = update_ticket(paid=True, checking_id=checking_id) - + ticket = update_ticket(paid=True, payment_hash=payment_hash) return jsonify({"paid": True, "ticket_id": ticket.id}), HTTPStatus.OK return jsonify({"paid": False}), HTTPStatus.OK diff --git a/lnbits/extensions/paywall/templates/paywall/_api_docs.html b/lnbits/extensions/paywall/templates/paywall/_api_docs.html index 60e6108..56dbf56 100644 --- a/lnbits/extensions/paywall/templates/paywall/_api_docs.html +++ b/lnbits/extensions/paywall/templates/paywall/_api_docs.html @@ -75,7 +75,7 @@ Returns 201 CREATED (application/json) {"checking_id": <string>, "payment_request": + >{"payment_hash": <string>, "payment_request": <string>}
Curl example
@@ -100,7 +100,7 @@ /paywall/api/v1/paywalls/<paywall_id>/check_invoice
Body (application/json)
- {"checking_id": <string>} + {"payment_hash": <string>}
Returns 200 OK (application/json)
@@ -113,7 +113,7 @@ curl -X POST {{ request.url_root }}paywall/api/v1/paywalls/<paywall_id>/check_invoice -d - '{"checking_id": <string>}' -H "Content-type: application/json" + '{"payment_hash": <string>}' -H "Content-type: application/json" diff --git a/lnbits/extensions/paywall/templates/paywall/display.html b/lnbits/extensions/paywall/templates/paywall/display.html index f3b7c6f..b9248ed 100644 --- a/lnbits/extensions/paywall/templates/paywall/display.html +++ b/lnbits/extensions/paywall/templates/paywall/display.html @@ -121,7 +121,7 @@ axios .post( '/paywall/api/v1/paywalls/{{ paywall.id }}/check_invoice', - {checking_id: response.data.checking_id} + {payment_hash: response.data.payment_hash} ) .then(function (res) { if (res.data.paid) { diff --git a/lnbits/extensions/paywall/views_api.py b/lnbits/extensions/paywall/views_api.py index 012d355..96b616c 100644 --- a/lnbits/extensions/paywall/views_api.py +++ b/lnbits/extensions/paywall/views_api.py @@ -2,9 +2,8 @@ from flask import g, jsonify, request from http import HTTPStatus from lnbits.core.crud import get_user, get_wallet -from lnbits.core.services import create_invoice +from lnbits.core.services import create_invoice, check_invoice_status from lnbits.decorators import api_check_wallet_key, api_validate_post_request -from lnbits.settings import WALLET from lnbits.extensions.paywall import paywall_ext from .crud import create_paywall, get_paywall, get_paywalls, delete_paywall @@ -64,17 +63,17 @@ def api_paywall_create_invoice(paywall_id): try: amount = g.data["amount"] if g.data["amount"] > paywall.amount else paywall.amount - checking_id, payment_request = create_invoice( + payment_hash, payment_request = create_invoice( wallet_id=paywall.wallet, amount=amount, memo=f"#paywall {paywall.memo}" ) except Exception as e: return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR - return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.CREATED + return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.CREATED @paywall_ext.route("/api/v1/paywalls//check_invoice", methods=["POST"]) -@api_validate_post_request(schema={"checking_id": {"type": "string", "empty": False, "required": True}}) +@api_validate_post_request(schema={"payment_hash": {"type": "string", "empty": False, "required": True}}) def api_paywal_check_invoice(paywall_id): paywall = get_paywall(paywall_id) @@ -82,13 +81,13 @@ def api_paywal_check_invoice(paywall_id): return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND try: - is_paid = not WALLET.get_invoice_status(g.data["checking_id"]).pending + is_paid = not check_invoice_status(paywall.wallet, g.data["payment_hash"]).pending except Exception: return jsonify({"paid": False}), HTTPStatus.OK if is_paid: wallet = get_wallet(paywall.wallet) - payment = wallet.get_payment(g.data["checking_id"]) + payment = wallet.get_payment(g.data["payment_hash"]) payment.set_pending(False) return jsonify({"paid": True, "url": paywall.url, "remembers": paywall.remembers}), HTTPStatus.OK diff --git a/lnbits/extensions/tpos/templates/tpos/tpos.html b/lnbits/extensions/tpos/templates/tpos/tpos.html index 6d96233..800114a 100644 --- a/lnbits/extensions/tpos/templates/tpos/tpos.html +++ b/lnbits/extensions/tpos/templates/tpos/tpos.html @@ -224,7 +224,7 @@ '/tpos/api/v1/tposs/' + self.tposId + '/invoices/' + - response.data.checking_id + response.data.payment_hash ) .then(function (res) { if (res.data.paid) { diff --git a/lnbits/extensions/tpos/views_api.py b/lnbits/extensions/tpos/views_api.py index c273ab1..c80fed8 100644 --- a/lnbits/extensions/tpos/views_api.py +++ b/lnbits/extensions/tpos/views_api.py @@ -2,9 +2,8 @@ from flask import g, jsonify, request from http import HTTPStatus from lnbits.core.crud import get_user, get_wallet -from lnbits.core.services import create_invoice +from lnbits.core.services import create_invoice, check_invoice_status from lnbits.decorators import api_check_wallet_key, api_validate_post_request -from lnbits.settings import WALLET from lnbits.extensions.tpos import tpos_ext from .crud import create_tpos, get_tpos, get_tposs, delete_tpos @@ -60,30 +59,31 @@ def api_tpos_create_invoice(tpos_id): return jsonify({"message": "TPoS does not exist."}), HTTPStatus.NOT_FOUND try: - checking_id, payment_request = create_invoice( + payment_hash, payment_request = create_invoice( wallet_id=tpos.wallet, amount=g.data["amount"], memo=f"#tpos {tpos.name}" ) except Exception as e: return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR - return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.CREATED + return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.CREATED -@tpos_ext.route("/api/v1/tposs//invoices/", methods=["GET"]) -def api_tpos_check_invoice(tpos_id, checking_id): +@tpos_ext.route("/api/v1/tposs//invoices/", methods=["GET"]) +def api_tpos_check_invoice(tpos_id, payment_hash): tpos = get_tpos(tpos_id) if not tpos: return jsonify({"message": "TPoS does not exist."}), HTTPStatus.NOT_FOUND try: - is_paid = not WALLET.get_invoice_status(checking_id).pending - except Exception: + is_paid = not check_invoice_status(tpos.wallet, payment_hash).pending + except Exception as exc: + print(exc) return jsonify({"paid": False}), HTTPStatus.OK if is_paid: wallet = get_wallet(tpos.wallet) - payment = wallet.get_payment(checking_id) + payment = wallet.get_payment(payment_hash) payment.set_pending(False) return jsonify({"paid": True}), HTTPStatus.OK diff --git a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html index c9f80e5..6a0980c 100644 --- a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html +++ b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html @@ -122,7 +122,8 @@ Returns 201 CREATED (application/json) {"checking_id": <string>,"payment_request": + >{"id": <string>, "name": <string>, "admin": + <string>, "email": <string>, "password": <string>}
Curl example
@@ -158,8 +159,9 @@ Returns 201 CREATED (application/json) {"checking_id": <string>,"payment_request": - <string>}{"id": <string>, "admin": <string>, "name": + <string>, "user": <string>, "adminkey": <string>, + "inkey": <string>}
Curl example
0 obj.isOut = obj.amount < 0 - obj.isPaid = obj.pending == 0 + obj.isPaid = obj.pending === 0 obj._q = [obj.memo, obj.sat].join(' ').toLowerCase() return obj } }, utils: { - confirmDialog: function (msg) { + confirmDialog: function(msg) { return Quasar.plugins.Dialog.create({ message: msg, ok: { @@ -119,16 +125,16 @@ var LNbits = { } }) }, - formatCurrency: function (value, currency) { + formatCurrency: function(value, currency) { return new Intl.NumberFormat(LOCALE, { style: 'currency', currency: currency }).format(value) }, - formatSat: function (value) { + formatSat: function(value) { return new Intl.NumberFormat(LOCALE).format(value) }, - notifyApiError: function (error) { + notifyApiError: function(error) { var types = { 400: 'warning', 401: 'warning', @@ -145,24 +151,22 @@ var LNbits = { icon: null }) }, - search: function (data, q, field, separator) { - var field = field || '_q' - + search: function(data, q, field, separator) { try { var queries = q.toLowerCase().split(separator || ' ') - return data.filter(function (obj) { + return data.filter(function(obj) { var matches = 0 - _.each(queries, function (q) { + _.each(queries, function(q) { if (obj[field].indexOf(q) !== -1) matches++ }) - return matches == queries.length + return matches === queries.length }) } catch (err) { return data } }, - exportCSV: function (columns, data) { - var wrapCsvValue = function (val, formatFn) { + exportCSV: function(columns, data) { + var wrapCsvValue = function(val, formatFn) { var formatted = formatFn !== void 0 ? formatFn(val) : val formatted = @@ -174,14 +178,14 @@ var LNbits = { } var content = [ - columns.map(function (col) { + columns.map(function(col) { return wrapCsvValue(col.label) }) ] .concat( - data.map(function (row) { + data.map(function(row) { return columns - .map(function (col) { + .map(function(col) { return wrapCsvValue( typeof col.field === 'function' ? col.field(row) @@ -212,7 +216,7 @@ var LNbits = { } var windowMixin = { - data: function () { + data: function() { return { g: { visibleDrawer: false, @@ -224,13 +228,13 @@ var windowMixin = { } }, methods: { - toggleDarkMode: function () { + toggleDarkMode: function() { this.$q.dark.toggle() this.$q.localStorage.set('lnbits.darkMode', this.$q.dark.isActive) }, - copyText: function (text, message, position) { + copyText: function(text, message, position) { var notify = this.$q.notify - Quasar.utils.copyToClipboard(text).then(function () { + Quasar.utils.copyToClipboard(text).then(function() { notify({ message: message || 'Copied to clipboard!', position: position || 'bottom' @@ -238,7 +242,7 @@ var windowMixin = { }) } }, - created: function () { + created: function() { this.$q.dark.set(this.$q.localStorage.getItem('lnbits.darkMode')) if (window.user) { this.g.user = Object.freeze(LNbits.map.user(window.user)) @@ -250,18 +254,18 @@ var windowMixin = { var user = this.g.user this.g.extensions = Object.freeze( window.extensions - .map(function (data) { + .map(function(data) { return LNbits.map.extension(data) }) - .map(function (obj) { + .map(function(obj) { if (user) { - obj.isEnabled = user.extensions.indexOf(obj.code) != -1 + obj.isEnabled = user.extensions.indexOf(obj.code) !== -1 } else { obj.isEnabled = false } return obj }) - .sort(function (a, b) { + .sort(function(a, b) { return a.name > b.name }) ) From b56877d470aaa04c0ed872197d6a0183fd7721a8 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 1 Sep 2020 22:36:52 -0300 Subject: [PATCH 3/9] make db.py context manager operate in a transaction. --- lnbits/db.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lnbits/db.py b/lnbits/db.py index d9a8660..316bb21 100644 --- a/lnbits/db.py +++ b/lnbits/db.py @@ -15,22 +15,26 @@ class Database: return self def __exit__(self, exc_type, exc_val, exc_tb): + self.connection.commit() self.cursor.close() self.connection.close() def fetchall(self, query: str, values: tuple = ()) -> list: """Given a query, return cursor.fetchall() rows.""" - self.cursor.execute(query, values) + self.execute(query, values) return self.cursor.fetchall() def fetchone(self, query: str, values: tuple = ()): - self.cursor.execute(query, values) + self.execute(query, values) return self.cursor.fetchone() def execute(self, query: str, values: tuple = ()) -> None: """Given a query, cursor.execute() it.""" - self.cursor.execute(query, values) - self.connection.commit() + try: + self.cursor.execute(query, values) + except sqlite3.Error as exc: + self.connection.rollback() + raise exc def open_db(db_name: str = "database") -> Database: From d2650d6e2c2c39da59735d871701c5197fa0c8ec Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 2 Sep 2020 00:32:52 -0300 Subject: [PATCH 4/9] don't try to check payments that start with temp_ or internal_. --- lnbits/core/models.py | 4 ++++ lnbits/core/services.py | 4 +++- lnbits/core/views/api.py | 8 ++++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lnbits/core/models.py b/lnbits/core/models.py index 9dec751..175c5fe 100644 --- a/lnbits/core/models.py +++ b/lnbits/core/models.py @@ -96,6 +96,10 @@ class Payment(NamedTuple): def is_out(self) -> bool: return self.amount < 0 + @property + def is_uncheckable(self) -> bool: + return self.checking_id.startswith("temp_") or self.checking_id.startswith("internal_") + def set_pending(self, pending: bool) -> None: from .crud import update_payment_status diff --git a/lnbits/core/services.py b/lnbits/core/services.py index e074a34..c15f78a 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -45,6 +45,8 @@ def pay_invoice( *, wallet_id: str, payment_request: str, max_sat: Optional[int] = None, extra: Optional[Dict] = None ) -> str: temp_id = f"temp_{urlsafe_short_hash()}" + internal_id = f"internal_{urlsafe_short_hash()}" + try: invoice = bolt11.decode(payment_request) if invoice.amount_msat == 0: @@ -66,7 +68,7 @@ def pay_invoice( internal = check_internal(invoice.payment_hash) if internal: # create a new payment from this wallet - create_payment(checking_id=temp_id, fee=0, pending=False, **payment_kwargs) + create_payment(checking_id=internal_id, fee=0, pending=False, **payment_kwargs) else: # create a temporary payment here so we can check if # the balance is enough in the next step diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 4018fbd..52c67cd 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -16,7 +16,9 @@ def api_payments(): g.wallet.delete_expired_payments() for payment in g.wallet.get_payments(complete=False, pending=True): - if payment.is_out: + if payment.is_uncheckable: + pass + elif payment.is_out: payment.set_pending(WALLET.get_payment_status(payment.checking_id).pending) else: payment.set_pending(WALLET.get_invoice_status(payment.checking_id).pending) @@ -104,7 +106,9 @@ def api_payment(payment_hash): return jsonify({"paid": True}), HTTPStatus.OK try: - if payment.is_out: + if payment.is_uncheckable: + pass + elif payment.is_out: is_paid = not WALLET.get_payment_status(payment.checking_id).pending elif payment.is_in: is_paid = not WALLET.get_invoice_status(payment.checking_id).pending From 4447a487240bb65679ec7e5bbb0b53ffdcd73ec4 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 2 Sep 2020 00:58:21 -0300 Subject: [PATCH 5/9] deleting expired invoices based on their actual expiry date. also fixes a possible bug that could have caused pending outgoing payments to be deleted and affecting the balance. probably never happened. --- lnbits/core/crud.py | 33 ++++++++++++++++++++++++++------- lnbits/core/models.py | 5 ----- lnbits/core/views/api.py | 3 ++- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 2c3ae7a..50fe158 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -1,7 +1,9 @@ -from typing import List, Optional, Dict +import datetime from uuid import uuid4 +from typing import List, Optional, Dict from lnbits.db import open_db +from lnbits import bolt11 from lnbits.settings import DEFAULT_WALLET_NAME from .models import User, Wallet, Payment @@ -190,15 +192,32 @@ def get_wallet_payments( return [Payment.from_row(row) for row in rows] -def delete_wallet_payments_expired(wallet_id: str, *, seconds: int = 86400) -> None: +def delete_expired_invoices() -> None: with open_db() as db: - db.execute( + rows = db.fetchall( """ - DELETE - FROM apipayments WHERE wallet = ? AND pending = 1 AND time < strftime('%s', 'now') - ? - """, - (wallet_id, seconds), + SELECT bolt11 + FROM apipayments + WHERE pending = 1 AND amount > 0 AND time < strftime('%s', 'now') - 86400 + """ ) + for (payment_request,) in rows: + try: + invoice = bolt11.decode(payment_request) + except: + continue + + expiration_date = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry) + if expiration_date > datetime.datetime.utcnow(): + continue + + db.execute( + """ + DELETE FROM apipayments + WHERE pending = 1 AND payment_hash = ? + """, + (invoice.payment_hash,), + ) # payments diff --git a/lnbits/core/models.py b/lnbits/core/models.py index 175c5fe..ed4ed43 100644 --- a/lnbits/core/models.py +++ b/lnbits/core/models.py @@ -43,11 +43,6 @@ class Wallet(NamedTuple): return get_wallet_payments(self.id, complete=complete, pending=pending, outgoing=outgoing, incoming=incoming) - def delete_expired_payments(self, seconds: int = 86400) -> None: - from .crud import delete_wallet_payments_expired - - delete_wallet_payments_expired(self.id, seconds=seconds) - class Payment(NamedTuple): checking_id: str diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 52c67cd..9a587ee 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -5,6 +5,7 @@ from binascii import unhexlify from lnbits import bolt11 from lnbits.core import core_app from lnbits.core.services import create_invoice, pay_invoice +from lnbits.core.crud import delete_expired_invoices from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.settings import WALLET @@ -13,7 +14,7 @@ from lnbits.settings import WALLET @api_check_wallet_key("invoice") def api_payments(): if "check_pending" in request.args: - g.wallet.delete_expired_payments() + delete_expired_invoices() for payment in g.wallet.get_payments(complete=False, pending=True): if payment.is_uncheckable: From 197af922d003cc7700ab5db5f4c451219589e12b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 2 Sep 2020 12:44:54 -0300 Subject: [PATCH 6/9] use {"tag": ext} for extension-related payments. --- lnbits/core/crud.py | 14 ++++++++++- lnbits/core/migrations.py | 20 ++++++++++++++++ lnbits/core/static/js/wallet.js | 18 ++++++++++----- lnbits/core/templates/core/wallet.html | 11 ++++++--- lnbits/extensions/amilk/views_api.py | 4 +++- lnbits/extensions/events/views_api.py | 6 ++--- .../lnticket/templates/lnticket/display.html | 15 ++++-------- lnbits/extensions/lnticket/views_api.py | 23 +++++++++++-------- lnbits/extensions/lnurlp/views_api.py | 1 + lnbits/extensions/paywall/views_api.py | 2 +- lnbits/extensions/tpos/views_api.py | 2 +- lnbits/extensions/withdraw/models.py | 2 +- lnbits/extensions/withdraw/views_api.py | 7 +++++- lnbits/static/js/base.js | 14 ++++++++++- 14 files changed, 100 insertions(+), 39 deletions(-) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 50fe158..2d064af 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -1,3 +1,4 @@ +import json import datetime from uuid import uuid4 from typing import List, Optional, Dict @@ -245,7 +246,18 @@ def create_payment( amount, pending, memo, fee, extra) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, - (wallet_id, checking_id, payment_request, payment_hash, preimage, amount, int(pending), memo, fee, extra), + ( + wallet_id, + checking_id, + payment_request, + payment_hash, + preimage, + amount, + int(pending), + memo, + fee, + json.dumps(extra) if extra and extra != {} and type(extra) is dict else None, + ), ) new_payment = get_wallet_payment(wallet_id, payment_hash) diff --git a/lnbits/core/migrations.py b/lnbits/core/migrations.py index 826b217..78631f4 100644 --- a/lnbits/core/migrations.py +++ b/lnbits/core/migrations.py @@ -83,6 +83,26 @@ def m002_add_fields_to_apipayments(db): db.execute("ALTER TABLE apipayments ADD COLUMN bolt11 TEXT") db.execute("ALTER TABLE apipayments ADD COLUMN extra TEXT") + import json + + rows = db.fetchall("SELECT * FROM apipayments") + for row in rows: + if not row["memo"] or not row["memo"].startswith("#"): + continue + + for ext in ["withdraw", "events", "lnticket", "paywall", "tpos"]: + prefix = "#" + ext + " " + if row["memo"].startswith(prefix): + new = row["memo"][len(prefix) :] + db.execute( + """ + UPDATE apipayments SET extra = ?, memo = ? + WHERE checking_id = ? AND memo = ? + """, + (json.dumps({"tag": ext}), new, row["checking_id"], row["memo"]), + ) + break + def migrate(): with open_db() as db: diff --git a/lnbits/core/static/js/wallet.js b/lnbits/core/static/js/wallet.js index ae01162..67aa850 100644 --- a/lnbits/core/static/js/wallet.js +++ b/lnbits/core/static/js/wallet.js @@ -1,4 +1,4 @@ -/* globals Vue, VueQrcodeReader, VueQrcode, Quasar, LNbits, _ */ +/* globals decode, Vue, VueQrcodeReader, VueQrcode, Quasar, LNbits, _, EventHub, Chart */ Vue.component(VueQrcode.name, VueQrcode) Vue.use(VueQrcodeReader) @@ -123,6 +123,7 @@ new Vue({ mixins: [windowMixin], data: function() { return { + user: LNbits.map.user(window.user), receive: { show: false, status: 'pending', @@ -146,7 +147,12 @@ new Vue({ payments: [], paymentsTable: { columns: [ - {name: 'memo', align: 'left', label: 'Memo', field: 'memo'}, + { + name: 'memo', + align: 'left', + label: 'Memo', + field: 'memo' + }, { name: 'date', align: 'left', @@ -179,7 +185,7 @@ new Vue({ computed: { filteredPayments: function() { var q = this.paymentsTable.filter - if (!q || q == '') return this.payments + if (!q || q === '') return this.payments return LNbits.utils.search(this.payments, q) }, @@ -316,11 +322,11 @@ new Vue({ _.each(invoice.data.tags, function(tag) { if (_.isObject(tag) && _.has(tag, 'description')) { - if (tag.description == 'payment_hash') { + if (tag.description === 'payment_hash') { cleanInvoice.hash = tag.value - } else if (tag.description == 'description') { + } else if (tag.description === 'description') { cleanInvoice.description = tag.value - } else if (tag.description == 'expiry') { + } else if (tag.description === 'expiry') { var expireDate = new Date( (invoice.data.time_stamp + tag.value) * 1000 ) diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html index bc24876..766f2ea 100644 --- a/lnbits/core/templates/core/wallet.html +++ b/lnbits/core/templates/core/wallet.html @@ -8,7 +8,7 @@ {% endblock %} {% block scripts %} {{ window_vars(user, wallet) }} {% assets filters='rjsmin', output='__bundle__/core/chart.js', -'vendor/moment@2.25.1/moment.min.js', 'vendor/chart.js@2.9.3/chart.min.js' %} +'vendor/moment@2.27.0/moment.min.js', 'vendor/chart.js@2.9.3/chart.min.js' %} {% endassets %} {% assets filters='rjsmin', output='__bundle__/core/wallet.js', 'vendor/bolt11/utils.js', 'vendor/bolt11/decoder.js', @@ -76,7 +76,7 @@ clearable v-model="paymentsTable.filter" debounce="300" - placeholder="Search by memo, amount" + placeholder="Search by tag, memo, amount" class="q-mb-md" > @@ -84,7 +84,7 @@ dense flat :data="filteredPayments" - row-key="checking_id" + row-key="payment_hash" :columns="paymentsTable.columns" :pagination.sync="paymentsTable.pagination" > @@ -111,6 +111,11 @@ + + + #{{ props.row.tag }} + + {{ props.row.memo }} diff --git a/lnbits/extensions/amilk/views_api.py b/lnbits/extensions/amilk/views_api.py index dccad42..816ca99 100644 --- a/lnbits/extensions/amilk/views_api.py +++ b/lnbits/extensions/amilk/views_api.py @@ -34,7 +34,9 @@ def api_amilkit(amilk_id): except LnurlException: abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.") - payment_hash, payment_request = create_invoice(wallet_id=milk.wallet, amount=withdraw_res.max_sats, memo=memo) + payment_hash, payment_request = create_invoice( + wallet_id=milk.wallet, amount=withdraw_res.max_sats, memo=memo, extra={"tag": "amilk"} + ) r = requests.get( withdraw_res.callback.base, diff --git a/lnbits/extensions/events/views_api.py b/lnbits/extensions/events/views_api.py index 0680cbe..82be585 100644 --- a/lnbits/extensions/events/views_api.py +++ b/lnbits/extensions/events/views_api.py @@ -109,10 +109,10 @@ def api_tickets(): def api_ticket_make_ticket(event_id, sats): event = get_event(event_id) if not event: - return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND + return jsonify({"message": "Event does not exist."}), HTTPStatus.NOT_FOUND try: payment_hash, payment_request = create_invoice( - wallet_id=event.wallet, amount=int(sats), memo=f"#lnticket {event_id}" + wallet_id=event.wallet, amount=int(sats), memo=f"{event_id}", extra={"tag": "events"} ) except Exception as e: return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR @@ -120,7 +120,7 @@ def api_ticket_make_ticket(event_id, sats): ticket = create_ticket(payment_hash=payment_hash, wallet=event.wallet, event=event_id, **g.data) if not ticket: - return jsonify({"message": "LNTicket could not be fetched."}), HTTPStatus.NOT_FOUND + return jsonify({"message": "Event could not be fetched."}), HTTPStatus.NOT_FOUND return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.OK diff --git a/lnbits/extensions/lnticket/templates/lnticket/display.html b/lnbits/extensions/lnticket/templates/lnticket/display.html index 9001519..e1d1c90 100644 --- a/lnbits/extensions/lnticket/templates/lnticket/display.html +++ b/lnbits/extensions/lnticket/templates/lnticket/display.html @@ -106,15 +106,15 @@ computed: { amountWords() { var regex = /\s+/gi - var char = this.formDialog.data.text + var nwords = this.formDialog.data.text .trim() .replace(regex, ' ') .split(' ').length - this.formDialog.data.sats = char * parseInt('{{ form_costpword }}') - if (this.formDialog.data.sats == parseInt('{{ form_costpword }}')) { + var sats = nwords * parseInt('{{ form_costpword }}') + if (sats === parseInt('{{ form_costpword }}')) { return '0 Sats to pay' } else { - return this.formDialog.data.sats + ' Sats to pay' + return sats + ' Sats to pay' } } }, @@ -125,7 +125,6 @@ this.formDialog.data.name = '' this.formDialog.data.email = '' this.formDialog.data.text = '' - this.formDialog.data.sats = 0 }, closeReceiveDialog: function () { @@ -139,15 +138,12 @@ var self = this axios .post( - '/lnticket/api/v1/tickets/' + - '{{ form_id }}/' + - self.formDialog.data.sats, + '/lnticket/api/v1/tickets/{{ form_id }}', { form: '{{ form_id }}', name: self.formDialog.data.name, email: self.formDialog.data.email, ltext: self.formDialog.data.text, - sats: self.formDialog.data.sats } ) .then(function (response) { @@ -175,7 +171,6 @@ self.formDialog.data.name = '' self.formDialog.data.email = '' self.formDialog.data.text = '' - self.formDialog.data.sats = 0 self.$q.notify({ type: 'positive', diff --git a/lnbits/extensions/lnticket/views_api.py b/lnbits/extensions/lnticket/views_api.py index be5cb8a..b2740ef 100644 --- a/lnbits/extensions/lnticket/views_api.py +++ b/lnbits/extensions/lnticket/views_api.py @@ -1,3 +1,4 @@ +import re from flask import g, jsonify, request from http import HTTPStatus @@ -48,7 +49,6 @@ def api_forms(): def api_form_create(form_id=None): if form_id: form = get_form(form_id) - print(g.data) if not form: return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND @@ -92,29 +92,32 @@ def api_tickets(): return jsonify([form._asdict() for form in get_tickets(wallet_ids)]), HTTPStatus.OK -@lnticket_ext.route("/api/v1/tickets//", methods=["POST"]) +@lnticket_ext.route("/api/v1/tickets/", methods=["POST"]) @api_validate_post_request( schema={ "form": {"type": "string", "empty": False, "required": True}, "name": {"type": "string", "empty": False, "required": True}, - "email": {"type": "string", "empty": False, "required": True}, + "email": {"type": "string", "empty": True, "required": True}, "ltext": {"type": "string", "empty": False, "required": True}, - "sats": {"type": "integer", "min": 0, "required": True}, } ) -def api_ticket_make_ticket(form_id, sats): - event = get_form(form_id) - - if not event: +def api_ticket_make_ticket(form_id): + form = get_form(form_id) + if not form: return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND try: + nwords = len(re.split(r"\s+", g.data["ltext"])) + sats = nwords * form.costpword payment_hash, payment_request = create_invoice( - wallet_id=event.wallet, amount=int(sats), memo=f"#lnticket {form_id}" + wallet_id=form.wallet, + amount=sats, + memo=f"ticket with {nwords} words on {form_id}", + extra={"tag": "lnticket"}, ) except Exception as e: return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR - ticket = create_ticket(payment_hash=payment_hash, wallet=event.wallet, **g.data) + ticket = create_ticket(payment_hash=payment_hash, wallet=form.wallet, sats=sats, **g.data) if not ticket: return jsonify({"message": "LNTicket could not be fetched."}), HTTPStatus.NOT_FOUND diff --git a/lnbits/extensions/lnurlp/views_api.py b/lnbits/extensions/lnurlp/views_api.py index e62ddd7..327310d 100644 --- a/lnbits/extensions/lnurlp/views_api.py +++ b/lnbits/extensions/lnurlp/views_api.py @@ -123,6 +123,7 @@ def api_lnurl_callback(link_id): amount=link.amount, memo=link.description, description_hash=hashlib.sha256(link.lnurlpay_metadata.encode("utf-8")).digest(), + extra={"tag": "lnurlp"}, ) resp = LnurlPayActionResponse(pr=payment_request, success_action=None, routes=[]) diff --git a/lnbits/extensions/paywall/views_api.py b/lnbits/extensions/paywall/views_api.py index 96b616c..f00ce79 100644 --- a/lnbits/extensions/paywall/views_api.py +++ b/lnbits/extensions/paywall/views_api.py @@ -64,7 +64,7 @@ def api_paywall_create_invoice(paywall_id): try: amount = g.data["amount"] if g.data["amount"] > paywall.amount else paywall.amount payment_hash, payment_request = create_invoice( - wallet_id=paywall.wallet, amount=amount, memo=f"#paywall {paywall.memo}" + wallet_id=paywall.wallet, amount=amount, memo=f"{paywall.memo}", extra={'tag': 'paywall'} ) except Exception as e: return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR diff --git a/lnbits/extensions/tpos/views_api.py b/lnbits/extensions/tpos/views_api.py index c80fed8..7e30819 100644 --- a/lnbits/extensions/tpos/views_api.py +++ b/lnbits/extensions/tpos/views_api.py @@ -60,7 +60,7 @@ def api_tpos_create_invoice(tpos_id): try: payment_hash, payment_request = create_invoice( - wallet_id=tpos.wallet, amount=g.data["amount"], memo=f"#tpos {tpos.name}" + wallet_id=tpos.wallet, amount=g.data["amount"], memo=f"{tpos.name}", extra={"tag": "tpos"} ) except Exception as e: return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR diff --git a/lnbits/extensions/withdraw/models.py b/lnbits/extensions/withdraw/models.py index 878cbba..67ed28c 100644 --- a/lnbits/extensions/withdraw/models.py +++ b/lnbits/extensions/withdraw/models.py @@ -62,5 +62,5 @@ class WithdrawLink(NamedTuple): k1=self.k1, min_withdrawable=self.min_withdrawable * 1000, max_withdrawable=self.max_withdrawable * 1000, - default_description="#withdraw LNbits LNURL", + default_description="LNbits voucher", ) diff --git a/lnbits/extensions/withdraw/views_api.py b/lnbits/extensions/withdraw/views_api.py index 51f1b03..b50a041 100644 --- a/lnbits/extensions/withdraw/views_api.py +++ b/lnbits/extensions/withdraw/views_api.py @@ -182,7 +182,12 @@ def api_lnurl_callback(unique_hash): return jsonify({"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."}), HTTPStatus.OK try: - pay_invoice(wallet_id=link.wallet, payment_request=payment_request, max_sat=link.max_withdrawable) + pay_invoice( + wallet_id=link.wallet, + payment_request=payment_request, + max_sat=link.max_withdrawable, + extra={"tag": "withdraw"}, + ) changes = { "open_time": link.wait_time + now, diff --git a/lnbits/static/js/base.js b/lnbits/static/js/base.js index f620a90..9449181 100644 --- a/lnbits/static/js/base.js +++ b/lnbits/static/js/base.js @@ -94,7 +94,18 @@ var LNbits = { }, payment: function(data) { var obj = _.object( - ['checking_id', 'pending', 'amount', 'fee', 'memo', 'time'], + [ + 'checking_id', + 'pending', + 'amount', + 'fee', + 'memo', + 'time', + 'bolt11', + 'preimage', + 'payment_hash', + 'extra' + ], data ) obj.date = Quasar.utils.date.formatDate( @@ -103,6 +114,7 @@ var LNbits = { ) obj.msat = obj.amount obj.sat = obj.msat / 1000 + obj.tag = obj.extra.tag obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat) obj.isIn = obj.amount > 0 obj.isOut = obj.amount < 0 From ce28db76c966cadf781cd08690dd2c0354f37804 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 2 Sep 2020 19:19:18 -0300 Subject: [PATCH 7/9] add a dialog with payment details for each payment. for outgoing payments this needs a preimage to be good, but we don't have it yet because we don't get it from backends. --- lnbits/core/static/js/wallet.js | 104 +++++++++--------- lnbits/core/templates/core/wallet.html | 81 ++++++++++++-- .../lnticket/templates/lnticket/display.html | 15 +-- lnbits/static/css/base.css | 9 ++ lnbits/static/js/base.js | 68 ++++++------ 5 files changed, 172 insertions(+), 105 deletions(-) diff --git a/lnbits/core/static/js/wallet.js b/lnbits/core/static/js/wallet.js index 67aa850..140ca47 100644 --- a/lnbits/core/static/js/wallet.js +++ b/lnbits/core/static/js/wallet.js @@ -14,10 +14,10 @@ function generateChart(canvas, payments) { } _.each( - payments.slice(0).sort(function(a, b) { + payments.slice(0).sort(function (a, b) { return a.time - b.time }), - function(tx) { + function (tx) { txs.push({ hour: Quasar.utils.date.formatDate(tx.date, 'YYYY-MM-DDTHH:00'), sat: tx.sat @@ -25,17 +25,17 @@ function generateChart(canvas, payments) { } ) - _.each(_.groupBy(txs, 'hour'), function(value, day) { + _.each(_.groupBy(txs, 'hour'), function (value, day) { var income = _.reduce( value, - function(memo, tx) { + function (memo, tx) { return tx.sat >= 0 ? memo + tx.sat : memo }, 0 ) var outcome = _.reduce( value, - function(memo, tx) { + function (memo, tx) { return tx.sat < 0 ? memo + Math.abs(tx.sat) : memo }, 0 @@ -67,20 +67,14 @@ function generateChart(canvas, payments) { type: 'bar', label: 'in', barPercentage: 0.75, - backgroundColor: window - .Color('rgb(76,175,80)') - .alpha(0.5) - .rgbString() // green + 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 + backgroundColor: window.Color('rgb(233,30,99)').alpha(0.5).rgbString() // pink } ] }, @@ -121,7 +115,7 @@ function generateChart(canvas, payments) { new Vue({ el: '#vue', mixins: [windowMixin], - data: function() { + data: function () { return { user: LNbits.map.user(window.user), receive: { @@ -183,49 +177,49 @@ new Vue({ } }, computed: { - filteredPayments: function() { + filteredPayments: function () { var q = this.paymentsTable.filter if (!q || q === '') return this.payments return LNbits.utils.search(this.payments, q) }, - balance: function() { + balance: function () { if (this.payments.length) { return ( - _.pluck(this.payments, 'amount').reduce(function(a, b) { + _.pluck(this.payments, 'amount').reduce(function (a, b) { return a + b }, 0) / 1000 ) } return this.g.wallet.sat }, - fbalance: function() { + fbalance: function () { return LNbits.utils.formatSat(this.balance) }, - canPay: function() { + canPay: function () { if (!this.send.invoice) return false return this.send.invoice.sat <= this.balance }, - pendingPaymentsExist: function() { + pendingPaymentsExist: function () { return this.payments ? _.where(this.payments, {pending: 1}).length > 0 : false } }, methods: { - closeCamera: function() { + closeCamera: function () { this.sendCamera.show = false }, - showCamera: function() { + showCamera: function () { this.sendCamera.show = true }, - showChart: function() { + showChart: function () { this.paymentsChart.show = true - this.$nextTick(function() { + this.$nextTick(function () { generateChart(this.$refs.canvas, this.payments) }) }, - showReceiveDialog: function() { + showReceiveDialog: function () { this.receive = { show: true, status: 'pending', @@ -237,7 +231,7 @@ new Vue({ paymentChecker: null } }, - showSendDialog: function() { + showSendDialog: function () { this.send = { show: true, invoice: null, @@ -247,20 +241,20 @@ new Vue({ paymentChecker: null } }, - closeReceiveDialog: function() { + closeReceiveDialog: function () { var checker = this.receive.paymentChecker - setTimeout(function() { + setTimeout(function () { clearInterval(checker) }, 10000) }, - closeSendDialog: function() { + closeSendDialog: function () { this.sendCamera.show = false var checker = this.send.paymentChecker - setTimeout(function() { + setTimeout(function () { clearInterval(checker) }, 1000) }, - createInvoice: function() { + createInvoice: function () { var self = this this.receive.status = 'loading' LNbits.api @@ -269,14 +263,14 @@ new Vue({ this.receive.data.amount, this.receive.data.memo ) - .then(function(response) { + .then(function (response) { self.receive.status = 'success' self.receive.paymentReq = response.data.payment_request - self.receive.paymentChecker = setInterval(function() { + self.receive.paymentChecker = setInterval(function () { LNbits.api .getPayment(self.g.wallet, response.data.payment_hash) - .then(function(response) { + .then(function (response) { if (response.data.paid) { self.fetchPayments() self.receive.show = false @@ -285,17 +279,17 @@ new Vue({ }) }, 2000) }) - .catch(function(error) { + .catch(function (error) { LNbits.utils.notifyApiError(error) self.receive.status = 'pending' }) }, - decodeQR: function(res) { + decodeQR: function (res) { this.send.data.bolt11 = res this.decodeInvoice() this.sendCamera.show = false }, - decodeInvoice: function() { + decodeInvoice: function () { if (this.send.data.bolt11.startsWith('lightning:')) { this.send.data.bolt11 = this.send.data.bolt11.slice(10) } @@ -320,7 +314,7 @@ new Vue({ fsat: LNbits.utils.formatSat(invoice.human_readable_part.amount / 1000) } - _.each(invoice.data.tags, function(tag) { + _.each(invoice.data.tags, function (tag) { if (_.isObject(tag) && _.has(tag, 'description')) { if (tag.description === 'payment_hash') { cleanInvoice.hash = tag.value @@ -341,7 +335,7 @@ new Vue({ this.send.invoice = Object.freeze(cleanInvoice) }, - payInvoice: function() { + payInvoice: function () { var self = this let dismissPaymentMsg = this.$q.notify({ @@ -352,11 +346,11 @@ new Vue({ LNbits.api .payInvoice(this.g.wallet, this.send.data.bolt11) - .then(function(response) { - self.send.paymentChecker = setInterval(function() { + .then(function (response) { + self.send.paymentChecker = setInterval(function () { LNbits.api .getPayment(self.g.wallet, response.data.payment_hash) - .then(function(res) { + .then(function (res) { if (res.data.paid) { self.send.show = false clearInterval(self.send.paymentChecker) @@ -366,58 +360,58 @@ new Vue({ }) }, 2000) }) - .catch(function(error) { + .catch(function (error) { dismissPaymentMsg() LNbits.utils.notifyApiError(error) }) }, - deleteWallet: function(walletId, user) { + deleteWallet: function (walletId, user) { LNbits.utils .confirmDialog('Are you sure you want to delete this wallet?') - .onOk(function() { + .onOk(function () { LNbits.href.deleteWallet(walletId, user) }) }, - fetchPayments: function(checkPending) { + fetchPayments: function (checkPending) { var self = this return LNbits.api .getPayments(this.g.wallet, checkPending) - .then(function(response) { + .then(function (response) { self.payments = response.data - .map(function(obj) { + .map(function (obj) { return LNbits.map.payment(obj) }) - .sort(function(a, b) { + .sort(function (a, b) { return b.time - a.time }) }) }, - checkPendingPayments: function() { + checkPendingPayments: function () { var dismissMsg = this.$q.notify({ timeout: 0, message: 'Checking pending transactions...', icon: null }) - this.fetchPayments(true).then(function() { + this.fetchPayments(true).then(function () { dismissMsg() }) }, - exportCSV: function() { + exportCSV: function () { LNbits.utils.exportCSV(this.paymentsTable.columns, this.payments) } }, watch: { - payments: function() { + payments: function () { EventHub.$emit('update-wallet-balance', [this.g.wallet.id, this.balance]) } }, - created: function() { + created: function () { this.fetchPayments() setTimeout(this.checkPendingPayments(), 1200) }, - mounted: function() { + mounted: function () { if ( this.$refs.disclaimer && !this.$q.localStorage.getItem('lnbits.disclaimerShown') diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html index 766f2ea..2d47674 100644 --- a/lnbits/core/templates/core/wallet.html +++ b/lnbits/core/templates/core/wallet.html @@ -103,19 +103,28 @@ - + Pending - - #{{ props.row.tag }} - - + + #{{ props.row.tag }} + + {{ props.row.memo }} @@ -125,6 +134,64 @@ {{ props.row.fsat }} + + + + +
+ Copy invoice + Close +
+
+ +
+
+ + Payment Received +
+
+ + Payment Sent +
+
+ + Outgoing payment pending +
+ Payment Hash +
+ {{ props.row.payment_hash }} +
+
+
+
{% endraw %} diff --git a/lnbits/extensions/lnticket/templates/lnticket/display.html b/lnbits/extensions/lnticket/templates/lnticket/display.html index e1d1c90..4ab829e 100644 --- a/lnbits/extensions/lnticket/templates/lnticket/display.html +++ b/lnbits/extensions/lnticket/templates/lnticket/display.html @@ -137,15 +137,12 @@ Invoice: function () { var self = this axios - .post( - '/lnticket/api/v1/tickets/{{ form_id }}', - { - form: '{{ form_id }}', - name: self.formDialog.data.name, - email: self.formDialog.data.email, - ltext: self.formDialog.data.text, - } - ) + .post('/lnticket/api/v1/tickets/{{ form_id }}', { + form: '{{ form_id }}', + name: self.formDialog.data.name, + email: self.formDialog.data.email, + ltext: self.formDialog.data.text + }) .then(function (response) { self.paymentReq = response.data.payment_request self.paymentCheck = response.data.payment_hash diff --git a/lnbits/static/css/base.css b/lnbits/static/css/base.css index f5a9268..ffd5309 100644 --- a/lnbits/static/css/base.css +++ b/lnbits/static/css/base.css @@ -66,3 +66,12 @@ a.inherit { direction: ltr; -moz-font-feature-settings: 'liga'; -moz-osx-font-smoothing: grayscale; } + +.text-wrap { + word-wrap: break-word; + word-break: break-all; +} + +.mono { + font-family: monospace; +} diff --git a/lnbits/static/js/base.js b/lnbits/static/js/base.js index 9449181..c32141b 100644 --- a/lnbits/static/js/base.js +++ b/lnbits/static/js/base.js @@ -6,7 +6,7 @@ var EventHub = new Vue() var LNbits = { api: { - request: function(method, url, apiKey, data) { + request: function (method, url, apiKey, data) { return axios({ method: method, url: url, @@ -16,20 +16,20 @@ var LNbits = { data: data }) }, - createInvoice: function(wallet, amount, memo) { + createInvoice: function (wallet, amount, memo) { return this.request('post', '/api/v1/payments', wallet.inkey, { out: false, amount: amount, memo: memo }) }, - payInvoice: function(wallet, bolt11) { + payInvoice: function (wallet, bolt11) { return this.request('post', '/api/v1/payments', wallet.adminkey, { out: true, bolt11: bolt11 }) }, - getPayments: function(wallet, checkPending) { + getPayments: function (wallet, checkPending) { var query_param = checkPending ? '?check_pending' : '' return this.request( 'get', @@ -37,7 +37,7 @@ var LNbits = { wallet.inkey ) }, - getPayment: function(wallet, paymentHash) { + getPayment: function (wallet, paymentHash) { return this.request( 'get', '/api/v1/payments/' + paymentHash, @@ -46,16 +46,16 @@ var LNbits = { } }, href: { - createWallet: function(walletName, userId) { + createWallet: function (walletName, userId) { window.location.href = '/wallet?' + (userId ? 'usr=' + userId + '&' : '') + 'nme=' + walletName }, - deleteWallet: function(walletId, userId) { + deleteWallet: function (walletId, userId) { window.location.href = '/deletewallet?usr=' + userId + '&wal=' + walletId } }, map: { - extension: function(data) { + extension: function (data) { var obj = _.object( ['code', 'isValid', 'name', 'shortDescription', 'icon'], data @@ -63,17 +63,17 @@ var LNbits = { obj.url = ['/', obj.code, '/'].join('') return obj }, - user: function(data) { + user: function (data) { var obj = _.object(['id', 'email', 'extensions', 'wallets'], data) var mapWallet = this.wallet obj.wallets = obj.wallets - .map(function(obj) { + .map(function (obj) { return mapWallet(obj) }) - .sort(function(a, b) { + .sort(function (a, b) { return a.name.localeCompare(b.name) }) - obj.walletOptions = obj.wallets.map(function(obj) { + obj.walletOptions = obj.wallets.map(function (obj) { return { label: [obj.name, ' - ', obj.id].join(''), value: obj.id @@ -81,7 +81,7 @@ var LNbits = { }) return obj }, - wallet: function(data) { + wallet: function (data) { var obj = _.object( ['id', 'name', 'user', 'adminkey', 'inkey', 'balance'], data @@ -92,7 +92,7 @@ var LNbits = { obj.url = ['/wallet?usr=', obj.user, '&wal=', obj.id].join('') return obj }, - payment: function(data) { + payment: function (data) { var obj = _.object( [ 'checking_id', @@ -124,7 +124,7 @@ var LNbits = { } }, utils: { - confirmDialog: function(msg) { + confirmDialog: function (msg) { return Quasar.plugins.Dialog.create({ message: msg, ok: { @@ -137,16 +137,16 @@ var LNbits = { } }) }, - formatCurrency: function(value, currency) { + formatCurrency: function (value, currency) { return new Intl.NumberFormat(LOCALE, { style: 'currency', currency: currency }).format(value) }, - formatSat: function(value) { + formatSat: function (value) { return new Intl.NumberFormat(LOCALE).format(value) }, - notifyApiError: function(error) { + notifyApiError: function (error) { var types = { 400: 'warning', 401: 'warning', @@ -163,12 +163,12 @@ var LNbits = { icon: null }) }, - search: function(data, q, field, separator) { + search: function (data, q, field, separator) { try { var queries = q.toLowerCase().split(separator || ' ') - return data.filter(function(obj) { + return data.filter(function (obj) { var matches = 0 - _.each(queries, function(q) { + _.each(queries, function (q) { if (obj[field].indexOf(q) !== -1) matches++ }) return matches === queries.length @@ -177,8 +177,8 @@ var LNbits = { return data } }, - exportCSV: function(columns, data) { - var wrapCsvValue = function(val, formatFn) { + exportCSV: function (columns, data) { + var wrapCsvValue = function (val, formatFn) { var formatted = formatFn !== void 0 ? formatFn(val) : val formatted = @@ -190,14 +190,14 @@ var LNbits = { } var content = [ - columns.map(function(col) { + columns.map(function (col) { return wrapCsvValue(col.label) }) ] .concat( - data.map(function(row) { + data.map(function (row) { return columns - .map(function(col) { + .map(function (col) { return wrapCsvValue( typeof col.field === 'function' ? col.field(row) @@ -228,7 +228,7 @@ var LNbits = { } var windowMixin = { - data: function() { + data: function () { return { g: { visibleDrawer: false, @@ -240,13 +240,13 @@ var windowMixin = { } }, methods: { - toggleDarkMode: function() { + toggleDarkMode: function () { this.$q.dark.toggle() this.$q.localStorage.set('lnbits.darkMode', this.$q.dark.isActive) }, - copyText: function(text, message, position) { + copyText: function (text, message, position) { var notify = this.$q.notify - Quasar.utils.copyToClipboard(text).then(function() { + Quasar.utils.copyToClipboard(text).then(function () { notify({ message: message || 'Copied to clipboard!', position: position || 'bottom' @@ -254,7 +254,7 @@ var windowMixin = { }) } }, - created: function() { + created: function () { this.$q.dark.set(this.$q.localStorage.getItem('lnbits.darkMode')) if (window.user) { this.g.user = Object.freeze(LNbits.map.user(window.user)) @@ -266,10 +266,10 @@ var windowMixin = { var user = this.g.user this.g.extensions = Object.freeze( window.extensions - .map(function(data) { + .map(function (data) { return LNbits.map.extension(data) }) - .map(function(obj) { + .map(function (obj) { if (user) { obj.isEnabled = user.extensions.indexOf(obj.code) !== -1 } else { @@ -277,7 +277,7 @@ var windowMixin = { } return obj }) - .sort(function(a, b) { + .sort(function (a, b) { return a.name > b.name }) ) From dc3d96c6a8fcb3925a5851ee66de4f6d92e6d0c2 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 2 Sep 2020 21:11:08 -0300 Subject: [PATCH 8/9] fix many mypy complaints, specially on bolt11.py --- lnbits/__init__.py | 1 - lnbits/bolt11.py | 66 ++++++------- lnbits/core/crud.py | 4 +- lnbits/core/services.py | 132 +++++++++++++------------ lnbits/extensions/paywall/views_api.py | 2 +- 5 files changed, 104 insertions(+), 101 deletions(-) diff --git a/lnbits/__init__.py b/lnbits/__init__.py index 479338e..b243376 100644 --- a/lnbits/__init__.py +++ b/lnbits/__init__.py @@ -85,4 +85,3 @@ def migrate_databases(): if __name__ == "__main__": app.run() - diff --git a/lnbits/bolt11.py b/lnbits/bolt11.py index d934461..184844f 100644 --- a/lnbits/bolt11.py +++ b/lnbits/bolt11.py @@ -1,12 +1,10 @@ -# type: ignore - -import bitstring +import bitstring # type: ignore import re import hashlib -from typing import List, NamedTuple +from typing import List, NamedTuple, Optional from bech32 import bech32_decode, CHARSET -from ecdsa import SECP256k1, VerifyingKey -from ecdsa.util import sigdecode_string +from ecdsa import SECP256k1, VerifyingKey # type: ignore +from ecdsa.util import sigdecode_string # type: ignore from binascii import unhexlify @@ -19,40 +17,40 @@ class Route(NamedTuple): class Invoice(object): - payment_hash: str = None + payment_hash: str amount_msat: int = 0 - description: str = None - payee: str = None - date: int = None + description: Optional[str] = None + description_hash: Optional[str] = None + payee: str + date: int expiry: int = 3600 - secret: str = None + secret: Optional[str] = None route_hints: List[Route] = [] min_final_cltv_expiry: int = 18 def decode(pr: str) -> Invoice: - """ Super naïve bolt11 decoder, - only gets payment_hash, description/description_hash and amount in msatoshi. + """bolt11 decoder, based on https://github.com/rustyrussell/lightning-payencode/blob/master/lnaddr.py """ - hrp, data = bech32_decode(pr) - if not hrp: - raise ValueError("Bad bech32 checksum") + hrp, decoded_data = bech32_decode(pr) + if hrp is None or decoded_data is None: + raise ValueError("Bad bech32 checksum") if not hrp.startswith("ln"): raise ValueError("Does not start with ln") - data = u5_to_bitarray(data) + bitarray = _u5_to_bitarray(decoded_data) # final signature 65 bytes, split it off. - if len(data) < 65 * 8: + if len(bitarray) < 65 * 8: raise ValueError("Too short to contain signature") # extract the signature - signature = data[-65 * 8 :].tobytes() + signature = bitarray[-65 * 8 :].tobytes() # the tagged fields as a bitstream - data = bitstring.ConstBitStream(data[: -65 * 8]) + data = bitstring.ConstBitStream(bitarray[: -65 * 8]) # build the invoice object invoice = Invoice() @@ -62,35 +60,35 @@ def decode(pr: str) -> Invoice: if m: amountstr = hrp[2 + m.end() :] if amountstr != "": - invoice.amount_msat = unshorten_amount(amountstr) + invoice.amount_msat = _unshorten_amount(amountstr) # pull out date invoice.date = data.read(35).uint while data.pos != data.len: - tag, tagdata, data = pull_tagged(data) + tag, tagdata, data = _pull_tagged(data) data_length = len(tagdata) / 5 if tag == "d": - invoice.description = trim_to_bytes(tagdata).decode("utf-8") + invoice.description = _trim_to_bytes(tagdata).decode("utf-8") elif tag == "h" and data_length == 52: - invoice.description = trim_to_bytes(tagdata).hex() + invoice.description_hash = _trim_to_bytes(tagdata).hex() elif tag == "p" and data_length == 52: - invoice.payment_hash = trim_to_bytes(tagdata).hex() + invoice.payment_hash = _trim_to_bytes(tagdata).hex() elif tag == "x": invoice.expiry = tagdata.uint elif tag == "n": - invoice.payee = trim_to_bytes(tagdata).hex() + invoice.payee = _trim_to_bytes(tagdata).hex() # this won't work in most cases, we must extract the payee # from the signature elif tag == "s": - invoice.secret = trim_to_bytes(tagdata).hex() + invoice.secret = _trim_to_bytes(tagdata).hex() elif tag == "r": s = bitstring.ConstBitStream(tagdata) while s.pos + 264 + 64 + 32 + 32 + 16 < s.len: route = Route( pubkey=s.read(264).tobytes().hex(), - short_channel_id=readable_scid(s.read(64).intbe), + short_channel_id=_readable_scid(s.read(64).intbe), base_fee_msat=s.read(32).intbe, ppm_fee=s.read(32).intbe, cltv=s.read(16).intbe, @@ -116,7 +114,7 @@ def decode(pr: str) -> Invoice: return invoice -def unshorten_amount(amount: str) -> int: +def _unshorten_amount(amount: str) -> int: """ Given a shortened amount, return millisatoshis """ # BOLT #11: @@ -141,18 +139,18 @@ def unshorten_amount(amount: str) -> int: raise ValueError("Invalid amount '{}'".format(amount)) if unit in units: - return int(amount[:-1]) * 100_000_000_000 / units[unit] + return int(int(amount[:-1]) * 100_000_000_000 / units[unit]) else: return int(amount) * 100_000_000_000 -def pull_tagged(stream): +def _pull_tagged(stream): tag = stream.read(5).uint length = stream.read(5).uint * 32 + stream.read(5).uint return (CHARSET[tag], stream.read(length * 5), stream) -def trim_to_bytes(barr): +def _trim_to_bytes(barr): # Adds a byte if necessary. b = barr.tobytes() if barr.len % 8 != 0: @@ -160,7 +158,7 @@ def trim_to_bytes(barr): return b -def readable_scid(short_channel_id: int) -> str: +def _readable_scid(short_channel_id: int) -> str: return "{blockheight}x{transactionindex}x{outputindex}".format( blockheight=((short_channel_id >> 40) & 0xFFFFFF), transactionindex=((short_channel_id >> 16) & 0xFFFFFF), @@ -168,7 +166,7 @@ def readable_scid(short_channel_id: int) -> str: ) -def u5_to_bitarray(arr): +def _u5_to_bitarray(arr: List[int]) -> bitstring.BitArray: ret = bitstring.BitArray() for a in arr: ret += bitstring.pack("uint:5", a) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 2d064af..4733a49 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -276,10 +276,10 @@ def delete_payment(checking_id: str) -> None: db.execute("DELETE FROM apipayments WHERE checking_id = ?", (checking_id,)) -def check_internal(payment_hash: str) -> None: +def check_internal(payment_hash: str) -> Optional[str]: with open_db() as db: row = db.fetchone("SELECT checking_id FROM apipayments WHERE hash = ?", (payment_hash,)) if not row: - return False + return None else: return row["checking_id"] diff --git a/lnbits/core/services.py b/lnbits/core/services.py index c15f78a..6f39111 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -1,27 +1,22 @@ -from typing import Optional, Tuple, Dict +from typing import Optional, Tuple, Dict, TypedDict from lnbits import bolt11 from lnbits.helpers import urlsafe_short_hash from lnbits.settings import WALLET +from lnbits.wallets.base import PaymentStatus from .crud import get_wallet, create_payment, delete_payment, check_internal, update_payment_status, get_wallet_payment def create_invoice( - *, - wallet_id: str, - amount: int, - memo: Optional[str] = None, - description_hash: Optional[bytes] = None, - extra: Optional[Dict] = None, + *, wallet_id: str, amount: int, memo: str, description_hash: Optional[bytes] = None, extra: Optional[Dict] = None, ) -> Tuple[str, str]: - try: - ok, checking_id, payment_request, error_message = WALLET.create_invoice( - amount=amount, memo=memo, description_hash=description_hash - ) - except Exception as e: - ok, error_message = False, str(e) + invoice_memo = None if description_hash else memo + storeable_memo = memo + ok, checking_id, payment_request, error_message = WALLET.create_invoice( + amount=amount, memo=invoice_memo, description_hash=description_hash + ) if not ok: raise Exception(error_message or "Unexpected backend error.") @@ -34,7 +29,7 @@ def create_invoice( payment_request=payment_request, payment_hash=invoice.payment_hash, amount=amount_msat, - memo=memo, + memo=storeable_memo, extra=extra, ) @@ -47,60 +42,71 @@ def pay_invoice( temp_id = f"temp_{urlsafe_short_hash()}" internal_id = f"internal_{urlsafe_short_hash()}" - try: - invoice = bolt11.decode(payment_request) - if invoice.amount_msat == 0: - raise ValueError("Amountless invoices not supported.") - if max_sat and invoice.amount_msat > max_sat * 1000: - raise ValueError("Amount in invoice is too high.") - - # put all parameters that don't change here - payment_kwargs = dict( - wallet_id=wallet_id, - payment_request=payment_request, - payment_hash=invoice.payment_hash, - amount=-invoice.amount_msat, - memo=invoice.description, - extra=extra, - ) - - # check_internal() returns the checking_id of the invoice we're waiting for - internal = check_internal(invoice.payment_hash) - if internal: - # create a new payment from this wallet - create_payment(checking_id=internal_id, fee=0, pending=False, **payment_kwargs) - else: - # create a temporary payment here so we can check if - # the balance is enough in the next step - fee_reserve = max(1000, int(invoice.amount_msat * 0.01)) - create_payment(checking_id=temp_id, fee=-fee_reserve, **payment_kwargs) - - # do the balance check - wallet = get_wallet(wallet_id) - assert wallet, "invalid wallet id" - if wallet.balance_msat < 0: - raise PermissionError("Insufficient balance.") - - if internal: - # mark the invoice from the other side as not pending anymore - # so the other side only has access to his new money when we are sure - # the payer has enough to deduct from - update_payment_status(checking_id=internal, pending=False) - else: - # actually pay the external invoice - ok, checking_id, fee_msat, error_message = WALLET.pay_invoice(payment_request) - if ok: - create_payment(checking_id=checking_id, fee=fee_msat, **payment_kwargs) - delete_payment(temp_id) - - except Exception as e: - ok, error_message = False, str(e) + invoice = bolt11.decode(payment_request) + if invoice.amount_msat == 0: + raise ValueError("Amountless invoices not supported.") + if max_sat and invoice.amount_msat > max_sat * 1000: + raise ValueError("Amount in invoice is too high.") + + # put all parameters that don't change here + PaymentKwargs = TypedDict( + "PaymentKwargs", + { + "wallet_id": str, + "payment_request": str, + "payment_hash": str, + "amount": int, + "memo": str, + "extra": Optional[Dict], + }, + ) + payment_kwargs: PaymentKwargs = dict( + wallet_id=wallet_id, + payment_request=payment_request, + payment_hash=invoice.payment_hash, + amount=-invoice.amount_msat, + memo=invoice.description or "", + extra=extra, + ) + + # check_internal() returns the checking_id of the invoice we're waiting for + internal = check_internal(invoice.payment_hash) + if internal: + # create a new payment from this wallet + create_payment(checking_id=internal_id, fee=0, pending=False, **payment_kwargs) + else: + # create a temporary payment here so we can check if + # the balance is enough in the next step + fee_reserve = max(1000, int(invoice.amount_msat * 0.01)) + create_payment(checking_id=temp_id, fee=-fee_reserve, **payment_kwargs) + + # do the balance check + wallet = get_wallet(wallet_id) + assert wallet, "invalid wallet id" + if wallet.balance_msat < 0: + raise PermissionError("Insufficient balance.") + + if internal: + # mark the invoice from the other side as not pending anymore + # so the other side only has access to his new money when we are sure + # the payer has enough to deduct from + update_payment_status(checking_id=internal, pending=False) + else: + # actually pay the external invoice + ok, checking_id, fee_msat, error_message = WALLET.pay_invoice(payment_request) + if ok: + create_payment(checking_id=checking_id, fee=fee_msat, **payment_kwargs) + delete_payment(temp_id) + if not ok: raise Exception(error_message or "Unexpected backend error.") return invoice.payment_hash -def check_invoice_status(wallet_id: str, payment_hash: str) -> str: +def check_invoice_status(wallet_id: str, payment_hash: str) -> PaymentStatus: payment = get_wallet_payment(wallet_id, payment_hash) + if not payment: + return PaymentStatus(None) + return WALLET.get_invoice_status(payment.checking_id) diff --git a/lnbits/extensions/paywall/views_api.py b/lnbits/extensions/paywall/views_api.py index f00ce79..85786a5 100644 --- a/lnbits/extensions/paywall/views_api.py +++ b/lnbits/extensions/paywall/views_api.py @@ -64,7 +64,7 @@ def api_paywall_create_invoice(paywall_id): try: amount = g.data["amount"] if g.data["amount"] > paywall.amount else paywall.amount payment_hash, payment_request = create_invoice( - wallet_id=paywall.wallet, amount=amount, memo=f"{paywall.memo}", extra={'tag': 'paywall'} + wallet_id=paywall.wallet, amount=amount, memo=f"{paywall.memo}", extra={"tag": "paywall"} ) except Exception as e: return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR From 753e9c5b15113ffcd9b4d2937fc0261a81a21257 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 2 Sep 2020 22:06:53 -0300 Subject: [PATCH 9/9] make linting workflow reflect what is done locally (Makefile). --- .github/workflows/linting.yml | 42 +++++++++++++++++++++-------------- .github/workflows/tests.yml | 2 +- Makefile | 18 +++++++++++---- package.json | 3 --- 4 files changed, 40 insertions(+), 25 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index ce0233d..57abd8b 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -1,25 +1,33 @@ -name: Run Linters +name: Linters -on: [push, pull_request] +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] jobs: - mypy: + black: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v1 - - name: Run MyPy python type checker - uses: jpetrucciani/mypy-check@master - with: - path: 'lnbits' - + - uses: actions/checkout@v2 + - run: sudo apt-get install python3-venv + - run: python3 -m venv venv + - run: ./venv/bin/pip install black + - run: make checkblack prettier: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v1 - - name: Check JS code formatting convention - uses: creyD/prettier_action@v2.2 - with: - dry: True - prettier_options: --write lnbits/static/js/** lnbits/core/static/js/** lnbits/extensions/*/templates/** + - uses: actions/checkout@v2 + - run: npm install + - run: make checkprettier + mypy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: sudo apt-get install python3-venv + - run: sudo apt-get install libev-dev + - run: python3 -m venv venv + - run: ./venv/bin/pip install -r requirements.txt + - run: ./venv/bin/pip install mypy + - run: make mypy diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 415fd15..e271512 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8] + python-version: [3.8] steps: - uses: actions/checkout@v2 diff --git a/Makefile b/Makefile index b96d1ed..fd8c70d 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,20 @@ -all: prettier mypy black +all: format check + +format: prettier black + +check: mypy checkprettier checkblack prettier: $(shell find lnbits -name "*.js" -name ".html") ./node_modules/.bin/prettier --write lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js +black: $(shell find lnbits -name "*.py") + ./venv/bin/black --line-length 120 lnbits + mypy: $(shell find lnbits -name "*.py") - mypy lnbits + ./venv/bin/mypy lnbits -black: $(shell find lnbits -name "*.py") - black lnbits +checkprettier: $(shell find lnbits -name "*.js" -name ".html") + ./node_modules/.bin/prettier --check lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js + +checkblack: $(shell find lnbits -name "*.py") + ./venv/bin/black --check --line-length 120 lnbits diff --git a/package.json b/package.json index d2b6560..835ff77 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,5 @@ { "devDependencies": { "prettier": "^2.0.5" - }, - "scripts": { - "lint": "prettier --write lnbits/static/js/** lnbits/core/static/js/** lnbits/extensions/*/templates/**" } }