2021-06-25 18:40:27 +02:00
|
|
|
import asyncio
|
2021-06-20 07:47:30 +02:00
|
|
|
import binascii
|
|
|
|
|
|
|
|
|
|
import zmq
|
|
|
|
|
import zmq.asyncio
|
refactor: improve startup procedure
During startup the API will try to connect to Bitcoin Core and the
Lightning Node. If it can't connect it will check every "n" seconds
(currently 2s) and connect when available. A new SSE event
called "system_startup_info" is introduced. This event contains
all startup status information during the startup procedure.
The old wallet_locked event is obsolete.
Sample:
--------------------------
event: system_startup_info
data: {"bitcoin": "offline", "bitcoin_msg": "", "lightning": "offline", "lightning_msg": "Unable to connect to LND daemon, waiting..."}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "offline", "lightning_msg": "Unable to connect to LND daemon, waiting..."}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "locked", "lightning_msg": "Wallet locked, unlock it to enable full RPC access"}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "done", "lightning_msg": ""}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "bootstraping", "lightning_msg": "RPC not yet available"}
--------------------------
refs #97
2022-06-06 19:29:21 +02:00
|
|
|
from aiohttp import client_exceptions
|
2021-07-09 19:45:29 +02:00
|
|
|
from fastapi.exceptions import HTTPException
|
2023-04-21 22:58:29 +02:00
|
|
|
from loguru import logger
|
2021-07-09 19:45:29 +02:00
|
|
|
from starlette import status
|
|
|
|
|
|
2026-07-11 11:29:55 +02:00
|
|
|
from app.api.utils import Event, broadcast_msg
|
2022-10-03 20:22:00 +02:00
|
|
|
from app.bitcoind.models import (
|
2022-04-05 16:40:18 +02:00
|
|
|
BlockchainInfo,
|
|
|
|
|
BlockRpcFunc,
|
|
|
|
|
BtcInfo,
|
|
|
|
|
FeeEstimationMode,
|
|
|
|
|
NetworkInfo,
|
2022-11-01 20:46:08 +01:00
|
|
|
RawTransaction,
|
2022-04-05 16:40:18 +02:00
|
|
|
)
|
2022-10-03 20:22:00 +02:00
|
|
|
from app.bitcoind.utils import bitcoin_config, bitcoin_rpc_async
|
2022-03-20 17:20:56 +01:00
|
|
|
|
refactor: improve startup procedure
During startup the API will try to connect to Bitcoin Core and the
Lightning Node. If it can't connect it will check every "n" seconds
(currently 2s) and connect when available. A new SSE event
called "system_startup_info" is introduced. This event contains
all startup status information during the startup procedure.
The old wallet_locked event is obsolete.
Sample:
--------------------------
event: system_startup_info
data: {"bitcoin": "offline", "bitcoin_msg": "", "lightning": "offline", "lightning_msg": "Unable to connect to LND daemon, waiting..."}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "offline", "lightning_msg": "Unable to connect to LND daemon, waiting..."}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "locked", "lightning_msg": "Wallet locked, unlock it to enable full RPC access"}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "done", "lightning_msg": ""}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "bootstraping", "lightning_msg": "RPC not yet available"}
--------------------------
refs #97
2022-06-06 19:29:21 +02:00
|
|
|
_initialized = False
|
|
|
|
|
|
|
|
|
|
|
2023-04-21 22:58:29 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
refactor: improve startup procedure
During startup the API will try to connect to Bitcoin Core and the
Lightning Node. If it can't connect it will check every "n" seconds
(currently 2s) and connect when available. A new SSE event
called "system_startup_info" is introduced. This event contains
all startup status information during the startup procedure.
The old wallet_locked event is obsolete.
Sample:
--------------------------
event: system_startup_info
data: {"bitcoin": "offline", "bitcoin_msg": "", "lightning": "offline", "lightning_msg": "Unable to connect to LND daemon, waiting..."}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "offline", "lightning_msg": "Unable to connect to LND daemon, waiting..."}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "locked", "lightning_msg": "Wallet locked, unlock it to enable full RPC access"}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "done", "lightning_msg": ""}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "bootstraping", "lightning_msg": "RPC not yet available"}
--------------------------
refs #97
2022-06-06 19:29:21 +02:00
|
|
|
async def initialize_bitcoin_repo() -> bool:
|
|
|
|
|
global _initialized
|
|
|
|
|
if _initialized:
|
|
|
|
|
return True
|
|
|
|
|
|
2023-04-21 22:58:29 +02:00
|
|
|
logger.info("Initializing bitcoin repository")
|
refactor: improve startup procedure
During startup the API will try to connect to Bitcoin Core and the
Lightning Node. If it can't connect it will check every "n" seconds
(currently 2s) and connect when available. A new SSE event
called "system_startup_info" is introduced. This event contains
all startup status information during the startup procedure.
The old wallet_locked event is obsolete.
Sample:
--------------------------
event: system_startup_info
data: {"bitcoin": "offline", "bitcoin_msg": "", "lightning": "offline", "lightning_msg": "Unable to connect to LND daemon, waiting..."}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "offline", "lightning_msg": "Unable to connect to LND daemon, waiting..."}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "locked", "lightning_msg": "Wallet locked, unlock it to enable full RPC access"}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "done", "lightning_msg": ""}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "bootstraping", "lightning_msg": "RPC not yet available"}
--------------------------
refs #97
2022-06-06 19:29:21 +02:00
|
|
|
# Wait until the bitcoin node is ready to accept RPC calls
|
|
|
|
|
while not _initialized:
|
|
|
|
|
try:
|
|
|
|
|
await get_blockchain_info()
|
|
|
|
|
_initialized = True
|
2023-04-21 22:58:29 +02:00
|
|
|
logger.success("Bitcoin repository initialized")
|
refactor: improve startup procedure
During startup the API will try to connect to Bitcoin Core and the
Lightning Node. If it can't connect it will check every "n" seconds
(currently 2s) and connect when available. A new SSE event
called "system_startup_info" is introduced. This event contains
all startup status information during the startup procedure.
The old wallet_locked event is obsolete.
Sample:
--------------------------
event: system_startup_info
data: {"bitcoin": "offline", "bitcoin_msg": "", "lightning": "offline", "lightning_msg": "Unable to connect to LND daemon, waiting..."}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "offline", "lightning_msg": "Unable to connect to LND daemon, waiting..."}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "locked", "lightning_msg": "Wallet locked, unlock it to enable full RPC access"}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "done", "lightning_msg": ""}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "bootstraping", "lightning_msg": "RPC not yet available"}
--------------------------
refs #97
2022-06-06 19:29:21 +02:00
|
|
|
return True
|
|
|
|
|
except client_exceptions.ClientConnectorError:
|
2023-04-21 22:58:29 +02:00
|
|
|
logger.debug("Unable to connect to Bitcoin Core, waiting 5 seconds...")
|
|
|
|
|
await asyncio.sleep(5)
|
2022-06-26 11:06:56 +02:00
|
|
|
except HTTPException as e:
|
2023-04-21 22:58:29 +02:00
|
|
|
if e.status_code == status.HTTP_425_TOO_EARLY:
|
|
|
|
|
logger.info("Bitcoin Core initializing, waiting 10 seconds...")
|
|
|
|
|
await asyncio.sleep(10)
|
|
|
|
|
continue
|
|
|
|
|
|
2022-06-26 11:06:56 +02:00
|
|
|
if e.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
|
2023-04-21 22:58:29 +02:00
|
|
|
logger.error(e.detail)
|
|
|
|
|
|
|
|
|
|
logger.debug(
|
2023-05-17 16:02:28 +02:00
|
|
|
(
|
|
|
|
|
"Connected to Bitcoin Core but it seems to be "
|
|
|
|
|
f"initializing, waiting 2 seconds... \n{e.detail}"
|
|
|
|
|
)
|
2023-04-21 22:58:29 +02:00
|
|
|
)
|
refactor: improve startup procedure
During startup the API will try to connect to Bitcoin Core and the
Lightning Node. If it can't connect it will check every "n" seconds
(currently 2s) and connect when available. A new SSE event
called "system_startup_info" is introduced. This event contains
all startup status information during the startup procedure.
The old wallet_locked event is obsolete.
Sample:
--------------------------
event: system_startup_info
data: {"bitcoin": "offline", "bitcoin_msg": "", "lightning": "offline", "lightning_msg": "Unable to connect to LND daemon, waiting..."}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "offline", "lightning_msg": "Unable to connect to LND daemon, waiting..."}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "locked", "lightning_msg": "Wallet locked, unlock it to enable full RPC access"}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "done", "lightning_msg": ""}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "bootstraping", "lightning_msg": "RPC not yet available"}
--------------------------
refs #97
2022-06-06 19:29:21 +02:00
|
|
|
|
|
|
|
|
await asyncio.sleep(2)
|
|
|
|
|
|
2021-07-09 19:45:29 +02:00
|
|
|
|
2023-04-21 22:58:29 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2021-09-04 15:38:17 +02:00
|
|
|
async def get_blockchain_info() -> BlockchainInfo:
|
|
|
|
|
result = await bitcoin_rpc_async("getblockchaininfo")
|
2023-04-21 22:58:29 +02:00
|
|
|
|
2025-02-16 20:35:21 +01:00
|
|
|
if "error" in result and result["error"] is not None:
|
2023-04-21 22:58:29 +02:00
|
|
|
raise HTTPException(result["status"], detail=result["error"])
|
|
|
|
|
|
2021-09-04 15:38:17 +02:00
|
|
|
return BlockchainInfo.from_rpc(result["result"])
|
2021-07-09 19:45:29 +02:00
|
|
|
|
2021-09-05 08:56:53 +02:00
|
|
|
|
2023-04-21 22:58:29 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2021-12-06 18:38:01 +01:00
|
|
|
async def estimate_fee(
|
|
|
|
|
target_conf: int = 6,
|
|
|
|
|
mode: FeeEstimationMode = FeeEstimationMode.CONSERVATIVE,
|
|
|
|
|
) -> int:
|
|
|
|
|
result = await bitcoin_rpc_async("estimatesmartfee", [target_conf, mode])
|
2023-04-21 22:58:29 +02:00
|
|
|
|
2025-02-16 20:35:21 +01:00
|
|
|
if "error" in result and result["error"] is not None:
|
2023-04-21 22:58:29 +02:00
|
|
|
raise HTTPException(result["status"], detail=result["error"])
|
|
|
|
|
|
2021-12-06 18:38:01 +01:00
|
|
|
if "errors" in result["result"]:
|
|
|
|
|
errors = "Bitcoin Core returned error(s):\n"
|
|
|
|
|
for e in result["result"]["errors"]:
|
|
|
|
|
errors += f"{e}\n"
|
|
|
|
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR, detail=errors[0 : len(errors) - 1]
|
|
|
|
|
)
|
|
|
|
|
|
2022-06-04 14:46:03 +02:00
|
|
|
# returned in BTC by Bitcoin Core => convert to msat
|
2021-12-06 18:38:01 +01:00
|
|
|
rate_btc = result["result"]["feerate"]
|
2023-04-21 22:58:29 +02:00
|
|
|
|
2021-12-06 18:38:01 +01:00
|
|
|
return rate_btc * 100000000
|
|
|
|
|
|
|
|
|
|
|
2023-04-21 22:58:29 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2021-09-04 15:38:17 +02:00
|
|
|
async def get_network_info() -> NetworkInfo:
|
|
|
|
|
result = await bitcoin_rpc_async("getnetworkinfo")
|
2023-04-21 22:58:29 +02:00
|
|
|
|
2025-02-16 20:35:21 +01:00
|
|
|
if "error" in result and result["error"] is not None:
|
2023-04-21 22:58:29 +02:00
|
|
|
raise HTTPException(result["status"], detail=result["error"])
|
|
|
|
|
|
2021-09-04 15:38:17 +02:00
|
|
|
return NetworkInfo.from_rpc(result["result"])
|
2021-07-09 19:45:29 +02:00
|
|
|
|
2021-09-05 08:56:53 +02:00
|
|
|
|
2023-04-21 22:58:29 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2022-11-01 20:46:08 +01:00
|
|
|
async def get_raw_transaction(txid: str) -> RawTransaction:
|
|
|
|
|
result = await bitcoin_rpc_async("getrawtransaction", [txid, 1])
|
|
|
|
|
|
2025-02-16 20:35:21 +01:00
|
|
|
if "error" not in result or result["error"] is None:
|
2022-11-01 20:46:08 +01:00
|
|
|
return RawTransaction.from_rpc(result["result"])
|
|
|
|
|
|
|
|
|
|
if "No such mempool or blockchain transaction." in result["error"]:
|
|
|
|
|
raise HTTPException(status.HTTP_404_NOT_FOUND, detail=result["error"])
|
|
|
|
|
|
2024-02-24 07:59:44 +01:00
|
|
|
if "-txindex option" in result["error"]:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
detail="-txindex option for Bitcoin Core not enabled",
|
|
|
|
|
)
|
|
|
|
|
|
2022-11-01 20:46:08 +01:00
|
|
|
if "must be of length 64" in result["error"]:
|
|
|
|
|
raise HTTPException(status.HTTP_404_NOT_FOUND, detail=result["error"])
|
|
|
|
|
|
|
|
|
|
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail=result["error"])
|
|
|
|
|
|
|
|
|
|
|
2023-04-21 22:58:29 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2021-10-05 19:34:06 +02:00
|
|
|
async def get_btc_info() -> BtcInfo:
|
2021-09-04 15:38:17 +02:00
|
|
|
binfo = await get_blockchain_info()
|
|
|
|
|
ninfo = await get_network_info()
|
2023-04-21 22:58:29 +02:00
|
|
|
|
2024-02-24 07:59:44 +01:00
|
|
|
if binfo is None or ninfo is None:
|
|
|
|
|
logger.error(f"Blockchain Info or Network Info not available: {binfo} {ninfo}")
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
|
|
|
detail=(
|
|
|
|
|
"Unable to get blockchain or network data. "
|
|
|
|
|
"See the log files for more information"
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
2021-10-05 19:34:06 +02:00
|
|
|
return BtcInfo.from_rpc(binfo, ninfo)
|
2021-06-20 07:47:30 +02:00
|
|
|
|
|
|
|
|
|
2023-04-21 22:58:29 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2021-06-25 18:40:27 +02:00
|
|
|
async def handle_block_sub_redis(verbosity: int = 1) -> str:
|
|
|
|
|
ctx = zmq.asyncio.Context()
|
|
|
|
|
zmq_socket = ctx.socket(zmq.SUB)
|
|
|
|
|
zmq_socket.setsockopt(zmq.RCVHWM, 0)
|
2022-04-05 16:40:18 +02:00
|
|
|
zmq_socket.setsockopt_string(zmq.SUBSCRIBE, bitcoin_config.zmq_block_rpc)
|
2021-06-25 18:40:27 +02:00
|
|
|
zmq_socket.connect(bitcoin_config.zmq_url)
|
|
|
|
|
|
|
|
|
|
while True:
|
2022-04-05 16:40:18 +02:00
|
|
|
hash = ""
|
2021-06-25 18:40:27 +02:00
|
|
|
_, body, _ = await zmq_socket.recv_multipart()
|
2022-04-05 16:40:18 +02:00
|
|
|
if bitcoin_config.zmq_block_rpc == BlockRpcFunc.HASHBLOCK:
|
|
|
|
|
hash = binascii.hexlify(body).decode("utf-8")
|
|
|
|
|
elif bitcoin_config.zmq_block_rpc == BlockRpcFunc.RAWBLOCK:
|
|
|
|
|
r1 = await bitcoin_rpc_async("getbestblockhash", [])
|
2026-07-08 14:33:59 +02:00
|
|
|
if "result" not in r1 or r1.get("error") is not None:
|
|
|
|
|
logger.error(
|
|
|
|
|
f"getbestblockhash failed, skipping block: {r1.get('error')}"
|
|
|
|
|
)
|
|
|
|
|
continue
|
2022-04-05 16:40:18 +02:00
|
|
|
hash = r1["result"]
|
|
|
|
|
else:
|
|
|
|
|
raise NotImplementedError(
|
|
|
|
|
f"ZMQ block function {bitcoin_config.zmq_block_rpc} not supported"
|
|
|
|
|
)
|
|
|
|
|
|
2022-03-20 10:04:00 +01:00
|
|
|
r = await bitcoin_rpc_async("getblock", [hash, verbosity])
|
2026-07-08 14:33:59 +02:00
|
|
|
# strict JSON-RPC 2.0: an errored reply has no "result" key
|
|
|
|
|
if "result" not in r or r.get("error") is not None:
|
|
|
|
|
logger.error(f"getblock failed, skipping block: {r.get('error')}")
|
|
|
|
|
continue
|
|
|
|
|
|
2026-07-11 11:29:55 +02:00
|
|
|
await broadcast_msg(Event.BTC_NEW_BLOC, r["result"])
|
2021-06-25 18:40:27 +02:00
|
|
|
|
|
|
|
|
|
2023-04-21 22:58:29 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2021-06-25 18:40:27 +02:00
|
|
|
async def register_bitcoin_zmq_sub():
|
2026-07-03 22:34:44 +02:00
|
|
|
asyncio.create_task(handle_block_sub_redis())
|
2021-07-09 19:45:29 +02:00
|
|
|
|
|
|
|
|
|
2023-04-21 22:58:29 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2021-09-04 15:38:17 +02:00
|
|
|
async def _handle_gather_bitcoin_status():
|
2021-07-09 19:45:29 +02:00
|
|
|
last_info = {}
|
|
|
|
|
while True:
|
|
|
|
|
try:
|
2021-10-05 19:34:06 +02:00
|
|
|
info = await get_btc_info()
|
2023-05-17 16:02:28 +02:00
|
|
|
if info is None:
|
2023-04-21 22:58:29 +02:00
|
|
|
continue
|
|
|
|
|
|
2021-11-28 08:58:07 +01:00
|
|
|
info.verification_progress = round(info.verification_progress, 2)
|
2021-07-09 19:45:29 +02:00
|
|
|
except HTTPException as e:
|
2023-04-21 22:58:29 +02:00
|
|
|
logger.error(e.detail)
|
2021-07-09 19:45:29 +02:00
|
|
|
await asyncio.sleep(2)
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if last_info != info:
|
|
|
|
|
# only send data if anything has changed
|
2026-07-11 11:29:55 +02:00
|
|
|
await broadcast_msg(Event.BTC_INFO, info.model_dump())
|
2021-07-09 19:45:29 +02:00
|
|
|
last_info = info
|
|
|
|
|
|
|
|
|
|
await asyncio.sleep(2)
|
|
|
|
|
|
|
|
|
|
|
2023-04-21 22:58:29 +02:00
|
|
|
@logger.catch(exclude=(HTTPException,))
|
2021-09-04 15:38:17 +02:00
|
|
|
async def register_bitcoin_status_gatherer():
|
2026-07-03 22:34:44 +02:00
|
|
|
asyncio.create_task(_handle_gather_bitcoin_status())
|