deg 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  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. 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 = 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', '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 in ('b', '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. parent_child_pairs = []
  151. for item in dag_list:
  152. parents = item[1]['parents']
  153. child = item[0]
  154. for parent in parents:
  155. if parent == '0' * 64:
  156. continue
  157. parent_child_pairs.append((parent, child))
  158. # Create a directed graph
  159. dag = nx.DiGraph()
  160. # Add edges to the graph (this also adds nodes)
  161. dag.add_edges_from(parent_child_pairs)
  162. l = []
  163. topological_order = list(nx.topological_sort(dag))
  164. for node in reversed(topological_order):
  165. event_details = dag_dict.get(node) # details
  166. if event_details is None:
  167. continue
  168. layer = int(event_details['layer'])
  169. content = event_details['content'] # event content
  170. timestamp = event_details['timestamp']
  171. children = list(dag.successors(node))
  172. parents = list(dag.predecessors(node))
  173. pattern = r'\\x[0-9A-Fa-f]{2}'
  174. decoded_str = str(base58.b58decode(content))
  175. matches = re.sub(pattern, ' ', decoded_str).replace("b'", "")[:-1]
  176. l.append({"layer":f"{layer}", "hash":f"{node}", "children":children, "parents":parents, "content":f"{matches}", "timestamp": f"{timestamp}"})
  177. longest_path = nx.dag_longest_path(dag)
  178. self.view_one.set_data(l, longest_path)
  179. async def start(self, config, replay_mode):
  180. await self.update_data(config, replay_mode)
  181. self.loop = u.MainLoop(self.current_view, self.palette, unhandled_input=self.unhandled_input)
  182. self.loop.run()
  183. async def recreate_dag(config, replay_mode):
  184. host = config['host']
  185. port = config['port']
  186. rpc = src.rpc.JsonRpc()
  187. while True:
  188. try:
  189. await rpc.start(host, port)
  190. break
  191. except OSError:
  192. print(f"Error: Connection Refused to '{host}:{port}', Either because the daemon is down, is currently syncing or wrong url.")
  193. sys.exit(-1)
  194. await rpc.deg_switch(True)
  195. await rpc.deg_switch(False)
  196. if replay_mode:
  197. json_result = await rpc._make_request("eventgraph.replay", [])
  198. else:
  199. json_result = await rpc._make_request("eventgraph.get_info", [])
  200. if json_result['result']['eventgraph_info']:
  201. return json_result['result']['eventgraph_info']['dag']
  202. async def main(argv):
  203. val = str('127.0.0.1:26660')
  204. for i in range(1, len(sys.argv)):
  205. if sys.argv[i] == "-e":
  206. try:
  207. val = sys.argv[i+1]
  208. except IndexError:
  209. print("Please provide a value for \'-e\'")
  210. exit(-1)
  211. break
  212. config = {}
  213. try:
  214. config['host'], config['port'] = val.split(':')
  215. except ValueError:
  216. print("Please provide a port as in: 127.0.0.1:26660")
  217. exit(-1)
  218. replay_mode = False
  219. if len(argv) > 1:
  220. if '-r' in argv:
  221. replay_mode = True
  222. if set(argv) & set(['darkirc', 'irc']):
  223. config['port'] = 26660
  224. elif set(argv) & set(['taud', 'tau']):
  225. config['port'] = 23330
  226. app = App()
  227. await app.start(config, replay_mode)
  228. asyncio.run(main(sys.argv))