From 9cbdb21c6556ba0c46e77c6d3209e71cf32ed3ce Mon Sep 17 00:00:00 2001 From: Keon Kim Date: Mon, 1 Jun 2026 23:32:07 +0900 Subject: [PATCH] fix: register explorer dependency routes --- node/rustchain_ergo_anchor.py | 31 ++++--- node/rustchain_v2_integrated_v2.2.1_rip200.py | 86 +++++++++++++++++++ node/tests/test_ergo_anchor_routes.py | 9 ++ node/tests/test_explorer_api_routes.py | 22 +++++ 4 files changed, 135 insertions(+), 13 deletions(-) diff --git a/node/rustchain_ergo_anchor.py b/node/rustchain_ergo_anchor.py index 3179625c0..5afaf827d 100644 --- a/node/rustchain_ergo_anchor.py +++ b/node/rustchain_ergo_anchor.py @@ -46,6 +46,22 @@ ANCHOR_WALLET_ADDRESS = os.environ.get("ANCHOR_WALLET", "") +def _ensure_anchor_table(cursor) -> None: + cursor.execute(""" + CREATE TABLE IF NOT EXISTS ergo_anchors ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + rustchain_height INTEGER NOT NULL, + rustchain_hash TEXT NOT NULL, + commitment_hash TEXT NOT NULL, + ergo_tx_id TEXT NOT NULL, + ergo_height INTEGER, + confirmations INTEGER DEFAULT 0, + status TEXT DEFAULT 'pending', + created_at INTEGER NOT NULL + ) + """) + + # ============================================================================= # ANCHOR COMMITMENT # ============================================================================= @@ -292,19 +308,7 @@ def get_last_anchor(self) -> Optional[Dict]: cursor = conn.cursor() # Ensure table exists - cursor.execute(""" - CREATE TABLE IF NOT EXISTS ergo_anchors ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - rustchain_height INTEGER NOT NULL, - rustchain_hash TEXT NOT NULL, - commitment_hash TEXT NOT NULL, - ergo_tx_id TEXT NOT NULL, - ergo_height INTEGER, - confirmations INTEGER DEFAULT 0, - status TEXT DEFAULT 'pending', - created_at INTEGER NOT NULL - ) - """) + _ensure_anchor_table(cursor) cursor.execute(""" SELECT * FROM ergo_anchors @@ -546,6 +550,7 @@ def list_anchors(): try: conn.row_factory = sqlite3.Row cursor = conn.cursor() + _ensure_anchor_table(cursor) cursor.execute(""" SELECT * FROM ergo_anchors diff --git a/node/rustchain_v2_integrated_v2.2.1_rip200.py b/node/rustchain_v2_integrated_v2.2.1_rip200.py index 2ea510466..8a4a81283 100644 --- a/node/rustchain_v2_integrated_v2.2.1_rip200.py +++ b/node/rustchain_v2_integrated_v2.2.1_rip200.py @@ -1391,6 +1391,92 @@ def ensure_epoch_enroll_integer_weights(conn: sqlite3.Connection): except Exception as e: print(f"[RIP-0305 Track C] Failed to register bridge endpoints: {e}") +# RIP-302 Agent Economy endpoints used by the explorer dashboard +try: + if REPO_ROOT not in sys.path: + sys.path.insert(0, REPO_ROOT) + from rip302_agent_economy import register_agent_economy + + register_agent_economy(app, DB_PATH) + print("[RIP-302] Agent Economy endpoints registered") +except ImportError as e: + print(f"[RIP-302] Agent Economy module not available: {e}") +except Exception as e: + print(f"[RIP-302] Failed to register Agent Economy endpoints: {e}") + +# Ergo anchor transparency endpoints used by explorer/navigation links. +_ANCHOR_ROUTES_REGISTERED = False +try: + from rustchain_ergo_anchor import AnchorService, create_anchor_api_routes + + create_anchor_api_routes(app, AnchorService(DB_PATH)) + _ANCHOR_ROUTES_REGISTERED = True + print("[ANCHOR] Ergo anchor read-only endpoints registered") +except ImportError as e: + print(f"[ANCHOR] Ergo anchor module not available: {e}") +except Exception as e: + print(f"[ANCHOR] Failed to register Ergo anchor endpoints: {e}") + + +if not _ANCHOR_ROUTES_REGISTERED: + def _fallback_anchor_int_arg(name, default, minimum, maximum=None): + raw_value = request.args.get(name) + if raw_value is None or raw_value == "": + return default, None + try: + value = int(raw_value) + except (TypeError, ValueError): + return None, f"{name}_must_be_integer" + if value < minimum: + return None, f"{name}_must_be_at_least_{minimum}" + if maximum is not None: + value = min(value, maximum) + return value, None + + def _ensure_fallback_anchor_table(cursor): + cursor.execute(""" + CREATE TABLE IF NOT EXISTS ergo_anchors ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + rustchain_height INTEGER NOT NULL, + rustchain_hash TEXT NOT NULL, + commitment_hash TEXT NOT NULL, + ergo_tx_id TEXT NOT NULL, + ergo_height INTEGER, + confirmations INTEGER DEFAULT 0, + status TEXT DEFAULT 'pending', + created_at INTEGER NOT NULL + ) + """) + + @app.route("/anchor/list", methods=["GET"]) + def fallback_anchor_list(): + limit, error = _fallback_anchor_int_arg("limit", 50, 1, 100) + if error: + return jsonify({"error": error}), 400 + offset, error = _fallback_anchor_int_arg("offset", 0, 0, 10_000) + if error: + return jsonify({"error": error}), 400 + + with sqlite3.connect(DB_PATH) as conn: + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + _ensure_fallback_anchor_table(cursor) + rows = cursor.execute(""" + SELECT * FROM ergo_anchors + ORDER BY rustchain_height DESC + LIMIT ? OFFSET ? + """, (limit, offset)).fetchall() + + return jsonify({ + "count": len(rows), + "anchors": [dict(row) for row in rows], + }) + + +@app.route("/anchors", methods=["GET"]) +def anchors_alias(): + return redirect("/anchor/list", code=302) + # BoTTube RSS/Atom Feed endpoints (Issue #759) if HAVE_BOTTUBE_FEED: try: diff --git a/node/tests/test_ergo_anchor_routes.py b/node/tests/test_ergo_anchor_routes.py index 92cbac2b1..5359167e4 100644 --- a/node/tests/test_ergo_anchor_routes.py +++ b/node/tests/test_ergo_anchor_routes.py @@ -127,3 +127,12 @@ def test_anchor_list_clamps_oversized_limit(): assert response.status_code == 200 assert response.get_json()["count"] == 3 + + +def test_anchor_list_returns_empty_when_table_missing(): + with tempfile.TemporaryDirectory() as tmpdir: + db_path = Path(tmpdir) / "anchors.db" + response = _client(db_path).get("/anchor/list") + + assert response.status_code == 200 + assert response.get_json() == {"count": 0, "anchors": []} diff --git a/node/tests/test_explorer_api_routes.py b/node/tests/test_explorer_api_routes.py index 3c6f020e8..1bbeb4ac2 100644 --- a/node/tests/test_explorer_api_routes.py +++ b/node/tests/test_explorer_api_routes.py @@ -18,6 +18,7 @@ def setUpClass(cls): cls._tmp = tempfile.TemporaryDirectory(ignore_cleanup_errors=True) cls._prev_db_path = os.environ.get("RUSTCHAIN_DB_PATH") cls._prev_admin_key = os.environ.get("RC_ADMIN_KEY") + cls._prev_rustchain_crypto = sys.modules.pop("rustchain_crypto", None) os.environ["RUSTCHAIN_DB_PATH"] = os.path.join(cls._tmp.name, "import.db") os.environ["RC_ADMIN_KEY"] = "0123456789abcdef0123456789abcdef" @@ -36,6 +37,8 @@ def tearDownClass(cls): os.environ.pop("RC_ADMIN_KEY", None) else: os.environ["RC_ADMIN_KEY"] = cls._prev_admin_key + if cls._prev_rustchain_crypto is not None: + sys.modules["rustchain_crypto"] = cls._prev_rustchain_crypto cls._tmp.cleanup() def setUp(self): @@ -210,6 +213,25 @@ def test_explorer_endpoints_default_empty_pagination_values(self): self.assertEqual(tx_resp.status_code, 200) self.assertEqual(tx_resp.get_json(), {"ok": True, "transactions": [], "count": 0, "total": 0}) + def test_explorer_dependencies_register_agent_and_anchor_routes(self): + stats = self.client.get("/agent/stats") + self.assertEqual(stats.status_code, 200) + self.assertTrue(stats.get_json()["ok"]) + + jobs = self.client.get("/agent/jobs?status=open&limit=1") + self.assertEqual(jobs.status_code, 200) + jobs_body = jobs.get_json() + self.assertTrue(jobs_body["ok"]) + self.assertEqual(jobs_body["jobs"], []) + + anchors = self.client.get("/anchors", follow_redirects=False) + self.assertEqual(anchors.status_code, 302) + self.assertEqual(anchors.headers["Location"], "/anchor/list") + + anchor_list = self.client.get("/anchor/list") + self.assertEqual(anchor_list.status_code, 200) + self.assertEqual(anchor_list.get_json(), {"count": 0, "anchors": []}) + def test_explorer_endpoints_reject_invalid_pagination(self): blocks_resp = self.client.get("/api/blocks?limit=bad") tx_resp = self.client.get("/api/transactions?offset=bad")