explorer.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. #!/usr/bin/env python3
  2. # This file is part of DarkFi (https://dark.fi)
  3. #
  4. # Copyright (C) 2020-2026 Dyne.org foundation
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU Affero General Public License as
  8. # published by the Free Software Foundation, either version 3 of the
  9. # License, or (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU Affero General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU Affero General Public License
  17. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. from datetime import datetime, timezone
  19. from quart import Quart, render_template, abort, request, redirect, url_for
  20. from rpc_client import JsonRpcPool, JsonRpcError
  21. app = Quart(__name__)
  22. app.config.update(
  23. RPC_HOST="127.0.0.1",
  24. RPC_PORT="22222",
  25. RPC_MIN_CONNECTIONS=5,
  26. RPC_MAX_CONNECTIONS=50,
  27. NETWORK="Testnet",
  28. )
  29. # Global pool
  30. rpc: JsonRpcPool = None
  31. @app.before_serving
  32. async def startup():
  33. global rpc
  34. rpc = JsonRpcPool(
  35. host=app.config["RPC_HOST"],
  36. port=app.config["RPC_PORT"],
  37. min_connections=app.config["RPC_MIN_CONNECTIONS"],
  38. max_connections=app.config["RPC_MAX_CONNECTIONS"],
  39. )
  40. await rpc.start()
  41. app.logger.info(f"RPC pool started: {app.config['RPC_HOST']}:{app.config['RPC_PORT']}")
  42. @app.after_serving
  43. async def shutdown():
  44. await rpc.close()
  45. app.logger.info("RPC pool closed")
  46. @app.errorhandler(JsonRpcError)
  47. async def handle_rpc_error(error: JsonRpcError):
  48. app.logger.error(f"RPC Error: {error.code} - {error.message}")
  49. if error.code == -32600:
  50. return await render_template(
  51. "error.html",
  52. network=app.config["NETWORK"],
  53. error_code="404",
  54. error="The requested resource was not found"
  55. ), 404
  56. return await render_template(
  57. "error.html",
  58. network=app.config["NETWORK"],
  59. error_code="500",
  60. error=error.message
  61. ), 500
  62. @app.errorhandler(ConnectionError)
  63. async def handle_connection_error(error):
  64. app.logger.error(f"RPC Connection Error: {error}")
  65. return await render_template(
  66. "error.html",
  67. network=app.config["NETWORK"],
  68. error_code="503",
  69. error="Service temporarily unavailable"
  70. ), 503
  71. @app.errorhandler(404)
  72. async def handle_not_found(error):
  73. return await render_template(
  74. "error.html",
  75. network=app.config["NETWORK"],
  76. error_code="404",
  77. error="Page not found"
  78. ), 404
  79. def format_hashrate(hashrate: float) -> str:
  80. """Format hashrate with appropriate unit."""
  81. if hashrate >= 1e12:
  82. return f"{hashrate / 1e12:.2f} TH/s"
  83. elif hashrate >= 1e9:
  84. return f"{hashrate / 1e9:.2f} GH/s"
  85. elif hashrate >= 1e6:
  86. return f"{hashrate / 1e6:.2f} MH/s"
  87. elif hashrate >= 1e3:
  88. return f"{hashrate / 1e3:.2f} KH/s"
  89. else:
  90. return f"{hashrate:.2f} H/s"
  91. @app.route("/")
  92. async def index():
  93. current_difficulty = await rpc.call("current_difficulty", params=[])
  94. current_height = await rpc.call("current_height", params=[])
  95. latest_blocks = await rpc.call("latest_blocks", params=[20])
  96. hashrate = await rpc.call("get_hashrate", params=[])
  97. for block in latest_blocks:
  98. dt = datetime.fromtimestamp(block["timestamp"], tz=timezone.utc)
  99. block["timestamp"] = dt.strftime("%B %d, %Y at %I:%M %p UTC")
  100. return await render_template(
  101. "index.html",
  102. network=app.config["NETWORK"],
  103. current_difficulty=current_difficulty[0],
  104. current_height=current_height,
  105. hashrate=format_hashrate(hashrate),
  106. latest_blocks=latest_blocks,
  107. )
  108. @app.route("/block/<int:block_height>")
  109. async def get_block_by_height(block_height: int):
  110. if block_height < 0:
  111. abort(404)
  112. current_difficulty = await rpc.call("current_difficulty", params=[])
  113. current_height = await rpc.call("current_height", params=[])
  114. hashrate = await rpc.call("get_hashrate", params=[])
  115. block = await rpc.call("get_block", params=[block_height])
  116. dt = datetime.fromtimestamp(block["timestamp"], tz=timezone.utc)
  117. block["timestamp"] = dt.strftime("%B %d, %Y at %I:%M %p UTC")
  118. block["n_txs"] = len(block["txs"])
  119. return await render_template(
  120. "block.html",
  121. network=app.config["NETWORK"],
  122. current_difficulty=current_difficulty[0],
  123. current_height=current_height,
  124. hashrate=format_hashrate(hashrate),
  125. block=block,
  126. )
  127. @app.route("/tx/<tx_hash>")
  128. async def get_tx_by_hash(tx_hash: str):
  129. # Validate hex string
  130. if not all(c in '0123456789abcdefABCDEF' for c in tx_hash):
  131. abort(404)
  132. current_difficulty = await rpc.call("current_difficulty", params=[])
  133. current_height = await rpc.call("current_height", params=[])
  134. hashrate = await rpc.call("get_hashrate", params=[])
  135. tx = await rpc.call("get_tx", params=[tx_hash])
  136. return await render_template(
  137. "tx.html",
  138. network=app.config["NETWORK"],
  139. current_difficulty=current_difficulty[0],
  140. current_height=current_height,
  141. hashrate=format_hashrate(hashrate),
  142. tx=tx,
  143. )
  144. @app.route("/search")
  145. async def search():
  146. """Search for blocks by height/hash or transactions by hash."""
  147. query = request.args.get("q", "").strip()
  148. if not query:
  149. return redirect(url_for("index"))
  150. # Try to interpret as block height (integer)
  151. if query.isdigit():
  152. return redirect(url_for("get_block_by_height", block_height=int(query)))
  153. # Check if it looks like a hex hash
  154. if all(c in '0123456789abcdefABCDEF' for c in query):
  155. # Use the search RPC to determine if it's a block or tx hash
  156. try:
  157. result = await rpc.call("search", params=[query])
  158. if result["type"] == "block":
  159. return redirect(f"/block/{result['height']}")
  160. elif result["type"] == "tx":
  161. return redirect(url_for("get_tx_by_hash", tx_hash=query))
  162. except JsonRpcError:
  163. pass
  164. # Nothing found
  165. return await render_template(
  166. "error.html",
  167. network=app.config["NETWORK"],
  168. error_code="Not Found",
  169. error=f"No block or transaction found for: {query}"
  170. ), 404
  171. if __name__ == "__main__":
  172. app.run(host="127.0.0.1", port=5000, debug=True)