add service
This commit is contained in:
@@ -1,3 +1,10 @@
|
|||||||
|
from .blockexplorer import (
|
||||||
|
fetch_fee_estimates,
|
||||||
|
fetch_onchain_balance,
|
||||||
|
fetch_recent_blocks,
|
||||||
|
fetch_tip,
|
||||||
|
fetch_transaction,
|
||||||
|
)
|
||||||
from .fiat_providers import check_fiat_status
|
from .fiat_providers import check_fiat_status
|
||||||
from .funding_source import (
|
from .funding_source import (
|
||||||
get_balance_delta,
|
get_balance_delta,
|
||||||
@@ -56,7 +63,12 @@ __all__ = [
|
|||||||
"enqueue_admin_notification",
|
"enqueue_admin_notification",
|
||||||
"fee_reserve",
|
"fee_reserve",
|
||||||
"fee_reserve_total",
|
"fee_reserve_total",
|
||||||
|
"fetch_fee_estimates",
|
||||||
"fetch_lnurl_pay_request",
|
"fetch_lnurl_pay_request",
|
||||||
|
"fetch_onchain_balance",
|
||||||
|
"fetch_recent_blocks",
|
||||||
|
"fetch_tip",
|
||||||
|
"fetch_transaction",
|
||||||
"get_balance_delta",
|
"get_balance_delta",
|
||||||
"get_payments_daily_stats",
|
"get_payments_daily_stats",
|
||||||
"get_pr_from_lnurl",
|
"get_pr_from_lnurl",
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
from lnbits.settings import settings
|
||||||
|
from lnbits.task_manager import OnchainAddressEvent
|
||||||
|
from lnbits.utils.electrum import (
|
||||||
|
AddressResponse,
|
||||||
|
Balance,
|
||||||
|
BlockHeader,
|
||||||
|
BlockInfo,
|
||||||
|
ElectrumClient,
|
||||||
|
FeeResponse,
|
||||||
|
Transaction,
|
||||||
|
network_from_name,
|
||||||
|
parse_block_header,
|
||||||
|
parse_raw_tx,
|
||||||
|
scripthash_from_address,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _client() -> ElectrumClient:
|
||||||
|
return ElectrumClient(
|
||||||
|
settings.lnbits_blockexplorer_electrum_url,
|
||||||
|
network=network_from_name(settings.lnbits_blockexplorer_network),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_recent_blocks(count: int = 5) -> list[BlockInfo]:
|
||||||
|
async with _client() as c:
|
||||||
|
tip = await c.get_tip()
|
||||||
|
start = max(0, tip.height - count + 1)
|
||||||
|
headers = await c.get_block_headers(start, tip.height - start + 1)
|
||||||
|
raw = bytes.fromhex(headers.hex)
|
||||||
|
blocks = [
|
||||||
|
parse_block_header(raw[i * 80 : (i + 1) * 80].hex(), start + i)
|
||||||
|
for i in range(headers.count)
|
||||||
|
]
|
||||||
|
return list(reversed(blocks))
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_tip() -> BlockHeader:
|
||||||
|
async with _client() as c:
|
||||||
|
return await c.get_tip()
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_fee_estimates() -> FeeResponse:
|
||||||
|
async with _client() as c:
|
||||||
|
estimates_raw = await asyncio.gather(
|
||||||
|
c.estimate_fee(1),
|
||||||
|
c.estimate_fee(3),
|
||||||
|
c.estimate_fee(6),
|
||||||
|
c.estimate_fee(144),
|
||||||
|
)
|
||||||
|
histogram = await c.fee_histogram()
|
||||||
|
estimates = {
|
||||||
|
str(blocks): fee
|
||||||
|
for blocks, fee in zip([1, 3, 6, 144], estimates_raw, strict=False)
|
||||||
|
if fee >= 0
|
||||||
|
}
|
||||||
|
return FeeResponse(estimates=estimates, histogram=histogram)
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_transaction(txid: str) -> Transaction:
|
||||||
|
async with _client() as c:
|
||||||
|
raw_hex = await c.get_transaction(txid)
|
||||||
|
return parse_raw_tx(raw_hex, network=c.network)
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_onchain_balance(onchain_address: str) -> AddressResponse:
|
||||||
|
scripthash = scripthash_from_address(onchain_address)
|
||||||
|
async with _client() as client:
|
||||||
|
balance_res, history_res = await asyncio.gather(
|
||||||
|
client.get_balance(scripthash),
|
||||||
|
client.get_history(scripthash),
|
||||||
|
return_exceptions=True,
|
||||||
|
)
|
||||||
|
if isinstance(balance_res, BaseException):
|
||||||
|
raise balance_res
|
||||||
|
history = [] if isinstance(history_res, BaseException) else history_res
|
||||||
|
history_error = (
|
||||||
|
str(history_res) if isinstance(history_res, BaseException) else None
|
||||||
|
)
|
||||||
|
return AddressResponse(
|
||||||
|
balance=balance_res, history=history, history_error=history_error
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def address_event_to_response(event: OnchainAddressEvent) -> AddressResponse:
|
||||||
|
return AddressResponse(
|
||||||
|
balance=Balance(confirmed=event.confirmed, unconfirmed=event.unconfirmed),
|
||||||
|
history=event.history,
|
||||||
|
history_error=event.history_error,
|
||||||
|
)
|
||||||
@@ -5,6 +5,14 @@ from typing import Annotated
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Request, WebSocket
|
from fastapi import APIRouter, Depends, HTTPException, Request, WebSocket
|
||||||
from pydantic.types import UUID4
|
from pydantic.types import UUID4
|
||||||
|
|
||||||
|
from lnbits.core.services.blockexplorer import (
|
||||||
|
address_event_to_response,
|
||||||
|
fetch_fee_estimates,
|
||||||
|
fetch_onchain_balance,
|
||||||
|
fetch_recent_blocks,
|
||||||
|
fetch_tip,
|
||||||
|
fetch_transaction,
|
||||||
|
)
|
||||||
from lnbits.decorators import check_access_token, check_user_exists
|
from lnbits.decorators import check_access_token, check_user_exists
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.task_manager import (
|
from lnbits.task_manager import (
|
||||||
@@ -15,16 +23,11 @@ from lnbits.task_manager import (
|
|||||||
)
|
)
|
||||||
from lnbits.utils.electrum import (
|
from lnbits.utils.electrum import (
|
||||||
AddressResponse,
|
AddressResponse,
|
||||||
Balance,
|
|
||||||
BlockHeader,
|
BlockHeader,
|
||||||
BlockInfo,
|
BlockInfo,
|
||||||
ElectrumClient,
|
|
||||||
ElectrumError,
|
ElectrumError,
|
||||||
FeeResponse,
|
FeeResponse,
|
||||||
Transaction,
|
Transaction,
|
||||||
network_from_name,
|
|
||||||
parse_block_header,
|
|
||||||
parse_raw_tx,
|
|
||||||
scripthash_from_address,
|
scripthash_from_address,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -52,29 +55,13 @@ async def _check_api_access(
|
|||||||
await check_user_exists(r, access_token, usr)
|
await check_user_exists(r, access_token, usr)
|
||||||
|
|
||||||
|
|
||||||
def _client() -> ElectrumClient:
|
|
||||||
return ElectrumClient(
|
|
||||||
settings.lnbits_blockexplorer_electrum_url,
|
|
||||||
network=network_from_name(settings.lnbits_blockexplorer_network),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ---- REST ----
|
# ---- REST ----
|
||||||
|
|
||||||
|
|
||||||
@blockexplorer_router.get("/blocks", dependencies=[Depends(_check_api_access)])
|
@blockexplorer_router.get("/blocks", dependencies=[Depends(_check_api_access)])
|
||||||
async def api_blocks() -> list[BlockInfo]:
|
async def api_blocks() -> list[BlockInfo]:
|
||||||
try:
|
try:
|
||||||
async with _client() as c:
|
return await fetch_recent_blocks()
|
||||||
tip = await c.get_tip()
|
|
||||||
start = max(0, tip.height - 4)
|
|
||||||
headers = await c.get_block_headers(start, tip.height - start + 1)
|
|
||||||
raw = bytes.fromhex(headers.hex)
|
|
||||||
blocks = [
|
|
||||||
parse_block_header(raw[i * 80 : (i + 1) * 80].hex(), start + i)
|
|
||||||
for i in range(headers.count)
|
|
||||||
]
|
|
||||||
return list(reversed(blocks))
|
|
||||||
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
|
||||||
|
|
||||||
@@ -82,8 +69,7 @@ async def api_blocks() -> list[BlockInfo]:
|
|||||||
@blockexplorer_router.get("/tip", dependencies=[Depends(_check_api_access)])
|
@blockexplorer_router.get("/tip", dependencies=[Depends(_check_api_access)])
|
||||||
async def api_tip() -> BlockHeader:
|
async def api_tip() -> BlockHeader:
|
||||||
try:
|
try:
|
||||||
async with _client() as c:
|
return await fetch_tip()
|
||||||
return await c.get_tip()
|
|
||||||
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
|
||||||
|
|
||||||
@@ -91,20 +77,7 @@ async def api_tip() -> BlockHeader:
|
|||||||
@blockexplorer_router.get("/fees", dependencies=[Depends(_check_api_access)])
|
@blockexplorer_router.get("/fees", dependencies=[Depends(_check_api_access)])
|
||||||
async def api_fees() -> FeeResponse:
|
async def api_fees() -> FeeResponse:
|
||||||
try:
|
try:
|
||||||
async with _client() as c:
|
return await fetch_fee_estimates()
|
||||||
estimates_raw = await asyncio.gather(
|
|
||||||
c.estimate_fee(1),
|
|
||||||
c.estimate_fee(3),
|
|
||||||
c.estimate_fee(6),
|
|
||||||
c.estimate_fee(144),
|
|
||||||
)
|
|
||||||
histogram = await c.fee_histogram()
|
|
||||||
estimates = {
|
|
||||||
str(blocks): fee
|
|
||||||
for blocks, fee in zip([1, 3, 6, 144], estimates_raw, strict=False)
|
|
||||||
if fee >= 0
|
|
||||||
}
|
|
||||||
return FeeResponse(estimates=estimates, histogram=histogram)
|
|
||||||
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
|
||||||
|
|
||||||
@@ -112,9 +85,7 @@ async def api_fees() -> FeeResponse:
|
|||||||
@blockexplorer_router.get("/tx/{txid}", dependencies=[Depends(_check_api_access)])
|
@blockexplorer_router.get("/tx/{txid}", dependencies=[Depends(_check_api_access)])
|
||||||
async def api_tx(txid: str) -> Transaction:
|
async def api_tx(txid: str) -> Transaction:
|
||||||
try:
|
try:
|
||||||
async with _client() as c:
|
return await fetch_transaction(txid)
|
||||||
raw_hex = await c.get_transaction(txid)
|
|
||||||
return parse_raw_tx(raw_hex, network=c.network)
|
|
||||||
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
|
||||||
|
|
||||||
@@ -124,27 +95,11 @@ async def api_tx(txid: str) -> Transaction:
|
|||||||
)
|
)
|
||||||
async def api_address(address: str) -> AddressResponse:
|
async def api_address(address: str) -> AddressResponse:
|
||||||
try:
|
try:
|
||||||
scripthash = scripthash_from_address(address)
|
scripthash_from_address(address)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, detail=str(e)) from e
|
raise HTTPException(HTTPStatus.BAD_REQUEST, detail=str(e)) from e
|
||||||
try:
|
try:
|
||||||
async with _client() as c:
|
return await fetch_onchain_balance(address)
|
||||||
balance_res, history_res = await asyncio.gather(
|
|
||||||
c.get_balance(scripthash),
|
|
||||||
c.get_history(scripthash),
|
|
||||||
return_exceptions=True,
|
|
||||||
)
|
|
||||||
if isinstance(balance_res, BaseException):
|
|
||||||
raise HTTPException(HTTPStatus.SERVICE_UNAVAILABLE, detail=str(balance_res))
|
|
||||||
history = [] if isinstance(history_res, BaseException) else history_res
|
|
||||||
history_error = (
|
|
||||||
str(history_res) if isinstance(history_res, BaseException) else None
|
|
||||||
)
|
|
||||||
return AddressResponse(
|
|
||||||
balance=balance_res, history=history, history_error=history_error
|
|
||||||
)
|
|
||||||
except HTTPException:
|
|
||||||
raise
|
|
||||||
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
|
||||||
|
|
||||||
@@ -167,14 +122,6 @@ async def ws_blocks(websocket: WebSocket) -> None:
|
|||||||
task_manager.unregister_ws_block_queue(queue)
|
task_manager.unregister_ws_block_queue(queue)
|
||||||
|
|
||||||
|
|
||||||
def _address_event_to_response(event: OnchainAddressEvent) -> AddressResponse:
|
|
||||||
return AddressResponse(
|
|
||||||
balance=Balance(confirmed=event.confirmed, unconfirmed=event.unconfirmed),
|
|
||||||
history=event.history,
|
|
||||||
history_error=event.history_error,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@blockexplorer_router.websocket("/ws/address/{address}")
|
@blockexplorer_router.websocket("/ws/address/{address}")
|
||||||
async def ws_address(websocket: WebSocket, address: str) -> None:
|
async def ws_address(websocket: WebSocket, address: str) -> None:
|
||||||
if not settings.lnbits_blockexplorer_enabled:
|
if not settings.lnbits_blockexplorer_enabled:
|
||||||
@@ -189,7 +136,7 @@ async def ws_address(websocket: WebSocket, address: str) -> None:
|
|||||||
await websocket.close(code=1008, reason=str(e))
|
await websocket.close(code=1008, reason=str(e))
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
await relay_ws_queue(websocket, queue, serialize=_address_event_to_response)
|
await relay_ws_queue(websocket, queue, serialize=address_event_to_response)
|
||||||
finally:
|
finally:
|
||||||
task_manager.unregister_ws_address_queue(address, queue)
|
task_manager.unregister_ws_address_queue(address, queue)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user