Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

_gui.py 39KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058
  1. ###############################################################################
  2. #
  3. # The MIT License (MIT)
  4. #
  5. # Copyright (c) typedef int GmbH
  6. #
  7. # Permission is hereby granted, free of charge, to any person obtaining a copy
  8. # of this software and associated documentation files (the "Software"), to deal
  9. # in the Software without restriction, including without limitation the rights
  10. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. # copies of the Software, and to permit persons to whom the Software is
  12. # furnished to do so, subject to the following conditions:
  13. #
  14. # The above copyright notice and this permission notice shall be included in
  15. # all copies or substantial portions of the Software.
  16. #
  17. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. # THE SOFTWARE.
  24. #
  25. ###############################################################################
  26. import os
  27. import argparse
  28. import uuid
  29. import binascii
  30. import random
  31. import pkg_resources
  32. from pprint import pprint
  33. from time import time_ns
  34. import gi
  35. gi.require_version("Gtk", "3.0")
  36. from gi.repository import Gtk
  37. from gi.repository.Gdk import Color
  38. from twisted.internet import gtk3reactor
  39. gtk3reactor.install()
  40. import txaio
  41. txaio.use_twisted()
  42. from twisted.internet.task import react
  43. from twisted.internet.defer import inlineCallbacks
  44. from twisted.internet import reactor
  45. import web3
  46. import numpy as np
  47. import click
  48. from humanize import naturaldelta, naturaltime
  49. from autobahn.util import parse_activation_code, hltype, hlid, hlval
  50. from autobahn.wamp.serializer import CBORSerializer
  51. from autobahn.twisted.util import sleep
  52. from autobahn.twisted.wamp import ApplicationRunner
  53. from autobahn.xbr import unpack_uint256
  54. from autobahn.xbr import account_from_seedphrase, generate_seedphrase, account_from_ethkey
  55. from autobahn.xbr._cli import Client
  56. from autobahn.xbr._config import UserConfig, Profile
  57. LOGO_RESOURCE = pkg_resources.resource_filename('autobahn', 'asset/xbr_gray.svg')
  58. print(LOGO_RESOURCE, os.path.isfile(LOGO_RESOURCE))
  59. class ApplicationWindow(Gtk.Assistant):
  60. """
  61. Main application window which provides UI for the following functions:
  62. * N) New account
  63. * R) Recover account:
  64. - R1) Backup cloud device in account enabled => download encrypted account data
  65. from cloud backup device, requires email (and 2FA) verification and password
  66. - R2) At least one device left in account and at hand => synchronize with existing device,
  67. direct device-to-device encrypted account data transfer
  68. - R3) Only cold storage recovery seed phrase left => account from seed-phrase full
  69. recovery, including new email and 2FA verification.
  70. See also:
  71. * https://python-gtk-3-tutorial.readthedocs.io/en/latest/
  72. * https://twistedmatrix.com/documents/current/core/howto/choosing-reactor.html
  73. """
  74. log = txaio.make_logger()
  75. SELECTED_NONE = 0
  76. SELECTED_NEW = 1
  77. SELECTED_SYNCRONIZE = 2
  78. SELECTED_RECOVER = 3
  79. def __init__(self, reactor, session, config, config_path, profile, profile_name):
  80. Gtk.Assistant.__init__(self)
  81. self.reactor = reactor
  82. self.session = session
  83. self.config = config
  84. self.config_path = config_path
  85. self.profile = profile
  86. self.profile_name = profile_name
  87. self.input_seedphrase = None
  88. self.input_email = None
  89. self.input_password = None
  90. self.output_account = None
  91. self.output_ethadr = None
  92. self.output_ethadr_raw = None
  93. self.output_member_data = None
  94. self.output_member_data_oid = uuid.UUID(bytes=b'\x00' * 16)
  95. # configure assistant window/widget
  96. self.set_title("XBR Network")
  97. self.set_default_size(600, 600)
  98. self.set_border_width(50)
  99. self.set_resizable(False)
  100. # setup assistant pages
  101. self._setup_page1()
  102. self._setup_page2()
  103. self._setup_page3()
  104. self._setup_page4()
  105. self._setup_page5()
  106. @inlineCallbacks
  107. def start(self):
  108. # start page depends on available user profile
  109. if self.profile:
  110. self.output_account = account_from_ethkey(self.profile.ethkey)
  111. self.output_ethadr = web3.Web3.toChecksumAddress(self.output_account.address)
  112. self.output_ethadr_raw = binascii.a2b_hex(self.output_ethadr[2:])
  113. info = yield self.session.get_status()
  114. if info:
  115. now = str(np.datetime64(np.datetime64(info['status']['now'], 'ns'), 's'))
  116. self._label5_now.set_label(now)
  117. self._label5_chain.set_label(str(info['status']['chain']))
  118. self._label5_status.set_label(str(info['status']['status']))
  119. self._label5_xbrnetwork.set_label(str(info['config']['contracts']['xbrnetwork']))
  120. self._label5_xbrtoken.set_label(str(info['config']['contracts']['xbrtoken']))
  121. self._label5_blockhash.set_label('0x' + binascii.b2a_hex(info['status']['block']['hash']).decode())
  122. self._label5_blocknumber.set_label(str(info['status']['block']['number']))
  123. pprint(info)
  124. member_data = yield self.session.get_member(self.output_ethadr_raw)
  125. if not member_data:
  126. self.log.info('ethadr {output_ethadr} is NOT yet a member',
  127. output_ethadr=self.output_ethadr)
  128. if self.profile.vaction_oid:
  129. # switch to page "_setup_page4"
  130. self.set_current_page(3)
  131. else:
  132. if self.profile.ethkey:
  133. # switch to page "_setup_page3"
  134. self.set_current_page(2)
  135. else:
  136. # switch to page "_setup_page2"
  137. self.set_current_page(1)
  138. else:
  139. self.log.info('ok, ethadr {output_ethadr} already is a member: {member_data}',
  140. output_ethadr=self.output_ethadr, member_data=member_data)
  141. self.output_member_data = member_data
  142. created_ago = naturaldelta((np.datetime64(time_ns(), 'ns') - self.output_member_data['created']) / 1000000000.)
  143. created = naturaltime(np.datetime64(self.output_member_data['created'], 's'))
  144. self._label2.set_label(str(self.output_member_data['oid']))
  145. self._label4.set_label(str(self.output_member_data['address']))
  146. self._label6.set_label('{} ({} ago)'.format(created, created_ago))
  147. self._label8.set_label(str(self.output_member_data['level']))
  148. self._label10.set_label(str(self.output_member_data['balance']['eth']))
  149. self._label12.set_label(str(self.output_member_data['balance']['xbr']))
  150. # switch to page "_setup_page5"
  151. self.set_current_page(4)
  152. else:
  153. profile = Profile()
  154. profile.path = self.config_path
  155. profile.ethkey = None
  156. profile.cskey = None
  157. profile.username = None
  158. profile.email = None
  159. profile.network_url = 'ws://localhost:8090/ws'
  160. profile.network_realm = 'xbrnetwork'
  161. profile.member_oid = None
  162. profile.vaction_oid = None
  163. profile.vaction_requested = None
  164. profile.vaction_verified = None
  165. profile.data_url = None
  166. profile.data_realm = None
  167. profile.infura_url = None
  168. profile.infura_network = None
  169. profile.infura_key = None
  170. profile.infura_secret = None
  171. self.profile = profile
  172. # switch to page "_setup_page1"
  173. self.set_current_page(0)
  174. def on_complete_toggled(self, checkbutton):
  175. self.set_page_complete(self.complete, checkbutton.get_active())
  176. # no config: select new/recovery
  177. def _setup_page1(self):
  178. """
  179. Setup page shown when no config/profile could be found. Allows to select from:
  180. * new account
  181. * synchronize device
  182. * recover account
  183. """
  184. grid1 = Gtk.Grid()
  185. grid1.set_row_spacing(20)
  186. grid1.set_column_spacing(20)
  187. grid1.set_margin_top(20)
  188. grid1.set_margin_bottom(20)
  189. grid1.set_margin_start(20)
  190. grid1.set_margin_end(20)
  191. image1 = Gtk.Image()
  192. image1.set_from_file(LOGO_RESOURCE)
  193. grid1.attach(image1, 0, 0, 2, 1)
  194. label0 = Gtk.Label(label='\n\nI am new and do not have an account yet:\n')
  195. label0.set_alignment(0, 0.5)
  196. grid1.attach(label0, 0, 1, 2, 1)
  197. label1 = Gtk.Label(label='Create a new account or start from fresh. You only need an email address. [N]')
  198. label1.set_alignment(0, 0.5)
  199. label1.set_justify(Gtk.Justification.LEFT)
  200. grid1.attach(label1, 1, 2, 1, 1)
  201. button1 = Gtk.Button.new_with_label('New account')
  202. def on_button1(res):
  203. self.log.info('SELECTED_NEW: {res}', res=res)
  204. self.set_current_page(1)
  205. button1.connect('clicked', on_button1)
  206. grid1.attach(button1, 0, 2, 1, 1)
  207. label12 = Gtk.Label(label='\n\nI already have an existing account and want to use that:\n')
  208. label12.set_alignment(0, 0.5)
  209. grid1.attach(label12, 0, 3, 2, 1)
  210. label22 = Gtk.Label(label='Restore account from cloud backup to this device. You will need access to\n'
  211. 'your account password and access to your account email address. [R1]')
  212. label22.set_alignment(0, 0.5)
  213. label22.set_justify(Gtk.Justification.LEFT)
  214. label22.set_line_wrap(True)
  215. label22.set_width_chars(12)
  216. grid1.attach(label22, 1, 4, 1, 1)
  217. button22 = Gtk.Button.new_with_label('Restore account')
  218. def on_button22(res):
  219. self.log.info('SELECTED_RESTORE: {res}', res=res)
  220. self.set_current_page(2)
  221. button22.connect('clicked', on_button22)
  222. grid1.attach(button22, 0, 4, 1, 1)
  223. label2 = Gtk.Label(label='Synchronize device with other device in account. You will need access to\n'
  224. 'another device currently connected to your account. [R2]')
  225. label2.set_alignment(0, 0.5)
  226. label2.set_justify(Gtk.Justification.LEFT)
  227. label2.set_line_wrap(True)
  228. label2.set_width_chars(12)
  229. grid1.attach(label2, 1, 5, 1, 1)
  230. button2 = Gtk.Button.new_with_label('Synchronize account')
  231. def on_button2(res):
  232. self.log.info('SELECTED_SYNCRONIZE: {res}', res=res)
  233. self.set_current_page(2)
  234. button2.connect('clicked', on_button2)
  235. grid1.attach(button2, 0, 5, 1, 1)
  236. label3 = Gtk.Label(label='Recover account from account seed phrase. You only need access to\n'
  237. 'your 12-24 word account recovery seed phrase. [R3]')
  238. label3.set_alignment(0, 0.5)
  239. label3.set_justify(Gtk.Justification.LEFT)
  240. label3.set_line_wrap(True)
  241. label3.set_width_chars(12)
  242. grid1.attach(label3, 1, 6, 1, 1)
  243. button3 = Gtk.Button.new_with_label('Recover account')
  244. def on_button3(res):
  245. self.log.info('SELECTED_RECOVER: {res}', res=res)
  246. self.set_current_page(3)
  247. button3.connect('clicked', on_button3)
  248. grid1.attach(button3, 0, 6, 1, 1)
  249. self.append_page(grid1)
  250. # generate seed phrase
  251. def _setup_page2(self):
  252. """
  253. Setup page shown to generate a new seed phrase.
  254. """
  255. box2_1 = Gtk.VBox()
  256. box2_2 = Gtk.HBox()
  257. image2_1 = Gtk.Image()
  258. image2_1.set_from_file(LOGO_RESOURCE)
  259. box2_2.add(image2_1)
  260. box2_1.add(box2_2)
  261. button2_1 = Gtk.Button.new_with_label('Generate seedphrase')
  262. def on_button2_1(_):
  263. self.input_seedphrase = generate_seedphrase(strength=256, language='english')
  264. textbuffer2_1.set_text(self.input_seedphrase)
  265. checkbutton2_1.set_sensitive(True)
  266. button2_1.connect('clicked', on_button2_1)
  267. box2_1.add(button2_1)
  268. label2_1 = Gtk.Label(label='Backup your new seed phrase in a secure offline location (e.g. on printed paper):')
  269. label2_1.set_alignment(0, 0.5)
  270. label2_1.set_justify(Gtk.Justification.LEFT)
  271. box2_1.add(label2_1)
  272. textview2_1 = Gtk.TextView()
  273. textbuffer2_1 = textview2_1.get_buffer()
  274. textbuffer2_1.set_text('\n' * 5)
  275. textview2_1.set_editable(False)
  276. textview2_1.set_justification(Gtk.Justification.CENTER)
  277. textview2_1.set_monospace(True)
  278. textview2_1.set_wrap_mode(Gtk.WrapMode.WORD)
  279. box2_1.add(textview2_1)
  280. box2_3 = Gtk.HBox()
  281. checkbutton2_1 = Gtk.CheckButton(label="I have backed up my seed phrase")
  282. checkbutton2_1.set_active(False)
  283. checkbutton2_1.set_sensitive(False)
  284. def on_checkbutton2_1(_):
  285. button2_2.set_sensitive(True)
  286. checkbutton2_1.connect("toggled", on_checkbutton2_1)
  287. box2_3.add(checkbutton2_1)
  288. button2_2 = Gtk.Button.new_with_label('Continue')
  289. button2_2.set_sensitive(False)
  290. @inlineCallbacks
  291. def on_button2_2(_):
  292. self.output_account = account_from_seedphrase(self.input_seedphrase, index=0)
  293. self.output_ethadr = web3.Web3.toChecksumAddress(self.output_account.address)
  294. self.output_ethadr_raw = binascii.a2b_hex(self.output_ethadr[2:])
  295. # https://eth-account.readthedocs.io/en/latest/eth_account.signers.html#eth_account.signers.local.LocalAccount.key
  296. self.profile.ethkey = bytes(self.output_account.key)
  297. self.profile.cskey = bytes(self.session._cskey_raw)
  298. # set user eth key on client session
  299. self.session.set_ethkey_from_profile(self.profile)
  300. # save user config
  301. self.config.profiles[self.profile_name] = self.profile
  302. self.config.save(self.input_password)
  303. self.log.info('XBR ETH key at address {ethadr} set from seed phrase (BIP39 account 0): "{seedphrase}"',
  304. ethadr=self.output_ethadr,
  305. seedphrase=self.input_seedphrase)
  306. member_data = yield self.session.get_member(self.output_ethadr_raw)
  307. pprint(member_data)
  308. if not member_data:
  309. self.log.info('ethadr {output_ethadr} is NOT yet a member',
  310. output_ethadr=self.output_ethadr)
  311. self.set_current_page(2)
  312. else:
  313. self.log.info('ok, ethadr {output_ethadr} already is a member: {member_data}',
  314. output_ethadr=self.output_ethadr, member_data=member_data)
  315. self.profile.member_oid = uuid.UUID(bytes=member_data['member_oid'])
  316. self.profile.member_adr = self.output_ethadr
  317. self.profile.email = member_data['email']
  318. self.profile.username = member_data['username']
  319. # save user config
  320. self.config.profiles[self.profile_name] = self.profile
  321. self.config.save(self.input_password)
  322. self.output_member_data = member_data
  323. self._label2.set_label(str(self._member_data['oid']))
  324. self.set_current_page(4)
  325. def run_on_button2_2(widget):
  326. self.log.info('{func}({widget})', func=hltype(run_on_button2_2), widget=widget)
  327. reactor.callLater(0, on_button2_2, widget)
  328. button2_2.connect('clicked', run_on_button2_2)
  329. box2_3.add(button2_2)
  330. box2_1.add(box2_3)
  331. self.append_page(box2_1)
  332. # submit onboard request
  333. def _setup_page3(self):
  334. """
  335. :return:
  336. """
  337. box1 = Gtk.VBox()
  338. box2 = Gtk.HBox()
  339. image1 = Gtk.Image()
  340. image1.set_from_file(LOGO_RESOURCE)
  341. box2.add(image1)
  342. box1.add(box2)
  343. grid1 = Gtk.Grid()
  344. grid1.set_row_spacing(20)
  345. grid1.set_column_spacing(20)
  346. grid1.set_margin_top(20)
  347. grid1.set_margin_bottom(20)
  348. grid1.set_margin_start(20)
  349. grid1.set_margin_end(20)
  350. label1 = Gtk.Label(label='Your email address:')
  351. grid1.attach(label1, 0, 0, 1, 1)
  352. entry1 = Gtk.Entry()
  353. entry1.set_text('')
  354. entry1.set_max_length(255)
  355. entry1.set_max_width_chars(40)
  356. grid1.attach(entry1, 1, 0, 1, 1)
  357. checks = {
  358. 'email': None,
  359. 'password': None,
  360. 'eula': None,
  361. }
  362. def check_all():
  363. print('check_all')
  364. for c in checks:
  365. if checks[c] is None:
  366. print('check failed', c)
  367. button3.set_sensitive(False)
  368. return
  369. button3.set_sensitive(True)
  370. def check_email(email):
  371. if '@' in email:
  372. return email
  373. else:
  374. return None
  375. def check_password(password):
  376. return True
  377. def on_entry1(entry):
  378. # joe.doe@example.com
  379. checks['email'] = check_email(entry.get_text())
  380. if checks['email']:
  381. entry1.modify_fg(Gtk.StateFlags.NORMAL, None)
  382. else:
  383. entry1.modify_fg(Gtk.StateFlags.NORMAL, Color(50000, 0, 0))
  384. check_all()
  385. entry1.connect('changed', on_entry1)
  386. label2 = Gtk.Label(label='New password:')
  387. grid1.attach(label2, 0, 1, 1, 1)
  388. entry2 = Gtk.Entry()
  389. entry2.set_text('')
  390. entry2.set_max_length(20)
  391. entry2.set_max_width_chars(20)
  392. entry2.set_visibility(False)
  393. grid1.attach(entry2, 1, 1, 1, 1)
  394. label2 = Gtk.Label(label='Repeat new password:')
  395. grid1.attach(label2, 0, 2, 1, 1)
  396. entry3 = Gtk.Entry()
  397. entry3.set_text('')
  398. entry3.set_max_length(20)
  399. entry3.set_max_width_chars(20)
  400. entry3.set_visibility(False)
  401. grid1.attach(entry3, 1, 2, 1, 1)
  402. def on_entry23(_):
  403. pw1_ok = False
  404. if check_password(entry2.get_text()):
  405. pw1_ok = True
  406. entry2.modify_fg(Gtk.StateFlags.NORMAL, None)
  407. else:
  408. entry2.modify_fg(Gtk.StateFlags.NORMAL, Color(50000, 0, 0))
  409. pw2_ok = False
  410. if check_password(entry3.get_text()):
  411. pw2_ok = True
  412. entry3.modify_fg(Gtk.StateFlags.NORMAL, None)
  413. else:
  414. entry3.modify_fg(Gtk.StateFlags.NORMAL, Color(50000, 0, 0))
  415. if pw1_ok and pw2_ok and entry2.get_text() == entry3.get_text() and check_password(entry2.get_text()):
  416. checks['password'] = entry2.get_text()
  417. else:
  418. checks['password'] = None
  419. check_all()
  420. entry2.connect('changed', on_entry23)
  421. entry3.connect('changed', on_entry23)
  422. label3 = Gtk.Label(label='EULA:')
  423. grid1.attach(label3, 0, 3, 1, 1)
  424. button1 = Gtk.CheckButton(label='I accept the EULA and terms of use')
  425. button1.set_active(False)
  426. button1.set_sensitive(True)
  427. def on_button1(button):
  428. if button.get_active():
  429. checks['eula'] = True
  430. else:
  431. checks['eula'] = False
  432. check_all()
  433. button1.connect('toggled', on_button1)
  434. grid1.attach(button1, 1, 3, 1, 1)
  435. label3 = Gtk.Label(label='Cloud backup:')
  436. grid1.attach(label3, 0, 4, 1, 1)
  437. button2 = Gtk.CheckButton(label='Yes, enable encrypted cloud backup of my private keys')
  438. button2.set_active(False)
  439. button2.set_sensitive(True)
  440. def on_button2(button):
  441. check_all()
  442. button2.connect('toggled', on_button2)
  443. grid1.attach(button2, 1, 4, 1, 1)
  444. button3 = Gtk.Button.new_with_label('Register account')
  445. button3.set_sensitive(False)
  446. @inlineCallbacks
  447. def on_button3(_):
  448. self.input_email = checks['email']
  449. self.input_password = checks['password']
  450. self.input_backup_enabled = button2.get_active()
  451. self.input_username = 'anonymous'
  452. self.input_username = '{}{}'.format(self.input_username, random.randint(0, 10000))
  453. self.session.set_ethkey_from_profile(self.profile)
  454. self.log.info('input_email: {input_email}', input_email=self.input_email)
  455. self.log.info('input_username: {input_username}', input_username=self.input_username)
  456. self.log.info('input_password: {input_password}', input_password=self.input_password)
  457. result = yield self.session._do_onboard_member(self.input_username, self.input_email)
  458. pprint(result)
  459. self.profile.email = self.input_email
  460. self.profile.username = self.input_username
  461. self.profile.vaction_oid = str(uuid.UUID(bytes=result['vaction_oid']))
  462. self.profile.vaction_requested = str(np.datetime64(result['timestamp'], 'ns'))
  463. self.config.profiles[self.profile_name] = self.profile
  464. self.config.save(self.input_password)
  465. self.set_current_page(3)
  466. def run_on_button3(widget):
  467. self.log.info('{func}({widget})', func=hltype(run_on_button3), widget=widget)
  468. reactor.callLater(0, on_button3, widget)
  469. button3.connect('clicked', run_on_button3)
  470. grid1.attach(button3, 2, 4, 1, 1)
  471. box1.add(grid1)
  472. self.append_page(box1)
  473. # submit verification code
  474. def _setup_page4(self):
  475. """
  476. Page shown when member registration request was submitted, a verification email
  477. sent, and the verification request ID returned.
  478. The user now should check the email inbox for the received verification code,
  479. and continue verifying the code.
  480. :return:
  481. """
  482. box1 = Gtk.VBox()
  483. box2 = Gtk.HBox()
  484. image1 = Gtk.Image()
  485. image1.set_from_file(LOGO_RESOURCE)
  486. box2.add(image1)
  487. box1.add(box2)
  488. box3 = Gtk.HBox()
  489. label1 = Gtk.Label(label='Member registration submitted, verification request:')
  490. label2 = Gtk.Label(label='8d5d7ffd-23d9-45a0-a686-00a49f29d3cd')
  491. box3.add(label1)
  492. box3.add(label2)
  493. box1.add(box3)
  494. label3 = Gtk.Label(label='Please check your email inbox, and enter the verification code received here:')
  495. box1.add(label3)
  496. entry1 = Gtk.Entry()
  497. entry1.set_text('')
  498. entry1.set_max_length(255)
  499. entry1.set_max_width_chars(40)
  500. box1.add(entry1)
  501. def on_entry1(entry):
  502. # "RWCN-94NV-CEHR" -> ("RWCN", "94NV", "CEHR") | None
  503. vaction_code = parse_activation_code(entry.get_text())
  504. if vaction_code:
  505. entry1.modify_fg(Gtk.StateFlags.NORMAL, None)
  506. button1.set_sensitive(True)
  507. else:
  508. entry1.modify_fg(Gtk.StateFlags.NORMAL, Color(50000, 0, 0))
  509. button1.set_sensitive(False)
  510. entry1.connect('changed', on_entry1)
  511. button1 = Gtk.Button.new_with_label('Verify')
  512. button1.set_sensitive(False)
  513. @inlineCallbacks
  514. def on_button1(_):
  515. vaction_code = parse_activation_code(entry1.get_text())
  516. if vaction_code:
  517. vaction_code = '-'.join(vaction_code.groups())
  518. if type(self.profile.vaction_oid) == str:
  519. vaction_oid = uuid.UUID(self.profile.vaction_oid)
  520. else:
  521. vaction_oid = self.profile.vaction_oid
  522. result = yield self.session._do_onboard_member_verify(vaction_oid, vaction_code)
  523. pprint(result)
  524. self.profile.vaction_verified = str(np.datetime64(result['created'], 'ns'))
  525. self.profile.vaction_transaction = '0x' + str(binascii.b2a_hex(result['transaction']).decode())
  526. self.profile.member_oid = str(uuid.UUID(bytes=result['member_oid']))
  527. self.config.profiles[self.profile_name] = self.profile
  528. self.config.save(self.input_password)
  529. def run_on_button1(widget):
  530. self.log.info('{func}({widget})', func=hltype(run_on_button1), widget=widget)
  531. reactor.callLater(0, on_button1, widget)
  532. button1.connect('clicked', run_on_button1)
  533. box1.add(button1)
  534. self.append_page(box1)
  535. # show member data
  536. def _setup_page5(self):
  537. """
  538. Page shown for a user (private eth key) that already is member.
  539. :return:
  540. """
  541. box1 = Gtk.VBox()
  542. box2 = Gtk.HBox()
  543. image1 = Gtk.Image()
  544. image1.set_from_file(LOGO_RESOURCE)
  545. box2.add(image1)
  546. box1.add(box2)
  547. grid2 = Gtk.Grid()
  548. grid2.set_row_spacing(20)
  549. grid2.set_column_spacing(20)
  550. grid2.set_margin_top(20)
  551. grid2.set_margin_bottom(20)
  552. grid2.set_margin_start(20)
  553. grid2.set_margin_end(20)
  554. grid2_y = 0
  555. # Current server time
  556. #
  557. label5_now_title = Gtk.Label(label='Current server time:')
  558. label5_now_title.set_alignment(1, 0.5)
  559. grid2.attach(label5_now_title, 0, grid2_y, 1, 1)
  560. self._label5_now = Gtk.Label()
  561. self._label5_now.set_alignment(0, 0.5)
  562. self._label5_now.set_selectable(False)
  563. grid2.attach(self._label5_now, 1, grid2_y, 1, 1)
  564. grid2_y += 1
  565. # Blockchain ID (e.g. 1, 3 or 5777)
  566. #
  567. label5_chain_title = Gtk.Label(label='Blockchain ID:')
  568. label5_chain_title.set_alignment(1, 0.5)
  569. grid2.attach(label5_chain_title, 0, grid2_y, 1, 1)
  570. self._label5_chain = Gtk.Label()
  571. self._label5_chain.set_alignment(0, 0.5)
  572. self._label5_chain.set_selectable(True)
  573. grid2.attach(self._label5_chain, 1, grid2_y, 1, 1)
  574. grid2_y += 1
  575. # Current server time
  576. #
  577. label5_status_title = Gtk.Label(label='Service status:')
  578. label5_status_title.set_alignment(1, 0.5)
  579. grid2.attach(label5_status_title, 0, grid2_y, 1, 1)
  580. self._label5_status = Gtk.Label()
  581. self._label5_status.set_alignment(0, 0.5)
  582. self._label5_status.set_selectable(False)
  583. grid2.attach(self._label5_status, 1, grid2_y, 1, 1)
  584. grid2_y += 1
  585. # xbrnetwork address
  586. #
  587. label5_xbrnetwork_title = Gtk.Label(label='XBRNetwork contract:')
  588. label5_xbrnetwork_title.set_alignment(1, 0.5)
  589. grid2.attach(label5_xbrnetwork_title, 0, grid2_y, 1, 1)
  590. self._label5_xbrnetwork = Gtk.Label()
  591. self._label5_xbrnetwork.set_alignment(0, 0.5)
  592. self._label5_xbrnetwork.set_selectable(True)
  593. grid2.attach(self._label5_xbrnetwork, 1, grid2_y, 1, 1)
  594. grid2_y += 1
  595. # xbrtoken address
  596. #
  597. label5_xbrtoken_title = Gtk.Label(label='XBRToken contract:')
  598. label5_xbrtoken_title.set_alignment(1, 0.5)
  599. grid2.attach(label5_xbrtoken_title, 0, grid2_y, 1, 1)
  600. self._label5_xbrtoken = Gtk.Label()
  601. self._label5_xbrtoken.set_alignment(0, 0.5)
  602. self._label5_xbrtoken.set_selectable(True)
  603. grid2.attach(self._label5_xbrtoken, 1, grid2_y, 1, 1)
  604. grid2_y += 1
  605. # Current block hash
  606. #
  607. label5_blockhash_title = Gtk.Label(label='Current block hash:')
  608. label5_blockhash_title.set_alignment(1, 0.5)
  609. grid2.attach(label5_blockhash_title, 0, grid2_y, 1, 1)
  610. self._label5_blockhash = Gtk.Label()
  611. self._label5_blockhash.set_alignment(0, 0.5)
  612. self._label5_blockhash.set_selectable(True)
  613. grid2.attach(self._label5_blockhash, 1, grid2_y, 1, 1)
  614. grid2_y += 1
  615. # Current block number
  616. #
  617. label5_blocknumber_title = Gtk.Label(label='Current block number:')
  618. label5_blocknumber_title.set_alignment(1, 0.5)
  619. grid2.attach(label5_blocknumber_title, 0, grid2_y, 1, 1)
  620. self._label5_blocknumber = Gtk.Label()
  621. self._label5_blocknumber.set_alignment(0, 0.5)
  622. self._label5_blocknumber.set_selectable(True)
  623. grid2.attach(self._label5_blocknumber, 1, grid2_y, 1, 1)
  624. grid2_y += 1
  625. box1.add(grid2)
  626. grid1 = Gtk.Grid()
  627. grid1.set_row_spacing(20)
  628. grid1.set_column_spacing(20)
  629. grid1.set_margin_top(20)
  630. grid1.set_margin_bottom(20)
  631. grid1.set_margin_start(20)
  632. grid1.set_margin_end(20)
  633. label1 = Gtk.Label(label='User ID:')
  634. label1.set_alignment(1, 0.5)
  635. grid1.attach(label1, 0, 0, 1, 1)
  636. self._label2 = Gtk.Label()
  637. self._label2.set_alignment(0, 0.5)
  638. self._label2.set_selectable(True)
  639. grid1.attach(self._label2, 1, 0, 1, 1)
  640. label3 = Gtk.Label(label='Eth Address:')
  641. label3.set_alignment(1, 0.5)
  642. grid1.attach(label3, 0, 1, 1, 1)
  643. self._label4 = Gtk.Label()
  644. self._label4.set_alignment(0, 0.5)
  645. self._label4.set_selectable(True)
  646. grid1.attach(self._label4, 1, 1, 1, 1)
  647. label5 = Gtk.Label(label='Account Created:')
  648. label5.set_alignment(1, 0.5)
  649. grid1.attach(label5, 0, 2, 1, 1)
  650. self._label6 = Gtk.Label()
  651. self._label6.set_alignment(0, 0.5)
  652. grid1.attach(self._label6, 1, 2, 1, 1)
  653. label7 = Gtk.Label(label='Membership:')
  654. label7.set_alignment(1, 0.5)
  655. grid1.attach(label7, 0, 3, 1, 1)
  656. self._label8 = Gtk.Label()
  657. self._label8.set_alignment(0, 0.5)
  658. grid1.attach(self._label8, 1, 3, 1, 1)
  659. label9 = Gtk.Label(label='ETH Balance:')
  660. label9.set_alignment(1, 0.5)
  661. grid1.attach(label9, 0, 4, 1, 1)
  662. self._label10 = Gtk.Label()
  663. self._label10.set_alignment(0, 0.5)
  664. grid1.attach(self._label10, 1, 4, 1, 1)
  665. label11 = Gtk.Label(label='XBR Balance:')
  666. label11.set_alignment(1, 0.5)
  667. grid1.attach(label11, 0, 5, 1, 1)
  668. self._label12 = Gtk.Label()
  669. self._label12.set_alignment(0, 0.5)
  670. grid1.attach(self._label12, 1, 5, 1, 1)
  671. box1.add(grid1)
  672. self.append_page(box1)
  673. class ApplicationClient(Client):
  674. async def onJoin(self, details):
  675. self.log.info('Ok, client joined on realm "{realm}" [session={session}, authid="{authid}", authrole="{authrole}"]',
  676. realm=hlid(details.realm),
  677. session=hlid(details.session),
  678. authid=hlid(details.authid),
  679. authrole=hlid(details.authrole),
  680. details=details)
  681. if 'ready' in self.config.extra:
  682. txaio.resolve(self.config.extra['ready'], (self, details))
  683. @inlineCallbacks
  684. def get_status(self):
  685. if self.is_attached():
  686. config = yield self.call('xbr.network.get_config')
  687. status = yield self.call('xbr.network.get_status')
  688. return {'config': config, 'status': status}
  689. else:
  690. self.log.warn('not connected: could not retrieve status')
  691. @inlineCallbacks
  692. def get_member(self, ethadr_raw):
  693. if self.is_attached():
  694. is_member = yield self.call('xbr.network.is_member', ethadr_raw)
  695. if is_member:
  696. member_data = yield self.call('xbr.network.get_member_by_wallet', ethadr_raw)
  697. member_data['address'] = web3.Web3.toChecksumAddress(member_data['address'])
  698. member_data['oid'] = uuid.UUID(bytes=member_data['oid'])
  699. member_data['balance']['eth'] = web3.Web3.fromWei(unpack_uint256(member_data['balance']['eth']),
  700. 'ether')
  701. member_data['balance']['xbr'] = web3.Web3.fromWei(unpack_uint256(member_data['balance']['xbr']),
  702. 'ether')
  703. member_data['created'] = np.datetime64(member_data['created'], 'ns')
  704. member_level = member_data['level']
  705. member_data['level'] = {
  706. # Member is active.
  707. 1: 'ACTIVE',
  708. # Member is active and verified.
  709. 2: 'VERIFIED',
  710. # Member is retired.
  711. 3: 'RETIRED',
  712. # Member is subject to a temporary penalty.
  713. 4: 'PENALTY',
  714. # Member is currently blocked and cannot current actively participate in the market.
  715. 5: 'BLOCKED',
  716. }.get(member_level, None)
  717. self.log.info(
  718. 'Member {member_oid} found for address 0x{member_adr} - current member level {member_level}',
  719. member_level=hlval(member_data['level']),
  720. member_oid=hlid(member_data['oid']),
  721. member_adr=hlval(member_data['address']))
  722. return member_data
  723. else:
  724. self.log.warn('Address {output_ethadr} is not a member in the XBR network',
  725. output_ethadr=ethadr_raw)
  726. else:
  727. self.log.warn('not connected: could not retrieve member data for address {output_ethadr}',
  728. output_ethadr=ethadr_raw)
  729. class Application(object):
  730. """
  731. Main XBR member application.
  732. """
  733. log = txaio.make_logger()
  734. DOTDIR = os.path.abspath(os.path.expanduser('~/.xbrnetwork'))
  735. DOTFILE = 'config.ini'
  736. async def start(self, reactor, url=None, realm=None, profile=None):
  737. """
  738. Start main application. This will read the user configuration, potentially asking
  739. for a user password.
  740. :param reactor: Twisted reactor to use.
  741. :param url: Optionally override network URL as defined in profile.
  742. :param realm: Optionally override network URL as defined in profile.
  743. :param profile: User profile name to load.
  744. :return:
  745. """
  746. txaio.start_logging(level='info')
  747. self.log.info('ok, application starting for user profile "{profile}"', profile=profile)
  748. if not os.path.isdir(self.DOTDIR):
  749. os.mkdir(self.DOTDIR)
  750. self.log.info('dotdir created: "{dotdir}"', dotdir=self.DOTDIR)
  751. self._config_path = config_path = os.path.join(self.DOTDIR, self.DOTFILE)
  752. self._profile_name = profile or 'default'
  753. if not os.path.isfile(self._config_path):
  754. self.log.info('no config exist under "{config_path}"', config_path=self._config_path)
  755. self._config = UserConfig(self._config_path)
  756. self._profile = None
  757. else:
  758. self._config = UserConfig(self._config_path)
  759. # FIXME: start modal dialog to get password from user
  760. def getpw():
  761. return '123secret'
  762. self._config.load(cb_get_password=getpw)
  763. if self._profile_name not in self._config.profiles:
  764. raise click.ClickException('no such profile "{}" in config "{}" with {} profiles'.format(self._profile_name, config_path, len(self._config.profiles)))
  765. else:
  766. self._profile = self._config.profiles[self._profile_name]
  767. self.log.info('user profile "{profile_name}" loaded from "{config_path}":\n\n',
  768. config_path=self._config_path, profile_name=self._profile_name)
  769. pprint(self._profile.marshal())
  770. print('\n\n')
  771. extra = {
  772. 'ready': txaio.create_future(),
  773. 'done': txaio.create_future(),
  774. 'running': True,
  775. 'config': self._config,
  776. 'config_path': self._config_path,
  777. 'profile': self._profile,
  778. 'profile_name': self._profile_name,
  779. }
  780. # XBR network node used as a directory server and gateway to XBR smart contracts
  781. network_url = url or (self._profile.network_url if self._profile and self._profile.network_url else 'ws://localhost:8090/ws')
  782. # WAMP realm on network node, usually "xbrnetwork"
  783. network_realm = realm or (self._profile.network_realm if self._profile and self._profile.network_realm else 'xbrnetwork')
  784. runner = ApplicationRunner(url=network_url,
  785. realm=network_realm,
  786. extra=extra,
  787. serializers=[CBORSerializer()])
  788. self.log.info('ok, now connecting to "{network_url}", joining realm "{network_realm}" ..',
  789. network_url=network_url,
  790. network_realm=network_realm)
  791. await runner.run(ApplicationClient,
  792. reactor=reactor,
  793. auto_reconnect=True,
  794. start_reactor=False)
  795. self.log.info('ok, application client connected!')
  796. session, details = await extra['ready']
  797. self.log.info('ok, application session joined: {details}', details=details)
  798. def on_exit(_):
  799. self.log.info('exiting application ..')
  800. extra['running'] = False
  801. txaio.resolve(extra['done'], None)
  802. win = ApplicationWindow(reactor, session, self._config, self._config_path, self._profile, self._profile_name)
  803. win.connect("cancel", on_exit)
  804. win.connect("destroy", on_exit)
  805. win.show_all()
  806. await win.start()
  807. ticks = 0
  808. while extra['running']:
  809. ticks += 1
  810. self.log.info('ok, application main task still running at tick {ticks}', ticks=ticks)
  811. await sleep(5)
  812. self.log.info('ok, application main task ended!')
  813. async def main(reactor, url, realm, profile):
  814. """
  815. Load the named user profile (or create a new one), overriding URL/realm,
  816. connect to a network node, and start the network member on-boarding.
  817. If the user credentials are already for a member, fetch member information
  818. and display member page.
  819. :param reactor: Twisted reactor to use.
  820. :param url: Override network URL from user profile with this value.
  821. :param realm: Override network realm from user profile with this value.
  822. :param profile: Name of user profile within user
  823. configuration to load (eg from ``$HOME/.xbrnetwork/config.ini``)
  824. """
  825. app = Application()
  826. await app.start(reactor, url=url, realm=realm, profile=profile)
  827. def _main():
  828. """
  829. GUI entry point, parsing command line arguments and then starting the
  830. actual main GUI program with parsed parameters.
  831. To use, run:
  832. .. code:: console
  833. xbrnetwork-ui --profile default --url ws://localhost:8090/ws --realm xbrnetwork
  834. This will load the user profile ``"default"`` from the user configuration, but
  835. overriding any network URL and realm found therin.
  836. """
  837. parser = argparse.ArgumentParser()
  838. parser.add_argument('--url',
  839. dest='url',
  840. type=str,
  841. default=None,
  842. help='The router URL to connect to, e.g. "ws://localhost:8090/ws"')
  843. parser.add_argument('--realm',
  844. dest='realm',
  845. type=str,
  846. default=None,
  847. help='The realm to join, e.g. "xbrnetwork"')
  848. parser.add_argument('--profile',
  849. dest='profile',
  850. type=str,
  851. default='default',
  852. help='The user profile to use, e.g. "default"')
  853. args = parser.parse_args()
  854. react(main, (args.url, args.realm, args.profile,))
  855. if __name__ == '__main__':
  856. _main()