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