diff --git a/.env.example b/.env.example
index 987c6ca69..4edaea971 100644
--- a/.env.example
+++ b/.env.example
@@ -1,6 +1,9 @@
HOST=127.0.0.1
PORT=5000
+# uvicorn variable, allow https behind a proxy
+# FORWARDED_ALLOW_IPS="*"
+
DEBUG=false
LNBITS_ALLOWED_USERS=""
@@ -13,7 +16,7 @@ LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
LNBITS_AD_SPACE=""
# Hides wallet api, extensions can choose to honor
-LNBITS_HIDE_API=false
+LNBITS_HIDE_API=false
# Disable extensions for all users, use "all" to disable all extensions
LNBITS_DISABLED_EXTENSIONS="amilk"
@@ -67,7 +70,7 @@ LNBITS_KEY=LNBITS_ADMIN_KEY
LND_REST_ENDPOINT=https://127.0.0.1:8080/
LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
LND_REST_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING"
-# To use an AES-encrypted macaroon, set
+# To use an AES-encrypted macaroon, set
# LND_REST_MACAROON_ENCRYPTED="eNcRyPtEdMaCaRoOn"
# LNPayWallet
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..bfaddbebc
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,31 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: "[BUG]"
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - LNbits version: [e.g. 0.9.2 or commit hash]
+ - Database [e.g. sqlite, postgres]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 000000000..4f49a4973
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: "[Feature request]"
+labels: feature request
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/ISSUE_TEMPLATE/something-else.md b/.github/ISSUE_TEMPLATE/something-else.md
new file mode 100644
index 000000000..4bd9ec2ac
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/something-else.md
@@ -0,0 +1,10 @@
+---
+name: Something else
+about: Anything else that you need to say
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+
diff --git a/docs/guide/installation.md b/docs/guide/installation.md
index 6b95f93b1..bf40418d9 100644
--- a/docs/guide/installation.md
+++ b/docs/guide/installation.md
@@ -48,7 +48,9 @@ poetry run lnbits
# Note that you have to add the line DEBUG=true in your .env file, too.
```
-## Option 2: Nix
+## Option 2: Nix
+
+> note: currently not supported while we make some architectural changes on the path to leave beta
```sh
git clone https://github.com/lnbits/lnbits-legend.git
@@ -155,6 +157,7 @@ kill_timeout = 30
HOST="127.0.0.1"
PORT=5000
LNBITS_FORCE_HTTPS=true
+ FORWARDED_ALLOW_IPS="*"
LNBITS_DATA_FOLDER="/data"
${PUT_YOUR_LNBITS_ENV_VARS_HERE}
diff --git a/lnbits/extensions/gerty/helpers.py b/lnbits/extensions/gerty/helpers.py
index d7e0e9511..4852fb583 100644
--- a/lnbits/extensions/gerty/helpers.py
+++ b/lnbits/extensions/gerty/helpers.py
@@ -15,17 +15,15 @@ def get_percent_difference(current, previous, precision=4):
# A helper function get a nicely formated dict for the text
def get_text_item_dict(text: str, font_size: int, x_pos: int = None, y_pos: int = None):
# Get line size by font size
- line_width = 60
+ line_width = 20
if font_size <= 12:
- line_width = 75
+ line_width = 60
elif font_size <= 15:
- line_width = 58
+ line_width = 45
elif font_size <= 20:
- line_width = 40
+ line_width = 35
elif font_size <= 40:
- line_width = 30
- else:
- line_width = 20
+ line_width = 25
# wrap the text
wrapper = textwrap.TextWrapper(width=line_width)
@@ -241,3 +239,42 @@ def get_time_remaining(seconds, granularity=2):
name = name.rstrip("s")
result.append("{} {}".format(round(value), name))
return ", ".join(result[:granularity])
+
+
+async def get_mining_stat(stat_slug: str, gerty):
+ text = []
+ if stat_slug == "mining_current_hash_rate":
+ stat = await api_get_mining_stat(stat_slug, gerty)
+ logger.debug(stat)
+ current = "{0}hash".format(si_format(stat['current'], 6, True, " "))
+ text.append(get_text_item_dict("Current Mining Hashrate", 20))
+ text.append(get_text_item_dict(current, 40))
+ # compare vs previous time period
+ difference = get_percent_difference(current=stat['current'], previous=stat['1w'])
+ text.append(get_text_item_dict("{0} in last 7 days".format(difference), 12))
+ elif stat_slug == "mining_current_difficulty":
+ stat = await api_get_mining_stat(stat_slug, gerty)
+ text.append(get_text_item_dict("Current Mining Difficulty", 20))
+ text.append(get_text_item_dict(format_number(stat['current']), 40))
+ difference = get_percent_difference(current=stat['current'], previous=stat['previous'])
+ text.append(get_text_item_dict("{0} since last adjustment".format(difference), 12))
+ # text.append(get_text_item_dict("Required threshold for mining proof-of-work", 12))
+ return text
+
+async def api_get_mining_stat(stat_slug: str, gerty):
+ stat = ""
+ if stat_slug == "mining_current_hash_rate":
+ async with httpx.AsyncClient() as client:
+ r = await client.get(gerty.mempool_endpoint + "/api/v1/mining/hashrate/1m")
+ data = r.json()
+ stat = {}
+ stat['current'] = data['currentHashrate']
+ stat['1w'] = data['hashrates'][len(data['hashrates']) - 7]['avgHashrate']
+ elif stat_slug == "mining_current_difficulty":
+ async with httpx.AsyncClient() as client:
+ r = await client.get(gerty.mempool_endpoint + "/api/v1/mining/hashrate/1m")
+ data = r.json()
+ stat = {}
+ stat['current'] = data['currentDifficulty']
+ stat['previous'] = data['difficulty'][len(data['difficulty']) - 2]['difficulty']
+ return stat
\ No newline at end of file
diff --git a/lnbits/extensions/gerty/migrations.py b/lnbits/extensions/gerty/migrations.py
index 61722835f..e98fc4f2d 100644
--- a/lnbits/extensions/gerty/migrations.py
+++ b/lnbits/extensions/gerty/migrations.py
@@ -17,10 +17,9 @@ async def m001_initial(db):
"""
)
+
async def m002_add_utc_offset_col(db):
"""
support for UTC offset
"""
- await db.execute(
- "ALTER TABLE gerty.gertys ADD COLUMN utc_offset INT;"
- )
+ await db.execute("ALTER TABLE gerty.gertys ADD COLUMN utc_offset INT;")
diff --git a/lnbits/extensions/gerty/templates/gerty/index.html b/lnbits/extensions/gerty/templates/gerty/index.html
index 3c258c1c8..55e67a2d8 100644
--- a/lnbits/extensions/gerty/templates/gerty/index.html
+++ b/lnbits/extensions/gerty/templates/gerty/index.html
@@ -1,641 +1,781 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block page %}
-
-
-
-
- New Gerty
-
-
-
-
-
-
-
-
Gerty
-
-
- Export to CSV
-
+{% extends "base.html" %} {% from "macros.jinja" import window_vars with context %} {% block page %}
+
+
+
+
+ New Gerty
+
+
+
+
+
+
+
+
Gerty
+
+
+ Export to CSV
+
+
+
+ {% raw %}
+
+
+
+
+ {{ col.label }}
+
+
+
+
+
+
+
+
+
+
+ Launch software Gerty
+
+
+ View Gerty API
+
+
+
+ {{ (col.name == 'tip_options' && col.value ?
+ JSON.parse(col.value).join(", ") : col.value) }}
+
+
+
+
+
+
+
+
+
+ {% endraw %}
+
+
+
-
- {% raw %}
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
-
-
- Launch software Gerty
-
-
- View Gerty API
-
-
-
- {{ (col.name == 'tip_options' && col.value ?
- JSON.parse(col.value).join(", ") : col.value) }}
-
-
-
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
-
- {{ SITE_TITLE }} Gerty extension
-
-
-
-
- {% include "gerty/_api_docs.html" %}
-
-
-
-
-
-
-
-
-
-
- Hit enter to add values
-
-
-
- Used for getting onchain/ln stats
-
-
-
- The amount of time in seconds between screen updates
-
-
-
- Enter a UTC time offset value (e.g. -1)
-
-
- Use the toggles below to control what your Gerty will display
-
-
-
-
- Displays random quotes from Satoshi
-
-
-
-
-
-
-
-
-
-
-
-
-
Create Gerty
-
-
Update Gerty
-
-
Cancel
-
+
+
+
+
+ {{ SITE_TITLE }} Gerty extension
+
+
+
+
+ {% include "gerty/_api_docs.html" %}
+
+
-
-
-
-
+
+
+
+
+
+
+
+ Hit enter to add values
+
+
+
+ Used for getting onchain/ln stats
+
+
+
+ The amount of time in seconds between screen updates
+
+
+
+
+ Enter a UTC time offset value (e.g. -1)
+
+
+ Use the toggles below to control what your Gerty will display
+
+
+
+
+
+ Displays random quotes from Satoshi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Toggle all
+
+
+
+
+
+
+
+
+
+
+
+
+ Toggle all
+
+
+
+
+
+
+
+
+
+
+ Toggle all
+
+
+
+
+
+
+
+
+
+
+
+ Create Gerty
+
+ Update Gerty
+
+ Cancel
+
+
+
+
+
+
{% endblock %} {% block scripts %} {{ window_vars(user) }}
-
+ LNbits.utils
+ .confirmDialog('Are you sure you want to delete this Gerty?')
+ .onOk(function () {
+ LNbits.api
+ .request(
+ 'DELETE',
+ '/gerty/api/v1/gerty/' + gertyId,
+ _.findWhere(self.g.user.wallets, {id: gerty.wallet}).adminkey
+ )
+ .then(function (response) {
+ self.gertys = _.reject(self.gertys, function (obj) {
+ return obj.id == gertyId
+ })
+ })
+ .catch(function (error) {
+ LNbits.utils.notifyApiError(error)
+ })
+ })
+ },
+ exportCSV: function () {
+ LNbits.utils.exportCSV(this.gertysTable.columns, this.gertys)
+ }
+ },
+ created: function () {
+ if (this.g.user.wallets.length) {
+ this.getGertys()
+ }
+ },
+ watch: {
+ toggleStates: {
+ handler(toggleStatesValue) {
+ // Switch all the toggles in each section to the relevant state
+ for (const [toggleKey, toggleValue] of Object.entries(toggleStatesValue)) {
+ if (this.oldToggleStates[toggleKey] !== toggleValue) {
+ for (const [dpKey, dpValue] of Object.entries(this.formDialog.data.display_preferences)) {
+ if (dpKey.indexOf(toggleKey) === 0) {
+ this.formDialog.data.display_preferences[dpKey] = toggleValue
+ }
+ }
+ }
+ }
+ // This is a weird hack we have to use to get VueJS to persist the previous toggle state between
+ // watches. VueJS passes the old and new values by reference so when comparing objects they
+ // will have the same values unless we do this
+ this.oldToggleStates = JSON.parse(JSON.stringify(toggleStatesValue))
+ },
+ deep: true
+ }
+ }
+ })
+
{% endblock %} {% block styles %}
-
+
{% endblock %}
diff --git a/lnbits/extensions/gerty/views_api.py b/lnbits/extensions/gerty/views_api.py
index 05a7f5d75..2eea366c2 100644
--- a/lnbits/extensions/gerty/views_api.py
+++ b/lnbits/extensions/gerty/views_api.py
@@ -153,8 +153,10 @@ async def api_gerty_json(gerty_id: str, p: int = None): # page number
# Get a screen slug by its position in the screens_list
def get_screen_slug_by_index(index: int, screens_list):
- if(index < len(screens_list) - 1):
- return list(screens_list)[index]
+ logger.debug("Index: {0}".format(index))
+ logger.debug("len(screens_list) - 1: {0} ".format(len(screens_list) - 1))
+ if index <= len(screens_list) - 1:
+ return list(screens_list)[index - 1]
else:
return None
@@ -171,16 +173,37 @@ async def get_screen_data(screen_num: int, screens_list: dict, gerty):
if screen_slug == "dashboard":
title = gerty.name
areas = await get_dashboard(gerty)
+ if screen_slug == "lnbits_wallets_balance":
+ wallets = await get_lnbits_wallet_balances(gerty)
+ text = []
+ for wallet in wallets:
+ text.append(get_text_item_dict("{0}'s Wallet".format(wallet['name']), 20))
+ text.append(get_text_item_dict("{0} sats".format(format_number(wallet['balance'])), 40))
+ areas.append(text)
elif screen_slug == "fun_satoshi_quotes":
areas.append(await get_satoshi_quotes())
elif screen_slug == "fun_exchange_market_rate":
areas.append(await get_exchange_rate(gerty))
- elif screen_slug == "onchain_dashboard":
+ elif screen_slug == "onchain_difficulty_epoch_progress":
+ areas.append(await get_onchain_stat(screen_slug, gerty))
+ elif screen_slug == "onchain_difficulty_retarget_date":
+ areas.append(await get_onchain_stat(screen_slug, gerty))
+ elif screen_slug == "onchain_difficulty_blocks_remaining":
+ areas.append(await get_onchain_stat(screen_slug, gerty))
+ elif screen_slug == "onchain_difficulty_epoch_time_remaining":
+ areas.append(await get_onchain_stat(screen_slug, gerty))
+ elif screen_slug == "dashboard_onchain":
title = "Onchain Data"
areas = await get_onchain_dashboard(gerty)
elif screen_slug == "mempool_recommended_fees":
areas.append(await get_mempool_stat(screen_slug, gerty))
- elif screen_slug == "mining_dashboard":
+ elif screen_slug == "mempool_tx_count":
+ areas.append(await get_mempool_stat(screen_slug, gerty))
+ elif screen_slug == "mining_current_hash_rate":
+ areas.append(await get_mining_stat(screen_slug, gerty))
+ elif screen_slug == "mining_current_difficulty":
+ areas.append(await get_mining_stat(screen_slug, gerty))
+ elif screen_slug == "dashboard_mining":
title = "Mining Data"
areas = await get_mining_dashboard(gerty)
elif screen_slug == "lightning_dashboard":
@@ -290,6 +313,34 @@ async def get_exchange_rate(gerty):
pass
return text
+async def get_onchain_stat(stat_slug: str, gerty):
+ text = []
+ if (
+ stat_slug == "onchain_difficulty_epoch_progress" or
+ stat_slug == "onchain_difficulty_retarget_date" or
+ stat_slug == "onchain_difficulty_blocks_remaining" or
+ stat_slug == "onchain_difficulty_epoch_time_remaining"
+ ):
+ async with httpx.AsyncClient() as client:
+ r = await client.get(gerty.mempool_endpoint + "/api/v1/difficulty-adjustment")
+ if stat_slug == "onchain_difficulty_epoch_progress":
+ stat = round(r.json()['progressPercent'])
+ text.append(get_text_item_dict("Progress through current difficulty epoch", 15))
+ text.append(get_text_item_dict("{0}%".format(stat), 80))
+ elif stat_slug == "onchain_difficulty_retarget_date":
+ stat = r.json()['estimatedRetargetDate']
+ dt = datetime.fromtimestamp(stat / 1000).strftime("%e %b %Y at %H:%M")
+ text.append(get_text_item_dict("Estimated date of next difficulty adjustment", 15))
+ text.append(get_text_item_dict(dt, 40))
+ elif stat_slug == "onchain_difficulty_blocks_remaining":
+ stat = r.json()['remainingBlocks']
+ text.append(get_text_item_dict("Blocks remaining until next difficulty adjustment", 15))
+ text.append(get_text_item_dict("{0}".format(format_number(stat)), 80))
+ elif stat_slug == "onchain_difficulty_epoch_time_remaining":
+ stat = r.json()['remainingTime']
+ text.append(get_text_item_dict("Blocks remaining until next difficulty adjustment", 15))
+ text.append(get_text_item_dict(get_time_remaining(stat / 1000, 4), 20))
+ return text
async def get_onchain_dashboard(gerty):
areas = []
@@ -300,39 +351,27 @@ async def get_onchain_dashboard(gerty):
)
text = []
stat = round(r.json()["progressPercent"])
- text.append(
- get_text_item_dict("Progress through current epoch", 12)
- )
+ text.append(get_text_item_dict("Progress through epoch", 12))
text.append(get_text_item_dict("{0}%".format(stat), 60))
areas.append(text)
text = []
stat = r.json()["estimatedRetargetDate"]
dt = datetime.fromtimestamp(stat / 1000).strftime("%e %b %Y at %H:%M")
- text.append(
- get_text_item_dict("Date of next difficulty adjustment", 12)
- )
+ text.append(get_text_item_dict("Date of next adjustment", 12))
text.append(get_text_item_dict(dt, 20))
areas.append(text)
text = []
stat = r.json()["remainingBlocks"]
- text.append(
- get_text_item_dict(
- "Blocks until next adjustment", 12
- )
- )
+ text.append(get_text_item_dict("Blocks until adjustment", 12))
text.append(get_text_item_dict("{0}".format(format_number(stat)), 60))
areas.append(text)
text = []
stat = r.json()["remainingTime"]
- text.append(
- get_text_item_dict(
- "Blocks until next adjustment", 12
- )
- )
- text.append(get_text_item_dict(get_time_remaining(stat / 1000, 4), 60))
+ text.append(get_text_item_dict("Time until adjustment", 12))
+ text.append(get_text_item_dict(get_time_remaining(stat / 1000, 4), 20))
areas.append(text)
return areas
@@ -379,16 +418,16 @@ async def get_mempool_stat(stat_slug: str, gerty):
pos_y = 280 + y_offset
text.append(
- get_text_item_dict("{0}".format("No Priority"), 15, 30, pos_y)
+ get_text_item_dict("{0}".format("None"), 15, 30, pos_y)
)
text.append(
- get_text_item_dict("{0}".format("Low Priority"), 15, 235, pos_y)
+ get_text_item_dict("{0}".format("Low"), 15, 235, pos_y)
)
text.append(
- get_text_item_dict("{0}".format("Medium Priority"), 15, 460, pos_y)
+ get_text_item_dict("{0}".format("Medium"), 15, 460, pos_y)
)
text.append(
- get_text_item_dict("{0}".format("High Priority"), 15, 750, pos_y)
+ get_text_item_dict("{0}".format("High"), 15, 750, pos_y)
)
pos_y = 340 + y_offset
@@ -450,3 +489,30 @@ async def get_mempool_stat(stat_slug: str, gerty):
)
)
return text
+
+
+def get_date_suffix(dayNumber):
+ if 4 <= dayNumber <= 20 or 24 <= dayNumber <= 30:
+ return "th"
+ else:
+ return ["st", "nd", "rd"][dayNumber % 10 - 1]
+
+def get_time_remaining(seconds, granularity=2):
+ intervals = (
+ # ('weeks', 604800), # 60 * 60 * 24 * 7
+ ('days', 86400), # 60 * 60 * 24
+ ('hours', 3600), # 60 * 60
+ ('minutes', 60),
+ ('seconds', 1),
+ )
+
+ result = []
+
+ for name, count in intervals:
+ value = seconds // count
+ if value:
+ seconds -= value * count
+ if value == 1:
+ name = name.rstrip('s')
+ result.append("{} {}".format(round(value), name))
+ return ', '.join(result[:granularity])
diff --git a/lnbits/extensions/lnurldevice/crud.py b/lnbits/extensions/lnurldevice/crud.py
index 4c25e4cb4..e02d23b8b 100644
--- a/lnbits/extensions/lnurldevice/crud.py
+++ b/lnbits/extensions/lnurldevice/crud.py
@@ -23,9 +23,22 @@ async def create_lnurldevice(
currency,
device,
profit,
- amount
+ amount,
+ pin,
+ profit1,
+ amount1,
+ pin1,
+ profit2,
+ amount2,
+ pin2,
+ profit3,
+ amount3,
+ pin3,
+ profit4,
+ amount4,
+ pin4
)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
lnurldevice_id,
@@ -36,6 +49,19 @@ async def create_lnurldevice(
data.device,
data.profit,
data.amount,
+ data.pin,
+ data.profit1,
+ data.amount1,
+ data.pin1,
+ data.profit2,
+ data.amount2,
+ data.pin2,
+ data.profit3,
+ data.amount3,
+ data.pin3,
+ data.profit4,
+ data.amount4,
+ data.pin4,
),
)
return await get_lnurldevice(lnurldevice_id)
diff --git a/lnbits/extensions/lnurldevice/lnurl.py b/lnbits/extensions/lnurldevice/lnurl.py
index 79892b78a..c8f9675e4 100644
--- a/lnbits/extensions/lnurldevice/lnurl.py
+++ b/lnbits/extensions/lnurldevice/lnurl.py
@@ -8,6 +8,7 @@ from typing import Optional
from embit import bech32, compact
from fastapi import Request
from fastapi.param_functions import Query
+from loguru import logger
from starlette.exceptions import HTTPException
from lnbits.core.services import create_invoice
@@ -91,6 +92,9 @@ async def lnurl_v1_params(
device_id: str = Query(None),
p: str = Query(None),
atm: str = Query(None),
+ gpio: str = Query(None),
+ profit: str = Query(None),
+ amount: str = Query(None),
):
device = await get_lnurldevice(device_id)
if not device:
@@ -105,16 +109,24 @@ async def lnurl_v1_params(
if device.device == "switch":
price_msat = (
- await fiat_amount_as_satoshis(float(device.profit), device.currency)
+ await fiat_amount_as_satoshis(float(profit), device.currency)
if device.currency != "sat"
else amount_in_cent
) * 1000
+ # Check they're not trying to trick the switch!
+ check = False
+ for switch in device.switches(request):
+ if switch[0] == gpio and switch[1] == profit and switch[2] == amount:
+ check = True
+ if not check:
+ return {"status": "ERROR", "reason": f"Switch params wrong"}
+
lnurldevicepayment = await create_lnurldevicepayment(
deviceid=device.id,
- payload="bla",
+ payload=amount,
sats=price_msat,
- pin=1,
+ pin=gpio,
payhash="bla",
)
if not lnurldevicepayment:
@@ -126,7 +138,7 @@ async def lnurl_v1_params(
),
"minSendable": price_msat,
"maxSendable": price_msat,
- "metadata": await device.lnurlpay_metadata(),
+ "metadata": device.lnurlpay_metadata,
}
if len(p) % 4 > 0:
p += "=" * (4 - (len(p) % 4))
@@ -188,7 +200,7 @@ async def lnurl_v1_params(
),
"minSendable": price_msat * 1000,
"maxSendable": price_msat * 1000,
- "metadata": await device.lnurlpay_metadata(),
+ "metadata": device.lnurlpay_metadata,
}
@@ -233,11 +245,17 @@ async def lnurl_callback(
if device.device == "switch":
payment_hash, payment_request = await create_invoice(
wallet_id=device.wallet,
- amount=lnurldevicepayment.sats / 1000,
- memo=device.title + "-" + lnurldevicepayment.id,
- unhashed_description=(await device.lnurlpay_metadata()).encode("utf-8"),
- extra={"tag": "Switch", "id": paymentid, "time": device.amount},
+ amount=int(lnurldevicepayment.sats / 1000),
+ memo=device.id + " PIN " + str(lnurldevicepayment.pin),
+ unhashed_description=device.lnurlpay_metadata.encode("utf-8"),
+ extra={
+ "tag": "Switch",
+ "pin": str(lnurldevicepayment.pin),
+ "amount": str(lnurldevicepayment.payload),
+ "id": paymentid,
+ },
)
+
lnurldevicepayment = await update_lnurldevicepayment(
lnurldevicepayment_id=paymentid, payhash=payment_hash
)
@@ -248,9 +266,9 @@ async def lnurl_callback(
payment_hash, payment_request = await create_invoice(
wallet_id=device.wallet,
- amount=lnurldevicepayment.sats / 1000,
+ amount=int(lnurldevicepayment.sats / 1000),
memo=device.title,
- unhashed_description=(await device.lnurlpay_metadata()).encode("utf-8"),
+ unhashed_description=device.lnurlpay_metadata.encode("utf-8"),
extra={"tag": "PoS"},
)
lnurldevicepayment = await update_lnurldevicepayment(
diff --git a/lnbits/extensions/lnurldevice/migrations.py b/lnbits/extensions/lnurldevice/migrations.py
index 7305ccebe..1df04075d 100644
--- a/lnbits/extensions/lnurldevice/migrations.py
+++ b/lnbits/extensions/lnurldevice/migrations.py
@@ -88,3 +88,52 @@ async def m003_redux(db):
await db.execute(
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount INT DEFAULT 0;"
)
+
+
+async def m004_redux(db):
+ """
+ Add 'meta' for storing various metadata about the wallet
+ """
+ await db.execute(
+ "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN pin INT DEFAULT 0"
+ )
+
+ await db.execute(
+ "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN profit1 FLOAT DEFAULT 0"
+ )
+ await db.execute(
+ "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount1 INT DEFAULT 0"
+ )
+ await db.execute(
+ "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN pin1 INT DEFAULT 0"
+ )
+
+ await db.execute(
+ "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN profit2 FLOAT DEFAULT 0"
+ )
+ await db.execute(
+ "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount2 INT DEFAULT 0"
+ )
+ await db.execute(
+ "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN pin2 INT DEFAULT 0"
+ )
+
+ await db.execute(
+ "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN profit3 FLOAT DEFAULT 0"
+ )
+ await db.execute(
+ "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount3 INT DEFAULT 0"
+ )
+ await db.execute(
+ "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN pin3 INT DEFAULT 0"
+ )
+
+ await db.execute(
+ "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN profit4 FLOAT DEFAULT 0"
+ )
+ await db.execute(
+ "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount4 INT DEFAULT 0"
+ )
+ await db.execute(
+ "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN pin4 INT DEFAULT 0"
+ )
diff --git a/lnbits/extensions/lnurldevice/models.py b/lnbits/extensions/lnurldevice/models.py
index 01bcc2ba6..c27470b73 100644
--- a/lnbits/extensions/lnurldevice/models.py
+++ b/lnbits/extensions/lnurldevice/models.py
@@ -1,12 +1,13 @@
import json
from sqlite3 import Row
-from typing import Optional
+from typing import List, Optional
from fastapi import Request
from lnurl import Lnurl
from lnurl import encode as lnurl_encode # type: ignore
from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
from lnurl.types import LnurlPayMetadata # type: ignore
+from loguru import logger
from pydantic import BaseModel
from pydantic.main import BaseModel
@@ -18,6 +19,19 @@ class createLnurldevice(BaseModel):
device: str
profit: float
amount: int
+ pin: int = 0
+ profit1: float = 0
+ amount1: int = 0
+ pin1: int = 0
+ profit2: float = 0
+ amount2: int = 0
+ pin2: int = 0
+ profit3: float = 0
+ amount3: int = 0
+ pin3: int = 0
+ profit4: float = 0
+ amount4: int = 0
+ pin4: int = 0
class lnurldevices(BaseModel):
@@ -29,18 +43,122 @@ class lnurldevices(BaseModel):
device: str
profit: float
amount: int
+ pin: int
+ profit1: float
+ amount1: int
+ pin1: int
+ profit2: float
+ amount2: int
+ pin2: int
+ profit3: float
+ amount3: int
+ pin3: int
+ profit4: float
+ amount4: int
+ pin4: int
timestamp: str
def from_row(cls, row: Row) -> "lnurldevices":
return cls(**dict(row))
- def lnurl(self, req: Request) -> Lnurl:
- url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id)
- return lnurl_encode(url)
-
- async def lnurlpay_metadata(self) -> LnurlPayMetadata:
+ @property
+ def lnurlpay_metadata(self) -> LnurlPayMetadata:
return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
+ def switches(self, req: Request) -> List:
+ switches = []
+ if self.profit > 0:
+ url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id)
+ switches.append(
+ [
+ str(self.pin),
+ str(self.profit),
+ str(self.amount),
+ lnurl_encode(
+ url
+ + "?gpio="
+ + str(self.pin)
+ + "&profit="
+ + str(self.profit)
+ + "&amount="
+ + str(self.amount)
+ ),
+ ]
+ )
+ if self.profit1 > 0:
+ url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id)
+ switches.append(
+ [
+ str(self.pin1),
+ str(self.profit1),
+ str(self.amount1),
+ lnurl_encode(
+ url
+ + "?gpio="
+ + str(self.pin1)
+ + "&profit="
+ + str(self.profit1)
+ + "&amount="
+ + str(self.amount1)
+ ),
+ ]
+ )
+ if self.profit2 > 0:
+ url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id)
+ switches.append(
+ [
+ str(self.pin2),
+ str(self.profit2),
+ str(self.amount2),
+ lnurl_encode(
+ url
+ + "?gpio="
+ + str(self.pin2)
+ + "&profit="
+ + str(self.profit2)
+ + "&amount="
+ + str(self.amount2)
+ ),
+ ]
+ )
+ if self.profit3 > 0:
+ url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id)
+ switches.append(
+ [
+ str(self.pin3),
+ str(self.profit3),
+ str(self.amount3),
+ lnurl_encode(
+ url
+ + "?gpio="
+ + str(self.pin3)
+ + "&profit="
+ + str(self.profit3)
+ + "&amount="
+ + str(self.amount3)
+ ),
+ ]
+ )
+ if self.profit4 > 0:
+ url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id)
+ switches.append(
+ [
+ str(self.pin4),
+ str(self.profit4),
+ str(self.amount4),
+ lnurl_encode(
+ url
+ + "?gpio="
+ + str(self.pin4)
+ + "&profit="
+ + str(self.profit4)
+ + "&amount="
+ + str(self.amount4)
+ ),
+ ]
+ )
+ return switches
+
class lnurldevicepayment(BaseModel):
id: str
diff --git a/lnbits/extensions/lnurldevice/tasks.py b/lnbits/extensions/lnurldevice/tasks.py
index c8f3db04f..d3248ad57 100644
--- a/lnbits/extensions/lnurldevice/tasks.py
+++ b/lnbits/extensions/lnurldevice/tasks.py
@@ -36,5 +36,9 @@ async def on_invoice_paid(payment: Payment) -> None:
lnurldevicepayment = await update_lnurldevicepayment(
lnurldevicepayment_id=payment.extra.get("id"), payhash="used"
)
- return await updater(lnurldevicepayment.deviceid)
+ return await updater(
+ lnurldevicepayment.deviceid,
+ lnurldevicepayment.pin,
+ lnurldevicepayment.payload,
+ )
return
diff --git a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html
index 028dd94b4..b0b223fff 100644
--- a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html
+++ b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html
@@ -105,7 +105,7 @@
@click="openQrCodeDialog(props.row.id)"
>
LNURLs only work over HTTPS view LNURL view LNURLS
-
-
-
+
+
+
+
+
+
+
+
+
+
+
- {% raw %}
-
- ID: {{ qrCodeDialog.data.id }}
-
- {% endraw %}
+ Copy LNURL
+
Copy LNURL
+ color="primary"
+ :label="'Switch PIN:' + switch_[0]"
+ @click="lnurlValueFetch(switch_[3])"
+ >
Close
@@ -333,11 +527,14 @@
mixins: [windowMixin],
data: function () {
return {
+ tab: 'mails',
protocol: window.location.protocol,
location: window.location.hostname,
wslocation: window.location.hostname,
filter: '',
currency: 'USD',
+ lnurlValue: '',
+ switches: 0,
lnurldeviceLinks: [],
lnurldeviceLinksObj: [],
devices: [
@@ -386,12 +583,6 @@
label: 'device',
field: 'device'
},
- {
- name: 'profit',
- align: 'left',
- label: 'profit',
- field: 'profit'
- },
{
name: 'currency',
align: 'left',
@@ -440,8 +631,20 @@
this.qrCodeDialog.data = _.clone(lnurldevice)
this.qrCodeDialog.data.url =
window.location.protocol + '//' + window.location.host
+ this.lnurlValueFetch(this.qrCodeDialog.data.switches[0][3])
this.qrCodeDialog.show = true
},
+ lnurlValueFetch: function (lnurl) {
+ this.lnurlValue = lnurl
+ },
+ addSwitch: function () {
+ var self = this
+ self.switches = self.switches + 1
+ },
+ removeSwitch: function () {
+ var self = this
+ self.switches = self.switches - 1
+ },
cancellnurldevice: function (data) {
var self = this
self.formDialoglnurldevice.show = false
@@ -498,7 +701,9 @@
.then(function (response) {
if (response.data) {
self.lnurldeviceLinks = response.data.map(maplnurldevice)
+ console.log('response.data')
console.log(response.data)
+ console.log('response.data')
}
})
.catch(function (error) {
diff --git a/lnbits/extensions/lnurldevice/views.py b/lnbits/extensions/lnurldevice/views.py
index 5c6eba24b..f435931bd 100644
--- a/lnbits/extensions/lnurldevice/views.py
+++ b/lnbits/extensions/lnurldevice/views.py
@@ -103,8 +103,10 @@ async def websocket_endpoint(websocket: WebSocket, lnurldevice_id: str):
manager.disconnect(websocket)
-async def updater(lnurldevice_id):
+async def updater(lnurldevice_id, lnurldevice_pin, lnurldevice_amount):
lnurldevice = await get_lnurldevice(lnurldevice_id)
if not lnurldevice:
return
- await manager.send_personal_message(f"{lnurldevice.amount}", lnurldevice_id)
+ return await manager.send_personal_message(
+ f"{lnurldevice_pin}-{lnurldevice_amount}", lnurldevice_id
+ )
diff --git a/lnbits/extensions/lnurldevice/views_api.py b/lnbits/extensions/lnurldevice/views_api.py
index c034f66ed..c6766423c 100644
--- a/lnbits/extensions/lnurldevice/views_api.py
+++ b/lnbits/extensions/lnurldevice/views_api.py
@@ -39,10 +39,10 @@ async def api_lnurldevice_create_or_update(
):
if not lnurldevice_id:
lnurldevice = await create_lnurldevice(data)
- return {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(req)}}
+ return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
else:
lnurldevice = await update_lnurldevice(data, lnurldevice_id=lnurldevice_id)
- return {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(req)}}
+ return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
@lnurldevice_ext.get("/api/v1/lnurlpos")
@@ -52,7 +52,7 @@ async def api_lnurldevices_retrieve(
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
try:
return [
- {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(req)}}
+ {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
for lnurldevice in await get_lnurldevices(wallet_ids)
]
except:
@@ -78,7 +78,7 @@ async def api_lnurldevice_retrieve(
)
if not lnurldevice.lnurl_toggle:
return {**lnurldevice.dict()}
- return {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(req)}}
+ return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
@lnurldevice_ext.delete("/api/v1/lnurlpos/{lnurldevice_id}")
diff --git a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html
index 886589e66..36593d74b 100644
--- a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html
+++ b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html
@@ -38,7 +38,7 @@
>
Body (application/json)
- Returns 201 CREATED (application/json)
+ Returns 200 OK (application/json)
JSON list of users
Curl example
@@ -57,10 +57,16 @@
/usermanager/api/v1/users/<user_id>
Body (application/json)
+
- Returns 201 CREATED (application/json)
+ Returns 200 OK (application/json)
- JSON list of users
+ {"id": <string>, "name": <string>, "admin":
+ <string>, "email": <string>, "password":
+ <string>}
+
Curl example
curl -X GET {{ request.base_url
@@ -81,7 +87,7 @@
{"X-Api-Key": <string>}
Body (application/json)
- Returns 201 CREATED (application/json)
+ Returns 200 OK (application/json)
JSON wallet data
Curl example
@@ -104,7 +110,7 @@
{"X-Api-Key": <string>}
Body (application/json)
- Returns 201 CREATED (application/json)
+ Returns 200 OK (application/json)
JSON a wallets transactions
Curl example
@@ -254,11 +260,15 @@
{"X-Api-Key": <string>}
Curl example
curl -X POST {{ request.base_url }}usermanager/api/v1/extensions -d
- '{"userid": <string>, "extension": <string>, "active":
- <integer>}' -H "X-Api-Key: {{ user.wallets[0].inkey }}" -H
- "Content-type: application/json"
+ >curl -X POST {{ request.base_url
+ }}usermanager/api/v1/extensions?extension=withdraw&userid=user_id&active=true
+ -H "X-Api-Key: {{ user.wallets[0].inkey }}" -H "Content-type:
+ application/json"
+
+ Returns 200 OK (application/json)
+
+ {"extension": "updated"}
diff --git a/lnbits/extensions/withdraw/lnurl.py b/lnbits/extensions/withdraw/lnurl.py
index 18a99599c..660e5b7dd 100644
--- a/lnbits/extensions/withdraw/lnurl.py
+++ b/lnbits/extensions/withdraw/lnurl.py
@@ -78,34 +78,35 @@ async def api_lnurl_callback(
return {"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."}
usescsv = ""
+
+ for x in range(1, link.uses - link.used):
+ usecv = link.usescsv.split(",")
+ usescsv += "," + str(usecv[x])
+ usecsvback = usescsv
+
+ found = False
+ if id_unique_hash is not None:
+ useslist = link.usescsv.split(",")
+ for ind, x in enumerate(useslist):
+ tohash = link.id + link.unique_hash + str(x)
+ if id_unique_hash == shortuuid.uuid(name=tohash):
+ found = True
+ useslist.pop(ind)
+ usescsv = ",".join(useslist)
+ if not found:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND, detail="LNURL-withdraw not found."
+ )
+ else:
+ usescsv = usescsv[1:]
+
+ changesback = {
+ "open_time": link.wait_time,
+ "used": link.used,
+ "usescsv": usecsvback,
+ }
+
try:
- for x in range(1, link.uses - link.used):
- usecv = link.usescsv.split(",")
- usescsv += "," + str(usecv[x])
- usecsvback = usescsv
-
- found = False
- if id_unique_hash is not None:
- useslist = link.usescsv.split(",")
- for ind, x in enumerate(useslist):
- tohash = link.id + link.unique_hash + str(x)
- if id_unique_hash == shortuuid.uuid(name=tohash):
- found = True
- useslist.pop(ind)
- usescsv = ",".join(useslist)
- if not found:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="LNURL-withdraw not found."
- )
- else:
- usescsv = usescsv[1:]
-
- changesback = {
- "open_time": link.wait_time,
- "used": link.used,
- "usescsv": usecsvback,
- }
-
changes = {
"open_time": link.wait_time + now,
"used": link.used + 1,