Merge pull request #1130 from blackcoffeexbt/gerty

Gerty
This commit is contained in:
Arc 2022-11-14 20:28:44 -08:00 committed by GitHub
commit d4645496f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1512 additions and 770 deletions

View file

@ -1,6 +1,9 @@
HOST=127.0.0.1 HOST=127.0.0.1
PORT=5000 PORT=5000
# uvicorn variable, allow https behind a proxy
# FORWARDED_ALLOW_IPS="*"
DEBUG=false DEBUG=false
LNBITS_ALLOWED_USERS="" LNBITS_ALLOWED_USERS=""
@ -13,7 +16,7 @@ LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
LNBITS_AD_SPACE="" LNBITS_AD_SPACE=""
# Hides wallet api, extensions can choose to honor # 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 # Disable extensions for all users, use "all" to disable all extensions
LNBITS_DISABLED_EXTENSIONS="amilk" LNBITS_DISABLED_EXTENSIONS="amilk"
@ -67,7 +70,7 @@ LNBITS_KEY=LNBITS_ADMIN_KEY
LND_REST_ENDPOINT=https://127.0.0.1:8080/ 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_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" 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" # LND_REST_MACAROON_ENCRYPTED="eNcRyPtEdMaCaRoOn"
# LNPayWallet # LNPayWallet

31
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -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.

View file

@ -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.

View file

@ -0,0 +1,10 @@
---
name: Something else
about: Anything else that you need to say
title: ''
labels: ''
assignees: ''
---

View file

@ -48,7 +48,9 @@ poetry run lnbits
# Note that you have to add the line DEBUG=true in your .env file, too. # 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 ```sh
git clone https://github.com/lnbits/lnbits-legend.git git clone https://github.com/lnbits/lnbits-legend.git
@ -155,6 +157,7 @@ kill_timeout = 30
HOST="127.0.0.1" HOST="127.0.0.1"
PORT=5000 PORT=5000
LNBITS_FORCE_HTTPS=true LNBITS_FORCE_HTTPS=true
FORWARDED_ALLOW_IPS="*"
LNBITS_DATA_FOLDER="/data" LNBITS_DATA_FOLDER="/data"
${PUT_YOUR_LNBITS_ENV_VARS_HERE} ${PUT_YOUR_LNBITS_ENV_VARS_HERE}

View file

@ -15,17 +15,15 @@ def get_percent_difference(current, previous, precision=4):
# A helper function get a nicely formated dict for the text # 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): def get_text_item_dict(text: str, font_size: int, x_pos: int = None, y_pos: int = None):
# Get line size by font size # Get line size by font size
line_width = 60 line_width = 20
if font_size <= 12: if font_size <= 12:
line_width = 75 line_width = 60
elif font_size <= 15: elif font_size <= 15:
line_width = 58 line_width = 45
elif font_size <= 20: elif font_size <= 20:
line_width = 40 line_width = 35
elif font_size <= 40: elif font_size <= 40:
line_width = 30 line_width = 25
else:
line_width = 20
# wrap the text # wrap the text
wrapper = textwrap.TextWrapper(width=line_width) wrapper = textwrap.TextWrapper(width=line_width)
@ -241,3 +239,42 @@ def get_time_remaining(seconds, granularity=2):
name = name.rstrip("s") name = name.rstrip("s")
result.append("{} {}".format(round(value), name)) result.append("{} {}".format(round(value), name))
return ", ".join(result[:granularity]) 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

View file

@ -17,10 +17,9 @@ async def m001_initial(db):
""" """
) )
async def m002_add_utc_offset_col(db): async def m002_add_utc_offset_col(db):
""" """
support for UTC offset support for UTC offset
""" """
await db.execute( await db.execute("ALTER TABLE gerty.gertys ADD COLUMN utc_offset INT;")
"ALTER TABLE gerty.gertys ADD COLUMN utc_offset INT;"
)

File diff suppressed because it is too large Load diff

View file

@ -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 # Get a screen slug by its position in the screens_list
def get_screen_slug_by_index(index: int, screens_list): def get_screen_slug_by_index(index: int, screens_list):
if(index < len(screens_list) - 1): logger.debug("Index: {0}".format(index))
return list(screens_list)[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: else:
return None return None
@ -171,16 +173,37 @@ async def get_screen_data(screen_num: int, screens_list: dict, gerty):
if screen_slug == "dashboard": if screen_slug == "dashboard":
title = gerty.name title = gerty.name
areas = await get_dashboard(gerty) 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": elif screen_slug == "fun_satoshi_quotes":
areas.append(await get_satoshi_quotes()) areas.append(await get_satoshi_quotes())
elif screen_slug == "fun_exchange_market_rate": elif screen_slug == "fun_exchange_market_rate":
areas.append(await get_exchange_rate(gerty)) 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" title = "Onchain Data"
areas = await get_onchain_dashboard(gerty) areas = await get_onchain_dashboard(gerty)
elif screen_slug == "mempool_recommended_fees": elif screen_slug == "mempool_recommended_fees":
areas.append(await get_mempool_stat(screen_slug, gerty)) 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" title = "Mining Data"
areas = await get_mining_dashboard(gerty) areas = await get_mining_dashboard(gerty)
elif screen_slug == "lightning_dashboard": elif screen_slug == "lightning_dashboard":
@ -290,6 +313,34 @@ async def get_exchange_rate(gerty):
pass pass
return text 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): async def get_onchain_dashboard(gerty):
areas = [] areas = []
@ -300,39 +351,27 @@ async def get_onchain_dashboard(gerty):
) )
text = [] text = []
stat = round(r.json()["progressPercent"]) stat = round(r.json()["progressPercent"])
text.append( text.append(get_text_item_dict("Progress through epoch", 12))
get_text_item_dict("Progress through current epoch", 12)
)
text.append(get_text_item_dict("{0}%".format(stat), 60)) text.append(get_text_item_dict("{0}%".format(stat), 60))
areas.append(text) areas.append(text)
text = [] text = []
stat = r.json()["estimatedRetargetDate"] stat = r.json()["estimatedRetargetDate"]
dt = datetime.fromtimestamp(stat / 1000).strftime("%e %b %Y at %H:%M") dt = datetime.fromtimestamp(stat / 1000).strftime("%e %b %Y at %H:%M")
text.append( text.append(get_text_item_dict("Date of next adjustment", 12))
get_text_item_dict("Date of next difficulty adjustment", 12)
)
text.append(get_text_item_dict(dt, 20)) text.append(get_text_item_dict(dt, 20))
areas.append(text) areas.append(text)
text = [] text = []
stat = r.json()["remainingBlocks"] stat = r.json()["remainingBlocks"]
text.append( text.append(get_text_item_dict("Blocks until adjustment", 12))
get_text_item_dict(
"Blocks until next adjustment", 12
)
)
text.append(get_text_item_dict("{0}".format(format_number(stat)), 60)) text.append(get_text_item_dict("{0}".format(format_number(stat)), 60))
areas.append(text) areas.append(text)
text = [] text = []
stat = r.json()["remainingTime"] stat = r.json()["remainingTime"]
text.append( text.append(get_text_item_dict("Time until adjustment", 12))
get_text_item_dict( text.append(get_text_item_dict(get_time_remaining(stat / 1000, 4), 20))
"Blocks until next adjustment", 12
)
)
text.append(get_text_item_dict(get_time_remaining(stat / 1000, 4), 60))
areas.append(text) areas.append(text)
return areas return areas
@ -379,16 +418,16 @@ async def get_mempool_stat(stat_slug: str, gerty):
pos_y = 280 + y_offset pos_y = 280 + y_offset
text.append( 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( 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( 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( 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 pos_y = 340 + y_offset
@ -450,3 +489,30 @@ async def get_mempool_stat(stat_slug: str, gerty):
) )
) )
return text 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])

View file

@ -23,9 +23,22 @@ async def create_lnurldevice(
currency, currency,
device, device,
profit, profit,
amount amount,
pin,
profit1,
amount1,
pin1,
profit2,
amount2,
pin2,
profit3,
amount3,
pin3,
profit4,
amount4,
pin4
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
lnurldevice_id, lnurldevice_id,
@ -36,6 +49,19 @@ async def create_lnurldevice(
data.device, data.device,
data.profit, data.profit,
data.amount, 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) return await get_lnurldevice(lnurldevice_id)

View file

@ -8,6 +8,7 @@ from typing import Optional
from embit import bech32, compact from embit import bech32, compact
from fastapi import Request from fastapi import Request
from fastapi.param_functions import Query from fastapi.param_functions import Query
from loguru import logger
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from lnbits.core.services import create_invoice from lnbits.core.services import create_invoice
@ -91,6 +92,9 @@ async def lnurl_v1_params(
device_id: str = Query(None), device_id: str = Query(None),
p: str = Query(None), p: str = Query(None),
atm: 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) device = await get_lnurldevice(device_id)
if not device: if not device:
@ -105,16 +109,24 @@ async def lnurl_v1_params(
if device.device == "switch": if device.device == "switch":
price_msat = ( 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" if device.currency != "sat"
else amount_in_cent else amount_in_cent
) * 1000 ) * 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( lnurldevicepayment = await create_lnurldevicepayment(
deviceid=device.id, deviceid=device.id,
payload="bla", payload=amount,
sats=price_msat, sats=price_msat,
pin=1, pin=gpio,
payhash="bla", payhash="bla",
) )
if not lnurldevicepayment: if not lnurldevicepayment:
@ -126,7 +138,7 @@ async def lnurl_v1_params(
), ),
"minSendable": price_msat, "minSendable": price_msat,
"maxSendable": price_msat, "maxSendable": price_msat,
"metadata": await device.lnurlpay_metadata(), "metadata": device.lnurlpay_metadata,
} }
if len(p) % 4 > 0: if len(p) % 4 > 0:
p += "=" * (4 - (len(p) % 4)) p += "=" * (4 - (len(p) % 4))
@ -188,7 +200,7 @@ async def lnurl_v1_params(
), ),
"minSendable": price_msat * 1000, "minSendable": price_msat * 1000,
"maxSendable": 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": if device.device == "switch":
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=device.wallet, wallet_id=device.wallet,
amount=lnurldevicepayment.sats / 1000, amount=int(lnurldevicepayment.sats / 1000),
memo=device.title + "-" + lnurldevicepayment.id, memo=device.id + " PIN " + str(lnurldevicepayment.pin),
unhashed_description=(await device.lnurlpay_metadata()).encode("utf-8"), unhashed_description=device.lnurlpay_metadata.encode("utf-8"),
extra={"tag": "Switch", "id": paymentid, "time": device.amount}, extra={
"tag": "Switch",
"pin": str(lnurldevicepayment.pin),
"amount": str(lnurldevicepayment.payload),
"id": paymentid,
},
) )
lnurldevicepayment = await update_lnurldevicepayment( lnurldevicepayment = await update_lnurldevicepayment(
lnurldevicepayment_id=paymentid, payhash=payment_hash lnurldevicepayment_id=paymentid, payhash=payment_hash
) )
@ -248,9 +266,9 @@ async def lnurl_callback(
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=device.wallet, wallet_id=device.wallet,
amount=lnurldevicepayment.sats / 1000, amount=int(lnurldevicepayment.sats / 1000),
memo=device.title, memo=device.title,
unhashed_description=(await device.lnurlpay_metadata()).encode("utf-8"), unhashed_description=device.lnurlpay_metadata.encode("utf-8"),
extra={"tag": "PoS"}, extra={"tag": "PoS"},
) )
lnurldevicepayment = await update_lnurldevicepayment( lnurldevicepayment = await update_lnurldevicepayment(

View file

@ -88,3 +88,52 @@ async def m003_redux(db):
await db.execute( await db.execute(
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount INT DEFAULT 0;" "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"
)

View file

@ -1,12 +1,13 @@
import json import json
from sqlite3 import Row from sqlite3 import Row
from typing import Optional from typing import List, Optional
from fastapi import Request from fastapi import Request
from lnurl import Lnurl from lnurl import Lnurl
from lnurl import encode as lnurl_encode # type: ignore from lnurl import encode as lnurl_encode # type: ignore
from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
from lnurl.types import LnurlPayMetadata # type: ignore from lnurl.types import LnurlPayMetadata # type: ignore
from loguru import logger
from pydantic import BaseModel from pydantic import BaseModel
from pydantic.main import BaseModel from pydantic.main import BaseModel
@ -18,6 +19,19 @@ class createLnurldevice(BaseModel):
device: str device: str
profit: float profit: float
amount: int 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): class lnurldevices(BaseModel):
@ -29,18 +43,122 @@ class lnurldevices(BaseModel):
device: str device: str
profit: float profit: float
amount: int 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 timestamp: str
def from_row(cls, row: Row) -> "lnurldevices": def from_row(cls, row: Row) -> "lnurldevices":
return cls(**dict(row)) return cls(**dict(row))
def lnurl(self, req: Request) -> Lnurl: @property
url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id) def lnurlpay_metadata(self) -> LnurlPayMetadata:
return lnurl_encode(url)
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
return LnurlPayMetadata(json.dumps([["text/plain", self.title]])) 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): class lnurldevicepayment(BaseModel):
id: str id: str

View file

@ -36,5 +36,9 @@ async def on_invoice_paid(payment: Payment) -> None:
lnurldevicepayment = await update_lnurldevicepayment( lnurldevicepayment = await update_lnurldevicepayment(
lnurldevicepayment_id=payment.extra.get("id"), payhash="used" lnurldevicepayment_id=payment.extra.get("id"), payhash="used"
) )
return await updater(lnurldevicepayment.deviceid) return await updater(
lnurldevicepayment.deviceid,
lnurldevicepayment.pin,
lnurldevicepayment.payload,
)
return return

View file

@ -105,7 +105,7 @@
@click="openQrCodeDialog(props.row.id)" @click="openQrCodeDialog(props.row.id)"
><q-tooltip v-if="protocol == 'http:'"> ><q-tooltip v-if="protocol == 'http:'">
LNURLs only work over HTTPS </q-tooltip LNURLs only work over HTTPS </q-tooltip
><q-tooltip v-else> view LNURL </q-tooltip></q-btn ><q-tooltip v-else> view LNURLS </q-tooltip></q-btn
> >
</q-td> </q-td>
<q-td <q-td
@ -230,29 +230,221 @@
label="Profit margin (% added to invoices/deducted from faucets)" label="Profit margin (% added to invoices/deducted from faucets)"
></q-input> ></q-input>
<div v-else> <div v-else>
<q-input <q-btn
ref="setAmount" unelevated
filled class="q-mb-lg"
dense round
v-model.trim="formDialoglnurldevice.data.profit" size="sm"
class="q-pb-md" icon="add"
:label="'Amount (' + formDialoglnurldevice.data.currency + ') *'" @click="addSwitch"
:mask="'#.##'" v-model="switches"
fill-mask="0" color="primary"
reverse-fill-mask ></q-btn>
:step="'0.01'" <q-btn
value="0.00" unelevated
></q-input> class="q-mb-lg"
<q-input round
filled size="sm"
dense icon="remove"
v-model.trim="formDialoglnurldevice.data.amount" @click="removeSwitch"
type="number" v-model="switches"
value="1000" color="primary"
label="milesecs to turn Switch on for (1sec = 1000ms)" ></q-btn>
></q-input>
</div>
<div v-if="switches >= 0">
<div class="row">
<div class="col">
<q-input
ref="setAmount"
filled
dense
v-model.trim="formDialoglnurldevice.data.profit"
class="q-pb-md"
:label="'Amount (' + formDialoglnurldevice.data.currency + ') *'"
:mask="'#.##'"
fill-mask="0"
reverse-fill-mask
:step="'0.01'"
value="0.00"
></q-input>
</div>
<div class="col q-ml-md">
<q-input
filled
dense
v-model.trim="formDialoglnurldevice.data.amount"
type="number"
value="1000"
label="milesecs to turn Switch on for (1sec = 1000ms)"
></q-input>
</div>
<div class="col q-ml-md">
<q-input
filled
dense
v-model.trim="formDialoglnurldevice.data.pin"
type="number"
label="GPIO to turn on"
></q-input>
</div>
</div>
</div>
<div v-if="switches >= 1">
<div class="row">
<div class="col">
<q-input
ref="setAmount"
filled
dense
v-model.trim="formDialoglnurldevice.data.profit1"
class="q-pb-md"
:label="'Amount (' + formDialoglnurldevice.data.currency + ') *'"
:mask="'#.##'"
fill-mask="0"
reverse-fill-mask
:step="'0.01'"
value="0.00"
></q-input>
</div>
<div class="col q-ml-md">
<q-input
filled
dense
v-model.trim="formDialoglnurldevice.data.amount1"
type="number"
value="1000"
label="milesecs to turn Switch on for (1sec = 1000ms)"
></q-input>
</div>
<div class="col q-ml-md">
<q-input
filled
dense
v-model.trim="formDialoglnurldevice.data.pin1"
type="number"
label="GPIO to turn on"
></q-input>
</div>
</div>
</div>
<div v-if="switches >= 2">
<div class="row">
<div class="col">
<q-input
ref="setAmount"
filled
dense
v-model.trim="formDialoglnurldevice.data.profit2"
class="q-pb-md"
:label="'Amount (' + formDialoglnurldevice.data.currency + ') *'"
:mask="'#.##'"
fill-mask="0"
reverse-fill-mask
:step="'0.01'"
value="0.00"
></q-input>
</div>
<div class="col q-ml-md">
<q-input
filled
dense
v-model.trim="formDialoglnurldevice.data.amount2"
type="number"
value="1000"
label="milesecs to turn Switch on for (1sec = 1000ms)"
></q-input>
</div>
<div class="col q-ml-md">
<q-input
filled
dense
v-model.trim="formDialoglnurldevice.data.pin2"
type="number"
label="GPIO to turn on"
></q-input>
</div>
</div>
</div>
<div v-if="switches >= 3">
<div class="row">
<div class="col">
<q-input
ref="setAmount"
filled
dense
v-model.trim="formDialoglnurldevice.data.profit3"
class="q-pb-md"
:label="'Amount (' + formDialoglnurldevice.data.currency + ') *'"
:mask="'#.##'"
fill-mask="0"
reverse-fill-mask
:step="'0.01'"
value="0.00"
></q-input>
</div>
<div class="col q-ml-md">
<q-input
filled
dense
v-model.trim="formDialoglnurldevice.data.amount3"
type="number"
value="1000"
label="milesecs to turn Switch on for (1sec = 1000ms)"
></q-input>
</div>
<div class="col q-ml-md">
<q-input
filled
dense
v-model.trim="formDialoglnurldevice.data.pin3"
type="number"
label="GPIO to turn on"
></q-input>
</div>
</div>
</div>
<div v-if="switches >= 4">
<div class="row">
<div class="col">
<q-input
ref="setAmount"
filled
dense
v-model.trim="formDialoglnurldevice.data.profit4"
class="q-pb-md"
:label="'Amount (' + formDialoglnurldevice.data.currency + ') *'"
:mask="'#.##'"
fill-mask="0"
reverse-fill-mask
:step="'0.01'"
value="0.00"
></q-input>
</div>
<div class="col q-ml-md">
<q-input
filled
dense
v-model.trim="formDialoglnurldevice.data.amount4"
type="number"
value="1000"
label="milesecs to turn Switch on for (1sec = 1000ms)"
></q-input>
</div>
<div class="col q-ml-md">
<q-input
filled
dense
v-model.trim="formDialoglnurldevice.data.pin4"
type="number"
label="GPIO to turn on"
></q-input>
</div>
</div>
</div>
</div>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<q-btn <q-btn
v-if="formDialoglnurldevice.data.id" v-if="formDialoglnurldevice.data.id"
@ -284,24 +476,26 @@
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card"> <q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
<q-responsive :ratio="1" class="q-mx-xl q-mb-md"> <q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode <qrcode
:value="qrCodeDialog.data.url + '/?lightning=' + qrCodeDialog.data.lnurl" :value="lnurlValue"
:options="{width: 800}" :options="{width: 800}"
class="rounded-borders" class="rounded-borders"
></qrcode> ></qrcode>
{% raw %}
</q-responsive> </q-responsive>
<p style="word-break: break-all"> <q-btn
<strong>ID:</strong> {{ qrCodeDialog.data.id }}<br /> outline
</p> color="grey"
{% endraw %} @click="copyText(lnurlValue, 'LNURL copied to clipboard!')"
>Copy LNURL</q-btn
>
<br />
<div class="row q-mt-lg q-gutter-sm"> <div class="row q-mt-lg q-gutter-sm">
<q-btn <q-btn
v-for="switch_ in qrCodeDialog.data.switches"
outline outline
color="grey" color="primary"
@click="copyText(qrCodeDialog.data.lnurl, 'LNURL copied to clipboard!')" :label="'Switch PIN:' + switch_[0]"
class="q-ml-sm" @click="lnurlValueFetch(switch_[3])"
>Copy LNURL</q-btn ></q-btn>
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn> <q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div> </div>
</q-card> </q-card>
@ -333,11 +527,14 @@
mixins: [windowMixin], mixins: [windowMixin],
data: function () { data: function () {
return { return {
tab: 'mails',
protocol: window.location.protocol, protocol: window.location.protocol,
location: window.location.hostname, location: window.location.hostname,
wslocation: window.location.hostname, wslocation: window.location.hostname,
filter: '', filter: '',
currency: 'USD', currency: 'USD',
lnurlValue: '',
switches: 0,
lnurldeviceLinks: [], lnurldeviceLinks: [],
lnurldeviceLinksObj: [], lnurldeviceLinksObj: [],
devices: [ devices: [
@ -386,12 +583,6 @@
label: 'device', label: 'device',
field: 'device' field: 'device'
}, },
{
name: 'profit',
align: 'left',
label: 'profit',
field: 'profit'
},
{ {
name: 'currency', name: 'currency',
align: 'left', align: 'left',
@ -440,8 +631,20 @@
this.qrCodeDialog.data = _.clone(lnurldevice) this.qrCodeDialog.data = _.clone(lnurldevice)
this.qrCodeDialog.data.url = this.qrCodeDialog.data.url =
window.location.protocol + '//' + window.location.host window.location.protocol + '//' + window.location.host
this.lnurlValueFetch(this.qrCodeDialog.data.switches[0][3])
this.qrCodeDialog.show = true 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) { cancellnurldevice: function (data) {
var self = this var self = this
self.formDialoglnurldevice.show = false self.formDialoglnurldevice.show = false
@ -498,7 +701,9 @@
.then(function (response) { .then(function (response) {
if (response.data) { if (response.data) {
self.lnurldeviceLinks = response.data.map(maplnurldevice) self.lnurldeviceLinks = response.data.map(maplnurldevice)
console.log('response.data')
console.log(response.data) console.log(response.data)
console.log('response.data')
} }
}) })
.catch(function (error) { .catch(function (error) {

View file

@ -103,8 +103,10 @@ async def websocket_endpoint(websocket: WebSocket, lnurldevice_id: str):
manager.disconnect(websocket) manager.disconnect(websocket)
async def updater(lnurldevice_id): async def updater(lnurldevice_id, lnurldevice_pin, lnurldevice_amount):
lnurldevice = await get_lnurldevice(lnurldevice_id) lnurldevice = await get_lnurldevice(lnurldevice_id)
if not lnurldevice: if not lnurldevice:
return return
await manager.send_personal_message(f"{lnurldevice.amount}", lnurldevice_id) return await manager.send_personal_message(
f"{lnurldevice_pin}-{lnurldevice_amount}", lnurldevice_id
)

View file

@ -39,10 +39,10 @@ async def api_lnurldevice_create_or_update(
): ):
if not lnurldevice_id: if not lnurldevice_id:
lnurldevice = await create_lnurldevice(data) lnurldevice = await create_lnurldevice(data)
return {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(req)}} return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
else: else:
lnurldevice = await update_lnurldevice(data, lnurldevice_id=lnurldevice_id) 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") @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 wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
try: try:
return [ return [
{**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(req)}} {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
for lnurldevice in await get_lnurldevices(wallet_ids) for lnurldevice in await get_lnurldevices(wallet_ids)
] ]
except: except:
@ -78,7 +78,7 @@ async def api_lnurldevice_retrieve(
) )
if not lnurldevice.lnurl_toggle: if not lnurldevice.lnurl_toggle:
return {**lnurldevice.dict()} 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}") @lnurldevice_ext.delete("/api/v1/lnurlpos/{lnurldevice_id}")

View file

@ -38,7 +38,7 @@
> >
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none"> <h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json) Returns 200 OK (application/json)
</h5> </h5>
<code>JSON list of users</code> <code>JSON list of users</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
@ -57,10 +57,16 @@
/usermanager/api/v1/users/&lt;user_id&gt;</code /usermanager/api/v1/users/&lt;user_id&gt;</code
> >
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none"> <h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json) Returns 200 OK (application/json)
</h5> </h5>
<code>JSON list of users</code> <code
>{"id": &lt;string&gt;, "name": &lt;string&gt;, "admin":
&lt;string&gt;, "email": &lt;string&gt;, "password":
&lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.base_url >curl -X GET {{ request.base_url
@ -81,7 +87,7 @@
<code>{"X-Api-Key": &lt;string&gt;}</code> <code>{"X-Api-Key": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none"> <h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json) Returns 200 OK (application/json)
</h5> </h5>
<code>JSON wallet data</code> <code>JSON wallet data</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
@ -104,7 +110,7 @@
<code>{"X-Api-Key": &lt;string&gt;}</code> <code>{"X-Api-Key": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none"> <h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json) Returns 200 OK (application/json)
</h5> </h5>
<code>JSON a wallets transactions</code> <code>JSON a wallets transactions</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
@ -254,11 +260,15 @@
<code>{"X-Api-Key": &lt;string&gt;}</code> <code>{"X-Api-Key": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X POST {{ request.base_url }}usermanager/api/v1/extensions -d >curl -X POST {{ request.base_url
'{"userid": &lt;string&gt;, "extension": &lt;string&gt;, "active": }}usermanager/api/v1/extensions?extension=withdraw&userid=user_id&active=true
&lt;integer&gt;}' -H "X-Api-Key: {{ user.wallets[0].inkey }}" -H -H "X-Api-Key: {{ user.wallets[0].inkey }}" -H "Content-type:
"Content-type: application/json" application/json"
</code> </code>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>{"extension": "updated"}</code>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>

View file

@ -78,34 +78,35 @@ async def api_lnurl_callback(
return {"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."} return {"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."}
usescsv = "" 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: 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 = { changes = {
"open_time": link.wait_time + now, "open_time": link.wait_time + now,
"used": link.used + 1, "used": link.used + 1,