git / moonbite-network / node_rpc_server.py · raw

"""JSON-RPC API Server for Full Node.Clients (wallets, miners, dashboards) connect here via HTTP/WebSocketto interact with the local full node."""from flask import Flask, jsonify, request, render_templatefrom full_node import BigCoinFullNodeimport loggingimport jsonimport syslogging.basicConfig(level=logging.INFO, stream=sys.stdout)logger = logging.getLogger(__name__)logging.basicConfig(level=logging.INFO)logger = logging.getLogger(__name__)class NodeRPCServer:    """Wraps a full node with JSON-RPC HTTP API."""    def __init__(self, node: BigCoinFullNode, rpc_host: str = "127.0.0.1", rpc_port: int = 8000):        self.node = node        self.rpc_host = rpc_host        self.rpc_port = rpc_port        # Create Flask app        self.app = Flask(__name__, template_folder="templates", static_folder="static")        self.setup_routes()    def setup_routes(self):        """Setup HTTP routes."""        @self.app.route("/", methods=["GET"])        def index():            """Dashboard home page."""            return render_template("index.html")        @self.app.route("/wallet", methods=["GET"])        def wallet_page():            return render_template("wallet.html")        @self.app.route("/mining", methods=["GET"])        def mining_page():            return render_template("mining.html")        # ===================================================================== #        # JSON-RPC ENDPOINTS        # ===================================================================== #        @self.app.route("/rpc", methods=["POST"])        def rpc():            """JSON-RPC endpoint."""            try:                request_data = request.json                result = self.node.process_rpc_request(request_data)                return jsonify(result)            except Exception as e:                logger.error(f"RPC error: {e}")                return jsonify({"error": str(e)}), 500        # ===================================================================== #        # REST API (for convenience)        # ===================================================================== #        @self.app.route("/api/wallet/new", methods=["GET"])        def api_wallet_new():            """Generate new address."""            try:                result = self.node.generate_new_address()                return jsonify({"status": "success", **result})            except Exception as e:                return jsonify({"status": "error", "message": str(e)}), 500        @self.app.route("/api/wallet/balance", methods=["GET"])        def api_wallet_balance():            """Get balance."""            try:                result = self.node.get_balance()                return jsonify({"status": "success", **result})            except Exception as e:                return jsonify({"status": "error", "message": str(e)}), 500        @self.app.route("/api/wallet/send", methods=["POST"])        def api_wallet_send():            try:                data = request.json or {}                to_address = data.get("to_address")                amount = data.get("amount")  # amount in COINS (float), from the UI                if not to_address or amount is None:                    return jsonify(                        {"status": "error", "message": "to_address and amount required"}                    ), 400                try:                    amount_cents = int(round(float(amount) * 100_000_000))                except (TypeError, ValueError):                    return jsonify({"status": "error", "message": "invalid amount"}), 400                if amount_cents <= 0:                    return jsonify({"status": "error", "message": "amount must be positive"}), 400                result = self.node.send_to_address(to_address, amount_cents)                code = 200 if result.get("status") == "success" else 400                return jsonify(result), code            except Exception as e:                return jsonify({"status": "error", "message": str(e)}), 500        @self.app.route("/api/mempool", methods=["GET"])        def api_mempool():            try:                result = self.node.get_mempool()                return jsonify({"status": "success", **result})            except Exception as e:                return jsonify({"status": "error", "message": str(e)}), 500        @self.app.route("/api/blockchain/info", methods=["GET"])        def api_blockchain_info():            """Get blockchain info."""            try:                result = self.node.get_blockchain_info()                return jsonify({"status": "success", **result})            except Exception as e:                return jsonify({"status": "error", "message": str(e)}), 500        @self.app.route("/api/block/<int:height>", methods=["GET"])        def api_get_block(height):            """Return the active-chain block at a given height (used for sync)."""            try:                block = self.node.get_block_by_height(height)                if block is None:                    return jsonify({"status": "error", "message": "not found"}), 404                return jsonify({"status": "success", "block": block})            except Exception as e:                return jsonify({"status": "error", "message": str(e)}), 500        @self.app.route("/api/mining/start", methods=["POST"])        def api_mining_start():            """Start mining."""            try:                data = request.json or {}                address = data.get("address")                if not address:                    return jsonify({"status": "error", "message": "No address"}), 400                try:                    self.node.start_mining(address)                    return jsonify({"status": "success", "message": "Mining started"})                except Exception as e:                    logger.error(f"Mining start error: {e}", exc_info=True)                    return jsonify({"status": "error", "message": str(e)}), 400            except Exception as e:                logger.error(f"Mining API error: {e}", exc_info=True)                return jsonify({"status": "error", "message": str(e)}), 500        @self.app.route("/api/mining/status", methods=["GET"])        def api_mining_status():            """Get mining status."""            try:                status = self.node.process_rpc_request({"method": "mining_status"})                return jsonify({"status": "success", **status})            except Exception as e:                return jsonify({"status": "error", "message": str(e)}), 500        @self.app.route("/api/mining/stop", methods=["GET"])        def api_mining_stop():            """Stop mining."""            try:                self.node.stop_mining()                return jsonify({"status": "success", "message": "Mining stopped"})            except Exception as e:                return jsonify({"status": "error", "message": str(e)}), 500        @self.app.route("/api/node/peers", methods=["GET"])        def api_node_peers():            """Get connected peers."""            try:                peers = self.node.get_connected_peers()                return jsonify({                    "status": "success",                    "peer_count": len(peers),                    "peers": [p.to_dict() for p in peers]                })            except Exception as e:                return jsonify({"status": "error", "message": str(e)}), 500        @self.app.route("/api/node/stats", methods=["GET"])        def api_node_stats():            """Get node statistics."""            try:                stats = self.node.get_stats()                return jsonify({"status": "success", **stats})            except Exception as e:                return jsonify({"status": "error", "message": str(e)}), 500    def run(self, debug=False):        """Run the RPC server.        In normal operation the app is served via `wsgi_server.serve` (waitress,        which handles high concurrent request load far better than Flask's dev        server). `debug=True` keeps the Flask dev server for local debugging.        """        logger.info(f"Starting RPC server on {self.rpc_host}:{self.rpc_port}")        if debug:            self.app.run(                host=self.rpc_host,                port=self.rpc_port,                debug=True,                threaded=True,                use_reloader=False            )        else:            from wsgi_server import serve            serve(self.app, host=self.rpc_host, port=self.rpc_port)def main():    """Run RPC server for a full node."""    import sys    import os    node_id = sys.argv[1] if len(sys.argv) > 1 else "node1"    p2p_port = int(sys.argv[2]) if len(sys.argv) > 2 else 9000    rpc_port = int(sys.argv[3]) if len(sys.argv) > 3 else 8000    # Coinbase maturity is 100 by default (Bitcoin-like); override for demos.    maturity = int(os.environ.get("COINBASE_MATURITY", "100"))    # Create full node    node = BigCoinFullNode(node_id, host="127.0.0.1", port=p2p_port,                           coinbase_maturity=maturity)    node.start_server()    # Start RPC server    rpc_server = NodeRPCServer(node, rpc_host="127.0.0.1", rpc_port=rpc_port)    rpc_server.run(debug=False)if __name__ == "__main__":    main()