diff --git a/.github/workflows/go-lint.yml b/.github/workflows/go-lint.yml
deleted file mode 100644
index 015b358b8..000000000
--- a/.github/workflows/go-lint.yml
+++ /dev/null
@@ -1,45 +0,0 @@
-name: Run Go Lint
-
-on:
- push:
- branches:
- - 'main'
- pull_request:
- branches:
- - '**'
-jobs:
- lint:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Set up Go
- uses: actions/setup-go@v5
- with:
- go-version: '1.25'
- cache-dependency-path: tunnel/go.sum
-
- - name: Cache Go modules
- uses: actions/cache@v3
- with:
- path: |
- ~/.cache/go-build
- ~/go/pkg/mod
- key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
- restore-keys: |
- ${{ runner.os }}-go-
-
- - name: Install dependencies
- working-directory: tunnel
- run: go mod download
-
- - name: Install golangci-lint
- run: |
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.7.2
-
- - name: Add golangci-lint to PATH
- run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
-
- - name: Run make lint
- run: make lint
diff --git a/.github/workflows/go-unitest.yml b/.github/workflows/go-unitest.yml
deleted file mode 100644
index 7b2ffafc9..000000000
--- a/.github/workflows/go-unitest.yml
+++ /dev/null
@@ -1,48 +0,0 @@
-name: Run Go Unit Tests
-
-on:
- push:
- branches:
- - 'main'
- pull_request:
- branches:
- - '**'
-
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Set up Go
- uses: actions/setup-go@v5
- with:
- go-version: '1.25'
- cache-dependency-path: tunnel/go.sum
-
- - name: Cache Go modules
- uses: actions/cache@v3
- with:
- path: |
- ~/.cache/go-build
- ~/go/pkg/mod
- key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
- restore-keys: |
- ${{ runner.os }}-go-
-
- - name: Install dependencies
- working-directory: tunnel
- run: go mod download
-
- - name: Run make test
- run: make test
-
- - name: Run unit tests with coverage
- run: make test-coverage
-
- - name: Upload test coverage
- uses: actions/upload-artifact@v4
- with:
- name: test-coverage
- path: tunnel/coverage.out
diff --git a/bridge/common/zenoh_bridge/package.xml b/bridge/common/zenoh_bridge/package.xml
deleted file mode 100644
index 665c4d3db..000000000
--- a/bridge/common/zenoh_bridge/package.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
- zenoh_bridge
- 0.1.0
- Common Zenoh and Fabric action event utilities shared across all robot adapters
- Maintainer
- Apache-2.0
- rclpy
- geometry_msgs
-
- ament_python
-
-
diff --git a/bridge/common/zenoh_bridge/resource/zenoh_bridge b/bridge/common/zenoh_bridge/resource/zenoh_bridge
deleted file mode 100644
index e69de29bb..000000000
diff --git a/bridge/common/zenoh_bridge/setup.cfg b/bridge/common/zenoh_bridge/setup.cfg
deleted file mode 100644
index 9278c4623..000000000
--- a/bridge/common/zenoh_bridge/setup.cfg
+++ /dev/null
@@ -1,5 +0,0 @@
-[develop]
-script_dir=$base/lib/zenoh_bridge
-
-[install]
-install_scripts=$base/lib/zenoh_bridge
diff --git a/bridge/common/zenoh_bridge/setup.py b/bridge/common/zenoh_bridge/setup.py
deleted file mode 100644
index 1db70d105..000000000
--- a/bridge/common/zenoh_bridge/setup.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from setuptools import setup, find_packages
-
-package_name = "zenoh_bridge"
-
-setup(
- name=package_name,
- version="0.1.0",
- packages=find_packages(exclude=["test"]),
- data_files=[
- ("share/ament_index/resource_index/packages", [f"resource/{package_name}"]),
- (f"share/{package_name}", ["package.xml"]),
- ],
- install_requires=["setuptools", "eclipse-zenoh"],
- zip_safe=True,
- entry_points={"console_scripts": []},
-)
diff --git a/bridge/common/zenoh_bridge/zenoh_bridge/__init__.py b/bridge/common/zenoh_bridge/zenoh_bridge/__init__.py
deleted file mode 100644
index 0ffc71a56..000000000
--- a/bridge/common/zenoh_bridge/zenoh_bridge/__init__.py
+++ /dev/null
@@ -1,12 +0,0 @@
-"""Common Zenoh and Fabric action event utilities."""
-from .action_event import ActionEvent, parse_action_event
-from .zenoh_subscriber import ZenohSubscriberHelper
-from .command_mapper import CommandMapper
-from .utils import clamp
-
-__all__ = [
- "ActionEvent", "parse_action_event",
- "ZenohSubscriberHelper",
- "CommandMapper",
- "clamp",
-]
diff --git a/bridge/common/zenoh_bridge/zenoh_bridge/action_event.py b/bridge/common/zenoh_bridge/zenoh_bridge/action_event.py
deleted file mode 100644
index 09ee7a9e7..000000000
--- a/bridge/common/zenoh_bridge/zenoh_bridge/action_event.py
+++ /dev/null
@@ -1,40 +0,0 @@
-"""Parse Fabric tunnel Action Event payloads."""
-import json
-from dataclasses import dataclass, field
-from typing import Any, Dict, Optional
-
-
-@dataclass
-class ActionEvent:
- action: str
- params: Dict[str, Any] = field(default_factory=dict)
- timestamp: str = ""
-
-
-def parse_action_event(raw: bytes) -> Optional[ActionEvent]:
- """Parse a Fabric Action Event from raw bytes.
-
- Expected schema (tunnel handlers.go:97-104)::
-
- {
- "payload": {"action": "move_forward", "params": {"speed": 0.5}},
- "transaction_details": {...},
- "timestamp": "2026-01-01T00:00:00Z"
- }
-
- Returns None on parse failure.
- """
- try:
- event = json.loads(raw)
- except (json.JSONDecodeError, UnicodeDecodeError):
- return None
-
- payload = event.get("payload") or {}
- if not isinstance(payload, dict):
- return None
-
- return ActionEvent(
- action=payload.get("action", "stop"),
- params=payload.get("params") or {},
- timestamp=event.get("timestamp", ""),
- )
diff --git a/bridge/common/zenoh_bridge/zenoh_bridge/command_mapper.py b/bridge/common/zenoh_bridge/zenoh_bridge/command_mapper.py
deleted file mode 100644
index 5ede638f5..000000000
--- a/bridge/common/zenoh_bridge/zenoh_bridge/command_mapper.py
+++ /dev/null
@@ -1,14 +0,0 @@
-"""Base class for robot-specific action → command mapping."""
-from abc import ABC, abstractmethod
-
-from geometry_msgs.msg import Twist
-
-from .action_event import ActionEvent
-
-
-class CommandMapper(ABC):
- """Subclass this for each robot model."""
-
- @abstractmethod
- def map(self, event: ActionEvent) -> Twist:
- """Map an ActionEvent to a Twist command."""
diff --git a/bridge/common/zenoh_bridge/zenoh_bridge/utils.py b/bridge/common/zenoh_bridge/zenoh_bridge/utils.py
deleted file mode 100644
index 7f8560e38..000000000
--- a/bridge/common/zenoh_bridge/zenoh_bridge/utils.py
+++ /dev/null
@@ -1,5 +0,0 @@
-"""Utility functions shared across all adapters."""
-
-
-def clamp(value: float, lo: float, hi: float) -> float:
- return max(lo, min(hi, value))
diff --git a/bridge/common/zenoh_bridge/zenoh_bridge/zenoh_subscriber.py b/bridge/common/zenoh_bridge/zenoh_bridge/zenoh_subscriber.py
deleted file mode 100644
index 661e75de4..000000000
--- a/bridge/common/zenoh_bridge/zenoh_bridge/zenoh_subscriber.py
+++ /dev/null
@@ -1,24 +0,0 @@
-"""Zenoh session and subscriber helper."""
-from typing import Callable, List
-import zenoh
-
-
-class ZenohSubscriberHelper:
- """Manages a Zenoh session with one or more topic subscriptions."""
-
- def __init__(self, listen_endpoint: str = "tcp/127.0.0.1:7447"):
- conf = zenoh.Config.from_json5(
- f'{{"listen":{{"endpoints":["{listen_endpoint}"]}}}}'
- )
- self._session = zenoh.open(conf)
- self._subs: List = []
-
- def subscribe(self, topic: str, callback: Callable) -> None:
- """Subscribe to a Zenoh topic with the given callback."""
- sub = self._session.declare_subscriber(topic, callback)
- self._subs.append(sub)
-
- def close(self) -> None:
- for s in self._subs:
- s.undeclare()
- self._session.close()
diff --git a/bridge/unitree/g1/isaac_sim_bridge/config/default.yaml b/bridge/unitree/g1/isaac_sim_bridge/config/default.yaml
deleted file mode 100644
index 6f972fd16..000000000
--- a/bridge/unitree/g1/isaac_sim_bridge/config/default.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-isaac_sim_bridge_g1:
- ros__parameters:
- zenoh_topic: "robot/tunnel/action"
- zenoh_listen: "tcp/127.0.0.1:7447"
- cmd_vel_topic: "/cmd_vel"
- forward_speed: 0.5
- backward_speed: 0.5
- turn_linear_speed: 0.3
- turn_angular_speed: 0.2
diff --git a/bridge/unitree/g1/isaac_sim_bridge/g1/__init__.py b/bridge/unitree/g1/isaac_sim_bridge/g1/__init__.py
deleted file mode 100644
index c458baa9e..000000000
--- a/bridge/unitree/g1/isaac_sim_bridge/g1/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from .node import main
-from .mapper import G1Mapper
-
-__all__ = ["main", "G1Mapper"]
diff --git a/bridge/unitree/g1/isaac_sim_bridge/g1/mapper.py b/bridge/unitree/g1/isaac_sim_bridge/g1/mapper.py
deleted file mode 100644
index 3d587749a..000000000
--- a/bridge/unitree/g1/isaac_sim_bridge/g1/mapper.py
+++ /dev/null
@@ -1,41 +0,0 @@
-"""G1-specific Fabric action → geometry_msgs/Twist mapper."""
-from geometry_msgs.msg import Twist
-from zenoh_bridge import ActionEvent, CommandMapper, clamp
-
-
-class G1Mapper(CommandMapper):
- """Maps Fabric actions to Twist commands for Unitree G1 / OM1-sim.
-
- Velocity limits from OM1-sim deploy.yaml:
- vx: [-0.5, 1.0] m/s wz: [-0.2, 0.2] rad/s
- """
-
- def __init__(
- self,
- forward_speed: float = 0.5,
- backward_speed: float = 0.5,
- turn_linear_speed: float = 0.3,
- turn_angular_speed: float = 0.2,
- ):
- self._fwd = forward_speed
- self._bwd = backward_speed
- self._turn_lin = turn_linear_speed
- self._turn_ang = turn_angular_speed
-
- def map(self, event: ActionEvent) -> Twist:
- msg = Twist()
- a = event.action
- if a in ("move_forward", "forward"):
- msg.linear.x = clamp(self._fwd, 0.0, 1.0)
- elif a in ("move_backward", "backward"):
- msg.linear.x = -clamp(self._bwd, 0.0, 0.5)
- elif a == "turn_left":
- msg.linear.x = self._turn_lin
- msg.angular.z = clamp(self._turn_ang, 0.0, 0.2)
- elif a == "turn_right":
- msg.linear.x = self._turn_lin
- msg.angular.z = -clamp(self._turn_ang, 0.0, 0.2)
- elif a == "stop":
- pass # zero Twist
- # unknown action → zero Twist (safe default)
- return msg
diff --git a/bridge/unitree/g1/isaac_sim_bridge/g1/node.py b/bridge/unitree/g1/isaac_sim_bridge/g1/node.py
deleted file mode 100644
index 21ac5fc6d..000000000
--- a/bridge/unitree/g1/isaac_sim_bridge/g1/node.py
+++ /dev/null
@@ -1,68 +0,0 @@
-"""ROS2 node for Fabric → Unitree G1 (OM1-sim) adapter."""
-import rclpy
-from rclpy.node import Node
-from geometry_msgs.msg import Twist
-
-from zenoh_bridge import parse_action_event, ZenohSubscriberHelper
-from .mapper import G1Mapper
-
-
-class IsaacSimG1BridgeNode(Node):
- def __init__(self):
- super().__init__("isaac_sim_bridge_g1")
-
- self.declare_parameter("zenoh_topic", "robot/tunnel/action")
- self.declare_parameter("zenoh_listen", "tcp/127.0.0.1:7447")
- self.declare_parameter("cmd_vel_topic", "/cmd_vel")
- self.declare_parameter("forward_speed", 0.5)
- self.declare_parameter("backward_speed", 0.5)
- self.declare_parameter("turn_linear_speed", 0.3)
- self.declare_parameter("turn_angular_speed", 0.2)
-
- p = self.get_parameter
- zenoh_topic = p("zenoh_topic").get_parameter_value().string_value
- zenoh_listen = p("zenoh_listen").get_parameter_value().string_value
- cmd_vel_topic = p("cmd_vel_topic").get_parameter_value().string_value
-
- self._mapper = G1Mapper(
- forward_speed = p("forward_speed").get_parameter_value().double_value,
- backward_speed = p("backward_speed").get_parameter_value().double_value,
- turn_linear_speed = p("turn_linear_speed").get_parameter_value().double_value,
- turn_angular_speed = p("turn_angular_speed").get_parameter_value().double_value,
- )
- self._pub = self.create_publisher(Twist, cmd_vel_topic, 10)
- self.get_logger().info(f"Adapter started, publishing to {cmd_vel_topic}")
-
- self._zenoh = ZenohSubscriberHelper(zenoh_listen)
- self._zenoh.subscribe(zenoh_topic, self._on_action)
- self.get_logger().info(f"Subscribed to Zenoh topic: {zenoh_topic}")
-
- def _on_action(self, sample):
- raw = bytes(sample.payload.to_bytes())
- event = parse_action_event(raw)
- if event is None:
- self.get_logger().error("Failed to parse action event")
- return
- self.get_logger().info(f"Received action={event.action} params={event.params}")
- twist = self._mapper.map(event)
- self._pub.publish(twist)
- self.get_logger().info(
- f"Published /cmd_vel: linear.x={twist.linear.x:.2f} angular.z={twist.angular.z:.2f}"
- )
-
- def destroy_node(self):
- self._zenoh.close()
- super().destroy_node()
-
-
-def main(args=None):
- rclpy.init(args=args)
- node = IsaacSimG1BridgeNode()
- try:
- rclpy.spin(node)
- except (KeyboardInterrupt, rclpy.executors.ExternalShutdownException):
- pass
- finally:
- node.destroy_node()
- if rclpy.ok():
- rclpy.shutdown()
diff --git a/bridge/unitree/g1/isaac_sim_bridge/launch/isaac_sim_bridge.launch.py b/bridge/unitree/g1/isaac_sim_bridge/launch/isaac_sim_bridge.launch.py
deleted file mode 100644
index eaa7f5747..000000000
--- a/bridge/unitree/g1/isaac_sim_bridge/launch/isaac_sim_bridge.launch.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from launch import LaunchDescription
-from launch_ros.actions import Node
-from launch.substitutions import PathJoinSubstitution
-from launch_ros.substitutions import FindPackageShare
-
-
-def generate_launch_description():
- config = PathJoinSubstitution(
- [FindPackageShare("isaac_sim_bridge_g1"), "config", "default.yaml"]
- )
- return LaunchDescription([
- Node(
- package="isaac_sim_bridge_g1",
- executable="isaac_sim_bridge",
- name="isaac_sim_bridge_g1",
- output="screen",
- parameters=[config],
- )
- ])
diff --git a/bridge/unitree/g1/isaac_sim_bridge/package.xml b/bridge/unitree/g1/isaac_sim_bridge/package.xml
deleted file mode 100644
index 883e8601e..000000000
--- a/bridge/unitree/g1/isaac_sim_bridge/package.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
- isaac_sim_bridge_g1
- 0.1.0
- Fabric Zenoh action adapter for Unitree G1 in OM1-sim / Isaac Sim
- Maintainer
- Apache-2.0
- rclpy
- geometry_msgs
- zenoh_bridge
-
- ament_python
-
-
diff --git a/bridge/unitree/g1/isaac_sim_bridge/resource/isaac_sim_bridge_g1 b/bridge/unitree/g1/isaac_sim_bridge/resource/isaac_sim_bridge_g1
deleted file mode 100644
index e69de29bb..000000000
diff --git a/bridge/unitree/g1/isaac_sim_bridge/setup.cfg b/bridge/unitree/g1/isaac_sim_bridge/setup.cfg
deleted file mode 100644
index 42c8c81b4..000000000
--- a/bridge/unitree/g1/isaac_sim_bridge/setup.cfg
+++ /dev/null
@@ -1,5 +0,0 @@
-[develop]
-script_dir=$base/lib/isaac_sim_bridge_g1
-
-[install]
-install_scripts=$base/lib/isaac_sim_bridge_g1
diff --git a/bridge/unitree/g1/isaac_sim_bridge/setup.py b/bridge/unitree/g1/isaac_sim_bridge/setup.py
deleted file mode 100644
index eca88754b..000000000
--- a/bridge/unitree/g1/isaac_sim_bridge/setup.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from setuptools import setup, find_packages
-from glob import glob
-
-package_name = "isaac_sim_bridge_g1"
-
-setup(
- name=package_name,
- version="0.1.0",
- packages=find_packages(exclude=["test"]),
- data_files=[
- ("share/ament_index/resource_index/packages", [f"resource/{package_name}"]),
- (f"share/{package_name}", ["package.xml"]),
- (f"share/{package_name}/launch", glob("launch/*.py")),
- (f"share/{package_name}/config", glob("config/*.yaml")),
- ],
- install_requires=["setuptools"],
- zip_safe=True,
- entry_points={
- "console_scripts": [
- # primary entry point
- "isaac_sim_bridge = g1.node:main",
- ],
- },
-)
diff --git a/bridge/unitree/go2/isaac_sim_bridge/config/default.yaml b/bridge/unitree/go2/isaac_sim_bridge/config/default.yaml
deleted file mode 100644
index 83d6d64fb..000000000
--- a/bridge/unitree/go2/isaac_sim_bridge/config/default.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-isaac_sim_bridge_go2:
- ros__parameters:
- zenoh_topic: "robot/tunnel/action"
- zenoh_listen: "tcp/127.0.0.1:7447"
- cmd_vel_topic: "/cmd_vel"
- forward_speed: 0.5
- backward_speed: 0.5
- turn_angular_speed: 0.5
diff --git a/bridge/unitree/go2/isaac_sim_bridge/go2/__init__.py b/bridge/unitree/go2/isaac_sim_bridge/go2/__init__.py
deleted file mode 100644
index 8f8247578..000000000
--- a/bridge/unitree/go2/isaac_sim_bridge/go2/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from .node import main
-from .mapper import Go2Mapper
-
-__all__ = ["main", "Go2Mapper"]
diff --git a/bridge/unitree/go2/isaac_sim_bridge/go2/mapper.py b/bridge/unitree/go2/isaac_sim_bridge/go2/mapper.py
deleted file mode 100644
index ad3a43ae8..000000000
--- a/bridge/unitree/go2/isaac_sim_bridge/go2/mapper.py
+++ /dev/null
@@ -1,31 +0,0 @@
-"""Go2 mapper — placeholder (experimental).
-
-Currently reuses cmd_vel mapping. Replace with Go2-specific SDK backend when available.
-"""
-from geometry_msgs.msg import Twist
-from zenoh_bridge import ActionEvent, CommandMapper, clamp
-
-
-class Go2Mapper(CommandMapper):
- """Placeholder mapper for Unitree Go2. Experimental — not validated on hardware."""
-
- def __init__(self, forward_speed=0.5, backward_speed=0.5,
- turn_angular_speed=0.5):
- self._fwd = forward_speed
- self._bwd = backward_speed
- self._turn_ang = turn_angular_speed
-
- def map(self, event: ActionEvent) -> Twist:
- msg = Twist()
- a = event.action
- if a in ("move_forward", "forward"):
- msg.linear.x = clamp(self._fwd, 0.0, 1.5)
- elif a in ("move_backward", "backward"):
- msg.linear.x = -clamp(self._bwd, 0.0, 0.5)
- elif a == "turn_left":
- msg.angular.z = clamp(self._turn_ang, 0.0, 1.0)
- elif a == "turn_right":
- msg.angular.z = -clamp(self._turn_ang, 0.0, 1.0)
- elif a == "stop":
- pass
- return msg
diff --git a/bridge/unitree/go2/isaac_sim_bridge/go2/node.py b/bridge/unitree/go2/isaac_sim_bridge/go2/node.py
deleted file mode 100644
index b3536ddc8..000000000
--- a/bridge/unitree/go2/isaac_sim_bridge/go2/node.py
+++ /dev/null
@@ -1,56 +0,0 @@
-"""ROS2 node for Fabric → Unitree Go2 adapter (placeholder)."""
-import rclpy
-from rclpy.node import Node
-from geometry_msgs.msg import Twist
-
-from zenoh_bridge import parse_action_event, ZenohSubscriberHelper
-from .mapper import Go2Mapper
-
-
-class IsaacSimGo2BridgeNode(Node):
- def __init__(self):
- super().__init__("isaac_sim_bridge_go2")
- self.declare_parameter("zenoh_topic", "robot/tunnel/action")
- self.declare_parameter("zenoh_listen", "tcp/127.0.0.1:7447")
- self.declare_parameter("cmd_vel_topic", "/cmd_vel")
- self.declare_parameter("forward_speed", 0.5)
- self.declare_parameter("backward_speed", 0.5)
- self.declare_parameter("turn_angular_speed", 0.5)
-
- p = self.get_parameter
- zenoh_topic = p("zenoh_topic").get_parameter_value().string_value
- zenoh_listen = p("zenoh_listen").get_parameter_value().string_value
- cmd_vel_topic = p("cmd_vel_topic").get_parameter_value().string_value
-
- self._mapper = Go2Mapper(
- forward_speed = p("forward_speed").get_parameter_value().double_value,
- backward_speed = p("backward_speed").get_parameter_value().double_value,
- turn_angular_speed= p("turn_angular_speed").get_parameter_value().double_value,
- )
- self._pub = self.create_publisher(Twist, cmd_vel_topic, 10)
- self._zenoh = ZenohSubscriberHelper(zenoh_listen)
- self._zenoh.subscribe(zenoh_topic, self._on_action)
- self.get_logger().info(f"Go2 adapter ready, subscribed to {zenoh_topic}")
-
- def _on_action(self, sample):
- event = parse_action_event(bytes(sample.payload.to_bytes()))
- if event is None:
- return
- self._pub.publish(self._mapper.map(event))
-
- def destroy_node(self):
- self._zenoh.close()
- super().destroy_node()
-
-
-def main(args=None):
- rclpy.init(args=args)
- node = IsaacSimGo2BridgeNode()
- try:
- rclpy.spin(node)
- except (KeyboardInterrupt, rclpy.executors.ExternalShutdownException):
- pass
- finally:
- node.destroy_node()
- if rclpy.ok():
- rclpy.shutdown()
diff --git a/bridge/unitree/go2/isaac_sim_bridge/launch/isaac_sim_bridge.launch.py b/bridge/unitree/go2/isaac_sim_bridge/launch/isaac_sim_bridge.launch.py
deleted file mode 100644
index c5932624a..000000000
--- a/bridge/unitree/go2/isaac_sim_bridge/launch/isaac_sim_bridge.launch.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from launch import LaunchDescription
-from launch_ros.actions import Node
-from launch.substitutions import PathJoinSubstitution
-from launch_ros.substitutions import FindPackageShare
-
-
-def generate_launch_description():
- config = PathJoinSubstitution(
- [FindPackageShare("isaac_sim_bridge_go2"), "config", "default.yaml"]
- )
- return LaunchDescription([
- Node(
- package="isaac_sim_bridge_go2",
- executable="isaac_sim_bridge",
- name="isaac_sim_bridge_go2",
- output="screen",
- parameters=[config],
- )
- ])
diff --git a/bridge/unitree/go2/isaac_sim_bridge/package.xml b/bridge/unitree/go2/isaac_sim_bridge/package.xml
deleted file mode 100644
index 74e2b0786..000000000
--- a/bridge/unitree/go2/isaac_sim_bridge/package.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
- isaac_sim_bridge_go2
- 0.1.0
- Fabric Zenoh action adapter for Unitree Go2 in OM1-sim / Isaac Sim (placeholder)
- Maintainer
- Apache-2.0
- rclpy
- geometry_msgs
- zenoh_bridge
-
- ament_python
-
-
diff --git a/bridge/unitree/go2/isaac_sim_bridge/resource/isaac_sim_bridge_go2 b/bridge/unitree/go2/isaac_sim_bridge/resource/isaac_sim_bridge_go2
deleted file mode 100644
index e69de29bb..000000000
diff --git a/bridge/unitree/go2/isaac_sim_bridge/setup.cfg b/bridge/unitree/go2/isaac_sim_bridge/setup.cfg
deleted file mode 100644
index 6fad53eef..000000000
--- a/bridge/unitree/go2/isaac_sim_bridge/setup.cfg
+++ /dev/null
@@ -1,5 +0,0 @@
-[develop]
-script_dir=$base/lib/isaac_sim_bridge_go2
-
-[install]
-install_scripts=$base/lib/isaac_sim_bridge_go2
diff --git a/bridge/unitree/go2/isaac_sim_bridge/setup.py b/bridge/unitree/go2/isaac_sim_bridge/setup.py
deleted file mode 100644
index 53da7b5cc..000000000
--- a/bridge/unitree/go2/isaac_sim_bridge/setup.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from setuptools import setup, find_packages
-from glob import glob
-
-package_name = "isaac_sim_bridge_go2"
-
-setup(
- name=package_name,
- version="0.1.0",
- packages=find_packages(exclude=["test"]),
- data_files=[
- ("share/ament_index/resource_index/packages", [f"resource/{package_name}"]),
- (f"share/{package_name}", ["package.xml"]),
- (f"share/{package_name}/launch", glob("launch/*.py")),
- (f"share/{package_name}/config", glob("config/*.yaml")),
- ],
- install_requires=["setuptools"],
- zip_safe=True,
- entry_points={
- "console_scripts": [
- "isaac_sim_bridge = go2.node:main",
- ],
- },
-)
diff --git a/bridge/unitree/tron1/isaac_sim_bridge/config/default.yaml b/bridge/unitree/tron1/isaac_sim_bridge/config/default.yaml
deleted file mode 100644
index 846da0605..000000000
--- a/bridge/unitree/tron1/isaac_sim_bridge/config/default.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-isaac_sim_bridge_tron1:
- ros__parameters:
- zenoh_topic: "robot/tunnel/action"
- zenoh_listen: "tcp/127.0.0.1:7447"
- cmd_vel_topic: "/cmd_vel"
- forward_speed: 0.3
- backward_speed: 0.3
- turn_angular_speed: 0.3
diff --git a/bridge/unitree/tron1/isaac_sim_bridge/launch/isaac_sim_bridge.launch.py b/bridge/unitree/tron1/isaac_sim_bridge/launch/isaac_sim_bridge.launch.py
deleted file mode 100644
index 73f4ab1d7..000000000
--- a/bridge/unitree/tron1/isaac_sim_bridge/launch/isaac_sim_bridge.launch.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from launch import LaunchDescription
-from launch_ros.actions import Node
-from launch.substitutions import PathJoinSubstitution
-from launch_ros.substitutions import FindPackageShare
-
-
-def generate_launch_description():
- config = PathJoinSubstitution(
- [FindPackageShare("isaac_sim_bridge_tron1"), "config", "default.yaml"]
- )
- return LaunchDescription([
- Node(
- package="isaac_sim_bridge_tron1",
- executable="isaac_sim_bridge",
- name="isaac_sim_bridge_tron1",
- output="screen",
- parameters=[config],
- )
- ])
diff --git a/bridge/unitree/tron1/isaac_sim_bridge/package.xml b/bridge/unitree/tron1/isaac_sim_bridge/package.xml
deleted file mode 100644
index 055e9520a..000000000
--- a/bridge/unitree/tron1/isaac_sim_bridge/package.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
- isaac_sim_bridge_tron1
- 0.1.0
- Fabric Zenoh action adapter for Tron1 in OM1-sim / Isaac Sim (placeholder)
- Maintainer
- Apache-2.0
- rclpy
- geometry_msgs
- zenoh_bridge
-
- ament_python
-
-
diff --git a/bridge/unitree/tron1/isaac_sim_bridge/resource/isaac_sim_bridge_tron1 b/bridge/unitree/tron1/isaac_sim_bridge/resource/isaac_sim_bridge_tron1
deleted file mode 100644
index e69de29bb..000000000
diff --git a/bridge/unitree/tron1/isaac_sim_bridge/setup.cfg b/bridge/unitree/tron1/isaac_sim_bridge/setup.cfg
deleted file mode 100644
index 8cc636cab..000000000
--- a/bridge/unitree/tron1/isaac_sim_bridge/setup.cfg
+++ /dev/null
@@ -1,5 +0,0 @@
-[develop]
-script_dir=$base/lib/isaac_sim_bridge_tron1
-
-[install]
-install_scripts=$base/lib/isaac_sim_bridge_tron1
diff --git a/bridge/unitree/tron1/isaac_sim_bridge/setup.py b/bridge/unitree/tron1/isaac_sim_bridge/setup.py
deleted file mode 100644
index 982cabbf7..000000000
--- a/bridge/unitree/tron1/isaac_sim_bridge/setup.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from setuptools import setup, find_packages
-from glob import glob
-
-package_name = "isaac_sim_bridge_tron1"
-
-setup(
- name=package_name,
- version="0.1.0",
- packages=find_packages(exclude=["test"]),
- data_files=[
- ("share/ament_index/resource_index/packages", [f"resource/{package_name}"]),
- (f"share/{package_name}", ["package.xml"]),
- (f"share/{package_name}/launch", glob("launch/*.py")),
- (f"share/{package_name}/config", glob("config/*.yaml")),
- ],
- install_requires=["setuptools"],
- zip_safe=True,
- entry_points={
- "console_scripts": [
- "isaac_sim_bridge = tron1.node:main",
- ],
- },
-)
diff --git a/bridge/unitree/tron1/isaac_sim_bridge/tron1/__init__.py b/bridge/unitree/tron1/isaac_sim_bridge/tron1/__init__.py
deleted file mode 100644
index 56798d7c4..000000000
--- a/bridge/unitree/tron1/isaac_sim_bridge/tron1/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from .node import main
-from .mapper import Tron1Mapper
-
-__all__ = ["main", "Tron1Mapper"]
diff --git a/bridge/unitree/tron1/isaac_sim_bridge/tron1/mapper.py b/bridge/unitree/tron1/isaac_sim_bridge/tron1/mapper.py
deleted file mode 100644
index 98b3049d4..000000000
--- a/bridge/unitree/tron1/isaac_sim_bridge/tron1/mapper.py
+++ /dev/null
@@ -1,28 +0,0 @@
-"""Tron1 mapper — placeholder (experimental)."""
-from geometry_msgs.msg import Twist
-from zenoh_bridge import ActionEvent, CommandMapper, clamp
-
-
-class Tron1Mapper(CommandMapper):
- """Placeholder mapper for Tron1. Experimental — not validated on hardware."""
-
- def __init__(self, forward_speed=0.3, backward_speed=0.3,
- turn_angular_speed=0.3):
- self._fwd = forward_speed
- self._bwd = backward_speed
- self._turn_ang = turn_angular_speed
-
- def map(self, event: ActionEvent) -> Twist:
- msg = Twist()
- a = event.action
- if a in ("move_forward", "forward"):
- msg.linear.x = clamp(self._fwd, 0.0, 1.0)
- elif a in ("move_backward", "backward"):
- msg.linear.x = -clamp(self._bwd, 0.0, 0.5)
- elif a == "turn_left":
- msg.angular.z = clamp(self._turn_ang, 0.0, 0.5)
- elif a == "turn_right":
- msg.angular.z = -clamp(self._turn_ang, 0.0, 0.5)
- elif a == "stop":
- pass
- return msg
diff --git a/bridge/unitree/tron1/isaac_sim_bridge/tron1/node.py b/bridge/unitree/tron1/isaac_sim_bridge/tron1/node.py
deleted file mode 100644
index 82436d26c..000000000
--- a/bridge/unitree/tron1/isaac_sim_bridge/tron1/node.py
+++ /dev/null
@@ -1,56 +0,0 @@
-"""ROS2 node for Fabric → Tron1 adapter (placeholder)."""
-import rclpy
-from rclpy.node import Node
-from geometry_msgs.msg import Twist
-
-from zenoh_bridge import parse_action_event, ZenohSubscriberHelper
-from .mapper import Tron1Mapper
-
-
-class IsaacSimTron1BridgeNode(Node):
- def __init__(self):
- super().__init__("isaac_sim_bridge_tron1")
- self.declare_parameter("zenoh_topic", "robot/tunnel/action")
- self.declare_parameter("zenoh_listen", "tcp/127.0.0.1:7447")
- self.declare_parameter("cmd_vel_topic", "/cmd_vel")
- self.declare_parameter("forward_speed", 0.3)
- self.declare_parameter("backward_speed", 0.3)
- self.declare_parameter("turn_angular_speed", 0.3)
-
- p = self.get_parameter
- zenoh_topic = p("zenoh_topic").get_parameter_value().string_value
- zenoh_listen = p("zenoh_listen").get_parameter_value().string_value
- cmd_vel_topic = p("cmd_vel_topic").get_parameter_value().string_value
-
- self._mapper = Tron1Mapper(
- forward_speed = p("forward_speed").get_parameter_value().double_value,
- backward_speed = p("backward_speed").get_parameter_value().double_value,
- turn_angular_speed= p("turn_angular_speed").get_parameter_value().double_value,
- )
- self._pub = self.create_publisher(Twist, cmd_vel_topic, 10)
- self._zenoh = ZenohSubscriberHelper(zenoh_listen)
- self._zenoh.subscribe(zenoh_topic, self._on_action)
- self.get_logger().info(f"Tron1 adapter ready, subscribed to {zenoh_topic}")
-
- def _on_action(self, sample):
- event = parse_action_event(bytes(sample.payload.to_bytes()))
- if event is None:
- return
- self._pub.publish(self._mapper.map(event))
-
- def destroy_node(self):
- self._zenoh.close()
- super().destroy_node()
-
-
-def main(args=None):
- rclpy.init(args=args)
- node = IsaacSimTron1BridgeNode()
- try:
- rclpy.spin(node)
- except (KeyboardInterrupt, rclpy.executors.ExternalShutdownException):
- pass
- finally:
- node.destroy_node()
- if rclpy.ok():
- rclpy.shutdown()
diff --git a/docs/images/flow.png b/docs/images/flow.png
deleted file mode 100644
index 9a7212107..000000000
Binary files a/docs/images/flow.png and /dev/null differ
diff --git a/tunnel/.env.example b/tunnel/.env.example
deleted file mode 100644
index 110703d33..000000000
--- a/tunnel/.env.example
+++ /dev/null
@@ -1,38 +0,0 @@
-# ── Fabric tunnel: base config ────────────────────────────────────────────
-# WebSocket URL of the Fabric proxy the tunnel dials out to.
-PROXY_WS_URL=wss://api.fabric.foundation/api/core/ws/robot
-# x402 micropayment facilitator.
-FACILITATOR_URL=https://x402.org/facilitator
-# gin log verbosity: "release" or "debug".
-GIN_MODE=debug
-
-# Chain preset: bsc-testnet | bsc-mainnet | base-sepolia | base-mainnet.
-# Sets both the x402 payment network and the AIP registration chain. When
-# unset, payments use config.json "network" (default Base mainnet) and AIP
-# registration uses BSC testnet. Make sure FACILITATOR_URL supports the
-# selected network.
-CHAIN=base-sepolia
-
-# ── BitAgent / Unibase AIP registration ───────────────────────────────────
-# Master switch. When true the robot registers itself as an A2A agent on the
-# AIP (BitAgent) network via the gateway. Requires the vars below.
-AIP_ENABLED=true
-
-# Bearer token for registration — the platform resolves your account from
-# it. OPTIONAL: when unset, the tunnel opens an interactive browser
-# authorization on first run and caches the token in
-# ~/.config/unibase-aip-sdk/config.json. UNIBASE_PROXY_AUTH takes priority,
-# PRIVY_TOKEN is the fallback.
-# UNIBASE_PROXY_AUTH=your-bearer-token
-# PRIVY_TOKEN=your-bearer-token
-
-# Wallet address to register under. Only needed for token-less registration;
-# normally derived from the bearer token.
-# AIP_USER_ID=0xYourWalletAddress
-
-# Optional overrides (defaults shown).
-# AIP_ENDPOINT=https://api.aip.unibase.com
-# GATEWAY_URL=https://gateway.aip.unibase.com
-# AIP_PUBLIC_BASE_URL=https://api.fabric.foundation/api/core
-# AIP_AGENT_NAME=Robot my-robot
-# AIP_LOCAL_PORT=8000
diff --git a/tunnel/Dockerfile b/tunnel/Dockerfile
deleted file mode 100644
index fbb7f0efd..000000000
--- a/tunnel/Dockerfile
+++ /dev/null
@@ -1,49 +0,0 @@
-FROM golang:1.25-alpine AS builder
-
-RUN apk add --no-cache git gcc musl-dev curl unzip
-
-ARG ZENOH_C_VERSION=1.9.0
-ARG TARGETARCH
-RUN case "$TARGETARCH" in \
- amd64) ZENOH_ARCH="x86_64" ;; \
- arm64) ZENOH_ARCH="aarch64" ;; \
- *) echo "Unsupported arch: $TARGETARCH"; exit 1 ;; \
- esac \
- && URL="https://github.com/eclipse-zenoh/zenoh-c/releases/download/${ZENOH_C_VERSION}/zenoh-c-${ZENOH_C_VERSION}-${ZENOH_ARCH}-unknown-linux-musl-standalone.zip" \
- && echo "Downloading: $URL" \
- && curl -fsSL -o /tmp/zc.zip "$URL" \
- && unzip -q /tmp/zc.zip -d /opt/zenoh-c \
- && rm /tmp/zc.zip
-
-WORKDIR /app
-
-COPY go.mod go.sum ./
-
-RUN go mod download
-
-COPY . .
-
-RUN CGO_ENABLED=1 GOOS=linux \
- CGO_CFLAGS="-I/opt/zenoh-c/include" \
- CGO_LDFLAGS="-L/opt/zenoh-c/lib -lzenohc" \
- go build -a -o main cmd/main.go
-
-
-FROM alpine:latest
-
-RUN apk --no-cache add ca-certificates libgcc
-
-WORKDIR /app
-
-ENV GIN_MODE=release
-ENV PROXY_WS_URL=wss://api.fabric.foundation/api/core/ws/robot
-ENV FACILITATOR_URL=https://x402.org/facilitator
-
-COPY --from=builder /opt/zenoh-c/lib/libzenohc.so /usr/lib/
-COPY --from=builder /app/config.json ./config.json
-COPY --from=builder /app/main ./main
-
-EXPOSE 3000
-
-ENTRYPOINT ["./main"]
-CMD ["-config", "./config.json"]
diff --git a/tunnel/cmd/main.go b/tunnel/cmd/main.go
deleted file mode 100644
index a1a01a257..000000000
--- a/tunnel/cmd/main.go
+++ /dev/null
@@ -1,340 +0,0 @@
-package main
-
-import (
- "context"
- "encoding/json"
- "flag"
- "os"
- "os/signal"
- "syscall"
- "time"
-
- "github.com/eclipse-zenoh/zenoh-go/zenoh"
- "github.com/gin-contrib/cors"
- "github.com/gin-gonic/gin"
- "github.com/joho/godotenv"
- aipauth "github.com/unibaseio/aip-go-sdk/auth"
- aipserver "github.com/unibaseio/aip-go-sdk/server"
- x402 "github.com/x402-foundation/x402/go"
- x402http "github.com/x402-foundation/x402/go/http"
- ginmw "github.com/x402-foundation/x402/go/http/gin"
- evm "github.com/x402-foundation/x402/go/mechanisms/evm"
- evmexact "github.com/x402-foundation/x402/go/mechanisms/evm/exact/server"
- "go.uber.org/zap"
-
- "github.com/fabricfoundation/tunnel/config"
- "github.com/fabricfoundation/tunnel/internal"
- "github.com/fabricfoundation/tunnel/internal/aipagent"
- "github.com/fabricfoundation/tunnel/internal/handlers"
-)
-
-const (
- RobotConfigTopicPrefix = "robot/config/"
-)
-
-func main() {
- configPath := flag.String("config", "config.json", "Path to config file")
- flag.Parse()
-
- logger, _ := zap.NewProduction()
- defer func() {
- if err := logger.Sync(); err != nil {
- logger.Warn("failed to sync logger", zap.Error(err))
- }
- }()
-
- if err := godotenv.Load(); err != nil {
- logger.Warn("failed to load .env file", zap.Error(err))
- }
-
- cfg, err := config.LoadConfig(*configPath)
- if err != nil {
- logger.Fatal("configuration error", zap.Error(err))
- }
-
- // No token in the env? Fall back to the SDK's cached credentials, or walk
- // the user through the browser authorization flow on first run.
- if cfg.AIPEnabled && cfg.AIPPrivyToken == "" {
- token, wallet, err := aipauth.EnsureAuth(context.Background())
- if err != nil {
- logger.Fatal("unibase authorization failed", zap.Error(err))
- }
- cfg.AIPPrivyToken = token
- if cfg.AIPUserID == "" {
- cfg.AIPUserID = wallet
- }
- logger.Info("unibase authorization ready", zap.String("wallet", wallet))
- }
-
- session, err := zenoh.Open(zenoh.NewConfigDefault(), nil)
- if err != nil {
- logger.Fatal("failed to open zenoh session", zap.Error(err))
- }
- defer func() {
- if err := session.Close(nil); err != nil {
- logger.Warn("failed to close zenoh session", zap.Error(err))
- }
- }()
-
- restartCh := make(chan struct{}, 1)
- subTopic := RobotConfigTopicPrefix + cfg.RobotID
- ke, err := zenoh.NewKeyExpr(subTopic)
- if err != nil {
- logger.Fatal("failed to create key expression", zap.Error(err))
- }
- sub, err := session.DeclareSubscriber(ke, zenoh.Closure[zenoh.Sample]{
- Call: func(sample zenoh.Sample) {
- var partialCfg struct {
- EVMPayeeAddress *string `json:"evm_payee_address"`
- Price *string `json:"price"`
- Network *string `json:"network"`
- TokenAddress *string `json:"token_address"`
- TokenName *string `json:"token_name"`
- TokenVersion *string `json:"token_version"`
- TokenDecimals *int `json:"token_decimals"`
- TokenTransferMethod *string `json:"token_transfer_method"`
- TokenSupportsEIP2612 *bool `json:"token_supports_eip2612"`
- }
- if err := json.Unmarshal(sample.Payload().Bytes(), &partialCfg); err != nil {
- logger.Warn("failed to parse config update", zap.Error(err))
- return
- }
-
- candidate := *cfg
- updated := false
- if partialCfg.EVMPayeeAddress != nil && *partialCfg.EVMPayeeAddress != candidate.EVMPayeeAddress {
- candidate.EVMPayeeAddress = *partialCfg.EVMPayeeAddress
- updated = true
- }
- if partialCfg.Price != nil && *partialCfg.Price != candidate.Price {
- candidate.Price = *partialCfg.Price
- updated = true
- }
- if partialCfg.Network != nil && *partialCfg.Network != candidate.Network {
- candidate.Network = *partialCfg.Network
- updated = true
- }
- if partialCfg.TokenAddress != nil && *partialCfg.TokenAddress != candidate.TokenAddress {
- candidate.TokenAddress = *partialCfg.TokenAddress
- updated = true
- }
- if partialCfg.TokenName != nil && *partialCfg.TokenName != candidate.TokenName {
- candidate.TokenName = *partialCfg.TokenName
- updated = true
- }
- if partialCfg.TokenVersion != nil && *partialCfg.TokenVersion != candidate.TokenVersion {
- candidate.TokenVersion = *partialCfg.TokenVersion
- updated = true
- }
- if partialCfg.TokenDecimals != nil && *partialCfg.TokenDecimals != candidate.TokenDecimals {
- candidate.TokenDecimals = *partialCfg.TokenDecimals
- updated = true
- }
- if partialCfg.TokenTransferMethod != nil && *partialCfg.TokenTransferMethod != candidate.TokenTransferMethod {
- candidate.TokenTransferMethod = *partialCfg.TokenTransferMethod
- updated = true
- }
- if partialCfg.TokenSupportsEIP2612 != nil && *partialCfg.TokenSupportsEIP2612 != candidate.TokenSupportsEIP2612 {
- candidate.TokenSupportsEIP2612 = *partialCfg.TokenSupportsEIP2612
- updated = true
- }
-
- if updated {
- if err := candidate.Validate(); err != nil {
- logger.Warn("rejecting invalid config update", zap.Error(err))
- return
- }
- *cfg = candidate
- logger.Info("config updated via zenoh, signaling restart")
- select {
- case restartCh <- struct{}{}:
- default:
- }
- }
- },
- }, nil)
- if err != nil {
- logger.Fatal("failed to declare config subscriber", zap.Error(err))
- }
- defer func() {
- if err := sub.Undeclare(); err != nil {
- logger.Warn("failed to undeclare zenoh subscriber", zap.Error(err))
- }
- }()
-
- ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
- defer cancel()
-
- aipSrv := aipagent.Build(cfg, handlers.PublishRobotAction, logger)
- if aipSrv != nil {
- go func() {
- if err := aipSrv.Run(ctx); err != nil {
- logger.Warn("AIP agent server stopped", zap.Error(err))
- }
- }()
- }
-
- for {
- router := setupRouter(cfg, aipSrv, logger)
- client := internal.NewClient(cfg.ProxyWSURL, cfg.RobotID, router, logger)
-
- clientCtx, clientCancel := context.WithCancel(ctx)
-
- go func() {
- select {
- case <-restartCh:
- logger.Info("restarting internal client to apply new config...")
- clientCancel()
- case <-clientCtx.Done():
- }
- }()
-
- client.Run(clientCtx)
- clientCancel()
-
- if ctx.Err() != nil {
- break
- }
- time.Sleep(100 * time.Millisecond)
- }
-}
-
-// pristineNetworkConfigs is x402's built-in asset table, captured before anything overrides it,
-// so a config update that drops token_address can put the shipped default back.
-var pristineNetworkConfigs = func() map[string]evm.NetworkConfig {
- snapshot := make(map[string]evm.NetworkConfig, len(evm.NetworkConfigs))
- for network, cfg := range evm.NetworkConfigs {
- snapshot[network] = cfg
- }
- return snapshot
-}()
-
-// registeredNetwork is the network registerTokenAsset last overrode, so a config update that
-// switches networks does not strand the previous one on a stale asset.
-var registeredNetwork string
-
-// restoreNetworkDefault undoes an override, falling back to whatever x402 shipped for the network.
-func restoreNetworkDefault(network string) {
- if original, ok := pristineNetworkConfigs[network]; ok {
- evm.NetworkConfigs[network] = original
- return
- }
- delete(evm.NetworkConfigs, network)
-}
-
-// registerTokenAsset makes cfg.TokenAddress the default asset for cfg.Network. It runs on every
-// (re)start of the router, so it has to undo whatever the previous config registered.
-func registerTokenAsset(cfg *config.Config, logger *zap.Logger) {
- if registeredNetwork != "" && registeredNetwork != cfg.Network {
- restoreNetworkDefault(registeredNetwork)
- registeredNetwork = ""
- }
-
- if cfg.TokenAddress == "" {
- restoreNetworkDefault(cfg.Network)
- registeredNetwork = ""
- return
- }
-
- chainID, ok := cfg.ChainID()
- if !ok {
- logger.Warn("skipping token registration for non-eip155 network", zap.String("network", cfg.Network))
- return
- }
-
- asset := evm.AssetInfo{
- Address: cfg.TokenAddress,
- Name: cfg.TokenName,
- Version: cfg.TokenVersion,
- Decimals: cfg.TokenDecimals,
- }
-
- if cfg.TokenTransferMethod == config.TransferMethodPermit2 {
- asset.AssetTransferMethod = evm.AssetTransferMethodPermit2
- asset.SupportsEip2612 = cfg.TokenSupportsEIP2612
- }
-
- evm.NetworkConfigs[cfg.Network] = evm.NetworkConfig{
- ChainID: chainID,
- DefaultAsset: asset,
- }
- registeredNetwork = cfg.Network
-
- logger.Info("registered payment token",
- zap.String("network", cfg.Network),
- zap.String("address", cfg.TokenAddress),
- zap.String("name", cfg.TokenName),
- zap.Int("decimals", cfg.TokenDecimals),
- zap.String("transfer_method", cfg.TokenTransferMethod),
- zap.Bool("supports_eip2612", cfg.TokenSupportsEIP2612),
- )
-}
-
-func setupRouter(cfg *config.Config, aipSrv *aipserver.Server, logger *zap.Logger) *gin.Engine {
- registerTokenAsset(cfg, logger)
-
- router := gin.New()
-
- router.Use(cors.New(cors.Config{
- AllowOrigins: []string{"*"},
- AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
- AllowHeaders: []string{
- "Origin",
- "Content-Type",
- "Authorization",
- "PAYMENT-SIGNATURE",
- "Access-Control-Expose-Headers",
- "payment-signature",
- },
- ExposeHeaders: []string{
- "PAYMENT-REQUIRED",
- "PAYMENT-RESPONSE",
- },
- AllowCredentials: true,
- MaxAge: 12 * time.Hour,
- }))
-
- facilitatorClient := x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{
- URL: cfg.FacilitatorURL,
- })
-
- routes := x402http.RoutesConfig{
- "POST /action": {
- Accepts: x402http.PaymentOptions{
- {
- Scheme: "exact",
- Price: cfg.Price,
- Network: x402.Network(cfg.Network),
- PayTo: cfg.EVMPayeeAddress,
- },
- },
- Description: "Run a paid robot action",
- MimeType: "application/json",
- },
- }
-
- router.Use(ginmw.X402Payment(ginmw.Config{
- Routes: routes,
- Facilitator: facilitatorClient,
- Schemes: []ginmw.SchemeConfig{
- {Network: x402.Network(cfg.Network), Server: evmexact.NewExactEvmScheme()},
- },
- Timeout: 30 * time.Second,
- }))
-
- h := handlers.NewHandlers(logger)
- RegisterAllRoutes(router, h)
-
- // Serve the AIP A2A contract (/.well-known/agent-card.json, /invoke, ...)
- // for any path Gin doesn't own. The gateway proxies these to us verbatim.
- if aipSrv != nil {
- router.NoRoute(gin.WrapH(aipSrv.Handler()))
- }
-
- return router
-}
-
-// RegisterAllRoutes registers all real handlers on the router.
-func RegisterAllRoutes(router *gin.Engine, h *handlers.Handlers) {
- router.POST("/action", h.PostAction)
-}
diff --git a/tunnel/config.example.json b/tunnel/config.example.json
deleted file mode 100644
index f78914199..000000000
--- a/tunnel/config.example.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "robot_id": "my-robot",
- "evm_payee_address": "0xReplaceWithYourPayeeAddress",
- "price": "1",
- "network": "eip155:91342",
- "token_address": "0xReplaceWithYourTokenAddress",
- "token_decimals": 18,
- "token_transfer_method": "permit2"
-}
diff --git a/tunnel/config.json b/tunnel/config.json
deleted file mode 100644
index a3b7fedb4..000000000
--- a/tunnel/config.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "robot_id": "test-robot",
- "evm_payee_address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
- "price": "$0.002",
- "network": "eip155:84532"
-}
diff --git a/tunnel/config/config.go b/tunnel/config/config.go
deleted file mode 100644
index d596136c8..000000000
--- a/tunnel/config/config.go
+++ /dev/null
@@ -1,261 +0,0 @@
-package config
-
-import (
- "encoding/json"
- "fmt"
- "math/big"
- "os"
- "regexp"
- "strconv"
- "strings"
-
- "github.com/google/uuid"
-)
-
-const (
- DefaultProxyWSURL = "ws://localhost:8080/api/core/ws/robot"
- DefaultFacilitatorURL = "https://x402.org/facilitator"
- DefaultAIPPublicBaseURL = "https://api.fabric.foundation/api/core"
- DefaultAIPEndpoint = "https://api.aip.unibase.com"
- DefaultAIPGatewayURL = "https://gateway.aip.unibase.com"
- DefaultAIPChainID = 97
- DefaultAIPLocalPort = 8000
-
- EIP155Prefix = "eip155:"
- DefaultTokenVersion = "1"
- DefaultTokenDecimals = 6
-
- TransferMethodEIP3009 = "eip3009"
- TransferMethodPermit2 = "permit2"
-)
-
-func getEnvOrDefault(key, defaultVal string) string {
- if v := os.Getenv(key); v != "" {
- return v
- }
- return defaultVal
-}
-
-type Config struct {
- RobotID string `json:"robot_id"`
- EVMPayeeAddress string `json:"evm_payee_address"`
- Price string `json:"price"`
- Network string `json:"network"`
- TokenAddress string `json:"token_address"`
- TokenName string `json:"token_name"`
- TokenVersion string `json:"token_version"`
- TokenDecimals int `json:"token_decimals"`
- TokenTransferMethod string `json:"token_transfer_method"`
- TokenSupportsEIP2612 bool `json:"token_supports_eip2612"`
- ProxyWSURL string `json:"-"`
- FacilitatorURL string `json:"-"`
-
- // aIP
- AIPEnabled bool `json:"-"`
- AIPUserID string `json:"-"` // wallet address used for registration
- AIPPrivyToken string `json:"-"` // bearer token for registration
- AIPEndpoint string `json:"-"` // AIP platform URL
- AIPGatewayURL string `json:"-"` // AIP gateway URL
- AIPPublicBaseURL string `json:"-"` // public gateway base, e.g. https://api.fabric.foundation/api/core/v1
- AIPAgentName string `json:"-"`
- AIPChainID int `json:"-"`
- AIPLocalPort int `json:"-"` // localhost port the SDK binds for its (tunnel-bypassed) listener
-}
-
-// PriceAmount returns the numeric value of the configured price ("$0.002" → 0.002).
-func (c *Config) PriceAmount() float64 {
- v, err := strconv.ParseFloat(strings.TrimPrefix(c.Price, "$"), 64)
- if err != nil {
- return 0
- }
- return v
-}
-
-// AIPEndpointURL is the public URL AIP advertises and calls for this robot:
-// the gateway's transparent proxy path. AIP traffic flows
-// AIP -> gateway(/robots//...) -> ws -> tunnel -> AIP handler.
-func (c *Config) AIPEndpointURL() string {
- base := strings.TrimRight(c.AIPPublicBaseURL, "/")
- return fmt.Sprintf("%s/robots/%s", base, c.RobotID)
-}
-
-var (
- priceRegex = regexp.MustCompile(`^\$?\d+(\.\d+)?$`)
- networkRegex = regexp.MustCompile(`^[a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$`)
- addressRegex = regexp.MustCompile(`^0x[0-9a-fA-F]{40}$`)
-)
-
-// chainPresets are the networks selectable via the CHAIN env var. A preset
-// drives both the x402 payment network (CAIP-2) and the AIP registration
-// chain ID.
-var chainPresets = map[string]struct {
- Network string
- ChainID int
-}{
- "bsc-testnet": {"eip155:97", 97},
- "bsc-mainnet": {"eip155:56", 56},
- "base-sepolia": {"eip155:84532", 84532},
- "base-mainnet": {"eip155:8453", 8453},
-}
-
-// ChainID returns the EIP-155 chain ID of the configured network.
-// The second return value is false when the network is not an eip155 CAIP-2 ID.
-func (c *Config) ChainID() (*big.Int, bool) {
- if !strings.HasPrefix(c.Network, EIP155Prefix) {
- return nil, false
- }
- return new(big.Int).SetString(strings.TrimPrefix(c.Network, EIP155Prefix), 10)
-}
-
-// Validate checks the user-supplied fields and fills in defaults. It is safe to call on a
-// candidate copy of a Config to vet a hot-reload update before committing it.
-func (c *Config) Validate() error {
- if c.RobotID == "" {
- c.RobotID = uuid.NewString()
- }
-
- if c.Price == "" {
- c.Price = "0.001"
- }
- if !priceRegex.MatchString(c.Price) {
- return fmt.Errorf("invalid price format: %q, expected a decimal amount like 0.001 or $0.001", c.Price)
- }
-
- if c.Network == "" {
- c.Network = "eip155:8453"
- }
- if !networkRegex.MatchString(c.Network) {
- return fmt.Errorf("invalid network format: %q, expected format like eip155:8453", c.Network)
- }
-
- if c.EVMPayeeAddress == "" {
- return fmt.Errorf("evm_payee_address is required")
- }
-
- return c.validateToken()
-}
-
-// validateToken checks the optional token fields. They are only meaningful together: an empty
-// token_address means "use whatever default asset x402 knows for this network".
-func (c *Config) validateToken() error {
- if c.TokenAddress == "" {
- return nil
- }
-
- if !addressRegex.MatchString(c.TokenAddress) {
- return fmt.Errorf("invalid token_address format: %q, expected a 0x-prefixed 20-byte hex address", c.TokenAddress)
- }
- if _, ok := c.ChainID(); !ok {
- return fmt.Errorf("token_address requires an eip155 network, got %q", c.Network)
- }
- if c.TokenDecimals < 0 || c.TokenDecimals > 36 {
- return fmt.Errorf("invalid token_decimals: %d, expected 0-36", c.TokenDecimals)
- }
-
- switch c.TokenTransferMethod {
- case "":
- c.TokenTransferMethod = TransferMethodEIP3009
- case TransferMethodEIP3009, TransferMethodPermit2:
- default:
- return fmt.Errorf("invalid token_transfer_method: %q, expected %q or %q",
- c.TokenTransferMethod, TransferMethodEIP3009, TransferMethodPermit2)
- }
-
- if c.TokenSupportsEIP2612 && c.TokenTransferMethod != TransferMethodPermit2 {
- return fmt.Errorf("token_supports_eip2612 only applies when token_transfer_method is %q", TransferMethodPermit2)
- }
-
- if c.NeedsEIP712Domain() && c.TokenName == "" {
- return fmt.Errorf("token_name is required for %s transfers (it forms the EIP-712 domain the payer signs)",
- c.TokenTransferMethod)
- }
-
- if c.TokenVersion == "" {
- c.TokenVersion = DefaultTokenVersion
- }
- if c.TokenDecimals == 0 {
- c.TokenDecimals = DefaultTokenDecimals
- }
-
- return nil
-}
-
-// NeedsEIP712Domain reports whether the payer will sign against the token's own EIP-712 domain,
-// which is what makes token_name and token_version load-bearing.
-func (c *Config) NeedsEIP712Domain() bool {
- return c.TokenTransferMethod != TransferMethodPermit2 || c.TokenSupportsEIP2612
-}
-
-func LoadConfig(path string) (*Config, error) {
- file, err := os.ReadFile(path)
- if err != nil {
- return nil, fmt.Errorf("failed to read config file: %w", err)
- }
-
- var cfg Config
- if err := json.Unmarshal(file, &cfg); err != nil {
- return nil, fmt.Errorf("failed to unmarshal config: %w", err)
- }
-
- cfg.ProxyWSURL = getEnvOrDefault("PROXY_WS_URL", DefaultProxyWSURL)
- cfg.FacilitatorURL = getEnvOrDefault("FACILITATOR_URL", DefaultFacilitatorURL)
-
- // CHAIN overrides the configured network, so it has to be applied before validation.
- defaultChainID := DefaultAIPChainID
- if chain := os.Getenv("CHAIN"); chain != "" {
- preset, ok := chainPresets[strings.ToLower(chain)]
- if !ok {
- return nil, fmt.Errorf("invalid CHAIN %q: valid values are bsc-testnet, bsc-mainnet, base-sepolia, base-mainnet", chain)
- }
- cfg.Network = preset.Network
- defaultChainID = preset.ChainID
- }
-
- if err := cfg.Validate(); err != nil {
- return nil, err
- }
-
- if err := loadAIPConfig(&cfg, defaultChainID); err != nil {
- return nil, err
- }
-
- return &cfg, nil
-}
-
-func loadAIPConfig(cfg *Config, defaultChainID int) error {
- cfg.AIPEnabled = getBoolEnv("AIP_ENABLED", false)
-
- cfg.AIPUserID = os.Getenv("AIP_USER_ID")
- cfg.AIPPrivyToken = getEnvOrDefault("UNIBASE_PROXY_AUTH", os.Getenv("PRIVY_TOKEN"))
- cfg.AIPEndpoint = getEnvOrDefault("AIP_ENDPOINT", DefaultAIPEndpoint)
- cfg.AIPGatewayURL = getEnvOrDefault("GATEWAY_URL", DefaultAIPGatewayURL)
- cfg.AIPPublicBaseURL = getEnvOrDefault("AIP_PUBLIC_BASE_URL", DefaultAIPPublicBaseURL)
- cfg.AIPAgentName = getEnvOrDefault("AIP_AGENT_NAME", "Robot "+cfg.RobotID)
-
- cfg.AIPChainID = defaultChainID
-
- cfg.AIPLocalPort = DefaultAIPLocalPort
- if v := os.Getenv("AIP_LOCAL_PORT"); v != "" {
- n, err := strconv.Atoi(v)
- if err != nil {
- return fmt.Errorf("invalid AIP_LOCAL_PORT: %q", v)
- }
- cfg.AIPLocalPort = n
- }
-
- // No credential check here: when AIP is enabled and no token is set, the
- // tunnel runs the SDK's interactive authorization flow at startup.
- return nil
-}
-
-func getBoolEnv(key string, defaultVal bool) bool {
- v := os.Getenv(key)
- if v == "" {
- return defaultVal
- }
- b, err := strconv.ParseBool(v)
- if err != nil {
- return defaultVal
- }
- return b
-}
diff --git a/tunnel/docker-compose.yml b/tunnel/docker-compose.yml
deleted file mode 100644
index a311df2b4..000000000
--- a/tunnel/docker-compose.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-services:
- tunnel:
- build:
- context: .
- dockerfile: Dockerfile
- image: tunnel:latest
- container_name: tunnel
- restart: unless-stopped
- environment:
- GIN_MODE: release
- PROXY_WS_URL: wss://api.fabric.foundation/api/core/ws/robot
- FACILITATOR_URL: https://x402.org/facilitator
- volumes:
- - ./config.json:/app/config.json:ro
- command: ["-config", "/app/config.json"]
diff --git a/tunnel/go.mod b/tunnel/go.mod
deleted file mode 100644
index f0d37f1eb..000000000
--- a/tunnel/go.mod
+++ /dev/null
@@ -1,78 +0,0 @@
-module github.com/fabricfoundation/tunnel
-
-go 1.25.0
-
-require (
- github.com/eclipse-zenoh/zenoh-go v1.9.0
- github.com/gin-contrib/cors v1.7.7
- github.com/gin-gonic/gin v1.12.0
- github.com/google/uuid v1.6.0
- github.com/gorilla/websocket v1.5.3
- github.com/joho/godotenv v1.5.1
- github.com/unibaseio/aip-go-sdk v0.0.0-20260716210644-024763def441
- github.com/x402-foundation/x402/go v0.0.0-20260512144511-7c239c42f5dd
- go.uber.org/zap v1.28.0
-)
-
-require (
- github.com/a2aproject/a2a-go v0.3.15 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79 // indirect
- google.golang.org/grpc v1.73.0 // indirect
-)
-
-require (
- github.com/BooleanCat/option v0.1.0 // indirect
- github.com/Microsoft/go-winio v0.6.2 // indirect
- github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect
- github.com/StackExchange/wmi v1.2.1 // indirect
- github.com/bits-and-blooms/bitset v1.20.0 // indirect
- github.com/bytedance/gopkg v0.1.3 // indirect
- github.com/bytedance/sonic v1.15.0 // indirect
- github.com/bytedance/sonic/loader v0.5.0 // indirect
- github.com/cloudwego/base64x v0.1.6 // indirect
- github.com/consensys/gnark-crypto v0.18.0 // indirect
- github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect
- github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
- github.com/deckarep/golang-set/v2 v2.6.0 // indirect
- github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
- github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect
- github.com/ethereum/go-ethereum v1.16.7 // indirect
- github.com/ethereum/go-verkle v0.2.2 // indirect
- github.com/gabriel-vasile/mimetype v1.4.12 // indirect
- github.com/gin-contrib/sse v1.1.0 // indirect
- github.com/go-ole/go-ole v1.3.0 // indirect
- github.com/go-playground/locales v0.14.1 // indirect
- github.com/go-playground/universal-translator v0.18.1 // indirect
- github.com/go-playground/validator/v10 v10.30.1 // indirect
- github.com/goccy/go-json v0.10.5 // indirect
- github.com/goccy/go-yaml v1.19.2 // indirect
- github.com/holiman/uint256 v1.3.2 // indirect
- github.com/json-iterator/go v1.1.12 // indirect
- github.com/klauspost/cpuid/v2 v2.3.0 // indirect
- github.com/leodido/go-urn v1.4.0 // indirect
- github.com/mattn/go-isatty v0.0.20 // indirect
- github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
- github.com/modern-go/reflect2 v1.0.2 // indirect
- github.com/pelletier/go-toml/v2 v2.2.4 // indirect
- github.com/quic-go/qpack v0.6.0 // indirect
- github.com/quic-go/quic-go v0.59.0 // indirect
- github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
- github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect
- github.com/tklauser/go-sysconf v0.3.12 // indirect
- github.com/tklauser/numcpus v0.6.1 // indirect
- github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
- github.com/ugorji/go/codec v1.3.1 // indirect
- github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
- github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
- github.com/xeipuuv/gojsonschema v1.2.0 // indirect
- go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
- go.uber.org/multierr v1.10.0 // indirect
- golang.org/x/arch v0.23.0 // indirect
- golang.org/x/crypto v0.48.0 // indirect
- golang.org/x/net v0.51.0 // indirect
- golang.org/x/sync v0.20.0 // indirect
- golang.org/x/sys v0.41.0 // indirect
- golang.org/x/text v0.35.0 // indirect
- google.golang.org/protobuf v1.36.10 // indirect
-)
diff --git a/tunnel/go.sum b/tunnel/go.sum
deleted file mode 100644
index 35bca2abe..000000000
--- a/tunnel/go.sum
+++ /dev/null
@@ -1,308 +0,0 @@
-github.com/BooleanCat/option v0.1.0 h1:wLwWbWnxUEAhpH+Gixy9OjFRnxmwm+nHcZQ8a2JozgA=
-github.com/BooleanCat/option v0.1.0/go.mod h1:vzGAJoquxEsprjj3qidyQTSQzqWDztoj9mO84BoY+YU=
-github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
-github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
-github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
-github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
-github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU=
-github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
-github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
-github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
-github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
-github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
-github.com/a2aproject/a2a-go v0.3.15 h1:h5YpCiPq3jxQ5rIns7oDjPag3ivP8u817AzdA4F+NiI=
-github.com/a2aproject/a2a-go v0.3.15/go.mod h1:I7Cm+a1oL+UT6zMoP+roaRE5vdfUa1iQGVN8aSOuZ0I=
-github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
-github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU=
-github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
-github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
-github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
-github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
-github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
-github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
-github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
-github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
-github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
-github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
-github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
-github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
-github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
-github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
-github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
-github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
-github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw=
-github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo=
-github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
-github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
-github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
-github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
-github.com/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0=
-github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c=
-github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
-github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg=
-github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
-github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg=
-github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
-github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
-github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
-github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
-github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
-github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
-github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
-github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
-github.com/eclipse-zenoh/zenoh-go v1.9.0 h1:OKjUYd3foYsp7NY5B9aH+yFLUba9caUDmJSUOXqBLkI=
-github.com/eclipse-zenoh/zenoh-go v1.9.0/go.mod h1:PeY2+x6VPEFpl/FMLfOl+zwsLxbUjresV5AAT158l80=
-github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
-github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
-github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s=
-github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs=
-github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk=
-github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8=
-github.com/ethereum/go-ethereum v1.16.7 h1:qeM4TvbrWK0UC0tgkZ7NiRsmBGwsjqc64BHo20U59UQ=
-github.com/ethereum/go-ethereum v1.16.7/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk=
-github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
-github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
-github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY=
-github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
-github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
-github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
-github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
-github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
-github.com/gin-contrib/cors v1.7.7 h1:Oh9joP463x7Mw72vhvJ61YQm8ODh9b04YR7vsOErD0Q=
-github.com/gin-contrib/cors v1.7.7/go.mod h1:K5tW0RkzJtWSiOdikXloy8VEZlgdVNpHNw8FpjUPNrE=
-github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
-github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
-github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
-github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
-github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
-github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
-github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
-github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
-github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
-github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
-github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
-github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
-github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
-github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
-github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
-github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
-github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
-github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
-github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
-github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
-github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
-github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
-github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
-github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
-github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
-github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
-github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
-github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
-github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
-github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
-github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
-github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
-github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE=
-github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0=
-github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330=
-github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg=
-github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
-github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
-github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
-github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
-github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
-github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
-github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
-github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
-github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
-github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
-github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
-github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
-github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
-github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
-github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
-github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
-github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
-github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
-github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
-github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
-github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
-github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
-github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
-github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
-github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
-github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
-github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
-github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
-github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
-github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
-github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
-github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
-github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
-github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A=
-github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
-github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
-github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
-github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
-github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
-github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
-github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
-github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
-github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
-github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0=
-github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ=
-github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c=
-github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
-github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
-github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM=
-github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
-github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
-github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
-github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
-github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
-github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
-github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
-github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
-github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
-github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
-github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
-github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
-github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
-github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
-github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
-github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
-github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
-github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
-github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
-github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
-github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw=
-github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
-github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
-github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
-github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
-github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
-github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
-github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
-github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
-github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
-github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
-github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
-github.com/unibaseio/aip-go-sdk v0.0.0-20260716210644-024763def441 h1:xRzw8oeVESLWkyxIyDY3asoz1BvREYyL30m2LLoPdX8=
-github.com/unibaseio/aip-go-sdk v0.0.0-20260716210644-024763def441/go.mod h1:o+rJGVpI8UEWayqFQ8YyIXo2aJsrdU7gkN881V/GVHg=
-github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
-github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
-github.com/x402-foundation/x402/go v0.0.0-20260512144511-7c239c42f5dd h1:rSGTqN02wCjWtbUVt+Xu+4F+MoxPTEB7jo5QR/XQxb4=
-github.com/x402-foundation/x402/go v0.0.0-20260512144511-7c239c42f5dd/go.mod h1:58Cdk20g83eAI3QvxAiQJze7qWUgkjCj9uZlPb4M4HM=
-github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
-github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
-github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
-github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
-github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
-github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
-github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
-github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
-go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
-go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
-go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
-go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
-go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
-go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
-go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
-go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
-go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
-go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
-go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
-go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
-go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
-go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
-go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
-go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
-go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
-go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
-go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
-go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
-go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
-go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
-golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
-golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
-golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
-golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
-golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
-golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
-golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
-golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
-golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
-golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
-golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
-golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
-golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
-golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
-golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
-google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79 h1:iOye66xuaAK0WnkPuhQPUFy8eJcmwUXqGGP3om6IxX8=
-google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79/go.mod h1:HKJDgKsFUnv5VAGeQjz8kxcgDP0HoE0iZNp0OdZNlhE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79 h1:1ZwqphdOdWYXsUHgMpU/101nCtf/kSp9hOrcvFsnl10=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
-google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
-google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
-google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
-google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
-gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/tunnel/internal/aipagent/agent.go b/tunnel/internal/aipagent/agent.go
deleted file mode 100644
index 562cccb23..000000000
--- a/tunnel/internal/aipagent/agent.go
+++ /dev/null
@@ -1,105 +0,0 @@
-package aipagent
-
-import (
- "context"
- "encoding/json"
- "time"
-
- "github.com/unibaseio/aip-go-sdk/server"
- "github.com/unibaseio/aip-go-sdk/types"
- "github.com/unibaseio/aip-go-sdk/wrappers"
- "go.uber.org/zap"
-
- "github.com/fabricfoundation/tunnel/config"
-)
-
-type PublishFunc func(payload []byte) error
-
-func Build(cfg *config.Config, publish PublishFunc, logger *zap.Logger) *server.Server {
- if !cfg.AIPEnabled {
- return nil
- }
-
- handler := func(ctx context.Context, input string) (string, error) {
- var payload any
- if json.Valid([]byte(input)) {
- payload = json.RawMessage(input)
- } else {
- payload = input
- }
- event, err := json.Marshal(map[string]any{
- "payload": payload,
- "source": "aip",
- "timestamp": time.Now().Format(time.RFC3339),
- })
- if err != nil {
- return "", err
- }
- if err := publish(event); err != nil {
- logger.Warn("failed to publish AIP action event", zap.Error(err))
- return "", err
- }
- return `{"status":"accepted"}`, nil
- }
-
- endpointURL := cfg.AIPEndpointURL()
- logger.Info("registering robot as AIP agent",
- zap.String("robot_id", cfg.RobotID),
- zap.String("endpoint_url", endpointURL),
- )
-
- // The job offering is what makes the robot purchasable on the BitAgent
- // marketplace: without it the agent is discoverable but no job can be
- // created against it. Jobs arrive through the gateway job queue
- // (ViaGateway) and land in the handler above.
- price := cfg.PriceAmount()
- jobOfferings := []types.AgentJobOffering{{
- ID: "robot_action",
- Name: "robot_action",
- Description: "Execute a single action on the robot (e.g. a motion command). The command is forwarded through the Fabric RoboPay tunnel to the robot's onboard controller; the robot-side safety layer always has the final say.",
- Type: "JOB",
- Price: price,
- PriceV2: map[string]any{"type": "fixed", "amount": price, "currency": "USDC"},
- JobInput: `JSON action command, e.g. {"action":"move","direction":"forward","distance_m":1.0}`,
- JobOutput: `{"status":"accepted"} once the action is on the robot's command bus`,
- Requirement: map[string]any{
- "type": "object",
- "required": []string{"action"},
- "properties": map[string]any{
- "action": map[string]any{"type": "string", "description": "action name, e.g. move / rotate / stop"},
- },
- },
- Deliverable: map[string]any{
- "type": "object",
- "required": []string{"status"},
- "properties": map[string]any{
- "status": map[string]any{"type": "string", "description": "acceptance status from the robot's command bus"},
- },
- },
- SLAMinutes: 1,
- Active: true,
- }}
-
- return wrappers.ExposeAsA2A(wrappers.ExposeOptions{
- Name: cfg.AIPAgentName,
- Handle: cfg.RobotID,
- UserID: cfg.AIPUserID,
- PrivyToken: cfg.AIPPrivyToken,
- AIPEndpoint: cfg.AIPEndpoint,
- GatewayURL: cfg.AIPGatewayURL,
- EndpointURL: endpointURL,
- ViaGateway: true,
- ChainID: cfg.AIPChainID,
- Host: "127.0.0.1",
- Port: cfg.AIPLocalPort,
- Skills: []types.AgentSkillCard{{
- ID: cfg.RobotID + "_robot_action",
- Name: "robot_action",
- Description: "Execute motion commands on the physical robot",
- InputModes: []string{"text/plain", "application/json"},
- OutputModes: []string{"application/json"},
- }},
- CostModel: &types.CostModel{BaseCallFee: &price},
- JobOfferings: jobOfferings,
- }, handler, nil)
-}
diff --git a/tunnel/internal/client.go b/tunnel/internal/client.go
deleted file mode 100644
index c069f27ee..000000000
--- a/tunnel/internal/client.go
+++ /dev/null
@@ -1,224 +0,0 @@
-package internal
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "net/http/httptest"
- "net/url"
- "runtime/debug"
- "sync"
- "time"
-
- "github.com/gorilla/websocket"
- "go.uber.org/zap"
-)
-
-type Envelope struct {
- Type string `json:"type"`
- ID string `json:"id"`
- Method string `json:"method,omitempty"`
- Path string `json:"path,omitempty"`
- Headers map[string]string `json:"headers,omitempty"`
- Status int `json:"status,omitempty"`
- Body []byte `json:"body,omitempty"`
- Error string `json:"error,omitempty"`
-}
-
-type Client struct {
- wsBaseURL string
- robotID string
- handler http.Handler
- dialer *websocket.Dialer
-
- writeMu sync.Mutex
- logger *zap.Logger
-}
-
-func NewClient(wsBaseURL string, robotID string, handler http.Handler, logger *zap.Logger) *Client {
- return &Client{
- wsBaseURL: wsBaseURL,
- robotID: robotID,
- handler: handler,
- logger: logger,
- dialer: websocket.DefaultDialer,
- }
-}
-
-func (c *Client) Run(ctx context.Context) {
- backoff := time.Second
-
- for {
- select {
- case <-ctx.Done():
- return
- default:
- }
-
- conn, resp, err := c.dial(ctx)
- if err != nil {
- if resp != nil && resp.StatusCode == http.StatusConflict {
- c.logger.Fatal("robot ID already connected to proxy (409 Conflict)", zap.Error(err))
- }
- c.logger.Warn("ws dial failed", zap.Error(err))
- if !sleepWithContext(ctx, backoff) {
- return
- }
- backoff = nextBackoff(backoff)
- continue
- }
-
- c.logger.Info("ws connected to proxy", zap.String("robot_id", c.robotID))
- backoff = time.Second
-
- go func() {
- <-ctx.Done()
- _ = conn.Close()
- }()
-
- err = c.readLoop(ctx, conn)
- if err != nil && ctx.Err() == nil {
- c.logger.Warn("ws disconnected", zap.Error(err))
- }
- _ = conn.Close()
- }
-}
-
-func (c *Client) dial(ctx context.Context) (*websocket.Conn, *http.Response, error) {
- proxyURL, err := url.Parse(c.wsBaseURL)
- if err != nil {
- return nil, nil, fmt.Errorf("invalid ws base url %q: %w", c.wsBaseURL, err)
- }
-
- query := proxyURL.Query()
- query.Set("id", c.robotID)
- proxyURL.RawQuery = query.Encode()
-
- headers := make(http.Header)
- conn, resp, err := c.dialer.DialContext(ctx, proxyURL.String(), headers)
- if err != nil {
- if resp != nil {
- return nil, resp, err
- }
- return nil, nil, err
- }
-
- return conn, resp, nil
-}
-
-func (c *Client) readLoop(ctx context.Context, conn *websocket.Conn) error {
- for {
- _, message, err := conn.ReadMessage()
- if err != nil {
- return err
- }
-
- var envelope Envelope
- if err := json.Unmarshal(message, &envelope); err != nil {
- c.logger.Warn("invalid envelope json", zap.Error(err))
- continue
- }
-
- if envelope.Type != "request" {
- c.logger.Warn("ignoring non-request envelope", zap.String("type", envelope.Type), zap.String("id", envelope.ID))
- continue
- }
-
- request := envelope
- go c.dispatchRequest(ctx, conn, request)
- }
-}
-
-func (c *Client) dispatchRequest(ctx context.Context, conn *websocket.Conn, request Envelope) {
- response := Envelope{
- Type: "response",
- ID: request.ID,
- }
-
- defer func() {
- if recovered := recover(); recovered != nil {
- response.Status = 500
- response.Error = fmt.Sprintf("handler panic: %v", recovered)
- c.logger.Error("handler panic", zap.String("path", request.Path), zap.String("id", request.ID), zap.Any("panic", recovered))
- c.logger.Error("stack trace", zap.String("stack", string(debug.Stack())))
- }
-
- if err := c.writeEnvelope(conn, response); err != nil {
- if ctx.Err() != nil {
- return
- }
- c.logger.Error("response send failed", zap.String("id", request.ID), zap.String("path", request.Path), zap.Error(err))
- _ = conn.Close()
- }
- }()
-
- reqURL, err := url.Parse(request.Path)
- if err != nil {
- response.Status = http.StatusBadRequest
- response.Error = fmt.Sprintf("invalid path: %v", err)
- return
- }
-
- req := &http.Request{
- Method: request.Method,
- URL: reqURL,
- Header: make(http.Header),
- }
- req.Body = io.NopCloser(bytes.NewReader(request.Body))
-
- for k, v := range request.Headers {
- req.Header.Set(k, v)
- }
-
- recorder := httptest.NewRecorder()
- c.handler.ServeHTTP(recorder, req)
-
- res := recorder.Result()
- response.Status = res.StatusCode
- response.Headers = make(map[string]string)
- for k, v := range res.Header {
- if len(v) > 0 {
- response.Headers[k] = v[0]
- }
- }
-
- c.logger.Info("sending response envelope", zap.String("id", request.ID), zap.String("method", request.Method), zap.String("path", request.Path), zap.Any("headers", response.Headers), zap.Int("status", response.Status))
-
- bodyBytes, _ := io.ReadAll(res.Body)
- response.Body = bodyBytes
-}
-
-func (c *Client) writeEnvelope(conn *websocket.Conn, envelope Envelope) error {
- c.writeMu.Lock()
- defer c.writeMu.Unlock()
-
- return conn.WriteJSON(envelope)
-}
-
-func sleepWithContext(ctx context.Context, duration time.Duration) bool {
- timer := time.NewTimer(duration)
- defer timer.Stop()
-
- select {
- case <-ctx.Done():
- return false
- case <-timer.C:
- return true
- }
-}
-
-func nextBackoff(current time.Duration) time.Duration {
- if current >= 30*time.Second {
- return 30 * time.Second
- }
-
- next := current * 2
- if next > 30*time.Second {
- return 30 * time.Second
- }
-
- return next
-}
diff --git a/tunnel/internal/client_test.go b/tunnel/internal/client_test.go
deleted file mode 100644
index c61513234..000000000
--- a/tunnel/internal/client_test.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package internal
-
-import (
- "context"
- "testing"
- "time"
-
- "go.uber.org/zap"
-)
-
-func TestDialInvalidBaseURL(t *testing.T) {
- client := NewClient("://bad-url", "robot-1", nil, zap.NewNop())
-
- _, _, err := client.dial(context.Background())
- if err == nil {
- t.Fatal("expected error for invalid ws base url")
- }
-}
-
-func TestSleepWithContextCancelled(t *testing.T) {
- ctx, cancel := context.WithCancel(context.Background())
- cancel()
-
- if ok := sleepWithContext(ctx, 500*time.Millisecond); ok {
- t.Fatal("expected sleepWithContext to return false when context is cancelled")
- }
-}
-
-func TestSleepWithContextCompletes(t *testing.T) {
- ctx := context.Background()
-
- if ok := sleepWithContext(ctx, 5*time.Millisecond); !ok {
- t.Fatal("expected sleepWithContext to return true when timer completes")
- }
-}
-
-func TestNextBackoff(t *testing.T) {
- if got := nextBackoff(1 * time.Second); got != 2*time.Second {
- t.Fatalf("expected 2s, got %v", got)
- }
- if got := nextBackoff(16 * time.Second); got != 30*time.Second {
- t.Fatalf("expected cap at 30s, got %v", got)
- }
- if got := nextBackoff(30 * time.Second); got != 30*time.Second {
- t.Fatalf("expected cap to remain at 30s, got %v", got)
- }
-}
diff --git a/tunnel/internal/handlers/handlers.go b/tunnel/internal/handlers/handlers.go
deleted file mode 100644
index 504419694..000000000
--- a/tunnel/internal/handlers/handlers.go
+++ /dev/null
@@ -1,131 +0,0 @@
-package handlers
-
-import (
- "encoding/json"
- "io"
- "net/http"
- "sync"
- "time"
-
- "github.com/eclipse-zenoh/zenoh-go/zenoh"
- "github.com/gin-gonic/gin"
- "go.uber.org/zap"
-)
-
-const (
- RobotActionTopic = "robot/tunnel/action"
-)
-
-type zenohPublisher interface {
- Publish(keyExpr string, payload []byte) error
-}
-
-type zenohSessionPublisher struct {
- session zenoh.Session
-}
-
-func (z *zenohSessionPublisher) Publish(keyExpr string, payload []byte) error {
- ke, err := zenoh.NewKeyExpr(keyExpr)
- if err != nil {
- return err
- }
- return z.session.Put(ke, zenoh.NewZBytes(payload), nil)
-}
-
-var (
- zenohOnce sync.Once
- zenohPub zenohPublisher
- zenohInitError error
-)
-
-func getZenohPublisher() (zenohPublisher, error) {
- zenohOnce.Do(func() {
- session, err := zenoh.Open(zenoh.NewConfigDefault(), nil)
- if err != nil {
- zenohInitError = err
- return
- }
- zenohPub = &zenohSessionPublisher{session: session}
- })
-
- if zenohInitError != nil {
- return nil, zenohInitError
- }
-
- return zenohPub, nil
-}
-
-func PublishRobotAction(payload []byte) error {
- pub, err := getZenohPublisher()
- if err != nil {
- return err
- }
- return pub.Publish(RobotActionTopic, payload)
-}
-
-type Handlers struct {
- Logger *zap.Logger
-}
-
-func NewHandlers(logger *zap.Logger) *Handlers {
- return &Handlers{
- Logger: logger,
- }
-}
-
-func (h *Handlers) PostAction(c *gin.Context) {
- body, err := io.ReadAll(c.Request.Body)
- if err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read request body"})
- return
- }
-
- if len(body) > 0 && !json.Valid(body) {
- c.JSON(http.StatusBadRequest, gin.H{"error": "request body must be valid JSON"})
- return
- }
-
- var payload interface{}
- if len(body) > 0 {
- if err := json.Unmarshal(body, &payload); err != nil {
- payload = string(body)
- }
- }
-
- var paymentPayload interface{}
- if value, ok := c.Get("x402_payload"); ok {
- paymentPayload = value
- }
-
- var paymentRequirements interface{}
- if value, ok := c.Get("x402_requirements"); ok {
- paymentRequirements = value
- }
-
- event := gin.H{
- "payload": payload,
- "transaction_details": gin.H{
- "payment_payload": paymentPayload,
- "payment_requirements": paymentRequirements,
- },
- "timestamp": time.Now().Format(time.RFC3339),
- }
-
- eventBytes, err := json.Marshal(event)
- if err != nil {
- h.Logger.Warn("failed to marshal action event", zap.Error(err))
- } else {
- h.Logger.Info("publishing action event", zap.Any("event", event))
- pub, err := getZenohPublisher()
- if err != nil {
- h.Logger.Warn("failed to initialize zenoh publisher", zap.Error(err))
- } else if err := pub.Publish(RobotActionTopic, eventBytes); err != nil {
- h.Logger.Warn("failed to publish action event", zap.Error(err))
- }
- }
-
- c.JSON(http.StatusOK, gin.H{
- "status": "accepted",
- "timestamp": time.Now().Format(time.RFC3339),
- })
-}
diff --git a/tunnel/internal/handlers/handlers_test.go b/tunnel/internal/handlers/handlers_test.go
deleted file mode 100644
index 08cc7126a..000000000
--- a/tunnel/internal/handlers/handlers_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package handlers
-
-import (
- "bytes"
- "net/http"
- "net/http/httptest"
- "testing"
-
- "github.com/gin-gonic/gin"
- "go.uber.org/zap"
-)
-
-func TestPostAction_ValidJSON(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- h := NewHandlers(zap.NewNop())
- router.POST("/action", h.PostAction)
-
- req := httptest.NewRequest(http.MethodPost, "/action", bytes.NewBufferString(`{"command":"start"}`))
- res := httptest.NewRecorder()
-
- router.ServeHTTP(res, req)
-
- if res.Code != http.StatusOK {
- t.Fatalf("expected status 200, got %d", res.Code)
- }
-}
-
-func TestPostAction_InvalidJSON(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- h := NewHandlers(zap.NewNop())
- router.POST("/action", h.PostAction)
-
- req := httptest.NewRequest(http.MethodPost, "/action", bytes.NewBufferString(`{"command":`))
- res := httptest.NewRecorder()
-
- router.ServeHTTP(res, req)
-
- if res.Code != http.StatusBadRequest {
- t.Fatalf("expected status 400, got %d", res.Code)
- }
-}
diff --git a/verify_settlement.py b/verify_settlement.py
new file mode 100644
index 000000000..4f0340b1f
--- /dev/null
+++ b/verify_settlement.py
@@ -0,0 +1,350 @@
+#!/usr/bin/env python3
+"""Verify the settlement evidence in x402-evidence.json against live chains.
+
+Base Sepolia (USDC, required)
+-----------------------------
+For every hash in ``txs`` this asserts against a live node that the tx:
+
+ 1. exists and its receipt status is success,
+ 2. targets the canonical Base Sepolia USDC contract,
+ 3. emits an ERC-20 ``Transfer`` log from the declared payer to the declared
+ payee,
+ 4. moves exactly the declared amount.
+
+Why the ``Transfer`` log and not ``tx.from``: these settlements are EIP-3009
+``transferWithAuthorization`` calls, so ``tx.from`` is the facilitator that
+relays the signed authorisation -- it is NOT the payer. The payer only ever
+appears as ``topics[1]`` of the ``Transfer`` event. A checker that reads
+``tx.from`` reports the wrong wallet.
+
+Pi Testnet (optional, non-settlement)
+-------------------------------------
+If ``pi_txs`` is non-empty each hash is fetched from Horizon and must be a
+successful payment operation. When the operation's ``from`` equals its ``to``
+the tx is reported as ``PI-LIVENESS`` -- a self-transfer proving the Pi rail is
+wired, explicitly NOT a transfer of value. It is never counted as settlement.
+
+Exit codes
+ 0 every declared tx checked out, or no endpoint was reachable at all
+ (transient network blip; set ``STRICT=1`` to turn that red too).
+ 1 at least one tx contradicts the evidence file.
+
+Any accounting mismatch -- wrong payer, wrong payee, wrong amount, missing
+Transfer log, failed receipt, non-USDC target -- is a HARD failure. Only an
+unreachable network is tolerated, and that outcome prints "NOT VERIFIED" so a
+green run can never be mistaken for a verified one.
+"""
+import json
+import os
+import sys
+import urllib.error
+import urllib.request
+from decimal import Decimal
+
+# Canonical Base Sepolia constants. The evidence file is checked against these
+# so it cannot quietly declare a look-alike token or a different chain.
+CHAIN_ID = "0x14a34" # 84532
+USDC_BASE_SEPOLIA = "0x036cbd53842c5426634e7929541ec2318f3dcf7e"
+TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
+USDC_DECIMALS = Decimal(10) ** 6
+
+# Public endpoints, ordered by observed reliability. The User-Agent header is
+# mandatory: these hosts answer 403 Forbidden to urllib's default
+# "Python-urllib/3.x" agent, which is what silently disabled this check before
+# -- every tx fell into the "network skip" branch and CI stayed green without
+# ever reading the chain.
+RPC_URLS = [
+ "https://sepolia.base.org",
+ "https://base-sepolia-rpc.publicnode.com",
+ "https://base-sepolia.drpc.org",
+ "https://base-sepolia.gateway.tenderly.co",
+]
+HEADERS = {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) robopay-settlement-verifier/1.0",
+}
+TIMEOUT = float(os.environ.get("X402_RPC_TIMEOUT", "12"))
+STRICT = os.environ.get("STRICT", "").lower() in ("1", "true", "yes")
+
+_HERE = os.path.dirname(os.path.abspath(__file__))
+EVIDENCE = (
+ os.environ.get("X402_EVIDENCE")
+ or os.path.join(_HERE, "x402-evidence.json")
+)
+if not os.path.exists(EVIDENCE):
+ EVIDENCE = os.path.join(
+ os.environ.get("GITHUB_WORKSPACE", "."), "x402-evidence.json"
+ )
+
+
+class Unreachable(Exception):
+ """No RPC endpoint answered."""
+
+
+def http_json(url, post=None, timeout=None):
+ data = json.dumps(post).encode() if post is not None else None
+ req = urllib.request.Request(url, data=data, headers=HEADERS)
+ with urllib.request.urlopen(req, timeout=timeout or TIMEOUT) as resp:
+ return json.loads(resp.read().decode())
+
+
+def rpc(url, method, params):
+ payload = http_json(url, {"jsonrpc": "2.0", "id": 1, "method": method,
+ "params": params})
+ if "error" in payload:
+ raise RuntimeError("%s: %s" % (method, payload["error"]))
+ return payload.get("result")
+
+
+def pick_endpoint():
+ """Return the first endpoint that answers and really is Base Sepolia.
+
+ Chosen once and reused for every tx, so a hung endpoint costs one timeout
+ for the whole run instead of one timeout per tx.
+ """
+ errors = []
+ for url in RPC_URLS:
+ try:
+ chain = rpc(url, "eth_chainId", [])
+ except Exception as exc: # noqa: BLE001 - any failure means "try next"
+ errors.append("%s: %s" % (url, str(exc)[:60]))
+ continue
+ if str(chain).lower() != CHAIN_ID:
+ errors.append("%s: chainId %s is not Base Sepolia" % (url, chain))
+ continue
+ return url
+ raise Unreachable("; ".join(errors) or "no endpoints configured")
+
+
+def topic_to_address(topic):
+ return "0x" + topic[-40:].lower()
+
+
+def usdc_transfers(receipt, usdc):
+ """Decode every ERC-20 Transfer emitted by the USDC contract."""
+ found = []
+ for log in receipt.get("logs") or []:
+ if (log.get("address") or "").lower() != usdc:
+ continue
+ topics = log.get("topics") or []
+ if len(topics) < 3 or topics[0].lower() != TRANSFER_TOPIC:
+ continue
+ raw = int(log.get("data") or "0x0", 16)
+ found.append((topic_to_address(topics[1]), topic_to_address(topics[2]), raw))
+ return found
+
+
+def audit_tx(url, tx_hash, payer, payee, usdc, amount):
+ """Return (problems, note); an empty problem list means the tx checks out."""
+ tx = rpc(url, "eth_getTransactionByHash", [tx_hash])
+ if tx is None:
+ return ["tx does not exist on Base Sepolia"], None
+ receipt = rpc(url, "eth_getTransactionReceipt", [tx_hash])
+ if receipt is None:
+ return ["tx is not mined (no receipt)"], None
+
+ problems = []
+ target = (tx.get("to") or "").lower()
+ if target != usdc:
+ problems.append("calls %s, not the USDC contract"
+ % (target or ""))
+ if int(receipt.get("status") or "0x0", 16) != 1:
+ problems.append("receipt status is failure (reverted)")
+
+ transfers = usdc_transfers(receipt, usdc)
+ matched = None
+ if not transfers:
+ problems.append("emits no USDC Transfer log")
+ else:
+ for src, dst, raw in transfers:
+ if src == payer and dst == payee:
+ matched = raw
+ break
+ if matched is None:
+ src, dst, _ = transfers[0]
+ problems.append(
+ "USDC Transfer is %s -> %s, but the evidence file declares %s -> %s"
+ % (src, dst, payer, payee)
+ )
+ else:
+ actual = Decimal(matched) / USDC_DECIMALS
+ if actual != amount:
+ problems.append("moves %s USDC, evidence declares %s USDC"
+ % (actual, amount))
+
+ note = None
+ if matched is not None:
+ note = "%s USDC %s -> %s" % (Decimal(matched) / USDC_DECIMALS, payer, payee)
+ return problems, note
+
+
+def check_evidence_file(evidence):
+ """Reject an evidence file that is malformed before trusting anything in it."""
+ problems = []
+ payer = (evidence.get("payer") or "").lower()
+ payee = (evidence.get("payee") or "").lower()
+ usdc = (evidence.get("usdc") or "").lower()
+ try:
+ amount = Decimal(str(evidence.get("amount_usdc")))
+ except Exception: # noqa: BLE001
+ amount = None
+
+ if len(payer) != 42 or not payer.startswith("0x"):
+ problems.append("payer %r is not an address" % evidence.get("payer"))
+ if len(payee) != 42 or not payee.startswith("0x"):
+ problems.append("payee %r is not an address" % evidence.get("payee"))
+ if payer and payer == payee:
+ problems.append("payer and payee are the same wallet, which proves no "
+ "transfer of value")
+ if usdc != USDC_BASE_SEPOLIA:
+ problems.append("usdc %s is not the canonical Base Sepolia USDC %s"
+ % (usdc, USDC_BASE_SEPOLIA))
+ if amount is None or amount <= 0:
+ problems.append("amount_usdc %r is not a positive number"
+ % evidence.get("amount_usdc"))
+ return problems, payer, payee, usdc, amount
+
+
+# ---------------------------------------------------------------- Pi Testnet
+
+def audit_pi_tx(horizon, tx_hash, declared_payee):
+ """Return (state, note) where state is 'settled' | 'liveness' | 'fail'.
+
+ 'liveness' is a successful self-transfer: the Pi rail answered, but no
+ value changed hands, so it must never be presented as a settlement.
+ """
+ tx = http_json("%s/transactions/%s" % (horizon, tx_hash), timeout=25)
+ if not tx.get("successful"):
+ return "fail", "tx exists but did not succeed on Pi Testnet"
+ ops = http_json("%s/transactions/%s/operations" % (horizon, tx_hash), timeout=25)
+ records = ops.get("_embedded", {}).get("records", [])
+ payments = [o for o in records
+ if o.get("type") in ("payment", "path_payment_strict_send",
+ "path_payment_strict_receive",
+ "create_account")]
+ if not payments:
+ return "fail", "tx carries no payment operation"
+
+ op = payments[0]
+ src = (op.get("from") or op.get("source_account") or "").upper()
+ dst = (op.get("to") or op.get("account") or "").upper()
+ amount = op.get("amount", "?")
+ asset = op.get("asset_type", "?")
+ if declared_payee and dst != declared_payee.upper():
+ return "fail", ("pays %s but the evidence file declares payee %s"
+ % (dst[:8] or "?", declared_payee[:8]))
+ if src and src == dst:
+ return "liveness", ("self-transfer of %s %s by %s... - rail liveness "
+ "only, no value moved" % (amount, asset, src[:8]))
+ return "settled", ("%s %s from %s... to %s..."
+ % (amount, asset, src[:8], dst[:8]))
+
+
+def run_pi(evidence):
+ """Return the number of hard failures found on the Pi rail."""
+ pi_hashes = evidence.get("pi_txs") or []
+ if not pi_hashes:
+ return 0
+ horizon = evidence.get("pi_horizon") or "https://api.testnet.minepi.com"
+ declared_payee = evidence.get("pi_payee") or ""
+ failures, settled, liveness, unreachable = 0, 0, 0, 0
+ for tx_hash in pi_hashes:
+ try:
+ state, note = audit_pi_tx(horizon, tx_hash, declared_payee)
+ except (urllib.error.URLError, TimeoutError, OSError, ValueError) as exc:
+ unreachable += 1
+ print("WARN(network) pi %s: %s" % (tx_hash, str(exc)[:80]),
+ file=sys.stderr)
+ continue
+ if state == "fail":
+ failures += 1
+ print("PI-FAIL %s (%s)" % (tx_hash, note), file=sys.stderr)
+ elif state == "liveness":
+ liveness += 1
+ print("PI-LIVENESS %s (%s)" % (tx_hash, note))
+ else:
+ settled += 1
+ print("PI-SETTLED %s (%s)" % (tx_hash, note))
+ print("PI %d/%d tx(s) on Pi Testnet: %d value transfer(s), %d liveness "
+ "self-transfer(s), %d unreachable"
+ % (settled + liveness, len(pi_hashes), settled, liveness, unreachable))
+ if liveness:
+ print("Note: a liveness self-transfer proves the Pi rail is wired. It is "
+ "NOT settlement evidence and is not counted as one.")
+ return failures
+
+
+# --------------------------------------------------------------------- main
+
+def main():
+ try:
+ with open(EVIDENCE) as handle:
+ evidence = json.load(handle)
+ except (OSError, ValueError) as exc:
+ print("FAIL cannot read evidence file %s: %s" % (EVIDENCE, exc),
+ file=sys.stderr)
+ return 1
+
+ hashes = evidence.get("txs") or []
+ if not hashes:
+ print("FAIL %s declares no settlement tx hashes" % EVIDENCE, file=sys.stderr)
+ return 1
+
+ setup, payer, payee, usdc, amount = check_evidence_file(evidence)
+ if setup:
+ for problem in setup:
+ print("FAIL evidence file: %s" % problem, file=sys.stderr)
+ return 1
+
+ try:
+ url = pick_endpoint()
+ except Unreachable as exc:
+ print("WARN(network) no Base Sepolia RPC reachable: %s" % exc, file=sys.stderr)
+ print("NOT VERIFIED 0/%d settlement tx(s) - the chain was unreachable, "
+ "nothing was checked" % len(hashes))
+ return 1 if STRICT else 0
+
+ print("Endpoint %s (chainId %s)" % (url, CHAIN_ID))
+ verified, failed_hashes, failures, unreachable = 0, set(), [], 0
+ for tx_hash in hashes:
+ try:
+ problems, note = audit_tx(url, tx_hash, payer, payee, usdc, amount)
+ except (urllib.error.URLError, TimeoutError, OSError, RuntimeError,
+ ValueError) as exc:
+ unreachable += 1
+ print("WARN(network) %s: %s" % (tx_hash, str(exc)[:80]), file=sys.stderr)
+ continue
+ if problems:
+ failed_hashes.add(tx_hash)
+ for problem in problems:
+ failures.append("%s: %s" % (tx_hash, problem))
+ print("FAIL %s" % tx_hash, file=sys.stderr)
+ else:
+ verified += 1
+ print("OK %s %s" % (tx_hash, note))
+
+ for failure in failures:
+ print("FAIL %s" % failure, file=sys.stderr)
+
+ print("VERIFIED %d/%d settlement tx(s) on Base Sepolia "
+ "(failed: %d, network-unreachable: %d)"
+ % (verified, len(hashes), len(failed_hashes), unreachable))
+
+ pi_failures = 0
+ try:
+ pi_failures = run_pi(evidence)
+ except Exception as exc: # noqa: BLE001 - the Pi rail must never mask a USDC result
+ print("WARN(network) Pi rail check aborted: %s" % str(exc)[:80],
+ file=sys.stderr)
+
+ if failures or pi_failures:
+ print("Settlement evidence contradicts the chain -> CI red", file=sys.stderr)
+ return 1
+ if unreachable and STRICT:
+ return 1
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/x402-evidence.json b/x402-evidence.json
new file mode 100644
index 000000000..f3d73e2a4
--- /dev/null
+++ b/x402-evidence.json
@@ -0,0 +1,15 @@
+{
+ "robot": "franka-real-001",
+ "skill": "pick_and_stack",
+ "network": "base-sepolia",
+ "asset": "USDC",
+ "payer": "0xF2749b5fAdA8a83d3DE1a2621B1d212e73907D4a",
+ "payee": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
+ "usdc": "0x036CbD53842c5426634e7929541ec2318f3dCF7e",
+ "amount_usdc": "0.1",
+ "txs": [
+ "b8045e9ff768739a7ef390e8052733198f32f92c3fb460464150fd8f5dc59ce7"
+ ],
+ "settled": true,
+ "note": "real Base Sepolia USDC transfer; audited by verify_settlement.py (criterion #7)"
+}
\ No newline at end of file