Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 18 additions & 13 deletions node/rustchain_ergo_anchor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# =============================================================================
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -546,6 +550,7 @@ def list_anchors():
try:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
_ensure_anchor_table(cursor)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good extraction here: using _ensure_anchor_table(cursor) before the list query keeps /anchor/list aligned with get_last_anchor() on empty deployments. That matters for the explorer fix because a fresh DB should render an empty anchor list instead of surfacing a route-level error to first-time users.


cursor.execute("""
SELECT * FROM ergo_anchors
Expand Down
86 changes: 86 additions & 0 deletions node/rustchain_v2_integrated_v2.2.1_rip200.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fallback branch is now part of the explorer's availability guarantee, but the added integrated-app test only exercises it if rustchain_ergo_anchor happens not to import in the test environment. Since the module normally imports, the fallback query parsing/table-creation path can regress without failing CI. Consider adding a focused test that forces the optional anchor import/registration to fail and then asserts /anchor/list still returns 200 with the empty payload plus the expected 400 responses for invalid limit/offset.

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:
Expand Down
9 changes: 9 additions & 0 deletions node/tests/test_ergo_anchor_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": []}
22 changes: 22 additions & 0 deletions node/tests/test_explorer_api_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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):
Expand Down Expand Up @@ -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")
Expand Down
Loading