rpc.py
1 # This file is part of DarkFi (https://dark.fi) 2 # 3 # Copyright (C) 2020-2025 Dyne.org foundation 4 # 5 # This program is free software: you can redistribute it and/or modify 6 # it under the terms of the GNU Affero General Public License as 7 # published by the Free Software Foundation, either version 3 of the 8 # License, or (at your option) any later version. 9 # 10 # This program is distributed in the hope that it will be useful, 11 # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 # GNU Affero General Public License for more details. 14 # 15 # You should have received a copy of the GNU Affero General Public License 16 # along with this program. If not, see <https://www.gnu.org/licenses/>. 17 18 """ 19 Module: rpc.py 20 21 This module provides an asynchronous interface for interacting with the DarkFi explorer daemon 22 using JSON-RPC. It includes functionality to create a communication channel, send requests, 23 and handle responses from the server. 24 """ 25 26 import asyncio, json, random 27 28 from flask import abort, current_app 29 30 class Channel: 31 """Class representing the channel with the JSON-RPC server.""" 32 def __init__(self, reader, writer): 33 """Initialize the channel with a reader and writer.""" 34 self.reader = reader 35 self.writer = writer 36 37 async def readline(self): 38 """Read a line from the channel, closing it if the connection is lost.""" 39 if not (line := await self.reader.readline()): 40 self.writer.close() 41 return None 42 return line[:-1].decode() # Strip the newline 43 44 async def receive(self): 45 """Receive and decode a message from the channel.""" 46 if (plaintext := await self.readline()) is None: 47 return None 48 49 message = plaintext 50 response = json.loads(message) 51 return response 52 53 async def send(self, obj): 54 """Send a JSON-encoded object to the channel.""" 55 message = json.dumps(obj) 56 data = message.encode() 57 58 self.writer.write(data + b"\n") 59 await self.writer.drain() 60 61 async def create_channel(server_name, port): 62 """ 63 Creates a channel used to send RPC requests to the DarkFi explorer daemon. 64 """ 65 try: 66 reader, writer = await asyncio.open_connection(server_name, port) 67 except ConnectionRefusedError: 68 print( 69 f"Error: Connection Refused to '{server_name}:{port}', Either because the daemon is down, is currently syncing or wrong url.") 70 abort(500) 71 channel = Channel(reader, writer) 72 return channel 73 74 async def query(method, params): 75 """ 76 Execute a request towards the JSON-RPC server by constructing a JSON-RPC 77 request and sending it to the server. It handles connection errors and server responses, 78 returning the result of the query or raising an error if the request fails. 79 """ 80 # Create the channel to send RPC request 81 channel = await create_channel(current_app.config['explorer_rpc_url'], current_app.config['explorer_rpc_port']) 82 83 # Prepare request 84 request = { 85 "id": random.randint(0, 2 ** 32), 86 "method": method, 87 "params": params, 88 "jsonrpc": "2.0", 89 } 90 91 # Send request and await response 92 await channel.send(request) 93 response = await channel.receive() 94 95 # Closed connect returns None 96 if response is None: 97 print("error: connection with server was closed") 98 abort(500) 99 100 # Erroneous query is handled with not found 101 if "error" in response: 102 error = response["error"] 103 errcode, errmsg = error["code"], error["message"] 104 print(f"error: {errcode} - {errmsg}") 105 abort(404) 106 107 return response["result"] 108 109 async def get_last_n_blocks(n: str): 110 """Retrieves the last n blocks.""" 111 return await query("blocks.get_last_n_blocks", [n]) 112 113 async def get_basic_statistics(): 114 """Retrieves basic statistics.""" 115 return await query("statistics.get_basic_statistics", []) 116 117 async def get_metric_statistics(): 118 """Retrieves metrics statistics.""" 119 return await query("statistics.get_metric_statistics", []) 120 121 async def get_block(header_hash: str): 122 """Retrieves block information for a given header hash.""" 123 return await query("blocks.get_block_by_hash", [header_hash]) 124 125 async def get_block_transactions(header_hash: str): 126 """Retrieves transactions associated with a given block header hash.""" 127 return await query("transactions.get_transactions_by_header_hash", [header_hash]) 128 129 130 async def get_transaction(transaction_hash: str): 131 """Retrieves transaction information for a given transaction hash.""" 132 return await query("transactions.get_transaction_by_hash", [transaction_hash]) 133 134 async def get_native_contracts(): 135 """Retrieves native contracts.""" 136 return await query("contracts.get_native_contracts", []) 137 138 139 async def get_contract_source_paths(contract_id: str): 140 """Retrieves contract source code paths for a given contract ID.""" 141 return await query("contracts.get_contract_source_code_paths", [contract_id]) 142 143 async def get_contract_source(contract_id: str, source_path): 144 """Retrieves the contract source file for a given contract ID and source path.""" 145 return await query("contracts.get_contract_source", [contract_id, source_path])