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_ADMIN_USERS=""
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
# 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"
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC),
# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet
# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
# just so you can see the UI before dealing with this file.
@ -72,3 +73,7 @@ LNTXBOT_KEY=LNTXBOT_ADMIN_KEY
# OpenNodeWallet
OPENNODE_API_ENDPOINT=https://api.opennode.com/
OPENNODE_KEY=OPENNODE_ADMIN_KEY
# FakeWallet
FAKE_WALLET_SECRET="ToTheMoon1"
LNBITS_DENOMINATION=sats

View file

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

View file

@ -29,6 +29,7 @@ uvicorn = {extras = ["standard"], version = "*"}
sse-starlette = "*"
jinja2 = "3.0.1"
pyngrok = "*"
secp256k1 = "*"
[dev-packages]
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
Install Postgres and setup a database for LNbits:
```sh
# on debian/ubuntu 'sudo apt-get -y install postgresql'
# 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!
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
# STOP LNbits
# 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
### 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
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 = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_methods=["*"],
allow_headers=["*"],
CORSMiddleware, allow_origins=origins, allow_methods=["*"], allow_headers=["*"]
)
g().config = lnbits.settings

View file

@ -2,10 +2,14 @@ import bitstring # type: ignore
import re
import hashlib
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.util import sigdecode_string # type: ignore
from binascii import unhexlify
import time
from decimal import Decimal
import embit
import secp256k1
class Route(NamedTuple):
@ -116,6 +120,166 @@ def decode(pr: str) -> 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:
"""Given a shortened amount, return millisatoshis"""
# BOLT #11:
@ -146,6 +310,34 @@ def _pull_tagged(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):
# Adds a byte if necessary.
b = barr.tobytes()
@ -156,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),
)
@ -167,3 +359,12 @@ def _u5_to_bitarray(arr: List[int]) -> bitstring.BitArray:
for a in arr:
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

View file

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

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

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

View file

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

View file

@ -15,7 +15,51 @@
<q-card>
<q-card-section>
<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>
</q-card-section>
<div class="row q-pb-md q-px-md q-col-gutter-md">
@ -141,7 +185,17 @@
<q-tooltip>{{ props.row.date }}</q-tooltip>
{{ props.row.dateFrom }}
</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 }}
</q-td>
<q-td auto-width key="fee" :props="props">
@ -223,7 +277,7 @@
<q-card>
<q-card-section>
<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>
</q-card-section>
<q-card-section class="q-pa-none">
@ -337,7 +391,20 @@
<p v-if="receive.lnurl" class="text-h6 text-center q-my-none">
<b>{{receive.lnurl.domain}}</b> is requesting an invoice:
</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
filled
dense
@ -351,19 +418,21 @@
dense
v-model.number="receive.data.amount"
type="number"
:label="`Amount (${receive.unit}) *`"
label="Amount ({{LNBITS_DENOMINATION}}) *"
:step="receive.unit != 'sat' ? '0.001' : '1'"
:min="receive.minMax[0]"
:max="receive.minMax[1]"
:readonly="receive.lnurl && receive.lnurl.fixed"
></q-input>
{% endif %}
<q-input
filled
dense
v-model.trim="receive.data.memo"
label="Memo *"
placeholder="LNbits invoice"
label="Memo"
></q-input>
{% raw %}
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
<q-btn
unelevated
@ -410,8 +479,13 @@
<q-dialog v-model="parse.show" @hide="closeParseDialog">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div v-if="parse.invoice">
{% raw %}
<h6 class="q-my-none">{{ parse.invoice.fsat }} sat</h6>
<h6 v-if="'{{LNBITS_DENOMINATION}}' != 'sats'" class="q-my-none">
{% 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>
<p class="text-wrap">
<strong>Description:</strong> {{ parse.invoice.description }}<br />
@ -461,7 +535,7 @@
<q-form @submit="payLnurl" class="q-gutter-md">
<p v-if="parse.lnurlpay.fixed" class="q-my-none text-h6">
<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">
<br />
and a {{parse.lnurlpay.commentAllowed}}-char comment
@ -471,7 +545,8 @@
<b>{{ parse.lnurlpay.targetUser || parse.lnurlpay.domain }}</b> is
requesting <br />
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">
<br />
and a {{parse.lnurlpay.commentAllowed}}-char comment
@ -488,16 +563,18 @@
</div>
<div class="row">
<div class="col">
{% endraw %}
<q-input
filled
dense
v-model.number="parse.data.amount"
type="number"
label="Amount (sat) *"
label="Amount ({{LNBITS_DENOMINATION}}) *"
:min="parse.lnurlpay.minSendable / 1000"
:max="parse.lnurlpay.maxSendable / 1000"
:readonly="parse.lnurlpay.fixed"
></q-input>
{% raw %}
</div>
<div class="col-8 q-pl-md" v-if="parse.lnurlpay.commentAllowed > 0">
<q-input
@ -511,7 +588,9 @@
</div>
</div>
<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"
>Cancel</q-btn
>

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": amount,
}
@core_app.put("/api/v1/wallet/{new_name}")
async def api_update_wallet(
new_name: str, wallet: WalletTypeInfo = Depends(WalletAdminKeyChecker())
@ -77,7 +111,9 @@ async def api_update_wallet(
@core_app.get("/api/v1/payments")
async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)):
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:
await check_invoice_status(
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
if data.lnurl_callback:
if "lnurl_balance_check" in g().data:
save_balance_check(g().wallet.id, data.lnurl_balance_check)
if "lnurl_balance_check" in data:
save_balance_check(wallet.id, data.lnurl_balance_check)
async with httpx.AsyncClient() as client:
try:
@ -143,7 +179,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
"balanceNotify": url_for(
f"/withdraw/notify/{urlparse(data.lnurl_callback).netloc}",
external=True,
wal=g().wallet.id,
wal=wallet.id,
),
},
timeout=10,
@ -198,8 +234,7 @@ async def api_payments_create(
invoiceData: CreateInvoiceData = Body(...),
):
if wallet.wallet_type < 0 or wallet.wallet_type > 2:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
if invoiceData.out is True and wallet.wallet_type == 0:
if not invoiceData.bolt11:
@ -254,14 +289,14 @@ async def api_payments_pay_lnurl(
if invoice.amount_msat != data.amount:
raise HTTPException(
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:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} returned an invalid invoice. Expected description_hash == {data['description_hash']}, got {invoice.description_hash}.",
)
# if invoice.description_hash != data.description_hash:
# raise HTTPException(
# status_code=HTTPStatus.BAD_REQUEST,
# detail=f"{domain} returned an invalid invoice. Expected description_hash == {data.description_hash}, got {invoice.description_hash}.",
# )
extra = {}
@ -379,16 +414,14 @@ async def api_lnurlscan(code: str):
params.update(callback=url) # with k1 already in it
lnurlauth_key = g().wallet.lnurlauth_key(domain)
params.update(
pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
else:
async with httpx.AsyncClient() as client:
r = await client.get(url, timeout=5)
if r.is_error:
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail={"domain": domain,
"message": "failed to get parameters"},
detail={"domain": domain, "message": "failed to get parameters"},
)
try:
@ -418,8 +451,7 @@ async def api_lnurlscan(code: str):
if tag == "withdrawRequest":
params.update(kind="withdraw")
params.update(fixed=data["minWithdrawable"]
== data["maxWithdrawable"])
params.update(fixed=data["minWithdrawable"] == data["maxWithdrawable"])
# callback with k1 already in it
parsed_callback: ParseResult = urlparse(data["callback"])
@ -509,18 +541,21 @@ async def api_list_currencies_available():
class ConversionData(BaseModel):
from_: str = Field('sat', alias="from")
from_: str = Field("sat", alias="from")
amount: float
to: str = Query('usd')
to: str = Query("usd")
@core_app.post("/api/v1/conversion")
async def api_fiat_as_sats(data: ConversionData):
output = {}
if data.from_ == 'sat':
if data.from_ == "sat":
output["sats"] = int(data.amount)
output["BTC"] = data.amount / 100000000
for currency in data.to.split(','):
output[currency.strip().upper()] = await satoshis_amount_as_fiat(data.amount, currency.strip())
for currency in data.to.split(","):
output[currency.strip().upper()] = await satoshis_amount_as_fiat(
data.amount, currency.strip()
)
return output
else:
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.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,
@ -43,9 +48,7 @@ async def home(request: Request, lightning: str = None):
@core_html_routes.get(
"/extensions",
name="core.extensions",
response_class=HTMLResponse,
"/extensions", name="core.extensions", response_class=HTMLResponse
)
async def extensions(
request: Request,
@ -115,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):
@ -171,6 +171,7 @@ async def require_admin_key(
else:
return wallet
async def require_invoice_key(
r: Request,
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
# This also covers when the user passes an invalid key type
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:
return wallet
@ -202,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

@ -71,7 +71,9 @@ async def lnurl_callback(address_id, amount: int = Query(...)):
"out": False,
"amount": int(amount_received / 1000),
"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(),
"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_cost": domain.cost,
"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")
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(
@ -24,7 +26,15 @@ async def create_lnurldevice(data: createLnurldevice,) -> lnurldevices:
)
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)
@ -63,7 +73,9 @@ async def get_lnurldevices(wallet_ids: Union[str, List[str]]) -> List[lnurldevic
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###########################
@ -102,19 +114,23 @@ async def update_lnurldevicepayment(
(*kwargs.values(), lnurldevicepayment_id),
)
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
async def get_lnurldevicepayment(lnurldevicepayment_id: str) -> lnurldevicepayment:
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
async def get_lnurlpayload(lnurldevicepayment_payload: str) -> lnurldevicepayment:
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

View file

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

View file

@ -2,6 +2,7 @@ from lnbits.db import Database
db2 = Database("ext_lnurlpos")
async def m001_initial(db):
"""
Initial lnurldevice table.
@ -39,6 +40,7 @@ async def m002_redux(db):
"""
Moves everything from lnurlpos to lnurldevices
"""
try:
for row in [
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),
)
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(
"""
@ -74,3 +77,5 @@ async def m002_redux(db):
""",
(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))
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)
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
class lnurldevicepayment(BaseModel):
id: str
deviceid: str

View file

@ -41,10 +41,13 @@ async def displaypin(request: Request, paymentid: str = Query(None)):
)
status = await api_payment(lnurldevicepayment.payhash)
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(
"lnurldevice/paid.html", {"request": request, "pin": lnurldevicepayment.pin}
)
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)):
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
try:
return [{**lnurldevice.dict()} for lnurldevice in await get_lnurldevices(wallet_ids)]
return [
{**lnurldevice.dict()} for lnurldevice in await get_lnurldevices(wallet_ids)
]
except:
return ""
@ -71,7 +73,8 @@ async def api_lnurldevice_retrieve(
@lnurldevice_ext.delete("/api/v1/lnurlpos/{lnurldevice_id}")
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)

View file

@ -13,10 +13,12 @@ lnurlpayout_ext: APIRouter = APIRouter(prefix="/lnurlpayout", tags=["lnurlpayout
def lnurlpayout_renderer():
return template_renderer(["lnbits/extensions/lnurlpayout/templates"])
from .tasks import wait_for_paid_invoices
from .views import * # noqa
from .views_api import * # noqa
def lnurlpayout_start():
loop = asyncio.get_event_loop()
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
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()
await db.execute(
"""
INSERT INTO lnurlpayout.lnurlpayouts (id, title, wallet, admin_key, lnurlpay, threshold)
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)
@ -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]:
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
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
async def get_lnurlpayouts(wallet_ids: Union[str, List[str]]) -> List[lnurlpayout]:
if isinstance(wallet_ids, str):
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:
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
class CreateLnurlPayoutData(BaseModel):
title: str
lnurlpay: str
threshold: int
class lnurlpayout(BaseModel):
id: 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
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
# Get the invoice from the LNURL to pay
@ -38,16 +40,19 @@ async def on_invoice_paid(payment: Payment) -> None:
try:
url = await api_payments_decode({"data": lnurlpayout_link.lnurlpay})
if str(url["domain"])[0:4] != "http":
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken")
try:
r = await client.get(
str(url["domain"]),
timeout=40,
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken"
)
try:
r = await client.get(str(url["domain"]), timeout=40)
res = r.json()
try:
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,
)
res = r.json()
@ -65,6 +70,9 @@ async def on_invoice_paid(payment: Payment) -> None:
except (httpx.ConnectError, httpx.RequestError):
return
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:
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 . 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 .tasks import on_invoice_paid
@lnurlpayout_ext.get("/api/v1/lnurlpayouts", status_code=HTTPStatus.OK)
async def api_lnurlpayouts(
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)
):
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
url = await api_payments_decode({"data": data.lnurlpay})
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
if str(url["domain"])[0:4] != "http":
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not valid LNURL")
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:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Failed to save LNURLPayout")
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Failed to save LNURLPayout"
)
return
return lnurlpayout.dict()
@lnurlpayout_ext.delete("/api/v1/lnurlpayouts/{lnurlpayout_id}")
async def api_lnurlpayout_delete(
lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
@ -57,22 +74,30 @@ async def api_lnurlpayout_delete(
)
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)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
@lnurlpayout_ext.get("/api/v1/lnurlpayouts/{lnurlpayout_id}", status_code=HTTPStatus.OK)
async def api_lnurlpayout_check(
lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
):
lnurlpayout = await get_lnurlpayout(lnurlpayout_id)
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])
return
# get payouts func
# lnurlpayouts = await get_lnurlpayouts(wallet_ids)
# 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))
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

@ -15,6 +15,7 @@ satspay_ext: APIRouter = APIRouter(prefix="/satspay", tags=["satspay"])
def satspay_renderer():
return template_renderer(["lnbits/extensions/satspay/templates"])
from .tasks import wait_for_paid_invoices
from .views 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 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)
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##########################
@satspay_ext.post("/api/v1/charge")
async def api_charge_create(
data: CreateCharge,
wallet: WalletTypeInfo = Depends(require_invoice_key)
data: CreateCharge, wallet: WalletTypeInfo = Depends(require_invoice_key)
):
charge = await create_charge(user=wallet.wallet.user, data=data)
return charge.dict()
@satspay_ext.put("/api/v1/charge/{charge_id}")
async def api_charge_update(
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["LNBITS_DENOMINATION"] = settings.LNBITS_DENOMINATION
t.env.globals["SITE_TAGLINE"] = settings.LNBITS_SITE_TAGLINE
t.env.globals["SITE_DESCRIPTION"] = settings.LNBITS_SITE_DESCRIPTION
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", 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
)
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", default="free and open-source lightning wallet"
)

View file

@ -22,7 +22,8 @@ Vue.component('lnbits-wallet-list', {
activeWallet: null,
activeBalance: [],
showForm: false,
walletName: ''
walletName: '',
LNBITS_DENOMINATION: LNBITS_DENOMINATION
}
},
template: `
@ -43,7 +44,8 @@ Vue.component('lnbits-wallet-list', {
</q-item-section>
<q-item-section>
<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 side v-show="activeWallet && activeWallet.id === wallet.id">
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
@ -194,11 +196,11 @@ Vue.component('lnbits-payment-details', {
</div>
<div class="row">
<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 class="row">
<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 class="row">
<div class="col-3"><b>Payment hash</b>:</div>

View file

@ -192,6 +192,8 @@
<script src="/static/js/components.js"></script>
<script type="text/javascript">
const themes = {{ LNBITS_THEME_OPTIONS | tojson }}
const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson}}
console.log(LNBITS_DENOMINATION)
if(themes && themes.length) {
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:
return int(amount * (await get_fiat_rate_satoshis(currency)))
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
return float(amount / (await get_fiat_rate_satoshis(currency)))

View file

@ -9,3 +9,4 @@ from .lnpay import LNPayWallet
from .lnbits import LNbitsWallet
from .lndrest import LndRestWallet
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 = endpoint[:-1] if endpoint.endswith("/") else endpoint
endpoint = (
"https://" +
endpoint if not endpoint.startswith("http") else endpoint
"https://" + endpoint if not endpoint.startswith("http") else endpoint
)
self.endpoint = endpoint
@ -103,10 +102,7 @@ class LndRestWallet(Wallet):
r = await client.post(
url=f"{self.endpoint}/v1/channels/transactions",
headers=self.auth,
json={
"payment_request": bolt11,
"fee_limit": lnrpcFeeLimit,
},
json={"payment_request": bolt11, "fee_limit": lnrpcFeeLimit},
timeout=180,
)
@ -183,8 +179,7 @@ class LndRestWallet(Wallet):
except:
continue
payment_hash = base64.b64decode(
inv["r_hash"]).hex()
payment_hash = base64.b64decode(inv["r_hash"]).hex()
yield payment_hash
except (OSError, httpx.ConnectError, httpx.ReadError):
pass

View file

@ -20,11 +20,10 @@ class VoidWallet(Wallet):
raise Unsupported("")
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.")
return StatusResponse(
None,
0,
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."
)
return StatusResponse(None, 0)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
raise Unsupported("")

2
package-lock.json generated
View file

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

View file

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