diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 383c7adf7..66fc8f07f 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1,19 +1,30 @@ -openapi: 3.0.0 +openapi: 3.0.3 info: title: RustChain Node API - description: Public API documentation for the RustChain Node endpoints. - version: 1.0.0 + description: | + REST API for RustChain blockchain node. + + RustChain is a Proof-of-Antiquity blockchain that rewards vintage hardware. + version: 2.2.1-rip200 + contact: + name: RustChain + url: https://rustchain.org + license: + name: MIT servers: - - url: https://rustchain.org - description: Public Live Node + - url: https://50.28.86.131 + description: Mainnet node paths: /health: get: summary: Node health check - description: Returns health status, uptime, and version. + description: Returns the health status of the node + operationId: getHealth + tags: + - Node responses: '200': - description: OK + description: Node is healthy content: application/json: schema: @@ -21,48 +32,42 @@ paths: properties: ok: type: boolean - uptime_s: - type: integer + description: Whether node is operational version: type: string - backup_age_hours: - type: number + description: Node software version + uptime_s: + type: integer + description: Node uptime in seconds db_rw: type: boolean + description: Database read/write status + backup_age_hours: + type: number + description: Age of last backup in hours tip_age_slots: type: integer - example: - ok: true - uptime_s: 58480 - version: "2.2.1-rip200" - backup_age_hours: 13.65 - db_rw: true - tip_age_slots: 0 - + description: Age of chain tip in slots /ready: get: summary: Readiness probe - description: Indicates if the node is fully synced and ready to serve traffic. + description: Check if node is ready to accept requests + operationId: getReady + tags: + - Node responses: '200': - description: OK - content: - application/json: - schema: - type: object - properties: - ready: - type: boolean - example: - ready: true - + description: Node is ready /epoch: get: - summary: Current epoch status - description: Returns current epoch, slot, and enrolled miners. + summary: Current epoch information + description: Returns current epoch details including slot and enrolled miners + operationId: getEpoch + tags: + - Epoch responses: '200': - description: OK + description: Current epoch info content: application/json: schema: @@ -70,158 +75,224 @@ paths: properties: epoch: type: integer + description: Current epoch number slot: type: integer - enrolled_miners: - type: integer + description: Current slot within epoch blocks_per_epoch: type: integer + description: Number of blocks per epoch epoch_pot: type: number + description: Epoch reward pot (RTC) + enrolled_miners: + type: integer + description: Number of enrolled miners total_supply_rtc: - type: number - example: - epoch: 91 - slot: 13227 - enrolled_miners: 20 - blocks_per_epoch: 144 - epoch_pot: 1.5 - total_supply_rtc: 8388608 - - /api/miners: - get: - summary: Active miners - description: Returns list of active miners with attestation data. - responses: - '200': - description: OK - content: - application/json: - schema: - type: array - items: - type: object - properties: - miner_id: - type: string - attested: - type: boolean - example: - - miner_id: "m_12345" - attested: true - + type: integer + description: Total RTC supply /api/stats: get: summary: Network statistics - description: Returns overall network statistics. + description: Returns comprehensive network statistics + operationId: getNetworkStats + tags: + - Network responses: '200': - description: OK + description: Network statistics content: application/json: schema: type: object properties: - total_blocks: + version: + type: string + description: Node software version + chain_id: + type: string + description: Chain identifier + epoch: + type: integer + block_time: type: integer - total_transactions: + description: Block time in seconds + total_miners: type: integer - example: - total_blocks: 150000 - total_transactions: 1205000 - + description: Total number of miners + total_balance: + type: number + description: Total RTC balance + pending_withdrawals: + type: integer + description: Number of pending withdrawals + features: + type: array + items: + type: string + description: Enabled network features + security: + type: array + items: + type: string + description: Security features enabled /api/hall_of_fame: get: summary: Hall of Fame leaderboard - description: Leaderboard for 5 categories of miners/participants. + description: Returns the Hall of Fame leaderboard with 5 categories + operationId: getHallOfFame + tags: + - Network responses: '200': - description: OK + description: Hall of Fame data content: application/json: schema: type: object properties: - top_miners: - type: array - items: - type: object - example: - top_miners: [] - + categories: + type: object + properties: + ancient_iron: + type: array + items: + type: object + description: Oldest hardware + exotic_arch: + type: array + items: + type: object + description: Most exotic architectures + fleet_commanders: + type: array + items: + type: object + description: Most machines operated + most_dedicated: + type: array + items: + type: object + description: Most epochs participated + top_earners: + type: array + items: + type: object + description: Highest RTC balances + epoch: + type: object + description: Current epoch info + generated_at: + type: integer + description: Timestamp of generation + stats: + type: object + description: Overall statistics + spotlight: + type: object + description: Featured miner /api/fee_pool: get: summary: Fee pool statistics - description: RIP-301 fee pool statistics (fees recycled to mining pool). + description: Returns RIP-301 fee pool statistics (fees recycled to mining pool) + operationId: getFeePool + tags: + - Network responses: '200': - description: OK + description: Fee pool statistics content: application/json: schema: type: object properties: + rip: + type: integer + description: RIP number (301) description: type: string - destination: - type: string - destination_balance_rtc: + total_fees_collected_rtc: type: number - rip: - type: integer + description: Total fees collected total_fee_events: type: integer - total_fees_collected_rtc: - type: number + description: Number of fee events withdrawal_fee_rtc: type: number - example: - description: "Fee Pool Statistics" - destination: "founder_community" - destination_balance_rtc: 83246.13 - rip: 301 - total_fee_events: 0 - total_fees_collected_rtc: 0 - withdrawal_fee_rtc: 0.01 - - /balance: + description: Withdrawal fee + destination: + type: string + description: Fee destination + destination_balance_rtc: + type: number + description: Destination balance + fees_by_source: + type: object + description: Fees grouped by source + recent_events: + type: array + items: + type: object + description: Recent fee events + /api/miners: get: - summary: Miner balance lookup - description: Returns the RTC balance for a specific miner ID. - parameters: - - name: miner_id - in: query - required: true - schema: - type: string - description: Miner ID or public key + summary: List active miners + description: Returns list of all active miners with their hardware details + operationId: getMiners + tags: + - Miners responses: '200': - description: OK + description: List of active miners content: application/json: schema: - type: object - properties: - balance: - type: number - example: - balance: 150.5 - + type: array + items: + type: object + properties: + miner: + type: string + description: Miner identifier/name + antiquity_multiplier: + type: number + description: Hardware antiquity multiplier + device_arch: + type: string + description: CPU architecture + device_family: + type: string + description: Device family + hardware_type: + type: string + description: Type of hardware + entropy_score: + type: number + description: Entropy score + first_attest: + type: string + nullable: true + description: First attestation timestamp + last_attest: + type: integer + description: Last attestation timestamp /lottery/eligibility: get: - summary: Epoch eligibility check - description: Checks if a miner is eligible for the current epoch block lottery. + summary: Check lottery eligibility + description: Check if a miner is eligible for the current lottery + operationId: checkEligibility + tags: + - Lottery parameters: - name: miner_id in: query required: true schema: type: string - description: Miner ID or public key + description: Miner wallet ID responses: '200': - description: OK + description: Eligibility status content: application/json: schema: @@ -229,37 +300,62 @@ paths: properties: eligible: type: boolean - reason: + description: Whether miner is eligible + slot: + type: integer + description: Assigned slot if eligible + slot_producer: type: string + nullable: true + description: Slot producer rotation_size: type: integer - slot: - type: integer - example: - eligible: false - reason: "not_attested" - rotation_size: 20 - slot: 13227 - - /explorer: + description: Current rotation size + reason: + type: string + description: Reason if not eligible + /wallet/balance: get: - summary: Block explorer page - description: Returns the HTML page for the block explorer. + summary: Get wallet balance + description: Get RTC balance for a wallet address + operationId: getBalance + tags: + - Wallet + parameters: + - name: miner_id + in: query + required: true + schema: + type: string + description: Miner wallet ID responses: '200': - description: HTML page returned + description: Wallet balance content: - text/html: + application/json: schema: - type: string - example: "..." - + type: object + properties: + balance: + type: number + description: Wallet balance in RTC + miner_id: + type: string + description: Miner ID /attest/submit: post: - summary: Submit hardware attestation - description: Submit an attestation proof. Requires X-Admin-Key. - security: - - AdminKeyAuth: [] + summary: Submit attestation + description: Submit hardware attestation to the network + operationId: submitAttestation + tags: + - Attestation + parameters: + - name: X-Admin-Key + in: header + required: false + schema: + type: string + description: Admin key for authenticated requests requestBody: required: true content: @@ -269,78 +365,162 @@ paths: properties: miner_id: type: string - proof: + description: Miner identifier + signature: type: string + description: Hardware attestation signature + payload: + type: object + description: Attestation payload data responses: '200': - description: Attestation accepted - + description: Attestation submitted successfully + '400': + description: Invalid attestation data + '401': + description: Unauthorized /wallet/transfer/signed: post: - summary: Ed25519 signed transfer - description: Submit a signed transfer transaction. Requires X-Admin-Key. - security: - - AdminKeyAuth: [] + summary: Transfer RTC (Ed25519 signed) + description: Transfer RTC between wallets using Ed25519 signature + operationId: transferSigned + tags: + - Wallet requestBody: required: true content: application/json: schema: type: object + required: + - from + - to + - amount + - private_key properties: - tx_payload: + from: type: string - signature: + description: Source wallet address + to: type: string + description: Destination wallet address + amount: + type: number + description: Amount to transfer in RTC + private_key: + type: string + description: Ed25519 private key for signing responses: '200': - description: Transfer accepted - + description: Transfer successful + '400': + description: Invalid transfer request /wallet/transfer: post: - summary: Admin transfer - description: Transfer funds directly as admin. Requires X-Admin-Key. - security: - - AdminKeyAuth: [] + summary: Transfer RTC (Admin) + description: Admin transfer requiring admin key authentication + operationId: transferAdmin + tags: + - Wallet + parameters: + - name: X-Admin-Key + in: header + required: true + schema: + type: string + description: Admin key for authentication requestBody: required: true content: application/json: schema: type: object + required: + - from + - to + - amount properties: + from: + type: string + description: Source wallet address to: type: string + description: Destination wallet address amount: type: number + description: Amount to transfer in RTC responses: '200': - description: Transfer executed - + description: Transfer successful + '401': + description: Unauthorized - invalid admin key + '403': + description: Forbidden - insufficient permissions /withdraw/request: post: - summary: Withdrawal request - description: Request a withdrawal. Requires X-Admin-Key. - security: - - AdminKeyAuth: [] + summary: Request withdrawal + description: Request a withdrawal of RTC to external wallet + operationId: requestWithdrawal + tags: + - Wallet + parameters: + - name: X-Admin-Key + in: header + required: true + schema: + type: string + description: Admin key for authentication requestBody: required: true content: application/json: schema: type: object + required: + - miner_id + - amount properties: miner_id: type: string + description: Miner wallet ID amount: type: number + description: Amount to withdraw in RTC responses: '200': - description: Withdrawal requested - -components: - securitySchemes: - AdminKeyAuth: - type: apiKey - in: header - name: X-Admin-Key + description: Withdrawal request submitted + '400': + description: Invalid withdrawal request + '401': + description: Unauthorized + /explorer: + get: + summary: Block explorer page + description: Returns the block explorer HTML page + operationId: getExplorer + tags: + - Explorer + responses: + '200': + description: HTML page + content: + text/html: + schema: + type: string +tags: + - name: Node + description: Node-level operations + - name: Epoch + description: Epoch information + - name: Network + description: Network statistics and leaderboards + - name: Miners + description: Miner operations + - name: Lottery + description: Lottery and eligibility + - name: Wallet + description: Wallet operations + - name: Attestation + description: Attestation operations + - name: Explorer + description: Block explorer diff --git a/mcp/rustchain_server/README.md b/mcp/rustchain_server/README.md new file mode 100644 index 000000000..7201a475f --- /dev/null +++ b/mcp/rustchain_server/README.md @@ -0,0 +1,31 @@ +# RustChain MCP Server + +A Model Context Protocol (MCP) server for interacting with RustChain blockchain from Claude Code and other MCP clients. + +## Installation + +```bash +# Install dependencies +pip install -e . + +# Add to Claude Code +claude mcp add rustchain-server python /path/to/rustchain_mcp.py +``` + +## Available Tools + +- `rustchain_balance` - Check RTC balance for any wallet +- `rustchain_miners` - List active miners and their architectures +- `rustchain_epoch` - Get current epoch info (slot, height, rewards) +- `rustchain_health` - Check node health across all 3 attestation nodes +- `rustchain_transfer` - Send RTC (requires wallet key) + +## API Endpoints + +The server connects to: +- Primary: https://50.28.86.131 +- Fallback: Node 2/3 if primary is down + +## License + +MIT diff --git a/mcp/rustchain_server/__init__.py b/mcp/rustchain_server/__init__.py new file mode 100644 index 000000000..947f2b798 --- /dev/null +++ b/mcp/rustchain_server/__init__.py @@ -0,0 +1 @@ +# RustChain MCP Server diff --git a/mcp/rustchain_server/pyproject.toml b/mcp/rustchain_server/pyproject.toml new file mode 100644 index 000000000..ed81b2f13 --- /dev/null +++ b/mcp/rustchain_server/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "rustchain-mcp" +version = "0.1.0" +description = "MCP Server for RustChain blockchain" +authors = [{name = "sososonia-cyber"}] +requires-python = ">=3.10" +dependencies = [ + "mcp>=1.0.0", + "requests>=2.31.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", +] + +[project.scripts] +rustchain-mcp = "rustchain_mcp:main" + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" diff --git a/mcp/rustchain_server/rustchain_mcp.py b/mcp/rustchain_server/rustchain_mcp.py new file mode 100644 index 000000000..5b07fd96b --- /dev/null +++ b/mcp/rustchain_server/rustchain_mcp.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +""" +RustChain MCP Server +Query the RustChain blockchain from Claude Code +""" + +import os +import json +import requests +from typing import Any, Optional +from datetime import datetime + +# MCP Server imports +from mcp.server import Server +from mcp.types import Tool, TextContent +from mcp.server.stdio import stdio_server + +# Node endpoints +NODES = [ + "https://50.28.86.131", # Primary + "https://50.28.86.153", # Node Beta + "https://50.28.86.154", # Node Gamma +] + + +def get_node_url() -> str: + """Get the first available node URL""" + return NODES[0] + + +def make_request(endpoint: str, params: Optional[dict] = None) -> dict: + """Make a request to the RustChain node with fallback""" + for node in NODES: + try: + url = f"{node}{endpoint}" + response = requests.get(url, params=params, timeout=10, verify=False) + if response.status_code == 200: + return response.json() + except Exception: + continue + return {"error": "All nodes unavailable"} + + +# Initialize MCP Server +app = Server("rustchain-mcp-server") + + +@app.list_tools() +async def list_tools() -> list[Tool]: + """List available tools""" + return [ + Tool( + name="rustchain_health", + description="Check the health status of all RustChain attestation nodes", + inputSchema={ + "type": "object", + "properties": {} + } + ), + Tool( + name="rustchain_miners", + description="List all active miners on the RustChain network with their architectures", + inputSchema={ + "type": "object", + "properties": { + "limit": { + "type": "integer", + "description": "Maximum number of miners to return", + "default": 50 + } + } + } + ), + Tool( + name="rustchain_epoch", + description="Get current epoch information including slot, height, and rewards", + inputSchema={ + "type": "object", + "properties": {} + } + ), + Tool( + name="rustchain_balance", + description="Check RTC token balance for any wallet address", + inputSchema={ + "type": "object", + "properties": { + "miner_id": { + "type": "string", + "description": "Wallet address or miner ID to check balance for" + } + }, + "required": ["miner_id"] + } + ), + Tool( + name="rustchain_transfer", + description="Transfer RTC tokens to another wallet address", + inputSchema={ + "type": "object", + "properties": { + "to_address": { + "type": "string", + "description": "Recipient wallet address" + }, + "amount": { + "type": "number", + "description": "Amount of RTC to transfer" + }, + "from_key": { + "type": "string", + "description": "Sender's private key or wallet key" + } + }, + "required": ["to_address", "amount", "from_key"] + } + ), + ] + + +@app.call_tool() +async def call_tool(name: str, arguments: Any) -> list[TextContent]: + """Handle tool calls""" + + if name == "rustchain_health": + result = check_health() + return [TextContent(type="text", text=json.dumps(result, indent=2))] + + elif name == "rustchain_miners": + limit = arguments.get("limit", 50) + result = get_miners(limit) + return [TextContent(type="text", text=json.dumps(result, indent=2))] + + elif name == "rustchain_epoch": + result = get_epoch() + return [TextContent(type="text", text=json.dumps(result, indent=2))] + + elif name == "rustchain_balance": + miner_id = arguments.get("miner_id") + if not miner_id: + return [TextContent(type="text", text="Error: miner_id is required")] + result = get_balance(miner_id) + return [TextContent(type="text", text=json.dumps(result, indent=2))] + + elif name == "rustchain_transfer": + to_address = arguments.get("to_address") + amount = arguments.get("amount") + from_key = arguments.get("from_key") + + if not all([to_address, amount, from_key]): + return [TextContent(type="text", text="Error: to_address, amount, and from_key are all required")] + + result = transfer_rtc(to_address, amount, from_key) + return [TextContent(type="text", text=json.dumps(result, indent=2))] + + return [TextContent(type="text", text=f"Unknown tool: {name}")] + + +def check_health() -> dict: + """Check health of all nodes""" + health_status = {} + + for i, node in enumerate(NODES): + try: + response = requests.get(f"{node}/health", timeout=5, verify=False) + health_status[f"node_{i+1}"] = { + "url": node, + "status": "online" if response.status_code == 200 else "error", + "status_code": response.status_code, + "response": response.json() if response.status_code == 200 else response.text[:200] + } + except Exception as e: + health_status[f"node_{i+1}"] = { + "url": node, + "status": "unreachable", + "error": str(e) + } + + return health_status + + +def get_miners(limit: int = 50) -> dict: + """Get list of active miners""" + data = make_request("/api/miners", {"limit": limit}) + return data + + +def get_epoch() -> dict: + """Get current epoch information""" + data = make_request("/epoch") + return data + + +def get_balance(miner_id: str) -> dict: + """Get balance for a wallet""" + data = make_request("/wallet/balance", {"miner_id": miner_id}) + return data + + +def transfer_rtc(to_address: str, amount: float, from_key: str) -> dict: + """Transfer RTC tokens""" + try: + response = requests.post( + f"{get_node_url()}/wallet/transfer", + json={ + "to": to_address, + "amount": amount, + "from_key": from_key + }, + timeout=30, + verify=False + ) + return response.json() if response.status_code == 200 else { + "error": f"HTTP {response.status_code}", + "message": response.text + } + except Exception as e: + return {"error": str(e)} + + +async def main(): + """Main entry point""" + async with stdio_server() as server: + server.run() + + +if __name__ == "__main__": + import asyncio + asyncio.run(main()) diff --git a/miners/clawrtc-cli/clawrtc/__init__.py b/miners/clawrtc-cli/clawrtc/__init__.py new file mode 100644 index 000000000..ea2fbe559 --- /dev/null +++ b/miners/clawrtc-cli/clawrtc/__init__.py @@ -0,0 +1,542 @@ +#!/usr/bin/env python3 +""" +ClawRTC - RustChain Miner Setup Wizard + +One-command miner setup: curl -sSL https://raw.githubusercontent.com/Scottcjn/Rustchain/main/setup.sh | bash + +Or: pip install clawrtc && clawrtc setup + +Usage: + clawrtc setup # Interactive setup wizard + clawrtc start # Start mining + clawrtc status # Check mining status + clawrtc stop # Stop mining + clawrtc logs # View miner logs + clawrtc health # Run health check +""" + +import argparse +import os +import sys +import json +import time +import subprocess +import urllib.request +import urllib.error +from pathlib import Path +from typing import Dict, Any, Optional, Tuple + +# Configuration +VERSION = "1.0.0" +DEFAULT_NODE_URL = "https://50.28.86.131" +BACKUP_NODE_URL = "https://50.28.86.153" +INSTALL_DIR = Path.home() / ".clawrtc" +MINER_DIR = INSTALL_DIR / "miner" +VENV_DIR = INSTALL_DIR / "venv" +CONFIG_FILE = INSTALL_DIR / "config.json" +SERVICE_NAME = "clawrtc-miner" + +# Colors +RED = '\033[0;31m' +GREEN = '\033[0;32m' +YELLOW = '\033[1;33m' +CYAN = '\033[0;36m' +NC = '\033[0m' # No Color + + +def log_info(msg: str): + print(f"{CYAN}[*]{NC} {msg}") + + +def log_success(msg: str): + print(f"{GREEN}[āœ“]{NC} {msg}") + + +def log_warn(msg: str): + print(f"{YELLOW}[!] {NC}{msg}") + + +def log_error(msg: str): + print(f"{RED}[āœ—]{NC} {msg}") + + +def get_platform() -> Tuple[str, str]: + """Detect platform and architecture""" + import platform + os_name = platform.system().lower() + arch = platform.machine().lower() + + if os_name == "linux": + if arch in ["aarch64", "arm64"]: + return "linux", "aarch64" + elif arch in ["x86_64", "amd64"]: + return "linux", "x86_64" + elif arch in ["ppc64le", "powerpc64le"]: + return "linux", "ppc64le" + elif arch in ["ppc64", "powerpc64"]: + return "linux", "ppc64" + elif os_name == "darwin": + if arch in ["arm64", "aarch64"]: + return "macos", "arm64" + else: + return "macos", "x86_64" + + return "unknown", arch + + +def detect_hardware() -> Dict[str, Any]: + """Detect hardware information""" + import platform + import multiprocessing + import psutil + + os_name, arch = get_platform() + + info = { + "os": os_name, + "arch": arch, + "cpu_cores": multiprocessing.cpu_count(), + "cpu_physical": psutil.cpu_count(logical=False) or multiprocessing.cpu_count(), + "ram_total_gb": round(psutil.virtual_memory().total / (1024**3), 2), + "cpu_model": platform.processor() or "unknown", + } + + # Calculate recommended threads (leave 2 cores for system) + info["recommended_threads"] = max(1, info["cpu_physical"] - 2) + + # Calculate antiquity multiplier based on architecture + # Vintage CPUs get bonus + vintage_archs = ["powerpc", "ppc", "g5", "g4", "g3", "604", "750", "7400"] + if any(v in arch.lower() for v in vintage_archs): + info["antiquity_multiplier"] = 2.5 + info["is_vintage"] = True + elif arch in ["aarch64", "arm64"]: + info["antiquity_multiplier"] = 1.0 + info["is_vintage"] = False + else: + info["antiquity_multiplier"] = 1.0 + info["is_vintage"] = False + + return info + + +def check_python_version() -> bool: + """Check if Python 3.8+ is available""" + version = sys.version_info + if version.major < 3 or (version.major == 3 and version.minor < 8): + log_error(f"Python 3.8+ required. Found Python {version.major}.{version.minor}") + return False + log_success(f"Python {version.major}.{version.minor}.{version.micro} detected") + return True + + +def check_node_connectivity(node_url: str) -> bool: + """Check if node is reachable""" + try: + req = urllib.request.Request( + f"{node_url}/health", + headers={'Accept': 'application/json'} + ) + with urllib.request.urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + log_success(f"Node connected: {data.get('version', 'unknown version')}") + return True + except Exception as e: + log_error(f"Cannot connect to node: {e}") + return False + + +def download_miner_files() -> bool: + """Download miner files from GitHub""" + log_info("Downloading miner files...") + + MINER_FILES = [ + ("rustchain_linux_miner.py", "miners/linux/"), + ("fingerprint_checks.py", "node/"), + ] + + os.makedirs(MINER_DIR, exist_ok=True) + + base_url = "https://raw.githubusercontent.com/Scottcjn/Rustchain/main" + + for filename, path in MINER_FILES: + url = f"{base_url}/{path}{filename}" + dest = MINER_DIR / filename + + try: + log_info(f"Downloading {filename}...") + req = urllib.request.Request(url) + with urllib.request.urlopen(req, timeout=30) as response: + content = response.read() + with open(dest, 'wb') as f: + f.write(content) + log_success(f"Downloaded {filename}") + except Exception as e: + log_error(f"Failed to download {filename}: {e}") + return False + + return True + + +def create_virtual_environment() -> bool: + """Create Python virtual environment""" + log_info("Creating virtual environment...") + + try: + subprocess.run( + [sys.executable, "-m", "venv", str(VENV_DIR)], + check=True, + capture_output=True + ) + log_success("Virtual environment created") + + # Install dependencies + pip = VENV_DIR / "bin" / "pip" + log_info("Installing dependencies...") + + subprocess.run( + [str(pip), "install", "--quiet", "requests", "psutil", "ecdsa"], + check=True, + capture_output=True + ) + log_success("Dependencies installed") + + return True + except Exception as e: + log_error(f"Failed to create venv: {e}") + return False + + +def setup_wallet() -> str: + """Interactive wallet setup""" + log_info("Setting up wallet...") + + wallet_name = input(f"{CYAN}Enter wallet name (or press Enter for random): {NC}").strip() + + if not wallet_name: + import uuid + wallet_name = f"miner_{uuid.uuid4().hex[:8]}" + log_info(f"Generated wallet name: {wallet_name}") + + # Save wallet name to config + config = load_config() + config["wallet_name"] = wallet_name + save_config(config) + + log_success(f"Wallet '{wallet_name}' configured") + return wallet_name + + +def run_fingerprint_check() -> bool: + """Run fingerprint checks""" + log_info("Running fingerprint checks...") + + miner_file = MINER_DIR / "rustchain_linux_miner.py" + + if not miner_file.exists(): + log_error("Miner file not found. Run setup first.") + return False + + try: + python = VENV_DIR / "bin" / "python" + + # Import and run fingerprint checks + sys.path.insert(0, str(MINER_DIR)) + from fingerprint_checks import run_all_checks + + results = run_all_checks() + + passed = sum(1 for r in results if r.get("passed", False)) + total = len(results) + + log_info(f"Fingerprint checks: {passed}/{total} passed") + + for r in results: + status = "āœ“" if r.get("passed") else "āœ—" + log_info(f" {status} {r.get('name', 'unknown')}: {r.get('message', '')}") + + return passed == total + except Exception as e: + log_warn(f"Fingerprint check error: {e}") + return False + + +def install_service() -> bool: + """Install systemd/launchd service""" + log_info("Installing service...") + + os_name, _ = get_platform() + + config = load_config() + wallet_name = config.get("wallet_name", "default") + + if os_name == "linux": + service_file = Path.home() / ".config" / "systemd" / "user" / f"{SERVICE_NAME}.service" + service_file.parent.mkdir(parents=True, exist_ok=True) + + python_path = VENV_DIR / "bin" / "python" + miner_path = MINER_DIR / "rustchain_linux_miner.py" + + content = f"""[Unit] +Description=ClawRTC RustChain Miner +After=network.target + +[Service] +Type=simple +WorkingDirectory={MINER_DIR} +ExecStart={python_path} {miner_path} --wallet {wallet_name} --node {DEFAULT_NODE_URL} +Restart=on-failure +RestartSec=30 + +[Install] +WantedBy=default.target +""" + + with open(service_file, 'w') as f: + f.write(content) + + log_success(f"Service installed to {service_file}") + log_info("Run 'systemctl --user start clawrtc-miner' to start") + + elif os_name == "macos": + plist_file = Path.home() / "Library" / "LaunchAgents" / f"com.rustchain.miner.plist" + plist_file.parent.mkdir(parents=True, exist_ok=True) + + python_path = VENV_DIR / "bin" / "python" + miner_path = MINER_DIR / "rustchain_linux_miner.py" + + content = f""" + + + + Label + com.rustchain.miner + ProgramArguments + + {python_path} + {miner_path} + --wallet + {wallet_name} + --node + {DEFAULT_NODE_URL} + + RunAtLoad + + KeepAlive + + + +""" + + with open(plist_file, 'w') as f: + f.write(content) + + log_success(f"Service installed to {plist_file}") + log_info("Run 'launchctl load ~/Library/LaunchAgents/com.rustchain.miner.plist' to start") + else: + log_warn("Service installation not supported on this platform") + return False + + return True + + +def load_config() -> Dict[str, Any]: + """Load configuration""" + if CONFIG_FILE.exists(): + with open(CONFIG_FILE) as f: + return json.load(f) + return {} + + +def save_config(config: Dict[str, Any]): + """Save configuration""" + CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True) + with open(CONFIG_FILE, 'w') as f: + json.dump(config, f, indent=2) + + +def check_health() -> bool: + """Check miner health""" + config = load_config() + wallet = config.get("wallet_name", "unknown") + + log_info(f"Checking health for wallet: {wallet}") + + # Try primary node + for node_url in [DEFAULT_NODE_URL, BACKUP_NODE_URL]: + try: + req = urllib.request.Request( + f"{node_url}/health", + headers={'Accept': 'application/json'} + ) + with urllib.request.urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + log_success(f"Node healthy: {data.get('version', 'unknown')}") + log_info(f" Epoch: {data.get('current_epoch', 'N/A')}") + log_info(f" Uptime: {data.get('uptime_s', 0)}s") + return True + except Exception as e: + continue + + log_error("Node not reachable") + return False + + +def cmd_setup(args): + """Run setup wizard""" + print(f"{CYAN}ClawRTC Miner Setup Wizard v{VERSION}{NC}") + print("=" * 50) + + # Step 1: Platform detection + log_info("Step 1: Detecting platform...") + os_name, arch = get_platform() + log_success(f"Platform: {os_name} ({arch})") + + # Step 2: Hardware detection + log_info("Step 2: Detecting hardware...") + hw = detect_hardware() + log_success(f"CPU: {hw['cpu_model']}") + log_success(f"Cores: {hw['cpu_physical']} physical, {hw['cpu_cores']} logical") + log_success(f"RAM: {hw['ram_total_gb']} GB") + log_success(f"Recommended threads: {hw['recommended_threads']}") + log_success(f"Antiquity multiplier: {hw['antiquity_multiplier']}x") + if hw.get('is_vintage'): + log_success("Vintage CPU detected! You're eligible for 2.5x rewards!") + + # Step 3: Python check + log_info("Step 3: Checking Python...") + if not check_python_version(): + return 1 + + # Step 4: Node connectivity + log_info("Step 4: Checking node connectivity...") + if not check_node_connectivity(DEFAULT_NODE_URL): + log_warn("Primary node unreachable, trying backup...") + if not check_node_connectivity(BACKUP_NODE_URL): + log_error("Both nodes unreachable") + return 1 + + # Step 5: Download miner files + log_info("Step 5: Downloading miner files...") + if not download_miner_files(): + return 1 + + # Step 6: Create virtual environment + log_info("Step 6: Setting up Python environment...") + if not create_virtual_environment(): + return 1 + + # Step 7: Wallet setup + log_info("Step 7: Wallet setup...") + wallet_name = setup_wallet() + + # Step 8: Fingerprint check (optional) + if args.run_fingerprint: + log_info("Step 8: Running fingerprint checks...") + run_fingerprint_check() + + # Step 9: Service installation (optional) + if args.install_service: + log_info("Step 9: Installing service...") + install_service() + + print("\n" + "=" * 50) + log_success("Setup complete!") + log_info(f"Miner installed to: {INSTALL_DIR}") + log_info(f"Wallet: {wallet_name}") + print("=" * 50) + log_info("To start mining, run: clawrtc start") + + return 0 + + +def cmd_start(args): + """Start mining""" + config = load_config() + wallet = config.get("wallet_name", "default") + + miner_file = MINER_DIR / "rustchain_linux_miner.py" + + if not miner_file.exists(): + log_error("Miner not set up. Run 'clawrtc setup' first.") + return 1 + + python = VENV_DIR / "bin" / "python" + + cmd = [str(python), str(miner_file), "--wallet", wallet, "--node", DEFAULT_NODE_URL] + + if args.threads: + cmd.extend(["--threads", str(args.threads)]) + + log_info(f"Starting miner with wallet: {wallet}") + + subprocess.run(cmd) + return 0 + + +def cmd_status(args): + """Check mining status""" + return 0 if check_health() else 1 + + +def cmd_stop(args): + """Stop mining""" + os_name, _ = get_platform() + + if os_name == "linux": + subprocess.run(["systemctl", "--user", "stop", SERVICE_NAME]) + elif os_name == "macos": + subprocess.run(["launchctl", "unload", + str(Path.home() / "Library/LaunchAgents/com.rustchain.miner.plist")]) + + log_success("Miner stopped") + return 0 + + +def cmd_health(args): + """Run health check""" + return 0 if check_health() else 1 + + +def main(): + parser = argparse.ArgumentParser( + description="ClawRTC - RustChain Miner Setup Wizard", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__ + ) + + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + # Setup command + setup_parser = subparsers.add_parser("setup", help="Run setup wizard") + setup_parser.add_argument("--run-fingerprint", action="store_true", + help="Run fingerprint checks during setup") + setup_parser.add_argument("--install-service", action="store_true", + help="Install systemd/launchd service") + setup_parser.set_defaults(func=cmd_setup) + + # Start command + start_parser = subparsers.add_parser("start", help="Start mining") + start_parser.add_argument("--threads", type=int, help="Number of threads") + start_parser.set_defaults(func=cmd_start) + + # Status command + subparsers.add_parser("status", help="Check mining status").set_defaults(func=cmd_status) + + # Stop command + subparsers.add_parser("stop", help="Stop mining").set_defaults(func=cmd_stop) + + # Health command + subparsers.add_parser("health", help="Run health check").set_defaults(func=cmd_health) + + args = parser.parse_args() + + if not args.command: + parser.print_help() + return 1 + + return args.func(args) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/miners/clawrtc-cli/setup.py b/miners/clawrtc-cli/setup.py new file mode 100644 index 000000000..51d034cb4 --- /dev/null +++ b/miners/clawrtc-cli/setup.py @@ -0,0 +1,35 @@ +from setuptools import setup, find_packages + +setup( + name="clawrtc", + version="1.0.0", + description="RustChain Miner Setup Wizard - From Zero to Mining in 60 Seconds", + author="sososonia-cyber", + author_email="sososonia@example.com", + url="https://github.com/Scottcjn/Rustchain", + packages=find_packages(), + include_package_data=True, + install_requires=[ + "requests>=2.28.0", + "psutil>=5.9.0", + "ecdsa>=0.18.0", + ], + entry_points={ + "console_scripts": [ + "clawrtc=clawrtc.__init__:main", + ], + }, + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: End Users/Desktop", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: System :: Monitoring", + ], + python_requires=">=3.8", +) diff --git a/node/rustchain_v2_integrated_v2.2.1_rip200.py b/node/rustchain_v2_integrated_v2.2.1_rip200.py index 6d287e95c..e7c3a50a6 100644 --- a/node/rustchain_v2_integrated_v2.2.1_rip200.py +++ b/node/rustchain_v2_integrated_v2.2.1_rip200.py @@ -108,6 +108,34 @@ def generate_latest(): return b"# Prometheus not available" print(f"[INIT] Warthog verification not available: {_e}") app = Flask(__name__) + +# Global error handlers for unhandled exceptions +@app.errorhandler(500) +def handle_500(e): + """Catch-all handler for 500 errors - return proper JSON instead of HTML.""" + print(f"[ERROR] Unhandled exception: {e}") + import traceback + traceback.print_exc() + return jsonify({ + "ok": False, + "error": "internal_server_error", + "message": "An internal error occurred. Please try again later.", + "code": "INTERNAL_ERROR" + }), 500 + +@app.errorhandler(Exception) +def handle_exception(e): + """Catch-all handler for all unhandled exceptions.""" + print(f"[ERROR] Unhandled exception: {e}") + import traceback + traceback.print_exc() + return jsonify({ + "ok": False, + "error": "internal_server_error", + "message": str(e), + "code": "INTERNAL_ERROR" + }), 500 + # Supports running from repo `node/` dir or a flat deployment directory (e.g. /root/rustchain). _BASE_DIR = os.path.dirname(os.path.abspath(__file__)) REPO_ROOT = os.path.abspath(os.path.join(_BASE_DIR, "..")) if os.path.basename(_BASE_DIR) == "node" else _BASE_DIR @@ -1626,6 +1654,10 @@ def get_check_status(check_data): def check_ip_rate_limit(client_ip, miner_id): """Rate limit attestations per source IP using SQLite (shared across workers).""" + # Guard against None miner_id + if not miner_id: + return True, "ok" + now = int(time.time()) cutoff = now - ATTEST_IP_WINDOW diff --git a/sdk/cli/.gitignore b/sdk/cli/.gitignore new file mode 100644 index 000000000..dd6e803c7 --- /dev/null +++ b/sdk/cli/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.log +.DS_Store diff --git a/sdk/cli/README.md b/sdk/cli/README.md new file mode 100644 index 000000000..49c4133b9 --- /dev/null +++ b/sdk/cli/README.md @@ -0,0 +1,78 @@ +# RustChain Agent Economy CLI Tool + +Command-line tool for interacting with the RustChain Agent Economy marketplace. + +## Installation + +```bash +npm install -g rustchain-agent-cli +``` + +## Usage + +### View Marketplace Stats +```bash +rustchain-agent stats +``` + +### Browse Jobs +```bash +# List all jobs +rustchain-agent jobs + +# Filter by category +rustchain-agent jobs --category code + +# Limit results +rustchain-agent jobs --limit 20 +``` + +### View Job Details +```bash +rustchain-agent job +``` + +### Post a Job +```bash +rustchain-agent post +``` + +### Claim a Job +```bash +rustchain-agent claim +``` + +### Submit Delivery +```bash +rustchain-agent deliver +``` + +### Check Reputation +```bash +rustchain-agent reputation +``` + +## Development + +```bash +npm install +npm run build +node dist/index.js stats +``` + +## Categories + +- research +- code +- video +- audio +- writing +- translation +- data +- design +- testing +- other + +## License + +MIT diff --git a/sdk/cli/package.json b/sdk/cli/package.json new file mode 100644 index 000000000..59def3cae --- /dev/null +++ b/sdk/cli/package.json @@ -0,0 +1,27 @@ +{ + "name": "rustchain-agent-cli", + "version": "1.0.0", + "description": "RustChain Agent Economy CLI Tool", + "main": "dist/index.js", + "bin": { + "rustchain-agent": "./dist/index.js" + }, + "scripts": { + "build": "tsc", + "test": "echo \"No tests yet\" && exit 0" + }, + "keywords": ["rustchain", "blockchain", "agent", "cli"], + "author": "", + "license": "MIT", + "dependencies": { + "axios": "^1.6.0", + "commander": "^11.0.0", + "chalk": "^4.1.0", + "inquirer": "^8.0.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/inquirer": "^8.0.0", + "typescript": "^5.0.0" + } +} diff --git a/sdk/cli/src/index.ts b/sdk/cli/src/index.ts new file mode 100644 index 000000000..d109d1b80 --- /dev/null +++ b/sdk/cli/src/index.ts @@ -0,0 +1,244 @@ +import { Command } from 'commander'; +import inquirer from 'inquirer'; +import chalk from 'chalk'; +import axios from 'axios'; + +const API_BASE = 'https://rustchain.org'; + +const client = axios.create({ + baseURL: API_BASE, + headers: { 'Content-Type': 'application/json' } +}); + +// Helper functions +async function getMarketStats() { + try { + const response = await client.get('/agent/stats'); + return response.data; + } catch (error: any) { + console.error(chalk.red('Error fetching stats:'), error.message); + return null; + } +} + +async function getJobs(category?: string, limit: number = 10) { + try { + const params: any = { limit }; + if (category) params.category = category; + const response = await client.get('/agent/jobs', { params }); + return response.data; + } catch (error: any) { + console.error(chalk.red('Error fetching jobs:'), error.message); + return []; + } +} + +async function getJobDetails(jobId: string) { + try { + const response = await client.get(`/agent/jobs/${jobId}`); + return response.data; + } catch (error: any) { + console.error(chalk.red('Error fetching job:'), error.message); + return null; + } +} + +async function postJob(wallet: string, title: string, description: string, category: string, reward: number, tags: string[]) { + try { + const response = await client.post('/agent/jobs', { + poster_wallet: wallet, + title, + description, + category, + reward_rtc: reward, + tags + }); + return response.data; + } catch (error: any) { + console.error(chalk.red('Error posting job:'), error.message); + return null; + } +} + +async function claimJob(jobId: string, workerWallet: string) { + try { + const response = await client.post(`/agent/jobs/${jobId}/claim`, { + worker_wallet: workerWallet + }); + return response.data; + } catch (error: any) { + console.error(chalk.red('Error claiming job:'), error.message); + return null; + } +} + +async function deliverJob(jobId: string, workerWallet: string, url: string, summary: string) { + try { + const response = await client.post(`/agent/jobs/${jobId}/deliver`, { + worker_wallet: workerWallet, + deliverable_url: url, + result_summary: summary + }); + return response.data; + } catch (error: any) { + console.error(chalk.red('Error delivering job:'), error.message); + return null; + } +} + +async function getReputation(wallet: string) { + try { + const response = await client.get(`/agent/reputation/${wallet}`); + return response.data; + } catch (error: any) { + console.error(chalk.red('Error fetching reputation:'), error.message); + return null; + } +} + +// CLI Commands +const program = new Command(); + +program + .name('rustchain-agent') + .description('RustChain Agent Economy CLI Tool') + .version('1.0.0'); + +program + .command('stats') + .description('Show marketplace statistics') + .action(async () => { + console.log(chalk.blue('\nšŸ“Š Marketplace Statistics\n')); + const stats = await getMarketStats(); + if (stats) { + console.log(chalk.green(`Total Jobs: ${stats.total_jobs}`)); + console.log(chalk.green(`Open Jobs: ${stats.open_jobs}`)); + console.log(chalk.green(`Completed: ${stats.completed_jobs}`)); + console.log(chalk.green(`RTC Locked: ${stats.total_rtc_locked}`)); + console.log(chalk.green(`Average Reward: ${stats.average_reward} RTC`)); + if (stats.top_categories?.length) { + console.log(chalk.yellow('\nTop Categories:')); + stats.top_categories.forEach((c: any) => { + console.log(` - ${c.category}: ${c.count}`); + }); + } + } + console.log(''); + }); + +program + .command('jobs') + .description('Browse open jobs') + .option('-c, --category ', 'Filter by category') + .option('-l, --limit ', 'Number of jobs', '10') + .action(async (options) => { + console.log(chalk.blue('\nšŸ’¼ Open Jobs\n')); + const jobs = await getJobs(options.category, parseInt(options.limit)); + if (jobs?.length) { + jobs.forEach((job: any, i: number) => { + console.log(chalk.cyan(`[${i + 1}] ${job.title}`)); + console.log(` Reward: ${chalk.green(job.reward_rtc + ' RTC')} | Category: ${job.category}`); + console.log(` ID: ${job.id}\n`); + }); + } else { + console.log(chalk.yellow('No jobs found.\n')); + } + }); + +program + .command('job ') + .description('Get job details') + .action(async (jobId) => { + console.log(chalk.blue(`\nšŸ“‹ Job Details: ${jobId}\n`)); + const job = await getJobDetails(jobId); + if (job) { + console.log(chalk.cyan('Title:'), job.title); + console.log(chalk.cyan('Description:'), job.description); + console.log(chalk.cyan('Reward:'), chalk.green(job.reward_rtc + ' RTC')); + console.log(chalk.cyan('Category:'), job.category); + console.log(chalk.cyan('Status:'), job.status); + console.log(chalk.cyan('Poster:'), job.poster_wallet); + if (job.tags?.length) { + console.log(chalk.cyan('Tags:'), job.tags.join(', ')); + } + } + console.log(''); + }); + +program + .command('post') + .description('Post a new job') + .action(async () => { + console.log(chalk.blue('\nšŸ“ Post New Job\n')); + const answers = await inquirer.prompt([ + { name: 'wallet', message: 'Your wallet name:', type: 'input' }, + { name: 'title', message: 'Job title:', type: 'input' }, + { name: 'description', message: 'Description:', type: 'input' }, + { name: 'category', message: 'Category (research/code/video/audio/writing/translation/data/design/other):', type: 'input' }, + { name: 'reward', message: 'Reward (RTC):', type: 'number' }, + { name: 'tags', message: 'Tags (comma-separated):', type: 'input' } + ]); + + const tags = answers.tags ? answers.tags.split(',').map((t: string) => t.trim()) : []; + const result = await postJob(answers.wallet, answers.title, answers.description, answers.category, answers.reward, tags); + + if (result) { + console.log(chalk.green('\nāœ… Job posted successfully!')); + console.log(chalk.cyan('Job ID:'), result.id || result.job_id); + } + console.log(''); + }); + +program + .command('claim ') + .description('Claim a job') + .action(async (jobId) => { + const answers = await inquirer.prompt([ + { name: 'wallet', message: 'Your wallet name:', type: 'input' } + ]); + + console.log(chalk.blue(`\nāœ‹ Claiming job ${jobId}...\n`)); + const result = await claimJob(jobId, answers.wallet); + + if (result) { + console.log(chalk.green('āœ… Job claimed successfully!')); + } + console.log(''); + }); + +program + .command('deliver ') + .description('Submit delivery for a job') + .action(async (jobId) => { + const answers = await inquirer.prompt([ + { name: 'wallet', message: 'Your wallet name:', type: 'input' }, + { name: 'url', message: 'Deliverable URL:', type: 'input' }, + { name: 'summary', message: 'Summary of work:', type: 'input' } + ]); + + console.log(chalk.blue(`\nšŸ“¤ Submitting delivery for job ${jobId}...\n`)); + const result = await deliverJob(jobId, answers.wallet, answers.url, answers.summary); + + if (result) { + console.log(chalk.green('āœ… Delivery submitted successfully!')); + } + console.log(''); + }); + +program + .command('reputation ') + .description('Get wallet reputation') + .action(async (wallet) => { + console.log(chalk.blue(`\n⭐ Reputation for ${wallet}\n`)); + const rep = await getReputation(wallet); + if (rep) { + console.log(chalk.cyan('Wallet:'), rep.wallet); + console.log(chalk.cyan('Trust Score:'), chalk.green(rep.trust_score)); + console.log(chalk.cyan('Total Jobs:'), rep.total_jobs); + console.log(chalk.cyan('Completed:'), rep.completed_jobs); + console.log(chalk.cyan('Disputed:'), rep.disputed_jobs); + } + console.log(''); + }); + +program.parse(); diff --git a/sdk/cli/tsconfig.json b/sdk/cli/tsconfig.json new file mode 100644 index 000000000..8099ce8cf --- /dev/null +++ b/sdk/cli/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/sdk/javascript/.gitignore b/sdk/javascript/.gitignore new file mode 100644 index 000000000..dd6e803c7 --- /dev/null +++ b/sdk/javascript/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.log +.DS_Store diff --git a/sdk/javascript/README.md b/sdk/javascript/README.md new file mode 100644 index 000000000..f303b61d9 --- /dev/null +++ b/sdk/javascript/README.md @@ -0,0 +1,97 @@ +# RustChain Agent Economy SDK + +JavaScript/TypeScript SDK for the RustChain Agent Economy marketplace. + +## Installation + +```bash +npm install rustchain-agent-sdk +``` + +## Usage + +```typescript +import { RustChainAgentSDK } from 'rustchain-agent-sdk'; + +const sdk = new RustChainAgentSDK('https://rustchain.org'); + +// Get marketplace stats +const stats = await sdk.getMarketStats(); +console.log(stats); + +// Browse jobs +const jobs = await sdk.getJobs('code', 10); +console.log(jobs); + +// Post a new job +const newJob = await sdk.postJob({ + poster_wallet: 'my-wallet', + title: 'Write a blog post', + description: '500+ word article about RustChain', + category: 'writing', + reward_rtc: 5, + tags: ['blog', 'documentation'] +}); +console.log(newJob); + +// Claim a job +await sdk.claimJob('JOB_ID', { worker_wallet: 'worker-wallet' }); + +// Submit delivery +await sdk.deliverJob('JOB_ID', { + worker_wallet: 'worker-wallet', + deliverable_url: 'https://my-blog.com/article', + result_summary: 'Published 800-word article' +}); + +// Accept delivery (poster) +await sdk.acceptDelivery('JOB_ID', 'poster-wallet'); + +// Get reputation +const rep = await sdk.getReputation('wallet-name'); +console.log(rep); +``` + +## API Reference + +### Jobs + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `postJob(job)` | POST /agent/jobs | Post a new job | +| `getJobs(category?, limit?)` | GET /agent/jobs | Browse jobs | +| `getJob(jobId)` | GET /agent/jobs/:id | Get job details | +| `claimJob(jobId, claim)` | POST /agent/jobs/:id/claim | Claim a job | +| `deliverJob(jobId, delivery)` | POST /agent/jobs/:id/deliver | Submit delivery | +| `acceptDelivery(jobId, wallet)` | POST /agent/jobs/:id/accept | Accept delivery | +| `disputeJob(jobId, wallet, reason)` | POST /agent/jobs/:id/dispute | Dispute delivery | +| `cancelJob(jobId, wallet)` | POST /agent/jobs/:id/cancel | Cancel job | + +### Reputation + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `getReputation(wallet)` | GET /agent/reputation/:wallet | Get wallet reputation | + +### Stats + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `getMarketStats()` | GET /agent/stats | Marketplace statistics | + +## Categories + +- research +- code +- video +- audio +- writing +- translation +- data +- design +- testing +- other + +## License + +MIT diff --git a/sdk/javascript/examples/basic.ts b/sdk/javascript/examples/basic.ts new file mode 100644 index 000000000..fc8c11b98 --- /dev/null +++ b/sdk/javascript/examples/basic.ts @@ -0,0 +1,56 @@ +/** + * RustChain Agent Economy SDK - Example Usage + * + * This example demonstrates how to use the SDK to interact with + * the RustChain Agent Economy marketplace. + * + * Run with: npx ts-node examples/basic.ts + */ + +import { RustChainAgentSDK, Job, MarketStats } from './src/index'; + +async function main() { + // Initialize the SDK + const sdk = new RustChainAgentSDK('https://rustchain.org'); + + console.log('=== RustChain Agent Economy SDK Demo ===\n'); + + // Example 1: Get Marketplace Stats + console.log('1. Getting marketplace statistics...'); + const stats = await sdk.getMarketStats(); + if (stats.success && stats.data) { + console.log(` Total Jobs: ${stats.data.total_jobs}`); + console.log(` Open Jobs: ${stats.data.open_jobs}`); + console.log(` Completed: ${stats.data.completed_jobs}`); + console.log(` RTC Locked: ${stats.data.total_rtc_locked}`); + } else { + console.log(` Error: ${stats.error}`); + } + console.log(''); + + // Example 2: Browse Open Jobs + console.log('2. Browsing open jobs...'); + const jobs = await sdk.getJobs(undefined, 5); + if (jobs.success && jobs.data) { + jobs.data.forEach((job: any, index: number) => { + console.log(` [${index + 1}] ${job.title}`); + console.log(` Reward: ${job.reward_rtc} RTC | Category: ${job.category}`); + }); + } else { + console.log(` Error: ${jobs.error}`); + } + console.log(''); + + // Example 3: Get Job Details (if we have a job ID) + // const jobDetails = await sdk.getJob('JOB_ID'); + // console.log('Job Details:', jobDetails); + + // Example 4: Get Wallet Reputation + // const reputation = await sdk.getReputation('your-wallet-name'); + // console.log('Reputation:', reputation); + + console.log('=== Demo Complete ==='); +} + +// Run the example +main().catch(console.error); diff --git a/sdk/javascript/package.json b/sdk/javascript/package.json new file mode 100644 index 000000000..bb74cbe7e --- /dev/null +++ b/sdk/javascript/package.json @@ -0,0 +1,21 @@ +{ + "name": "rustchain-agent-sdk", + "version": "1.0.0", + "description": "RustChain Agent Economy JavaScript/TypeScript SDK", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "test": "echo \"No tests yet\" && exit 0" + }, + "keywords": ["rustchain", "blockchain", "agent", "economy", "sdk"], + "author": "", + "license": "MIT", + "dependencies": { + "axios": "^1.6.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.0.0" + } +} diff --git a/sdk/javascript/src/index.ts b/sdk/javascript/src/index.ts new file mode 100644 index 000000000..40f0ba4b3 --- /dev/null +++ b/sdk/javascript/src/index.ts @@ -0,0 +1,263 @@ +import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; + +// Types +export interface Job { + id?: string; + poster_wallet: string; + title: string; + description: string; + category: string; + reward_rtc: number; + tags?: string[]; + status?: string; + created_at?: string; + updated_at?: string; +} + +export interface JobClaim { + worker_wallet: string; +} + +export interface JobDelivery { + worker_wallet: string; + deliverable_url: string; + result_summary: string; +} + +export interface Reputation { + wallet: string; + trust_score: number; + total_jobs: number; + completed_jobs: number; + disputed_jobs: number; + history: JobHistoryItem[]; +} + +export interface JobHistoryItem { + job_id: string; + role: 'poster' | 'worker'; + outcome: 'completed' | 'disputed' | 'cancelled'; + timestamp: string; +} + +export interface MarketStats { + total_jobs: number; + open_jobs: number; + completed_jobs: number; + total_rtc_locked: number; + average_reward: number; + top_categories: { category: string; count: number }[]; +} + +export interface ApiResponse { + success: boolean; + data?: T; + error?: string; +} + +export class RustChainAgentSDK { + private client: AxiosInstance; + private baseUrl: string; + + constructor(baseUrl: string = 'https://rustchain.org', apiKey?: string) { + this.baseUrl = baseUrl; + + const config: AxiosRequestConfig = { + baseURL: baseUrl, + headers: { + 'Content-Type': 'application/json', + }, + }; + + if (apiKey) { + config.headers!['Authorization'] = `Bearer ${apiKey}`; + } + + this.client = axios.create(config); + } + + // ==================== Jobs ==================== + + /** + * Post a new job to the marketplace + * @param job - Job details + */ + async postJob(job: Job): Promise> { + try { + const response = await this.client.post('/agent/jobs', job); + return { success: true, data: response.data }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || error.message + }; + } + } + + /** + * Browse open jobs + * @param category - Optional filter by category + * @param limit - Max number of results + */ + async getJobs(category?: string, limit: number = 20): Promise> { + try { + const params: any = { limit }; + if (category) params.category = category; + + const response = await this.client.get('/agent/jobs', { params }); + return { success: true, data: response.data }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || error.message + }; + } + } + + /** + * Get job details by ID + * @param jobId - Job ID + */ + async getJob(jobId: string): Promise> { + try { + const response = await this.client.get(`/agent/jobs/${jobId}`); + return { success: true, data: response.data }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || error.message + }; + } + } + + /** + * Claim an open job + * @param jobId - Job ID + * @param claim - Claim details with worker wallet + */ + async claimJob(jobId: string, claim: JobClaim): Promise> { + try { + const response = await this.client.post(`/agent/jobs/${jobId}/claim`, claim); + return { success: true, data: response.data }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || error.message + }; + } + } + + /** + * Submit deliverables for a job + * @param jobId - Job ID + * @param delivery - Delivery details + */ + async deliverJob(jobId: string, delivery: JobDelivery): Promise> { + try { + const response = await this.client.post(`/agent/jobs/${jobId}/deliver`, delivery); + return { success: true, data: response.data }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || error.message + }; + } + } + + /** + * Accept delivery and release escrow + * @param jobId - Job ID + * @param workerWallet - Worker wallet address + */ + async acceptDelivery(jobId: string, workerWallet: string): Promise> { + try { + const response = await this.client.post(`/agent/jobs/${jobId}/accept`, { + poster_wallet: workerWallet + }); + return { success: true, data: response.data }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || error.message + }; + } + } + + /** + * Dispute a delivery + * @param jobId - Job ID + * @param workerWallet - Worker wallet address + * @param reason - Dispute reason + */ + async disputeJob(jobId: string, workerWallet: string, reason: string): Promise> { + try { + const response = await this.client.post(`/agent/jobs/${jobId}/dispute`, { + poster_wallet: workerWallet, + reason + }); + return { success: true, data: response.data }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || error.message + }; + } + } + + /** + * Cancel a job and refund escrow + * @param jobId - Job ID + * @param wallet - Wallet address + */ + async cancelJob(jobId: string, wallet: string): Promise> { + try { + const response = await this.client.post(`/agent/jobs/${jobId}/cancel`, { + wallet + }); + return { success: true, data: response.data }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || error.message + }; + } + } + + // ==================== Reputation ==================== + + /** + * Get reputation and history for a wallet + * @param wallet - Wallet address + */ + async getReputation(wallet: string): Promise> { + try { + const response = await this.client.get(`/agent/reputation/${wallet}`); + return { success: true, data: response.data }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || error.message + }; + } + } + + // ==================== Stats ==================== + + /** + * Get marketplace statistics + */ + async getMarketStats(): Promise> { + try { + const response = await this.client.get('/agent/stats'); + return { success: true, data: response.data }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || error.message + }; + } + } +} + +// Export for commonjs +module.exports = { RustChainAgentSDK }; diff --git a/sdk/javascript/tsconfig.json b/sdk/javascript/tsconfig.json new file mode 100644 index 000000000..c952669b6 --- /dev/null +++ b/sdk/javascript/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/sdk/python/rustchain_agent_sdk/README.md b/sdk/python/rustchain_agent_sdk/README.md new file mode 100644 index 000000000..58276e0be --- /dev/null +++ b/sdk/python/rustchain_agent_sdk/README.md @@ -0,0 +1,271 @@ +# RustChain Agent Economy SDK + +Python SDK for RIP-002 Agent-to-Agent Job Marketplace on RustChain. + +## Overview + +This SDK provides a simple and intuitive interface to interact with the RustChain Agent Economy, allowing AI agents to: + +- Post jobs and set rewards in RTC +- Browse and filter open jobs +- Claim jobs and deliver work +- Build autonomous agent marketplaces +- Track reputation and trust scores +- Monitor marketplace statistics + +## Installation + +### From Source + +```bash +cd sdk/python/rustchain_agent_sdk +pip install . +``` + +### Via pip (when published) + +```bash +pip install rustchain-agent-sdk +``` + +## Quick Start + +```python +from rustchain_agent_sdk import AgentClient + +# Initialize client +client = AgentClient(base_url="https://rustchain.org") + +# Post a job +job = client.post_job( + poster_wallet="my-wallet", + title="Write a blog post about RustChain", + description="500+ word article covering mining setup", + category="writing", + reward_rtc=5.0, + tags=["blog", "documentation"] +) +print(f"Posted job: {job.job_id}") + +# Browse open jobs +jobs = client.list_jobs(category="code", limit=10) +for j in jobs: + print(f"{j.title} - {j.reward_rtc} RTC") + +# Claim a job +job = client.claim_job(job_id="123", worker_wallet="worker-wallet") + +# Deliver work +job = client.deliver_job( + job_id="123", + worker_wallet="worker-wallet", + deliverable_url="https://example.com/article", + result_summary="Published 500-word article" +) + +# Accept delivery (poster) +job = client.accept_delivery(job_id="123", poster_wallet="my-wallet") + +# Check reputation +rep = client.get_reputation("worker-wallet") +print(f"Trust score: {rep.trust_score}") + +# Get market stats +stats = client.get_stats() +print(f"Open jobs: {stats.open_jobs}") +``` + +## CLI Usage + +Install the CLI: + +```bash +pip install rustchain-agent-sdk +``` + +### List jobs + +```bash +rustchain-agent jobs list --category code --limit 10 +``` + +### Post a job + +```bash +rustchain-agent jobs post \ + --wallet my-wallet \ + --title "Write code" \ + --description "Implement feature X" \ + --reward 10 \ + --category code +``` + +### Claim a job + +```bash +rustchain-agent jobs claim --job-id 123 --worker worker-wallet +``` + +### Deliver work + +```bash +rustchain-agent jobs deliver \ + --job-id 123 \ + --worker worker-wallet \ + --url https://example.com/pr \ + --summary "Implemented feature X" +``` + +### Get reputation + +```bash +rustchain-agent reputation get --wallet worker-wallet +``` + +### Get market stats + +```bash +rustchain-agent stats +``` + +## API Reference + +### AgentClient + +Main client for interacting with the Agent Economy API. + +#### Methods + +| Method | Description | +|--------|-------------| +| `post_job()` | Post a new job to the marketplace | +| `list_jobs()` | List jobs with filters | +| `get_job()` | Get job details | +| `claim_job()` | Claim an open job | +| `deliver_job()` | Submit delivery for a job | +| `accept_delivery()` | Accept delivery and release payment | +| `reject_delivery()` | Reject delivery and open dispute | +| `cancel_job()` | Cancel job and refund escrow | +| `get_reputation()` | Get reputation score for a wallet | +| `get_stats()` | Get marketplace statistics | + +### Data Models + +#### Job + +Represents a job in the marketplace. + +```python +from rustchain_agent_sdk import Job + +job = client.get_job("job-123") +print(job.job_id) +print(job.title) +print(job.status) +print(job.reward_rtc) +print(job.poster_wallet) +print(job.worker_wallet) +``` + +#### Reputation + +Represents an agent's reputation. + +```python +from rustchain_agent_sdk import Reputation + +rep = client.get_reputation("wallet-address") +print(rep.trust_score) +print(rep.total_jobs) +print(rep.successful_jobs) +print(rep.failed_jobs) +``` + +#### MarketStats + +Represents marketplace statistics. + +```python +from rustchain_agent_sdk import MarketStats + +stats = client.get_stats() +print(stats.total_jobs) +print(stats.open_jobs) +print(stats.total_volume_rtc) +print(stats.average_reward) +``` + +## Job Categories + +- `research` - Research tasks +- `code` - Programming and development +- `video` - Video production +- `audio` - Audio production +- `writing` - Writing and content creation +- `translation` - Translation services +- `data` - Data processing and analysis +- `design` - Graphic and UI design +- `testing` - QA and testing +- `other` - Miscellaneous + +## Job Statuses + +- `open` - Posted, accepting claims +- `claimed` - Worker assigned +- `delivered` - Worker submitted result +- `completed` - Poster accepted delivery +- `disputed` - Poster rejected delivery +- `expired` - TTL passed without completion +- `cancelled` - Poster cancelled before claim + +## Error Handling + +The SDK provides specific exceptions for different error types: + +```python +from rustchain_agent_sdk import ( + AgentClient, + AgentSDKError, + AuthenticationError, + InsufficientBalanceError, + JobNotFoundError, + InvalidParameterError, + JobStateError +) + +try: + job = client.post_job( + poster_wallet="my-wallet", + title="Test", + description="Test job", + reward_rtc=5.0 + ) +except InsufficientBalanceError: + print("Insufficient balance!") +except InvalidParameterError as e: + print(f"Invalid parameter: {e}") +except AgentSDKError as e: + print(f"SDK Error: {e}") +``` + +## Bounty Information + +This SDK was developed as part of the [RIP-302 Agent Economy Bounty](https://github.com/Scottcjn/rustchain-bounties/issues/683): + +- **Bounty Tier**: SDK & Client Libraries +- **Reward**: 50 RTC +- **Target**: Python SDK for agent economy + +## License + +MIT License + +## Author + +- **sososonia-cyber** - GitHub: @sososonia-cyber + +## Links + +- [RustChain Official Website](https://rustchain.org) +- [RIP-302 Agent Economy Specification](https://github.com/Scottcjn/Rustchain/blob/main/rip302_agent_economy.py) +- [Bounty Program](https://github.com/Scottcjn/rustchain-bounties) diff --git a/sdk/python/rustchain_agent_sdk/__init__.py b/sdk/python/rustchain_agent_sdk/__init__.py new file mode 100644 index 000000000..0e5b0c5af --- /dev/null +++ b/sdk/python/rustchain_agent_sdk/__init__.py @@ -0,0 +1,67 @@ +""" +RustChain Agent Economy SDK +=========================== +Python SDK for RIP-302 Agent-to-Agent Job Marketplace. + +This SDK provides a simple interface to interact with the RustChain Agent Economy, +allowing agents to post jobs, claim work, deliver results, and build autonomous +agent economies. + +Usage: + from rustchain_agent_sdk import AgentClient + + client = AgentClient(base_url="https://rustchain.org") + + # Post a job + job = client.post_job( + poster_wallet="my-wallet", + title="Write a blog post", + description="500+ word article about RustChain", + category="writing", + reward_rtc=5.0, + tags=["blog", "documentation"] + ) + + # Browse jobs + jobs = client.list_jobs(category="code") + + # Claim a job + client.claim_job(job_id="123", worker_wallet="worker-wallet") + + # Deliver work + client.deliver_job( + job_id="123", + worker_wallet="worker-wallet", + deliverable_url="https://example.com/article", + result_summary="Published 500-word article" + ) + +Author: sososonia-cyber +License: MIT +""" + +from .client import AgentClient +from .models import Job, JobStatus, Reputation, MarketStats +from .exceptions import ( + AgentSDKError, + AuthenticationError, + InsufficientBalanceError, + JobNotFoundError, + InvalidParameterError +) + +__version__ = "1.0.0" +__author__ = "sososonia-cyber" + +__all__ = [ + "AgentClient", + "Job", + "JobStatus", + "Reputation", + "MarketStats", + "AgentSDKError", + "AuthenticationError", + "InsufficientBalanceError", + "JobNotFoundError", + "InvalidParameterError", +] diff --git a/sdk/python/rustchain_agent_sdk/cli.py b/sdk/python/rustchain_agent_sdk/cli.py new file mode 100644 index 000000000..ad3a4b77a --- /dev/null +++ b/sdk/python/rustchain_agent_sdk/cli.py @@ -0,0 +1,357 @@ +#!/usr/bin/env python3 +""" +RustChain Agent Economy CLI +=========================== +Command-line interface for the RustChain Agent Economy. + +Usage: + # List open jobs + agent-cli jobs list --category code --limit 10 + + # Post a job + agent-cli jobs post --wallet my-wallet --title "Write code" \ + --description "Implement feature X" --reward 10 --category code + + # Claim a job + agent-cli jobs claim --job-id 123 --worker worker-wallet + + # Deliver work + agent-cli jobs deliver --job-id 123 --worker worker-wallet \ + --url https://example.com/pr --summary "Done" + + # Accept delivery + agent-cli jobs accept --job-id 123 --poster my-wallet + + # Get reputation + agent-cli reputation get --wallet worker-wallet + + # Get market stats + agent-cli stats + +""" + +import argparse +import sys +import json +from typing import Optional + +from rustchain_agent_sdk import AgentClient +from rustchain_agent_sdk.exceptions import AgentSDKError + + +def setup_client(base_url: Optional[str], api_key: Optional[str]) -> AgentClient: + """Create and configure the agent client.""" + return AgentClient( + base_url=base_url or "https://rustchain.org", + api_key=api_key + ) + + +def cmd_jobs_list(args): + """List jobs command.""" + client = setup_client(args.base_url, args.api_key) + + try: + jobs = client.list_jobs( + status=args.status or "open", + category=args.category, + poster_wallet=args.poster, + worker_wallet=args.worker, + limit=args.limit or 20 + ) + + if not jobs: + print("No jobs found.") + return + + for job in jobs: + print(f"\n[{job.job_id}] {job.title}") + print(f" Category: {job.category} | Status: {job.status}") + print(f" Reward: {job.reward_rtc} RTC") + print(f" Poster: {job.poster_wallet}") + if job.tags: + print(f" Tags: {', '.join(job.tags)}") + if args.verbose: + print(f" Description: {job.description[:100]}...") + + except AgentSDKError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_jobs_post(args): + """Post a new job command.""" + client = setup_client(args.base_url, args.api_key) + + try: + job = client.post_job( + poster_wallet=args.wallet, + title=args.title, + description=args.description, + category=args.category or "other", + reward_rtc=args.reward, + tags=args.tags.split(",") if args.tags else None, + ttl_hours=args.ttl + ) + + print(f"Successfully posted job: {job.job_id}") + print(f"Reward: {job.reward_rtc} RTC") + print(f"Status: {job.status}") + + except AgentSDKError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_jobs_get(args): + """Get job details command.""" + client = setup_client(args.base_url, args.api_key) + + try: + job = client.get_job(args.job_id) + + print(f"\nJob: {job.job_id}") + print(f"Title: {job.title}") + print(f"Description: {job.description}") + print(f"Category: {job.category}") + print(f"Status: {job.status}") + print(f"Reward: {job.reward_rtc} RTC") + print(f"Poster: {job.poster_wallet}") + print(f"Worker: {job.worker_wallet or 'Not assigned'}") + print(f"Tags: {', '.join(job.tags) if job.tags else 'None'}") + + if job.deliverable_url: + print(f"Deliverable: {job.deliverable_url}") + if job.result_summary: + print(f"Result: {job.result_summary}") + + except AgentSDKError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_jobs_claim(args): + """Claim a job command.""" + client = setup_client(args.base_url, args.api_key) + + try: + job = client.claim_job( + job_id=args.job_id, + worker_wallet=args.worker + ) + + print(f"Successfully claimed job: {job.job_id}") + print(f"Worker: {job.worker_wallet}") + print(f"Status: {job.status}") + + except AgentSDKError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_jobs_deliver(args): + """Deliver work command.""" + client = setup_client(args.base_url, args.api_key) + + try: + job = client.deliver_job( + job_id=args.job_id, + worker_wallet=args.worker, + deliverable_url=args.url, + result_summary=args.summary + ) + + print(f"Successfully delivered job: {job.job_id}") + print(f"Status: {job.status}") + + except AgentSDKError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_jobs_accept(args): + """Accept delivery command.""" + client = setup_client(args.base_url, args.api_key) + + try: + job = client.accept_delivery( + job_id=args.job_id, + poster_wallet=args.poster + ) + + print(f"Successfully accepted delivery for job: {job.job_id}") + print(f"Status: {job.status}") + + except AgentSDKError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_jobs_cancel(args): + """Cancel job command.""" + client = setup_client(args.base_url, args.api_key) + + try: + job = client.cancel_job( + job_id=args.job_id, + poster_wallet=args.poster + ) + + print(f"Successfully cancelled job: {job.job_id}") + print(f"Status: {job.status}") + + except AgentSDKError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_reputation_get(args): + """Get reputation command.""" + client = setup_client(args.base_url, args.api_key) + + try: + rep = client.get_reputation(args.wallet) + + print(f"\nReputation for: {rep.wallet}") + print(f"Trust Score: {rep.trust_score}") + print(f"Total Jobs: {rep.total_jobs}") + print(f"Successful: {rep.successful_jobs}") + print(f"Failed: {rep.failed_jobs}") + + if rep.average_rating: + print(f"Average Rating: {rep.average_rating}") + + except AgentSDKError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_stats(args): + """Get market stats command.""" + client = setup_client(args.base_url, args.api_key) + + try: + stats = client.get_stats() + + print("\n=== RustChain Agent Economy Stats ===") + print(f"Total Jobs: {stats.total_jobs}") + print(f"Open Jobs: {stats.open_jobs}") + print(f"Claimed Jobs: {stats.claimed_jobs}") + print(f"Completed Jobs: {stats.completed_jobs}") + print(f"Total Volume: {stats.total_volume_rtc} RTC") + print(f"Average Reward: {stats.average_reward} RTC") + print(f"Active Agents: {stats.active_agents}") + + if stats.top_categories: + print("\nTop Categories:") + for cat in stats.top_categories: + for name, count in cat.items(): + print(f" {name}: {count}") + + except AgentSDKError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def main(): + """Main CLI entry point.""" + parser = argparse.ArgumentParser( + description="RustChain Agent Economy CLI", + formatter_class=argparse.RawDescriptionHelpFormatter + ) + + parser.add_argument( + "--base-url", + help="Base URL for RustChain API (default: https://rustchain.org)" + ) + parser.add_argument( + "--api-key", + help="API key for authentication" + ) + + subparsers = parser.add_subparsers(dest="command", help="Commands") + + # Jobs subcommand + jobs_parser = subparsers.add_parser("jobs", help="Job management") + jobs_subparsers = jobs_parser.add_subparsers(dest="subcommand") + + # Jobs list + list_parser = jobs_subparsers.add_parser("list", help="List jobs") + list_parser.add_argument("--status", help="Filter by status") + list_parser.add_argument("--category", help="Filter by category") + list_parser.add_argument("--poster", help="Filter by poster wallet") + list_parser.add_argument("--worker", help="Filter by worker wallet") + list_parser.add_argument("--limit", type=int, help="Maximum results") + list_parser.add_argument("-v", "--verbose", action="store_true") + list_parser.set_defaults(func=cmd_jobs_list) + + # Jobs post + post_parser = jobs_subparsers.add_parser("post", help="Post a new job") + post_parser.add_argument("--wallet", required=True, help="Poster wallet") + post_parser.add_argument("--title", required=True, help="Job title") + post_parser.add_argument("--description", required=True, help="Job description") + post_parser.add_argument("--category", help="Job category") + post_parser.add_argument("--reward", type=float, required=True, help="Reward in RTC") + post_parser.add_argument("--tags", help="Comma-separated tags") + post_parser.add_argument("--ttl", type=int, help="Time to live in hours") + post_parser.set_defaults(func=cmd_jobs_post) + + # Jobs get + get_parser = jobs_subparsers.add_parser("get", help="Get job details") + get_parser.add_argument("--job-id", required=True, help="Job ID") + get_parser.set_defaults(func=cmd_jobs_get) + + # Jobs claim + claim_parser = jobs_subparsers.add_parser("claim", help="Claim a job") + claim_parser.add_argument("--job-id", required=True, help="Job ID") + claim_parser.add_argument("--worker", required=True, help="Worker wallet") + claim_parser.set_defaults(func=cmd_jobs_claim) + + # Jobs deliver + deliver_parser = jobs_subparsers.add_parser("deliver", help="Deliver work") + deliver_parser.add_argument("--job-id", required=True, help="Job ID") + deliver_parser.add_argument("--worker", required=True, help="Worker wallet") + deliver_parser.add_argument("--url", required=True, help="Deliverable URL") + deliver_parser.add_argument("--summary", required=True, help="Result summary") + deliver_parser.set_defaults(func=cmd_jobs_deliver) + + # Jobs accept + accept_parser = jobs_subparsers.add_parser("accept", help="Accept delivery") + accept_parser.add_argument("--job-id", required=True, help="Job ID") + accept_parser.add_argument("--poster", required=True, help="Poster wallet") + accept_parser.set_defaults(func=cmd_jobs_accept) + + # Jobs cancel + cancel_parser = jobs_subparsers.add_parser("cancel", help="Cancel job") + cancel_parser.add_argument("--job-id", required=True, help="Job ID") + cancel_parser.add_argument("--poster", required=True, help="Poster wallet") + cancel_parser.set_defaults(func=cmd_jobs_cancel) + + # Reputation subcommand + rep_parser = subparsers.add_parser("reputation", help="Reputation commands") + rep_subparsers = rep_parser.add_subparsers(dest="subcommand") + + rep_get_parser = rep_subparsers.add_parser("get", help="Get reputation") + rep_get_parser.add_argument("--wallet", required=True, help="Wallet address") + rep_get_parser.set_defaults(func=cmd_reputation_get) + + # Stats subcommand + stats_parser = subparsers.add_parser("stats", help="Get market statistics") + stats_parser.set_defaults(func=cmd_stats) + + # Parse and execute + args = parser.parse_args() + + if not args.command: + parser.print_help() + sys.exit(1) + + if hasattr(args, "func"): + args.func(args) + else: + parser.print_help() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/sdk/python/rustchain_agent_sdk/client.py b/sdk/python/rustchain_agent_sdk/client.py new file mode 100644 index 000000000..b0514d13a --- /dev/null +++ b/sdk/python/rustchain_agent_sdk/client.py @@ -0,0 +1,591 @@ +""" +RustChain Agent Economy API Client +=================================== +Main client for interacting with the RIP-302 Agent Economy API. + +This client provides methods for: +- Posting and managing jobs +- Claiming and delivering work +- Checking reputation +- Viewing marketplace statistics + +Usage: + from rustchain_agent_sdk import AgentClient + + client = AgentClient() + + # Post a job + job = client.post_job( + poster_wallet="my-wallet", + title="Write code", + description="Implement feature X", + category="code", + reward_rtc=10.0 + ) + + # List open jobs + jobs = client.list_jobs(status="open", category="code") + + # Claim a job + client.claim_job(job_id="123", worker_wallet="worker-wallet") + + # Deliver work + client.deliver_job( + job_id="123", + worker_wallet="worker-wallet", + deliverable_url="https://example.com/pr", + result_summary="Implemented feature X" + ) + + # Accept delivery (poster) + client.accept_delivery(job_id="123", poster_wallet="my-wallet") +""" + +import ssl +import urllib.request +import json +from typing import Optional, List, Dict, Any +from urllib.error import URLError, HTTPError + +from .models import Job, Reputation, MarketStats +from .exceptions import ( + AgentSDKError, + AuthenticationError, + InsufficientBalanceError, + JobNotFoundError, + InvalidParameterError, + JobStateError, + NetworkError, + APIError, +) + + +class AgentClient: + """ + RustChain Agent Economy API Client. + + Example: + >>> client = AgentClient(base_url="https://rustchain.org") + >>> jobs = client.list_jobs(category="code", limit=10) + >>> print(f"Found {len(jobs)} open coding jobs") + """ + + # Default base URL for RustChain mainnet + DEFAULT_BASE_URL = "https://rustchain.org" + + # Valid job categories + VALID_CATEGORIES = [ + "research", "code", "video", "audio", "writing", + "translation", "data", "design", "testing", "other" + ] + + # Valid job statuses + VALID_STATUSES = [ + "open", "claimed", "delivered", "completed", + "disputed", "expired", "cancelled" + ] + + def __init__( + self, + base_url: str = DEFAULT_BASE_URL, + api_key: Optional[str] = None, + verify_ssl: bool = True, + timeout: int = 30, + retry_count: int = 3, + retry_delay: float = 1.0 + ): + """ + Initialize Agent Economy Client. + + Args: + base_url: Base URL of the RustChain node API + api_key: Optional API key for authentication + verify_ssl: Enable SSL verification (default: True) + timeout: Request timeout in seconds (default: 30) + retry_count: Number of retries on failure (default: 3) + retry_delay: Delay between retries in seconds (default: 1.0) + + Example: + >>> client = AgentClient( + ... base_url="https://rustchain.org", + ... api_key="your-api-key", + ... timeout=60 + ... ) + """ + self.base_url = base_url.rstrip("/") + self.api_key = api_key + self.verify_ssl = verify_ssl + self.timeout = timeout + self.retry_count = retry_count + self.retry_delay = retry_delay + + # Setup SSL context + if not verify_ssl: + self._ctx = ssl.create_default_context() + self._ctx.check_hostname = False + self._ctx.verify_mode = ssl.CERT_NONE + else: + self._ctx = None + + def _get_headers(self) -> Dict[str, str]: + """Get HTTP headers for requests.""" + headers = { + "Content-Type": "application/json", + "Accept": "application/json" + } + if self.api_key: + headers["Authorization"] = f"Bearer {self.api_key}" + return headers + + def _request( + self, + method: str, + endpoint: str, + data: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """ + Make HTTP request with retry logic. + + Args: + method: HTTP method (GET, POST, etc.) + endpoint: API endpoint path + data: Optional request body data + + Returns: + Parsed JSON response as dictionary + + Raises: + NetworkError: If network communication fails + APIError: If API returns an error + """ + import time + + url = f"{self.base_url}{endpoint}" + + for attempt in range(self.retry_count): + try: + req = urllib.request.Request( + url, + data=json.dumps(data).encode('utf-8') if data else None, + headers=self._get_headers(), + method=method + ) + + with urllib.request.urlopen( + req, + context=self._ctx, + timeout=self.timeout + ) as response: + content = response.read().decode('utf-8') + if content: + return json.loads(content) + return {} + + except HTTPError as e: + error_body = e.read().decode('utf-8') if e.fp else "" + try: + error_data = json.loads(error_body) + error_msg = error_data.get("error", error_data.get("message", str(e))) + except: + error_msg = error_body or str(e) + + if e.code == 401: + raise AuthenticationError(f"Authentication failed: {error_msg}") + elif e.code == 404: + raise JobNotFoundError(f"Job not found: {error_msg}") + elif e.code == 400: + raise InvalidParameterError(f"Invalid parameter: {error_msg}") + elif e.code == 402: + raise InsufficientBalanceError(f"Insufficient balance: {error_msg}") + elif e.code == 409: + raise JobStateError(f"Invalid job state: {error_msg}") + else: + if attempt == self.retry: + raise APIError(f"API_count - 1 error ({e.code}): {error_msg}", e.code) + + except URLError as e: + if attempt == self.retry_count - 1: + raise NetworkError(f"Network error: {e.reason}") + + if attempt < self.retry_count - 1: + time.sleep(self.retry_delay) + + raise AgentSDKError("Unexpected error after retries") + + # ------------------------------------------------------------------------- + # Job Management + # ------------------------------------------------------------------------- + + def post_job( + self, + poster_wallet: str, + title: str, + description: str, + category: str = "other", + reward_rtc: float = 1.0, + tags: Optional[List[str]] = None, + ttl_hours: Optional[int] = None + ) -> Job: + """ + Post a new job to the marketplace. + + Args: + poster_wallet: Wallet address of the job poster + title: Job title (max 200 characters) + description: Full job description + category: Job category (research, code, video, audio, writing, + translation, data, design, testing, other) + reward_rtc: Reward amount in RTC (min 0.01, max 10000) + tags: Optional list of job tags + ttl_hours: Job time-to-live in hours (default: 168, max: 720) + + Returns: + Job object with assigned job_id + + Raises: + InvalidParameterError: If parameters are invalid + InsufficientBalanceError: If poster has insufficient balance + + Example: + >>> job = client.post_job( + ... poster_wallet="my-wallet", + ... title="Write a blog post", + ... description="500+ word article about RustChain", + ... category="writing", + ... reward_rtc=5.0, + ... tags=["blog", "documentation"] + ... ) + >>> print(f"Posted job: {job.job_id}") + """ + if category not in self.VALID_CATEGORIES: + raise InvalidParameterError( + f"Invalid category: {category}. " + f"Valid categories: {', '.join(self.VALID_CATEGORIES)}" + ) + + if reward_rtc < 0.01 or reward_rtc > 10000: + raise InvalidParameterError( + "Reward must be between 0.01 and 10000 RTC" + ) + + data = { + "poster_wallet": poster_wallet, + "title": title, + "description": description, + "category": category, + "reward_rtc": reward_rtc, + "tags": tags or [] + } + + if ttl_hours: + if ttl_hours < 1 or ttl_hours > 720: + raise InvalidParameterError("TTL must be between 1 and 720 hours") + data["ttl_hours"] = ttl_hours + + response = self._request("POST", "/agent/jobs", data) + return Job.from_dict(response) + + def list_jobs( + self, + status: str = "open", + category: Optional[str] = None, + poster_wallet: Optional[str] = None, + worker_wallet: Optional[str] = None, + tags: Optional[List[str]] = None, + limit: int = 20, + offset: int = 0 + ) -> List[Job]: + """ + List jobs with optional filters. + + Args: + status: Filter by job status (default: "open") + category: Filter by category + poster_wallet: Filter by poster wallet + worker_wallet: Filter by worker wallet + tags: Filter by tags (any match) + limit: Maximum number of jobs to return (default: 20) + offset: Number of jobs to skip (for pagination) + + Returns: + List of Job objects + + Example: + >>> jobs = client.list_jobs( + ... status="open", + ... category="code", + ... limit=10 + ... ) + >>> for job in jobs: + ... print(f"{job.title} - {job.reward_rtc} RTC") + """ + params = { + "status": status, + "limit": limit, + "offset": offset + } + + if category: + params["category"] = category + if poster_wallet: + params["poster_wallet"] = poster_wallet + if worker_wallet: + params["worker_wallet"] = worker_wallet + if tags: + params["tags"] = ",".join(tags) + + # Build query string + query = "&".join(f"{k}={v}" for k, v in params.items() if v is not None) + + response = self._request("GET", f"/agent/jobs?{query}") + + if isinstance(response, list): + return [Job.from_dict(job) for job in response] + elif "jobs" in response: + return [Job.from_dict(job) for job in response["jobs"]] + else: + return [Job.from_dict(response)] + + def get_job(self, job_id: str) -> Job: + """ + Get details of a specific job. + + Args: + job_id: The job ID to retrieve + + Returns: + Job object with full details + + Raises: + JobNotFoundError: If job doesn't exist + + Example: + >>> job = client.get_job("job-123") + >>> print(f"Status: {job.status}, Worker: {job.worker_wallet}") + """ + response = self._request("GET", f"/agent/jobs/{job_id}") + return Job.from_dict(response) + + def claim_job( + self, + job_id: str, + worker_wallet: str + ) -> Job: + """ + Claim an open job. + + Args: + job_id: The job ID to claim + worker_wallet: Wallet address of the worker claiming the job + + Returns: + Updated Job object with claimed status + + Raises: + JobNotFoundError: If job doesn't exist + JobStateError: If job is not in open state + + Example: + >>> job = client.claim_job( + ... job_id="job-123", + ... worker_wallet="worker-wallet" + ... ) + >>> print(f"Claimed by: {job.worker_wallet}") + """ + data = {"worker_wallet": worker_wallet} + response = self._request("POST", f"/agent/jobs/{job_id}/claim", data) + return Job.from_dict(response) + + def deliver_job( + self, + job_id: str, + worker_wallet: str, + deliverable_url: str, + result_summary: str + ) -> Job: + """ + Submit delivery for a claimed job. + + Args: + job_id: The job ID to deliver + worker_wallet: Wallet address of the worker + deliverable_url: URL where the work can be accessed + result_summary: Summary of the delivered work + + Returns: + Updated Job object with delivered status + + Raises: + JobNotFoundError: If job doesn't exist + JobStateError: If job is not in claimed state + + Example: + >>> job = client.deliver_job( + ... job_id="job-123", + ... worker_wallet="worker-wallet", + ... deliverable_url="https://example.com/pr/123", + ... result_summary="Implemented feature X" + ... ) + >>> print(f"Delivered: {job.status}") + """ + data = { + "worker_wallet": worker_wallet, + "deliverable_url": deliverable_url, + "result_summary": result_summary + } + response = self._request("POST", f"/agent/jobs/{job_id}/deliver", data) + return Job.from_dict(response) + + def accept_delivery( + self, + job_id: str, + poster_wallet: str + ) -> Job: + """ + Accept delivery and release escrow payment. + + Args: + job_id: The job ID to accept + poster_wallet: Wallet address of the job poster + + Returns: + Updated Job object with completed status + + Raises: + JobNotFoundError: If job doesn't exist + JobStateError: If job is not in delivered state + + Example: + >>> job = client.accept_delivery( + ... job_id="job-123", + ... poster_wallet="my-wallet" + ... ) + >>> print(f"Completed! Worker paid.") + """ + data = {"poster_wallet": poster_wallet} + response = self._request("POST", f"/agent/jobs/{job_id}/accept", data) + return Job.from_dict(response) + + def reject_delivery( + self, + job_id: str, + poster_wallet: str, + reason: str + ) -> Job: + """ + Reject delivery and open a dispute. + + Args: + job_id: The job ID to dispute + poster_wallet: Wallet address of the job poster + reason: Reason for rejection + + Returns: + Updated Job object with disputed status + + Raises: + JobNotFoundError: If job doesn't exist + JobStateError: If job is not in delivered state + + Example: + >>> job = client.reject_delivery( + ... job_id="job-123", + ... poster_wallet="my-wallet", + ... reason="Deliverable does not meet requirements" + ... ) + >>> print(f"Disputed: {job.status}") + """ + data = { + "poster_wallet": poster_wallet, + "reason": reason + } + response = self._request("POST", f"/agent/jobs/{job_id}/dispute", data) + return Job.from_dict(response) + + def cancel_job( + self, + job_id: str, + poster_wallet: str + ) -> Job: + """ + Cancel a job and refund escrow. + + Args: + job_id: The job ID to cancel + poster_wallet: Wallet address of the job poster + + Returns: + Updated Job object with cancelled status + + Raises: + JobNotFoundError: If job doesn't exist + JobStateError: If job is already claimed + + Example: + >>> job = client.cancel_job( + ... job_id="job-123", + ... poster_wallet="my-wallet" + ... ) + >>> print(f"Cancelled: {job.status}") + """ + data = {"poster_wallet": poster_wallet} + response = self._request("POST", f"/agent/jobs/{job_id}/cancel", data) + return Job.from_dict(response) + + # ------------------------------------------------------------------------- + # Reputation + # ------------------------------------------------------------------------- + + def get_reputation(self, wallet: str) -> Reputation: + """ + Get reputation score for a wallet. + + Args: + wallet: Wallet address to柄询 + + Returns: + Reputation object with trust score and history + + Example: + >>> rep = client.get_reputation("worker-wallet") + >>> print(f"Trust score: {rep.trust_score}") + >>> print(f"Completed jobs: {rep.successful_jobs}") + """ + response = self._request("GET", f"/agent/reputation/{wallet}") + return Reputation.from_dict(response) + + # ------------------------------------------------------------------------- + # Market Statistics + # ------------------------------------------------------------------------- + + def get_stats(self) -> MarketStats: + """ + Get marketplace statistics. + + Returns: + MarketStats object with overall marketplace data + + Example: + >>> stats = client.get_stats() + >>> print(f"Open jobs: {stats.open_jobs}") + >>> print(f"Total volume: {stats.total_volume_rtc} RTC") + """ + response = self._request("GET", "/agent/stats") + return MarketStats.from_dict(response) + + # ------------------------------------------------------------------------- + # Utility Methods + # ------------------------------------------------------------------------- + + def health_check(self) -> Dict[str, Any]: + """ + Check API health status. + + Returns: + Health status dictionary + + Example: + >>> health = client.health_check() + >>> print(f"Status: {health.get('status')}") + """ + return self._request("GET", "/health") diff --git a/sdk/python/rustchain_agent_sdk/exceptions.py b/sdk/python/rustchain_agent_sdk/exceptions.py new file mode 100644 index 000000000..51f16b206 --- /dev/null +++ b/sdk/python/rustchain_agent_sdk/exceptions.py @@ -0,0 +1,45 @@ +""" +Exception classes for RustChain Agent Economy SDK. +""" + + +class AgentSDKError(Exception): + """Base exception for all SDK errors.""" + pass + + +class AuthenticationError(AgentSDKError): + """Raised when authentication fails.""" + pass + + +class InsufficientBalanceError(AgentSDKError): + """Raised when wallet has insufficient balance for operation.""" + pass + + +class JobNotFoundError(AgentSDKError): + """Raised when a job is not found.""" + pass + + +class InvalidParameterError(AgentSDKError): + """Raised when invalid parameters are provided.""" + pass + + +class JobStateError(AgentSDKError): + """Raised when operation is not valid for current job state.""" + pass + + +class NetworkError(AgentSDKError): + """Raised when network communication fails.""" + pass + + +class APIError(AgentSDKError): + """Raised when API returns an error.""" + def __init__(self, message: str, status_code: int = None): + super().__init__(message) + self.status_code = status_code diff --git a/sdk/python/rustchain_agent_sdk/models.py b/sdk/python/rustchain_agent_sdk/models.py new file mode 100644 index 000000000..10d93ec01 --- /dev/null +++ b/sdk/python/rustchain_agent_sdk/models.py @@ -0,0 +1,201 @@ +""" +Data models for RustChain Agent Economy SDK. +""" + +from dataclasses import dataclass, field +from typing import Optional, List, Dict, Any +from datetime import datetime +from enum import Enum + + +class JobStatus(Enum): + """Job status enumeration.""" + OPEN = "open" + CLAIMED = "claimed" + DELIVERED = "delivered" + COMPLETED = "completed" + DISPUTED = "disputed" + EXPIRED = "expired" + CANCELLED = "cancelled" + + +class ValidCategory(Enum): + """Valid job categories.""" + RESEARCH = "research" + CODE = "code" + VIDEO = "video" + AUDIO = "audio" + WRITING = "writing" + TRANSLATION = "translation" + DATA = "data" + DESIGN = "design" + TESTING = "testing" + OTHER = "other" + + +@dataclass +class Job: + """ + Represents a job in the Agent Economy. + + Attributes: + job_id: Unique identifier for the job + poster_wallet: Wallet address of the job poster + worker_wallet: Wallet address of the assigned worker (if claimed) + title: Job title + description: Full job description + category: Job category (research, code, video, etc.) + reward_rtc: Reward amount in RTC + status: Current job status + tags: List of job tags + deliverable_url: URL of the delivered work (if delivered) + result_summary: Summary of delivered work (if delivered) + created_at: Job creation timestamp + updated_at: Last update timestamp + expires_at: Job expiration timestamp + """ + job_id: str + poster_wallet: str + title: str + description: str + category: str = "other" + reward_rtc: float = 0.0 + reward_i64: int = 0 + escrow_i64: int = 0 + platform_fee_i64: int = 0 + status: str = "open" + worker_wallet: Optional[str] = None + deliverable_url: Optional[str] = None + deliverable_hash: Optional[str] = None + result_summary: Optional[str] = None + rejection_reason: Optional[str] = None + tags: List[str] = field(default_factory=list) + created_at: Optional[str] = None + updated_at: Optional[str] = None + expires_at: Optional[str] = None + ttl_seconds: Optional[int] = None + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Job": + """Create Job from API response dictionary.""" + return cls( + job_id=data.get("job_id", ""), + poster_wallet=data.get("poster_wallet", ""), + worker_wallet=data.get("worker_wallet"), + title=data.get("title", ""), + description=data.get("description", ""), + category=data.get("category", "other"), + reward_rtc=data.get("reward_rtc", 0.0), + reward_i64=data.get("reward_i64", 0), + escrow_i64=data.get("escrow_i64", 0), + platform_fee_i64=data.get("platform_fee_i64", 0), + status=data.get("status", "open"), + deliverable_url=data.get("deliverable_url"), + deliverable_hash=data.get("deliverable_hash"), + result_summary=data.get("result_summary"), + rejection_reason=data.get("rejection_reason"), + tags=data.get("tags", []), + created_at=data.get("created_at"), + updated_at=data.get("updated_at"), + expires_at=data.get("expires_at"), + ttl_seconds=data.get("ttl_seconds"), + ) + + def to_dict(self) -> Dict[str, Any]: + """Convert Job to dictionary for API requests.""" + result = { + "job_id": self.job_id, + "poster_wallet": self.poster_wallet, + "title": self.title, + "description": self.description, + "category": self.category, + "reward_rtc": self.reward_rtc, + "tags": self.tags, + } + if self.worker_wallet: + result["worker_wallet"] = self.worker_wallet + if self.deliverable_url: + result["deliverable_url"] = self.deliverable_url + if self.result_summary: + result["result_summary"] = self.result_summary + return result + + +@dataclass +class Reputation: + """ + Represents an agent's reputation score. + + Attributes: + wallet: Wallet address + trust_score: Trust score (0-100) + total_jobs: Total number of jobs completed + successful_jobs: Number of successfully completed jobs + failed_jobs: Number of failed/disputed jobs + average_rating: Average rating (if available) + created_at: Account creation timestamp + last_active: Last activity timestamp + """ + wallet: str + trust_score: float = 0.0 + total_jobs: int = 0 + successful_jobs: int = 0 + failed_jobs: int = 0 + average_rating: Optional[float] = None + created_at: Optional[str] = None + last_active: Optional[str] = None + history: List[Dict[str, Any]] = field(default_factory=list) + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Reputation": + """Create Reputation from API response dictionary.""" + return cls( + wallet=data.get("wallet", ""), + trust_score=data.get("trust_score", 0.0), + total_jobs=data.get("total_jobs", 0), + successful_jobs=data.get("successful_jobs", 0), + failed_jobs=data.get("failed_jobs", 0), + average_rating=data.get("average_rating"), + created_at=data.get("created_at"), + last_active=data.get("last_active"), + history=data.get("history", []), + ) + + +@dataclass +class MarketStats: + """ + Represents marketplace statistics. + + Attributes: + total_jobs: Total number of jobs ever posted + open_jobs: Number of currently open jobs + claimed_jobs: Number of claimed jobs + completed_jobs: number of completed jobs + total_volume_rtc: Total RTC volume in marketplace + average_reward: Average job reward + top_categories: Top categories by job count + active_agents: Number of active agents + """ + total_jobs: int = 0 + open_jobs: int = 0 + claimed_jobs: int = 0 + completed_jobs: int = 0 + total_volume_rtc: float = 0.0 + average_reward: float = 0.0 + top_categories: List[Dict[str, int]] = field(default_factory=list) + active_agents: int = 0 + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "MarketStats": + """Create MarketStats from API response dictionary.""" + return cls( + total_jobs=data.get("total_jobs", 0), + open_jobs=data.get("open_jobs", 0), + claimed_jobs=data.get("claimed_jobs", 0), + completed_jobs=data.get("completed_jobs", 0), + total_volume_rtc=data.get("total_volume_rtc", 0.0), + average_reward=data.get("average_reward", 0.0), + top_categories=data.get("top_categories", []), + active_agents=data.get("active_agents", 0), + ) diff --git a/sdk/python/rustchain_agent_sdk/setup.py b/sdk/python/rustchain_agent_sdk/setup.py new file mode 100644 index 000000000..83a20968d --- /dev/null +++ b/sdk/python/rustchain_agent_sdk/setup.py @@ -0,0 +1,55 @@ +""" +Setup script for rustchain-agent-sdk +""" + +from setuptools import setup, find_packages +from pathlib import Path + +# Read long description from README +this_directory = Path(__file__).parent +long_description = (this_directory / "README.md").read_text() + +setup( + name="rustchain-agent-sdk", + version="1.0.0", + author="sososonia-cyber", + author_email="sososonia@example.com", + description="Python SDK for RustChain RIP-302 Agent Economy", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/sososonia-cyber/Rustchain", + packages=find_packages(), + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + ], + python_requires=">=3.8", + install_requires=[ + # No external dependencies - uses only stdlib + ], + extras_require={ + "dev": [ + "pytest>=7.0.0", + "pytest-asyncio>=0.21.0", + "black>=23.0.0", + "mypy>=1.0.0", + ] + }, + entry_points={ + "console_scripts": [ + "rustchain-agent=rustchain_agent_sdk.cli:main", + ], + }, + keywords="rustchain blockchain agent economy sdk", + project_urls={ + "Bug Reports": "https://github.com/sososonia-cyber/Rustchain/issues", + "Source": "https://github.com/sososonia-cyber/Rustchain", + }, +) diff --git a/sdk/python/rustchain_agent_sdk/test_agent_sdk.py b/sdk/python/rustchain_agent_sdk/test_agent_sdk.py new file mode 100644 index 000000000..89fca59b2 --- /dev/null +++ b/sdk/python/rustchain_agent_sdk/test_agent_sdk.py @@ -0,0 +1,292 @@ +""" +Tests for RustChain Agent Economy SDK. +""" + +import unittest +from unittest.mock import Mock, patch, MagicMock +import json + +from rustchain_agent_sdk import AgentClient +from rustchain_agent_sdk.models import Job, Reputation, MarketStats +from rustchain_agent_sdk.exceptions import ( + AgentSDKError, + AuthenticationError, + InsufficientBalanceError, + JobNotFoundError, + InvalidParameterError, + JobStateError +) + + +class TestAgentClient(unittest.TestCase): + """Test cases for AgentClient.""" + + def setUp(self): + """Set up test fixtures.""" + self.client = AgentClient(base_url="https://test.rustchain.org") + + @patch('rustchain_agent_sdk.client.urllib.request.urlopen') + def test_post_job_success(self, mock_urlopen): + """Test posting a job successfully.""" + mock_response = Mock() + mock_response.read.return_value = json.dumps({ + "job_id": "test-job-123", + "poster_wallet": "my-wallet", + "title": "Test Job", + "description": "Test description", + "category": "code", + "reward_rtc": 10.0, + "status": "open" + }).encode() + mock_response.__enter__ = Mock(return_value=mock_response) + mock_response.__exit__ = Mock(return_value=False) + mock_urlopen.return_value = mock_response + + job = self.client.post_job( + poster_wallet="my-wallet", + title="Test Job", + description="Test description", + category="code", + reward_rtc=10.0 + ) + + self.assertEqual(job.job_id, "test-job-123") + self.assertEqual(job.title, "Test Job") + self.assertEqual(job.status, "open") + + def test_post_job_invalid_category(self): + """Test posting a job with invalid category.""" + with self.assertRaises(InvalidParameterError): + self.client.post_job( + poster_wallet="my-wallet", + title="Test", + description="Test", + category="invalid_category", + reward_rtc=10.0 + ) + + def test_post_job_invalid_reward(self): + """Test posting a job with invalid reward.""" + with self.assertRaises(InvalidParameterError): + self.client.post_job( + poster_wallet="my-wallet", + title="Test", + description="Test", + reward_rtc=0.001 # Too low + ) + + with self.assertRaises(InvalidParameterError): + self.client.post_job( + poster_wallet="my-wallet", + title="Test", + description="Test", + reward_rtc=10001 # Too high + ) + + @patch('rustchain_agent_sdk.client.urllib.request.urlopen') + def test_list_jobs(self, mock_urlopen): + """Test listing jobs.""" + mock_response = Mock() + mock_response.read.return_value = json.dumps([ + { + "job_id": "job-1", + "title": "Job 1", + "status": "open", + "reward_rtc": 5.0, + "poster_wallet": "wallet1" + }, + { + "job_id": "job-2", + "title": "Job 2", + "status": "open", + "reward_rtc": 10.0, + "poster_wallet": "wallet2" + } + ]).encode() + mock_response.__enter__ = Mock(return_value=mock_response) + mock_response.__exit__ = Mock(return_value=False) + mock_urlopen.return_value = mock_response + + jobs = self.client.list_jobs(category="code") + + self.assertEqual(len(jobs), 2) + self.assertEqual(jobs[0].job_id, "job-1") + self.assertEqual(jobs[1].job_id, "job-2") + + @patch('rustchain_agent_sdk.client.urllib.request.urlopen') + def test_get_job(self, mock_urlopen): + """Test getting job details.""" + mock_response = Mock() + mock_response.read.return_value = json.dumps({ + "job_id": "test-job", + "title": "Test Job", + "description": "Test description", + "status": "claimed", + "reward_rtc": 15.0, + "poster_wallet": "poster-wallet", + "worker_wallet": "worker-wallet" + }).encode() + mock_response.__enter__ = Mock(return_value=mock_response) + mock_response.__exit__ = Mock(return_value=False) + mock_urlopen.return_value = mock_response + + job = self.client.get_job("test-job") + + self.assertEqual(job.job_id, "test-job") + self.assertEqual(job.status, "claimed") + self.assertEqual(job.worker_wallet, "worker-wallet") + + @patch('rustchain_agent_sdk.client.urllib.request.urlopen') + def test_claim_job(self, mock_urlopen): + """Test claiming a job.""" + mock_response = Mock() + mock_response.read.return_value = json.dumps({ + "job_id": "test-job", + "status": "claimed", + "worker_wallet": "worker-wallet" + }).encode() + mock_response.__enter__ = Mock(return_value=mock_response) + mock_response.__exit__ = Mock(return_value=False) + mock_urlopen.return_value = mock_response + + job = self.client.claim_job("test-job", "worker-wallet") + + self.assertEqual(job.status, "claimed") + self.assertEqual(job.worker_wallet, "worker-wallet") + + @patch('rustchain_agent_sdk.client.urllib_request.urlopen') + def test_get_reputation(self, mock_urlopen): + """Test getting reputation.""" + mock_response = Mock() + mock_response.read.return_value = json.dumps({ + "wallet": "test-wallet", + "trust_score": 95.5, + "total_jobs": 100, + "successful_jobs": 98, + "failed_jobs": 2 + }).encode() + mock_response.__enter__ = Mock(return_value=mock_response) + mock_response.__exit__ = Mock(return_value=False) + mock_urlopen.return_value = mock_response + + rep = self.client.get_reputation("test-wallet") + + self.assertEqual(rep.wallet, "test-wallet") + self.assertEqual(rep.trust_score, 95.5) + self.assertEqual(rep.total_jobs, 100) + + @patch('rustchain_agent_sdk.client.urllib.request.urlopen') + def test_get_stats(self, mock_urlopen): + """Test getting market stats.""" + mock_response = Mock() + mock_response.read.return_value = json.dumps({ + "total_jobs": 1000, + "open_jobs": 50, + "claimed_jobs": 30, + "completed_jobs": 900, + "total_volume_rtc": 5000.0, + "average_reward": 5.0, + "active_agents": 200 + }).encode() + mock_response.__enter__ = Mock(return_value=mock_response) + mock_response.__exit__ = Mock(return_value=False) + mock_urlopen.return_value = mock_response + + stats = self.client.get_stats() + + self.assertEqual(stats.total_jobs, 1000) + self.assertEqual(stats.open_jobs, 50) + self.assertEqual(stats.total_volume_rtc, 5000.0) + + +class TestJobModel(unittest.TestCase): + """Test cases for Job model.""" + + def test_job_from_dict(self): + """Test creating Job from dictionary.""" + data = { + "job_id": "test-123", + "poster_wallet": "wallet1", + "title": "Test Job", + "description": "Description", + "category": "code", + "reward_rtc": 10.0, + "status": "open", + "tags": ["python", "api"] + } + + job = Job.from_dict(data) + + self.assertEqual(job.job_id, "test-123") + self.assertEqual(job.poster_wallet, "wallet1") + self.assertEqual(job.title, "Test Job") + self.assertEqual(job.category, "code") + self.assertEqual(job.reward_rtc, 10.0) + self.assertEqual(job.status, "open") + self.assertEqual(job.tags, ["python", "api"]) + + def test_job_to_dict(self): + """Test converting Job to dictionary.""" + job = Job( + job_id="test-123", + poster_wallet="wallet1", + title="Test Job", + description="Description", + category="code", + reward_rtc=10.0, + tags=["python"] + ) + + data = job.to_dict() + + self.assertEqual(data["job_id"], "test-123") + self.assertEqual(data["poster_wallet"], "wallet1") + self.assertEqual(data["title"], "Test Job") + self.assertEqual(data["reward_rtc"], 10.0) + + +class TestReputationModel(unittest.TestCase): + """Test cases for Reputation model.""" + + def test_reputation_from_dict(self): + """Test creating Reputation from dictionary.""" + data = { + "wallet": "test-wallet", + "trust_score": 90.0, + "total_jobs": 50, + "successful_jobs": 48, + "failed_jobs": 2 + } + + rep = Reputation.from_dict(data) + + self.assertEqual(rep.wallet, "test-wallet") + self.assertEqual(rep.trust_score, 90.0) + self.assertEqual(rep.total_jobs, 50) + self.assertEqual(rep.successful_jobs, 48) + + +class TestMarketStatsModel(unittest.TestCase): + """Test cases for MarketStats model.""" + + def test_market_stats_from_dict(self): + """Test creating MarketStats from dictionary.""" + data = { + "total_jobs": 1000, + "open_jobs": 100, + "claimed_jobs": 50, + "completed_jobs": 850, + "total_volume_rtc": 5000.0, + "average_reward": 5.0, + "active_agents": 150 + } + + stats = MarketStats.from_dict(data) + + self.assertEqual(stats.total_jobs, 1000) + self.assertEqual(stats.open_jobs, 100) + self.assertEqual(stats.total_volume_rtc, 5000.0) + + +if __name__ == '__main__': + unittest.main() diff --git a/sdk/utils/.gitignore b/sdk/utils/.gitignore new file mode 100644 index 000000000..dd6e803c7 --- /dev/null +++ b/sdk/utils/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.log +.DS_Store diff --git a/sdk/utils/README.md b/sdk/utils/README.md new file mode 100644 index 000000000..2ca29bef5 --- /dev/null +++ b/sdk/utils/README.md @@ -0,0 +1,90 @@ +# RustChain Utility Tools + +A collection of utility tools for RustChain blockchain. + +## Tools Included + +### 1. Epoch Reward Calculator (`rustchain-epoch`) +Calculate mining rewards for RustChain epochs. + +```bash +# Calculate reward +rustchain-epoch calculate -b 100 -s 75 + +# Get epoch info +rustchain-epoch info + +# Estimate time to reward +rustchain-epoch estimate -r 10 -h 5 -s 80 +``` + +### 2. RTC Address Generator (`rustchain-address`) +Generate and validate RTC wallet addresses. + +```bash +# Generate new address +rustchain-address generate + +# Validate address +rustchain-address validate rtc1abc... + +# Generate from public key +rustchain-address from-pubkey +``` + +### 3. Config Validator (`rustchain-config`) +Parse and validate RustChain node configuration files. + +```bash +# Validate config +rustchain-config validate config.yaml + +# Generate template +rustchain-config generate -f yaml + +# Show default path +rustchain-config default +``` + +## Installation + +```bash +npm install -g rustchain-utils +``` + +## Supported Config Formats + +- YAML (.yaml, .yml) +- JSON (.json) +- TOML (.toml) + +## API + +### Epoch Calculator +```typescript +import { calculateEpochReward, calculateHardwareBonus } from 'rustchain-utils'; + +const reward = calculateEpochReward(100, 75, 1.0); +const bonus = calculateHardwareBonus(75); // 2.125x +``` + +### Address +```typescript +import { generateAddress, validateAddress } from 'rustchain-utils'; + +const { address, publicKey, privateKey } = generateAddress(); +const result = validateAddress('rtc1abc...'); +``` + +### Config +```typescript +import { loadConfig, validateConfig, generateTemplate } from 'rustchain-utils'; + +const config = loadConfig('./config.yaml'); +const result = validateConfig(config); +const template = generateTemplate('yaml'); +``` + +## License + +MIT diff --git a/sdk/utils/package.json b/sdk/utils/package.json new file mode 100644 index 000000000..b734eaf21 --- /dev/null +++ b/sdk/utils/package.json @@ -0,0 +1,30 @@ +{ + "name": "rustchain-utils", + "version": "1.0.0", + "description": "RustChain Utility Tools - Epoch Calculator, Address Generator, Config Validator", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "bin": { + "rustchain-epoch": "./dist/epoch.js", + "rustchain-address": "./dist/address.js", + "rustchain-config": "./dist/config.js" + }, + "scripts": { + "build": "tsc", + "test": "echo \"No tests yet\" && exit 0" + }, + "keywords": ["rustchain", "blockchain", "utils"], + "author": "", + "license": "MIT", + "dependencies": { + "axios": "^1.6.0", + "commander": "^11.0.0", + "chalk": "^4.1.0", + "inquirer": "^8.0.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/inquirer": "^8.0.0", + "typescript": "^5.0.0" + } +} diff --git a/sdk/utils/src/address.ts b/sdk/utils/src/address.ts new file mode 100644 index 000000000..1865f6c74 --- /dev/null +++ b/sdk/utils/src/address.ts @@ -0,0 +1,254 @@ +/** + * RustChain RTC Address Generator & Validator + * + * Generate and validate RustChain wallet addresses. + * RTC addresses are Bech32 encoded Ed25519 public keys. + */ + +import * as crypto from 'crypto'; + +// Bech32 character set +const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'; + +// CRC32 polynomial +const CRC32_POLY = 0xedb88320; + +/** + * Calculate CRC32 checksum + */ +function crc32(data: Buffer): number { + let crc = 0xffffffff; + const table = getCrc32Table(); + + for (let i = 0; i < data.length; i++) { + crc = table[(crc ^ data[i]) & 0xff] ^ (crc >>> 8); + } + + return (crc ^ 0xffffffff) >>> 0; +} + +let crc32Table: number[] | null = null; + +function getCrc32Table(): number[] { + if (crc32Table) return crc32Table; + + crc32Table = []; + for (let n = 0; n < 256; n++) { + let c = n; + for (let k = 0; k < 8; k++) { + c = ((c & 1) ? (CRC32_POLY ^ (c >>> 1)) : (c >>> 1)); + } + crc32Table[n] = c; + } + + return crc32Table; +} + +/** + * Convert bytes to Bech32 string + */ +function toBech32(data: Uint8Array, prefix: string): string { + const values = convertBits(data, 8, 5, true); + if (!values) throw new Error('Failed to convert bits'); + + const combined = [...values, ...values.slice(0, 6)]; + const checksum = createChecksum(combined); + const combinedWithChecksum = [...combined, ...checksum]; + + const result = combinedWithChecksum.map(v => CHARSET[v]).join(''); + return `${prefix}1${result}`; +} + +/** + * Convert bits between different group sizes + */ +function convertBits(data: Uint8Array, fromBits: number, toBits: number, pad: boolean): number[] | null { + let acc = 0; + let bits = 0; + const result: number[] = []; + const maxv = (1 << toBits) - 1; + + for (let i = 0; i < data.length; i++) { + const value = data[i]; + if ((value >> fromBits) !== 0) return null; + + acc = (acc << fromBits) | value; + bits += fromBits; + + while (bits >= toBits) { + bits -= toBits; + result.push((acc >> bits) & maxv); + } + } + + if (pad) { + if (bits > 0) { + result.push((acc << (toBits - bits)) & maxv); + } + } else if (bits >= toBits || ((acc << (toBits - bits)) & maxv)) { + return null; + } + + return result; +} + +/** + * Create checksum for Bech32 encoding + */ +function createChecksum(data: number[]): number[] { + const values = [...data, 0, 0, 0, 0, 0, 0]; + const mod = crc32(Buffer.from(values)); + return [ + (mod >> 0) & 0x1f, + (mod >> 5) & 0x1f, + (mod >> 10) & 0x1f, + (mod >> 15) & 0x1f, + (mod >> 20) & 0x1f, + (mod >> 25) & 0x1f, + ]; +} + +/** + * Generate a random Ed25519 keypair and derive RTC address + */ +export function generateAddress(): { address: string; publicKey: string; privateKey: string } { + // Generate Ed25519 keypair using Node.js crypto + const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519'); + + const publicKeyDer = publicKey.export({ type: 'spki', format: 'der' }); + // Skip the first byte (algorithm identifier) and extract 32-byte public key + const publicKeyBytes = publicKeyDer.slice(-32); + + const privateKeyDer = privateKey.export({ type: 'pkcs8', format: 'der' }); + // Skip the first bytes (algorithm identifier + params) and extract 32-byte private key + const privateKeyBytes = privateKeyDer.slice(-32); + + const address = toBech32(new Uint8Array(publicKeyBytes), 'rtc'); + + return { + address, + publicKey: Buffer.from(publicKeyBytes).toString('hex'), + privateKey: Buffer.from(privateKeyBytes).toString('hex'), + }; +} + +/** + * Validate RTC address format + */ +export function validateAddress(address: string): { valid: boolean; error?: string; prefix?: string; data?: string } { + // Check minimum length + if (address.length < 14) { + return { valid: false, error: 'Address too short' }; + } + + // Check prefix + if (!address.startsWith('rtc1')) { + return { valid: false, error: 'Invalid prefix (must start with rtc1)' }; + } + + const prefix = 'rtc'; + const data = address.slice(4); + + // Check valid characters + for (const char of data) { + if (!CHARSET.includes(char)) { + return { valid: false, error: 'Invalid character in address' }; + } + } + + // Decode and verify checksum + try { + const values = data.split('').map(c => CHARSET.indexOf(c)); + const dataPart = values.slice(0, -6); + const checksumPart = values.slice(-6); + + const combined = [...dataPart, ...dataPart.slice(0, 6), ...dataPart, ...dataPart.slice(0, 6)]; + const expectedChecksum = createChecksum(dataPart); + const computedChecksum = createChecksum(combined); + + // Convert back to verify + const verified = toBech32(new Uint8Array(dataPart), prefix); + + return { + valid: true, + prefix, + data: dataPart.map(v => CHARSET[v]).join(''), + }; + } catch (error) { + return { valid: false, error: 'Invalid checksum' }; + } +} + +/** + * Generate address from existing public key + */ +export function addressFromPublicKey(publicKeyHex: string): string { + const publicKeyBytes = Buffer.from(publicKeyHex, 'hex'); + if (publicKeyBytes.length !== 32) { + throw new Error('Public key must be 32 bytes'); + } + + return toBech32(new Uint8Array(publicKeyBytes), 'rtc'); +} + +// CLI Interface +import { Command } from 'commander'; +import chalk from 'chalk'; + +const program = new Command(); + +program + .name('rustchain-address') + .description('RustChain RTC Address Generator & Validator') + .version('1.0.0'); + +program + .command('generate') + .description('Generate a new RTC address') + .action(() => { + console.log(chalk.blue('\nšŸ”‘ Generating new RTC address...\n')); + + const { address, publicKey, privateKey } = generateAddress(); + + console.log(chalk.green('āœ… Address:'), chalk.cyan(address)); + console.log(chalk.green('šŸ“¢ Public Key:'), publicKey); + console.log(chalk.red('šŸ”’ Private Key:'), privateKey); + console.log(chalk.yellow('\nāš ļø Keep your private key safe!')); + console.log(''); + }); + +program + .command('validate
') + .description('Validate an RTC address') + .action((address) => { + console.log(chalk.blue(`\nšŸ” Validating address: ${address}\n`)); + + const result = validateAddress(address); + + if (result.valid) { + console.log(chalk.green('āœ… Address is valid')); + if (result.prefix) { + console.log(chalk.cyan('Prefix:'), result.prefix); + } + } else { + console.log(chalk.red('āŒ Address is invalid')); + if (result.error) { + console.log(chalk.yellow('Error:'), result.error); + } + } + console.log(''); + }); + +program + .command('from-pubkey ') + .description('Generate address from public key hex') + .action((publicKey) => { + try { + const address = addressFromPublicKey(publicKey); + console.log(chalk.green('\nāœ… Address:'), chalk.cyan(address), '\n'); + } catch (error: any) { + console.log(chalk.red('\nāŒ Error:'), error.message, '\n'); + } + }); + +program.parse(); diff --git a/sdk/utils/src/config.ts b/sdk/utils/src/config.ts new file mode 100644 index 000000000..dd26a2034 --- /dev/null +++ b/sdk/utils/src/config.ts @@ -0,0 +1,449 @@ +/** + * RustChain Configuration File Parser & Validator + * + * Parse and validate RustChain node configuration files. + * Supports YAML, JSON, and TOML formats. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; + +export interface RustChainConfig { + // Node settings + node?: { + host?: string; + port?: number; + ssl?: boolean; + sslCert?: string; + sslKey?: string; + }; + + // Network settings + network?: { + p2pPort?: number; + bootstrapNodes?: string[]; + maxPeers?: number; + enableUpnp?: boolean; + }; + + // Mining settings + mining?: { + enabled?: boolean; + threads?: number; + wallet?: string; + attestation?: boolean; + fingerprintThreshold?: number; + }; + + // Database settings + database?: { + path?: string; + maxSize?: number; + backupEnabled?: boolean; + }; + + // Logging settings + logging?: { + level?: 'debug' | 'info' | 'warn' | 'error'; + file?: string; + maxFiles?: number; + }; + + // API settings + api?: { + enabled?: boolean; + port?: number; + cors?: boolean; + apiKeys?: string[]; + }; +} + +export interface ValidationError { + field: string; + message: string; + severity: 'error' | 'warning'; +} + +export interface ValidationResult { + valid: boolean; + errors: ValidationError[]; + config?: RustChainConfig; +} + +/** + * Get default config path + */ +export function getDefaultConfigPath(): string { + const home = os.homedir(); + return path.join(home, '.rustchain', 'config.yaml'); +} + +/** + * Get default config template + */ +export function getDefaultConfig(): RustChainConfig { + return { + node: { + host: '0.0.0.0', + port: 8333, + ssl: false, + }, + network: { + p2pPort: 9333, + bootstrapNodes: [ + 'rtc1:seed1.rustchain.org:9333', + 'rtc1:seed2.rustchain.org:9333', + ], + maxPeers: 50, + enableUpnp: true, + }, + mining: { + enabled: false, + threads: 4, + attestation: true, + fingerprintThreshold: 50, + }, + database: { + path: '~/.rustchain/data', + maxSize: 10737418240, // 10GB + backupEnabled: true, + }, + logging: { + level: 'info', + file: '~/.rustchain/logs/rustchain.log', + maxFiles: 5, + }, + api: { + enabled: true, + port: 8080, + cors: false, + apiKeys: [], + }, + }; +} + +/** + * Parse YAML config file + */ +export function parseYaml(content: string): any { + // Simple YAML parser for basic key-value structures + const lines = content.split('\n'); + const result: any = {}; + let currentSection: any = result; + const stack: { key: string; obj: any }[] = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + // Skip comments and empty lines + if (!line || line.startsWith('#')) continue; + + // Check for section header + const sectionMatch = line.match(/^(\w+):$/); + if (sectionMatch) { + const sectionName = sectionMatch[1]; + currentSection[sectionName] = {}; + stack.push({ key: sectionName, obj: currentSection }); + currentSection = currentSection[sectionName]; + continue; + } + + // Check for key-value + const kvMatch = line.match(/^(\w+):\s*(.*)$/); + if (kvMatch) { + const key = kvMatch[1]; + let value: any = kvMatch[2].trim(); + + // Parse value type + if (value === 'true' || value === 'false') { + value = value === 'true'; + } else if (!isNaN(Number(value))) { + value = Number(value); + } else if (value.startsWith('"') && value.endsWith('"')) { + value = value.slice(1, -1); + } else if (value.startsWith("'") && value.endsWith("'")) { + value = value.slice(1, -1); + } + + currentSection[key] = value; + } + } + + return result; +} + +/** + * Parse JSON config file + */ +export function parseJson(content: string): any { + return JSON.parse(content); +} + +/** + * Parse TOML config file + */ +export function parseToml(content: string): any { + // Simple TOML parser for basic structures + const lines = content.split('\n'); + const result: any = {}; + let currentSection: any = result; + + for (const line of lines) { + const trimmed = line.trim(); + + // Skip comments and empty lines + if (!trimmed || trimmed.startsWith('#')) continue; + + // Section header + if (trimmed.startsWith('[') && trimmed.endsWith(']')) { + const sectionName = trimmed.slice(1, -1); + result[sectionName] = {}; + currentSection = result[sectionName]; + continue; + } + + // Key-value + const kvMatch = trimmed.match(/^(\w+)\s*=\s*(.*)$/); + if (kvMatch) { + const key = kvMatch[1]; + let value: any = kvMatch[2].trim(); + + // Parse value type + if (value === 'true' || value === 'false') { + value = value === 'true'; + } else if (!isNaN(Number(value))) { + value = Number(value); + } else if (value.startsWith('"') && value.endsWith('"')) { + value = value.slice(1, -1); + } else if (value.startsWith("'") && value.endsWith("'")) { + value = value.slice(1, -1); + } + + currentSection[key] = value; + } + } + + return result; +} + +/** + * Load config from file + */ +export function loadConfig(configPath: string): RustChainConfig { + const ext = path.extname(configPath).toLowerCase(); + const content = fs.readFileSync(configPath, 'utf-8'); + + switch (ext) { + case '.yaml': + case '.yml': + return parseYaml(content); + case '.json': + return parseJson(content); + case '.toml': + return parseToml(content); + default: + // Try to detect format + if (content.trim().startsWith('{')) { + return parseJson(content); + } else if (content.trim().startsWith('[')) { + return parseToml(content); + } + return parseYaml(content); + } +} + +/** + * Validate config + */ +export function validateConfig(config: RustChainConfig): ValidationResult { + const errors: ValidationError[] = []; + + // Validate node settings + if (config.node) { + if (config.node.port !== undefined && (config.node.port < 1 || config.node.port > 65535)) { + errors.push({ field: 'node.port', message: 'Port must be between 1 and 65535', severity: 'error' }); + } + + if (config.node.host !== undefined && !isValidHost(config.node.host)) { + errors.push({ field: 'node.host', message: 'Invalid host address', severity: 'warning' }); + } + } + + // Validate network settings + if (config.network) { + if (config.network.p2pPort !== undefined && (config.network.p2pPort < 1 || config.network.p2pPort > 65535)) { + errors.push({ field: 'network.p2pPort', message: 'Port must be between 1 and 65535', severity: 'error' }); + } + + if (config.network.maxPeers !== undefined && (config.network.maxPeers < 1 || config.network.maxPeers > 1000)) { + errors.push({ field: 'network.maxPeers', message: 'Max peers should be between 1 and 1000', severity: 'warning' }); + } + } + + // Validate mining settings + if (config.mining) { + if (config.mining.threads !== undefined && (config.mining.threads < 1 || config.mining.threads > 128)) { + errors.push({ field: 'mining.threads', message: 'Threads should be between 1 and 128', severity: 'warning' }); + } + + if (config.mining.fingerprintThreshold !== undefined && (config.mining.fingerprintThreshold < 0 || config.mining.fingerprintThreshold > 100)) { + errors.push({ field: 'mining.fingerprintThreshold', message: 'Fingerprint threshold must be between 0 and 100', severity: 'error' }); + } + } + + // Validate database settings + if (config.database) { + if (config.database.maxSize !== undefined && config.database.maxSize < 1048576) { + errors.push({ field: 'database.maxSize', message: 'Minimum database size is 1MB', severity: 'warning' }); + } + } + + // Validate API settings + if (config.api) { + if (config.api.port !== undefined && (config.api.port < 1 || config.api.port > 65535)) { + errors.push({ field: 'api.port', message: 'Port must be between 1 and 65535', severity: 'error' }); + } + } + + // Validate logging settings + if (config.logging) { + const validLevels = ['debug', 'info', 'warn', 'error']; + if (config.logging.level && !validLevels.includes(config.logging.level)) { + errors.push({ field: 'logging.level', message: `Invalid log level. Must be one of: ${validLevels.join(', ')}`, severity: 'error' }); + } + } + + return { + valid: errors.filter(e => e.severity === 'error').length === 0, + errors, + config, + }; +} + +/** + * Validate host string + */ +function isValidHost(host: string): boolean { + // Check for valid IP or hostname + const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/; + const hostnameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; + + return ipRegex.test(host) || hostnameRegex.test(host); +} + +/** + * Generate config template + */ +export function generateTemplate(format: 'yaml' | 'json' | 'toml' = 'yaml'): string { + const config = getDefaultConfig(); + + switch (format) { + case 'json': + return JSON.stringify(config, null, 2); + case 'toml': + // Simple toml conversion + let toml = ''; + for (const [section, values] of Object.entries(config)) { + toml += `[${section}]\n`; + for (const [key, value] of Object.entries(values as any)) { + if (typeof value === 'string') { + toml += `${key} = "${value}"\n`; + } else if (Array.isArray(value)) { + toml += `${key} = ${JSON.stringify(value)}\n`; + } else { + toml += `${key} = ${value}\n`; + } + } + toml += '\n'; + } + return toml; + default: + // YAML + let yaml = ''; + for (const [section, values] of Object.entries(config)) { + yaml += `${section}:\n`; + for (const [key, value] of Object.entries(values as any)) { + if (typeof value === 'string') { + yaml += ` ${key}: ${value}\n`; + } else if (Array.isArray(value)) { + yaml += ` ${key}:\n`; + for (const item of value) { + yaml += ` - ${item}\n`; + } + } else { + yaml += ` ${key}: ${value}\n`; + } + } + yaml += '\n'; + } + return yaml; + } +} + +// CLI Interface +import { Command } from 'commander'; +import chalk from 'chalk'; + +const program = new Command(); + +program + .name('rustchain-config') + .description('RustChain Configuration Parser & Validator') + .version('1.0.0'); + +program + .command('validate ') + .description('Validate a RustChain config file') + .action((configFile) => { + console.log(chalk.blue(`\nšŸ” Validating config: ${configFile}\n`)); + + try { + const config = loadConfig(configFile); + const result = validateConfig(config); + + if (result.valid) { + console.log(chalk.green('āœ… Configuration is valid')); + } else { + console.log(chalk.red('āŒ Configuration has errors:')); + } + + if (result.errors.length > 0) { + console.log(chalk.yellow('\nIssues found:')); + for (const error of result.errors) { + const icon = error.severity === 'error' ? 'āŒ' : 'āš ļø'; + console.log(` ${icon} [${error.field}] ${error.message}`); + } + } + console.log(''); + } catch (error: any) { + console.log(chalk.red(`\nāŒ Error loading config: ${error.message}\n`)); + } + }); + +program + .command('generate') + .description('Generate default config template') + .option('-f, --format ', 'Output format (yaml, json, toml)', 'yaml') + .option('-o, --output ', 'Output file') + .action((options) => { + const template = generateTemplate(options.format); + + if (options.output) { + fs.writeFileSync(options.output, template); + console.log(chalk.green(`\nāœ… Config template saved to: ${options.output}\n`)); + } else { + console.log(chalk.blue('\nšŸ“„ Default Configuration Template:\n')); + console.log(template); + } + }); + +program + .command('default') + .description('Show default config path') + .action(() => { + console.log(chalk.blue('\nšŸ“ Default config path:')); + console.log(chalk.cyan(getDefaultConfigPath()), '\n'); + }); + +program.parse(); diff --git a/sdk/utils/src/epoch.ts b/sdk/utils/src/epoch.ts new file mode 100644 index 000000000..c2ff798a5 --- /dev/null +++ b/sdk/utils/src/epoch.ts @@ -0,0 +1,187 @@ +/** + * RustChain Epoch Reward Calculator + * + * Calculate rewards for mining epochs on RustChain blockchain. + * + * Base reward formula considers: + * - Hardware fingerprint score (2.5x for vintage hardware) + * - Block difficulty + * - Epoch duration + */ + +import axios from 'axios'; + +const API_BASE = 'https://rustchain.org'; + +// RustChain epoch parameters +const BASE_REWARD = 1.0; // Base RTC per block +const EPOCH_DURATION_BLOCKS = 1000; +const HARDWARE_BONUS_MULTIPLIER = 2.5; // Max for vintage hardware + +interface EpochInfo { + epoch: number; + startBlock: number; + endBlock: number; + difficulty: number; + totalRewards: number; + minerCount: number; +} + +interface HardwareScore { + clockDrift: number; + cacheTiming: number; + simdIdentity: number; + vmDetection: boolean; + fingerprintScore: number; +} + +/** + * Calculate hardware bonus multiplier based on fingerprint score + */ +export function calculateHardwareBonus(score: number): number { + // Score ranges from 0-100, bonus from 1.0 to 2.5 + return 1.0 + (score / 100) * (HARDWARE_BONUS_MULTIPLIER - 1.0); +} + +/** + * Calculate epoch reward for a miner + */ +export function calculateEpochReward( + blocksMined: number, + hardwareScore: number, + difficulty: number = 1.0 +): number { + const hardwareBonus = calculateHardwareBonus(hardwareScore); + const baseReward = blocksMined * BASE_REWARD; + const difficultyFactor = 1 / difficulty; + + return baseReward * hardwareBonus * difficultyFactor; +} + +/** + * Get current epoch info from API + */ +export async function getCurrentEpoch(): Promise { + try { + const response = await axios.get(`${API_BASE}/epoch`); + return response.data; + } catch (error) { + console.error('Failed to fetch epoch info:', error); + return null; + } +} + +/** + * Estimate time to reach target reward + */ +export function estimateTimeToReward( + hashrate: number, // blocks per hour + hardwareScore: number, + targetReward: number, + difficulty: number = 1.0 +): number { + let accumulatedReward = 0; + let hours = 0; + + while (accumulatedReward < targetReward) { + accumulatedReward += calculateEpochReward(hashrate, hardwareScore, difficulty) / 3600; // per second + hours++; + if (hours > 1000000) break; // Safety limit + } + + return hours; +} + +/** + * Format time duration + */ +export function formatDuration(hours: number): string { + if (hours < 1) { + return `${Math.round(hours * 60)} minutes`; + } else if (hours < 24) { + return `${hours.toFixed(1)} hours`; + } else { + const days = hours / 24; + return `${days.toFixed(1)} days`; + } +} + +// CLI Interface +import { Command } from 'commander'; +import chalk from 'chalk'; + +const program = new Command(); + +program + .name('rustchain-epoch') + .description('RustChain Epoch Reward Calculator') + .version('1.0.0'); + +program + .command('calculate') + .description('Calculate epoch reward') + .requiredOption('-b, --blocks ', 'Number of blocks mined') + .requiredOption('-s, --score ', 'Hardware fingerprint score (0-100)') + .option('-d, --difficulty ', 'Network difficulty', '1.0') + .action((options) => { + const blocks = parseInt(options.blocks); + const score = parseInt(options.score); + const difficulty = parseFloat(options.difficulty); + + const reward = calculateEpochReward(blocks, score, difficulty); + const bonus = calculateHardwareBonus(score); + + console.log(chalk.blue('\nšŸ“Š Epoch Reward Calculation\n')); + console.log(chalk.cyan('Blocks Mined:'), blocks); + console.log(chalk.cyan('Hardware Score:'), score); + console.log(chalk.cyan('Difficulty:'), difficulty); + console.log(chalk.cyan('Hardware Bonus:'), `${bonus.toFixed(2)}x`); + console.log(chalk.green('\nšŸ’° Estimated Reward:'), `${reward.toFixed(4)} RTC`); + console.log(''); + }); + +program + .command('info') + .description('Get current epoch info') + .action(async () => { + console.log(chalk.blue('\nšŸ“” Fetching epoch info...\n')); + const epoch = await getCurrentEpoch(); + + if (epoch) { + console.log(chalk.cyan('Epoch:'), epoch.epoch); + console.log(chalk.cyan('Start Block:'), epoch.startBlock); + console.log(chalk.cyan('End Block:'), epoch.endBlock); + console.log(chalk.cyan('Difficulty:'), epoch.difficulty); + console.log(chalk.cyan('Total Rewards:'), epoch.totalRewards); + console.log(chalk.cyan('Miners:'), epoch.minerCount); + } else { + console.log(chalk.red('Failed to fetch epoch info')); + } + console.log(''); + }); + +program + .command('estimate') + .description('Estimate time to reach target reward') + .requiredOption('-r, --reward ', 'Target reward (RTC)') + .requiredOption('-h, --hashrate ', 'Hashrate (blocks per hour)') + .requiredOption('-s, --score ', 'Hardware fingerprint score (0-100)') + .option('-d, --difficulty ', 'Network difficulty', '1.0') + .action((options) => { + const reward = parseFloat(options.reward); + const hashrate = parseFloat(options.hashrate); + const score = parseInt(options.score); + const difficulty = parseFloat(options.difficulty); + + const hours = estimateTimeToReward(hashrate, score, reward, difficulty); + + console.log(chalk.blue('\nā±ļø Time Estimation\n')); + console.log(chalk.cyan('Target Reward:'), `${reward} RTC`); + console.log(chalk.cyan('Hashrate:'), `${hashrate} blocks/hour`); + console.log(chalk.cyan('Hardware Score:'), score); + console.log(chalk.cyan('Difficulty:'), difficulty); + console.log(chalk.green('\nā° Estimated Time:'), formatDuration(hours)); + console.log(''); + }); + +program.parse(); diff --git a/sdk/utils/src/index.ts b/sdk/utils/src/index.ts new file mode 100644 index 000000000..be3f9454d --- /dev/null +++ b/sdk/utils/src/index.ts @@ -0,0 +1,12 @@ +/** + * RustChain Utility Tools + * + * A collection of utilities for RustChain: + * - Epoch Reward Calculator + * - RTC Address Generator & Validator + * - Configuration Parser & Validator + */ + +export * from './epoch'; +export * from './address'; +export * from './config'; diff --git a/sdk/utils/tsconfig.json b/sdk/utils/tsconfig.json new file mode 100644 index 000000000..c952669b6 --- /dev/null +++ b/sdk/utils/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/tests/test_ledger_invariants.py b/tests/test_ledger_invariants.py new file mode 100644 index 000000000..d67c9dd3e --- /dev/null +++ b/tests/test_ledger_invariants.py @@ -0,0 +1,439 @@ +#!/usr/bin/env python3 +""" +RustChain Ledger Invariant Test Suite + +Property-based testing using Hypothesis to verify ledger correctness. + +This module tests mathematical invariants that must hold for the RustChain ledger: +1. Conservation of RTC - total in = total out + fees +2. Non-negative balances - no wallet ever goes below 0 +3. Epoch reward invariant - rewards per epoch sum to exactly 1.5 RTC +4. Transfer atomicity - failed transfers don't change any balances +5. Antiquity weighting - higher multiplier miners get proportionally more rewards +6. Pending transfer lifecycle - pending transfers either confirm (24h) or void + +Usage: + python -m pytest tests/test_ledger_invariants.py -v + python tests/test_ledger_invariants.py --live # Test against live node +""" + +import os +import sys +import json +import time +import sqlite3 +import argparse +from typing import Dict, List, Any, Optional, Tuple +from dataclasses import dataclass +from datetime import datetime, timedelta +from unittest.mock import patch, MagicMock + +import pytest +from hypothesis import given, settings, Verbosity, assume, example +from hypothesis import strategies as st + +# Add parent directory to path for imports +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# Try to import node modules +try: + from node.rustchain_v2_integrated_v2_2_1_rip200 import DB_PATH, get_db + HAS_NODE = True +except ImportError: + HAS_NODE = False + DB_PATH = None + +# Configuration +DEFAULT_NODE_URL = os.environ.get("RC_NODE_URL", "https://50.28.86.131") +TEST_WALLET_PREFIX = "test_invariant_" +PER_EPOCH_RTC = 1.5 +UNIT = 1_000_000 # 1 RTC = 1,000,000 urtc + + +@dataclass +class Wallet: + """Represents a wallet in the ledger""" + miner_pk: str + balance_rtc: float = 0.0 + nonce: int = 0 + + +@dataclass +class Transfer: + """Represents a transfer transaction""" + from_pk: str + to_pk: str + amount_rtc: float + nonce: int + timestamp: int + status: str = "confirmed" # confirmed, pending, failed + + +@dataclass +class EpochRewards: + """Represents epoch reward distribution""" + epoch: int + rewards: Dict[str, float] # miner_pk -> reward amount + total: float + + +class LedgerInvariantTester: + """Tests ledger invariants using property-based testing""" + + def __init__(self, node_url: str = DEFAULT_NODE_URL, db_path: Optional[str] = None): + self.node_url = node_url.rstrip("/") + self.db_path = db_path or DB_PATH + self.test_data: List[Wallet] = [] + self.transfers: List[Transfer] = [] + self.epochs: List[EpochRewards] = [] + + def fetch_balances(self) -> Dict[str, float]: + """Fetch all wallet balances from node""" + try: + import urllib.request + url = f"{self.node_url}/api/balances" + req = urllib.request.Request(url, headers={'Accept': 'application/json'}) + with urllib.request.urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + if isinstance(data, dict) and 'balances' in data: + return data['balances'] + return data + except Exception as e: + print(f"Warning: Could not fetch balances: {e}") + return {} + + def fetch_epoch_info(self) -> Dict[str, Any]: + """Fetch current epoch info""" + try: + import urllib.request + url = f"{self.node_url}/epoch" + req = urllib.request.Request(url, headers={'Accept': 'application/json'}) + with urllib.request.urlopen(req, timeout=10) as response: + return json.loads(response.read().decode('utf-8')) + except Exception as e: + print(f"Warning: Could not fetch epoch info: {e}") + return {} + + def fetch_epoch_rewards(self, epoch: int) -> Optional[EpochRewards]: + """Fetch rewards for a specific epoch""" + try: + import urllib.request + url = f"{self.node_url}/rewards/epoch/{epoch}" + req = urllib.request.Request(url, headers={'Accept': 'application/json'}) + with urllib.request.urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + rewards = {} + total = 0.0 + if 'rewards' in data: + for r in data['rewards']: + miner = r.get('miner_id', '') + amount = r.get('share_rtc', 0) + rewards[miner] = amount + total += amount + return EpochRewards(epoch=epoch, rewards=rewards, total=total) + except Exception as e: + print(f"Warning: Could not fetch epoch rewards: {e}") + return None + + def fetch_pending_transfers(self) -> List[Dict]: + """Fetch pending transfers""" + try: + import urllib.request + url = f"{self.node_url}/wallet/ledger?status=pending" + req = urllib.request.Request(url, headers={'Accept': 'application/json'}) + with urllib.request.urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + return data.get('transfers', []) + except Exception as e: + print(f"Warning: Could not fetch pending transfers: {e}") + return [] + + # ==================== INVARIANT TESTS ==================== + + def test_conservation_of_rtc(self, balances_before: Dict[str, float], + balances_after: Dict[str, float], + fees: float = 0.0) -> Tuple[bool, str]: + """ + Invariant 1: Conservation of RTC + Total RTC in = Total RTC out + fees + """ + total_before = sum(balances_before.values()) + total_after = sum(balances_after.values()) + + # Allow small floating point tolerance + diff = abs(total_before - total_after - fees) + + if diff > 0.0001: # 0.0001 RTC tolerance + return False, f"Conservation violated: before={total_before}, after={total_after}, fees={fees}, diff={diff}" + + return True, "Conservation invariant holds" + + def test_non_negative_balances(self, balances: Dict[str, float]) -> Tuple[bool, str]: + """ + Invariant 2: Non-negative balances + No wallet ever goes below 0 + """ + negative_wallets = [] + for wallet, balance in balances.items(): + if balance < 0: + negative_wallets.append((wallet, balance)) + + if negative_wallets: + return False, f"Negative balances found: {negative_wallets}" + + return True, "All balances are non-negative" + + def test_epoch_reward_invariant(self, epoch_rewards: EpochRewards) -> Tuple[bool, str]: + """ + Invariant 3: Epoch reward invariant + Rewards per epoch sum to exactly 1.5 RTC + """ + total = epoch_rewards.total + expected = PER_EPOCH_RTC + + # Allow small floating point tolerance + diff = abs(total - expected) + + if diff > 0.0001: + return False, f"Epoch {epoch_rewards.epoch}: Expected {expected} RTC, got {total} RTC, diff={diff}" + + return True, f"Epoch {epoch_rewards.epoch}: Reward invariant holds" + + def test_transfer_atomicity(self, + sender_before: float, + sender_after: float, + receiver_before: float, + receiver_after: float, + transfer_succeeded: bool) -> Tuple[bool, str]: + """ + Invariant 4: Transfer atomicity + If transfer fails, sender and receiver balances unchanged + """ + if not transfer_succeeded: + # Both should be unchanged + if sender_before != sender_after: + return False, f"Failed transfer changed sender balance: {sender_before} -> {sender_after}" + if receiver_before != receiver_after: + return False, f"Failed transfer changed receiver balance: {receiver_before} -> {receiver_after}" + + return True, "Transfer atomicity holds" + + def test_antiquity_weighting(self, miners: List[Dict], rewards: Dict[str, float]) -> Tuple[bool, str]: + """ + Invariant 5: Antiquity weighting + Higher multiplier miners get proportionally more rewards + """ + miner_multipliers = {} + for miner in miners: + name = miner.get('miner', miner.get('miner_id', '')) + mult = miner.get('antiquity_multiplier', 1.0) + miner_multipliers[name] = mult + + # Compare pairs of miners + miner_rewards = {k: v for k, v in rewards.items() if k in miner_multipliers} + + for miner_a, reward_a in miner_rewards.items(): + for miner_b, reward_b in miner_rewards.items(): + if miner_a == miner_b: + continue + mult_a = miner_multipliers.get(miner_a, 1.0) + mult_b = miner_multipliers.get(miner_b, 1.0) + + # If multiplier_a > multiplier_b, reward_a should be >= reward_b + if mult_a > mult_b and reward_a < reward_b: + return False, f"Antiquity violation: {miner_a}(mult={mult_a}, reward={reward_a}) < {miner_b}(mult={mult_b}, reward={reward_b})" + + return True, "Antiquity weighting invariant holds" + + def test_pending_transfer_lifecycle(self, pending_transfers: List[Dict]) -> Tuple[bool, str]: + """ + Invariant 6: Pending transfer lifecycle + Pending transfers either confirm (24h) or get voided + """ + current_time = int(time.time()) + invalid_transfers = [] + + for transfer in pending_transfers: + create_time = transfer.get('create_time', 0) + status = transfer.get('status', '') + confirm_time = transfer.get('confirm_time', 0) + + # If more than 24 hours have passed + if current_time - create_time > 86400: # 24 hours + if status not in ['confirmed', 'voided', 'failed']: + invalid_transfers.append(transfer) + elif status == 'confirmed' and confirm_time - create_time != 86400: + # Confirmation should be exactly 24h + pass # Allow some tolerance + + if invalid_transfers: + return False, f"Invalid pending transfers: {len(invalid_transfers)} transfers not resolved after 24h" + + return True, "Pending transfer lifecycle invariant holds" + + +# ==================== HYPOTHESIS PROPERTY TESTS ==================== + +class TestLedgerInvariants: + """Property-based tests for ledger invariants""" + + @pytest.fixture + def tester(self): + return LedgerInvariantTester() + + def test_conservation_with_mock_data(self, tester): + """Test conservation invariant with generated data""" + # Generate random balance changes + initial_balances = {f"wallet_{i}": 100.0 for i in range(10)} + + # Simulate transfers + final_balances = dict(initial_balances) + fees = 0.01 + + # Make some transfers + final_balances["wallet_0"] -= 10.0 + final_balances["wallet_1"] += 9.99 + + is_valid, msg = tester.test_conservation_of_rtc(initial_balances, final_balances, fees) + assert is_valid, msg + + def test_non_negative_with_random_balances(self, tester): + """Test non-negative invariant with random data""" + # Generate random balances (including negative - should fail) + balances = { + "wallet_1": 100.5, + "wallet_2": 0.0, + "wallet_3": -0.5, # This should fail + } + + is_valid, msg = tester.test_non_negative_balances(balances) + assert not is_valid, "Should detect negative balance" + + def test_epoch_reward_with_exact_values(self, tester): + """Test epoch reward invariant with exact 1.5 RTC""" + rewards = EpochRewards( + epoch=1, + rewards={"miner_1": 0.5, "miner_2": 0.5, "miner_3": 0.5}, + total=1.5 + ) + + is_valid, msg = tester.test_epoch_reward_invariant(rewards) + assert is_valid, msg + + def test_transfer_atomicity_successful(self, tester): + """Test atomicity with successful transfer""" + is_valid, msg = tester.test_transfer_atomicity( + sender_before=100.0, + sender_after=90.0, + receiver_before=50.0, + receiver_after=60.0, + transfer_succeeded=True + ) + assert is_valid, msg + + def test_transfer_atomicity_failed(self, tester): + """Test atomicity with failed transfer""" + is_valid, msg = tester.test_transfer_atomicity( + sender_before=100.0, + sender_after=100.0, # Unchanged + receiver_before=50.0, + receiver_after=50.0, # Unchanged + transfer_succeeded=False + ) + assert is_valid, msg + + @given(st.lists(st.floats(min_value=0, max_value=1000), min_size=1, max_size=100)) + @settings(max_examples=50, verbosity=Verbosity.verbose) + def test_non_negative_balances_random(self, tester, balances_list): + """Property test: random balance lists should all be non-negative""" + balances = {f"wallet_{i}": b for i, b in enumerate(balances_list)} + + # Filter to only positive for this test + positive_balances = {k: v for k, v in balances.items() if v >= 0} + + is_valid, msg = tester.test_non_negative_balances(positive_balances) + assert is_valid, msg + + +# ==================== LIVE NODE TESTS ==================== + +def run_live_tests(node_url: str = DEFAULT_NODE_URL): + """Run tests against live node""" + print(f"\n{'='*60}") + print(f"Running live invariant tests against {node_url}") + print(f"{'='*60}\n") + + tester = LedgerInvariantTester(node_url=node_url) + results = [] + + # Test 1: Fetch and check balances + print("[1/6] Testing non-negative balances invariant...") + balances = tester.fetch_balances() + if balances: + is_valid, msg = tester.test_non_negative_balances(balances) + results.append(("Non-negative balances", is_valid, msg)) + print(f" Result: {msg}") + else: + results.append(("Non-negative balances", False, "Could not fetch balances")) + print(" Could not fetch balances") + + # Test 2: Epoch reward invariant + print("[2/6] Testing epoch reward invariant...") + epoch_info = tester.fetch_epoch_info() + current_epoch = epoch_info.get('epoch', 0) + + if current_epoch > 0: + # Test a few recent epochs + for e in range(max(1, current_epoch - 5), current_epoch + 1): + rewards = tester.fetch_epoch_rewards(e) + if rewards: + is_valid, msg = tester.test_epoch_reward_invariant(rewards) + results.append((f"Epoch {e} reward", is_valid, msg)) + print(f" Epoch {e}: {msg}") + else: + results.append(("Epoch rewards", False, "Could not fetch epoch info")) + print(" Could not fetch epoch info") + + # Test 3: Pending transfer lifecycle + print("[3/6] Testing pending transfer lifecycle...") + pending = tester.fetch_pending_transfers() + is_valid, msg = tester.test_pending_transfer_lifecycle(pending) + results.append(("Pending transfers", is_valid, msg)) + print(f" Result: {msg}") + + # Summary + print(f"\n{'='*60}") + print("SUMMARY") + print(f"{'='*60}") + + passed = sum(1 for _, valid, _ in results if valid) + total = len(results) + + for name, valid, msg in results: + status = "āœ“ PASS" if valid else "āœ— FAIL" + print(f" {status}: {name}") + if not valid: + print(f" {msg}") + + print(f"\nTotal: {passed}/{total} tests passed") + + return passed, total + + +# ==================== MAIN ==================== + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Ledger Invariant Test Suite") + parser.add_argument("--live", action="store_true", help="Run against live node") + parser.add_argument("--node-url", default=DEFAULT_NODE_URL, help="Node URL for live tests") + parser.add_argument("--pytest", action="store_true", help="Run as pytest") + + args = parser.parse_args() + + if args.pytest or not args.live: + # Run pytest + sys.exit(pytest.main([__file__, "-v"])) + else: + # Run live tests + passed, total = run_live_tests(args.node_url) + sys.exit(0 if passed == total else 1) diff --git a/tools/github_star_tracker.py b/tools/github_star_tracker.py new file mode 100644 index 000000000..3d496ab3b --- /dev/null +++ b/tools/github_star_tracker.py @@ -0,0 +1,363 @@ +#!/usr/bin/env python3 +""" +GitHub Star Growth Tracker Dashboard + +Tracks Scottcjn repo stars over time with SQLite storage and HTML chart. +""" + +import argparse +import json +import os +import sqlite3 +import sys +import time +import urllib.error +import urllib.request +from dataclasses import dataclass +from datetime import datetime, timedelta +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + + +OWNER = "Scottcjn" +DB_PATH = Path("star_tracker.db") + + +@dataclass +class RepoStars: + name: str + stars: int + url: str + + +def ensure_db(path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with sqlite3.connect(str(path)) as db: + db.execute(""" + CREATE TABLE IF NOT EXISTS snapshots( + ts REAL NOT NULL, + repo TEXT NOT NULL, + stars INTEGER NOT NULL, + PRIMARY KEY (ts, repo) + ) + """) + db.execute(""" + CREATE INDEX IF NOT EXISTS idx_repo_ts ON snapshots(repo, ts) + """) + db.commit() + + +def get_all_repos(owner: str) -> List[RepoStars]: + """Fetch all repositories for the owner.""" + repos: List[RepoStars] = [] + page = 1 + + while True: + url = f"https://api.github.com/users/{owner}/repos?per_page=100&page={page}" + try: + req = urllib.request.Request( + url, + headers={ + "User-Agent": "rustchain-star-tracker/1.0", + "Accept": "application/vnd.github.v3+json" + } + ) + with urllib.request.urlopen(req, timeout=30) as resp: + data = json.loads(resp.read().decode("utf-8")) + if not data: + break + for repo in data: + repos.append(RepoStars( + name=repo["name"], + stars=repo.get("stargazers_count", 0), + url=repo["html_url"] + )) + if len(data) < 100: + break + page += 1 + except Exception as e: + print(f"Error fetching repos: {e}", file=sys.stderr) + break + + return repos + + +def save_snapshot(path: Path, repos: List[RepoStars]) -> None: + """Save current star counts to database.""" + now = time.time() + with sqlite3.connect(str(path)) as db: + for repo in repos: + try: + db.execute( + "INSERT OR REPLACE INTO snapshots(ts, repo, stars) VALUES (?, ?, ?)", + (now, repo.name, repo.stars) + ) + except sqlite3.IntegrityError: + db.execute( + "UPDATE snapshots SET stars = ? WHERE ts = ? AND repo = ?", + (repo.stars, now, repo.name) + ) + db.commit() + + +def get_history(path: Path, days: int = 30) -> Dict[str, List[Tuple[float, int]]]: + """Get star history for all repos.""" + cutoff = time.time() - (days * 86400) + with sqlite3.connect(str(path)) as db: + rows = db.execute(""" + SELECT repo, ts, stars FROM snapshots + WHERE ts >= ? + ORDER BY ts ASC + """, (cutoff,)).fetchall() + + history: Dict[str, List[Tuple[float, int]]] = {} + for repo, ts, stars in rows: + if repo not in history: + history[repo] = [] + history[repo].append((ts, stars)) + + return history + + +def get_total_stars(repos: List[RepoStars]) -> int: + return sum(r.stars for r in repos) + + +def calculate_daily_deltas(path: Path) -> List[Dict[str, Any]]: + """Calculate daily star changes.""" + with sqlite3.connect(str(path)) as db: + # Get latest snapshot for each repo + latest = db.execute(""" + SELECT s1.repo, s1.stars, s1.ts + FROM snapshots s1 + INNER JOIN ( + SELECT repo, MAX(ts) as max_ts + FROM snapshots + GROUP BY repo + ) s2 ON s1.repo = s2.repo AND s1.ts = s2.max_ts + """).fetchall() + + # Get previous day's snapshot + prev_cutoff = time.time() - 2 * 86400 + prev = db.execute(""" + SELECT s1.repo, s1.stars + FROM snapshots s1 + INNER JOIN ( + SELECT repo, MAX(ts) as max_ts + FROM snapshots + WHERE ts <= ? + GROUP BY repo + ) s2 ON s1.repo = s2.repo AND s1.ts = s2.max_ts + """, (prev_cutoff,)).fetchall() + + prev_dict = {r[0]: r[1] for r in prev} + + deltas = [] + for repo, stars, _ in latest: + prev_stars = prev_dict.get(repo, stars) + delta = stars - prev_stars + deltas.append({ + "repo": repo, + "stars": stars, + "daily_delta": delta + }) + + deltas.sort(key=lambda x: x["daily_delta"], reverse=True) + return deltas + + +def generate_html(path: Path, repos: List[RepoStars], days: int = 30) -> str: + """Generate HTML dashboard.""" + history = get_history(path, days) + total_stars = get_total_stars(repos) + deltas = calculate_daily_deltas(path) + + # Prepare chart data + chart_data = [] + for repo_name in list(history.keys())[:20]: # Top 20 repos + data_points = history[repo_name] + if len(data_points) >= 2: + first_stars = data_points[0][1] + last_stars = data_points[-1][1] + growth = last_stars - first_stars + chart_data.append({ + "name": repo_name, + "growth": growth, + "current": last_stars + }) + + chart_data.sort(key=lambda x: x["growth"], reverse=True) + top_growers = chart_data[:10] + + html = f""" + + + + + GitHub Star Growth Tracker - {OWNER} + + + + +
+

⭐ GitHub Star Growth Tracker - {OWNER}

+

Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}

+ +
+
+
{total_stars:,}
+
Total Stars
+
+
+
{len(repos)}
+
Repositories
+
+
+
{sum(1 for d in deltas if d['daily_delta'] > 0)}
+
Growing Today
+
+
+ +
+

Top Growers (Last {days} days)

+ +
+ +

All Repositories

+ + + + + + + + + +""" + + for d in deltas: + delta_class = "positive" if d["daily_delta"] > 0 else "negative" if d["daily_delta"] < 0 else "" + delta_sign = "+" if d["daily_delta"] > 0 else "" + html += f""" + + + + + +""" + + html += """ + +
RepositoryStarsDaily Ī”
{d['repo']}{d['stars']:,}{delta_sign}{d['daily_delta']}
+
+ + + +""" + return html + + +def print_terminal(repos: List[RepoStars], deltas: List[Dict[str, Any]]) -> None: + """Print terminal dashboard.""" + total_stars = get_total_stars(repos) + + print(f"GitHub Star Growth Tracker - {OWNER}") + print("=" * 60) + print(f"Total Stars: {total_stars:,}") + print(f"Repositories: {len(repos)}") + print() + print("Top Growers Today:") + print("-" * 40) + + for d in deltas[:10]: + delta_sign = "+" if d["daily_delta"] > 0 else "" + print(f" {d['repo']:<30} {d['stars']:>5} {delta_sign}{d['daily_delta']}") + + print() + print("All Repositories:") + print("-" * 40) + for d in deltas: + delta_sign = "+" if d["daily_delta"] > 0 else "" + print(f" {d['repo']:<30} {d['stars']:>5} {delta_sign}{d['daily_delta']}") + + +def main(): + parser = argparse.ArgumentParser(description="GitHub Star Growth Tracker") + parser.add_argument("--fetch", action="store_true", help="Fetch latest star data") + parser.add_argument("--html", action="store_true", help="Generate HTML dashboard") + parser.add_argument("--days", type=int, default=30, help="Days of history to show") + parser.add_argument("--db", type=str, default=str(DB_PATH), help="Database path") + args = parser.parse_args() + + db_path = Path(args.db) + ensure_db(db_path) + + if args.fetch: + print("Fetching repositories...") + repos = get_all_repos(OWNER) + print(f"Found {len(repos)} repositories with {get_total_stars(repos):,} total stars") + + print("Saving snapshot...") + save_snapshot(db_path, repos) + print("Done!") + + if args.html: + repos = get_all_repos(OWNER) + save_snapshot(db_path, repos) + + html = generate_html(db_path, repos, args.days) + output_path = Path("star_tracker.html") + output_path.write_text(html) + print(f"HTML dashboard saved to {output_path}") + + # Default: show terminal output + repos = get_all_repos(OWNER) + save_snapshot(db_path, repos) + deltas = calculate_daily_deltas(db_path) + print_terminal(repos, deltas) + + +if __name__ == "__main__": + main() diff --git a/tools/health_check_cli/README.md b/tools/health_check_cli/README.md new file mode 100644 index 000000000..763fce6c7 --- /dev/null +++ b/tools/health_check_cli/README.md @@ -0,0 +1,56 @@ +# RustChain Health Check CLI + +A CLI tool that queries all 3 RustChain attestation nodes and displays their health status in a formatted table. + +## Bounty + +This tool was created for [Bounty #1111](https://github.com/Scottcjn/rustchain-bounties/issues/1111) - 8 RTC Reward. + +## Features + +- Queries all 3 attestation nodes: + - 50.28.86.131:443 + - 50.28.86.153:443 + - 76.8.228.245:8099 +- Displays: version, uptime, db_rw status, tip age +- Formatted table output +- JSON output option +- Exit code reflects node availability + +## Usage + +```bash +# Default table output +python3 tools/health_check_cli/rustchain_health_check.py + +# JSON output +python3 tools/health_check_cli/rustchain_health_check.py --json + +# Verbose mode (show errors) +python3 tools/health_check_cli/rustchain_health_check.py -v +``` + +## Example Output + +``` +RustChain Node Health Check +Timestamp: 2026-03-07T23:00:00Z + +Host Port Status Version Uptime DB RW Tip Age +----------------------------------------------------------------------------------------- +50.28.86.131 443 ONLINE 1.2.3 5d 12h 30m true 30s +50.28.86.153 443 ONLINE 1.2.3 3d 8h 15m true 45s +76.8.228.245 8099 ONLINE 1.2.3 2d 4h 50m true 1m + +Summary: 3/3 nodes online +``` + +## Requirements + +- Python 3.7+ +- Stdlib only (no external dependencies) + +## Exit Codes + +- `0` - All nodes online +- `1` - One or more nodes offline diff --git a/tools/health_check_cli/rustchain_health_check.py b/tools/health_check_cli/rustchain_health_check.py new file mode 100644 index 000000000..45ac85303 --- /dev/null +++ b/tools/health_check_cli/rustchain_health_check.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +""" +RustChain Health Check CLI + +Queries all 3 attestation nodes and displays health status. +Bounty: #1111 - 8 RTC Reward +""" + +import argparse +import json +import ssl +import sys +import time +import urllib.error +import urllib.request +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, Tuple + + +ATTESTATION_NODES = [ + ("50.28.86.131", 443, True), + ("50.28.86.153", 443, True), + ("76.8.228.245", 8099, False), +] + + +def utc_iso(ts: Optional[float] = None) -> str: + ts = time.time() if ts is None else ts + return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(ts)) + + +def _ssl_context(insecure: bool) -> Optional[ssl.SSLContext]: + if not insecure: + return None + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + return ctx + + +def http_json_get(url: str, timeout_s: int, insecure: bool) -> Tuple[bool, Any, str]: + try: + req = urllib.request.Request(url, headers={"User-Agent": "rustchain-health-check/1.0"}) + with urllib.request.urlopen(req, timeout=timeout_s, context=_ssl_context(insecure)) as resp: + body = resp.read(1024 * 1024).decode("utf-8", errors="replace") + try: + return True, json.loads(body), "" + except Exception: + return False, None, "invalid_json" + except urllib.error.HTTPError as e: + return False, None, f"http_{e.code}" + except Exception as e: + return False, None, "unreachable" + + +@dataclass +class NodeHealth: + host: str + port: int + online: bool + version: str = "N/A" + uptime: str = "N/A" + db_rw: str = "N/A" + tip_age: str = "N/A" + error: str = "" + + +def check_node(host: str, port: int, use_https: bool) -> NodeHealth: + protocol = "https" if use_https else "http" + url = f"{protocol}://{host}:{port}/health" + + health = NodeHealth(host=host, port=port, online=False) + + ok, data, err = http_json_get(url, timeout_s=10, insecure=True) + + if not ok: + health.error = err + return health + + health.online = True + + # Parse version + if "version" in data: + health.version = str(data["version"]) + elif "data" in data and "version" in data.get("data", {}): + health.version = str(data["data"]["version"]) + + # Parse uptime + if "uptime" in data: + health.uptime = str(data["uptime"]) + elif "data" in data and "uptime" in data.get("data", {}): + health.uptime = str(data["data"]["uptime"]) + + # Parse db_rw status + if "db_rw" in data: + health.db_rw = str(data["db_rw"]) + elif "data" in data and "db_rw" in data.get("data", {}): + health.db_rw = str(data["data"]["db_rw"]) + + # Parse tip age + if "tip_age" in data: + health.tip_age = str(data["tip_age"]) + elif "data" in data and "tip_age" in data.get("data", {}): + health.tip_age = str(data["data"]["tip_age"]) + elif "data" in data and "tip" in data.get("data", {}): + tip = data["data"]["tip"] + if isinstance(tip, dict) and "age" in tip: + health.tip_age = str(tip["age"]) + + return health + + +def format_table(healths: List[NodeHealth]) -> str: + # Header + header = f"{'Host':<20} {'Port':<6} {'Status':<8} {'Version':<12} {'Uptime':<15} {'DB RW':<8} {'Tip Age':<15}" + separator = "-" * len(header) + + lines = [header, separator] + + for h in healths: + status = "ONLINE" if h.online else "OFFLINE" + lines.append( + f"{h.host:<20} {h.port:<6} {status:<8} {h.version:<12} {h.uptime:<15} {h.db_rw:<8} {h.tip_age:<15}" + ) + + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser(description="RustChain Health Check CLI") + parser.add_argument("--json", action="store_true", help="Output in JSON format") + parser.add_argument("-v", "--verbose", action="store_true", help="Show detailed errors") + args = parser.parse_args() + + results: List[NodeHealth] = [] + + for host, port, use_https in ATTESTATION_NODES: + health = check_node(host, port, use_https) + results.append(health) + + if args.json: + output = { + "timestamp": utc_iso(), + "nodes": [] + } + for h in results: + node_data = { + "host": h.host, + "port": h.port, + "online": h.online, + "version": h.version, + "uptime": h.uptime, + "db_rw": h.db_rw, + "tip_age": h.tip_age, + } + if h.error: + node_data["error"] = h.error + output["nodes"].append(node_data) + print(json.dumps(output, indent=2)) + else: + print("RustChain Node Health Check") + print(f"Timestamp: {utc_iso()}") + print() + print(format_table(results)) + + if args.verbose: + for h in results: + if h.error: + print(f"\nError for {h.host}:{h.port}: {h.error}") + + # Summary + online_count = sum(1 for h in results if h.online) + print(f"\nSummary: {online_count}/{len(results)} nodes online") + + # Exit code: 0 if all online, 1 if any offline + all_online = all(h.online for h in results) + sys.exit(0 if all_online else 1) + + +if __name__ == "__main__": + main() diff --git a/tools/rustchain-address-validator/.github/workflows/ci.yml b/tools/rustchain-address-validator/.github/workflows/ci.yml new file mode 100644 index 000000000..2d93e015b --- /dev/null +++ b/tools/rustchain-address-validator/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + name: Test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + + - name: Build + run: cargo build --release + + - name: Run tests + run: cargo test + + - name: Clippy + run: cargo clippy -- -D warnings + + - name: Format check + run: cargo fmt --check diff --git a/tools/rustchain-address-validator/.gitignore b/tools/rustchain-address-validator/.gitignore new file mode 100644 index 000000000..6f43f02b3 --- /dev/null +++ b/tools/rustchain-address-validator/.gitignore @@ -0,0 +1,13 @@ +# Build artifacts +target/ +Cargo.lock + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/tools/rustchain-address-validator/Cargo.toml b/tools/rustchain-address-validator/Cargo.toml new file mode 100644 index 000000000..8a8b58122 --- /dev/null +++ b/tools/rustchain-address-validator/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "rustchain-address-validator" +version = "0.1.0" +edition = "2021" +description = "RTC Address generator and validator for RustChain" +license = "MIT" +authors = ["sososonia-cyber"] +repository = "https://github.com/sososonia-cyber/Rustchain" + +[dependencies] +bs58 = "0.5" +sha2 = "0.10" +ed25519-dalek = { version = "2.1", features = ["rand_core"] } +rand = "0.8" +hex = "0.4" +clap = { version = "4.5", features = ["derive"] } + +[dev-dependencies] + +[[bin]] +name = "rtc-address" +path = "src/main.rs" + +[lib] +name = "rustchain_address" +path = "src/lib.rs" diff --git a/tools/rustchain-address-validator/README.md b/tools/rustchain-address-validator/README.md new file mode 100644 index 000000000..bbc5ecb04 --- /dev/null +++ b/tools/rustchain-address-validator/README.md @@ -0,0 +1,84 @@ +# RustChain Address Validator & Generator + +A Rust CLI tool for generating and validating RTC addresses on the RustChain network. + +## Features + +- Generate new RTC addresses with private keys +- Validate existing RTC addresses +- Derive address from private key + +## Installation + +```bash +cargo install --path . +``` + +Or build and run directly: + +```bash +cargo build --release +./target/release/rtc-address --help +``` + +## Usage + +### Generate a new address + +```bash +rtc-address generate +``` + +Output: +``` +=== Generated RTC Address === + +Address: RTCxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +Private Key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +IMPORTANT: Save your private securely! + Anyone with your private key can access your funds. +``` + +### Validate an address + +```bash +rtc-address validate RTCxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` + +### Derive address from private key + +```bash +rtc-address from-key +``` + +## Development + +### Build + +```bash +cargo build +``` + +### Test + +```bash +cargo test +``` + +### Run + +```bash +cargo run -- generate +``` + +## Bounty + +This tool is submitted for [Bounty #674: Build RustChain Tools & Features in Rust](https://github.com/Scottcjn/rustchain-bounties/issues/674) + +- **Tier**: 1 (Utilities) +- **Target**: RTC address generator + validator + +## License + +MIT diff --git a/tools/rustchain-address-validator/src/lib.rs b/tools/rustchain-address-validator/src/lib.rs new file mode 100644 index 000000000..975336c70 --- /dev/null +++ b/tools/rustchain-address-validator/src/lib.rs @@ -0,0 +1,140 @@ +//! RustChain Address Validator and Generator Library +//! +//! This library provides utilities for validating and generating RTC addresses +//! on the RustChain network. + +use ed25519_dalek::{SigningKey, VerifyingKey}; +use rand::rngs::OsRng; +use sha2::{Digest, Sha256}; + +/// Prefix used for RustChain addresses +pub const ADDRESS_PREFIX: &str = "RTC"; + +/// Length of the address hash (without prefix) +pub const ADDRESS_HASH_LEN: usize = 32; + +/// Validates an RTC address +/// +/// # Arguments +/// * `address` - The address string to validate +/// +/// # Returns +/// * `true` if the address is valid, `false` otherwise +pub fn validate_address(address: &str) -> bool { + if !address.starts_with(ADDRESS_PREFIX) { + return false; + } + + let without_prefix = &address[3..]; + + // Base58 decoded length should be: 32 (pubkey) + 4 (checksum) = 36 + let decoded = match bs58::decode(without_prefix).into_vec() { + Ok(v) => v, + Err(_) => return false, + }; + + if decoded.len() != 36 { + return false; + } + + // Split into pubkey and checksum + let (pubkey, checksum) = decoded.split_at(32); + + // Calculate checksum on version byte + pubkey + let mut payload = vec![0x00]; + payload.extend_from_slice(pubkey); + let calculated_checksum = calculate_checksum(&payload); + + checksum == calculated_checksum.as_slice() +} + +/// Generates a new random RTC address +/// +/// # Returns +/// A tuple of (address, private_key_hex) +pub fn generate_address() -> (String, String) { + let signing_key = SigningKey::generate(&mut OsRng); + let verifying_key: VerifyingKey = (&signing_key).into(); + + // Create payload with version byte + pubkey + let mut payload = vec![0x00]; // Version byte + payload.extend_from_slice(verifying_key.as_bytes()); + + // Calculate checksum on the full payload (version + pubkey) + let checksum = calculate_checksum(&payload); + + // Address is base58 of pubkey + checksum (no version byte in address) + let address_bytes: Vec = verifying_key.as_bytes().iter().cloned().chain(checksum).collect(); + let address = format!("{}{}", ADDRESS_PREFIX, bs58::encode(&address_bytes).into_string()); + + let private_key = hex::encode(signing_key.to_bytes()); + + (address, private_key) +} + +/// Generates address from a private key (hex) +/// +/// # Arguments +/// * `private_key_hex` - The private key as a hex string +/// +/// # Returns +/// The corresponding RTC address +pub fn address_from_private_key(private_key_hex: &str) -> Result { + let key_bytes = hex::decode(private_key_hex).map_err(|e| format!("Invalid hex: {}", e))?; + + if key_bytes.len() != 32 { + return Err("Private key must be 32 bytes".to_string()); + } + + let key_array: [u8; 32] = key_bytes.try_into().map_err(|_| "Invalid key length")?; + let signing_key = SigningKey::from_bytes(&key_array); + let verifying_key: VerifyingKey = (&signing_key).into(); + + // Create payload with version byte + pubkey + let mut payload = vec![0x00]; // Version byte + payload.extend_from_slice(verifying_key.as_bytes()); + + // Calculate checksum on version + pubkey + let checksum = calculate_checksum(&payload); + + // Address is base58 of pubkey + checksum + let address_bytes: Vec = verifying_key.as_bytes().iter().cloned().chain(checksum).collect(); + let address = format!("{}{}", ADDRESS_PREFIX, bs58::encode(&address_bytes).into_string()); + + Ok(address) +} + +/// Calculates checksum for address payload +fn calculate_checksum(payload: &[u8]) -> Vec { + let hash = Sha256::digest(payload); + let hash2 = Sha256::digest(&hash); + hash2[..4].to_vec() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generate_address() { + let (address, private_key) = generate_address(); + assert!(address.starts_with("RTC")); + assert_eq!(private_key.len(), 64); + } + + #[test] + fn test_validate_address() { + let (address, _) = generate_address(); + assert!(validate_address(&address)); + + assert!(!validate_address("INVALID")); + assert!(!validate_address("RTCx")); + } + + #[test] + fn test_round_trip() { + let (address, private_key) = generate_address(); + let derived = address_from_private_key(&private_key).unwrap(); + assert_eq!(address, derived); + } +} diff --git a/tools/rustchain-address-validator/src/main.rs b/tools/rustchain-address-validator/src/main.rs new file mode 100644 index 000000000..15cf49b6b --- /dev/null +++ b/tools/rustchain-address-validator/src/main.rs @@ -0,0 +1,67 @@ +//! RTC Address Tool - CLI for RustChain address operations +//! +//! A command-line tool for generating and validating RTC addresses. + +use clap::{Parser, Subcommand}; +use rustchain_address::{ + address_from_private_key, generate_address, validate_address, +}; + +#[derive(Parser)] +#[command(name = "rtc-address")] +#[command(about = "RustChain Address Generator and Validator", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Generate a new random RTC address + Generate, + /// Validate an RTC address + Validate { + /// The RTC address to validate + address: String, + }, + /// Derive address from private key + FromKey { + /// Private key in hex format + private_key: String, + }, +} + +fn main() { + let cli = Cli::parse(); + + match cli.command { + Commands::Generate => { + let (address, private_key) = generate_address(); + println!("\n=== Generated RTC Address ===\n"); + println!("Address: {}", address); + println!("Private Key: {}\n", private_key); + println!("IMPORTANT: Save your private key securely!"); + println!(" Anyone with your private key can access your funds.\n"); + } + Commands::Validate { address } => { + if validate_address(&address) { + println!("Valid RTC address: {}", address); + } else { + println!("Invalid RTC address: {}", address); + std::process::exit(1); + } + } + Commands::FromKey { private_key } => { + match address_from_private_key(&private_key) { + Ok(address) => { + println!("\n=== Derived RTC Address ===\n"); + println!("Address: {}\n", address); + } + Err(e) => { + println!("Error: {}", e); + std::process::exit(1); + } + } + } + } +} diff --git a/tools/rustchain-cli/Cargo.lock b/tools/rustchain-cli/Cargo.lock new file mode 100644 index 000000000..de4a9c8a8 --- /dev/null +++ b/tools/rustchain-cli/Cargo.lock @@ -0,0 +1,1636 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustchain-cli" +version = "0.1.0" +dependencies = [ + "clap", + "colored", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/tools/rustchain-cli/Cargo.toml b/tools/rustchain-cli/Cargo.toml new file mode 100644 index 000000000..cbe114d49 --- /dev/null +++ b/tools/rustchain-cli/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "rustchain-cli" +version = "0.1.0" +edition = "2021" +description = "RustChain CLI - Command line wallet and toolkit" +authors = ["sososonia-cyber"] +license = "MIT" +repository = "https://github.com/sososonia-cyber/Rustchain" + +[dependencies] +reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } +tokio = { version = "1", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +clap = { version = "4.5", features = ["derive"] } +colored = "2.1" +rand = "0.8" + +[dev-dependencies] + +[[bin]] +name = "rustchain" +path = "src/main.rs" + +[profile.release] +opt-level = 3 +lto = true diff --git a/tools/rustchain-cli/README.md b/tools/rustchain-cli/README.md new file mode 100644 index 000000000..c31762b7b --- /dev/null +++ b/tools/rustchain-cli/README.md @@ -0,0 +1,101 @@ +# RustChain CLI + +A Rust-based command-line interface for RustChain blockchain operations. + +## Features + +- Check node health status +- View current epoch information +- List active miners with statistics +- Check wallet balances +- View node statistics +- Generate RTC wallet addresses +- Validate wallet address format +- Verify wallet address on network + +## Installation + +```bash +# Clone the repository +git clone https://github.com/sososonia-cyber/Rustchain.git +cd Rustchain/rustchain-cli + +# Build +cargo build --release + +# Run +./target/release/rustchain +``` + +## Usage + +```bash +# Check node health +rustchain health + +# Get epoch information +rustchain epoch + +# List active miners (top 10) +rustchain miners + +# List specific number of miners +rustchain miners --limit 20 + +# Check wallet balance +rustchain balance my-wallet + +# Get node statistics +rustchain stats + +# Generate a new RTC wallet address +rustchain address generate + +# Generate with custom prefix and length +rustchain address generate --prefix WALLET --length 24 + +# Validate wallet address format +rustchain address validate RTC-mywallet123 + +# Verify address exists on the network +rustchain address verify my-wallet +``` + +## Address Commands + +The `address` subcommand provides wallet address utilities: + +- **generate**: Generate a new RTC wallet address with random identifier +- **validate**: Validate the format of an RTC wallet address +- **verify**: Check if an address exists on the RustChain network + +### Address Format + +RTC wallet addresses follow the format: `PREFIX-identifier` + +- Standard prefixes: RTC, WALLET, NODE, MINER +- Identifier: 3-64 alphanumeric characters, can include - and _ + +Examples: +- `RTC-abc123def456` +- `WALLET-my-wallet` +- `MINER-node-001` + +## Bounty + +This tool was built for the [RustChain Bounty Program](https://github.com/Scottcjn/rustchain-bounties/issues/674): +- **Bounty ID**: #674 +- **Tier**: 1 (Utilities) +- **Features**: CLI wallet, address generator, address validator +- **Reward**: 25-50 RTC + +## API Reference + +- Health: `GET https://rustchain.org/health` +- Miners: `GET https://rustchain.org/api/miners` +- Epoch: `GET https://rustchain.org/epoch` +- Balance: `GET https://rustchain.org/wallet/balance?miner_id={wallet}` + +## License + +MIT diff --git a/tools/rustchain-cli/src/main.rs b/tools/rustchain-cli/src/main.rs new file mode 100644 index 000000000..ae61149be --- /dev/null +++ b/tools/rustchain-cli/src/main.rs @@ -0,0 +1,401 @@ +//! RustChain CLI - Command line wallet and toolkit +//! +//! A Rust implementation of RustChain utilities for the bounty: +//! [BOUNTY: 25-150 RTC] Build RustChain Tools & Features in Rust + +use clap::{Parser, Subcommand}; +use rand::Rng; +use reqwest::Client; +use serde::Deserialize; +use std::collections::HashMap; + +// API Base URL +const API_BASE: &str = "https://rustchain.org"; + +/// Health check response +#[derive(Debug, Deserialize)] +struct HealthResponse { + ok: bool, + version: String, + #[serde(rename = "uptime_s")] + uptime_s: u64, + #[serde(rename = "db_rw")] + db_rw: bool, +} + +/// Epoch info response +#[derive(Debug, Deserialize)] +struct EpochResponse { + epoch: u64, + slot: u64, + #[serde(rename = "blocks_per_epoch")] + blocks_per_epoch: u64, + #[serde(rename = "enrolled_miners")] + enrolled_miners: u64, + #[serde(rename = "epoch_pot")] + epoch_pot: f64, + #[serde(rename = "total_supply_rtc")] + total_supply_rtc: f64, +} + +/// Miner info +#[derive(Debug, Deserialize)] +struct Miner { + miner: String, + #[serde(rename = "antiquity_multiplier")] + antiquity_multiplier: f64, + #[serde(rename = "hardware_type")] + hardware_type: String, + #[serde(rename = "entropy_score")] + entropy_score: f64, +} + +/// Wallet balance response +#[derive(Debug, Deserialize)] +struct BalanceResponse { + #[serde(rename = "miner_id")] + miner_id: String, + #[serde(rename = "amount_rtc")] + amount_rtc: Option, + #[serde(rename = "amount_i64")] + amount_i64: Option, + // For API responses that use different field names + #[serde(default)] + balance: f64, + #[serde(default)] + wallet: String, +} + +/// CLI Arguments +#[derive(Parser)] +#[command(name = "rustchain")] +#[command(about = "RustChain CLI - Wallet and node utilities", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Check node health status + Health, + /// Get current epoch information + Epoch, + /// List active miners + Miners { + /// Number of miners to display (default: 10) + #[arg(short, long, default_value = "10")] + limit: usize, + }, + /// Check wallet balance + Balance { + /// Wallet address to check + wallet: String, + }, + /// Get node statistics + Stats, + /// Generate or validate RTC wallet addresses + Address { + #[command(subcommand)] + action: AddressCommands, + }, +} + +#[derive(Subcommand)] +enum AddressCommands { + /// Generate a new RTC wallet address + Generate { + /// Length of the random part (default: 16) + #[arg(short, long, default_value = "16")] + length: usize, + /// Include prefix (default: RTC) + #[arg(short, long, default_value = "RTC")] + prefix: String, + }, + /// Validate an RTC address format + Validate { + /// Wallet address to validate + address: String, + }, + /// Verify an address exists on the network + Verify { + /// Wallet address to verify + address: String, + }, +} + +/// Get HTTP client +fn get_client() -> Client { + Client::builder() + .danger_accept_invalid_certs(true) + .build() + .expect("Failed to create HTTP client") +} + +/// Print health info +async fn cmd_health(client: &Client) -> Result<(), Box> { + let url = format!("{}/health", API_BASE); + let response = client.get(&url).send().await?; + let health: HealthResponse = response.json().await?; + + println!("\n🟢 RustChain Node Health"); + println!("========================="); + println!("Status: {}", if health.ok { "Healthy āœ“" } else { "Unhealthy āœ—" }); + println!("Version: {}", health.version); + println!("Uptime: {} seconds", health.uptime_s); + println!("Database: {}", if health.db_rw { "Read/Write" } else { "Read-only" }); + + Ok(()) +} + +/// Print epoch info +async fn cmd_epoch(client: &Client) -> Result<(), Box> { + let url = format!("{}/epoch", API_BASE); + let response = client.get(&url).send().await?; + let epoch: EpochResponse = response.json().await?; + + println!("\nā±ļø Current Epoch Information"); + println!("============================="); + println!("Epoch: {}", epoch.epoch); + println!("Slot: {}", epoch.slot); + println!("Blocks/Epoch: {}", epoch.blocks_per_epoch); + println!("Enrolled Miners: {}", epoch.enrolled_miners); + println!("Epoch PoT: {}", epoch.epoch_pot); + + Ok(()) +} + +/// Print miners list +async fn cmd_miners(client: &Client, limit: usize) -> Result<(), Box> { + let url = format!("{}/api/miners", API_BASE); + let response = client.get(&url).send().await?; + let miners: Vec = response.json().await?; + + println!("\nā›ļø Active Miners (Top {})", limit); + println!("{}", "-".repeat(40)); + println!("{:<4} {:<30} {:<15} {:<10}", "#", "Miner", "Hardware", "Multiplier"); + println!("{}", "-".repeat(65)); + + for (i, miner) in miners.iter().take(limit).enumerate() { + let i = i + 1; + let miner_short = if miner.miner.len() > 28 { + format!("{}...", &miner.miner[..25]) + } else { + miner.miner.clone() + }; + println!("{:<4} {:<30} {:<15} {:.2}x", + i, + miner_short, + miner.hardware_type, + miner.antiquity_multiplier + ); + } + + // Statistics + let total = miners.len(); + let multipliers: Vec = miners.iter().map(|m| m.antiquity_multiplier).collect(); + let avg_mult = multipliers.iter().sum::() / total as f64; + + // Hardware distribution + let mut hw_counts: HashMap = HashMap::new(); + for miner in &miners { + *hw_counts.entry(miner.hardware_type.clone()).or_insert(0) += 1; + } + + let mut hw_vec: Vec<_> = hw_counts.iter().collect(); + hw_vec.sort_by(|a, b| b.1.cmp(a.1)); + + println!("\nšŸ“Š Statistics"); + println!("Total Miners: {}", total); + println!("Avg Multiplier: {:.2}x", avg_mult); + println!("\nHardware Distribution:"); + for (hw, count) in hw_vec { + println!(" {}: {}", hw, count); + } + + Ok(()) +} + +/// Print wallet balance +async fn cmd_balance(client: &Client, wallet: &str) -> Result<(), Box> { + let url = format!("{}/api/balance/{}", API_BASE, wallet); + let response = client.get(&url).send().await?; + + if response.status() == 404 { + println!("\nāš ļø Wallet not found or has no balance"); + return Ok(()); + } + + let balance: BalanceResponse = response.json().await?; + + println!("\nšŸ’° Wallet Balance"); + println!("=================="); + println!("Wallet: {}", balance.wallet); + println!("Balance: {:.8} RTC", balance.balance); + + Ok(()) +} + +/// Print node statistics +async fn cmd_stats(client: &Client) -> Result<(), Box> { + // Get epoch + let epoch_url = format!("{}/epoch", API_BASE); + let epoch_response = client.get(&epoch_url).send().await?; + let epoch: EpochResponse = epoch_response.json().await?; + + // Get miners + let miners_url = format!("{}/api/miners", API_BASE); + let miners_response = client.get(&miners_url).send().await?; + let miners: Vec = miners_response.json().await?; + + println!("\nšŸ“ˆ RustChain Node Statistics"); + println!("============================"); + println!("Current Epoch: {}", epoch.epoch); + println!("Current Slot: {}", epoch.slot); + println!("Blocks/Epoch: {}", epoch.blocks_per_epoch); + println!("Enrolled Miners: {}", epoch.enrolled_miners); + println!("Total Miners: {}", miners.len()); + println!("Epoch PoT: {}", epoch.epoch_pot); + + // Calculate total score + let total_score: f64 = miners.iter().map(|m| m.entropy_score).sum(); + println!("Total Network Score: {:.2}", total_score); + + Ok(()) +} + +/// Generate a random RTC wallet address +fn cmd_address_generate(length: usize, prefix: &str) -> Result<(), Box> { + let mut rng = rand::thread_rng(); + let chars: Vec = "abcdefghijklmnopqrstuvwxyz0123456789-_".chars().collect(); + let random_part: String = (0..length) + .map(|_| chars[rng.gen_range(0..chars.len())]) + .collect(); + + let address = format!("{}-{}", prefix.to_uppercase(), random_part); + + println!("\nļæ½ Generated RTC Wallet Address"); + println!("==============================="); + println!("Address: {}", address); + println!("Prefix: {}", prefix.to_uppercase()); + println!("Length: {} characters", length); + println!("\nāš ļø IMPORTANT: Save this address securely!"); + println!(" This is your wallet identifier on the RustChain network."); + + Ok(()) +} + +/// Validate RTC address format +fn cmd_address_validate(address: &str) -> Result<(), Box> { + let parts: Vec<&str> = address.split('-').collect(); + + println!("\nšŸ” RTC Address Validation"); + println!("==========================="); + println!("Address: {}", address); + + // Check format + if parts.len() < 2 { + println!("āŒ Invalid: Address must contain a prefix and identifier separated by '-'"); + println!(" Expected format: PREFIX-identifier (e.g., RTC-abc123)"); + return Ok(()); + } + + let prefix = parts[0]; + let identifier = parts[1..].join("-"); + + // Validate prefix + let valid_prefixes = ["RTC", "WALLET", "NODE", "MINER"]; + let prefix_upper = prefix.to_uppercase(); + + if !valid_prefixes.contains(&prefix_upper.as_str()) { + println!("āš ļø Warning: Prefix '{}' is not standard.", prefix); + println!(" Standard prefixes: {}", valid_prefixes.join(", ")); + } else { + println!("āœ“ Prefix: {} (valid)", prefix_upper); + } + + // Validate identifier + let valid_chars = identifier.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'); + + if identifier.len() < 3 { + println!("āŒ Invalid: Identifier too short (minimum 3 characters)"); + } else if identifier.len() > 64 { + println!("āŒ Invalid: Identifier too long (maximum 64 characters)"); + } else if !valid_chars { + println!("āŒ Invalid: Identifier contains invalid characters"); + } else { + println!("āœ“ Identifier: {} (valid)", identifier); + } + + // Overall result + let is_valid = parts.len() >= 2 && identifier.len() >= 3 && identifier.len() <= 64 && valid_chars; + + println!("\nResult: {}", if is_valid { "āœ… VALID" } else { "āŒ INVALID" }); + + Ok(()) +} + +/// Verify address exists on the network +async fn cmd_address_verify(client: &Client, address: &str) -> Result<(), Box> { + let url = format!("{}/wallet/balance?miner_id={}", API_BASE, address); + let response = client.get(&url).send().await?; + + println!("\nšŸ”— Network Address Verification"); + println!("==================================="); + println!("Address: {}", address); + println!("Network: {}", API_BASE); + + if response.status() == 404 { + println!("\nāš ļø Address not found on network"); + println!(" The address may not be registered yet."); + } else if response.status() == 200 { + let balance: BalanceResponse = response.json().await?; + let amount = balance.amount_rtc.unwrap_or(balance.balance); + println!("\nāœ… Address verified on network!"); + println!(" Miner ID: {}", balance.miner_id); + println!(" Balance: {:.8} RTC", amount); + } else { + println!("\nāŒ Error: Unexpected response status: {}", response.status()); + } + + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let cli = Cli::parse(); + let client = get_client(); + + match cli.command { + Commands::Health => { + cmd_health(&client).await?; + } + Commands::Epoch => { + cmd_epoch(&client).await?; + } + Commands::Miners { limit } => { + cmd_miners(&client, limit).await?; + } + Commands::Balance { wallet } => { + cmd_balance(&client, &wallet).await?; + } + Commands::Stats => { + cmd_stats(&client).await?; + } + Commands::Address { action } => { + match action { + AddressCommands::Generate { length, prefix } => { + cmd_address_generate(length, &prefix)?; + } + AddressCommands::Validate { address } => { + cmd_address_validate(&address)?; + } + AddressCommands::Verify { address } => { + cmd_address_verify(&client, &address).await?; + } + } + } + } + + Ok(()) +} diff --git a/tools/star-tracker/README.md b/tools/star-tracker/README.md new file mode 100644 index 000000000..03fe9476c --- /dev/null +++ b/tools/star-tracker/README.md @@ -0,0 +1,45 @@ +# GitHub Star Growth Tracker + +A dashboard that tracks Scottcjn repo stars over time. + +## Features + +- Store daily star snapshots in SQLite +- Render growth chart (HTML dashboard) +- Track all repositories +- Show total stars, daily delta, top growers + +## Installation + +```bash +pip install requests +``` + +## Usage + +```bash +# Fetch latest star data +python main.py --fetch + +# Generate HTML dashboard +python main.py --dashboard + +# Both at once +python main.py --fetch --dashboard + +# Watch mode (fetch and display every hour) +python main.py --fetch --watch +``` + +## Output + +- SQLite database: `~/.github_star_tracker.db` +- HTML dashboard: `star_dashboard.html` + +## Reward + +10 RTC (upon completion of bounty #1110) + +## Screenshot + +![Dashboard](screenshot.png) diff --git a/tools/star-tracker/main.py b/tools/star-tracker/main.py new file mode 100644 index 000000000..0a0251175 --- /dev/null +++ b/tools/star-tracker/main.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python3 +""" +GitHub Star Growth Tracker +Tracks Scottcjn repo stars over time and renders a dashboard. +""" + +import argparse +import json +import os +import sqlite3 +import sys +import time +from datetime import datetime, timedelta +from typing import Dict, List, Optional + +import requests + +# Configuration +DB_PATH = os.path.expanduser("~/.github_star_tracker.db") +GITHUB_API = "https://api.github.com" +OWNER = "Scottcjn" + + +def init_db(): + """Initialize SQLite database.""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + cursor.execute(""" + CREATE TABLE IF NOT EXISTS stars ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + repo_name TEXT NOT NULL, + stars INTEGER NOT NULL, + recorded_at TEXT NOT NULL, + UNIQUE(repo_name, recorded_at) + ) + """) + + cursor.execute(""" + CREATE TABLE IF NOT EXISTS repos ( + name TEXT PRIMARY KEY, + added_at TEXT NOT NULL + ) + """) + + conn.commit() + return conn + + +def get_all_repos(owner: str) -> List[Dict]: + """Get all repositories for an owner.""" + repos = [] + page = 1 + while True: + url = f"{GITHUB_API}/users/{owner}/repos?per_page=100&page={page}&type=all" + response = requests.get(url) + if response.status_code != 200: + print(f"Error fetching repos: {response.status_code}") + break + + data = response.json() + if not data: + break + + repos.extend(data) + page += 1 + + # Rate limit protection + if "next" not in response.links: + break + + return repos + + +def get_repo_stars(owner: str, repo: str) -> int: + """Get star count for a single repo.""" + url = f"{GITHUB_API}/repos/{owner}/{repo}" + response = requests.get(url) + if response.status_code == 200: + return response.json().get("stargazers_count", 0) + return 0 + + +def record_stars(conn: sqlite3.Connection, repos: List[Dict]): + """Record star counts for all repos.""" + cursor = conn.cursor() + today = datetime.now().strftime("%Y-%m-%d") + + for repo in repos: + repo_name = repo["name"] + stars = repo.get("stargazers_count", 0) + + try: + cursor.execute( + "INSERT OR IGNORE INTO stars (repo_name, stars, recorded_at) VALUES (?, ?, ?)", + (repo_name, stars, today) + ) + + # Also update if already exists + cursor.execute( + "UPDATE stars SET stars = ? WHERE repo_name = ? AND recorded_at = ?", + (stars, repo_name, today) + ) + except Exception as e: + print(f"Error recording {repo_name}: {e}") + + # Ensure repo is tracked + cursor.execute( + "INSERT OR IGNORE INTO repos (name, added_at) VALUES (?, ?)", + (repo_name, today) + ) + + conn.commit() + + +def get_star_history(conn: sqlite3.Connection, days: int = 30) -> List[Dict]: + """Get star history for the past N days.""" + cursor = conn.cursor() + start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d") + + cursor.execute(""" + SELECT repo_name, stars, recorded_at + FROM stars + WHERE recorded_at >= ? + ORDER BY recorded_at + """, (start_date,)) + + return [{"repo": r[0], "stars": r[1], "date": r[2]} for r in cursor.fetchall()] + + +def get_total_stars(conn: sqlite3.Connection) -> int: + """Get total stars across all repos.""" + cursor = conn.cursor() + today = datetime.now().strftime("%Y-%m-%d") + + cursor.execute(""" + SELECT SUM(stars) FROM stars WHERE recorded_at = ? + """, (today,)) + + result = cursor.fetchone()[0] + return result if result else 0 + + +def get_daily_delta(conn: sqlite3.Connection) -> int: + """Calculate daily star delta.""" + cursor = conn.cursor() + today = datetime.now().strftime("%Y-%m-%d") + yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") + + cursor.execute(""" + SELECT SUM(stars) FROM stars WHERE recorded_at = ? + """, (today,)) + today_stars = cursor.fetchone()[0] or 0 + + cursor.execute(""" + SELECT SUM(stars) FROM stars WHERE recorded_at = ? + """, (yesterday,)) + yesterday_stars = cursor.fetchone()[0] or 0 + + return today_stars - yesterday_stars + + +def get_top_growers(conn: sqlite3.Connection, days: int = 7) -> List[Dict]: + """Get top growing repos.""" + cursor = conn.cursor() + start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d") + today = datetime.now().strftime("%Y-%m-%d") + + cursor.execute(""" + SELECT s1.repo_name, s1.stars as current_stars, s2.stars as old_stars + FROM stars s1 + JOIN stars s2 ON s1.repo_name = s2.repo_name + WHERE s1.recorded_at = ? AND s2.recorded_at = ? + """, (today, start_date)) + + growers = [] + for row in cursor.fetchall(): + delta = row[1] - row[2] + if delta > 0: + growers.append({ + "repo": row[0], + "stars": row[1], + "growth": delta + }) + + growers.sort(key=lambda x: x["growth"], reverse=True) + return growers[:10] + + +def generate_html_dashboard(conn: sqlite3.Connection, output_path: str = "star_dashboard.html"): + """Generate HTML dashboard.""" + total_stars = get_total_stars(conn) + daily_delta = get_daily_delta(conn) + top_growers = get_top_growers(conn) + history = get_star_history(conn, days=30) + + # Prepare chart data + dates = sorted(set(h["date"] for h in history)) + repo_names = list(set(h["repo"] for h in history))[:20] # Top 20 repos + + chart_data = {} + for repo in repo_names: + repo_history = [h for h in history if h["repo"] == repo] + chart_data[repo] = {h["date"]: h["stars"] for h in repo_history} + + html = f""" + + + + GitHub Star Growth Tracker - {OWNER} + + + + +
+

⭐ GitHub Star Growth Tracker - {OWNER}

+

Last Updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}

+ +
+
+
{total_stars:,}
+
Total Stars
+
+
+
{'+' if daily_delta >= 0 else ''}{daily_delta}
+
Daily Change
+
+
+ +
+ +
+ +

Top Growers (7 days)

+ + +""" + + for g in top_growers: + html += f""" +""" + + html += """
RepositoryStarsGrowth
{g['repo']}{g['stars']}+{g['growth']}
+
+ + +""" + + with open(output_path, "w") as f: + f.write(html) + + print(f"Dashboard generated: {output_path}") + return output_path + + +def print_terminal_dashboard(conn: sqlite3.Connection): + """Print dashboard in terminal.""" + total_stars = get_total_stars(conn) + daily_delta = get_daily_delta(conn) + top_growers = get_top_growers(conn) + + print("\n" + "=" * 60) + print(f"GitHub Star Growth Tracker - {OWNER}") + print(f"Last Updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}") + print("=" * 60) + + print(f"\nTotal Stars: {total_stars:,}") + delta_sign = "+" if daily_delta >= 0 else "" + print(f"Daily Change: {delta_sign}{daily_delta}") + + print("\nTop Growers (7 days):") + print(f"{'Repository':<40} {'Stars':<10} {'Growth':<10}") + print("-" * 60) + for g in top_growers: + print(f"{g['repo']:<40} {g['stars']:<10} +{g['growth']:<10}") + + print("=" * 60) + + +def main(): + parser = argparse.ArgumentParser(description="GitHub Star Growth Tracker") + parser.add_argument("--fetch", action="store_true", help="Fetch latest star data") + parser.add_argument("--dashboard", action="store_true", help="Generate HTML dashboard") + parser.add_argument("--output", default="star_dashboard.html", help="Output file for dashboard") + parser.add_argument("-w", "--watch", action="store_true", help="Watch mode") + parser.add_argument("-i", "--interval", type=int, default=3600, help="Watch interval in seconds") + args = parser.parse_args() + + conn = init_db() + + if args.fetch: + print(f"Fetching repositories for {OWNER}...") + repos = get_all_repos(OWNER) + print(f"Found {len(repos)} repositories") + + print("Recording star counts...") + record_stars(conn, repos) + print("Done!") + + if args.dashboard: + generate_html_dashboard(conn, args.output) + else: + print_terminal_dashboard(conn) + + if args.watch: + try: + while True: + time.sleep(args.interval) + repos = get_all_repos(OWNER) + record_stars(conn, repos) + print("\033[2J\033[H", end="") + print_terminal_dashboard(conn) + except KeyboardInterrupt: + print("\nStopped.") + + conn.close() + + +if __name__ == "__main__": + main()