diff --git a/.gitignore b/.gitignore index 862683a..7a4e336 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ cscope.po.out *.lo *.o *.obj +*.pyc # Precompiled Headers *.gch diff --git a/README.md b/README.md index 0368d8f..fabfa86 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,36 @@ -# Ombre GUI Wallet - -Copyright (c) 2018, OMBRE -Copyright (c) 2017, Sumokoin.org - -**One of the most easy-to-use, intuitive GUI (full) wallets in crypto.** - -# Installation & running from source codes - -1. Clone the repo: - - git clone https://github.com/ombre-projects/OmbreGUIWallet/ SumoGUIWallet - +# Sumokoin GUI Wallet + +Copyright (c) 2017, Sumokoin.org + +**One of the most easy-to-use, intuitive GUI (full) wallets in crypto.** + +![](https://www.sumokoin.org/images/sumokoin-gui-wallet-v0.0.1-b2.png) + + +# Installation & running from source codes + +1. Clone the repo: + + git clone https://github.com/sumoprojects/SumoGUIWallet SumoGUIWallet + 2. Install dependencies (with Python 2.7): - + * Generally, you can use Python `pip` to install required components: - - pip install PySide, requests, psutil - - * or - - pip install -r requirements.txt - + + pip install PySide, requests, psutil + + * or + + pip install -r requirements.txt + * On some OSes, PySide may be required to install from pre-built packages. For example, on Ubuntu 16.04, install PySide with the following command: - - sudo apt install python-pyside - - -3. Build/download Sumokoin binaries from [Ombre repo](https://github.com/ombre-projects/ombre) and put it to `Resources/bin` sub-directory. - -4. Run the wallet (Python 2.7): - - cd /path/to/SumoGUIWallet - python wallet.py + + sudo apt install python-pyside + + +3. Build/download Sumokoin binaries from [Sumokoin repo](https://github.com/sumoprojects/sumokoin) and put it to `Resources/bin` sub-directory. + +4. Run the wallet (Python 2.7): + + cd /path/to/SumoGUIWallet + python wallet.py diff --git a/Resources/icons/logo_black_32.png b/Resources/icons/logo_black_32.png deleted file mode 100644 index 3df2f53..0000000 Binary files a/Resources/icons/logo_black_32.png and /dev/null differ diff --git a/Resources/icons/logo_black_64.png b/Resources/icons/logo_black_64.png deleted file mode 100644 index 092f147..0000000 Binary files a/Resources/icons/logo_black_64.png and /dev/null differ diff --git a/Resources/icons/sumokoin.icns b/Resources/icons/ombre.icns similarity index 100% rename from Resources/icons/sumokoin.icns rename to Resources/icons/ombre.icns diff --git a/Resources/icons/ombre.ico b/Resources/icons/ombre.ico new file mode 100644 index 0000000..5d137ad Binary files /dev/null and b/Resources/icons/ombre.ico differ diff --git a/Resources/icons/ombre.png b/Resources/icons/ombre.png new file mode 100644 index 0000000..baf70ff Binary files /dev/null and b/Resources/icons/ombre.png differ diff --git a/Resources/icons/ombre_16x16.png b/Resources/icons/ombre_16x16.png new file mode 100644 index 0000000..f0340dc Binary files /dev/null and b/Resources/icons/ombre_16x16.png differ diff --git a/Resources/icons/ombre_16x16_mac.png b/Resources/icons/ombre_16x16_mac.png new file mode 100644 index 0000000..f0340dc Binary files /dev/null and b/Resources/icons/ombre_16x16_mac.png differ diff --git a/Resources/icons/ombre_icon_32.png b/Resources/icons/ombre_icon_32.png new file mode 100644 index 0000000..baf70ff Binary files /dev/null and b/Resources/icons/ombre_icon_32.png differ diff --git a/Resources/icons/ombre_icon_64.png b/Resources/icons/ombre_icon_64.png new file mode 100644 index 0000000..021c0bf Binary files /dev/null and b/Resources/icons/ombre_icon_64.png differ diff --git a/Resources/icons/sumokoin.ico b/Resources/icons/sumokoin.ico deleted file mode 100644 index 8f78868..0000000 Binary files a/Resources/icons/sumokoin.ico and /dev/null differ diff --git a/app/QSingleApplication.py b/app/QSingleApplication.py index b7d3aed..7ca5829 100644 --- a/app/QSingleApplication.py +++ b/app/QSingleApplication.py @@ -2,8 +2,8 @@ # -*- coding: utf-8 -*- ## Copyright (c) 2017, The Sumokoin Project (www.sumokoin.org) ''' -QSingleApplication is a wrapper class for creating single interface -of appliaction +QSingleApplication is a wrapper class for creating single interface +of appliaction ''' from __future__ import print_function @@ -11,12 +11,12 @@ from PySide.QtGui import QApplication from PySide.QtCore import QIODevice, QTimer -from PySide.QtNetwork import QLocalServer, QLocalSocket +from PySide.QtNetwork import QLocalServer, QLocalSocket from utils.common import getSockDir, makeDir -DATA_DIR = makeDir(os.path.join(getSockDir(), 'OmbreGUIWallet')) - +DATA_DIR = makeDir(os.path.join(getSockDir(), 'OmbreGUI')) + class QSingleApplication(QApplication): sock_file = 'ombre_wallet_sock' if sys.platform == 'win32': @@ -25,7 +25,7 @@ class QSingleApplication(QApplication): sock_file = os.path.join(DATA_DIR, '.%s' % sock_file) else: sock_file = os.path.join(getSockDir(), sock_file) - + def singleStart(self, appMain): self.appMain = appMain # Socket @@ -33,13 +33,13 @@ def singleStart(self, appMain): self.m_socket.connected.connect(self.connectToExistingApp) self.m_socket.error.connect(lambda:self.startApplication(first_start=True)) self.m_socket.connectToServer(self.sock_file, QIODevice.WriteOnly) - + def connectToExistingApp(self): # Quit application in 250 ms QTimer.singleShot(250, self.quit) print( "App is already running.", file=sys.stderr ) - - + + def startApplication(self, first_start=True): self.m_server = QLocalServer() if self.m_server.listen(self.sock_file): @@ -50,7 +50,7 @@ def startApplication(self, first_start=True): print( "Error listening the socket. App can't start!", file=sys.stderr ) QTimer.singleShot(250, self.quit) return - + # remove the listener path file and try to restart app one more time print( "Error listening the socket. Try to restart application...", file=sys.stderr ) if sys.platform != 'win32': @@ -58,13 +58,13 @@ def startApplication(self, first_start=True): os.unlink(self.sock_file) except Exception, err: print( err, file=sys.stderr ) - + QTimer.singleShot(250, lambda : self.startApplication(first_start=False)) - + def getNewConnection(self): self.new_socket = self.m_server.nextPendingConnection() self.new_socket.readyRead.connect(self.readSocket) - + def readSocket(self): f = self.new_socket.readLine() - self.appMain.processURLProtocol(str(f)) + self.appMain.processURLProtocol(str(f)) \ No newline at end of file diff --git a/app/hub.py b/app/hub.py index 3394de3..b74ef9d 100644 --- a/app/hub.py +++ b/app/hub.py @@ -22,7 +22,7 @@ from PySide.QtCore import QObject, Slot, Signal from utils.common import print_money, print_money2, readFile - + from settings import APP_NAME, VERSION, DATA_DIR, COIN, makeDir, seed_languages from utils.logger import log, LEVEL_ERROR, LEVEL_INFO @@ -35,7 +35,7 @@ wallet_dir_path = os.path.join(DATA_DIR, 'wallets') makeDir(wallet_dir_path) - + password_regex = re.compile(r"^([a-zA-Z0-9!@#\$%\^&\*]{1,256})$") from webui import LogViewer @@ -44,14 +44,14 @@ class Hub(QObject): def __init__(self, app): super(Hub, self).__init__() self.app = app - - + + def setUI(self, ui): self.ui = ui - + def setNewWalletUI(self, new_wallet_ui): self.new_wallet_ui = new_wallet_ui - + @Slot() def import_wallet(self): dlg = QFileDialog(self.new_wallet_ui, "Select Import Wallet File") @@ -61,12 +61,12 @@ def import_wallet(self): wallet_filename = dlg.selectedFiles()[0] else: return - + new_wallet_file = os.path.join(wallet_dir_path, str(uuid.uuid4().hex) + '.bin') if wallet_filename: wallet_key_filename = wallet_filename + '.keys' wallet_address_filename = wallet_filename + '.address.txt' - + if not os.path.exists(wallet_key_filename): QMessageBox.warning(self.new_wallet_ui, \ 'Import Wallet',\ @@ -74,7 +74,7 @@ def import_wallet(self): Are you sure to select correct wallet file?

Hint: Wallet file often ends with .bin""") return False - try: + try: copy2(wallet_filename, os.path.join(wallet_dir_path, new_wallet_file)) copy2(wallet_key_filename, os.path.join(wallet_dir_path, \ new_wallet_file + '.keys')) @@ -84,10 +84,10 @@ def import_wallet(self): self._detail_error_msg("Importing Wallet", "Error importing wallet!", str(err)) self.ui.reset_wallet() return False - + if not os.path.exists(new_wallet_file): return False - + wallet_password = None while True: wallet_password, result = self._custom_input_dialog(self.new_wallet_ui, \ @@ -100,11 +100,11 @@ def import_wallet(self): else: break else: - return False - + return False + self.on_new_wallet_show_progress_event.emit('Importing wallet...') self.app_process_events() - + wallet_address_filepath = new_wallet_file + ".address.txt" wallet_address = readFile(wallet_address_filepath) self.ui.wallet_info.wallet_filepath = new_wallet_file @@ -137,7 +137,7 @@ def import_wallet(self): QMessageBox.critical(self.new_wallet_ui, \ 'Error Importing Wallet',\ """Error: Unknown error.
- Imported wallet file may be corrupted. + Imported wallet file may be corrupted. Try to restore it with mnemonic seed instead.""") self.ui.reset_wallet() return False @@ -145,8 +145,8 @@ def import_wallet(self): self._show_wallet_info() self.ui.wallet_info.save() return True - - + + @Slot() def close_new_wallet_dialog(self): log("Closing new wallet dialog...", LEVEL_INFO) @@ -154,7 +154,7 @@ def close_new_wallet_dialog(self): if self.ui.wallet_info.wallet_filepath \ and os.path.exists(self.ui.wallet_info.wallet_filepath): self.ui.show_wallet(); - + @Slot(unicode) def create_new_wallet(self, mnemonic_seed=u''): @@ -172,7 +172,7 @@ def create_new_wallet(self, mnemonic_seed=u''): "Password must contain at least 1 character [a-zA-Z] or number [0-9]\
or special character like !@#$%^&* and not contain spaces") continue - + confirm_password, result = self._custom_input_dialog(self.new_wallet_ui, \ 'Wallet Password', \ "Confirm wallet password:", QLineEdit.Password) @@ -187,13 +187,13 @@ def create_new_wallet(self, mnemonic_seed=u''): break else: break - + if has_password: if not mnemonic_seed: # i.e. create new wallet mnemonic_seed_language = "0" # english seed_language_list = [sl[1] for sl in seed_languages] - lang, ok = QInputDialog.getItem(self.new_wallet_ui, "Mnemonic Seed Language", - "Select a language for wallet mnemonic seed:", + lang, ok = QInputDialog.getItem(self.new_wallet_ui, "Mnemonic Seed Language", + "Select a language for wallet mnemonic seed:", seed_language_list, 0, False) if ok and lang: for sl in seed_languages: @@ -205,12 +205,12 @@ def create_new_wallet(self, mnemonic_seed=u''): 'Mnemonic Seed Language',\ "No language is selected!\
'English' will be used for mnemonic seed") - + self.on_new_wallet_show_progress_event.emit("Restoring wallet..." \ if mnemonic_seed else "Creating wallet...") self.app_process_events() wallet_filepath = os.path.join(wallet_dir_path, str(uuid.uuid4().hex) + '.bin') - wallet_log_path = os.path.join(wallet_dir_path, 'sumo-wallet-cli.log') + wallet_log_path = os.path.join(wallet_dir_path, 'ombre-wallet-cli.log') resources_path = self.app.property("ResPath") if not mnemonic_seed: # i.e. create new wallet self.wallet_cli_manager = WalletCliManager(resources_path, \ @@ -238,7 +238,7 @@ def create_new_wallet(self, mnemonic_seed=u''): self.wallet_cli_manager.send_command(wallet_password) self.app_process_events(0.5) self.wallet_cli_manager.send_command("exit") - + self.app_process_events(2) except Exception, err: log(str(err), LEVEL_ERROR) @@ -249,7 +249,7 @@ def create_new_wallet(self, mnemonic_seed=u''): error_detailed_text) self.on_new_wallet_ui_reset_event.emit() return - + if os.path.exists(wallet_filepath): wallet_address = readFile(wallet_filepath + ".address.txt") self.ui.wallet_info.wallet_filepath = wallet_filepath @@ -258,7 +258,7 @@ def create_new_wallet(self, mnemonic_seed=u''): self.ui.wallet_info.is_loaded = True self.ui.run_wallet_rpc(wallet_password, 2) counter = 0 - + while not self.ui.wallet_rpc_manager.is_ready(): self.app_process_events(0.5) if self.ui.wallet_rpc_manager.block_hex: @@ -280,26 +280,26 @@ def create_new_wallet(self, mnemonic_seed=u''): """Error: Unknown error! Wallet RPC appears not responding.""") self.ui.reset_wallet() return - + self._show_wallet_info() self.ui.wallet_info.save() - else: + else: self.on_new_wallet_ui_reset_event.emit() - + @Slot() def rescan_spent(self): self.app_process_events() self.ui.wallet_rpc_manager.rpc_request.rescan_spent() - + # refresh wallet_info tx cache self.ui.wallet_info.top_tx_height = 0 self.ui.wallet_info.bc_height = -1 self.ui.wallet_info.wallet_transfers = [] self.ui.update_wallet_info() - + self.on_wallet_rescan_spent_completed_event.emit() - - + + @Slot() def rescan_bc(self): result = QMessageBox.question(self.ui, "Rescan Blockchain", \ @@ -308,19 +308,19 @@ def rescan_bc(self): if result == QMessageBox.No: self.on_wallet_rescan_bc_completed_event.emit() return - + self.app_process_events() self.ui.wallet_rpc_manager.rpc_request.rescan_bc() - + # refresh wallet_info tx cache self.ui.wallet_info.top_tx_height = 0 self.ui.wallet_info.bc_height = -1 self.ui.wallet_info.wallet_transfers = [] self.ui.update_wallet_info() - + self.on_wallet_rescan_bc_completed_event.emit() - - + + @Slot(float, str, str, int, int, str, bool, bool) def send_tx(self, amount, address, payment_id, priority, mixin, tx_desc, save_address, sweep_all): if not payment_id and not address.startswith("Sumi"): @@ -330,7 +330,7 @@ def send_tx(self, amount, address, payment_id, priority, mixin, tx_desc, save_ad if result == QMessageBox.No: self.on_wallet_send_tx_completed_event.emit('{"status": "CANCELLED", "message": "Sending coin cancelled"}') return - + if sweep_all: result = QMessageBox.question(self.ui, "Sending all your coins?", \ "This will send all your coins to target address.

Are you sure you want to proceed?", \ @@ -338,21 +338,21 @@ def send_tx(self, amount, address, payment_id, priority, mixin, tx_desc, save_ad if result == QMessageBox.No: self.on_wallet_send_tx_completed_event.emit('{"status": "CANCELLED", "message": "Sending coin cancelled"}') return - + wallet_password, result = self._custom_input_dialog(self.ui, \ "Wallet Password", "Please enter wallet password:", QLineEdit.Password) - + if not result: self.on_wallet_send_tx_completed_event.emit('{"status": "CANCELLED", "message": "Wrong wallet password"}'); return - + if hashlib.sha256(wallet_password).hexdigest() != self.ui.wallet_info.wallet_password: self.on_wallet_send_tx_completed_event.emit('{"status": "CANCELLED", "message": "Wrong wallet password"}'); QMessageBox.warning(self.ui, "Incorrect Wallet Password", "Wallet password is not correct!") return - + amount = int(amount*COIN) - + if sweep_all: _, _, per_subaddress = self.ui.wallet_rpc_manager.rpc_request.get_balance() subaddr_indices = [] @@ -360,10 +360,10 @@ def send_tx(self, amount, address, payment_id, priority, mixin, tx_desc, save_ad if s['unlocked_balance'] > 0: subaddr_indices.append(s['address_index']) ret = self.ui.wallet_rpc_manager.rpc_request.transfer_all(address, payment_id, priority, mixin, 0, subaddr_indices) - else: + else: ret = self.ui.wallet_rpc_manager.rpc_request.transfer_split(amount, \ - address, payment_id, priority, mixin) - + address, payment_id, priority, mixin) + if ret['status'] == "ERROR": self.on_wallet_send_tx_completed_event.emit(json.dumps(ret)); self.app_process_events() @@ -371,11 +371,11 @@ def send_tx(self, amount, address, payment_id, priority, mixin, tx_desc, save_ad msg = "Not possible to send coins. Probably, there is not enough money left for fee." else: msg = ret['message'] - + QMessageBox.critical(self.ui, \ 'Error Sending Coins',\ "ERROR

" + msg) - + if ret['status'] == "OK": if tx_desc: # set the same note for all txs: @@ -383,10 +383,10 @@ def send_tx(self, amount, address, payment_id, priority, mixin, tx_desc, save_ad for i in range(len(ret['tx_hash_list'])): notes.append(tx_desc) self.ui.wallet_rpc_manager.rpc_request.set_tx_notes(ret['tx_hash_list'], notes) - + self.on_wallet_send_tx_completed_event.emit(json.dumps(ret)); self.app_process_events() - + msg = "Coins successfully sent in the following transaction(s):

" for i in range(len(ret['tx_hash_list'])): if sweep_all: @@ -397,7 +397,7 @@ def send_tx(self, amount, address, payment_id, priority, mixin, tx_desc, save_ad print_money2(ret['fee_list'][i])) QMessageBox.information(self.ui, 'Coins Sent', msg) self.ui.update_wallet_info() - + if save_address: desc, _ = self._custom_input_dialog(self.ui, \ 'Saving Address...', \ @@ -406,28 +406,28 @@ def send_tx(self, amount, address, payment_id, priority, mixin, tx_desc, save_ad payment_id, desc) if ret['status'] == "OK": if self.ui.wallet_info.wallet_address_book: - address_entry = {"address": address, - "payment_id": payment_id, - "description": desc[0:200], + address_entry = {"address": address, + "payment_id": payment_id, + "description": desc[0:200], "index": ret["index"] } self.ui.wallet_info.wallet_address_book.append(address_entry) - + QMessageBox.information(self.ui, "Address Saved", \ - "Address (and payment ID) saved to address book.") - - + "Address (and payment ID) saved to address book.") + + @Slot(int) def generate_payment_id(self, hex_length=16): payment_id = binascii.b2a_hex(os.urandom(hex_length/2)) integrated_address = self.ui.wallet_rpc_manager.rpc_request.make_integrated_address(payment_id)["integrated_address"] self.on_generate_payment_id_event.emit(payment_id, integrated_address); - - + + @Slot(str) def copy_text(self, text): QApplication.clipboard().setText(text) - + @Slot() def load_address_book(self): if not self.ui.wallet_info.wallet_address_book: @@ -436,9 +436,9 @@ def load_address_book(self): if ret['status'] == "OK" and "entries" in ret: address_book = ret["entries"] self.ui.wallet_info.wallet_address_book = address_book - + self.on_load_address_book_completed_event.emit( json.dumps(self.ui.wallet_info.wallet_address_book) ) - + @Slot(int) def delete_address_book(self, index): result = QMessageBox.question(self.ui, "Delete Address Book Entry", \ @@ -453,8 +453,8 @@ def delete_address_book(self, index): address_book.pop(i) break self.load_address_book() - - + + def _find_tx(self, transfers, tx_id, bc_height): if transfers["status"] == "OK": if "out" in transfers: @@ -464,7 +464,7 @@ def _find_tx(self, transfers, tx_id, bc_height): tx["status"] = "out" tx["confirmation"] = bc_height - tx["height"] if bc_height > tx["height"] else 0 return tx - + if "in" in transfers: for tx in transfers["in"]: if tx['txid'] == tx_id: @@ -472,7 +472,7 @@ def _find_tx(self, transfers, tx_id, bc_height): tx["status"] = "in" tx["confirmation"] = bc_height - tx["height"] if bc_height > tx["height"] else 0 return tx - + if "pending" in transfers: for tx in transfers["pending"]: if tx['txid'] == tx_id: @@ -480,7 +480,7 @@ def _find_tx(self, transfers, tx_id, bc_height): tx["status"] = "pool" tx["confirmation"] = 0 return tx - + if "pool" in transfers: for tx in transfers["pool"]: if tx['txid'] == tx_id: @@ -489,23 +489,23 @@ def _find_tx(self, transfers, tx_id, bc_height): tx["confirmation"] = 0 return tx return None - + @Slot(int, str) def view_tx_detail(self, height, tx_id): if height > 0: transfers = self.ui.wallet_rpc_manager.rpc_request.get_transfers(filter_by_height=True, min_height=height-1, max_height=height) else: transfers = self.ui.wallet_rpc_manager.rpc_request.get_transfers(tx_pending=True, tx_in_pool=True) - + tx = self._find_tx(transfers, tx_id, self.ui.target_height) if not tx: self.on_tx_detail_found_event.emit('{"status": "ERROR"}') QMessageBox.warning(self.ui, "Transaction Details", "Transaction id: %s

not found!" % tx_id) return - + self.on_tx_detail_found_event.emit( json.dumps(tx) ) - - + + @Slot(int) def load_tx_history(self, current_page=0): if current_page <= 0: current_page = 1 @@ -516,21 +516,21 @@ def load_tx_history(self, current_page=0): for tx in txs: tx["confirmation"] = self.ui.target_height - tx["height"] \ if self.ui.target_height > tx["height"] and tx["height"] > 0 else 0 - + num_of_pages = int(len(all_txs) + txs_per_page - 1)/txs_per_page pagination_slots = 10 start_page = (int(current_page - 1)/pagination_slots)*pagination_slots + 1 end_page = start_page + (pagination_slots - 1) \ if start_page + (pagination_slots - 1) < num_of_pages else num_of_pages ret = {"txs": txs, - "current_page": current_page, + "current_page": current_page, "num_of_pages": num_of_pages, "start_page": start_page, "end_page": end_page} - + self.on_load_tx_history_completed_event.emit( json.dumps(ret) ); - - + + @Slot() def open_new_wallet(self): result = QMessageBox.question(self.ui, "Open/Create New Wallet", \ @@ -538,19 +538,19 @@ def open_new_wallet(self): QMessageBox.Yes | QMessageBox.No, defaultButton=QMessageBox.No) if result == QMessageBox.No: return - + wallet_password, result = self._custom_input_dialog(self.ui, \ "Wallet Password", "Please enter wallet password:", QLineEdit.Password) if not result: return - + if hashlib.sha256(wallet_password).hexdigest() != self.ui.wallet_info.wallet_password: QMessageBox.warning(self.ui, "Incorrect Wallet Password", "Wallet password is not correct!") return - + self.ui.show_new_wallet_ui() - - + + @Slot(str) def view_wallet_key(self, key_type): wallet_password, result = self._custom_input_dialog(self.ui, \ @@ -558,12 +558,12 @@ def view_wallet_key(self, key_type): if not result: self.on_view_wallet_key_completed_event.emit("", ""); return - + if hashlib.sha256(wallet_password).hexdigest() != self.ui.wallet_info.wallet_password: self.on_view_wallet_key_completed_event.emit("", ""); QMessageBox.warning(self.ui, "Incorrect Wallet Password", "Wallet password is not correct!") return - + ret = self.ui.wallet_rpc_manager.rpc_request.query_key(key_type) title = "Wallet Key" if key_type == "mnemonic": @@ -572,66 +572,66 @@ def view_wallet_key(self, key_type): title = "Wallet view-key" if key_type == "spend_key": title = "Wallet spend-key" - + self.on_view_wallet_key_completed_event.emit(title, ret) - - + + @Slot(int) def set_daemon_log_level(self, level): # save log level self.ui.app_settings.settings['daemon']['log_level'] = level self.ui.app_settings.save() - - + + @Slot(int) def set_block_sync_size(self, sync_size): self.ui.app_settings.settings['daemon']['block_sync_size'] = sync_size self.ui.app_settings.save() - + @Slot() def load_app_settings(self): self.on_load_app_settings_completed_event.emit( json.dumps(self.ui.app_settings.settings) ) - + @Slot() def about_app(self): self.ui.about() - + @Slot(str) def open_link(self, link): webbrowser.open(link) - + @Slot() def restart_daemon(self): self.app_process_events(1) - self.ui.sumokoind_daemon_manager.stop() + self.ui.omrbredd_daemon_manager.stop() self.ui.start_deamon() self.app_process_events(5) self.on_restart_daemon_completed_event.emit() - - + + @Slot() def view_daemon_log(self): - log_file = os.path.join(DATA_DIR, 'logs', "sumokoind.log") + log_file = os.path.join(DATA_DIR, 'logs', "ombred.log") log_dialog = LogViewer(parent=self.ui, log_file=log_file) log_dialog.load_log() - - @Slot() + + @Slot() def paste_seed_words(self): text = QApplication.clipboard().text() self.on_paste_seed_words_event.emit(text) - - + + def update_daemon_status(self, status): self.on_daemon_update_status_event.emit(status) - - + + def app_process_events(self, seconds=1): for _ in range(int(seconds*10)): self.app.processEvents() sleep(.1) - - - + + + def _detail_error_msg(self, title, error_text, error_detailed_text): msg = QMessageBox(self.new_wallet_ui) msg.setWindowTitle(title) @@ -641,22 +641,22 @@ def _detail_error_msg(self, title, error_text, error_detailed_text): msg.setIcon(QMessageBox.Critical) msg.setStandardButtons(QMessageBox.Ok) msg.exec_() - - - def _custom_input_dialog(self, ui, title, label, - text_echo_mode=QLineEdit.Normal, + + + def _custom_input_dialog(self, ui, title, label, + text_echo_mode=QLineEdit.Normal, input_mode=QInputDialog.TextInput): - dlg = QInputDialog(ui) + dlg = QInputDialog(ui) dlg.setTextEchoMode(text_echo_mode) dlg.setInputMode(input_mode) dlg.setWindowTitle(title) - dlg.setLabelText(label) - dlg.resize(250, 100) - result = dlg.exec_() + dlg.setLabelText(label) + dlg.resize(250, 100) + result = dlg.exec_() text = dlg.textValue() return (text, result) - - + + def _show_wallet_info(self): wallet_rpc_request = self.ui.wallet_rpc_manager.rpc_request wallet_info = {} @@ -666,15 +666,15 @@ def _show_wallet_info(self): balance, unlocked_balance, _ = wallet_rpc_request.get_balance() wallet_info['balance'] = print_money(balance) wallet_info['unlocked_balance'] = print_money(unlocked_balance) - + self.on_new_wallet_show_info_event.emit(json.dumps(wallet_info)) - - + + on_new_wallet_show_info_event = Signal(str) on_new_wallet_show_progress_event = Signal(str) on_new_wallet_ui_reset_event = Signal() on_new_wallet_update_processed_block_height_event = Signal(str) - + on_daemon_update_status_event = Signal(str) on_main_wallet_ui_reset_event = Signal() on_wallet_update_info_event = Signal(str) @@ -689,4 +689,4 @@ def _show_wallet_info(self): on_load_app_settings_completed_event = Signal(str) on_restart_daemon_completed_event = Signal() on_paste_seed_words_event = Signal(str) - + \ No newline at end of file diff --git a/html/index.py b/html/index.py index 8fbcc6e..a30e5af 100644 --- a/html/index.py +++ b/html/index.py @@ -17,11 +17,11 @@ -moz-box-sizing: border-box; box-sizing: border-box; } - + body { -webkit-user-select: none; user-select: none; - + cursor: default; background-color: #fff; color: #76A500; @@ -34,21 +34,21 @@ height: 100%; overflow: hidden; } - + a, a:hover, a:active, a:focus { text-decoration: none; outline: 0; cursor: default; } - + a, a:active, a:focus{ color: #337AB7; } - + a:hover{ color: #fff; } - + .nav-tabs{ /*width: 760px;*/ } @@ -57,176 +57,176 @@ text-align: center; font-size: 120% } - + .container{ width: 760px; padding: 0; margin: 5px 0px 5px 20px; } - + h3{ text-align: center; margin-bottom: 1em; font-size: 180%; } - - + + .tab-content{ font-size: 12px; } - + .tab-content h3{ margin-top: 0; } - + #balance_tab h4, #balance_tab h5{ color: #76A500; } - + #balance_tab h5 span{ color: #ccc; } - + #settings_tab h3{ margin-top: 20px; margin-bottom: 30px; } - + .syncing{ font-size: 60%; } - - .tab-content .tab-pane { + + .tab-content .tab-pane { position: relative; } - + .form-horizontal .control-label{ text-align: left; } - - + + .progress{ height: 22px; text-align: center; background: #ddd; } - + #progress_bar_text_high{ - font-size: 90%; + font-size: 90%; display: none; } - + #progress_bar_text_low{ font-size: 80%; color: #c7254e; } - + .control-label{ font-weight: bold; } - + .tx-list{ color: #666; margin-right: 20px; font-weight: bold; } - + .tx-list a{ cursor: pointer; } - + .tx-list.tx-out, .tx-list.tx-in, .tx-list.tx-pool, .tx-list.tx-pending, .tx-list.tx-out a, .tx-list.tx-out a:active, .tx-list.tx-out a:focus{ color: #c7254e; margin-bottom: 0; } - + .tx-list.tx-in, .tx-list.tx-in a, .tx-list.tx-in a:active, .tx-list.tx-in a:focus{ color: green; } - + .tx-list.tx-pool, .tx-list.tx-pending, .tx-list.tx-pending a, .tx-list.tx-pending a:active, .tx-list.tx-pending a:focus, .tx-list.tx-pool a, .tx-list.tx-pool a:active, .tx-list.tx-pool a:focus{ color: orange; } - + .tx-list a:hover{ color: #337AB7; } - + .tx-list.txid{ color: inherit; } - + .tx-list.tx-payment-id{ font-weight: normal; } - + .tx-fee-hide, .tx-note-hide, .tx-destinations-hide{ display: none; } - + .tx-list.tx-lock{ color: #666; } - + .modal-progress-text{ color: #333; font-size: 90%; font-weight: bold; margin-left: 10px; } - + #form_receive input, #form_send_tx input, #form_send_tx select{ font-size: 14px; } - + .btn-sm{ border-radius: 0; } - + table { border-spacing: 0; border-collapse: collapse; font-size: 12px; } - + table thead tr{ height: 3em; } - + table tbody tr { color: #aaa; height: 3em; line-height: 1.6em; } - + table thead tr th{ text-align: left; text-size: 18px; padding: auto 1em; } - + table tbody tr td a:hover{ color: #666; cursor: pointer; } - + .address-book-row{ cursor: pointer; } - + #address-book-box{ max-height: 450px; } - + #address-book-box table{ width: 100%; } - + #address-book-box table thead { display: inline-block; width: 100%; } - + #address-book-box table tbody { border-top: none; max-height: 300px; @@ -234,7 +234,7 @@ width: 100%; overflow: auto; } - + #address-book-box table tbody::-webkit-scrollbar-track, .tx-destinations::-webkit-scrollbar-track { @@ -242,8 +242,8 @@ background-color: #F5F5F5; border-radius: 6px; } - - + + #address-book-box table tbody::-webkit-scrollbar, .tx-destinations::-webkit-scrollbar { @@ -251,11 +251,11 @@ background-color: #F5F5F5; border-radius: 6px; } - + .tx-destinations::-webkit-scrollbar{ height: 8px; } - + #address-book-box table tbody::-webkit-scrollbar-thumb, .tx-destinations::-webkit-scrollbar-thumb { @@ -263,35 +263,35 @@ -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); background-color: #5BB0F7; } - + .tx-destinations { width: 100%; max-height: 200px; overflow: auto; font-size: 90%; } - + .wallet-settings{ text-align: center; } - + .wallet-settings button{ margin-left: 20px; } - + .form-control.address-box{ font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 85%; color: #000; } - + textarea{ border:none; width:100%; resize:none; font-weight:bold; } - + .panel-default>.panel-heading { color: #666; background-color: #eee; @@ -302,12 +302,12 @@ -ms-user-select: none; user-select: none; } - + .panel-default>.panel-heading a { display: block; padding: 10px 15px; } - + .panel-default>.panel-heading a:after { font-family:'Glyphicons Halflings'; content: ""; @@ -323,62 +323,62 @@ transition: transform .25s linear; -webkit-transition: -webkit-transform .25s linear; } - + .panel-default>.panel-heading a[aria-expanded="true"] { background-color: #2196F3; color: #fff; font-weight: bold; } - + .panel-default>.panel-heading a[aria-expanded="false"] { color: #666; } - + .panel-default>.panel-heading a[aria-expanded="true"]:after { content:"\e114"; - + } - + .panel-default>.panel-heading a[aria-expanded="false"]:after { content:"\e080"; } - + .panel-default > .panel-heading + .panel-collapse > .panel-body { height: 295px; overflow: auto; } - - + + .panel-default > .panel-heading + .panel-collapse > .panel-body::-webkit-scrollbar-track { -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); border-radius: 6px; background-color: #F5F5F5; } - - + + .panel-default > .panel-heading + .panel-collapse > .panel-body::-webkit-scrollbar { width: 8px; background-color: #F5F5F5; } - + .panel-default > .panel-heading + .panel-collapse > .panel-body::-webkit-scrollbar-thumb { border-radius: 6px; -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); background-color: #5BB0F7; } - + - + - + - + - + - + - + - + -""" +""" \ No newline at end of file diff --git a/html/newwallet.py b/html/newwallet.py index 788b727..b09d820 100644 --- a/html/newwallet.py +++ b/html/newwallet.py @@ -272,7 +272,7 @@
- +
diff --git a/main.py b/main.py index 9693434..ebc4711 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,9 @@ #!/usr/bin/python # -*- coding: utf-8 -*- ## Copyright (c) 2017, The Sumokoin Project (www.sumokoin.org) -## Copyright (c) 2018, The OMBRE Project - -import os +''' +App main function +''' import sys, os, hashlib from PySide import QtCore @@ -17,6 +17,35 @@ from app.hub import Hub from webui import MainWebUI +file_hashes = [ + ('www/scripts/jquery-1.9.1.min.js', 'c12f6098e641aaca96c60215800f18f5671039aecf812217fab3c0d152f6adb4'), + ('www/scripts/bootstrap.min.js', '2979f9a6e32fc42c3e7406339ee9fe76b31d1b52059776a02b4a7fa6a4fd280a'), + ('www/scripts/mustache.min.js', '3258bb61f5b69f33076dd0c91e13ddd2c7fe771882adff9345e90d4ab7c32426'), + ('www/scripts/jquery.qrcode.min.js', 'f4ccf02b69092819ac24575c717a080c3b6c6d6161f1b8d82bf0bb523075032d'), + ('www/scripts/utils.js', 'd0c6870ed19c92cd123c7443cb202c7629f9cd6807daed698485fda25214bdb4'), + + ('www/css/bootstrap.min.css', '9d517cad6f1744ab5eba382ccf0f53969f7d326e1336a6c2771e82830bc2c5ac'), + ('www/css/font-awesome.min.css', 'b8b02026a298258ce5069d7b6723c2034058d99220b6612b54bc0c5bf774dcfb'), + + ('www/css/fonts/fontawesome-webfont.ttf', '7b5a4320fba0d4c8f79327645b4b9cc875a2ec617a557e849b813918eb733499'), + ('www/css/fonts/glyphicons-halflings-regular.ttf', 'e395044093757d82afcb138957d06a1ea9361bdcf0b442d06a18a8051af57456'), + ('www/css/fonts/RoboReg.ttf', 'dc66a0e6527b9e41f390f157a30f96caed33c68d5db0efc6864b4f06d3a41a50'), + ] + +def _check_file_integrity(app): + ''' Check file integrity to make sure all resources loaded + to webview won't be modified by an unknown party ''' + for file_name, file_hash in file_hashes: + file_path = os.path.normpath(os.path.join(app.property("ResPath"), file_name)) + if not os.path.exists(file_path): + return False + data = readFile(file_path) +# print( file_path, hashlib.sha256(data).hexdigest() ) + if hashlib.sha256(data).hexdigest() != file_hash: + return False + + return True + def main(): if getattr(sys, "frozen", False) and sys.platform in ['win32','cygwin','win64']: @@ -27,19 +56,16 @@ def main(): sys.__stdout__ = DummyStream() sys.__stderr__ = DummyStream() sys.__stdin__ = DummyStream() - + # Get application path - - abspath = os.path.abspath(__file__) - app_path = os.path.dirname(abspath) - #os.chdir(app_path) + app_path = getAppPath() if sys.platform == 'darwin' and hasattr(sys, 'frozen'): resources_path = os.path.normpath(os.path.abspath(os.path.join(app_path, "..", "Resources"))) else: resources_path = os.path.normpath(os.path.abspath(os.path.join(app_path, "Resources"))) - + # Application setup - + app = QSingleApplication(sys.argv) app.setOrganizationName('Ombre') app.setOrganizationDomain('www.ombre.io') @@ -48,10 +74,15 @@ def main(): app.setProperty("ResPath", resources_path) if sys.platform == 'darwin': app.setAttribute(QtCore.Qt.AA_DontShowIconsInMenus) - - hub = Hub(app=app) - ui = MainWebUI(app=app, hub=hub, debug=False) - hub.setUI(ui) - app.singleStart(ui) - - sys.exit(app.exec_()) + + if not _check_file_integrity(app): + QMessageBox.critical(None, "Application Fatal Error", """File integrity check failed! +

This could be a result of unknown (maybe, malicious) action
to wallet code files.""") + app.quit() + else: + hub = Hub(app=app) + ui = MainWebUI(app=app, hub=hub, debug=False) + hub.setUI(ui) + app.singleStart(ui) + + sys.exit(app.exec_()) \ No newline at end of file diff --git a/manager/ProcessManager.py b/manager/ProcessManager.py index cd1ca18..c139773 100644 --- a/manager/ProcessManager.py +++ b/manager/ProcessManager.py @@ -1,6 +1,5 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -## Copyright (c) 2018, The OMBRE Project ## Copyright (c) 2017, The Sumokoin Project (www.sumokoin.org) from __future__ import print_function @@ -32,25 +31,25 @@ def __init__(self, proc_args, proc_name=""): Thread.__init__(self) args_array = proc_args.encode( sys.getfilesystemencoding() ).split(u' ') self.proc = Popen(args_array, - shell=False, - stdout=PIPE, stderr=STDOUT, stdin=PIPE, + shell=False, + stdout=PIPE, stderr=STDOUT, stdin=PIPE, creationflags=CREATE_NO_WINDOW) self.proc_name = proc_name self.daemon = True log("[%s] started" % proc_name, LEVEL_INFO, self.proc_name) - + def run(self): for line in iter(self.proc.stdout.readline, b''): log(">>> " + line.rstrip(), LEVEL_DEBUG, self.proc_name) - + if not self.proc.stdout.closed: self.proc.stdout.close() - + def send_command(self, cmd): self.proc.stdin.write( (cmd + u"\n").encode("utf-8") ) sleep(0.1) - - + + def stop(self): if self.is_proc_running(): self.send_command('exit') @@ -73,18 +72,18 @@ def stop(self): else: break log("[%s] stopped" % self.proc_name, LEVEL_INFO, self.proc_name) - + def is_proc_running(self): return (self.proc.poll() is None) + - -class SumokoindManager(ProcessManager): +class OmbredManager(ProcessManager): def __init__(self, resources_path, log_level=0, block_sync_size=10): proc_args = u'%s/bin/ombred --log-level %d --block-sync-size %d' % (resources_path, log_level, block_sync_size) ProcessManager.__init__(self, proc_args, "ombred") self.synced = Event() self.stopped = Event() - + def run(self): # synced_str = "You are now synchronized with the network" err_str = "ERROR" @@ -97,26 +96,26 @@ def run(self): log("[%s]>>> %s" % (self.proc_name, line.rstrip()), LEVEL_ERROR, self.proc_name) else: log("[%s]>>> %s" % (self.proc_name, line.rstrip()), LEVEL_INFO, self.proc_name) - + if not self.proc.stdout.closed: self.proc.stdout.close() - + self.stopped.set() class WalletCliManager(ProcessManager): fail_to_connect_str = "wallet failed to connect to daemon" - + def __init__(self, resources_path, wallet_file_path, wallet_log_path, restore_wallet=False): if not restore_wallet: wallet_args = u'%s/bin/ombre-wallet-cli --generate-new-wallet=%s --log-file=%s' \ % (resources_path, wallet_file_path, wallet_log_path) else: - wallet_args = u'%s/bin/ombre-wallet-cli --log-file=%s --restore-deterministic-wallet' \ + wallet_args = u'%s/bin/ombre-wallet-cli --log-file=%s --daemon-port 19744 --restore-deterministic-wallet' \ % (resources_path, wallet_log_path) ProcessManager.__init__(self, wallet_args, "ombre-wallet-cli") self.ready = Event() self.last_error = "" - + def run(self): is_ready_str = "Background refresh thread started" err_str = "Error:" @@ -129,20 +128,20 @@ def run(self): log("[%s]>>> %s" % (self.proc_name, line.rstrip()), LEVEL_ERROR, self.proc_name) # else: # log("[%s]>>> %s" % (self.proc_name, line.rstrip()), LEVEL_DEBUG, self.proc_name) - + if not self.proc.stdout.closed: self.proc.stdout.close() - + def is_ready(self): return self.ready.is_set() - - + + def is_connected(self): self.send_command("refresh") if self.fail_to_connect_str in self.last_error: return False return True - + class WalletRPCManager(ProcessManager): @@ -151,24 +150,24 @@ def __init__(self, resources_path, wallet_file_path, wallet_password, app, log_l wallet_log_path = os.path.join(os.path.dirname(wallet_file_path), "ombre-wallet-rpc.log") wallet_rpc_args = u'%s/bin/ombre-wallet-rpc --wallet-file %s --log-file %s --rpc-bind-port 19745 --user-agent %s --log-level %d' \ % (resources_path, wallet_file_path, wallet_log_path, self.user_agent, log_level) - + ProcessManager.__init__(self, wallet_rpc_args, "ombre-wallet-rpc") sleep(0.2) self.send_command(wallet_password) - + self.rpc_request = WalletRPCRequest(app, self.user_agent) # self.rpc_request.start() self.ready = False self.block_hex = None self.block_height = 0 - self.is_password_invalid = Event() - + self.is_password_invalid = Event() + def run(self): is_ready_str = "Run net_service loop" err_str = "ERROR" invalid_password = "invalid password" height_regex = re.compile(r"Processed block: \<([a-z0-9]+)\>, height (\d+)") - + for line in iter(self.proc.stdout.readline, b''): if not self.ready and is_ready_str in line: self.ready = True @@ -180,21 +179,21 @@ def run(self): self.is_password_invalid.set() else: log("[%s]>>> %s" % (self.proc_name, line.rstrip()), LEVEL_DEBUG, self.proc_name) - + m_height = height_regex.search(line) if m_height: self.block_hex = m_height.group(1) self.block_height = m_height.group(2) - + if not self.proc.stdout.closed: - self.proc.stdout.close() - + self.proc.stdout.close() + def is_ready(self): return self.ready - + def is_invalid_password(self): return self.is_password_invalid.is_set() - + def stop(self): self.rpc_request.stop_wallet() if self.is_proc_running(): @@ -211,5 +210,5 @@ def stop(self): else: break self.ready = False - log("[%s] stopped" % self.proc_name, LEVEL_INFO, self.proc_name) - + log("[%s] stopped" % self.proc_name, LEVEL_INFO, self.proc_name) + \ No newline at end of file diff --git a/rpc/__init__.py b/rpc/__init__.py index 0f0eb82..c656a3b 100644 --- a/rpc/__init__.py +++ b/rpc/__init__.py @@ -38,16 +38,16 @@ class RPCRequest(Thread): headers = {'content-type': 'application/json'} - + def __init__(self, rpc_input, url, app, user_agent=None): Thread.__init__(self) self.url = url self.rpc_input = rpc_input self.app = app - + if user_agent is not None: self.headers.update({"User-Agent": user_agent}) - + self.response_queue = Queue(1) self.daemon = True @@ -55,17 +55,17 @@ def __init__(self, rpc_input, url, app, user_agent=None): def run(self): res = self._send_request() self.response_queue.put(res) - - + + def stop(self): self.is_stopped = True - - + + def _send_request(self): global rpc_id rpc_id += 1 self.rpc_input.update({"jsonrpc": "2.0", "id": "%d" % rpc_id}) - + try: response = requests.post( self.url, @@ -91,14 +91,14 @@ def _send_request(self): break return res_json['error'] return res_json - - + + def get_result(self): while self.response_queue.empty(): self.app.processEvents() sleep(.1) return self.response_queue.get() - + class DaemonRPCRequest(): @@ -106,36 +106,36 @@ def __init__(self, app): self.port = 19744 self.url = "http://localhost:%d/json_rpc" % self.port self.app = app - + def send_request(self, rpc_input): req = RPCRequest(rpc_input, self.url, self.app) req.start() return req.get_result() - + def get_info(self): rpc_input = {"method": "get_info"} return self.send_request(rpc_input) - - + + class WalletRPCRequest(): def __init__(self, app, user_agent): self.port = 19745 self.url = "http://localhost:%d/json_rpc" % self.port self.app = app self.user_agent = user_agent - + def send_request(self, rpc_input): req = RPCRequest(rpc_input, self.url, self.app, self.user_agent) req.start() return req.get_result() - + def query_key(self, key_type="mnemonic"): rpc_input = {"method":"query_key", "params": {"key_type": key_type}} res = self.send_request(rpc_input) if res['status'] == 'OK': return res['key'] return res['status'] - + def get_address(self, account_index = 0): params = {"account_index": account_index} rpc_input = {"method": "getaddress", @@ -144,14 +144,14 @@ def get_address(self, account_index = 0): if res['status'] == 'OK': return res return res['status'] - + def create_address(self): rpc_input = {"method":"create_address"} res = self.send_request(rpc_input) if res['status'] == 'OK': return res return res['status'] - + def get_balance(self): rpc_input = {"method":"getbalance"} res = self.send_request(rpc_input) @@ -161,7 +161,7 @@ def get_balance(self): per_subaddress = res['per_subaddress'] return (res['balance'], res['unlocked_balance'], per_subaddress) return (0, 0) - + def get_transfers(self, filter_by_height=False, min_height=0, max_height=0, tx_in=True, tx_out=True, tx_pending=False, tx_in_pool=False): rpc_input = {"method":"get_transfers"} params = {} @@ -175,15 +175,15 @@ def get_transfers(self, filter_by_height=False, min_height=0, max_height=0, tx_i params["pool"] = tx_in_pool rpc_input["params"] = params return self.send_request(rpc_input) - + def rescan_spent(self): rpc_input = {"method": "rescan_spent"} return self.send_request(rpc_input) - + def rescan_bc(self): rpc_input = {"method": "rescan_blockchain"} return self.send_request(rpc_input) - + def transfer_split(self, amount, address, payment_id, priority, mixin): rpc_input = {"method": "transfer_split"} params = {"destinations": [{"amount" : amount, "address": address}], @@ -191,10 +191,10 @@ def transfer_split(self, amount, address, payment_id, priority, mixin): "mixin": mixin} if payment_id: params["payment_id"] = payment_id - + rpc_input["params"] = params return self.send_request(rpc_input) - + def transfer_all(self, address, payment_id, priority, mixin, account_index=0, subaddr_indices=[0]): rpc_input = {"method": "sweep_all"} params = { @@ -206,26 +206,26 @@ def transfer_all(self, address, payment_id, priority, mixin, account_index=0, su } if payment_id: params["payment_id"] = payment_id - + rpc_input["params"] = params return self.send_request(rpc_input) - + def set_tx_notes(self, txids, notes): rpc_input = {"method": "set_tx_notes"} params = {"txids": txids, "notes": notes} rpc_input["params"] = params return self.send_request(rpc_input) - + def make_integrated_address(self, payment_id): rpc_input = {"method": "make_integrated_address"} params = {"payment_id": payment_id} rpc_input["params"] = params return self.send_request(rpc_input) - + def get_address_book(self): rpc_input = {"method": "get_address_book"} return self.send_request(rpc_input) - + def add_address_book(self, address, payment_id, desc): rpc_input = {"method": "add_address_book"} params = {"address": address} @@ -235,14 +235,14 @@ def add_address_book(self, address, payment_id, desc): params["description"] = desc rpc_input["params"] = params return self.send_request(rpc_input) - + def delete_address_book(self, index): rpc_input = {"method": "delete_address_book"} params = {"index": index} rpc_input["params"] = params return self.send_request(rpc_input) - + def stop_wallet(self): rpc_input = {"method":"stop_wallet"} return self.send_request(rpc_input) - + \ No newline at end of file diff --git a/settings/__init__.py b/settings/__init__.py index 6324684..ebc14e2 100644 --- a/settings/__init__.py +++ b/settings/__init__.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -## Copyright (c) 2017, The Sumokoin Project (www.sumokoin.org) +## Copyright (c) 2017-2018, The Sumokoin Project (www.sumokoin.org) ''' App top-level settings ''' @@ -14,24 +14,24 @@ from utils.common import getHomeDir, makeDir USER_AGENT = "Ombre GUI Wallet" -APP_NAME = "Ombre Wallet" -VERSION = [0, 0, 1] +APP_NAME = "Ombre GUI Wallet" +VERSION = [1, 0, 0] -_data_dir = makeDir(os.path.join(getHomeDir(), 'OmbreGUIWallet')) +_data_dir = makeDir(os.path.join(getHomeDir(), 'OmbreGUI')) DATA_DIR = _data_dir log_file = os.path.join(DATA_DIR, 'logs', 'app.log') # default logging file log_level = logging.DEBUG # logging level -seed_languages = [("0", "English"), - ("1", "Spanish"), - ("2", "German"), - ("3", "Italian"), +seed_languages = [("0", "English"), + ("1", "Spanish"), + ("2", "German"), + ("3", "Italian"), ("4", "Portuguese"), ("5", "Russian"), ("6", "Japanese"), ] # COIN - number of smallest units in one coin -COIN = 1000000000.0 +COIN = 1000000000.0 \ No newline at end of file diff --git a/utils/common.py b/utils/common.py index 16fcab7..bfbe954 100644 --- a/utils/common.py +++ b/utils/common.py @@ -11,7 +11,7 @@ from cStringIO import StringIO except ImportError: from StringIO import StringIO - + class DummyStream: ''' dummyStream behaves like a stream but does nothing. ''' @@ -20,7 +20,7 @@ def write(self,data): pass def read(self,data): pass def flush(self): pass def close(self): pass - + def getAppPath(): '''Get the path to this script no matter how it's run.''' #Determine if the application is a py/pyw or a frozen exe. @@ -36,7 +36,7 @@ def getAppPath(): dir_path = os.getcwdu() return dir_path - + def getHomeDir(): if sys.platform == 'win32': import winpaths @@ -52,7 +52,7 @@ def getSockDir(): else: homedir = os.path.expanduser("~") return homedir - + def makeDir(d): if not os.path.exists(d): os.makedirs(d) @@ -76,7 +76,7 @@ def readFile(path, offset=0, size=-1, xor_data=False): data = fd.read(size) fd.close() return _xorData(data) if xor_data else data - + def writeFile(path, buf, offset=0, xor_data=False): """Write specified block on file at the given offset""" if xor_data: @@ -100,4 +100,4 @@ def print_money2(amount): except: raise Exception("Error parsing amount. Money amount must be an integer.") return "%s" % ("{:,.9f}".format(amount/1000000000.)) - + \ No newline at end of file diff --git a/webui/__init__.py b/webui/__init__.py index 1790d66..1993c0b 100644 --- a/webui/__init__.py +++ b/webui/__init__.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -## Copyright (c) 2017, The Sumokoin Project (www.sumokoin.org) +## Copyright (c) 2017-2018, The Sumokoin Project (www.sumokoin.org) ''' App UIs ''' @@ -16,7 +16,7 @@ from PySide.QtGui import QApplication, QMainWindow, QIcon, \ QSystemTrayIcon, QMenu, QAction, QMessageBox, QFileDialog, \ QInputDialog, QLineEdit - + from PySide.QtCore import QObject, Slot, Signal import PySide.QtCore as qt_core @@ -28,7 +28,12 @@ from utils.logger import log, LEVEL_DEBUG, LEVEL_ERROR, LEVEL_INFO from utils.common import readFile -from manager.ProcessManager import SumokoindManager, WalletRPCManager +from utils.notify import Notify +MSG_TYPE_INFO = 1 +MSG_TYPE_WARNING = 2 +MSG_TYPE_CRITICAL = 3 + +from manager.ProcessManager import OmbredManager, WalletRPCManager from rpc import RPCRequest, DaemonRPCRequest from classes import AppSettings, WalletInfo @@ -38,6 +43,7 @@ TIMER2_INTERVAL = 60000 MAX_NEW_SUBADDRESSES = 10 +tray_icon_tooltip = "%s v%d.%d.%s" % (APP_NAME, VERSION[0], VERSION[1], VERSION[2]) log_text_tmpl = """ @@ -66,10 +72,10 @@ def __init__(self, parent, log_file): self.view.setCursor(qt_core.Qt.ArrowCursor) self.view.setZoomFactor(1) self.setCentralWidget(self.view) - + self.log_file = log_file self.setWindowTitle("%s - Log view [%s]" % (APP_NAME, os.path.basename(log_file))) - + def load_log(self): if not os.path.exists(self.log_file): _text = "[No logs]" @@ -92,50 +98,50 @@ def __init__(self, html, app, hub, window_size, debug=False): self.html = html self.url = "file:///" \ + os.path.join(self.app.property("ResPath"), "www/index.html").replace('\\', '/') - - + + self.is_first_load = True self.view = web_core.QWebView(self) - + if not self.debug: self.view.setContextMenuPolicy(qt_core.Qt.NoContextMenu) - + self.view.setCursor(qt_core.Qt.ArrowCursor) self.view.setZoomFactor(1) - + self.setWindowTitle(APP_NAME) - self.icon = self._getQIcon('logo_black_64.png') + self.icon = self._getQIcon('ombre_icon_64.png') self.setWindowIcon(self.icon) - + self.setCentralWidget(self.view) self.setFixedSize(window_size) self.center() - + if sys.platform == 'win32': psutil.Process().nice(psutil.HIGH_PRIORITY_CLASS) - - + + def run(self): self.view.loadFinished.connect(self.load_finished) # self.view.load(qt_core.QUrl(self.url)) self.view.setHtml(self.html, qt_core.QUrl(self.url)) - - + + def center(self): frameGm = self.frameGeometry() screen = QApplication.desktop().screenNumber(QApplication.desktop().cursor().pos()) centerPoint = QApplication.desktop().screenGeometry(screen).center() frameGm.moveCenter(centerPoint) self.move(frameGm.topLeft()) - + def load_finished(self): - #This is the actual context/frame a webpage is running in. + #This is the actual context/frame a webpage is running in. # Other frames could include iframes or such. main_page = self.view.page() main_frame = main_page.mainFrame() # ATTENTION here's the magic that sets a bridge between Python to HTML main_frame.addToJavaScriptWindowObject("app_hub", self.hub) - + if self.is_first_load: ## Avoid re-settings on page reload (if happened) change_setting = main_page.settings().setAttribute settings = web_core.QWebSettings @@ -145,23 +151,23 @@ def load_finished(self): change_setting(settings.OfflineWebApplicationCacheEnabled, True) change_setting(settings.JavascriptCanOpenWindows, True) change_setting(settings.PluginsEnabled, False) - + # Show web inspector if debug on if self.debug: self.inspector = web_core.QWebInspector() self.inspector.setPage(self.view.page()) self.inspector.show() - + self.is_first_load = False - + #Tell the HTML side, we are open for business main_frame.evaluateJavaScript("app_ready()") - + def _getQIcon(self, icon_file): return QIcon(os.path.join(self.app.property("ResPath"), 'icons', icon_file)) - + class NewWalletWebUI(BaseWebUI): def __init__(self, app, hub, debug): window_size = qt_core.QSize(810, 560) @@ -169,119 +175,181 @@ def __init__(self, app, hub, debug): self.setWindowFlags(qt_core.Qt.FramelessWindowHint) self.show() - + class MainWebUI(BaseWebUI): def __init__(self, app, hub, debug): window_size = qt_core.QSize(800, 600) BaseWebUI.__init__(self, index.html, app, hub, window_size, debug) self.agent = '%s v.%s' % (USER_AGENT, '.'.join(str(v) for v in VERSION)) log("Starting [%s]..." % self.agent, LEVEL_INFO) - + + # Setup the system tray icon + if sys.platform == 'darwin': + tray_icon = 'ombre_16x16_mac.png' + elif sys.platform == "win32": + tray_icon = 'ombre_16x16.png' + else: + tray_icon = 'ombre_32x32_ubuntu.png' + + self.trayIcon = QSystemTrayIcon(self._getQIcon(tray_icon)) + self.trayIcon.setToolTip(tray_icon_tooltip) + self.app = app self.debug = debug self.hub = hub - + self.app.aboutToQuit.connect(self._handleAboutToQuit) - - self.sumokoind_daemon_manager = None + + self.ombred_daemon_manager = None self.wallet_cli_manager = None self.wallet_rpc_manager = None - + self.new_wallet_ui = None - + self.wallet_info = WalletInfo(app) - + # load app settings self.app_settings = AppSettings() self.app_settings.load() - + ## Blockchain height self.target_height = self.app_settings.settings['blockchain']['height'] self.current_height = 0 - - - - + + # Setup the tray icon context menu + self.trayMenu = QMenu() + + self.showAppAction = QAction('&Show %s' % APP_NAME, self) + f = self.showAppAction.font() + f.setBold(True) + self.showAppAction.setFont(f) + self.trayMenu.addAction(self.showAppAction) + + + self.aboutAction = QAction('&About...', self) + self.trayMenu.addAction(self.aboutAction) + + self.trayMenu.addSeparator() + self.exitAction = QAction('&Exit', self) + self.trayMenu.addAction(self.exitAction) + # Add menu to tray icon + self.trayIcon.setContextMenu(self.trayMenu) + + # connect signals + self.trayIcon.activated.connect(self._handleTrayIconActivate) + self.exitAction.triggered.connect(self.handleExitAction) + self.aboutAction.triggered.connect(self.handleAboutAction) + self.showAppAction.triggered.connect(self._handleShowAppAction) + self.app.aboutToQuit.connect(self._handleAboutToQuit) + + # Setup notification support + self.system_tray_running_notified = False + self.notifier = Notify(APP_NAME) + self.trayIcon.show() + + + def closeEvent(self, event): + """ Override QT close event + """ + event.ignore() + self.hide() + if not self.system_tray_running_notified: + self.notify("%s is still running at system tray." % APP_NAME, + "Running Status") + self.system_tray_running_notified = True + def run(self): self.view.loadFinished.connect(self.load_finished) # self.view.load(qt_core.QUrl(self.url)) self.view.setHtml(self.html, qt_core.QUrl(self.url)) - + self.start_deamon() self.daemon_rpc_request = DaemonRPCRequest(self.app) - + self.timer = QTimer(self) self.timer.timeout.connect(self._update_daemon_status) self.timer.start(10000) - + QTimer.singleShot(1000, self._load_wallet) QTimer.singleShot(2000, self._update_daemon_status) - - + + def start_deamon(self): - #start sumokoind daemon - self.sumokoind_daemon_manager = SumokoindManager(self.app.property("ResPath"), - self.app_settings.settings['daemon']['log_level'], + #start ombred daemon + self.ombred_daemon_manager = OmbredManager(self.app.property("ResPath"), + self.app_settings.settings['daemon']['log_level'], self.app_settings.settings['daemon']['block_sync_size']) - - self.sumokoind_daemon_manager.start() - - + + self.ombred_daemon_manager.start() + + def show_wallet(self): QTimer.singleShot(1000, self.update_wallet_info) if not hasattr(self, "timer2"): self.timer2 = QTimer(self) self.timer2.timeout.connect(self.update_wallet_info) - + self.timer2.stop() self.timer2.start(TIMER2_INTERVAL) self.show() - - + self.trayIcon.show() + + + def hide_wallet(self): + self.hide() + self.trayIcon.hide() + def run_wallet_rpc(self, wallet_password, log_level=0): # first, try to stop any wallet RPC server running try: RPCRequest('wallet_rpc').send_request({"method":"stop_wallet"}) except: pass - + while True: self.hub.app_process_events() - sumokoind_info = self.daemon_rpc_request.get_info() - if sumokoind_info['status'] == "OK": + ombred_info = self.daemon_rpc_request.get_info() + if ombred_info['status'] == "OK": self.wallet_rpc_manager = WalletRPCManager(self.app.property("ResPath"), \ self.wallet_info.wallet_filepath, \ wallet_password, \ self.app, log_level) self.wallet_rpc_manager.start() break - + def _update_daemon_status(self): target_height = 0 - sumokoind_info = self.daemon_rpc_request.get_info() - if sumokoind_info['status'] == "OK": + ombred_info = self.daemon_rpc_request.get_info() + if ombred_info['status'] == "OK": status = "Connected" - self.current_height = int(sumokoind_info['height']) - target_height = int(sumokoind_info['target_height']) + self.current_height = int(ombred_info['height']) + target_height = int(ombred_info['target_height']) if target_height == 0 or target_height < self.current_height: target_height = self.current_height if self.target_height < target_height: self.target_height = target_height; else: - status = sumokoind_info['status'] - - info = {"status": status, - "current_height": self.current_height, + status = ombred_info['status'] + + info = {"status": status, + "current_height": self.current_height, "target_height": self.target_height, } - + self.hub.update_daemon_status(json.dumps(info)) - - + + sync_status = "Disconnected" if ombred_info['status'] != "OK" else "Synchronizing..." + if ombred_info['status'] == "OK" and self.current_height == self.target_height: + sync_status = "Network synchronized" + + self.trayIcon.setToolTip("%s\n%s (%d/%d)" % (tray_icon_tooltip, sync_status, + self.current_height, self.target_height)) + + def update_wallet_info(self): if not self.wallet_rpc_manager.is_ready(): return - + wallet_info = {} try: balance, unlocked_balance, per_subaddress = self.wallet_rpc_manager.rpc_request.get_balance() @@ -301,19 +369,19 @@ def update_wallet_info(self): tx["status"] = "out" txs.append(tx) self.app.processEvents() - + if "in" in transfers: for tx in transfers["in"]: tx["direction"] = "in" tx["status"] = "in" txs.append(tx) self.app.processEvents() - + sorted_txs = sorted(txs, key=lambda k: k['height']) self.wallet_info.add_transfers(sorted_txs) self.wallet_info.bc_height = self.current_height - - pending_transfers = self.wallet_rpc_manager.rpc_request.get_transfers(tx_pending=True, tx_in_pool=True) + + pending_transfers = self.wallet_rpc_manager.rpc_request.get_transfers(tx_pending=True, tx_in_pool=True) pending_txs = [] if pending_transfers["status"] == "OK": if "pending" in pending_transfers: @@ -323,7 +391,7 @@ def update_wallet_info(self): tx["confirmation"] = 0 pending_txs.append(tx) self.app.processEvents() - + if "pool" in pending_transfers: for tx in pending_transfers["pool"]: tx["direction"] = "in" @@ -331,21 +399,21 @@ def update_wallet_info(self): tx["confirmation"] = 0 pending_txs.append(tx) self.app.processEvents() - - self.wallet_info.wallet_pending_transfers = sorted(pending_txs, - key=lambda k: k['timestamp'], + + self.wallet_info.wallet_pending_transfers = sorted(pending_txs, + key=lambda k: k['timestamp'], reverse=True) - + if len(self.wallet_info.wallet_pending_transfers) > 0: wallet_info["recent_txs"] = self.wallet_info.wallet_pending_transfers[:2] else: wallet_info["recent_txs"] = [] - + if len(wallet_info["recent_txs"]) < 2: for tx in self.wallet_info.wallet_transfers[:2 - len(wallet_info["recent_txs"])]: tx["confirmation"] = self.target_height - tx["height"] if self.target_height > tx["height"] else 0 wallet_info["recent_txs"].append(tx) - + adddress_info = self.wallet_rpc_manager.rpc_request.get_address() wallet_info['address'] = adddress_info['address'] wallet_info['used_subaddresses'] = [] @@ -364,11 +432,11 @@ def update_wallet_info(self): else: if subaddress['address_index'] > 0: wallet_info['new_subaddresses'].append(subaddress) - - wallet_info['used_subaddresses'] = sorted(wallet_info['used_subaddresses'], - key=lambda k:k['balance'], + + wallet_info['used_subaddresses'] = sorted(wallet_info['used_subaddresses'], + key=lambda k:k['balance'], reverse=True) - + # auto-generate new subaddresses if not enough available if len(wallet_info['new_subaddresses']) < MAX_NEW_SUBADDRESSES: for _ in range(MAX_NEW_SUBADDRESSES - len(wallet_info['new_subaddresses'])): @@ -376,29 +444,29 @@ def update_wallet_info(self): new_subaddress['label'] = "" new_subaddress['used'] = False wallet_info['new_subaddresses'].append(new_subaddress) - + if len(wallet_info['new_subaddresses']) > MAX_NEW_SUBADDRESSES: wallet_info['new_subaddresses'] = wallet_info['new_subaddresses'][0:MAX_NEW_SUBADDRESSES] - + # print(json.dumps(wallet_info, indent=4)) - + self.hub.on_wallet_update_info_event.emit(json.dumps(wallet_info)) except Exception, err: log(str(err), LEVEL_ERROR) - - + + def show_new_wallet_ui(self): self.reset_wallet(delete_files=False) - self.hide() + self.hide_wallet() self.new_wallet_ui = NewWalletWebUI(self.app, self.hub, self.debug) self.hub.setNewWalletUI(self.new_wallet_ui) self.new_wallet_ui.run() - - + + def reset_wallet(self, delete_files=True): if self.wallet_rpc_manager is not None: self.wallet_rpc_manager.stop() - + wallet_filepath = self.wallet_info.wallet_filepath if delete_files and wallet_filepath and os.path.exists(wallet_filepath): try: @@ -407,18 +475,39 @@ def reset_wallet(self, delete_files=True): os.remove(wallet_filepath + ".address.txt") except: pass - + self.wallet_info.reset() if self.new_wallet_ui: self.hub.on_new_wallet_ui_reset_event.emit() self.hub.on_main_wallet_ui_reset_event.emit() - + + def notify(self, message, title="", icon=None, msg_type=None): + if self.notifier.notifier is not None: + self.notifier.notify(title, message, icon) + else: + self.showMessage(message, title, msg_type) + + def showMessage(self, message, title="", msg_type=None, timeout=2000): + """Displays 'message' through the tray icon's showMessage function, + with title 'title'. 'type' is one of the enumerations of + 'common.MessageTypes'. + """ + if msg_type is None or msg_type == MSG_TYPE_INFO: + icon = QSystemTrayIcon.Information + + elif msg_type == MSG_TYPE_WARNING: + icon = QSystemTrayIcon.Warning + + elif msg_type == MSG_TYPE_CRITICAL: + icon = QSystemTrayIcon.Critical + + title = "%s - %s" % (APP_NAME, title) if title else APP_NAME + self.trayIcon.showMessage(title, message, icon, timeout) + def about(self): - pass - QMessageBox.about(self, "About", - "

Copyright© 2017 - Sumokoin Projects (www.sumokoin.org)
" + - "

Copyright© 2018 - Ombre Project (www.ombre.io)
") - + QMessageBox.about(self, "About", \ + u"%s

Copyright© 2017 -2018 - Sumokoin Projects (www.sumokoin.org)" % self.agent) + def _load_wallet(self): if self.wallet_info.load(): wallet_password = None @@ -435,13 +524,14 @@ def _load_wallet(self): "Password is required to open wallet!") else: break - + if not wallet_password: result = QMessageBox.question(self, "Create/Restore Wallet?", \ "Do you want to create (or restore to) a new wallet instead?", \ QMessageBox.Yes | QMessageBox.No, defaultButton=QMessageBox.No) if result == QMessageBox.No: - self.close() + self.trayIcon.hide() + QTimer.singleShot(250, self.app.quit) else: self.show_new_wallet_ui() return @@ -455,22 +545,47 @@ def _load_wallet(self): "Error: Wallet password is incorrect!

Please retry...") self._load_wallet() return - + self.wallet_info.wallet_password = hashlib.sha256(wallet_password).hexdigest() self.update_wallet_info() if not hasattr(self, "timer2"): self.timer2 = QTimer(self) self.timer2.timeout.connect(self.update_wallet_info) - + self.timer2.stop() self.timer2.start(TIMER2_INTERVAL) else: + self.hide_wallet() self.new_wallet_ui = NewWalletWebUI(self.app, self.hub, self.debug) self.hub.setNewWalletUI(self.new_wallet_ui) self.new_wallet_ui.run() - - + + def _handleTrayIconActivate(self, reason): + if reason == QSystemTrayIcon.DoubleClick: + self.showNormal() + self.activateWindow() + + def handleExitAction(self, show_confirmation=True): + reply = QMessageBox.No + if show_confirmation: + self._handleShowAppAction() + reply=QMessageBox.question(self,'Exit %s?' % APP_NAME, + "Are you sure to exit %s?" % APP_NAME, QMessageBox.Yes,QMessageBox.No) + if not show_confirmation or reply==QMessageBox.Yes: + self.trayIcon.hide() + QTimer.singleShot(250, self.app.quit) + + def _handleShowAppAction(self): + self.showNormal() + self.activateWindow() + + def handleAboutAction(self): + self._handleShowAppAction() + self.about() + + def _handleAboutToQuit(self): + self.hide_wallet() log("%s is about to quit..." % APP_NAME, LEVEL_INFO) if hasattr(self, "timer"): self.timer.stop() @@ -478,8 +593,8 @@ def _handleAboutToQuit(self): self.timer2.stop() if self.wallet_rpc_manager is not None: self.wallet_rpc_manager.stop() - if self.sumokoind_daemon_manager is not None: - self.sumokoind_daemon_manager.stop() - + if self.ombred_daemon_manager is not None: + self.ombred_daemon_manager.stop() + self.app_settings.settings['blockchain']['height'] = self.target_height self.app_settings.save()