diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index ce0233de..57abd8ba 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 415fd15c..e2715129 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 b96d1eda..fd8c70d0 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 -mypy: $(shell find lnbits -name "*.py") - mypy lnbits - black: $(shell find lnbits -name "*.py") - black lnbits + ./venv/bin/black --line-length 120 lnbits + +mypy: $(shell find lnbits -name "*.py") + ./venv/bin/mypy 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/README.md b/README.md index ed9fe356..dc70c3b8 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/bolt11.py b/lnbits/bolt11.py index d9344617..184844f7 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 1b73786c..4733a493 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -1,7 +1,10 @@ -from typing import List, Optional +import json +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 @@ -136,18 +139,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 payhash as checking_id, amount, fee, pending, memo, time + SELECT * FROM apipayments - WHERE wallet = ? AND payhash = ? + 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 +182,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 * FROM apipayments WHERE wallet = ? {clause} ORDER BY time DESC @@ -187,18 +190,35 @@ 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: +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 @@ -206,18 +226,41 @@ 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_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 apipayments (wallet, payhash, amount, pending, memo, fee) - VALUES (?, ?, ?, ?, ?, ?) + INSERT INTO apipayments + (wallet, checking_id, bolt11, hash, preimage, + amount, pending, memo, fee, extra) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, - (wallet_id, checking_id, amount, int(pending), memo, fee), + ( + 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, 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,9 +268,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 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 apipayments WHERE payhash = ?", (checking_id,)) + db.execute("DELETE FROM apipayments WHERE checking_id = ?", (checking_id,)) + + +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 None + else: + return row["checking_id"] diff --git a/lnbits/core/migrations.py b/lnbits/core/migrations.py index a8443003..78631f44 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 @@ -70,6 +71,40 @@ def m001_initial(db): ) +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") + + 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: m001_initial(db) + m002_add_fields_to_apipayments(db) diff --git a/lnbits/core/models.py b/lnbits/core/models.py index 10a87ad1..ed4ed43d 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 @@ -41,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 @@ -54,6 +51,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: @@ -71,6 +91,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 717cfc17..6f39111f 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -1,72 +1,112 @@ -from typing import Optional, Tuple +from typing import Optional, Tuple, Dict, TypedDict -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 lnbits.wallets.base import PaymentStatus -from .crud import get_wallet, create_payment, delete_payment +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]: - - 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) +def create_invoice( + *, wallet_id: str, amount: int, memo: str, description_hash: Optional[bytes] = None, extra: Optional[Dict] = None, +) -> Tuple[str, str]: + 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.") + 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_request=payment_request, + payment_hash=invoice.payment_hash, + amount=amount_msat, + memo=storeable_memo, + extra=extra, + ) - return checking_id, payment_request + return invoice.payment_hash, payment_request -def pay_invoice(*, wallet_id: str, bolt11: str, max_sat: Optional[int] = None) -> str: +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_id = f"internal_{urlsafe_short_hash()}" - if invoice.amount_msat == 0: - raise ValueError("Amountless invoices not supported.") + 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.") - 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( - wallet_id=wallet_id, checking_id=temp_id, amount=-invoice.amount_msat, fee=-fee_reserve, memo=temp_id, - ) + create_payment(checking_id=temp_id, fee=-fee_reserve, **payment_kwargs) - 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) + # 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( - wallet_id=wallet_id, - checking_id=checking_id, - amount=-invoice.amount_msat, - fee=fee_msat, - memo=invoice.description, - ) - - except Exception as e: - ok, error_message = False, str(e) - - delete_payment(temp_id) + 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 checking_id + return invoice.payment_hash -def check_payment(*, checking_id: str) -> str: - pass +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/core/static/js/wallet.js b/lnbits/core/static/js/wallet.js index 20e7778f..140ca479 100644 --- a/lnbits/core/static/js/wallet.js +++ b/lnbits/core/static/js/wallet.js @@ -1,3 +1,5 @@ +/* globals decode, Vue, VueQrcodeReader, VueQrcode, Quasar, LNbits, _, EventHub, Chart */ + Vue.component(VueQrcode.name, VueQrcode) Vue.use(VueQrcodeReader) @@ -115,6 +117,7 @@ new Vue({ mixins: [windowMixin], data: function () { return { + user: LNbits.map.user(window.user), receive: { show: false, status: 'pending', @@ -138,7 +141,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', @@ -171,7 +179,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) }, @@ -261,7 +269,7 @@ new Vue({ self.receive.paymentChecker = setInterval(function () { LNbits.api - .getPayment(self.g.wallet, response.data.checking_id) + .getPayment(self.g.wallet, response.data.payment_hash) .then(function (response) { if (response.data.paid) { self.fetchPayments() @@ -308,11 +316,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 ) @@ -330,7 +338,7 @@ new Vue({ payInvoice: function () { var self = this - dismissPaymentMsg = this.$q.notify({ + let dismissPaymentMsg = this.$q.notify({ timeout: 0, message: 'Processing payment...', icon: null @@ -341,7 +349,7 @@ new Vue({ .then(function (response) { self.send.paymentChecker = setInterval(function () { LNbits.api - .getPayment(self.g.wallet, response.data.checking_id) + .getPayment(self.g.wallet, response.data.payment_hash) .then(function (res) { if (res.data.paid) { self.send.show = false diff --git a/lnbits/core/templates/core/_api_docs.html b/lnbits/core/templates/core/_api_docs.html index 38c35685..f1fddd5e 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 6d19bcaa..2d476746 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="payhash" + row-key="payment_hash" :columns="paymentsTable.columns" :pagination.sync="paymentsTable.pagination" > @@ -103,14 +103,28 @@ - + Pending + + + #{{ props.row.tag }} + + {{ props.row.memo }} @@ -120,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/core/views/api.py b/lnbits/core/views/api.py index 05670318..9a587ee6 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -2,21 +2,24 @@ 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.core.crud import delete_expired_invoices 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") 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_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) @@ -41,20 +44,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 +76,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 +96,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 @@ -84,10 +107,12 @@ def api_payment(checking_id): return jsonify({"paid": True}), HTTPStatus.OK try: - if payment.is_out: - is_paid = not WALLET.get_payment_status(checking_id).pending + 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(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 f712cb48..62b194e3 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/db.py b/lnbits/db.py index d9a86609..316bb217 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: diff --git a/lnbits/extensions/amilk/views_api.py b/lnbits/extensions/amilk/views_api.py index 6d51da0b..816ca99e 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 lnbits.core.crud import get_user -from lnbits.decorators import api_check_wallet_key, api_validate_post_request - -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 + +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 @amilk_ext.route("/api/v1/amilk", methods=["GET"]) @@ -36,13 +33,10 @@ 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, extra={"tag": "amilk"} + ) r = requests.get( withdraw_res.callback.base, @@ -50,19 +44,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 1c8c968b..b7219f28 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 59a06ef2..542feb91 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 cfc67ffd..82be5855 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 + return jsonify({"message": "Event does not exist."}), HTTPStatus.NOT_FOUND try: - checking_id, payment_request = create_invoice( - wallet_id=event.wallet, amount=int(sats), memo=f"#lnticket {event_id}" + payment_hash, payment_request = create_invoice( + 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 - 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({"message": "Event 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 5a532fc3..561137d9 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 1b72615f..4ab829e5 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 () { @@ -138,21 +137,15 @@ Invoice: function () { var self = this axios - .post( - '/lnticket/api/v1/tickets/' + - '{{ form_id }}/' + - self.formDialog.data.sats, - { - form: '{{ form_id }}', - name: self.formDialog.data.name, - email: self.formDialog.data.email, - ltext: self.formDialog.data.text, - sats: self.formDialog.data.sats - } - ) + .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.checking_id + self.paymentCheck = response.data.payment_hash dismissMsg = self.$q.notify({ timeout: 0, @@ -175,7 +168,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 69a2ac8c..b2740ef3 100644 --- a/lnbits/extensions/lnticket/views_api.py +++ b/lnbits/extensions/lnticket/views_api.py @@ -1,10 +1,10 @@ +import re 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 ( @@ -49,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 @@ -93,51 +92,52 @@ 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: - checking_id, payment_request = create_invoice( - wallet_id=event.wallet, amount=int(sats), memo=f"#lnticket {form_id}" + nwords = len(re.split(r"\s+", g.data["ltext"])) + sats = nwords * form.costpword + payment_hash, payment_request = create_invoice( + 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(checking_id=checking_id, 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 - 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/lnurlp/views_api.py b/lnbits/extensions/lnurlp/views_api.py index e62ddd74..327310d9 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/templates/paywall/_api_docs.html b/lnbits/extensions/paywall/templates/paywall/_api_docs.html index 60e61081..56dbf564 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 f3b7c6ff..b9248ed3 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 012d355d..85786a5f 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( - wallet_id=paywall.wallet, amount=amount, memo=f"#paywall {paywall.memo}" + payment_hash, payment_request = create_invoice( + 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 - 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 6d96233e..800114a1 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 c273ab15..7e30819d 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( - wallet_id=tpos.wallet, amount=g.data["amount"], memo=f"#tpos {tpos.name}" + payment_hash, payment_request = create_invoice( + 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 - 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 c9f80e5a..6a0980c9 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 } @@ -146,8 +164,6 @@ var LNbits = { }) }, search: function (data, q, field, separator) { - var field = field || '_q' - try { var queries = q.toLowerCase().split(separator || ' ') return data.filter(function (obj) { @@ -155,7 +171,7 @@ var LNbits = { _.each(queries, function (q) { if (obj[field].indexOf(q) !== -1) matches++ }) - return matches == queries.length + return matches === queries.length }) } catch (err) { return data @@ -255,7 +271,7 @@ var windowMixin = { }) .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 } diff --git a/package.json b/package.json index d2b65603..835ff779 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/**" } }