Merge branch 'main' into FinalAdminUI
This commit is contained in:
commit
4cd2fe7ee1
|
@ -9,53 +9,10 @@ nav_order: 2
|
|||
Websockets
|
||||
=================
|
||||
|
||||
`websockets` are a great way to add a two way instant data channel between server and client. This example was taken from the `copilot` extension, we create a websocket endpoint which can be restricted by `id`, then can feed it data to broadcast to any client on the socket using the `updater(extension_id, data)` function (`extension` has been used in place of an extension name, wreplace to your own extension):
|
||||
`websockets` are a great way to add a two way instant data channel between server and client.
|
||||
|
||||
LNbits has a useful in built websocket tool. With a websocket client connect to (obv change `somespecificid`) `wss://legend.lnbits.com/api/v1/ws/somespecificid` (you can use an online websocket tester). Now make a get to `https://legend.lnbits.com/api/v1/ws/somespecificid/somedata`. You can send data to that websocket by using `from lnbits.core.services import websocketUpdater` and the function `websocketUpdater("somespecificid", "somdata")`.
|
||||
|
||||
```sh
|
||||
from fastapi import Request, WebSocket, WebSocketDisconnect
|
||||
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
self.active_connections: List[WebSocket] = []
|
||||
|
||||
async def connect(self, websocket: WebSocket, extension_id: str):
|
||||
await websocket.accept()
|
||||
websocket.id = extension_id
|
||||
self.active_connections.append(websocket)
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
self.active_connections.remove(websocket)
|
||||
|
||||
async def send_personal_message(self, message: str, extension_id: str):
|
||||
for connection in self.active_connections:
|
||||
if connection.id == extension_id:
|
||||
await connection.send_text(message)
|
||||
|
||||
async def broadcast(self, message: str):
|
||||
for connection in self.active_connections:
|
||||
await connection.send_text(message)
|
||||
|
||||
|
||||
manager = ConnectionManager()
|
||||
|
||||
|
||||
@extension_ext.websocket("/ws/{extension_id}", name="extension.websocket_by_id")
|
||||
async def websocket_endpoint(websocket: WebSocket, extension_id: str):
|
||||
await manager.connect(websocket, extension_id)
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect(websocket)
|
||||
|
||||
|
||||
async def updater(extension_id, data):
|
||||
extension = await get_extension(extension_id)
|
||||
if not extension:
|
||||
return
|
||||
await manager.send_personal_message(f"{data}", extension_id)
|
||||
```
|
||||
|
||||
Example vue-js function for listening to the websocket:
|
||||
|
||||
|
@ -67,16 +24,16 @@ initWs: async function () {
|
|||
document.domain +
|
||||
':' +
|
||||
location.port +
|
||||
'/extension/ws/' +
|
||||
self.extension.id
|
||||
'/api/v1/ws/' +
|
||||
self.item.id
|
||||
} else {
|
||||
localUrl =
|
||||
'ws://' +
|
||||
document.domain +
|
||||
':' +
|
||||
location.port +
|
||||
'/extension/ws/' +
|
||||
self.extension.id
|
||||
'/api/v1/ws/' +
|
||||
self.item.id
|
||||
}
|
||||
this.ws = new WebSocket(localUrl)
|
||||
this.ws.addEventListener('message', async ({data}) => {
|
||||
|
|
|
@ -47,6 +47,15 @@ poetry run lnbits
|
|||
# adding --debug in the start-up command above to help your troubleshooting and generate a more verbose output
|
||||
# Note that you have to add the line DEBUG=true in your .env file, too.
|
||||
```
|
||||
#### Updating the server
|
||||
|
||||
```
|
||||
cd lnbits-legend/
|
||||
# Stop LNbits with `ctrl + x`
|
||||
git pull
|
||||
poetry install --only main
|
||||
# Start LNbits with `poetry run lnbits`
|
||||
```
|
||||
|
||||
## Option 2: Nix
|
||||
|
||||
|
@ -75,8 +84,8 @@ LNBITS_DATA_FOLDER=data LNBITS_BACKEND_WALLET_CLASS=LNbitsWallet LNBITS_ENDPOINT
|
|||
```sh
|
||||
git clone https://github.com/lnbits/lnbits-legend.git
|
||||
cd lnbits-legend/
|
||||
# ensure you have virtualenv installed, on debian/ubuntu 'apt install python3-venv'
|
||||
python3 -m venv venv
|
||||
# ensure you have virtualenv installed, on debian/ubuntu 'apt install python3.9-venv'
|
||||
python3.9 -m venv venv
|
||||
# If you have problems here, try `sudo apt install -y pkg-config libpq-dev`
|
||||
./venv/bin/pip install -r requirements.txt
|
||||
# create the data folder and the .env file
|
||||
|
@ -106,7 +115,7 @@ docker run --detach --publish 5000:5000 --name lnbits-legend --volume ${PWD}/.en
|
|||
|
||||
## Option 5: Fly.io
|
||||
|
||||
Fly.io is a docker container hosting platform that has a generous free tier. You can host LNBits for free on Fly.io for personal use.
|
||||
Fly.io is a docker container hosting platform that has a generous free tier. You can host LNbits for free on Fly.io for personal use.
|
||||
|
||||
First, sign up for an account at [Fly.io](https://fly.io) (no credit card required).
|
||||
|
||||
|
@ -169,7 +178,7 @@ kill_timeout = 30
|
|||
...
|
||||
```
|
||||
|
||||
Next, create a volume to store the sqlite database for LNBits. Be sure to choose the same region for the volume that you chose earlier.
|
||||
Next, create a volume to store the sqlite database for LNbits. Be sure to choose the same region for the volume that you chose earlier.
|
||||
|
||||
```
|
||||
fly volumes create lnbits_data --size 1
|
||||
|
|
|
@ -2,11 +2,11 @@ import asyncio
|
|||
import json
|
||||
from binascii import unhexlify
|
||||
from io import BytesIO
|
||||
from typing import Dict, Optional, Tuple
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import httpx
|
||||
from fastapi import Depends
|
||||
from fastapi import Depends, WebSocket, WebSocketDisconnect
|
||||
from lnurl import LnurlErrorResponse
|
||||
from lnurl import decode as decode_lnurl # type: ignore
|
||||
from loguru import logger
|
||||
|
@ -447,3 +447,27 @@ async def check_admin_settings():
|
|||
and settings.lnbits_saas_instance_id
|
||||
):
|
||||
settings.send_admin_user_to_saas()
|
||||
|
||||
class WebsocketConnectionManager:
|
||||
def __init__(self):
|
||||
self.active_connections: List[WebSocket] = []
|
||||
|
||||
async def connect(self, websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
logger.debug(websocket)
|
||||
self.active_connections.append(websocket)
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
self.active_connections.remove(websocket)
|
||||
|
||||
async def send_data(self, message: str, item_id: str):
|
||||
for connection in self.active_connections:
|
||||
if connection.path_params["item_id"] == item_id:
|
||||
await connection.send_text(message)
|
||||
|
||||
|
||||
websocketManager = WebsocketConnectionManager()
|
||||
|
||||
|
||||
async def websocketUpdater(item_id, data):
|
||||
return await websocketManager.send_data(f"{data}", item_id)
|
||||
|
|
|
@ -12,7 +12,15 @@ from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
|||
import async_timeout
|
||||
import httpx
|
||||
import pyqrcode
|
||||
from fastapi import Depends, Header, Query, Request, Response
|
||||
from fastapi import (
|
||||
Depends,
|
||||
Header,
|
||||
Query,
|
||||
Request,
|
||||
Response,
|
||||
WebSocket,
|
||||
WebSocketDisconnect,
|
||||
)
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.params import Body
|
||||
from loguru import logger
|
||||
|
@ -57,6 +65,8 @@ from ..services import (
|
|||
create_invoice,
|
||||
pay_invoice,
|
||||
perform_lnurlauth,
|
||||
websocketManager,
|
||||
websocketUpdater,
|
||||
)
|
||||
from ..tasks import api_invoice_listeners
|
||||
|
||||
|
@ -675,7 +685,6 @@ async def img(request: Request, data):
|
|||
|
||||
@core_app.get("/api/v1/audit/", dependencies=[Depends(check_admin)])
|
||||
async def api_auditor():
|
||||
|
||||
WALLET = get_wallet_class()
|
||||
total_balance = await get_total_balance()
|
||||
error_message, node_balance = await WALLET.status()
|
||||
|
@ -686,8 +695,39 @@ async def api_auditor():
|
|||
node_balance, delta = None, None
|
||||
|
||||
return {
|
||||
"node_balance_msats": node_balance,
|
||||
"lnbits_balance_msats": total_balance,
|
||||
"delta_msats": delta,
|
||||
"node_balance_msats": int(node_balance),
|
||||
"lnbits_balance_msats": int(total_balance),
|
||||
"delta_msats": int(delta),
|
||||
"timestamp": int(time.time()),
|
||||
}
|
||||
|
||||
|
||||
##################UNIVERSAL WEBSOCKET MANAGER########################
|
||||
|
||||
|
||||
@core_app.websocket("/api/v1/ws/{item_id}")
|
||||
async def websocket_connect(websocket: WebSocket, item_id: str):
|
||||
await websocketManager.connect(websocket)
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
except WebSocketDisconnect:
|
||||
websocketManager.disconnect(websocket)
|
||||
|
||||
|
||||
@core_app.post("/api/v1/ws/{item_id}")
|
||||
async def websocket_update_post(item_id: str, data: str):
|
||||
try:
|
||||
await websocketUpdater(item_id, data)
|
||||
return {"sent": True, "data": data}
|
||||
except:
|
||||
return {"sent": False, "data": data}
|
||||
|
||||
|
||||
@core_app.get("/api/v1/ws/{item_id}/{data}")
|
||||
async def websocket_update_get(item_id: str, data: str):
|
||||
try:
|
||||
await websocketUpdater(item_id, data)
|
||||
return {"sent": True, "data": data}
|
||||
except:
|
||||
return {"sent": False, "data": data}
|
||||
|
|
|
@ -6,7 +6,7 @@ This extension allows you to link your Bolt Card (or other compatible NXP NTAG d
|
|||
|
||||
**Disclaimer:** ***Use this only if you either know what you are doing or are a reckless lightning pioneer. Only you are responsible for all your sats, cards and other devices. Always backup all your card keys!***
|
||||
|
||||
***In order to use this extension you need to be able to setup your own card.*** That means writing a URL template pointing to your LNBits instance, configuring some SUN (SDM) settings and optionally changing the card's keys. There's a [guide](https://www.whitewolftech.com/articles/payment-card/) to set it up with a card reader connected to your computer. It can be done (without setting the keys) with [TagWriter app by NXP](https://play.google.com/store/apps/details?id=com.nxp.nfc.tagwriter) Android app. Last but not least, an OSS android app by name [bolt-nfc-android-app](https://github.com/boltcard/bolt-nfc-android-app) is being developed for these purposes. It's available from Google Play [here](https://play.google.com/store/apps/details?id=com.lightningnfcapp).
|
||||
***In order to use this extension you need to be able to setup your own card.*** That means writing a URL template pointing to your LNbits instance, configuring some SUN (SDM) settings and optionally changing the card's keys. There's a [guide](https://www.whitewolftech.com/articles/payment-card/) to set it up with a card reader connected to your computer. It can be done (without setting the keys) with [TagWriter app by NXP](https://play.google.com/store/apps/details?id=com.nxp.nfc.tagwriter) Android app. Last but not least, an OSS android app by name [bolt-nfc-android-app](https://github.com/boltcard/bolt-nfc-android-app) is being developed for these purposes. It's available from Google Play [here](https://play.google.com/store/apps/details?id=com.lightningnfcapp).
|
||||
|
||||
## About the keys
|
||||
|
||||
|
@ -25,12 +25,12 @@ So far, regarding the keys, the app can only write a new key set on an empty car
|
|||
|
||||
- Read the card with the app. Note UID so you can fill it in the extension later.
|
||||
- Write the link on the card. It shoud be like `YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan/{external_id}`
|
||||
- `{external_id}` should be replaced with the External ID found in the LNBits dialog.
|
||||
- `{external_id}` should be replaced with the External ID found in the LNbits dialog.
|
||||
|
||||
- Add new card in the extension.
|
||||
- Set a max sats per transaction. Any transaction greater than this amount will be rejected.
|
||||
- Set a max sats per day. After the card spends this amount of sats in a day, additional transactions will be rejected.
|
||||
- Set a card name. This is just for your reference inside LNBits.
|
||||
- Set a card name. This is just for your reference inside LNbits.
|
||||
- Set the card UID. This is the unique identifier on your NFC card and is 7 bytes.
|
||||
- If on an Android device with a newish version of Chrome, you can click the icon next to the input and tap your card to autofill this field.
|
||||
- Advanced Options
|
||||
|
|
|
@ -7,11 +7,11 @@ from starlette.exceptions import HTTPException
|
|||
|
||||
from lnbits.core import db as core_db
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.services import websocketUpdater
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .crud import get_copilot
|
||||
from .views import updater
|
||||
|
||||
|
||||
async def wait_for_paid_invoices():
|
||||
|
@ -65,9 +65,11 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||
except (httpx.ConnectError, httpx.RequestError):
|
||||
await mark_webhook_sent(payment, -1)
|
||||
if payment.extra.get("comment"):
|
||||
await updater(copilot.id, data, payment.extra.get("comment"))
|
||||
await websocketUpdater(
|
||||
copilot.id, str(data) + "-" + str(payment.extra.get("comment"))
|
||||
)
|
||||
|
||||
await updater(copilot.id, data, "none")
|
||||
await websocketUpdater(copilot.id, str(data) + "-none")
|
||||
|
||||
|
||||
async def mark_webhook_sent(payment: Payment, status: int) -> None:
|
||||
|
|
|
@ -238,7 +238,7 @@
|
|||
document.domain +
|
||||
':' +
|
||||
location.port +
|
||||
'/copilot/ws/' +
|
||||
'/api/v1/ws/' +
|
||||
self.copilot.id
|
||||
} else {
|
||||
localUrl =
|
||||
|
@ -246,7 +246,7 @@
|
|||
document.domain +
|
||||
':' +
|
||||
location.port +
|
||||
'/copilot/ws/' +
|
||||
'/api/v1/ws/' +
|
||||
self.copilot.id
|
||||
}
|
||||
this.connection = new WebSocket(localUrl)
|
||||
|
|
|
@ -35,48 +35,3 @@ async def panel(request: Request):
|
|||
return copilot_renderer().TemplateResponse(
|
||||
"copilot/panel.html", {"request": request}
|
||||
)
|
||||
|
||||
|
||||
##################WEBSOCKET ROUTES########################
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
self.active_connections: List[WebSocket] = []
|
||||
|
||||
async def connect(self, websocket: WebSocket, copilot_id: str):
|
||||
await websocket.accept()
|
||||
websocket.id = copilot_id # type: ignore
|
||||
self.active_connections.append(websocket)
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
self.active_connections.remove(websocket)
|
||||
|
||||
async def send_personal_message(self, message: str, copilot_id: str):
|
||||
for connection in self.active_connections:
|
||||
if connection.id == copilot_id: # type: ignore
|
||||
await connection.send_text(message)
|
||||
|
||||
async def broadcast(self, message: str):
|
||||
for connection in self.active_connections:
|
||||
await connection.send_text(message)
|
||||
|
||||
|
||||
manager = ConnectionManager()
|
||||
|
||||
|
||||
@copilot_ext.websocket("/ws/{copilot_id}", name="copilot.websocket_by_id")
|
||||
async def websocket_endpoint(websocket: WebSocket, copilot_id: str):
|
||||
await manager.connect(websocket, copilot_id)
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect(websocket)
|
||||
|
||||
|
||||
async def updater(copilot_id, data, comment):
|
||||
copilot = await get_copilot(copilot_id)
|
||||
if not copilot:
|
||||
return
|
||||
await manager.send_personal_message(f"{data + '-' + comment}", copilot_id)
|
||||
|
|
|
@ -5,6 +5,7 @@ from fastapi.param_functions import Query
|
|||
from fastapi.params import Depends
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.core.services import websocketUpdater
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||
|
||||
from . import copilot_ext
|
||||
|
@ -16,7 +17,6 @@ from .crud import (
|
|||
update_copilot,
|
||||
)
|
||||
from .models import CreateCopilotData
|
||||
from .views import updater
|
||||
|
||||
#######################COPILOT##########################
|
||||
|
||||
|
@ -92,7 +92,7 @@ async def api_copilot_ws_relay(
|
|||
status_code=HTTPStatus.NOT_FOUND, detail="Copilot does not exist"
|
||||
)
|
||||
try:
|
||||
await updater(copilot_id, data, comment)
|
||||
await websocketUpdater(copilot_id, str(data) + "-" + str(comment))
|
||||
except:
|
||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your copilot")
|
||||
return ""
|
||||
|
|
|
@ -118,7 +118,7 @@
|
|||
dense
|
||||
v-model.trim="formDialog.data.company_name"
|
||||
label="Company Name"
|
||||
placeholder="LNBits Labs"
|
||||
placeholder="LNbits Labs"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
|
|
|
@ -22,8 +22,8 @@
|
|||
![redirect url](https://i.imgur.com/GMzl0lG.png)
|
||||
- on Spotify click the "EDIT SETTINGS" button and paste the copied link in the _Redirect URI's_ prompt
|
||||
![spotify app setting](https://i.imgur.com/vb0x4Tl.png)
|
||||
- back on LNBits, click "AUTORIZE ACCESS" and "Agree" on the page that will open
|
||||
- choose on which device the LNBits Jukebox extensions will stream to, you may have to be logged in in order to select the device (browser, smartphone app, etc...)
|
||||
- back on LNbits, click "AUTORIZE ACCESS" and "Agree" on the page that will open
|
||||
- choose on which device the LNbits Jukebox extensions will stream to, you may have to be logged in in order to select the device (browser, smartphone app, etc...)
|
||||
- and select what playlist will be available for users to choose songs (you need to have already playlist on Spotify)\
|
||||
![select playlists](https://i.imgur.com/g4dbtED.png)
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Help DJ's and music producers conduct music livestreams
|
||||
|
||||
LNBits Livestream extension produces a static QR code that can be shown on screen while livestreaming a DJ set for example. If someone listening to the livestream likes a song and want to support the DJ and/or the producer he can scan the QR code with a LNURL-pay capable wallet.
|
||||
LNbits Livestream extension produces a static QR code that can be shown on screen while livestreaming a DJ set for example. If someone listening to the livestream likes a song and want to support the DJ and/or the producer he can scan the QR code with a LNURL-pay capable wallet.
|
||||
|
||||
When scanned, the QR code sends up information about the song playing at the moment (name and the producer of that song). Also, if the user likes the song and would like to support the producer, he can send a tip and a message for that specific track. If the user sends an amount over a specific threshold they will be given a link to download it (optional).
|
||||
|
||||
|
@ -25,7 +25,7 @@ The revenue will be sent to a wallet created specifically for that producer, wit
|
|||
![adjust percentage](https://i.imgur.com/9weHKAB.jpg)
|
||||
3. For every different producer added, when adding tracks, a wallet is generated for them\
|
||||
![producer wallet](https://i.imgur.com/YFIZ7Tm.jpg)
|
||||
4. On the bottom of the LNBits DJ Livestream extension you'll find the static QR code ([LNURL-pay](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/lnurlp/README.md)) you can add to the livestream or if you're a street performer you can print it and have it displayed
|
||||
4. On the bottom of the LNbits DJ Livestream extension you'll find the static QR code ([LNURL-pay](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/lnurlp/README.md)) you can add to the livestream or if you're a street performer you can print it and have it displayed
|
||||
5. After all tracks and producers are added, you can start "playing" songs\
|
||||
![play tracks](https://i.imgur.com/7ytiBkq.jpg)
|
||||
6. You'll see the current track playing and a green icon indicating active track also\
|
||||
|
|
|
@ -3,4 +3,4 @@
|
|||
|
||||
Lndhub has nothing to do with lnd, it is just the name of the HTTP/JSON protocol https://bluewallet.io/ uses to talk to their Lightning custodian server at https://lndhub.io/.
|
||||
|
||||
Despite not having been planned to this, Lndhub because somewhat a standard for custodian wallet communication when https://t.me/lntxbot and https://zeusln.app/ implemented the same interface. And with this extension LNBits joins the same club.
|
||||
Despite not having been planned to this, Lndhub because somewhat a standard for custodian wallet communication when https://t.me/lntxbot and https://zeusln.app/ implemented the same interface. And with this extension LNbits joins the same club.
|
||||
|
|
|
@ -21,7 +21,7 @@ from .utils import decoded_as_lndhub, to_buffer
|
|||
|
||||
@lndhub_ext.get("/ext/getinfo")
|
||||
async def lndhub_getinfo():
|
||||
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="bad auth")
|
||||
return {"alias": LNBITS_SITE_TITLE}
|
||||
|
||||
|
||||
class AuthData(BaseModel):
|
||||
|
|
|
@ -8,12 +8,11 @@ from fastapi import HTTPException
|
|||
|
||||
from lnbits import bolt11
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.services import pay_invoice
|
||||
from lnbits.core.services import pay_invoice, websocketUpdater
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .crud import get_lnurldevice, get_lnurldevicepayment, update_lnurldevicepayment
|
||||
from .views import updater
|
||||
|
||||
|
||||
async def wait_for_paid_invoices():
|
||||
|
@ -36,9 +35,8 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||
lnurldevicepayment = await update_lnurldevicepayment(
|
||||
lnurldevicepayment_id=payment.extra.get("id"), payhash="used"
|
||||
)
|
||||
return await updater(
|
||||
return await websocketUpdater(
|
||||
lnurldevicepayment.deviceid,
|
||||
lnurldevicepayment.pin,
|
||||
lnurldevicepayment.payload,
|
||||
str(lnurldevicepayment.pin) + "-" + str(lnurldevicepayment.payload),
|
||||
)
|
||||
return
|
||||
|
|
|
@ -157,9 +157,9 @@
|
|||
unelevated
|
||||
color="primary"
|
||||
size="md"
|
||||
@click="copyText(wslocation + '/lnurldevice/ws/' + settingsDialog.data.id, 'Link copied to clipboard!')"
|
||||
>{% raw %}{{wslocation}}/lnurldevice/ws/{{settingsDialog.data.id}}{%
|
||||
endraw %}<q-tooltip> Click to copy URL </q-tooltip>
|
||||
@click="copyText(wslocation + '/api/v1/ws/' + settingsDialog.data.id, 'Link copied to clipboard!')"
|
||||
>{% raw %}{{wslocation}}/api/v1/ws/{{settingsDialog.data.id}}{% endraw
|
||||
%}<q-tooltip> Click to copy URL </q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-else
|
||||
|
@ -657,7 +657,7 @@
|
|||
lnurlValueFetch: function (lnurl, switchId) {
|
||||
this.lnurlValue = lnurl
|
||||
this.websocketConnector(
|
||||
'wss://' + window.location.host + '/lnurldevice/ws/' + switchId
|
||||
'wss://' + window.location.host + '/api/v1/ws/' + switchId
|
||||
)
|
||||
},
|
||||
addSwitch: function () {
|
||||
|
|
|
@ -2,7 +2,7 @@ from http import HTTPStatus
|
|||
from io import BytesIO
|
||||
|
||||
import pyqrcode
|
||||
from fastapi import Request, WebSocket, WebSocketDisconnect
|
||||
from fastapi import Request
|
||||
from fastapi.param_functions import Query
|
||||
from fastapi.params import Depends
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
@ -63,50 +63,3 @@ async def img(request: Request, lnurldevice_id):
|
|||
status_code=HTTPStatus.NOT_FOUND, detail="LNURLDevice does not exist."
|
||||
)
|
||||
return lnurldevice.lnurl(request)
|
||||
|
||||
|
||||
##################WEBSOCKET ROUTES########################
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
self.active_connections: List[WebSocket] = []
|
||||
|
||||
async def connect(self, websocket: WebSocket, lnurldevice_id: str):
|
||||
await websocket.accept()
|
||||
websocket.id = lnurldevice_id
|
||||
self.active_connections.append(websocket)
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
self.active_connections.remove(websocket)
|
||||
|
||||
async def send_personal_message(self, message: str, lnurldevice_id: str):
|
||||
for connection in self.active_connections:
|
||||
if connection.id == lnurldevice_id:
|
||||
await connection.send_text(message)
|
||||
|
||||
async def broadcast(self, message: str):
|
||||
for connection in self.active_connections:
|
||||
await connection.send_text(message)
|
||||
|
||||
|
||||
manager = ConnectionManager()
|
||||
|
||||
|
||||
@lnurldevice_ext.websocket("/ws/{lnurldevice_id}", name="lnurldevice.lnurldevice_by_id")
|
||||
async def websocket_endpoint(websocket: WebSocket, lnurldevice_id: str):
|
||||
await manager.connect(websocket, lnurldevice_id)
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect(websocket)
|
||||
|
||||
|
||||
async def updater(lnurldevice_id, lnurldevice_pin, lnurldevice_amount):
|
||||
lnurldevice = await get_lnurldevice(lnurldevice_id)
|
||||
if not lnurldevice:
|
||||
return
|
||||
return await manager.send_personal_message(
|
||||
f"{lnurldevice_pin}-{lnurldevice_amount}", lnurldevice_id
|
||||
)
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
[![video tutorial offline shop](http://img.youtube.com/vi/_XAvM_LNsoo/0.jpg)](https://youtu.be/_XAvM_LNsoo 'video tutorial offline shop')
|
||||
|
||||
LNBits Offline Shop allows for merchants to receive Bitcoin payments while offline and without any electronic device.
|
||||
LNbits Offline Shop allows for merchants to receive Bitcoin payments while offline and without any electronic device.
|
||||
|
||||
Merchant will create items and associate a QR code ([a LNURLp](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/lnurlp/README.md)) with a price. He can then print the QR codes and display them on their shop. When a customer chooses an item, scans the QR code, gets the description and price. After payment, the customer gets a confirmation code that the merchant can validate to be sure the payment was successful.
|
||||
|
||||
|
@ -23,7 +23,7 @@ Customers must use an LNURL pay capable wallet.
|
|||
![add new item](https://i.imgur.com/pkZqRgj.png)
|
||||
3. After creating some products, click on "PRINT QR CODES"\
|
||||
![print qr codes](https://i.imgur.com/2GAiSTe.png)
|
||||
4. You'll see a QR code for each product in your LNBits Offline Shop with a title and price ready for printing\
|
||||
4. You'll see a QR code for each product in your LNbits Offline Shop with a title and price ready for printing\
|
||||
![qr codes sheet](https://i.imgur.com/faEqOcd.png)
|
||||
5. Place the printed QR codes on your shop, or at the fair stall, or have them as a menu style laminated sheet
|
||||
6. Choose what type of confirmation do you want customers to report to merchant after a successful payment\
|
||||
|
|
|
@ -23,5 +23,5 @@ Easilly create invoices that support Lightning Network and on-chain BTC payment.
|
|||
![offchain payment](https://i.imgur.com/4191SMV.png)
|
||||
- or pay on chain\
|
||||
![onchain payment](https://i.imgur.com/wzLRR5N.png)
|
||||
5. You can check the state of your charges in LNBits\
|
||||
5. You can check the state of your charges in LNbits\
|
||||
![invoice state](https://i.imgur.com/JnBd22p.png)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Have payments split between multiple wallets
|
||||
|
||||
LNBits Split Payments extension allows for distributing payments across multiple wallets. Set it and forget it. It will keep splitting your payments across wallets forever.
|
||||
LNbits Split Payments extension allows for distributing payments across multiple wallets. Set it and forget it. It will keep splitting your payments across wallets forever.
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ So the goal of the extension is to allow the owner of a domain to sell subdomain
|
|||
4. Get Cloudflare API TOKEN
|
||||
<img src="https://i.imgur.com/BZbktTy.png">
|
||||
<img src="https://i.imgur.com/YDZpW7D.png">
|
||||
5. Open the LNBits subdomains extension and register your domain
|
||||
5. Open the LNbits subdomains extension and register your domain
|
||||
6. Click on the button in the table to open the public form that was generated for your domain
|
||||
|
||||
- Extension also supports webhooks so you can get notified when someone buys a new subdomain\
|
||||
|
|
|
@ -3,7 +3,7 @@ import asyncio
|
|||
from loguru import logger
|
||||
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.services import create_invoice, pay_invoice
|
||||
from lnbits.core.services import create_invoice, pay_invoice, websocketUpdater
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
|
@ -26,6 +26,16 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||
tpos = await get_tpos(payment.extra.get("tposId"))
|
||||
tipAmount = payment.extra.get("tipAmount")
|
||||
|
||||
strippedPayment = {
|
||||
"amount": payment.amount,
|
||||
"fee": payment.fee,
|
||||
"checking_id": payment.checking_id,
|
||||
"payment_hash": payment.payment_hash,
|
||||
"bolt11": payment.bolt11,
|
||||
}
|
||||
|
||||
await websocketUpdater(payment.extra.get("tposId"), str(strippedPayment))
|
||||
|
||||
if tipAmount is None:
|
||||
# no tip amount
|
||||
return
|
||||
|
|
|
@ -12,6 +12,7 @@ from lnbits.core.models import Payment
|
|||
from lnbits.core.services import create_invoice
|
||||
from lnbits.core.views.api import api_payment
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||
from lnbits.settings import LNBITS_COMMIT
|
||||
|
||||
from . import tpos_ext
|
||||
from .crud import create_tpos, delete_tpos, get_tpos, get_tposs
|
||||
|
@ -134,7 +135,8 @@ async def api_tpos_pay_invoice(
|
|||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
r = await client.get(lnurl, follow_redirects=True)
|
||||
headers = {"user-agent": f"lnbits/tpos commit {LNBITS_COMMIT[:7]}"}
|
||||
r = await client.get(lnurl, follow_redirects=True, headers=headers)
|
||||
if r.is_error:
|
||||
lnurl_response = {"success": False, "detail": "Error loading"}
|
||||
else:
|
||||
|
@ -145,6 +147,7 @@ async def api_tpos_pay_invoice(
|
|||
r2 = await client.get(
|
||||
resp["callback"],
|
||||
follow_redirects=True,
|
||||
headers=headers,
|
||||
params={
|
||||
"k1": resp["k1"],
|
||||
"pr": payment_request,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
Monitor an extended public key and generate deterministic fresh public keys with this simple watch only wallet. Invoice payments can also be generated, both through a publically shareable page and API.
|
||||
|
||||
You can now use this wallet on the LNBits [SatsPayServer](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/satspay/README.md) extension
|
||||
You can now use this wallet on the LNbits [SatsPayServer](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/satspay/README.md) extension
|
||||
|
||||
<a href="https://www.youtube.com/watch?v=rQMHzQEPwZY">Video demo</a>
|
||||
|
||||
|
|
|
@ -76,9 +76,13 @@ class CreatePsbt(BaseModel):
|
|||
tx_size: int
|
||||
|
||||
|
||||
class SerializedTransaction(BaseModel):
|
||||
tx_hex: str
|
||||
|
||||
|
||||
class ExtractPsbt(BaseModel):
|
||||
psbtBase64 = "" # // todo snake case
|
||||
inputs: List[TransactionInput]
|
||||
inputs: List[SerializedTransaction]
|
||||
network = "Mainnet"
|
||||
|
||||
|
||||
|
@ -87,10 +91,6 @@ class SignedTransaction(BaseModel):
|
|||
tx_json: Optional[str]
|
||||
|
||||
|
||||
class BroadcastTransaction(BaseModel):
|
||||
tx_hex: str
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
mempool_endpoint = "https://mempool.space"
|
||||
receive_gap_limit = 20
|
||||
|
|
|
@ -272,15 +272,35 @@ async function payment(path) {
|
|||
this.showChecking = false
|
||||
}
|
||||
},
|
||||
|
||||
fetchUtxoHexForPsbt: async function (psbtBase64) {
|
||||
if (this.tx?.inputs && this.tx?.inputs.length) return this.tx.inputs
|
||||
|
||||
const {data: psbtUtxos} = await LNbits.api.request(
|
||||
'PUT',
|
||||
'/watchonly/api/v1/psbt/utxos',
|
||||
this.adminkey,
|
||||
{psbtBase64}
|
||||
)
|
||||
|
||||
const inputs = []
|
||||
for (const utxo of psbtUtxos) {
|
||||
const txHex = await this.fetchTxHex(utxo.tx_id)
|
||||
inputs.push({tx_hex: txHex})
|
||||
}
|
||||
return inputs
|
||||
},
|
||||
extractTxFromPsbt: async function (psbtBase64) {
|
||||
try {
|
||||
const inputs = await this.fetchUtxoHexForPsbt(psbtBase64)
|
||||
|
||||
const {data} = await LNbits.api.request(
|
||||
'PUT',
|
||||
'/watchonly/api/v1/psbt/extract',
|
||||
this.adminkey,
|
||||
{
|
||||
psbtBase64,
|
||||
inputs: this.tx.inputs,
|
||||
inputs,
|
||||
network: this.network
|
||||
}
|
||||
)
|
||||
|
|
|
@ -54,7 +54,10 @@ const watchOnly = async () => {
|
|||
showPayment: false,
|
||||
fetchedUtxos: false,
|
||||
utxosFilter: '',
|
||||
network: null
|
||||
network: null,
|
||||
|
||||
showEnterSignedPsbt: false,
|
||||
signedBase64Psbt: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -173,6 +176,15 @@ const watchOnly = async () => {
|
|||
this.$refs.paymentRef.updateSignedPsbt(psbtBase64)
|
||||
},
|
||||
|
||||
showEnterSignedPsbtDialog: function () {
|
||||
this.signedBase64Psbt = ''
|
||||
this.showEnterSignedPsbt = true
|
||||
},
|
||||
|
||||
checkPsbt: function () {
|
||||
this.$refs.paymentRef.updateSignedPsbt(this.signedBase64Psbt)
|
||||
},
|
||||
|
||||
//################### UTXOs ###################
|
||||
scanAllAddresses: async function () {
|
||||
await this.refreshAddresses()
|
||||
|
|
|
@ -52,14 +52,38 @@
|
|||
></q-spinner>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-5 q-pr-md">
|
||||
<q-btn
|
||||
<q-btn-dropdown
|
||||
v-if="!showPayment"
|
||||
split
|
||||
unelevated
|
||||
label="New Payment"
|
||||
color="secondary"
|
||||
class="btn-full"
|
||||
@click="goToPaymentView"
|
||||
>New Payment</q-btn
|
||||
>
|
||||
<q-list>
|
||||
<q-item @click="goToPaymentView" clickable v-close-popup>
|
||||
<q-item-section>
|
||||
<q-item-label>New Payment</q-item-label>
|
||||
<q-item-label caption
|
||||
>Create a new payment by selecting Inputs and
|
||||
Outputs</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
@click="showEnterSignedPsbtDialog"
|
||||
clickable
|
||||
v-close-popup
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label>From Signed PSBT</q-item-label>
|
||||
<q-item-label caption> Paste a signed PSBT</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
|
||||
<q-btn
|
||||
v-if="showPayment"
|
||||
outline
|
||||
|
@ -226,6 +250,36 @@
|
|||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="showEnterSignedPsbt" position="top">
|
||||
<q-card class="q-pa-lg lnbits__dialog-card">
|
||||
<h5 class="text-subtitle1 q-my-none">Enter the Signed PSBT</h5>
|
||||
<q-separator></q-separator><br />
|
||||
|
||||
<p>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="signedBase64Psbt"
|
||||
type="textarea"
|
||||
label="Signed PSBT"
|
||||
></q-input>
|
||||
</p>
|
||||
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn
|
||||
outline
|
||||
v-close-popup
|
||||
color="grey"
|
||||
@click="checkPsbt"
|
||||
class="q-ml-sm"
|
||||
>Check PSBT</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
<div class="row q-mt-lg q-gutter-sm"></div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
{% endraw %}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -31,11 +31,11 @@ from .crud import (
|
|||
)
|
||||
from .helpers import parse_key
|
||||
from .models import (
|
||||
BroadcastTransaction,
|
||||
Config,
|
||||
CreatePsbt,
|
||||
CreateWallet,
|
||||
ExtractPsbt,
|
||||
SerializedTransaction,
|
||||
SignedTransaction,
|
||||
WalletAccount,
|
||||
)
|
||||
|
@ -291,6 +291,24 @@ async def api_psbt_create(
|
|||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
|
||||
|
||||
|
||||
@watchonly_ext.put("/api/v1/psbt/utxos")
|
||||
async def api_psbt_extract_tx(
|
||||
req: Request, w: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
"""Extract previous unspent transaction outputs (tx_id, vout) from PSBT"""
|
||||
|
||||
body = await req.json()
|
||||
try:
|
||||
psbt = PSBT.from_base64(body["psbtBase64"])
|
||||
res = []
|
||||
for _, inp in enumerate(psbt.inputs):
|
||||
res.append({"tx_id": inp.txid.hex(), "vout": inp.vout})
|
||||
|
||||
return res
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
|
||||
|
||||
|
||||
@watchonly_ext.put("/api/v1/psbt/extract")
|
||||
async def api_psbt_extract_tx(
|
||||
data: ExtractPsbt, w: WalletTypeInfo = Depends(require_admin_key)
|
||||
|
@ -327,7 +345,7 @@ async def api_psbt_extract_tx(
|
|||
|
||||
@watchonly_ext.post("/api/v1/tx")
|
||||
async def api_tx_broadcast(
|
||||
data: BroadcastTransaction, w: WalletTypeInfo = Depends(require_admin_key)
|
||||
data: SerializedTransaction, w: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
try:
|
||||
config = await get_config(w.wallet.user)
|
||||
|
|
|
@ -14,7 +14,7 @@ LNURL withdraw is a **very powerful tool** and should not have his use limited t
|
|||
|
||||
#### Quick Vouchers
|
||||
|
||||
LNBits Quick Vouchers allows you to easily create a batch of LNURLw's QR codes that you can print and distribute as rewards, onboarding people into Lightning Network, gifts, etc...
|
||||
LNbits Quick Vouchers allows you to easily create a batch of LNURLw's QR codes that you can print and distribute as rewards, onboarding people into Lightning Network, gifts, etc...
|
||||
|
||||
1. Create Quick Vouchers\
|
||||
![quick vouchers](https://i.imgur.com/IUfwdQz.jpg)
|
||||
|
@ -37,12 +37,12 @@ LNBits Quick Vouchers allows you to easily create a batch of LNURLw's QR codes t
|
|||
- set a title for the LNURLw (it will show up in users wallet)
|
||||
- define the minimum and maximum a user can withdraw, if you want a fixed amount set them both to an equal value
|
||||
- set how many times can the LNURLw be scanned, if it's a one time use or it can be scanned 100 times
|
||||
- LNBits has the "_Time between withdraws_" setting, you can define how long the LNURLw will be unavailable between scans
|
||||
- LNbits has the "_Time between withdraws_" setting, you can define how long the LNURLw will be unavailable between scans
|
||||
- you can set the time in _seconds, minutes or hours_
|
||||
- the "_Use unique withdraw QR..._" reduces the chance of your LNURL withdraw being exploited and depleted by one person, by generating a new QR code every time it's scanned
|
||||
2. Print, share or display your LNURLw link or it's QR code\
|
||||
![lnurlw created](https://i.imgur.com/X00twiX.jpg)
|
||||
|
||||
**LNBits bonus:** If a user doesn't have a Lightning Network wallet and scans the LNURLw QR code with their smartphone camera, or a QR scanner app, they can follow the link provided to claim their satoshis and get an instant LNBits wallet!
|
||||
**LNbits bonus:** If a user doesn't have a Lightning Network wallet and scans the LNURLw QR code with their smartphone camera, or a QR scanner app, they can follow the link provided to claim their satoshis and get an instant LNbits wallet!
|
||||
|
||||
![](https://i.imgur.com/2zZ7mi8.jpg)
|
||||
|
|
|
@ -17,6 +17,7 @@ from lnbits.settings import set_cli_settings, settings
|
|||
allow_extra_args=True,
|
||||
)
|
||||
)
|
||||
|
||||
@click.option("--port", default=settings.port, help="Port to listen on")
|
||||
@click.option("--host", default=settings.host, help="Host to run LNBits on")
|
||||
@click.option(
|
||||
|
|
|
@ -7,7 +7,7 @@ attrs==22.1.0 ; python_version >= "3.7" and python_version < "4.0"
|
|||
base58==2.1.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
bech32==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
bitstring==3.1.9 ; python_version >= "3.7" and python_version < "4.0"
|
||||
cashu==0.5.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||
cashu==0.5.5 ; python_version >= "3.7" and python_version < "4.0"
|
||||
cerberus==1.3.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||
certifi==2022.9.24 ; python_version >= "3.7" and python_version < "4.0"
|
||||
cffi==1.15.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
|
|
|
@ -22,7 +22,7 @@ async def accounting_invoice(invoices_wallet):
|
|||
invoice_data = CreateInvoiceData(
|
||||
status="open",
|
||||
currency="USD",
|
||||
company_name="LNBits, Inc",
|
||||
company_name="LNbits, Inc",
|
||||
first_name="Ben",
|
||||
last_name="Arc",
|
||||
items=[{"amount": 10.20, "description": "Item costs 10.20"}],
|
||||
|
|
|
@ -20,7 +20,7 @@ async def test_invoices_api_create_invoice_valid(client, invoices_wallet):
|
|||
query = {
|
||||
"status": "open",
|
||||
"currency": "EUR",
|
||||
"company_name": "LNBits, Inc.",
|
||||
"company_name": "LNbits, Inc.",
|
||||
"first_name": "Ben",
|
||||
"last_name": "Arc",
|
||||
"email": "ben@legend.arc",
|
||||
|
|
Loading…
Reference in New Issue
Block a user