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 %} + + + + {% endraw %} + +
+
- - {% raw %} - - - {% 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,