/ bin / explorer / site / rpc.py
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])