Merge branch 'main' into diagon-alley

This commit is contained in:
benarc 2022-02-03 13:57:04 +00:00
commit 3bf1d6c6ae
48 changed files with 1279 additions and 509 deletions

View file

@ -7,6 +7,7 @@ PORT=5000
LNBITS_ALLOWED_USERS="" LNBITS_ALLOWED_USERS=""
LNBITS_ADMIN_USERS=""
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
# Database: to use SQLite, specify LNBITS_DATA_FOLDER # Database: to use SQLite, specify LNBITS_DATA_FOLDER
@ -30,7 +31,7 @@ LNBITS_SITE_DESCRIPTION="Some description about your service, will display if ti
LNBITS_THEME_OPTIONS="mint, flamingo, classic, autumn, monochrome, salvador" LNBITS_THEME_OPTIONS="mint, flamingo, classic, autumn, monochrome, salvador"
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC), # Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC),
# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet # LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet
LNBITS_BACKEND_WALLET_CLASS=VoidWallet LNBITS_BACKEND_WALLET_CLASS=VoidWallet
# VoidWallet is just a fallback that works without any actual Lightning capabilities, # VoidWallet is just a fallback that works without any actual Lightning capabilities,
# just so you can see the UI before dealing with this file. # just so you can see the UI before dealing with this file.
@ -72,3 +73,7 @@ LNTXBOT_KEY=LNTXBOT_ADMIN_KEY
# OpenNodeWallet # OpenNodeWallet
OPENNODE_API_ENDPOINT=https://api.opennode.com/ OPENNODE_API_ENDPOINT=https://api.opennode.com/
OPENNODE_KEY=OPENNODE_ADMIN_KEY OPENNODE_KEY=OPENNODE_ADMIN_KEY
# FakeWallet
FAKE_WALLET_SECRET="ToTheMoon1"
LNBITS_DENOMINATION=sats

View file

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2019 Arc Copyright (c) 2022 Arc
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -29,6 +29,7 @@ uvicorn = {extras = ["standard"], version = "*"}
sse-starlette = "*" sse-starlette = "*"
jinja2 = "3.0.1" jinja2 = "3.0.1"
pyngrok = "*" pyngrok = "*"
secp256k1 = "*"
[dev-packages] [dev-packages]
black = "==20.8b1" black = "==20.8b1"

840
Pipfile.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,6 +6,7 @@ nav_order: 2
# Basic installation # Basic installation
Install Postgres and setup a database for LNbits: Install Postgres and setup a database for LNbits:
```sh ```sh
# on debian/ubuntu 'sudo apt-get -y install postgresql' # on debian/ubuntu 'sudo apt-get -y install postgresql'
# or follow instructions at https://www.postgresql.org/download/linux/ # or follow instructions at https://www.postgresql.org/download/linux/
@ -50,6 +51,7 @@ You might also need to install additional packages or perform additional setup s
If you already have LNbits installed and running, on an SQLite database, we **HIGHLY** recommend you migrate to postgres! If you already have LNbits installed and running, on an SQLite database, we **HIGHLY** recommend you migrate to postgres!
There's a script included that can do the migration easy. You should have Postgres already installed and there should be a password for the user, check the guide above. There's a script included that can do the migration easy. You should have Postgres already installed and there should be a password for the user, check the guide above.
```sh ```sh
# STOP LNbits # STOP LNbits
# on the LNBits folder, locate and edit 'conv.py' with the relevant credentials # on the LNBits folder, locate and edit 'conv.py' with the relevant credentials
@ -67,6 +69,39 @@ Hopefully, everything works and get migrated... Launch LNbits again and check if
# Additional guides # Additional guides
### LNbits as a systemd service
Systemd is great for taking care of your LNbits instance. It will start it on boot and restart it in case it crashes. If you want to run LNbits as a systemd service on your Debian/Ubuntu/Raspbian server, create a file at `/etc/systemd/system/lnbits.service` with the following content:
```
# Systemd unit for lnbits
# /etc/systemd/system/lnbits.service
[Unit]
Description=LNbits
#Wants=lnd.service # you can uncomment these lines if you know what you're doing
#After=lnd.service # it will make sure that lnbits starts after lnd (replace with your own backend service)
[Service]
WorkingDirectory=/home/bitcoin/lnbits # replace with the absolute path of your lnbits installation
ExecStart=/home/bitcoin/lnbits/venv/bin/uvicorn lnbits.__main__:app --port 5000 # same here
User=bitcoin # replace with the user that you're running lnbits on
Restart=always
TimeoutSec=120
RestartSec=30
Environment=PYTHONUNBUFFERED=1 # this makes sure that you receive logs in real time
[Install]
WantedBy=multi-user.target
```
Save the file and run the following commands:
```sh
sudo systemctl enable lnbits.service
sudo systemctl start lnbits.service
```
### LNbits running on Umbrel behind Tor ### LNbits running on Umbrel behind Tor
If you want to run LNbits on your Umbrel but want it to be reached through clearnet, _Uxellodunum_ made an extensive [guide](https://community.getumbrel.com/t/guide-lnbits-without-tor/604) on how to do it. If you want to run LNbits on your Umbrel but want it to be reached through clearnet, _Uxellodunum_ made an extensive [guide](https://community.getumbrel.com/t/guide-lnbits-without-tor/604) on how to do it.

View file

@ -48,10 +48,7 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
origins = ["*"] origins = ["*"]
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware, allow_origins=origins, allow_methods=["*"], allow_headers=["*"]
allow_origins=origins,
allow_methods=["*"],
allow_headers=["*"],
) )
g().config = lnbits.settings g().config = lnbits.settings

View file

@ -2,10 +2,14 @@ import bitstring # type: ignore
import re import re
import hashlib import hashlib
from typing import List, NamedTuple, Optional from typing import List, NamedTuple, Optional
from bech32 import bech32_decode, CHARSET # type: ignore from bech32 import bech32_encode, bech32_decode, CHARSET
from ecdsa import SECP256k1, VerifyingKey # type: ignore from ecdsa import SECP256k1, VerifyingKey # type: ignore
from ecdsa.util import sigdecode_string # type: ignore from ecdsa.util import sigdecode_string # type: ignore
from binascii import unhexlify from binascii import unhexlify
import time
from decimal import Decimal
import embit
import secp256k1
class Route(NamedTuple): class Route(NamedTuple):
@ -116,6 +120,166 @@ def decode(pr: str) -> Invoice:
return invoice return invoice
def encode(options):
"""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
if options["amount"]:
addr.amount = options["amount"]
if options["timestamp"]:
addr.date = int(options["timestamp"])
addr.paymenthash = unhexlify(options["paymenthash"])
if options["description"]:
addr.tags.append(("d", options["description"]))
if options["description_hash"]:
addr.tags.append(("h", options["description_hash"]))
if options["expires"]:
addr.tags.append(("x", options["expires"]))
if options["fallback"]:
addr.tags.append(("f", options["fallback"]))
if options["route"]:
for r in options["route"]:
splits = r.split("/")
route = []
while len(splits) >= 5:
route.append(
(
unhexlify(splits[0]),
unhexlify(splits[1]),
int(splits[2]),
int(splits[3]),
int(splits[4]),
)
)
splits = splits[5:]
assert len(splits) == 0
addr.tags.append(("r", route))
return lnencode(addr, options["privkey"])
def lnencode(addr, privkey):
if addr.amount:
amount = Decimal(str(addr.amount))
# We can only send down to millisatoshi.
if amount * 10 ** 12 % 10:
raise ValueError(
"Cannot encode {}: too many decimal places".format(addr.amount)
)
amount = addr.currency + shorten_amount(amount)
else:
amount = addr.currency if addr.currency else ""
hrp = "ln" + amount + "0n"
# Start with the timestamp
data = bitstring.pack("uint:35", addr.date)
# Payment hash
data += tagged_bytes("p", addr.paymenthash)
tags_set = set()
for k, v in addr.tags:
# BOLT #11:
#
# A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields,
if k in ("d", "h", "n", "x"):
if k in tags_set:
raise ValueError("Duplicate '{}' tag".format(k))
if k == "r":
route = bitstring.BitArray()
for step in v:
pubkey, channel, feebase, feerate, cltv = step
route.append(
bitstring.BitArray(pubkey)
+ bitstring.BitArray(channel)
+ bitstring.pack("intbe:32", feebase)
+ bitstring.pack("intbe:32", feerate)
+ bitstring.pack("intbe:16", cltv)
)
data += tagged("r", route)
elif k == "f":
data += encode_fallback(v, addr.currency)
elif k == "d":
data += tagged_bytes("d", v.encode())
elif k == "x":
# Get minimal length by trimming leading 5 bits at a time.
expirybits = bitstring.pack("intbe:64", v)[4:64]
while expirybits.startswith("0b00000"):
expirybits = expirybits[5:]
data += tagged("x", expirybits)
elif k == "h":
data += tagged_bytes("h", hashlib.sha256(v.encode("utf-8")).digest())
elif k == "n":
data += tagged_bytes("n", v)
else:
# FIXME: Support unknown tags?
raise ValueError("Unknown tag {}".format(k))
tags_set.add(k)
# BOLT #11:
#
# A writer MUST include either a `d` or `h` field, and MUST NOT include
# both.
if "d" in tags_set and "h" in tags_set:
raise ValueError("Cannot include both 'd' and 'h'")
if not "d" in tags_set and not "h" in tags_set:
raise ValueError("Must include either 'd' or 'h'")
# We actually sign the hrp, then data (padded to 8 bits with zeroes).
privkey = secp256k1.PrivateKey(bytes(unhexlify(privkey)))
sig = privkey.ecdsa_sign_recoverable(
bytearray([ord(c) for c in hrp]) + data.tobytes()
)
# This doesn't actually serialize, but returns a pair of values :(
sig, recid = privkey.ecdsa_recoverable_serialize(sig)
data += bytes(sig) + bytes([recid])
return bech32_encode(hrp, bitarray_to_u5(data))
class LnAddr(object):
def __init__(
self, paymenthash=None, amount=None, currency="bc", tags=None, date=None
):
self.date = int(time.time()) if not date else int(date)
self.tags = [] if not tags else tags
self.unknown_tags = []
self.paymenthash = paymenthash
self.signature = None
self.pubkey = None
self.currency = currency
self.amount = amount
def __str__(self):
return "LnAddr[{}, amount={}{} tags=[{}]]".format(
hexlify(self.pubkey.serialize()).decode("utf-8"),
self.amount,
self.currency,
", ".join([k + "=" + str(v) for k, v in self.tags]),
)
def shorten_amount(amount):
"""Given an amount in bitcoin, shorten it"""
# Convert to pico initially
amount = int(amount * 10 ** 12)
units = ["p", "n", "u", "m", ""]
for unit in units:
if amount % 1000 == 0:
amount //= 1000
else:
break
return str(amount) + unit
def _unshorten_amount(amount: str) -> int: def _unshorten_amount(amount: str) -> int:
"""Given a shortened amount, return millisatoshis""" """Given a shortened amount, return millisatoshis"""
# BOLT #11: # BOLT #11:
@ -146,6 +310,34 @@ def _pull_tagged(stream):
return (CHARSET[tag], stream.read(length * 5), stream) return (CHARSET[tag], stream.read(length * 5), 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
)
def tagged_bytes(char, l):
return tagged(char, bitstring.BitArray(l))
def _trim_to_bytes(barr): def _trim_to_bytes(barr):
# Adds a byte if necessary. # Adds a byte if necessary.
b = barr.tobytes() b = barr.tobytes()
@ -156,9 +348,9 @@ def _trim_to_bytes(barr):
def _readable_scid(short_channel_id: int) -> str: def _readable_scid(short_channel_id: int) -> str:
return "{blockheight}x{transactionindex}x{outputindex}".format( return "{blockheight}x{transactionindex}x{outputindex}".format(
blockheight=((short_channel_id >> 40) & 0xFFFFFF), blockheight=((short_channel_id >> 40) & 0xffffff),
transactionindex=((short_channel_id >> 16) & 0xFFFFFF), transactionindex=((short_channel_id >> 16) & 0xffffff),
outputindex=(short_channel_id & 0xFFFF), outputindex=(short_channel_id & 0xffff),
) )
@ -167,3 +359,12 @@ def _u5_to_bitarray(arr: List[int]) -> bitstring.BitArray:
for a in arr: for a in arr:
ret += bitstring.pack("uint:5", a) ret += bitstring.pack("uint:5", a)
return ret 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

View file

@ -11,7 +11,6 @@ from lnbits.settings import DEFAULT_WALLET_NAME
from . import db from . import db
from .models import User, Wallet, Payment, BalanceCheck from .models import User, Wallet, Payment, BalanceCheck
# accounts # accounts
# -------- # --------
@ -30,8 +29,7 @@ async def get_account(
user_id: str, conn: Optional[Connection] = None user_id: str, conn: Optional[Connection] = None
) -> Optional[User]: ) -> Optional[User]:
row = await (conn or db).fetchone( row = await (conn or db).fetchone(
"SELECT id, email, pass as password FROM accounts WHERE id = ?", ( "SELECT id, email, pass as password FROM accounts WHERE id = ?", (user_id,)
user_id,)
) )
return User(**row) if row else None return User(**row) if row else None
@ -185,7 +183,7 @@ async def get_standalone_payment(
""" """
SELECT * SELECT *
FROM apipayments FROM apipayments
WHERE (checking_id = ? OR hash = ?) AND amount > 0 -- only the incoming payment WHERE checking_id = ? OR hash = ?
LIMIT 1 LIMIT 1
""", """,
(checking_id_or_hash, checking_id_or_hash), (checking_id_or_hash, checking_id_or_hash),
@ -279,7 +277,9 @@ async def get_payments(
return [Payment.from_row(row) for row in rows] 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 # first we delete all invoices older than one month
await (conn or db).execute( await (conn or db).execute(
f""" f"""
@ -305,8 +305,7 @@ async def delete_expired_invoices(conn: Optional[Connection] = None,) -> None:
except: except:
continue continue
expiration_date = datetime.datetime.fromtimestamp( expiration_date = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
invoice.date + invoice.expiry)
if expiration_date > datetime.datetime.utcnow(): if expiration_date > datetime.datetime.utcnow():
continue continue

View file

@ -57,6 +57,7 @@ class User(BaseModel):
extensions: List[str] = [] extensions: List[str] = []
wallets: List[Wallet] = [] wallets: List[Wallet] = []
password: Optional[str] = None password: Optional[str] = None
admin: bool = False
@property @property
def wallet_ids(self) -> List[str]: def wallet_ids(self) -> List[str]:

View file

@ -148,8 +148,12 @@ async def pay_invoice(
# do the balance check if external payment # do the balance check if external payment
else: else:
if invoice.amount_msat > wallet.balance_msat - (wallet.balance_msat / 100 * 2): if invoice.amount_msat > wallet.balance_msat - (
raise PermissionError("LNbits requires you keep at least 2% reserve to cover potential routing fees.") wallet.balance_msat / 100 * 2
):
raise PermissionError(
"LNbits requires you keep at least 2% reserve to cover potential routing fees."
)
if internal_checking_id: if internal_checking_id:
# mark the invoice from the other side as not pending anymore # mark the invoice from the other side as not pending anymore
@ -326,8 +330,7 @@ async def check_invoice_status(
if not payment.pending: if not payment.pending:
return status return status
if payment.is_out and status.failed: if payment.is_out and status.failed:
print( print(f" - deleting outgoing failed payment {payment.checking_id}: {status}")
f" - deleting outgoing failed payment {payment.checking_id}: {status}")
await payment.delete() await payment.delete()
elif not status.pending: elif not status.pending:
print( print(

View file

@ -161,14 +161,14 @@ new Vue({
{ {
name: 'sat', name: 'sat',
align: 'right', align: 'right',
label: 'Amount (sat)', label: 'Amount (' + LNBITS_DENOMINATION + ')',
field: 'sat', field: 'sat',
sortable: true sortable: true
}, },
{ {
name: 'fee', name: 'fee',
align: 'right', align: 'right',
label: 'Fee (msat)', label: 'Fee (m' + LNBITS_DENOMINATION + ')',
field: 'fee' field: 'fee'
} }
], ],
@ -185,12 +185,17 @@ new Vue({
location: window.location location: window.location
}, },
balance: 0, balance: 0,
credit: 0,
newName: '' newName: ''
} }
}, },
computed: { computed: {
formattedBalance: function () { formattedBalance: function () {
if (LNBITS_DENOMINATION != 'sats') {
return this.balance / 100
} else {
return LNbits.utils.formatSat(this.balance || this.g.wallet.sat) return LNbits.utils.formatSat(this.balance || this.g.wallet.sat)
}
}, },
filteredPayments: function () { filteredPayments: function () {
var q = this.paymentsTable.filter var q = this.paymentsTable.filter
@ -249,6 +254,28 @@ new Vue({
this.parse.data.paymentChecker = null this.parse.data.paymentChecker = null
this.parse.camera.show = false this.parse.camera.show = false
}, },
updateBalance: function (credit) {
if (LNBITS_DENOMINATION != 'sats') {
credit = credit * 100
}
LNbits.api
.request('PUT', '/api/v1/wallet/balance/' + credit, 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 () { closeReceiveDialog: function () {
setTimeout(() => { setTimeout(() => {
clearInterval(this.receive.paymentChecker) clearInterval(this.receive.paymentChecker)
@ -271,7 +298,9 @@ new Vue({
}, },
createInvoice: function () { createInvoice: function () {
this.receive.status = 'loading' this.receive.status = 'loading'
if (LNBITS_DENOMINATION != 'sats') {
this.receive.data.amount = this.receive.data.amount * 100
}
LNbits.api LNbits.api
.createInvoice( .createInvoice(
this.g.wallet, this.g.wallet,

View file

@ -15,7 +15,51 @@
<q-card> <q-card>
<q-card-section> <q-card-section>
<h3 class="q-my-none"> <h3 class="q-my-none">
<strong>{% raw %}{{ formattedBalance }}{% endraw %}</strong> sat <strong>{% raw %}{{ formattedBalance }} {% endraw %}</strong>
{{LNBITS_DENOMINATION}}
<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"
v-model="credit"
>
<q-input
v-if="'{{LNBITS_DENOMINATION}}' != 'sats'"
label="Amount to credit account"
v-model="scope.value"
dense
autofocus
mask="#.##"
fill-mask="0"
reverse-fill-mask
@keyup.enter="updateBalance(scope.value)"
>
<template v-slot:append>
<q-icon name="edit" />
</template>
</q-input>
<q-input
v-else
type="number"
label="Amount to credit account"
v-model="scope.value"
dense
autofocus
@keyup.enter="updateBalance(scope.value)"
>
<template v-slot:append>
<q-icon name="edit" />
</template>
</q-input>
</q-popup-edit>
</q-btn>
</h3> </h3>
</q-card-section> </q-card-section>
<div class="row q-pb-md q-px-md q-col-gutter-md"> <div class="row q-pb-md q-px-md q-col-gutter-md">
@ -141,7 +185,17 @@
<q-tooltip>{{ props.row.date }}</q-tooltip> <q-tooltip>{{ props.row.date }}</q-tooltip>
{{ props.row.dateFrom }} {{ props.row.dateFrom }}
</q-td> </q-td>
<q-td auto-width key="sat" :props="props"> {% endraw %}
<q-td
auto-width
key="sat"
v-if="'{{LNBITS_DENOMINATION}}' != 'sats'"
:props="props"
>{% raw %} {{ parseFloat(String(props.row.fsat).replaceAll(",",
"")) / 100 }}
</q-td>
<q-td auto-width key="sat" v-else :props="props">
{{ props.row.fsat }} {{ props.row.fsat }}
</q-td> </q-td>
<q-td auto-width key="fee" :props="props"> <q-td auto-width key="fee" :props="props">
@ -223,7 +277,7 @@
<q-card> <q-card>
<q-card-section> <q-card-section>
<h6 class="text-subtitle1 q-mt-none q-mb-sm"> <h6 class="text-subtitle1 q-mt-none q-mb-sm">
{{ SITE_TITLE }} wallet: <strong><em>{{ wallet.name }}</em></strong> {{ SITE_TITLE }} Wallet: <strong><em>{{ wallet.name }}</em></strong>
</h6> </h6>
</q-card-section> </q-card-section>
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
@ -337,7 +391,20 @@
<p v-if="receive.lnurl" class="text-h6 text-center q-my-none"> <p v-if="receive.lnurl" class="text-h6 text-center q-my-none">
<b>{{receive.lnurl.domain}}</b> is requesting an invoice: <b>{{receive.lnurl.domain}}</b> is requesting an invoice:
</p> </p>
{% endraw %} {% if LNBITS_DENOMINATION != 'sats' %}
<q-input
filled
dense
v-model.number="receive.data.amount"
label="Amount ({{LNBITS_DENOMINATION}}) *"
mask="#.##"
fill-mask="0"
reverse-fill-mask
:min="receive.minMax[0]"
:max="receive.minMax[1]"
:readonly="receive.lnurl && receive.lnurl.fixed"
></q-input>
{% else %}
<q-select <q-select
filled filled
dense dense
@ -351,19 +418,21 @@
dense dense
v-model.number="receive.data.amount" v-model.number="receive.data.amount"
type="number" type="number"
:label="`Amount (${receive.unit}) *`" label="Amount ({{LNBITS_DENOMINATION}}) *"
:step="receive.unit != 'sat' ? '0.001' : '1'" :step="receive.unit != 'sat' ? '0.001' : '1'"
:min="receive.minMax[0]" :min="receive.minMax[0]"
:max="receive.minMax[1]" :max="receive.minMax[1]"
:readonly="receive.lnurl && receive.lnurl.fixed" :readonly="receive.lnurl && receive.lnurl.fixed"
></q-input> ></q-input>
{% endif %}
<q-input <q-input
filled filled
dense dense
v-model.trim="receive.data.memo" v-model.trim="receive.data.memo"
label="Memo *" label="Memo"
placeholder="LNbits invoice"
></q-input> ></q-input>
{% raw %}
<div v-if="receive.status == 'pending'" class="row q-mt-lg"> <div v-if="receive.status == 'pending'" class="row q-mt-lg">
<q-btn <q-btn
unelevated unelevated
@ -410,8 +479,13 @@
<q-dialog v-model="parse.show" @hide="closeParseDialog"> <q-dialog v-model="parse.show" @hide="closeParseDialog">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> <q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div v-if="parse.invoice"> <div v-if="parse.invoice">
{% raw %} <h6 v-if="'{{LNBITS_DENOMINATION}}' != 'sats'" class="q-my-none">
<h6 class="q-my-none">{{ parse.invoice.fsat }} sat</h6> {% raw %} {{ parseFloat(String(parse.invoice.fsat).replaceAll(",", ""))
/ 100 }} {% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
</h6>
<h6 v-else class="q-my-none">
{{ parse.invoice.fsat }}{% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
</h6>
<q-separator class="q-my-sm"></q-separator> <q-separator class="q-my-sm"></q-separator>
<p class="text-wrap"> <p class="text-wrap">
<strong>Description:</strong> {{ parse.invoice.description }}<br /> <strong>Description:</strong> {{ parse.invoice.description }}<br />
@ -461,7 +535,7 @@
<q-form @submit="payLnurl" class="q-gutter-md"> <q-form @submit="payLnurl" class="q-gutter-md">
<p v-if="parse.lnurlpay.fixed" class="q-my-none text-h6"> <p v-if="parse.lnurlpay.fixed" class="q-my-none text-h6">
<b>{{ parse.lnurlpay.domain }}</b> is requesting {{ <b>{{ parse.lnurlpay.domain }}</b> is requesting {{
parse.lnurlpay.maxSendable | msatoshiFormat }} sat parse.lnurlpay.maxSendable | msatoshiFormat }} {{LNBITS_DENOMINATION}}
<span v-if="parse.lnurlpay.commentAllowed > 0"> <span v-if="parse.lnurlpay.commentAllowed > 0">
<br /> <br />
and a {{parse.lnurlpay.commentAllowed}}-char comment and a {{parse.lnurlpay.commentAllowed}}-char comment
@ -471,7 +545,8 @@
<b>{{ parse.lnurlpay.targetUser || parse.lnurlpay.domain }}</b> is <b>{{ parse.lnurlpay.targetUser || parse.lnurlpay.domain }}</b> is
requesting <br /> requesting <br />
between <b>{{ parse.lnurlpay.minSendable | msatoshiFormat }}</b> and between <b>{{ parse.lnurlpay.minSendable | msatoshiFormat }}</b> and
<b>{{ parse.lnurlpay.maxSendable | msatoshiFormat }}</b> sat <b>{{ parse.lnurlpay.maxSendable | msatoshiFormat }}</b>
{% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
<span v-if="parse.lnurlpay.commentAllowed > 0"> <span v-if="parse.lnurlpay.commentAllowed > 0">
<br /> <br />
and a {{parse.lnurlpay.commentAllowed}}-char comment and a {{parse.lnurlpay.commentAllowed}}-char comment
@ -488,16 +563,18 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
{% endraw %}
<q-input <q-input
filled filled
dense dense
v-model.number="parse.data.amount" v-model.number="parse.data.amount"
type="number" type="number"
label="Amount (sat) *" label="Amount ({{LNBITS_DENOMINATION}}) *"
:min="parse.lnurlpay.minSendable / 1000" :min="parse.lnurlpay.minSendable / 1000"
:max="parse.lnurlpay.maxSendable / 1000" :max="parse.lnurlpay.maxSendable / 1000"
:readonly="parse.lnurlpay.fixed" :readonly="parse.lnurlpay.fixed"
></q-input> ></q-input>
{% raw %}
</div> </div>
<div class="col-8 q-pl-md" v-if="parse.lnurlpay.commentAllowed > 0"> <div class="col-8 q-pl-md" v-if="parse.lnurlpay.commentAllowed > 0">
<q-input <q-input
@ -511,7 +588,9 @@
</div> </div>
</div> </div>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit">Send satoshis</q-btn> <q-btn unelevated color="primary" type="submit"
>Send {{LNBITS_DENOMINATION}}</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto" <q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn >Cancel</q-btn
> >

View file

@ -38,6 +38,9 @@ from ..crud import (
get_standalone_payment, get_standalone_payment,
save_balance_check, save_balance_check,
update_wallet, update_wallet,
create_payment,
get_wallet,
update_payment_status,
) )
from ..services import ( from ..services import (
InvoiceFailure, InvoiceFailure,
@ -48,6 +51,8 @@ from ..services import (
perform_lnurlauth, perform_lnurlauth,
) )
from ..tasks import api_invoice_listeners 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") @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} 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": amount,
}
@core_app.put("/api/v1/wallet/{new_name}") @core_app.put("/api/v1/wallet/{new_name}")
async def api_update_wallet( async def api_update_wallet(
new_name: str, wallet: WalletTypeInfo = Depends(WalletAdminKeyChecker()) new_name: str, wallet: WalletTypeInfo = Depends(WalletAdminKeyChecker())
@ -77,7 +111,9 @@ async def api_update_wallet(
@core_app.get("/api/v1/payments") @core_app.get("/api/v1/payments")
async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)): async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)):
await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True) await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True)
pendingPayments = await get_payments(wallet_id=wallet.wallet.id, pending=True, exclude_uncheckable=True) pendingPayments = await get_payments(
wallet_id=wallet.wallet.id, pending=True, exclude_uncheckable=True
)
for payment in pendingPayments: for payment in pendingPayments:
await check_invoice_status( await check_invoice_status(
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
@ -131,8 +167,8 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
lnurl_response: Union[None, bool, str] = None lnurl_response: Union[None, bool, str] = None
if data.lnurl_callback: if data.lnurl_callback:
if "lnurl_balance_check" in g().data: if "lnurl_balance_check" in data:
save_balance_check(g().wallet.id, data.lnurl_balance_check) save_balance_check(wallet.id, data.lnurl_balance_check)
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
try: try:
@ -143,7 +179,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
"balanceNotify": url_for( "balanceNotify": url_for(
f"/withdraw/notify/{urlparse(data.lnurl_callback).netloc}", f"/withdraw/notify/{urlparse(data.lnurl_callback).netloc}",
external=True, external=True,
wal=g().wallet.id, wal=wallet.id,
), ),
}, },
timeout=10, timeout=10,
@ -198,8 +234,7 @@ async def api_payments_create(
invoiceData: CreateInvoiceData = Body(...), invoiceData: CreateInvoiceData = Body(...),
): ):
if wallet.wallet_type < 0 or wallet.wallet_type > 2: if wallet.wallet_type < 0 or wallet.wallet_type > 2:
raise HTTPException( raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
if invoiceData.out is True and wallet.wallet_type == 0: if invoiceData.out is True and wallet.wallet_type == 0:
if not invoiceData.bolt11: if not invoiceData.bolt11:
@ -254,14 +289,14 @@ async def api_payments_pay_lnurl(
if invoice.amount_msat != data.amount: if invoice.amount_msat != data.amount:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} returned an invalid invoice. Expected {data['amount']} msat, got {invoice.amount_msat}.", detail=f"{domain} returned an invalid invoice. Expected {data.amount} msat, got {invoice.amount_msat}.",
) )
if invoice.description_hash != data.description_hash: # if invoice.description_hash != data.description_hash:
raise HTTPException( # raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, # status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} returned an invalid invoice. Expected description_hash == {data['description_hash']}, got {invoice.description_hash}.", # detail=f"{domain} returned an invalid invoice. Expected description_hash == {data.description_hash}, got {invoice.description_hash}.",
) # )
extra = {} extra = {}
@ -379,16 +414,14 @@ async def api_lnurlscan(code: str):
params.update(callback=url) # with k1 already in it params.update(callback=url) # with k1 already in it
lnurlauth_key = g().wallet.lnurlauth_key(domain) lnurlauth_key = g().wallet.lnurlauth_key(domain)
params.update( params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
else: else:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
r = await client.get(url, timeout=5) r = await client.get(url, timeout=5)
if r.is_error: if r.is_error:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE, status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail={"domain": domain, detail={"domain": domain, "message": "failed to get parameters"},
"message": "failed to get parameters"},
) )
try: try:
@ -418,8 +451,7 @@ async def api_lnurlscan(code: str):
if tag == "withdrawRequest": if tag == "withdrawRequest":
params.update(kind="withdraw") params.update(kind="withdraw")
params.update(fixed=data["minWithdrawable"] params.update(fixed=data["minWithdrawable"] == data["maxWithdrawable"])
== data["maxWithdrawable"])
# callback with k1 already in it # callback with k1 already in it
parsed_callback: ParseResult = urlparse(data["callback"]) parsed_callback: ParseResult = urlparse(data["callback"])
@ -509,18 +541,21 @@ async def api_list_currencies_available():
class ConversionData(BaseModel): class ConversionData(BaseModel):
from_: str = Field('sat', alias="from") from_: str = Field("sat", alias="from")
amount: float amount: float
to: str = Query('usd') to: str = Query("usd")
@core_app.post("/api/v1/conversion") @core_app.post("/api/v1/conversion")
async def api_fiat_as_sats(data: ConversionData): async def api_fiat_as_sats(data: ConversionData):
output = {} output = {}
if data.from_ == 'sat': if data.from_ == "sat":
output["sats"] = int(data.amount) output["sats"] = int(data.amount)
output["BTC"] = data.amount / 100000000 output["BTC"] = data.amount / 100000000
for currency in data.to.split(','): for currency in data.to.split(","):
output[currency.strip().upper()] = await satoshis_amount_as_fiat(data.amount, currency.strip()) output[currency.strip().upper()] = await satoshis_amount_as_fiat(
data.amount, currency.strip()
)
return output return output
else: else:
output[data.from_.upper()] = data.amount output[data.from_.upper()] = data.amount

View file

@ -14,7 +14,12 @@ from lnbits.core import db
from lnbits.core.models import User from lnbits.core.models import User
from lnbits.decorators import check_user_exists from lnbits.decorators import check_user_exists
from lnbits.helpers import template_renderer, url_for 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 ( from ..crud import (
create_account, create_account,
@ -43,9 +48,7 @@ async def home(request: Request, lightning: str = None):
@core_html_routes.get( @core_html_routes.get(
"/extensions", "/extensions", name="core.extensions", response_class=HTMLResponse
name="core.extensions",
response_class=HTMLResponse,
) )
async def extensions( async def extensions(
request: Request, request: Request,
@ -115,6 +118,8 @@ async def wallet(
return template_renderer().TemplateResponse( return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": "User not authorized."} "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 not wallet_id:
if user.wallets and not wallet_name: if user.wallets and not wallet_name:
wallet = user.wallets[0] 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.crud import get_user, get_wallet_for_key
from lnbits.core.models import User, Wallet from lnbits.core.models import User, Wallet
from lnbits.requestvars import g 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): class KeyChecker(SecurityBase):
@ -171,6 +171,7 @@ async def require_admin_key(
else: else:
return wallet return wallet
async def require_invoice_key( async def require_invoice_key(
r: Request, r: Request,
api_key_header: str = Security(api_key_header), api_key_header: str = Security(api_key_header),
@ -184,7 +185,8 @@ async def require_invoice_key(
# If wallet type is not invoice then return the unauthorized status # If wallet type is not invoice then return the unauthorized status
# This also covers when the user passes an invalid key type # This also covers when the user passes an invalid key type
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invoice (or Admin) key required." status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invoice (or Admin) key required.",
) )
else: else:
return wallet return wallet
@ -202,4 +204,7 @@ async def check_user_exists(usr: UUID4) -> User:
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized." 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 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): 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 = ?", "SELECT COUNT(username) FROM lnaddress.address WHERE username = ? AND domain = ?",
(username, domain), (username, domain),
) )

View file

@ -71,7 +71,9 @@ async def lnurl_callback(address_id, amount: int = Query(...)):
"out": False, "out": False,
"amount": int(amount_received / 1000), "amount": int(amount_received / 1000),
"description_hash": hashlib.sha256( "description_hash": hashlib.sha256(
(await address.lnurlpay_metadata(domain=domain.domain)).encode("utf-8") (await address.lnurlpay_metadata(domain=domain.domain)).encode(
"utf-8"
)
).hexdigest(), ).hexdigest(),
"extra": {"tag": f"Payment to {address.username}@{domain.domain}"}, "extra": {"tag": f"Payment to {address.username}@{domain.domain}"},
}, },

View file

@ -45,6 +45,6 @@ async def display(domain_id, request: Request):
"domain_domain": domain.domain, "domain_domain": domain.domain,
"domain_cost": domain.cost, "domain_cost": domain.cost,
"domain_wallet_inkey": wallet.inkey, "domain_wallet_inkey": wallet.inkey,
"root_url": f"{url.scheme}://{url.netloc}" "root_url": f"{url.scheme}://{url.netloc}",
}, },
) )

View file

@ -108,7 +108,9 @@ async def lndhub_payinvoice(
@lndhub_ext.get("/ext/balance") @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}} return {"BTC": {"AvailableBalance": wallet.wallet.balance}}

View file

@ -8,7 +8,9 @@ from .models import createLnurldevice, lnurldevicepayment, lnurldevices
###############lnurldeviceS########################## ###############lnurldeviceS##########################
async def create_lnurldevice(data: createLnurldevice,) -> lnurldevices: async def create_lnurldevice(
data: createLnurldevice,
) -> lnurldevices:
lnurldevice_id = urlsafe_short_hash() lnurldevice_id = urlsafe_short_hash()
lnurldevice_key = urlsafe_short_hash() lnurldevice_key = urlsafe_short_hash()
await db.execute( await db.execute(
@ -24,7 +26,15 @@ async def create_lnurldevice(data: createLnurldevice,) -> lnurldevices:
) )
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
""", """,
(lnurldevice_id, lnurldevice_key, data.title, data.wallet, data.currency, data.device, data.profit,), (
lnurldevice_id,
lnurldevice_key,
data.title,
data.wallet,
data.currency,
data.device,
data.profit,
),
) )
return await get_lnurldevice(lnurldevice_id) return await get_lnurldevice(lnurldevice_id)
@ -63,7 +73,9 @@ async def get_lnurldevices(wallet_ids: Union[str, List[str]]) -> List[lnurldevic
async def delete_lnurldevice(lnurldevice_id: str) -> None: async def delete_lnurldevice(lnurldevice_id: str) -> None:
await db.execute("DELETE FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)) await db.execute(
"DELETE FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)
)
########################lnuldevice payments########################### ########################lnuldevice payments###########################
@ -102,19 +114,23 @@ async def update_lnurldevicepayment(
(*kwargs.values(), lnurldevicepayment_id), (*kwargs.values(), lnurldevicepayment_id),
) )
row = await db.fetchone( row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE id = ?", (lnurldevicepayment_id,) "SELECT * FROM lnurldevice.lnurldevicepayment WHERE id = ?",
(lnurldevicepayment_id,),
) )
return lnurldevicepayment(**row) if row else None return lnurldevicepayment(**row) if row else None
async def get_lnurldevicepayment(lnurldevicepayment_id: str) -> lnurldevicepayment: async def get_lnurldevicepayment(lnurldevicepayment_id: str) -> lnurldevicepayment:
row = await db.fetchone( row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE id = ?", (lnurldevicepayment_id,) "SELECT * FROM lnurldevice.lnurldevicepayment WHERE id = ?",
(lnurldevicepayment_id,),
) )
return lnurldevicepayment(**row) if row else None return lnurldevicepayment(**row) if row else None
async def get_lnurlpayload(lnurldevicepayment_payload: str) -> lnurldevicepayment: async def get_lnurlpayload(lnurldevicepayment_payload: str) -> lnurldevicepayment:
row = await db.fetchone( row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE payload = ?", (lnurldevicepayment_payload,) "SELECT * FROM lnurldevice.lnurldevicepayment WHERE payload = ?",
(lnurldevicepayment_payload,),
) )
return lnurldevicepayment(**row) if row else None return lnurldevicepayment(**row) if row else None

View file

@ -105,10 +105,7 @@ async def lnurl_v1_params(
paymentcheck = await get_lnurlpayload(p) paymentcheck = await get_lnurlpayload(p)
if device.device == "atm": if device.device == "atm":
if paymentcheck: if paymentcheck:
return { return {"status": "ERROR", "reason": f"Payment already claimed"}
"status": "ERROR",
"reason": f"Payment already claimed",
}
if len(p) % 4 > 0: if len(p) % 4 > 0:
p += "=" * (4 - (len(p) % 4)) p += "=" * (4 - (len(p) % 4))
@ -174,13 +171,17 @@ async def lnurl_v1_params(
} }
@lnurldevice_ext.get( @lnurldevice_ext.get(
"/api/v1/lnurl/cb/{paymentid}", "/api/v1/lnurl/cb/{paymentid}",
status_code=HTTPStatus.OK, status_code=HTTPStatus.OK,
name="lnurldevice.lnurl_callback", name="lnurldevice.lnurl_callback",
) )
async def lnurl_callback(request: Request, paymentid: str = Query(None), pr: str = Query(None), k1: str = Query(None)): async def lnurl_callback(
request: Request,
paymentid: str = Query(None),
pr: str = Query(None),
k1: str = Query(None),
):
lnurldevicepayment = await get_lnurldevicepayment(paymentid) lnurldevicepayment = await get_lnurldevicepayment(paymentid)
device = await get_lnurldevice(lnurldevicepayment.deviceid) device = await get_lnurldevice(lnurldevicepayment.deviceid)
if not device: if not device:
@ -191,10 +192,7 @@ async def lnurl_callback(request: Request, paymentid: str = Query(None), pr: str
if lnurldevicepayment.id != k1: if lnurldevicepayment.id != k1:
return {"status": "ERROR", "reason": "Bad K1"} return {"status": "ERROR", "reason": "Bad K1"}
if lnurldevicepayment.payhash != "payment_hash": if lnurldevicepayment.payhash != "payment_hash":
return { return {"status": "ERROR", "reason": f"Payment already claimed"}
"status": "ERROR",
"reason": f"Payment already claimed",
}
lnurldevicepayment = await update_lnurldevicepayment( lnurldevicepayment = await update_lnurldevicepayment(
lnurldevicepayment_id=paymentid, payhash=lnurldevicepayment.payload lnurldevicepayment_id=paymentid, payhash=lnurldevicepayment.payload
) )

View file

@ -2,6 +2,7 @@ from lnbits.db import Database
db2 = Database("ext_lnurlpos") db2 = Database("ext_lnurlpos")
async def m001_initial(db): async def m001_initial(db):
""" """
Initial lnurldevice table. Initial lnurldevice table.
@ -39,6 +40,7 @@ async def m002_redux(db):
""" """
Moves everything from lnurlpos to lnurldevices Moves everything from lnurlpos to lnurldevices
""" """
try:
for row in [ for row in [
list(row) for row in await db2.fetchall("SELECT * FROM lnurlpos.lnurlposs") list(row) for row in await db2.fetchall("SELECT * FROM lnurlpos.lnurlposs")
]: ]:
@ -58,7 +60,8 @@ async def m002_redux(db):
(row[0], row[1], row[2], row[3], row[4], "pos", 0), (row[0], row[1], row[2], row[3], row[4], "pos", 0),
) )
for row in [ for row in [
list(row) for row in await db2.fetchall("SELECT * FROM lnurlpos.lnurlpospayment") list(row)
for row in await db2.fetchall("SELECT * FROM lnurlpos.lnurlpospayment")
]: ]:
await db.execute( await db.execute(
""" """
@ -74,3 +77,5 @@ async def m002_redux(db):
""", """,
(row[0], row[1], row[3], row[4], row[5], row[6]), (row[0], row[1], row[3], row[4], row[5], row[6]),
) )
except:
return

View file

@ -33,12 +33,15 @@ class lnurldevices(BaseModel):
return cls(**dict(row)) return cls(**dict(row))
def lnurl(self, req: Request) -> Lnurl: def lnurl(self, req: Request) -> Lnurl:
url = req.url_for("lnurldevice.lnurl_response", device_id=self.id, _external=True) url = req.url_for(
"lnurldevice.lnurl_response", device_id=self.id, _external=True
)
return lnurl_encode(url) return lnurl_encode(url)
async def lnurlpay_metadata(self) -> LnurlPayMetadata: async def lnurlpay_metadata(self) -> LnurlPayMetadata:
return LnurlPayMetadata(json.dumps([["text/plain", self.title]])) return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
class lnurldevicepayment(BaseModel): class lnurldevicepayment(BaseModel):
id: str id: str
deviceid: str deviceid: str

View file

@ -41,10 +41,13 @@ async def displaypin(request: Request, paymentid: str = Query(None)):
) )
status = await api_payment(lnurldevicepayment.payhash) status = await api_payment(lnurldevicepayment.payhash)
if status["paid"]: if status["paid"]:
await update_payment_status(checking_id=lnurldevicepayment.payhash, pending=True) await update_payment_status(
checking_id=lnurldevicepayment.payhash, pending=True
)
return lnurldevice_renderer().TemplateResponse( return lnurldevice_renderer().TemplateResponse(
"lnurldevice/paid.html", {"request": request, "pin": lnurldevicepayment.pin} "lnurldevice/paid.html", {"request": request, "pin": lnurldevicepayment.pin}
) )
return lnurldevice_renderer().TemplateResponse( return lnurldevice_renderer().TemplateResponse(
"lnurldevice/error.html", {"request": request, "pin": "filler", "not_paid": True} "lnurldevice/error.html",
{"request": request, "pin": "filler", "not_paid": True},
) )

View file

@ -48,7 +48,9 @@ async def api_lnurldevice_create_or_update(
async def api_lnurldevices_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)): async def api_lnurldevices_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
try: try:
return [{**lnurldevice.dict()} for lnurldevice in await get_lnurldevices(wallet_ids)] return [
{**lnurldevice.dict()} for lnurldevice in await get_lnurldevices(wallet_ids)
]
except: except:
return "" return ""
@ -71,7 +73,8 @@ async def api_lnurldevice_retrieve(
@lnurldevice_ext.delete("/api/v1/lnurlpos/{lnurldevice_id}") @lnurldevice_ext.delete("/api/v1/lnurlpos/{lnurldevice_id}")
async def api_lnurldevice_delete( async def api_lnurldevice_delete(
wallet: WalletTypeInfo = Depends(require_admin_key), lnurldevice_id: str = Query(None) wallet: WalletTypeInfo = Depends(require_admin_key),
lnurldevice_id: str = Query(None),
): ):
lnurldevice = await get_lnurldevice(lnurldevice_id) lnurldevice = await get_lnurldevice(lnurldevice_id)

View file

@ -13,10 +13,12 @@ lnurlpayout_ext: APIRouter = APIRouter(prefix="/lnurlpayout", tags=["lnurlpayout
def lnurlpayout_renderer(): def lnurlpayout_renderer():
return template_renderer(["lnbits/extensions/lnurlpayout/templates"]) return template_renderer(["lnbits/extensions/lnurlpayout/templates"])
from .tasks import wait_for_paid_invoices from .tasks import wait_for_paid_invoices
from .views import * # noqa from .views import * # noqa
from .views_api import * # noqa from .views_api import * # noqa
def lnurlpayout_start(): def lnurlpayout_start():
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))

View file

@ -6,14 +6,23 @@ from . import db
from .models import lnurlpayout, CreateLnurlPayoutData from .models import lnurlpayout, CreateLnurlPayoutData
async def create_lnurlpayout(wallet_id: str, admin_key: str, data: CreateLnurlPayoutData) -> lnurlpayout: async def create_lnurlpayout(
wallet_id: str, admin_key: str, data: CreateLnurlPayoutData
) -> lnurlpayout:
lnurlpayout_id = urlsafe_short_hash() lnurlpayout_id = urlsafe_short_hash()
await db.execute( await db.execute(
""" """
INSERT INTO lnurlpayout.lnurlpayouts (id, title, wallet, admin_key, lnurlpay, threshold) INSERT INTO lnurlpayout.lnurlpayouts (id, title, wallet, admin_key, lnurlpay, threshold)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
""", """,
(lnurlpayout_id, data.title, wallet_id, admin_key, data.lnurlpay, data.threshold), (
lnurlpayout_id,
data.title,
wallet_id,
admin_key,
data.lnurlpay,
data.threshold,
),
) )
lnurlpayout = await get_lnurlpayout(lnurlpayout_id) lnurlpayout = await get_lnurlpayout(lnurlpayout_id)
@ -22,13 +31,19 @@ async def create_lnurlpayout(wallet_id: str, admin_key: str, data: CreateLnurlPa
async def get_lnurlpayout(lnurlpayout_id: str) -> Optional[lnurlpayout]: async def get_lnurlpayout(lnurlpayout_id: str) -> Optional[lnurlpayout]:
row = await db.fetchone("SELECT * FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,)) row = await db.fetchone(
"SELECT * FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,)
)
return lnurlpayout(**row) if row else None return lnurlpayout(**row) if row else None
async def get_lnurlpayout_from_wallet(wallet_id: str) -> Optional[lnurlpayout]: async def get_lnurlpayout_from_wallet(wallet_id: str) -> Optional[lnurlpayout]:
row = await db.fetchone("SELECT * FROM lnurlpayout.lnurlpayouts WHERE wallet = ?", (wallet_id,)) row = await db.fetchone(
"SELECT * FROM lnurlpayout.lnurlpayouts WHERE wallet = ?", (wallet_id,)
)
return lnurlpayout(**row) if row else None return lnurlpayout(**row) if row else None
async def get_lnurlpayouts(wallet_ids: Union[str, List[str]]) -> List[lnurlpayout]: async def get_lnurlpayouts(wallet_ids: Union[str, List[str]]) -> List[lnurlpayout]:
if isinstance(wallet_ids, str): if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
@ -42,4 +57,6 @@ async def get_lnurlpayouts(wallet_ids: Union[str, List[str]]) -> List[lnurlpayou
async def delete_lnurlpayout(lnurlpayout_id: str) -> None: async def delete_lnurlpayout(lnurlpayout_id: str) -> None:
await db.execute("DELETE FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,)) await db.execute(
"DELETE FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,)
)

View file

@ -2,11 +2,13 @@ from sqlite3 import Row
from pydantic import BaseModel from pydantic import BaseModel
class CreateLnurlPayoutData(BaseModel): class CreateLnurlPayoutData(BaseModel):
title: str title: str
lnurlpay: str lnurlpay: str
threshold: int threshold: int
class lnurlpayout(BaseModel): class lnurlpayout(BaseModel):
id: str id: str
title: str title: str

View file

@ -30,7 +30,9 @@ async def on_invoice_paid(payment: Payment) -> None:
# Check the wallet balance is more than the threshold # Check the wallet balance is more than the threshold
wallet = await get_wallet(lnurlpayout_link.wallet) wallet = await get_wallet(lnurlpayout_link.wallet)
if wallet.balance < lnurlpayout_link.threshold + (lnurlpayout_link.threshold*0.02): if wallet.balance < lnurlpayout_link.threshold + (
lnurlpayout_link.threshold * 0.02
):
return return
# Get the invoice from the LNURL to pay # Get the invoice from the LNURL to pay
@ -38,16 +40,19 @@ async def on_invoice_paid(payment: Payment) -> None:
try: try:
url = await api_payments_decode({"data": lnurlpayout_link.lnurlpay}) url = await api_payments_decode({"data": lnurlpayout_link.lnurlpay})
if str(url["domain"])[0:4] != "http": if str(url["domain"])[0:4] != "http":
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken") raise HTTPException(
try: status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken"
r = await client.get(
str(url["domain"]),
timeout=40,
) )
try:
r = await client.get(str(url["domain"]), timeout=40)
res = r.json() res = r.json()
try: try:
r = await client.get( r = await client.get(
res["callback"] + "?amount=" + str(int((wallet.balance - wallet.balance*0.02) * 1000)), res["callback"]
+ "?amount="
+ str(
int((wallet.balance - wallet.balance * 0.02) * 1000)
),
timeout=40, timeout=40,
) )
res = r.json() res = r.json()
@ -65,6 +70,9 @@ async def on_invoice_paid(payment: Payment) -> None:
except (httpx.ConnectError, httpx.RequestError): except (httpx.ConnectError, httpx.RequestError):
return return
except Exception: except Exception:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Failed to save LNURLPayout") raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Failed to save LNURLPayout",
)
except: except:
return return

View file

@ -10,10 +10,17 @@ from lnbits.core.views.api import api_payment, api_payments_decode
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from . import lnurlpayout_ext from . import lnurlpayout_ext
from .crud import create_lnurlpayout, delete_lnurlpayout, get_lnurlpayout, get_lnurlpayouts, get_lnurlpayout_from_wallet from .crud import (
create_lnurlpayout,
delete_lnurlpayout,
get_lnurlpayout,
get_lnurlpayouts,
get_lnurlpayout_from_wallet,
)
from .models import lnurlpayout, CreateLnurlPayoutData from .models import lnurlpayout, CreateLnurlPayoutData
from .tasks import on_invoice_paid from .tasks import on_invoice_paid
@lnurlpayout_ext.get("/api/v1/lnurlpayouts", status_code=HTTPStatus.OK) @lnurlpayout_ext.get("/api/v1/lnurlpayouts", status_code=HTTPStatus.OK)
async def api_lnurlpayouts( async def api_lnurlpayouts(
all_wallets: bool = Query(None), wallet: WalletTypeInfo = Depends(get_key_type) all_wallets: bool = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)
@ -30,21 +37,31 @@ async def api_lnurlpayout_create(
data: CreateLnurlPayoutData, wallet: WalletTypeInfo = Depends(get_key_type) data: CreateLnurlPayoutData, wallet: WalletTypeInfo = Depends(get_key_type)
): ):
if await get_lnurlpayout_from_wallet(wallet.wallet.id): if await get_lnurlpayout_from_wallet(wallet.wallet.id):
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Wallet already has lnurlpayout set") raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Wallet already has lnurlpayout set",
)
return return
url = await api_payments_decode({"data": data.lnurlpay}) url = await api_payments_decode({"data": data.lnurlpay})
if "domain" not in url: if "domain" not in url:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="LNURL could not be decoded") raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="LNURL could not be decoded"
)
return return
if str(url["domain"])[0:4] != "http": if str(url["domain"])[0:4] != "http":
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not valid LNURL") raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not valid LNURL")
return return
lnurlpayout = await create_lnurlpayout(wallet_id=wallet.wallet.id, admin_key=wallet.wallet.adminkey, data=data) lnurlpayout = await create_lnurlpayout(
wallet_id=wallet.wallet.id, admin_key=wallet.wallet.adminkey, data=data
)
if not lnurlpayout: if not lnurlpayout:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Failed to save LNURLPayout") raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Failed to save LNURLPayout"
)
return return
return lnurlpayout.dict() return lnurlpayout.dict()
@lnurlpayout_ext.delete("/api/v1/lnurlpayouts/{lnurlpayout_id}") @lnurlpayout_ext.delete("/api/v1/lnurlpayouts/{lnurlpayout_id}")
async def api_lnurlpayout_delete( async def api_lnurlpayout_delete(
lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
@ -57,22 +74,30 @@ async def api_lnurlpayout_delete(
) )
if lnurlpayout.wallet != wallet.wallet.id: if lnurlpayout.wallet != wallet.wallet.id:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your lnurlpayout.") raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your lnurlpayout."
)
await delete_lnurlpayout(lnurlpayout_id) await delete_lnurlpayout(lnurlpayout_id)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT) raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
@lnurlpayout_ext.get("/api/v1/lnurlpayouts/{lnurlpayout_id}", status_code=HTTPStatus.OK) @lnurlpayout_ext.get("/api/v1/lnurlpayouts/{lnurlpayout_id}", status_code=HTTPStatus.OK)
async def api_lnurlpayout_check( async def api_lnurlpayout_check(
lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(get_key_type) lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
): ):
lnurlpayout = await get_lnurlpayout(lnurlpayout_id) lnurlpayout = await get_lnurlpayout(lnurlpayout_id)
payments = await get_payments( payments = await get_payments(
wallet_id=lnurlpayout.wallet, complete=True, pending=False, outgoing=True, incoming=True wallet_id=lnurlpayout.wallet,
complete=True,
pending=False,
outgoing=True,
incoming=True,
) )
result = await on_invoice_paid(payments[0]) result = await on_invoice_paid(payments[0])
return return
# get payouts func # get payouts func
# lnurlpayouts = await get_lnurlpayouts(wallet_ids) # lnurlpayouts = await get_lnurlpayouts(wallet_ids)
# for lnurlpayout in lnurlpayouts: # for lnurlpayout in lnurlpayouts:

View file

@ -8,8 +8,8 @@ def hotp(key, counter, digits=6, digest="sha1"):
key = base64.b32decode(key.upper() + "=" * ((8 - len(key)) % 8)) key = base64.b32decode(key.upper() + "=" * ((8 - len(key)) % 8))
counter = struct.pack(">Q", counter) counter = struct.pack(">Q", counter)
mac = hmac.new(key, counter, digest).digest() mac = hmac.new(key, counter, digest).digest()
offset = mac[-1] & 0x0F offset = mac[-1] & 0x0f
binary = struct.unpack(">L", mac[offset : offset + 4])[0] & 0x7FFFFFFF binary = struct.unpack(">L", mac[offset : offset + 4])[0] & 0x7fffffff
return str(binary)[-digits:].zfill(digits) return str(binary)[-digits:].zfill(digits)

View file

@ -15,6 +15,7 @@ satspay_ext: APIRouter = APIRouter(prefix="/satspay", tags=["satspay"])
def satspay_renderer(): def satspay_renderer():
return template_renderer(["lnbits/extensions/satspay/templates"]) return template_renderer(["lnbits/extensions/satspay/templates"])
from .tasks import wait_for_paid_invoices from .tasks import wait_for_paid_invoices
from .views import * # noqa from .views import * # noqa
from .views_api import * # noqa from .views_api import * # noqa

View file

@ -28,4 +28,3 @@ async def on_invoice_paid(payment: Payment) -> None:
await payment.set_pending(False) await payment.set_pending(False)
await check_address_balance(charge_id=charge.id) await check_address_balance(charge_id=charge.id)

View file

@ -32,5 +32,6 @@ async def display(request: Request, charge_id):
) )
wallet = await get_wallet(charge.lnbitswallet) wallet = await get_wallet(charge.lnbitswallet)
return satspay_renderer().TemplateResponse( return satspay_renderer().TemplateResponse(
"satspay/display.html", {"request": request, "charge": charge, "wallet_key": wallet.inkey} "satspay/display.html",
{"request": request, "charge": charge, "wallet_key": wallet.inkey},
) )

View file

@ -25,14 +25,15 @@ from .models import CreateCharge
#############################CHARGES########################## #############################CHARGES##########################
@satspay_ext.post("/api/v1/charge") @satspay_ext.post("/api/v1/charge")
async def api_charge_create( async def api_charge_create(
data: CreateCharge, data: CreateCharge, wallet: WalletTypeInfo = Depends(require_invoice_key)
wallet: WalletTypeInfo = Depends(require_invoice_key)
): ):
charge = await create_charge(user=wallet.wallet.user, data=data) charge = await create_charge(user=wallet.wallet.user, data=data)
return charge.dict() return charge.dict()
@satspay_ext.put("/api/v1/charge/{charge_id}") @satspay_ext.put("/api/v1/charge/{charge_id}")
async def api_charge_update( async def api_charge_update(
data: CreateCharge, data: CreateCharge,

View file

@ -156,6 +156,7 @@ def template_renderer(additional_folders: List = []) -> Jinja2Templates:
) )
) )
t.env.globals["SITE_TITLE"] = settings.LNBITS_SITE_TITLE t.env.globals["SITE_TITLE"] = settings.LNBITS_SITE_TITLE
t.env.globals["LNBITS_DENOMINATION"] = settings.LNBITS_DENOMINATION
t.env.globals["SITE_TAGLINE"] = settings.LNBITS_SITE_TAGLINE t.env.globals["SITE_TAGLINE"] = settings.LNBITS_SITE_TAGLINE
t.env.globals["SITE_DESCRIPTION"] = settings.LNBITS_SITE_DESCRIPTION t.env.globals["SITE_DESCRIPTION"] = settings.LNBITS_SITE_DESCRIPTION
t.env.globals["LNBITS_THEME_OPTIONS"] = settings.LNBITS_THEME_OPTIONS t.env.globals["LNBITS_THEME_OPTIONS"] = settings.LNBITS_THEME_OPTIONS

View file

@ -28,11 +28,13 @@ LNBITS_DATABASE_URL = env.str("LNBITS_DATABASE_URL", default=None)
LNBITS_ALLOWED_USERS: List[str] = env.list( LNBITS_ALLOWED_USERS: List[str] = env.list(
"LNBITS_ALLOWED_USERS", default=[], subcast=str "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: List[str] = env.list(
"LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str "LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str
) )
LNBITS_SITE_TITLE = env.str("LNBITS_SITE_TITLE", default="LNbits") LNBITS_SITE_TITLE = env.str("LNBITS_SITE_TITLE", default="LNbits")
LNBITS_DENOMINATION = env.str("LNBITS_DENOMINATION", default="sats")
LNBITS_SITE_TAGLINE = env.str( LNBITS_SITE_TAGLINE = env.str(
"LNBITS_SITE_TAGLINE", default="free and open-source lightning wallet" "LNBITS_SITE_TAGLINE", default="free and open-source lightning wallet"
) )

View file

@ -22,7 +22,8 @@ Vue.component('lnbits-wallet-list', {
activeWallet: null, activeWallet: null,
activeBalance: [], activeBalance: [],
showForm: false, showForm: false,
walletName: '' walletName: '',
LNBITS_DENOMINATION: LNBITS_DENOMINATION
} }
}, },
template: ` template: `
@ -43,7 +44,8 @@ Vue.component('lnbits-wallet-list', {
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label lines="1">{{ wallet.name }}</q-item-label> <q-item-label lines="1">{{ wallet.name }}</q-item-label>
<q-item-label caption>{{ wallet.live_fsat }} sat</q-item-label> <q-item-label v-if="LNBITS_DENOMINATION != 'sats'" caption>{{ parseFloat(String(wallet.live_fsat).replaceAll(",", "")) / 100 }} {{ LNBITS_DENOMINATION }}</q-item-label>
<q-item-label v-else caption>{{ wallet.live_fsat }} {{ LNBITS_DENOMINATION }}</q-item-label>
</q-item-section> </q-item-section>
<q-item-section side v-show="activeWallet && activeWallet.id === wallet.id"> <q-item-section side v-show="activeWallet && activeWallet.id === wallet.id">
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon> <q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
@ -194,11 +196,11 @@ Vue.component('lnbits-payment-details', {
</div> </div>
<div class="row"> <div class="row">
<div class="col-3"><b>Amount</b>:</div> <div class="col-3"><b>Amount</b>:</div>
<div class="col-9">{{ (payment.amount / 1000).toFixed(3) }} sat</div> <div class="col-9">{{ (payment.amount / 1000).toFixed(3) }} {{LNBITS_DENOMINATION}}</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-3"><b>Fee</b>:</div> <div class="col-3"><b>Fee</b>:</div>
<div class="col-9">{{ (payment.fee / 1000).toFixed(3) }} sat</div> <div class="col-9">{{ (payment.fee / 1000).toFixed(3) }} {{LNBITS_DENOMINATION}}</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-3"><b>Payment hash</b>:</div> <div class="col-3"><b>Payment hash</b>:</div>

View file

@ -192,6 +192,8 @@
<script src="/static/js/components.js"></script> <script src="/static/js/components.js"></script>
<script type="text/javascript"> <script type="text/javascript">
const themes = {{ LNBITS_THEME_OPTIONS | tojson }} const themes = {{ LNBITS_THEME_OPTIONS | tojson }}
const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson}}
console.log(LNBITS_DENOMINATION)
if(themes && themes.length) { if(themes && themes.length) {
window.allowedThemes = themes.map(str => str.trim()) window.allowedThemes = themes.map(str => str.trim())
} }

View file

@ -285,5 +285,6 @@ async def get_fiat_rate_satoshis(currency: str) -> float:
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int: async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
return int(amount * (await get_fiat_rate_satoshis(currency))) return int(amount * (await get_fiat_rate_satoshis(currency)))
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float: async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
return float(amount / (await get_fiat_rate_satoshis(currency))) return float(amount / (await get_fiat_rate_satoshis(currency)))

View file

@ -9,3 +9,4 @@ from .lnpay import LNPayWallet
from .lnbits import LNbitsWallet from .lnbits import LNbitsWallet
from .lndrest import LndRestWallet from .lndrest import LndRestWallet
from .spark import SparkWallet from .spark import SparkWallet
from .fake import FakeWallet

83
lnbits/wallets/fake.py Normal file
View file

@ -0,0 +1,83 @@
import asyncio
import json
import httpx
from os import getenv
from datetime import datetime, timedelta
from typing import Optional, Dict, AsyncGenerator
import random
import string
from lnbits.helpers import urlsafe_short_hash
import hashlib
from ..bolt11 import encode, decode
from .base import (
StatusResponse,
InvoiceResponse,
PaymentResponse,
PaymentStatus,
Wallet,
)
class FakeWallet(Wallet):
async def status(self) -> StatusResponse:
print(
"FakeWallet funding source is for using LNbits as a centralised, stand-alone payment system with brrrrrr."
)
return StatusResponse(None, float("inf"))
async def create_invoice(
self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
) -> InvoiceResponse:
secret = getenv("FAKE_WALLET_SECRET")
data: Dict = {
"out": False,
"amount": amount,
"currency": "bc",
"privkey": hashlib.pbkdf2_hmac('sha256', secret.encode("utf-8"), ("FakeWallet").encode("utf-8"), 2048, 32).hex(),
"memo": None,
"description_hash": None,
"description": "",
"fallback": None,
"expires": None,
"route": None,
}
data["amount"] = amount * 1000
data["timestamp"] = datetime.now().timestamp()
if description_hash:
data["tags_set"] = ["h"]
data["description_hash"] = description_hash.hex()
else:
data["tags_set"] = ["d"]
data["memo"] = memo
data["description"] = memo
randomHash = data["privkey"][:6] + hashlib.sha256(
str(random.getrandbits(256)).encode("utf-8")
).hexdigest()[6:]
data["paymenthash"] = randomHash
payment_request = encode(data)
checking_id = randomHash
return InvoiceResponse(True, checking_id, payment_request)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
invoice = decode(bolt11)
if hasattr(invoice, 'checking_id') and invoice.checking_id[6:] == data["privkey"][:6]:
return PaymentResponse(True, invoice.payment_hash, 0)
else:
return PaymentResponse(ok = False, error_message="Only internal invoices can be used!")
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
return PaymentStatus(False)
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
return PaymentStatus(False)
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
self.queue = asyncio.Queue(0)
while True:
value = await self.queue.get()
yield value

View file

@ -23,8 +23,7 @@ class LndRestWallet(Wallet):
endpoint = getenv("LND_REST_ENDPOINT") endpoint = getenv("LND_REST_ENDPOINT")
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
endpoint = ( endpoint = (
"https://" + "https://" + endpoint if not endpoint.startswith("http") else endpoint
endpoint if not endpoint.startswith("http") else endpoint
) )
self.endpoint = endpoint self.endpoint = endpoint
@ -103,10 +102,7 @@ class LndRestWallet(Wallet):
r = await client.post( r = await client.post(
url=f"{self.endpoint}/v1/channels/transactions", url=f"{self.endpoint}/v1/channels/transactions",
headers=self.auth, headers=self.auth,
json={ json={"payment_request": bolt11, "fee_limit": lnrpcFeeLimit},
"payment_request": bolt11,
"fee_limit": lnrpcFeeLimit,
},
timeout=180, timeout=180,
) )
@ -183,8 +179,7 @@ class LndRestWallet(Wallet):
except: except:
continue continue
payment_hash = base64.b64decode( payment_hash = base64.b64decode(inv["r_hash"]).hex()
inv["r_hash"]).hex()
yield payment_hash yield payment_hash
except (OSError, httpx.ConnectError, httpx.ReadError): except (OSError, httpx.ConnectError, httpx.ReadError):
pass pass

View file

@ -20,11 +20,10 @@ class VoidWallet(Wallet):
raise Unsupported("") raise Unsupported("")
async def status(self) -> StatusResponse: async def status(self) -> StatusResponse:
print("This backend does nothing, it is here just as a placeholder, you must configure an actual backend before being able to do anything useful with LNbits.") print(
return StatusResponse( "This backend does nothing, it is here just as a placeholder, you must configure an actual backend before being able to do anything useful with LNbits."
None,
0,
) )
return StatusResponse(None, 0)
async def pay_invoice(self, bolt11: str) -> PaymentResponse: async def pay_invoice(self, bolt11: str) -> PaymentResponse:
raise Unsupported("") raise Unsupported("")

2
package-lock.json generated
View file

@ -1,5 +1,5 @@
{ {
"name": "lnbits", "name": "lnbits-legend",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {

View file

@ -33,6 +33,7 @@ python-dotenv==0.19.0
pyyaml==5.4.1 pyyaml==5.4.1
represent==1.6.0.post0 represent==1.6.0.post0
rfc3986==1.5.0 rfc3986==1.5.0
secp256k1==0.14.0
shortuuid==1.0.1 shortuuid==1.0.1
six==1.16.0 six==1.16.0
sniffio==1.2.0 sniffio==1.2.0