From dbab18175935a65fa3a41a4869257953f5ec38cb Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Mon, 31 Jan 2022 16:29:42 +0000 Subject: [PATCH] Admin users can credit accounts --- lnbits/bolt11.py | 39 +++++++++++++++--------- lnbits/core/crud.py | 5 +-- lnbits/core/models.py | 1 + lnbits/core/static/js/wallet.js | 23 ++++++++++++++ lnbits/core/templates/core/wallet.html | 26 +++++++++++++++- lnbits/core/views/api.py | 34 +++++++++++++++++++++ lnbits/core/views/generic.py | 9 +++++- lnbits/decorators.py | 5 ++- lnbits/extensions/lnaddress/crud.py | 2 +- lnbits/extensions/lndhub/views_api.py | 4 ++- lnbits/extensions/lnurldevice/crud.py | 4 ++- lnbits/extensions/offlineshop/helpers.py | 4 +-- lnbits/settings.py | 1 + lnbits/wallets/fake.py | 6 ++-- 14 files changed, 137 insertions(+), 26 deletions(-) diff --git a/lnbits/bolt11.py b/lnbits/bolt11.py index 8b049cbcc..352ceac43 100644 --- a/lnbits/bolt11.py +++ b/lnbits/bolt11.py @@ -11,6 +11,7 @@ from decimal import Decimal import embit import secp256k1 + class Route(NamedTuple): pubkey: str short_channel_id: str @@ -120,8 +121,7 @@ def decode(pr: str) -> Invoice: def encode(options): - """ Convert options into LnAddr and pass it to the encoder - """ + """Convert options into LnAddr and pass it to the encoder""" addr = LnAddr() addr.currency = options.currency addr.fallback = options.fallback if options.fallback else None @@ -268,11 +268,10 @@ class LnAddr(object): def shorten_amount(amount): - """ Given an amount in bitcoin, shorten it - """ + """Given an amount in bitcoin, shorten it""" # Convert to pico initially - amount = int(amount * 10**12) - units = ['p', 'n', 'u', 'm', ''] + amount = int(amount * 10 ** 12) + units = ["p", "n", "u", "m", ""] for unit in units: if amount % 1000 == 0: amount //= 1000 @@ -280,6 +279,7 @@ def shorten_amount(amount): break return str(amount) + unit + def _unshorten_amount(amount: str) -> int: """Given a shortened amount, return millisatoshis""" # BOLT #11: @@ -313,21 +313,31 @@ def _pull_tagged(stream): def is_p2pkh(currency, prefix): return prefix == base58_prefix_map[currency][0] + def is_p2sh(currency, prefix): return prefix == base58_prefix_map[currency][1] + # Tagged field containing BitArray def tagged(char, l): # Tagged fields need to be zero-padded to 5 bits. while l.len % 5 != 0: - l.append('0b0') - return bitstring.pack("uint:5, uint:5, uint:5", - CHARSET.find(char), - (l.len / 5) / 32, (l.len / 5) % 32) + l + l.append("0b0") + return ( + bitstring.pack( + "uint:5, uint:5, uint:5", + CHARSET.find(char), + (l.len / 5) / 32, + (l.len / 5) % 32, + ) + + l + ) + def tagged_bytes(char, l): return tagged(char, bitstring.BitArray(l)) + def _trim_to_bytes(barr): # Adds a byte if necessary. b = barr.tobytes() @@ -338,9 +348,9 @@ def _trim_to_bytes(barr): 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), - outputindex=(short_channel_id & 0xFFFF), + blockheight=((short_channel_id >> 40) & 0xffffff), + transactionindex=((short_channel_id >> 16) & 0xffffff), + outputindex=(short_channel_id & 0xffff), ) @@ -350,10 +360,11 @@ def _u5_to_bitarray(arr: List[int]) -> bitstring.BitArray: ret += bitstring.pack("uint:5", a) return ret + def bitarray_to_u5(barr): assert barr.len % 5 == 0 ret = [] s = bitstring.ConstBitStream(barr) while s.pos != s.len: ret.append(s.read(5).uint) - return ret \ No newline at end of file + return ret diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index f69ca95b1..ad2d9f2cc 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -11,7 +11,6 @@ from lnbits.settings import DEFAULT_WALLET_NAME from . import db from .models import User, Wallet, Payment, BalanceCheck - # accounts # -------- @@ -278,7 +277,9 @@ async def get_payments( return [Payment.from_row(row) for row in rows] -async def delete_expired_invoices(conn: Optional[Connection] = None,) -> None: +async def delete_expired_invoices( + conn: Optional[Connection] = None, +) -> None: # first we delete all invoices older than one month await (conn or db).execute( f""" diff --git a/lnbits/core/models.py b/lnbits/core/models.py index e802c2f8d..88963b2b8 100644 --- a/lnbits/core/models.py +++ b/lnbits/core/models.py @@ -57,6 +57,7 @@ class User(BaseModel): extensions: List[str] = [] wallets: List[Wallet] = [] password: Optional[str] = None + admin: bool = False @property def wallet_ids(self) -> List[str]: diff --git a/lnbits/core/static/js/wallet.js b/lnbits/core/static/js/wallet.js index 3baefc6e3..6b078bdb6 100644 --- a/lnbits/core/static/js/wallet.js +++ b/lnbits/core/static/js/wallet.js @@ -249,6 +249,29 @@ new Vue({ this.parse.data.paymentChecker = null this.parse.camera.show = false }, + updateBalance: function(scopeValue){ + LNbits.api + .request( + 'PUT', + '/api/v1/wallet/balance/' + scopeValue, + this.g.wallet.inkey + ) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + .then(response => { + let data = response.data + if (data.status === 'ERROR') { + this.$q.notify({ + timeout: 5000, + type: 'warning', + message: `Failed to update.`, + }) + return + } + this.balance = this.balance + data.balance + }) + }, closeReceiveDialog: function () { setTimeout(() => { clearInterval(this.receive.paymentChecker) diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html index deca6cbbb..d87800104 100644 --- a/lnbits/core/templates/core/wallet.html +++ b/lnbits/core/templates/core/wallet.html @@ -15,7 +15,31 @@

- {% raw %}{{ formattedBalance }}{% endraw %} sat + {% raw %}{{ formattedBalance }}{% endraw %} + sat + + + + + + +

diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 2dceed490..c3feb4e21 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -38,6 +38,9 @@ from ..crud import ( get_standalone_payment, save_balance_check, update_wallet, + create_payment, + get_wallet, + update_payment_status, ) from ..services import ( InvoiceFailure, @@ -48,6 +51,8 @@ from ..services import ( perform_lnurlauth, ) from ..tasks import api_invoice_listeners +from lnbits.settings import LNBITS_ADMIN_USERS +from lnbits.helpers import urlsafe_short_hash @core_app.get("/api/v1/wallet") @@ -62,6 +67,35 @@ async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)): return {"name": wallet.wallet.name, "balance": wallet.wallet.balance_msat} +@core_app.put("/api/v1/wallet/balance/{amount}") +async def api_update_balance( + amount: int, wallet: WalletTypeInfo = Depends(get_key_type) +): + if LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, detail="Not an admin user" + ) + + payHash = urlsafe_short_hash() + await create_payment( + wallet_id=wallet.wallet.id, + checking_id=payHash, + payment_request="selfPay", + payment_hash=payHash, + amount=amount*1000, + memo="selfPay", + fee=0, + ) + await update_payment_status(checking_id=payHash, pending=False) + updatedWallet = await get_wallet(wallet.wallet.id) + + return { + "id": wallet.wallet.id, + "name": wallet.wallet.name, + "balance": wallet.wallet.balance_msat + amount, + } + + @core_app.put("/api/v1/wallet/{new_name}") async def api_update_wallet( new_name: str, wallet: WalletTypeInfo = Depends(WalletAdminKeyChecker()) diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index b3ae97b13..d917ffab8 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -14,7 +14,12 @@ from lnbits.core import db from lnbits.core.models import User from lnbits.decorators import check_user_exists from lnbits.helpers import template_renderer, url_for -from lnbits.settings import LNBITS_ALLOWED_USERS, LNBITS_SITE_TITLE, SERVICE_FEE +from lnbits.settings import ( + LNBITS_ALLOWED_USERS, + LNBITS_ADMIN_USERS, + LNBITS_SITE_TITLE, + SERVICE_FEE, +) from ..crud import ( create_account, @@ -113,6 +118,8 @@ async def wallet( return template_renderer().TemplateResponse( "error.html", {"request": request, "err": "User not authorized."} ) + if LNBITS_ADMIN_USERS and user_id in LNBITS_ADMIN_USERS: + user.admin = True if not wallet_id: if user.wallets and not wallet_name: wallet = user.wallets[0] diff --git a/lnbits/decorators.py b/lnbits/decorators.py index e76a1fd1c..fc92594ed 100644 --- a/lnbits/decorators.py +++ b/lnbits/decorators.py @@ -13,7 +13,7 @@ from starlette.requests import Request from lnbits.core.crud import get_user, get_wallet_for_key from lnbits.core.models import User, Wallet from lnbits.requestvars import g -from lnbits.settings import LNBITS_ALLOWED_USERS +from lnbits.settings import LNBITS_ALLOWED_USERS, LNBITS_ADMIN_USERS class KeyChecker(SecurityBase): @@ -204,4 +204,7 @@ async def check_user_exists(usr: UUID4) -> User: status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized." ) + if LNBITS_ADMIN_USERS and g().user.id in LNBITS_ADMIN_USERS: + g().user.admin = True + return g().user diff --git a/lnbits/extensions/lnaddress/crud.py b/lnbits/extensions/lnaddress/crud.py index 3bd636f19..3b5822b47 100644 --- a/lnbits/extensions/lnaddress/crud.py +++ b/lnbits/extensions/lnaddress/crud.py @@ -160,7 +160,7 @@ async def set_address_renewed(address_id: str, duration: int): async def check_address_available(username: str, domain: str): - row, = await db.fetchone( + (row,) = await db.fetchone( "SELECT COUNT(username) FROM lnaddress.address WHERE username = ? AND domain = ?", (username, domain), ) diff --git a/lnbits/extensions/lndhub/views_api.py b/lnbits/extensions/lndhub/views_api.py index d28bbd9e9..fdda13562 100644 --- a/lnbits/extensions/lndhub/views_api.py +++ b/lnbits/extensions/lndhub/views_api.py @@ -108,7 +108,9 @@ async def lndhub_payinvoice( @lndhub_ext.get("/ext/balance") -async def lndhub_balance(wallet: WalletTypeInfo = Depends(check_wallet),): +async def lndhub_balance( + wallet: WalletTypeInfo = Depends(check_wallet), +): return {"BTC": {"AvailableBalance": wallet.wallet.balance}} diff --git a/lnbits/extensions/lnurldevice/crud.py b/lnbits/extensions/lnurldevice/crud.py index 431108279..451665212 100644 --- a/lnbits/extensions/lnurldevice/crud.py +++ b/lnbits/extensions/lnurldevice/crud.py @@ -8,7 +8,9 @@ from .models import createLnurldevice, lnurldevicepayment, lnurldevices ###############lnurldeviceS########################## -async def create_lnurldevice(data: createLnurldevice,) -> lnurldevices: +async def create_lnurldevice( + data: createLnurldevice, +) -> lnurldevices: lnurldevice_id = urlsafe_short_hash() lnurldevice_key = urlsafe_short_hash() await db.execute( diff --git a/lnbits/extensions/offlineshop/helpers.py b/lnbits/extensions/offlineshop/helpers.py index 6b56cf559..db2c19cce 100644 --- a/lnbits/extensions/offlineshop/helpers.py +++ b/lnbits/extensions/offlineshop/helpers.py @@ -8,8 +8,8 @@ def hotp(key, counter, digits=6, digest="sha1"): key = base64.b32decode(key.upper() + "=" * ((8 - len(key)) % 8)) counter = struct.pack(">Q", counter) mac = hmac.new(key, counter, digest).digest() - offset = mac[-1] & 0x0F - binary = struct.unpack(">L", mac[offset : offset + 4])[0] & 0x7FFFFFFF + offset = mac[-1] & 0x0f + binary = struct.unpack(">L", mac[offset : offset + 4])[0] & 0x7fffffff return str(binary)[-digits:].zfill(digits) diff --git a/lnbits/settings.py b/lnbits/settings.py index 475f5f478..1ca0c2dac 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -28,6 +28,7 @@ LNBITS_DATABASE_URL = env.str("LNBITS_DATABASE_URL", default=None) LNBITS_ALLOWED_USERS: List[str] = env.list( "LNBITS_ALLOWED_USERS", default=[], subcast=str ) +LNBITS_ADMIN_USERS: List[str] = env.list("LNBITS_ADMIN_USERS", default=[], subcast=str) LNBITS_DISABLED_EXTENSIONS: List[str] = env.list( "LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str ) diff --git a/lnbits/wallets/fake.py b/lnbits/wallets/fake.py index 2c61be759..dcefd2399 100644 --- a/lnbits/wallets/fake.py +++ b/lnbits/wallets/fake.py @@ -37,6 +37,7 @@ class FakeWallet(Wallet): "The FakeWallet backend is for using LNbits as a centralised, stand-alone payment system." ) return StatusResponse(None, float("inf")) + async def create_invoice( self, amount: int, @@ -54,7 +55,9 @@ class FakeWallet(Wallet): self.memo = memo self.description = memo letters = string.ascii_lowercase - randomHash = hashlib.sha256(str(random.getrandbits(256)).encode('utf-8')).hexdigest() + randomHash = hashlib.sha256( + str(random.getrandbits(256)).encode("utf-8") + ).hexdigest() self.paymenthash = randomHash payment_request = encode(self) print(payment_request) @@ -62,7 +65,6 @@ class FakeWallet(Wallet): return InvoiceResponse(True, checking_id, payment_request) - async def pay_invoice(self, bolt11: str) -> PaymentResponse: invoice = decode(bolt11) return PaymentResponse(True, invoice.payment_hash, 0)