3434 DEFAULT_RPC_TIMEOUT_SECONDS ,
3535 default_home_for_backend ,
3636 fetch_json ,
37+ get_xian_stack_node_endpoints ,
3738 get_xian_stack_node_status ,
3839 resolve_stack_dir ,
3940 start_xian_stack_node ,
@@ -1180,11 +1181,7 @@ def _handle_node_stop(args: argparse.Namespace) -> int:
11801181 return 0
11811182
11821183
1183- def _collect_node_status (
1184- args : argparse .Namespace ,
1185- * ,
1186- check_rpc : bool ,
1187- ) -> dict :
1184+ def _resolve_node_context (args : argparse .Namespace ) -> dict [str , object ]:
11881185 base_dir = args .base_dir .resolve ()
11891186 profile_path , profile , network_path , network = _load_profile_and_network (
11901187 base_dir = base_dir ,
@@ -1211,6 +1208,180 @@ def _collect_node_status(
12111208 stack_dir = stack_dir ,
12121209 explicit_home = getattr (args , "home" , None ),
12131210 )
1211+ return {
1212+ "base_dir" : base_dir ,
1213+ "profile_path" : profile_path ,
1214+ "profile" : profile ,
1215+ "network_path" : network_path ,
1216+ "network" : network ,
1217+ "runtime_backend" : runtime_backend ,
1218+ "stack_dir" : stack_dir ,
1219+ "home" : home ,
1220+ }
1221+
1222+
1223+ def _format_url_host (hostname : str ) -> str :
1224+ if ":" in hostname and not hostname .startswith ("[" ):
1225+ return f"[{ hostname } ]"
1226+ return hostname
1227+
1228+
1229+ def _replace_url_port (url : str , * , port : int , suffix : str = "" ) -> str :
1230+ parsed = urlparse (url )
1231+ scheme = parsed .scheme or "http"
1232+ hostname = _format_url_host (parsed .hostname or "127.0.0.1" )
1233+ return f"{ scheme } ://{ hostname } :{ port } { suffix } "
1234+
1235+
1236+ def _rpc_base_url (rpc_status_url : str ) -> str :
1237+ if rpc_status_url .endswith ("/status" ):
1238+ return rpc_status_url [: - len ("/status" )]
1239+ return rpc_status_url .rstrip ("/" )
1240+
1241+
1242+ def _fallback_node_endpoints (
1243+ * ,
1244+ rpc_status_url : str ,
1245+ profile : NodeProfile ,
1246+ ) -> dict [str , str ]:
1247+ base_url = _rpc_base_url (rpc_status_url )
1248+ endpoints = {
1249+ "rpc" : base_url ,
1250+ "rpc_status" : rpc_status_url ,
1251+ "abci_query" : f"{ base_url } /abci_query" ,
1252+ "cometbft_metrics" : _replace_url_port (
1253+ base_url ,
1254+ port = 26660 ,
1255+ suffix = "/metrics" ,
1256+ ),
1257+ "xian_metrics" : _replace_url_port (
1258+ base_url ,
1259+ port = 9108 ,
1260+ suffix = "/metrics" ,
1261+ ),
1262+ }
1263+ if bool (profile .get ("dashboard_enabled" )):
1264+ dashboard_host = str (profile .get ("dashboard_host" , "127.0.0.1" ))
1265+ dashboard_port = int (profile .get ("dashboard_port" , 8080 ))
1266+ dashboard_url = f"http://{ dashboard_host } :{ dashboard_port } "
1267+ endpoints ["dashboard" ] = dashboard_url
1268+ endpoints ["dashboard_status" ] = f"{ dashboard_url } /api/status"
1269+ if bool (profile .get ("monitoring_enabled" )):
1270+ endpoints ["prometheus" ] = _replace_url_port (base_url , port = 9090 )
1271+ endpoints ["grafana" ] = _replace_url_port (base_url , port = 3000 )
1272+ return endpoints
1273+
1274+
1275+ def _collect_node_endpoints (args : argparse .Namespace ) -> dict [str , object ]:
1276+ context = _resolve_node_context (args )
1277+ profile = context ["profile" ]
1278+ runtime_backend = context ["runtime_backend" ]
1279+ stack_dir = context ["stack_dir" ]
1280+ rpc_status_url = getattr (args , "rpc_url" , "http://127.0.0.1:26657/status" )
1281+
1282+ payload : dict [str , object ] = {
1283+ "profile_path" : str (context ["profile_path" ]),
1284+ "network_path" : str (context ["network_path" ]),
1285+ "runtime_backend" : runtime_backend ,
1286+ "service_node" : bool (profile .get ("service_node" )),
1287+ "dashboard_enabled" : bool (profile .get ("dashboard_enabled" )),
1288+ "monitoring_enabled" : bool (profile .get ("monitoring_enabled" )),
1289+ }
1290+ if stack_dir is not None :
1291+ payload ["stack_dir" ] = str (stack_dir )
1292+
1293+ if runtime_backend == "xian-stack" and stack_dir is not None :
1294+ try :
1295+ backend_payload = get_xian_stack_node_endpoints (
1296+ stack_dir = stack_dir ,
1297+ service_node = bool (profile .get ("service_node" )),
1298+ dashboard_enabled = bool (profile .get ("dashboard_enabled" )),
1299+ monitoring_enabled = bool (profile .get ("monitoring_enabled" )),
1300+ dashboard_host = str (profile .get ("dashboard_host" , "127.0.0.1" )),
1301+ dashboard_port = int (profile .get ("dashboard_port" , 8080 )),
1302+ )
1303+ payload ["endpoints" ] = backend_payload ["endpoints" ]
1304+ payload ["backend_checked" ] = True
1305+ return payload
1306+ except Exception as exc :
1307+ payload ["backend_checked" ] = True
1308+ payload ["backend_error" ] = str (exc )
1309+
1310+ payload ["endpoints" ] = _fallback_node_endpoints (
1311+ rpc_status_url = rpc_status_url ,
1312+ profile = profile ,
1313+ )
1314+ return payload
1315+
1316+
1317+ def _summarize_node_status (result : dict [str , object ]) -> dict [str , object ]:
1318+ rpc_payload = result .get ("rpc_status" )
1319+ rpc_result = (
1320+ rpc_payload .get ("result" , {}) if isinstance (rpc_payload , dict ) else {}
1321+ )
1322+ sync_info = (
1323+ rpc_result .get ("sync_info" , {}) if isinstance (rpc_result , dict ) else {}
1324+ )
1325+ node_info = (
1326+ rpc_result .get ("node_info" , {}) if isinstance (rpc_result , dict ) else {}
1327+ )
1328+ other = node_info .get ("other" , {}) if isinstance (node_info , dict ) else {}
1329+
1330+ state = "ready"
1331+ if not result .get ("initialized" ):
1332+ state = "not_initialized"
1333+ elif result .get ("backend_checked" ) and not result .get ("backend_running" ):
1334+ state = "stopped"
1335+ elif result .get ("rpc_checked" ) and not result .get ("rpc_reachable" ):
1336+ state = "rpc_unreachable"
1337+
1338+ summary : dict [str , object ] = {
1339+ "state" : state ,
1340+ "initialized" : bool (result .get ("initialized" )),
1341+ "service_node" : bool (result .get ("profile" , {}).get ("service_node" )),
1342+ "dashboard_enabled" : bool (
1343+ result .get ("profile" , {}).get ("dashboard_enabled" )
1344+ ),
1345+ "monitoring_enabled" : bool (
1346+ result .get ("profile" , {}).get ("monitoring_enabled" )
1347+ ),
1348+ "backend_running" : result .get ("backend_running" ),
1349+ "rpc_reachable" : result .get ("rpc_reachable" ),
1350+ "rpc_height" : sync_info .get ("latest_block_height" ),
1351+ "rpc_catching_up" : sync_info .get ("catching_up" ),
1352+ "rpc_network" : node_info .get ("network" ),
1353+ "peer_count" : other .get ("n_peers" ),
1354+ }
1355+
1356+ backend_status = result .get ("backend_status" )
1357+ if isinstance (backend_status , dict ):
1358+ if summary ["dashboard_enabled" ]:
1359+ summary ["dashboard_reachable" ] = backend_status .get (
1360+ "dashboard_reachable"
1361+ )
1362+ if summary ["monitoring_enabled" ]:
1363+ summary ["prometheus_reachable" ] = backend_status .get (
1364+ "prometheus_reachable"
1365+ )
1366+ summary ["grafana_reachable" ] = backend_status .get (
1367+ "grafana_reachable"
1368+ )
1369+ return summary
1370+
1371+
1372+ def _collect_node_status (
1373+ args : argparse .Namespace ,
1374+ * ,
1375+ check_rpc : bool ,
1376+ ) -> dict :
1377+ context = _resolve_node_context (args )
1378+ profile_path = context ["profile_path" ]
1379+ profile = context ["profile" ]
1380+ network_path = context ["network_path" ]
1381+ network = context ["network" ]
1382+ runtime_backend = context ["runtime_backend" ]
1383+ stack_dir = context ["stack_dir" ]
1384+ home = context ["home" ]
12141385 config_path = home / "config" / "config.toml"
12151386 genesis_path = home / "config" / "genesis.json"
12161387 node_key_path = home / "config" / "node_key.json"
@@ -1234,6 +1405,13 @@ def _collect_node_status(
12341405 network = network ,
12351406 ),
12361407 "rpc_checked" : check_rpc ,
1408+ "profile" : {
1409+ "name" : args .name ,
1410+ "network" : profile .get ("network" ),
1411+ "service_node" : bool (profile .get ("service_node" )),
1412+ "dashboard_enabled" : bool (profile .get ("dashboard_enabled" )),
1413+ "monitoring_enabled" : bool (profile .get ("monitoring_enabled" )),
1414+ },
12371415 }
12381416 if stack_dir is not None :
12391417 result ["stack_dir" ] = str (stack_dir )
@@ -1271,6 +1449,17 @@ def _collect_node_status(
12711449 result ["rpc_reachable" ] = False
12721450 result ["rpc_error" ] = str (exc )
12731451
1452+ if isinstance (result .get ("backend_status" ), dict ) and isinstance (
1453+ result ["backend_status" ].get ("endpoints" ), dict
1454+ ):
1455+ result ["endpoints" ] = result ["backend_status" ]["endpoints" ]
1456+ else :
1457+ result ["endpoints" ] = _fallback_node_endpoints (
1458+ rpc_status_url = args .rpc_url ,
1459+ profile = profile ,
1460+ )
1461+
1462+ result ["summary" ] = _summarize_node_status (result )
12741463 return result
12751464
12761465
@@ -1280,6 +1469,12 @@ def _handle_node_status(args: argparse.Namespace) -> int:
12801469 return 0
12811470
12821471
1472+ def _handle_node_endpoints (args : argparse .Namespace ) -> int :
1473+ result = _collect_node_endpoints (args )
1474+ print (json .dumps (result , indent = 2 ))
1475+ return 0
1476+
1477+
12831478def _handle_snapshot_restore (args : argparse .Namespace ) -> int :
12841479 base_dir = args .base_dir .resolve ()
12851480 profile_path , profile , _ , network = _load_profile_and_network (
@@ -2106,6 +2301,65 @@ def build_parser() -> argparse.ArgumentParser:
21062301 )
21072302 status_parser .set_defaults (handler = _handle_node_status )
21082303
2304+ endpoints_parser = node_subparsers .add_parser (
2305+ "endpoints" ,
2306+ help = (
2307+ "print the expected local URLs for RPC, metrics, dashboard, "
2308+ "and monitoring"
2309+ ),
2310+ )
2311+ endpoints_parser .add_argument ("name" , help = "node profile name" )
2312+ endpoints_parser .add_argument (
2313+ "--base-dir" ,
2314+ type = Path ,
2315+ default = Path .cwd (),
2316+ help = (
2317+ "base directory containing ./nodes, ./networks, ./keys, "
2318+ "and optionally sibling repos"
2319+ ),
2320+ )
2321+ endpoints_parser .add_argument (
2322+ "--profile" ,
2323+ type = Path ,
2324+ help = "explicit node profile path; defaults to ./nodes/<name>.json" ,
2325+ )
2326+ endpoints_parser .add_argument (
2327+ "--network" ,
2328+ type = Path ,
2329+ help = (
2330+ "explicit network manifest path; defaults to "
2331+ "./networks/<profile.network>/manifest.json"
2332+ ),
2333+ )
2334+ endpoints_parser .add_argument (
2335+ "--stack-dir" ,
2336+ type = Path ,
2337+ help = (
2338+ "explicit xian-stack checkout path when using the xian-stack "
2339+ "backend; "
2340+ "overrides stack_dir in the profile"
2341+ ),
2342+ )
2343+ endpoints_parser .add_argument (
2344+ "--configs-dir" ,
2345+ type = Path ,
2346+ help = (
2347+ "explicit xian-configs checkout path for canonical manifests "
2348+ "or the sibling workspace layout"
2349+ ),
2350+ )
2351+ endpoints_parser .add_argument (
2352+ "--home" ,
2353+ type = Path ,
2354+ help = "explicit CometBFT home path; overrides the profile home" ,
2355+ )
2356+ endpoints_parser .add_argument (
2357+ "--rpc-url" ,
2358+ default = "http://127.0.0.1:26657/status" ,
2359+ help = "RPC status endpoint used to derive default host/port URLs" ,
2360+ )
2361+ endpoints_parser .set_defaults (handler = _handle_node_endpoints )
2362+
21092363 snapshot_parser = subparsers .add_parser ("snapshot" , help = "snapshot tools" )
21102364 snapshot_subparsers = snapshot_parser .add_subparsers (
21112365 dest = "snapshot_command" , required = True
0 commit comments