add websocket endpoints
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException, WebSocket
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.electrum import (
|
from lnbits.utils.electrum import (
|
||||||
@@ -11,10 +13,12 @@ from lnbits.utils.electrum import (
|
|||||||
ElectrumClient,
|
ElectrumClient,
|
||||||
ElectrumError,
|
ElectrumError,
|
||||||
FeeResponse,
|
FeeResponse,
|
||||||
|
HistoryEntry,
|
||||||
Transaction,
|
Transaction,
|
||||||
parse_block_header,
|
parse_block_header,
|
||||||
parse_raw_tx,
|
parse_raw_tx,
|
||||||
scripthash_from_address,
|
scripthash_from_address,
|
||||||
|
scripthash_from_scriptpubkey,
|
||||||
)
|
)
|
||||||
|
|
||||||
blockexplorer_router = APIRouter(
|
blockexplorer_router = APIRouter(
|
||||||
@@ -35,6 +39,33 @@ def _client() -> ElectrumClient:
|
|||||||
return ElectrumClient(settings.lnbits_blockexplorer_electrum_url)
|
return ElectrumClient(settings.lnbits_blockexplorer_electrum_url)
|
||||||
|
|
||||||
|
|
||||||
|
def _watch_scripthash(tx: Transaction) -> str | None:
|
||||||
|
"""Return scripthash of first spendable output to watch for tx confirmation."""
|
||||||
|
for out in tx.vout:
|
||||||
|
if out.scriptPubKey.type != "nulldata":
|
||||||
|
return scripthash_from_scriptpubkey(bytes.fromhex(out.scriptPubKey.hex))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def _tx_status(
|
||||||
|
c: ElectrumClient, txid: str, scripthash: str | None
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
if scripthash:
|
||||||
|
history: list[HistoryEntry] = await c.get_history(scripthash)
|
||||||
|
for entry in history:
|
||||||
|
if entry.tx_hash == txid:
|
||||||
|
return {
|
||||||
|
"txid": txid,
|
||||||
|
"confirmed": entry.height > 0,
|
||||||
|
"height": entry.height if entry.height > 0 else None,
|
||||||
|
"fee": entry.fee,
|
||||||
|
}
|
||||||
|
return {"txid": txid, "confirmed": False, "height": None, "fee": None}
|
||||||
|
|
||||||
|
|
||||||
|
# ---- REST ----
|
||||||
|
|
||||||
|
|
||||||
@blockexplorer_router.get("/blocks")
|
@blockexplorer_router.get("/blocks")
|
||||||
async def api_blocks() -> list[BlockInfo]:
|
async def api_blocks() -> list[BlockInfo]:
|
||||||
_check_enabled()
|
_check_enabled()
|
||||||
@@ -112,3 +143,109 @@ async def api_address(address: str) -> AddressResponse:
|
|||||||
return AddressResponse(balance=balance, history=history)
|
return AddressResponse(balance=balance, history=history)
|
||||||
except ElectrumError as e:
|
except ElectrumError as e:
|
||||||
raise HTTPException(HTTPStatus.SERVICE_UNAVAILABLE, detail=str(e)) from e
|
raise HTTPException(HTTPStatus.SERVICE_UNAVAILABLE, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
# ---- WebSocket ----
|
||||||
|
|
||||||
|
|
||||||
|
@blockexplorer_router.websocket("/ws/blocks")
|
||||||
|
async def ws_blocks(websocket: WebSocket) -> None:
|
||||||
|
if not settings.lnbits_blockexplorer_enabled:
|
||||||
|
await websocket.close(code=1008)
|
||||||
|
return
|
||||||
|
await websocket.accept()
|
||||||
|
try:
|
||||||
|
async with _client() as c:
|
||||||
|
tip = await c.subscribe_headers()
|
||||||
|
await websocket.send_json(parse_block_header(tip.hex, tip.height).dict())
|
||||||
|
|
||||||
|
async def on_header(params: list[Any]) -> None:
|
||||||
|
h = params[0]
|
||||||
|
try:
|
||||||
|
await websocket.send_json(
|
||||||
|
parse_block_header(h["hex"], h["height"]).dict()
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"ws_blocks send error: {e}")
|
||||||
|
|
||||||
|
c.on("blockchain.headers.subscribe", on_header)
|
||||||
|
while True:
|
||||||
|
msg = await websocket.receive()
|
||||||
|
if msg["type"] == "websocket.disconnect":
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"ws_blocks error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@blockexplorer_router.websocket("/ws/address/{address}")
|
||||||
|
async def ws_address(websocket: WebSocket, address: str) -> None:
|
||||||
|
if not settings.lnbits_blockexplorer_enabled:
|
||||||
|
await websocket.close(code=1008)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
scripthash = scripthash_from_address(address)
|
||||||
|
except ValueError as e:
|
||||||
|
await websocket.close(code=1008, reason=str(e))
|
||||||
|
return
|
||||||
|
await websocket.accept()
|
||||||
|
try:
|
||||||
|
async with _client() as c:
|
||||||
|
await c.subscribe_scripthash(scripthash)
|
||||||
|
balance, history = await asyncio.gather(
|
||||||
|
c.get_balance(scripthash), c.get_history(scripthash)
|
||||||
|
)
|
||||||
|
resp = AddressResponse(balance=balance, history=history)
|
||||||
|
await websocket.send_json(resp.dict())
|
||||||
|
|
||||||
|
async def on_address_change(params: list[Any]) -> None:
|
||||||
|
try:
|
||||||
|
bal, hist = await asyncio.gather(
|
||||||
|
c.get_balance(scripthash), c.get_history(scripthash)
|
||||||
|
)
|
||||||
|
await websocket.send_json(
|
||||||
|
AddressResponse(balance=bal, history=hist).dict()
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"ws_address send error: {e}")
|
||||||
|
|
||||||
|
c.on("blockchain.scripthash.subscribe", on_address_change)
|
||||||
|
while True:
|
||||||
|
msg = await websocket.receive()
|
||||||
|
if msg["type"] == "websocket.disconnect":
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"ws_address error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@blockexplorer_router.websocket("/ws/tx/{txid}")
|
||||||
|
async def ws_tx(websocket: WebSocket, txid: str) -> None:
|
||||||
|
if not settings.lnbits_blockexplorer_enabled:
|
||||||
|
await websocket.close(code=1008)
|
||||||
|
return
|
||||||
|
await websocket.accept()
|
||||||
|
try:
|
||||||
|
async with _client() as c:
|
||||||
|
try:
|
||||||
|
raw = await c.get_transaction(txid)
|
||||||
|
except ElectrumError as e:
|
||||||
|
await websocket.send_json({"error": str(e)})
|
||||||
|
return
|
||||||
|
tx = parse_raw_tx(raw)
|
||||||
|
watch = _watch_scripthash(tx)
|
||||||
|
await websocket.send_json(await _tx_status(c, txid, watch))
|
||||||
|
if watch:
|
||||||
|
await c.subscribe_scripthash(watch)
|
||||||
|
|
||||||
|
async def on_tx_change(params: list[Any]) -> None:
|
||||||
|
try:
|
||||||
|
await websocket.send_json(await _tx_status(c, txid, watch))
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"ws_tx send error: {e}")
|
||||||
|
|
||||||
|
c.on("blockchain.scripthash.subscribe", on_tx_change)
|
||||||
|
while True:
|
||||||
|
msg = await websocket.receive()
|
||||||
|
if msg["type"] == "websocket.disconnect":
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"ws_tx error: {e}")
|
||||||
|
|||||||
Reference in New Issue
Block a user