deg 9.3 KB

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