deg 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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, dag, longest_path):
  29. global resolved
  30. merge = len(list(dag.predecessors(event['hash']))) > 1
  31. fork = len(list(dag.successors(event['hash']))) > 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, dag, longest_path):
  58. g = graph(event, dag, 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. u.emit_signal(self, 'show_details', focus_w.content)
  83. def set_data(self, events, dag, longest_path):
  84. events_widgets = [ListItem(e, dag, longest_path) for e in events]
  85. u.disconnect_signal(self.walker, 'modified', self.modified)
  86. while len(self.walker) > 0:
  87. self.walker.pop()
  88. self.walker.extend(events_widgets)
  89. u.connect_signal(self.walker, "modified", self.modified)
  90. self.walker.set_focus(0)
  91. class DetailView(u.WidgetWrap):
  92. def __init__ (self):
  93. t = u.Text("")
  94. u.WidgetWrap.__init__(self, t)
  95. def set_event(self, c):
  96. s = f'Hash: {c["hash"]}\nChildren: {c["children"]}\nParents: {c["parents"]}\nContent: {c["content"]}\nLayer: {c["layer"]}'
  97. self._w.set_text(s)
  98. class App(object):
  99. def unhandled_input(self, key):
  100. if key in ('q',):
  101. raise u.ExitMainLoop()
  102. # if key == 'r':
  103. # await self.update_data(self.config)
  104. if key == 'enter':
  105. self.current_view = self.frame2
  106. self.loop.widget = self.frame2
  107. if key == 'b':
  108. self.current_view = self.frame1
  109. self.loop.widget = self.frame1
  110. def show_details(self, event):
  111. self.view_two.set_event(event)
  112. def __init__(self):
  113. self.view_one = ListView()
  114. u.connect_signal(self.view_one, 'show_details', self.show_details)
  115. footer = u.AttrWrap(u.Text(" Q to exit"), "footer")
  116. col_rows = u.raw_display.Screen().get_cols_rows()
  117. h = col_rows[0] - 2
  118. f1 = u.Filler(self.view_one, valign='top', height=h)
  119. c_list = u.LineBox(f1, title="Events")
  120. columns = u.Columns([('weight', 100, c_list)])
  121. frame1 = u.AttrMap(u.Frame(body=columns, footer=footer), 'bg')
  122. self.frame1 = frame1
  123. ############
  124. self.view_two = DetailView()
  125. f2 = u.Filler(self.view_two, valign='top')
  126. c_details = u.LineBox(f2, title="Details")
  127. footer = u.AttrWrap(u.Text(" Q to exit, B to main view"), "footer")
  128. columns = u.Columns([('weight', 100, c_details)])
  129. frame2 = u.AttrMap(u.Frame(body=columns, footer=footer), 'bg')
  130. self.frame2 = frame2
  131. ##########
  132. self.current_view = self.frame1 # Start with View One
  133. self.palette = {
  134. ("bg", "white", "black"),
  135. ("event", "white", "black"),
  136. ("event_selected", "white", "light green"),
  137. ('datetime', "light blue", "black"),
  138. ('reporter', "dark green", "black"),
  139. ('content', "", "black"),
  140. ("footer", "white, bold", "dark red")
  141. }
  142. async def update_data(self, config):
  143. self.config = config
  144. dag_dict = await recreate_dag(config)
  145. dag_list = list(dag_dict.items())
  146. # sorted_dag = sorted(dag_list, key=lambda x:x[1]['layer'])
  147. # genesis_hash = sorted_dag[0][0]
  148. parent_child_pairs = []
  149. for item in dag_list:
  150. parents = item[1]['parents']
  151. child = item[0]
  152. for parent in parents:
  153. if parent == '0' * 64:
  154. continue
  155. parent_child_pairs.append((parent, child))
  156. # Create a directed graph
  157. dag = nx.DiGraph()
  158. # Add edges to the graph (this also adds nodes)
  159. dag.add_edges_from(parent_child_pairs)
  160. l = []
  161. topological_order = list(nx.topological_sort(dag))
  162. for node in reversed(topological_order):
  163. event_details = dag_dict.get(node) # details
  164. layer = int(event_details['layer'])
  165. content = event_details['content'] # event content
  166. timestamp = event_details['timestamp']
  167. children = list(dag.successors(node))
  168. parents = list(dag.predecessors(node))
  169. pattern = r'\\x[0-9A-Fa-f]{2}'
  170. decoded_str = str(base58.b58decode(content))
  171. matches = re.sub(pattern, ' ', decoded_str).replace("b'", "")[:-1]
  172. l.append({"layer":f"{layer}", "hash":f"{node}", "children":children, "parents":parents, "content":f"{matches}", "timestamp": f"{timestamp}"})
  173. longest_path = nx.dag_longest_path(dag)
  174. self.view_one.set_data(l, dag, longest_path)
  175. async def start(self, config):
  176. await self.update_data(config)
  177. self.loop = u.MainLoop(self.current_view, self.palette, unhandled_input=self.unhandled_input)
  178. self.loop.run()
  179. async def recreate_dag(config):
  180. host = config['host']
  181. port = config['port']
  182. rpc = src.rpc.JsonRpc()
  183. while True:
  184. try:
  185. await rpc.start(host, port)
  186. break
  187. except OSError:
  188. print(f"Error: Connection Refused to '{host}:{port}', Either because the daemon is down, is currently syncing or wrong url.")
  189. sys.exit(-1)
  190. await rpc.deg_switch(True)
  191. await rpc.deg_switch(False)
  192. json_result = await rpc._make_request("eventgraph.get_info", [])
  193. if json_result['result']['eventgraph_info']:
  194. return json_result['result']['eventgraph_info']['dag']
  195. async def main(argv):
  196. os = src.util.get_os()
  197. config_path = src.util.user_config_dir('darkfi', os)
  198. suffix = '.toml'
  199. filename = 'deg_config'
  200. path = join(config_path, filename + suffix)
  201. config = src.util.spawn_config(path)
  202. config = config['nodes'][0]
  203. if len(argv) > 1:
  204. if argv[1] in ['darkirc', 'irc']:
  205. config['port'] = 26660
  206. elif argv[1] in ['taud', 'tau']:
  207. config['port'] = 23330
  208. app = App()
  209. await app.start(config)
  210. asyncio.run(main(sys.argv))