diff --git a/api_validator.py b/api_validator.py new file mode 100644 index 00000000..14ff9739 --- /dev/null +++ b/api_validator.py @@ -0,0 +1,238 @@ +# SPDX-License-Identifier: MIT + +import json +import sqlite3 +import requests +import yaml +from typing import Dict, List, Any, Optional +from urllib.parse import urljoin +import time +import os + +DB_PATH = "node/blockchain.db" + +class APIValidator: + def __init__(self, openapi_spec_path: str, base_url: str = "http://localhost:5000"): + self.openapi_spec_path = openapi_spec_path + self.base_url = base_url + self.spec = None + self.validation_results = [] + + def load_openapi_spec(self) -> bool: + """Load and validate OpenAPI specification""" + try: + with open(self.openapi_spec_path, 'r') as f: + if self.openapi_spec_path.endswith('.yaml') or self.openapi_spec_path.endswith('.yml'): + self.spec = yaml.safe_load(f) + else: + self.spec = json.load(f) + return True + except Exception as e: + print(f"Failed to load OpenAPI spec: {e}") + return False + + def get_node_status(self) -> Dict[str, Any]: + """Check if node is running and accessible""" + try: + response = requests.get(f"{self.base_url}/api/stats", timeout=5) + return { + "accessible": response.status_code == 200, + "status_code": response.status_code, + "response_time": response.elapsed.total_seconds() + } + except requests.RequestException as e: + return { + "accessible": False, + "error": str(e), + "response_time": None + } + + def validate_endpoint(self, path: str, method: str, spec_info: Dict) -> Dict[str, Any]: + """Validate single endpoint against live node""" + result = { + "path": path, + "method": method, + "success": False, + "status_code": None, + "response_time": None, + "schema_valid": False, + "errors": [] + } + + try: + # Make request to endpoint + url = urljoin(self.base_url, path) + start_time = time.time() + + if method.lower() == 'get': + response = requests.get(url, timeout=10) + elif method.lower() == 'post': + response = requests.post(url, json={}, timeout=10) + else: + result["errors"].append(f"Unsupported method: {method}") + return result + + result["response_time"] = time.time() - start_time + result["status_code"] = response.status_code + + # Check if response is successful + if response.status_code < 400: + result["success"] = True + + # Try to validate response schema if available + try: + response_data = response.json() + result["schema_valid"] = True + except json.JSONDecodeError: + result["errors"].append("Invalid JSON response") + else: + result["errors"].append(f"HTTP {response.status_code}: {response.text}") + + except requests.RequestException as e: + result["errors"].append(f"Request failed: {str(e)}") + except Exception as e: + result["errors"].append(f"Validation error: {str(e)}") + + return result + + def validate_all_endpoints(self) -> List[Dict[str, Any]]: + """Validate all endpoints defined in OpenAPI spec""" + if not self.spec: + if not self.load_openapi_spec(): + return [] + + results = [] + paths = self.spec.get('paths', {}) + + for path, methods in paths.items(): + for method, spec_info in methods.items(): + if method.lower() in ['get', 'post', 'put', 'delete', 'patch']: + result = self.validate_endpoint(path, method.upper(), spec_info) + results.append(result) + self.validation_results.append(result) + + return results + + def generate_validation_report(self) -> Dict[str, Any]: + """Generate comprehensive validation report""" + if not self.validation_results: + self.validate_all_endpoints() + + total_endpoints = len(self.validation_results) + successful_endpoints = len([r for r in self.validation_results if r['success']]) + failed_endpoints = total_endpoints - successful_endpoints + + return { + "summary": { + "total_endpoints": total_endpoints, + "successful": successful_endpoints, + "failed": failed_endpoints, + "success_rate": (successful_endpoints / total_endpoints * 100) if total_endpoints > 0 else 0 + }, + "node_status": self.get_node_status(), + "endpoint_results": self.validation_results, + "timestamp": time.time() + } + + def save_report_to_db(self, report: Dict[str, Any]) -> bool: + """Save validation report to database""" + try: + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + + # Create table if not exists + cursor.execute(''' + CREATE TABLE IF NOT EXISTS api_validation_reports ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp REAL, + total_endpoints INTEGER, + successful_endpoints INTEGER, + failed_endpoints INTEGER, + success_rate REAL, + node_accessible BOOLEAN, + report_data TEXT + ) + ''') + + # Insert report + cursor.execute(''' + INSERT INTO api_validation_reports ( + timestamp, total_endpoints, successful_endpoints, + failed_endpoints, success_rate, node_accessible, report_data + ) VALUES (?, ?, ?, ?, ?, ?, ?) + ''', ( + report['timestamp'], + report['summary']['total_endpoints'], + report['summary']['successful'], + report['summary']['failed'], + report['summary']['success_rate'], + report['node_status']['accessible'], + json.dumps(report) + )) + + conn.commit() + return True + + except Exception as e: + print(f"Failed to save report to database: {e}") + return False + + def get_latest_reports(self, limit: int = 10) -> List[Dict[str, Any]]: + """Get latest validation reports from database""" + try: + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + cursor.execute(''' + SELECT report_data FROM api_validation_reports + ORDER BY timestamp DESC LIMIT ? + ''', (limit,)) + + reports = [] + for row in cursor.fetchall(): + reports.append(json.loads(row[0])) + + return reports + + except Exception as e: + print(f"Failed to retrieve reports from database: {e}") + return [] + +def main(): + """Main function for running API validation""" + validator = APIValidator('openapi.yaml') + + print("Loading OpenAPI specification...") + if not validator.load_openapi_spec(): + print("Failed to load OpenAPI spec. Exiting.") + return + + print("Validating API endpoints...") + results = validator.validate_all_endpoints() + + print("Generating validation report...") + report = validator.generate_validation_report() + + print(f"\nValidation Report:") + print(f"Total endpoints: {report['summary']['total_endpoints']}") + print(f"Successful: {report['summary']['successful']}") + print(f"Failed: {report['summary']['failed']}") + print(f"Success rate: {report['summary']['success_rate']:.2f}%") + print(f"Node accessible: {report['node_status']['accessible']}") + + # Save report to database + if validator.save_report_to_db(report): + print("\nReport saved to database.") + else: + print("\nFailed to save report to database.") + + # Print detailed results + print("\nDetailed Results:") + for result in results: + status = "✓" if result['success'] else "✗" + print(f"{status} {result['method']} {result['path']} - {result['status_code']}") + if result['errors']: + for error in result['errors']: + print(f" Error: {error}") + +if __name__ == "__main__": + main() diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 00000000..a1c0a80e --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,603 @@ +# SPDX-License-Identifier: MIT +openapi: 3.0.3 +info: + title: RustChain Node API + description: | + RustChain node API for blockchain operations, mining, transactions, and network management. + + This API provides comprehensive access to blockchain data, transaction management, + mining operations, and peer-to-peer network functionality. + version: 2.2.1 + contact: + name: RustChain Development Team + url: https://github.com/Scottcjn/Rustchain + license: + name: MIT + url: https://opensource.org/licenses/MIT + +servers: + - url: http://localhost:5000 + description: Local development node + - url: http://localhost:8080 + description: Default production node + +tags: + - name: blockchain + description: Blockchain data and operations + - name: transactions + description: Transaction management + - name: mining + description: Mining operations + - name: network + description: Network and peer management + - name: wallet + description: Wallet operations + - name: auth + description: Authentication endpoints + +paths: + /api/blockchain: + get: + tags: [blockchain] + summary: Get full blockchain + description: Retrieve the complete blockchain with all blocks + responses: + '200': + description: Blockchain data + content: + application/json: + schema: + type: object + properties: + chain: + type: array + items: + $ref: '#/components/schemas/Block' + length: + type: integer + + /api/blockchain/latest: + get: + tags: [blockchain] + summary: Get latest block + description: Retrieve the most recent block in the chain + responses: + '200': + description: Latest block data + content: + application/json: + schema: + $ref: '#/components/schemas/Block' + + /api/blockchain/height: + get: + tags: [blockchain] + summary: Get blockchain height + description: Get the current height of the blockchain + responses: + '200': + description: Blockchain height + content: + application/json: + schema: + type: object + properties: + height: + type: integer + + /api/blockchain/block/{index}: + get: + tags: [blockchain] + summary: Get block by index + description: Retrieve a specific block by its index + parameters: + - name: index + in: path + required: true + schema: + type: integer + minimum: 0 + responses: + '200': + description: Block data + content: + application/json: + schema: + $ref: '#/components/schemas/Block' + '404': + description: Block not found + + /api/blockchain/validate: + get: + tags: [blockchain] + summary: Validate blockchain + description: Check if the blockchain is valid + responses: + '200': + description: Validation result + content: + application/json: + schema: + type: object + properties: + valid: + type: boolean + message: + type: string + + /api/transactions: + get: + tags: [transactions] + summary: Get all transactions + description: Retrieve all transactions from the mempool and blockchain + parameters: + - name: limit + in: query + schema: + type: integer + default: 50 + maximum: 1000 + - name: offset + in: query + schema: + type: integer + default: 0 + responses: + '200': + description: Transaction list + content: + application/json: + schema: + type: object + properties: + transactions: + type: array + items: + $ref: '#/components/schemas/Transaction' + total: + type: integer + + post: + tags: [transactions] + summary: Submit new transaction + description: Submit a new transaction to the network + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TransactionInput' + responses: + '201': + description: Transaction submitted successfully + content: + application/json: + schema: + type: object + properties: + txid: + type: string + message: + type: string + '400': + description: Invalid transaction + '403': + description: Insufficient balance + + /api/transactions/{txid}: + get: + tags: [transactions] + summary: Get transaction by ID + description: Retrieve a specific transaction by its ID + parameters: + - name: txid + in: path + required: true + schema: + type: string + responses: + '200': + description: Transaction data + content: + application/json: + schema: + $ref: '#/components/schemas/Transaction' + '404': + description: Transaction not found + + /api/transactions/pending: + get: + tags: [transactions] + summary: Get pending transactions + description: Retrieve transactions waiting in the mempool + responses: + '200': + description: Pending transactions + content: + application/json: + schema: + type: object + properties: + pending: + type: array + items: + $ref: '#/components/schemas/Transaction' + count: + type: integer + + /api/mining/start: + post: + tags: [mining] + summary: Start mining + description: Begin mining operations + security: + - ApiKeyAuth: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + miner_address: + type: string + description: Address to receive mining rewards + responses: + '200': + description: Mining started + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + '401': + description: Unauthorized + + /api/mining/stop: + post: + tags: [mining] + summary: Stop mining + description: Stop mining operations + security: + - ApiKeyAuth: [] + responses: + '200': + description: Mining stopped + content: + application/json: + schema: + type: object + properties: + status: + type: string + + /api/mining/status: + get: + tags: [mining] + summary: Get mining status + description: Check current mining status and statistics + responses: + '200': + description: Mining status + content: + application/json: + schema: + type: object + properties: + mining: + type: boolean + hashrate: + type: number + blocks_mined: + type: integer + difficulty: + type: integer + + /api/mining/difficulty: + get: + tags: [mining] + summary: Get current difficulty + description: Retrieve the current mining difficulty + responses: + '200': + description: Current difficulty + content: + application/json: + schema: + type: object + properties: + difficulty: + type: integer + next_adjustment: + type: integer + + /api/network/peers: + get: + tags: [network] + summary: Get connected peers + description: List all connected network peers + responses: + '200': + description: Peer list + content: + application/json: + schema: + type: object + properties: + peers: + type: array + items: + $ref: '#/components/schemas/Peer' + count: + type: integer + + post: + tags: [network] + summary: Add peer + description: Manually add a peer to the network + security: + - ApiKeyAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + address: + type: string + port: + type: integer + required: [address, port] + responses: + '201': + description: Peer added successfully + '400': + description: Invalid peer address + + /api/network/sync: + post: + tags: [network] + summary: Trigger network sync + description: Manually trigger blockchain synchronization + security: + - ApiKeyAuth: [] + responses: + '200': + description: Sync initiated + content: + application/json: + schema: + type: object + properties: + status: + type: string + + /api/wallet/balance/{address}: + get: + tags: [wallet] + summary: Get address balance + description: Retrieve the balance for a specific address + parameters: + - name: address + in: path + required: true + schema: + type: string + responses: + '200': + description: Address balance + content: + application/json: + schema: + type: object + properties: + address: + type: string + balance: + type: number + confirmed: + type: number + unconfirmed: + type: number + + /api/wallet/utxo/{address}: + get: + tags: [wallet] + summary: Get UTXOs for address + description: Retrieve unspent transaction outputs for an address + parameters: + - name: address + in: path + required: true + schema: + type: string + responses: + '200': + description: UTXO list + content: + application/json: + schema: + type: object + properties: + utxos: + type: array + items: + $ref: '#/components/schemas/UTXO' + total_value: + type: number + + /api/auth/login: + post: + tags: [auth] + summary: Authenticate user + description: Login and receive API key for authenticated endpoints + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + password: + type: string + required: [username, password] + responses: + '200': + description: Authentication successful + content: + application/json: + schema: + type: object + properties: + token: + type: string + expires_in: + type: integer + '401': + description: Invalid credentials + + /api/stats: + get: + tags: [blockchain] + summary: Get blockchain statistics + description: Retrieve general blockchain statistics + responses: + '200': + description: Blockchain statistics + content: + application/json: + schema: + type: object + properties: + total_blocks: + type: integer + total_transactions: + type: integer + total_supply: + type: number + market_cap: + type: number + avg_block_time: + type: number + network_hashrate: + type: number + +components: + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: X-API-Key + + schemas: + Block: + type: object + properties: + index: + type: integer + timestamp: + type: number + previous_hash: + type: string + hash: + type: string + nonce: + type: integer + difficulty: + type: integer + merkle_root: + type: string + transactions: + type: array + items: + $ref: '#/components/schemas/Transaction' + miner: + type: string + + Transaction: + type: object + properties: + txid: + type: string + timestamp: + type: number + inputs: + type: array + items: + $ref: '#/components/schemas/TransactionInput' + outputs: + type: array + items: + $ref: '#/components/schemas/TransactionOutput' + fee: + type: number + signature: + type: string + confirmed: + type: boolean + block_index: + type: integer + + TransactionInput: + type: object + properties: + sender: + type: string + recipient: + type: string + amount: + type: number + private_key: + type: string + description: Only required for transaction submission + required: [sender, recipient, amount] + + TransactionOutput: + type: object + properties: + address: + type: string + amount: + type: number + script: + type: string + + UTXO: + type: object + properties: + txid: + type: string + output_index: + type: integer + address: + type: string + amount: + type: number + confirmations: + type: integer + spendable: + type: boolean + + Peer: + type: object + properties: + address: + type: string + port: + type: integer + connected_at: + type: number + last_seen: + type: number + version: + type: string + height: + type: integer + latency: + type: number diff --git a/swagger_ui.py b/swagger_ui.py new file mode 100644 index 00000000..6ed3e8f9 --- /dev/null +++ b/swagger_ui.py @@ -0,0 +1,191 @@ +# SPDX-License-Identifier: MIT + +import sqlite3 +from flask import Flask, request, jsonify, render_template_string +import json +import os + +app = Flask(__name__) +DB_PATH = 'blockchain.db' + +@app.route('/api/docs') +def swagger_ui(): + """Serve Swagger UI for the RustChain API documentation""" + swagger_html = ''' + + +
+ +