deg 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. #!/usr/bin/env python
  2. # This file is part of DarkFi (https://dark.fi)
  3. #
  4. # Copyright (C) 2020-2025 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. import asyncio, re, sys, base58
  19. import urwid as u
  20. import networkx as nx
  21. from datetime import datetime
  22. # import matplotlib.pyplot as plt
  23. import src.rpc
  24. from os.path import join
  25. # this is counter-intuitive because the dag is reversed
  26. resolved = [True, True]
  27. def graph(event, longest_path):
  28. global resolved
  29. merge = len(event['parents']) > 1
  30. fork = len(event['children']) > 1
  31. if merge and not fork:
  32. if not resolved[0]:
  33. resolved[1] = False
  34. if resolved[0]:
  35. resolved[0] = False
  36. return "M━┑"
  37. if fork and not merge:
  38. if resolved[1]:
  39. resolved[0] = True
  40. if not resolved[1]:
  41. resolved[1] = True
  42. return "o─┘"
  43. if merge and fork:
  44. return "M━┪"
  45. if not merge and not fork:
  46. if not resolved[0]:
  47. if event['hash'] in longest_path:
  48. return "o │"
  49. else:
  50. return "│ o"
  51. else:
  52. return "o "
  53. # because tab character is broken in urwid texts
  54. def indent(num):
  55. return " " * (8 - len(str(num)))
  56. class ListItem(u.WidgetWrap):
  57. def __init__ (self, event, longest_path):
  58. g = graph(event, longest_path)
  59. self.content = event
  60. layer_num = int(event["layer"])
  61. layer = "layer " + str(layer_num) + indent(layer_num) if layer_num != 0 else "genesis "
  62. dt = datetime.fromtimestamp(int(event['timestamp'])) if int(event['timestamp']) < 1e10 else datetime.fromtimestamp(int(int(event['timestamp'])/1000))
  63. dt = event['hash'][:10] + " │ " + str(dt)
  64. t = u.AttrMap(u.Text([('word', dt),
  65. ('layer-num', " " + layer),
  66. ('word', g),
  67. ('cont', event['content'])], wrap="ellipsis"),
  68. {'word':'datetime', 'layer-num': 'reporter', 'cont': 'content'},
  69. {'word':'event_selected', 'layer-num': 'event_selected', 'cont': 'event_selected'})
  70. u.WidgetWrap.__init__(self, t)
  71. def selectable (self):
  72. return True
  73. def keypress(self, size, key):
  74. return key
  75. class ListView(u.WidgetWrap):
  76. def __init__(self):
  77. u.register_signal(self.__class__, ['show_details'])
  78. self.walker = u.SimpleFocusListWalker([])
  79. lb = u.ListBox(self.walker)
  80. u.WidgetWrap.__init__(self, lb)
  81. def modified(self):
  82. focus_w, _ = self.walker.get_focus()
  83. if focus_w is not None:
  84. u.emit_signal(self, 'show_details', focus_w.content)
  85. else:
  86. print("DAG is empty!")
  87. exit(-1)
  88. def set_data(self, events, longest_path):
  89. events_widgets = [ListItem(e, longest_path) for e in events]
  90. u.disconnect_signal(self.walker, 'modified', self.modified)
  91. while len(self.walker) > 0:
  92. self.walker.pop()
  93. self.walker.extend(events_widgets)
  94. u.connect_signal(self.walker, "modified", self.modified)
  95. self.walker.set_focus(0)
  96. class DetailView(u.WidgetWrap):
  97. def __init__ (self):
  98. t = u.Text("")
  99. u.WidgetWrap.__init__(self, t)
  100. def set_event(self, c):
  101. s = f'Hash: {c["hash"]}\nChildren: {c["children"]}\nParents: {c["parents"]}\nContent: {c["content"]}\nLayer: {c["layer"]}\nTimestamp: {c["timestamp"]}'
  102. self._w.set_text(s)
  103. class App(object):
  104. def unhandled_input(self, key):
  105. if key in ('q', 'Q'):
  106. raise u.ExitMainLoop()
  107. # if key == 'r':
  108. # await self.update_data(self.config)
  109. if key == 'enter':
  110. self.current_view = self.frame2
  111. self.loop.widget = self.frame2
  112. if key in ('b', 'B'):
  113. self.current_view = self.frame1
  114. self.loop.widget = self.frame1
  115. def show_details(self, event):
  116. self.view_two.set_event(event)
  117. def __init__(self):
  118. self.view_one = ListView()
  119. u.connect_signal(self.view_one, 'show_details', self.show_details)
  120. footer = u.AttrWrap(u.Text(" Q to exit"), "footer")
  121. col_rows = u.raw_display.Screen().get_cols_rows()
  122. h = col_rows[0] - 2
  123. f1 = u.Filler(self.view_one, valign='top', height=h)
  124. c_list = u.LineBox(f1, title="Events")
  125. columns = u.Columns([('weight', 100, c_list)])
  126. frame1 = u.AttrMap(u.Frame(body=columns, footer=footer), 'bg')
  127. self.frame1 = frame1
  128. ############
  129. self.view_two = DetailView()
  130. f2 = u.Filler(self.view_two, valign='top')
  131. c_details = u.LineBox(f2, title="Details")
  132. footer = u.AttrWrap(u.Text(" Q to exit, B to main view"), "footer")
  133. columns = u.Columns([('weight', 100, c_details)])
  134. frame2 = u.AttrMap(u.Frame(body=columns, footer=footer), 'bg')
  135. self.frame2 = frame2
  136. ##########
  137. self.current_view = self.frame1 # Start with View One
  138. self.palette = {
  139. ("bg", "white", "black"),
  140. ("event", "white", "black"),
  141. ("event_selected", "white", "light green"),
  142. ('datetime', "light blue", "black"),
  143. ('reporter', "dark green", "black"),
  144. ('content', "", "black"),
  145. ("footer", "white, bold", "dark red")
  146. }
  147. async def update_data(self, config, replay_mode):
  148. self.config = config
  149. dag_dict = await recreate_dag(config, replay_mode)
  150. dag_list = list(dag_dict.items())
  151. parent_child_pairs = []
  152. for item in dag_list:
  153. parents = item[1]['parents']
  154. child = item[0]
  155. for parent in parents:
  156. if parent == '0' * 64:
  157. continue
  158. parent_child_pairs.append((parent, child))
  159. # Create a directed graph
  160. dag = nx.DiGraph()
  161. # Add edges to the graph (this also adds nodes)
  162. dag.add_edges_from(parent_child_pairs)
  163. l = []
  164. topological_order = list(nx.topological_sort(dag))
  165. for node in reversed(topological_order):
  166. event_details = dag_dict.get(node) # details
  167. if event_details is None:
  168. continue
  169. layer = int(event_details['layer'])
  170. content = event_details['content'] # event content
  171. timestamp = event_details['timestamp']
  172. children = list(dag.successors(node))
  173. parents = list(dag.predecessors(node))
  174. pattern = r'\\x[0-9A-Fa-f]{2}'
  175. decoded_str = str(base58.b58decode(content))
  176. y = re.sub(pattern, ' ', decoded_str)
  177. if y.startswith("b'"):
  178. matches = y.replace("b'", "")[:-1]
  179. elif y.startswith('b"'):
  180. matches = y.replace('b"', "")[:-1]
  181. l.append({"layer":f"{layer}", "hash":f"{node}", "children":children, "parents":parents, "content":f"{matches}", "timestamp": f"{timestamp}"})
  182. longest_path = nx.dag_longest_path(dag)
  183. self.view_one.set_data(l, longest_path)
  184. async def start(self, config, replay_mode):
  185. await self.update_data(config, replay_mode)
  186. self.loop = u.MainLoop(self.current_view, self.palette, unhandled_input=self.unhandled_input)
  187. self.loop.run()
  188. async def recreate_dag(config, replay_mode):
  189. host = config['host']
  190. port = config['port']
  191. rpc = src.rpc.JsonRpc()
  192. while True:
  193. try:
  194. await rpc.start(host, port)
  195. break
  196. except OSError:
  197. print(f"Error: Connection Refused to '{host}:{port}', Either because the daemon is down, is currently syncing or wrong url.")
  198. sys.exit(-1)
  199. await rpc.deg_switch(True)
  200. await rpc.deg_switch(False)
  201. if replay_mode:
  202. json_result = await rpc._make_request("eventgraph.replay", [])
  203. else:
  204. json_result = await rpc._make_request("eventgraph.get_info", [])
  205. if json_result['result']['eventgraph_info']:
  206. return json_result['result']['eventgraph_info']['dag']
  207. async def main(argv):
  208. val = str('127.0.0.1:26660')
  209. for i in range(1, len(sys.argv)):
  210. if sys.argv[i] == "-e":
  211. try:
  212. val = sys.argv[i+1]
  213. except IndexError:
  214. print("Please provide a value for \'-e\'")
  215. exit(-1)
  216. break
  217. config = {}
  218. try:
  219. config['host'], config['port'] = val.split(':')
  220. except ValueError:
  221. print("Please provide a port as in: 127.0.0.1:26660")
  222. exit(-1)
  223. replay_mode = False
  224. if len(argv) > 1:
  225. if '-r' in argv:
  226. replay_mode = True
  227. if set(argv) & set(['darkirc', 'irc']):
  228. config['port'] = 26660
  229. elif set(argv) & set(['taud', 'tau']):
  230. config['port'] = 23330
  231. app = App()
  232. await app.start(config, replay_mode)
  233. asyncio.run(main(sys.argv))