Admin users can credit accounts

This commit is contained in:
Ben Arc 2022-01-31 16:29:42 +00:00
parent f5069f9699
commit dbab181759
14 changed files with 137 additions and 26 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,31 @@
<q-card>
<q-card-section>
<h3 class="q-my-none">
<strong>{% raw %}{{ formattedBalance }}{% endraw %}</strong> sat
<strong>{% raw %}{{ formattedBalance }}{% endraw %} </strong>
sat
<q-btn
v-if="'{{user.admin}}' == 'True'"
flat
round
color="primary"
icon="add"
size="md"
>
<q-popup-edit class="bg-accent text-white" v-slot="scope">
<q-input
label="Amount to credit account"
v-model="scope.value"
dense
autofocus
type="number"
@keyup.enter="updateBalance(scope.value)"
>
<template v-slot:append>
<q-icon name="edit" />
</template>
</q-input>
</q-popup-edit>
</q-btn>
</h3>
</q-card-section>
<div class="row q-pb-md q-px-md q-col-gutter-md">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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