rpc.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. # This file is part of DarkFi (https://dark.fi)
  2. #
  3. # Copyright (C) 2020-2026 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. Module: rpc.py
  19. This module provides an asynchronous interface for interacting with the DarkFi explorer daemon
  20. using JSON-RPC. It includes functionality to create a communication channel, send requests,
  21. and handle responses from the server.
  22. """
  23. import asyncio, json, random
  24. from flask import abort, current_app
  25. class Channel:
  26. """Class representing the channel with the JSON-RPC server."""
  27. def __init__(self, reader, writer):
  28. """Initialize the channel with a reader and writer."""
  29. self.reader = reader
  30. self.writer = writer
  31. async def readline(self):
  32. """Read a line from the channel, closing it if the connection is lost."""
  33. if not (line := await self.reader.readline()):
  34. self.writer.close()
  35. return None
  36. return line[:-1].decode() # Strip the newline
  37. async def receive(self):
  38. """Receive and decode a message from the channel."""
  39. if (plaintext := await self.readline()) is None:
  40. return None
  41. message = plaintext
  42. response = json.loads(message)
  43. return response
  44. async def send(self, obj):
  45. """Send a JSON-encoded object to the channel."""
  46. message = json.dumps(obj)
  47. data = message.encode()
  48. self.writer.write(data + b"\n")
  49. await self.writer.drain()
  50. async def create_channel(server_name, port):
  51. """
  52. Creates a channel used to send RPC requests to the DarkFi explorer daemon.
  53. """
  54. try:
  55. reader, writer = await asyncio.open_connection(server_name, port)
  56. except ConnectionRefusedError:
  57. print(
  58. f"Error: Connection Refused to '{server_name}:{port}', Either because the daemon is down, is currently syncing or wrong url.")
  59. abort(500)
  60. channel = Channel(reader, writer)
  61. return channel
  62. async def query(method, params):
  63. """
  64. Execute a request towards the JSON-RPC server by constructing a JSON-RPC
  65. request and sending it to the server. It handles connection errors and server responses,
  66. returning the result of the query or raising an error if the request fails.
  67. """
  68. # Create the channel to send RPC request
  69. channel = await create_channel(current_app.config['explorer_rpc_url'], current_app.config['explorer_rpc_port'])
  70. # Prepare request
  71. request = {
  72. "id": random.randint(0, 2 ** 32),
  73. "method": method,
  74. "params": params,
  75. "jsonrpc": "2.0",
  76. }
  77. # Send request and await response
  78. await channel.send(request)
  79. response = await channel.receive()
  80. # Closed connect returns None
  81. if response is None:
  82. print("error: connection with server was closed")
  83. abort(500)
  84. # Erroneous query is handled with not found
  85. if "error" in response:
  86. error = response["error"]
  87. errcode, errmsg = error["code"], error["message"]
  88. print(f"error: {errcode} - {errmsg}")
  89. abort(404)
  90. return response["result"]
  91. async def get_last_n_blocks(n: str):
  92. """Retrieves the last n blocks."""
  93. return await query("blocks.get_last_n_blocks", [n])
  94. async def get_basic_statistics():
  95. """Retrieves basic statistics."""
  96. return await query("statistics.get_basic_statistics", [])
  97. async def get_metric_statistics():
  98. """Retrieves metrics statistics."""
  99. return await query("statistics.get_metric_statistics", [])
  100. async def get_block(header_hash: str):
  101. """Retrieves block information for a given header hash."""
  102. return await query("blocks.get_block_by_hash", [header_hash])
  103. async def get_block_transactions(header_hash: str):
  104. """Retrieves transactions associated with a given block header hash."""
  105. return await query("transactions.get_transactions_by_header_hash", [header_hash])
  106. async def get_transaction(transaction_hash: str):
  107. """Retrieves transaction information for a given transaction hash."""
  108. return await query("transactions.get_transaction_by_hash", [transaction_hash])
  109. async def get_native_contracts():
  110. """Retrieves native contracts."""
  111. return await query("contracts.get_native_contracts", [])
  112. async def get_contract_source_paths(contract_id: str):
  113. """Retrieves contract source code paths for a given contract ID."""
  114. return await query("contracts.get_contract_source_code_paths", [contract_id])
  115. async def get_contract_source(contract_id: str, source_path):
  116. """Retrieves the contract source file for a given contract ID and source path."""
  117. return await query("contracts.get_contract_source", [contract_id, source_path])