Source code for xbr._gui

###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

import argparse
import binascii
import os
import random
import sys
import uuid
from pprint import pprint
from time import time_ns

if sys.version_info < (3, 10):
    import importlib_resources as resources
else:
    from importlib import resources

import gi

gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
from gi.repository.Gdk import Color
from twisted.internet import gtk3reactor

gtk3reactor.install()

import txaio

txaio.use_twisted()

import click
import numpy as np
import web3
from autobahn.twisted.util import sleep
from autobahn.twisted.wamp import ApplicationRunner
from autobahn.util import hlid, hltype, hlval, parse_activation_code
from autobahn.wamp.serializer import CBORSerializer
from humanize import naturaldelta, naturaltime
from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks
from twisted.internet.task import react

from xbr import account_from_ethkey, account_from_seedphrase, generate_seedphrase, unpack_uint256
from xbr._cli import Client
from xbr._config import Profile, UserConfig

[docs] LOGO_RESOURCE = str(resources.files("autobahn.asset") / "xbr_gray.svg")
print(LOGO_RESOURCE, os.path.isfile(LOGO_RESOURCE))
[docs] class ApplicationWindow(Gtk.Assistant): """ Main application window which provides UI for the following functions: * N) New account * R) Recover account: - R1) Backup cloud device in account enabled => download encrypted account data from cloud backup device, requires email (and 2FA) verification and password - R2) At least one device left in account and at hand => synchronize with existing device, direct device-to-device encrypted account data transfer - R3) Only cold storage recovery seed phrase left => account from seed-phrase full recovery, including new email and 2FA verification. See also: * https://python-gtk-3-tutorial.readthedocs.io/en/latest/ * https://twistedmatrix.com/documents/current/core/howto/choosing-reactor.html """
[docs] log = txaio.make_logger()
[docs] SELECTED_NONE = 0
[docs] SELECTED_NEW = 1
[docs] SELECTED_SYNCRONIZE = 2
[docs] SELECTED_RECOVER = 3
def __init__(self, reactor, session, config, config_path, profile, profile_name): Gtk.Assistant.__init__(self)
[docs] self.reactor = reactor
[docs] self.session = session
[docs] self.config = config
[docs] self.config_path = config_path
[docs] self.profile = profile
[docs] self.profile_name = profile_name
[docs] self.input_seedphrase = None
[docs] self.input_email = None
[docs] self.input_password = None
[docs] self.output_account = None
[docs] self.output_ethadr = None
[docs] self.output_ethadr_raw = None
[docs] self.output_member_data = None
[docs] self.output_member_data_oid = uuid.UUID(bytes=b"\x00" * 16)
# configure assistant window/widget self.set_title("XBR Network") self.set_default_size(600, 600) self.set_border_width(50) self.set_resizable(False) # setup assistant pages self._setup_page1() self._setup_page2() self._setup_page3() self._setup_page4() self._setup_page5() @inlineCallbacks
[docs] def start(self): # start page depends on available user profile if self.profile: self.output_account = account_from_ethkey(self.profile.ethkey) self.output_ethadr = web3.Web3.toChecksumAddress(self.output_account.address) self.output_ethadr_raw = binascii.a2b_hex(self.output_ethadr[2:]) info = yield self.session.get_status() if info: now = str(np.datetime64(np.datetime64(info["status"]["now"], "ns"), "s")) self._label5_now.set_label(now) self._label5_chain.set_label(str(info["status"]["chain"])) self._label5_status.set_label(str(info["status"]["status"])) self._label5_xbrnetwork.set_label(str(info["config"]["contracts"]["xbrnetwork"])) self._label5_xbrtoken.set_label(str(info["config"]["contracts"]["xbrtoken"])) self._label5_blockhash.set_label("0x" + binascii.b2a_hex(info["status"]["block"]["hash"]).decode()) self._label5_blocknumber.set_label(str(info["status"]["block"]["number"])) pprint(info) member_data = yield self.session.get_member(self.output_ethadr_raw) if not member_data: self.log.info("ethadr {output_ethadr} is NOT yet a member", output_ethadr=self.output_ethadr) if self.profile.vaction_oid: # switch to page "_setup_page4" self.set_current_page(3) else: if self.profile.ethkey: # switch to page "_setup_page3" self.set_current_page(2) else: # switch to page "_setup_page2" self.set_current_page(1) else: self.log.info( "ok, ethadr {output_ethadr} already is a member: {member_data}", output_ethadr=self.output_ethadr, member_data=member_data, ) self.output_member_data = member_data created_ago = naturaldelta( (np.datetime64(time_ns(), "ns") - self.output_member_data["created"]) / 1000000000.0 ) created = naturaltime(np.datetime64(self.output_member_data["created"], "s")) self._label2.set_label(str(self.output_member_data["oid"])) self._label4.set_label(str(self.output_member_data["address"])) self._label6.set_label("{} ({} ago)".format(created, created_ago)) self._label8.set_label(str(self.output_member_data["level"])) self._label10.set_label(str(self.output_member_data["balance"]["eth"])) self._label12.set_label(str(self.output_member_data["balance"]["xbr"])) # switch to page "_setup_page5" self.set_current_page(4) else: profile = Profile() profile.path = self.config_path profile.ethkey = None profile.cskey = None profile.username = None profile.email = None profile.network_url = "ws://localhost:8090/ws" profile.network_realm = "xbrnetwork" profile.member_oid = None profile.vaction_oid = None profile.vaction_requested = None profile.vaction_verified = None profile.data_url = None profile.data_realm = None profile.infura_url = None profile.infura_network = None profile.infura_key = None profile.infura_secret = None self.profile = profile # switch to page "_setup_page1" self.set_current_page(0)
[docs] def on_complete_toggled(self, checkbutton): self.set_page_complete(self.complete, checkbutton.get_active())
# no config: select new/recovery
[docs] def _setup_page1(self): """ Setup page shown when no config/profile could be found. Allows to select from: * new account * synchronize device * recover account """ grid1 = Gtk.Grid() grid1.set_row_spacing(20) grid1.set_column_spacing(20) grid1.set_margin_top(20) grid1.set_margin_bottom(20) grid1.set_margin_start(20) grid1.set_margin_end(20) image1 = Gtk.Image() image1.set_from_file(LOGO_RESOURCE) grid1.attach(image1, 0, 0, 2, 1) label0 = Gtk.Label(label="\n\nI am new and do not have an account yet:\n") label0.set_alignment(0, 0.5) grid1.attach(label0, 0, 1, 2, 1) label1 = Gtk.Label(label="Create a new account or start from fresh. You only need an email address. [N]") label1.set_alignment(0, 0.5) label1.set_justify(Gtk.Justification.LEFT) grid1.attach(label1, 1, 2, 1, 1) button1 = Gtk.Button.new_with_label("New account") def on_button1(res): self.log.info("SELECTED_NEW: {res}", res=res) self.set_current_page(1) button1.connect("clicked", on_button1) grid1.attach(button1, 0, 2, 1, 1) label12 = Gtk.Label(label="\n\nI already have an existing account and want to use that:\n") label12.set_alignment(0, 0.5) grid1.attach(label12, 0, 3, 2, 1) label22 = Gtk.Label( label="Restore account from cloud backup to this device. You will need access to\n" "your account password and access to your account email address. [R1]" ) label22.set_alignment(0, 0.5) label22.set_justify(Gtk.Justification.LEFT) label22.set_line_wrap(True) label22.set_width_chars(12) grid1.attach(label22, 1, 4, 1, 1) button22 = Gtk.Button.new_with_label("Restore account") def on_button22(res): self.log.info("SELECTED_RESTORE: {res}", res=res) self.set_current_page(2) button22.connect("clicked", on_button22) grid1.attach(button22, 0, 4, 1, 1) label2 = Gtk.Label( label="Synchronize device with other device in account. You will need access to\n" "another device currently connected to your account. [R2]" ) label2.set_alignment(0, 0.5) label2.set_justify(Gtk.Justification.LEFT) label2.set_line_wrap(True) label2.set_width_chars(12) grid1.attach(label2, 1, 5, 1, 1) button2 = Gtk.Button.new_with_label("Synchronize account") def on_button2(res): self.log.info("SELECTED_SYNCRONIZE: {res}", res=res) self.set_current_page(2) button2.connect("clicked", on_button2) grid1.attach(button2, 0, 5, 1, 1) label3 = Gtk.Label( label="Recover account from account seed phrase. You only need access to\n" "your 12-24 word account recovery seed phrase. [R3]" ) label3.set_alignment(0, 0.5) label3.set_justify(Gtk.Justification.LEFT) label3.set_line_wrap(True) label3.set_width_chars(12) grid1.attach(label3, 1, 6, 1, 1) button3 = Gtk.Button.new_with_label("Recover account") def on_button3(res): self.log.info("SELECTED_RECOVER: {res}", res=res) self.set_current_page(3) button3.connect("clicked", on_button3) grid1.attach(button3, 0, 6, 1, 1) self.append_page(grid1)
# generate seed phrase
[docs] def _setup_page2(self): """ Setup page shown to generate a new seed phrase. """ box2_1 = Gtk.VBox() box2_2 = Gtk.HBox() image2_1 = Gtk.Image() image2_1.set_from_file(LOGO_RESOURCE) box2_2.add(image2_1) box2_1.add(box2_2) button2_1 = Gtk.Button.new_with_label("Generate seedphrase") def on_button2_1(_): self.input_seedphrase = generate_seedphrase(strength=256, language="english") textbuffer2_1.set_text(self.input_seedphrase) checkbutton2_1.set_sensitive(True) button2_1.connect("clicked", on_button2_1) box2_1.add(button2_1) label2_1 = Gtk.Label(label="Backup your new seed phrase in a secure offline location (e.g. on printed paper):") label2_1.set_alignment(0, 0.5) label2_1.set_justify(Gtk.Justification.LEFT) box2_1.add(label2_1) textview2_1 = Gtk.TextView() textbuffer2_1 = textview2_1.get_buffer() textbuffer2_1.set_text("\n" * 5) textview2_1.set_editable(False) textview2_1.set_justification(Gtk.Justification.CENTER) textview2_1.set_monospace(True) textview2_1.set_wrap_mode(Gtk.WrapMode.WORD) box2_1.add(textview2_1) box2_3 = Gtk.HBox() checkbutton2_1 = Gtk.CheckButton(label="I have backed up my seed phrase") checkbutton2_1.set_active(False) checkbutton2_1.set_sensitive(False) def on_checkbutton2_1(_): button2_2.set_sensitive(True) checkbutton2_1.connect("toggled", on_checkbutton2_1) box2_3.add(checkbutton2_1) button2_2 = Gtk.Button.new_with_label("Continue") button2_2.set_sensitive(False) @inlineCallbacks def on_button2_2(_): self.output_account = account_from_seedphrase(self.input_seedphrase, index=0) self.output_ethadr = web3.Web3.toChecksumAddress(self.output_account.address) self.output_ethadr_raw = binascii.a2b_hex(self.output_ethadr[2:]) # https://eth-account.readthedocs.io/en/latest/eth_account.signers.html#eth_account.signers.local.LocalAccount.key self.profile.ethkey = bytes(self.output_account.key) self.profile.cskey = bytes(self.session._cskey_raw) # set user eth key on client session self.session.set_ethkey_from_profile(self.profile) # save user config self.config.profiles[self.profile_name] = self.profile self.config.save(self.input_password) self.log.info( 'XBR ETH key at address {ethadr} set from seed phrase (BIP39 account 0): "{seedphrase}"', ethadr=self.output_ethadr, seedphrase=self.input_seedphrase, ) member_data = yield self.session.get_member(self.output_ethadr_raw) pprint(member_data) if not member_data: self.log.info("ethadr {output_ethadr} is NOT yet a member", output_ethadr=self.output_ethadr) self.set_current_page(2) else: self.log.info( "ok, ethadr {output_ethadr} already is a member: {member_data}", output_ethadr=self.output_ethadr, member_data=member_data, ) self.profile.member_oid = uuid.UUID(bytes=member_data["member_oid"]) self.profile.member_adr = self.output_ethadr self.profile.email = member_data["email"] self.profile.username = member_data["username"] # save user config self.config.profiles[self.profile_name] = self.profile self.config.save(self.input_password) self.output_member_data = member_data self._label2.set_label(str(self._member_data["oid"])) self.set_current_page(4) def run_on_button2_2(widget): self.log.info("{func}({widget})", func=hltype(run_on_button2_2), widget=widget) reactor.callLater(0, on_button2_2, widget) button2_2.connect("clicked", run_on_button2_2) box2_3.add(button2_2) box2_1.add(box2_3) self.append_page(box2_1)
# submit onboard request
[docs] def _setup_page3(self): """ :return: """ box1 = Gtk.VBox() box2 = Gtk.HBox() image1 = Gtk.Image() image1.set_from_file(LOGO_RESOURCE) box2.add(image1) box1.add(box2) grid1 = Gtk.Grid() grid1.set_row_spacing(20) grid1.set_column_spacing(20) grid1.set_margin_top(20) grid1.set_margin_bottom(20) grid1.set_margin_start(20) grid1.set_margin_end(20) label1 = Gtk.Label(label="Your email address:") grid1.attach(label1, 0, 0, 1, 1) entry1 = Gtk.Entry() entry1.set_text("") entry1.set_max_length(255) entry1.set_max_width_chars(40) grid1.attach(entry1, 1, 0, 1, 1) checks = { "email": None, "password": None, "eula": None, } def check_all(): print("check_all") for c in checks: if checks[c] is None: print("check failed", c) button3.set_sensitive(False) return button3.set_sensitive(True) def check_email(email): if "@" in email: return email else: return None def check_password(password): return True def on_entry1(entry): # joe.doe@example.com checks["email"] = check_email(entry.get_text()) if checks["email"]: entry1.modify_fg(Gtk.StateFlags.NORMAL, None) else: entry1.modify_fg(Gtk.StateFlags.NORMAL, Color(50000, 0, 0)) check_all() entry1.connect("changed", on_entry1) label2 = Gtk.Label(label="New password:") grid1.attach(label2, 0, 1, 1, 1) entry2 = Gtk.Entry() entry2.set_text("") entry2.set_max_length(20) entry2.set_max_width_chars(20) entry2.set_visibility(False) grid1.attach(entry2, 1, 1, 1, 1) label2 = Gtk.Label(label="Repeat new password:") grid1.attach(label2, 0, 2, 1, 1) entry3 = Gtk.Entry() entry3.set_text("") entry3.set_max_length(20) entry3.set_max_width_chars(20) entry3.set_visibility(False) grid1.attach(entry3, 1, 2, 1, 1) def on_entry23(_): pw1_ok = False if check_password(entry2.get_text()): pw1_ok = True entry2.modify_fg(Gtk.StateFlags.NORMAL, None) else: entry2.modify_fg(Gtk.StateFlags.NORMAL, Color(50000, 0, 0)) pw2_ok = False if check_password(entry3.get_text()): pw2_ok = True entry3.modify_fg(Gtk.StateFlags.NORMAL, None) else: entry3.modify_fg(Gtk.StateFlags.NORMAL, Color(50000, 0, 0)) if pw1_ok and pw2_ok and entry2.get_text() == entry3.get_text() and check_password(entry2.get_text()): checks["password"] = entry2.get_text() else: checks["password"] = None check_all() entry2.connect("changed", on_entry23) entry3.connect("changed", on_entry23) label3 = Gtk.Label(label="EULA:") grid1.attach(label3, 0, 3, 1, 1) button1 = Gtk.CheckButton(label="I accept the EULA and terms of use") button1.set_active(False) button1.set_sensitive(True) def on_button1(button): if button.get_active(): checks["eula"] = True else: checks["eula"] = False check_all() button1.connect("toggled", on_button1) grid1.attach(button1, 1, 3, 1, 1) label3 = Gtk.Label(label="Cloud backup:") grid1.attach(label3, 0, 4, 1, 1) button2 = Gtk.CheckButton(label="Yes, enable encrypted cloud backup of my private keys") button2.set_active(False) button2.set_sensitive(True) def on_button2(button): check_all() button2.connect("toggled", on_button2) grid1.attach(button2, 1, 4, 1, 1) button3 = Gtk.Button.new_with_label("Register account") button3.set_sensitive(False) @inlineCallbacks def on_button3(_): self.input_email = checks["email"] self.input_password = checks["password"] self.input_backup_enabled = button2.get_active() self.input_username = "anonymous" self.input_username = "{}{}".format(self.input_username, random.randint(0, 10000)) self.session.set_ethkey_from_profile(self.profile) self.log.info("input_email: {input_email}", input_email=self.input_email) self.log.info("input_username: {input_username}", input_username=self.input_username) self.log.info("input_password: {input_password}", input_password=self.input_password) result = yield self.session._do_onboard_member(self.input_username, self.input_email) pprint(result) self.profile.email = self.input_email self.profile.username = self.input_username self.profile.vaction_oid = str(uuid.UUID(bytes=result["vaction_oid"])) self.profile.vaction_requested = str(np.datetime64(result["timestamp"], "ns")) self.config.profiles[self.profile_name] = self.profile self.config.save(self.input_password) self.set_current_page(3) def run_on_button3(widget): self.log.info("{func}({widget})", func=hltype(run_on_button3), widget=widget) reactor.callLater(0, on_button3, widget) button3.connect("clicked", run_on_button3) grid1.attach(button3, 2, 4, 1, 1) box1.add(grid1) self.append_page(box1)
# submit verification code
[docs] def _setup_page4(self): """ Page shown when member registration request was submitted, a verification email sent, and the verification request ID returned. The user now should check the email inbox for the received verification code, and continue verifying the code. :return: """ box1 = Gtk.VBox() box2 = Gtk.HBox() image1 = Gtk.Image() image1.set_from_file(LOGO_RESOURCE) box2.add(image1) box1.add(box2) box3 = Gtk.HBox() label1 = Gtk.Label(label="Member registration submitted, verification request:") label2 = Gtk.Label(label="8d5d7ffd-23d9-45a0-a686-00a49f29d3cd") box3.add(label1) box3.add(label2) box1.add(box3) label3 = Gtk.Label(label="Please check your email inbox, and enter the verification code received here:") box1.add(label3) entry1 = Gtk.Entry() entry1.set_text("") entry1.set_max_length(255) entry1.set_max_width_chars(40) box1.add(entry1) def on_entry1(entry): # "RWCN-94NV-CEHR" -> ("RWCN", "94NV", "CEHR") | None vaction_code = parse_activation_code(entry.get_text()) if vaction_code: entry1.modify_fg(Gtk.StateFlags.NORMAL, None) button1.set_sensitive(True) else: entry1.modify_fg(Gtk.StateFlags.NORMAL, Color(50000, 0, 0)) button1.set_sensitive(False) entry1.connect("changed", on_entry1) button1 = Gtk.Button.new_with_label("Verify") button1.set_sensitive(False) @inlineCallbacks def on_button1(_): vaction_code = parse_activation_code(entry1.get_text()) if vaction_code: vaction_code = "-".join(vaction_code.groups()) if type(self.profile.vaction_oid) == str: vaction_oid = uuid.UUID(self.profile.vaction_oid) else: vaction_oid = self.profile.vaction_oid result = yield self.session._do_onboard_member_verify(vaction_oid, vaction_code) pprint(result) self.profile.vaction_verified = str(np.datetime64(result["created"], "ns")) self.profile.vaction_transaction = "0x" + str(binascii.b2a_hex(result["transaction"]).decode()) self.profile.member_oid = str(uuid.UUID(bytes=result["member_oid"])) self.config.profiles[self.profile_name] = self.profile self.config.save(self.input_password) def run_on_button1(widget): self.log.info("{func}({widget})", func=hltype(run_on_button1), widget=widget) reactor.callLater(0, on_button1, widget) button1.connect("clicked", run_on_button1) box1.add(button1) self.append_page(box1)
# show member data
[docs] def _setup_page5(self): """ Page shown for a user (private eth key) that already is member. :return: """ box1 = Gtk.VBox() box2 = Gtk.HBox() image1 = Gtk.Image() image1.set_from_file(LOGO_RESOURCE) box2.add(image1) box1.add(box2) grid2 = Gtk.Grid() grid2.set_row_spacing(20) grid2.set_column_spacing(20) grid2.set_margin_top(20) grid2.set_margin_bottom(20) grid2.set_margin_start(20) grid2.set_margin_end(20) grid2_y = 0 # Current server time # label5_now_title = Gtk.Label(label="Current server time:") label5_now_title.set_alignment(1, 0.5) grid2.attach(label5_now_title, 0, grid2_y, 1, 1) self._label5_now = Gtk.Label() self._label5_now.set_alignment(0, 0.5) self._label5_now.set_selectable(False) grid2.attach(self._label5_now, 1, grid2_y, 1, 1) grid2_y += 1 # Blockchain ID (e.g. 1, 3 or 5777) # label5_chain_title = Gtk.Label(label="Blockchain ID:") label5_chain_title.set_alignment(1, 0.5) grid2.attach(label5_chain_title, 0, grid2_y, 1, 1) self._label5_chain = Gtk.Label() self._label5_chain.set_alignment(0, 0.5) self._label5_chain.set_selectable(True) grid2.attach(self._label5_chain, 1, grid2_y, 1, 1) grid2_y += 1 # Current server time # label5_status_title = Gtk.Label(label="Service status:") label5_status_title.set_alignment(1, 0.5) grid2.attach(label5_status_title, 0, grid2_y, 1, 1) self._label5_status = Gtk.Label() self._label5_status.set_alignment(0, 0.5) self._label5_status.set_selectable(False) grid2.attach(self._label5_status, 1, grid2_y, 1, 1) grid2_y += 1 # xbrnetwork address # label5_xbrnetwork_title = Gtk.Label(label="XBRNetwork contract:") label5_xbrnetwork_title.set_alignment(1, 0.5) grid2.attach(label5_xbrnetwork_title, 0, grid2_y, 1, 1) self._label5_xbrnetwork = Gtk.Label() self._label5_xbrnetwork.set_alignment(0, 0.5) self._label5_xbrnetwork.set_selectable(True) grid2.attach(self._label5_xbrnetwork, 1, grid2_y, 1, 1) grid2_y += 1 # xbrtoken address # label5_xbrtoken_title = Gtk.Label(label="XBRToken contract:") label5_xbrtoken_title.set_alignment(1, 0.5) grid2.attach(label5_xbrtoken_title, 0, grid2_y, 1, 1) self._label5_xbrtoken = Gtk.Label() self._label5_xbrtoken.set_alignment(0, 0.5) self._label5_xbrtoken.set_selectable(True) grid2.attach(self._label5_xbrtoken, 1, grid2_y, 1, 1) grid2_y += 1 # Current block hash # label5_blockhash_title = Gtk.Label(label="Current block hash:") label5_blockhash_title.set_alignment(1, 0.5) grid2.attach(label5_blockhash_title, 0, grid2_y, 1, 1) self._label5_blockhash = Gtk.Label() self._label5_blockhash.set_alignment(0, 0.5) self._label5_blockhash.set_selectable(True) grid2.attach(self._label5_blockhash, 1, grid2_y, 1, 1) grid2_y += 1 # Current block number # label5_blocknumber_title = Gtk.Label(label="Current block number:") label5_blocknumber_title.set_alignment(1, 0.5) grid2.attach(label5_blocknumber_title, 0, grid2_y, 1, 1) self._label5_blocknumber = Gtk.Label() self._label5_blocknumber.set_alignment(0, 0.5) self._label5_blocknumber.set_selectable(True) grid2.attach(self._label5_blocknumber, 1, grid2_y, 1, 1) grid2_y += 1 box1.add(grid2) grid1 = Gtk.Grid() grid1.set_row_spacing(20) grid1.set_column_spacing(20) grid1.set_margin_top(20) grid1.set_margin_bottom(20) grid1.set_margin_start(20) grid1.set_margin_end(20) label1 = Gtk.Label(label="User ID:") label1.set_alignment(1, 0.5) grid1.attach(label1, 0, 0, 1, 1) self._label2 = Gtk.Label() self._label2.set_alignment(0, 0.5) self._label2.set_selectable(True) grid1.attach(self._label2, 1, 0, 1, 1) label3 = Gtk.Label(label="Eth Address:") label3.set_alignment(1, 0.5) grid1.attach(label3, 0, 1, 1, 1) self._label4 = Gtk.Label() self._label4.set_alignment(0, 0.5) self._label4.set_selectable(True) grid1.attach(self._label4, 1, 1, 1, 1) label5 = Gtk.Label(label="Account Created:") label5.set_alignment(1, 0.5) grid1.attach(label5, 0, 2, 1, 1) self._label6 = Gtk.Label() self._label6.set_alignment(0, 0.5) grid1.attach(self._label6, 1, 2, 1, 1) label7 = Gtk.Label(label="Membership:") label7.set_alignment(1, 0.5) grid1.attach(label7, 0, 3, 1, 1) self._label8 = Gtk.Label() self._label8.set_alignment(0, 0.5) grid1.attach(self._label8, 1, 3, 1, 1) label9 = Gtk.Label(label="ETH Balance:") label9.set_alignment(1, 0.5) grid1.attach(label9, 0, 4, 1, 1) self._label10 = Gtk.Label() self._label10.set_alignment(0, 0.5) grid1.attach(self._label10, 1, 4, 1, 1) label11 = Gtk.Label(label="XBR Balance:") label11.set_alignment(1, 0.5) grid1.attach(label11, 0, 5, 1, 1) self._label12 = Gtk.Label() self._label12.set_alignment(0, 0.5) grid1.attach(self._label12, 1, 5, 1, 1) box1.add(grid1) self.append_page(box1)
[docs] class ApplicationClient(Client):
[docs] async def onJoin(self, details): self.log.info( 'Ok, client joined on realm "{realm}" [session={session}, authid="{authid}", authrole="{authrole}"]', realm=hlid(details.realm), session=hlid(details.session), authid=hlid(details.authid), authrole=hlid(details.authrole), details=details, ) if "ready" in self.config.extra: txaio.resolve(self.config.extra["ready"], (self, details))
@inlineCallbacks
[docs] def get_status(self): if self.is_attached(): config = yield self.call("xbr.network.get_config") status = yield self.call("xbr.network.get_status") return {"config": config, "status": status} else: self.log.warn("not connected: could not retrieve status")
@inlineCallbacks
[docs] def get_member(self, ethadr_raw): if self.is_attached(): is_member = yield self.call("xbr.network.is_member", ethadr_raw) if is_member: member_data = yield self.call("xbr.network.get_member_by_wallet", ethadr_raw) member_data["address"] = web3.Web3.toChecksumAddress(member_data["address"]) member_data["oid"] = uuid.UUID(bytes=member_data["oid"]) member_data["balance"]["eth"] = web3.Web3.fromWei( unpack_uint256(member_data["balance"]["eth"]), "ether" ) member_data["balance"]["xbr"] = web3.Web3.fromWei( unpack_uint256(member_data["balance"]["xbr"]), "ether" ) member_data["created"] = np.datetime64(member_data["created"], "ns") member_level = member_data["level"] member_data["level"] = { # Member is active. 1: "ACTIVE", # Member is active and verified. 2: "VERIFIED", # Member is retired. 3: "RETIRED", # Member is subject to a temporary penalty. 4: "PENALTY", # Member is currently blocked and cannot current actively participate in the market. 5: "BLOCKED", }.get(member_level, None) self.log.info( "Member {member_oid} found for address 0x{member_adr} - current member level {member_level}", member_level=hlval(member_data["level"]), member_oid=hlid(member_data["oid"]), member_adr=hlval(member_data["address"]), ) return member_data else: self.log.warn("Address {output_ethadr} is not a member in the XBR network", output_ethadr=ethadr_raw) else: self.log.warn( "not connected: could not retrieve member data for address {output_ethadr}", output_ethadr=ethadr_raw )
[docs] class Application(object): """ Main XBR member application. """
[docs] log = txaio.make_logger()
[docs] DOTDIR = os.path.abspath(os.path.expanduser("~/.xbrnetwork"))
[docs] DOTFILE = "config.ini"
[docs] async def start(self, reactor, url=None, realm=None, profile=None): """ Start main application. This will read the user configuration, potentially asking for a user password. :param reactor: Twisted reactor to use. :param url: Optionally override network URL as defined in profile. :param realm: Optionally override network URL as defined in profile. :param profile: User profile name to load. :return: """ txaio.start_logging(level="info") self.log.info('ok, application starting for user profile "{profile}"', profile=profile) if not os.path.isdir(self.DOTDIR): os.mkdir(self.DOTDIR) self.log.info('dotdir created: "{dotdir}"', dotdir=self.DOTDIR) self._config_path = config_path = os.path.join(self.DOTDIR, self.DOTFILE) self._profile_name = profile or "default" if not os.path.isfile(self._config_path): self.log.info('no config exist under "{config_path}"', config_path=self._config_path) self._config = UserConfig(self._config_path) self._profile = None else: self._config = UserConfig(self._config_path) # FIXME: start modal dialog to get password from user def getpw(): return "123secret" self._config.load(cb_get_password=getpw) if self._profile_name not in self._config.profiles: raise click.ClickException( 'no such profile "{}" in config "{}" with {} profiles'.format( self._profile_name, config_path, len(self._config.profiles) ) ) else: self._profile = self._config.profiles[self._profile_name] self.log.info( 'user profile "{profile_name}" loaded from "{config_path}":\n\n', config_path=self._config_path, profile_name=self._profile_name, ) pprint(self._profile.marshal()) print("\n\n") extra = { "ready": txaio.create_future(), "done": txaio.create_future(), "running": True, "config": self._config, "config_path": self._config_path, "profile": self._profile, "profile_name": self._profile_name, } # XBR network node used as a directory server and gateway to XBR smart contracts network_url = url or ( self._profile.network_url if self._profile and self._profile.network_url else "ws://localhost:8090/ws" ) # WAMP realm on network node, usually "xbrnetwork" network_realm = realm or ( self._profile.network_realm if self._profile and self._profile.network_realm else "xbrnetwork" ) runner = ApplicationRunner(url=network_url, realm=network_realm, extra=extra, serializers=[CBORSerializer()]) self.log.info( 'ok, now connecting to "{network_url}", joining realm "{network_realm}" ..', network_url=network_url, network_realm=network_realm, ) await runner.run(ApplicationClient, reactor=reactor, auto_reconnect=True, start_reactor=False) self.log.info("ok, application client connected!") session, details = await extra["ready"] self.log.info("ok, application session joined: {details}", details=details) def on_exit(_): self.log.info("exiting application ..") extra["running"] = False txaio.resolve(extra["done"], None) win = ApplicationWindow(reactor, session, self._config, self._config_path, self._profile, self._profile_name) win.connect("cancel", on_exit) win.connect("destroy", on_exit) win.show_all() await win.start() ticks = 0 while extra["running"]: ticks += 1 self.log.info("ok, application main task still running at tick {ticks}", ticks=ticks) await sleep(5) self.log.info("ok, application main task ended!")
[docs] async def main(reactor, url, realm, profile): """ Load the named user profile (or create a new one), overriding URL/realm, connect to a network node, and start the network member on-boarding. If the user credentials are already for a member, fetch member information and display member page. :param reactor: Twisted reactor to use. :param url: Override network URL from user profile with this value. :param realm: Override network realm from user profile with this value. :param profile: Name of user profile within user configuration to load (eg from ``$HOME/.xbrnetwork/config.ini``) """ app = Application() await app.start(reactor, url=url, realm=realm, profile=profile)
[docs] def _main(): """ GUI entry point, parsing command line arguments and then starting the actual main GUI program with parsed parameters. To use, run: .. code:: console xbrnetwork-ui --profile default --url ws://localhost:8090/ws --realm xbrnetwork This will load the user profile ``"default"`` from the user configuration, but overriding any network URL and realm found therin. """ parser = argparse.ArgumentParser() parser.add_argument( "--url", dest="url", type=str, default=None, help='The router URL to connect to, e.g. "ws://localhost:8090/ws"' ) parser.add_argument("--realm", dest="realm", type=str, default=None, help='The realm to join, e.g. "xbrnetwork"') parser.add_argument( "--profile", dest="profile", type=str, default="default", help='The user profile to use, e.g. "default"' ) args = parser.parse_args() react( main, ( args.url, args.realm, args.profile, ), )
if __name__ == "__main__": _main()