diff --git a/docs/devs/websockets.md b/docs/devs/websockets.md index 0638e4f28..9ea53a624 100644 --- a/docs/devs/websockets.md +++ b/docs/devs/websockets.md @@ -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}) => { diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 072c4d91c..9f8b26da0 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -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 diff --git a/lnbits/core/services.py b/lnbits/core/services.py index f47e6359b..2a3e48526 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -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) diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 36feac37e..8d66a2285 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -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} diff --git a/lnbits/extensions/boltcards/README.md b/lnbits/extensions/boltcards/README.md index f9c594093..b86de62cb 100644 --- a/lnbits/extensions/boltcards/README.md +++ b/lnbits/extensions/boltcards/README.md @@ -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 diff --git a/lnbits/extensions/copilot/tasks.py b/lnbits/extensions/copilot/tasks.py index 48ad7813d..384070cdf 100644 --- a/lnbits/extensions/copilot/tasks.py +++ b/lnbits/extensions/copilot/tasks.py @@ -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: diff --git a/lnbits/extensions/copilot/templates/copilot/compose.html b/lnbits/extensions/copilot/templates/copilot/compose.html index b4022ee0f..ea44114c1 100644 --- a/lnbits/extensions/copilot/templates/copilot/compose.html +++ b/lnbits/extensions/copilot/templates/copilot/compose.html @@ -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) diff --git a/lnbits/extensions/copilot/views.py b/lnbits/extensions/copilot/views.py index b4a2354a0..3b1ebf032 100644 --- a/lnbits/extensions/copilot/views.py +++ b/lnbits/extensions/copilot/views.py @@ -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) diff --git a/lnbits/extensions/copilot/views_api.py b/lnbits/extensions/copilot/views_api.py index 46611a2ea..39d0f7fd9 100644 --- a/lnbits/extensions/copilot/views_api.py +++ b/lnbits/extensions/copilot/views_api.py @@ -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 "" diff --git a/lnbits/extensions/invoices/templates/invoices/index.html b/lnbits/extensions/invoices/templates/invoices/index.html index e3093e3cb..4ef3b7f1c 100644 --- a/lnbits/extensions/invoices/templates/invoices/index.html +++ b/lnbits/extensions/invoices/templates/invoices/index.html @@ -118,7 +118,7 @@ dense v-model.trim="formDialog.data.company_name" label="Company Name" - placeholder="LNBits Labs" + placeholder="LNbits Labs" > 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 diff --git a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html index 25dcf8c99..b2165590e 100644 --- a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html +++ b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html @@ -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 %} Click to copy URL + @click="copyText(wslocation + '/api/v1/ws/' + settingsDialog.data.id, 'Link copied to clipboard!')" + >{% raw %}{{wslocation}}/api/v1/ws/{{settingsDialog.data.id}}{% endraw + %} Click to copy URL -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\ diff --git a/lnbits/extensions/tpos/tasks.py b/lnbits/extensions/tpos/tasks.py index 6369bbc7f..6eb1d5d1c 100644 --- a/lnbits/extensions/tpos/tasks.py +++ b/lnbits/extensions/tpos/tasks.py @@ -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 diff --git a/lnbits/extensions/tpos/views_api.py b/lnbits/extensions/tpos/views_api.py index fe63a2471..e13dee9b7 100644 --- a/lnbits/extensions/tpos/views_api.py +++ b/lnbits/extensions/tpos/views_api.py @@ -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, diff --git a/lnbits/extensions/watchonly/README.md b/lnbits/extensions/watchonly/README.md index 45abdb936..d154d8a34 100644 --- a/lnbits/extensions/watchonly/README.md +++ b/lnbits/extensions/watchonly/README.md @@ -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 Video demo diff --git a/lnbits/extensions/watchonly/models.py b/lnbits/extensions/watchonly/models.py index d8c278ff8..c6265d6c5 100644 --- a/lnbits/extensions/watchonly/models.py +++ b/lnbits/extensions/watchonly/models.py @@ -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 diff --git a/lnbits/extensions/watchonly/static/components/payment/payment.js b/lnbits/extensions/watchonly/static/components/payment/payment.js index 9f38df1d0..e9689003f 100644 --- a/lnbits/extensions/watchonly/static/components/payment/payment.js +++ b/lnbits/extensions/watchonly/static/components/payment/payment.js @@ -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 } ) diff --git a/lnbits/extensions/watchonly/static/js/index.js b/lnbits/extensions/watchonly/static/js/index.js index 7e410104d..880d6b302 100644 --- a/lnbits/extensions/watchonly/static/js/index.js +++ b/lnbits/extensions/watchonly/static/js/index.js @@ -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() diff --git a/lnbits/extensions/watchonly/templates/watchonly/index.html b/lnbits/extensions/watchonly/templates/watchonly/index.html index 67f898101..84be5c815 100644 --- a/lnbits/extensions/watchonly/templates/watchonly/index.html +++ b/lnbits/extensions/watchonly/templates/watchonly/index.html @@ -52,14 +52,38 @@ >
- New Payment + + + + New Payment + Create a new payment by selecting Inputs and + Outputs + + + + + From Signed PSBT + Paste a signed PSBT + + + + + + + +
Enter the Signed PSBT
+
+ +

+ +

+ +
+ Check PSBT + Close +
+
+
+
+ {% endraw %}
diff --git a/lnbits/extensions/watchonly/views_api.py b/lnbits/extensions/watchonly/views_api.py index 9030b9c3d..97f731c3e 100644 --- a/lnbits/extensions/watchonly/views_api.py +++ b/lnbits/extensions/watchonly/views_api.py @@ -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) diff --git a/lnbits/extensions/withdraw/README.md b/lnbits/extensions/withdraw/README.md index 7bf7c232c..fce2c6e5a 100644 --- a/lnbits/extensions/withdraw/README.md +++ b/lnbits/extensions/withdraw/README.md @@ -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) diff --git a/lnbits/server.py b/lnbits/server.py index 70b158682..2d9be6123 100644 --- a/lnbits/server.py +++ b/lnbits/server.py @@ -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( diff --git a/requirements.txt b/requirements.txt index 06e8642f7..d54714559 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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" diff --git a/tests/extensions/invoices/conftest.py b/tests/extensions/invoices/conftest.py index 09ac42ecb..00b9c2375 100644 --- a/tests/extensions/invoices/conftest.py +++ b/tests/extensions/invoices/conftest.py @@ -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"}], diff --git a/tests/extensions/invoices/test_invoices_api.py b/tests/extensions/invoices/test_invoices_api.py index eaadd07b3..ed236a8fd 100644 --- a/tests/extensions/invoices/test_invoices_api.py +++ b/tests/extensions/invoices/test_invoices_api.py @@ -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",