Merge branch 'main' into diagon-alley
This commit is contained in:
commit
3bf1d6c6ae
48 changed files with 1279 additions and 509 deletions
|
|
@ -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
|
||||
2
LICENSE
2
LICENSE
|
|
@ -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
|
||||
|
|
|
|||
1
Pipfile
1
Pipfile
|
|
@ -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
840
Pipfile.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
209
lnbits/bolt11.py
209
lnbits/bolt11.py
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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}"},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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,)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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
83
lnbits/wallets/fake.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
2
package-lock.json
generated
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "lnbits",
|
||||
"name": "lnbits-legend",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue