tfile reorganization with top-level module - electrum - Electrum Bitcoin wallet
HTML git clone https://git.parazyd.org/electrum
DIR Log
DIR Files
DIR Refs
DIR Submodules
---
DIR commit 097ac144d976eb46dff809e1809783dc78ab6d8b
DIR parent 30a7952cbb2e1c59c5eabaaee64d8a4e15a1b0cb
HTML Author: Janus <ysangkok@gmail.com>
Date: Wed, 11 Jul 2018 17:38:47 +0200
file reorganization with top-level module
Diffstat:
M .gitignore | 3 +--
M README.rst | 6 +++---
M contrib/build-osx/make_osx | 4 ++--
M contrib/build-osx/osx.spec | 193 +++++++++++++++----------------
M contrib/build-wine/build-electrum-… | 4 ++--
M contrib/build-wine/deterministic.s… | 41 +++++++++++++++----------------
M contrib/make_apk | 2 +-
M contrib/make_locale | 11 +++++------
D electrum | 480 -------------------------------
M electrum-env | 2 +-
A electrum/__init__.py | 14 ++++++++++++++
A electrum/base_crash_reporter.py | 128 +++++++++++++++++++++++++++++++
R lib/base_wizard.py -> electrum/bas… | 0
R lib/bitcoin.py -> electrum/bitcoin… | 0
R lib/blockchain.py -> electrum/bloc… | 0
R lib/checkpoints.json -> electrum/c… | 0
R lib/checkpoints_testnet.json -> el… | 0
R lib/coinchooser.py -> electrum/coi… | 0
A electrum/commands.py | 892 ++++++++++++++++++++++++++++++
R lib/constants.py -> electrum/const… | 0
R lib/contacts.py -> electrum/contac… | 0
R lib/crypto.py -> electrum/crypto.py | 0
R lib/currencies.json -> electrum/cu… | 0
A electrum/daemon.py | 316 +++++++++++++++++++++++++++++++
R lib/dnssec.py -> electrum/dnssec.py | 0
R lib/ecc.py -> electrum/ecc.py | 0
R lib/ecc_fast.py -> electrum/ecc_fa… | 0
A electrum/electrum | 2 ++
A electrum/exchange_rate.py | 573 +++++++++++++++++++++++++++++++
R gui/__init__.py -> electrum/gui/__… | 0
A electrum/gui/kivy/Makefile | 32 +++++++++++++++++++++++++++++++
R gui/kivy/Readme.md -> electrum/gui… | 0
R gui/kivy/__init__.py -> electrum/g… | 0
R gui/kivy/data/background.png -> el… | 0
R gui/kivy/data/fonts/Roboto-Bold.tt… | 0
R gui/kivy/data/fonts/Roboto-Condens… | 0
R gui/kivy/data/fonts/Roboto-Medium.… | 0
R gui/kivy/data/fonts/Roboto.ttf -> … | 0
R gui/kivy/data/fonts/tron/License.t… | 0
R gui/kivy/data/fonts/tron/Readme.tx… | 0
R gui/kivy/data/fonts/tron/Tr2n.ttf … | 0
R gui/kivy/data/glsl/default.fs -> e… | 0
R gui/kivy/data/glsl/default.png -> … | 0
R gui/kivy/data/glsl/default.vs -> e… | 0
R gui/kivy/data/glsl/header.fs -> el… | 0
R gui/kivy/data/glsl/header.vs -> el… | 0
R gui/kivy/data/images/defaulttheme-… | 0
R gui/kivy/data/images/defaulttheme.… | 0
R gui/kivy/data/java-classes/org/ele… | 0
R gui/kivy/data/logo/kivy-icon-32.pn… | 0
R gui/kivy/data/style.kv -> electrum… | 0
R gui/kivy/i18n.py -> electrum/gui/k… | 0
A electrum/gui/kivy/main.kv | 464 ++++++++++++++++++++++++++++++
A electrum/gui/kivy/main_window.py | 1028 +++++++++++++++++++++++++++++++
A electrum/gui/kivy/nfc_scanner/__in… | 44 +++++++++++++++++++++++++++++++
A electrum/gui/kivy/nfc_scanner/scan… | 242 +++++++++++++++++++++++++++++++
A electrum/gui/kivy/nfc_scanner/scan… | 52 +++++++++++++++++++++++++++++++
R gui/kivy/theming/light/action_bar.… | 0
R gui/kivy/theming/light/action_butt… | 0
R gui/kivy/theming/light/action_grou… | 0
R gui/kivy/theming/light/action_grou… | 0
R gui/kivy/theming/light/add_contact… | 0
R gui/kivy/theming/light/arrow_back.… | 0
R gui/kivy/theming/light/bit_logo.pn… | 0
R gui/kivy/theming/light/blue_bg_rou… | 0
R gui/kivy/theming/light/btn_create_… | 0
R gui/kivy/theming/light/btn_create_… | 0
R gui/kivy/theming/light/btn_nfc.png… | 0
R gui/kivy/theming/light/btn_send_ad… | 0
R gui/kivy/theming/light/btn_send_nf… | 0
R gui/kivy/theming/light/calculator.… | 0
R gui/kivy/theming/light/camera.png … | 0
R gui/kivy/theming/light/card.png ->… | 0
R gui/kivy/theming/light/card_bottom… | 0
R gui/kivy/theming/light/card_btn.pn… | 0
R gui/kivy/theming/light/card_top.pn… | 0
R gui/kivy/theming/light/carousel_de… | 0
R gui/kivy/theming/light/carousel_se… | 0
R gui/kivy/theming/light/clock1.png … | 0
R gui/kivy/theming/light/clock2.png … | 0
R gui/kivy/theming/light/clock3.png … | 0
R gui/kivy/theming/light/clock4.png … | 0
R gui/kivy/theming/light/clock5.png … | 0
R gui/kivy/theming/light/close.png -… | 0
R gui/kivy/theming/light/closebutton… | 0
R gui/kivy/theming/light/confirmed.p… | 0
R gui/kivy/theming/light/contact.png… | 0
R gui/kivy/theming/light/contact_ove… | 0
R gui/kivy/theming/light/create_act_… | 0
R gui/kivy/theming/light/create_act_… | 0
R gui/kivy/theming/light/dialog.png … | 0
R gui/kivy/theming/light/dropdown_ba… | 0
R gui/kivy/theming/light/electrum_ic… | 0
R gui/kivy/theming/light/error.png -… | 0
R gui/kivy/theming/light/gear.png ->… | 0
R gui/kivy/theming/light/globe.png -… | 0
R gui/kivy/theming/light/icon_border… | 0
R gui/kivy/theming/light/important.p… | 0
R gui/kivy/theming/light/info.png ->… | 0
R gui/kivy/theming/light/lightblue_b… | 0
R gui/kivy/theming/light/logo.png ->… | 0
R gui/kivy/theming/light/logo_atom_d… | 0
R gui/kivy/theming/light/mail_icon.p… | 0
R gui/kivy/theming/light/manualentry… | 0
R gui/kivy/theming/light/network.png… | 0
R gui/kivy/theming/light/nfc.png -> … | 0
R gui/kivy/theming/light/nfc_clock.p… | 0
R gui/kivy/theming/light/nfc_phone.p… | 0
R gui/kivy/theming/light/nfc_stage_o… | 0
R gui/kivy/theming/light/overflow_ba… | 0
R gui/kivy/theming/light/overflow_bt… | 0
R gui/kivy/theming/light/paste_icon.… | 0
R gui/kivy/theming/light/pen.png -> … | 0
R gui/kivy/theming/light/qrcode.png … | 0
R gui/kivy/theming/light/save.png ->… | 0
R gui/kivy/theming/light/settings.pn… | 0
R gui/kivy/theming/light/shadow.png … | 0
R gui/kivy/theming/light/shadow_righ… | 0
R gui/kivy/theming/light/share.png -… | 0
R gui/kivy/theming/light/star_big_in… | 0
R gui/kivy/theming/light/stepper_ful… | 0
R gui/kivy/theming/light/stepper_lef… | 0
R gui/kivy/theming/light/stepper_res… | 0
R gui/kivy/theming/light/stepper_res… | 0
R gui/kivy/theming/light/tab.png -> … | 0
R gui/kivy/theming/light/tab_btn.png… | 0
R gui/kivy/theming/light/tab_btn_dis… | 0
R gui/kivy/theming/light/tab_btn_pre… | 0
R gui/kivy/theming/light/tab_disable… | 0
R gui/kivy/theming/light/tab_strip.p… | 0
R gui/kivy/theming/light/textinput_a… | 0
R gui/kivy/theming/light/unconfirmed… | 0
R gui/kivy/theming/light/wallet.png … | 0
R gui/kivy/theming/light/wallets.png… | 0
R gui/kivy/theming/light/white_bg_ro… | 0
R gui/kivy/tools/bitcoin_intent.xml … | 0
R gui/kivy/tools/blacklist.txt -> el… | 0
R gui/kivy/tools/buildozer.spec -> e… | 0
R gui/kivy/uix/__init__.py -> electr… | 0
R gui/kivy/uix/combobox.py -> electr… | 0
A electrum/gui/kivy/uix/context_menu… | 56 +++++++++++++++++++++++++++++++
A electrum/gui/kivy/uix/dialogs/__in… | 220 +++++++++++++++++++++++++++++++
A electrum/gui/kivy/uix/dialogs/addr… | 180 +++++++++++++++++++++++++++++++
R gui/kivy/uix/dialogs/amount_dialog… | 0
A electrum/gui/kivy/uix/dialogs/bump… | 118 +++++++++++++++++++++++++++++++
R gui/kivy/uix/dialogs/checkbox_dial… | 0
R gui/kivy/uix/dialogs/choice_dialog… | 0
R gui/kivy/uix/dialogs/crash_reporte… | 0
A electrum/gui/kivy/uix/dialogs/fee_… | 131 +++++++++++++++++++++++++++++++
A electrum/gui/kivy/uix/dialogs/fx_d… | 111 ++++++++++++++++++++++++++++++
A electrum/gui/kivy/uix/dialogs/inst… | 1038 +++++++++++++++++++++++++++++++
A electrum/gui/kivy/uix/dialogs/invo… | 169 +++++++++++++++++++++++++++++++
A electrum/gui/kivy/uix/dialogs/labe… | 55 +++++++++++++++++++++++++++++++
A electrum/gui/kivy/uix/dialogs/nfc_… | 33 +++++++++++++++++++++++++++++++
A electrum/gui/kivy/uix/dialogs/pass… | 142 +++++++++++++++++++++++++++++++
R gui/kivy/uix/dialogs/qr_dialog.py … | 0
A electrum/gui/kivy/uix/dialogs/qr_s… | 44 +++++++++++++++++++++++++++++++
A electrum/gui/kivy/uix/dialogs/ques… | 53 ++++++++++++++++++++++++++++++
A electrum/gui/kivy/uix/dialogs/requ… | 157 +++++++++++++++++++++++++++++++
R gui/kivy/uix/dialogs/seed_options.… | 0
A electrum/gui/kivy/uix/dialogs/sett… | 220 +++++++++++++++++++++++++++++++
A electrum/gui/kivy/uix/dialogs/tx_d… | 184 +++++++++++++++++++++++++++++++
R gui/kivy/uix/dialogs/wallets.py ->… | 0
R gui/kivy/uix/drawer.py -> electrum… | 0
R gui/kivy/uix/gridview.py -> electr… | 0
A electrum/gui/kivy/uix/menus.py | 95 ++++++++++++++++++++++++++++++
R gui/kivy/uix/qrcodewidget.py -> el… | 0
A electrum/gui/kivy/uix/screens.py | 484 +++++++++++++++++++++++++++++++
R gui/kivy/uix/ui_screens/about.kv -… | 0
A electrum/gui/kivy/uix/ui_screens/h… | 78 +++++++++++++++++++++++++++++++
R gui/kivy/uix/ui_screens/invoice.kv… | 0
R gui/kivy/uix/ui_screens/network.kv… | 0
R gui/kivy/uix/ui_screens/proxy.kv -… | 0
A electrum/gui/kivy/uix/ui_screens/r… | 142 +++++++++++++++++++++++++++++++
A electrum/gui/kivy/uix/ui_screens/s… | 127 +++++++++++++++++++++++++++++++
R gui/kivy/uix/ui_screens/server.kv … | 0
R gui/kivy/uix/ui_screens/status.kv … | 0
A electrum/gui/qt/__init__.py | 313 +++++++++++++++++++++++++++++++
R gui/qt/address_dialog.py -> electr… | 0
A electrum/gui/qt/address_list.py | 195 +++++++++++++++++++++++++++++++
R gui/qt/amountedit.py -> electrum/g… | 0
A electrum/gui/qt/completion_text_ed… | 120 +++++++++++++++++++++++++++++++
R gui/qt/console.py -> electrum/gui/… | 0
A electrum/gui/qt/contact_list.py | 98 +++++++++++++++++++++++++++++++
R gui/qt/exception_window.py -> elec… | 0
R gui/qt/fee_slider.py -> electrum/g… | 0
R gui/qt/history_list.py -> electrum… | 0
A electrum/gui/qt/installwizard.py | 644 +++++++++++++++++++++++++++++++
R gui/qt/invoice_list.py -> electrum… | 0
A electrum/gui/qt/main_window.py | 3220 +++++++++++++++++++++++++++++++
R gui/qt/network_dialog.py -> electr… | 0
A electrum/gui/qt/password_dialog.py | 305 +++++++++++++++++++++++++++++++
R gui/qt/paytoedit.py -> electrum/gu… | 0
R gui/qt/qrcodewidget.py -> electrum… | 0
A electrum/gui/qt/qrtextedit.py | 76 +++++++++++++++++++++++++++++++
A electrum/gui/qt/qrwindow.py | 89 +++++++++++++++++++++++++++++++
A electrum/gui/qt/request_list.py | 129 +++++++++++++++++++++++++++++++
A electrum/gui/qt/seed_dialog.py | 211 +++++++++++++++++++++++++++++++
A electrum/gui/qt/transaction_dialog… | 328 +++++++++++++++++++++++++++++++
R gui/qt/util.py -> electrum/gui/qt/… | 0
R gui/qt/utxo_list.py -> electrum/gu… | 0
R gui/stdio.py -> electrum/gui/stdio… | 0
A electrum/gui/text.py | 503 +++++++++++++++++++++++++++++++
A electrum/i18n.py | 81 ++++++++++++++++++++++++++++++
A electrum/interface.py | 407 +++++++++++++++++++++++++++++++
A electrum/jsonrpc.py | 98 +++++++++++++++++++++++++++++++
A electrum/keystore.py | 798 ++++++++++++++++++++++++++++++
A electrum/mnemonic.py | 183 +++++++++++++++++++++++++++++++
A electrum/msqr.py | 94 +++++++++++++++++++++++++++++++
A electrum/network.py | 1297 +++++++++++++++++++++++++++++++
A electrum/old_mnemonic.py | 1697 +++++++++++++++++++++++++++++++
A electrum/paymentrequest.proto | 47 +++++++++++++++++++++++++++++++
A electrum/paymentrequest.py | 523 +++++++++++++++++++++++++++++++
A electrum/paymentrequest_pb2.py | 367 ++++++++++++++++++++++++++++++
A electrum/pem.py | 191 +++++++++++++++++++++++++++++++
A electrum/plot.py | 63 +++++++++++++++++++++++++++++++
A electrum/plugin.py | 566 +++++++++++++++++++++++++++++++
A electrum/plugins/README | 31 +++++++++++++++++++++++++++++++
A electrum/plugins/__init__.py | 26 ++++++++++++++++++++++++++
A electrum/plugins/audio_modem/__ini… | 7 +++++++
A electrum/plugins/audio_modem/qt.py | 128 +++++++++++++++++++++++++++++++
A electrum/plugins/cosigner_pool/__i… | 9 +++++++++
A electrum/plugins/cosigner_pool/qt.… | 228 +++++++++++++++++++++++++++++++
A electrum/plugins/digitalbitbox/__i… | 6 ++++++
A electrum/plugins/digitalbitbox/cmd… | 14 ++++++++++++++
A electrum/plugins/digitalbitbox/dig… | 767 +++++++++++++++++++++++++++++++
A electrum/plugins/digitalbitbox/qt.… | 43 ++++++++++++++++++++++++++++++
A electrum/plugins/email_requests/__… | 5 +++++
A electrum/plugins/email_requests/qt… | 271 +++++++++++++++++++++++++++++++
A electrum/plugins/greenaddress_inst… | 5 +++++
A electrum/plugins/greenaddress_inst… | 107 +++++++++++++++++++++++++++++++
A electrum/plugins/hw_wallet/__init_… | 2 ++
A electrum/plugins/hw_wallet/cmdline… | 46 +++++++++++++++++++++++++++++++
A electrum/plugins/hw_wallet/plugin.… | 89 +++++++++++++++++++++++++++++++
A electrum/plugins/hw_wallet/qt.py | 235 +++++++++++++++++++++++++++++++
A electrum/plugins/keepkey/__init__.… | 7 +++++++
A electrum/plugins/keepkey/client.py | 14 ++++++++++++++
A electrum/plugins/keepkey/clientbas… | 250 +++++++++++++++++++++++++++++++
A electrum/plugins/keepkey/cmdline.py | 14 ++++++++++++++
A electrum/plugins/keepkey/keepkey.py | 438 +++++++++++++++++++++++++++++++
A electrum/plugins/keepkey/qt.py | 586 ++++++++++++++++++++++++++++++
A electrum/plugins/labels/__init__.py | 9 +++++++++
A electrum/plugins/labels/cmdline.py | 11 +++++++++++
A electrum/plugins/labels/kivy.py | 14 ++++++++++++++
A electrum/plugins/labels/labels.py | 167 +++++++++++++++++++++++++++++++
A electrum/plugins/labels/qt.py | 78 +++++++++++++++++++++++++++++++
A electrum/plugins/ledger/__init__.py | 7 +++++++
A electrum/plugins/ledger/auth2fa.py | 358 +++++++++++++++++++++++++++++++
A electrum/plugins/ledger/cmdline.py | 14 ++++++++++++++
A electrum/plugins/ledger/ledger.py | 637 +++++++++++++++++++++++++++++++
A electrum/plugins/ledger/qt.py | 81 ++++++++++++++++++++++++++++++
A electrum/plugins/revealer/DejaVuSa… | 0
A electrum/plugins/revealer/LICENSE_… | 99 +++++++++++++++++++++++++++++++
A electrum/plugins/revealer/SIL Open… | 44 +++++++++++++++++++++++++++++++
A electrum/plugins/revealer/SourceSa… | 0
A electrum/plugins/revealer/__init__… | 16 ++++++++++++++++
A electrum/plugins/revealer/qt.py | 724 +++++++++++++++++++++++++++++++
A electrum/plugins/trezor/__init__.py | 8 ++++++++
A electrum/plugins/trezor/client.py | 11 +++++++++++
A electrum/plugins/trezor/clientbase… | 265 +++++++++++++++++++++++++++++++
A electrum/plugins/trezor/cmdline.py | 14 ++++++++++++++
A electrum/plugins/trezor/qt.py | 613 +++++++++++++++++++++++++++++++
A electrum/plugins/trezor/transport.… | 95 ++++++++++++++++++++++++++++++
A electrum/plugins/trezor/trezor.py | 516 +++++++++++++++++++++++++++++++
A electrum/plugins/trustedcoin/__ini… | 11 +++++++++++
A electrum/plugins/trustedcoin/cmdli… | 45 +++++++++++++++++++++++++++++++
A electrum/plugins/trustedcoin/kivy.… | 110 +++++++++++++++++++++++++++++++
A electrum/plugins/trustedcoin/qt.py | 313 +++++++++++++++++++++++++++++++
A electrum/plugins/trustedcoin/trust… | 672 +++++++++++++++++++++++++++++++
A electrum/plugins/virtualkeyboard/_… | 5 +++++
A electrum/plugins/virtualkeyboard/q… | 61 +++++++++++++++++++++++++++++++
A electrum/qrscanner.py | 88 +++++++++++++++++++++++++++++++
A electrum/ripemd.py | 393 +++++++++++++++++++++++++++++++
A electrum/rsakey.py | 542 +++++++++++++++++++++++++++++++
A electrum/scripts/bip70.py | 35 +++++++++++++++++++++++++++++++
A electrum/scripts/block_headers.py | 29 +++++++++++++++++++++++++++++
A electrum/scripts/estimate_fee.py | 7 +++++++
A electrum/scripts/get_history.py | 18 ++++++++++++++++++
A electrum/scripts/peers.py | 14 ++++++++++++++
A electrum/scripts/servers.py | 10 ++++++++++
A electrum/scripts/txradar.py | 20 ++++++++++++++++++++
A electrum/scripts/util.py | 87 +++++++++++++++++++++++++++++++
A electrum/scripts/watch_address.py | 36 +++++++++++++++++++++++++++++++
A electrum/segwit_addr.py | 122 +++++++++++++++++++++++++++++++
A electrum/servers.json | 304 +++++++++++++++++++++++++++++++
A electrum/servers_regtest.json | 8 ++++++++
A electrum/servers_testnet.json | 31 +++++++++++++++++++++++++++++++
A electrum/simple_config.py | 552 ++++++++++++++++++++++++++++++
A electrum/storage.py | 645 +++++++++++++++++++++++++++++++
A electrum/synchronizer.py | 213 +++++++++++++++++++++++++++++++
A electrum/tests/__init__.py | 38 +++++++++++++++++++++++++++++++
A electrum/tests/test_bitcoin.py | 761 ++++++++++++++++++++++++++++++
A electrum/tests/test_commands.py | 33 +++++++++++++++++++++++++++++++
A electrum/tests/test_dnssec.py | 41 +++++++++++++++++++++++++++++++
A electrum/tests/test_interface.py | 28 ++++++++++++++++++++++++++++
A electrum/tests/test_mnemonic.py | 42 +++++++++++++++++++++++++++++++
A electrum/tests/test_simple_config.… | 149 +++++++++++++++++++++++++++++++
A electrum/tests/test_storage_upgrad… | 301 +++++++++++++++++++++++++++++++
A electrum/tests/test_transaction.py | 813 ++++++++++++++++++++++++++++++
A electrum/tests/test_util.py | 109 +++++++++++++++++++++++++++++++
A electrum/tests/test_wallet.py | 71 +++++++++++++++++++++++++++++++
A electrum/tests/test_wallet_vertica… | 1603 +++++++++++++++++++++++++++++++
A electrum/transaction.py | 1229 +++++++++++++++++++++++++++++++
A electrum/util.py | 903 ++++++++++++++++++++++++++++++
A electrum/verifier.py | 158 +++++++++++++++++++++++++++++++
A electrum/version.py | 18 ++++++++++++++++++
A electrum/wallet.py | 2374 +++++++++++++++++++++++++++++++
A electrum/websockets.py | 140 +++++++++++++++++++++++++++++++
A electrum/wordlist/chinese_simplifi… | 2048 +++++++++++++++++++++++++++++++
A electrum/wordlist/english.txt | 2048 +++++++++++++++++++++++++++++++
A electrum/wordlist/japanese.txt | 2048 +++++++++++++++++++++++++++++++
A electrum/wordlist/portuguese.txt | 1654 +++++++++++++++++++++++++++++++
A electrum/wordlist/spanish.txt | 2048 +++++++++++++++++++++++++++++++
A electrum/x509.py | 341 +++++++++++++++++++++++++++++++
D gui/kivy/Makefile | 32 -------------------------------
D gui/kivy/main.kv | 464 ------------------------------
D gui/kivy/main_window.py | 1028 -------------------------------
D gui/kivy/nfc_scanner/__init__.py | 44 -------------------------------
D gui/kivy/nfc_scanner/scanner_andro… | 242 -------------------------------
D gui/kivy/nfc_scanner/scanner_dummy… | 52 -------------------------------
D gui/kivy/uix/context_menu.py | 56 -------------------------------
D gui/kivy/uix/dialogs/__init__.py | 220 -------------------------------
D gui/kivy/uix/dialogs/addresses.py | 180 -------------------------------
D gui/kivy/uix/dialogs/bump_fee_dial… | 118 -------------------------------
D gui/kivy/uix/dialogs/fee_dialog.py | 131 -------------------------------
D gui/kivy/uix/dialogs/fx_dialog.py | 111 ------------------------------
D gui/kivy/uix/dialogs/installwizard… | 1038 -------------------------------
D gui/kivy/uix/dialogs/invoices.py | 169 -------------------------------
D gui/kivy/uix/dialogs/label_dialog.… | 55 -------------------------------
D gui/kivy/uix/dialogs/nfc_transacti… | 33 -------------------------------
D gui/kivy/uix/dialogs/password_dial… | 142 -------------------------------
D gui/kivy/uix/dialogs/qr_scanner.py | 44 -------------------------------
D gui/kivy/uix/dialogs/question.py | 53 ------------------------------
D gui/kivy/uix/dialogs/requests.py | 157 -------------------------------
D gui/kivy/uix/dialogs/settings.py | 220 -------------------------------
D gui/kivy/uix/dialogs/tx_dialog.py | 184 -------------------------------
D gui/kivy/uix/menus.py | 95 ------------------------------
D gui/kivy/uix/screens.py | 484 -------------------------------
D gui/kivy/uix/ui_screens/history.kv | 78 -------------------------------
D gui/kivy/uix/ui_screens/receive.kv | 142 -------------------------------
D gui/kivy/uix/ui_screens/send.kv | 127 -------------------------------
D gui/qt/__init__.py | 313 -------------------------------
D gui/qt/address_list.py | 195 -------------------------------
D gui/qt/completion_text_edit.py | 121 -------------------------------
D gui/qt/contact_list.py | 98 -------------------------------
D gui/qt/installwizard.py | 643 -------------------------------
D gui/qt/main_window.py | 3221 -------------------------------
D gui/qt/password_dialog.py | 305 -------------------------------
D gui/qt/qrtextedit.py | 76 -------------------------------
D gui/qt/qrwindow.py | 89 -------------------------------
D gui/qt/request_list.py | 129 -------------------------------
D gui/qt/seed_dialog.py | 211 -------------------------------
D gui/qt/transaction_dialog.py | 328 -------------------------------
D gui/text.py | 503 -------------------------------
D lib/__init__.py | 14 --------------
D lib/base_crash_reporter.py | 127 -------------------------------
D lib/commands.py | 892 ------------------------------
D lib/daemon.py | 316 -------------------------------
D lib/exchange_rate.py | 573 -------------------------------
D lib/i18n.py | 81 ------------------------------
D lib/interface.py | 407 -------------------------------
D lib/jsonrpc.py | 98 -------------------------------
D lib/keystore.py | 799 -------------------------------
D lib/mnemonic.py | 183 -------------------------------
D lib/msqr.py | 94 -------------------------------
D lib/network.py | 1297 -------------------------------
D lib/old_mnemonic.py | 1697 -------------------------------
D lib/paymentrequest.proto | 47 -------------------------------
D lib/paymentrequest.py | 528 -------------------------------
D lib/paymentrequest_pb2.py | 367 ------------------------------
D lib/pem.py | 191 -------------------------------
D lib/plot.py | 63 -------------------------------
D lib/plugins.py | 572 -------------------------------
D lib/qrscanner.py | 88 -------------------------------
D lib/ripemd.py | 393 -------------------------------
D lib/rsakey.py | 542 -------------------------------
D lib/segwit_addr.py | 122 -------------------------------
D lib/servers.json | 304 -------------------------------
D lib/servers_regtest.json | 8 --------
D lib/servers_testnet.json | 31 -------------------------------
D lib/simple_config.py | 552 ------------------------------
D lib/storage.py | 647 -------------------------------
D lib/synchronizer.py | 213 -------------------------------
D lib/tests/__init__.py | 38 -------------------------------
D lib/tests/test_bitcoin.py | 761 ------------------------------
D lib/tests/test_commands.py | 33 -------------------------------
D lib/tests/test_dnssec.py | 41 -------------------------------
D lib/tests/test_interface.py | 28 ----------------------------
D lib/tests/test_mnemonic.py | 42 -------------------------------
D lib/tests/test_simple_config.py | 149 -------------------------------
D lib/tests/test_storage_upgrade.py | 301 -------------------------------
D lib/tests/test_transaction.py | 813 ------------------------------
D lib/tests/test_util.py | 109 -------------------------------
D lib/tests/test_wallet.py | 71 -------------------------------
D lib/tests/test_wallet_vertical.py | 1604 -------------------------------
D lib/transaction.py | 1229 -------------------------------
D lib/util.py | 903 ------------------------------
D lib/verifier.py | 158 -------------------------------
D lib/version.py | 18 ------------------
D lib/wallet.py | 2377 -------------------------------
D lib/websockets.py | 140 -------------------------------
D lib/wordlist/chinese_simplified.txt | 2048 -------------------------------
D lib/wordlist/english.txt | 2048 -------------------------------
D lib/wordlist/japanese.txt | 2048 -------------------------------
D lib/wordlist/portuguese.txt | 1654 -------------------------------
D lib/wordlist/spanish.txt | 2048 -------------------------------
D lib/x509.py | 341 -------------------------------
D plugins/README | 31 -------------------------------
D plugins/__init__.py | 26 --------------------------
D plugins/audio_modem/__init__.py | 7 -------
D plugins/audio_modem/qt.py | 128 -------------------------------
D plugins/cosigner_pool/__init__.py | 9 ---------
D plugins/cosigner_pool/qt.py | 228 -------------------------------
D plugins/digitalbitbox/__init__.py | 6 ------
D plugins/digitalbitbox/cmdline.py | 14 --------------
D plugins/digitalbitbox/digitalbitbo… | 768 -------------------------------
D plugins/digitalbitbox/qt.py | 43 ------------------------------
D plugins/email_requests/__init__.py | 5 -----
D plugins/email_requests/qt.py | 271 -------------------------------
D plugins/greenaddress_instant/__ini… | 5 -----
D plugins/greenaddress_instant/qt.py | 107 -------------------------------
D plugins/hw_wallet/__init__.py | 2 --
D plugins/hw_wallet/cmdline.py | 46 -------------------------------
D plugins/hw_wallet/plugin.py | 89 -------------------------------
D plugins/hw_wallet/qt.py | 235 -------------------------------
D plugins/keepkey/__init__.py | 7 -------
D plugins/keepkey/client.py | 14 --------------
D plugins/keepkey/clientbase.py | 250 -------------------------------
D plugins/keepkey/cmdline.py | 14 --------------
D plugins/keepkey/keepkey.py | 438 -------------------------------
D plugins/keepkey/qt.py | 586 ------------------------------
D plugins/labels/__init__.py | 9 ---------
D plugins/labels/cmdline.py | 11 -----------
D plugins/labels/kivy.py | 14 --------------
D plugins/labels/labels.py | 168 -------------------------------
D plugins/labels/qt.py | 78 -------------------------------
D plugins/ledger/__init__.py | 7 -------
D plugins/ledger/auth2fa.py | 358 -------------------------------
D plugins/ledger/cmdline.py | 14 --------------
D plugins/ledger/ledger.py | 637 -------------------------------
D plugins/ledger/qt.py | 81 ------------------------------
D plugins/revealer/DejaVuSansMono-Bo… | 0
D plugins/revealer/LICENSE_DEJAVU.txt | 99 -------------------------------
D plugins/revealer/SIL Open Font Lic… | 44 -------------------------------
D plugins/revealer/SourceSansPro-Bol… | 0
D plugins/revealer/__init__.py | 17 -----------------
D plugins/revealer/qt.py | 723 -------------------------------
D plugins/trezor/__init__.py | 8 --------
D plugins/trezor/client.py | 11 -----------
D plugins/trezor/clientbase.py | 265 -------------------------------
D plugins/trezor/cmdline.py | 14 --------------
D plugins/trezor/qt.py | 613 -------------------------------
D plugins/trezor/transport.py | 95 ------------------------------
D plugins/trezor/trezor.py | 516 -------------------------------
D plugins/trustedcoin/__init__.py | 11 -----------
D plugins/trustedcoin/cmdline.py | 45 -------------------------------
D plugins/trustedcoin/kivy.py | 110 -------------------------------
D plugins/trustedcoin/qt.py | 313 -------------------------------
D plugins/trustedcoin/trustedcoin.py | 676 -------------------------------
D plugins/virtualkeyboard/__init__.py | 5 -----
D plugins/virtualkeyboard/qt.py | 61 -------------------------------
A run_electrum | 473 ++++++++++++++++++++++++++++++
D scripts/bip70 | 35 -------------------------------
D scripts/block_headers | 29 -----------------------------
D scripts/estimate_fee | 6 ------
D scripts/get_history | 18 ------------------
D scripts/peers | 14 --------------
D scripts/servers | 9 ---------
D scripts/txradar | 19 -------------------
D scripts/util.py | 87 -------------------------------
D scripts/watch_address | 36 -------------------------------
M setup.py | 44 +++++++++++++++----------------
M snap/snapcraft.yaml | 2 +-
M tox.ini | 2 +-
474 files changed, 51372 insertions(+), 51404 deletions(-)
---
DIR diff --git a/.gitignore b/.gitignore
t@@ -4,10 +4,9 @@
build/
dist/
*.egg/
-/electrum.py
contrib/pyinstaller/
Electrum.egg-info/
-gui/qt/icons_rc.py
+electrum/gui/qt/icons_rc.py
locale/
.devlocaltmp/
*_trial_temp
DIR diff --git a/README.rst b/README.rst
t@@ -36,7 +36,7 @@ Electrum from its root directory, without installing it on your
system; all the python dependencies are included in the 'packages'
directory. To run Electrum from its root directory, just do::
- ./electrum
+ ./run_electrum
You can also install Electrum on your system, by running this command::
t@@ -73,12 +73,12 @@ Render the SVG icons to PNGs (optional)::
Compile the icons file for Qt::
sudo apt-get install pyqt5-dev-tools
- pyrcc5 icons.qrc -o gui/qt/icons_rc.py
+ pyrcc5 icons.qrc -o electrum/gui/qt/icons_rc.py
Compile the protobuf description file::
sudo apt-get install protobuf-compiler
- protoc --proto_path=lib/ --python_out=lib/ lib/paymentrequest.proto
+ protoc --proto_path=electrum --python_out=electrum electrum/paymentrequest.proto
Create translations (optional)::
DIR diff --git a/contrib/build-osx/make_osx b/contrib/build-osx/make_osx
t@@ -46,8 +46,8 @@ git submodule update
rm -rf $BUILDDIR > /dev/null 2>&1
mkdir $BUILDDIR
-cp -R ./contrib/deterministic-build/electrum-locale/locale/ ./lib/locale/
-cp ./contrib/deterministic-build/electrum-icons/icons_rc.py ./gui/qt/
+cp -R ./contrib/deterministic-build/electrum-locale/locale/ ./electrum/locale/
+cp ./contrib/deterministic-build/electrum-icons/icons_rc.py ./electrum/gui/qt/
info "Downloading libusb..."
DIR diff --git a/contrib/build-osx/osx.spec b/contrib/build-osx/osx.spec
t@@ -1,97 +1,96 @@
-# -*- mode: python -*-
-
-from PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs
-
-import sys
-import os
-
-PACKAGE='Electrum'
-PYPKG='electrum'
-MAIN_SCRIPT='electrum'
-ICONS_FILE='electrum.icns'
-
-for i, x in enumerate(sys.argv):
- if x == '--name':
- VERSION = sys.argv[i+1]
- break
-else:
- raise Exception('no version')
-
-electrum = os.path.abspath(".") + "/"
-block_cipher = None
-
-# see https://github.com/pyinstaller/pyinstaller/issues/2005
-hiddenimports = []
-hiddenimports += collect_submodules('trezorlib')
-hiddenimports += collect_submodules('btchip')
-hiddenimports += collect_submodules('keepkeylib')
-hiddenimports += collect_submodules('websocket')
-
-datas = [
- (electrum+'lib/*.json', PYPKG),
- (electrum+'lib/wordlist/english.txt', PYPKG + '/wordlist'),
- (electrum+'lib/locale', PYPKG + '/locale'),
- (electrum+'plugins', PYPKG + '_plugins'),
-]
-datas += collect_data_files('trezorlib')
-datas += collect_data_files('btchip')
-datas += collect_data_files('keepkeylib')
-
-# Add libusb so Trezor will work
-binaries = [(electrum + "contrib/build-osx/libusb-1.0.dylib", ".")]
-binaries += [(electrum + "contrib/build-osx/libsecp256k1.0.dylib", ".")]
-
-# Workaround for "Retro Look":
-binaries += [b for b in collect_dynamic_libs('PyQt5') if 'macstyle' in b[0]]
-
-# We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports
-a = Analysis([electrum+MAIN_SCRIPT,
- electrum+'gui/qt/main_window.py',
- electrum+'gui/text.py',
- electrum+'lib/util.py',
- electrum+'lib/wallet.py',
- electrum+'lib/simple_config.py',
- electrum+'lib/bitcoin.py',
- electrum+'lib/dnssec.py',
- electrum+'lib/commands.py',
- electrum+'plugins/cosigner_pool/qt.py',
- electrum+'plugins/email_requests/qt.py',
- electrum+'plugins/trezor/client.py',
- electrum+'plugins/trezor/qt.py',
- electrum+'plugins/keepkey/qt.py',
- electrum+'plugins/ledger/qt.py',
- ],
- binaries=binaries,
- datas=datas,
- hiddenimports=hiddenimports,
- hookspath=[])
-
-# http://stackoverflow.com/questions/19055089/pyinstaller-onefile-warning-pyconfig-h-when-importing-scipy-or-scipy-signal
-for d in a.datas:
- if 'pyconfig' in d[0]:
- a.datas.remove(d)
- break
-
-pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
-
-exe = EXE(pyz,
- a.scripts,
- a.binaries,
- a.datas,
- name=PACKAGE,
- debug=False,
- strip=False,
- upx=True,
- icon=electrum+ICONS_FILE,
- console=False)
-
-app = BUNDLE(exe,
- version = VERSION,
- name=PACKAGE + '.app',
- icon=electrum+ICONS_FILE,
- bundle_identifier=None,
- info_plist={
- 'NSHighResolutionCapable': 'True',
- 'NSSupportsAutomaticGraphicsSwitching': 'True'
- }
-)
+# -*- mode: python -*-
+
+from PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs
+
+import sys
+import os
+
+PACKAGE='Electrum'
+PYPKG='electrum'
+MAIN_SCRIPT='run_electrum'
+ICONS_FILE='electrum.icns'
+
+for i, x in enumerate(sys.argv):
+ if x == '--name':
+ VERSION = sys.argv[i+1]
+ break
+else:
+ raise Exception('no version')
+
+electrum = os.path.abspath(".") + "/"
+block_cipher = None
+
+# see https://github.com/pyinstaller/pyinstaller/issues/2005
+hiddenimports = []
+hiddenimports += collect_submodules('trezorlib')
+hiddenimports += collect_submodules('btchip')
+hiddenimports += collect_submodules('keepkeylib')
+hiddenimports += collect_submodules('websocket')
+
+datas = [
+ (electrum+'electrum/*.json', PYPKG),
+ (electrum+'electrum/wordlist/english.txt', PYPKG + '/wordlist'),
+ (electrum+'electrum/locale', PYPKG + '/locale')
+]
+datas += collect_data_files('trezorlib')
+datas += collect_data_files('btchip')
+datas += collect_data_files('keepkeylib')
+
+# Add libusb so Trezor will work
+binaries = [(electrum + "contrib/build-osx/libusb-1.0.dylib", ".")]
+binaries += [(electrum + "contrib/build-osx/libsecp256k1.0.dylib", ".")]
+
+# Workaround for "Retro Look":
+binaries += [b for b in collect_dynamic_libs('PyQt5') if 'macstyle' in b[0]]
+
+# We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports
+a = Analysis([electrum+ MAIN_SCRIPT,
+ electrum+'electrum/gui/qt/main_window.py',
+ electrum+'electrum/gui/text.py',
+ electrum+'electrum/util.py',
+ electrum+'electrum/wallet.py',
+ electrum+'electrum/simple_config.py',
+ electrum+'electrum/bitcoin.py',
+ electrum+'electrum/dnssec.py',
+ electrum+'electrum/commands.py',
+ electrum+'electrum/plugins/cosigner_pool/qt.py',
+ electrum+'electrum/plugins/email_requests/qt.py',
+ electrum+'electrum/plugins/trezor/client.py',
+ electrum+'electrum/plugins/trezor/qt.py',
+ electrum+'electrum/plugins/keepkey/qt.py',
+ electrum+'electrum/plugins/ledger/qt.py',
+ ],
+ binaries=binaries,
+ datas=datas,
+ hiddenimports=hiddenimports,
+ hookspath=[])
+
+# http://stackoverflow.com/questions/19055089/pyinstaller-onefile-warning-pyconfig-h-when-importing-scipy-or-scipy-signal
+for d in a.datas:
+ if 'pyconfig' in d[0]:
+ a.datas.remove(d)
+ break
+
+pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
+
+exe = EXE(pyz,
+ a.scripts,
+ a.binaries,
+ a.datas,
+ name=PACKAGE,
+ debug=False,
+ strip=False,
+ upx=True,
+ icon=electrum+ICONS_FILE,
+ console=False)
+
+app = BUNDLE(exe,
+ version = VERSION,
+ name=PACKAGE + '.app',
+ icon=electrum+ICONS_FILE,
+ bundle_identifier=None,
+ info_plist={
+ 'NSHighResolutionCapable': 'True',
+ 'NSSupportsAutomaticGraphicsSwitching': 'True'
+ }
+)
DIR diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh
t@@ -62,8 +62,8 @@ popd
rm -rf $WINEPREFIX/drive_c/electrum
cp -r electrum $WINEPREFIX/drive_c/electrum
cp electrum/LICENCE .
-cp -r ./electrum/contrib/deterministic-build/electrum-locale/locale $WINEPREFIX/drive_c/electrum/lib/
-cp ./electrum/contrib/deterministic-build/electrum-icons/icons_rc.py $WINEPREFIX/drive_c/electrum/gui/qt/
+cp -r ./electrum/contrib/deterministic-build/electrum-locale/locale $WINEPREFIX/drive_c/electrum/electrum/
+cp ./electrum/contrib/deterministic-build/electrum-icons/icons_rc.py $WINEPREFIX/drive_c/electrum/electrum/gui/qt/
# Install frozen dependencies
$PYTHON -m pip install -r ../../deterministic-build/requirements.txt
DIR diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec
t@@ -31,10 +31,9 @@ binaries += [b for b in collect_dynamic_libs('PyQt5') if 'qwindowsvista' in b[0]
binaries += [('C:/tmp/libsecp256k1.dll', '.')]
datas = [
- (home+'lib/*.json', 'electrum'),
- (home+'lib/wordlist/english.txt', 'electrum/wordlist'),
- (home+'lib/locale', 'electrum/locale'),
- (home+'plugins', 'electrum_plugins'),
+ (home+'electrum/*.json', 'electrum'),
+ (home+'electrum/wordlist/english.txt', 'electrum/wordlist'),
+ (home+'electrum/locale', 'electrum/locale'),
('C:\\Program Files (x86)\\ZBar\\bin\\', '.')
]
datas += collect_data_files('trezorlib')
t@@ -42,21 +41,21 @@ datas += collect_data_files('btchip')
datas += collect_data_files('keepkeylib')
# We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports
-a = Analysis([home+'electrum',
- home+'gui/qt/main_window.py',
- home+'gui/text.py',
- home+'lib/util.py',
- home+'lib/wallet.py',
- home+'lib/simple_config.py',
- home+'lib/bitcoin.py',
- home+'lib/dnssec.py',
- home+'lib/commands.py',
- home+'plugins/cosigner_pool/qt.py',
- home+'plugins/email_requests/qt.py',
- home+'plugins/trezor/client.py',
- home+'plugins/trezor/qt.py',
- home+'plugins/keepkey/qt.py',
- home+'plugins/ledger/qt.py',
+a = Analysis([home+'run_electrum',
+ home+'electrum/gui/qt/main_window.py',
+ home+'electrum/gui/text.py',
+ home+'electrum/util.py',
+ home+'electrum/wallet.py',
+ home+'electrum/simple_config.py',
+ home+'electrum/bitcoin.py',
+ home+'electrum/dnssec.py',
+ home+'electrum/commands.py',
+ home+'electrum/plugins/cosigner_pool/qt.py',
+ home+'electrum/plugins/email_requests/qt.py',
+ home+'electrum/plugins/trezor/client.py',
+ home+'electrum/plugins/trezor/qt.py',
+ home+'electrum/plugins/keepkey/qt.py',
+ home+'electrum/plugins/ledger/qt.py',
#home+'packages/requests/utils.py'
],
binaries=binaries,
t@@ -68,7 +67,7 @@ a = Analysis([home+'electrum',
# http://stackoverflow.com/questions/19055089/pyinstaller-onefile-warning-pyconfig-h-when-importing-scipy-or-scipy-signal
for d in a.datas:
- if 'pyconfig' in d[0]:
+ if 'pyconfig' in d[0]:
a.datas.remove(d)
break
t@@ -85,7 +84,7 @@ exe_standalone = EXE(
pyz,
a.scripts,
a.binaries,
- a.datas,
+ a.datas,
name=os.path.join('build\\pyi.win32\\electrum', cmdline_name + ".exe"),
debug=False,
strip=None,
DIR diff --git a/contrib/make_apk b/contrib/make_apk
t@@ -1,6 +1,6 @@
#!/bin/bash
-pushd ./gui/kivy/
+pushd ./electrum/gui/kivy/
if [[ -n "$1" && "$1" == "release" ]] ; then
echo -n Keystore Password:
DIR diff --git a/contrib/make_locale b/contrib/make_locale
t@@ -8,8 +8,7 @@ import requests
os.chdir(os.path.dirname(os.path.realpath(__file__)))
os.chdir('..')
-code_directories = 'gui plugins lib'
-cmd = "find {} -type f -name '*.py' -o -name '*.kv'".format(code_directories)
+cmd = "find electrum -type f -name '*.py' -o -name '*.kv'"
files = subprocess.check_output(cmd, shell=True)
t@@ -19,13 +18,13 @@ with open("app.fil", "wb") as f:
print("Found {} files to translate".format(len(files.splitlines())))
# Generate fresh translation template
-if not os.path.exists('lib/locale'):
- os.mkdir('lib/locale')
-cmd = 'xgettext -s --from-code UTF-8 --language Python --no-wrap -f app.fil --output=lib/locale/messages.pot'
+if not os.path.exists('electrum/locale'):
+ os.mkdir('electrum/locale')
+cmd = 'xgettext -s --from-code UTF-8 --language Python --no-wrap -f app.fil --output=electrum/locale/messages.pot'
print('Generate template')
os.system(cmd)
-os.chdir('lib')
+os.chdir('electrum')
crowdin_identifier = 'electrum'
crowdin_file_name = 'files[electrum-client/messages.pot]'
DIR diff --git a/electrum b/electrum
t@@ -1,480 +0,0 @@
-#!/usr/bin/env python3
-# -*- mode: python -*-
-#
-# Electrum - lightweight Bitcoin client
-# Copyright (C) 2011 thomasv@gitorious
-#
-# 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 os
-import sys
-
-script_dir = os.path.dirname(os.path.realpath(__file__))
-is_bundle = getattr(sys, 'frozen', False)
-is_local = not is_bundle and os.path.exists(os.path.join(script_dir, "electrum.desktop"))
-is_android = 'ANDROID_DATA' in os.environ
-
-# move this back to gui/kivy/__init.py once plugins are moved
-os.environ['KIVY_DATA_DIR'] = os.path.abspath(os.path.dirname(__file__)) + '/gui/kivy/data/'
-
-if is_local or is_android:
- sys.path.insert(0, os.path.join(script_dir, 'packages'))
-
-
-def check_imports():
- # pure-python dependencies need to be imported here for pyinstaller
- try:
- import dns
- import pyaes
- import ecdsa
- import requests
- import qrcode
- import pbkdf2
- import google.protobuf
- import jsonrpclib
- except ImportError as e:
- sys.exit("Error: %s. Try 'sudo pip install <module-name>'"%str(e))
- # the following imports are for pyinstaller
- from google.protobuf import descriptor
- from google.protobuf import message
- from google.protobuf import reflection
- from google.protobuf import descriptor_pb2
- from jsonrpclib import SimpleJSONRPCServer
- # make sure that certificates are here
- assert os.path.exists(requests.utils.DEFAULT_CA_BUNDLE_PATH)
-
-
-if not is_android:
- check_imports()
-
-# load local module as electrum
-if is_local or is_android:
- import imp
- imp.load_module('electrum', *imp.find_module('lib'))
- imp.load_module('electrum_gui', *imp.find_module('gui'))
-
-
-
-from electrum import bitcoin, util
-from electrum import constants
-from electrum import SimpleConfig, Network
-from electrum.wallet import Wallet, Imported_Wallet
-from electrum.storage import WalletStorage, get_derivation_used_for_hw_device_encryption
-from electrum.util import print_msg, print_stderr, json_encode, json_decode, UserCancelled
-from electrum.util import set_verbosity, InvalidPassword
-from electrum.commands import get_parser, known_commands, Commands, config_variables
-from electrum import daemon
-from electrum import keystore
-from electrum.mnemonic import Mnemonic
-
-# get password routine
-def prompt_password(prompt, confirm=True):
- import getpass
- password = getpass.getpass(prompt, stream=None)
- if password and confirm:
- password2 = getpass.getpass("Confirm: ")
- if password != password2:
- sys.exit("Error: Passwords do not match.")
- if not password:
- password = None
- return password
-
-
-
-def run_non_RPC(config):
- cmdname = config.get('cmd')
-
- storage = WalletStorage(config.get_wallet_path())
- if storage.file_exists():
- sys.exit("Error: Remove the existing wallet first!")
-
- def password_dialog():
- return prompt_password("Password (hit return if you do not wish to encrypt your wallet):")
-
- if cmdname == 'restore':
- text = config.get('text').strip()
- passphrase = config.get('passphrase', '')
- password = password_dialog() if keystore.is_private(text) else None
- if keystore.is_address_list(text):
- wallet = Imported_Wallet(storage)
- for x in text.split():
- wallet.import_address(x)
- elif keystore.is_private_key_list(text):
- k = keystore.Imported_KeyStore({})
- storage.put('keystore', k.dump())
- storage.put('use_encryption', bool(password))
- wallet = Imported_Wallet(storage)
- for x in text.split():
- wallet.import_private_key(x, password)
- storage.write()
- else:
- if keystore.is_seed(text):
- k = keystore.from_seed(text, passphrase, False)
- elif keystore.is_master_key(text):
- k = keystore.from_master_key(text)
- else:
- sys.exit("Error: Seed or key not recognized")
- if password:
- k.update_password(None, password)
- storage.put('keystore', k.dump())
- storage.put('wallet_type', 'standard')
- storage.put('use_encryption', bool(password))
- storage.write()
- wallet = Wallet(storage)
- if not config.get('offline'):
- network = Network(config)
- network.start()
- wallet.start_threads(network)
- print_msg("Recovering wallet...")
- wallet.synchronize()
- wallet.wait_until_synchronized()
- wallet.stop_threads()
- # note: we don't wait for SPV
- msg = "Recovery successful" if wallet.is_found() else "Found no history for this wallet"
- else:
- msg = "This wallet was restored offline. It may contain more addresses than displayed."
- print_msg(msg)
-
- elif cmdname == 'create':
- password = password_dialog()
- passphrase = config.get('passphrase', '')
- seed_type = 'segwit' if config.get('segwit') else 'standard'
- seed = Mnemonic('en').make_seed(seed_type)
- k = keystore.from_seed(seed, passphrase, False)
- storage.put('keystore', k.dump())
- storage.put('wallet_type', 'standard')
- wallet = Wallet(storage)
- wallet.update_password(None, password, True)
- wallet.synchronize()
- print_msg("Your wallet generation seed is:\n\"%s\"" % seed)
- print_msg("Please keep it in a safe place; if you lose it, you will not be able to restore your wallet.")
-
- wallet.storage.write()
- print_msg("Wallet saved in '%s'" % wallet.storage.path)
- sys.exit(0)
-
-
-def init_daemon(config_options):
- config = SimpleConfig(config_options)
- storage = WalletStorage(config.get_wallet_path())
- if not storage.file_exists():
- print_msg("Error: Wallet file not found.")
- print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option")
- sys.exit(0)
- if storage.is_encrypted():
- if storage.is_encrypted_with_hw_device():
- plugins = init_plugins(config, 'cmdline')
- password = get_password_for_hw_device_encrypted_storage(plugins)
- elif config.get('password'):
- password = config.get('password')
- else:
- password = prompt_password('Password:', False)
- if not password:
- print_msg("Error: Password required")
- sys.exit(1)
- else:
- password = None
- config_options['password'] = password
-
-
-def init_cmdline(config_options, server):
- config = SimpleConfig(config_options)
- cmdname = config.get('cmd')
- cmd = known_commands[cmdname]
-
- if cmdname == 'signtransaction' and config.get('privkey'):
- cmd.requires_wallet = False
- cmd.requires_password = False
-
- if cmdname in ['payto', 'paytomany'] and config.get('unsigned'):
- cmd.requires_password = False
-
- if cmdname in ['payto', 'paytomany'] and config.get('broadcast'):
- cmd.requires_network = True
-
- # instantiate wallet for command-line
- storage = WalletStorage(config.get_wallet_path())
-
- if cmd.requires_wallet and not storage.file_exists():
- print_msg("Error: Wallet file not found.")
- print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option")
- sys.exit(0)
-
- # important warning
- if cmd.name in ['getprivatekeys']:
- print_stderr("WARNING: ALL your private keys are secret.")
- print_stderr("Exposing a single private key can compromise your entire wallet!")
- print_stderr("In particular, DO NOT use 'redeem private key' services proposed by third parties.")
-
- # commands needing password
- if (cmd.requires_wallet and storage.is_encrypted() and server is None)\
- or (cmd.requires_password and (storage.get('use_encryption') or storage.is_encrypted())):
- if storage.is_encrypted_with_hw_device():
- # this case is handled later in the control flow
- password = None
- elif config.get('password'):
- password = config.get('password')
- else:
- password = prompt_password('Password:', False)
- if not password:
- print_msg("Error: Password required")
- sys.exit(1)
- else:
- password = None
-
- config_options['password'] = password
-
- if cmd.name == 'password':
- new_password = prompt_password('New password:')
- config_options['new_password'] = new_password
-
- return cmd, password
-
-
-def get_connected_hw_devices(plugins):
- support = plugins.get_hardware_support()
- if not support:
- print_msg('No hardware wallet support found on your system.')
- sys.exit(1)
- # scan devices
- devices = []
- devmgr = plugins.device_manager
- for name, description, plugin in support:
- try:
- u = devmgr.unpaired_device_infos(None, plugin)
- except:
- devmgr.print_error("error", name)
- continue
- devices += list(map(lambda x: (name, x), u))
- return devices
-
-
-def get_password_for_hw_device_encrypted_storage(plugins):
- devices = get_connected_hw_devices(plugins)
- if len(devices) == 0:
- print_msg("Error: No connected hw device found. Cannot decrypt this wallet.")
- sys.exit(1)
- elif len(devices) > 1:
- print_msg("Warning: multiple hardware devices detected. "
- "The first one will be used to decrypt the wallet.")
- # FIXME we use the "first" device, in case of multiple ones
- name, device_info = devices[0]
- plugin = plugins.get_plugin(name)
- derivation = get_derivation_used_for_hw_device_encryption()
- try:
- xpub = plugin.get_xpub(device_info.device.id_, derivation, 'standard', plugin.handler)
- except UserCancelled:
- sys.exit(0)
- password = keystore.Xpub.get_pubkey_from_xpub(xpub, ())
- return password
-
-
-def run_offline_command(config, config_options, plugins):
- cmdname = config.get('cmd')
- cmd = known_commands[cmdname]
- password = config_options.get('password')
- if cmd.requires_wallet:
- storage = WalletStorage(config.get_wallet_path())
- if storage.is_encrypted():
- if storage.is_encrypted_with_hw_device():
- password = get_password_for_hw_device_encrypted_storage(plugins)
- config_options['password'] = password
- storage.decrypt(password)
- wallet = Wallet(storage)
- else:
- wallet = None
- # check password
- if cmd.requires_password and wallet.has_password():
- try:
- seed = wallet.check_password(password)
- except InvalidPassword:
- print_msg("Error: This password does not decode this wallet.")
- sys.exit(1)
- if cmd.requires_network:
- print_msg("Warning: running command offline")
- # arguments passed to function
- args = [config.get(x) for x in cmd.params]
- # decode json arguments
- if cmdname not in ('setconfig',):
- args = list(map(json_decode, args))
- # options
- kwargs = {}
- for x in cmd.options:
- kwargs[x] = (config_options.get(x) if x in ['password', 'new_password'] else config.get(x))
- cmd_runner = Commands(config, wallet, None)
- func = getattr(cmd_runner, cmd.name)
- result = func(*args, **kwargs)
- # save wallet
- if wallet:
- wallet.storage.write()
- return result
-
-def init_plugins(config, gui_name):
- from electrum.plugins import Plugins
- return Plugins(config, is_local or is_android, gui_name)
-
-
-if __name__ == '__main__':
- # The hook will only be used in the Qt GUI right now
- util.setup_thread_excepthook()
- # on macOS, delete Process Serial Number arg generated for apps launched in Finder
- sys.argv = list(filter(lambda x: not x.startswith('-psn'), sys.argv))
-
- # old 'help' syntax
- if len(sys.argv) > 1 and sys.argv[1] == 'help':
- sys.argv.remove('help')
- sys.argv.append('-h')
-
- # read arguments from stdin pipe and prompt
- for i, arg in enumerate(sys.argv):
- if arg == '-':
- if not sys.stdin.isatty():
- sys.argv[i] = sys.stdin.read()
- break
- else:
- raise Exception('Cannot get argument from stdin')
- elif arg == '?':
- sys.argv[i] = input("Enter argument:")
- elif arg == ':':
- sys.argv[i] = prompt_password('Enter argument (will not echo):', False)
-
- # parse command line
- parser = get_parser()
- args = parser.parse_args()
-
- # config is an object passed to the various constructors (wallet, interface, gui)
- if is_android:
- config_options = {
- 'verbose': True,
- 'cmd': 'gui',
- 'gui': 'kivy',
- }
- else:
- config_options = args.__dict__
- f = lambda key: config_options[key] is not None and key not in config_variables.get(args.cmd, {}).keys()
- config_options = {key: config_options[key] for key in filter(f, config_options.keys())}
- if config_options.get('server'):
- config_options['auto_connect'] = False
-
- config_options['cwd'] = os.getcwd()
-
- # fixme: this can probably be achieved with a runtime hook (pyinstaller)
- if is_bundle and os.path.exists(os.path.join(sys._MEIPASS, 'is_portable')):
- config_options['portable'] = True
-
- if config_options.get('portable'):
- config_options['electrum_path'] = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'electrum_data')
-
- # kivy sometimes freezes when we write to sys.stderr
- set_verbosity(config_options.get('verbose') and config_options.get('gui')!='kivy')
-
- # check uri
- uri = config_options.get('url')
- if uri:
- if not uri.startswith('bitcoin:'):
- print_stderr('unknown command:', uri)
- sys.exit(1)
- config_options['url'] = uri
-
- # todo: defer this to gui
- config = SimpleConfig(config_options)
- cmdname = config.get('cmd')
-
- if config.get('testnet'):
- constants.set_testnet()
- elif config.get('regtest'):
- constants.set_regtest()
- elif config.get('simnet'):
- constants.set_simnet()
-
- # run non-RPC commands separately
- if cmdname in ['create', 'restore']:
- run_non_RPC(config)
- sys.exit(0)
-
- if cmdname == 'gui':
- fd, server = daemon.get_fd_or_server(config)
- if fd is not None:
- plugins = init_plugins(config, config.get('gui', 'qt'))
- d = daemon.Daemon(config, fd, True)
- d.start()
- d.init_gui(config, plugins)
- sys.exit(0)
- else:
- result = server.gui(config_options)
-
- elif cmdname == 'daemon':
- subcommand = config.get('subcommand')
- if subcommand in ['load_wallet']:
- init_daemon(config_options)
-
- if subcommand in [None, 'start']:
- fd, server = daemon.get_fd_or_server(config)
- if fd is not None:
- if subcommand == 'start':
- pid = os.fork()
- if pid:
- print_stderr("starting daemon (PID %d)" % pid)
- sys.exit(0)
- init_plugins(config, 'cmdline')
- d = daemon.Daemon(config, fd, False)
- d.start()
- if config.get('websocket_server'):
- from electrum import websockets
- websockets.WebSocketServer(config, d.network).start()
- if config.get('requests_dir'):
- path = os.path.join(config.get('requests_dir'), 'index.html')
- if not os.path.exists(path):
- print("Requests directory not configured.")
- print("You can configure it using https://github.com/spesmilo/electrum-merchant")
- sys.exit(1)
- d.join()
- sys.exit(0)
- else:
- result = server.daemon(config_options)
- else:
- server = daemon.get_server(config)
- if server is not None:
- result = server.daemon(config_options)
- else:
- print_msg("Daemon not running")
- sys.exit(1)
- else:
- # command line
- server = daemon.get_server(config)
- init_cmdline(config_options, server)
- if server is not None:
- result = server.run_cmdline(config_options)
- else:
- cmd = known_commands[cmdname]
- if cmd.requires_network:
- print_msg("Daemon not running; try 'electrum daemon start'")
- sys.exit(1)
- else:
- plugins = init_plugins(config, 'cmdline')
- result = run_offline_command(config, config_options, plugins)
- # print result
- if isinstance(result, str):
- print_msg(result)
- elif type(result) is dict and result.get('error'):
- print_stderr(result.get('error'))
- elif result is not None:
- print_msg(json_encode(result))
- sys.exit(0)
DIR diff --git a/electrum-env b/electrum-env
t@@ -22,6 +22,6 @@ fi
export PYTHONPATH="/usr/local/lib/python${PYTHON_VER}/site-packages:$PYTHONPATH"
-./electrum "$@"
+./run_electrum "$@"
deactivate
DIR diff --git a/electrum/__init__.py b/electrum/__init__.py
t@@ -0,0 +1,14 @@
+from .version import ELECTRUM_VERSION
+from .util import format_satoshis, print_msg, print_error, set_verbosity
+from .wallet import Synchronizer, Wallet
+from .storage import WalletStorage
+from .coinchooser import COIN_CHOOSERS
+from .network import Network, pick_random_server
+from .interface import Connection, Interface
+from .simple_config import SimpleConfig, get_config, set_config
+from . import bitcoin
+from . import transaction
+from . import daemon
+from .transaction import Transaction
+from .plugin import BasePlugin
+from .commands import Commands, known_commands
DIR diff --git a/electrum/base_crash_reporter.py b/electrum/base_crash_reporter.py
t@@ -0,0 +1,128 @@
+# Electrum - lightweight Bitcoin client
+#
+# 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 json
+import locale
+import traceback
+import subprocess
+import sys
+import os
+
+import requests
+
+from .version import ELECTRUM_VERSION
+from .import constants
+from .i18n import _
+
+
+class BaseCrashReporter(object):
+ report_server = "https://crashhub.electrum.org"
+ config_key = "show_crash_reporter"
+ issue_template = """<h2>Traceback</h2>
+<pre>
+{traceback}
+</pre>
+
+<h2>Additional information</h2>
+<ul>
+ <li>Electrum version: {app_version}</li>
+ <li>Python version: {python_version}</li>
+ <li>Operating system: {os}</li>
+ <li>Wallet type: {wallet_type}</li>
+ <li>Locale: {locale}</li>
+</ul>
+ """
+ CRASH_MESSAGE = _('Something went wrong while executing Electrum.')
+ CRASH_TITLE = _('Sorry!')
+ REQUEST_HELP_MESSAGE = _('To help us diagnose and fix the problem, you can send us a bug report that contains '
+ 'useful debug information:')
+ DESCRIBE_ERROR_MESSAGE = _("Please briefly describe what led to the error (optional):")
+ ASK_CONFIRM_SEND = _("Do you want to send this report?")
+
+ def __init__(self, exctype, value, tb):
+ self.exc_args = (exctype, value, tb)
+
+ def send_report(self, endpoint="/crash"):
+ if constants.net.GENESIS[-4:] not in ["4943", "e26f"] and ".electrum.org" in BaseCrashReporter.report_server:
+ # Gah! Some kind of altcoin wants to send us crash reports.
+ raise Exception(_("Missing report URL."))
+ report = self.get_traceback_info()
+ report.update(self.get_additional_info())
+ report = json.dumps(report)
+ response = requests.post(BaseCrashReporter.report_server + endpoint, data=report)
+ return response
+
+ def get_traceback_info(self):
+ exc_string = str(self.exc_args[1])
+ stack = traceback.extract_tb(self.exc_args[2])
+ readable_trace = "".join(traceback.format_list(stack))
+ id = {
+ "file": stack[-1].filename,
+ "name": stack[-1].name,
+ "type": self.exc_args[0].__name__
+ }
+ return {
+ "exc_string": exc_string,
+ "stack": readable_trace,
+ "id": id
+ }
+
+ def get_additional_info(self):
+ args = {
+ "app_version": ELECTRUM_VERSION,
+ "python_version": sys.version,
+ "os": self.get_os_version(),
+ "wallet_type": "unknown",
+ "locale": locale.getdefaultlocale()[0] or "?",
+ "description": self.get_user_description()
+ }
+ try:
+ args["wallet_type"] = self.get_wallet_type()
+ except:
+ # Maybe the wallet isn't loaded yet
+ pass
+ try:
+ args["app_version"] = self.get_git_version()
+ except:
+ # This is probably not running from source
+ pass
+ return args
+
+ @staticmethod
+ def get_git_version():
+ dir = os.path.dirname(os.path.realpath(sys.argv[0]))
+ version = subprocess.check_output(
+ ['git', 'describe', '--always', '--dirty'], cwd=dir)
+ return str(version, "utf8").strip()
+
+ def get_report_string(self):
+ info = self.get_additional_info()
+ info["traceback"] = "".join(traceback.format_exception(*self.exc_args))
+ return self.issue_template.format(**info)
+
+ def get_user_description(self):
+ raise NotImplementedError
+
+ def get_wallet_type(self):
+ raise NotImplementedError
+
+ def get_os_version(self):
+ raise NotImplementedError
DIR diff --git a/lib/base_wizard.py b/electrum/base_wizard.py
DIR diff --git a/lib/bitcoin.py b/electrum/bitcoin.py
DIR diff --git a/lib/blockchain.py b/electrum/blockchain.py
DIR diff --git a/lib/checkpoints.json b/electrum/checkpoints.json
DIR diff --git a/lib/checkpoints_testnet.json b/electrum/checkpoints_testnet.json
DIR diff --git a/lib/coinchooser.py b/electrum/coinchooser.py
DIR diff --git a/electrum/commands.py b/electrum/commands.py
t@@ -0,0 +1,892 @@
+#!/usr/bin/env python
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2011 thomasv@gitorious
+#
+# 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 sys
+import datetime
+import copy
+import argparse
+import json
+import ast
+import base64
+from functools import wraps
+from decimal import Decimal
+
+from .import util, ecc
+from .util import bfh, bh2u, format_satoshis, json_decode, print_error, json_encode
+from . import bitcoin
+from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS
+from .i18n import _
+from .transaction import Transaction, multisig_script
+from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
+from .plugin import run_hook
+
+known_commands = {}
+
+
+def satoshis(amount):
+ # satoshi conversion must not be performed by the parser
+ return int(COIN*Decimal(amount)) if amount not in ['!', None] else amount
+
+
+class Command:
+ def __init__(self, func, s):
+ self.name = func.__name__
+ self.requires_network = 'n' in s
+ self.requires_wallet = 'w' in s
+ self.requires_password = 'p' in s
+ self.description = func.__doc__
+ self.help = self.description.split('.')[0] if self.description else None
+ varnames = func.__code__.co_varnames[1:func.__code__.co_argcount]
+ self.defaults = func.__defaults__
+ if self.defaults:
+ n = len(self.defaults)
+ self.params = list(varnames[:-n])
+ self.options = list(varnames[-n:])
+ else:
+ self.params = list(varnames)
+ self.options = []
+ self.defaults = []
+
+
+def command(s):
+ def decorator(func):
+ global known_commands
+ name = func.__name__
+ known_commands[name] = Command(func, s)
+ @wraps(func)
+ def func_wrapper(*args, **kwargs):
+ c = known_commands[func.__name__]
+ wallet = args[0].wallet
+ password = kwargs.get('password')
+ if c.requires_wallet and wallet is None:
+ raise Exception("wallet not loaded. Use 'electrum daemon load_wallet'")
+ if c.requires_password and password is None and wallet.has_password():
+ return {'error': 'Password required' }
+ return func(*args, **kwargs)
+ return func_wrapper
+ return decorator
+
+
+class Commands:
+
+ def __init__(self, config, wallet, network, callback = None):
+ self.config = config
+ self.wallet = wallet
+ self.network = network
+ self._callback = callback
+
+ def _run(self, method, args, password_getter):
+ # this wrapper is called from the python console
+ cmd = known_commands[method]
+ if cmd.requires_password and self.wallet.has_password():
+ password = password_getter()
+ if password is None:
+ return
+ else:
+ password = None
+
+ f = getattr(self, method)
+ if cmd.requires_password:
+ result = f(*args, **{'password':password})
+ else:
+ result = f(*args)
+
+ if self._callback:
+ self._callback()
+ return result
+
+ @command('')
+ def commands(self):
+ """List of commands"""
+ return ' '.join(sorted(known_commands.keys()))
+
+ @command('')
+ def create(self, segwit=False):
+ """Create a new wallet"""
+ raise Exception('Not a JSON-RPC command')
+
+ @command('wn')
+ def restore(self, text):
+ """Restore a wallet from text. Text can be a seed phrase, a master
+ public key, a master private key, a list of bitcoin addresses
+ or bitcoin private keys. If you want to be prompted for your
+ seed, type '?' or ':' (concealed) """
+ raise Exception('Not a JSON-RPC command')
+
+ @command('wp')
+ def password(self, password=None, new_password=None):
+ """Change wallet password. """
+ if self.wallet.storage.is_encrypted_with_hw_device() and new_password:
+ raise Exception("Can't change the password of a wallet encrypted with a hw device.")
+ b = self.wallet.storage.is_encrypted()
+ self.wallet.update_password(password, new_password, b)
+ self.wallet.storage.write()
+ return {'password':self.wallet.has_password()}
+
+ @command('')
+ def getconfig(self, key):
+ """Return a configuration variable. """
+ return self.config.get(key)
+
+ @classmethod
+ def _setconfig_normalize_value(cls, key, value):
+ if key not in ('rpcuser', 'rpcpassword'):
+ value = json_decode(value)
+ try:
+ value = ast.literal_eval(value)
+ except:
+ pass
+ return value
+
+ @command('')
+ def setconfig(self, key, value):
+ """Set a configuration variable. 'value' may be a string or a Python expression."""
+ value = self._setconfig_normalize_value(key, value)
+ self.config.set_key(key, value)
+ return True
+
+ @command('')
+ def make_seed(self, nbits=132, language=None, segwit=False):
+ """Create a seed"""
+ from .mnemonic import Mnemonic
+ t = 'segwit' if segwit else 'standard'
+ s = Mnemonic(language).make_seed(t, nbits)
+ return s
+
+ @command('n')
+ def getaddresshistory(self, address):
+ """Return the transaction history of any address. Note: This is a
+ walletless server query, results are not checked by SPV.
+ """
+ sh = bitcoin.address_to_scripthash(address)
+ return self.network.get_history_for_scripthash(sh)
+
+ @command('w')
+ def listunspent(self):
+ """List unspent outputs. Returns the list of unspent transaction
+ outputs in your wallet."""
+ l = copy.deepcopy(self.wallet.get_utxos(exclude_frozen=False))
+ for i in l:
+ v = i["value"]
+ i["value"] = str(Decimal(v)/COIN) if v is not None else None
+ return l
+
+ @command('n')
+ def getaddressunspent(self, address):
+ """Returns the UTXO list of any address. Note: This
+ is a walletless server query, results are not checked by SPV.
+ """
+ sh = bitcoin.address_to_scripthash(address)
+ return self.network.listunspent_for_scripthash(sh)
+
+ @command('')
+ def serialize(self, jsontx):
+ """Create a transaction from json inputs.
+ Inputs must have a redeemPubkey.
+ Outputs must be a list of {'address':address, 'value':satoshi_amount}.
+ """
+ keypairs = {}
+ inputs = jsontx.get('inputs')
+ outputs = jsontx.get('outputs')
+ locktime = jsontx.get('lockTime', 0)
+ for txin in inputs:
+ if txin.get('output'):
+ prevout_hash, prevout_n = txin['output'].split(':')
+ txin['prevout_n'] = int(prevout_n)
+ txin['prevout_hash'] = prevout_hash
+ sec = txin.get('privkey')
+ if sec:
+ txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec)
+ pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed)
+ keypairs[pubkey] = privkey, compressed
+ txin['type'] = txin_type
+ txin['x_pubkeys'] = [pubkey]
+ txin['signatures'] = [None]
+ txin['num_sig'] = 1
+
+ outputs = [(TYPE_ADDRESS, x['address'], int(x['value'])) for x in outputs]
+ tx = Transaction.from_io(inputs, outputs, locktime=locktime)
+ tx.sign(keypairs)
+ return tx.as_dict()
+
+ @command('wp')
+ def signtransaction(self, tx, privkey=None, password=None):
+ """Sign a transaction. The wallet keys will be used unless a private key is provided."""
+ tx = Transaction(tx)
+ if privkey:
+ txin_type, privkey2, compressed = bitcoin.deserialize_privkey(privkey)
+ pubkey_bytes = ecc.ECPrivkey(privkey2).get_public_key_bytes(compressed=compressed)
+ h160 = bitcoin.hash_160(pubkey_bytes)
+ x_pubkey = 'fd' + bh2u(b'\x00' + h160)
+ tx.sign({x_pubkey:(privkey2, compressed)})
+ else:
+ self.wallet.sign_transaction(tx, password)
+ return tx.as_dict()
+
+ @command('')
+ def deserialize(self, tx):
+ """Deserialize a serialized transaction"""
+ tx = Transaction(tx)
+ return tx.deserialize()
+
+ @command('n')
+ def broadcast(self, tx):
+ """Broadcast a transaction to the network. """
+ tx = Transaction(tx)
+ return self.network.broadcast_transaction(tx)
+
+ @command('')
+ def createmultisig(self, num, pubkeys):
+ """Create multisig address"""
+ assert isinstance(pubkeys, list), (type(num), type(pubkeys))
+ redeem_script = multisig_script(pubkeys, num)
+ address = bitcoin.hash160_to_p2sh(hash_160(bfh(redeem_script)))
+ return {'address':address, 'redeemScript':redeem_script}
+
+ @command('w')
+ def freeze(self, address):
+ """Freeze address. Freeze the funds at one of your wallet\'s addresses"""
+ return self.wallet.set_frozen_state([address], True)
+
+ @command('w')
+ def unfreeze(self, address):
+ """Unfreeze address. Unfreeze the funds at one of your wallet\'s address"""
+ return self.wallet.set_frozen_state([address], False)
+
+ @command('wp')
+ def getprivatekeys(self, address, password=None):
+ """Get private keys of addresses. You may pass a single wallet address, or a list of wallet addresses."""
+ if isinstance(address, str):
+ address = address.strip()
+ if is_address(address):
+ return self.wallet.export_private_key(address, password)[0]
+ domain = address
+ return [self.wallet.export_private_key(address, password)[0] for address in domain]
+
+ @command('w')
+ def ismine(self, address):
+ """Check if address is in wallet. Return true if and only address is in wallet"""
+ return self.wallet.is_mine(address)
+
+ @command('')
+ def dumpprivkeys(self):
+ """Deprecated."""
+ return "This command is deprecated. Use a pipe instead: 'electrum listaddresses | electrum getprivatekeys - '"
+
+ @command('')
+ def validateaddress(self, address):
+ """Check that an address is valid. """
+ return is_address(address)
+
+ @command('w')
+ def getpubkeys(self, address):
+ """Return the public keys for a wallet address. """
+ return self.wallet.get_public_keys(address)
+
+ @command('w')
+ def getbalance(self):
+ """Return the balance of your wallet. """
+ c, u, x = self.wallet.get_balance()
+ out = {"confirmed": str(Decimal(c)/COIN)}
+ if u:
+ out["unconfirmed"] = str(Decimal(u)/COIN)
+ if x:
+ out["unmatured"] = str(Decimal(x)/COIN)
+ return out
+
+ @command('n')
+ def getaddressbalance(self, address):
+ """Return the balance of any address. Note: This is a walletless
+ server query, results are not checked by SPV.
+ """
+ sh = bitcoin.address_to_scripthash(address)
+ out = self.network.get_balance_for_scripthash(sh)
+ out["confirmed"] = str(Decimal(out["confirmed"])/COIN)
+ out["unconfirmed"] = str(Decimal(out["unconfirmed"])/COIN)
+ return out
+
+ @command('n')
+ def getmerkle(self, txid, height):
+ """Get Merkle branch of a transaction included in a block. Electrum
+ uses this to verify transactions (Simple Payment Verification)."""
+ return self.network.get_merkle_for_transaction(txid, int(height))
+
+ @command('n')
+ def getservers(self):
+ """Return the list of available servers"""
+ return self.network.get_servers()
+
+ @command('')
+ def version(self):
+ """Return the version of Electrum."""
+ from .version import ELECTRUM_VERSION
+ return ELECTRUM_VERSION
+
+ @command('w')
+ def getmpk(self):
+ """Get master public key. Return your wallet\'s master public key"""
+ return self.wallet.get_master_public_key()
+
+ @command('wp')
+ def getmasterprivate(self, password=None):
+ """Get master private key. Return your wallet\'s master private key"""
+ return str(self.wallet.keystore.get_master_private_key(password))
+
+ @command('wp')
+ def getseed(self, password=None):
+ """Get seed phrase. Print the generation seed of your wallet."""
+ s = self.wallet.get_seed(password)
+ return s
+
+ @command('wp')
+ def importprivkey(self, privkey, password=None):
+ """Import a private key."""
+ if not self.wallet.can_import_privkey():
+ return "Error: This type of wallet cannot import private keys. Try to create a new wallet with that key."
+ try:
+ addr = self.wallet.import_private_key(privkey, password)
+ out = "Keypair imported: " + addr
+ except BaseException as e:
+ out = "Error: " + str(e)
+ return out
+
+ def _resolver(self, x):
+ if x is None:
+ return None
+ out = self.wallet.contacts.resolve(x)
+ if out.get('type') == 'openalias' and self.nocheck is False and out.get('validated') is False:
+ raise Exception('cannot verify alias', x)
+ return out['address']
+
+ @command('n')
+ def sweep(self, privkey, destination, fee=None, nocheck=False, imax=100):
+ """Sweep private keys. Returns a transaction that spends UTXOs from
+ privkey to a destination address. The transaction is not
+ broadcasted."""
+ from .wallet import sweep
+ tx_fee = satoshis(fee)
+ privkeys = privkey.split()
+ self.nocheck = nocheck
+ #dest = self._resolver(destination)
+ tx = sweep(privkeys, self.network, self.config, destination, tx_fee, imax)
+ return tx.as_dict() if tx else None
+
+ @command('wp')
+ def signmessage(self, address, message, password=None):
+ """Sign a message with a key. Use quotes if your message contains
+ whitespaces"""
+ sig = self.wallet.sign_message(address, message, password)
+ return base64.b64encode(sig).decode('ascii')
+
+ @command('')
+ def verifymessage(self, address, signature, message):
+ """Verify a signature."""
+ sig = base64.b64decode(signature)
+ message = util.to_bytes(message)
+ return ecc.verify_message_with_address(address, sig, message)
+
+ def _mktx(self, outputs, fee, change_addr, domain, nocheck, unsigned, rbf, password, locktime=None):
+ self.nocheck = nocheck
+ change_addr = self._resolver(change_addr)
+ domain = None if domain is None else map(self._resolver, domain)
+ final_outputs = []
+ for address, amount in outputs:
+ address = self._resolver(address)
+ amount = satoshis(amount)
+ final_outputs.append((TYPE_ADDRESS, address, amount))
+
+ coins = self.wallet.get_spendable_coins(domain, self.config)
+ tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr)
+ if locktime != None:
+ tx.locktime = locktime
+ if rbf is None:
+ rbf = self.config.get('use_rbf', True)
+ if rbf:
+ tx.set_rbf(True)
+ if not unsigned:
+ self.wallet.sign_transaction(tx, password)
+ return tx
+
+ @command('wp')
+ def payto(self, destination, amount, fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False, rbf=None, password=None, locktime=None):
+ """Create a transaction. """
+ tx_fee = satoshis(fee)
+ domain = from_addr.split(',') if from_addr else None
+ tx = self._mktx([(destination, amount)], tx_fee, change_addr, domain, nocheck, unsigned, rbf, password, locktime)
+ return tx.as_dict()
+
+ @command('wp')
+ def paytomany(self, outputs, fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False, rbf=None, password=None, locktime=None):
+ """Create a multi-output transaction. """
+ tx_fee = satoshis(fee)
+ domain = from_addr.split(',') if from_addr else None
+ tx = self._mktx(outputs, tx_fee, change_addr, domain, nocheck, unsigned, rbf, password, locktime)
+ return tx.as_dict()
+
+ @command('w')
+ def history(self, year=None, show_addresses=False, show_fiat=False):
+ """Wallet history. Returns the transaction history of your wallet."""
+ kwargs = {'show_addresses': show_addresses}
+ if year:
+ import time
+ start_date = datetime.datetime(year, 1, 1)
+ end_date = datetime.datetime(year+1, 1, 1)
+ kwargs['from_timestamp'] = time.mktime(start_date.timetuple())
+ kwargs['to_timestamp'] = time.mktime(end_date.timetuple())
+ if show_fiat:
+ from .exchange_rate import FxThread
+ fx = FxThread(self.config, None)
+ kwargs['fx'] = fx
+ return json_encode(self.wallet.get_full_history(**kwargs))
+
+ @command('w')
+ def setlabel(self, key, label):
+ """Assign a label to an item. Item may be a bitcoin address or a
+ transaction ID"""
+ self.wallet.set_label(key, label)
+
+ @command('w')
+ def listcontacts(self):
+ """Show your list of contacts"""
+ return self.wallet.contacts
+
+ @command('w')
+ def getalias(self, key):
+ """Retrieve alias. Lookup in your list of contacts, and for an OpenAlias DNS record."""
+ return self.wallet.contacts.resolve(key)
+
+ @command('w')
+ def searchcontacts(self, query):
+ """Search through contacts, return matching entries. """
+ results = {}
+ for key, value in self.wallet.contacts.items():
+ if query.lower() in key.lower():
+ results[key] = value
+ return results
+
+ @command('w')
+ def listaddresses(self, receiving=False, change=False, labels=False, frozen=False, unused=False, funded=False, balance=False):
+ """List wallet addresses. Returns the list of all addresses in your wallet. Use optional arguments to filter the results."""
+ out = []
+ for addr in self.wallet.get_addresses():
+ if frozen and not self.wallet.is_frozen(addr):
+ continue
+ if receiving and self.wallet.is_change(addr):
+ continue
+ if change and not self.wallet.is_change(addr):
+ continue
+ if unused and self.wallet.is_used(addr):
+ continue
+ if funded and self.wallet.is_empty(addr):
+ continue
+ item = addr
+ if labels or balance:
+ item = (item,)
+ if balance:
+ item += (format_satoshis(sum(self.wallet.get_addr_balance(addr))),)
+ if labels:
+ item += (repr(self.wallet.labels.get(addr, '')),)
+ out.append(item)
+ return out
+
+ @command('n')
+ def gettransaction(self, txid):
+ """Retrieve a transaction. """
+ if self.wallet and txid in self.wallet.transactions:
+ tx = self.wallet.transactions[txid]
+ else:
+ raw = self.network.get_transaction(txid)
+ if raw:
+ tx = Transaction(raw)
+ else:
+ raise Exception("Unknown transaction")
+ return tx.as_dict()
+
+ @command('')
+ def encrypt(self, pubkey, message):
+ """Encrypt a message with a public key. Use quotes if the message contains whitespaces."""
+ public_key = ecc.ECPubkey(bfh(pubkey))
+ encrypted = public_key.encrypt_message(message)
+ return encrypted
+
+ @command('wp')
+ def decrypt(self, pubkey, encrypted, password=None):
+ """Decrypt a message encrypted with a public key."""
+ return self.wallet.decrypt_message(pubkey, encrypted, password)
+
+ def _format_request(self, out):
+ pr_str = {
+ PR_UNKNOWN: 'Unknown',
+ PR_UNPAID: 'Pending',
+ PR_PAID: 'Paid',
+ PR_EXPIRED: 'Expired',
+ }
+ out['amount (BTC)'] = format_satoshis(out.get('amount'))
+ out['status'] = pr_str[out.get('status', PR_UNKNOWN)]
+ return out
+
+ @command('w')
+ def getrequest(self, key):
+ """Return a payment request"""
+ r = self.wallet.get_payment_request(key, self.config)
+ if not r:
+ raise Exception("Request not found")
+ return self._format_request(r)
+
+ #@command('w')
+ #def ackrequest(self, serialized):
+ # """<Not implemented>"""
+ # pass
+
+ @command('w')
+ def listrequests(self, pending=False, expired=False, paid=False):
+ """List the payment requests you made."""
+ out = self.wallet.get_sorted_requests(self.config)
+ if pending:
+ f = PR_UNPAID
+ elif expired:
+ f = PR_EXPIRED
+ elif paid:
+ f = PR_PAID
+ else:
+ f = None
+ if f is not None:
+ out = list(filter(lambda x: x.get('status')==f, out))
+ return list(map(self._format_request, out))
+
+ @command('w')
+ def createnewaddress(self):
+ """Create a new receiving address, beyond the gap limit of the wallet"""
+ return self.wallet.create_new_address(False)
+
+ @command('w')
+ def getunusedaddress(self):
+ """Returns the first unused address of the wallet, or None if all addresses are used.
+ An address is considered as used if it has received a transaction, or if it is used in a payment request."""
+ return self.wallet.get_unused_address()
+
+ @command('w')
+ def addrequest(self, amount, memo='', expiration=None, force=False):
+ """Create a payment request, using the first unused address of the wallet.
+ The address will be considered as used after this operation.
+ If no payment is received, the address will be considered as unused if the payment request is deleted from the wallet."""
+ addr = self.wallet.get_unused_address()
+ if addr is None:
+ if force:
+ addr = self.wallet.create_new_address(False)
+ else:
+ return False
+ amount = satoshis(amount)
+ expiration = int(expiration) if expiration else None
+ req = self.wallet.make_payment_request(addr, amount, memo, expiration)
+ self.wallet.add_payment_request(req, self.config)
+ out = self.wallet.get_payment_request(addr, self.config)
+ return self._format_request(out)
+
+ @command('w')
+ def addtransaction(self, tx):
+ """ Add a transaction to the wallet history """
+ tx = Transaction(tx)
+ if not self.wallet.add_transaction(tx.txid(), tx):
+ return False
+ self.wallet.save_transactions()
+ return tx.txid()
+
+ @command('wp')
+ def signrequest(self, address, password=None):
+ "Sign payment request with an OpenAlias"
+ alias = self.config.get('alias')
+ if not alias:
+ raise Exception('No alias in your configuration')
+ alias_addr = self.wallet.contacts.resolve(alias)['address']
+ self.wallet.sign_payment_request(address, alias, alias_addr, password)
+
+ @command('w')
+ def rmrequest(self, address):
+ """Remove a payment request"""
+ return self.wallet.remove_payment_request(address, self.config)
+
+ @command('w')
+ def clearrequests(self):
+ """Remove all payment requests"""
+ for k in list(self.wallet.receive_requests.keys()):
+ self.wallet.remove_payment_request(k, self.config)
+
+ @command('n')
+ def notify(self, address, URL):
+ """Watch an address. Every time the address changes, a http POST is sent to the URL."""
+ def callback(x):
+ import urllib.request
+ headers = {'content-type':'application/json'}
+ data = {'address':address, 'status':x.get('result')}
+ serialized_data = util.to_bytes(json.dumps(data))
+ try:
+ req = urllib.request.Request(URL, serialized_data, headers)
+ response_stream = urllib.request.urlopen(req, timeout=5)
+ util.print_error('Got Response for %s' % address)
+ except BaseException as e:
+ util.print_error(str(e))
+ self.network.subscribe_to_addresses([address], callback)
+ return True
+
+ @command('wn')
+ def is_synchronized(self):
+ """ return wallet synchronization status """
+ return self.wallet.is_up_to_date()
+
+ @command('n')
+ def getfeerate(self, fee_method=None, fee_level=None):
+ """Return current suggested fee rate (in sat/kvByte), according to config
+ settings or supplied parameters.
+ """
+ if fee_method is None:
+ dyn, mempool = None, None
+ elif fee_method.lower() == 'static':
+ dyn, mempool = False, False
+ elif fee_method.lower() == 'eta':
+ dyn, mempool = True, False
+ elif fee_method.lower() == 'mempool':
+ dyn, mempool = True, True
+ else:
+ raise Exception('Invalid fee estimation method: {}'.format(fee_method))
+ if fee_level is not None:
+ fee_level = Decimal(fee_level)
+ return self.config.fee_per_kb(dyn=dyn, mempool=mempool, fee_level=fee_level)
+
+ @command('')
+ def help(self):
+ # for the python console
+ return sorted(known_commands.keys())
+
+param_descriptions = {
+ 'privkey': 'Private key. Type \'?\' to get a prompt.',
+ 'destination': 'Bitcoin address, contact or alias',
+ 'address': 'Bitcoin address',
+ 'seed': 'Seed phrase',
+ 'txid': 'Transaction ID',
+ 'pos': 'Position',
+ 'height': 'Block height',
+ 'tx': 'Serialized transaction (hexadecimal)',
+ 'key': 'Variable name',
+ 'pubkey': 'Public key',
+ 'message': 'Clear text message. Use quotes if it contains spaces.',
+ 'encrypted': 'Encrypted message',
+ 'amount': 'Amount to be sent (in BTC). Type \'!\' to send the maximum available.',
+ 'requested_amount': 'Requested amount (in BTC).',
+ 'outputs': 'list of ["address", amount]',
+ 'redeem_script': 'redeem script (hexadecimal)',
+}
+
+command_options = {
+ 'password': ("-W", "Password"),
+ 'new_password':(None, "New Password"),
+ 'receiving': (None, "Show only receiving addresses"),
+ 'change': (None, "Show only change addresses"),
+ 'frozen': (None, "Show only frozen addresses"),
+ 'unused': (None, "Show only unused addresses"),
+ 'funded': (None, "Show only funded addresses"),
+ 'balance': ("-b", "Show the balances of listed addresses"),
+ 'labels': ("-l", "Show the labels of listed addresses"),
+ 'nocheck': (None, "Do not verify aliases"),
+ 'imax': (None, "Maximum number of inputs"),
+ 'fee': ("-f", "Transaction fee (in BTC)"),
+ 'from_addr': ("-F", "Source address (must be a wallet address; use sweep to spend from non-wallet address)."),
+ 'change_addr': ("-c", "Change address. Default is a spare address, or the source address if it's not in the wallet"),
+ 'nbits': (None, "Number of bits of entropy"),
+ 'segwit': (None, "Create segwit seed"),
+ 'language': ("-L", "Default language for wordlist"),
+ 'privkey': (None, "Private key. Set to '?' to get a prompt."),
+ 'unsigned': ("-u", "Do not sign transaction"),
+ 'rbf': (None, "Replace-by-fee transaction"),
+ 'locktime': (None, "Set locktime block number"),
+ 'domain': ("-D", "List of addresses"),
+ 'memo': ("-m", "Description of the request"),
+ 'expiration': (None, "Time in seconds"),
+ 'timeout': (None, "Timeout in seconds"),
+ 'force': (None, "Create new address beyond gap limit, if no more addresses are available."),
+ 'pending': (None, "Show only pending requests."),
+ 'expired': (None, "Show only expired requests."),
+ 'paid': (None, "Show only paid requests."),
+ 'show_addresses': (None, "Show input and output addresses"),
+ 'show_fiat': (None, "Show fiat value of transactions"),
+ 'year': (None, "Show history for a given year"),
+ 'fee_method': (None, "Fee estimation method to use"),
+ 'fee_level': (None, "Float between 0.0 and 1.0, representing fee slider position")
+}
+
+
+# don't use floats because of rounding errors
+from .transaction import tx_from_str
+json_loads = lambda x: json.loads(x, parse_float=lambda x: str(Decimal(x)))
+arg_types = {
+ 'num': int,
+ 'nbits': int,
+ 'imax': int,
+ 'year': int,
+ 'tx': tx_from_str,
+ 'pubkeys': json_loads,
+ 'jsontx': json_loads,
+ 'inputs': json_loads,
+ 'outputs': json_loads,
+ 'fee': lambda x: str(Decimal(x)) if x is not None else None,
+ 'amount': lambda x: str(Decimal(x)) if x != '!' else '!',
+ 'locktime': int,
+ 'fee_method': str,
+ 'fee_level': json_loads,
+}
+
+config_variables = {
+
+ 'addrequest': {
+ 'requests_dir': 'directory where a bip70 file will be written.',
+ 'ssl_privkey': 'Path to your SSL private key, needed to sign the request.',
+ 'ssl_chain': 'Chain of SSL certificates, needed for signed requests. Put your certificate at the top and the root CA at the end',
+ 'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"',
+ },
+ 'listrequests':{
+ 'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"',
+ }
+}
+
+def set_default_subparser(self, name, args=None):
+ """see http://stackoverflow.com/questions/5176691/argparse-how-to-specify-a-default-subcommand"""
+ subparser_found = False
+ for arg in sys.argv[1:]:
+ if arg in ['-h', '--help']: # global help if no subparser
+ break
+ else:
+ for x in self._subparsers._actions:
+ if not isinstance(x, argparse._SubParsersAction):
+ continue
+ for sp_name in x._name_parser_map.keys():
+ if sp_name in sys.argv[1:]:
+ subparser_found = True
+ if not subparser_found:
+ # insert default in first position, this implies no
+ # global options without a sub_parsers specified
+ if args is None:
+ sys.argv.insert(1, name)
+ else:
+ args.insert(0, name)
+
+argparse.ArgumentParser.set_default_subparser = set_default_subparser
+
+
+# workaround https://bugs.python.org/issue23058
+# see https://github.com/nickstenning/honcho/pull/121
+
+def subparser_call(self, parser, namespace, values, option_string=None):
+ from argparse import ArgumentError, SUPPRESS, _UNRECOGNIZED_ARGS_ATTR
+ parser_name = values[0]
+ arg_strings = values[1:]
+ # set the parser name if requested
+ if self.dest is not SUPPRESS:
+ setattr(namespace, self.dest, parser_name)
+ # select the parser
+ try:
+ parser = self._name_parser_map[parser_name]
+ except KeyError:
+ tup = parser_name, ', '.join(self._name_parser_map)
+ msg = _('unknown parser {!r} (choices: {})').format(*tup)
+ raise ArgumentError(self, msg)
+ # parse all the remaining options into the namespace
+ # store any unrecognized options on the object, so that the top
+ # level parser can decide what to do with them
+ namespace, arg_strings = parser.parse_known_args(arg_strings, namespace)
+ if arg_strings:
+ vars(namespace).setdefault(_UNRECOGNIZED_ARGS_ATTR, [])
+ getattr(namespace, _UNRECOGNIZED_ARGS_ATTR).extend(arg_strings)
+
+argparse._SubParsersAction.__call__ = subparser_call
+
+
+def add_network_options(parser):
+ parser.add_argument("-1", "--oneserver", action="store_true", dest="oneserver", default=None, help="connect to one server only")
+ parser.add_argument("-s", "--server", dest="server", default=None, help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)")
+ parser.add_argument("-p", "--proxy", dest="proxy", default=None, help="set proxy [type:]host[:port], where type is socks4,socks5 or http")
+
+def add_global_options(parser):
+ group = parser.add_argument_group('global options')
+ group.add_argument("-v", "--verbose", action="store_true", dest="verbose", default=False, help="Show debugging information")
+ group.add_argument("-D", "--dir", dest="electrum_path", help="electrum directory")
+ group.add_argument("-P", "--portable", action="store_true", dest="portable", default=False, help="Use local 'electrum_data' directory")
+ group.add_argument("-w", "--wallet", dest="wallet_path", help="wallet path")
+ group.add_argument("--testnet", action="store_true", dest="testnet", default=False, help="Use Testnet")
+ group.add_argument("--regtest", action="store_true", dest="regtest", default=False, help="Use Regtest")
+ group.add_argument("--simnet", action="store_true", dest="simnet", default=False, help="Use Simnet")
+
+def get_parser():
+ # create main parser
+ parser = argparse.ArgumentParser(
+ epilog="Run 'electrum help <command>' to see the help for a command")
+ add_global_options(parser)
+ subparsers = parser.add_subparsers(dest='cmd', metavar='<command>')
+ # gui
+ parser_gui = subparsers.add_parser('gui', description="Run Electrum's Graphical User Interface.", help="Run GUI (default)")
+ parser_gui.add_argument("url", nargs='?', default=None, help="bitcoin URI (or bip70 file)")
+ parser_gui.add_argument("-g", "--gui", dest="gui", help="select graphical user interface", choices=['qt', 'kivy', 'text', 'stdio'])
+ parser_gui.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline")
+ parser_gui.add_argument("-m", action="store_true", dest="hide_gui", default=False, help="hide GUI on startup")
+ parser_gui.add_argument("-L", "--lang", dest="language", default=None, help="default language used in GUI")
+ add_network_options(parser_gui)
+ add_global_options(parser_gui)
+ # daemon
+ parser_daemon = subparsers.add_parser('daemon', help="Run Daemon")
+ parser_daemon.add_argument("subcommand", choices=['start', 'status', 'stop', 'load_wallet', 'close_wallet'], nargs='?')
+ #parser_daemon.set_defaults(func=run_daemon)
+ add_network_options(parser_daemon)
+ add_global_options(parser_daemon)
+ # commands
+ for cmdname in sorted(known_commands.keys()):
+ cmd = known_commands[cmdname]
+ p = subparsers.add_parser(cmdname, help=cmd.help, description=cmd.description)
+ add_global_options(p)
+ if cmdname == 'restore':
+ p.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline")
+ for optname, default in zip(cmd.options, cmd.defaults):
+ a, help = command_options[optname]
+ b = '--' + optname
+ action = "store_true" if type(default) is bool else 'store'
+ args = (a, b) if a else (b,)
+ if action == 'store':
+ _type = arg_types.get(optname, str)
+ p.add_argument(*args, dest=optname, action=action, default=default, help=help, type=_type)
+ else:
+ p.add_argument(*args, dest=optname, action=action, default=default, help=help)
+
+ for param in cmd.params:
+ h = param_descriptions.get(param, '')
+ _type = arg_types.get(param, str)
+ p.add_argument(param, help=h, type=_type)
+
+ cvh = config_variables.get(cmdname)
+ if cvh:
+ group = p.add_argument_group('configuration variables', '(set with setconfig/getconfig)')
+ for k, v in cvh.items():
+ group.add_argument(k, nargs='?', help=v)
+
+ # 'gui' is the default command
+ parser.set_default_subparser('gui')
+ return parser
DIR diff --git a/lib/constants.py b/electrum/constants.py
DIR diff --git a/lib/contacts.py b/electrum/contacts.py
DIR diff --git a/lib/crypto.py b/electrum/crypto.py
DIR diff --git a/lib/currencies.json b/electrum/currencies.json
DIR diff --git a/electrum/daemon.py b/electrum/daemon.py
t@@ -0,0 +1,316 @@
+#!/usr/bin/env python
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2015 Thomas Voegtlin
+#
+# 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 ast
+import os
+import time
+import traceback
+import sys
+
+# from jsonrpc import JSONRPCResponseManager
+import jsonrpclib
+from .jsonrpc import VerifyingJSONRPCServer
+
+from .version import ELECTRUM_VERSION
+from .network import Network
+from .util import json_decode, DaemonThread
+from .util import print_error, to_string
+from .wallet import Wallet
+from .storage import WalletStorage
+from .commands import known_commands, Commands
+from .simple_config import SimpleConfig
+from .exchange_rate import FxThread
+from .plugin import run_hook
+
+
+def get_lockfile(config):
+ return os.path.join(config.path, 'daemon')
+
+
+def remove_lockfile(lockfile):
+ os.unlink(lockfile)
+
+
+def get_fd_or_server(config):
+ '''Tries to create the lockfile, using O_EXCL to
+ prevent races. If it succeeds it returns the FD.
+ Otherwise try and connect to the server specified in the lockfile.
+ If this succeeds, the server is returned. Otherwise remove the
+ lockfile and try again.'''
+ lockfile = get_lockfile(config)
+ while True:
+ try:
+ return os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644), None
+ except OSError:
+ pass
+ server = get_server(config)
+ if server is not None:
+ return None, server
+ # Couldn't connect; remove lockfile and try again.
+ remove_lockfile(lockfile)
+
+
+def get_server(config):
+ lockfile = get_lockfile(config)
+ while True:
+ create_time = None
+ try:
+ with open(lockfile) as f:
+ (host, port), create_time = ast.literal_eval(f.read())
+ rpc_user, rpc_password = get_rpc_credentials(config)
+ if rpc_password == '':
+ # authentication disabled
+ server_url = 'http://%s:%d' % (host, port)
+ else:
+ server_url = 'http://%s:%s@%s:%d' % (
+ rpc_user, rpc_password, host, port)
+ server = jsonrpclib.Server(server_url)
+ # Test daemon is running
+ server.ping()
+ return server
+ except Exception as e:
+ print_error("[get_server]", e)
+ if not create_time or create_time < time.time() - 1.0:
+ return None
+ # Sleep a bit and try again; it might have just been started
+ time.sleep(1.0)
+
+
+def get_rpc_credentials(config):
+ rpc_user = config.get('rpcuser', None)
+ rpc_password = config.get('rpcpassword', None)
+ if rpc_user is None or rpc_password is None:
+ rpc_user = 'user'
+ import ecdsa, base64
+ bits = 128
+ nbytes = bits // 8 + (bits % 8 > 0)
+ pw_int = ecdsa.util.randrange(pow(2, bits))
+ pw_b64 = base64.b64encode(
+ pw_int.to_bytes(nbytes, 'big'), b'-_')
+ rpc_password = to_string(pw_b64, 'ascii')
+ config.set_key('rpcuser', rpc_user)
+ config.set_key('rpcpassword', rpc_password, save=True)
+ elif rpc_password == '':
+ from .util import print_stderr
+ print_stderr('WARNING: RPC authentication is disabled.')
+ return rpc_user, rpc_password
+
+
+class Daemon(DaemonThread):
+
+ def __init__(self, config, fd, is_gui):
+ DaemonThread.__init__(self)
+ self.config = config
+ if config.get('offline'):
+ self.network = None
+ else:
+ self.network = Network(config)
+ self.network.start()
+ self.fx = FxThread(config, self.network)
+ if self.network:
+ self.network.add_jobs([self.fx])
+ self.gui = None
+ self.wallets = {}
+ # Setup JSONRPC server
+ self.init_server(config, fd, is_gui)
+
+ def init_server(self, config, fd, is_gui):
+ host = config.get('rpchost', '127.0.0.1')
+ port = config.get('rpcport', 0)
+
+ rpc_user, rpc_password = get_rpc_credentials(config)
+ try:
+ server = VerifyingJSONRPCServer((host, port), logRequests=False,
+ rpc_user=rpc_user, rpc_password=rpc_password)
+ except Exception as e:
+ self.print_error('Warning: cannot initialize RPC server on host', host, e)
+ self.server = None
+ os.close(fd)
+ return
+ os.write(fd, bytes(repr((server.socket.getsockname(), time.time())), 'utf8'))
+ os.close(fd)
+ self.server = server
+ server.timeout = 0.1
+ server.register_function(self.ping, 'ping')
+ if is_gui:
+ server.register_function(self.run_gui, 'gui')
+ else:
+ server.register_function(self.run_daemon, 'daemon')
+ self.cmd_runner = Commands(self.config, None, self.network)
+ for cmdname in known_commands:
+ server.register_function(getattr(self.cmd_runner, cmdname), cmdname)
+ server.register_function(self.run_cmdline, 'run_cmdline')
+
+ def ping(self):
+ return True
+
+ def run_daemon(self, config_options):
+ config = SimpleConfig(config_options)
+ sub = config.get('subcommand')
+ assert sub in [None, 'start', 'stop', 'status', 'load_wallet', 'close_wallet']
+ if sub in [None, 'start']:
+ response = "Daemon already running"
+ elif sub == 'load_wallet':
+ path = config.get_wallet_path()
+ wallet = self.load_wallet(path, config.get('password'))
+ if wallet is not None:
+ self.cmd_runner.wallet = wallet
+ run_hook('load_wallet', wallet, None)
+ response = wallet is not None
+ elif sub == 'close_wallet':
+ path = config.get_wallet_path()
+ if path in self.wallets:
+ self.stop_wallet(path)
+ response = True
+ else:
+ response = False
+ elif sub == 'status':
+ if self.network:
+ p = self.network.get_parameters()
+ current_wallet = self.cmd_runner.wallet
+ current_wallet_path = current_wallet.storage.path \
+ if current_wallet else None
+ response = {
+ 'path': self.network.config.path,
+ 'server': p[0],
+ 'blockchain_height': self.network.get_local_height(),
+ 'server_height': self.network.get_server_height(),
+ 'spv_nodes': len(self.network.get_interfaces()),
+ 'connected': self.network.is_connected(),
+ 'auto_connect': p[4],
+ 'version': ELECTRUM_VERSION,
+ 'wallets': {k: w.is_up_to_date()
+ for k, w in self.wallets.items()},
+ 'current_wallet': current_wallet_path,
+ 'fee_per_kb': self.config.fee_per_kb(),
+ }
+ else:
+ response = "Daemon offline"
+ elif sub == 'stop':
+ self.stop()
+ response = "Daemon stopped"
+ return response
+
+ def run_gui(self, config_options):
+ config = SimpleConfig(config_options)
+ if self.gui:
+ #if hasattr(self.gui, 'new_window'):
+ # path = config.get_wallet_path()
+ # self.gui.new_window(path, config.get('url'))
+ # response = "ok"
+ #else:
+ # response = "error: current GUI does not support multiple windows"
+ response = "error: Electrum GUI already running"
+ else:
+ response = "Error: Electrum is running in daemon mode. Please stop the daemon first."
+ return response
+
+ def load_wallet(self, path, password):
+ # wizard will be launched if we return
+ if path in self.wallets:
+ wallet = self.wallets[path]
+ return wallet
+ storage = WalletStorage(path, manual_upgrades=True)
+ if not storage.file_exists():
+ return
+ if storage.is_encrypted():
+ if not password:
+ return
+ storage.decrypt(password)
+ if storage.requires_split():
+ return
+ if storage.get_action():
+ return
+ wallet = Wallet(storage)
+ wallet.start_threads(self.network)
+ self.wallets[path] = wallet
+ return wallet
+
+ def add_wallet(self, wallet):
+ path = wallet.storage.path
+ self.wallets[path] = wallet
+
+ def get_wallet(self, path):
+ return self.wallets.get(path)
+
+ def stop_wallet(self, path):
+ wallet = self.wallets.pop(path)
+ wallet.stop_threads()
+
+ def run_cmdline(self, config_options):
+ password = config_options.get('password')
+ new_password = config_options.get('new_password')
+ config = SimpleConfig(config_options)
+ # FIXME this is ugly...
+ config.fee_estimates = self.network.config.fee_estimates.copy()
+ config.mempool_fees = self.network.config.mempool_fees.copy()
+ cmdname = config.get('cmd')
+ cmd = known_commands[cmdname]
+ if cmd.requires_wallet:
+ path = config.get_wallet_path()
+ wallet = self.wallets.get(path)
+ if wallet is None:
+ return {'error': 'Wallet "%s" is not loaded. Use "electrum daemon load_wallet"'%os.path.basename(path) }
+ else:
+ wallet = None
+ # arguments passed to function
+ args = map(lambda x: config.get(x), cmd.params)
+ # decode json arguments
+ args = [json_decode(i) for i in args]
+ # options
+ kwargs = {}
+ for x in cmd.options:
+ kwargs[x] = (config_options.get(x) if x in ['password', 'new_password'] else config.get(x))
+ cmd_runner = Commands(config, wallet, self.network)
+ func = getattr(cmd_runner, cmd.name)
+ result = func(*args, **kwargs)
+ return result
+
+ def run(self):
+ while self.is_running():
+ self.server.handle_request() if self.server else time.sleep(0.1)
+ for k, wallet in self.wallets.items():
+ wallet.stop_threads()
+ if self.network:
+ self.print_error("shutting down network")
+ self.network.stop()
+ self.network.join()
+ self.on_stop()
+
+ def stop(self):
+ self.print_error("stopping, removing lockfile")
+ remove_lockfile(get_lockfile(self.config))
+ DaemonThread.stop(self)
+
+ def init_gui(self, config, plugins):
+ gui_name = config.get('gui', 'qt')
+ if gui_name in ['lite', 'classic']:
+ gui_name = 'qt'
+ gui = __import__('electrum.gui.' + gui_name, fromlist=['electrum'])
+ self.gui = gui.ElectrumGui(config, self, plugins)
+ try:
+ self.gui.main()
+ except BaseException as e:
+ traceback.print_exc(file=sys.stdout)
+ # app will exit now
DIR diff --git a/lib/dnssec.py b/electrum/dnssec.py
DIR diff --git a/lib/ecc.py b/electrum/ecc.py
DIR diff --git a/lib/ecc_fast.py b/electrum/ecc_fast.py
DIR diff --git a/electrum/electrum b/electrum/electrum
t@@ -0,0 +1 @@
+../run_electrum
+\ No newline at end of file
DIR diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py
t@@ -0,0 +1,573 @@
+from datetime import datetime
+import inspect
+import requests
+import sys
+import os
+import json
+from threading import Thread
+import time
+import csv
+import decimal
+from decimal import Decimal
+
+from .bitcoin import COIN
+from .i18n import _
+from .util import PrintError, ThreadJob, make_dir
+
+
+# See https://en.wikipedia.org/wiki/ISO_4217
+CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0,
+ 'CVE': 0, 'DJF': 0, 'GNF': 0, 'IQD': 3, 'ISK': 0,
+ 'JOD': 3, 'JPY': 0, 'KMF': 0, 'KRW': 0, 'KWD': 3,
+ 'LYD': 3, 'MGA': 1, 'MRO': 1, 'OMR': 3, 'PYG': 0,
+ 'RWF': 0, 'TND': 3, 'UGX': 0, 'UYI': 0, 'VND': 0,
+ 'VUV': 0, 'XAF': 0, 'XAU': 4, 'XOF': 0, 'XPF': 0}
+
+
+class ExchangeBase(PrintError):
+
+ def __init__(self, on_quotes, on_history):
+ self.history = {}
+ self.quotes = {}
+ self.on_quotes = on_quotes
+ self.on_history = on_history
+
+ def get_json(self, site, get_string):
+ # APIs must have https
+ url = ''.join(['https://', site, get_string])
+ response = requests.request('GET', url, headers={'User-Agent' : 'Electrum'}, timeout=10)
+ return response.json()
+
+ def get_csv(self, site, get_string):
+ url = ''.join(['https://', site, get_string])
+ response = requests.request('GET', url, headers={'User-Agent' : 'Electrum'})
+ reader = csv.DictReader(response.content.decode().split('\n'))
+ return list(reader)
+
+ def name(self):
+ return self.__class__.__name__
+
+ def update_safe(self, ccy):
+ try:
+ self.print_error("getting fx quotes for", ccy)
+ self.quotes = self.get_rates(ccy)
+ self.print_error("received fx quotes")
+ except BaseException as e:
+ self.print_error("failed fx quotes:", e)
+ self.on_quotes()
+
+ def update(self, ccy):
+ t = Thread(target=self.update_safe, args=(ccy,))
+ t.setDaemon(True)
+ t.start()
+
+ def read_historical_rates(self, ccy, cache_dir):
+ filename = os.path.join(cache_dir, self.name() + '_'+ ccy)
+ if os.path.exists(filename):
+ timestamp = os.stat(filename).st_mtime
+ try:
+ with open(filename, 'r', encoding='utf-8') as f:
+ h = json.loads(f.read())
+ h['timestamp'] = timestamp
+ except:
+ h = None
+ else:
+ h = None
+ if h:
+ self.history[ccy] = h
+ self.on_history()
+ return h
+
+ def get_historical_rates_safe(self, ccy, cache_dir):
+ try:
+ self.print_error("requesting fx history for", ccy)
+ h = self.request_history(ccy)
+ self.print_error("received fx history for", ccy)
+ except BaseException as e:
+ self.print_error("failed fx history:", e)
+ return
+ filename = os.path.join(cache_dir, self.name() + '_' + ccy)
+ with open(filename, 'w', encoding='utf-8') as f:
+ f.write(json.dumps(h))
+ h['timestamp'] = time.time()
+ self.history[ccy] = h
+ self.on_history()
+
+ def get_historical_rates(self, ccy, cache_dir):
+ if ccy not in self.history_ccys():
+ return
+ h = self.history.get(ccy)
+ if h is None:
+ h = self.read_historical_rates(ccy, cache_dir)
+ if h is None or h['timestamp'] < time.time() - 24*3600:
+ t = Thread(target=self.get_historical_rates_safe, args=(ccy, cache_dir))
+ t.setDaemon(True)
+ t.start()
+
+ def history_ccys(self):
+ return []
+
+ def historical_rate(self, ccy, d_t):
+ return self.history.get(ccy, {}).get(d_t.strftime('%Y-%m-%d'), 'NaN')
+
+ def get_currencies(self):
+ rates = self.get_rates('')
+ return sorted([str(a) for (a, b) in rates.items() if b is not None and len(a)==3])
+
+class BitcoinAverage(ExchangeBase):
+
+ def get_rates(self, ccy):
+ json = self.get_json('apiv2.bitcoinaverage.com', '/indices/global/ticker/short')
+ return dict([(r.replace("BTC", ""), Decimal(json[r]['last']))
+ for r in json if r != 'timestamp'])
+
+ def history_ccys(self):
+ return ['AUD', 'BRL', 'CAD', 'CHF', 'CNY', 'EUR', 'GBP', 'IDR', 'ILS',
+ 'MXN', 'NOK', 'NZD', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'USD',
+ 'ZAR']
+
+ def request_history(self, ccy):
+ history = self.get_csv('apiv2.bitcoinaverage.com',
+ "/indices/global/history/BTC%s?period=alltime&format=csv" % ccy)
+ return dict([(h['DateTime'][:10], h['Average'])
+ for h in history])
+
+
+class Bitcointoyou(ExchangeBase):
+
+ def get_rates(self, ccy):
+ json = self.get_json('bitcointoyou.com', "/API/ticker.aspx")
+ return {'BRL': Decimal(json['ticker']['last'])}
+
+ def history_ccys(self):
+ return ['BRL']
+
+
+class BitcoinVenezuela(ExchangeBase):
+
+ def get_rates(self, ccy):
+ json = self.get_json('api.bitcoinvenezuela.com', '/')
+ rates = [(r, json['BTC'][r]) for r in json['BTC']
+ if json['BTC'][r] is not None] # Giving NULL for LTC
+ return dict(rates)
+
+ def history_ccys(self):
+ return ['ARS', 'EUR', 'USD', 'VEF']
+
+ def request_history(self, ccy):
+ return self.get_json('api.bitcoinvenezuela.com',
+ "/historical/index.php?coin=BTC")[ccy +'_BTC']
+
+
+class Bitbank(ExchangeBase):
+
+ def get_rates(self, ccy):
+ json = self.get_json('public.bitbank.cc', '/btc_jpy/ticker')
+ return {'JPY': Decimal(json['data']['last'])}
+
+
+class BitFlyer(ExchangeBase):
+
+ def get_rates(self, ccy):
+ json = self.get_json('bitflyer.jp', '/api/echo/price')
+ return {'JPY': Decimal(json['mid'])}
+
+
+class Bitmarket(ExchangeBase):
+
+ def get_rates(self, ccy):
+ json = self.get_json('www.bitmarket.pl', '/json/BTCPLN/ticker.json')
+ return {'PLN': Decimal(json['last'])}
+
+
+class BitPay(ExchangeBase):
+
+ def get_rates(self, ccy):
+ json = self.get_json('bitpay.com', '/api/rates')
+ return dict([(r['code'], Decimal(r['rate'])) for r in json])
+
+
+class Bitso(ExchangeBase):
+
+ def get_rates(self, ccy):
+ json = self.get_json('api.bitso.com', '/v2/ticker')
+ return {'MXN': Decimal(json['last'])}
+
+
+class BitStamp(ExchangeBase):
+
+ def get_rates(self, ccy):
+ json = self.get_json('www.bitstamp.net', '/api/ticker/')
+ return {'USD': Decimal(json['last'])}
+
+
+class Bitvalor(ExchangeBase):
+
+ def get_rates(self,ccy):
+ json = self.get_json('api.bitvalor.com', '/v1/ticker.json')
+ return {'BRL': Decimal(json['ticker_1h']['total']['last'])}
+
+
+class BlockchainInfo(ExchangeBase):
+
+ def get_rates(self, ccy):
+ json = self.get_json('blockchain.info', '/ticker')
+ return dict([(r, Decimal(json[r]['15m'])) for r in json])
+
+
+class BTCChina(ExchangeBase):
+
+ def get_rates(self, ccy):
+ json = self.get_json('data.btcchina.com', '/data/ticker')
+ return {'CNY': Decimal(json['ticker']['last'])}
+
+
+class BTCParalelo(ExchangeBase):
+
+ def get_rates(self, ccy):
+ json = self.get_json('btcparalelo.com', '/api/price')
+ return {'VEF': Decimal(json['price'])}
+
+
+class Coinbase(ExchangeBase):
+
+ def get_rates(self, ccy):
+ json = self.get_json('coinbase.com',
+ '/api/v1/currencies/exchange_rates')
+ return dict([(r[7:].upper(), Decimal(json[r]))
+ for r in json if r.startswith('btc_to_')])
+
+
+class CoinDesk(ExchangeBase):
+
+ def get_currencies(self):
+ dicts = self.get_json('api.coindesk.com',
+ '/v1/bpi/supported-currencies.json')
+ return [d['currency'] for d in dicts]
+
+ def get_rates(self, ccy):
+ json = self.get_json('api.coindesk.com',
+ '/v1/bpi/currentprice/%s.json' % ccy)
+ result = {ccy: Decimal(json['bpi'][ccy]['rate_float'])}
+ return result
+
+ def history_starts(self):
+ return { 'USD': '2012-11-30', 'EUR': '2013-09-01' }
+
+ def history_ccys(self):
+ return self.history_starts().keys()
+
+ def request_history(self, ccy):
+ start = self.history_starts()[ccy]
+ end = datetime.today().strftime('%Y-%m-%d')
+ # Note ?currency and ?index don't work as documented. Sigh.
+ query = ('/v1/bpi/historical/close.json?start=%s&end=%s'
+ % (start, end))
+ json = self.get_json('api.coindesk.com', query)
+ return json['bpi']
+
+
+class Coinsecure(ExchangeBase):
+
+ def get_rates(self, ccy):
+ json = self.get_json('api.coinsecure.in', '/v0/noauth/newticker')
+ return {'INR': Decimal(json['lastprice'] / 100.0 )}
+
+
+class Foxbit(ExchangeBase):
+
+ def get_rates(self,ccy):
+ json = self.get_json('api.bitvalor.com', '/v1/ticker.json')
+ return {'BRL': Decimal(json['ticker_1h']['exchanges']['FOX']['last'])}
+
+
+class itBit(ExchangeBase):
+
+ def get_rates(self, ccy):
+ ccys = ['USD', 'EUR', 'SGD']
+ json = self.get_json('api.itbit.com', '/v1/markets/XBT%s/ticker' % ccy)
+ result = dict.fromkeys(ccys)
+ if ccy in ccys:
+ result[ccy] = Decimal(json['lastPrice'])
+ return result
+
+
+class Kraken(ExchangeBase):
+
+ def get_rates(self, ccy):
+ ccys = ['EUR', 'USD', 'CAD', 'GBP', 'JPY']
+ pairs = ['XBT%s' % c for c in ccys]
+ json = self.get_json('api.kraken.com',
+ '/0/public/Ticker?pair=%s' % ','.join(pairs))
+ return dict((k[-3:], Decimal(float(v['c'][0])))
+ for k, v in json['result'].items())
+
+
+class LocalBitcoins(ExchangeBase):
+
+ def get_rates(self, ccy):
+ json = self.get_json('localbitcoins.com',
+ '/bitcoinaverage/ticker-all-currencies/')
+ return dict([(r, Decimal(json[r]['rates']['last'])) for r in json])
+
+
+class MercadoBitcoin(ExchangeBase):
+
+ def get_rates(self, ccy):
+ json = self.get_json('api.bitvalor.com', '/v1/ticker.json')
+ return {'BRL': Decimal(json['ticker_1h']['exchanges']['MBT']['last'])}
+
+
+class NegocieCoins(ExchangeBase):
+
+ def get_rates(self,ccy):
+ json = self.get_json('api.bitvalor.com', '/v1/ticker.json')
+ return {'BRL': Decimal(json['ticker_1h']['exchanges']['NEG']['last'])}
+
+class TheRockTrading(ExchangeBase):
+
+ def get_rates(self, ccy):
+ json = self.get_json('api.therocktrading.com',
+ '/v1/funds/BTCEUR/ticker')
+ return {'EUR': Decimal(json['last'])}
+
+class Unocoin(ExchangeBase):
+
+ def get_rates(self, ccy):
+ json = self.get_json('www.unocoin.com', 'trade?buy')
+ return {'INR': Decimal(json)}
+
+
+class WEX(ExchangeBase):
+
+ def get_rates(self, ccy):
+ json_eur = self.get_json('wex.nz', '/api/3/ticker/btc_eur')
+ json_rub = self.get_json('wex.nz', '/api/3/ticker/btc_rur')
+ json_usd = self.get_json('wex.nz', '/api/3/ticker/btc_usd')
+ return {'EUR': Decimal(json_eur['btc_eur']['last']),
+ 'RUB': Decimal(json_rub['btc_rur']['last']),
+ 'USD': Decimal(json_usd['btc_usd']['last'])}
+
+
+class Winkdex(ExchangeBase):
+
+ def get_rates(self, ccy):
+ json = self.get_json('winkdex.com', '/api/v0/price')
+ return {'USD': Decimal(json['price'] / 100.0)}
+
+ def history_ccys(self):
+ return ['USD']
+
+ def request_history(self, ccy):
+ json = self.get_json('winkdex.com',
+ "/api/v0/series?start_time=1342915200")
+ history = json['series'][0]['results']
+ return dict([(h['timestamp'][:10], h['price'] / 100.0)
+ for h in history])
+
+
+class Zaif(ExchangeBase):
+ def get_rates(self, ccy):
+ json = self.get_json('api.zaif.jp', '/api/1/last_price/btc_jpy')
+ return {'JPY': Decimal(json['last_price'])}
+
+
+def dictinvert(d):
+ inv = {}
+ for k, vlist in d.items():
+ for v in vlist:
+ keys = inv.setdefault(v, [])
+ keys.append(k)
+ return inv
+
+def get_exchanges_and_currencies():
+ import os, json
+ path = os.path.join(os.path.dirname(__file__), 'currencies.json')
+ try:
+ with open(path, 'r', encoding='utf-8') as f:
+ return json.loads(f.read())
+ except:
+ pass
+ d = {}
+ is_exchange = lambda obj: (inspect.isclass(obj)
+ and issubclass(obj, ExchangeBase)
+ and obj != ExchangeBase)
+ exchanges = dict(inspect.getmembers(sys.modules[__name__], is_exchange))
+ for name, klass in exchanges.items():
+ exchange = klass(None, None)
+ try:
+ d[name] = exchange.get_currencies()
+ print(name, "ok")
+ except:
+ print(name, "error")
+ continue
+ with open(path, 'w', encoding='utf-8') as f:
+ f.write(json.dumps(d, indent=4, sort_keys=True))
+ return d
+
+
+CURRENCIES = get_exchanges_and_currencies()
+
+
+def get_exchanges_by_ccy(history=True):
+ if not history:
+ return dictinvert(CURRENCIES)
+ d = {}
+ exchanges = CURRENCIES.keys()
+ for name in exchanges:
+ klass = globals()[name]
+ exchange = klass(None, None)
+ d[name] = exchange.history_ccys()
+ return dictinvert(d)
+
+
+class FxThread(ThreadJob):
+
+ def __init__(self, config, network):
+ self.config = config
+ self.network = network
+ self.ccy = self.get_currency()
+ self.history_used_spot = False
+ self.ccy_combo = None
+ self.hist_checkbox = None
+ self.cache_dir = os.path.join(config.path, 'cache')
+ self.set_exchange(self.config_exchange())
+ make_dir(self.cache_dir)
+
+ def get_currencies(self, h):
+ d = get_exchanges_by_ccy(h)
+ return sorted(d.keys())
+
+ def get_exchanges_by_ccy(self, ccy, h):
+ d = get_exchanges_by_ccy(h)
+ return d.get(ccy, [])
+
+ def ccy_amount_str(self, amount, commas):
+ prec = CCY_PRECISIONS.get(self.ccy, 2)
+ fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec))
+ try:
+ rounded_amount = round(amount, prec)
+ except decimal.InvalidOperation:
+ rounded_amount = amount
+ return fmt_str.format(rounded_amount)
+
+ def run(self):
+ # This runs from the plugins thread which catches exceptions
+ if self.is_enabled():
+ if self.timeout ==0 and self.show_history():
+ self.exchange.get_historical_rates(self.ccy, self.cache_dir)
+ if self.timeout <= time.time():
+ self.timeout = time.time() + 150
+ self.exchange.update(self.ccy)
+
+ def is_enabled(self):
+ return bool(self.config.get('use_exchange_rate'))
+
+ def set_enabled(self, b):
+ return self.config.set_key('use_exchange_rate', bool(b))
+
+ def get_history_config(self):
+ return bool(self.config.get('history_rates'))
+
+ def set_history_config(self, b):
+ self.config.set_key('history_rates', bool(b))
+
+ def get_history_capital_gains_config(self):
+ return bool(self.config.get('history_rates_capital_gains', False))
+
+ def set_history_capital_gains_config(self, b):
+ self.config.set_key('history_rates_capital_gains', bool(b))
+
+ def get_fiat_address_config(self):
+ return bool(self.config.get('fiat_address'))
+
+ def set_fiat_address_config(self, b):
+ self.config.set_key('fiat_address', bool(b))
+
+ def get_currency(self):
+ '''Use when dynamic fetching is needed'''
+ return self.config.get("currency", "EUR")
+
+ def config_exchange(self):
+ return self.config.get('use_exchange', 'BitcoinAverage')
+
+ def show_history(self):
+ return self.is_enabled() and self.get_history_config() and self.ccy in self.exchange.history_ccys()
+
+ def set_currency(self, ccy):
+ self.ccy = ccy
+ self.config.set_key('currency', ccy, True)
+ self.timeout = 0 # Because self.ccy changes
+ self.on_quotes()
+
+ def set_exchange(self, name):
+ class_ = globals().get(name, BitcoinAverage)
+ self.print_error("using exchange", name)
+ if self.config_exchange() != name:
+ self.config.set_key('use_exchange', name, True)
+ self.exchange = class_(self.on_quotes, self.on_history)
+ # A new exchange means new fx quotes, initially empty. Force
+ # a quote refresh
+ self.timeout = 0
+ self.exchange.read_historical_rates(self.ccy, self.cache_dir)
+
+ def on_quotes(self):
+ if self.network:
+ self.network.trigger_callback('on_quotes')
+
+ def on_history(self):
+ if self.network:
+ self.network.trigger_callback('on_history')
+
+ def exchange_rate(self):
+ '''Returns None, or the exchange rate as a Decimal'''
+ rate = self.exchange.quotes.get(self.ccy)
+ if rate is None:
+ return Decimal('NaN')
+ return Decimal(rate)
+
+ def format_amount(self, btc_balance):
+ rate = self.exchange_rate()
+ return '' if rate.is_nan() else "%s" % self.value_str(btc_balance, rate)
+
+ def format_amount_and_units(self, btc_balance):
+ rate = self.exchange_rate()
+ return '' if rate.is_nan() else "%s %s" % (self.value_str(btc_balance, rate), self.ccy)
+
+ def get_fiat_status_text(self, btc_balance, base_unit, decimal_point):
+ rate = self.exchange_rate()
+ return _(" (No FX rate available)") if rate.is_nan() else " 1 %s~%s %s" % (base_unit,
+ self.value_str(COIN / (10**(8 - decimal_point)), rate), self.ccy)
+
+ def fiat_value(self, satoshis, rate):
+ return Decimal('NaN') if satoshis is None else Decimal(satoshis) / COIN * Decimal(rate)
+
+ def value_str(self, satoshis, rate):
+ return self.format_fiat(self.fiat_value(satoshis, rate))
+
+ def format_fiat(self, value):
+ if value.is_nan():
+ return _("No data")
+ return "%s" % (self.ccy_amount_str(value, True))
+
+ def history_rate(self, d_t):
+ if d_t is None:
+ return Decimal('NaN')
+ rate = self.exchange.historical_rate(self.ccy, d_t)
+ # Frequently there is no rate for today, until tomorrow :)
+ # Use spot quotes in that case
+ if rate == 'NaN' and (datetime.today().date() - d_t.date()).days <= 2:
+ rate = self.exchange.quotes.get(self.ccy, 'NaN')
+ self.history_used_spot = True
+ return Decimal(rate)
+
+ def historical_value_str(self, satoshis, d_t):
+ return self.format_fiat(self.historical_value(satoshis, d_t))
+
+ def historical_value(self, satoshis, d_t):
+ return self.fiat_value(satoshis, self.history_rate(d_t))
+
+ def timestamp_rate(self, timestamp):
+ from .util import timestamp_to_datetime
+ date = timestamp_to_datetime(timestamp)
+ return self.history_rate(date)
DIR diff --git a/gui/__init__.py b/electrum/gui/__init__.py
DIR diff --git a/electrum/gui/kivy/Makefile b/electrum/gui/kivy/Makefile
t@@ -0,0 +1,32 @@
+PYTHON = python3
+
+# needs kivy installed or in PYTHONPATH
+
+.PHONY: theming apk clean
+
+theming:
+ $(PYTHON) -m kivy.atlas theming/light 1024 theming/light/*.png
+prepare:
+ # running pre build setup
+ @cp tools/buildozer.spec ../../buildozer.spec
+ # copy electrum to main.py
+ @cp ../../../run_electrum ../../main.py
+ @-if [ ! -d "../../.buildozer" ];then \
+ cd ../..; buildozer android debug;\
+ cp -f electrum/gui/kivy/tools/blacklist.txt .buildozer/android/platform/python-for-android/src/blacklist.txt;\
+ rm -rf ./.buildozer/android/platform/python-for-android/dist;\
+ fi
+apk:
+ @make prepare
+ @-cd ../..; buildozer android debug deploy run
+ @make clean
+release:
+ @make prepare
+ @-cd ../..; buildozer android release
+ @make clean
+clean:
+ # Cleaning up
+ # rename main.py to electrum
+ @-rm ../../main.py
+ # remove buildozer.spec
+ @-rm ../../buildozer.spec
DIR diff --git a/gui/kivy/Readme.md b/electrum/gui/kivy/Readme.md
DIR diff --git a/gui/kivy/__init__.py b/electrum/gui/kivy/__init__.py
DIR diff --git a/gui/kivy/data/background.png b/electrum/gui/kivy/data/background.png
Binary files differ.
DIR diff --git a/gui/kivy/data/fonts/Roboto-Bold.ttf b/electrum/gui/kivy/data/fonts/Roboto-Bold.ttf
Binary files differ.
DIR diff --git a/gui/kivy/data/fonts/Roboto-Condensed.ttf b/electrum/gui/kivy/data/fonts/Roboto-Condensed.ttf
Binary files differ.
DIR diff --git a/gui/kivy/data/fonts/Roboto-Medium.ttf b/electrum/gui/kivy/data/fonts/Roboto-Medium.ttf
Binary files differ.
DIR diff --git a/gui/kivy/data/fonts/Roboto.ttf b/electrum/gui/kivy/data/fonts/Roboto.ttf
Binary files differ.
DIR diff --git a/gui/kivy/data/fonts/tron/License.txt b/electrum/gui/kivy/data/fonts/tron/License.txt
DIR diff --git a/gui/kivy/data/fonts/tron/Readme.txt b/electrum/gui/kivy/data/fonts/tron/Readme.txt
DIR diff --git a/gui/kivy/data/fonts/tron/Tr2n.ttf b/electrum/gui/kivy/data/fonts/tron/Tr2n.ttf
Binary files differ.
DIR diff --git a/gui/kivy/data/glsl/default.fs b/electrum/gui/kivy/data/glsl/default.fs
DIR diff --git a/gui/kivy/data/glsl/default.png b/electrum/gui/kivy/data/glsl/default.png
Binary files differ.
DIR diff --git a/gui/kivy/data/glsl/default.vs b/electrum/gui/kivy/data/glsl/default.vs
DIR diff --git a/gui/kivy/data/glsl/header.fs b/electrum/gui/kivy/data/glsl/header.fs
DIR diff --git a/gui/kivy/data/glsl/header.vs b/electrum/gui/kivy/data/glsl/header.vs
DIR diff --git a/gui/kivy/data/images/defaulttheme-0.png b/electrum/gui/kivy/data/images/defaulttheme-0.png
Binary files differ.
DIR diff --git a/gui/kivy/data/images/defaulttheme.atlas b/electrum/gui/kivy/data/images/defaulttheme.atlas
DIR diff --git a/gui/kivy/data/java-classes/org/electrum/qr/SimpleScannerActivity.java b/electrum/gui/kivy/data/java-classes/org/electrum/qr/SimpleScannerActivity.java
DIR diff --git a/gui/kivy/data/logo/kivy-icon-32.png b/electrum/gui/kivy/data/logo/kivy-icon-32.png
Binary files differ.
DIR diff --git a/gui/kivy/data/style.kv b/electrum/gui/kivy/data/style.kv
DIR diff --git a/gui/kivy/i18n.py b/electrum/gui/kivy/i18n.py
DIR diff --git a/electrum/gui/kivy/main.kv b/electrum/gui/kivy/main.kv
t@@ -0,0 +1,464 @@
+#:import Clock kivy.clock.Clock
+#:import Window kivy.core.window.Window
+#:import Factory kivy.factory.Factory
+#:import _ electrum.gui.kivy.i18n._
+
+
+###########################
+# Global Defaults
+###########################
+
+<Label>
+ markup: True
+ font_name: 'Roboto'
+ font_size: '16sp'
+ bound: False
+ on_text: if isinstance(self.text, _) and not self.bound: self.bound=True; _.bind(self)
+
+<TextInput>
+ on_focus: app._focused_widget = root
+ font_size: '18sp'
+
+<Button>
+ on_parent: self.MIN_STATE_TIME = 0.1
+
+<ListItemButton>
+ font_size: '12sp'
+
+<Carousel>:
+ canvas.before:
+ Color:
+ rgba: 0.1, 0.1, 0.1, 1
+ Rectangle:
+ size: self.size
+ pos: self.pos
+
+<ActionView>:
+ canvas.before:
+ Color:
+ rgba: 0.1, 0.1, 0.1, 1
+ Rectangle:
+ size: self.size
+ pos: self.pos
+
+
+# Custom Global Widgets
+
+<TopLabel>
+ size_hint_y: None
+ text_size: self.width, None
+ height: self.texture_size[1]
+
+<VGridLayout@GridLayout>:
+ rows: 1
+ size_hint: 1, None
+ height: self.minimum_height
+
+
+
+<IconButton@Button>:
+ icon: ''
+ AnchorLayout:
+ pos: self.parent.pos
+ size: self.parent.size
+ orientation: 'lr-tb'
+ Image:
+ source: self.parent.parent.icon
+ size_hint_x: None
+ size: '30dp', '30dp'
+
+
+
+#########################
+# Dialogs
+#########################
+<BoxLabel@BoxLayout>
+ text: ''
+ value: ''
+ size_hint_y: None
+ height: max(lbl1.height, lbl2.height)
+ TopLabel
+ id: lbl1
+ text: root.text
+ pos_hint: {'top':1}
+ TopLabel
+ id: lbl2
+ text: root.value
+
+<OutputItem>
+ address: ''
+ value: ''
+ size_hint_y: None
+ height: max(lbl1.height, lbl2.height)
+ TopLabel
+ id: lbl1
+ text: '[ref=%s]%s[/ref]'%(root.address, root.address)
+ font_size: '6pt'
+ shorten: True
+ size_hint_x: 0.65
+ on_ref_press:
+ app._clipboard.copy(root.address)
+ app.show_info(_('Address copied to clipboard') + ' ' + root.address)
+ TopLabel
+ id: lbl2
+ text: root.value
+ font_size: '6pt'
+ size_hint_x: 0.35
+ halign: 'right'
+
+
+<OutputList>
+ viewclass: 'OutputItem'
+ size_hint: 1, None
+ height: min(output_list_layout.minimum_height, dp(144))
+ scroll_type: ['bars', 'content']
+ bar_width: dp(15)
+ RecycleBoxLayout:
+ orientation: 'vertical'
+ default_size: None, pt(6)
+ default_size_hint: 1, None
+ size_hint: 1, None
+ height: self.minimum_height
+ id: output_list_layout
+ spacing: '10dp'
+ padding: '10dp'
+ canvas.before:
+ Color:
+ rgb: .3, .3, .3
+ Rectangle:
+ size: self.size
+ pos: self.pos
+
+<RefLabel>
+ font_size: '6pt'
+ name: ''
+ data: ''
+ text: self.data
+ touched: False
+ padding: '10dp', '10dp'
+ on_touch_down:
+ touch = args[1]
+ if self.collide_point(*touch.pos): app.on_ref_label(self, touch)
+ else: self.touched = False
+ canvas.before:
+ Color:
+ rgb: .3, .3, .3
+ Rectangle:
+ size: self.size
+ pos: self.pos
+
+<TxHashLabel@RefLabel>
+ data: ''
+ text: ' '.join(map(''.join, zip(*[iter(self.data)]*4))) if self.data else ''
+
+<InfoBubble>
+ size_hint: None, None
+ width: '270dp' if root.fs else min(self.width, dp(270))
+ height: self.width if self.fs else (lbl.texture_size[1] + dp(27))
+ BoxLayout:
+ padding: '5dp' if root.fs else 0
+ Widget:
+ size_hint: None, 1
+ width: '4dp' if root.fs else '2dp'
+ Image:
+ id: img
+ source: root.icon
+ mipmap: True
+ size_hint: None, 1
+ width: (root.width - dp(20)) if root.fs else (0 if not root.icon else '32dp')
+ Widget:
+ size_hint_x: None
+ width: '5dp'
+ Label:
+ id: lbl
+ markup: True
+ font_size: '12sp'
+ text: root.message
+ text_size: self.width, None
+ valign: 'middle'
+ size_hint: 1, 1
+ width: 0 if root.fs else (root.width - img.width)
+
+
+<SendReceiveBlueBottom@GridLayout>
+ item_height: dp(42)
+ foreground_color: .843, .914, .972, 1
+ cols: 1
+ padding: '12dp', 0
+ canvas.before:
+ Color:
+ rgba: 0.192, .498, 0.745, 1
+ BorderImage:
+ source: 'atlas://electrum/gui/kivy/theming/light/card_bottom'
+ size: self.size
+ pos: self.pos
+
+
+<AddressFilter@GridLayout>
+ item_height: dp(42)
+ item_width: dp(60)
+ foreground_color: .843, .914, .972, 1
+ cols: 1
+ canvas.before:
+ Color:
+ rgba: 0.192, .498, 0.745, 1
+ BorderImage:
+ source: 'atlas://electrum/gui/kivy/theming/light/card_bottom'
+ size: self.size
+ pos: self.pos
+
+<SearchBox@GridLayout>
+ item_height: dp(42)
+ foreground_color: .843, .914, .972, 1
+ cols: 1
+ padding: '12dp', 0
+ canvas.before:
+ Color:
+ rgba: 0.192, .498, 0.745, 1
+ BorderImage:
+ source: 'atlas://electrum/gui/kivy/theming/light/card_bottom'
+ size: self.size
+ pos: self.pos
+
+<CardSeparator@Widget>
+ size_hint: 1, None
+ height: dp(1)
+ color: .909, .909, .909, 1
+ canvas:
+ Color:
+ rgba: root.color if root.color else (0, 0, 0, 0)
+ Rectangle:
+ size: self.size
+ pos: self.pos
+
+<CardItem@ToggleButtonBehavior+BoxLayout>
+ size_hint: 1, None
+ height: '65dp'
+ group: 'requests'
+ padding: dp(12)
+ spacing: dp(5)
+ screen: None
+ on_release:
+ self.screen.show_menu(args[0]) if self.state == 'down' else self.screen.hide_menu()
+ canvas.before:
+ Color:
+ rgba: (0.192, .498, 0.745, 1) if self.state == 'down' else (0.15, 0.15, 0.17, 1)
+ Rectangle:
+ size: self.size
+ pos: self.pos
+
+<BlueButton@Button>:
+ background_color: 1, .585, .878, 0
+ halign: 'left'
+ text_size: (self.width-10, None)
+ size_hint: 0.5, None
+ default_text: ''
+ text: self.default_text
+ padding: '5dp', '5dp'
+ height: '40dp'
+ text_color: self.foreground_color
+ disabled_color: 1, 1, 1, 1
+ foreground_color: 1, 1, 1, 1
+ canvas.before:
+ Color:
+ rgba: (0.9, .498, 0.745, 1) if self.state == 'down' else self.background_color
+ Rectangle:
+ size: self.size
+ pos: self.pos
+
+<AddressButton@Button>:
+ background_color: 1, .585, .878, 0
+ halign: 'center'
+ text_size: (self.width, None)
+ shorten: True
+ size_hint: 0.5, None
+ default_text: ''
+ text: self.default_text
+ padding: '5dp', '5dp'
+ height: '40dp'
+ text_color: self.foreground_color
+ disabled_color: 1, 1, 1, 1
+ foreground_color: 1, 1, 1, 1
+ canvas.before:
+ Color:
+ rgba: (0.9, .498, 0.745, 1) if self.state == 'down' else self.background_color
+ Rectangle:
+ size: self.size
+ pos: self.pos
+
+<KButton@Button>:
+ size_hint: 1, None
+ height: '60dp'
+ font_size: '30dp'
+ on_release:
+ self.parent.update_amount(self.text)
+
+
+<StripLayout>
+ padding: 0, 0, 0, 0
+
+<TabbedPanelStrip>:
+ on_parent:
+ if self.parent: self.parent.bar_width = 0
+ if self.parent: self.parent.scroll_x = 0.5
+
+
+<TabbedCarousel>
+ carousel: carousel
+ do_default_tab: False
+ Carousel:
+ scroll_timeout: 250
+ scroll_distance: '100dp'
+ anim_type: 'out_quart'
+ min_move: .05
+ anim_move_duration: .1
+ anim_cancel_duration: .54
+ on_index: root.on_index(*args)
+ id: carousel
+
+
+
+<CleanHeader@TabbedPanelHeader>
+ border: 16, 0, 16, 0
+ markup: False
+ text_size: self.size
+ halign: 'center'
+ valign: 'middle'
+ bold: True
+ font_size: '12.5sp'
+ background_normal: 'atlas://electrum/gui/kivy/theming/light/tab_btn'
+ background_down: 'atlas://electrum/gui/kivy/theming/light/tab_btn_pressed'
+
+
+<ColoredLabel@Label>:
+ font_size: '48sp'
+ color: (.6, .6, .6, 1)
+ canvas.before:
+ Color:
+ rgb: (.9, .9, .9)
+ Rectangle:
+ pos: self.x + sp(2), self.y + sp(2)
+ size: self.width - sp(4), self.height - sp(4)
+
+
+<SettingsItem@ButtonBehavior+BoxLayout>
+ orientation: 'vertical'
+ title: ''
+ description: ''
+ size_hint: 1, None
+ height: '60dp'
+ canvas.before:
+ Color:
+ rgba: (0.192, .498, 0.745, 1) if self.state == 'down' else (0.3, 0.3, 0.3, 0)
+ Rectangle:
+ size: self.size
+ pos: self.pos
+ on_release:
+ Clock.schedule_once(self.action)
+ Widget
+ TopLabel:
+ id: title
+ text: self.parent.title
+ bold: True
+ halign: 'left'
+ TopLabel:
+ text: self.parent.description
+ color: 0.8, 0.8, 0.8, 1
+ halign: 'left'
+ Widget
+
+
+
+
+<ScreenTabs@Screen>
+ TabbedCarousel:
+ id: panel
+ tab_height: '48dp'
+ tab_width: panel.width/3
+ strip_border: 0, 0, 0, 0
+ SendScreen:
+ id: send_screen
+ tab: send_tab
+ HistoryScreen:
+ id: history_screen
+ tab: history_tab
+ ReceiveScreen:
+ id: receive_screen
+ tab: receive_tab
+ CleanHeader:
+ id: send_tab
+ text: _('Send')
+ slide: 0
+ CleanHeader:
+ id: history_tab
+ text: _('Balance')
+ slide: 1
+ CleanHeader:
+ id: receive_tab
+ text: _('Receive')
+ slide: 2
+
+
+<ActionOvrButton@ActionButton>
+ #on_release:
+ # fixme: the following line was commented out because it does not seem to do what it is intended
+ # Clock.schedule_once(lambda dt: self.parent.parent.dismiss() if self.parent else None, 0.05)
+ on_press:
+ Clock.schedule_once(lambda dt: app.popup_dialog(self.name), 0.05)
+ self.state = 'normal'
+
+
+BoxLayout:
+ orientation: 'vertical'
+
+ canvas.before:
+ Color:
+ rgb: .6, .6, .6
+ Rectangle:
+ size: self.size
+ source: 'electrum/gui/kivy/data/background.png'
+
+ ActionBar:
+
+ ActionView:
+ id: av
+ ActionPrevious:
+ app_icon: 'atlas://electrum/gui/kivy/theming/light/logo'
+ app_icon_width: '100dp'
+ with_previous: False
+ size_hint_x: None
+ on_release: app.popup_dialog('network')
+
+ ActionButton:
+ id: action_status
+ important: True
+ size_hint: 1, 1
+ bold: True
+ color: 0.7, 0.7, 0.7, 1
+ text: app.status
+ font_size: '22dp'
+ #minimum_width: '1dp'
+ on_release: app.popup_dialog('status')
+
+ ActionOverflow:
+ id: ao
+ ActionOvrButton:
+ name: 'about'
+ text: _('About')
+ ActionOvrButton:
+ name: 'wallets'
+ text: _('Wallets')
+ ActionOvrButton:
+ name: 'network'
+ text: _('Network')
+ ActionOvrButton:
+ name: 'settings'
+ text: _('Settings')
+ on_parent:
+ # when widget overflow drop down is shown, adjust the width
+ parent = args[1]
+ if parent: ao._dropdown.width = sp(200)
+ ScreenManager:
+ id: manager
+ ScreenTabs:
+ id: tabs
DIR diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
t@@ -0,0 +1,1028 @@
+import re
+import os
+import sys
+import time
+import datetime
+import traceback
+from decimal import Decimal
+import threading
+
+from electrum.bitcoin import TYPE_ADDRESS
+from electrum.storage import WalletStorage
+from electrum.wallet import Wallet
+from electrum.i18n import _
+from electrum.paymentrequest import InvoiceStore
+from electrum.util import profiler, InvalidPassword
+from electrum.plugin import run_hook
+from electrum.util import format_satoshis, format_satoshis_plain
+from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
+
+from kivy.app import App
+from kivy.core.window import Window
+from kivy.logger import Logger
+from kivy.utils import platform
+from kivy.properties import (OptionProperty, AliasProperty, ObjectProperty,
+ StringProperty, ListProperty, BooleanProperty, NumericProperty)
+from kivy.cache import Cache
+from kivy.clock import Clock
+from kivy.factory import Factory
+from kivy.metrics import inch
+from kivy.lang import Builder
+
+## lazy imports for factory so that widgets can be used in kv
+#Factory.register('InstallWizard', module='electrum.gui.kivy.uix.dialogs.installwizard')
+#Factory.register('InfoBubble', module='electrum.gui.kivy.uix.dialogs')
+#Factory.register('OutputList', module='electrum.gui.kivy.uix.dialogs')
+#Factory.register('OutputItem', module='electrum.gui.kivy.uix.dialogs')
+
+from .uix.dialogs.installwizard import InstallWizard
+from .uix.dialogs import InfoBubble, crash_reporter
+from .uix.dialogs import OutputList, OutputItem
+from .uix.dialogs import TopLabel, RefLabel
+
+#from kivy.core.window import Window
+#Window.softinput_mode = 'below_target'
+
+# delayed imports: for startup speed on android
+notification = app = ref = None
+util = False
+
+# register widget cache for keeping memory down timeout to forever to cache
+# the data
+Cache.register('electrum_widgets', timeout=0)
+
+from kivy.uix.screenmanager import Screen
+from kivy.uix.tabbedpanel import TabbedPanel
+from kivy.uix.label import Label
+from kivy.core.clipboard import Clipboard
+
+Factory.register('TabbedCarousel', module='electrum.gui.kivy.uix.screens')
+
+# Register fonts without this you won't be able to use bold/italic...
+# inside markup.
+from kivy.core.text import Label
+Label.register('Roboto',
+ 'electrum/gui/kivy/data/fonts/Roboto.ttf',
+ 'electrum/gui/kivy/data/fonts/Roboto.ttf',
+ 'electrum/gui/kivy/data/fonts/Roboto-Bold.ttf',
+ 'electrum/gui/kivy/data/fonts/Roboto-Bold.ttf')
+
+
+from electrum.util import (base_units, NoDynamicFeeEstimates, decimal_point_to_base_unit_name,
+ base_unit_name_to_decimal_point, NotEnoughFunds)
+
+
+class ElectrumWindow(App):
+
+ electrum_config = ObjectProperty(None)
+ language = StringProperty('en')
+
+ # properties might be updated by the network
+ num_blocks = NumericProperty(0)
+ num_nodes = NumericProperty(0)
+ server_host = StringProperty('')
+ server_port = StringProperty('')
+ num_chains = NumericProperty(0)
+ blockchain_name = StringProperty('')
+ fee_status = StringProperty('Fee')
+ balance = StringProperty('')
+ fiat_balance = StringProperty('')
+ is_fiat = BooleanProperty(False)
+ blockchain_checkpoint = NumericProperty(0)
+
+ auto_connect = BooleanProperty(False)
+ def on_auto_connect(self, instance, x):
+ host, port, protocol, proxy, auto_connect = self.network.get_parameters()
+ self.network.set_parameters(host, port, protocol, proxy, self.auto_connect)
+ def toggle_auto_connect(self, x):
+ self.auto_connect = not self.auto_connect
+
+ def choose_server_dialog(self, popup):
+ from .uix.dialogs.choice_dialog import ChoiceDialog
+ protocol = 's'
+ def cb2(host):
+ from electrum import constants
+ pp = servers.get(host, constants.net.DEFAULT_PORTS)
+ port = pp.get(protocol, '')
+ popup.ids.host.text = host
+ popup.ids.port.text = port
+ servers = self.network.get_servers()
+ ChoiceDialog(_('Choose a server'), sorted(servers), popup.ids.host.text, cb2).open()
+
+ def choose_blockchain_dialog(self, dt):
+ from .uix.dialogs.choice_dialog import ChoiceDialog
+ chains = self.network.get_blockchains()
+ def cb(name):
+ for index, b in self.network.blockchains.items():
+ if name == b.get_name():
+ self.network.follow_chain(index)
+ names = [self.network.blockchains[b].get_name() for b in chains]
+ if len(names) > 1:
+ cur_chain = self.network.blockchain().get_name()
+ ChoiceDialog(_('Choose your chain'), names, cur_chain, cb).open()
+
+ use_rbf = BooleanProperty(False)
+ def on_use_rbf(self, instance, x):
+ self.electrum_config.set_key('use_rbf', self.use_rbf, True)
+
+ use_change = BooleanProperty(False)
+ def on_use_change(self, instance, x):
+ self.electrum_config.set_key('use_change', self.use_change, True)
+
+ use_unconfirmed = BooleanProperty(False)
+ def on_use_unconfirmed(self, instance, x):
+ self.electrum_config.set_key('confirmed_only', not self.use_unconfirmed, True)
+
+ def set_URI(self, uri):
+ self.switch_to('send')
+ self.send_screen.set_URI(uri)
+
+ def on_new_intent(self, intent):
+ if intent.getScheme() != 'bitcoin':
+ return
+ uri = intent.getDataString()
+ self.set_URI(uri)
+
+ def on_language(self, instance, language):
+ Logger.info('language: {}'.format(language))
+ _.switch_lang(language)
+
+ def update_history(self, *dt):
+ if self.history_screen:
+ self.history_screen.update()
+
+ def on_quotes(self, d):
+ Logger.info("on_quotes")
+ self._trigger_update_history()
+
+ def on_history(self, d):
+ Logger.info("on_history")
+ self._trigger_update_history()
+
+ def _get_bu(self):
+ decimal_point = self.electrum_config.get('decimal_point', 5)
+ return decimal_point_to_base_unit_name(decimal_point)
+
+ def _set_bu(self, value):
+ assert value in base_units.keys()
+ decimal_point = base_unit_name_to_decimal_point(value)
+ self.electrum_config.set_key('decimal_point', decimal_point, True)
+ self._trigger_update_status()
+ self._trigger_update_history()
+
+ base_unit = AliasProperty(_get_bu, _set_bu)
+ status = StringProperty('')
+ fiat_unit = StringProperty('')
+
+ def on_fiat_unit(self, a, b):
+ self._trigger_update_history()
+
+ def decimal_point(self):
+ return base_units[self.base_unit]
+
+ def btc_to_fiat(self, amount_str):
+ if not amount_str:
+ return ''
+ if not self.fx.is_enabled():
+ return ''
+ rate = self.fx.exchange_rate()
+ if rate.is_nan():
+ return ''
+ fiat_amount = self.get_amount(amount_str + ' ' + self.base_unit) * rate / pow(10, 8)
+ return "{:.2f}".format(fiat_amount).rstrip('0').rstrip('.')
+
+ def fiat_to_btc(self, fiat_amount):
+ if not fiat_amount:
+ return ''
+ rate = self.fx.exchange_rate()
+ if rate.is_nan():
+ return ''
+ satoshis = int(pow(10,8) * Decimal(fiat_amount) / Decimal(rate))
+ return format_satoshis_plain(satoshis, self.decimal_point())
+
+ def get_amount(self, amount_str):
+ a, u = amount_str.split()
+ assert u == self.base_unit
+ try:
+ x = Decimal(a)
+ except:
+ return None
+ p = pow(10, self.decimal_point())
+ return int(p * x)
+
+
+ _orientation = OptionProperty('landscape',
+ options=('landscape', 'portrait'))
+
+ def _get_orientation(self):
+ return self._orientation
+
+ orientation = AliasProperty(_get_orientation,
+ None,
+ bind=('_orientation',))
+ '''Tries to ascertain the kind of device the app is running on.
+ Cane be one of `tablet` or `phone`.
+
+ :data:`orientation` is a read only `AliasProperty` Defaults to 'landscape'
+ '''
+
+ _ui_mode = OptionProperty('phone', options=('tablet', 'phone'))
+
+ def _get_ui_mode(self):
+ return self._ui_mode
+
+ ui_mode = AliasProperty(_get_ui_mode,
+ None,
+ bind=('_ui_mode',))
+ '''Defines tries to ascertain the kind of device the app is running on.
+ Cane be one of `tablet` or `phone`.
+
+ :data:`ui_mode` is a read only `AliasProperty` Defaults to 'phone'
+ '''
+
+ def __init__(self, **kwargs):
+ # initialize variables
+ self._clipboard = Clipboard
+ self.info_bubble = None
+ self.nfcscanner = None
+ self.tabs = None
+ self.is_exit = False
+ self.wallet = None
+ self.pause_time = 0
+
+ App.__init__(self)#, **kwargs)
+
+ title = _('Electrum App')
+ self.electrum_config = config = kwargs.get('config', None)
+ self.language = config.get('language', 'en')
+ self.network = network = kwargs.get('network', None)
+ if self.network:
+ self.num_blocks = self.network.get_local_height()
+ self.num_nodes = len(self.network.get_interfaces())
+ host, port, protocol, proxy_config, auto_connect = self.network.get_parameters()
+ self.server_host = host
+ self.server_port = port
+ self.auto_connect = auto_connect
+ self.proxy_config = proxy_config if proxy_config else {}
+
+ self.plugins = kwargs.get('plugins', [])
+ self.gui_object = kwargs.get('gui_object', None)
+ self.daemon = self.gui_object.daemon
+ self.fx = self.daemon.fx
+
+ self.use_rbf = config.get('use_rbf', True)
+ self.use_change = config.get('use_change', True)
+ self.use_unconfirmed = not config.get('confirmed_only', False)
+
+ # create triggers so as to minimize updating a max of 2 times a sec
+ self._trigger_update_wallet = Clock.create_trigger(self.update_wallet, .5)
+ self._trigger_update_status = Clock.create_trigger(self.update_status, .5)
+ self._trigger_update_history = Clock.create_trigger(self.update_history, .5)
+ self._trigger_update_interfaces = Clock.create_trigger(self.update_interfaces, .5)
+ # cached dialogs
+ self._settings_dialog = None
+ self._password_dialog = None
+ self.fee_status = self.electrum_config.get_fee_status()
+
+ def wallet_name(self):
+ return os.path.basename(self.wallet.storage.path) if self.wallet else ' '
+
+ def on_pr(self, pr):
+ if not self.wallet:
+ self.show_error(_('No wallet loaded.'))
+ return
+ if pr.verify(self.wallet.contacts):
+ key = self.wallet.invoices.add(pr)
+ if self.invoices_screen:
+ self.invoices_screen.update()
+ status = self.wallet.invoices.get_status(key)
+ if status == PR_PAID:
+ self.show_error("invoice already paid")
+ self.send_screen.do_clear()
+ else:
+ if pr.has_expired():
+ self.show_error(_('Payment request has expired'))
+ else:
+ self.switch_to('send')
+ self.send_screen.set_request(pr)
+ else:
+ self.show_error("invoice error:" + pr.error)
+ self.send_screen.do_clear()
+
+ def on_qr(self, data):
+ from electrum.bitcoin import base_decode, is_address
+ data = data.strip()
+ if is_address(data):
+ self.set_URI(data)
+ return
+ if data.startswith('bitcoin:'):
+ self.set_URI(data)
+ return
+ # try to decode transaction
+ from electrum.transaction import Transaction
+ from electrum.util import bh2u
+ try:
+ text = bh2u(base_decode(data, None, base=43))
+ tx = Transaction(text)
+ tx.deserialize()
+ except:
+ tx = None
+ if tx:
+ self.tx_dialog(tx)
+ return
+ # show error
+ self.show_error("Unable to decode QR data")
+
+ def update_tab(self, name):
+ s = getattr(self, name + '_screen', None)
+ if s:
+ s.update()
+
+ @profiler
+ def update_tabs(self):
+ for tab in ['invoices', 'send', 'history', 'receive', 'address']:
+ self.update_tab(tab)
+
+ def switch_to(self, name):
+ s = getattr(self, name + '_screen', None)
+ if s is None:
+ s = self.tabs.ids[name + '_screen']
+ s.load_screen()
+ panel = self.tabs.ids.panel
+ tab = self.tabs.ids[name + '_tab']
+ panel.switch_to(tab)
+
+ def show_request(self, addr):
+ self.switch_to('receive')
+ self.receive_screen.screen.address = addr
+
+ def show_pr_details(self, req, status, is_invoice):
+ from electrum.util import format_time
+ requestor = req.get('requestor')
+ exp = req.get('exp')
+ memo = req.get('memo')
+ amount = req.get('amount')
+ fund = req.get('fund')
+ popup = Builder.load_file('electrum/gui/kivy/uix/ui_screens/invoice.kv')
+ popup.is_invoice = is_invoice
+ popup.amount = amount
+ popup.requestor = requestor if is_invoice else req.get('address')
+ popup.exp = format_time(exp) if exp else ''
+ popup.description = memo if memo else ''
+ popup.signature = req.get('signature', '')
+ popup.status = status
+ popup.fund = fund if fund else 0
+ txid = req.get('txid')
+ popup.tx_hash = txid or ''
+ popup.on_open = lambda: popup.ids.output_list.update(req.get('outputs', []))
+ popup.export = self.export_private_keys
+ popup.open()
+
+ def show_addr_details(self, req, status):
+ from electrum.util import format_time
+ fund = req.get('fund')
+ isaddr = 'y'
+ popup = Builder.load_file('electrum/gui/kivy/uix/ui_screens/invoice.kv')
+ popup.isaddr = isaddr
+ popup.is_invoice = False
+ popup.status = status
+ popup.requestor = req.get('address')
+ popup.fund = fund if fund else 0
+ popup.export = self.export_private_keys
+ popup.open()
+
+ def qr_dialog(self, title, data, show_text=False):
+ from .uix.dialogs.qr_dialog import QRDialog
+ popup = QRDialog(title, data, show_text)
+ popup.open()
+
+ def scan_qr(self, on_complete):
+ if platform != 'android':
+ return
+ from jnius import autoclass, cast
+ from android import activity
+ PythonActivity = autoclass('org.kivy.android.PythonActivity')
+ SimpleScannerActivity = autoclass("org.electrum.qr.SimpleScannerActivity")
+ Intent = autoclass('android.content.Intent')
+ intent = Intent(PythonActivity.mActivity, SimpleScannerActivity)
+
+ def on_qr_result(requestCode, resultCode, intent):
+ try:
+ if resultCode == -1: # RESULT_OK:
+ # this doesn't work due to some bug in jnius:
+ # contents = intent.getStringExtra("text")
+ String = autoclass("java.lang.String")
+ contents = intent.getStringExtra(String("text"))
+ on_complete(contents)
+ finally:
+ activity.unbind(on_activity_result=on_qr_result)
+ activity.bind(on_activity_result=on_qr_result)
+ PythonActivity.mActivity.startActivityForResult(intent, 0)
+
+ def do_share(self, data, title):
+ if platform != 'android':
+ return
+ from jnius import autoclass, cast
+ JS = autoclass('java.lang.String')
+ Intent = autoclass('android.content.Intent')
+ sendIntent = Intent()
+ sendIntent.setAction(Intent.ACTION_SEND)
+ sendIntent.setType("text/plain")
+ sendIntent.putExtra(Intent.EXTRA_TEXT, JS(data))
+ PythonActivity = autoclass('org.kivy.android.PythonActivity')
+ currentActivity = cast('android.app.Activity', PythonActivity.mActivity)
+ it = Intent.createChooser(sendIntent, cast('java.lang.CharSequence', JS(title)))
+ currentActivity.startActivity(it)
+
+ def build(self):
+ return Builder.load_file('electrum/gui/kivy/main.kv')
+
+ def _pause(self):
+ if platform == 'android':
+ # move activity to back
+ from jnius import autoclass
+ python_act = autoclass('org.kivy.android.PythonActivity')
+ mActivity = python_act.mActivity
+ mActivity.moveTaskToBack(True)
+
+ def on_start(self):
+ ''' This is the start point of the kivy ui
+ '''
+ import time
+ Logger.info('Time to on_start: {} <<<<<<<<'.format(time.clock()))
+ win = Window
+ win.bind(size=self.on_size, on_keyboard=self.on_keyboard)
+ win.bind(on_key_down=self.on_key_down)
+ #win.softinput_mode = 'below_target'
+ self.on_size(win, win.size)
+ self.init_ui()
+ crash_reporter.ExceptionHook(self)
+ # init plugins
+ run_hook('init_kivy', self)
+ # fiat currency
+ self.fiat_unit = self.fx.ccy if self.fx.is_enabled() else ''
+ # default tab
+ self.switch_to('history')
+ # bind intent for bitcoin: URI scheme
+ if platform == 'android':
+ from android import activity
+ from jnius import autoclass
+ PythonActivity = autoclass('org.kivy.android.PythonActivity')
+ mactivity = PythonActivity.mActivity
+ self.on_new_intent(mactivity.getIntent())
+ activity.bind(on_new_intent=self.on_new_intent)
+ # connect callbacks
+ if self.network:
+ interests = ['updated', 'status', 'new_transaction', 'verified', 'interfaces']
+ self.network.register_callback(self.on_network_event, interests)
+ self.network.register_callback(self.on_fee, ['fee'])
+ self.network.register_callback(self.on_quotes, ['on_quotes'])
+ self.network.register_callback(self.on_history, ['on_history'])
+ # load wallet
+ self.load_wallet_by_name(self.electrum_config.get_wallet_path())
+ # URI passed in config
+ uri = self.electrum_config.get('url')
+ if uri:
+ self.set_URI(uri)
+
+
+ def get_wallet_path(self):
+ if self.wallet:
+ return self.wallet.storage.path
+ else:
+ return ''
+
+ def on_wizard_complete(self, wizard, wallet):
+ if wallet: # wizard returned a wallet
+ wallet.start_threads(self.daemon.network)
+ self.daemon.add_wallet(wallet)
+ self.load_wallet(wallet)
+ elif not self.wallet:
+ # wizard did not return a wallet; and there is no wallet open atm
+ # try to open last saved wallet (potentially start wizard again)
+ self.load_wallet_by_name(self.electrum_config.get_wallet_path(), ask_if_wizard=True)
+
+ def load_wallet_by_name(self, path, ask_if_wizard=False):
+ if not path:
+ return
+ if self.wallet and self.wallet.storage.path == path:
+ return
+ wallet = self.daemon.load_wallet(path, None)
+ if wallet:
+ if wallet.has_password():
+ self.password_dialog(wallet, _('Enter PIN code'), lambda x: self.load_wallet(wallet), self.stop)
+ else:
+ self.load_wallet(wallet)
+ else:
+ Logger.debug('Electrum: Wallet not found or action needed. Launching install wizard')
+
+ def launch_wizard():
+ storage = WalletStorage(path, manual_upgrades=True)
+ wizard = Factory.InstallWizard(self.electrum_config, self.plugins, storage)
+ wizard.bind(on_wizard_complete=self.on_wizard_complete)
+ action = wizard.storage.get_action()
+ wizard.run(action)
+ if not ask_if_wizard:
+ launch_wizard()
+ else:
+ from .uix.dialogs.question import Question
+
+ def handle_answer(b: bool):
+ if b:
+ launch_wizard()
+ else:
+ try: os.unlink(path)
+ except FileNotFoundError: pass
+ self.stop()
+ d = Question(_('Do you want to launch the wizard again?'), handle_answer)
+ d.open()
+
+ def on_stop(self):
+ Logger.info('on_stop')
+ if self.wallet:
+ self.electrum_config.save_last_wallet(self.wallet)
+ self.stop_wallet()
+
+ def stop_wallet(self):
+ if self.wallet:
+ self.daemon.stop_wallet(self.wallet.storage.path)
+ self.wallet = None
+
+ def on_key_down(self, instance, key, keycode, codepoint, modifiers):
+ if 'ctrl' in modifiers:
+ # q=24 w=25
+ if keycode in (24, 25):
+ self.stop()
+ elif keycode == 27:
+ # r=27
+ # force update wallet
+ self.update_wallet()
+ elif keycode == 112:
+ # pageup
+ #TODO move to next tab
+ pass
+ elif keycode == 117:
+ # pagedown
+ #TODO move to prev tab
+ pass
+ #TODO: alt+tab_number to activate the particular tab
+
+ def on_keyboard(self, instance, key, keycode, codepoint, modifiers):
+ if key == 27 and self.is_exit is False:
+ self.is_exit = True
+ self.show_info(_('Press again to exit'))
+ return True
+ # override settings button
+ if key in (319, 282): #f1/settings button on android
+ #self.gui.main_gui.toggle_settings(self)
+ return True
+
+ def settings_dialog(self):
+ from .uix.dialogs.settings import SettingsDialog
+ if self._settings_dialog is None:
+ self._settings_dialog = SettingsDialog(self)
+ self._settings_dialog.update()
+ self._settings_dialog.open()
+
+ def popup_dialog(self, name):
+ if name == 'settings':
+ self.settings_dialog()
+ elif name == 'wallets':
+ from .uix.dialogs.wallets import WalletDialog
+ d = WalletDialog()
+ d.open()
+ elif name == 'status':
+ popup = Builder.load_file('electrum/gui/kivy/uix/ui_screens/'+name+'.kv')
+ master_public_keys_layout = popup.ids.master_public_keys
+ for xpub in self.wallet.get_master_public_keys()[1:]:
+ master_public_keys_layout.add_widget(TopLabel(text=_('Master Public Key')))
+ ref = RefLabel()
+ ref.name = _('Master Public Key')
+ ref.data = xpub
+ master_public_keys_layout.add_widget(ref)
+ popup.open()
+ else:
+ popup = Builder.load_file('electrum/gui/kivy/uix/ui_screens/'+name+'.kv')
+ popup.open()
+
+ @profiler
+ def init_ui(self):
+ ''' Initialize The Ux part of electrum. This function performs the basic
+ tasks of setting up the ui.
+ '''
+ #from weakref import ref
+
+ self.funds_error = False
+ # setup UX
+ self.screens = {}
+
+ #setup lazy imports for mainscreen
+ Factory.register('AnimatedPopup',
+ module='electrum.gui.kivy.uix.dialogs')
+ Factory.register('QRCodeWidget',
+ module='electrum.gui.kivy.uix.qrcodewidget')
+
+ # preload widgets. Remove this if you want to load the widgets on demand
+ #Cache.append('electrum_widgets', 'AnimatedPopup', Factory.AnimatedPopup())
+ #Cache.append('electrum_widgets', 'QRCodeWidget', Factory.QRCodeWidget())
+
+ # load and focus the ui
+ self.root.manager = self.root.ids['manager']
+
+ self.history_screen = None
+ self.contacts_screen = None
+ self.send_screen = None
+ self.invoices_screen = None
+ self.receive_screen = None
+ self.requests_screen = None
+ self.address_screen = None
+ self.icon = "icons/electrum.png"
+ self.tabs = self.root.ids['tabs']
+
+ def update_interfaces(self, dt):
+ self.num_nodes = len(self.network.get_interfaces())
+ self.num_chains = len(self.network.get_blockchains())
+ chain = self.network.blockchain()
+ self.blockchain_checkpoint = chain.get_checkpoint()
+ self.blockchain_name = chain.get_name()
+ interface = self.network.interface
+ if interface:
+ self.server_host = interface.host
+
+ def on_network_event(self, event, *args):
+ Logger.info('network event: '+ event)
+ if event == 'interfaces':
+ self._trigger_update_interfaces()
+ elif event == 'updated':
+ self._trigger_update_wallet()
+ self._trigger_update_status()
+ elif event == 'status':
+ self._trigger_update_status()
+ elif event == 'new_transaction':
+ self._trigger_update_wallet()
+ elif event == 'verified':
+ self._trigger_update_wallet()
+
+ @profiler
+ def load_wallet(self, wallet):
+ if self.wallet:
+ self.stop_wallet()
+ self.wallet = wallet
+ self.update_wallet()
+ # Once GUI has been initialized check if we want to announce something
+ # since the callback has been called before the GUI was initialized
+ if self.receive_screen:
+ self.receive_screen.clear()
+ self.update_tabs()
+ run_hook('load_wallet', wallet, self)
+
+ def update_status(self, *dt):
+ self.num_blocks = self.network.get_local_height()
+ if not self.wallet:
+ self.status = _("No Wallet")
+ return
+ if self.network is None or not self.network.is_running():
+ status = _("Offline")
+ elif self.network.is_connected():
+ server_height = self.network.get_server_height()
+ server_lag = self.network.get_local_height() - server_height
+ if not self.wallet.up_to_date or server_height == 0:
+ status = _("Synchronizing...")
+ elif server_lag > 1:
+ status = _("Server lagging")
+ else:
+ status = ''
+ else:
+ status = _("Disconnected")
+ self.status = self.wallet.basename() + (' [size=15dp](%s)[/size]'%status if status else '')
+ # balance
+ c, u, x = self.wallet.get_balance()
+ text = self.format_amount(c+x+u)
+ self.balance = str(text.strip()) + ' [size=22dp]%s[/size]'% self.base_unit
+ self.fiat_balance = self.fx.format_amount(c+u+x) + ' [size=22dp]%s[/size]'% self.fx.ccy
+
+ def get_max_amount(self):
+ if run_hook('abort_send', self):
+ return ''
+ inputs = self.wallet.get_spendable_coins(None, self.electrum_config)
+ if not inputs:
+ return ''
+ addr = str(self.send_screen.screen.address) or self.wallet.dummy_address()
+ outputs = [(TYPE_ADDRESS, addr, '!')]
+ try:
+ tx = self.wallet.make_unsigned_transaction(inputs, outputs, self.electrum_config)
+ except NoDynamicFeeEstimates as e:
+ Clock.schedule_once(lambda dt, bound_e=e: self.show_error(str(bound_e)))
+ return ''
+ except NotEnoughFunds:
+ return ''
+ amount = tx.output_value()
+ __, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet, tx) or (None, 0)
+ amount_after_all_fees = amount - x_fee_amount
+ return format_satoshis_plain(amount_after_all_fees, self.decimal_point())
+
+ def format_amount(self, x, is_diff=False, whitespaces=False):
+ return format_satoshis(x, 0, self.decimal_point(), is_diff=is_diff, whitespaces=whitespaces)
+
+ def format_amount_and_units(self, x):
+ return format_satoshis_plain(x, self.decimal_point()) + ' ' + self.base_unit
+
+ #@profiler
+ def update_wallet(self, *dt):
+ self._trigger_update_status()
+ if self.wallet and (self.wallet.up_to_date or not self.network or not self.network.is_connected()):
+ self.update_tabs()
+
+ def notify(self, message):
+ try:
+ global notification, os
+ if not notification:
+ from plyer import notification
+ icon = (os.path.dirname(os.path.realpath(__file__))
+ + '/../../' + self.icon)
+ notification.notify('Electrum', message,
+ app_icon=icon, app_name='Electrum')
+ except ImportError:
+ Logger.Error('Notification: needs plyer; `sudo pip install plyer`')
+
+ def on_pause(self):
+ self.pause_time = time.time()
+ # pause nfc
+ if self.nfcscanner:
+ self.nfcscanner.nfc_disable()
+ return True
+
+ def on_resume(self):
+ now = time.time()
+ if self.wallet and self.wallet.has_password() and now - self.pause_time > 60:
+ self.password_dialog(self.wallet, _('Enter PIN'), None, self.stop)
+ if self.nfcscanner:
+ self.nfcscanner.nfc_enable()
+
+ def on_size(self, instance, value):
+ width, height = value
+ self._orientation = 'landscape' if width > height else 'portrait'
+ self._ui_mode = 'tablet' if min(width, height) > inch(3.51) else 'phone'
+
+ def on_ref_label(self, label, touch):
+ if label.touched:
+ label.touched = False
+ self.qr_dialog(label.name, label.data, True)
+ else:
+ label.touched = True
+ self._clipboard.copy(label.data)
+ Clock.schedule_once(lambda dt: self.show_info(_('Text copied to clipboard.\nTap again to display it as QR code.')))
+
+ def set_send(self, address, amount, label, message):
+ self.send_payment(address, amount=amount, label=label, message=message)
+
+ def show_error(self, error, width='200dp', pos=None, arrow_pos=None,
+ exit=False, icon='atlas://electrum/gui/kivy/theming/light/error', duration=0,
+ modal=False):
+ ''' Show an error Message Bubble.
+ '''
+ self.show_info_bubble( text=error, icon=icon, width=width,
+ pos=pos or Window.center, arrow_pos=arrow_pos, exit=exit,
+ duration=duration, modal=modal)
+
+ def show_info(self, error, width='200dp', pos=None, arrow_pos=None,
+ exit=False, duration=0, modal=False):
+ ''' Show an Info Message Bubble.
+ '''
+ self.show_error(error, icon='atlas://electrum/gui/kivy/theming/light/important',
+ duration=duration, modal=modal, exit=exit, pos=pos,
+ arrow_pos=arrow_pos)
+
+ def show_info_bubble(self, text=_('Hello World'), pos=None, duration=0,
+ arrow_pos='bottom_mid', width=None, icon='', modal=False, exit=False):
+ '''Method to show an Information Bubble
+
+ .. parameters::
+ text: Message to be displayed
+ pos: position for the bubble
+ duration: duration the bubble remains on screen. 0 = click to hide
+ width: width of the Bubble
+ arrow_pos: arrow position for the bubble
+ '''
+ info_bubble = self.info_bubble
+ if not info_bubble:
+ info_bubble = self.info_bubble = Factory.InfoBubble()
+
+ win = Window
+ if info_bubble.parent:
+ win.remove_widget(info_bubble
+ if not info_bubble.modal else
+ info_bubble._modal_view)
+
+ if not arrow_pos:
+ info_bubble.show_arrow = False
+ else:
+ info_bubble.show_arrow = True
+ info_bubble.arrow_pos = arrow_pos
+ img = info_bubble.ids.img
+ if text == 'texture':
+ # icon holds a texture not a source image
+ # display the texture in full screen
+ text = ''
+ img.texture = icon
+ info_bubble.fs = True
+ info_bubble.show_arrow = False
+ img.allow_stretch = True
+ info_bubble.dim_background = True
+ info_bubble.background_image = 'atlas://electrum/gui/kivy/theming/light/card'
+ else:
+ info_bubble.fs = False
+ info_bubble.icon = icon
+ #if img.texture and img._coreimage:
+ # img.reload()
+ img.allow_stretch = False
+ info_bubble.dim_background = False
+ info_bubble.background_image = 'atlas://data/images/defaulttheme/bubble'
+ info_bubble.message = text
+ if not pos:
+ pos = (win.center[0], win.center[1] - (info_bubble.height/2))
+ info_bubble.show(pos, duration, width, modal=modal, exit=exit)
+
+ def tx_dialog(self, tx):
+ from .uix.dialogs.tx_dialog import TxDialog
+ d = TxDialog(self, tx)
+ d.open()
+
+ def sign_tx(self, *args):
+ threading.Thread(target=self._sign_tx, args=args).start()
+
+ def _sign_tx(self, tx, password, on_success, on_failure):
+ try:
+ self.wallet.sign_transaction(tx, password)
+ except InvalidPassword:
+ Clock.schedule_once(lambda dt: on_failure(_("Invalid PIN")))
+ return
+ on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success, on_failure) or on_success
+ Clock.schedule_once(lambda dt: on_success(tx))
+
+ def _broadcast_thread(self, tx, on_complete):
+ ok, txid = self.network.broadcast_transaction(tx)
+ Clock.schedule_once(lambda dt: on_complete(ok, txid))
+
+ def broadcast(self, tx, pr=None):
+ def on_complete(ok, msg):
+ if ok:
+ self.show_info(_('Payment sent.'))
+ if self.send_screen:
+ self.send_screen.do_clear()
+ if pr:
+ self.wallet.invoices.set_paid(pr, tx.txid())
+ self.wallet.invoices.save()
+ self.update_tab('invoices')
+ else:
+ self.show_error(msg)
+
+ if self.network and self.network.is_connected():
+ self.show_info(_('Sending'))
+ threading.Thread(target=self._broadcast_thread, args=(tx, on_complete)).start()
+ else:
+ self.show_info(_('Cannot broadcast transaction') + ':\n' + _('Not connected'))
+
+ def description_dialog(self, screen):
+ from .uix.dialogs.label_dialog import LabelDialog
+ text = screen.message
+ def callback(text):
+ screen.message = text
+ d = LabelDialog(_('Enter description'), text, callback)
+ d.open()
+
+ def amount_dialog(self, screen, show_max):
+ from .uix.dialogs.amount_dialog import AmountDialog
+ amount = screen.amount
+ if amount:
+ amount, u = str(amount).split()
+ assert u == self.base_unit
+ def cb(amount):
+ screen.amount = amount
+ popup = AmountDialog(show_max, amount, cb)
+ popup.open()
+
+ def invoices_dialog(self, screen):
+ from .uix.dialogs.invoices import InvoicesDialog
+ if len(self.wallet.invoices.sorted_list()) == 0:
+ self.show_info(' '.join([
+ _('No saved invoices.'),
+ _('Signed invoices are saved automatically when you scan them.'),
+ _('You may also save unsigned requests or contact addresses using the save button.')
+ ]))
+ return
+ popup = InvoicesDialog(self, screen, None)
+ popup.update()
+ popup.open()
+
+ def requests_dialog(self, screen):
+ from .uix.dialogs.requests import RequestsDialog
+ if len(self.wallet.get_sorted_requests(self.electrum_config)) == 0:
+ self.show_info(_('No saved requests.'))
+ return
+ popup = RequestsDialog(self, screen, None)
+ popup.update()
+ popup.open()
+
+ def addresses_dialog(self, screen):
+ from .uix.dialogs.addresses import AddressesDialog
+ popup = AddressesDialog(self, screen, None)
+ popup.update()
+ popup.open()
+
+ def fee_dialog(self, label, dt):
+ from .uix.dialogs.fee_dialog import FeeDialog
+ def cb():
+ self.fee_status = self.electrum_config.get_fee_status()
+ fee_dialog = FeeDialog(self, self.electrum_config, cb)
+ fee_dialog.open()
+
+ def on_fee(self, event, *arg):
+ self.fee_status = self.electrum_config.get_fee_status()
+
+ def protected(self, msg, f, args):
+ if self.wallet.has_password():
+ on_success = lambda pw: f(*(args + (pw,)))
+ self.password_dialog(self.wallet, msg, on_success, lambda: None)
+ else:
+ f(*(args + (None,)))
+
+ def delete_wallet(self):
+ from .uix.dialogs.question import Question
+ basename = os.path.basename(self.wallet.storage.path)
+ d = Question(_('Delete wallet?') + '\n' + basename, self._delete_wallet)
+ d.open()
+
+ def _delete_wallet(self, b):
+ if b:
+ basename = self.wallet.basename()
+ self.protected(_("Enter your PIN code to confirm deletion of {}").format(basename), self.__delete_wallet, ())
+
+ def __delete_wallet(self, pw):
+ wallet_path = self.get_wallet_path()
+ dirname = os.path.dirname(wallet_path)
+ basename = os.path.basename(wallet_path)
+ if self.wallet.has_password():
+ try:
+ self.wallet.check_password(pw)
+ except:
+ self.show_error("Invalid PIN")
+ return
+ self.stop_wallet()
+ os.unlink(wallet_path)
+ self.show_error(_("Wallet removed: {}").format(basename))
+ new_path = self.electrum_config.get_wallet_path()
+ self.load_wallet_by_name(new_path)
+
+ def show_seed(self, label):
+ self.protected(_("Enter your PIN code in order to decrypt your seed"), self._show_seed, (label,))
+
+ def _show_seed(self, label, password):
+ if self.wallet.has_password() and password is None:
+ return
+ keystore = self.wallet.keystore
+ try:
+ seed = keystore.get_seed(password)
+ passphrase = keystore.get_passphrase(password)
+ except:
+ self.show_error("Invalid PIN")
+ return
+ label.text = _('Seed') + ':\n' + seed
+ if passphrase:
+ label.text += '\n\n' + _('Passphrase') + ': ' + passphrase
+
+ def password_dialog(self, wallet, msg, on_success, on_failure):
+ from .uix.dialogs.password_dialog import PasswordDialog
+ if self._password_dialog is None:
+ self._password_dialog = PasswordDialog()
+ self._password_dialog.init(self, wallet, msg, on_success, on_failure)
+ self._password_dialog.open()
+
+ def change_password(self, cb):
+ from .uix.dialogs.password_dialog import PasswordDialog
+ if self._password_dialog is None:
+ self._password_dialog = PasswordDialog()
+ message = _("Changing PIN code.") + '\n' + _("Enter your current PIN:")
+ def on_success(old_password, new_password):
+ self.wallet.update_password(old_password, new_password)
+ self.show_info(_("Your PIN code was updated"))
+ on_failure = lambda: self.show_error(_("PIN codes do not match"))
+ self._password_dialog.init(self, self.wallet, message, on_success, on_failure, is_change=1)
+ self._password_dialog.open()
+
+ def export_private_keys(self, pk_label, addr):
+ if self.wallet.is_watching_only():
+ self.show_info(_('This is a watching-only wallet. It does not contain private keys.'))
+ return
+ def show_private_key(addr, pk_label, password):
+ if self.wallet.has_password() and password is None:
+ return
+ if not self.wallet.can_export():
+ return
+ try:
+ key = str(self.wallet.export_private_key(addr, password)[0])
+ pk_label.data = key
+ except InvalidPassword:
+ self.show_error("Invalid PIN")
+ return
+ self.protected(_("Enter your PIN code in order to decrypt your private key"), show_private_key, (addr, pk_label))
DIR diff --git a/electrum/gui/kivy/nfc_scanner/__init__.py b/electrum/gui/kivy/nfc_scanner/__init__.py
t@@ -0,0 +1,44 @@
+__all__ = ('NFCBase', 'NFCScanner')
+
+class NFCBase(Widget):
+ ''' This is the base Abstract definition class that the actual hardware dependent
+ implementations would be based on. If you want to define a feature that is
+ accessible and implemented by every platform implementation then define that
+ method in this class.
+ '''
+
+ payload = ObjectProperty(None)
+ '''This is the data gotten from the tag.
+ '''
+
+ def nfc_init(self):
+ ''' Initialize the adapter.
+ '''
+ pass
+
+ def nfc_disable(self):
+ ''' Disable scanning
+ '''
+ pass
+
+ def nfc_enable(self):
+ ''' Enable Scanning
+ '''
+ pass
+
+ def nfc_enable_exchange(self, data):
+ ''' Enable P2P Ndef exchange
+ '''
+ pass
+
+ def nfc_disable_exchange(self):
+ ''' Disable/Stop P2P Ndef exchange
+ '''
+ pass
+
+# load NFCScanner implementation
+
+NFCScanner = core_select_lib('nfc_manager', (
+ # keep the dummy implementation as the last one to make it the fallback provider.NFCScanner = core_select_lib('nfc_scanner', (
+ ('android', 'scanner_android', 'ScannerAndroid'),
+ ('dummy', 'scanner_dummy', 'ScannerDummy')), True, 'electrum.gui.kivy')
DIR diff --git a/electrum/gui/kivy/nfc_scanner/scanner_android.py b/electrum/gui/kivy/nfc_scanner/scanner_android.py
t@@ -0,0 +1,242 @@
+'''This is the Android implementation of NFC Scanning using the
+built in NFC adapter of some android phones.
+'''
+
+from kivy.app import App
+from kivy.clock import Clock
+#Detect which platform we are on
+from kivy.utils import platform
+if platform != 'android':
+ raise ImportError
+import threading
+
+from . import NFCBase
+from jnius import autoclass, cast
+from android.runnable import run_on_ui_thread
+from android import activity
+
+BUILDVERSION = autoclass('android.os.Build$VERSION').SDK_INT
+NfcAdapter = autoclass('android.nfc.NfcAdapter')
+PythonActivity = autoclass('org.kivy.android.PythonActivity')
+JString = autoclass('java.lang.String')
+Charset = autoclass('java.nio.charset.Charset')
+locale = autoclass('java.util.Locale')
+Intent = autoclass('android.content.Intent')
+IntentFilter = autoclass('android.content.IntentFilter')
+PendingIntent = autoclass('android.app.PendingIntent')
+Ndef = autoclass('android.nfc.tech.Ndef')
+NdefRecord = autoclass('android.nfc.NdefRecord')
+NdefMessage = autoclass('android.nfc.NdefMessage')
+
+app = None
+
+
+
+class ScannerAndroid(NFCBase):
+ ''' This is the class responsible for handling the interface with the
+ Android NFC adapter. See Module Documentation for details.
+ '''
+
+ name = 'NFCAndroid'
+
+ def nfc_init(self):
+ ''' This is where we initialize NFC adapter.
+ '''
+ # Initialize NFC
+ global app
+ app = App.get_running_app()
+
+ # Make sure we are listening to new intent
+ activity.bind(on_new_intent=self.on_new_intent)
+
+ # Configure nfc
+ self.j_context = context = PythonActivity.mActivity
+ self.nfc_adapter = NfcAdapter.getDefaultAdapter(context)
+ # Check if adapter exists
+ if not self.nfc_adapter:
+ return False
+
+ # specify that we want our activity to remain on top when a new intent
+ # is fired
+ self.nfc_pending_intent = PendingIntent.getActivity(context, 0,
+ Intent(context, context.getClass()).addFlags(
+ Intent.FLAG_ACTIVITY_SINGLE_TOP), 0)
+
+ # Filter for different types of action, by default we enable all.
+ # These are only for handling different NFC technologies when app is in foreground
+ self.ndef_detected = IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED)
+ #self.tech_detected = IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED)
+ #self.tag_detected = IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED)
+
+ # setup tag discovery for ourt tag type
+ try:
+ self.ndef_detected.addCategory(Intent.CATEGORY_DEFAULT)
+ # setup the foreground dispatch to detect all mime types
+ self.ndef_detected.addDataType('*/*')
+
+ self.ndef_exchange_filters = [self.ndef_detected]
+ except Exception as err:
+ raise Exception(repr(err))
+ return True
+
+ def get_ndef_details(self, tag):
+ ''' Get all the details from the tag.
+ '''
+ details = {}
+
+ try:
+ #print 'id'
+ details['uid'] = ':'.join(['{:02x}'.format(bt & 0xff) for bt in tag.getId()])
+ #print 'technologies'
+ details['Technologies'] = tech_list = [tech.split('.')[-1] for tech in tag.getTechList()]
+ #print 'get NDEF tag details'
+ ndefTag = cast('android.nfc.tech.Ndef', Ndef.get(tag))
+ #print 'tag size'
+ details['MaxSize'] = ndefTag.getMaxSize()
+ #details['usedSize'] = '0'
+ #print 'is tag writable?'
+ details['writable'] = ndefTag.isWritable()
+ #print 'Data format'
+ # Can be made readonly
+ # get NDEF message details
+ ndefMesg = ndefTag.getCachedNdefMessage()
+ # get size of current records
+ details['consumed'] = len(ndefMesg.toByteArray())
+ #print 'tag type'
+ details['Type'] = ndefTag.getType()
+
+ # check if tag is empty
+ if not ndefMesg:
+ details['Message'] = None
+ return details
+
+ ndefrecords = ndefMesg.getRecords()
+ length = len(ndefrecords)
+ #print 'length', length
+ # will contain the NDEF record types
+ recTypes = []
+ for record in ndefrecords:
+ recTypes.append({
+ 'type': ''.join(map(unichr, record.getType())),
+ 'payload': ''.join(map(unichr, record.getPayload()))
+ })
+
+ details['recTypes'] = recTypes
+ except Exception as err:
+ print(str(err))
+
+ return details
+
+ def on_new_intent(self, intent):
+ ''' This function is called when the application receives a
+ new intent, for the ones the application has registered previously,
+ either in the manifest or in the foreground dispatch setup in the
+ nfc_init function above.
+ '''
+
+ action_list = (NfcAdapter.ACTION_NDEF_DISCOVERED,)
+ # get TAG
+ #tag = cast('android.nfc.Tag', intent.getParcelableExtra(NfcAdapter.EXTRA_TAG))
+
+ #details = self.get_ndef_details(tag)
+
+ if intent.getAction() not in action_list:
+ print('unknow action, avoid.')
+ return
+
+ rawmsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
+ if not rawmsgs:
+ return
+ for message in rawmsgs:
+ message = cast(NdefMessage, message)
+ payload = message.getRecords()[0].getPayload()
+ print('payload: {}'.format(''.join(map(chr, payload))))
+
+ def nfc_disable(self):
+ '''Disable app from handling tags.
+ '''
+ self.disable_foreground_dispatch()
+
+ def nfc_enable(self):
+ '''Enable app to handle tags when app in foreground.
+ '''
+ self.enable_foreground_dispatch()
+
+ def create_AAR(self):
+ '''Create the record responsible for linking our application to the tag.
+ '''
+ return NdefRecord.createApplicationRecord(JString("org.electrum.kivy"))
+
+ def create_TNF_EXTERNAL(self, data):
+ '''Create our actual payload record.
+ '''
+ if BUILDVERSION >= 14:
+ domain = "org.electrum"
+ stype = "externalType"
+ extRecord = NdefRecord.createExternal(domain, stype, data)
+ else:
+ # Creating the NdefRecord manually:
+ extRecord = NdefRecord(
+ NdefRecord.TNF_EXTERNAL_TYPE,
+ "org.electrum:externalType",
+ '',
+ data)
+ return extRecord
+
+ def create_ndef_message(self, *recs):
+ ''' Create the Ndef message that will be written to tag
+ '''
+ records = []
+ for record in recs:
+ if record:
+ records.append(record)
+
+ return NdefMessage(records)
+
+
+ @run_on_ui_thread
+ def disable_foreground_dispatch(self):
+ '''Disable foreground dispatch when app is paused.
+ '''
+ self.nfc_adapter.disableForegroundDispatch(self.j_context)
+
+ @run_on_ui_thread
+ def enable_foreground_dispatch(self):
+ '''Start listening for new tags
+ '''
+ self.nfc_adapter.enableForegroundDispatch(self.j_context,
+ self.nfc_pending_intent, self.ndef_exchange_filters, self.ndef_tech_list)
+
+ @run_on_ui_thread
+ def _nfc_enable_ndef_exchange(self, data):
+ # Enable p2p exchange
+ # Create record
+ ndef_record = NdefRecord(
+ NdefRecord.TNF_MIME_MEDIA,
+ 'org.electrum.kivy', '', data)
+
+ # Create message
+ ndef_message = NdefMessage([ndef_record])
+
+ # Enable ndef push
+ self.nfc_adapter.enableForegroundNdefPush(self.j_context, ndef_message)
+
+ # Enable dispatch
+ self.nfc_adapter.enableForegroundDispatch(self.j_context,
+ self.nfc_pending_intent, self.ndef_exchange_filters, [])
+
+ @run_on_ui_thread
+ def _nfc_disable_ndef_exchange(self):
+ # Disable p2p exchange
+ self.nfc_adapter.disableForegroundNdefPush(self.j_context)
+ self.nfc_adapter.disableForegroundDispatch(self.j_context)
+
+ def nfc_enable_exchange(self, data):
+ '''Enable Ndef exchange for p2p
+ '''
+ self._nfc_enable_ndef_exchange()
+
+ def nfc_disable_exchange(self):
+ ''' Disable Ndef exchange for p2p
+ '''
+ self._nfc_disable_ndef_exchange()
DIR diff --git a/electrum/gui/kivy/nfc_scanner/scanner_dummy.py b/electrum/gui/kivy/nfc_scanner/scanner_dummy.py
t@@ -0,0 +1,52 @@
+''' Dummy NFC Provider to be used on desktops in case no other provider is found
+'''
+from . import NFCBase
+from kivy.clock import Clock
+from kivy.logger import Logger
+
+class ScannerDummy(NFCBase):
+ '''This is the dummy interface that gets selected in case any other
+ hardware interface to NFC is not available.
+ '''
+
+ _initialised = False
+
+ name = 'NFCDummy'
+
+ def nfc_init(self):
+ # print 'nfc_init()'
+
+ Logger.debug('NFC: configure nfc')
+ self._initialised = True
+ self.nfc_enable()
+ return True
+
+ def on_new_intent(self, dt):
+ tag_info = {'type': 'dymmy',
+ 'message': 'dummy',
+ 'extra details': None}
+
+ # let Main app know that a tag has been detected
+ app = App.get_running_app()
+ app.tag_discovered(tag_info)
+ app.show_info('New tag detected.', duration=2)
+ Logger.debug('NFC: got new dummy tag')
+
+ def nfc_enable(self):
+ Logger.debug('NFC: enable')
+ if self._initialised:
+ Clock.schedule_interval(self.on_new_intent, 22)
+
+ def nfc_disable(self):
+ # print 'nfc_enable()'
+ Clock.unschedule(self.on_new_intent)
+
+ def nfc_enable_exchange(self, data):
+ ''' Start sending data
+ '''
+ Logger.debug('NFC: sending data {}'.format(data))
+
+ def nfc_disable_exchange(self):
+ ''' Disable/Stop ndef exchange
+ '''
+ Logger.debug('NFC: disable nfc exchange')
DIR diff --git a/gui/kivy/theming/light/action_bar.png b/electrum/gui/kivy/theming/light/action_bar.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/action_button_group.png b/electrum/gui/kivy/theming/light/action_button_group.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/action_group_dark.png b/electrum/gui/kivy/theming/light/action_group_dark.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/action_group_light.png b/electrum/gui/kivy/theming/light/action_group_light.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/add_contact.png b/electrum/gui/kivy/theming/light/add_contact.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/arrow_back.png b/electrum/gui/kivy/theming/light/arrow_back.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/bit_logo.png b/electrum/gui/kivy/theming/light/bit_logo.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/blue_bg_round_rb.png b/electrum/gui/kivy/theming/light/blue_bg_round_rb.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/btn_create_account.png b/electrum/gui/kivy/theming/light/btn_create_account.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/btn_create_act_disabled.png b/electrum/gui/kivy/theming/light/btn_create_act_disabled.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/btn_nfc.png b/electrum/gui/kivy/theming/light/btn_nfc.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/btn_send_address.png b/electrum/gui/kivy/theming/light/btn_send_address.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/btn_send_nfc.png b/electrum/gui/kivy/theming/light/btn_send_nfc.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/calculator.png b/electrum/gui/kivy/theming/light/calculator.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/camera.png b/electrum/gui/kivy/theming/light/camera.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/card.png b/electrum/gui/kivy/theming/light/card.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/card_bottom.png b/electrum/gui/kivy/theming/light/card_bottom.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/card_btn.png b/electrum/gui/kivy/theming/light/card_btn.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/card_top.png b/electrum/gui/kivy/theming/light/card_top.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/carousel_deselected.png b/electrum/gui/kivy/theming/light/carousel_deselected.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/carousel_selected.png b/electrum/gui/kivy/theming/light/carousel_selected.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/clock1.png b/electrum/gui/kivy/theming/light/clock1.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/clock2.png b/electrum/gui/kivy/theming/light/clock2.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/clock3.png b/electrum/gui/kivy/theming/light/clock3.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/clock4.png b/electrum/gui/kivy/theming/light/clock4.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/clock5.png b/electrum/gui/kivy/theming/light/clock5.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/close.png b/electrum/gui/kivy/theming/light/close.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/closebutton.png b/electrum/gui/kivy/theming/light/closebutton.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/confirmed.png b/electrum/gui/kivy/theming/light/confirmed.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/contact.png b/electrum/gui/kivy/theming/light/contact.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/contact_overlay.png b/electrum/gui/kivy/theming/light/contact_overlay.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/create_act_text.png b/electrum/gui/kivy/theming/light/create_act_text.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/create_act_text_active.png b/electrum/gui/kivy/theming/light/create_act_text_active.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/dialog.png b/electrum/gui/kivy/theming/light/dialog.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/dropdown_background.png b/electrum/gui/kivy/theming/light/dropdown_background.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/electrum_icon640.png b/electrum/gui/kivy/theming/light/electrum_icon640.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/error.png b/electrum/gui/kivy/theming/light/error.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/gear.png b/electrum/gui/kivy/theming/light/gear.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/globe.png b/electrum/gui/kivy/theming/light/globe.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/icon_border.png b/electrum/gui/kivy/theming/light/icon_border.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/important.png b/electrum/gui/kivy/theming/light/important.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/info.png b/electrum/gui/kivy/theming/light/info.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/lightblue_bg_round_lb.png b/electrum/gui/kivy/theming/light/lightblue_bg_round_lb.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/logo.png b/electrum/gui/kivy/theming/light/logo.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/logo_atom_dull.png b/electrum/gui/kivy/theming/light/logo_atom_dull.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/mail_icon.png b/electrum/gui/kivy/theming/light/mail_icon.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/manualentry.png b/electrum/gui/kivy/theming/light/manualentry.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/network.png b/electrum/gui/kivy/theming/light/network.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/nfc.png b/electrum/gui/kivy/theming/light/nfc.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/nfc_clock.png b/electrum/gui/kivy/theming/light/nfc_clock.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/nfc_phone.png b/electrum/gui/kivy/theming/light/nfc_phone.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/nfc_stage_one.png b/electrum/gui/kivy/theming/light/nfc_stage_one.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/overflow_background.png b/electrum/gui/kivy/theming/light/overflow_background.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/overflow_btn_dn.png b/electrum/gui/kivy/theming/light/overflow_btn_dn.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/paste_icon.png b/electrum/gui/kivy/theming/light/paste_icon.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/pen.png b/electrum/gui/kivy/theming/light/pen.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/qrcode.png b/electrum/gui/kivy/theming/light/qrcode.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/save.png b/electrum/gui/kivy/theming/light/save.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/settings.png b/electrum/gui/kivy/theming/light/settings.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/shadow.png b/electrum/gui/kivy/theming/light/shadow.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/shadow_right.png b/electrum/gui/kivy/theming/light/shadow_right.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/share.png b/electrum/gui/kivy/theming/light/share.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/star_big_inactive.png b/electrum/gui/kivy/theming/light/star_big_inactive.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/stepper_full.png b/electrum/gui/kivy/theming/light/stepper_full.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/stepper_left.png b/electrum/gui/kivy/theming/light/stepper_left.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/stepper_restore_password.png b/electrum/gui/kivy/theming/light/stepper_restore_password.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/stepper_restore_seed.png b/electrum/gui/kivy/theming/light/stepper_restore_seed.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/tab.png b/electrum/gui/kivy/theming/light/tab.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/tab_btn.png b/electrum/gui/kivy/theming/light/tab_btn.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/tab_btn_disabled.png b/electrum/gui/kivy/theming/light/tab_btn_disabled.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/tab_btn_pressed.png b/electrum/gui/kivy/theming/light/tab_btn_pressed.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/tab_disabled.png b/electrum/gui/kivy/theming/light/tab_disabled.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/tab_strip.png b/electrum/gui/kivy/theming/light/tab_strip.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/textinput_active.png b/electrum/gui/kivy/theming/light/textinput_active.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/unconfirmed.png b/electrum/gui/kivy/theming/light/unconfirmed.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/wallet.png b/electrum/gui/kivy/theming/light/wallet.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/wallets.png b/electrum/gui/kivy/theming/light/wallets.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light/white_bg_round_top.png b/electrum/gui/kivy/theming/light/white_bg_round_top.png
Binary files differ.
DIR diff --git a/gui/kivy/tools/bitcoin_intent.xml b/electrum/gui/kivy/tools/bitcoin_intent.xml
DIR diff --git a/gui/kivy/tools/blacklist.txt b/electrum/gui/kivy/tools/blacklist.txt
DIR diff --git a/gui/kivy/tools/buildozer.spec b/electrum/gui/kivy/tools/buildozer.spec
DIR diff --git a/gui/kivy/uix/__init__.py b/electrum/gui/kivy/uix/__init__.py
DIR diff --git a/gui/kivy/uix/combobox.py b/electrum/gui/kivy/uix/combobox.py
DIR diff --git a/electrum/gui/kivy/uix/context_menu.py b/electrum/gui/kivy/uix/context_menu.py
t@@ -0,0 +1,56 @@
+#!python
+#!/usr/bin/env python
+from kivy.app import App
+from kivy.uix.bubble import Bubble
+from kivy.animation import Animation
+from kivy.uix.floatlayout import FloatLayout
+from kivy.lang import Builder
+from kivy.factory import Factory
+from kivy.clock import Clock
+
+from electrum.gui.kivy.i18n import _
+
+Builder.load_string('''
+<MenuItem@Button>
+ background_normal: ''
+ background_color: (0.192, .498, 0.745, 1)
+ height: '48dp'
+ size_hint: 1, None
+
+<ContextMenu>
+ size_hint: 1, None
+ height: '48dp'
+ pos: (0, 0)
+ show_arrow: False
+ arrow_pos: 'top_mid'
+ padding: 0
+ orientation: 'horizontal'
+ BoxLayout:
+ size_hint: 1, 1
+ height: '48dp'
+ padding: '12dp', '0dp'
+ spacing: '3dp'
+ orientation: 'horizontal'
+ id: buttons
+''')
+
+
+class MenuItem(Factory.Button):
+ pass
+
+class ContextMenu(Bubble):
+
+ def __init__(self, obj, action_list):
+ Bubble.__init__(self)
+ self.obj = obj
+ for k, v in action_list:
+ l = MenuItem()
+ l.text = _(k)
+ def func(f=v):
+ Clock.schedule_once(lambda dt: f(obj), 0.15)
+ l.on_release = func
+ self.ids.buttons.add_widget(l)
+
+ def hide(self):
+ if self.parent:
+ self.parent.hide_menu()
DIR diff --git a/electrum/gui/kivy/uix/dialogs/__init__.py b/electrum/gui/kivy/uix/dialogs/__init__.py
t@@ -0,0 +1,220 @@
+from kivy.app import App
+from kivy.clock import Clock
+from kivy.factory import Factory
+from kivy.properties import NumericProperty, StringProperty, BooleanProperty
+from kivy.core.window import Window
+from kivy.uix.recycleview import RecycleView
+from kivy.uix.boxlayout import BoxLayout
+
+from electrum.gui.kivy.i18n import _
+
+
+
+class AnimatedPopup(Factory.Popup):
+ ''' An Animated Popup that animates in and out.
+ '''
+
+ anim_duration = NumericProperty(.36)
+ '''Duration of animation to be used
+ '''
+
+ __events__ = ['on_activate', 'on_deactivate']
+
+
+ def on_activate(self):
+ '''Base function to be overridden on inherited classes.
+ Called when the popup is done animating.
+ '''
+ pass
+
+ def on_deactivate(self):
+ '''Base function to be overridden on inherited classes.
+ Called when the popup is done animating.
+ '''
+ pass
+
+ def open(self):
+ '''Do the initialization of incoming animation here.
+ Override to set your custom animation.
+ '''
+ def on_complete(*l):
+ self.dispatch('on_activate')
+
+ self.opacity = 0
+ super(AnimatedPopup, self).open()
+ anim = Factory.Animation(opacity=1, d=self.anim_duration)
+ anim.bind(on_complete=on_complete)
+ anim.start(self)
+
+ def dismiss(self):
+ '''Do the initialization of incoming animation here.
+ Override to set your custom animation.
+ '''
+ def on_complete(*l):
+ super(AnimatedPopup, self).dismiss()
+ self.dispatch('on_deactivate')
+
+ anim = Factory.Animation(opacity=0, d=.25)
+ anim.bind(on_complete=on_complete)
+ anim.start(self)
+
+class EventsDialog(Factory.Popup):
+ ''' Abstract Popup that provides the following events
+ .. events::
+ `on_release`
+ `on_press`
+ '''
+
+ __events__ = ('on_release', 'on_press')
+
+ def __init__(self, **kwargs):
+ super(EventsDialog, self).__init__(**kwargs)
+
+ def on_release(self, instance):
+ pass
+
+ def on_press(self, instance):
+ pass
+
+ def close(self):
+ self.dismiss()
+
+
+class SelectionDialog(EventsDialog):
+
+ def add_widget(self, widget, index=0):
+ if self.content:
+ self.content.add_widget(widget, index)
+ return
+ super(SelectionDialog, self).add_widget(widget)
+
+
+class InfoBubble(Factory.Bubble):
+ '''Bubble to be used to display short Help Information'''
+
+ message = StringProperty(_('Nothing set !'))
+ '''Message to be displayed; defaults to "nothing set"'''
+
+ icon = StringProperty('')
+ ''' Icon to be displayed along with the message defaults to ''
+
+ :attr:`icon` is a `StringProperty` defaults to `''`
+ '''
+
+ fs = BooleanProperty(False)
+ ''' Show Bubble in half screen mode
+
+ :attr:`fs` is a `BooleanProperty` defaults to `False`
+ '''
+
+ modal = BooleanProperty(False)
+ ''' Allow bubble to be hidden on touch.
+
+ :attr:`modal` is a `BooleanProperty` defauult to `False`.
+ '''
+
+ exit = BooleanProperty(False)
+ '''Indicates whether to exit app after bubble is closed.
+
+ :attr:`exit` is a `BooleanProperty` defaults to False.
+ '''
+
+ dim_background = BooleanProperty(False)
+ ''' Indicates Whether to draw a background on the windows behind the bubble.
+
+ :attr:`dim` is a `BooleanProperty` defaults to `False`.
+ '''
+
+ def on_touch_down(self, touch):
+ if self.modal:
+ return True
+ self.hide()
+ if self.collide_point(*touch.pos):
+ return True
+
+ def show(self, pos, duration, width=None, modal=False, exit=False):
+ '''Animate the bubble into position'''
+ self.modal, self.exit = modal, exit
+ if width:
+ self.width = width
+ if self.modal:
+ from kivy.uix.modalview import ModalView
+ self._modal_view = m = ModalView(background_color=[.5, .5, .5, .2])
+ Window.add_widget(m)
+ m.add_widget(self)
+ else:
+ Window.add_widget(self)
+
+ # wait for the bubble to adjust its size according to text then animate
+ Clock.schedule_once(lambda dt: self._show(pos, duration))
+
+ def _show(self, pos, duration):
+
+ def on_stop(*l):
+ if duration:
+ Clock.schedule_once(self.hide, duration + .5)
+
+ self.opacity = 0
+ arrow_pos = self.arrow_pos
+ if arrow_pos[0] in ('l', 'r'):
+ pos = pos[0], pos[1] - (self.height/2)
+ else:
+ pos = pos[0] - (self.width/2), pos[1]
+
+ self.limit_to = Window
+
+ anim = Factory.Animation(opacity=1, pos=pos, d=.32)
+ anim.bind(on_complete=on_stop)
+ anim.cancel_all(self)
+ anim.start(self)
+
+
+ def hide(self, now=False):
+ ''' Auto fade out the Bubble
+ '''
+ def on_stop(*l):
+ if self.modal:
+ m = self._modal_view
+ m.remove_widget(self)
+ Window.remove_widget(m)
+ Window.remove_widget(self)
+ if self.exit:
+ App.get_running_app().stop()
+ import sys
+ sys.exit()
+ else:
+ App.get_running_app().is_exit = False
+
+ if now:
+ return on_stop()
+
+ anim = Factory.Animation(opacity=0, d=.25)
+ anim.bind(on_complete=on_stop)
+ anim.cancel_all(self)
+ anim.start(self)
+
+
+
+class OutputItem(BoxLayout):
+ pass
+
+class OutputList(RecycleView):
+
+ def __init__(self, **kwargs):
+ super(OutputList, self).__init__(**kwargs)
+ self.app = App.get_running_app()
+
+ def update(self, outputs):
+ res = []
+ for (type, address, amount) in outputs:
+ value = self.app.format_amount_and_units(amount)
+ res.append({'address': address, 'value': value})
+ self.data = res
+
+
+class TopLabel(Factory.Label):
+ pass
+
+
+class RefLabel(TopLabel):
+ pass
DIR diff --git a/electrum/gui/kivy/uix/dialogs/addresses.py b/electrum/gui/kivy/uix/dialogs/addresses.py
t@@ -0,0 +1,180 @@
+from kivy.app import App
+from kivy.factory import Factory
+from kivy.properties import ObjectProperty
+from kivy.lang import Builder
+from decimal import Decimal
+
+Builder.load_string('''
+<AddressLabel@Label>
+ text_size: self.width, None
+ halign: 'left'
+ valign: 'top'
+
+<AddressItem@CardItem>
+ address: ''
+ memo: ''
+ amount: ''
+ status: ''
+ BoxLayout:
+ spacing: '8dp'
+ height: '32dp'
+ orientation: 'vertical'
+ Widget
+ AddressLabel:
+ text: root.address
+ shorten: True
+ Widget
+ AddressLabel:
+ text: (root.amount if root.status == 'Funded' else root.status) + ' ' + root.memo
+ color: .699, .699, .699, 1
+ font_size: '13sp'
+ shorten: True
+ Widget
+
+<AddressesDialog@Popup>
+ id: popup
+ title: _('Addresses')
+ message: ''
+ pr_status: 'Pending'
+ show_change: 0
+ show_used: 0
+ on_message:
+ self.update()
+ BoxLayout:
+ id:box
+ padding: '12dp', '70dp', '12dp', '12dp'
+ spacing: '12dp'
+ orientation: 'vertical'
+ size_hint: 1, 1.1
+ BoxLayout:
+ spacing: '6dp'
+ size_hint: 1, None
+ orientation: 'horizontal'
+ AddressFilter:
+ opacity: 1
+ size_hint: 1, None
+ height: self.minimum_height
+ spacing: '5dp'
+ AddressButton:
+ id: search
+ text: {0:_('Receiving'), 1:_('Change'), 2:_('All')}[root.show_change]
+ on_release:
+ root.show_change = (root.show_change + 1) % 3
+ Clock.schedule_once(lambda dt: root.update())
+ AddressFilter:
+ opacity: 1
+ size_hint: 1, None
+ height: self.minimum_height
+ spacing: '5dp'
+ AddressButton:
+ id: search
+ text: {0:_('All'), 1:_('Unused'), 2:_('Funded'), 3:_('Used')}[root.show_used]
+ on_release:
+ root.show_used = (root.show_used + 1) % 4
+ Clock.schedule_once(lambda dt: root.update())
+ AddressFilter:
+ opacity: 1
+ size_hint: 1, None
+ height: self.minimum_height
+ spacing: '5dp'
+ canvas.before:
+ Color:
+ rgba: 0.9, 0.9, 0.9, 1
+ AddressButton:
+ id: change
+ text: root.message if root.message else _('Search')
+ on_release: Clock.schedule_once(lambda dt: app.description_dialog(popup))
+ RecycleView:
+ scroll_type: ['bars', 'content']
+ bar_width: '15dp'
+ viewclass: 'AddressItem'
+ id: search_container
+ RecycleBoxLayout:
+ orientation: 'vertical'
+ default_size: None, dp(56)
+ default_size_hint: 1, None
+ size_hint_y: None
+ height: self.minimum_height
+''')
+
+
+from electrum.gui.kivy.i18n import _
+from electrum.gui.kivy.uix.context_menu import ContextMenu
+
+
+class AddressesDialog(Factory.Popup):
+
+ def __init__(self, app, screen, callback):
+ Factory.Popup.__init__(self)
+ self.app = app
+ self.screen = screen
+ self.callback = callback
+ self.context_menu = None
+
+ def get_card(self, addr, balance, is_used, label):
+ ci = {}
+ ci['screen'] = self
+ ci['address'] = addr
+ ci['memo'] = label
+ ci['amount'] = self.app.format_amount_and_units(balance)
+ ci['status'] = _('Used') if is_used else _('Funded') if balance > 0 else _('Unused')
+ return ci
+
+ def update(self):
+ self.menu_actions = [(_('Use'), self.do_use), (_('Details'), self.do_view)]
+ wallet = self.app.wallet
+ if self.show_change == 0:
+ _list = wallet.get_receiving_addresses()
+ elif self.show_change == 1:
+ _list = wallet.get_change_addresses()
+ else:
+ _list = wallet.get_addresses()
+ search = self.message
+ container = self.ids.search_container
+ n = 0
+ cards = []
+ for address in _list:
+ label = wallet.labels.get(address, '')
+ balance = sum(wallet.get_addr_balance(address))
+ is_used = wallet.is_used(address)
+ if self.show_used == 1 and (balance or is_used):
+ continue
+ if self.show_used == 2 and balance == 0:
+ continue
+ if self.show_used == 3 and not is_used:
+ continue
+ card = self.get_card(address, balance, is_used, label)
+ if search and not self.ext_search(card, search):
+ continue
+ cards.append(card)
+ n += 1
+ container.data = cards
+ if not n:
+ self.app.show_error('No address matching your search')
+
+ def do_use(self, obj):
+ self.hide_menu()
+ self.dismiss()
+ self.app.show_request(obj.address)
+
+ def do_view(self, obj):
+ req = { 'address': obj.address, 'status' : obj.status }
+ status = obj.status
+ c, u, x = self.app.wallet.get_addr_balance(obj.address)
+ balance = c + u + x
+ if balance > 0:
+ req['fund'] = balance
+ self.app.show_addr_details(req, status)
+
+ def ext_search(self, card, search):
+ return card['memo'].find(search) >= 0 or card['amount'].find(search) >= 0
+
+ def show_menu(self, obj):
+ self.hide_menu()
+ self.context_menu = ContextMenu(obj, self.menu_actions)
+ self.ids.box.add_widget(self.context_menu)
+
+ def hide_menu(self):
+ if self.context_menu is not None:
+ self.ids.box.remove_widget(self.context_menu)
+ self.context_menu = None
DIR diff --git a/gui/kivy/uix/dialogs/amount_dialog.py b/electrum/gui/kivy/uix/dialogs/amount_dialog.py
DIR diff --git a/electrum/gui/kivy/uix/dialogs/bump_fee_dialog.py b/electrum/gui/kivy/uix/dialogs/bump_fee_dialog.py
t@@ -0,0 +1,118 @@
+from kivy.app import App
+from kivy.factory import Factory
+from kivy.properties import ObjectProperty
+from kivy.lang import Builder
+
+from electrum.gui.kivy.i18n import _
+
+Builder.load_string('''
+<BumpFeeDialog@Popup>
+ title: _('Bump fee')
+ size_hint: 0.8, 0.8
+ pos_hint: {'top':0.9}
+ BoxLayout:
+ orientation: 'vertical'
+ padding: '10dp'
+
+ GridLayout:
+ height: self.minimum_height
+ size_hint_y: None
+ cols: 1
+ spacing: '10dp'
+ BoxLabel:
+ id: old_fee
+ text: _('Current Fee')
+ value: ''
+ BoxLabel:
+ id: new_fee
+ text: _('New Fee')
+ value: ''
+ Label:
+ id: tooltip1
+ text: ''
+ size_hint_y: None
+ Label:
+ id: tooltip2
+ text: ''
+ size_hint_y: None
+ Slider:
+ id: slider
+ range: 0, 4
+ step: 1
+ on_value: root.on_slider(self.value)
+ BoxLayout:
+ orientation: 'horizontal'
+ size_hint: 1, 0.2
+ Label:
+ text: _('Final')
+ CheckBox:
+ id: final_cb
+ Widget:
+ size_hint: 1, 1
+ BoxLayout:
+ orientation: 'horizontal'
+ size_hint: 1, 0.5
+ Button:
+ text: 'Cancel'
+ size_hint: 0.5, None
+ height: '48dp'
+ on_release: root.dismiss()
+ Button:
+ text: 'OK'
+ size_hint: 0.5, None
+ height: '48dp'
+ on_release:
+ root.dismiss()
+ root.on_ok()
+''')
+
+class BumpFeeDialog(Factory.Popup):
+
+ def __init__(self, app, fee, size, callback):
+ Factory.Popup.__init__(self)
+ self.app = app
+ self.init_fee = fee
+ self.tx_size = size
+ self.callback = callback
+ self.config = app.electrum_config
+ self.mempool = self.config.use_mempool_fees()
+ self.dynfees = self.config.is_dynfee() and bool(self.app.network) and self.config.has_dynamic_fees_ready()
+ self.ids.old_fee.value = self.app.format_amount_and_units(self.init_fee)
+ self.update_slider()
+ self.update_text()
+
+ def update_text(self):
+ fee = self.get_fee()
+ self.ids.new_fee.value = self.app.format_amount_and_units(fee)
+ pos = int(self.ids.slider.value)
+ fee_rate = self.get_fee_rate()
+ text, tooltip = self.config.get_fee_text(pos, self.dynfees, self.mempool, fee_rate)
+ self.ids.tooltip1.text = text
+ self.ids.tooltip2.text = tooltip
+
+ def update_slider(self):
+ slider = self.ids.slider
+ maxp, pos, fee_rate = self.config.get_fee_slider(self.dynfees, self.mempool)
+ slider.range = (0, maxp)
+ slider.step = 1
+ slider.value = pos
+
+ def get_fee_rate(self):
+ pos = int(self.ids.slider.value)
+ if self.dynfees:
+ fee_rate = self.config.depth_to_fee(pos) if self.mempool else self.config.eta_to_fee(pos)
+ else:
+ fee_rate = self.config.static_fee(pos)
+ return fee_rate
+
+ def get_fee(self):
+ fee_rate = self.get_fee_rate()
+ return int(fee_rate * self.tx_size // 1000)
+
+ def on_ok(self):
+ new_fee = self.get_fee()
+ is_final = self.ids.final_cb.active
+ self.callback(self.init_fee, new_fee, is_final)
+
+ def on_slider(self, value):
+ self.update_text()
DIR diff --git a/gui/kivy/uix/dialogs/checkbox_dialog.py b/electrum/gui/kivy/uix/dialogs/checkbox_dialog.py
DIR diff --git a/gui/kivy/uix/dialogs/choice_dialog.py b/electrum/gui/kivy/uix/dialogs/choice_dialog.py
DIR diff --git a/gui/kivy/uix/dialogs/crash_reporter.py b/electrum/gui/kivy/uix/dialogs/crash_reporter.py
DIR diff --git a/electrum/gui/kivy/uix/dialogs/fee_dialog.py b/electrum/gui/kivy/uix/dialogs/fee_dialog.py
t@@ -0,0 +1,131 @@
+from kivy.app import App
+from kivy.factory import Factory
+from kivy.properties import ObjectProperty
+from kivy.lang import Builder
+
+from electrum.gui.kivy.i18n import _
+
+Builder.load_string('''
+<FeeDialog@Popup>
+ id: popup
+ title: _('Transaction Fees')
+ size_hint: 0.8, 0.8
+ pos_hint: {'top':0.9}
+ method: 0
+ BoxLayout:
+ orientation: 'vertical'
+ BoxLayout:
+ orientation: 'horizontal'
+ size_hint: 1, 0.5
+ Label:
+ text: _('Method') + ':'
+ Button:
+ text: _('Mempool') if root.method == 2 else _('ETA') if root.method == 1 else _('Static')
+ background_color: (0,0,0,0)
+ bold: True
+ on_release:
+ root.method = (root.method + 1) % 3
+ root.update_slider()
+ root.update_text()
+ BoxLayout:
+ orientation: 'horizontal'
+ size_hint: 1, 0.5
+ Label:
+ text: (_('Target') if root.method > 0 else _('Fee')) + ':'
+ Label:
+ id: fee_target
+ text: ''
+ Slider:
+ id: slider
+ range: 0, 4
+ step: 1
+ on_value: root.on_slider(self.value)
+ Widget:
+ size_hint: 1, 0.5
+ BoxLayout:
+ orientation: 'horizontal'
+ size_hint: 1, 0.5
+ TopLabel:
+ id: fee_estimate
+ text: ''
+ font_size: '14dp'
+ Widget:
+ size_hint: 1, 0.5
+ BoxLayout:
+ orientation: 'horizontal'
+ size_hint: 1, 0.5
+ Button:
+ text: 'Cancel'
+ size_hint: 0.5, None
+ height: '48dp'
+ on_release: popup.dismiss()
+ Button:
+ text: 'OK'
+ size_hint: 0.5, None
+ height: '48dp'
+ on_release:
+ root.on_ok()
+ root.dismiss()
+''')
+
+class FeeDialog(Factory.Popup):
+
+ def __init__(self, app, config, callback):
+ Factory.Popup.__init__(self)
+ self.app = app
+ self.config = config
+ self.callback = callback
+ mempool = self.config.use_mempool_fees()
+ dynfees = self.config.is_dynfee()
+ self.method = (2 if mempool else 1) if dynfees else 0
+ self.update_slider()
+ self.update_text()
+
+ def update_text(self):
+ pos = int(self.ids.slider.value)
+ dynfees, mempool = self.get_method()
+ if self.method == 2:
+ fee_rate = self.config.depth_to_fee(pos)
+ target, estimate = self.config.get_fee_text(pos, dynfees, mempool, fee_rate)
+ msg = 'In the current network conditions, in order to be positioned %s, a transaction will require a fee of %s.' % (target, estimate)
+ elif self.method == 1:
+ fee_rate = self.config.eta_to_fee(pos)
+ target, estimate = self.config.get_fee_text(pos, dynfees, mempool, fee_rate)
+ msg = 'In the last few days, transactions that confirmed %s usually paid a fee of at least %s.' % (target.lower(), estimate)
+ else:
+ fee_rate = self.config.static_fee(pos)
+ target, estimate = self.config.get_fee_text(pos, dynfees, True, fee_rate)
+ msg = 'In the current network conditions, a transaction paying %s would be positioned %s.' % (target, estimate)
+
+ self.ids.fee_target.text = target
+ self.ids.fee_estimate.text = msg
+
+ def get_method(self):
+ dynfees = self.method > 0
+ mempool = self.method == 2
+ return dynfees, mempool
+
+ def update_slider(self):
+ slider = self.ids.slider
+ dynfees, mempool = self.get_method()
+ maxp, pos, fee_rate = self.config.get_fee_slider(dynfees, mempool)
+ slider.range = (0, maxp)
+ slider.step = 1
+ slider.value = pos
+
+ def on_ok(self):
+ value = int(self.ids.slider.value)
+ dynfees, mempool = self.get_method()
+ self.config.set_key('dynamic_fees', dynfees, False)
+ self.config.set_key('mempool_fees', mempool, False)
+ if dynfees:
+ if mempool:
+ self.config.set_key('depth_level', value, True)
+ else:
+ self.config.set_key('fee_level', value, True)
+ else:
+ self.config.set_key('fee_per_kb', self.config.static_fee(value), True)
+ self.callback()
+
+ def on_slider(self, value):
+ self.update_text()
DIR diff --git a/electrum/gui/kivy/uix/dialogs/fx_dialog.py b/electrum/gui/kivy/uix/dialogs/fx_dialog.py
t@@ -0,0 +1,111 @@
+from kivy.app import App
+from kivy.factory import Factory
+from kivy.properties import ObjectProperty
+from kivy.lang import Builder
+
+Builder.load_string('''
+<FxDialog@Popup>
+ id: popup
+ title: 'Fiat Currency'
+ size_hint: 0.8, 0.8
+ pos_hint: {'top':0.9}
+ BoxLayout:
+ orientation: 'vertical'
+
+ Widget:
+ size_hint: 1, 0.1
+
+ BoxLayout:
+ orientation: 'horizontal'
+ size_hint: 1, 0.1
+ Label:
+ text: _('Currency')
+ height: '48dp'
+ Spinner:
+ height: '48dp'
+ id: ccy
+ on_text: popup.on_currency(self.text)
+
+ Widget:
+ size_hint: 1, 0.1
+
+ BoxLayout:
+ orientation: 'horizontal'
+ size_hint: 1, 0.1
+ Label:
+ text: _('Source')
+ height: '48dp'
+ Spinner:
+ height: '48dp'
+ id: exchanges
+ on_text: popup.on_exchange(self.text)
+
+ Widget:
+ size_hint: 1, 0.2
+
+ BoxLayout:
+ orientation: 'horizontal'
+ size_hint: 1, 0.2
+ Button:
+ text: 'Cancel'
+ size_hint: 0.5, None
+ height: '48dp'
+ on_release: popup.dismiss()
+ Button:
+ text: 'OK'
+ size_hint: 0.5, None
+ height: '48dp'
+ on_release:
+ root.callback()
+ popup.dismiss()
+''')
+
+
+from kivy.uix.label import Label
+from kivy.uix.checkbox import CheckBox
+from kivy.uix.widget import Widget
+from kivy.clock import Clock
+
+from electrum.gui.kivy.i18n import _
+from functools import partial
+
+class FxDialog(Factory.Popup):
+
+ def __init__(self, app, plugins, config, callback):
+ Factory.Popup.__init__(self)
+ self.app = app
+ self.config = config
+ self.callback = callback
+ self.fx = self.app.fx
+ self.fx.set_history_config(True)
+ self.add_currencies()
+
+ def add_exchanges(self):
+ exchanges = sorted(self.fx.get_exchanges_by_ccy(self.fx.get_currency(), True)) if self.fx.is_enabled() else []
+ mx = self.fx.exchange.name() if self.fx.is_enabled() else ''
+ ex = self.ids.exchanges
+ ex.values = exchanges
+ ex.text = (mx if mx in exchanges else exchanges[0]) if self.fx.is_enabled() else ''
+
+ def on_exchange(self, text):
+ if not text:
+ return
+ if self.fx.is_enabled() and text != self.fx.exchange.name():
+ self.fx.set_exchange(text)
+
+ def add_currencies(self):
+ currencies = [_('None')] + self.fx.get_currencies(True)
+ my_ccy = self.fx.get_currency() if self.fx.is_enabled() else _('None')
+ self.ids.ccy.values = currencies
+ self.ids.ccy.text = my_ccy
+
+ def on_currency(self, ccy):
+ b = (ccy != _('None'))
+ self.fx.set_enabled(b)
+ if b:
+ if ccy != self.fx.get_currency():
+ self.fx.set_currency(ccy)
+ self.app.fiat_unit = ccy
+ else:
+ self.app.is_fiat = False
+ Clock.schedule_once(lambda dt: self.add_exchanges())
DIR diff --git a/electrum/gui/kivy/uix/dialogs/installwizard.py b/electrum/gui/kivy/uix/dialogs/installwizard.py
t@@ -0,0 +1,1038 @@
+
+from functools import partial
+import threading
+import os
+
+from kivy.app import App
+from kivy.clock import Clock
+from kivy.lang import Builder
+from kivy.properties import ObjectProperty, StringProperty, OptionProperty
+from kivy.core.window import Window
+from kivy.uix.button import Button
+from kivy.utils import platform
+from kivy.uix.widget import Widget
+from kivy.core.window import Window
+from kivy.clock import Clock
+from kivy.utils import platform
+
+from electrum.base_wizard import BaseWizard
+from electrum.util import is_valid_email
+
+
+from . import EventsDialog
+from ...i18n import _
+from .password_dialog import PasswordDialog
+
+# global Variables
+is_test = (platform == "linux")
+test_seed = "time taxi field recycle tiny license olive virus report rare steel portion achieve"
+test_seed = "grape impose jazz bind spatial mind jelly tourist tank today holiday stomach"
+test_xpub = "xpub661MyMwAqRbcEbvVtRRSjqxVnaWVUMewVzMiURAKyYratih4TtBpMypzzefmv8zUNebmNVzB3PojdC5sV2P9bDgMoo9B3SARw1MXUUfU1GL"
+
+Builder.load_string('''
+#:import Window kivy.core.window.Window
+#:import _ electrum.gui.kivy.i18n._
+
+
+<WizardTextInput@TextInput>
+ border: 4, 4, 4, 4
+ font_size: '15sp'
+ padding: '15dp', '15dp'
+ background_color: (1, 1, 1, 1) if self.focus else (0.454, 0.698, 0.909, 1)
+ foreground_color: (0.31, 0.31, 0.31, 1) if self.focus else (0.835, 0.909, 0.972, 1)
+ hint_text_color: self.foreground_color
+ background_active: 'atlas://electrum/gui/kivy/theming/light/create_act_text_active'
+ background_normal: 'atlas://electrum/gui/kivy/theming/light/create_act_text_active'
+ size_hint_y: None
+ height: '48sp'
+
+<WizardButton@Button>:
+ root: None
+ size_hint: 1, None
+ height: '48sp'
+ on_press: if self.root: self.root.dispatch('on_press', self)
+ on_release: if self.root: self.root.dispatch('on_release', self)
+
+<BigLabel@Label>
+ color: .854, .925, .984, 1
+ size_hint: 1, None
+ text_size: self.width, None
+ height: self.texture_size[1]
+ bold: True
+
+<-WizardDialog>
+ text_color: .854, .925, .984, 1
+ value: ''
+ #auto_dismiss: False
+ size_hint: None, None
+ canvas.before:
+ Color:
+ rgba: .239, .588, .882, 1
+ Rectangle:
+ size: Window.size
+
+ crcontent: crcontent
+ # add electrum icon
+ BoxLayout:
+ orientation: 'vertical' if self.width < self.height else 'horizontal'
+ padding:
+ min(dp(27), self.width/32), min(dp(27), self.height/32),\
+ min(dp(27), self.width/32), min(dp(27), self.height/32)
+ spacing: '10dp'
+ GridLayout:
+ id: grid_logo
+ cols: 1
+ pos_hint: {'center_y': .5}
+ size_hint: 1, None
+ height: self.minimum_height
+ Label:
+ color: root.text_color
+ text: 'ELECTRUM'
+ size_hint: 1, None
+ height: self.texture_size[1] if self.opacity else 0
+ font_size: '33sp'
+ font_name: 'electrum/gui/kivy/data/fonts/tron/Tr2n.ttf'
+ GridLayout:
+ cols: 1
+ id: crcontent
+ spacing: '1dp'
+ Widget:
+ size_hint: 1, 0.3
+ GridLayout:
+ rows: 1
+ spacing: '12dp'
+ size_hint: 1, None
+ height: self.minimum_height
+ WizardButton:
+ id: back
+ text: _('Back')
+ root: root
+ WizardButton:
+ id: next
+ text: _('Next')
+ root: root
+ disabled: root.value == ''
+
+
+<WizardMultisigDialog>
+ value: 'next'
+ Widget
+ size_hint: 1, 1
+ Label:
+ color: root.text_color
+ size_hint: 1, None
+ text_size: self.width, None
+ height: self.texture_size[1]
+ text: _("Choose the number of signatures needed to unlock funds in your wallet")
+ Widget
+ size_hint: 1, 1
+ GridLayout:
+ orientation: 'vertical'
+ cols: 2
+ spacing: '14dp'
+ size_hint: 1, 1
+ height: self.minimum_height
+ Label:
+ color: root.text_color
+ text: _('From {} cosigners').format(n.value)
+ Slider:
+ id: n
+ range: 2, 5
+ step: 1
+ value: 2
+ Label:
+ color: root.text_color
+ text: _('Require {} signatures').format(m.value)
+ Slider:
+ id: m
+ range: 1, n.value
+ step: 1
+ value: 2
+
+
+<WizardChoiceDialog>
+ message : ''
+ Widget:
+ size_hint: 1, 1
+ Label:
+ color: root.text_color
+ size_hint: 1, None
+ text_size: self.width, None
+ height: self.texture_size[1]
+ text: root.message
+ Widget
+ size_hint: 1, 1
+ GridLayout:
+ row_default_height: '48dp'
+ orientation: 'vertical'
+ id: choices
+ cols: 1
+ spacing: '14dp'
+ size_hint: 1, None
+
+<WizardConfirmDialog>
+ message : ''
+ Widget:
+ size_hint: 1, 1
+ Label:
+ color: root.text_color
+ size_hint: 1, None
+ text_size: self.width, None
+ height: self.texture_size[1]
+ text: root.message
+ Widget
+ size_hint: 1, 1
+
+<WizardTOSDialog>
+ message : ''
+ size_hint: 1, 1
+ ScrollView:
+ size_hint: 1, 1
+ TextInput:
+ color: root.text_color
+ size_hint: 1, None
+ text_size: self.width, None
+ height: self.minimum_height
+ text: root.message
+ disabled: True
+
+<WizardEmailDialog>
+ Label:
+ color: root.text_color
+ size_hint: 1, None
+ text_size: self.width, None
+ height: self.texture_size[1]
+ text: 'Please enter your email address'
+ WizardTextInput:
+ id: email
+ on_text: Clock.schedule_once(root.on_text)
+ multiline: False
+ on_text_validate: Clock.schedule_once(root.on_enter)
+
+<WizardKnownOTPDialog>
+ message : ''
+ message2: ''
+ Widget:
+ size_hint: 1, 1
+ Label:
+ color: root.text_color
+ size_hint: 1, None
+ text_size: self.width, None
+ height: self.texture_size[1]
+ text: root.message
+ Widget
+ size_hint: 1, 1
+ WizardTextInput:
+ id: otp
+ on_text: Clock.schedule_once(root.on_text)
+ multiline: False
+ on_text_validate: Clock.schedule_once(root.on_enter)
+ Widget
+ size_hint: 1, 1
+ Label:
+ color: root.text_color
+ size_hint: 1, None
+ text_size: self.width, None
+ height: self.texture_size[1]
+ text: root.message2
+ Widget
+ size_hint: 1, 1
+ height: '48sp'
+ BoxLayout:
+ orientation: 'horizontal'
+ WizardButton:
+ id: cb
+ text: _('Request new secret')
+ on_release: root.request_new_secret()
+ size_hint: 1, None
+ WizardButton:
+ id: abort
+ text: _('Abort creation')
+ on_release: root.abort_wallet_creation()
+ size_hint: 1, None
+
+
+<WizardNewOTPDialog>
+ message : ''
+ message2 : ''
+ Label:
+ color: root.text_color
+ size_hint: 1, None
+ text_size: self.width, None
+ height: self.texture_size[1]
+ text: root.message
+ QRCodeWidget:
+ id: qr
+ size_hint: 1, 1
+ Label:
+ color: root.text_color
+ size_hint: 1, None
+ text_size: self.width, None
+ height: self.texture_size[1]
+ text: root.message2
+ WizardTextInput:
+ id: otp
+ on_text: Clock.schedule_once(root.on_text)
+ multiline: False
+ on_text_validate: Clock.schedule_once(root.on_enter)
+
+<MButton@Button>:
+ size_hint: 1, None
+ height: '33dp'
+ on_release:
+ self.parent.update_amount(self.text)
+
+<WordButton@Button>:
+ size_hint: None, None
+ padding: '5dp', '5dp'
+ text_size: None, self.height
+ width: self.texture_size[0]
+ height: '30dp'
+ on_release:
+ self.parent.new_word(self.text)
+
+
+<SeedButton@Button>:
+ height: dp(100)
+ border: 4, 4, 4, 4
+ halign: 'justify'
+ valign: 'top'
+ font_size: '18dp'
+ text_size: self.width - dp(24), self.height - dp(12)
+ color: .1, .1, .1, 1
+ background_normal: 'atlas://electrum/gui/kivy/theming/light/white_bg_round_top'
+ background_down: self.background_normal
+ size_hint_y: None
+
+
+<SeedLabel@Label>:
+ font_size: '12sp'
+ text_size: self.width, None
+ size_hint: 1, None
+ height: self.texture_size[1]
+ halign: 'justify'
+ valign: 'middle'
+ border: 4, 4, 4, 4
+
+
+<RestoreSeedDialog>
+ message: ''
+ word: ''
+ BigLabel:
+ text: "ENTER YOUR SEED PHRASE"
+ GridLayout
+ cols: 1
+ padding: 0, '12dp'
+ orientation: 'vertical'
+ spacing: '12dp'
+ size_hint: 1, None
+ height: self.minimum_height
+ SeedButton:
+ id: text_input_seed
+ text: ''
+ on_text: Clock.schedule_once(root.on_text)
+ on_release: root.options_dialog()
+ SeedLabel:
+ text: root.message
+ BoxLayout:
+ id: suggestions
+ height: '35dp'
+ size_hint: 1, None
+ new_word: root.on_word
+ BoxLayout:
+ id: line1
+ update_amount: root.update_text
+ size_hint: 1, None
+ height: '30dp'
+ MButton:
+ text: 'Q'
+ MButton:
+ text: 'W'
+ MButton:
+ text: 'E'
+ MButton:
+ text: 'R'
+ MButton:
+ text: 'T'
+ MButton:
+ text: 'Y'
+ MButton:
+ text: 'U'
+ MButton:
+ text: 'I'
+ MButton:
+ text: 'O'
+ MButton:
+ text: 'P'
+ BoxLayout:
+ id: line2
+ update_amount: root.update_text
+ size_hint: 1, None
+ height: '30dp'
+ Widget:
+ size_hint: 0.5, None
+ height: '33dp'
+ MButton:
+ text: 'A'
+ MButton:
+ text: 'S'
+ MButton:
+ text: 'D'
+ MButton:
+ text: 'F'
+ MButton:
+ text: 'G'
+ MButton:
+ text: 'H'
+ MButton:
+ text: 'J'
+ MButton:
+ text: 'K'
+ MButton:
+ text: 'L'
+ Widget:
+ size_hint: 0.5, None
+ height: '33dp'
+ BoxLayout:
+ id: line3
+ update_amount: root.update_text
+ size_hint: 1, None
+ height: '30dp'
+ Widget:
+ size_hint: 1, None
+ MButton:
+ text: 'Z'
+ MButton:
+ text: 'X'
+ MButton:
+ text: 'C'
+ MButton:
+ text: 'V'
+ MButton:
+ text: 'B'
+ MButton:
+ text: 'N'
+ MButton:
+ text: 'M'
+ MButton:
+ text: ' '
+ MButton:
+ text: '<'
+
+<AddXpubDialog>
+ title: ''
+ message: ''
+ BigLabel:
+ text: root.title
+ GridLayout
+ cols: 1
+ padding: 0, '12dp'
+ orientation: 'vertical'
+ spacing: '12dp'
+ size_hint: 1, None
+ height: self.minimum_height
+ SeedButton:
+ id: text_input
+ text: ''
+ on_text: Clock.schedule_once(root.check_text)
+ SeedLabel:
+ text: root.message
+ GridLayout
+ rows: 1
+ spacing: '12dp'
+ size_hint: 1, None
+ height: self.minimum_height
+ IconButton:
+ id: scan
+ height: '48sp'
+ on_release: root.scan_xpub()
+ icon: 'atlas://electrum/gui/kivy/theming/light/camera'
+ size_hint: 1, None
+ WizardButton:
+ text: _('Paste')
+ on_release: root.do_paste()
+ WizardButton:
+ text: _('Clear')
+ on_release: root.do_clear()
+
+
+<ShowXpubDialog>
+ xpub: ''
+ message: _('Here is your master public key. Share it with your cosigners.')
+ BigLabel:
+ text: "MASTER PUBLIC KEY"
+ GridLayout
+ cols: 1
+ padding: 0, '12dp'
+ orientation: 'vertical'
+ spacing: '12dp'
+ size_hint: 1, None
+ height: self.minimum_height
+ SeedButton:
+ id: text_input
+ text: root.xpub
+ SeedLabel:
+ text: root.message
+ GridLayout
+ rows: 1
+ spacing: '12dp'
+ size_hint: 1, None
+ height: self.minimum_height
+ WizardButton:
+ text: _('QR code')
+ on_release: root.do_qr()
+ WizardButton:
+ text: _('Copy')
+ on_release: root.do_copy()
+ WizardButton:
+ text: _('Share')
+ on_release: root.do_share()
+
+
+<ShowSeedDialog>
+ spacing: '12dp'
+ value: 'next'
+ BigLabel:
+ text: "PLEASE WRITE DOWN YOUR SEED PHRASE"
+ GridLayout:
+ id: grid
+ cols: 1
+ pos_hint: {'center_y': .5}
+ size_hint_y: None
+ height: self.minimum_height
+ orientation: 'vertical'
+ spacing: '12dp'
+ SeedButton:
+ text: root.seed_text
+ on_release: root.options_dialog()
+ SeedLabel:
+ text: root.message
+
+
+<LineDialog>
+
+ BigLabel:
+ text: root.title
+ SeedLabel:
+ text: root.message
+ TextInput:
+ id: passphrase_input
+ multiline: False
+ size_hint: 1, None
+ height: '27dp'
+ SeedLabel:
+ text: root.warning
+
+''')
+
+
+
+class WizardDialog(EventsDialog):
+ ''' Abstract dialog to be used as the base for all Create Account Dialogs
+ '''
+ crcontent = ObjectProperty(None)
+
+ def __init__(self, wizard, **kwargs):
+ super(WizardDialog, self).__init__()
+ self.wizard = wizard
+ self.ids.back.disabled = not wizard.can_go_back()
+ self.app = App.get_running_app()
+ self.run_next = kwargs['run_next']
+ _trigger_size_dialog = Clock.create_trigger(self._size_dialog)
+ Window.bind(size=_trigger_size_dialog,
+ rotation=_trigger_size_dialog)
+ _trigger_size_dialog()
+ self._on_release = False
+
+ def _size_dialog(self, dt):
+ app = App.get_running_app()
+ if app.ui_mode[0] == 'p':
+ self.size = Window.size
+ else:
+ #tablet
+ if app.orientation[0] == 'p':
+ #portrait
+ self.size = Window.size[0]/1.67, Window.size[1]/1.4
+ else:
+ self.size = Window.size[0]/2.5, Window.size[1]
+
+ def add_widget(self, widget, index=0):
+ if not self.crcontent:
+ super(WizardDialog, self).add_widget(widget)
+ else:
+ self.crcontent.add_widget(widget, index=index)
+
+ def on_dismiss(self):
+ app = App.get_running_app()
+ if app.wallet is None and not self._on_release:
+ app.stop()
+
+ def get_params(self, button):
+ return (None,)
+
+ def on_release(self, button):
+ self._on_release = True
+ self.close()
+ if not button:
+ self.parent.dispatch('on_wizard_complete', None)
+ return
+ if button is self.ids.back:
+ self.wizard.go_back()
+ return
+ params = self.get_params(button)
+ self.run_next(*params)
+
+
+class WizardMultisigDialog(WizardDialog):
+
+ def get_params(self, button):
+ m = self.ids.m.value
+ n = self.ids.n.value
+ return m, n
+
+
+class WizardOTPDialogBase(WizardDialog):
+
+ def get_otp(self):
+ otp = self.ids.otp.text
+ if len(otp) != 6:
+ return
+ try:
+ return int(otp)
+ except:
+ return
+
+ def on_text(self, dt):
+ self.ids.next.disabled = self.get_otp() is None
+
+ def on_enter(self, dt):
+ # press next
+ next = self.ids.next
+ if not next.disabled:
+ next.dispatch('on_release')
+
+
+class WizardKnownOTPDialog(WizardOTPDialogBase):
+
+ def __init__(self, wizard, **kwargs):
+ WizardOTPDialogBase.__init__(self, wizard, **kwargs)
+ self.message = _("This wallet is already registered with TrustedCoin. To finalize wallet creation, please enter your Google Authenticator Code.")
+ self.message2 =_("If you have lost your Google Authenticator account, you can request a new secret. You will need to retype your seed.")
+ self.request_new = False
+
+ def get_params(self, button):
+ return (self.get_otp(), self.request_new)
+
+ def request_new_secret(self):
+ self.request_new = True
+ self.on_release(True)
+
+ def abort_wallet_creation(self):
+ self._on_release = True
+ os.unlink(self.wizard.storage.path)
+ self.wizard.terminate()
+ self.dismiss()
+
+
+class WizardNewOTPDialog(WizardOTPDialogBase):
+
+ def __init__(self, wizard, **kwargs):
+ WizardOTPDialogBase.__init__(self, wizard, **kwargs)
+ otp_secret = kwargs['otp_secret']
+ uri = "otpauth://totp/%s?secret=%s"%('trustedcoin.com', otp_secret)
+ self.message = "Please scan the following QR code in Google Authenticator. You may also use the secret key: %s"%otp_secret
+ self.message2 = _('Then, enter your Google Authenticator code:')
+ self.ids.qr.set_data(uri)
+
+ def get_params(self, button):
+ return (self.get_otp(), False)
+
+class WizardTOSDialog(WizardDialog):
+
+ def __init__(self, wizard, **kwargs):
+ WizardDialog.__init__(self, wizard, **kwargs)
+ self.ids.next.text = 'Accept'
+ self.ids.next.disabled = False
+ self.message = kwargs['tos']
+ self.message2 = _('Enter your email address:')
+
+class WizardEmailDialog(WizardDialog):
+
+ def get_params(self, button):
+ return (self.ids.email.text,)
+
+ def on_text(self, dt):
+ self.ids.next.disabled = not is_valid_email(self.ids.email.text)
+
+ def on_enter(self, dt):
+ # press next
+ next = self.ids.next
+ if not next.disabled:
+ next.dispatch('on_release')
+
+class WizardConfirmDialog(WizardDialog):
+
+ def __init__(self, wizard, **kwargs):
+ super(WizardConfirmDialog, self).__init__(wizard, **kwargs)
+ self.message = kwargs.get('message', '')
+ self.value = 'ok'
+
+ def on_parent(self, instance, value):
+ if value:
+ app = App.get_running_app()
+ self._back = _back = partial(app.dispatch, 'on_back')
+
+ def get_params(self, button):
+ return (True,)
+
+class WizardChoiceDialog(WizardDialog):
+
+ def __init__(self, wizard, **kwargs):
+ super(WizardChoiceDialog, self).__init__(wizard, **kwargs)
+ self.message = kwargs.get('message', '')
+ choices = kwargs.get('choices', [])
+ layout = self.ids.choices
+ layout.bind(minimum_height=layout.setter('height'))
+ for action, text in choices:
+ l = WizardButton(text=text)
+ l.action = action
+ l.height = '48dp'
+ l.root = self
+ layout.add_widget(l)
+
+ def on_parent(self, instance, value):
+ if value:
+ app = App.get_running_app()
+ self._back = _back = partial(app.dispatch, 'on_back')
+
+ def get_params(self, button):
+ return (button.action,)
+
+
+
+class LineDialog(WizardDialog):
+ title = StringProperty('')
+ message = StringProperty('')
+ warning = StringProperty('')
+
+ def __init__(self, wizard, **kwargs):
+ WizardDialog.__init__(self, wizard, **kwargs)
+ self.ids.next.disabled = False
+
+ def get_params(self, b):
+ return (self.ids.passphrase_input.text,)
+
+class ShowSeedDialog(WizardDialog):
+ seed_text = StringProperty('')
+ message = _("If you forget your PIN or lose your device, your seed phrase will be the only way to recover your funds.")
+ ext = False
+
+ def __init__(self, wizard, **kwargs):
+ super(ShowSeedDialog, self).__init__(wizard, **kwargs)
+ self.seed_text = kwargs['seed_text']
+
+ def on_parent(self, instance, value):
+ if value:
+ app = App.get_running_app()
+ self._back = _back = partial(self.ids.back.dispatch, 'on_release')
+
+ def options_dialog(self):
+ from .seed_options import SeedOptionsDialog
+ def callback(status):
+ self.ext = status
+ d = SeedOptionsDialog(self.ext, callback)
+ d.open()
+
+ def get_params(self, b):
+ return (self.ext,)
+
+
+class WordButton(Button):
+ pass
+
+class WizardButton(Button):
+ pass
+
+
+class RestoreSeedDialog(WizardDialog):
+
+ def __init__(self, wizard, **kwargs):
+ super(RestoreSeedDialog, self).__init__(wizard, **kwargs)
+ self._test = kwargs['test']
+ from electrum.mnemonic import Mnemonic
+ from electrum.old_mnemonic import words as old_wordlist
+ self.words = set(Mnemonic('en').wordlist).union(set(old_wordlist))
+ self.ids.text_input_seed.text = test_seed if is_test else ''
+ self.message = _('Please type your seed phrase using the virtual keyboard.')
+ self.title = _('Enter Seed')
+ self.ext = False
+
+ def options_dialog(self):
+ from .seed_options import SeedOptionsDialog
+ def callback(status):
+ self.ext = status
+ d = SeedOptionsDialog(self.ext, callback)
+ d.open()
+
+ def get_suggestions(self, prefix):
+ for w in self.words:
+ if w.startswith(prefix):
+ yield w
+
+ def on_text(self, dt):
+ self.ids.next.disabled = not bool(self._test(self.get_text()))
+
+ text = self.ids.text_input_seed.text
+ if not text:
+ last_word = ''
+ elif text[-1] == ' ':
+ last_word = ''
+ else:
+ last_word = text.split(' ')[-1]
+
+ enable_space = False
+ self.ids.suggestions.clear_widgets()
+ suggestions = [x for x in self.get_suggestions(last_word)]
+
+ if last_word in suggestions:
+ b = WordButton(text=last_word)
+ self.ids.suggestions.add_widget(b)
+ enable_space = True
+
+ for w in suggestions:
+ if w != last_word and len(suggestions) < 10:
+ b = WordButton(text=w)
+ self.ids.suggestions.add_widget(b)
+
+ i = len(last_word)
+ p = set()
+ for x in suggestions:
+ if len(x)>i: p.add(x[i])
+
+ for line in [self.ids.line1, self.ids.line2, self.ids.line3]:
+ for c in line.children:
+ if isinstance(c, Button):
+ if c.text in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
+ c.disabled = (c.text.lower() not in p) and bool(last_word)
+ elif c.text == ' ':
+ c.disabled = not enable_space
+
+ def on_word(self, w):
+ text = self.get_text()
+ words = text.split(' ')
+ words[-1] = w
+ text = ' '.join(words)
+ self.ids.text_input_seed.text = text + ' '
+ self.ids.suggestions.clear_widgets()
+
+ def get_text(self):
+ ti = self.ids.text_input_seed
+ return ' '.join(ti.text.strip().split())
+
+ def update_text(self, c):
+ c = c.lower()
+ text = self.ids.text_input_seed.text
+ if c == '<':
+ text = text[:-1]
+ else:
+ text += c
+ self.ids.text_input_seed.text = text
+
+ def on_parent(self, instance, value):
+ if value:
+ tis = self.ids.text_input_seed
+ tis.focus = True
+ #tis._keyboard.bind(on_key_down=self.on_key_down)
+ self._back = _back = partial(self.ids.back.dispatch,
+ 'on_release')
+ app = App.get_running_app()
+
+ def on_key_down(self, keyboard, keycode, key, modifiers):
+ if keycode[0] in (13, 271):
+ self.on_enter()
+ return True
+
+ def on_enter(self):
+ #self._remove_keyboard()
+ # press next
+ next = self.ids.next
+ if not next.disabled:
+ next.dispatch('on_release')
+
+ def _remove_keyboard(self):
+ tis = self.ids.text_input_seed
+ if tis._keyboard:
+ tis._keyboard.unbind(on_key_down=self.on_key_down)
+ tis.focus = False
+
+ def get_params(self, b):
+ return (self.get_text(), False, self.ext)
+
+
+class ConfirmSeedDialog(RestoreSeedDialog):
+ def get_params(self, b):
+ return (self.get_text(),)
+ def options_dialog(self):
+ pass
+
+
+class ShowXpubDialog(WizardDialog):
+
+ def __init__(self, wizard, **kwargs):
+ WizardDialog.__init__(self, wizard, **kwargs)
+ self.xpub = kwargs['xpub']
+ self.ids.next.disabled = False
+
+ def do_copy(self):
+ self.app._clipboard.copy(self.xpub)
+
+ def do_share(self):
+ self.app.do_share(self.xpub, _("Master Public Key"))
+
+ def do_qr(self):
+ from .qr_dialog import QRDialog
+ popup = QRDialog(_("Master Public Key"), self.xpub, True)
+ popup.open()
+
+
+class AddXpubDialog(WizardDialog):
+
+ def __init__(self, wizard, **kwargs):
+ WizardDialog.__init__(self, wizard, **kwargs)
+ self.is_valid = kwargs['is_valid']
+ self.title = kwargs['title']
+ self.message = kwargs['message']
+ self.allow_multi = kwargs.get('allow_multi', False)
+
+ def check_text(self, dt):
+ self.ids.next.disabled = not bool(self.is_valid(self.get_text()))
+
+ def get_text(self):
+ ti = self.ids.text_input
+ return ti.text.strip()
+
+ def get_params(self, button):
+ return (self.get_text(),)
+
+ def scan_xpub(self):
+ def on_complete(text):
+ if self.allow_multi:
+ self.ids.text_input.text += text + '\n'
+ else:
+ self.ids.text_input.text = text
+ self.app.scan_qr(on_complete)
+
+ def do_paste(self):
+ self.ids.text_input.text = test_xpub if is_test else self.app._clipboard.paste()
+
+ def do_clear(self):
+ self.ids.text_input.text = ''
+
+
+
+
+class InstallWizard(BaseWizard, Widget):
+ '''
+ events::
+ `on_wizard_complete` Fired when the wizard is done creating/ restoring
+ wallet/s.
+ '''
+
+ __events__ = ('on_wizard_complete', )
+
+ def on_wizard_complete(self, wallet):
+ """overriden by main_window"""
+ pass
+
+ def waiting_dialog(self, task, msg, on_finished=None):
+ '''Perform a blocking task in the background by running the passed
+ method in a thread.
+ '''
+ def target():
+ # run your threaded function
+ try:
+ task()
+ except Exception as err:
+ self.show_error(str(err))
+ # on completion hide message
+ Clock.schedule_once(lambda dt: app.info_bubble.hide(now=True), -1)
+ if on_finished:
+ Clock.schedule_once(lambda dt: on_finished(), -1)
+
+ app = App.get_running_app()
+ app.show_info_bubble(
+ text=msg, icon='atlas://electrum/gui/kivy/theming/light/important',
+ pos=Window.center, width='200sp', arrow_pos=None, modal=True)
+ t = threading.Thread(target = target)
+ t.start()
+
+ def terminate(self, **kwargs):
+ self.dispatch('on_wizard_complete', self.wallet)
+
+ def choice_dialog(self, **kwargs):
+ choices = kwargs['choices']
+ if len(choices) > 1:
+ WizardChoiceDialog(self, **kwargs).open()
+ else:
+ f = kwargs['run_next']
+ f(choices[0][0])
+
+ def multisig_dialog(self, **kwargs): WizardMultisigDialog(self, **kwargs).open()
+ def show_seed_dialog(self, **kwargs): ShowSeedDialog(self, **kwargs).open()
+ def line_dialog(self, **kwargs): LineDialog(self, **kwargs).open()
+
+ def confirm_seed_dialog(self, **kwargs):
+ kwargs['title'] = _('Confirm Seed')
+ kwargs['message'] = _('Please retype your seed phrase, to confirm that you properly saved it')
+ ConfirmSeedDialog(self, **kwargs).open()
+
+ def restore_seed_dialog(self, **kwargs):
+ RestoreSeedDialog(self, **kwargs).open()
+
+ def confirm_dialog(self, **kwargs):
+ WizardConfirmDialog(self, **kwargs).open()
+
+ def tos_dialog(self, **kwargs):
+ WizardTOSDialog(self, **kwargs).open()
+
+ def email_dialog(self, **kwargs):
+ WizardEmailDialog(self, **kwargs).open()
+
+ def otp_dialog(self, **kwargs):
+ if kwargs['otp_secret']:
+ WizardNewOTPDialog(self, **kwargs).open()
+ else:
+ WizardKnownOTPDialog(self, **kwargs).open()
+
+ def add_xpub_dialog(self, **kwargs):
+ kwargs['message'] += ' ' + _('Use the camera button to scan a QR code.')
+ AddXpubDialog(self, **kwargs).open()
+
+ def add_cosigner_dialog(self, **kwargs):
+ kwargs['title'] = _("Add Cosigner") + " %d"%kwargs['index']
+ kwargs['message'] = _('Please paste your cosigners master public key, or scan it using the camera button.')
+ AddXpubDialog(self, **kwargs).open()
+
+ def show_xpub_dialog(self, **kwargs): ShowXpubDialog(self, **kwargs).open()
+
+ def show_message(self, msg): self.show_error(msg)
+
+ def show_error(self, msg):
+ app = App.get_running_app()
+ Clock.schedule_once(lambda dt: app.show_error(msg))
+
+ def request_password(self, run_next, force_disable_encrypt_cb=False):
+ def on_success(old_pin, pin):
+ assert old_pin is None
+ run_next(pin, False)
+ def on_failure():
+ self.show_error(_('PIN mismatch'))
+ self.run('request_password', run_next)
+ popup = PasswordDialog()
+ app = App.get_running_app()
+ popup.init(app, None, _('Choose PIN code'), on_success, on_failure, is_change=2)
+ popup.open()
+
+ def action_dialog(self, action, run_next):
+ f = getattr(self, action)
+ f()
DIR diff --git a/electrum/gui/kivy/uix/dialogs/invoices.py b/electrum/gui/kivy/uix/dialogs/invoices.py
t@@ -0,0 +1,169 @@
+from kivy.app import App
+from kivy.factory import Factory
+from kivy.properties import ObjectProperty
+from kivy.lang import Builder
+from decimal import Decimal
+
+Builder.load_string('''
+<InvoicesLabel@Label>
+ #color: .305, .309, .309, 1
+ text_size: self.width, None
+ halign: 'left'
+ valign: 'top'
+
+<InvoiceItem@CardItem>
+ requestor: ''
+ memo: ''
+ amount: ''
+ status: ''
+ date: ''
+ icon: 'atlas://electrum/gui/kivy/theming/light/important'
+ Image:
+ id: icon
+ source: root.icon
+ size_hint: None, 1
+ width: self.height *.54
+ mipmap: True
+ BoxLayout:
+ spacing: '8dp'
+ height: '32dp'
+ orientation: 'vertical'
+ Widget
+ InvoicesLabel:
+ text: root.requestor
+ shorten: True
+ Widget
+ InvoicesLabel:
+ text: root.memo
+ color: .699, .699, .699, 1
+ font_size: '13sp'
+ shorten: True
+ Widget
+ BoxLayout:
+ spacing: '8dp'
+ height: '32dp'
+ orientation: 'vertical'
+ Widget
+ InvoicesLabel:
+ text: root.amount
+ font_size: '15sp'
+ halign: 'right'
+ width: '110sp'
+ Widget
+ InvoicesLabel:
+ text: root.status
+ font_size: '13sp'
+ halign: 'right'
+ color: .699, .699, .699, 1
+ Widget
+
+
+<InvoicesDialog@Popup>
+ id: popup
+ title: _('Invoices')
+ BoxLayout:
+ id: box
+ orientation: 'vertical'
+ spacing: '1dp'
+ ScrollView:
+ GridLayout:
+ cols: 1
+ id: invoices_container
+ size_hint: 1, None
+ height: self.minimum_height
+ spacing: '2dp'
+ padding: '12dp'
+''')
+
+from kivy.properties import BooleanProperty
+from electrum.gui.kivy.i18n import _
+from electrum.util import format_time
+from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
+from electrum.gui.kivy.uix.context_menu import ContextMenu
+
+invoice_text = {
+ PR_UNPAID:_('Pending'),
+ PR_UNKNOWN:_('Unknown'),
+ PR_PAID:_('Paid'),
+ PR_EXPIRED:_('Expired')
+}
+pr_icon = {
+ PR_UNPAID: 'atlas://electrum/gui/kivy/theming/light/important',
+ PR_UNKNOWN: 'atlas://electrum/gui/kivy/theming/light/important',
+ PR_PAID: 'atlas://electrum/gui/kivy/theming/light/confirmed',
+ PR_EXPIRED: 'atlas://electrum/gui/kivy/theming/light/close'
+}
+
+
+class InvoicesDialog(Factory.Popup):
+
+ def __init__(self, app, screen, callback):
+ Factory.Popup.__init__(self)
+ self.app = app
+ self.screen = screen
+ self.callback = callback
+ self.cards = {}
+ self.context_menu = None
+
+ def get_card(self, pr):
+ key = pr.get_id()
+ ci = self.cards.get(key)
+ if ci is None:
+ ci = Factory.InvoiceItem()
+ ci.key = key
+ ci.screen = self
+ self.cards[key] = ci
+ ci.requestor = pr.get_requestor()
+ ci.memo = pr.get_memo()
+ amount = pr.get_amount()
+ if amount:
+ ci.amount = self.app.format_amount_and_units(amount)
+ status = self.app.wallet.invoices.get_status(ci.key)
+ ci.status = invoice_text[status]
+ ci.icon = pr_icon[status]
+ else:
+ ci.amount = _('No Amount')
+ ci.status = ''
+ exp = pr.get_expiration_date()
+ ci.date = format_time(exp) if exp else _('Never')
+ return ci
+
+ def update(self):
+ self.menu_actions = [('Pay', self.do_pay), ('Details', self.do_view), ('Delete', self.do_delete)]
+ invoices_list = self.ids.invoices_container
+ invoices_list.clear_widgets()
+ _list = self.app.wallet.invoices.sorted_list()
+ for pr in _list:
+ ci = self.get_card(pr)
+ invoices_list.add_widget(ci)
+
+ def do_pay(self, obj):
+ self.hide_menu()
+ self.dismiss()
+ pr = self.app.wallet.invoices.get(obj.key)
+ self.app.on_pr(pr)
+
+ def do_view(self, obj):
+ pr = self.app.wallet.invoices.get(obj.key)
+ pr.verify(self.app.wallet.contacts)
+ self.app.show_pr_details(pr.get_dict(), obj.status, True)
+
+ def do_delete(self, obj):
+ from .question import Question
+ def cb(result):
+ if result:
+ self.app.wallet.invoices.remove(obj.key)
+ self.hide_menu()
+ self.update()
+ d = Question(_('Delete invoice?'), cb)
+ d.open()
+
+ def show_menu(self, obj):
+ self.hide_menu()
+ self.context_menu = ContextMenu(obj, self.menu_actions)
+ self.ids.box.add_widget(self.context_menu)
+
+ def hide_menu(self):
+ if self.context_menu is not None:
+ self.ids.box.remove_widget(self.context_menu)
+ self.context_menu = None
DIR diff --git a/electrum/gui/kivy/uix/dialogs/label_dialog.py b/electrum/gui/kivy/uix/dialogs/label_dialog.py
t@@ -0,0 +1,55 @@
+from kivy.app import App
+from kivy.factory import Factory
+from kivy.properties import ObjectProperty
+from kivy.lang import Builder
+
+Builder.load_string('''
+<LabelDialog@Popup>
+ id: popup
+ title: ''
+ size_hint: 0.8, 0.3
+ pos_hint: {'top':0.9}
+ BoxLayout:
+ orientation: 'vertical'
+ Widget:
+ size_hint: 1, 0.2
+ TextInput:
+ id:input
+ padding: '5dp'
+ size_hint: 1, None
+ height: '27dp'
+ pos_hint: {'center_y':.5}
+ text:''
+ multiline: False
+ background_normal: 'atlas://electrum/gui/kivy/theming/light/tab_btn'
+ background_active: 'atlas://electrum/gui/kivy/theming/light/textinput_active'
+ hint_text_color: self.foreground_color
+ foreground_color: 1, 1, 1, 1
+ font_size: '16dp'
+ focus: True
+ Widget:
+ size_hint: 1, 0.2
+ BoxLayout:
+ orientation: 'horizontal'
+ size_hint: 1, 0.5
+ Button:
+ text: 'Cancel'
+ size_hint: 0.5, None
+ height: '48dp'
+ on_release: popup.dismiss()
+ Button:
+ text: 'OK'
+ size_hint: 0.5, None
+ height: '48dp'
+ on_release:
+ root.callback(input.text)
+ popup.dismiss()
+''')
+
+class LabelDialog(Factory.Popup):
+
+ def __init__(self, title, text, callback):
+ Factory.Popup.__init__(self)
+ self.ids.input.text = text
+ self.callback = callback
+ self.title = title
DIR diff --git a/electrum/gui/kivy/uix/dialogs/nfc_transaction.py b/electrum/gui/kivy/uix/dialogs/nfc_transaction.py
t@@ -0,0 +1,32 @@
+class NFCTransactionDialog(AnimatedPopup):
+
+ mode = OptionProperty('send', options=('send','receive'))
+
+ scanner = ObjectProperty(None)
+
+ def __init__(self, **kwargs):
+ # Delayed Init
+ global NFCSCanner
+ if NFCSCanner is None:
+ from electrum.gui.kivy.nfc_scanner import NFCScanner
+ self.scanner = NFCSCanner
+
+ super(NFCTransactionDialog, self).__init__(**kwargs)
+ self.scanner.nfc_init()
+ self.scanner.bind()
+
+ def on_parent(self, instance, value):
+ sctr = self.ids.sctr
+ if value:
+ def _cmp(*l):
+ anim = Animation(rotation=2, scale=1, opacity=1)
+ anim.start(sctr)
+ anim.bind(on_complete=_start)
+
+ def _start(*l):
+ anim = Animation(rotation=350, scale=2, opacity=0)
+ anim.start(sctr)
+ anim.bind(on_complete=_cmp)
+ _start()
+ return
+ Animation.cancel_all(sctr)
+\ No newline at end of file
DIR diff --git a/electrum/gui/kivy/uix/dialogs/password_dialog.py b/electrum/gui/kivy/uix/dialogs/password_dialog.py
t@@ -0,0 +1,142 @@
+from kivy.app import App
+from kivy.factory import Factory
+from kivy.properties import ObjectProperty
+from kivy.lang import Builder
+from decimal import Decimal
+from kivy.clock import Clock
+
+from electrum.util import InvalidPassword
+from electrum.gui.kivy.i18n import _
+
+Builder.load_string('''
+
+<PasswordDialog@Popup>
+ id: popup
+ title: 'Electrum'
+ message: ''
+ BoxLayout:
+ size_hint: 1, 1
+ orientation: 'vertical'
+ Widget:
+ size_hint: 1, 0.05
+ Label:
+ font_size: '20dp'
+ text: root.message
+ text_size: self.width, None
+ size: self.texture_size
+ Widget:
+ size_hint: 1, 0.05
+ Label:
+ id: a
+ font_size: '50dp'
+ text: '*'*len(kb.password) + '-'*(6-len(kb.password))
+ size: self.texture_size
+ Widget:
+ size_hint: 1, 0.05
+ GridLayout:
+ id: kb
+ size_hint: 1, None
+ height: self.minimum_height
+ update_amount: popup.update_password
+ password: ''
+ on_password: popup.on_password(self.password)
+ spacing: '2dp'
+ cols: 3
+ KButton:
+ text: '1'
+ KButton:
+ text: '2'
+ KButton:
+ text: '3'
+ KButton:
+ text: '4'
+ KButton:
+ text: '5'
+ KButton:
+ text: '6'
+ KButton:
+ text: '7'
+ KButton:
+ text: '8'
+ KButton:
+ text: '9'
+ KButton:
+ text: 'Clear'
+ KButton:
+ text: '0'
+ KButton:
+ text: '<'
+''')
+
+
+class PasswordDialog(Factory.Popup):
+
+ def init(self, app, wallet, message, on_success, on_failure, is_change=0):
+ self.app = app
+ self.wallet = wallet
+ self.message = message
+ self.on_success = on_success
+ self.on_failure = on_failure
+ self.ids.kb.password = ''
+ self.success = False
+ self.is_change = is_change
+ self.pw = None
+ self.new_password = None
+ self.title = 'Electrum' + (' - ' + self.wallet.basename() if self.wallet else '')
+
+ def check_password(self, password):
+ if self.is_change > 1:
+ return True
+ try:
+ self.wallet.check_password(password)
+ return True
+ except InvalidPassword as e:
+ return False
+
+ def on_dismiss(self):
+ if not self.success:
+ if self.on_failure:
+ self.on_failure()
+ else:
+ # keep dialog open
+ return True
+ else:
+ if self.on_success:
+ args = (self.pw, self.new_password) if self.is_change else (self.pw,)
+ Clock.schedule_once(lambda dt: self.on_success(*args), 0.1)
+
+ def update_password(self, c):
+ kb = self.ids.kb
+ text = kb.password
+ if c == '<':
+ text = text[:-1]
+ elif c == 'Clear':
+ text = ''
+ else:
+ text += c
+ kb.password = text
+
+ def on_password(self, pw):
+ if len(pw) == 6:
+ if self.check_password(pw):
+ if self.is_change == 0:
+ self.success = True
+ self.pw = pw
+ self.message = _('Please wait...')
+ self.dismiss()
+ elif self.is_change == 1:
+ self.pw = pw
+ self.message = _('Enter new PIN')
+ self.ids.kb.password = ''
+ self.is_change = 2
+ elif self.is_change == 2:
+ self.new_password = pw
+ self.message = _('Confirm new PIN')
+ self.ids.kb.password = ''
+ self.is_change = 3
+ elif self.is_change == 3:
+ self.success = pw == self.new_password
+ self.dismiss()
+ else:
+ self.app.show_error(_('Wrong PIN'))
+ self.ids.kb.password = ''
DIR diff --git a/gui/kivy/uix/dialogs/qr_dialog.py b/electrum/gui/kivy/uix/dialogs/qr_dialog.py
DIR diff --git a/electrum/gui/kivy/uix/dialogs/qr_scanner.py b/electrum/gui/kivy/uix/dialogs/qr_scanner.py
t@@ -0,0 +1,44 @@
+from kivy.app import App
+from kivy.factory import Factory
+from kivy.lang import Builder
+
+Factory.register('QRScanner', module='electrum.gui.kivy.qr_scanner')
+
+class QrScannerDialog(Factory.AnimatedPopup):
+
+ __events__ = ('on_complete', )
+
+ def on_symbols(self, instance, value):
+ instance.stop()
+ self.dismiss()
+ data = value[0].data
+ self.dispatch('on_complete', data)
+
+ def on_complete(self, x):
+ ''' Default Handler for on_complete event.
+ '''
+ print(x)
+
+
+Builder.load_string('''
+<QrScannerDialog>
+ title:
+ _(\
+ '[size=18dp]Hold your QRCode up to the camera[/size][size=7dp]\\n[/size]')
+ title_size: '24sp'
+ border: 7, 7, 7, 7
+ size_hint: None, None
+ size: '340dp', '290dp'
+ pos_hint: {'center_y': .53}
+ #separator_color: .89, .89, .89, 1
+ #separator_height: '1.2dp'
+ #title_color: .437, .437, .437, 1
+ #background: 'atlas://electrum/gui/kivy/theming/light/dialog'
+ on_activate:
+ qrscr.start()
+ qrscr.size = self.size
+ on_deactivate: qrscr.stop()
+ QRScanner:
+ id: qrscr
+ on_symbols: root.on_symbols(*args)
+''')
DIR diff --git a/electrum/gui/kivy/uix/dialogs/question.py b/electrum/gui/kivy/uix/dialogs/question.py
t@@ -0,0 +1,53 @@
+from kivy.app import App
+from kivy.factory import Factory
+from kivy.properties import ObjectProperty
+from kivy.lang import Builder
+from kivy.uix.checkbox import CheckBox
+from kivy.uix.label import Label
+from kivy.uix.widget import Widget
+
+from electrum.gui.kivy.i18n import _
+
+Builder.load_string('''
+<Question@Popup>
+ id: popup
+ title: ''
+ message: ''
+ size_hint: 0.8, 0.5
+ pos_hint: {'top':0.9}
+ BoxLayout:
+ orientation: 'vertical'
+ Label:
+ id: label
+ text: root.message
+ text_size: self.width, None
+ Widget:
+ size_hint: 1, 0.1
+ BoxLayout:
+ orientation: 'horizontal'
+ size_hint: 1, 0.2
+ Button:
+ text: _('No')
+ size_hint: 0.5, None
+ height: '48dp'
+ on_release:
+ root.callback(False)
+ popup.dismiss()
+ Button:
+ text: _('Yes')
+ size_hint: 0.5, None
+ height: '48dp'
+ on_release:
+ root.callback(True)
+ popup.dismiss()
+''')
+
+
+
+class Question(Factory.Popup):
+
+ def __init__(self, msg, callback):
+ Factory.Popup.__init__(self)
+ self.title = _('Question')
+ self.message = msg
+ self.callback = callback
DIR diff --git a/electrum/gui/kivy/uix/dialogs/requests.py b/electrum/gui/kivy/uix/dialogs/requests.py
t@@ -0,0 +1,157 @@
+from kivy.app import App
+from kivy.factory import Factory
+from kivy.properties import ObjectProperty
+from kivy.lang import Builder
+from decimal import Decimal
+
+Builder.load_string('''
+<RequestLabel@Label>
+ #color: .305, .309, .309, 1
+ text_size: self.width, None
+ halign: 'left'
+ valign: 'top'
+
+<RequestItem@CardItem>
+ address: ''
+ memo: ''
+ amount: ''
+ status: ''
+ date: ''
+ icon: 'atlas://electrum/gui/kivy/theming/light/important'
+ Image:
+ id: icon
+ source: root.icon
+ size_hint: None, 1
+ width: self.height *.54
+ mipmap: True
+ BoxLayout:
+ spacing: '8dp'
+ height: '32dp'
+ orientation: 'vertical'
+ Widget
+ RequestLabel:
+ text: root.address
+ shorten: True
+ Widget
+ RequestLabel:
+ text: root.memo
+ color: .699, .699, .699, 1
+ font_size: '13sp'
+ shorten: True
+ Widget
+ BoxLayout:
+ spacing: '8dp'
+ height: '32dp'
+ orientation: 'vertical'
+ Widget
+ RequestLabel:
+ text: root.amount
+ halign: 'right'
+ font_size: '15sp'
+ Widget
+ RequestLabel:
+ text: root.status
+ halign: 'right'
+ font_size: '13sp'
+ color: .699, .699, .699, 1
+ Widget
+
+<RequestsDialog@Popup>
+ id: popup
+ title: _('Requests')
+ BoxLayout:
+ id:box
+ orientation: 'vertical'
+ spacing: '1dp'
+ ScrollView:
+ GridLayout:
+ cols: 1
+ id: requests_container
+ size_hint: 1, None
+ height: self.minimum_height
+ spacing: '2dp'
+ padding: '12dp'
+''')
+
+from kivy.properties import BooleanProperty
+from electrum.gui.kivy.i18n import _
+from electrum.util import format_time
+from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
+from electrum.gui.kivy.uix.context_menu import ContextMenu
+
+pr_icon = {
+ PR_UNPAID: 'atlas://electrum/gui/kivy/theming/light/important',
+ PR_UNKNOWN: 'atlas://electrum/gui/kivy/theming/light/important',
+ PR_PAID: 'atlas://electrum/gui/kivy/theming/light/confirmed',
+ PR_EXPIRED: 'atlas://electrum/gui/kivy/theming/light/close'
+}
+request_text = {
+ PR_UNPAID: _('Pending'),
+ PR_UNKNOWN: _('Unknown'),
+ PR_PAID: _('Received'),
+ PR_EXPIRED: _('Expired')
+}
+
+
+class RequestsDialog(Factory.Popup):
+
+ def __init__(self, app, screen, callback):
+ Factory.Popup.__init__(self)
+ self.app = app
+ self.screen = screen
+ self.callback = callback
+ self.cards = {}
+ self.context_menu = None
+
+ def get_card(self, req):
+ address = req['address']
+ ci = self.cards.get(address)
+ if ci is None:
+ ci = Factory.RequestItem()
+ ci.address = address
+ ci.screen = self
+ self.cards[address] = ci
+
+ amount = req.get('amount')
+ ci.amount = self.app.format_amount_and_units(amount) if amount else ''
+ ci.memo = req.get('memo', '')
+ status, conf = self.app.wallet.get_request_status(address)
+ ci.status = request_text[status]
+ ci.icon = pr_icon[status]
+ #exp = pr.get_expiration_date()
+ #ci.date = format_time(exp) if exp else _('Never')
+ return ci
+
+ def update(self):
+ self.menu_actions = [(_('Show'), self.do_show), (_('Delete'), self.do_delete)]
+ requests_list = self.ids.requests_container
+ requests_list.clear_widgets()
+ _list = self.app.wallet.get_sorted_requests(self.app.electrum_config)
+ for pr in _list:
+ ci = self.get_card(pr)
+ requests_list.add_widget(ci)
+
+ def do_show(self, obj):
+ self.hide_menu()
+ self.dismiss()
+ self.app.show_request(obj.address)
+
+ def do_delete(self, req):
+ from .question import Question
+ def cb(result):
+ if result:
+ self.app.wallet.remove_payment_request(req.address, self.app.electrum_config)
+ self.hide_menu()
+ self.update()
+ d = Question(_('Delete request'), cb)
+ d.open()
+
+ def show_menu(self, obj):
+ self.hide_menu()
+ self.context_menu = ContextMenu(obj, self.menu_actions)
+ self.ids.box.add_widget(self.context_menu)
+
+ def hide_menu(self):
+ if self.context_menu is not None:
+ self.ids.box.remove_widget(self.context_menu)
+ self.context_menu = None
DIR diff --git a/gui/kivy/uix/dialogs/seed_options.py b/electrum/gui/kivy/uix/dialogs/seed_options.py
DIR diff --git a/electrum/gui/kivy/uix/dialogs/settings.py b/electrum/gui/kivy/uix/dialogs/settings.py
t@@ -0,0 +1,220 @@
+from kivy.app import App
+from kivy.factory import Factory
+from kivy.properties import ObjectProperty
+from kivy.lang import Builder
+
+from electrum.util import base_units_list
+from electrum.i18n import languages
+from electrum.gui.kivy.i18n import _
+from electrum.plugin import run_hook
+from electrum import coinchooser
+
+from .choice_dialog import ChoiceDialog
+
+Builder.load_string('''
+#:import partial functools.partial
+#:import _ electrum.gui.kivy.i18n._
+
+<SettingsDialog@Popup>
+ id: settings
+ title: _('Electrum Settings')
+ disable_pin: False
+ use_encryption: False
+ BoxLayout:
+ orientation: 'vertical'
+ ScrollView:
+ GridLayout:
+ id: scrollviewlayout
+ cols:1
+ size_hint: 1, None
+ height: self.minimum_height
+ padding: '10dp'
+ SettingsItem:
+ lang: settings.get_language_name()
+ title: 'Language' + ': ' + str(self.lang)
+ description: _('Language')
+ action: partial(root.language_dialog, self)
+ CardSeparator
+ SettingsItem:
+ disabled: root.disable_pin
+ title: _('PIN code')
+ description: _("Change your PIN code.")
+ action: partial(root.change_password, self)
+ CardSeparator
+ SettingsItem:
+ bu: app.base_unit
+ title: _('Denomination') + ': ' + self.bu
+ description: _("Base unit for Bitcoin amounts.")
+ action: partial(root.unit_dialog, self)
+ CardSeparator
+ SettingsItem:
+ status: root.fx_status()
+ title: _('Fiat Currency') + ': ' + self.status
+ description: _("Display amounts in fiat currency.")
+ action: partial(root.fx_dialog, self)
+ CardSeparator
+ SettingsItem:
+ status: 'ON' if bool(app.plugins.get('labels')) else 'OFF'
+ title: _('Labels Sync') + ': ' + self.status
+ description: _("Save and synchronize your labels.")
+ action: partial(root.plugin_dialog, 'labels', self)
+ CardSeparator
+ SettingsItem:
+ status: 'ON' if app.use_rbf else 'OFF'
+ title: _('Replace-by-fee') + ': ' + self.status
+ description: _("Create replaceable transactions.")
+ message:
+ _('If you check this box, your transactions will be marked as non-final,') \
+ + ' ' + _('and you will have the possibility, while they are unconfirmed, to replace them with transactions that pays higher fees.') \
+ + ' ' + _('Note that some merchants do not accept non-final transactions until they are confirmed.')
+ action: partial(root.boolean_dialog, 'use_rbf', _('Replace by fee'), self.message)
+ CardSeparator
+ SettingsItem:
+ status: _('Yes') if app.use_unconfirmed else _('No')
+ title: _('Spend unconfirmed') + ': ' + self.status
+ description: _("Use unconfirmed coins in transactions.")
+ message: _('Spend unconfirmed coins')
+ action: partial(root.boolean_dialog, 'use_unconfirmed', _('Use unconfirmed'), self.message)
+ CardSeparator
+ SettingsItem:
+ status: _('Yes') if app.use_change else _('No')
+ title: _('Use change addresses') + ': ' + self.status
+ description: _("Send your change to separate addresses.")
+ message: _('Send excess coins to change addresses')
+ action: partial(root.boolean_dialog, 'use_change', _('Use change addresses'), self.message)
+
+ # disabled: there is currently only one coin selection policy
+ #CardSeparator
+ #SettingsItem:
+ # status: root.coinselect_status()
+ # title: _('Coin selection') + ': ' + self.status
+ # description: "Coin selection method"
+ # action: partial(root.coinselect_dialog, self)
+''')
+
+
+
+class SettingsDialog(Factory.Popup):
+
+ def __init__(self, app):
+ self.app = app
+ self.plugins = self.app.plugins
+ self.config = self.app.electrum_config
+ Factory.Popup.__init__(self)
+ layout = self.ids.scrollviewlayout
+ layout.bind(minimum_height=layout.setter('height'))
+ # cached dialogs
+ self._fx_dialog = None
+ self._proxy_dialog = None
+ self._language_dialog = None
+ self._unit_dialog = None
+ self._coinselect_dialog = None
+
+ def update(self):
+ self.wallet = self.app.wallet
+ self.disable_pin = self.wallet.is_watching_only() if self.wallet else True
+ self.use_encryption = self.wallet.has_password() if self.wallet else False
+
+ def get_language_name(self):
+ return languages.get(self.config.get('language', 'en_UK'), '')
+
+ def change_password(self, item, dt):
+ self.app.change_password(self.update)
+
+ def language_dialog(self, item, dt):
+ if self._language_dialog is None:
+ l = self.config.get('language', 'en_UK')
+ def cb(key):
+ self.config.set_key("language", key, True)
+ item.lang = self.get_language_name()
+ self.app.language = key
+ self._language_dialog = ChoiceDialog(_('Language'), languages, l, cb)
+ self._language_dialog.open()
+
+ def unit_dialog(self, item, dt):
+ if self._unit_dialog is None:
+ def cb(text):
+ self.app._set_bu(text)
+ item.bu = self.app.base_unit
+ self._unit_dialog = ChoiceDialog(_('Denomination'), base_units_list,
+ self.app.base_unit, cb, keep_choice_order=True)
+ self._unit_dialog.open()
+
+ def coinselect_status(self):
+ return coinchooser.get_name(self.app.electrum_config)
+
+ def coinselect_dialog(self, item, dt):
+ if self._coinselect_dialog is None:
+ choosers = sorted(coinchooser.COIN_CHOOSERS.keys())
+ chooser_name = coinchooser.get_name(self.config)
+ def cb(text):
+ self.config.set_key('coin_chooser', text)
+ item.status = text
+ self._coinselect_dialog = ChoiceDialog(_('Coin selection'), choosers, chooser_name, cb)
+ self._coinselect_dialog.open()
+
+ def proxy_status(self):
+ server, port, protocol, proxy, auto_connect = self.app.network.get_parameters()
+ return proxy.get('host') +':' + proxy.get('port') if proxy else _('None')
+
+ def proxy_dialog(self, item, dt):
+ if self._proxy_dialog is None:
+ server, port, protocol, proxy, auto_connect = self.app.network.get_parameters()
+ def callback(popup):
+ if popup.ids.mode.text != 'None':
+ proxy = {
+ 'mode':popup.ids.mode.text,
+ 'host':popup.ids.host.text,
+ 'port':popup.ids.port.text,
+ 'user':popup.ids.user.text,
+ 'password':popup.ids.password.text
+ }
+ else:
+ proxy = None
+ self.app.network.set_parameters(server, port, protocol, proxy, auto_connect)
+ item.status = self.proxy_status()
+ popup = Builder.load_file('electrum/gui/kivy/uix/ui_screens/proxy.kv')
+ popup.ids.mode.text = proxy.get('mode') if proxy else 'None'
+ popup.ids.host.text = proxy.get('host') if proxy else ''
+ popup.ids.port.text = proxy.get('port') if proxy else ''
+ popup.ids.user.text = proxy.get('user') if proxy else ''
+ popup.ids.password.text = proxy.get('password') if proxy else ''
+ popup.on_dismiss = lambda: callback(popup)
+ self._proxy_dialog = popup
+ self._proxy_dialog.open()
+
+ def plugin_dialog(self, name, label, dt):
+ from .checkbox_dialog import CheckBoxDialog
+ def callback(status):
+ self.plugins.enable(name) if status else self.plugins.disable(name)
+ label.status = 'ON' if status else 'OFF'
+ status = bool(self.plugins.get(name))
+ dd = self.plugins.descriptions.get(name)
+ descr = dd.get('description')
+ fullname = dd.get('fullname')
+ d = CheckBoxDialog(fullname, descr, status, callback)
+ d.open()
+
+ def fee_status(self):
+ return self.config.get_fee_status()
+
+ def boolean_dialog(self, name, title, message, dt):
+ from .checkbox_dialog import CheckBoxDialog
+ CheckBoxDialog(title, message, getattr(self.app, name), lambda x: setattr(self.app, name, x)).open()
+
+ def fx_status(self):
+ fx = self.app.fx
+ if fx.is_enabled():
+ source = fx.exchange.name()
+ ccy = fx.get_currency()
+ return '%s [%s]' %(ccy, source)
+ else:
+ return _('None')
+
+ def fx_dialog(self, label, dt):
+ if self._fx_dialog is None:
+ from .fx_dialog import FxDialog
+ def cb():
+ label.status = self.fx_status()
+ self._fx_dialog = FxDialog(self.app, self.plugins, self.config, cb)
+ self._fx_dialog.open()
DIR diff --git a/electrum/gui/kivy/uix/dialogs/tx_dialog.py b/electrum/gui/kivy/uix/dialogs/tx_dialog.py
t@@ -0,0 +1,184 @@
+from kivy.app import App
+from kivy.factory import Factory
+from kivy.properties import ObjectProperty
+from kivy.lang import Builder
+from kivy.clock import Clock
+from kivy.uix.label import Label
+
+from electrum.gui.kivy.i18n import _
+from datetime import datetime
+from electrum.util import InvalidPassword
+
+Builder.load_string('''
+
+<TxDialog>
+ id: popup
+ title: _('Transaction')
+ is_mine: True
+ can_sign: False
+ can_broadcast: False
+ can_rbf: False
+ fee_str: ''
+ date_str: ''
+ date_label:''
+ amount_str: ''
+ tx_hash: ''
+ status_str: ''
+ description: ''
+ outputs_str: ''
+ BoxLayout:
+ orientation: 'vertical'
+ ScrollView:
+ scroll_type: ['bars', 'content']
+ bar_width: '25dp'
+ GridLayout:
+ height: self.minimum_height
+ size_hint_y: None
+ cols: 1
+ spacing: '10dp'
+ padding: '10dp'
+ GridLayout:
+ height: self.minimum_height
+ size_hint_y: None
+ cols: 1
+ spacing: '10dp'
+ BoxLabel:
+ text: _('Status')
+ value: root.status_str
+ BoxLabel:
+ text: _('Description') if root.description else ''
+ value: root.description
+ BoxLabel:
+ text: root.date_label
+ value: root.date_str
+ BoxLabel:
+ text: _('Amount sent') if root.is_mine else _('Amount received')
+ value: root.amount_str
+ BoxLabel:
+ text: _('Transaction fee') if root.fee_str else ''
+ value: root.fee_str
+ TopLabel:
+ text: _('Transaction ID') + ':' if root.tx_hash else ''
+ TxHashLabel:
+ data: root.tx_hash
+ name: _('Transaction ID')
+ TopLabel:
+ text: _('Outputs') + ':'
+ OutputList:
+ id: output_list
+ Widget:
+ size_hint: 1, 0.1
+
+ BoxLayout:
+ size_hint: 1, None
+ height: '48dp'
+ Button:
+ size_hint: 0.5, None
+ height: '48dp'
+ text: _('Sign') if root.can_sign else _('Broadcast') if root.can_broadcast else _('Bump fee') if root.can_rbf else ''
+ disabled: not(root.can_sign or root.can_broadcast or root.can_rbf)
+ opacity: 0 if self.disabled else 1
+ on_release:
+ if root.can_sign: root.do_sign()
+ if root.can_broadcast: root.do_broadcast()
+ if root.can_rbf: root.do_rbf()
+ IconButton:
+ size_hint: 0.5, None
+ height: '48dp'
+ icon: 'atlas://electrum/gui/kivy/theming/light/qrcode'
+ on_release: root.show_qr()
+ Button:
+ size_hint: 0.5, None
+ height: '48dp'
+ text: _('Close')
+ on_release: root.dismiss()
+''')
+
+
+class TxDialog(Factory.Popup):
+
+ def __init__(self, app, tx):
+ Factory.Popup.__init__(self)
+ self.app = app
+ self.wallet = self.app.wallet
+ self.tx = tx
+
+ def on_open(self):
+ self.update()
+
+ def update(self):
+ format_amount = self.app.format_amount_and_units
+ tx_hash, self.status_str, self.description, self.can_broadcast, self.can_rbf, amount, fee, height, conf, timestamp, exp_n = self.wallet.get_tx_info(self.tx)
+ self.tx_hash = tx_hash or ''
+ if timestamp:
+ self.date_label = _('Date')
+ self.date_str = datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
+ elif exp_n:
+ self.date_label = _('Mempool depth')
+ self.date_str = _('{} from tip').format('%.2f MB'%(exp_n/1000000))
+ else:
+ self.date_label = ''
+ self.date_str = ''
+
+ if amount is None:
+ self.amount_str = _("Transaction unrelated to your wallet")
+ elif amount > 0:
+ self.is_mine = False
+ self.amount_str = format_amount(amount)
+ else:
+ self.is_mine = True
+ self.amount_str = format_amount(-amount)
+ self.fee_str = format_amount(fee) if fee is not None else _('unknown')
+ self.can_sign = self.wallet.can_sign(self.tx)
+ self.ids.output_list.update(self.tx.outputs())
+
+ def do_rbf(self):
+ from .bump_fee_dialog import BumpFeeDialog
+ is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(self.tx)
+ if fee is None:
+ self.app.show_error(_("Can't bump fee: unknown fee for original transaction."))
+ return
+ size = self.tx.estimated_size()
+ d = BumpFeeDialog(self.app, fee, size, self._do_rbf)
+ d.open()
+
+ def _do_rbf(self, old_fee, new_fee, is_final):
+ if new_fee is None:
+ return
+ delta = new_fee - old_fee
+ if delta < 0:
+ self.app.show_error("fee too low")
+ return
+ try:
+ new_tx = self.wallet.bump_fee(self.tx, delta)
+ except BaseException as e:
+ self.app.show_error(str(e))
+ return
+ if is_final:
+ new_tx.set_rbf(False)
+ self.tx = new_tx
+ self.update()
+ self.do_sign()
+
+ def do_sign(self):
+ self.app.protected(_("Enter your PIN code in order to sign this transaction"), self._do_sign, ())
+
+ def _do_sign(self, password):
+ self.status_str = _('Signing') + '...'
+ Clock.schedule_once(lambda dt: self.__do_sign(password), 0.1)
+
+ def __do_sign(self, password):
+ try:
+ self.app.wallet.sign_transaction(self.tx, password)
+ except InvalidPassword:
+ self.app.show_error(_("Invalid PIN"))
+ self.update()
+
+ def do_broadcast(self):
+ self.app.broadcast(self.tx)
+
+ def show_qr(self):
+ from electrum.bitcoin import base_encode, bfh
+ text = bfh(str(self.tx))
+ text = base_encode(text, base=43)
+ self.app.qr_dialog(_("Raw Transaction"), text)
DIR diff --git a/gui/kivy/uix/dialogs/wallets.py b/electrum/gui/kivy/uix/dialogs/wallets.py
DIR diff --git a/gui/kivy/uix/drawer.py b/electrum/gui/kivy/uix/drawer.py
DIR diff --git a/gui/kivy/uix/gridview.py b/electrum/gui/kivy/uix/gridview.py
DIR diff --git a/electrum/gui/kivy/uix/menus.py b/electrum/gui/kivy/uix/menus.py
t@@ -0,0 +1,95 @@
+from functools import partial
+
+from kivy.animation import Animation
+from kivy.core.window import Window
+from kivy.clock import Clock
+from kivy.uix.bubble import Bubble, BubbleButton
+from kivy.properties import ListProperty
+from kivy.uix.widget import Widget
+
+from ..i18n import _
+
+class ContextMenuItem(Widget):
+ '''abstract class
+ '''
+
+class ContextButton(ContextMenuItem, BubbleButton):
+ pass
+
+class ContextMenu(Bubble):
+
+ buttons = ListProperty([_('ok'), _('cancel')])
+ '''List of Buttons to be displayed at the bottom'''
+
+ __events__ = ('on_press', 'on_release')
+
+ def __init__(self, **kwargs):
+ self._old_buttons = self.buttons
+ super(ContextMenu, self).__init__(**kwargs)
+ self.on_buttons(self, self.buttons)
+
+ def on_touch_down(self, touch):
+ if not self.collide_point(*touch.pos):
+ self.hide()
+ return
+ return super(ContextMenu, self).on_touch_down(touch)
+
+ def on_buttons(self, _menu, value):
+ if 'menu_content' not in self.ids.keys():
+ return
+ if value == self._old_buttons:
+ return
+ blayout = self.ids.menu_content
+ blayout.clear_widgets()
+ for btn in value:
+ ib = ContextButton(text=btn)
+ ib.bind(on_press=partial(self.dispatch, 'on_press'))
+ ib.bind(on_release=partial(self.dispatch, 'on_release'))
+ blayout.add_widget(ib)
+ self._old_buttons = value
+
+ def on_press(self, instance):
+ pass
+
+ def on_release(self, instance):
+ pass
+
+ def show(self, pos, duration=0):
+ Window.add_widget(self)
+ # wait for the bubble to adjust it's size according to text then animate
+ Clock.schedule_once(lambda dt: self._show(pos, duration))
+
+ def _show(self, pos, duration):
+ def on_stop(*l):
+ if duration:
+ Clock.schedule_once(self.hide, duration + .5)
+
+ self.opacity = 0
+ arrow_pos = self.arrow_pos
+ if arrow_pos[0] in ('l', 'r'):
+ pos = pos[0], pos[1] - (self.height/2)
+ else:
+ pos = pos[0] - (self.width/2), pos[1]
+
+ self.limit_to = Window
+
+ anim = Animation(opacity=1, pos=pos, d=.32)
+ anim.bind(on_complete=on_stop)
+ anim.cancel_all(self)
+ anim.start(self)
+
+
+ def hide(self, *dt):
+
+ def on_stop(*l):
+ Window.remove_widget(self)
+ anim = Animation(opacity=0, d=.25)
+ anim.bind(on_complete=on_stop)
+ anim.cancel_all(self)
+ anim.start(self)
+
+ def add_widget(self, widget, index=0):
+ if not isinstance(widget, ContextMenuItem):
+ super(ContextMenu, self).add_widget(widget, index)
+ return
+ menu_content.add_widget(widget, index)
DIR diff --git a/gui/kivy/uix/qrcodewidget.py b/electrum/gui/kivy/uix/qrcodewidget.py
DIR diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py
t@@ -0,0 +1,484 @@
+from weakref import ref
+from decimal import Decimal
+import re
+import datetime
+import traceback, sys
+
+from kivy.app import App
+from kivy.cache import Cache
+from kivy.clock import Clock
+from kivy.compat import string_types
+from kivy.properties import (ObjectProperty, DictProperty, NumericProperty,
+ ListProperty, StringProperty)
+
+from kivy.uix.recycleview import RecycleView
+from kivy.uix.label import Label
+
+from kivy.lang import Builder
+from kivy.factory import Factory
+from kivy.utils import platform
+
+from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat
+from electrum import bitcoin
+from electrum.util import timestamp_to_datetime
+from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
+from electrum.plugin import run_hook
+
+from .context_menu import ContextMenu
+
+
+from electrum.gui.kivy.i18n import _
+
+class HistoryRecycleView(RecycleView):
+ pass
+
+class CScreen(Factory.Screen):
+ __events__ = ('on_activate', 'on_deactivate', 'on_enter', 'on_leave')
+ action_view = ObjectProperty(None)
+ loaded = False
+ kvname = None
+ context_menu = None
+ menu_actions = []
+ app = App.get_running_app()
+
+ def _change_action_view(self):
+ app = App.get_running_app()
+ action_bar = app.root.manager.current_screen.ids.action_bar
+ _action_view = self.action_view
+
+ if (not _action_view) or _action_view.parent:
+ return
+ action_bar.clear_widgets()
+ action_bar.add_widget(_action_view)
+
+ def on_enter(self):
+ # FIXME: use a proper event don't use animation time of screen
+ Clock.schedule_once(lambda dt: self.dispatch('on_activate'), .25)
+ pass
+
+ def update(self):
+ pass
+
+ @profiler
+ def load_screen(self):
+ self.screen = Builder.load_file('electrum/gui/kivy/uix/ui_screens/' + self.kvname + '.kv')
+ self.add_widget(self.screen)
+ self.loaded = True
+ self.update()
+ setattr(self.app, self.kvname + '_screen', self)
+
+ def on_activate(self):
+ if self.kvname and not self.loaded:
+ self.load_screen()
+ #Clock.schedule_once(lambda dt: self._change_action_view())
+
+ def on_leave(self):
+ self.dispatch('on_deactivate')
+
+ def on_deactivate(self):
+ self.hide_menu()
+
+ def hide_menu(self):
+ if self.context_menu is not None:
+ self.remove_widget(self.context_menu)
+ self.context_menu = None
+
+ def show_menu(self, obj):
+ self.hide_menu()
+ self.context_menu = ContextMenu(obj, self.menu_actions)
+ self.add_widget(self.context_menu)
+
+
+# note: this list needs to be kept in sync with another in qt
+TX_ICONS = [
+ "unconfirmed",
+ "close",
+ "unconfirmed",
+ "close",
+ "clock1",
+ "clock2",
+ "clock3",
+ "clock4",
+ "clock5",
+ "confirmed",
+]
+
+class HistoryScreen(CScreen):
+
+ tab = ObjectProperty(None)
+ kvname = 'history'
+ cards = {}
+
+ def __init__(self, **kwargs):
+ self.ra_dialog = None
+ super(HistoryScreen, self).__init__(**kwargs)
+ self.menu_actions = [ ('Label', self.label_dialog), ('Details', self.show_tx)]
+
+ def show_tx(self, obj):
+ tx_hash = obj.tx_hash
+ tx = self.app.wallet.transactions.get(tx_hash)
+ if not tx:
+ return
+ self.app.tx_dialog(tx)
+
+ def label_dialog(self, obj):
+ from .dialogs.label_dialog import LabelDialog
+ key = obj.tx_hash
+ text = self.app.wallet.get_label(key)
+ def callback(text):
+ self.app.wallet.set_label(key, text)
+ self.update()
+ d = LabelDialog(_('Enter Transaction Label'), text, callback)
+ d.open()
+
+ def get_card(self, tx_hash, height, conf, timestamp, value, balance):
+ status, status_str = self.app.wallet.get_tx_status(tx_hash, height, conf, timestamp)
+ icon = "atlas://electrum/gui/kivy/theming/light/" + TX_ICONS[status]
+ label = self.app.wallet.get_label(tx_hash) if tx_hash else _('Pruned transaction outputs')
+ ri = {}
+ ri['screen'] = self
+ ri['tx_hash'] = tx_hash
+ ri['icon'] = icon
+ ri['date'] = status_str
+ ri['message'] = label
+ ri['confirmations'] = conf
+ if value is not None:
+ ri['is_mine'] = value < 0
+ if value < 0: value = - value
+ ri['amount'] = self.app.format_amount_and_units(value)
+ if self.app.fiat_unit:
+ fx = self.app.fx
+ fiat_value = value / Decimal(bitcoin.COIN) * self.app.wallet.price_at_timestamp(tx_hash, fx.timestamp_rate)
+ fiat_value = Fiat(fiat_value, fx.ccy)
+ ri['quote_text'] = str(fiat_value)
+ return ri
+
+ def update(self, see_all=False):
+ if self.app.wallet is None:
+ return
+ history = reversed(self.app.wallet.get_history())
+ history_card = self.screen.ids.history_container
+ count = 0
+ history_card.data = [self.get_card(*item) for item in history]
+
+
+class SendScreen(CScreen):
+
+ kvname = 'send'
+ payment_request = None
+ payment_request_queued = None
+
+ def set_URI(self, text):
+ if not self.app.wallet:
+ self.payment_request_queued = text
+ return
+ import electrum
+ try:
+ uri = electrum.util.parse_URI(text, self.app.on_pr)
+ except:
+ self.app.show_info(_("Not a Bitcoin URI"))
+ return
+ amount = uri.get('amount')
+ self.screen.address = uri.get('address', '')
+ self.screen.message = uri.get('message', '')
+ self.screen.amount = self.app.format_amount_and_units(amount) if amount else ''
+ self.payment_request = None
+ self.screen.is_pr = False
+
+ def update(self):
+ if self.app.wallet and self.payment_request_queued:
+ self.set_URI(self.payment_request_queued)
+ self.payment_request_queued = None
+
+ def do_clear(self):
+ self.screen.amount = ''
+ self.screen.message = ''
+ self.screen.address = ''
+ self.payment_request = None
+ self.screen.is_pr = False
+
+ def set_request(self, pr):
+ self.screen.address = pr.get_requestor()
+ amount = pr.get_amount()
+ self.screen.amount = self.app.format_amount_and_units(amount) if amount else ''
+ self.screen.message = pr.get_memo()
+ if pr.is_pr():
+ self.screen.is_pr = True
+ self.payment_request = pr
+ else:
+ self.screen.is_pr = False
+ self.payment_request = None
+
+ def do_save(self):
+ if not self.screen.address:
+ return
+ if self.screen.is_pr:
+ # it should be already saved
+ return
+ # save address as invoice
+ from electrum.paymentrequest import make_unsigned_request, PaymentRequest
+ req = {'address':self.screen.address, 'memo':self.screen.message}
+ amount = self.app.get_amount(self.screen.amount) if self.screen.amount else 0
+ req['amount'] = amount
+ pr = make_unsigned_request(req).SerializeToString()
+ pr = PaymentRequest(pr)
+ self.app.wallet.invoices.add(pr)
+ self.app.show_info(_("Invoice saved"))
+ if pr.is_pr():
+ self.screen.is_pr = True
+ self.payment_request = pr
+ else:
+ self.screen.is_pr = False
+ self.payment_request = None
+
+ def do_paste(self):
+ contents = self.app._clipboard.paste()
+ if not contents:
+ self.app.show_info(_("Clipboard is empty"))
+ return
+ self.set_URI(contents)
+
+ def do_send(self):
+ if self.screen.is_pr:
+ if self.payment_request.has_expired():
+ self.app.show_error(_('Payment request has expired'))
+ return
+ outputs = self.payment_request.get_outputs()
+ else:
+ address = str(self.screen.address)
+ if not address:
+ self.app.show_error(_('Recipient not specified.') + ' ' + _('Please scan a Bitcoin address or a payment request'))
+ return
+ if not bitcoin.is_address(address):
+ self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address)
+ return
+ try:
+ amount = self.app.get_amount(self.screen.amount)
+ except:
+ self.app.show_error(_('Invalid amount') + ':\n' + self.screen.amount)
+ return
+ outputs = [(bitcoin.TYPE_ADDRESS, address, amount)]
+ message = self.screen.message
+ amount = sum(map(lambda x:x[2], outputs))
+ if self.app.electrum_config.get('use_rbf'):
+ from .dialogs.question import Question
+ d = Question(_('Should this transaction be replaceable?'), lambda b: self._do_send(amount, message, outputs, b))
+ d.open()
+ else:
+ self._do_send(amount, message, outputs, False)
+
+ def _do_send(self, amount, message, outputs, rbf):
+ # make unsigned transaction
+ config = self.app.electrum_config
+ coins = self.app.wallet.get_spendable_coins(None, config)
+ try:
+ tx = self.app.wallet.make_unsigned_transaction(coins, outputs, config, None)
+ except NotEnoughFunds:
+ self.app.show_error(_("Not enough funds"))
+ return
+ except Exception as e:
+ traceback.print_exc(file=sys.stdout)
+ self.app.show_error(str(e))
+ return
+ if rbf:
+ tx.set_rbf(True)
+ fee = tx.get_fee()
+ msg = [
+ _("Amount to be sent") + ": " + self.app.format_amount_and_units(amount),
+ _("Mining fee") + ": " + self.app.format_amount_and_units(fee),
+ ]
+ x_fee = run_hook('get_tx_extra_fee', self.app.wallet, tx)
+ if x_fee:
+ x_fee_address, x_fee_amount = x_fee
+ msg.append(_("Additional fees") + ": " + self.app.format_amount_and_units(x_fee_amount))
+
+ if fee >= config.get('confirm_fee', 100000):
+ msg.append(_('Warning')+ ': ' + _("The fee for this transaction seems unusually high."))
+ msg.append(_("Enter your PIN code to proceed"))
+ self.app.protected('\n'.join(msg), self.send_tx, (tx, message))
+
+ def send_tx(self, tx, message, password):
+ if self.app.wallet.has_password() and password is None:
+ return
+ def on_success(tx):
+ if tx.is_complete():
+ self.app.broadcast(tx, self.payment_request)
+ self.app.wallet.set_label(tx.txid(), message)
+ else:
+ self.app.tx_dialog(tx)
+ def on_failure(error):
+ self.app.show_error(error)
+ if self.app.wallet.can_sign(tx):
+ self.app.show_info("Signing...")
+ self.app.sign_tx(tx, password, on_success, on_failure)
+ else:
+ self.app.tx_dialog(tx)
+
+
+class ReceiveScreen(CScreen):
+
+ kvname = 'receive'
+
+ def update(self):
+ if not self.screen.address:
+ self.get_new_address()
+ else:
+ status = self.app.wallet.get_request_status(self.screen.address)
+ self.screen.status = _('Payment received') if status == PR_PAID else ''
+
+ def clear(self):
+ self.screen.address = ''
+ self.screen.amount = ''
+ self.screen.message = ''
+
+ def get_new_address(self):
+ if not self.app.wallet:
+ return False
+ self.clear()
+ addr = self.app.wallet.get_unused_address()
+ if addr is None:
+ addr = self.app.wallet.get_receiving_address() or ''
+ b = False
+ else:
+ b = True
+ self.screen.address = addr
+ return b
+
+ def on_address(self, addr):
+ req = self.app.wallet.get_payment_request(addr, self.app.electrum_config)
+ self.screen.status = ''
+ if req:
+ self.screen.message = req.get('memo', '')
+ amount = req.get('amount')
+ self.screen.amount = self.app.format_amount_and_units(amount) if amount else ''
+ status = req.get('status', PR_UNKNOWN)
+ self.screen.status = _('Payment received') if status == PR_PAID else ''
+ Clock.schedule_once(lambda dt: self.update_qr())
+
+ def get_URI(self):
+ from electrum.util import create_URI
+ amount = self.screen.amount
+ if amount:
+ a, u = self.screen.amount.split()
+ assert u == self.app.base_unit
+ amount = Decimal(a) * pow(10, self.app.decimal_point())
+ return create_URI(self.screen.address, amount, self.screen.message)
+
+ @profiler
+ def update_qr(self):
+ uri = self.get_URI()
+ qr = self.screen.ids.qr
+ qr.set_data(uri)
+
+ def do_share(self):
+ uri = self.get_URI()
+ self.app.do_share(uri, _("Share Bitcoin Request"))
+
+ def do_copy(self):
+ uri = self.get_URI()
+ self.app._clipboard.copy(uri)
+ self.app.show_info(_('Request copied to clipboard'))
+
+ def save_request(self):
+ addr = self.screen.address
+ if not addr:
+ return False
+ amount = self.screen.amount
+ message = self.screen.message
+ amount = self.app.get_amount(amount) if amount else 0
+ req = self.app.wallet.make_payment_request(addr, amount, message, None)
+ try:
+ self.app.wallet.add_payment_request(req, self.app.electrum_config)
+ added_request = True
+ except Exception as e:
+ self.app.show_error(_('Error adding payment request') + ':\n' + str(e))
+ added_request = False
+ finally:
+ self.app.update_tab('requests')
+ return added_request
+
+ def on_amount_or_message(self):
+ Clock.schedule_once(lambda dt: self.update_qr())
+
+ def do_new(self):
+ addr = self.get_new_address()
+ if not addr:
+ self.app.show_info(_('Please use the existing requests first.'))
+
+ def do_save(self):
+ if self.save_request():
+ self.app.show_info(_('Request was saved.'))
+
+
+class TabbedCarousel(Factory.TabbedPanel):
+ '''Custom TabbedPanel using a carousel used in the Main Screen
+ '''
+
+ carousel = ObjectProperty(None)
+
+ def animate_tab_to_center(self, value):
+ scrlv = self._tab_strip.parent
+ if not scrlv:
+ return
+ idx = self.tab_list.index(value)
+ n = len(self.tab_list)
+ if idx in [0, 1]:
+ scroll_x = 1
+ elif idx in [n-1, n-2]:
+ scroll_x = 0
+ else:
+ scroll_x = 1. * (n - idx - 1) / (n - 1)
+ mation = Factory.Animation(scroll_x=scroll_x, d=.25)
+ mation.cancel_all(scrlv)
+ mation.start(scrlv)
+
+ def on_current_tab(self, instance, value):
+ self.animate_tab_to_center(value)
+
+ def on_index(self, instance, value):
+ current_slide = instance.current_slide
+ if not hasattr(current_slide, 'tab'):
+ return
+ tab = current_slide.tab
+ ct = self.current_tab
+ try:
+ if ct.text != tab.text:
+ carousel = self.carousel
+ carousel.slides[ct.slide].dispatch('on_leave')
+ self.switch_to(tab)
+ carousel.slides[tab.slide].dispatch('on_enter')
+ except AttributeError:
+ current_slide.dispatch('on_enter')
+
+ def switch_to(self, header):
+ # we have to replace the functionality of the original switch_to
+ if not header:
+ return
+ if not hasattr(header, 'slide'):
+ header.content = self.carousel
+ super(TabbedCarousel, self).switch_to(header)
+ try:
+ tab = self.tab_list[-1]
+ except IndexError:
+ return
+ self._current_tab = tab
+ tab.state = 'down'
+ return
+
+ carousel = self.carousel
+ self.current_tab.state = "normal"
+ header.state = 'down'
+ self._current_tab = header
+ # set the carousel to load the appropriate slide
+ # saved in the screen attribute of the tab head
+ slide = carousel.slides[header.slide]
+ if carousel.current_slide != slide:
+ carousel.current_slide.dispatch('on_leave')
+ carousel.load_slide(slide)
+ slide.dispatch('on_enter')
+
+ def add_widget(self, widget, index=0):
+ if isinstance(widget, Factory.CScreen):
+ self.carousel.add_widget(widget)
+ return
+ super(TabbedCarousel, self).add_widget(widget, index=index)
DIR diff --git a/gui/kivy/uix/ui_screens/about.kv b/electrum/gui/kivy/uix/ui_screens/about.kv
DIR diff --git a/electrum/gui/kivy/uix/ui_screens/history.kv b/electrum/gui/kivy/uix/ui_screens/history.kv
t@@ -0,0 +1,78 @@
+#:import _ electrum.gui.kivy.i18n._
+#:import Factory kivy.factory.Factory
+#:set font_light 'electrum/gui/kivy/data/fonts/Roboto-Condensed.ttf'
+#:set btc_symbol chr(171)
+#:set mbtc_symbol chr(187)
+
+
+
+<CardLabel@Label>
+ color: 0.95, 0.95, 0.95, 1
+ size_hint: 1, None
+ text: ''
+ text_size: self.width, None
+ height: self.texture_size[1]
+ halign: 'left'
+ valign: 'top'
+
+
+<HistoryItem@CardItem>
+ icon: 'atlas://electrum/gui/kivy/theming/light/important'
+ message: ''
+ is_mine: True
+ amount: '--'
+ action: _('Sent') if self.is_mine else _('Received')
+ amount_color: '#FF6657' if self.is_mine else '#2EA442'
+ confirmations: 0
+ date: ''
+ quote_text: ''
+ Image:
+ id: icon
+ source: root.icon
+ size_hint: None, 1
+ allow_stretch: True
+ width: self.height*1.5
+ mipmap: True
+ BoxLayout:
+ orientation: 'vertical'
+ Widget
+ CardLabel:
+ text:
+ u'[color={color}]{s}[/color]'.format(s='<<' if root.is_mine else '>>', color=root.amount_color)\
+ + ' ' + root.action + ' ' + (root.quote_text if app.is_fiat else root.amount)
+ font_size: '15sp'
+ CardLabel:
+ color: .699, .699, .699, 1
+ font_size: '14sp'
+ shorten: True
+ text: root.date + ' ' + root.message
+ Widget
+
+<HistoryRecycleView>:
+ viewclass: 'HistoryItem'
+ RecycleBoxLayout:
+ default_size: None, dp(56)
+ default_size_hint: 1, None
+ size_hint: 1, None
+ height: self.minimum_height
+ orientation: 'vertical'
+
+
+HistoryScreen:
+ name: 'history'
+ content: history_container
+ BoxLayout:
+ orientation: 'vertical'
+ Button:
+ background_color: 0, 0, 0, 0
+ text: app.fiat_balance if app.is_fiat else app.balance
+ markup: True
+ color: .9, .9, .9, 1
+ font_size: '30dp'
+ bold: True
+ size_hint: 1, 0.25
+ on_release: app.is_fiat = not app.is_fiat if app.fx.is_enabled() else False
+ HistoryRecycleView:
+ id: history_container
+ scroll_type: ['bars', 'content']
+ bar_width: '25dp'
DIR diff --git a/gui/kivy/uix/ui_screens/invoice.kv b/electrum/gui/kivy/uix/ui_screens/invoice.kv
DIR diff --git a/gui/kivy/uix/ui_screens/network.kv b/electrum/gui/kivy/uix/ui_screens/network.kv
DIR diff --git a/gui/kivy/uix/ui_screens/proxy.kv b/electrum/gui/kivy/uix/ui_screens/proxy.kv
DIR diff --git a/electrum/gui/kivy/uix/ui_screens/receive.kv b/electrum/gui/kivy/uix/ui_screens/receive.kv
t@@ -0,0 +1,142 @@
+#:import _ electrum.gui.kivy.i18n._
+#:import Decimal decimal.Decimal
+#:set btc_symbol chr(171)
+#:set mbtc_symbol chr(187)
+#:set font_light 'electrum/gui/kivy/data/fonts/Roboto-Condensed.ttf'
+
+
+
+ReceiveScreen:
+ id: s
+ name: 'receive'
+
+ address: ''
+ amount: ''
+ message: ''
+ status: ''
+
+ on_address:
+ self.parent.on_address(self.address)
+ on_amount:
+ self.parent.on_amount_or_message()
+ on_message:
+ self.parent.on_amount_or_message()
+
+ BoxLayout
+ padding: '12dp', '12dp', '12dp', '12dp'
+ spacing: '12dp'
+ orientation: 'vertical'
+ size_hint: 1, 1
+ FloatLayout:
+ id: bl
+ QRCodeWidget:
+ id: qr
+ size_hint: None, 1
+ width: min(self.height, bl.width)
+ pos_hint: {'center': (.5, .5)}
+ shaded: False
+ foreground_color: (0, 0, 0, 0.5) if self.shaded else (0, 0, 0, 0)
+ on_touch_down:
+ touch = args[1]
+ if self.collide_point(*touch.pos): self.shaded = not self.shaded
+ Label:
+ text: root.status
+ opacity: 1 if root.status else 0
+ pos_hint: {'center': (.5, .5)}
+ size_hint: None, 1
+ width: min(self.height, bl.width)
+ bcolor: 0.3, 0.3, 0.3, 0.9
+ canvas.before:
+ Color:
+ rgba: self.bcolor
+ Rectangle:
+ pos: self.pos
+ size: self.size
+
+ SendReceiveBlueBottom:
+ id: blue_bottom
+ size_hint: 1, None
+ height: self.minimum_height
+ BoxLayout:
+ size_hint: 1, None
+ height: blue_bottom.item_height
+ spacing: '5dp'
+ Image:
+ source: 'atlas://electrum/gui/kivy/theming/light/globe'
+ size_hint: None, None
+ size: '22dp', '22dp'
+ pos_hint: {'center_y': .5}
+ BlueButton:
+ id: address_label
+ text: s.address if s.address else _('Bitcoin Address')
+ shorten: True
+ on_release: Clock.schedule_once(lambda dt: app.addresses_dialog(s))
+ CardSeparator:
+ opacity: message_selection.opacity
+ color: blue_bottom.foreground_color
+ BoxLayout:
+ size_hint: 1, None
+ height: blue_bottom.item_height
+ spacing: '5dp'
+ Image:
+ source: 'atlas://electrum/gui/kivy/theming/light/calculator'
+ opacity: 0.7
+ size_hint: None, None
+ size: '22dp', '22dp'
+ pos_hint: {'center_y': .5}
+ BlueButton:
+ id: amount_label
+ default_text: _('Amount')
+ text: s.amount if s.amount else _('Amount')
+ on_release: Clock.schedule_once(lambda dt: app.amount_dialog(s, False))
+ CardSeparator:
+ opacity: message_selection.opacity
+ color: blue_bottom.foreground_color
+ BoxLayout:
+ id: message_selection
+ opacity: 1
+ size_hint: 1, None
+ height: blue_bottom.item_height
+ spacing: '5dp'
+ Image:
+ source: 'atlas://electrum/gui/kivy/theming/light/pen'
+ size_hint: None, None
+ size: '22dp', '22dp'
+ pos_hint: {'center_y': .5}
+ BlueButton:
+ id: description
+ text: s.message if s.message else _('Description')
+ on_release: Clock.schedule_once(lambda dt: app.description_dialog(s))
+ BoxLayout:
+ size_hint: 1, None
+ height: '48dp'
+ IconButton:
+ icon: 'atlas://electrum/gui/kivy/theming/light/save'
+ size_hint: 0.6, None
+ height: '48dp'
+ on_release: s.parent.do_save()
+ Button:
+ text: _('Requests')
+ size_hint: 1, None
+ height: '48dp'
+ on_release: Clock.schedule_once(lambda dt: app.requests_dialog(s))
+ Button:
+ text: _('Copy')
+ size_hint: 1, None
+ height: '48dp'
+ on_release: s.parent.do_copy()
+ IconButton:
+ icon: 'atlas://electrum/gui/kivy/theming/light/share'
+ size_hint: 0.6, None
+ height: '48dp'
+ on_release: s.parent.do_share()
+ BoxLayout:
+ size_hint: 1, None
+ height: '48dp'
+ Widget
+ size_hint: 2, 1
+ Button:
+ text: _('New')
+ size_hint: 1, None
+ height: '48dp'
+ on_release: Clock.schedule_once(lambda dt: s.parent.do_new())
DIR diff --git a/electrum/gui/kivy/uix/ui_screens/send.kv b/electrum/gui/kivy/uix/ui_screens/send.kv
t@@ -0,0 +1,127 @@
+#:import _ electrum.gui.kivy.i18n._
+#:import Decimal decimal.Decimal
+#:set btc_symbol chr(171)
+#:set mbtc_symbol chr(187)
+#:set font_light 'electrum/gui/kivy/data/fonts/Roboto-Condensed.ttf'
+
+
+SendScreen:
+ id: s
+ name: 'send'
+ address: ''
+ amount: ''
+ message: ''
+ is_pr: False
+ BoxLayout
+ padding: '12dp', '12dp', '12dp', '12dp'
+ spacing: '12dp'
+ orientation: 'vertical'
+ SendReceiveBlueBottom:
+ id: blue_bottom
+ size_hint: 1, None
+ height: self.minimum_height
+ BoxLayout:
+ size_hint: 1, None
+ height: blue_bottom.item_height
+ spacing: '5dp'
+ Image:
+ source: 'atlas://electrum/gui/kivy/theming/light/globe'
+ size_hint: None, None
+ size: '22dp', '22dp'
+ pos_hint: {'center_y': .5}
+ BlueButton:
+ id: payto_e
+ text: s.address if s.address else _('Recipient')
+ shorten: True
+ on_release: Clock.schedule_once(lambda dt: app.show_info(_('Copy and paste the recipient address using the Paste button, or use the camera to scan a QR code.')))
+ #on_release: Clock.schedule_once(lambda dt: app.popup_dialog('contacts'))
+ CardSeparator:
+ opacity: int(not root.is_pr)
+ color: blue_bottom.foreground_color
+ BoxLayout:
+ size_hint: 1, None
+ height: blue_bottom.item_height
+ spacing: '5dp'
+ Image:
+ source: 'atlas://electrum/gui/kivy/theming/light/calculator'
+ opacity: 0.7
+ size_hint: None, None
+ size: '22dp', '22dp'
+ pos_hint: {'center_y': .5}
+ BlueButton:
+ id: amount_e
+ default_text: _('Amount')
+ text: s.amount if s.amount else _('Amount')
+ disabled: root.is_pr
+ on_release: Clock.schedule_once(lambda dt: app.amount_dialog(s, True))
+ CardSeparator:
+ opacity: int(not root.is_pr)
+ color: blue_bottom.foreground_color
+ BoxLayout:
+ id: message_selection
+ size_hint: 1, None
+ height: blue_bottom.item_height
+ spacing: '5dp'
+ Image:
+ source: 'atlas://electrum/gui/kivy/theming/light/pen'
+ size_hint: None, None
+ size: '22dp', '22dp'
+ pos_hint: {'center_y': .5}
+ BlueButton:
+ id: description
+ text: s.message if s.message else (_('No Description') if root.is_pr else _('Description'))
+ disabled: root.is_pr
+ on_release: Clock.schedule_once(lambda dt: app.description_dialog(s))
+ CardSeparator:
+ opacity: int(not root.is_pr)
+ color: blue_bottom.foreground_color
+ BoxLayout:
+ size_hint: 1, None
+ height: blue_bottom.item_height
+ spacing: '5dp'
+ Image:
+ source: 'atlas://electrum/gui/kivy/theming/light/star_big_inactive'
+ opacity: 0.7
+ size_hint: None, None
+ size: '22dp', '22dp'
+ pos_hint: {'center_y': .5}
+ BlueButton:
+ id: fee_e
+ default_text: _('Fee')
+ text: app.fee_status
+ on_release: Clock.schedule_once(lambda dt: app.fee_dialog(s, True))
+ BoxLayout:
+ size_hint: 1, None
+ height: '48dp'
+ IconButton:
+ size_hint: 0.6, 1
+ on_release: s.parent.do_save()
+ icon: 'atlas://electrum/gui/kivy/theming/light/save'
+ Button:
+ text: _('Invoices')
+ size_hint: 1, 1
+ on_release: Clock.schedule_once(lambda dt: app.invoices_dialog(s))
+ Button:
+ text: _('Paste')
+ on_release: s.parent.do_paste()
+ IconButton:
+ id: qr
+ size_hint: 0.6, 1
+ on_release: Clock.schedule_once(lambda dt: app.scan_qr(on_complete=app.on_qr))
+ icon: 'atlas://electrum/gui/kivy/theming/light/camera'
+ BoxLayout:
+ size_hint: 1, None
+ height: '48dp'
+ Button:
+ text: _('Clear')
+ on_release: s.parent.do_clear()
+ Widget:
+ size_hint: 1, 1
+ Button:
+ text: _('Pay')
+ size_hint: 1, 1
+ on_release: s.parent.do_send()
+ Widget:
+ size_hint: 1, 1
+
+
DIR diff --git a/gui/kivy/uix/ui_screens/server.kv b/electrum/gui/kivy/uix/ui_screens/server.kv
DIR diff --git a/gui/kivy/uix/ui_screens/status.kv b/electrum/gui/kivy/uix/ui_screens/status.kv
DIR diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py
t@@ -0,0 +1,313 @@
+#!/usr/bin/env python
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2012 thomasv@gitorious
+#
+# 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 signal
+import sys
+import traceback
+
+
+try:
+ import PyQt5
+except Exception:
+ sys.exit("Error: Could not import PyQt5 on Linux systems, you may try 'sudo apt-get install python3-pyqt5'")
+
+from PyQt5.QtGui import *
+from PyQt5.QtWidgets import *
+from PyQt5.QtCore import *
+import PyQt5.QtCore as QtCore
+
+from electrum.i18n import _, set_language
+from electrum.plugin import run_hook
+from electrum.storage import WalletStorage
+from electrum.base_wizard import GoBack
+# from electrum.synchronizer import Synchronizer
+# from electrum.verifier import SPV
+# from electrum.util import DebugMem
+from electrum.util import (UserCancelled, print_error,
+ WalletFileException, BitcoinException)
+# from electrum.wallet import Abstract_Wallet
+
+from .installwizard import InstallWizard
+
+
+try:
+ from . import icons_rc
+except Exception as e:
+ print(e)
+ print("Error: Could not find icons file.")
+ print("Please run 'pyrcc5 icons.qrc -o electrum/gui/qt/icons_rc.py'")
+ sys.exit(1)
+
+from .util import * # * needed for plugins
+from .main_window import ElectrumWindow
+from .network_dialog import NetworkDialog
+
+
+class OpenFileEventFilter(QObject):
+ def __init__(self, windows):
+ self.windows = windows
+ super(OpenFileEventFilter, self).__init__()
+
+ def eventFilter(self, obj, event):
+ if event.type() == QtCore.QEvent.FileOpen:
+ if len(self.windows) >= 1:
+ self.windows[0].pay_to_URI(event.url().toEncoded())
+ return True
+ return False
+
+
+class QElectrumApplication(QApplication):
+ new_window_signal = pyqtSignal(str, object)
+
+
+class QNetworkUpdatedSignalObject(QObject):
+ network_updated_signal = pyqtSignal(str, object)
+
+
+class ElectrumGui:
+
+ def __init__(self, config, daemon, plugins):
+ set_language(config.get('language'))
+ # Uncomment this call to verify objects are being properly
+ # GC-ed when windows are closed
+ #network.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer,
+ # ElectrumWindow], interval=5)])
+ QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads)
+ if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"):
+ QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts)
+ if hasattr(QGuiApplication, 'setDesktopFileName'):
+ QGuiApplication.setDesktopFileName('electrum.desktop')
+ self.config = config
+ self.daemon = daemon
+ self.plugins = plugins
+ self.windows = []
+ self.efilter = OpenFileEventFilter(self.windows)
+ self.app = QElectrumApplication(sys.argv)
+ self.app.installEventFilter(self.efilter)
+ self.timer = Timer()
+ self.nd = None
+ self.network_updated_signal_obj = QNetworkUpdatedSignalObject()
+ # init tray
+ self.dark_icon = self.config.get("dark_icon", False)
+ self.tray = QSystemTrayIcon(self.tray_icon(), None)
+ self.tray.setToolTip('Electrum')
+ self.tray.activated.connect(self.tray_activated)
+ self.build_tray_menu()
+ self.tray.show()
+ self.app.new_window_signal.connect(self.start_new_window)
+ self.set_dark_theme_if_needed()
+ run_hook('init_qt', self)
+
+ def set_dark_theme_if_needed(self):
+ use_dark_theme = self.config.get('qt_gui_color_theme', 'default') == 'dark'
+ if use_dark_theme:
+ try:
+ import qdarkstyle
+ self.app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
+ except BaseException as e:
+ use_dark_theme = False
+ print_error('Error setting dark theme: {}'.format(e))
+ # Even if we ourselves don't set the dark theme,
+ # the OS/window manager/etc might set *a dark theme*.
+ # Hence, try to choose colors accordingly:
+ ColorScheme.update_from_widget(QWidget(), force_dark=use_dark_theme)
+
+ def build_tray_menu(self):
+ # Avoid immediate GC of old menu when window closed via its action
+ if self.tray.contextMenu() is None:
+ m = QMenu()
+ self.tray.setContextMenu(m)
+ else:
+ m = self.tray.contextMenu()
+ m.clear()
+ for window in self.windows:
+ submenu = m.addMenu(window.wallet.basename())
+ submenu.addAction(_("Show/Hide"), window.show_or_hide)
+ submenu.addAction(_("Close"), window.close)
+ m.addAction(_("Dark/Light"), self.toggle_tray_icon)
+ m.addSeparator()
+ m.addAction(_("Exit Electrum"), self.close)
+
+ def tray_icon(self):
+ if self.dark_icon:
+ return QIcon(':icons/electrum_dark_icon.png')
+ else:
+ return QIcon(':icons/electrum_light_icon.png')
+
+ def toggle_tray_icon(self):
+ self.dark_icon = not self.dark_icon
+ self.config.set_key("dark_icon", self.dark_icon, True)
+ self.tray.setIcon(self.tray_icon())
+
+ def tray_activated(self, reason):
+ if reason == QSystemTrayIcon.DoubleClick:
+ if all([w.is_hidden() for w in self.windows]):
+ for w in self.windows:
+ w.bring_to_top()
+ else:
+ for w in self.windows:
+ w.hide()
+
+ def close(self):
+ for window in self.windows:
+ window.close()
+
+ def new_window(self, path, uri=None):
+ # Use a signal as can be called from daemon thread
+ self.app.new_window_signal.emit(path, uri)
+
+ def show_network_dialog(self, parent):
+ if not self.daemon.network:
+ parent.show_warning(_('You are using Electrum in offline mode; restart Electrum if you want to get connected'), title=_('Offline'))
+ return
+ if self.nd:
+ self.nd.on_update()
+ self.nd.show()
+ self.nd.raise_()
+ return
+ self.nd = NetworkDialog(self.daemon.network, self.config,
+ self.network_updated_signal_obj)
+ self.nd.show()
+
+ def create_window_for_wallet(self, wallet):
+ w = ElectrumWindow(self, wallet)
+ self.windows.append(w)
+ self.build_tray_menu()
+ # FIXME: Remove in favour of the load_wallet hook
+ run_hook('on_new_window', w)
+ return w
+
+ def start_new_window(self, path, uri, app_is_starting=False):
+ '''Raises the window for the wallet if it is open. Otherwise
+ opens the wallet and creates a new window for it'''
+ try:
+ wallet = self.daemon.load_wallet(path, None)
+ except BaseException as e:
+ traceback.print_exc(file=sys.stdout)
+ d = QMessageBox(QMessageBox.Warning, _('Error'),
+ _('Cannot load wallet') + ' (1):\n' + str(e))
+ d.exec_()
+ if app_is_starting:
+ # do not return so that the wizard can appear
+ wallet = None
+ else:
+ return
+ if not wallet:
+ storage = WalletStorage(path, manual_upgrades=True)
+ wizard = InstallWizard(self.config, self.app, self.plugins, storage)
+ try:
+ wallet = wizard.run_and_get_wallet(self.daemon.get_wallet)
+ except UserCancelled:
+ pass
+ except GoBack as e:
+ print_error('[start_new_window] Exception caught (GoBack)', e)
+ except (WalletFileException, BitcoinException) as e:
+ traceback.print_exc(file=sys.stderr)
+ d = QMessageBox(QMessageBox.Warning, _('Error'),
+ _('Cannot load wallet') + ' (2):\n' + str(e))
+ d.exec_()
+ return
+ finally:
+ wizard.terminate()
+ if not wallet:
+ return
+
+ if not self.daemon.get_wallet(wallet.storage.path):
+ # wallet was not in memory
+ wallet.start_threads(self.daemon.network)
+ self.daemon.add_wallet(wallet)
+ try:
+ for w in self.windows:
+ if w.wallet.storage.path == wallet.storage.path:
+ w.bring_to_top()
+ return
+ w = self.create_window_for_wallet(wallet)
+ except BaseException as e:
+ traceback.print_exc(file=sys.stdout)
+ d = QMessageBox(QMessageBox.Warning, _('Error'),
+ _('Cannot create window for wallet') + ':\n' + str(e))
+ d.exec_()
+ return
+ if uri:
+ w.pay_to_URI(uri)
+ w.bring_to_top()
+ w.setWindowState(w.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
+
+ # this will activate the window
+ w.activateWindow()
+ return w
+
+ def close_window(self, window):
+ self.windows.remove(window)
+ self.build_tray_menu()
+ # save wallet path of last open window
+ if not self.windows:
+ self.config.save_last_wallet(window.wallet)
+ run_hook('on_close_window', window)
+
+ def init_network(self):
+ # Show network dialog if config does not exist
+ if self.daemon.network:
+ if self.config.get('auto_connect') is None:
+ wizard = InstallWizard(self.config, self.app, self.plugins, None)
+ wizard.init_network(self.daemon.network)
+ wizard.terminate()
+
+ def main(self):
+ try:
+ self.init_network()
+ except UserCancelled:
+ return
+ except GoBack:
+ return
+ except BaseException as e:
+ traceback.print_exc(file=sys.stdout)
+ return
+ self.timer.start()
+ self.config.open_last_wallet()
+ path = self.config.get_wallet_path()
+ if not self.start_new_window(path, self.config.get('url'), app_is_starting=True):
+ return
+ signal.signal(signal.SIGINT, lambda *args: self.app.quit())
+
+ def quit_after_last_window():
+ # on some platforms, not only does exec_ not return but not even
+ # aboutToQuit is emitted (but following this, it should be emitted)
+ if self.app.quitOnLastWindowClosed():
+ self.app.quit()
+ self.app.lastWindowClosed.connect(quit_after_last_window)
+
+ def clean_up():
+ # Shut down the timer cleanly
+ self.timer.stop()
+ # clipboard persistence. see http://www.mail-archive.com/pyqt@riverbankcomputing.com/msg17328.html
+ event = QtCore.QEvent(QtCore.QEvent.Clipboard)
+ self.app.sendEvent(self.app.clipboard(), event)
+ self.tray.hide()
+ self.app.aboutToQuit.connect(clean_up)
+
+ # main loop
+ self.app.exec_()
+ # on some platforms the exec_ call may not return, so use clean_up()
DIR diff --git a/gui/qt/address_dialog.py b/electrum/gui/qt/address_dialog.py
DIR diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py
t@@ -0,0 +1,195 @@
+#!/usr/bin/env python
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2015 Thomas Voegtlin
+#
+# 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 webbrowser
+
+from electrum.i18n import _
+from electrum.util import block_explorer_URL
+from electrum.plugin import run_hook
+from electrum.bitcoin import is_address
+
+from .util import *
+
+
+class AddressList(MyTreeWidget):
+ filter_columns = [0, 1, 2, 3] # Type, Address, Label, Balance
+
+ def __init__(self, parent=None):
+ MyTreeWidget.__init__(self, parent, self.create_menu, [], 2)
+ self.refresh_headers()
+ self.setSelectionMode(QAbstractItemView.ExtendedSelection)
+ self.setSortingEnabled(True)
+ self.show_change = 0
+ self.show_used = 0
+ self.change_button = QComboBox(self)
+ self.change_button.currentIndexChanged.connect(self.toggle_change)
+ for t in [_('All'), _('Receiving'), _('Change')]:
+ self.change_button.addItem(t)
+ self.used_button = QComboBox(self)
+ self.used_button.currentIndexChanged.connect(self.toggle_used)
+ for t in [_('All'), _('Unused'), _('Funded'), _('Used')]:
+ self.used_button.addItem(t)
+
+ def get_toolbar_buttons(self):
+ return QLabel(_("Filter:")), self.change_button, self.used_button
+
+ def on_hide_toolbar(self):
+ self.show_change = 0
+ self.show_used = 0
+ self.update()
+
+ def save_toolbar_state(self, state, config):
+ config.set_key('show_toolbar_addresses', state)
+
+ def refresh_headers(self):
+ headers = [_('Type'), _('Address'), _('Label'), _('Balance')]
+ fx = self.parent.fx
+ if fx and fx.get_fiat_address_config():
+ headers.extend([_(fx.get_currency()+' Balance')])
+ headers.extend([_('Tx')])
+ self.update_headers(headers)
+
+ def toggle_change(self, state):
+ if state == self.show_change:
+ return
+ self.show_change = state
+ self.update()
+
+ def toggle_used(self, state):
+ if state == self.show_used:
+ return
+ self.show_used = state
+ self.update()
+
+ def on_update(self):
+ self.wallet = self.parent.wallet
+ item = self.currentItem()
+ current_address = item.data(0, Qt.UserRole) if item else None
+ if self.show_change == 1:
+ addr_list = self.wallet.get_receiving_addresses()
+ elif self.show_change == 2:
+ addr_list = self.wallet.get_change_addresses()
+ else:
+ addr_list = self.wallet.get_addresses()
+ self.clear()
+ for address in addr_list:
+ num = len(self.wallet.get_address_history(address))
+ is_used = self.wallet.is_used(address)
+ label = self.wallet.labels.get(address, '')
+ c, u, x = self.wallet.get_addr_balance(address)
+ balance = c + u + x
+ if self.show_used == 1 and (balance or is_used):
+ continue
+ if self.show_used == 2 and balance == 0:
+ continue
+ if self.show_used == 3 and not is_used:
+ continue
+ balance_text = self.parent.format_amount(balance, whitespaces=True)
+ fx = self.parent.fx
+ # create item
+ if fx and fx.get_fiat_address_config():
+ rate = fx.exchange_rate()
+ fiat_balance = fx.value_str(balance, rate)
+ address_item = SortableTreeWidgetItem(['', address, label, balance_text, fiat_balance, "%d"%num])
+ else:
+ address_item = SortableTreeWidgetItem(['', address, label, balance_text, "%d"%num])
+ # align text and set fonts
+ for i in range(address_item.columnCount()):
+ address_item.setTextAlignment(i, Qt.AlignVCenter)
+ if i not in (0, 2):
+ address_item.setFont(i, QFont(MONOSPACE_FONT))
+ if fx and fx.get_fiat_address_config():
+ address_item.setTextAlignment(4, Qt.AlignRight | Qt.AlignVCenter)
+ # setup column 0
+ if self.wallet.is_change(address):
+ address_item.setText(0, _('change'))
+ address_item.setBackground(0, ColorScheme.YELLOW.as_color(True))
+ else:
+ address_item.setText(0, _('receiving'))
+ address_item.setBackground(0, ColorScheme.GREEN.as_color(True))
+ address_item.setData(0, Qt.UserRole, address) # column 0; independent from address column
+ # setup column 1
+ if self.wallet.is_frozen(address):
+ address_item.setBackground(1, ColorScheme.BLUE.as_color(True))
+ if self.wallet.is_beyond_limit(address):
+ address_item.setBackground(1, ColorScheme.RED.as_color(True))
+ # add item
+ self.addChild(address_item)
+ if address == current_address:
+ self.setCurrentItem(address_item)
+
+ def create_menu(self, position):
+ from electrum.wallet import Multisig_Wallet
+ is_multisig = isinstance(self.wallet, Multisig_Wallet)
+ can_delete = self.wallet.can_delete_address()
+ selected = self.selectedItems()
+ multi_select = len(selected) > 1
+ addrs = [item.text(1) for item in selected]
+ if not addrs:
+ return
+ if not multi_select:
+ item = self.itemAt(position)
+ col = self.currentColumn()
+ if not item:
+ return
+ addr = addrs[0]
+ if not is_address(addr):
+ item.setExpanded(not item.isExpanded())
+ return
+
+ menu = QMenu()
+ if not multi_select:
+ column_title = self.headerItem().text(col)
+ copy_text = item.text(col)
+ menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(copy_text))
+ menu.addAction(_('Details'), lambda: self.parent.show_address(addr))
+ if col in self.editable_columns:
+ menu.addAction(_("Edit {}").format(column_title), lambda: self.editItem(item, col))
+ menu.addAction(_("Request payment"), lambda: self.parent.receive_at(addr))
+ if self.wallet.can_export():
+ menu.addAction(_("Private key"), lambda: self.parent.show_private_key(addr))
+ if not is_multisig and not self.wallet.is_watching_only():
+ menu.addAction(_("Sign/verify message"), lambda: self.parent.sign_verify_message(addr))
+ menu.addAction(_("Encrypt/decrypt message"), lambda: self.parent.encrypt_message(addr))
+ if can_delete:
+ menu.addAction(_("Remove from wallet"), lambda: self.parent.remove_address(addr))
+ addr_URL = block_explorer_URL(self.config, 'addr', addr)
+ if addr_URL:
+ menu.addAction(_("View on block explorer"), lambda: webbrowser.open(addr_URL))
+
+ if not self.wallet.is_frozen(addr):
+ menu.addAction(_("Freeze"), lambda: self.parent.set_frozen_state([addr], True))
+ else:
+ menu.addAction(_("Unfreeze"), lambda: self.parent.set_frozen_state([addr], False))
+
+ coins = self.wallet.get_utxos(addrs)
+ if coins:
+ menu.addAction(_("Spend from"), lambda: self.parent.spend_coins(coins))
+
+ run_hook('receive_menu', menu, addrs, self.wallet)
+ menu.exec_(self.viewport().mapToGlobal(position))
+
+ def on_permit_edit(self, item, column):
+ # labels for headings, e.g. "receiving" or "used" should not be editable
+ return item.childCount() == 0
DIR diff --git a/gui/qt/amountedit.py b/electrum/gui/qt/amountedit.py
DIR diff --git a/electrum/gui/qt/completion_text_edit.py b/electrum/gui/qt/completion_text_edit.py
t@@ -0,0 +1,120 @@
+#!/usr/bin/env python
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2018 The Electrum developers
+#
+# 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.
+
+from PyQt5.QtGui import *
+from PyQt5.QtCore import *
+from PyQt5.QtWidgets import *
+from .util import ButtonsTextEdit
+
+class CompletionTextEdit(ButtonsTextEdit):
+
+ def __init__(self, parent=None):
+ super(CompletionTextEdit, self).__init__(parent)
+ self.completer = None
+ self.moveCursor(QTextCursor.End)
+ self.disable_suggestions()
+
+ def set_completer(self, completer):
+ self.completer = completer
+ self.initialize_completer()
+
+ def initialize_completer(self):
+ self.completer.setWidget(self)
+ self.completer.setCompletionMode(QCompleter.PopupCompletion)
+ self.completer.activated.connect(self.insert_completion)
+ self.enable_suggestions()
+
+ def insert_completion(self, completion):
+ if self.completer.widget() != self:
+ return
+ text_cursor = self.textCursor()
+ extra = len(completion) - len(self.completer.completionPrefix())
+ text_cursor.movePosition(QTextCursor.Left)
+ text_cursor.movePosition(QTextCursor.EndOfWord)
+ if extra == 0:
+ text_cursor.insertText(" ")
+ else:
+ text_cursor.insertText(completion[-extra:] + " ")
+ self.setTextCursor(text_cursor)
+
+ def text_under_cursor(self):
+ tc = self.textCursor()
+ tc.select(QTextCursor.WordUnderCursor)
+ return tc.selectedText()
+
+ def enable_suggestions(self):
+ self.suggestions_enabled = True
+
+ def disable_suggestions(self):
+ self.suggestions_enabled = False
+
+ def keyPressEvent(self, e):
+ if self.isReadOnly():
+ return
+
+ if self.is_special_key(e):
+ e.ignore()
+ return
+
+ QPlainTextEdit.keyPressEvent(self, e)
+
+ ctrlOrShift = e.modifiers() and (Qt.ControlModifier or Qt.ShiftModifier)
+ if self.completer is None or (ctrlOrShift and not e.text()):
+ return
+
+ if not self.suggestions_enabled:
+ return
+
+ eow = "~!@#$%^&*()_+{}|:\"<>?,./;'[]\\-="
+ hasModifier = (e.modifiers() != Qt.NoModifier) and not ctrlOrShift
+ completionPrefix = self.text_under_cursor()
+
+ if hasModifier or not e.text() or len(completionPrefix) < 1 or eow.find(e.text()[-1]) >= 0:
+ self.completer.popup().hide()
+ return
+
+ if completionPrefix != self.completer.completionPrefix():
+ self.completer.setCompletionPrefix(completionPrefix)
+ self.completer.popup().setCurrentIndex(self.completer.completionModel().index(0, 0))
+
+ cr = self.cursorRect()
+ cr.setWidth(self.completer.popup().sizeHintForColumn(0) + self.completer.popup().verticalScrollBar().sizeHint().width())
+ self.completer.complete(cr)
+
+ def is_special_key(self, e):
+ if self.completer != None and self.completer.popup().isVisible():
+ if e.key() in [Qt.Key_Enter, Qt.Key_Return]:
+ return True
+ if e.key() in [Qt.Key_Tab, Qt.Key_Down, Qt.Key_Up]:
+ return True
+ return False
+
+if __name__ == "__main__":
+ app = QApplication([])
+ completer = QCompleter(["alabama", "arkansas", "avocado", "breakfast", "sausage"])
+ te = CompletionTextEdit()
+ te.set_completer(completer)
+ te.show()
+ app.exec_()
DIR diff --git a/gui/qt/console.py b/electrum/gui/qt/console.py
DIR diff --git a/electrum/gui/qt/contact_list.py b/electrum/gui/qt/contact_list.py
t@@ -0,0 +1,98 @@
+#!/usr/bin/env python
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2015 Thomas Voegtlin
+#
+# 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 webbrowser
+
+from electrum.i18n import _
+from electrum.bitcoin import is_address
+from electrum.util import block_explorer_URL
+from electrum.plugin import run_hook
+from PyQt5.QtGui import *
+from PyQt5.QtCore import *
+from PyQt5.QtWidgets import (
+ QAbstractItemView, QFileDialog, QMenu, QTreeWidgetItem)
+from .util import MyTreeWidget, import_meta_gui, export_meta_gui
+
+
+class ContactList(MyTreeWidget):
+ filter_columns = [0, 1] # Key, Value
+
+ def __init__(self, parent):
+ MyTreeWidget.__init__(self, parent, self.create_menu, [_('Name'), _('Address')], 0, [0])
+ self.setSelectionMode(QAbstractItemView.ExtendedSelection)
+ self.setSortingEnabled(True)
+
+ def on_permit_edit(self, item, column):
+ # openalias items shouldn't be editable
+ return item.text(1) != "openalias"
+
+ def on_edited(self, item, column, prior):
+ if column == 0: # Remove old contact if renamed
+ self.parent.contacts.pop(prior)
+ self.parent.set_contact(item.text(0), item.text(1))
+
+ def import_contacts(self):
+ import_meta_gui(self.parent, _('contacts'), self.parent.contacts.import_file, self.on_update)
+
+ def export_contacts(self):
+ export_meta_gui(self.parent, _('contacts'), self.parent.contacts.export_file)
+
+ def create_menu(self, position):
+ menu = QMenu()
+ selected = self.selectedItems()
+ if not selected:
+ menu.addAction(_("New contact"), lambda: self.parent.new_contact_dialog())
+ menu.addAction(_("Import file"), lambda: self.import_contacts())
+ menu.addAction(_("Export file"), lambda: self.export_contacts())
+ else:
+ names = [item.text(0) for item in selected]
+ keys = [item.text(1) for item in selected]
+ column = self.currentColumn()
+ column_title = self.headerItem().text(column)
+ column_data = '\n'.join([item.text(column) for item in selected])
+ menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
+ if column in self.editable_columns:
+ item = self.currentItem()
+ menu.addAction(_("Edit {}").format(column_title), lambda: self.editItem(item, column))
+ menu.addAction(_("Pay to"), lambda: self.parent.payto_contacts(keys))
+ menu.addAction(_("Delete"), lambda: self.parent.delete_contacts(keys))
+ URLs = [block_explorer_URL(self.config, 'addr', key) for key in filter(is_address, keys)]
+ if URLs:
+ menu.addAction(_("View on block explorer"), lambda: map(webbrowser.open, URLs))
+
+ run_hook('create_contact_menu', menu, selected)
+ menu.exec_(self.viewport().mapToGlobal(position))
+
+ def on_update(self):
+ item = self.currentItem()
+ current_key = item.data(0, Qt.UserRole) if item else None
+ self.clear()
+ for key in sorted(self.parent.contacts.keys()):
+ _type, name = self.parent.contacts[key]
+ item = QTreeWidgetItem([name, key])
+ item.setData(0, Qt.UserRole, key)
+ self.addTopLevelItem(item)
+ if key == current_key:
+ self.setCurrentItem(item)
+ run_hook('update_contacts_tab', self)
DIR diff --git a/gui/qt/exception_window.py b/electrum/gui/qt/exception_window.py
DIR diff --git a/gui/qt/fee_slider.py b/electrum/gui/qt/fee_slider.py
DIR diff --git a/gui/qt/history_list.py b/electrum/gui/qt/history_list.py
DIR diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py
t@@ -0,0 +1,644 @@
+
+import os
+import sys
+import threading
+import traceback
+
+from PyQt5.QtCore import *
+from PyQt5.QtGui import *
+from PyQt5.QtWidgets import *
+
+from electrum.wallet import Wallet
+from electrum.storage import WalletStorage
+from electrum.util import UserCancelled, InvalidPassword
+from electrum.base_wizard import BaseWizard, HWD_SETUP_DECRYPT_WALLET, GoBack
+from electrum.i18n import _
+
+from .seed_dialog import SeedLayout, KeysLayout
+from .network_dialog import NetworkChoiceLayout
+from .util import *
+from .password_dialog import PasswordLayout, PasswordLayoutForHW, PW_NEW
+
+
+MSG_ENTER_PASSWORD = _("Choose a password to encrypt your wallet keys.") + '\n'\
+ + _("Leave this field empty if you want to disable encryption.")
+MSG_HW_STORAGE_ENCRYPTION = _("Set wallet file encryption.") + '\n'\
+ + _("Your wallet file does not contain secrets, mostly just metadata. ") \
+ + _("It also contains your master public key that allows watching your addresses.") + '\n\n'\
+ + _("Note: If you enable this setting, you will need your hardware device to open your wallet.")
+WIF_HELP_TEXT = (_('WIF keys are typed in Electrum, based on script type.') + '\n\n' +
+ _('A few examples') + ':\n' +
+ 'p2pkh:KxZcY47uGp9a... \t-> 1DckmggQM...\n' +
+ 'p2wpkh-p2sh:KxZcY47uGp9a... \t-> 3NhNeZQXF...\n' +
+ 'p2wpkh:KxZcY47uGp9a... \t-> bc1q3fjfk...')
+# note: full key is KxZcY47uGp9aVQAb6VVvuBs8SwHKgkSR2DbZUzjDzXf2N2GPhG9n
+
+
+class CosignWidget(QWidget):
+ size = 120
+
+ def __init__(self, m, n):
+ QWidget.__init__(self)
+ self.R = QRect(0, 0, self.size, self.size)
+ self.setGeometry(self.R)
+ self.setMinimumHeight(self.size)
+ self.setMaximumHeight(self.size)
+ self.m = m
+ self.n = n
+
+ def set_n(self, n):
+ self.n = n
+ self.update()
+
+ def set_m(self, m):
+ self.m = m
+ self.update()
+
+ def paintEvent(self, event):
+ bgcolor = self.palette().color(QPalette.Background)
+ pen = QPen(bgcolor, 7, Qt.SolidLine)
+ qp = QPainter()
+ qp.begin(self)
+ qp.setPen(pen)
+ qp.setRenderHint(QPainter.Antialiasing)
+ qp.setBrush(Qt.gray)
+ for i in range(self.n):
+ alpha = int(16* 360 * i/self.n)
+ alpha2 = int(16* 360 * 1/self.n)
+ qp.setBrush(Qt.green if i<self.m else Qt.gray)
+ qp.drawPie(self.R, alpha, alpha2)
+ qp.end()
+
+
+
+def wizard_dialog(func):
+ def func_wrapper(*args, **kwargs):
+ run_next = kwargs['run_next']
+ wizard = args[0]
+ wizard.back_button.setText(_('Back') if wizard.can_go_back() else _('Cancel'))
+ try:
+ out = func(*args, **kwargs)
+ except GoBack:
+ wizard.go_back() if wizard.can_go_back() else wizard.close()
+ return
+ except UserCancelled:
+ return
+ #if out is None:
+ # out = ()
+ if type(out) is not tuple:
+ out = (out,)
+ run_next(*out)
+ return func_wrapper
+
+
+
+# WindowModalDialog must come first as it overrides show_error
+class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
+
+ accept_signal = pyqtSignal()
+ synchronized_signal = pyqtSignal(str)
+
+ def __init__(self, config, app, plugins, storage):
+ BaseWizard.__init__(self, config, plugins, storage)
+ QDialog.__init__(self, None)
+ self.setWindowTitle('Electrum - ' + _('Install Wizard'))
+ self.app = app
+ self.config = config
+ # Set for base base class
+ self.language_for_seed = config.get('language')
+ self.setMinimumSize(600, 400)
+ self.accept_signal.connect(self.accept)
+ self.title = QLabel()
+ self.main_widget = QWidget()
+ self.back_button = QPushButton(_("Back"), self)
+ self.back_button.setText(_('Back') if self.can_go_back() else _('Cancel'))
+ self.next_button = QPushButton(_("Next"), self)
+ self.next_button.setDefault(True)
+ self.logo = QLabel()
+ self.please_wait = QLabel(_("Please wait..."))
+ self.please_wait.setAlignment(Qt.AlignCenter)
+ self.icon_filename = None
+ self.loop = QEventLoop()
+ self.rejected.connect(lambda: self.loop.exit(0))
+ self.back_button.clicked.connect(lambda: self.loop.exit(1))
+ self.next_button.clicked.connect(lambda: self.loop.exit(2))
+ outer_vbox = QVBoxLayout(self)
+ inner_vbox = QVBoxLayout()
+ inner_vbox.addWidget(self.title)
+ inner_vbox.addWidget(self.main_widget)
+ inner_vbox.addStretch(1)
+ inner_vbox.addWidget(self.please_wait)
+ inner_vbox.addStretch(1)
+ scroll_widget = QWidget()
+ scroll_widget.setLayout(inner_vbox)
+ scroll = QScrollArea()
+ scroll.setWidget(scroll_widget)
+ scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+ scroll.setWidgetResizable(True)
+ icon_vbox = QVBoxLayout()
+ icon_vbox.addWidget(self.logo)
+ icon_vbox.addStretch(1)
+ hbox = QHBoxLayout()
+ hbox.addLayout(icon_vbox)
+ hbox.addSpacing(5)
+ hbox.addWidget(scroll)
+ hbox.setStretchFactor(scroll, 1)
+ outer_vbox.addLayout(hbox)
+ outer_vbox.addLayout(Buttons(self.back_button, self.next_button))
+ self.set_icon(':icons/electrum.png')
+ self.show()
+ self.raise_()
+ self.refresh_gui() # Need for QT on MacOSX. Lame.
+
+ def run_and_get_wallet(self, get_wallet_from_daemon):
+
+ vbox = QVBoxLayout()
+ hbox = QHBoxLayout()
+ hbox.addWidget(QLabel(_('Wallet') + ':'))
+ self.name_e = QLineEdit()
+ hbox.addWidget(self.name_e)
+ button = QPushButton(_('Choose...'))
+ hbox.addWidget(button)
+ vbox.addLayout(hbox)
+
+ self.msg_label = QLabel('')
+ vbox.addWidget(self.msg_label)
+ hbox2 = QHBoxLayout()
+ self.pw_e = QLineEdit('', self)
+ self.pw_e.setFixedWidth(150)
+ self.pw_e.setEchoMode(2)
+ self.pw_label = QLabel(_('Password') + ':')
+ hbox2.addWidget(self.pw_label)
+ hbox2.addWidget(self.pw_e)
+ hbox2.addStretch()
+ vbox.addLayout(hbox2)
+ self.set_layout(vbox, title=_('Electrum wallet'))
+
+ wallet_folder = os.path.dirname(self.storage.path)
+
+ def on_choose():
+ path, __ = QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder)
+ if path:
+ self.name_e.setText(path)
+
+ def on_filename(filename):
+ path = os.path.join(wallet_folder, filename)
+ wallet_from_memory = get_wallet_from_daemon(path)
+ try:
+ if wallet_from_memory:
+ self.storage = wallet_from_memory.storage
+ else:
+ self.storage = WalletStorage(path, manual_upgrades=True)
+ self.next_button.setEnabled(True)
+ except BaseException:
+ traceback.print_exc(file=sys.stderr)
+ self.storage = None
+ self.next_button.setEnabled(False)
+ if self.storage:
+ if not self.storage.file_exists():
+ msg =_("This file does not exist.") + '\n' \
+ + _("Press 'Next' to create this wallet, or choose another file.")
+ pw = False
+ elif not wallet_from_memory:
+ if self.storage.is_encrypted_with_user_pw():
+ msg = _("This file is encrypted with a password.") + '\n' \
+ + _('Enter your password or choose another file.')
+ pw = True
+ elif self.storage.is_encrypted_with_hw_device():
+ msg = _("This file is encrypted using a hardware device.") + '\n' \
+ + _("Press 'Next' to choose device to decrypt.")
+ pw = False
+ else:
+ msg = _("Press 'Next' to open this wallet.")
+ pw = False
+ else:
+ msg = _("This file is already open in memory.") + "\n" \
+ + _("Press 'Next' to create/focus window.")
+ pw = False
+ else:
+ msg = _('Cannot read file')
+ pw = False
+ self.msg_label.setText(msg)
+ if pw:
+ self.pw_label.show()
+ self.pw_e.show()
+ self.pw_e.setFocus()
+ else:
+ self.pw_label.hide()
+ self.pw_e.hide()
+
+ button.clicked.connect(on_choose)
+ self.name_e.textChanged.connect(on_filename)
+ n = os.path.basename(self.storage.path)
+ self.name_e.setText(n)
+
+ while True:
+ if self.loop.exec_() != 2: # 2 = next
+ return
+ if self.storage.file_exists() and not self.storage.is_encrypted():
+ break
+ if not self.storage.file_exists():
+ break
+ wallet_from_memory = get_wallet_from_daemon(self.storage.path)
+ if wallet_from_memory:
+ return wallet_from_memory
+ if self.storage.file_exists() and self.storage.is_encrypted():
+ if self.storage.is_encrypted_with_user_pw():
+ password = self.pw_e.text()
+ try:
+ self.storage.decrypt(password)
+ break
+ except InvalidPassword as e:
+ QMessageBox.information(None, _('Error'), str(e))
+ continue
+ except BaseException as e:
+ traceback.print_exc(file=sys.stdout)
+ QMessageBox.information(None, _('Error'), str(e))
+ return
+ elif self.storage.is_encrypted_with_hw_device():
+ try:
+ self.run('choose_hw_device', HWD_SETUP_DECRYPT_WALLET)
+ except InvalidPassword as e:
+ QMessageBox.information(
+ None, _('Error'),
+ _('Failed to decrypt using this hardware device.') + '\n' +
+ _('If you use a passphrase, make sure it is correct.'))
+ self.stack = []
+ return self.run_and_get_wallet(get_wallet_from_daemon)
+ except BaseException as e:
+ traceback.print_exc(file=sys.stdout)
+ QMessageBox.information(None, _('Error'), str(e))
+ return
+ if self.storage.is_past_initial_decryption():
+ break
+ else:
+ return
+ else:
+ raise Exception('Unexpected encryption version')
+
+ path = self.storage.path
+ if self.storage.requires_split():
+ self.hide()
+ msg = _("The wallet '{}' contains multiple accounts, which are no longer supported since Electrum 2.7.\n\n"
+ "Do you want to split your wallet into multiple files?").format(path)
+ if not self.question(msg):
+ return
+ file_list = '\n'.join(self.storage.split_accounts())
+ msg = _('Your accounts have been moved to') + ':\n' + file_list + '\n\n'+ _('Do you want to delete the old file') + ':\n' + path
+ if self.question(msg):
+ os.remove(path)
+ self.show_warning(_('The file was removed'))
+ return
+
+ action = self.storage.get_action()
+ if action and action not in ('new', 'upgrade_storage'):
+ self.hide()
+ msg = _("The file '{}' contains an incompletely created wallet.\n"
+ "Do you want to complete its creation now?").format(path)
+ if not self.question(msg):
+ if self.question(_("Do you want to delete '{}'?").format(path)):
+ os.remove(path)
+ self.show_warning(_('The file was removed'))
+ return
+ self.show()
+ if action:
+ # self.wallet is set in run
+ self.run(action)
+ return self.wallet
+
+ self.wallet = Wallet(self.storage)
+ return self.wallet
+
+ def finished(self):
+ """Called in hardware client wrapper, in order to close popups."""
+ return
+
+ def on_error(self, exc_info):
+ if not isinstance(exc_info[1], UserCancelled):
+ traceback.print_exception(*exc_info)
+ self.show_error(str(exc_info[1]))
+
+ def set_icon(self, filename):
+ prior_filename, self.icon_filename = self.icon_filename, filename
+ self.logo.setPixmap(QPixmap(filename).scaledToWidth(60, mode=Qt.SmoothTransformation))
+ return prior_filename
+
+ def set_layout(self, layout, title=None, next_enabled=True):
+ self.title.setText("<b>%s</b>"%title if title else "")
+ self.title.setVisible(bool(title))
+ # Get rid of any prior layout by assigning it to a temporary widget
+ prior_layout = self.main_widget.layout()
+ if prior_layout:
+ QWidget().setLayout(prior_layout)
+ self.main_widget.setLayout(layout)
+ self.back_button.setEnabled(True)
+ self.next_button.setEnabled(next_enabled)
+ if next_enabled:
+ self.next_button.setFocus()
+ self.main_widget.setVisible(True)
+ self.please_wait.setVisible(False)
+
+ def exec_layout(self, layout, title=None, raise_on_cancel=True,
+ next_enabled=True):
+ self.set_layout(layout, title, next_enabled)
+ result = self.loop.exec_()
+ if not result and raise_on_cancel:
+ raise UserCancelled
+ if result == 1:
+ raise GoBack from None
+ self.title.setVisible(False)
+ self.back_button.setEnabled(False)
+ self.next_button.setEnabled(False)
+ self.main_widget.setVisible(False)
+ self.please_wait.setVisible(True)
+ self.refresh_gui()
+ return result
+
+ def refresh_gui(self):
+ # For some reason, to refresh the GUI this needs to be called twice
+ self.app.processEvents()
+ self.app.processEvents()
+
+ def remove_from_recently_open(self, filename):
+ self.config.remove_from_recently_open(filename)
+
+ def text_input(self, title, message, is_valid, allow_multi=False):
+ slayout = KeysLayout(parent=self, header_layout=message, is_valid=is_valid,
+ allow_multi=allow_multi)
+ self.exec_layout(slayout, title, next_enabled=False)
+ return slayout.get_text()
+
+ def seed_input(self, title, message, is_seed, options):
+ slayout = SeedLayout(title=message, is_seed=is_seed, options=options, parent=self)
+ self.exec_layout(slayout, title, next_enabled=False)
+ return slayout.get_seed(), slayout.is_bip39, slayout.is_ext
+
+ @wizard_dialog
+ def add_xpub_dialog(self, title, message, is_valid, run_next, allow_multi=False, show_wif_help=False):
+ header_layout = QHBoxLayout()
+ label = WWLabel(message)
+ label.setMinimumWidth(400)
+ header_layout.addWidget(label)
+ if show_wif_help:
+ header_layout.addWidget(InfoButton(WIF_HELP_TEXT), alignment=Qt.AlignRight)
+ return self.text_input(title, header_layout, is_valid, allow_multi)
+
+ @wizard_dialog
+ def add_cosigner_dialog(self, run_next, index, is_valid):
+ title = _("Add Cosigner") + " %d"%index
+ message = ' '.join([
+ _('Please enter the master public key (xpub) of your cosigner.'),
+ _('Enter their master private key (xprv) if you want to be able to sign for them.')
+ ])
+ return self.text_input(title, message, is_valid)
+
+ @wizard_dialog
+ def restore_seed_dialog(self, run_next, test):
+ options = []
+ if self.opt_ext:
+ options.append('ext')
+ if self.opt_bip39:
+ options.append('bip39')
+ title = _('Enter Seed')
+ message = _('Please enter your seed phrase in order to restore your wallet.')
+ return self.seed_input(title, message, test, options)
+
+ @wizard_dialog
+ def confirm_seed_dialog(self, run_next, test):
+ self.app.clipboard().clear()
+ title = _('Confirm Seed')
+ message = ' '.join([
+ _('Your seed is important!'),
+ _('If you lose your seed, your money will be permanently lost.'),
+ _('To make sure that you have properly saved your seed, please retype it here.')
+ ])
+ seed, is_bip39, is_ext = self.seed_input(title, message, test, None)
+ return seed
+
+ @wizard_dialog
+ def show_seed_dialog(self, run_next, seed_text):
+ title = _("Your wallet generation seed is:")
+ slayout = SeedLayout(seed=seed_text, title=title, msg=True, options=['ext'])
+ self.exec_layout(slayout)
+ return slayout.is_ext
+
+ def pw_layout(self, msg, kind, force_disable_encrypt_cb):
+ playout = PasswordLayout(None, msg, kind, self.next_button,
+ force_disable_encrypt_cb=force_disable_encrypt_cb)
+ playout.encrypt_cb.setChecked(True)
+ self.exec_layout(playout.layout())
+ return playout.new_password(), playout.encrypt_cb.isChecked()
+
+ @wizard_dialog
+ def request_password(self, run_next, force_disable_encrypt_cb=False):
+ """Request the user enter a new password and confirm it. Return
+ the password or None for no password."""
+ return self.pw_layout(MSG_ENTER_PASSWORD, PW_NEW, force_disable_encrypt_cb)
+
+ @wizard_dialog
+ def request_storage_encryption(self, run_next):
+ playout = PasswordLayoutForHW(None, MSG_HW_STORAGE_ENCRYPTION, PW_NEW, self.next_button)
+ playout.encrypt_cb.setChecked(True)
+ self.exec_layout(playout.layout())
+ return playout.encrypt_cb.isChecked()
+
+ def show_restore(self, wallet, network):
+ # FIXME: these messages are shown after the install wizard is
+ # finished and the window closed. On macOS they appear parented
+ # with a re-appeared ghost install wizard window...
+ if network:
+ def task():
+ wallet.wait_until_synchronized()
+ if wallet.is_found():
+ msg = _("Recovery successful")
+ else:
+ msg = _("No transactions found for this seed")
+ self.synchronized_signal.emit(msg)
+ self.synchronized_signal.connect(self.show_message)
+ t = threading.Thread(target = task)
+ t.daemon = True
+ t.start()
+ else:
+ msg = _("This wallet was restored offline. It may "
+ "contain more addresses than displayed.")
+ self.show_message(msg)
+
+ @wizard_dialog
+ def confirm_dialog(self, title, message, run_next):
+ self.confirm(message, title)
+
+ def confirm(self, message, title):
+ label = WWLabel(message)
+ vbox = QVBoxLayout()
+ vbox.addWidget(label)
+ self.exec_layout(vbox, title)
+
+ @wizard_dialog
+ def action_dialog(self, action, run_next):
+ self.run(action)
+
+ def terminate(self):
+ self.accept_signal.emit()
+
+ def waiting_dialog(self, task, msg, on_finished=None):
+ label = WWLabel(msg)
+ vbox = QVBoxLayout()
+ vbox.addSpacing(100)
+ label.setMinimumWidth(300)
+ label.setAlignment(Qt.AlignCenter)
+ vbox.addWidget(label)
+ self.set_layout(vbox, next_enabled=False)
+ self.back_button.setEnabled(False)
+
+ t = threading.Thread(target=task)
+ t.start()
+ while True:
+ t.join(1.0/60)
+ if t.is_alive():
+ self.refresh_gui()
+ else:
+ break
+ if on_finished:
+ on_finished()
+
+ @wizard_dialog
+ def choice_dialog(self, title, message, choices, run_next):
+ c_values = [x[0] for x in choices]
+ c_titles = [x[1] for x in choices]
+ clayout = ChoicesLayout(message, c_titles)
+ vbox = QVBoxLayout()
+ vbox.addLayout(clayout.layout())
+ self.exec_layout(vbox, title)
+ action = c_values[clayout.selected_index()]
+ return action
+
+ def query_choice(self, msg, choices):
+ """called by hardware wallets"""
+ clayout = ChoicesLayout(msg, choices)
+ vbox = QVBoxLayout()
+ vbox.addLayout(clayout.layout())
+ self.exec_layout(vbox, '')
+ return clayout.selected_index()
+
+ @wizard_dialog
+ def choice_and_line_dialog(self, title, message1, choices, message2,
+ test_text, run_next) -> (str, str):
+ vbox = QVBoxLayout()
+
+ c_values = [x[0] for x in choices]
+ c_titles = [x[1] for x in choices]
+ c_default_text = [x[2] for x in choices]
+ def on_choice_click(clayout):
+ idx = clayout.selected_index()
+ line.setText(c_default_text[idx])
+ clayout = ChoicesLayout(message1, c_titles, on_choice_click)
+ vbox.addLayout(clayout.layout())
+
+ vbox.addSpacing(50)
+ vbox.addWidget(WWLabel(message2))
+
+ line = QLineEdit()
+ def on_text_change(text):
+ self.next_button.setEnabled(test_text(text))
+ line.textEdited.connect(on_text_change)
+ on_choice_click(clayout) # set default text for "line"
+ vbox.addWidget(line)
+
+ self.exec_layout(vbox, title)
+ choice = c_values[clayout.selected_index()]
+ return str(line.text()), choice
+
+ @wizard_dialog
+ def line_dialog(self, run_next, title, message, default, test, warning='',
+ presets=()):
+ vbox = QVBoxLayout()
+ vbox.addWidget(WWLabel(message))
+ line = QLineEdit()
+ line.setText(default)
+ def f(text):
+ self.next_button.setEnabled(test(text))
+ line.textEdited.connect(f)
+ vbox.addWidget(line)
+ vbox.addWidget(WWLabel(warning))
+
+ for preset in presets:
+ button = QPushButton(preset[0])
+ button.clicked.connect(lambda __, text=preset[1]: line.setText(text))
+ button.setMinimumWidth(150)
+ hbox = QHBoxLayout()
+ hbox.addWidget(button, alignment=Qt.AlignCenter)
+ vbox.addLayout(hbox)
+
+ self.exec_layout(vbox, title, next_enabled=test(default))
+ return ' '.join(line.text().split())
+
+ @wizard_dialog
+ def show_xpub_dialog(self, xpub, run_next):
+ msg = ' '.join([
+ _("Here is your master public key."),
+ _("Please share it with your cosigners.")
+ ])
+ vbox = QVBoxLayout()
+ layout = SeedLayout(xpub, title=msg, icon=False, for_seed_words=False)
+ vbox.addLayout(layout.layout())
+ self.exec_layout(vbox, _('Master Public Key'))
+ return None
+
+ def init_network(self, network):
+ message = _("Electrum communicates with remote servers to get "
+ "information about your transactions and addresses. The "
+ "servers all fulfill the same purpose only differing in "
+ "hardware. In most cases you simply want to let Electrum "
+ "pick one at random. However if you prefer feel free to "
+ "select a server manually.")
+ choices = [_("Auto connect"), _("Select server manually")]
+ title = _("How do you want to connect to a server? ")
+ clayout = ChoicesLayout(message, choices)
+ self.back_button.setText(_('Cancel'))
+ self.exec_layout(clayout.layout(), title)
+ r = clayout.selected_index()
+ if r == 1:
+ nlayout = NetworkChoiceLayout(network, self.config, wizard=True)
+ if self.exec_layout(nlayout.layout()):
+ nlayout.accept()
+ else:
+ network.auto_connect = True
+ self.config.set_key('auto_connect', True, True)
+
+ @wizard_dialog
+ def multisig_dialog(self, run_next):
+ cw = CosignWidget(2, 2)
+ m_edit = QSlider(Qt.Horizontal, self)
+ n_edit = QSlider(Qt.Horizontal, self)
+ n_edit.setMinimum(2)
+ n_edit.setMaximum(15)
+ m_edit.setMinimum(1)
+ m_edit.setMaximum(2)
+ n_edit.setValue(2)
+ m_edit.setValue(2)
+ n_label = QLabel()
+ m_label = QLabel()
+ grid = QGridLayout()
+ grid.addWidget(n_label, 0, 0)
+ grid.addWidget(n_edit, 0, 1)
+ grid.addWidget(m_label, 1, 0)
+ grid.addWidget(m_edit, 1, 1)
+ def on_m(m):
+ m_label.setText(_('Require {0} signatures').format(m))
+ cw.set_m(m)
+ def on_n(n):
+ n_label.setText(_('From {0} cosigners').format(n))
+ cw.set_n(n)
+ m_edit.setMaximum(n)
+ n_edit.valueChanged.connect(on_n)
+ m_edit.valueChanged.connect(on_m)
+ on_n(2)
+ on_m(2)
+ vbox = QVBoxLayout()
+ vbox.addWidget(cw)
+ vbox.addWidget(WWLabel(_("Choose the number of signatures needed to unlock funds in your wallet:")))
+ vbox.addLayout(grid)
+ self.exec_layout(vbox, _("Multi-Signature Wallet"))
+ m = int(m_edit.value())
+ n = int(n_edit.value())
+ return (m, n)
DIR diff --git a/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py
DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
t@@ -0,0 +1,3220 @@
+#!/usr/bin/env python
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2012 thomasv@gitorious
+#
+# 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 sys, time, threading
+import os, json, traceback
+import shutil
+import weakref
+import webbrowser
+import csv
+from decimal import Decimal
+import base64
+from functools import partial
+
+from PyQt5.QtGui import *
+from PyQt5.QtCore import *
+import PyQt5.QtCore as QtCore
+
+from .exception_window import Exception_Hook
+from PyQt5.QtWidgets import *
+
+from electrum import (keystore, simple_config, ecc, constants, util, bitcoin, commands,
+ coinchooser, paymentrequest)
+from electrum.bitcoin import COIN, is_address, TYPE_ADDRESS
+from electrum.plugin import run_hook
+from electrum.i18n import _
+from electrum.util import (format_time, format_satoshis, format_fee_satoshis,
+ format_satoshis_plain, NotEnoughFunds, PrintError,
+ UserCancelled, NoDynamicFeeEstimates, profiler,
+ export_meta, import_meta, bh2u, bfh, InvalidPassword,
+ base_units, base_units_list, base_unit_name_to_decimal_point,
+ decimal_point_to_base_unit_name, quantize_feerate)
+from electrum.transaction import Transaction
+from electrum.wallet import Multisig_Wallet, AddTransactionException, CannotBumpFee
+
+from .amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, FeerateEdit
+from .qrcodewidget import QRCodeWidget, QRDialog
+from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit
+from .transaction_dialog import show_transaction
+from .fee_slider import FeeSlider
+from .util import *
+from .installwizard import WIF_HELP_TEXT
+
+
+class StatusBarButton(QPushButton):
+ def __init__(self, icon, tooltip, func):
+ QPushButton.__init__(self, icon, '')
+ self.setToolTip(tooltip)
+ self.setFlat(True)
+ self.setMaximumWidth(25)
+ self.clicked.connect(self.onPress)
+ self.func = func
+ self.setIconSize(QSize(25,25))
+
+ def onPress(self, checked=False):
+ '''Drops the unwanted PyQt5 "checked" argument'''
+ self.func()
+
+ def keyPressEvent(self, e):
+ if e.key() == Qt.Key_Return:
+ self.func()
+
+
+from electrum.paymentrequest import PR_PAID
+
+
+class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
+
+ payment_request_ok_signal = pyqtSignal()
+ payment_request_error_signal = pyqtSignal()
+ notify_transactions_signal = pyqtSignal()
+ new_fx_quotes_signal = pyqtSignal()
+ new_fx_history_signal = pyqtSignal()
+ network_signal = pyqtSignal(str, object)
+ alias_received_signal = pyqtSignal()
+ computing_privkeys_signal = pyqtSignal()
+ show_privkeys_signal = pyqtSignal()
+
+ def __init__(self, gui_object, wallet):
+ QMainWindow.__init__(self)
+
+ self.gui_object = gui_object
+ self.config = config = gui_object.config
+
+ self.setup_exception_hook()
+
+ self.network = gui_object.daemon.network
+ self.fx = gui_object.daemon.fx
+ self.invoices = wallet.invoices
+ self.contacts = wallet.contacts
+ self.tray = gui_object.tray
+ self.app = gui_object.app
+ self.cleaned_up = False
+ self.is_max = False
+ self.payment_request = None
+ self.checking_accounts = False
+ self.qr_window = None
+ self.not_enough_funds = False
+ self.pluginsdialog = None
+ self.require_fee_update = False
+ self.tx_notifications = []
+ self.tl_windows = []
+ self.tx_external_keypairs = {}
+
+ self.create_status_bar()
+ self.need_update = threading.Event()
+
+ self.decimal_point = config.get('decimal_point', 5)
+ self.num_zeros = int(config.get('num_zeros',0))
+
+ self.completions = QStringListModel()
+
+ self.tabs = tabs = QTabWidget(self)
+ self.send_tab = self.create_send_tab()
+ self.receive_tab = self.create_receive_tab()
+ self.addresses_tab = self.create_addresses_tab()
+ self.utxo_tab = self.create_utxo_tab()
+ self.console_tab = self.create_console_tab()
+ self.contacts_tab = self.create_contacts_tab()
+ tabs.addTab(self.create_history_tab(), QIcon(":icons/tab_history.png"), _('History'))
+ tabs.addTab(self.send_tab, QIcon(":icons/tab_send.png"), _('Send'))
+ tabs.addTab(self.receive_tab, QIcon(":icons/tab_receive.png"), _('Receive'))
+
+ def add_optional_tab(tabs, tab, icon, description, name):
+ tab.tab_icon = icon
+ tab.tab_description = description
+ tab.tab_pos = len(tabs)
+ tab.tab_name = name
+ if self.config.get('show_{}_tab'.format(name), False):
+ tabs.addTab(tab, icon, description.replace("&", ""))
+
+ add_optional_tab(tabs, self.addresses_tab, QIcon(":icons/tab_addresses.png"), _("&Addresses"), "addresses")
+ add_optional_tab(tabs, self.utxo_tab, QIcon(":icons/tab_coins.png"), _("Co&ins"), "utxo")
+ add_optional_tab(tabs, self.contacts_tab, QIcon(":icons/tab_contacts.png"), _("Con&tacts"), "contacts")
+ add_optional_tab(tabs, self.console_tab, QIcon(":icons/tab_console.png"), _("Con&sole"), "console")
+
+ tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+ self.setCentralWidget(tabs)
+
+ if self.config.get("is_maximized"):
+ self.showMaximized()
+
+ self.setWindowIcon(QIcon(":icons/electrum.png"))
+ self.init_menubar()
+
+ wrtabs = weakref.proxy(tabs)
+ QShortcut(QKeySequence("Ctrl+W"), self, self.close)
+ QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
+ QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
+ QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: wrtabs.setCurrentIndex((wrtabs.currentIndex() - 1)%wrtabs.count()))
+ QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: wrtabs.setCurrentIndex((wrtabs.currentIndex() + 1)%wrtabs.count()))
+
+ for i in range(wrtabs.count()):
+ QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: wrtabs.setCurrentIndex(i))
+
+ self.payment_request_ok_signal.connect(self.payment_request_ok)
+ self.payment_request_error_signal.connect(self.payment_request_error)
+ self.notify_transactions_signal.connect(self.notify_transactions)
+ self.history_list.setFocus(True)
+
+ # network callbacks
+ if self.network:
+ self.network_signal.connect(self.on_network_qt)
+ interests = ['updated', 'new_transaction', 'status',
+ 'banner', 'verified', 'fee']
+ # To avoid leaking references to "self" that prevent the
+ # window from being GC-ed when closed, callbacks should be
+ # methods of this class only, and specifically not be
+ # partials, lambdas or methods of subobjects. Hence...
+ self.network.register_callback(self.on_network, interests)
+ # set initial message
+ self.console.showMessage(self.network.banner)
+ self.network.register_callback(self.on_quotes, ['on_quotes'])
+ self.network.register_callback(self.on_history, ['on_history'])
+ self.new_fx_quotes_signal.connect(self.on_fx_quotes)
+ self.new_fx_history_signal.connect(self.on_fx_history)
+
+ # update fee slider in case we missed the callback
+ self.fee_slider.update()
+ self.load_wallet(wallet)
+ self.connect_slots(gui_object.timer)
+ self.fetch_alias()
+
+ def on_history(self, b):
+ self.new_fx_history_signal.emit()
+
+ def setup_exception_hook(self):
+ Exception_Hook(self)
+
+ def on_fx_history(self):
+ self.history_list.refresh_headers()
+ self.history_list.update()
+ self.address_list.update()
+
+ def on_quotes(self, b):
+ self.new_fx_quotes_signal.emit()
+
+ def on_fx_quotes(self):
+ self.update_status()
+ # Refresh edits with the new rate
+ edit = self.fiat_send_e if self.fiat_send_e.is_last_edited else self.amount_e
+ edit.textEdited.emit(edit.text())
+ edit = self.fiat_receive_e if self.fiat_receive_e.is_last_edited else self.receive_amount_e
+ edit.textEdited.emit(edit.text())
+ # History tab needs updating if it used spot
+ if self.fx.history_used_spot:
+ self.history_list.update()
+
+ def toggle_tab(self, tab):
+ show = not self.config.get('show_{}_tab'.format(tab.tab_name), False)
+ self.config.set_key('show_{}_tab'.format(tab.tab_name), show)
+ item_text = (_("Hide") if show else _("Show")) + " " + tab.tab_description
+ tab.menu_action.setText(item_text)
+ if show:
+ # Find out where to place the tab
+ index = len(self.tabs)
+ for i in range(len(self.tabs)):
+ try:
+ if tab.tab_pos < self.tabs.widget(i).tab_pos:
+ index = i
+ break
+ except AttributeError:
+ pass
+ self.tabs.insertTab(index, tab, tab.tab_icon, tab.tab_description.replace("&", ""))
+ else:
+ i = self.tabs.indexOf(tab)
+ self.tabs.removeTab(i)
+
+ def push_top_level_window(self, window):
+ '''Used for e.g. tx dialog box to ensure new dialogs are appropriately
+ parented. This used to be done by explicitly providing the parent
+ window, but that isn't something hardware wallet prompts know.'''
+ self.tl_windows.append(window)
+
+ def pop_top_level_window(self, window):
+ self.tl_windows.remove(window)
+
+ def top_level_window(self, test_func=None):
+ '''Do the right thing in the presence of tx dialog windows'''
+ override = self.tl_windows[-1] if self.tl_windows else None
+ if override and test_func and not test_func(override):
+ override = None # only override if ok for test_func
+ return self.top_level_window_recurse(override, test_func)
+
+ def diagnostic_name(self):
+ return "%s/%s" % (PrintError.diagnostic_name(self),
+ self.wallet.basename() if self.wallet else "None")
+
+ def is_hidden(self):
+ return self.isMinimized() or self.isHidden()
+
+ def show_or_hide(self):
+ if self.is_hidden():
+ self.bring_to_top()
+ else:
+ self.hide()
+
+ def bring_to_top(self):
+ self.show()
+ self.raise_()
+
+ def on_error(self, exc_info):
+ if not isinstance(exc_info[1], UserCancelled):
+ try:
+ traceback.print_exception(*exc_info)
+ except OSError:
+ pass # see #4418; try to at least show popup:
+ self.show_error(str(exc_info[1]))
+
+ def on_network(self, event, *args):
+ if event == 'updated':
+ self.need_update.set()
+ self.gui_object.network_updated_signal_obj.network_updated_signal \
+ .emit(event, args)
+ elif event == 'new_transaction':
+ self.tx_notifications.append(args[0])
+ self.notify_transactions_signal.emit()
+ elif event in ['status', 'banner', 'verified', 'fee']:
+ # Handle in GUI thread
+ self.network_signal.emit(event, args)
+ else:
+ self.print_error("unexpected network message:", event, args)
+
+ def on_network_qt(self, event, args=None):
+ # Handle a network message in the GUI thread
+ if event == 'status':
+ self.update_status()
+ elif event == 'banner':
+ self.console.showMessage(args[0])
+ elif event == 'verified':
+ self.history_list.update_item(*args)
+ elif event == 'fee':
+ if self.config.is_dynfee():
+ self.fee_slider.update()
+ self.do_update_fee()
+ elif event == 'fee_histogram':
+ if self.config.is_dynfee():
+ self.fee_slider.update()
+ self.do_update_fee()
+ # todo: update only unconfirmed tx
+ self.history_list.update()
+ else:
+ self.print_error("unexpected network_qt signal:", event, args)
+
+ def fetch_alias(self):
+ self.alias_info = None
+ alias = self.config.get('alias')
+ if alias:
+ alias = str(alias)
+ def f():
+ self.alias_info = self.contacts.resolve_openalias(alias)
+ self.alias_received_signal.emit()
+ t = threading.Thread(target=f)
+ t.setDaemon(True)
+ t.start()
+
+ def close_wallet(self):
+ if self.wallet:
+ self.print_error('close_wallet', self.wallet.storage.path)
+ run_hook('close_wallet', self.wallet)
+
+ @profiler
+ def load_wallet(self, wallet):
+ wallet.thread = TaskThread(self, self.on_error)
+ self.wallet = wallet
+ self.update_recently_visited(wallet.storage.path)
+ # address used to create a dummy transaction and estimate transaction fee
+ self.history_list.update()
+ self.address_list.update()
+ self.utxo_list.update()
+ self.need_update.set()
+ # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
+ self.notify_transactions()
+ # update menus
+ self.seed_menu.setEnabled(self.wallet.has_seed())
+ self.update_lock_icon()
+ self.update_buttons_on_seed()
+ self.update_console()
+ self.clear_receive_tab()
+ self.request_list.update()
+ self.tabs.show()
+ self.init_geometry()
+ if self.config.get('hide_gui') and self.gui_object.tray.isVisible():
+ self.hide()
+ else:
+ self.show()
+ self.watching_only_changed()
+ run_hook('load_wallet', wallet, self)
+
+ def init_geometry(self):
+ winpos = self.wallet.storage.get("winpos-qt")
+ try:
+ screen = self.app.desktop().screenGeometry()
+ assert screen.contains(QRect(*winpos))
+ self.setGeometry(*winpos)
+ except:
+ self.print_error("using default geometry")
+ self.setGeometry(100, 100, 840, 400)
+
+ def watching_only_changed(self):
+ name = "Electrum Testnet" if constants.net.TESTNET else "Electrum"
+ title = '%s %s - %s' % (name, self.wallet.electrum_version,
+ self.wallet.basename())
+ extra = [self.wallet.storage.get('wallet_type', '?')]
+ if self.wallet.is_watching_only():
+ self.warn_if_watching_only()
+ extra.append(_('watching only'))
+ title += ' [%s]'% ', '.join(extra)
+ self.setWindowTitle(title)
+ self.password_menu.setEnabled(self.wallet.may_have_password())
+ self.import_privkey_menu.setVisible(self.wallet.can_import_privkey())
+ self.import_address_menu.setVisible(self.wallet.can_import_address())
+ self.export_menu.setEnabled(self.wallet.can_export())
+
+ def warn_if_watching_only(self):
+ if self.wallet.is_watching_only():
+ msg = ' '.join([
+ _("This wallet is watching-only."),
+ _("This means you will not be able to spend Bitcoins with it."),
+ _("Make sure you own the seed phrase or the private keys, before you request Bitcoins to be sent to this wallet.")
+ ])
+ self.show_warning(msg, title=_('Information'))
+
+ def open_wallet(self):
+ try:
+ wallet_folder = self.get_wallet_folder()
+ except FileNotFoundError as e:
+ self.show_error(str(e))
+ return
+ filename, __ = QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder)
+ if not filename:
+ return
+ self.gui_object.new_window(filename)
+
+
+ def backup_wallet(self):
+ path = self.wallet.storage.path
+ wallet_folder = os.path.dirname(path)
+ filename, __ = QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder)
+ if not filename:
+ return
+ new_path = os.path.join(wallet_folder, filename)
+ if new_path != path:
+ try:
+ shutil.copy2(path, new_path)
+ self.show_message(_("A copy of your wallet file was created in")+" '%s'" % str(new_path), title=_("Wallet backup created"))
+ except BaseException as reason:
+ self.show_critical(_("Electrum was unable to copy your wallet file to the specified location.") + "\n" + str(reason), title=_("Unable to create backup"))
+
+ def update_recently_visited(self, filename):
+ recent = self.config.get('recently_open', [])
+ try:
+ sorted(recent)
+ except:
+ recent = []
+ if filename in recent:
+ recent.remove(filename)
+ recent.insert(0, filename)
+ recent = recent[:5]
+ self.config.set_key('recently_open', recent)
+ self.recently_visited_menu.clear()
+ for i, k in enumerate(sorted(recent)):
+ b = os.path.basename(k)
+ def loader(k):
+ return lambda: self.gui_object.new_window(k)
+ self.recently_visited_menu.addAction(b, loader(k)).setShortcut(QKeySequence("Ctrl+%d"%(i+1)))
+ self.recently_visited_menu.setEnabled(len(recent))
+
+ def get_wallet_folder(self):
+ return os.path.dirname(os.path.abspath(self.config.get_wallet_path()))
+
+ def new_wallet(self):
+ try:
+ wallet_folder = self.get_wallet_folder()
+ except FileNotFoundError as e:
+ self.show_error(str(e))
+ return
+ i = 1
+ while True:
+ filename = "wallet_%d" % i
+ if filename in os.listdir(wallet_folder):
+ i += 1
+ else:
+ break
+ full_path = os.path.join(wallet_folder, filename)
+ self.gui_object.start_new_window(full_path, None)
+
+ def init_menubar(self):
+ menubar = QMenuBar()
+
+ file_menu = menubar.addMenu(_("&File"))
+ self.recently_visited_menu = file_menu.addMenu(_("&Recently open"))
+ file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
+ file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
+ file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
+ file_menu.addAction(_("Delete"), self.remove_wallet)
+ file_menu.addSeparator()
+ file_menu.addAction(_("&Quit"), self.close)
+
+ wallet_menu = menubar.addMenu(_("&Wallet"))
+ wallet_menu.addAction(_("&Information"), self.show_master_public_keys)
+ wallet_menu.addSeparator()
+ self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
+ self.seed_menu = wallet_menu.addAction(_("&Seed"), self.show_seed_dialog)
+ self.private_keys_menu = wallet_menu.addMenu(_("&Private keys"))
+ self.private_keys_menu.addAction(_("&Sweep"), self.sweep_key_dialog)
+ self.import_privkey_menu = self.private_keys_menu.addAction(_("&Import"), self.do_import_privkey)
+ self.export_menu = self.private_keys_menu.addAction(_("&Export"), self.export_privkeys_dialog)
+ self.import_address_menu = wallet_menu.addAction(_("Import addresses"), self.import_addresses)
+ wallet_menu.addSeparator()
+
+ addresses_menu = wallet_menu.addMenu(_("&Addresses"))
+ addresses_menu.addAction(_("&Filter"), lambda: self.address_list.toggle_toolbar(self.config))
+ labels_menu = wallet_menu.addMenu(_("&Labels"))
+ labels_menu.addAction(_("&Import"), self.do_import_labels)
+ labels_menu.addAction(_("&Export"), self.do_export_labels)
+ history_menu = wallet_menu.addMenu(_("&History"))
+ history_menu.addAction(_("&Filter"), lambda: self.history_list.toggle_toolbar(self.config))
+ history_menu.addAction(_("&Summary"), self.history_list.show_summary)
+ history_menu.addAction(_("&Plot"), self.history_list.plot_history_dialog)
+ history_menu.addAction(_("&Export"), self.history_list.export_history_dialog)
+ contacts_menu = wallet_menu.addMenu(_("Contacts"))
+ contacts_menu.addAction(_("&New"), self.new_contact_dialog)
+ contacts_menu.addAction(_("Import"), lambda: self.contact_list.import_contacts())
+ contacts_menu.addAction(_("Export"), lambda: self.contact_list.export_contacts())
+ invoices_menu = wallet_menu.addMenu(_("Invoices"))
+ invoices_menu.addAction(_("Import"), lambda: self.invoice_list.import_invoices())
+ invoices_menu.addAction(_("Export"), lambda: self.invoice_list.export_invoices())
+
+ wallet_menu.addSeparator()
+ wallet_menu.addAction(_("Find"), self.toggle_search).setShortcut(QKeySequence("Ctrl+F"))
+
+ def add_toggle_action(view_menu, tab):
+ is_shown = self.config.get('show_{}_tab'.format(tab.tab_name), False)
+ item_name = (_("Hide") if is_shown else _("Show")) + " " + tab.tab_description
+ tab.menu_action = view_menu.addAction(item_name, lambda: self.toggle_tab(tab))
+
+ view_menu = menubar.addMenu(_("&View"))
+ add_toggle_action(view_menu, self.addresses_tab)
+ add_toggle_action(view_menu, self.utxo_tab)
+ add_toggle_action(view_menu, self.contacts_tab)
+ add_toggle_action(view_menu, self.console_tab)
+
+ tools_menu = menubar.addMenu(_("&Tools"))
+
+ # Settings / Preferences are all reserved keywords in macOS using this as work around
+ tools_menu.addAction(_("Electrum preferences") if sys.platform == 'darwin' else _("Preferences"), self.settings_dialog)
+ tools_menu.addAction(_("&Network"), lambda: self.gui_object.show_network_dialog(self))
+ tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
+ tools_menu.addSeparator()
+ tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message)
+ tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message)
+ tools_menu.addSeparator()
+
+ paytomany_menu = tools_menu.addAction(_("&Pay to many"), self.paytomany)
+
+ raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
+ raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file)
+ raw_transaction_menu.addAction(_("&From text"), self.do_process_from_text)
+ raw_transaction_menu.addAction(_("&From the blockchain"), self.do_process_from_txid)
+ raw_transaction_menu.addAction(_("&From QR code"), self.read_tx_from_qrcode)
+ self.raw_transaction_menu = raw_transaction_menu
+ run_hook('init_menubar_tools', self, tools_menu)
+
+ help_menu = menubar.addMenu(_("&Help"))
+ help_menu.addAction(_("&About"), self.show_about)
+ help_menu.addAction(_("&Official website"), lambda: webbrowser.open("https://electrum.org"))
+ help_menu.addSeparator()
+ help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://docs.electrum.org/")).setShortcut(QKeySequence.HelpContents)
+ help_menu.addAction(_("&Report Bug"), self.show_report_bug)
+ help_menu.addSeparator()
+ help_menu.addAction(_("&Donate to server"), self.donate_to_server)
+
+ self.setMenuBar(menubar)
+
+ def donate_to_server(self):
+ d = self.network.get_donation_address()
+ if d:
+ host = self.network.get_parameters()[0]
+ self.pay_to_URI('bitcoin:%s?message=donation for %s'%(d, host))
+ else:
+ self.show_error(_('No donation address for this server'))
+
+ def show_about(self):
+ QMessageBox.about(self, "Electrum",
+ (_("Version")+" %s" % self.wallet.electrum_version + "\n\n" +
+ _("Electrum's focus is speed, with low resource usage and simplifying Bitcoin.") + " " +
+ _("You do not need to perform regular backups, because your wallet can be "
+ "recovered from a secret phrase that you can memorize or write on paper.") + " " +
+ _("Startup times are instant because it operates in conjunction with high-performance "
+ "servers that handle the most complicated parts of the Bitcoin system.") + "\n\n" +
+ _("Uses icons from the Icons8 icon pack (icons8.com).")))
+
+ def show_report_bug(self):
+ msg = ' '.join([
+ _("Please report any bugs as issues on github:<br/>"),
+ "<a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a><br/><br/>",
+ _("Before reporting a bug, upgrade to the most recent version of Electrum (latest release or git HEAD), and include the version number in your report."),
+ _("Try to explain not only what the bug is, but how it occurs.")
+ ])
+ self.show_message(msg, title="Electrum - " + _("Reporting Bugs"))
+
+ def notify_transactions(self):
+ if not self.network or not self.network.is_connected():
+ return
+ self.print_error("Notifying GUI")
+ if len(self.tx_notifications) > 0:
+ # Combine the transactions if there are at least three
+ num_txns = len(self.tx_notifications)
+ if num_txns >= 3:
+ total_amount = 0
+ for tx in self.tx_notifications:
+ is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
+ if v > 0:
+ total_amount += v
+ self.notify(_("{} new transactions received: Total amount received in the new transactions {}")
+ .format(num_txns, self.format_amount_and_units(total_amount)))
+ self.tx_notifications = []
+ else:
+ for tx in self.tx_notifications:
+ if tx:
+ self.tx_notifications.remove(tx)
+ is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
+ if v > 0:
+ self.notify(_("New transaction received: {}").format(self.format_amount_and_units(v)))
+
+ def notify(self, message):
+ if self.tray:
+ try:
+ # this requires Qt 5.9
+ self.tray.showMessage("Electrum", message, QIcon(":icons/electrum_dark_icon"), 20000)
+ except TypeError:
+ self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
+
+
+
+ # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
+ def getOpenFileName(self, title, filter = ""):
+ directory = self.config.get('io_dir', os.path.expanduser('~'))
+ fileName, __ = QFileDialog.getOpenFileName(self, title, directory, filter)
+ if fileName and directory != os.path.dirname(fileName):
+ self.config.set_key('io_dir', os.path.dirname(fileName), True)
+ return fileName
+
+ def getSaveFileName(self, title, filename, filter = ""):
+ directory = self.config.get('io_dir', os.path.expanduser('~'))
+ path = os.path.join( directory, filename )
+ fileName, __ = QFileDialog.getSaveFileName(self, title, path, filter)
+ if fileName and directory != os.path.dirname(fileName):
+ self.config.set_key('io_dir', os.path.dirname(fileName), True)
+ return fileName
+
+ def connect_slots(self, sender):
+ sender.timer_signal.connect(self.timer_actions)
+
+ def timer_actions(self):
+ # Note this runs in the GUI thread
+ if self.need_update.is_set():
+ self.need_update.clear()
+ self.update_wallet()
+ # resolve aliases
+ # FIXME this is a blocking network call that has a timeout of 5 sec
+ self.payto_e.resolve()
+ # update fee
+ if self.require_fee_update:
+ self.do_update_fee()
+ self.require_fee_update = False
+
+ def format_amount(self, x, is_diff=False, whitespaces=False):
+ return format_satoshis(x, self.num_zeros, self.decimal_point, is_diff=is_diff, whitespaces=whitespaces)
+
+ def format_amount_and_units(self, amount):
+ text = self.format_amount(amount) + ' '+ self.base_unit()
+ x = self.fx.format_amount_and_units(amount) if self.fx else None
+ if text and x:
+ text += ' (%s)'%x
+ return text
+
+ def format_fee_rate(self, fee_rate):
+ return format_fee_satoshis(fee_rate/1000, self.num_zeros) + ' sat/byte'
+
+ def get_decimal_point(self):
+ return self.decimal_point
+
+ def base_unit(self):
+ return decimal_point_to_base_unit_name(self.decimal_point)
+
+ def connect_fields(self, window, btc_e, fiat_e, fee_e):
+
+ def edit_changed(edit):
+ if edit.follows:
+ return
+ edit.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
+ fiat_e.is_last_edited = (edit == fiat_e)
+ amount = edit.get_amount()
+ rate = self.fx.exchange_rate() if self.fx else Decimal('NaN')
+ if rate.is_nan() or amount is None:
+ if edit is fiat_e:
+ btc_e.setText("")
+ if fee_e:
+ fee_e.setText("")
+ else:
+ fiat_e.setText("")
+ else:
+ if edit is fiat_e:
+ btc_e.follows = True
+ btc_e.setAmount(int(amount / Decimal(rate) * COIN))
+ btc_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet())
+ btc_e.follows = False
+ if fee_e:
+ window.update_fee()
+ else:
+ fiat_e.follows = True
+ fiat_e.setText(self.fx.ccy_amount_str(
+ amount * Decimal(rate) / COIN, False))
+ fiat_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet())
+ fiat_e.follows = False
+
+ btc_e.follows = False
+ fiat_e.follows = False
+ fiat_e.textChanged.connect(partial(edit_changed, fiat_e))
+ btc_e.textChanged.connect(partial(edit_changed, btc_e))
+ fiat_e.is_last_edited = False
+
+ def update_status(self):
+ if not self.wallet:
+ return
+
+ if self.network is None or not self.network.is_running():
+ text = _("Offline")
+ icon = QIcon(":icons/status_disconnected.png")
+
+ elif self.network.is_connected():
+ server_height = self.network.get_server_height()
+ server_lag = self.network.get_local_height() - server_height
+ # Server height can be 0 after switching to a new server
+ # until we get a headers subscription request response.
+ # Display the synchronizing message in that case.
+ if not self.wallet.up_to_date or server_height == 0:
+ text = _("Synchronizing...")
+ icon = QIcon(":icons/status_waiting.png")
+ elif server_lag > 1:
+ text = _("Server is lagging ({} blocks)").format(server_lag)
+ icon = QIcon(":icons/status_lagging.png")
+ else:
+ c, u, x = self.wallet.get_balance()
+ text = _("Balance" ) + ": %s "%(self.format_amount_and_units(c))
+ if u:
+ text += " [%s unconfirmed]"%(self.format_amount(u, is_diff=True).strip())
+ if x:
+ text += " [%s unmatured]"%(self.format_amount(x, is_diff=True).strip())
+
+ # append fiat balance and price
+ if self.fx.is_enabled():
+ text += self.fx.get_fiat_status_text(c + u + x,
+ self.base_unit(), self.get_decimal_point()) or ''
+ if not self.network.proxy:
+ icon = QIcon(":icons/status_connected.png")
+ else:
+ icon = QIcon(":icons/status_connected_proxy.png")
+ else:
+ if self.network.proxy:
+ text = "{} ({})".format(_("Not connected"), _("proxy enabled"))
+ else:
+ text = _("Not connected")
+ icon = QIcon(":icons/status_disconnected.png")
+
+ self.tray.setToolTip("%s (%s)" % (text, self.wallet.basename()))
+ self.balance_label.setText(text)
+ self.status_button.setIcon( icon )
+
+
+ def update_wallet(self):
+ self.update_status()
+ if self.wallet.up_to_date or not self.network or not self.network.is_connected():
+ self.update_tabs()
+
+ def update_tabs(self):
+ self.history_list.update()
+ self.request_list.update()
+ self.address_list.update()
+ self.utxo_list.update()
+ self.contact_list.update()
+ self.invoice_list.update()
+ self.update_completions()
+
+ def create_history_tab(self):
+ from .history_list import HistoryList
+ self.history_list = l = HistoryList(self)
+ l.searchable_list = l
+ toolbar = l.create_toolbar(self.config)
+ toolbar_shown = self.config.get('show_toolbar_history', False)
+ l.show_toolbar(toolbar_shown)
+ return self.create_list_tab(l, toolbar)
+
+ def show_address(self, addr):
+ from . import address_dialog
+ d = address_dialog.AddressDialog(self, addr)
+ d.exec_()
+
+ def show_transaction(self, tx, tx_desc = None):
+ '''tx_desc is set only for txs created in the Send tab'''
+ show_transaction(tx, self, tx_desc)
+
+ def create_receive_tab(self):
+ # A 4-column grid layout. All the stretch is in the last column.
+ # The exchange rate plugin adds a fiat widget in column 2
+ self.receive_grid = grid = QGridLayout()
+ grid.setSpacing(8)
+ grid.setColumnStretch(3, 1)
+
+ self.receive_address_e = ButtonsLineEdit()
+ self.receive_address_e.addCopyButton(self.app)
+ self.receive_address_e.setReadOnly(True)
+ msg = _('Bitcoin address where the payment should be received. Note that each payment request uses a different Bitcoin address.')
+ self.receive_address_label = HelpLabel(_('Receiving address'), msg)
+ self.receive_address_e.textChanged.connect(self.update_receive_qr)
+ self.receive_address_e.setFocusPolicy(Qt.ClickFocus)
+ grid.addWidget(self.receive_address_label, 0, 0)
+ grid.addWidget(self.receive_address_e, 0, 1, 1, -1)
+
+ self.receive_message_e = QLineEdit()
+ grid.addWidget(QLabel(_('Description')), 1, 0)
+ grid.addWidget(self.receive_message_e, 1, 1, 1, -1)
+ self.receive_message_e.textChanged.connect(self.update_receive_qr)
+
+ self.receive_amount_e = BTCAmountEdit(self.get_decimal_point)
+ grid.addWidget(QLabel(_('Requested amount')), 2, 0)
+ grid.addWidget(self.receive_amount_e, 2, 1)
+ self.receive_amount_e.textChanged.connect(self.update_receive_qr)
+
+ self.fiat_receive_e = AmountEdit(self.fx.get_currency if self.fx else '')
+ if not self.fx or not self.fx.is_enabled():
+ self.fiat_receive_e.setVisible(False)
+ grid.addWidget(self.fiat_receive_e, 2, 2, Qt.AlignLeft)
+ self.connect_fields(self, self.receive_amount_e, self.fiat_receive_e, None)
+
+ self.expires_combo = QComboBox()
+ self.expires_combo.addItems([i[0] for i in expiration_values])
+ self.expires_combo.setCurrentIndex(3)
+ self.expires_combo.setFixedWidth(self.receive_amount_e.width())
+ msg = ' '.join([
+ _('Expiration date of your request.'),
+ _('This information is seen by the recipient if you send them a signed payment request.'),
+ _('Expired requests have to be deleted manually from your list, in order to free the corresponding Bitcoin addresses.'),
+ _('The bitcoin address never expires and will always be part of this electrum wallet.'),
+ ])
+ grid.addWidget(HelpLabel(_('Request expires'), msg), 3, 0)
+ grid.addWidget(self.expires_combo, 3, 1)
+ self.expires_label = QLineEdit('')
+ self.expires_label.setReadOnly(1)
+ self.expires_label.setFocusPolicy(Qt.NoFocus)
+ self.expires_label.hide()
+ grid.addWidget(self.expires_label, 3, 1)
+
+ self.save_request_button = QPushButton(_('Save'))
+ self.save_request_button.clicked.connect(self.save_payment_request)
+
+ self.new_request_button = QPushButton(_('New'))
+ self.new_request_button.clicked.connect(self.new_payment_request)
+
+ self.receive_qr = QRCodeWidget(fixedSize=200)
+ self.receive_qr.mouseReleaseEvent = lambda x: self.toggle_qr_window()
+ self.receive_qr.enterEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.PointingHandCursor))
+ self.receive_qr.leaveEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.ArrowCursor))
+
+ self.receive_buttons = buttons = QHBoxLayout()
+ buttons.addStretch(1)
+ buttons.addWidget(self.save_request_button)
+ buttons.addWidget(self.new_request_button)
+ grid.addLayout(buttons, 4, 1, 1, 2)
+
+ self.receive_requests_label = QLabel(_('Requests'))
+
+ from .request_list import RequestList
+ self.request_list = RequestList(self)
+
+ # layout
+ vbox_g = QVBoxLayout()
+ vbox_g.addLayout(grid)
+ vbox_g.addStretch()
+
+ hbox = QHBoxLayout()
+ hbox.addLayout(vbox_g)
+ hbox.addWidget(self.receive_qr)
+
+ w = QWidget()
+ w.searchable_list = self.request_list
+ vbox = QVBoxLayout(w)
+ vbox.addLayout(hbox)
+ vbox.addStretch(1)
+ vbox.addWidget(self.receive_requests_label)
+ vbox.addWidget(self.request_list)
+ vbox.setStretchFactor(self.request_list, 1000)
+
+ return w
+
+
+ def delete_payment_request(self, addr):
+ self.wallet.remove_payment_request(addr, self.config)
+ self.request_list.update()
+ self.clear_receive_tab()
+
+ def get_request_URI(self, addr):
+ req = self.wallet.receive_requests[addr]
+ message = self.wallet.labels.get(addr, '')
+ amount = req['amount']
+ URI = util.create_URI(addr, amount, message)
+ if req.get('time'):
+ URI += "&time=%d"%req.get('time')
+ if req.get('exp'):
+ URI += "&exp=%d"%req.get('exp')
+ if req.get('name') and req.get('sig'):
+ sig = bfh(req.get('sig'))
+ sig = bitcoin.base_encode(sig, base=58)
+ URI += "&name=" + req['name'] + "&sig="+sig
+ return str(URI)
+
+
+ def sign_payment_request(self, addr):
+ alias = self.config.get('alias')
+ alias_privkey = None
+ if alias and self.alias_info:
+ alias_addr, alias_name, validated = self.alias_info
+ if alias_addr:
+ if self.wallet.is_mine(alias_addr):
+ msg = _('This payment request will be signed.') + '\n' + _('Please enter your password')
+ password = None
+ if self.wallet.has_keystore_encryption():
+ password = self.password_dialog(msg)
+ if not password:
+ return
+ try:
+ self.wallet.sign_payment_request(addr, alias, alias_addr, password)
+ except Exception as e:
+ self.show_error(str(e))
+ return
+ else:
+ return
+
+ def save_payment_request(self):
+ addr = str(self.receive_address_e.text())
+ amount = self.receive_amount_e.get_amount()
+ message = self.receive_message_e.text()
+ if not message and not amount:
+ self.show_error(_('No message or amount'))
+ return False
+ i = self.expires_combo.currentIndex()
+ expiration = list(map(lambda x: x[1], expiration_values))[i]
+ req = self.wallet.make_payment_request(addr, amount, message, expiration)
+ try:
+ self.wallet.add_payment_request(req, self.config)
+ except Exception as e:
+ traceback.print_exc(file=sys.stderr)
+ self.show_error(_('Error adding payment request') + ':\n' + str(e))
+ else:
+ self.sign_payment_request(addr)
+ self.save_request_button.setEnabled(False)
+ finally:
+ self.request_list.update()
+ self.address_list.update()
+
+ def view_and_paste(self, title, msg, data):
+ dialog = WindowModalDialog(self, title)
+ vbox = QVBoxLayout()
+ label = QLabel(msg)
+ label.setWordWrap(True)
+ vbox.addWidget(label)
+ pr_e = ShowQRTextEdit(text=data)
+ vbox.addWidget(pr_e)
+ vbox.addLayout(Buttons(CopyCloseButton(pr_e.text, self.app, dialog)))
+ dialog.setLayout(vbox)
+ dialog.exec_()
+
+ def export_payment_request(self, addr):
+ r = self.wallet.receive_requests.get(addr)
+ pr = paymentrequest.serialize_request(r).SerializeToString()
+ name = r['id'] + '.bip70'
+ fileName = self.getSaveFileName(_("Select where to save your payment request"), name, "*.bip70")
+ if fileName:
+ with open(fileName, "wb+") as f:
+ f.write(util.to_bytes(pr))
+ self.show_message(_("Request saved successfully"))
+ self.saved = True
+
+ def new_payment_request(self):
+ addr = self.wallet.get_unused_address()
+ if addr is None:
+ if not self.wallet.is_deterministic():
+ msg = [
+ _('No more addresses in your wallet.'),
+ _('You are using a non-deterministic wallet, which cannot create new addresses.'),
+ _('If you want to create new addresses, use a deterministic wallet instead.')
+ ]
+ self.show_message(' '.join(msg))
+ return
+ if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")):
+ return
+ addr = self.wallet.create_new_address(False)
+ self.set_receive_address(addr)
+ self.expires_label.hide()
+ self.expires_combo.show()
+ self.new_request_button.setEnabled(False)
+ self.receive_message_e.setFocus(1)
+
+ def set_receive_address(self, addr):
+ self.receive_address_e.setText(addr)
+ self.receive_message_e.setText('')
+ self.receive_amount_e.setAmount(None)
+
+ def clear_receive_tab(self):
+ addr = self.wallet.get_receiving_address() or ''
+ self.receive_address_e.setText(addr)
+ self.receive_message_e.setText('')
+ self.receive_amount_e.setAmount(None)
+ self.expires_label.hide()
+ self.expires_combo.show()
+
+ def toggle_qr_window(self):
+ from . import qrwindow
+ if not self.qr_window:
+ self.qr_window = qrwindow.QR_Window(self)
+ self.qr_window.setVisible(True)
+ self.qr_window_geometry = self.qr_window.geometry()
+ else:
+ if not self.qr_window.isVisible():
+ self.qr_window.setVisible(True)
+ self.qr_window.setGeometry(self.qr_window_geometry)
+ else:
+ self.qr_window_geometry = self.qr_window.geometry()
+ self.qr_window.setVisible(False)
+ self.update_receive_qr()
+
+ def show_send_tab(self):
+ self.tabs.setCurrentIndex(self.tabs.indexOf(self.send_tab))
+
+ def show_receive_tab(self):
+ self.tabs.setCurrentIndex(self.tabs.indexOf(self.receive_tab))
+
+ def receive_at(self, addr):
+ if not bitcoin.is_address(addr):
+ return
+ self.show_receive_tab()
+ self.receive_address_e.setText(addr)
+ self.new_request_button.setEnabled(True)
+
+ def update_receive_qr(self):
+ addr = str(self.receive_address_e.text())
+ amount = self.receive_amount_e.get_amount()
+ message = self.receive_message_e.text()
+ self.save_request_button.setEnabled((amount is not None) or (message != ""))
+ uri = util.create_URI(addr, amount, message)
+ self.receive_qr.setData(uri)
+ if self.qr_window and self.qr_window.isVisible():
+ self.qr_window.set_content(addr, amount, message, uri)
+
+ def set_feerounding_text(self, num_satoshis_added):
+ self.feerounding_text = (_('Additional {} satoshis are going to be added.')
+ .format(num_satoshis_added))
+
+ def create_send_tab(self):
+ # A 4-column grid layout. All the stretch is in the last column.
+ # The exchange rate plugin adds a fiat widget in column 2
+ self.send_grid = grid = QGridLayout()
+ grid.setSpacing(8)
+ grid.setColumnStretch(3, 1)
+
+ from .paytoedit import PayToEdit
+ self.amount_e = BTCAmountEdit(self.get_decimal_point)
+ self.payto_e = PayToEdit(self)
+ msg = _('Recipient of the funds.') + '\n\n'\
+ + _('You may enter a Bitcoin address, a label from your list of contacts (a list of completions will be proposed), or an alias (email-like address that forwards to a Bitcoin address)')
+ payto_label = HelpLabel(_('Pay to'), msg)
+ grid.addWidget(payto_label, 1, 0)
+ grid.addWidget(self.payto_e, 1, 1, 1, -1)
+
+ completer = QCompleter()
+ completer.setCaseSensitivity(False)
+ self.payto_e.set_completer(completer)
+ completer.setModel(self.completions)
+
+ msg = _('Description of the transaction (not mandatory).') + '\n\n'\
+ + _('The description is not sent to the recipient of the funds. It is stored in your wallet file, and displayed in the \'History\' tab.')
+ description_label = HelpLabel(_('Description'), msg)
+ grid.addWidget(description_label, 2, 0)
+ self.message_e = MyLineEdit()
+ grid.addWidget(self.message_e, 2, 1, 1, -1)
+
+ self.from_label = QLabel(_('From'))
+ grid.addWidget(self.from_label, 3, 0)
+ self.from_list = MyTreeWidget(self, self.from_list_menu, ['',''])
+ self.from_list.setHeaderHidden(True)
+ self.from_list.setMaximumHeight(80)
+ grid.addWidget(self.from_list, 3, 1, 1, -1)
+ self.set_pay_from([])
+
+ msg = _('Amount to be sent.') + '\n\n' \
+ + _('The amount will be displayed in red if you do not have enough funds in your wallet.') + ' ' \
+ + _('Note that if you have frozen some of your addresses, the available funds will be lower than your total balance.') + '\n\n' \
+ + _('Keyboard shortcut: type "!" to send all your coins.')
+ amount_label = HelpLabel(_('Amount'), msg)
+ grid.addWidget(amount_label, 4, 0)
+ grid.addWidget(self.amount_e, 4, 1)
+
+ self.fiat_send_e = AmountEdit(self.fx.get_currency if self.fx else '')
+ if not self.fx or not self.fx.is_enabled():
+ self.fiat_send_e.setVisible(False)
+ grid.addWidget(self.fiat_send_e, 4, 2)
+ self.amount_e.frozen.connect(
+ lambda: self.fiat_send_e.setFrozen(self.amount_e.isReadOnly()))
+
+ self.max_button = EnterButton(_("Max"), self.spend_max)
+ self.max_button.setFixedWidth(140)
+ grid.addWidget(self.max_button, 4, 3)
+ hbox = QHBoxLayout()
+ hbox.addStretch(1)
+ grid.addLayout(hbox, 4, 4)
+
+ msg = _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
+ + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
+ + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')
+ self.fee_e_label = HelpLabel(_('Fee'), msg)
+
+ def fee_cb(dyn, pos, fee_rate):
+ if dyn:
+ if self.config.use_mempool_fees():
+ self.config.set_key('depth_level', pos, False)
+ else:
+ self.config.set_key('fee_level', pos, False)
+ else:
+ self.config.set_key('fee_per_kb', fee_rate, False)
+
+ if fee_rate:
+ fee_rate = Decimal(fee_rate)
+ self.feerate_e.setAmount(quantize_feerate(fee_rate / 1000))
+ else:
+ self.feerate_e.setAmount(None)
+ self.fee_e.setModified(False)
+
+ self.fee_slider.activate()
+ self.spend_max() if self.is_max else self.update_fee()
+
+ self.fee_slider = FeeSlider(self, self.config, fee_cb)
+ self.fee_slider.setFixedWidth(140)
+
+ def on_fee_or_feerate(edit_changed, editing_finished):
+ edit_other = self.feerate_e if edit_changed == self.fee_e else self.fee_e
+ if editing_finished:
+ if edit_changed.get_amount() is None:
+ # This is so that when the user blanks the fee and moves on,
+ # we go back to auto-calculate mode and put a fee back.
+ edit_changed.setModified(False)
+ else:
+ # edit_changed was edited just now, so make sure we will
+ # freeze the correct fee setting (this)
+ edit_other.setModified(False)
+ self.fee_slider.deactivate()
+ self.update_fee()
+
+ class TxSizeLabel(QLabel):
+ def setAmount(self, byte_size):
+ self.setText(('x %s bytes =' % byte_size) if byte_size else '')
+
+ self.size_e = TxSizeLabel()
+ self.size_e.setAlignment(Qt.AlignCenter)
+ self.size_e.setAmount(0)
+ self.size_e.setFixedWidth(140)
+ self.size_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
+
+ self.feerate_e = FeerateEdit(lambda: 0)
+ self.feerate_e.setAmount(self.config.fee_per_byte())
+ self.feerate_e.textEdited.connect(partial(on_fee_or_feerate, self.feerate_e, False))
+ self.feerate_e.editingFinished.connect(partial(on_fee_or_feerate, self.feerate_e, True))
+
+ self.fee_e = BTCAmountEdit(self.get_decimal_point)
+ self.fee_e.textEdited.connect(partial(on_fee_or_feerate, self.fee_e, False))
+ self.fee_e.editingFinished.connect(partial(on_fee_or_feerate, self.fee_e, True))
+
+ def feerounding_onclick():
+ text = (self.feerounding_text + '\n\n' +
+ _('To somewhat protect your privacy, Electrum tries to create change with similar precision to other outputs.') + ' ' +
+ _('At most 100 satoshis might be lost due to this rounding.') + ' ' +
+ _("You can disable this setting in '{}'.").format(_('Preferences')) + '\n' +
+ _('Also, dust is not kept as change, but added to the fee.'))
+ QMessageBox.information(self, 'Fee rounding', text)
+
+ self.feerounding_icon = QPushButton(QIcon(':icons/info.png'), '')
+ self.feerounding_icon.setFixedWidth(20)
+ self.feerounding_icon.setFlat(True)
+ self.feerounding_icon.clicked.connect(feerounding_onclick)
+ self.feerounding_icon.setVisible(False)
+
+ self.connect_fields(self, self.amount_e, self.fiat_send_e, self.fee_e)
+
+ vbox_feelabel = QVBoxLayout()
+ vbox_feelabel.addWidget(self.fee_e_label)
+ vbox_feelabel.addStretch(1)
+ grid.addLayout(vbox_feelabel, 5, 0)
+
+ self.fee_adv_controls = QWidget()
+ hbox = QHBoxLayout(self.fee_adv_controls)
+ hbox.setContentsMargins(0, 0, 0, 0)
+ hbox.addWidget(self.feerate_e)
+ hbox.addWidget(self.size_e)
+ hbox.addWidget(self.fee_e)
+ hbox.addWidget(self.feerounding_icon, Qt.AlignLeft)
+ hbox.addStretch(1)
+
+ vbox_feecontrol = QVBoxLayout()
+ vbox_feecontrol.addWidget(self.fee_adv_controls)
+ vbox_feecontrol.addWidget(self.fee_slider)
+
+ grid.addLayout(vbox_feecontrol, 5, 1, 1, -1)
+
+ if not self.config.get('show_fee', False):
+ self.fee_adv_controls.setVisible(False)
+
+ self.preview_button = EnterButton(_("Preview"), self.do_preview)
+ self.preview_button.setToolTip(_('Display the details of your transaction before signing it.'))
+ self.send_button = EnterButton(_("Send"), self.do_send)
+ self.clear_button = EnterButton(_("Clear"), self.do_clear)
+ buttons = QHBoxLayout()
+ buttons.addStretch(1)
+ buttons.addWidget(self.clear_button)
+ buttons.addWidget(self.preview_button)
+ buttons.addWidget(self.send_button)
+ grid.addLayout(buttons, 6, 1, 1, 3)
+
+ self.amount_e.shortcut.connect(self.spend_max)
+ self.payto_e.textChanged.connect(self.update_fee)
+ self.amount_e.textEdited.connect(self.update_fee)
+
+ def reset_max(text):
+ self.is_max = False
+ enable = not bool(text) and not self.amount_e.isReadOnly()
+ self.max_button.setEnabled(enable)
+ self.amount_e.textEdited.connect(reset_max)
+ self.fiat_send_e.textEdited.connect(reset_max)
+
+ def entry_changed():
+ text = ""
+
+ amt_color = ColorScheme.DEFAULT
+ fee_color = ColorScheme.DEFAULT
+ feerate_color = ColorScheme.DEFAULT
+
+ if self.not_enough_funds:
+ amt_color, fee_color = ColorScheme.RED, ColorScheme.RED
+ feerate_color = ColorScheme.RED
+ text = _( "Not enough funds" )
+ c, u, x = self.wallet.get_frozen_balance()
+ if c+u+x:
+ text += ' (' + self.format_amount(c+u+x).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
+
+ # blue color denotes auto-filled values
+ elif self.fee_e.isModified():
+ feerate_color = ColorScheme.BLUE
+ elif self.feerate_e.isModified():
+ fee_color = ColorScheme.BLUE
+ elif self.amount_e.isModified():
+ fee_color = ColorScheme.BLUE
+ feerate_color = ColorScheme.BLUE
+ else:
+ amt_color = ColorScheme.BLUE
+ fee_color = ColorScheme.BLUE
+ feerate_color = ColorScheme.BLUE
+
+ self.statusBar().showMessage(text)
+ self.amount_e.setStyleSheet(amt_color.as_stylesheet())
+ self.fee_e.setStyleSheet(fee_color.as_stylesheet())
+ self.feerate_e.setStyleSheet(feerate_color.as_stylesheet())
+
+ self.amount_e.textChanged.connect(entry_changed)
+ self.fee_e.textChanged.connect(entry_changed)
+ self.feerate_e.textChanged.connect(entry_changed)
+
+ self.invoices_label = QLabel(_('Invoices'))
+ from .invoice_list import InvoiceList
+ self.invoice_list = InvoiceList(self)
+
+ vbox0 = QVBoxLayout()
+ vbox0.addLayout(grid)
+ hbox = QHBoxLayout()
+ hbox.addLayout(vbox0)
+ w = QWidget()
+ vbox = QVBoxLayout(w)
+ vbox.addLayout(hbox)
+ vbox.addStretch(1)
+ vbox.addWidget(self.invoices_label)
+ vbox.addWidget(self.invoice_list)
+ vbox.setStretchFactor(self.invoice_list, 1000)
+ w.searchable_list = self.invoice_list
+ run_hook('create_send_tab', grid)
+ return w
+
+ def spend_max(self):
+ if run_hook('abort_send', self):
+ return
+ self.is_max = True
+ self.do_update_fee()
+
+ def update_fee(self):
+ self.require_fee_update = True
+
+ def get_payto_or_dummy(self):
+ r = self.payto_e.get_recipient()
+ if r:
+ return r
+ return (TYPE_ADDRESS, self.wallet.dummy_address())
+
+ def do_update_fee(self):
+ '''Recalculate the fee. If the fee was manually input, retain it, but
+ still build the TX to see if there are enough funds.
+ '''
+ freeze_fee = self.is_send_fee_frozen()
+ freeze_feerate = self.is_send_feerate_frozen()
+ amount = '!' if self.is_max else self.amount_e.get_amount()
+ if amount is None:
+ if not freeze_fee:
+ self.fee_e.setAmount(None)
+ self.not_enough_funds = False
+ self.statusBar().showMessage('')
+ else:
+ fee_estimator = self.get_send_fee_estimator()
+ outputs = self.payto_e.get_outputs(self.is_max)
+ if not outputs:
+ _type, addr = self.get_payto_or_dummy()
+ outputs = [(_type, addr, amount)]
+ is_sweep = bool(self.tx_external_keypairs)
+ make_tx = lambda fee_est: \
+ self.wallet.make_unsigned_transaction(
+ self.get_coins(), outputs, self.config,
+ fixed_fee=fee_est, is_sweep=is_sweep)
+ try:
+ tx = make_tx(fee_estimator)
+ self.not_enough_funds = False
+ except (NotEnoughFunds, NoDynamicFeeEstimates) as e:
+ if not freeze_fee:
+ self.fee_e.setAmount(None)
+ if not freeze_feerate:
+ self.feerate_e.setAmount(None)
+ self.feerounding_icon.setVisible(False)
+
+ if isinstance(e, NotEnoughFunds):
+ self.not_enough_funds = True
+ elif isinstance(e, NoDynamicFeeEstimates):
+ try:
+ tx = make_tx(0)
+ size = tx.estimated_size()
+ self.size_e.setAmount(size)
+ except BaseException:
+ pass
+ return
+ except BaseException:
+ traceback.print_exc(file=sys.stderr)
+ return
+
+ size = tx.estimated_size()
+ self.size_e.setAmount(size)
+
+ fee = tx.get_fee()
+ fee = None if self.not_enough_funds else fee
+
+ # Displayed fee/fee_rate values are set according to user input.
+ # Due to rounding or dropping dust in CoinChooser,
+ # actual fees often differ somewhat.
+ if freeze_feerate or self.fee_slider.is_active():
+ displayed_feerate = self.feerate_e.get_amount()
+ if displayed_feerate is not None:
+ displayed_feerate = quantize_feerate(displayed_feerate)
+ else:
+ # fallback to actual fee
+ displayed_feerate = quantize_feerate(fee / size) if fee is not None else None
+ self.feerate_e.setAmount(displayed_feerate)
+ displayed_fee = round(displayed_feerate * size) if displayed_feerate is not None else None
+ self.fee_e.setAmount(displayed_fee)
+ else:
+ if freeze_fee:
+ displayed_fee = self.fee_e.get_amount()
+ else:
+ # fallback to actual fee if nothing is frozen
+ displayed_fee = fee
+ self.fee_e.setAmount(displayed_fee)
+ displayed_fee = displayed_fee if displayed_fee else 0
+ displayed_feerate = quantize_feerate(displayed_fee / size) if displayed_fee is not None else None
+ self.feerate_e.setAmount(displayed_feerate)
+
+ # show/hide fee rounding icon
+ feerounding = (fee - displayed_fee) if fee else 0
+ self.set_feerounding_text(int(feerounding))
+ self.feerounding_icon.setToolTip(self.feerounding_text)
+ self.feerounding_icon.setVisible(abs(feerounding) >= 1)
+
+ if self.is_max:
+ amount = tx.output_value()
+ __, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet, tx) or (None, 0)
+ amount_after_all_fees = amount - x_fee_amount
+ self.amount_e.setAmount(amount_after_all_fees)
+
+ def from_list_delete(self, item):
+ i = self.from_list.indexOfTopLevelItem(item)
+ self.pay_from.pop(i)
+ self.redraw_from_list()
+ self.update_fee()
+
+ def from_list_menu(self, position):
+ item = self.from_list.itemAt(position)
+ menu = QMenu()
+ menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
+ menu.exec_(self.from_list.viewport().mapToGlobal(position))
+
+ def set_pay_from(self, coins):
+ self.pay_from = list(coins)
+ self.redraw_from_list()
+
+ def redraw_from_list(self):
+ self.from_list.clear()
+ self.from_label.setHidden(len(self.pay_from) == 0)
+ self.from_list.setHidden(len(self.pay_from) == 0)
+
+ def format(x):
+ h = x.get('prevout_hash')
+ return h[0:10] + '...' + h[-10:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
+
+ for item in self.pay_from:
+ self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
+
+ def get_contact_payto(self, key):
+ _type, label = self.contacts.get(key)
+ return label + ' <' + key + '>' if _type == 'address' else key
+
+ def update_completions(self):
+ l = [self.get_contact_payto(key) for key in self.contacts.keys()]
+ self.completions.setStringList(l)
+
+ def protected(func):
+ '''Password request wrapper. The password is passed to the function
+ as the 'password' named argument. "None" indicates either an
+ unencrypted wallet, or the user cancelled the password request.
+ An empty input is passed as the empty string.'''
+ def request_password(self, *args, **kwargs):
+ parent = self.top_level_window()
+ password = None
+ while self.wallet.has_keystore_encryption():
+ password = self.password_dialog(parent=parent)
+ if password is None:
+ # User cancelled password input
+ return
+ try:
+ self.wallet.check_password(password)
+ break
+ except Exception as e:
+ self.show_error(str(e), parent=parent)
+ continue
+
+ kwargs['password'] = password
+ return func(self, *args, **kwargs)
+ return request_password
+
+ def is_send_fee_frozen(self):
+ return self.fee_e.isVisible() and self.fee_e.isModified() \
+ and (self.fee_e.text() or self.fee_e.hasFocus())
+
+ def is_send_feerate_frozen(self):
+ return self.feerate_e.isVisible() and self.feerate_e.isModified() \
+ and (self.feerate_e.text() or self.feerate_e.hasFocus())
+
+ def get_send_fee_estimator(self):
+ if self.is_send_fee_frozen():
+ fee_estimator = self.fee_e.get_amount()
+ elif self.is_send_feerate_frozen():
+ amount = self.feerate_e.get_amount() # sat/byte feerate
+ amount = 0 if amount is None else amount * 1000 # sat/kilobyte feerate
+ fee_estimator = partial(
+ simple_config.SimpleConfig.estimate_fee_for_feerate, amount)
+ else:
+ fee_estimator = None
+ return fee_estimator
+
+ def read_send_tab(self):
+ if self.payment_request and self.payment_request.has_expired():
+ self.show_error(_('Payment request has expired'))
+ return
+ label = self.message_e.text()
+
+ if self.payment_request:
+ outputs = self.payment_request.get_outputs()
+ else:
+ errors = self.payto_e.get_errors()
+ if errors:
+ self.show_warning(_("Invalid Lines found:") + "\n\n" + '\n'.join([ _("Line #") + str(x[0]+1) + ": " + x[1] for x in errors]))
+ return
+ outputs = self.payto_e.get_outputs(self.is_max)
+
+ if self.payto_e.is_alias and self.payto_e.validated is False:
+ alias = self.payto_e.toPlainText()
+ msg = _('WARNING: the alias "{}" could not be validated via an additional '
+ 'security check, DNSSEC, and thus may not be correct.').format(alias) + '\n'
+ msg += _('Do you wish to continue?')
+ if not self.question(msg):
+ return
+
+ if not outputs:
+ self.show_error(_('No outputs'))
+ return
+
+ for _type, addr, amount in outputs:
+ if addr is None:
+ self.show_error(_('Bitcoin Address is None'))
+ return
+ if _type == TYPE_ADDRESS and not bitcoin.is_address(addr):
+ self.show_error(_('Invalid Bitcoin Address'))
+ return
+ if amount is None:
+ self.show_error(_('Invalid Amount'))
+ return
+
+ fee_estimator = self.get_send_fee_estimator()
+ coins = self.get_coins()
+ return outputs, fee_estimator, label, coins
+
+ def do_preview(self):
+ self.do_send(preview = True)
+
+ def do_send(self, preview = False):
+ if run_hook('abort_send', self):
+ return
+ r = self.read_send_tab()
+ if not r:
+ return
+ outputs, fee_estimator, tx_desc, coins = r
+ try:
+ is_sweep = bool(self.tx_external_keypairs)
+ tx = self.wallet.make_unsigned_transaction(
+ coins, outputs, self.config, fixed_fee=fee_estimator,
+ is_sweep=is_sweep)
+ except NotEnoughFunds:
+ self.show_message(_("Insufficient funds"))
+ return
+ except BaseException as e:
+ traceback.print_exc(file=sys.stdout)
+ self.show_message(str(e))
+ return
+
+ amount = tx.output_value() if self.is_max else sum(map(lambda x:x[2], outputs))
+ fee = tx.get_fee()
+
+ use_rbf = self.config.get('use_rbf', True)
+ if use_rbf:
+ tx.set_rbf(True)
+
+ if fee < self.wallet.relayfee() * tx.estimated_size() / 1000:
+ self.show_error('\n'.join([
+ _("This transaction requires a higher fee, or it will not be propagated by your current server"),
+ _("Try to raise your transaction fee, or use a server with a lower relay fee.")
+ ]))
+ return
+
+ if preview:
+ self.show_transaction(tx, tx_desc)
+ return
+
+ if not self.network:
+ self.show_error(_("You can't broadcast a transaction without a live network connection."))
+ return
+
+ # confirmation dialog
+ msg = [
+ _("Amount to be sent") + ": " + self.format_amount_and_units(amount),
+ _("Mining fee") + ": " + self.format_amount_and_units(fee),
+ ]
+
+ x_fee = run_hook('get_tx_extra_fee', self.wallet, tx)
+ if x_fee:
+ x_fee_address, x_fee_amount = x_fee
+ msg.append( _("Additional fees") + ": " + self.format_amount_and_units(x_fee_amount) )
+
+ confirm_rate = simple_config.FEERATE_WARNING_HIGH_FEE
+ if fee > confirm_rate * tx.estimated_size() / 1000:
+ msg.append(_('Warning') + ': ' + _("The fee for this transaction seems unusually high."))
+
+ if self.wallet.has_keystore_encryption():
+ msg.append("")
+ msg.append(_("Enter your password to proceed"))
+ password = self.password_dialog('\n'.join(msg))
+ if not password:
+ return
+ else:
+ msg.append(_('Proceed?'))
+ password = None
+ if not self.question('\n'.join(msg)):
+ return
+
+ def sign_done(success):
+ if success:
+ if not tx.is_complete():
+ self.show_transaction(tx)
+ self.do_clear()
+ else:
+ self.broadcast_transaction(tx, tx_desc)
+ self.sign_tx_with_password(tx, sign_done, password)
+
+ @protected
+ def sign_tx(self, tx, callback, password):
+ self.sign_tx_with_password(tx, callback, password)
+
+ def sign_tx_with_password(self, tx, callback, password):
+ '''Sign the transaction in a separate thread. When done, calls
+ the callback with a success code of True or False.
+ '''
+ def on_success(result):
+ callback(True)
+ def on_failure(exc_info):
+ self.on_error(exc_info)
+ callback(False)
+ on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success, on_failure) or on_success
+ if self.tx_external_keypairs:
+ # can sign directly
+ task = partial(Transaction.sign, tx, self.tx_external_keypairs)
+ else:
+ task = partial(self.wallet.sign_transaction, tx, password)
+ msg = _('Signing transaction...')
+ WaitingDialog(self, msg, task, on_success, on_failure)
+
+ def broadcast_transaction(self, tx, tx_desc):
+
+ def broadcast_thread():
+ # non-GUI thread
+ pr = self.payment_request
+ if pr and pr.has_expired():
+ self.payment_request = None
+ return False, _("Payment request has expired")
+ status, msg = self.network.broadcast_transaction(tx)
+ if pr and status is True:
+ self.invoices.set_paid(pr, tx.txid())
+ self.invoices.save()
+ self.payment_request = None
+ refund_address = self.wallet.get_receiving_addresses()[0]
+ ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
+ if ack_status:
+ msg = ack_msg
+ return status, msg
+
+ # Capture current TL window; override might be removed on return
+ parent = self.top_level_window(lambda win: isinstance(win, MessageBoxMixin))
+
+ def broadcast_done(result):
+ # GUI thread
+ if result:
+ status, msg = result
+ if status:
+ if tx_desc is not None and tx.is_complete():
+ self.wallet.set_label(tx.txid(), tx_desc)
+ parent.show_message(_('Payment sent.') + '\n' + msg)
+ self.invoice_list.update()
+ self.do_clear()
+ else:
+ parent.show_error(msg)
+
+ WaitingDialog(self, _('Broadcasting transaction...'),
+ broadcast_thread, broadcast_done, self.on_error)
+
+ def query_choice(self, msg, choices):
+ # Needed by QtHandler for hardware wallets
+ dialog = WindowModalDialog(self.top_level_window())
+ clayout = ChoicesLayout(msg, choices)
+ vbox = QVBoxLayout(dialog)
+ vbox.addLayout(clayout.layout())
+ vbox.addLayout(Buttons(OkButton(dialog)))
+ if not dialog.exec_():
+ return None
+ return clayout.selected_index()
+
+ def lock_amount(self, b):
+ self.amount_e.setFrozen(b)
+ self.max_button.setEnabled(not b)
+
+ def prepare_for_payment_request(self):
+ self.show_send_tab()
+ self.payto_e.is_pr = True
+ for e in [self.payto_e, self.message_e]:
+ e.setFrozen(True)
+ self.lock_amount(True)
+ self.payto_e.setText(_("please wait..."))
+ return True
+
+ def delete_invoice(self, key):
+ self.invoices.remove(key)
+ self.invoice_list.update()
+
+ def payment_request_ok(self):
+ pr = self.payment_request
+ key = self.invoices.add(pr)
+ status = self.invoices.get_status(key)
+ self.invoice_list.update()
+ if status == PR_PAID:
+ self.show_message("invoice already paid")
+ self.do_clear()
+ self.payment_request = None
+ return
+ self.payto_e.is_pr = True
+ if not pr.has_expired():
+ self.payto_e.setGreen()
+ else:
+ self.payto_e.setExpired()
+ self.payto_e.setText(pr.get_requestor())
+ self.amount_e.setText(format_satoshis_plain(pr.get_amount(), self.decimal_point))
+ self.message_e.setText(pr.get_memo())
+ # signal to set fee
+ self.amount_e.textEdited.emit("")
+
+ def payment_request_error(self):
+ self.show_message(self.payment_request.error)
+ self.payment_request = None
+ self.do_clear()
+
+ def on_pr(self, request):
+ self.payment_request = request
+ if self.payment_request.verify(self.contacts):
+ self.payment_request_ok_signal.emit()
+ else:
+ self.payment_request_error_signal.emit()
+
+ def pay_to_URI(self, URI):
+ if not URI:
+ return
+ try:
+ out = util.parse_URI(URI, self.on_pr)
+ except BaseException as e:
+ self.show_error(_('Invalid bitcoin URI:') + '\n' + str(e))
+ return
+ self.show_send_tab()
+ r = out.get('r')
+ sig = out.get('sig')
+ name = out.get('name')
+ if r or (name and sig):
+ self.prepare_for_payment_request()
+ return
+ address = out.get('address')
+ amount = out.get('amount')
+ label = out.get('label')
+ message = out.get('message')
+ # use label as description (not BIP21 compliant)
+ if label and not message:
+ message = label
+ if address:
+ self.payto_e.setText(address)
+ if message:
+ self.message_e.setText(message)
+ if amount:
+ self.amount_e.setAmount(amount)
+ self.amount_e.textEdited.emit("")
+
+
+ def do_clear(self):
+ self.is_max = False
+ self.not_enough_funds = False
+ self.payment_request = None
+ self.payto_e.is_pr = False
+ for e in [self.payto_e, self.message_e, self.amount_e, self.fiat_send_e,
+ self.fee_e, self.feerate_e]:
+ e.setText('')
+ e.setFrozen(False)
+ self.fee_slider.activate()
+ self.feerate_e.setAmount(self.config.fee_per_byte())
+ self.size_e.setAmount(0)
+ self.feerounding_icon.setVisible(False)
+ self.set_pay_from([])
+ self.tx_external_keypairs = {}
+ self.update_status()
+ run_hook('do_clear', self)
+
+ def set_frozen_state(self, addrs, freeze):
+ self.wallet.set_frozen_state(addrs, freeze)
+ self.address_list.update()
+ self.utxo_list.update()
+ self.update_fee()
+
+ def create_list_tab(self, l, toolbar=None):
+ w = QWidget()
+ w.searchable_list = l
+ vbox = QVBoxLayout()
+ w.setLayout(vbox)
+ vbox.setContentsMargins(0, 0, 0, 0)
+ vbox.setSpacing(0)
+ if toolbar:
+ vbox.addLayout(toolbar)
+ vbox.addWidget(l)
+ return w
+
+ def create_addresses_tab(self):
+ from .address_list import AddressList
+ self.address_list = l = AddressList(self)
+ toolbar = l.create_toolbar(self.config)
+ toolbar_shown = self.config.get('show_toolbar_addresses', False)
+ l.show_toolbar(toolbar_shown)
+ return self.create_list_tab(l, toolbar)
+
+ def create_utxo_tab(self):
+ from .utxo_list import UTXOList
+ self.utxo_list = l = UTXOList(self)
+ return self.create_list_tab(l)
+
+ def create_contacts_tab(self):
+ from .contact_list import ContactList
+ self.contact_list = l = ContactList(self)
+ return self.create_list_tab(l)
+
+ def remove_address(self, addr):
+ if self.question(_("Do you want to remove {} from your wallet?").format(addr)):
+ self.wallet.delete_address(addr)
+ self.need_update.set() # history, addresses, coins
+ self.clear_receive_tab()
+
+ def get_coins(self):
+ if self.pay_from:
+ return self.pay_from
+ else:
+ return self.wallet.get_spendable_coins(None, self.config)
+
+ def spend_coins(self, coins):
+ self.set_pay_from(coins)
+ self.show_send_tab()
+ self.update_fee()
+
+ def paytomany(self):
+ self.show_send_tab()
+ self.payto_e.paytomany()
+ msg = '\n'.join([
+ _('Enter a list of outputs in the \'Pay to\' field.'),
+ _('One output per line.'),
+ _('Format: address, amount'),
+ _('You may load a CSV file using the file icon.')
+ ])
+ self.show_message(msg, title=_('Pay to many'))
+
+ def payto_contacts(self, labels):
+ paytos = [self.get_contact_payto(label) for label in labels]
+ self.show_send_tab()
+ if len(paytos) == 1:
+ self.payto_e.setText(paytos[0])
+ self.amount_e.setFocus()
+ else:
+ text = "\n".join([payto + ", 0" for payto in paytos])
+ self.payto_e.setText(text)
+ self.payto_e.setFocus()
+
+ def set_contact(self, label, address):
+ if not is_address(address):
+ self.show_error(_('Invalid Address'))
+ self.contact_list.update() # Displays original unchanged value
+ return False
+ self.contacts[address] = ('address', label)
+ self.contact_list.update()
+ self.history_list.update()
+ self.update_completions()
+ return True
+
+ def delete_contacts(self, labels):
+ if not self.question(_("Remove {} from your list of contacts?")
+ .format(" + ".join(labels))):
+ return
+ for label in labels:
+ self.contacts.pop(label)
+ self.history_list.update()
+ self.contact_list.update()
+ self.update_completions()
+
+ def show_invoice(self, key):
+ pr = self.invoices.get(key)
+ if pr is None:
+ self.show_error('Cannot find payment request in wallet.')
+ return
+ pr.verify(self.contacts)
+ self.show_pr_details(pr)
+
+ def show_pr_details(self, pr):
+ key = pr.get_id()
+ d = WindowModalDialog(self, _("Invoice"))
+ vbox = QVBoxLayout(d)
+ grid = QGridLayout()
+ grid.addWidget(QLabel(_("Requestor") + ':'), 0, 0)
+ grid.addWidget(QLabel(pr.get_requestor()), 0, 1)
+ grid.addWidget(QLabel(_("Amount") + ':'), 1, 0)
+ outputs_str = '\n'.join(map(lambda x: self.format_amount(x[2])+ self.base_unit() + ' @ ' + x[1], pr.get_outputs()))
+ grid.addWidget(QLabel(outputs_str), 1, 1)
+ expires = pr.get_expiration_date()
+ grid.addWidget(QLabel(_("Memo") + ':'), 2, 0)
+ grid.addWidget(QLabel(pr.get_memo()), 2, 1)
+ grid.addWidget(QLabel(_("Signature") + ':'), 3, 0)
+ grid.addWidget(QLabel(pr.get_verify_status()), 3, 1)
+ if expires:
+ grid.addWidget(QLabel(_("Expires") + ':'), 4, 0)
+ grid.addWidget(QLabel(format_time(expires)), 4, 1)
+ vbox.addLayout(grid)
+ def do_export():
+ fn = self.getSaveFileName(_("Save invoice to file"), "*.bip70")
+ if not fn:
+ return
+ with open(fn, 'wb') as f:
+ data = f.write(pr.raw)
+ self.show_message(_('Invoice saved as' + ' ' + fn))
+ exportButton = EnterButton(_('Save'), do_export)
+ def do_delete():
+ if self.question(_('Delete invoice?')):
+ self.invoices.remove(key)
+ self.history_list.update()
+ self.invoice_list.update()
+ d.close()
+ deleteButton = EnterButton(_('Delete'), do_delete)
+ vbox.addLayout(Buttons(exportButton, deleteButton, CloseButton(d)))
+ d.exec_()
+
+ def do_pay_invoice(self, key):
+ pr = self.invoices.get(key)
+ self.payment_request = pr
+ self.prepare_for_payment_request()
+ pr.error = None # this forces verify() to re-run
+ if pr.verify(self.contacts):
+ self.payment_request_ok()
+ else:
+ self.payment_request_error()
+
+ def create_console_tab(self):
+ from .console import Console
+ self.console = console = Console()
+ return console
+
+ def update_console(self):
+ console = self.console
+ console.history = self.config.get("console-history",[])
+ console.history_index = len(console.history)
+
+ console.updateNamespace({'wallet' : self.wallet,
+ 'network' : self.network,
+ 'plugins' : self.gui_object.plugins,
+ 'window': self})
+ console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
+
+ c = commands.Commands(self.config, self.wallet, self.network, lambda: self.console.set_json(True))
+ methods = {}
+ def mkfunc(f, method):
+ return lambda *args: f(method, args, self.password_dialog)
+ for m in dir(c):
+ if m[0]=='_' or m in ['network','wallet']: continue
+ methods[m] = mkfunc(c._run, m)
+
+ console.updateNamespace(methods)
+
+ def create_status_bar(self):
+
+ sb = QStatusBar()
+ sb.setFixedHeight(35)
+ qtVersion = qVersion()
+
+ self.balance_label = QLabel("")
+ self.balance_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
+ self.balance_label.setStyleSheet("""QLabel { padding: 0 }""")
+ sb.addWidget(self.balance_label)
+
+ self.search_box = QLineEdit()
+ self.search_box.textChanged.connect(self.do_search)
+ self.search_box.hide()
+ sb.addPermanentWidget(self.search_box)
+
+ self.lock_icon = QIcon()
+ self.password_button = StatusBarButton(self.lock_icon, _("Password"), self.change_password_dialog )
+ sb.addPermanentWidget(self.password_button)
+
+ sb.addPermanentWidget(StatusBarButton(QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
+ self.seed_button = StatusBarButton(QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
+ sb.addPermanentWidget(self.seed_button)
+ self.status_button = StatusBarButton(QIcon(":icons/status_disconnected.png"), _("Network"), lambda: self.gui_object.show_network_dialog(self))
+ sb.addPermanentWidget(self.status_button)
+ run_hook('create_status_bar', sb)
+ self.setStatusBar(sb)
+
+ def update_lock_icon(self):
+ icon = QIcon(":icons/lock.png") if self.wallet.has_password() else QIcon(":icons/unlock.png")
+ self.password_button.setIcon(icon)
+
+ def update_buttons_on_seed(self):
+ self.seed_button.setVisible(self.wallet.has_seed())
+ self.password_button.setVisible(self.wallet.may_have_password())
+ self.send_button.setVisible(not self.wallet.is_watching_only())
+
+ def change_password_dialog(self):
+ from electrum.storage import STO_EV_XPUB_PW
+ if self.wallet.get_available_storage_encryption_version() == STO_EV_XPUB_PW:
+ from .password_dialog import ChangePasswordDialogForHW
+ d = ChangePasswordDialogForHW(self, self.wallet)
+ ok, encrypt_file = d.run()
+ if not ok:
+ return
+
+ try:
+ hw_dev_pw = self.wallet.keystore.get_password_for_storage_encryption()
+ except UserCancelled:
+ return
+ except BaseException as e:
+ traceback.print_exc(file=sys.stderr)
+ self.show_error(str(e))
+ return
+ old_password = hw_dev_pw if self.wallet.has_password() else None
+ new_password = hw_dev_pw if encrypt_file else None
+ else:
+ from .password_dialog import ChangePasswordDialogForSW
+ d = ChangePasswordDialogForSW(self, self.wallet)
+ ok, old_password, new_password, encrypt_file = d.run()
+
+ if not ok:
+ return
+ try:
+ self.wallet.update_password(old_password, new_password, encrypt_file)
+ except InvalidPassword as e:
+ self.show_error(str(e))
+ return
+ except BaseException:
+ traceback.print_exc(file=sys.stdout)
+ self.show_error(_('Failed to update password'))
+ return
+ msg = _('Password was updated successfully') if self.wallet.has_password() else _('Password is disabled, this wallet is not protected')
+ self.show_message(msg, title=_("Success"))
+ self.update_lock_icon()
+
+ def toggle_search(self):
+ tab = self.tabs.currentWidget()
+ #if hasattr(tab, 'searchable_list'):
+ # tab.searchable_list.toggle_toolbar()
+ #return
+ self.search_box.setHidden(not self.search_box.isHidden())
+ if not self.search_box.isHidden():
+ self.search_box.setFocus(1)
+ else:
+ self.do_search('')
+
+ def do_search(self, t):
+ tab = self.tabs.currentWidget()
+ if hasattr(tab, 'searchable_list'):
+ tab.searchable_list.filter(t)
+
+ def new_contact_dialog(self):
+ d = WindowModalDialog(self, _("New Contact"))
+ vbox = QVBoxLayout(d)
+ vbox.addWidget(QLabel(_('New Contact') + ':'))
+ grid = QGridLayout()
+ line1 = QLineEdit()
+ line1.setFixedWidth(280)
+ line2 = QLineEdit()
+ line2.setFixedWidth(280)
+ grid.addWidget(QLabel(_("Address")), 1, 0)
+ grid.addWidget(line1, 1, 1)
+ grid.addWidget(QLabel(_("Name")), 2, 0)
+ grid.addWidget(line2, 2, 1)
+ vbox.addLayout(grid)
+ vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))
+ if d.exec_():
+ self.set_contact(line2.text(), line1.text())
+
+ def show_master_public_keys(self):
+ dialog = WindowModalDialog(self, _("Wallet Information"))
+ dialog.setMinimumSize(500, 100)
+ mpk_list = self.wallet.get_master_public_keys()
+ vbox = QVBoxLayout()
+ wallet_type = self.wallet.storage.get('wallet_type', '')
+ grid = QGridLayout()
+ basename = os.path.basename(self.wallet.storage.path)
+ grid.addWidget(QLabel(_("Wallet name")+ ':'), 0, 0)
+ grid.addWidget(QLabel(basename), 0, 1)
+ grid.addWidget(QLabel(_("Wallet type")+ ':'), 1, 0)
+ grid.addWidget(QLabel(wallet_type), 1, 1)
+ grid.addWidget(QLabel(_("Script type")+ ':'), 2, 0)
+ grid.addWidget(QLabel(self.wallet.txin_type), 2, 1)
+ vbox.addLayout(grid)
+ if self.wallet.is_deterministic():
+ mpk_text = ShowQRTextEdit()
+ mpk_text.setMaximumHeight(150)
+ mpk_text.addCopyButton(self.app)
+ def show_mpk(index):
+ mpk_text.setText(mpk_list[index])
+ # only show the combobox in case multiple accounts are available
+ if len(mpk_list) > 1:
+ def label(key):
+ if isinstance(self.wallet, Multisig_Wallet):
+ return _("cosigner") + ' ' + str(key+1)
+ return ''
+ labels = [label(i) for i in range(len(mpk_list))]
+ on_click = lambda clayout: show_mpk(clayout.selected_index())
+ labels_clayout = ChoicesLayout(_("Master Public Keys"), labels, on_click)
+ vbox.addLayout(labels_clayout.layout())
+ else:
+ vbox.addWidget(QLabel(_("Master Public Key")))
+ show_mpk(0)
+ vbox.addWidget(mpk_text)
+ vbox.addStretch(1)
+ vbox.addLayout(Buttons(CloseButton(dialog)))
+ dialog.setLayout(vbox)
+ dialog.exec_()
+
+ def remove_wallet(self):
+ if self.question('\n'.join([
+ _('Delete wallet file?'),
+ "%s"%self.wallet.storage.path,
+ _('If your wallet contains funds, make sure you have saved its seed.')])):
+ self._delete_wallet()
+
+ @protected
+ def _delete_wallet(self, password):
+ wallet_path = self.wallet.storage.path
+ basename = os.path.basename(wallet_path)
+ self.gui_object.daemon.stop_wallet(wallet_path)
+ self.close()
+ os.unlink(wallet_path)
+ self.show_error(_("Wallet removed: {}").format(basename))
+
+ @protected
+ def show_seed_dialog(self, password):
+ if not self.wallet.has_seed():
+ self.show_message(_('This wallet has no seed'))
+ return
+ keystore = self.wallet.get_keystore()
+ try:
+ seed = keystore.get_seed(password)
+ passphrase = keystore.get_passphrase(password)
+ except BaseException as e:
+ self.show_error(str(e))
+ return
+ from .seed_dialog import SeedDialog
+ d = SeedDialog(self, seed, passphrase)
+ d.exec_()
+
+ def show_qrcode(self, data, title = _("QR code"), parent=None):
+ if not data:
+ return
+ d = QRDialog(data, parent or self, title)
+ d.exec_()
+
+ @protected
+ def show_private_key(self, address, password):
+ if not address:
+ return
+ try:
+ pk, redeem_script = self.wallet.export_private_key(address, password)
+ except Exception as e:
+ traceback.print_exc(file=sys.stdout)
+ self.show_message(str(e))
+ return
+ xtype = bitcoin.deserialize_privkey(pk)[0]
+ d = WindowModalDialog(self, _("Private key"))
+ d.setMinimumSize(600, 150)
+ vbox = QVBoxLayout()
+ vbox.addWidget(QLabel(_("Address") + ': ' + address))
+ vbox.addWidget(QLabel(_("Script type") + ': ' + xtype))
+ vbox.addWidget(QLabel(_("Private key") + ':'))
+ keys_e = ShowQRTextEdit(text=pk)
+ keys_e.addCopyButton(self.app)
+ vbox.addWidget(keys_e)
+ if redeem_script:
+ vbox.addWidget(QLabel(_("Redeem Script") + ':'))
+ rds_e = ShowQRTextEdit(text=redeem_script)
+ rds_e.addCopyButton(self.app)
+ vbox.addWidget(rds_e)
+ vbox.addLayout(Buttons(CloseButton(d)))
+ d.setLayout(vbox)
+ d.exec_()
+
+ msg_sign = _("Signing with an address actually means signing with the corresponding "
+ "private key, and verifying with the corresponding public key. The "
+ "address you have entered does not have a unique public key, so these "
+ "operations cannot be performed.") + '\n\n' + \
+ _('The operation is undefined. Not just in Electrum, but in general.')
+
+ @protected
+ def do_sign(self, address, message, signature, password):
+ address = address.text().strip()
+ message = message.toPlainText().strip()
+ if not bitcoin.is_address(address):
+ self.show_message(_('Invalid Bitcoin address.'))
+ return
+ if self.wallet.is_watching_only():
+ self.show_message(_('This is a watching-only wallet.'))
+ return
+ if not self.wallet.is_mine(address):
+ self.show_message(_('Address not in wallet.'))
+ return
+ txin_type = self.wallet.get_txin_type(address)
+ if txin_type not in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']:
+ self.show_message(_('Cannot sign messages with this type of address:') + \
+ ' ' + txin_type + '\n\n' + self.msg_sign)
+ return
+ task = partial(self.wallet.sign_message, address, message, password)
+
+ def show_signed_message(sig):
+ try:
+ signature.setText(base64.b64encode(sig).decode('ascii'))
+ except RuntimeError:
+ # (signature) wrapped C/C++ object has been deleted
+ pass
+
+ self.wallet.thread.add(task, on_success=show_signed_message)
+
+ def do_verify(self, address, message, signature):
+ address = address.text().strip()
+ message = message.toPlainText().strip().encode('utf-8')
+ if not bitcoin.is_address(address):
+ self.show_message(_('Invalid Bitcoin address.'))
+ return
+ try:
+ # This can throw on invalid base64
+ sig = base64.b64decode(str(signature.toPlainText()))
+ verified = ecc.verify_message_with_address(address, sig, message)
+ except Exception as e:
+ verified = False
+ if verified:
+ self.show_message(_("Signature verified"))
+ else:
+ self.show_error(_("Wrong signature"))
+
+ def sign_verify_message(self, address=''):
+ d = WindowModalDialog(self, _('Sign/verify Message'))
+ d.setMinimumSize(610, 290)
+
+ layout = QGridLayout(d)
+
+ message_e = QTextEdit()
+ layout.addWidget(QLabel(_('Message')), 1, 0)
+ layout.addWidget(message_e, 1, 1)
+ layout.setRowStretch(2,3)
+
+ address_e = QLineEdit()
+ address_e.setText(address)
+ layout.addWidget(QLabel(_('Address')), 2, 0)
+ layout.addWidget(address_e, 2, 1)
+
+ signature_e = QTextEdit()
+ layout.addWidget(QLabel(_('Signature')), 3, 0)
+ layout.addWidget(signature_e, 3, 1)
+ layout.setRowStretch(3,1)
+
+ hbox = QHBoxLayout()
+
+ b = QPushButton(_("Sign"))
+ b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
+ hbox.addWidget(b)
+
+ b = QPushButton(_("Verify"))
+ b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
+ hbox.addWidget(b)
+
+ b = QPushButton(_("Close"))
+ b.clicked.connect(d.accept)
+ hbox.addWidget(b)
+ layout.addLayout(hbox, 4, 1)
+ d.exec_()
+
+ @protected
+ def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
+ if self.wallet.is_watching_only():
+ self.show_message(_('This is a watching-only wallet.'))
+ return
+ cyphertext = encrypted_e.toPlainText()
+ task = partial(self.wallet.decrypt_message, pubkey_e.text(), cyphertext, password)
+
+ def setText(text):
+ try:
+ message_e.setText(text.decode('utf-8'))
+ except RuntimeError:
+ # (message_e) wrapped C/C++ object has been deleted
+ pass
+
+ self.wallet.thread.add(task, on_success=setText)
+
+ def do_encrypt(self, message_e, pubkey_e, encrypted_e):
+ message = message_e.toPlainText()
+ message = message.encode('utf-8')
+ try:
+ public_key = ecc.ECPubkey(bfh(pubkey_e.text()))
+ except BaseException as e:
+ traceback.print_exc(file=sys.stdout)
+ self.show_warning(_('Invalid Public key'))
+ return
+ encrypted = public_key.encrypt_message(message)
+ encrypted_e.setText(encrypted.decode('ascii'))
+
+ def encrypt_message(self, address=''):
+ d = WindowModalDialog(self, _('Encrypt/decrypt Message'))
+ d.setMinimumSize(610, 490)
+
+ layout = QGridLayout(d)
+
+ message_e = QTextEdit()
+ layout.addWidget(QLabel(_('Message')), 1, 0)
+ layout.addWidget(message_e, 1, 1)
+ layout.setRowStretch(2,3)
+
+ pubkey_e = QLineEdit()
+ if address:
+ pubkey = self.wallet.get_public_key(address)
+ pubkey_e.setText(pubkey)
+ layout.addWidget(QLabel(_('Public key')), 2, 0)
+ layout.addWidget(pubkey_e, 2, 1)
+
+ encrypted_e = QTextEdit()
+ layout.addWidget(QLabel(_('Encrypted')), 3, 0)
+ layout.addWidget(encrypted_e, 3, 1)
+ layout.setRowStretch(3,1)
+
+ hbox = QHBoxLayout()
+ b = QPushButton(_("Encrypt"))
+ b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
+ hbox.addWidget(b)
+
+ b = QPushButton(_("Decrypt"))
+ b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
+ hbox.addWidget(b)
+
+ b = QPushButton(_("Close"))
+ b.clicked.connect(d.accept)
+ hbox.addWidget(b)
+
+ layout.addLayout(hbox, 4, 1)
+ d.exec_()
+
+ def password_dialog(self, msg=None, parent=None):
+ from .password_dialog import PasswordDialog
+ parent = parent or self
+ d = PasswordDialog(parent, msg)
+ return d.run()
+
+ def tx_from_text(self, txt):
+ from electrum.transaction import tx_from_str
+ try:
+ tx = tx_from_str(txt)
+ return Transaction(tx)
+ except BaseException as e:
+ self.show_critical(_("Electrum was unable to parse your transaction") + ":\n" + str(e))
+ return
+
+ def read_tx_from_qrcode(self):
+ from electrum import qrscanner
+ try:
+ data = qrscanner.scan_barcode(self.config.get_video_device())
+ except BaseException as e:
+ self.show_error(str(e))
+ return
+ if not data:
+ return
+ # if the user scanned a bitcoin URI
+ if str(data).startswith("bitcoin:"):
+ self.pay_to_URI(data)
+ return
+ # else if the user scanned an offline signed tx
+ try:
+ data = bh2u(bitcoin.base_decode(data, length=None, base=43))
+ except BaseException as e:
+ self.show_error((_('Could not decode QR code')+':\n{}').format(e))
+ return
+ tx = self.tx_from_text(data)
+ if not tx:
+ return
+ self.show_transaction(tx)
+
+ def read_tx_from_file(self):
+ fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
+ if not fileName:
+ return
+ try:
+ with open(fileName, "r") as f:
+ file_content = f.read()
+ except (ValueError, IOError, os.error) as reason:
+ self.show_critical(_("Electrum was unable to open your transaction file") + "\n" + str(reason), title=_("Unable to read file or no transaction found"))
+ return
+ return self.tx_from_text(file_content)
+
+ def do_process_from_text(self):
+ text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
+ if not text:
+ return
+ tx = self.tx_from_text(text)
+ if tx:
+ self.show_transaction(tx)
+
+ def do_process_from_file(self):
+ tx = self.read_tx_from_file()
+ if tx:
+ self.show_transaction(tx)
+
+ def do_process_from_txid(self):
+ from electrum import transaction
+ txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
+ if ok and txid:
+ txid = str(txid).strip()
+ try:
+ r = self.network.get_transaction(txid)
+ except BaseException as e:
+ self.show_message(str(e))
+ return
+ tx = transaction.Transaction(r)
+ self.show_transaction(tx)
+
+ @protected
+ def export_privkeys_dialog(self, password):
+ if self.wallet.is_watching_only():
+ self.show_message(_("This is a watching-only wallet"))
+ return
+
+ if isinstance(self.wallet, Multisig_Wallet):
+ self.show_message(_('WARNING: This is a multi-signature wallet.') + '\n' +
+ _('It cannot be "backed up" by simply exporting these private keys.'))
+
+ d = WindowModalDialog(self, _('Private keys'))
+ d.setMinimumSize(980, 300)
+ vbox = QVBoxLayout(d)
+
+ msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
+ _("Exposing a single private key can compromise your entire wallet!"),
+ _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
+ vbox.addWidget(QLabel(msg))
+
+ e = QTextEdit()
+ e.setReadOnly(True)
+ vbox.addWidget(e)
+
+ defaultname = 'electrum-private-keys.csv'
+ select_msg = _('Select file to export your private keys to')
+ hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
+ vbox.addLayout(hbox)
+
+ b = OkButton(d, _('Export'))
+ b.setEnabled(False)
+ vbox.addLayout(Buttons(CancelButton(d), b))
+
+ private_keys = {}
+ addresses = self.wallet.get_addresses()
+ done = False
+ cancelled = False
+ def privkeys_thread():
+ for addr in addresses:
+ time.sleep(0.1)
+ if done or cancelled:
+ break
+ privkey = self.wallet.export_private_key(addr, password)[0]
+ private_keys[addr] = privkey
+ self.computing_privkeys_signal.emit()
+ if not cancelled:
+ self.computing_privkeys_signal.disconnect()
+ self.show_privkeys_signal.emit()
+
+ def show_privkeys():
+ s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
+ e.setText(s)
+ b.setEnabled(True)
+ self.show_privkeys_signal.disconnect()
+ nonlocal done
+ done = True
+
+ def on_dialog_closed(*args):
+ nonlocal done
+ nonlocal cancelled
+ if not done:
+ cancelled = True
+ self.computing_privkeys_signal.disconnect()
+ self.show_privkeys_signal.disconnect()
+
+ self.computing_privkeys_signal.connect(lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
+ self.show_privkeys_signal.connect(show_privkeys)
+ d.finished.connect(on_dialog_closed)
+ threading.Thread(target=privkeys_thread).start()
+
+ if not d.exec_():
+ done = True
+ return
+
+ filename = filename_e.text()
+ if not filename:
+ return
+
+ try:
+ self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
+ except (IOError, os.error) as reason:
+ txt = "\n".join([
+ _("Electrum was unable to produce a private key-export."),
+ str(reason)
+ ])
+ self.show_critical(txt, title=_("Unable to create csv"))
+
+ except Exception as e:
+ self.show_message(str(e))
+ return
+
+ self.show_message(_("Private keys exported."))
+
+ def do_export_privkeys(self, fileName, pklist, is_csv):
+ with open(fileName, "w+") as f:
+ if is_csv:
+ transaction = csv.writer(f)
+ transaction.writerow(["address", "private_key"])
+ for addr, pk in pklist.items():
+ transaction.writerow(["%34s"%addr,pk])
+ else:
+ import json
+ f.write(json.dumps(pklist, indent = 4))
+
+ def do_import_labels(self):
+ def import_labels(path):
+ def _validate(data):
+ return data # TODO
+
+ def import_labels_assign(data):
+ for key, value in data.items():
+ self.wallet.set_label(key, value)
+ import_meta(path, _validate, import_labels_assign)
+
+ def on_import():
+ self.need_update.set()
+ import_meta_gui(self, _('labels'), import_labels, on_import)
+
+ def do_export_labels(self):
+ def export_labels(filename):
+ export_meta(self.wallet.labels, filename)
+ export_meta_gui(self, _('labels'), export_labels)
+
+ def sweep_key_dialog(self):
+ d = WindowModalDialog(self, title=_('Sweep private keys'))
+ d.setMinimumSize(600, 300)
+
+ vbox = QVBoxLayout(d)
+
+ hbox_top = QHBoxLayout()
+ hbox_top.addWidget(QLabel(_("Enter private keys:")))
+ hbox_top.addWidget(InfoButton(WIF_HELP_TEXT), alignment=Qt.AlignRight)
+ vbox.addLayout(hbox_top)
+
+ keys_e = ScanQRTextEdit(allow_multi=True)
+ keys_e.setTabChangesFocus(True)
+ vbox.addWidget(keys_e)
+
+ addresses = self.wallet.get_unused_addresses()
+ if not addresses:
+ try:
+ addresses = self.wallet.get_receiving_addresses()
+ except AttributeError:
+ addresses = self.wallet.get_addresses()
+ h, address_e = address_field(addresses)
+ vbox.addLayout(h)
+
+ vbox.addStretch(1)
+ button = OkButton(d, _('Sweep'))
+ vbox.addLayout(Buttons(CancelButton(d), button))
+ button.setEnabled(False)
+
+ def get_address():
+ addr = str(address_e.text()).strip()
+ if bitcoin.is_address(addr):
+ return addr
+
+ def get_pk():
+ text = str(keys_e.toPlainText())
+ return keystore.get_private_keys(text)
+
+ f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
+ on_address = lambda text: address_e.setStyleSheet((ColorScheme.DEFAULT if get_address() else ColorScheme.RED).as_stylesheet())
+ keys_e.textChanged.connect(f)
+ address_e.textChanged.connect(f)
+ address_e.textChanged.connect(on_address)
+ if not d.exec_():
+ return
+ from electrum.wallet import sweep_preparations
+ try:
+ self.do_clear()
+ coins, keypairs = sweep_preparations(get_pk(), self.network)
+ self.tx_external_keypairs = keypairs
+ self.spend_coins(coins)
+ self.payto_e.setText(get_address())
+ self.spend_max()
+ self.payto_e.setFrozen(True)
+ self.amount_e.setFrozen(True)
+ except BaseException as e:
+ self.show_message(str(e))
+ return
+ self.warn_if_watching_only()
+
+ def _do_import(self, title, header_layout, func):
+ text = text_dialog(self, title, header_layout, _('Import'), allow_multi=True)
+ if not text:
+ return
+ bad = []
+ good = []
+ for key in str(text).split():
+ try:
+ addr = func(key)
+ good.append(addr)
+ except BaseException as e:
+ bad.append(key)
+ continue
+ if good:
+ self.show_message(_("The following addresses were added") + ':\n' + '\n'.join(good))
+ if bad:
+ self.show_critical(_("The following inputs could not be imported") + ':\n'+ '\n'.join(bad))
+ self.address_list.update()
+ self.history_list.update()
+
+ def import_addresses(self):
+ if not self.wallet.can_import_address():
+ return
+ title, msg = _('Import addresses'), _("Enter addresses")+':'
+ self._do_import(title, msg, self.wallet.import_address)
+
+ @protected
+ def do_import_privkey(self, password):
+ if not self.wallet.can_import_privkey():
+ return
+ title = _('Import private keys')
+ header_layout = QHBoxLayout()
+ header_layout.addWidget(QLabel(_("Enter private keys")+':'))
+ header_layout.addWidget(InfoButton(WIF_HELP_TEXT), alignment=Qt.AlignRight)
+ self._do_import(title, header_layout, lambda x: self.wallet.import_private_key(x, password))
+
+ def update_fiat(self):
+ b = self.fx and self.fx.is_enabled()
+ self.fiat_send_e.setVisible(b)
+ self.fiat_receive_e.setVisible(b)
+ self.history_list.refresh_headers()
+ self.history_list.update()
+ self.address_list.refresh_headers()
+ self.address_list.update()
+ self.update_status()
+
+ def settings_dialog(self):
+ self.need_restart = False
+ d = WindowModalDialog(self, _('Preferences'))
+ vbox = QVBoxLayout()
+ tabs = QTabWidget()
+ gui_widgets = []
+ fee_widgets = []
+ tx_widgets = []
+ id_widgets = []
+
+ # language
+ lang_help = _('Select which language is used in the GUI (after restart).')
+ lang_label = HelpLabel(_('Language') + ':', lang_help)
+ lang_combo = QComboBox()
+ from electrum.i18n import languages
+ lang_combo.addItems(list(languages.values()))
+ lang_keys = list(languages.keys())
+ lang_cur_setting = self.config.get("language", '')
+ try:
+ index = lang_keys.index(lang_cur_setting)
+ except ValueError: # not in list
+ index = 0
+ lang_combo.setCurrentIndex(index)
+ if not self.config.is_modifiable('language'):
+ for w in [lang_combo, lang_label]: w.setEnabled(False)
+ def on_lang(x):
+ lang_request = list(languages.keys())[lang_combo.currentIndex()]
+ if lang_request != self.config.get('language'):
+ self.config.set_key("language", lang_request, True)
+ self.need_restart = True
+ lang_combo.currentIndexChanged.connect(on_lang)
+ gui_widgets.append((lang_label, lang_combo))
+
+ nz_help = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
+ nz_label = HelpLabel(_('Zeros after decimal point') + ':', nz_help)
+ nz = QSpinBox()
+ nz.setMinimum(0)
+ nz.setMaximum(self.decimal_point)
+ nz.setValue(self.num_zeros)
+ if not self.config.is_modifiable('num_zeros'):
+ for w in [nz, nz_label]: w.setEnabled(False)
+ def on_nz():
+ value = nz.value()
+ if self.num_zeros != value:
+ self.num_zeros = value
+ self.config.set_key('num_zeros', value, True)
+ self.history_list.update()
+ self.address_list.update()
+ nz.valueChanged.connect(on_nz)
+ gui_widgets.append((nz_label, nz))
+
+ msg = '\n'.join([
+ _('Time based: fee rate is based on average confirmation time estimates'),
+ _('Mempool based: fee rate is targeting a depth in the memory pool')
+ ]
+ )
+ fee_type_label = HelpLabel(_('Fee estimation') + ':', msg)
+ fee_type_combo = QComboBox()
+ fee_type_combo.addItems([_('Static'), _('ETA'), _('Mempool')])
+ fee_type_combo.setCurrentIndex((2 if self.config.use_mempool_fees() else 1) if self.config.is_dynfee() else 0)
+ def on_fee_type(x):
+ self.config.set_key('mempool_fees', x==2)
+ self.config.set_key('dynamic_fees', x>0)
+ self.fee_slider.update()
+ fee_type_combo.currentIndexChanged.connect(on_fee_type)
+ fee_widgets.append((fee_type_label, fee_type_combo))
+
+ feebox_cb = QCheckBox(_('Edit fees manually'))
+ feebox_cb.setChecked(self.config.get('show_fee', False))
+ feebox_cb.setToolTip(_("Show fee edit box in send tab."))
+ def on_feebox(x):
+ self.config.set_key('show_fee', x == Qt.Checked)
+ self.fee_adv_controls.setVisible(bool(x))
+ feebox_cb.stateChanged.connect(on_feebox)
+ fee_widgets.append((feebox_cb, None))
+
+ use_rbf_cb = QCheckBox(_('Use Replace-By-Fee'))
+ use_rbf_cb.setChecked(self.config.get('use_rbf', True))
+ use_rbf_cb.setToolTip(
+ _('If you check this box, your transactions will be marked as non-final,') + '\n' + \
+ _('and you will have the possibility, while they are unconfirmed, to replace them with transactions that pay higher fees.') + '\n' + \
+ _('Note that some merchants do not accept non-final transactions until they are confirmed.'))
+ def on_use_rbf(x):
+ self.config.set_key('use_rbf', x == Qt.Checked)
+ use_rbf_cb.stateChanged.connect(on_use_rbf)
+ fee_widgets.append((use_rbf_cb, None))
+
+ msg = _('OpenAlias record, used to receive coins and to sign payment requests.') + '\n\n'\
+ + _('The following alias providers are available:') + '\n'\
+ + '\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\n\n'\
+ + 'For more information, see https://openalias.org'
+ alias_label = HelpLabel(_('OpenAlias') + ':', msg)
+ alias = self.config.get('alias','')
+ alias_e = QLineEdit(alias)
+ def set_alias_color():
+ if not self.config.get('alias'):
+ alias_e.setStyleSheet("")
+ return
+ if self.alias_info:
+ alias_addr, alias_name, validated = self.alias_info
+ alias_e.setStyleSheet((ColorScheme.GREEN if validated else ColorScheme.RED).as_stylesheet(True))
+ else:
+ alias_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True))
+ def on_alias_edit():
+ alias_e.setStyleSheet("")
+ alias = str(alias_e.text())
+ self.config.set_key('alias', alias, True)
+ if alias:
+ self.fetch_alias()
+ set_alias_color()
+ self.alias_received_signal.connect(set_alias_color)
+ alias_e.editingFinished.connect(on_alias_edit)
+ id_widgets.append((alias_label, alias_e))
+
+ # SSL certificate
+ msg = ' '.join([
+ _('SSL certificate used to sign payment requests.'),
+ _('Use setconfig to set ssl_chain and ssl_privkey.'),
+ ])
+ if self.config.get('ssl_privkey') or self.config.get('ssl_chain'):
+ try:
+ SSL_identity = paymentrequest.check_ssl_config(self.config)
+ SSL_error = None
+ except BaseException as e:
+ SSL_identity = "error"
+ SSL_error = str(e)
+ else:
+ SSL_identity = ""
+ SSL_error = None
+ SSL_id_label = HelpLabel(_('SSL certificate') + ':', msg)
+ SSL_id_e = QLineEdit(SSL_identity)
+ SSL_id_e.setStyleSheet((ColorScheme.RED if SSL_error else ColorScheme.GREEN).as_stylesheet(True) if SSL_identity else '')
+ if SSL_error:
+ SSL_id_e.setToolTip(SSL_error)
+ SSL_id_e.setReadOnly(True)
+ id_widgets.append((SSL_id_label, SSL_id_e))
+
+ units = base_units_list
+ msg = (_('Base unit of your wallet.')
+ + '\n1 BTC = 1000 mBTC. 1 mBTC = 1000 bits. 1 bit = 100 sat.\n'
+ + _('This setting affects the Send tab, and all balance related fields.'))
+ unit_label = HelpLabel(_('Base unit') + ':', msg)
+ unit_combo = QComboBox()
+ unit_combo.addItems(units)
+ unit_combo.setCurrentIndex(units.index(self.base_unit()))
+ def on_unit(x, nz):
+ unit_result = units[unit_combo.currentIndex()]
+ if self.base_unit() == unit_result:
+ return
+ edits = self.amount_e, self.fee_e, self.receive_amount_e
+ amounts = [edit.get_amount() for edit in edits]
+ self.decimal_point = base_unit_name_to_decimal_point(unit_result)
+ self.config.set_key('decimal_point', self.decimal_point, True)
+ nz.setMaximum(self.decimal_point)
+ self.history_list.update()
+ self.request_list.update()
+ self.address_list.update()
+ for edit, amount in zip(edits, amounts):
+ edit.setAmount(amount)
+ self.update_status()
+ unit_combo.currentIndexChanged.connect(lambda x: on_unit(x, nz))
+ gui_widgets.append((unit_label, unit_combo))
+
+ block_explorers = sorted(util.block_explorer_info().keys())
+ msg = _('Choose which online block explorer to use for functions that open a web browser')
+ block_ex_label = HelpLabel(_('Online Block Explorer') + ':', msg)
+ block_ex_combo = QComboBox()
+ block_ex_combo.addItems(block_explorers)
+ block_ex_combo.setCurrentIndex(block_ex_combo.findText(util.block_explorer(self.config)))
+ def on_be(x):
+ be_result = block_explorers[block_ex_combo.currentIndex()]
+ self.config.set_key('block_explorer', be_result, True)
+ block_ex_combo.currentIndexChanged.connect(on_be)
+ gui_widgets.append((block_ex_label, block_ex_combo))
+
+ from electrum import qrscanner
+ system_cameras = qrscanner._find_system_cameras()
+ qr_combo = QComboBox()
+ qr_combo.addItem("Default","default")
+ for camera, device in system_cameras.items():
+ qr_combo.addItem(camera, device)
+ #combo.addItem("Manually specify a device", config.get("video_device"))
+ index = qr_combo.findData(self.config.get("video_device"))
+ qr_combo.setCurrentIndex(index)
+ msg = _("Install the zbar package to enable this.")
+ qr_label = HelpLabel(_('Video Device') + ':', msg)
+ qr_combo.setEnabled(qrscanner.libzbar is not None)
+ on_video_device = lambda x: self.config.set_key("video_device", qr_combo.itemData(x), True)
+ qr_combo.currentIndexChanged.connect(on_video_device)
+ gui_widgets.append((qr_label, qr_combo))
+
+ colortheme_combo = QComboBox()
+ colortheme_combo.addItem(_('Light'), 'default')
+ colortheme_combo.addItem(_('Dark'), 'dark')
+ index = colortheme_combo.findData(self.config.get('qt_gui_color_theme', 'default'))
+ colortheme_combo.setCurrentIndex(index)
+ colortheme_label = QLabel(_('Color theme') + ':')
+ def on_colortheme(x):
+ self.config.set_key('qt_gui_color_theme', colortheme_combo.itemData(x), True)
+ self.need_restart = True
+ colortheme_combo.currentIndexChanged.connect(on_colortheme)
+ gui_widgets.append((colortheme_label, colortheme_combo))
+
+ usechange_cb = QCheckBox(_('Use change addresses'))
+ usechange_cb.setChecked(self.wallet.use_change)
+ if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
+ def on_usechange(x):
+ usechange_result = x == Qt.Checked
+ if self.wallet.use_change != usechange_result:
+ self.wallet.use_change = usechange_result
+ self.wallet.storage.put('use_change', self.wallet.use_change)
+ multiple_cb.setEnabled(self.wallet.use_change)
+ usechange_cb.stateChanged.connect(on_usechange)
+ usechange_cb.setToolTip(_('Using change addresses makes it more difficult for other people to track your transactions.'))
+ tx_widgets.append((usechange_cb, None))
+
+ def on_multiple(x):
+ multiple = x == Qt.Checked
+ if self.wallet.multiple_change != multiple:
+ self.wallet.multiple_change = multiple
+ self.wallet.storage.put('multiple_change', multiple)
+ multiple_change = self.wallet.multiple_change
+ multiple_cb = QCheckBox(_('Use multiple change addresses'))
+ multiple_cb.setEnabled(self.wallet.use_change)
+ multiple_cb.setToolTip('\n'.join([
+ _('In some cases, use up to 3 change addresses in order to break '
+ 'up large coin amounts and obfuscate the recipient address.'),
+ _('This may result in higher transactions fees.')
+ ]))
+ multiple_cb.setChecked(multiple_change)
+ multiple_cb.stateChanged.connect(on_multiple)
+ tx_widgets.append((multiple_cb, None))
+
+ def fmt_docs(key, klass):
+ lines = [ln.lstrip(" ") for ln in klass.__doc__.split("\n")]
+ return '\n'.join([key, "", " ".join(lines)])
+
+ choosers = sorted(coinchooser.COIN_CHOOSERS.keys())
+ if len(choosers) > 1:
+ chooser_name = coinchooser.get_name(self.config)
+ msg = _('Choose coin (UTXO) selection method. The following are available:\n\n')
+ msg += '\n\n'.join(fmt_docs(*item) for item in coinchooser.COIN_CHOOSERS.items())
+ chooser_label = HelpLabel(_('Coin selection') + ':', msg)
+ chooser_combo = QComboBox()
+ chooser_combo.addItems(choosers)
+ i = choosers.index(chooser_name) if chooser_name in choosers else 0
+ chooser_combo.setCurrentIndex(i)
+ def on_chooser(x):
+ chooser_name = choosers[chooser_combo.currentIndex()]
+ self.config.set_key('coin_chooser', chooser_name)
+ chooser_combo.currentIndexChanged.connect(on_chooser)
+ tx_widgets.append((chooser_label, chooser_combo))
+
+ def on_unconf(x):
+ self.config.set_key('confirmed_only', bool(x))
+ conf_only = self.config.get('confirmed_only', False)
+ unconf_cb = QCheckBox(_('Spend only confirmed coins'))
+ unconf_cb.setToolTip(_('Spend only confirmed inputs.'))
+ unconf_cb.setChecked(conf_only)
+ unconf_cb.stateChanged.connect(on_unconf)
+ tx_widgets.append((unconf_cb, None))
+
+ def on_outrounding(x):
+ self.config.set_key('coin_chooser_output_rounding', bool(x))
+ enable_outrounding = self.config.get('coin_chooser_output_rounding', False)
+ outrounding_cb = QCheckBox(_('Enable output value rounding'))
+ outrounding_cb.setToolTip(
+ _('Set the value of the change output so that it has similar precision to the other outputs.') + '\n' +
+ _('This might improve your privacy somewhat.') + '\n' +
+ _('If enabled, at most 100 satoshis might be lost due to this, per transaction.'))
+ outrounding_cb.setChecked(enable_outrounding)
+ outrounding_cb.stateChanged.connect(on_outrounding)
+ tx_widgets.append((outrounding_cb, None))
+
+ # Fiat Currency
+ hist_checkbox = QCheckBox()
+ hist_capgains_checkbox = QCheckBox()
+ fiat_address_checkbox = QCheckBox()
+ ccy_combo = QComboBox()
+ ex_combo = QComboBox()
+
+ def update_currencies():
+ if not self.fx: return
+ currencies = sorted(self.fx.get_currencies(self.fx.get_history_config()))
+ ccy_combo.clear()
+ ccy_combo.addItems([_('None')] + currencies)
+ if self.fx.is_enabled():
+ ccy_combo.setCurrentIndex(ccy_combo.findText(self.fx.get_currency()))
+
+ def update_history_cb():
+ if not self.fx: return
+ hist_checkbox.setChecked(self.fx.get_history_config())
+ hist_checkbox.setEnabled(self.fx.is_enabled())
+
+ def update_fiat_address_cb():
+ if not self.fx: return
+ fiat_address_checkbox.setChecked(self.fx.get_fiat_address_config())
+
+ def update_history_capgains_cb():
+ if not self.fx: return
+ hist_capgains_checkbox.setChecked(self.fx.get_history_capital_gains_config())
+ hist_capgains_checkbox.setEnabled(hist_checkbox.isChecked())
+
+ def update_exchanges():
+ if not self.fx: return
+ b = self.fx.is_enabled()
+ ex_combo.setEnabled(b)
+ if b:
+ h = self.fx.get_history_config()
+ c = self.fx.get_currency()
+ exchanges = self.fx.get_exchanges_by_ccy(c, h)
+ else:
+ exchanges = self.fx.get_exchanges_by_ccy('USD', False)
+ ex_combo.clear()
+ ex_combo.addItems(sorted(exchanges))
+ ex_combo.setCurrentIndex(ex_combo.findText(self.fx.config_exchange()))
+
+ def on_currency(hh):
+ if not self.fx: return
+ b = bool(ccy_combo.currentIndex())
+ ccy = str(ccy_combo.currentText()) if b else None
+ self.fx.set_enabled(b)
+ if b and ccy != self.fx.ccy:
+ self.fx.set_currency(ccy)
+ update_history_cb()
+ update_exchanges()
+ self.update_fiat()
+
+ def on_exchange(idx):
+ exchange = str(ex_combo.currentText())
+ if self.fx and self.fx.is_enabled() and exchange and exchange != self.fx.exchange.name():
+ self.fx.set_exchange(exchange)
+
+ def on_history(checked):
+ if not self.fx: return
+ self.fx.set_history_config(checked)
+ update_exchanges()
+ self.history_list.refresh_headers()
+ if self.fx.is_enabled() and checked:
+ # reset timeout to get historical rates
+ self.fx.timeout = 0
+ update_history_capgains_cb()
+
+ def on_history_capgains(checked):
+ if not self.fx: return
+ self.fx.set_history_capital_gains_config(checked)
+ self.history_list.refresh_headers()
+
+ def on_fiat_address(checked):
+ if not self.fx: return
+ self.fx.set_fiat_address_config(checked)
+ self.address_list.refresh_headers()
+ self.address_list.update()
+
+ update_currencies()
+ update_history_cb()
+ update_history_capgains_cb()
+ update_fiat_address_cb()
+ update_exchanges()
+ ccy_combo.currentIndexChanged.connect(on_currency)
+ hist_checkbox.stateChanged.connect(on_history)
+ hist_capgains_checkbox.stateChanged.connect(on_history_capgains)
+ fiat_address_checkbox.stateChanged.connect(on_fiat_address)
+ ex_combo.currentIndexChanged.connect(on_exchange)
+
+ fiat_widgets = []
+ fiat_widgets.append((QLabel(_('Fiat currency')), ccy_combo))
+ fiat_widgets.append((QLabel(_('Show history rates')), hist_checkbox))
+ fiat_widgets.append((QLabel(_('Show capital gains in history')), hist_capgains_checkbox))
+ fiat_widgets.append((QLabel(_('Show Fiat balance for addresses')), fiat_address_checkbox))
+ fiat_widgets.append((QLabel(_('Source')), ex_combo))
+
+ tabs_info = [
+ (fee_widgets, _('Fees')),
+ (tx_widgets, _('Transactions')),
+ (gui_widgets, _('Appearance')),
+ (fiat_widgets, _('Fiat')),
+ (id_widgets, _('Identity')),
+ ]
+ for widgets, name in tabs_info:
+ tab = QWidget()
+ grid = QGridLayout(tab)
+ grid.setColumnStretch(0,1)
+ for a,b in widgets:
+ i = grid.rowCount()
+ if b:
+ if a:
+ grid.addWidget(a, i, 0)
+ grid.addWidget(b, i, 1)
+ else:
+ grid.addWidget(a, i, 0, 1, 2)
+ tabs.addTab(tab, name)
+
+ vbox.addWidget(tabs)
+ vbox.addStretch(1)
+ vbox.addLayout(Buttons(CloseButton(d)))
+ d.setLayout(vbox)
+
+ # run the dialog
+ d.exec_()
+
+ if self.fx:
+ self.fx.timeout = 0
+
+ self.alias_received_signal.disconnect(set_alias_color)
+
+ run_hook('close_settings_dialog')
+ if self.need_restart:
+ self.show_warning(_('Please restart Electrum to activate the new GUI settings'), title=_('Success'))
+
+
+ def closeEvent(self, event):
+ # It seems in some rare cases this closeEvent() is called twice
+ if not self.cleaned_up:
+ self.cleaned_up = True
+ self.clean_up()
+ event.accept()
+
+ def clean_up(self):
+ self.wallet.thread.stop()
+ if self.network:
+ self.network.unregister_callback(self.on_network)
+ self.config.set_key("is_maximized", self.isMaximized())
+ if not self.isMaximized():
+ g = self.geometry()
+ self.wallet.storage.put("winpos-qt", [g.left(),g.top(),
+ g.width(),g.height()])
+ self.config.set_key("console-history", self.console.history[-50:],
+ True)
+ if self.qr_window:
+ self.qr_window.close()
+ self.close_wallet()
+ self.gui_object.close_window(self)
+
+ def plugins_dialog(self):
+ self.pluginsdialog = d = WindowModalDialog(self, _('Electrum Plugins'))
+
+ plugins = self.gui_object.plugins
+
+ vbox = QVBoxLayout(d)
+
+ # plugins
+ scroll = QScrollArea()
+ scroll.setEnabled(True)
+ scroll.setWidgetResizable(True)
+ scroll.setMinimumSize(400,250)
+ vbox.addWidget(scroll)
+
+ w = QWidget()
+ scroll.setWidget(w)
+ w.setMinimumHeight(plugins.count() * 35)
+
+ grid = QGridLayout()
+ grid.setColumnStretch(0,1)
+ w.setLayout(grid)
+
+ settings_widgets = {}
+
+ def enable_settings_widget(p, name, i):
+ widget = settings_widgets.get(name)
+ if not widget and p and p.requires_settings():
+ widget = settings_widgets[name] = p.settings_widget(d)
+ grid.addWidget(widget, i, 1)
+ if widget:
+ widget.setEnabled(bool(p and p.is_enabled()))
+
+ def do_toggle(cb, name, i):
+ p = plugins.toggle(name)
+ cb.setChecked(bool(p))
+ enable_settings_widget(p, name, i)
+ run_hook('init_qt', self.gui_object)
+
+ for i, descr in enumerate(plugins.descriptions.values()):
+ full_name = descr['__name__']
+ prefix, _separator, name = full_name.rpartition('.')
+ p = plugins.get(name)
+ if descr.get('registers_keystore'):
+ continue
+ try:
+ cb = QCheckBox(descr['fullname'])
+ plugin_is_loaded = p is not None
+ cb_enabled = (not plugin_is_loaded and plugins.is_available(name, self.wallet)
+ or plugin_is_loaded and p.can_user_disable())
+ cb.setEnabled(cb_enabled)
+ cb.setChecked(plugin_is_loaded and p.is_enabled())
+ grid.addWidget(cb, i, 0)
+ enable_settings_widget(p, name, i)
+ cb.clicked.connect(partial(do_toggle, cb, name, i))
+ msg = descr['description']
+ if descr.get('requires'):
+ msg += '\n\n' + _('Requires') + ':\n' + '\n'.join(map(lambda x: x[1], descr.get('requires')))
+ grid.addWidget(HelpButton(msg), i, 2)
+ except Exception:
+ self.print_msg("error: cannot display plugin", name)
+ traceback.print_exc(file=sys.stdout)
+ grid.setRowStretch(len(plugins.descriptions.values()), 1)
+ vbox.addLayout(Buttons(CloseButton(d)))
+ d.exec_()
+
+ def cpfp(self, parent_tx, new_tx):
+ total_size = parent_tx.estimated_size() + new_tx.estimated_size()
+ d = WindowModalDialog(self, _('Child Pays for Parent'))
+ vbox = QVBoxLayout(d)
+ msg = (
+ "A CPFP is a transaction that sends an unconfirmed output back to "
+ "yourself, with a high fee. The goal is to have miners confirm "
+ "the parent transaction in order to get the fee attached to the "
+ "child transaction.")
+ vbox.addWidget(WWLabel(_(msg)))
+ msg2 = ("The proposed fee is computed using your "
+ "fee/kB settings, applied to the total size of both child and "
+ "parent transactions. After you broadcast a CPFP transaction, "
+ "it is normal to see a new unconfirmed transaction in your history.")
+ vbox.addWidget(WWLabel(_(msg2)))
+ grid = QGridLayout()
+ grid.addWidget(QLabel(_('Total size') + ':'), 0, 0)
+ grid.addWidget(QLabel('%d bytes'% total_size), 0, 1)
+ max_fee = new_tx.output_value()
+ grid.addWidget(QLabel(_('Input amount') + ':'), 1, 0)
+ grid.addWidget(QLabel(self.format_amount(max_fee) + ' ' + self.base_unit()), 1, 1)
+ output_amount = QLabel('')
+ grid.addWidget(QLabel(_('Output amount') + ':'), 2, 0)
+ grid.addWidget(output_amount, 2, 1)
+ fee_e = BTCAmountEdit(self.get_decimal_point)
+ # FIXME with dyn fees, without estimates, there are all kinds of crashes here
+ def f(x):
+ a = max_fee - fee_e.get_amount()
+ output_amount.setText((self.format_amount(a) + ' ' + self.base_unit()) if a else '')
+ fee_e.textChanged.connect(f)
+ fee = self.config.fee_per_kb() * total_size / 1000
+ fee_e.setAmount(fee)
+ grid.addWidget(QLabel(_('Fee' + ':')), 3, 0)
+ grid.addWidget(fee_e, 3, 1)
+ def on_rate(dyn, pos, fee_rate):
+ fee = fee_rate * total_size / 1000
+ fee = min(max_fee, fee)
+ fee_e.setAmount(fee)
+ fee_slider = FeeSlider(self, self.config, on_rate)
+ fee_slider.update()
+ grid.addWidget(fee_slider, 4, 1)
+ vbox.addLayout(grid)
+ vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))
+ if not d.exec_():
+ return
+ fee = fee_e.get_amount()
+ if fee > max_fee:
+ self.show_error(_('Max fee exceeded'))
+ return
+ new_tx = self.wallet.cpfp(parent_tx, fee)
+ new_tx.set_rbf(True)
+ self.show_transaction(new_tx)
+
+ def bump_fee_dialog(self, tx):
+ is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
+ if fee is None:
+ self.show_error(_("Can't bump fee: unknown fee for original transaction."))
+ return
+ tx_label = self.wallet.get_label(tx.txid())
+ tx_size = tx.estimated_size()
+ d = WindowModalDialog(self, _('Bump Fee'))
+ vbox = QVBoxLayout(d)
+ vbox.addWidget(QLabel(_('Current fee') + ': %s'% self.format_amount(fee) + ' ' + self.base_unit()))
+ vbox.addWidget(QLabel(_('New fee' + ':')))
+
+ fee_e = BTCAmountEdit(self.get_decimal_point)
+ fee_e.setAmount(fee * 1.5)
+ vbox.addWidget(fee_e)
+
+ def on_rate(dyn, pos, fee_rate):
+ fee = fee_rate * tx_size / 1000
+ fee_e.setAmount(fee)
+ fee_slider = FeeSlider(self, self.config, on_rate)
+ vbox.addWidget(fee_slider)
+ cb = QCheckBox(_('Final'))
+ vbox.addWidget(cb)
+ vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))
+ if not d.exec_():
+ return
+ is_final = cb.isChecked()
+ new_fee = fee_e.get_amount()
+ delta = new_fee - fee
+ if delta < 0:
+ self.show_error("fee too low")
+ return
+ try:
+ new_tx = self.wallet.bump_fee(tx, delta)
+ except CannotBumpFee as e:
+ self.show_error(str(e))
+ return
+ if is_final:
+ new_tx.set_rbf(False)
+ self.show_transaction(new_tx, tx_label)
+
+ def save_transaction_into_wallet(self, tx):
+ win = self.top_level_window()
+ try:
+ if not self.wallet.add_transaction(tx.txid(), tx):
+ win.show_error(_("Transaction could not be saved.") + "\n" +
+ _("It conflicts with current history."))
+ return False
+ except AddTransactionException as e:
+ win.show_error(e)
+ return False
+ else:
+ self.wallet.save_transactions(write=True)
+ # need to update at least: history_list, utxo_list, address_list
+ self.need_update.set()
+ msg = (_("Transaction added to wallet history.") + '\n\n' +
+ _("Note: this is an offline transaction, if you want the network "
+ "to see it, you need to broadcast it."))
+ win.msg_box(QPixmap(":icons/offline_tx.png"), None, _('Success'), msg)
+ return True
DIR diff --git a/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py
DIR diff --git a/electrum/gui/qt/password_dialog.py b/electrum/gui/qt/password_dialog.py
t@@ -0,0 +1,305 @@
+#!/usr/bin/env python
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2013 ecdsa@github
+#
+# 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.
+
+from PyQt5.QtCore import Qt
+from PyQt5.QtGui import *
+from PyQt5.QtWidgets import *
+from electrum.i18n import _
+from .util import *
+import re
+import math
+
+from electrum.plugin import run_hook
+
+def check_password_strength(password):
+
+ '''
+ Check the strength of the password entered by the user and return back the same
+ :param password: password entered by user in New Password
+ :return: password strength Weak or Medium or Strong
+ '''
+ password = password
+ n = math.log(len(set(password)))
+ num = re.search("[0-9]", password) is not None and re.match("^[0-9]*$", password) is None
+ caps = password != password.upper() and password != password.lower()
+ extra = re.match("^[a-zA-Z0-9]*$", password) is None
+ score = len(password)*( n + caps + num + extra)/20
+ password_strength = {0:"Weak",1:"Medium",2:"Strong",3:"Very Strong"}
+ return password_strength[min(3, int(score))]
+
+
+PW_NEW, PW_CHANGE, PW_PASSPHRASE = range(0, 3)
+
+
+class PasswordLayout(object):
+
+ titles = [_("Enter Password"), _("Change Password"), _("Enter Passphrase")]
+
+ def __init__(self, wallet, msg, kind, OK_button, force_disable_encrypt_cb=False):
+ self.wallet = wallet
+
+ self.pw = QLineEdit()
+ self.pw.setEchoMode(2)
+ self.new_pw = QLineEdit()
+ self.new_pw.setEchoMode(2)
+ self.conf_pw = QLineEdit()
+ self.conf_pw.setEchoMode(2)
+ self.kind = kind
+ self.OK_button = OK_button
+
+ vbox = QVBoxLayout()
+ label = QLabel(msg + "\n")
+ label.setWordWrap(True)
+
+ grid = QGridLayout()
+ grid.setSpacing(8)
+ grid.setColumnMinimumWidth(0, 150)
+ grid.setColumnMinimumWidth(1, 100)
+ grid.setColumnStretch(1,1)
+
+ if kind == PW_PASSPHRASE:
+ vbox.addWidget(label)
+ msgs = [_('Passphrase:'), _('Confirm Passphrase:')]
+ else:
+ logo_grid = QGridLayout()
+ logo_grid.setSpacing(8)
+ logo_grid.setColumnMinimumWidth(0, 70)
+ logo_grid.setColumnStretch(1,1)
+
+ logo = QLabel()
+ logo.setAlignment(Qt.AlignCenter)
+
+ logo_grid.addWidget(logo, 0, 0)
+ logo_grid.addWidget(label, 0, 1, 1, 2)
+ vbox.addLayout(logo_grid)
+
+ m1 = _('New Password:') if kind == PW_CHANGE else _('Password:')
+ msgs = [m1, _('Confirm Password:')]
+ if wallet and wallet.has_password():
+ grid.addWidget(QLabel(_('Current Password:')), 0, 0)
+ grid.addWidget(self.pw, 0, 1)
+ lockfile = ":icons/lock.png"
+ else:
+ lockfile = ":icons/unlock.png"
+ logo.setPixmap(QPixmap(lockfile).scaledToWidth(36, mode=Qt.SmoothTransformation))
+
+ grid.addWidget(QLabel(msgs[0]), 1, 0)
+ grid.addWidget(self.new_pw, 1, 1)
+
+ grid.addWidget(QLabel(msgs[1]), 2, 0)
+ grid.addWidget(self.conf_pw, 2, 1)
+ vbox.addLayout(grid)
+
+ # Password Strength Label
+ if kind != PW_PASSPHRASE:
+ self.pw_strength = QLabel()
+ grid.addWidget(self.pw_strength, 3, 0, 1, 2)
+ self.new_pw.textChanged.connect(self.pw_changed)
+
+ self.encrypt_cb = QCheckBox(_('Encrypt wallet file'))
+ self.encrypt_cb.setEnabled(False)
+ grid.addWidget(self.encrypt_cb, 4, 0, 1, 2)
+ self.encrypt_cb.setVisible(kind != PW_PASSPHRASE)
+
+ def enable_OK():
+ ok = self.new_pw.text() == self.conf_pw.text()
+ OK_button.setEnabled(ok)
+ self.encrypt_cb.setEnabled(ok and bool(self.new_pw.text())
+ and not force_disable_encrypt_cb)
+ self.new_pw.textChanged.connect(enable_OK)
+ self.conf_pw.textChanged.connect(enable_OK)
+
+ self.vbox = vbox
+
+ def title(self):
+ return self.titles[self.kind]
+
+ def layout(self):
+ return self.vbox
+
+ def pw_changed(self):
+ password = self.new_pw.text()
+ if password:
+ colors = {"Weak":"Red", "Medium":"Blue", "Strong":"Green",
+ "Very Strong":"Green"}
+ strength = check_password_strength(password)
+ label = (_("Password Strength") + ": " + "<font color="
+ + colors[strength] + ">" + strength + "</font>")
+ else:
+ label = ""
+ self.pw_strength.setText(label)
+
+ def old_password(self):
+ if self.kind == PW_CHANGE:
+ return self.pw.text() or None
+ return None
+
+ def new_password(self):
+ pw = self.new_pw.text()
+ # Empty passphrases are fine and returned empty.
+ if pw == "" and self.kind != PW_PASSPHRASE:
+ pw = None
+ return pw
+
+
+class PasswordLayoutForHW(object):
+
+ def __init__(self, wallet, msg, kind, OK_button):
+ self.wallet = wallet
+
+ self.kind = kind
+ self.OK_button = OK_button
+
+ vbox = QVBoxLayout()
+ label = QLabel(msg + "\n")
+ label.setWordWrap(True)
+
+ grid = QGridLayout()
+ grid.setSpacing(8)
+ grid.setColumnMinimumWidth(0, 150)
+ grid.setColumnMinimumWidth(1, 100)
+ grid.setColumnStretch(1,1)
+
+ logo_grid = QGridLayout()
+ logo_grid.setSpacing(8)
+ logo_grid.setColumnMinimumWidth(0, 70)
+ logo_grid.setColumnStretch(1,1)
+
+ logo = QLabel()
+ logo.setAlignment(Qt.AlignCenter)
+
+ logo_grid.addWidget(logo, 0, 0)
+ logo_grid.addWidget(label, 0, 1, 1, 2)
+ vbox.addLayout(logo_grid)
+
+ if wallet and wallet.has_storage_encryption():
+ lockfile = ":icons/lock.png"
+ else:
+ lockfile = ":icons/unlock.png"
+ logo.setPixmap(QPixmap(lockfile).scaledToWidth(36, mode=Qt.SmoothTransformation))
+
+ vbox.addLayout(grid)
+
+ self.encrypt_cb = QCheckBox(_('Encrypt wallet file'))
+ grid.addWidget(self.encrypt_cb, 1, 0, 1, 2)
+
+ self.vbox = vbox
+
+ def title(self):
+ return _("Toggle Encryption")
+
+ def layout(self):
+ return self.vbox
+
+
+class ChangePasswordDialogBase(WindowModalDialog):
+
+ def __init__(self, parent, wallet):
+ WindowModalDialog.__init__(self, parent)
+ is_encrypted = wallet.has_storage_encryption()
+ OK_button = OkButton(self)
+
+ self.create_password_layout(wallet, is_encrypted, OK_button)
+
+ self.setWindowTitle(self.playout.title())
+ vbox = QVBoxLayout(self)
+ vbox.addLayout(self.playout.layout())
+ vbox.addStretch(1)
+ vbox.addLayout(Buttons(CancelButton(self), OK_button))
+ self.playout.encrypt_cb.setChecked(is_encrypted)
+
+ def create_password_layout(self, wallet, is_encrypted, OK_button):
+ raise NotImplementedError()
+
+
+class ChangePasswordDialogForSW(ChangePasswordDialogBase):
+
+ def __init__(self, parent, wallet):
+ ChangePasswordDialogBase.__init__(self, parent, wallet)
+ if not wallet.has_password():
+ self.playout.encrypt_cb.setChecked(True)
+
+ def create_password_layout(self, wallet, is_encrypted, OK_button):
+ if not wallet.has_password():
+ msg = _('Your wallet is not protected.')
+ msg += ' ' + _('Use this dialog to add a password to your wallet.')
+ else:
+ if not is_encrypted:
+ msg = _('Your bitcoins are password protected. However, your wallet file is not encrypted.')
+ else:
+ msg = _('Your wallet is password protected and encrypted.')
+ msg += ' ' + _('Use this dialog to change your password.')
+ self.playout = PasswordLayout(
+ wallet, msg, PW_CHANGE, OK_button,
+ force_disable_encrypt_cb=not wallet.can_have_keystore_encryption())
+
+ def run(self):
+ if not self.exec_():
+ return False, None, None, None
+ return True, self.playout.old_password(), self.playout.new_password(), self.playout.encrypt_cb.isChecked()
+
+
+class ChangePasswordDialogForHW(ChangePasswordDialogBase):
+
+ def __init__(self, parent, wallet):
+ ChangePasswordDialogBase.__init__(self, parent, wallet)
+
+ def create_password_layout(self, wallet, is_encrypted, OK_button):
+ if not is_encrypted:
+ msg = _('Your wallet file is NOT encrypted.')
+ else:
+ msg = _('Your wallet file is encrypted.')
+ msg += '\n' + _('Note: If you enable this setting, you will need your hardware device to open your wallet.')
+ msg += '\n' + _('Use this dialog to toggle encryption.')
+ self.playout = PasswordLayoutForHW(wallet, msg, PW_CHANGE, OK_button)
+
+ def run(self):
+ if not self.exec_():
+ return False, None
+ return True, self.playout.encrypt_cb.isChecked()
+
+
+class PasswordDialog(WindowModalDialog):
+
+ def __init__(self, parent=None, msg=None):
+ msg = msg or _('Please enter your password')
+ WindowModalDialog.__init__(self, parent, _("Enter Password"))
+ self.pw = pw = QLineEdit()
+ pw.setEchoMode(2)
+ vbox = QVBoxLayout()
+ vbox.addWidget(QLabel(msg))
+ grid = QGridLayout()
+ grid.setSpacing(8)
+ grid.addWidget(QLabel(_('Password')), 1, 0)
+ grid.addWidget(pw, 1, 1)
+ vbox.addLayout(grid)
+ vbox.addLayout(Buttons(CancelButton(self), OkButton(self)))
+ self.setLayout(vbox)
+ run_hook('password_dialog', pw, grid, 1)
+
+ def run(self):
+ if not self.exec_():
+ return
+ return self.pw.text()
DIR diff --git a/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py
DIR diff --git a/gui/qt/qrcodewidget.py b/electrum/gui/qt/qrcodewidget.py
DIR diff --git a/electrum/gui/qt/qrtextedit.py b/electrum/gui/qt/qrtextedit.py
t@@ -0,0 +1,76 @@
+
+from electrum.i18n import _
+from electrum.plugin import run_hook
+from PyQt5.QtGui import *
+from PyQt5.QtCore import *
+from PyQt5.QtWidgets import QFileDialog
+
+from .util import ButtonsTextEdit, MessageBoxMixin, ColorScheme
+
+
+class ShowQRTextEdit(ButtonsTextEdit):
+
+ def __init__(self, text=None):
+ ButtonsTextEdit.__init__(self, text)
+ self.setReadOnly(1)
+ self.addButton(":icons/qrcode.png", self.qr_show, _("Show as QR code"))
+
+ run_hook('show_text_edit', self)
+
+ def qr_show(self):
+ from .qrcodewidget import QRDialog
+ try:
+ s = str(self.toPlainText())
+ except:
+ s = self.toPlainText()
+ QRDialog(s).exec_()
+
+ def contextMenuEvent(self, e):
+ m = self.createStandardContextMenu()
+ m.addAction(_("Show as QR code"), self.qr_show)
+ m.exec_(e.globalPos())
+
+
+class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin):
+
+ def __init__(self, text="", allow_multi=False):
+ ButtonsTextEdit.__init__(self, text)
+ self.allow_multi = allow_multi
+ self.setReadOnly(0)
+ self.addButton(":icons/file.png", self.file_input, _("Read file"))
+ icon = ":icons/qrcode_white.png" if ColorScheme.dark_scheme else ":icons/qrcode.png"
+ self.addButton(icon, self.qr_input, _("Read QR code"))
+ run_hook('scan_text_edit', self)
+
+ def file_input(self):
+ fileName, __ = QFileDialog.getOpenFileName(self, 'select file')
+ if not fileName:
+ return
+ try:
+ with open(fileName, "r") as f:
+ data = f.read()
+ except BaseException as e:
+ self.show_error(_('Error opening file') + ':\n' + str(e))
+ else:
+ self.setText(data)
+
+ def qr_input(self):
+ from electrum import qrscanner, get_config
+ try:
+ data = qrscanner.scan_barcode(get_config().get_video_device())
+ except BaseException as e:
+ self.show_error(str(e))
+ data = ''
+ if not data:
+ data = ''
+ if self.allow_multi:
+ new_text = self.text() + data + '\n'
+ else:
+ new_text = data
+ self.setText(new_text)
+ return data
+
+ def contextMenuEvent(self, e):
+ m = self.createStandardContextMenu()
+ m.addAction(_("Read QR code"), self.qr_input)
+ m.exec_(e.globalPos())
DIR diff --git a/electrum/gui/qt/qrwindow.py b/electrum/gui/qt/qrwindow.py
t@@ -0,0 +1,89 @@
+#!/usr/bin/env python
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2014 Thomas Voegtlin
+#
+# 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 platform
+
+from PyQt5.QtCore import Qt
+from PyQt5.QtGui import *
+from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout, QLabel, QWidget
+
+from .qrcodewidget import QRCodeWidget
+from electrum.i18n import _
+
+if platform.system() == 'Windows':
+ MONOSPACE_FONT = 'Lucida Console'
+elif platform.system() == 'Darwin':
+ MONOSPACE_FONT = 'Monaco'
+else:
+ MONOSPACE_FONT = 'monospace'
+
+column_index = 4
+
+class QR_Window(QWidget):
+
+ def __init__(self, win):
+ QWidget.__init__(self)
+ self.win = win
+ self.setWindowTitle('Electrum - '+_('Payment Request'))
+ self.setMinimumSize(800, 250)
+ self.address = ''
+ self.label = ''
+ self.amount = 0
+ self.setFocusPolicy(Qt.NoFocus)
+
+ main_box = QHBoxLayout()
+
+ self.qrw = QRCodeWidget()
+ main_box.addWidget(self.qrw, 1)
+
+ vbox = QVBoxLayout()
+ main_box.addLayout(vbox)
+
+ self.address_label = QLabel("")
+ #self.address_label.setFont(QFont(MONOSPACE_FONT))
+ vbox.addWidget(self.address_label)
+
+ self.label_label = QLabel("")
+ vbox.addWidget(self.label_label)
+
+ self.amount_label = QLabel("")
+ vbox.addWidget(self.amount_label)
+
+ vbox.addStretch(1)
+ self.setLayout(main_box)
+
+
+ def set_content(self, address, amount, message, url):
+ address_text = "<span style='font-size: 18pt'>%s</span>" % address if address else ""
+ self.address_label.setText(address_text)
+ if amount:
+ amount = self.win.format_amount(amount)
+ amount_text = "<span style='font-size: 21pt'>%s</span> <span style='font-size: 16pt'>%s</span> " % (amount, self.win.base_unit())
+ else:
+ amount_text = ''
+ self.amount_label.setText(amount_text)
+ label_text = "<span style='font-size: 21pt'>%s</span>" % message if message else ""
+ self.label_label.setText(label_text)
+ self.qrw.setData(url)
DIR diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py
t@@ -0,0 +1,129 @@
+#!/usr/bin/env python
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2015 Thomas Voegtlin
+#
+# 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.
+
+from electrum.i18n import _
+from electrum.util import format_time, age
+from electrum.plugin import run_hook
+from electrum.paymentrequest import PR_UNKNOWN
+from PyQt5.QtGui import *
+from PyQt5.QtCore import *
+from PyQt5.QtWidgets import QTreeWidgetItem, QMenu
+from .util import MyTreeWidget, pr_tooltips, pr_icons
+
+
+class RequestList(MyTreeWidget):
+ filter_columns = [0, 1, 2, 3, 4] # Date, Account, Address, Description, Amount
+
+
+ def __init__(self, parent):
+ MyTreeWidget.__init__(self, parent, self.create_menu, [_('Date'), _('Address'), '', _('Description'), _('Amount'), _('Status')], 3)
+ self.currentItemChanged.connect(self.item_changed)
+ self.itemClicked.connect(self.item_changed)
+ self.setSortingEnabled(True)
+ self.setColumnWidth(0, 180)
+ self.hideColumn(1)
+
+ def item_changed(self, item):
+ if item is None:
+ return
+ if not item.isSelected():
+ return
+ addr = str(item.text(1))
+ req = self.wallet.receive_requests.get(addr)
+ if req is None:
+ self.update()
+ return
+ expires = age(req['time'] + req['exp']) if req.get('exp') else _('Never')
+ amount = req['amount']
+ message = self.wallet.labels.get(addr, '')
+ self.parent.receive_address_e.setText(addr)
+ self.parent.receive_message_e.setText(message)
+ self.parent.receive_amount_e.setAmount(amount)
+ self.parent.expires_combo.hide()
+ self.parent.expires_label.show()
+ self.parent.expires_label.setText(expires)
+ self.parent.new_request_button.setEnabled(True)
+
+ def on_update(self):
+ self.wallet = self.parent.wallet
+ # hide receive tab if no receive requests available
+ b = len(self.wallet.receive_requests) > 0
+ self.setVisible(b)
+ self.parent.receive_requests_label.setVisible(b)
+ if not b:
+ self.parent.expires_label.hide()
+ self.parent.expires_combo.show()
+
+ # update the receive address if necessary
+ current_address = self.parent.receive_address_e.text()
+ domain = self.wallet.get_receiving_addresses()
+ addr = self.wallet.get_unused_address()
+ if not current_address in domain and addr:
+ self.parent.set_receive_address(addr)
+ self.parent.new_request_button.setEnabled(addr != current_address)
+
+ # clear the list and fill it again
+ self.clear()
+ for req in self.wallet.get_sorted_requests(self.config):
+ address = req['address']
+ if address not in domain:
+ continue
+ timestamp = req.get('time', 0)
+ amount = req.get('amount')
+ expiration = req.get('exp', None)
+ message = req.get('memo', '')
+ date = format_time(timestamp)
+ status = req.get('status')
+ signature = req.get('sig')
+ requestor = req.get('name', '')
+ amount_str = self.parent.format_amount(amount) if amount else ""
+ item = QTreeWidgetItem([date, address, '', message, amount_str, pr_tooltips.get(status,'')])
+ if signature is not None:
+ item.setIcon(2, self.icon_cache.get(":icons/seal.png"))
+ item.setToolTip(2, 'signed by '+ requestor)
+ if status is not PR_UNKNOWN:
+ item.setIcon(6, self.icon_cache.get(pr_icons.get(status)))
+ self.addTopLevelItem(item)
+
+
+ def create_menu(self, position):
+ item = self.itemAt(position)
+ if not item:
+ return
+ addr = str(item.text(1))
+ req = self.wallet.receive_requests.get(addr)
+ if req is None:
+ self.update()
+ return
+ column = self.currentColumn()
+ column_title = self.headerItem().text(column)
+ column_data = item.text(column)
+ menu = QMenu(self)
+ menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
+ menu.addAction(_("Copy URI"), lambda: self.parent.view_and_paste('URI', '', self.parent.get_request_URI(addr)))
+ menu.addAction(_("Save as BIP70 file"), lambda: self.parent.export_payment_request(addr))
+ menu.addAction(_("Delete"), lambda: self.parent.delete_payment_request(addr))
+ run_hook('receive_list_menu', menu, addr)
+ menu.exec_(self.viewport().mapToGlobal(position))
DIR diff --git a/electrum/gui/qt/seed_dialog.py b/electrum/gui/qt/seed_dialog.py
t@@ -0,0 +1,211 @@
+#!/usr/bin/env python
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2013 ecdsa@github
+#
+# 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.
+
+from electrum.i18n import _
+from electrum.mnemonic import Mnemonic
+import electrum.old_mnemonic
+from electrum.plugin import run_hook
+
+
+from .util import *
+from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit
+from .completion_text_edit import CompletionTextEdit
+
+
+def seed_warning_msg(seed):
+ return ''.join([
+ "<p>",
+ _("Please save these {0} words on paper (order is important). "),
+ _("This seed will allow you to recover your wallet in case "
+ "of computer failure."),
+ "</p>",
+ "<b>" + _("WARNING") + ":</b>",
+ "<ul>",
+ "<li>" + _("Never disclose your seed.") + "</li>",
+ "<li>" + _("Never type it on a website.") + "</li>",
+ "<li>" + _("Do not store it electronically.") + "</li>",
+ "</ul>"
+ ]).format(len(seed.split()))
+
+
+class SeedLayout(QVBoxLayout):
+
+ def seed_options(self):
+ dialog = QDialog()
+ vbox = QVBoxLayout(dialog)
+ if 'ext' in self.options:
+ cb_ext = QCheckBox(_('Extend this seed with custom words'))
+ cb_ext.setChecked(self.is_ext)
+ vbox.addWidget(cb_ext)
+ if 'bip39' in self.options:
+ def f(b):
+ self.is_seed = (lambda x: bool(x)) if b else self.saved_is_seed
+ self.is_bip39 = b
+ self.on_edit()
+ if b:
+ msg = ' '.join([
+ '<b>' + _('Warning') + ':</b> ',
+ _('BIP39 seeds can be imported in Electrum, so that users can access funds locked in other wallets.'),
+ _('However, we do not generate BIP39 seeds, because they do not meet our safety standard.'),
+ _('BIP39 seeds do not include a version number, which compromises compatibility with future software.'),
+ _('We do not guarantee that BIP39 imports will always be supported in Electrum.'),
+ ])
+ else:
+ msg = ''
+ self.seed_warning.setText(msg)
+ cb_bip39 = QCheckBox(_('BIP39 seed'))
+ cb_bip39.toggled.connect(f)
+ cb_bip39.setChecked(self.is_bip39)
+ vbox.addWidget(cb_bip39)
+ vbox.addLayout(Buttons(OkButton(dialog)))
+ if not dialog.exec_():
+ return None
+ self.is_ext = cb_ext.isChecked() if 'ext' in self.options else False
+ self.is_bip39 = cb_bip39.isChecked() if 'bip39' in self.options else False
+
+ def __init__(self, seed=None, title=None, icon=True, msg=None, options=None,
+ is_seed=None, passphrase=None, parent=None, for_seed_words=True):
+ QVBoxLayout.__init__(self)
+ self.parent = parent
+ self.options = options
+ if title:
+ self.addWidget(WWLabel(title))
+ if seed: # "read only", we already have the text
+ if for_seed_words:
+ self.seed_e = ButtonsTextEdit()
+ else: # e.g. xpub
+ self.seed_e = ShowQRTextEdit()
+ self.seed_e.setReadOnly(True)
+ self.seed_e.setText(seed)
+ else: # we expect user to enter text
+ assert for_seed_words
+ self.seed_e = CompletionTextEdit()
+ self.seed_e.setTabChangesFocus(False) # so that tab auto-completes
+ self.is_seed = is_seed
+ self.saved_is_seed = self.is_seed
+ self.seed_e.textChanged.connect(self.on_edit)
+ self.initialize_completer()
+
+ self.seed_e.setMaximumHeight(75)
+ hbox = QHBoxLayout()
+ if icon:
+ logo = QLabel()
+ logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(64, mode=Qt.SmoothTransformation))
+ logo.setMaximumWidth(60)
+ hbox.addWidget(logo)
+ hbox.addWidget(self.seed_e)
+ self.addLayout(hbox)
+ hbox = QHBoxLayout()
+ hbox.addStretch(1)
+ self.seed_type_label = QLabel('')
+ hbox.addWidget(self.seed_type_label)
+
+ # options
+ self.is_bip39 = False
+ self.is_ext = False
+ if options:
+ opt_button = EnterButton(_('Options'), self.seed_options)
+ hbox.addWidget(opt_button)
+ self.addLayout(hbox)
+ if passphrase:
+ hbox = QHBoxLayout()
+ passphrase_e = QLineEdit()
+ passphrase_e.setText(passphrase)
+ passphrase_e.setReadOnly(True)
+ hbox.addWidget(QLabel(_("Your seed extension is") + ':'))
+ hbox.addWidget(passphrase_e)
+ self.addLayout(hbox)
+ self.addStretch(1)
+ self.seed_warning = WWLabel('')
+ if msg:
+ self.seed_warning.setText(seed_warning_msg(seed))
+ self.addWidget(self.seed_warning)
+
+ def initialize_completer(self):
+ english_list = Mnemonic('en').wordlist
+ old_list = electrum.old_mnemonic.words
+ self.wordlist = english_list + list(set(old_list) - set(english_list)) #concat both lists
+ self.wordlist.sort()
+ self.completer = QCompleter(self.wordlist)
+ self.seed_e.set_completer(self.completer)
+
+ def get_seed(self):
+ text = self.seed_e.text()
+ return ' '.join(text.split())
+
+ def on_edit(self):
+ from electrum.bitcoin import seed_type
+ s = self.get_seed()
+ b = self.is_seed(s)
+ if not self.is_bip39:
+ t = seed_type(s)
+ label = _('Seed Type') + ': ' + t if t else ''
+ else:
+ from electrum.keystore import bip39_is_checksum_valid
+ is_checksum, is_wordlist = bip39_is_checksum_valid(s)
+ status = ('checksum: ' + ('ok' if is_checksum else 'failed')) if is_wordlist else 'unknown wordlist'
+ label = 'BIP39' + ' (%s)'%status
+ self.seed_type_label.setText(label)
+ self.parent.next_button.setEnabled(b)
+
+ # to account for bip39 seeds
+ for word in self.get_seed().split(" ")[:-1]:
+ if word not in self.wordlist:
+ self.seed_e.disable_suggestions()
+ return
+ self.seed_e.enable_suggestions()
+
+class KeysLayout(QVBoxLayout):
+ def __init__(self, parent=None, header_layout=None, is_valid=None, allow_multi=False):
+ QVBoxLayout.__init__(self)
+ self.parent = parent
+ self.is_valid = is_valid
+ self.text_e = ScanQRTextEdit(allow_multi=allow_multi)
+ self.text_e.textChanged.connect(self.on_edit)
+ if isinstance(header_layout, str):
+ self.addWidget(WWLabel(header_layout))
+ else:
+ self.addLayout(header_layout)
+ self.addWidget(self.text_e)
+
+ def get_text(self):
+ return self.text_e.text()
+
+ def on_edit(self):
+ b = self.is_valid(self.get_text())
+ self.parent.next_button.setEnabled(b)
+
+
+class SeedDialog(WindowModalDialog):
+
+ def __init__(self, parent, seed, passphrase):
+ WindowModalDialog.__init__(self, parent, ('Electrum - ' + _('Seed')))
+ self.setMinimumWidth(400)
+ vbox = QVBoxLayout(self)
+ title = _("Your wallet generation seed is:")
+ slayout = SeedLayout(title=title, seed=seed, msg=True, passphrase=passphrase)
+ vbox.addLayout(slayout)
+ run_hook('set_seed', seed, slayout.seed_e)
+ vbox.addLayout(Buttons(CloseButton(self)))
DIR diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py
t@@ -0,0 +1,328 @@
+#!/usr/bin/env python
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2012 thomasv@gitorious
+#
+# 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 copy
+import datetime
+import json
+import traceback
+
+from PyQt5.QtCore import *
+from PyQt5.QtGui import *
+from PyQt5.QtWidgets import *
+
+from electrum.bitcoin import base_encode
+from electrum.i18n import _
+from electrum.plugin import run_hook
+from electrum import simple_config
+
+from electrum.util import bfh
+from electrum.wallet import AddTransactionException
+from electrum.transaction import SerializationError
+
+from .util import *
+
+
+SAVE_BUTTON_ENABLED_TOOLTIP = _("Save transaction offline")
+SAVE_BUTTON_DISABLED_TOOLTIP = _("Please sign this transaction in order to save it")
+
+
+dialogs = [] # Otherwise python randomly garbage collects the dialogs...
+
+
+def show_transaction(tx, parent, desc=None, prompt_if_unsaved=False):
+ try:
+ d = TxDialog(tx, parent, desc, prompt_if_unsaved)
+ except SerializationError as e:
+ traceback.print_exc(file=sys.stderr)
+ parent.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e))
+ else:
+ dialogs.append(d)
+ d.show()
+
+
+class TxDialog(QDialog, MessageBoxMixin):
+
+ def __init__(self, tx, parent, desc, prompt_if_unsaved):
+ '''Transactions in the wallet will show their description.
+ Pass desc to give a description for txs not yet in the wallet.
+ '''
+ # We want to be a top-level window
+ QDialog.__init__(self, parent=None)
+ # Take a copy; it might get updated in the main window by
+ # e.g. the FX plugin. If this happens during or after a long
+ # sign operation the signatures are lost.
+ self.tx = tx = copy.deepcopy(tx)
+ try:
+ self.tx.deserialize()
+ except BaseException as e:
+ raise SerializationError(e)
+ self.main_window = parent
+ self.wallet = parent.wallet
+ self.prompt_if_unsaved = prompt_if_unsaved
+ self.saved = False
+ self.desc = desc
+
+ # if the wallet can populate the inputs with more info, do it now.
+ # as a result, e.g. we might learn an imported address tx is segwit,
+ # in which case it's ok to display txid
+ self.wallet.add_input_info_to_all_inputs(tx)
+
+ self.setMinimumWidth(950)
+ self.setWindowTitle(_("Transaction"))
+
+ vbox = QVBoxLayout()
+ self.setLayout(vbox)
+
+ vbox.addWidget(QLabel(_("Transaction ID:")))
+ self.tx_hash_e = ButtonsLineEdit()
+ qr_show = lambda: parent.show_qrcode(str(self.tx_hash_e.text()), 'Transaction ID', parent=self)
+ self.tx_hash_e.addButton(":icons/qrcode.png", qr_show, _("Show as QR code"))
+ self.tx_hash_e.setReadOnly(True)
+ vbox.addWidget(self.tx_hash_e)
+ self.tx_desc = QLabel()
+ vbox.addWidget(self.tx_desc)
+ self.status_label = QLabel()
+ vbox.addWidget(self.status_label)
+ self.date_label = QLabel()
+ vbox.addWidget(self.date_label)
+ self.amount_label = QLabel()
+ vbox.addWidget(self.amount_label)
+ self.size_label = QLabel()
+ vbox.addWidget(self.size_label)
+ self.fee_label = QLabel()
+ vbox.addWidget(self.fee_label)
+
+ self.add_io(vbox)
+
+ vbox.addStretch(1)
+
+ self.sign_button = b = QPushButton(_("Sign"))
+ b.clicked.connect(self.sign)
+
+ self.broadcast_button = b = QPushButton(_("Broadcast"))
+ b.clicked.connect(self.do_broadcast)
+
+ self.save_button = b = QPushButton(_("Save"))
+ save_button_disabled = not tx.is_complete()
+ b.setDisabled(save_button_disabled)
+ if save_button_disabled:
+ b.setToolTip(SAVE_BUTTON_DISABLED_TOOLTIP)
+ else:
+ b.setToolTip(SAVE_BUTTON_ENABLED_TOOLTIP)
+ b.clicked.connect(self.save)
+
+ self.export_button = b = QPushButton(_("Export"))
+ b.clicked.connect(self.export)
+
+ self.cancel_button = b = QPushButton(_("Close"))
+ b.clicked.connect(self.close)
+ b.setDefault(True)
+
+ self.qr_button = b = QPushButton()
+ b.setIcon(QIcon(":icons/qrcode.png"))
+ b.clicked.connect(self.show_qr)
+
+ self.copy_button = CopyButton(lambda: str(self.tx), parent.app)
+
+ # Action buttons
+ self.buttons = [self.sign_button, self.broadcast_button, self.cancel_button]
+ # Transaction sharing buttons
+ self.sharing_buttons = [self.copy_button, self.qr_button, self.export_button, self.save_button]
+
+ run_hook('transaction_dialog', self)
+
+ hbox = QHBoxLayout()
+ hbox.addLayout(Buttons(*self.sharing_buttons))
+ hbox.addStretch(1)
+ hbox.addLayout(Buttons(*self.buttons))
+ vbox.addLayout(hbox)
+ self.update()
+
+ def do_broadcast(self):
+ self.main_window.push_top_level_window(self)
+ try:
+ self.main_window.broadcast_transaction(self.tx, self.desc)
+ finally:
+ self.main_window.pop_top_level_window(self)
+ self.saved = True
+ self.update()
+
+ def closeEvent(self, event):
+ if (self.prompt_if_unsaved and not self.saved
+ and not self.question(_('This transaction is not saved. Close anyway?'), title=_("Warning"))):
+ event.ignore()
+ else:
+ event.accept()
+ try:
+ dialogs.remove(self)
+ except ValueError:
+ pass # was not in list already
+
+ def show_qr(self):
+ text = bfh(str(self.tx))
+ text = base_encode(text, base=43)
+ try:
+ self.main_window.show_qrcode(text, 'Transaction', parent=self)
+ except Exception as e:
+ self.show_message(str(e))
+
+ def sign(self):
+ def sign_done(success):
+ # note: with segwit we could save partially signed tx, because they have a txid
+ if self.tx.is_complete():
+ self.prompt_if_unsaved = True
+ self.saved = False
+ self.save_button.setDisabled(False)
+ self.save_button.setToolTip(SAVE_BUTTON_ENABLED_TOOLTIP)
+ self.update()
+ self.main_window.pop_top_level_window(self)
+
+ self.sign_button.setDisabled(True)
+ self.main_window.push_top_level_window(self)
+ self.main_window.sign_tx(self.tx, sign_done)
+
+ def save(self):
+ self.main_window.push_top_level_window(self)
+ if self.main_window.save_transaction_into_wallet(self.tx):
+ self.save_button.setDisabled(True)
+ self.saved = True
+ self.main_window.pop_top_level_window(self)
+
+
+ def export(self):
+ name = 'signed_%s.txn' % (self.tx.txid()[0:8]) if self.tx.is_complete() else 'unsigned.txn'
+ fileName = self.main_window.getSaveFileName(_("Select where to save your signed transaction"), name, "*.txn")
+ if fileName:
+ with open(fileName, "w+") as f:
+ f.write(json.dumps(self.tx.as_dict(), indent=4) + '\n')
+ self.show_message(_("Transaction exported successfully"))
+ self.saved = True
+
+ def update(self):
+ desc = self.desc
+ base_unit = self.main_window.base_unit()
+ format_amount = self.main_window.format_amount
+ tx_hash, status, label, can_broadcast, can_rbf, amount, fee, height, conf, timestamp, exp_n = self.wallet.get_tx_info(self.tx)
+ size = self.tx.estimated_size()
+ self.broadcast_button.setEnabled(can_broadcast)
+ can_sign = not self.tx.is_complete() and \
+ (self.wallet.can_sign(self.tx) or bool(self.main_window.tx_external_keypairs))
+ self.sign_button.setEnabled(can_sign)
+ self.tx_hash_e.setText(tx_hash or _('Unknown'))
+ if desc is None:
+ self.tx_desc.hide()
+ else:
+ self.tx_desc.setText(_("Description") + ': ' + desc)
+ self.tx_desc.show()
+ self.status_label.setText(_('Status:') + ' ' + status)
+
+ if timestamp:
+ time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
+ self.date_label.setText(_("Date: {}").format(time_str))
+ self.date_label.show()
+ elif exp_n:
+ text = '%.2f MB'%(exp_n/1000000)
+ self.date_label.setText(_('Position in mempool: {} from tip').format(text))
+ self.date_label.show()
+ else:
+ self.date_label.hide()
+ if amount is None:
+ amount_str = _("Transaction unrelated to your wallet")
+ elif amount > 0:
+ amount_str = _("Amount received:") + ' %s'% format_amount(amount) + ' ' + base_unit
+ else:
+ amount_str = _("Amount sent:") + ' %s'% format_amount(-amount) + ' ' + base_unit
+ size_str = _("Size:") + ' %d bytes'% size
+ fee_str = _("Fee") + ': %s' % (format_amount(fee) + ' ' + base_unit if fee is not None else _('unknown'))
+ if fee is not None:
+ fee_rate = fee/size*1000
+ fee_str += ' ( %s ) ' % self.main_window.format_fee_rate(fee_rate)
+ confirm_rate = simple_config.FEERATE_WARNING_HIGH_FEE
+ if fee_rate > confirm_rate:
+ fee_str += ' - ' + _('Warning') + ': ' + _("high fee") + '!'
+ self.amount_label.setText(amount_str)
+ self.fee_label.setText(fee_str)
+ self.size_label.setText(size_str)
+ run_hook('transaction_dialog_update', self)
+
+ def add_io(self, vbox):
+ if self.tx.locktime > 0:
+ vbox.addWidget(QLabel("LockTime: %d\n" % self.tx.locktime))
+
+ vbox.addWidget(QLabel(_("Inputs") + ' (%d)'%len(self.tx.inputs())))
+ ext = QTextCharFormat()
+ rec = QTextCharFormat()
+ rec.setBackground(QBrush(ColorScheme.GREEN.as_color(background=True)))
+ rec.setToolTip(_("Wallet receive address"))
+ chg = QTextCharFormat()
+ chg.setBackground(QBrush(ColorScheme.YELLOW.as_color(background=True)))
+ chg.setToolTip(_("Wallet change address"))
+ twofactor = QTextCharFormat()
+ twofactor.setBackground(QBrush(ColorScheme.BLUE.as_color(background=True)))
+ twofactor.setToolTip(_("TrustedCoin (2FA) fee for the next batch of transactions"))
+
+ def text_format(addr):
+ if self.wallet.is_mine(addr):
+ return chg if self.wallet.is_change(addr) else rec
+ elif self.wallet.is_billing_address(addr):
+ return twofactor
+ return ext
+
+ def format_amount(amt):
+ return self.main_window.format_amount(amt, whitespaces=True)
+
+ i_text = QTextEdit()
+ i_text.setFont(QFont(MONOSPACE_FONT))
+ i_text.setReadOnly(True)
+ i_text.setMaximumHeight(100)
+ cursor = i_text.textCursor()
+ for x in self.tx.inputs():
+ if x['type'] == 'coinbase':
+ cursor.insertText('coinbase')
+ else:
+ prevout_hash = x.get('prevout_hash')
+ prevout_n = x.get('prevout_n')
+ cursor.insertText(prevout_hash + ":%-4d " % prevout_n, ext)
+ addr = self.wallet.get_txin_address(x)
+ if addr is None:
+ addr = ''
+ cursor.insertText(addr, text_format(addr))
+ if x.get('value'):
+ cursor.insertText(format_amount(x['value']), ext)
+ cursor.insertBlock()
+
+ vbox.addWidget(i_text)
+ vbox.addWidget(QLabel(_("Outputs") + ' (%d)'%len(self.tx.outputs())))
+ o_text = QTextEdit()
+ o_text.setFont(QFont(MONOSPACE_FONT))
+ o_text.setReadOnly(True)
+ o_text.setMaximumHeight(100)
+ cursor = o_text.textCursor()
+ for addr, v in self.tx.get_outputs():
+ cursor.insertText(addr, text_format(addr))
+ if v is not None:
+ cursor.insertText('\t', ext)
+ cursor.insertText(format_amount(v), ext)
+ cursor.insertBlock()
+ vbox.addWidget(o_text)
DIR diff --git a/gui/qt/util.py b/electrum/gui/qt/util.py
DIR diff --git a/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py
DIR diff --git a/gui/stdio.py b/electrum/gui/stdio.py
DIR diff --git a/electrum/gui/text.py b/electrum/gui/text.py
t@@ -0,0 +1,503 @@
+import tty, sys
+import curses, datetime, locale
+from decimal import Decimal
+import getpass
+
+import electrum
+from electrum.util import format_satoshis, set_verbosity
+from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS
+from .. import Wallet, WalletStorage
+
+_ = lambda x:x
+
+
+
+class ElectrumGui:
+
+ def __init__(self, config, daemon, plugins):
+
+ self.config = config
+ self.network = daemon.network
+ storage = WalletStorage(config.get_wallet_path())
+ if not storage.file_exists():
+ print("Wallet not found. try 'electrum create'")
+ exit()
+ if storage.is_encrypted():
+ password = getpass.getpass('Password:', stream=None)
+ storage.decrypt(password)
+ self.wallet = Wallet(storage)
+ self.wallet.start_threads(self.network)
+ self.contacts = self.wallet.contacts
+
+ locale.setlocale(locale.LC_ALL, '')
+ self.encoding = locale.getpreferredencoding()
+
+ self.stdscr = curses.initscr()
+ curses.noecho()
+ curses.cbreak()
+ curses.start_color()
+ curses.use_default_colors()
+ curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)
+ curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_CYAN)
+ curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_WHITE)
+ self.stdscr.keypad(1)
+ self.stdscr.border(0)
+ self.maxy, self.maxx = self.stdscr.getmaxyx()
+ self.set_cursor(0)
+ self.w = curses.newwin(10, 50, 5, 5)
+
+ set_verbosity(False)
+ self.tab = 0
+ self.pos = 0
+ self.popup_pos = 0
+
+ self.str_recipient = ""
+ self.str_description = ""
+ self.str_amount = ""
+ self.str_fee = ""
+ self.history = None
+
+ if self.network:
+ self.network.register_callback(self.update, ['updated'])
+
+ self.tab_names = [_("History"), _("Send"), _("Receive"), _("Addresses"), _("Contacts"), _("Banner")]
+ self.num_tabs = len(self.tab_names)
+
+
+ def set_cursor(self, x):
+ try:
+ curses.curs_set(x)
+ except Exception:
+ pass
+
+ def restore_or_create(self):
+ pass
+
+ def verify_seed(self):
+ pass
+
+ def get_string(self, y, x):
+ self.set_cursor(1)
+ curses.echo()
+ self.stdscr.addstr( y, x, " "*20, curses.A_REVERSE)
+ s = self.stdscr.getstr(y,x)
+ curses.noecho()
+ self.set_cursor(0)
+ return s
+
+ def update(self, event):
+ self.update_history()
+ if self.tab == 0:
+ self.print_history()
+ self.refresh()
+
+ def print_history(self):
+
+ width = [20, 40, 14, 14]
+ delta = (self.maxx - sum(width) - 4)/3
+ format_str = "%"+"%d"%width[0]+"s"+"%"+"%d"%(width[1]+delta)+"s"+"%"+"%d"%(width[2]+delta)+"s"+"%"+"%d"%(width[3]+delta)+"s"
+
+ if self.history is None:
+ self.update_history()
+
+ self.print_list(self.history[::-1], format_str%( _("Date"), _("Description"), _("Amount"), _("Balance")))
+
+ def update_history(self):
+ width = [20, 40, 14, 14]
+ delta = (self.maxx - sum(width) - 4)/3
+ format_str = "%"+"%d"%width[0]+"s"+"%"+"%d"%(width[1]+delta)+"s"+"%"+"%d"%(width[2]+delta)+"s"+"%"+"%d"%(width[3]+delta)+"s"
+
+ b = 0
+ self.history = []
+ for item in self.wallet.get_history():
+ tx_hash, height, conf, timestamp, value, balance = item
+ if conf:
+ try:
+ time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
+ except Exception:
+ time_str = "------"
+ else:
+ time_str = 'unconfirmed'
+
+ label = self.wallet.get_label(tx_hash)
+ if len(label) > 40:
+ label = label[0:37] + '...'
+ self.history.append( format_str%( time_str, label, format_satoshis(value, whitespaces=True), format_satoshis(balance, whitespaces=True) ) )
+
+
+ def print_balance(self):
+ if not self.network:
+ msg = _("Offline")
+ elif self.network.is_connected():
+ if not self.wallet.up_to_date:
+ msg = _("Synchronizing...")
+ else:
+ c, u, x = self.wallet.get_balance()
+ msg = _("Balance")+": %f "%(Decimal(c) / COIN)
+ if u:
+ msg += " [%f unconfirmed]"%(Decimal(u) / COIN)
+ if x:
+ msg += " [%f unmatured]"%(Decimal(x) / COIN)
+ else:
+ msg = _("Not connected")
+
+ self.stdscr.addstr( self.maxy -1, 3, msg)
+
+ for i in range(self.num_tabs):
+ self.stdscr.addstr( 0, 2 + 2*i + len(''.join(self.tab_names[0:i])), ' '+self.tab_names[i]+' ', curses.A_BOLD if self.tab == i else 0)
+
+ self.stdscr.addstr(self.maxy -1, self.maxx-30, ' '.join([_("Settings"), _("Network"), _("Quit")]))
+
+ def print_receive(self):
+ addr = self.wallet.get_receiving_address()
+ self.stdscr.addstr(2, 1, "Address: "+addr)
+ self.print_qr(addr)
+
+ def print_contacts(self):
+ messages = map(lambda x: "%20s %45s "%(x[0], x[1][1]), self.contacts.items())
+ self.print_list(messages, "%19s %15s "%("Key", "Value"))
+
+ def print_addresses(self):
+ fmt = "%-35s %-30s"
+ messages = map(lambda addr: fmt % (addr, self.wallet.labels.get(addr,"")), self.wallet.get_addresses())
+ self.print_list(messages, fmt % ("Address", "Label"))
+
+ def print_edit_line(self, y, label, text, index, size):
+ text += " "*(size - len(text) )
+ self.stdscr.addstr( y, 2, label)
+ self.stdscr.addstr( y, 15, text, curses.A_REVERSE if self.pos%6==index else curses.color_pair(1))
+
+ def print_send_tab(self):
+ self.stdscr.clear()
+ self.print_edit_line(3, _("Pay to"), self.str_recipient, 0, 40)
+ self.print_edit_line(5, _("Description"), self.str_description, 1, 40)
+ self.print_edit_line(7, _("Amount"), self.str_amount, 2, 15)
+ self.print_edit_line(9, _("Fee"), self.str_fee, 3, 15)
+ self.stdscr.addstr( 12, 15, _("[Send]"), curses.A_REVERSE if self.pos%6==4 else curses.color_pair(2))
+ self.stdscr.addstr( 12, 25, _("[Clear]"), curses.A_REVERSE if self.pos%6==5 else curses.color_pair(2))
+ self.maxpos = 6
+
+ def print_banner(self):
+ if self.network:
+ self.print_list( self.network.banner.split('\n'))
+
+ def print_qr(self, data):
+ import qrcode
+ try:
+ from StringIO import StringIO
+ except ImportError:
+ from io import StringIO
+
+ s = StringIO()
+ self.qr = qrcode.QRCode()
+ self.qr.add_data(data)
+ self.qr.print_ascii(out=s, invert=False)
+ msg = s.getvalue()
+ lines = msg.split('\n')
+ for i, l in enumerate(lines):
+ l = l.encode("utf-8")
+ self.stdscr.addstr(i+5, 5, l, curses.color_pair(3))
+
+ def print_list(self, lst, firstline = None):
+ lst = list(lst)
+ self.maxpos = len(lst)
+ if not self.maxpos: return
+ if firstline:
+ firstline += " "*(self.maxx -2 - len(firstline))
+ self.stdscr.addstr( 1, 1, firstline )
+ for i in range(self.maxy-4):
+ msg = lst[i] if i < len(lst) else ""
+ msg += " "*(self.maxx - 2 - len(msg))
+ m = msg[0:self.maxx - 2]
+ m = m.encode(self.encoding)
+ self.stdscr.addstr( i+2, 1, m, curses.A_REVERSE if i == (self.pos % self.maxpos) else 0)
+
+ def refresh(self):
+ if self.tab == -1: return
+ self.stdscr.border(0)
+ self.print_balance()
+ self.stdscr.refresh()
+
+ def main_command(self):
+ c = self.stdscr.getch()
+ print(c)
+ cc = curses.unctrl(c).decode()
+ if c == curses.KEY_RIGHT: self.tab = (self.tab + 1)%self.num_tabs
+ elif c == curses.KEY_LEFT: self.tab = (self.tab - 1)%self.num_tabs
+ elif c == curses.KEY_DOWN: self.pos +=1
+ elif c == curses.KEY_UP: self.pos -= 1
+ elif c == 9: self.pos +=1 # tab
+ elif cc in ['^W', '^C', '^X', '^Q']: self.tab = -1
+ elif cc in ['^N']: self.network_dialog()
+ elif cc == '^S': self.settings_dialog()
+ else: return c
+ if self.pos<0: self.pos=0
+ if self.pos>=self.maxpos: self.pos=self.maxpos - 1
+
+ def run_tab(self, i, print_func, exec_func):
+ while self.tab == i:
+ self.stdscr.clear()
+ print_func()
+ self.refresh()
+ c = self.main_command()
+ if c: exec_func(c)
+
+
+ def run_history_tab(self, c):
+ if c == 10:
+ out = self.run_popup('',["blah","foo"])
+
+
+ def edit_str(self, target, c, is_num=False):
+ # detect backspace
+ cc = curses.unctrl(c).decode()
+ if c in [8, 127, 263] and target:
+ target = target[:-1]
+ elif not is_num or cc in '0123456789.':
+ target += cc
+ return target
+
+
+ def run_send_tab(self, c):
+ if self.pos%6 == 0:
+ self.str_recipient = self.edit_str(self.str_recipient, c)
+ if self.pos%6 == 1:
+ self.str_description = self.edit_str(self.str_description, c)
+ if self.pos%6 == 2:
+ self.str_amount = self.edit_str(self.str_amount, c, True)
+ elif self.pos%6 == 3:
+ self.str_fee = self.edit_str(self.str_fee, c, True)
+ elif self.pos%6==4:
+ if c == 10: self.do_send()
+ elif self.pos%6==5:
+ if c == 10: self.do_clear()
+
+
+ def run_receive_tab(self, c):
+ if c == 10:
+ out = self.run_popup('Address', ["Edit label", "Freeze", "Prioritize"])
+
+ def run_contacts_tab(self, c):
+ if c == 10 and self.contacts:
+ out = self.run_popup('Address', ["Copy", "Pay to", "Edit label", "Delete"]).get('button')
+ key = list(self.contacts.keys())[self.pos%len(self.contacts.keys())]
+ if out == "Pay to":
+ self.tab = 1
+ self.str_recipient = key
+ self.pos = 2
+ elif out == "Edit label":
+ s = self.get_string(6 + self.pos, 18)
+ if s:
+ self.wallet.labels[key] = s
+
+ def run_banner_tab(self, c):
+ self.show_message(repr(c))
+ pass
+
+ def main(self):
+
+ tty.setraw(sys.stdin)
+ while self.tab != -1:
+ self.run_tab(0, self.print_history, self.run_history_tab)
+ self.run_tab(1, self.print_send_tab, self.run_send_tab)
+ self.run_tab(2, self.print_receive, self.run_receive_tab)
+ self.run_tab(3, self.print_addresses, self.run_banner_tab)
+ self.run_tab(4, self.print_contacts, self.run_contacts_tab)
+ self.run_tab(5, self.print_banner, self.run_banner_tab)
+
+ tty.setcbreak(sys.stdin)
+ curses.nocbreak()
+ self.stdscr.keypad(0)
+ curses.echo()
+ curses.endwin()
+
+
+ def do_clear(self):
+ self.str_amount = ''
+ self.str_recipient = ''
+ self.str_fee = ''
+ self.str_description = ''
+
+ def do_send(self):
+ if not is_address(self.str_recipient):
+ self.show_message(_('Invalid Bitcoin address'))
+ return
+ try:
+ amount = int(Decimal(self.str_amount) * COIN)
+ except Exception:
+ self.show_message(_('Invalid Amount'))
+ return
+ try:
+ fee = int(Decimal(self.str_fee) * COIN)
+ except Exception:
+ self.show_message(_('Invalid Fee'))
+ return
+
+ if self.wallet.has_password():
+ password = self.password_dialog()
+ if not password:
+ return
+ else:
+ password = None
+ try:
+ tx = self.wallet.mktx([(TYPE_ADDRESS, self.str_recipient, amount)], password, self.config, fee)
+ except Exception as e:
+ self.show_message(str(e))
+ return
+
+ if self.str_description:
+ self.wallet.labels[tx.txid()] = self.str_description
+
+ self.show_message(_("Please wait..."), getchar=False)
+ status, msg = self.network.broadcast_transaction(tx)
+
+ if status:
+ self.show_message(_('Payment sent.'))
+ self.do_clear()
+ #self.update_contacts_tab()
+ else:
+ self.show_message(_('Error'))
+
+
+ def show_message(self, message, getchar = True):
+ w = self.w
+ w.clear()
+ w.border(0)
+ for i, line in enumerate(message.split('\n')):
+ w.addstr(2+i,2,line)
+ w.refresh()
+ if getchar: c = self.stdscr.getch()
+
+ def run_popup(self, title, items):
+ return self.run_dialog(title, list(map(lambda x: {'type':'button','label':x}, items)), interval=1, y_pos = self.pos+3)
+
+ def network_dialog(self):
+ if not self.network:
+ return
+ params = self.network.get_parameters()
+ host, port, protocol, proxy_config, auto_connect = params
+ srv = 'auto-connect' if auto_connect else self.network.default_server
+ out = self.run_dialog('Network', [
+ {'label':'server', 'type':'str', 'value':srv},
+ {'label':'proxy', 'type':'str', 'value':self.config.get('proxy', '')},
+ ], buttons = 1)
+ if out:
+ if out.get('server'):
+ server = out.get('server')
+ auto_connect = server == 'auto-connect'
+ if not auto_connect:
+ try:
+ host, port, protocol = server.split(':')
+ except Exception:
+ self.show_message("Error:" + server + "\nIn doubt, type \"auto-connect\"")
+ return False
+ if out.get('server') or out.get('proxy'):
+ proxy = electrum.network.deserialize_proxy(out.get('proxy')) if out.get('proxy') else proxy_config
+ self.network.set_parameters(host, port, protocol, proxy, auto_connect)
+
+ def settings_dialog(self):
+ fee = str(Decimal(self.config.fee_per_kb()) / COIN)
+ out = self.run_dialog('Settings', [
+ {'label':'Default fee', 'type':'satoshis', 'value': fee }
+ ], buttons = 1)
+ if out:
+ if out.get('Default fee'):
+ fee = int(Decimal(out['Default fee']) * COIN)
+ self.config.set_key('fee_per_kb', fee, True)
+
+
+ def password_dialog(self):
+ out = self.run_dialog('Password', [
+ {'label':'Password', 'type':'password', 'value':''}
+ ], buttons = 1)
+ return out.get('Password')
+
+
+ def run_dialog(self, title, items, interval=2, buttons=None, y_pos=3):
+ self.popup_pos = 0
+
+ self.w = curses.newwin( 5 + len(list(items))*interval + (2 if buttons else 0), 50, y_pos, 5)
+ w = self.w
+ out = {}
+ while True:
+ w.clear()
+ w.border(0)
+ w.addstr( 0, 2, title)
+
+ num = len(list(items))
+
+ numpos = num
+ if buttons: numpos += 2
+
+ for i in range(num):
+ item = items[i]
+ label = item.get('label')
+ if item.get('type') == 'list':
+ value = item.get('value','')
+ elif item.get('type') == 'satoshis':
+ value = item.get('value','')
+ elif item.get('type') == 'str':
+ value = item.get('value','')
+ elif item.get('type') == 'password':
+ value = '*'*len(item.get('value',''))
+ else:
+ value = ''
+ if value is None:
+ value = ''
+ if len(value)<20:
+ value += ' '*(20-len(value))
+
+ if 'value' in item:
+ w.addstr( 2+interval*i, 2, label)
+ w.addstr( 2+interval*i, 15, value, curses.A_REVERSE if self.popup_pos%numpos==i else curses.color_pair(1) )
+ else:
+ w.addstr( 2+interval*i, 2, label, curses.A_REVERSE if self.popup_pos%numpos==i else 0)
+
+ if buttons:
+ w.addstr( 5+interval*i, 10, "[ ok ]", curses.A_REVERSE if self.popup_pos%numpos==(numpos-2) else curses.color_pair(2))
+ w.addstr( 5+interval*i, 25, "[cancel]", curses.A_REVERSE if self.popup_pos%numpos==(numpos-1) else curses.color_pair(2))
+
+ w.refresh()
+
+ c = self.stdscr.getch()
+ if c in [ord('q'), 27]: break
+ elif c in [curses.KEY_LEFT, curses.KEY_UP]: self.popup_pos -= 1
+ elif c in [curses.KEY_RIGHT, curses.KEY_DOWN]: self.popup_pos +=1
+ else:
+ i = self.popup_pos%numpos
+ if buttons and c==10:
+ if i == numpos-2:
+ return out
+ elif i == numpos -1:
+ return {}
+
+ item = items[i]
+ _type = item.get('type')
+
+ if _type == 'str':
+ item['value'] = self.edit_str(item['value'], c)
+ out[item.get('label')] = item.get('value')
+
+ elif _type == 'password':
+ item['value'] = self.edit_str(item['value'], c)
+ out[item.get('label')] = item ['value']
+
+ elif _type == 'satoshis':
+ item['value'] = self.edit_str(item['value'], c, True)
+ out[item.get('label')] = item.get('value')
+
+ elif _type == 'list':
+ choices = item.get('choices')
+ try:
+ j = choices.index(item.get('value'))
+ except Exception:
+ j = 0
+ new_choice = choices[(j + 1)% len(choices)]
+ item['value'] = new_choice
+ out[item.get('label')] = item.get('value')
+
+ elif _type == 'button':
+ out['button'] = item.get('label')
+ break
+
+ return out
DIR diff --git a/electrum/i18n.py b/electrum/i18n.py
t@@ -0,0 +1,81 @@
+#!/usr/bin/env python
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2012 thomasv@gitorious
+#
+# 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 os
+
+import gettext
+
+LOCALE_DIR = os.path.join(os.path.dirname(__file__), 'locale')
+language = gettext.translation('electrum', LOCALE_DIR, fallback=True)
+
+
+def _(x):
+ global language
+ return language.gettext(x)
+
+
+def set_language(x):
+ global language
+ if x:
+ language = gettext.translation('electrum', LOCALE_DIR, fallback=True, languages=[x])
+
+
+languages = {
+ '': _('Default'),
+ 'ar_SA': _('Arabic'),
+ 'bg_BG': _('Bulgarian'),
+ 'cs_CZ': _('Czech'),
+ 'da_DK': _('Danish'),
+ 'de_DE': _('German'),
+ 'el_GR': _('Greek'),
+ 'eo_UY': _('Esperanto'),
+ 'en_UK': _('English'),
+ 'es_ES': _('Spanish'),
+ 'fa_IR': _('Persian'),
+ 'fr_FR': _('French'),
+ 'hu_HU': _('Hungarian'),
+ 'hy_AM': _('Armenian'),
+ 'id_ID': _('Indonesian'),
+ 'it_IT': _('Italian'),
+ 'ja_JP': _('Japanese'),
+ 'ky_KG': _('Kyrgyz'),
+ 'lv_LV': _('Latvian'),
+ 'nb_NO': _('Norwegian Bokmal'),
+ 'nl_NL': _('Dutch'),
+ 'pl_PL': _('Polish'),
+ 'pt_BR': _('Brasilian'),
+ 'pt_PT': _('Portuguese'),
+ 'ro_RO': _('Romanian'),
+ 'ru_RU': _('Russian'),
+ 'sk_SK': _('Slovak'),
+ 'sl_SI': _('Slovenian'),
+ 'sv_SE': _('Swedish'),
+ 'ta_IN': _('Tamil'),
+ 'th_TH': _('Thai'),
+ 'tr_TR': _('Turkish'),
+ 'uk_UA': _('Ukrainian'),
+ 'vi_VN': _('Vietnamese'),
+ 'zh_CN': _('Chinese Simplified'),
+ 'zh_TW': _('Chinese Traditional')
+}
DIR diff --git a/electrum/interface.py b/electrum/interface.py
t@@ -0,0 +1,407 @@
+#!/usr/bin/env python
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2011 thomasv@gitorious
+#
+# 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 os
+import re
+import socket
+import ssl
+import sys
+import threading
+import time
+import traceback
+
+import requests
+
+from .util import print_error
+
+ca_path = requests.certs.where()
+
+from . import util
+from . import x509
+from . import pem
+
+
+def Connection(server, queue, config_path):
+ """Makes asynchronous connections to a remote Electrum server.
+ Returns the running thread that is making the connection.
+
+ Once the thread has connected, it finishes, placing a tuple on the
+ queue of the form (server, socket), where socket is None if
+ connection failed.
+ """
+ host, port, protocol = server.rsplit(':', 2)
+ if not protocol in 'st':
+ raise Exception('Unknown protocol: %s' % protocol)
+ c = TcpConnection(server, queue, config_path)
+ c.start()
+ return c
+
+
+class TcpConnection(threading.Thread, util.PrintError):
+
+ def __init__(self, server, queue, config_path):
+ threading.Thread.__init__(self)
+ self.config_path = config_path
+ self.queue = queue
+ self.server = server
+ self.host, self.port, self.protocol = self.server.rsplit(':', 2)
+ self.host = str(self.host)
+ self.port = int(self.port)
+ self.use_ssl = (self.protocol == 's')
+ self.daemon = True
+
+ def diagnostic_name(self):
+ return self.host
+
+ def check_host_name(self, peercert, name):
+ """Simple certificate/host name checker. Returns True if the
+ certificate matches, False otherwise. Does not support
+ wildcards."""
+ # Check that the peer has supplied a certificate.
+ # None/{} is not acceptable.
+ if not peercert:
+ return False
+ if 'subjectAltName' in peercert:
+ for typ, val in peercert["subjectAltName"]:
+ if typ == "DNS" and val == name:
+ return True
+ else:
+ # Only check the subject DN if there is no subject alternative
+ # name.
+ cn = None
+ for attr, val in peercert["subject"]:
+ # Use most-specific (last) commonName attribute.
+ if attr == "commonName":
+ cn = val
+ if cn is not None:
+ return cn == name
+ return False
+
+ def get_simple_socket(self):
+ try:
+ l = socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM)
+ except socket.gaierror:
+ self.print_error("cannot resolve hostname")
+ return
+ e = None
+ for res in l:
+ try:
+ s = socket.socket(res[0], socket.SOCK_STREAM)
+ s.settimeout(10)
+ s.connect(res[4])
+ s.settimeout(2)
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
+ return s
+ except BaseException as _e:
+ e = _e
+ continue
+ else:
+ self.print_error("failed to connect", str(e))
+
+ @staticmethod
+ def get_ssl_context(cert_reqs, ca_certs):
+ context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=ca_certs)
+ context.check_hostname = False
+ context.verify_mode = cert_reqs
+
+ context.options |= ssl.OP_NO_SSLv2
+ context.options |= ssl.OP_NO_SSLv3
+ context.options |= ssl.OP_NO_TLSv1
+
+ return context
+
+ def get_socket(self):
+ if self.use_ssl:
+ cert_path = os.path.join(self.config_path, 'certs', self.host)
+ if not os.path.exists(cert_path):
+ is_new = True
+ s = self.get_simple_socket()
+ if s is None:
+ return
+ # try with CA first
+ try:
+ context = self.get_ssl_context(cert_reqs=ssl.CERT_REQUIRED, ca_certs=ca_path)
+ s = context.wrap_socket(s, do_handshake_on_connect=True)
+ except ssl.SSLError as e:
+ self.print_error(e)
+ except:
+ return
+ else:
+ try:
+ peer_cert = s.getpeercert()
+ except OSError:
+ return
+ if self.check_host_name(peer_cert, self.host):
+ self.print_error("SSL certificate signed by CA")
+ return s
+ # get server certificate.
+ # Do not use ssl.get_server_certificate because it does not work with proxy
+ s = self.get_simple_socket()
+ if s is None:
+ return
+ try:
+ context = self.get_ssl_context(cert_reqs=ssl.CERT_NONE, ca_certs=None)
+ s = context.wrap_socket(s)
+ except ssl.SSLError as e:
+ self.print_error("SSL error retrieving SSL certificate:", e)
+ return
+ except:
+ return
+
+ try:
+ dercert = s.getpeercert(True)
+ except OSError:
+ return
+ s.close()
+ cert = ssl.DER_cert_to_PEM_cert(dercert)
+ # workaround android bug
+ cert = re.sub("([^\n])-----END CERTIFICATE-----","\\1\n-----END CERTIFICATE-----",cert)
+ temporary_path = cert_path + '.temp'
+ util.assert_datadir_available(self.config_path)
+ with open(temporary_path, "w", encoding='utf-8') as f:
+ f.write(cert)
+ f.flush()
+ os.fsync(f.fileno())
+ else:
+ is_new = False
+
+ s = self.get_simple_socket()
+ if s is None:
+ return
+
+ if self.use_ssl:
+ try:
+ context = self.get_ssl_context(cert_reqs=ssl.CERT_REQUIRED,
+ ca_certs=(temporary_path if is_new else cert_path))
+ s = context.wrap_socket(s, do_handshake_on_connect=True)
+ except socket.timeout:
+ self.print_error('timeout')
+ return
+ except ssl.SSLError as e:
+ self.print_error("SSL error:", e)
+ if e.errno != 1:
+ return
+ if is_new:
+ rej = cert_path + '.rej'
+ if os.path.exists(rej):
+ os.unlink(rej)
+ os.rename(temporary_path, rej)
+ else:
+ util.assert_datadir_available(self.config_path)
+ with open(cert_path, encoding='utf-8') as f:
+ cert = f.read()
+ try:
+ b = pem.dePem(cert, 'CERTIFICATE')
+ x = x509.X509(b)
+ except:
+ traceback.print_exc(file=sys.stderr)
+ self.print_error("wrong certificate")
+ return
+ try:
+ x.check_date()
+ except:
+ self.print_error("certificate has expired:", cert_path)
+ os.unlink(cert_path)
+ return
+ self.print_error("wrong certificate")
+ if e.errno == 104:
+ return
+ return
+ except BaseException as e:
+ self.print_error(e)
+ traceback.print_exc(file=sys.stderr)
+ return
+
+ if is_new:
+ self.print_error("saving certificate")
+ os.rename(temporary_path, cert_path)
+
+ return s
+
+ def run(self):
+ socket = self.get_socket()
+ if socket:
+ self.print_error("connected")
+ self.queue.put((self.server, socket))
+
+
+class Interface(util.PrintError):
+ """The Interface class handles a socket connected to a single remote
+ Electrum server. Its exposed API is:
+
+ - Member functions close(), fileno(), get_responses(), has_timed_out(),
+ ping_required(), queue_request(), send_requests()
+ - Member variable server.
+ """
+
+ def __init__(self, server, socket):
+ self.server = server
+ self.host, _, _ = server.rsplit(':', 2)
+ self.socket = socket
+
+ self.pipe = util.SocketPipe(socket)
+ self.pipe.set_timeout(0.0) # Don't wait for data
+ # Dump network messages. Set at runtime from the console.
+ self.debug = False
+ self.unsent_requests = []
+ self.unanswered_requests = {}
+ self.last_send = time.time()
+ self.closed_remotely = False
+
+ def diagnostic_name(self):
+ return self.host
+
+ def fileno(self):
+ # Needed for select
+ return self.socket.fileno()
+
+ def close(self):
+ if not self.closed_remotely:
+ try:
+ self.socket.shutdown(socket.SHUT_RDWR)
+ except socket.error:
+ pass
+ self.socket.close()
+
+ def queue_request(self, *args): # method, params, _id
+ '''Queue a request, later to be send with send_requests when the
+ socket is available for writing.
+ '''
+ self.request_time = time.time()
+ self.unsent_requests.append(args)
+
+ def num_requests(self):
+ '''Keep unanswered requests below 100'''
+ n = 100 - len(self.unanswered_requests)
+ return min(n, len(self.unsent_requests))
+
+ def send_requests(self):
+ '''Sends queued requests. Returns False on failure.'''
+ self.last_send = time.time()
+ make_dict = lambda m, p, i: {'method': m, 'params': p, 'id': i}
+ n = self.num_requests()
+ wire_requests = self.unsent_requests[0:n]
+ try:
+ self.pipe.send_all([make_dict(*r) for r in wire_requests])
+ except BaseException as e:
+ self.print_error("pipe send error:", e)
+ return False
+ self.unsent_requests = self.unsent_requests[n:]
+ for request in wire_requests:
+ if self.debug:
+ self.print_error("-->", request)
+ self.unanswered_requests[request[2]] = request
+ return True
+
+ def ping_required(self):
+ '''Returns True if a ping should be sent.'''
+ return time.time() - self.last_send > 300
+
+ def has_timed_out(self):
+ '''Returns True if the interface has timed out.'''
+ if (self.unanswered_requests and time.time() - self.request_time > 10
+ and self.pipe.idle_time() > 10):
+ self.print_error("timeout", len(self.unanswered_requests))
+ return True
+
+ return False
+
+ def get_responses(self):
+ '''Call if there is data available on the socket. Returns a list of
+ (request, response) pairs. Notifications are singleton
+ unsolicited responses presumably as a result of prior
+ subscriptions, so request is None and there is no 'id' member.
+ Otherwise it is a response, which has an 'id' member and a
+ corresponding request. If the connection was closed remotely
+ or the remote server is misbehaving, a (None, None) will appear.
+ '''
+ responses = []
+ while True:
+ try:
+ response = self.pipe.get()
+ except util.timeout:
+ break
+ if not type(response) is dict:
+ responses.append((None, None))
+ if response is None:
+ self.closed_remotely = True
+ self.print_error("connection closed remotely")
+ break
+ if self.debug:
+ self.print_error("<--", response)
+ wire_id = response.get('id', None)
+ if wire_id is None: # Notification
+ responses.append((None, response))
+ else:
+ request = self.unanswered_requests.pop(wire_id, None)
+ if request:
+ responses.append((request, response))
+ else:
+ self.print_error("unknown wire ID", wire_id)
+ responses.append((None, None)) # Signal
+ break
+
+ return responses
+
+
+def check_cert(host, cert):
+ try:
+ b = pem.dePem(cert, 'CERTIFICATE')
+ x = x509.X509(b)
+ except:
+ traceback.print_exc(file=sys.stdout)
+ return
+
+ try:
+ x.check_date()
+ expired = False
+ except:
+ expired = True
+
+ m = "host: %s\n"%host
+ m += "has_expired: %s\n"% expired
+ util.print_msg(m)
+
+
+# Used by tests
+def _match_hostname(name, val):
+ if val == name:
+ return True
+
+ return val.startswith('*.') and name.endswith(val[1:])
+
+
+def test_certificates():
+ from .simple_config import SimpleConfig
+ config = SimpleConfig()
+ mydir = os.path.join(config.path, "certs")
+ certs = os.listdir(mydir)
+ for c in certs:
+ p = os.path.join(mydir,c)
+ with open(p, encoding='utf-8') as f:
+ cert = f.read()
+ check_cert(c, cert)
+
+if __name__ == "__main__":
+ test_certificates()
DIR diff --git a/electrum/jsonrpc.py b/electrum/jsonrpc.py
t@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2018 Thomas Voegtlin
+#
+# 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.
+
+from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer, SimpleJSONRPCRequestHandler
+from base64 import b64decode
+import time
+
+from . import util
+
+
+class RPCAuthCredentialsInvalid(Exception):
+ def __str__(self):
+ return 'Authentication failed (bad credentials)'
+
+
+class RPCAuthCredentialsMissing(Exception):
+ def __str__(self):
+ return 'Authentication failed (missing credentials)'
+
+
+class RPCAuthUnsupportedType(Exception):
+ def __str__(self):
+ return 'Authentication failed (only basic auth is supported)'
+
+
+# based on http://acooke.org/cute/BasicHTTPA0.html by andrew cooke
+class VerifyingJSONRPCServer(SimpleJSONRPCServer):
+
+ def __init__(self, *args, rpc_user, rpc_password, **kargs):
+
+ self.rpc_user = rpc_user
+ self.rpc_password = rpc_password
+
+ class VerifyingRequestHandler(SimpleJSONRPCRequestHandler):
+ def parse_request(myself):
+ # first, call the original implementation which returns
+ # True if all OK so far
+ if SimpleJSONRPCRequestHandler.parse_request(myself):
+ # Do not authenticate OPTIONS-requests
+ if myself.command.strip() == 'OPTIONS':
+ return True
+ try:
+ self.authenticate(myself.headers)
+ return True
+ except (RPCAuthCredentialsInvalid, RPCAuthCredentialsMissing,
+ RPCAuthUnsupportedType) as e:
+ myself.send_error(401, str(e))
+ except BaseException as e:
+ import traceback, sys
+ traceback.print_exc(file=sys.stderr)
+ myself.send_error(500, str(e))
+ return False
+
+ SimpleJSONRPCServer.__init__(
+ self, requestHandler=VerifyingRequestHandler, *args, **kargs)
+
+ def authenticate(self, headers):
+ if self.rpc_password == '':
+ # RPC authentication is disabled
+ return
+
+ auth_string = headers.get('Authorization', None)
+ if auth_string is None:
+ raise RPCAuthCredentialsMissing()
+
+ (basic, _, encoded) = auth_string.partition(' ')
+ if basic != 'Basic':
+ raise RPCAuthUnsupportedType()
+
+ encoded = util.to_bytes(encoded, 'utf8')
+ credentials = util.to_string(b64decode(encoded), 'utf8')
+ (username, _, password) = credentials.partition(':')
+ if not (util.constant_time_compare(username, self.rpc_user)
+ and util.constant_time_compare(password, self.rpc_password)):
+ time.sleep(0.050)
+ raise RPCAuthCredentialsInvalid()
DIR diff --git a/electrum/keystore.py b/electrum/keystore.py
t@@ -0,0 +1,798 @@
+#!/usr/bin/env python2
+# -*- mode: python -*-
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2016 The Electrum developers
+#
+# 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.
+
+from unicodedata import normalize
+
+from . import bitcoin, ecc, constants
+from .bitcoin import *
+from .ecc import string_to_number, number_to_string
+from .crypto import pw_decode, pw_encode
+from .util import (PrintError, InvalidPassword, hfu, WalletFileException,
+ BitcoinException)
+from .mnemonic import Mnemonic, load_wordlist
+from .plugin import run_hook
+
+
+class KeyStore(PrintError):
+
+ def has_seed(self):
+ return False
+
+ def is_watching_only(self):
+ return False
+
+ def can_import(self):
+ return False
+
+ def may_have_password(self):
+ """Returns whether the keystore can be encrypted with a password."""
+ raise NotImplementedError()
+
+ def get_tx_derivations(self, tx):
+ keypairs = {}
+ for txin in tx.inputs():
+ num_sig = txin.get('num_sig')
+ if num_sig is None:
+ continue
+ x_signatures = txin['signatures']
+ signatures = [sig for sig in x_signatures if sig]
+ if len(signatures) == num_sig:
+ # input is complete
+ continue
+ for k, x_pubkey in enumerate(txin['x_pubkeys']):
+ if x_signatures[k] is not None:
+ # this pubkey already signed
+ continue
+ derivation = self.get_pubkey_derivation(x_pubkey)
+ if not derivation:
+ continue
+ keypairs[x_pubkey] = derivation
+ return keypairs
+
+ def can_sign(self, tx):
+ if self.is_watching_only():
+ return False
+ return bool(self.get_tx_derivations(tx))
+
+ def ready_to_sign(self):
+ return not self.is_watching_only()
+
+
+class Software_KeyStore(KeyStore):
+
+ def __init__(self):
+ KeyStore.__init__(self)
+
+ def may_have_password(self):
+ return not self.is_watching_only()
+
+ def sign_message(self, sequence, message, password):
+ privkey, compressed = self.get_private_key(sequence, password)
+ key = ecc.ECPrivkey(privkey)
+ return key.sign_message(message, compressed)
+
+ def decrypt_message(self, sequence, message, password):
+ privkey, compressed = self.get_private_key(sequence, password)
+ ec = ecc.ECPrivkey(privkey)
+ decrypted = ec.decrypt_message(message)
+ return decrypted
+
+ def sign_transaction(self, tx, password):
+ if self.is_watching_only():
+ return
+ # Raise if password is not correct.
+ self.check_password(password)
+ # Add private keys
+ keypairs = self.get_tx_derivations(tx)
+ for k, v in keypairs.items():
+ keypairs[k] = self.get_private_key(v, password)
+ # Sign
+ if keypairs:
+ tx.sign(keypairs)
+
+
+class Imported_KeyStore(Software_KeyStore):
+ # keystore for imported private keys
+
+ def __init__(self, d):
+ Software_KeyStore.__init__(self)
+ self.keypairs = d.get('keypairs', {})
+
+ def is_deterministic(self):
+ return False
+
+ def get_master_public_key(self):
+ return None
+
+ def dump(self):
+ return {
+ 'type': 'imported',
+ 'keypairs': self.keypairs,
+ }
+
+ def can_import(self):
+ return True
+
+ def check_password(self, password):
+ pubkey = list(self.keypairs.keys())[0]
+ self.get_private_key(pubkey, password)
+
+ def import_privkey(self, sec, password):
+ txin_type, privkey, compressed = deserialize_privkey(sec)
+ pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed)
+ # re-serialize the key so the internal storage format is consistent
+ serialized_privkey = serialize_privkey(
+ privkey, compressed, txin_type, internal_use=True)
+ # NOTE: if the same pubkey is reused for multiple addresses (script types),
+ # there will only be one pubkey-privkey pair for it in self.keypairs,
+ # and the privkey will encode a txin_type but that txin_type cannot be trusted.
+ # Removing keys complicates this further.
+ self.keypairs[pubkey] = pw_encode(serialized_privkey, password)
+ return txin_type, pubkey
+
+ def delete_imported_key(self, key):
+ self.keypairs.pop(key)
+
+ def get_private_key(self, pubkey, password):
+ sec = pw_decode(self.keypairs[pubkey], password)
+ txin_type, privkey, compressed = deserialize_privkey(sec)
+ # this checks the password
+ if pubkey != ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed):
+ raise InvalidPassword()
+ return privkey, compressed
+
+ def get_pubkey_derivation(self, x_pubkey):
+ if x_pubkey[0:2] in ['02', '03', '04']:
+ if x_pubkey in self.keypairs.keys():
+ return x_pubkey
+ elif x_pubkey[0:2] == 'fd':
+ addr = bitcoin.script_to_address(x_pubkey[2:])
+ if addr in self.addresses:
+ return self.addresses[addr].get('pubkey')
+
+ def update_password(self, old_password, new_password):
+ self.check_password(old_password)
+ if new_password == '':
+ new_password = None
+ for k, v in self.keypairs.items():
+ b = pw_decode(v, old_password)
+ c = pw_encode(b, new_password)
+ self.keypairs[k] = c
+
+
+
+class Deterministic_KeyStore(Software_KeyStore):
+
+ def __init__(self, d):
+ Software_KeyStore.__init__(self)
+ self.seed = d.get('seed', '')
+ self.passphrase = d.get('passphrase', '')
+
+ def is_deterministic(self):
+ return True
+
+ def dump(self):
+ d = {}
+ if self.seed:
+ d['seed'] = self.seed
+ if self.passphrase:
+ d['passphrase'] = self.passphrase
+ return d
+
+ def has_seed(self):
+ return bool(self.seed)
+
+ def is_watching_only(self):
+ return not self.has_seed()
+
+ def add_seed(self, seed):
+ if self.seed:
+ raise Exception("a seed exists")
+ self.seed = self.format_seed(seed)
+
+ def get_seed(self, password):
+ return pw_decode(self.seed, password)
+
+ def get_passphrase(self, password):
+ return pw_decode(self.passphrase, password) if self.passphrase else ''
+
+
+class Xpub:
+
+ def __init__(self):
+ self.xpub = None
+ self.xpub_receive = None
+ self.xpub_change = None
+
+ def get_master_public_key(self):
+ return self.xpub
+
+ def derive_pubkey(self, for_change, n):
+ xpub = self.xpub_change if for_change else self.xpub_receive
+ if xpub is None:
+ xpub = bip32_public_derivation(self.xpub, "", "/%d"%for_change)
+ if for_change:
+ self.xpub_change = xpub
+ else:
+ self.xpub_receive = xpub
+ return self.get_pubkey_from_xpub(xpub, (n,))
+
+ @classmethod
+ def get_pubkey_from_xpub(self, xpub, sequence):
+ _, _, _, _, c, cK = deserialize_xpub(xpub)
+ for i in sequence:
+ cK, c = CKD_pub(cK, c, i)
+ return bh2u(cK)
+
+ def get_xpubkey(self, c, i):
+ s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (c, i)))
+ return 'ff' + bh2u(bitcoin.DecodeBase58Check(self.xpub)) + s
+
+ @classmethod
+ def parse_xpubkey(self, pubkey):
+ assert pubkey[0:2] == 'ff'
+ pk = bfh(pubkey)
+ pk = pk[1:]
+ xkey = bitcoin.EncodeBase58Check(pk[0:78])
+ dd = pk[78:]
+ s = []
+ while dd:
+ n = int(bitcoin.rev_hex(bh2u(dd[0:2])), 16)
+ dd = dd[2:]
+ s.append(n)
+ assert len(s) == 2
+ return xkey, s
+
+ def get_pubkey_derivation(self, x_pubkey):
+ if x_pubkey[0:2] != 'ff':
+ return
+ xpub, derivation = self.parse_xpubkey(x_pubkey)
+ if self.xpub != xpub:
+ return
+ return derivation
+
+
+class BIP32_KeyStore(Deterministic_KeyStore, Xpub):
+
+ def __init__(self, d):
+ Xpub.__init__(self)
+ Deterministic_KeyStore.__init__(self, d)
+ self.xpub = d.get('xpub')
+ self.xprv = d.get('xprv')
+
+ def format_seed(self, seed):
+ return ' '.join(seed.split())
+
+ def dump(self):
+ d = Deterministic_KeyStore.dump(self)
+ d['type'] = 'bip32'
+ d['xpub'] = self.xpub
+ d['xprv'] = self.xprv
+ return d
+
+ def get_master_private_key(self, password):
+ return pw_decode(self.xprv, password)
+
+ def check_password(self, password):
+ xprv = pw_decode(self.xprv, password)
+ if deserialize_xprv(xprv)[4] != deserialize_xpub(self.xpub)[4]:
+ raise InvalidPassword()
+
+ def update_password(self, old_password, new_password):
+ self.check_password(old_password)
+ if new_password == '':
+ new_password = None
+ if self.has_seed():
+ decoded = self.get_seed(old_password)
+ self.seed = pw_encode(decoded, new_password)
+ if self.passphrase:
+ decoded = self.get_passphrase(old_password)
+ self.passphrase = pw_encode(decoded, new_password)
+ if self.xprv is not None:
+ b = pw_decode(self.xprv, old_password)
+ self.xprv = pw_encode(b, new_password)
+
+ def is_watching_only(self):
+ return self.xprv is None
+
+ def add_xprv(self, xprv):
+ self.xprv = xprv
+ self.xpub = bitcoin.xpub_from_xprv(xprv)
+
+ def add_xprv_from_seed(self, bip32_seed, xtype, derivation):
+ xprv, xpub = bip32_root(bip32_seed, xtype)
+ xprv, xpub = bip32_private_derivation(xprv, "m/", derivation)
+ self.add_xprv(xprv)
+
+ def get_private_key(self, sequence, password):
+ xprv = self.get_master_private_key(password)
+ _, _, _, _, c, k = deserialize_xprv(xprv)
+ pk = bip32_private_key(sequence, k, c)
+ return pk, True
+
+
+
+class Old_KeyStore(Deterministic_KeyStore):
+
+ def __init__(self, d):
+ Deterministic_KeyStore.__init__(self, d)
+ self.mpk = d.get('mpk')
+
+ def get_hex_seed(self, password):
+ return pw_decode(self.seed, password).encode('utf8')
+
+ def dump(self):
+ d = Deterministic_KeyStore.dump(self)
+ d['mpk'] = self.mpk
+ d['type'] = 'old'
+ return d
+
+ def add_seed(self, seedphrase):
+ Deterministic_KeyStore.add_seed(self, seedphrase)
+ s = self.get_hex_seed(None)
+ self.mpk = self.mpk_from_seed(s)
+
+ def add_master_public_key(self, mpk):
+ self.mpk = mpk
+
+ def format_seed(self, seed):
+ from . import old_mnemonic, mnemonic
+ seed = mnemonic.normalize_text(seed)
+ # see if seed was entered as hex
+ if seed:
+ try:
+ bfh(seed)
+ return str(seed)
+ except Exception:
+ pass
+ words = seed.split()
+ seed = old_mnemonic.mn_decode(words)
+ if not seed:
+ raise Exception("Invalid seed")
+ return seed
+
+ def get_seed(self, password):
+ from . import old_mnemonic
+ s = self.get_hex_seed(password)
+ return ' '.join(old_mnemonic.mn_encode(s))
+
+ @classmethod
+ def mpk_from_seed(klass, seed):
+ secexp = klass.stretch_key(seed)
+ privkey = ecc.ECPrivkey.from_secret_scalar(secexp)
+ return privkey.get_public_key_hex(compressed=False)[2:]
+
+ @classmethod
+ def stretch_key(self, seed):
+ x = seed
+ for i in range(100000):
+ x = hashlib.sha256(x + seed).digest()
+ return string_to_number(x)
+
+ @classmethod
+ def get_sequence(self, mpk, for_change, n):
+ return string_to_number(Hash(("%d:%d:"%(n, for_change)).encode('ascii') + bfh(mpk)))
+
+ @classmethod
+ def get_pubkey_from_mpk(self, mpk, for_change, n):
+ z = self.get_sequence(mpk, for_change, n)
+ master_public_key = ecc.ECPubkey(bfh('04'+mpk))
+ public_key = master_public_key + z*ecc.generator()
+ return public_key.get_public_key_hex(compressed=False)
+
+ def derive_pubkey(self, for_change, n):
+ return self.get_pubkey_from_mpk(self.mpk, for_change, n)
+
+ def get_private_key_from_stretched_exponent(self, for_change, n, secexp):
+ secexp = (secexp + self.get_sequence(self.mpk, for_change, n)) % ecc.CURVE_ORDER
+ pk = number_to_string(secexp, ecc.CURVE_ORDER)
+ return pk
+
+ def get_private_key(self, sequence, password):
+ seed = self.get_hex_seed(password)
+ self.check_seed(seed)
+ for_change, n = sequence
+ secexp = self.stretch_key(seed)
+ pk = self.get_private_key_from_stretched_exponent(for_change, n, secexp)
+ return pk, False
+
+ def check_seed(self, seed):
+ secexp = self.stretch_key(seed)
+ master_private_key = ecc.ECPrivkey.from_secret_scalar(secexp)
+ master_public_key = master_private_key.get_public_key_bytes(compressed=False)[1:]
+ if master_public_key != bfh(self.mpk):
+ print_error('invalid password (mpk)', self.mpk, bh2u(master_public_key))
+ raise InvalidPassword()
+
+ def check_password(self, password):
+ seed = self.get_hex_seed(password)
+ self.check_seed(seed)
+
+ def get_master_public_key(self):
+ return self.mpk
+
+ def get_xpubkey(self, for_change, n):
+ s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (for_change, n)))
+ return 'fe' + self.mpk + s
+
+ @classmethod
+ def parse_xpubkey(self, x_pubkey):
+ assert x_pubkey[0:2] == 'fe'
+ pk = x_pubkey[2:]
+ mpk = pk[0:128]
+ dd = pk[128:]
+ s = []
+ while dd:
+ n = int(bitcoin.rev_hex(dd[0:4]), 16)
+ dd = dd[4:]
+ s.append(n)
+ assert len(s) == 2
+ return mpk, s
+
+ def get_pubkey_derivation(self, x_pubkey):
+ if x_pubkey[0:2] != 'fe':
+ return
+ mpk, derivation = self.parse_xpubkey(x_pubkey)
+ if self.mpk != mpk:
+ return
+ return derivation
+
+ def update_password(self, old_password, new_password):
+ self.check_password(old_password)
+ if new_password == '':
+ new_password = None
+ if self.has_seed():
+ decoded = pw_decode(self.seed, old_password)
+ self.seed = pw_encode(decoded, new_password)
+
+
+
+class Hardware_KeyStore(KeyStore, Xpub):
+ # Derived classes must set:
+ # - device
+ # - DEVICE_IDS
+ # - wallet_type
+
+ #restore_wallet_class = BIP32_RD_Wallet
+ max_change_outputs = 1
+
+ def __init__(self, d):
+ Xpub.__init__(self)
+ KeyStore.__init__(self)
+ # Errors and other user interaction is done through the wallet's
+ # handler. The handler is per-window and preserved across
+ # device reconnects
+ self.xpub = d.get('xpub')
+ self.label = d.get('label')
+ self.derivation = d.get('derivation')
+ self.handler = None
+ run_hook('init_keystore', self)
+
+ def set_label(self, label):
+ self.label = label
+
+ def may_have_password(self):
+ return False
+
+ def is_deterministic(self):
+ return True
+
+ def dump(self):
+ return {
+ 'type': 'hardware',
+ 'hw_type': self.hw_type,
+ 'xpub': self.xpub,
+ 'derivation':self.derivation,
+ 'label':self.label,
+ }
+
+ def unpaired(self):
+ '''A device paired with the wallet was disconnected. This can be
+ called in any thread context.'''
+ self.print_error("unpaired")
+
+ def paired(self):
+ '''A device paired with the wallet was (re-)connected. This can be
+ called in any thread context.'''
+ self.print_error("paired")
+
+ def can_export(self):
+ return False
+
+ def is_watching_only(self):
+ '''The wallet is not watching-only; the user will be prompted for
+ pin and passphrase as appropriate when needed.'''
+ assert not self.has_seed()
+ return False
+
+ def get_password_for_storage_encryption(self):
+ from .storage import get_derivation_used_for_hw_device_encryption
+ client = self.plugin.get_client(self)
+ derivation = get_derivation_used_for_hw_device_encryption()
+ xpub = client.get_xpub(derivation, "standard")
+ password = self.get_pubkey_from_xpub(xpub, ())
+ return password
+
+ def has_usable_connection_with_device(self):
+ if not hasattr(self, 'plugin'):
+ return False
+ client = self.plugin.get_client(self, force_pair=False)
+ if client is None:
+ return False
+ return client.has_usable_connection_with_device()
+
+ def ready_to_sign(self):
+ return super().ready_to_sign() and self.has_usable_connection_with_device()
+
+
+def bip39_normalize_passphrase(passphrase):
+ return normalize('NFKD', passphrase or '')
+
+def bip39_to_seed(mnemonic, passphrase):
+ import pbkdf2, hashlib, hmac
+ PBKDF2_ROUNDS = 2048
+ mnemonic = normalize('NFKD', ' '.join(mnemonic.split()))
+ passphrase = bip39_normalize_passphrase(passphrase)
+ return pbkdf2.PBKDF2(mnemonic, 'mnemonic' + passphrase,
+ iterations = PBKDF2_ROUNDS, macmodule = hmac,
+ digestmodule = hashlib.sha512).read(64)
+
+# returns tuple (is_checksum_valid, is_wordlist_valid)
+def bip39_is_checksum_valid(mnemonic):
+ words = [ normalize('NFKD', word) for word in mnemonic.split() ]
+ words_len = len(words)
+ wordlist = load_wordlist("english.txt")
+ n = len(wordlist)
+ checksum_length = 11*words_len//33
+ entropy_length = 32*checksum_length
+ i = 0
+ words.reverse()
+ while words:
+ w = words.pop()
+ try:
+ k = wordlist.index(w)
+ except ValueError:
+ return False, False
+ i = i*n + k
+ if words_len not in [12, 15, 18, 21, 24]:
+ return False, True
+ entropy = i >> checksum_length
+ checksum = i % 2**checksum_length
+ h = '{:x}'.format(entropy)
+ while len(h) < entropy_length/4:
+ h = '0'+h
+ b = bytearray.fromhex(h)
+ hashed = int(hfu(hashlib.sha256(b).digest()), 16)
+ calculated_checksum = hashed >> (256 - checksum_length)
+ return checksum == calculated_checksum, True
+
+
+def from_bip39_seed(seed, passphrase, derivation, xtype=None):
+ k = BIP32_KeyStore({})
+ bip32_seed = bip39_to_seed(seed, passphrase)
+ if xtype is None:
+ xtype = xtype_from_derivation(derivation)
+ k.add_xprv_from_seed(bip32_seed, xtype, derivation)
+ return k
+
+
+def xtype_from_derivation(derivation: str) -> str:
+ """Returns the script type to be used for this derivation."""
+ if derivation.startswith("m/84'"):
+ return 'p2wpkh'
+ elif derivation.startswith("m/49'"):
+ return 'p2wpkh-p2sh'
+ elif derivation.startswith("m/44'"):
+ return 'standard'
+ elif derivation.startswith("m/45'"):
+ return 'standard'
+
+ bip32_indices = list(bip32_derivation(derivation))
+ if len(bip32_indices) >= 4:
+ if bip32_indices[0] == 48 + BIP32_PRIME:
+ # m / purpose' / coin_type' / account' / script_type' / change / address_index
+ script_type_int = bip32_indices[3] - BIP32_PRIME
+ script_type = PURPOSE48_SCRIPT_TYPES_INV.get(script_type_int)
+ if script_type is not None:
+ return script_type
+ return 'standard'
+
+
+# extended pubkeys
+
+def is_xpubkey(x_pubkey):
+ return x_pubkey[0:2] == 'ff'
+
+
+def parse_xpubkey(x_pubkey):
+ assert x_pubkey[0:2] == 'ff'
+ return BIP32_KeyStore.parse_xpubkey(x_pubkey)
+
+
+def xpubkey_to_address(x_pubkey):
+ if x_pubkey[0:2] == 'fd':
+ address = bitcoin.script_to_address(x_pubkey[2:])
+ return x_pubkey, address
+ if x_pubkey[0:2] in ['02', '03', '04']:
+ pubkey = x_pubkey
+ elif x_pubkey[0:2] == 'ff':
+ xpub, s = BIP32_KeyStore.parse_xpubkey(x_pubkey)
+ pubkey = BIP32_KeyStore.get_pubkey_from_xpub(xpub, s)
+ elif x_pubkey[0:2] == 'fe':
+ mpk, s = Old_KeyStore.parse_xpubkey(x_pubkey)
+ pubkey = Old_KeyStore.get_pubkey_from_mpk(mpk, s[0], s[1])
+ else:
+ raise BitcoinException("Cannot parse pubkey. prefix: {}"
+ .format(x_pubkey[0:2]))
+ if pubkey:
+ address = public_key_to_p2pkh(bfh(pubkey))
+ return pubkey, address
+
+def xpubkey_to_pubkey(x_pubkey):
+ pubkey, address = xpubkey_to_address(x_pubkey)
+ return pubkey
+
+hw_keystores = {}
+
+def register_keystore(hw_type, constructor):
+ hw_keystores[hw_type] = constructor
+
+def hardware_keystore(d):
+ hw_type = d['hw_type']
+ if hw_type in hw_keystores:
+ constructor = hw_keystores[hw_type]
+ return constructor(d)
+ raise WalletFileException('unknown hardware type: {}. hw_keystores: {}'.format(hw_type, list(hw_keystores.keys())))
+
+def load_keystore(storage, name):
+ d = storage.get(name, {})
+ t = d.get('type')
+ if not t:
+ raise WalletFileException(
+ 'Wallet format requires update.\n'
+ 'Cannot find keystore for name {}'.format(name))
+ if t == 'old':
+ k = Old_KeyStore(d)
+ elif t == 'imported':
+ k = Imported_KeyStore(d)
+ elif t == 'bip32':
+ k = BIP32_KeyStore(d)
+ elif t == 'hardware':
+ k = hardware_keystore(d)
+ else:
+ raise WalletFileException(
+ 'Unknown type {} for keystore named {}'.format(t, name))
+ return k
+
+
+def is_old_mpk(mpk: str) -> bool:
+ try:
+ int(mpk, 16)
+ except:
+ return False
+ if len(mpk) != 128:
+ return False
+ try:
+ ecc.ECPubkey(bfh('04' + mpk))
+ except:
+ return False
+ return True
+
+
+def is_address_list(text):
+ parts = text.split()
+ return bool(parts) and all(bitcoin.is_address(x) for x in parts)
+
+
+def get_private_keys(text):
+ parts = text.split('\n')
+ parts = map(lambda x: ''.join(x.split()), parts)
+ parts = list(filter(bool, parts))
+ if bool(parts) and all(bitcoin.is_private_key(x) for x in parts):
+ return parts
+
+
+def is_private_key_list(text):
+ return bool(get_private_keys(text))
+
+
+is_mpk = lambda x: is_old_mpk(x) or is_xpub(x)
+is_private = lambda x: is_seed(x) or is_xprv(x) or is_private_key_list(x)
+is_master_key = lambda x: is_old_mpk(x) or is_xprv(x) or is_xpub(x)
+is_private_key = lambda x: is_xprv(x) or is_private_key_list(x)
+is_bip32_key = lambda x: is_xprv(x) or is_xpub(x)
+
+
+def bip44_derivation(account_id, bip43_purpose=44):
+ coin = constants.net.BIP44_COIN_TYPE
+ return "m/%d'/%d'/%d'" % (bip43_purpose, coin, int(account_id))
+
+
+def purpose48_derivation(account_id: int, xtype: str) -> str:
+ # m / purpose' / coin_type' / account' / script_type' / change / address_index
+ bip43_purpose = 48
+ coin = constants.net.BIP44_COIN_TYPE
+ account_id = int(account_id)
+ script_type_int = PURPOSE48_SCRIPT_TYPES.get(xtype)
+ if script_type_int is None:
+ raise Exception('unknown xtype: {}'.format(xtype))
+ return "m/%d'/%d'/%d'/%d'" % (bip43_purpose, coin, account_id, script_type_int)
+
+
+def from_seed(seed, passphrase, is_p2sh):
+ t = seed_type(seed)
+ if t == 'old':
+ keystore = Old_KeyStore({})
+ keystore.add_seed(seed)
+ elif t in ['standard', 'segwit']:
+ keystore = BIP32_KeyStore({})
+ keystore.add_seed(seed)
+ keystore.passphrase = passphrase
+ bip32_seed = Mnemonic.mnemonic_to_seed(seed, passphrase)
+ if t == 'standard':
+ der = "m/"
+ xtype = 'standard'
+ else:
+ der = "m/1'/" if is_p2sh else "m/0'/"
+ xtype = 'p2wsh' if is_p2sh else 'p2wpkh'
+ keystore.add_xprv_from_seed(bip32_seed, xtype, der)
+ else:
+ raise BitcoinException('Unexpected seed type {}'.format(t))
+ return keystore
+
+def from_private_key_list(text):
+ keystore = Imported_KeyStore({})
+ for x in get_private_keys(text):
+ keystore.import_key(x, None)
+ return keystore
+
+def from_old_mpk(mpk):
+ keystore = Old_KeyStore({})
+ keystore.add_master_public_key(mpk)
+ return keystore
+
+def from_xpub(xpub):
+ k = BIP32_KeyStore({})
+ k.xpub = xpub
+ return k
+
+def from_xprv(xprv):
+ xpub = bitcoin.xpub_from_xprv(xprv)
+ k = BIP32_KeyStore({})
+ k.xprv = xprv
+ k.xpub = xpub
+ return k
+
+def from_master_key(text):
+ if is_xprv(text):
+ k = from_xprv(text)
+ elif is_old_mpk(text):
+ k = from_old_mpk(text)
+ elif is_xpub(text):
+ k = from_xpub(text)
+ else:
+ raise BitcoinException('Invalid master key')
+ return k
DIR diff --git a/electrum/mnemonic.py b/electrum/mnemonic.py
t@@ -0,0 +1,183 @@
+#!/usr/bin/env python
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2014 Thomas Voegtlin
+#
+# 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 os
+import hmac
+import math
+import hashlib
+import unicodedata
+import string
+
+import ecdsa
+import pbkdf2
+
+from .util import print_error
+from .bitcoin import is_old_seed, is_new_seed
+from . import version
+
+# http://www.asahi-net.or.jp/~ax2s-kmtn/ref/unicode/e_asia.html
+CJK_INTERVALS = [
+ (0x4E00, 0x9FFF, 'CJK Unified Ideographs'),
+ (0x3400, 0x4DBF, 'CJK Unified Ideographs Extension A'),
+ (0x20000, 0x2A6DF, 'CJK Unified Ideographs Extension B'),
+ (0x2A700, 0x2B73F, 'CJK Unified Ideographs Extension C'),
+ (0x2B740, 0x2B81F, 'CJK Unified Ideographs Extension D'),
+ (0xF900, 0xFAFF, 'CJK Compatibility Ideographs'),
+ (0x2F800, 0x2FA1D, 'CJK Compatibility Ideographs Supplement'),
+ (0x3190, 0x319F , 'Kanbun'),
+ (0x2E80, 0x2EFF, 'CJK Radicals Supplement'),
+ (0x2F00, 0x2FDF, 'CJK Radicals'),
+ (0x31C0, 0x31EF, 'CJK Strokes'),
+ (0x2FF0, 0x2FFF, 'Ideographic Description Characters'),
+ (0xE0100, 0xE01EF, 'Variation Selectors Supplement'),
+ (0x3100, 0x312F, 'Bopomofo'),
+ (0x31A0, 0x31BF, 'Bopomofo Extended'),
+ (0xFF00, 0xFFEF, 'Halfwidth and Fullwidth Forms'),
+ (0x3040, 0x309F, 'Hiragana'),
+ (0x30A0, 0x30FF, 'Katakana'),
+ (0x31F0, 0x31FF, 'Katakana Phonetic Extensions'),
+ (0x1B000, 0x1B0FF, 'Kana Supplement'),
+ (0xAC00, 0xD7AF, 'Hangul Syllables'),
+ (0x1100, 0x11FF, 'Hangul Jamo'),
+ (0xA960, 0xA97F, 'Hangul Jamo Extended A'),
+ (0xD7B0, 0xD7FF, 'Hangul Jamo Extended B'),
+ (0x3130, 0x318F, 'Hangul Compatibility Jamo'),
+ (0xA4D0, 0xA4FF, 'Lisu'),
+ (0x16F00, 0x16F9F, 'Miao'),
+ (0xA000, 0xA48F, 'Yi Syllables'),
+ (0xA490, 0xA4CF, 'Yi Radicals'),
+]
+
+def is_CJK(c):
+ n = ord(c)
+ for imin,imax,name in CJK_INTERVALS:
+ if n>=imin and n<=imax: return True
+ return False
+
+
+def normalize_text(seed):
+ # normalize
+ seed = unicodedata.normalize('NFKD', seed)
+ # lower
+ seed = seed.lower()
+ # remove accents
+ seed = u''.join([c for c in seed if not unicodedata.combining(c)])
+ # normalize whitespaces
+ seed = u' '.join(seed.split())
+ # remove whitespaces between CJK
+ seed = u''.join([seed[i] for i in range(len(seed)) if not (seed[i] in string.whitespace and is_CJK(seed[i-1]) and is_CJK(seed[i+1]))])
+ return seed
+
+def load_wordlist(filename):
+ path = os.path.join(os.path.dirname(__file__), 'wordlist', filename)
+ with open(path, 'r', encoding='utf-8') as f:
+ s = f.read().strip()
+ s = unicodedata.normalize('NFKD', s)
+ lines = s.split('\n')
+ wordlist = []
+ for line in lines:
+ line = line.split('#')[0]
+ line = line.strip(' \r')
+ assert ' ' not in line
+ if line:
+ wordlist.append(line)
+ return wordlist
+
+
+filenames = {
+ 'en':'english.txt',
+ 'es':'spanish.txt',
+ 'ja':'japanese.txt',
+ 'pt':'portuguese.txt',
+ 'zh':'chinese_simplified.txt'
+}
+
+
+
+class Mnemonic(object):
+ # Seed derivation no longer follows BIP39
+ # Mnemonic phrase uses a hash based checksum, instead of a wordlist-dependent checksum
+
+ def __init__(self, lang=None):
+ lang = lang or 'en'
+ print_error('language', lang)
+ filename = filenames.get(lang[0:2], 'english.txt')
+ self.wordlist = load_wordlist(filename)
+ print_error("wordlist has %d words"%len(self.wordlist))
+
+ @classmethod
+ def mnemonic_to_seed(self, mnemonic, passphrase):
+ PBKDF2_ROUNDS = 2048
+ mnemonic = normalize_text(mnemonic)
+ passphrase = normalize_text(passphrase)
+ return pbkdf2.PBKDF2(mnemonic, 'electrum' + passphrase, iterations = PBKDF2_ROUNDS, macmodule = hmac, digestmodule = hashlib.sha512).read(64)
+
+ def mnemonic_encode(self, i):
+ n = len(self.wordlist)
+ words = []
+ while i:
+ x = i%n
+ i = i//n
+ words.append(self.wordlist[x])
+ return ' '.join(words)
+
+ def get_suggestions(self, prefix):
+ for w in self.wordlist:
+ if w.startswith(prefix):
+ yield w
+
+ def mnemonic_decode(self, seed):
+ n = len(self.wordlist)
+ words = seed.split()
+ i = 0
+ while words:
+ w = words.pop()
+ k = self.wordlist.index(w)
+ i = i*n + k
+ return i
+
+ def make_seed(self, seed_type='standard', num_bits=132):
+ prefix = version.seed_prefix(seed_type)
+ # increase num_bits in order to obtain a uniform distribution for the last word
+ bpw = math.log(len(self.wordlist), 2)
+ # rounding
+ n = int(math.ceil(num_bits/bpw) * bpw)
+ print_error("make_seed. prefix: '%s'"%prefix, "entropy: %d bits"%n)
+ entropy = 1
+ while entropy < pow(2, n - bpw):
+ # try again if seed would not contain enough words
+ entropy = ecdsa.util.randrange(pow(2, n))
+ nonce = 0
+ while True:
+ nonce += 1
+ i = entropy + nonce
+ seed = self.mnemonic_encode(i)
+ if i != self.mnemonic_decode(seed):
+ raise Exception('Cannot extract same entropy from mnemonic!')
+ if is_old_seed(seed):
+ continue
+ if is_new_seed(seed, prefix):
+ break
+ print_error('%d words'%len(seed.split()))
+ return seed
DIR diff --git a/electrum/msqr.py b/electrum/msqr.py
t@@ -0,0 +1,94 @@
+# from http://eli.thegreenplace.net/2009/03/07/computing-modular-square-roots-in-python/
+
+def modular_sqrt(a, p):
+ """ Find a quadratic residue (mod p) of 'a'. p
+ must be an odd prime.
+
+ Solve the congruence of the form:
+ x^2 = a (mod p)
+ And returns x. Note that p - x is also a root.
+
+ 0 is returned is no square root exists for
+ these a and p.
+
+ The Tonelli-Shanks algorithm is used (except
+ for some simple cases in which the solution
+ is known from an identity). This algorithm
+ runs in polynomial time (unless the
+ generalized Riemann hypothesis is false).
+ """
+ # Simple cases
+ #
+ if legendre_symbol(a, p) != 1:
+ return 0
+ elif a == 0:
+ return 0
+ elif p == 2:
+ return p
+ elif p % 4 == 3:
+ return pow(a, (p + 1) // 4, p)
+
+ # Partition p-1 to s * 2^e for an odd s (i.e.
+ # reduce all the powers of 2 from p-1)
+ #
+ s = p - 1
+ e = 0
+ while s % 2 == 0:
+ s //= 2
+ e += 1
+
+ # Find some 'n' with a legendre symbol n|p = -1.
+ # Shouldn't take long.
+ #
+ n = 2
+ while legendre_symbol(n, p) != -1:
+ n += 1
+
+ # Here be dragons!
+ # Read the paper "Square roots from 1; 24, 51,
+ # 10 to Dan Shanks" by Ezra Brown for more
+ # information
+ #
+
+ # x is a guess of the square root that gets better
+ # with each iteration.
+ # b is the "fudge factor" - by how much we're off
+ # with the guess. The invariant x^2 = ab (mod p)
+ # is maintained throughout the loop.
+ # g is used for successive powers of n to update
+ # both a and b
+ # r is the exponent - decreases with each update
+ #
+ x = pow(a, (s + 1) // 2, p)
+ b = pow(a, s, p)
+ g = pow(n, s, p)
+ r = e
+
+ while True:
+ t = b
+ m = 0
+ for m in range(r):
+ if t == 1:
+ break
+ t = pow(t, 2, p)
+
+ if m == 0:
+ return x
+
+ gs = pow(g, 2 ** (r - m - 1), p)
+ g = (gs * gs) % p
+ x = (x * gs) % p
+ b = (b * g) % p
+ r = m
+
+def legendre_symbol(a, p):
+ """ Compute the Legendre symbol a|p using
+ Euler's criterion. p is a prime, a is
+ relatively prime to p (if p divides
+ a, then a|p = 0)
+
+ Returns 1 if a has a square root modulo
+ p, -1 otherwise.
+ """
+ ls = pow(a, (p - 1) // 2, p)
+ return -1 if ls == p - 1 else ls
DIR diff --git a/electrum/network.py b/electrum/network.py
t@@ -0,0 +1,1297 @@
+# Electrum - Lightweight Bitcoin Client
+# Copyright (c) 2011-2016 Thomas Voegtlin
+#
+# 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 time
+import queue
+import os
+import errno
+import random
+import re
+import select
+from collections import defaultdict
+import threading
+import socket
+import json
+import sys
+import ipaddress
+
+import dns
+import dns.resolver
+import socks
+
+from . import util
+from .util import print_error
+from . import bitcoin
+from .bitcoin import COIN
+from . import constants
+from .interface import Connection, Interface
+from . import blockchain
+from .version import ELECTRUM_VERSION, PROTOCOL_VERSION
+from .i18n import _
+
+
+NODES_RETRY_INTERVAL = 60
+SERVER_RETRY_INTERVAL = 10
+
+
+def parse_servers(result):
+ """ parse servers list into dict format"""
+ servers = {}
+ for item in result:
+ host = item[1]
+ out = {}
+ version = None
+ pruning_level = '-'
+ if len(item) > 2:
+ for v in item[2]:
+ if re.match("[st]\d*", v):
+ protocol, port = v[0], v[1:]
+ if port == '': port = constants.net.DEFAULT_PORTS[protocol]
+ out[protocol] = port
+ elif re.match("v(.?)+", v):
+ version = v[1:]
+ elif re.match("p\d*", v):
+ pruning_level = v[1:]
+ if pruning_level == '': pruning_level = '0'
+ if out:
+ out['pruning'] = pruning_level
+ out['version'] = version
+ servers[host] = out
+ return servers
+
+
+def filter_version(servers):
+ def is_recent(version):
+ try:
+ return util.normalize_version(version) >= util.normalize_version(PROTOCOL_VERSION)
+ except Exception as e:
+ return False
+ return {k: v for k, v in servers.items() if is_recent(v.get('version'))}
+
+
+def filter_protocol(hostmap, protocol='s'):
+ '''Filters the hostmap for those implementing protocol.
+ The result is a list in serialized form.'''
+ eligible = []
+ for host, portmap in hostmap.items():
+ port = portmap.get(protocol)
+ if port:
+ eligible.append(serialize_server(host, port, protocol))
+ return eligible
+
+
+def pick_random_server(hostmap = None, protocol = 's', exclude_set = set()):
+ if hostmap is None:
+ hostmap = constants.net.DEFAULT_SERVERS
+ eligible = list(set(filter_protocol(hostmap, protocol)) - exclude_set)
+ return random.choice(eligible) if eligible else None
+
+
+from .simple_config import SimpleConfig
+
+proxy_modes = ['socks4', 'socks5', 'http']
+
+
+def serialize_proxy(p):
+ if not isinstance(p, dict):
+ return None
+ return ':'.join([p.get('mode'), p.get('host'), p.get('port'),
+ p.get('user', ''), p.get('password', '')])
+
+
+def deserialize_proxy(s):
+ if not isinstance(s, str):
+ return None
+ if s.lower() == 'none':
+ return None
+ proxy = { "mode":"socks5", "host":"localhost" }
+ args = s.split(':')
+ n = 0
+ if proxy_modes.count(args[n]) == 1:
+ proxy["mode"] = args[n]
+ n += 1
+ if len(args) > n:
+ proxy["host"] = args[n]
+ n += 1
+ if len(args) > n:
+ proxy["port"] = args[n]
+ n += 1
+ else:
+ proxy["port"] = "8080" if proxy["mode"] == "http" else "1080"
+ if len(args) > n:
+ proxy["user"] = args[n]
+ n += 1
+ if len(args) > n:
+ proxy["password"] = args[n]
+ return proxy
+
+
+def deserialize_server(server_str):
+ host, port, protocol = str(server_str).rsplit(':', 2)
+ if protocol not in 'st':
+ raise ValueError('invalid network protocol: {}'.format(protocol))
+ int(port) # Throw if cannot be converted to int
+ return host, port, protocol
+
+
+def serialize_server(host, port, protocol):
+ return str(':'.join([host, port, protocol]))
+
+
+class Network(util.DaemonThread):
+ """The Network class manages a set of connections to remote electrum
+ servers, each connected socket is handled by an Interface() object.
+ Connections are initiated by a Connection() thread which stops once
+ the connection succeeds or fails.
+
+ Our external API:
+
+ - Member functions get_header(), get_interfaces(), get_local_height(),
+ get_parameters(), get_server_height(), get_status_value(),
+ is_connected(), set_parameters(), stop()
+ """
+
+ def __init__(self, config=None):
+ if config is None:
+ config = {} # Do not use mutables as default values!
+ util.DaemonThread.__init__(self)
+ self.config = SimpleConfig(config) if isinstance(config, dict) else config
+ self.num_server = 10 if not self.config.get('oneserver') else 0
+ self.blockchains = blockchain.read_blockchains(self.config) # note: needs self.blockchains_lock
+ self.print_error("blockchains", self.blockchains.keys())
+ self.blockchain_index = config.get('blockchain_index', 0)
+ if self.blockchain_index not in self.blockchains.keys():
+ self.blockchain_index = 0
+ # Server for addresses and transactions
+ self.default_server = self.config.get('server', None)
+ # Sanitize default server
+ if self.default_server:
+ try:
+ deserialize_server(self.default_server)
+ except:
+ self.print_error('Warning: failed to parse server-string; falling back to random.')
+ self.default_server = None
+ if not self.default_server:
+ self.default_server = pick_random_server()
+
+ # locks: if you need to take multiple ones, acquire them in the order they are defined here!
+ self.interface_lock = threading.RLock() # <- re-entrant
+ self.callback_lock = threading.Lock()
+ self.pending_sends_lock = threading.Lock()
+ self.recent_servers_lock = threading.RLock() # <- re-entrant
+ self.subscribed_addresses_lock = threading.Lock()
+ self.blockchains_lock = threading.Lock()
+
+ self.pending_sends = []
+ self.message_id = 0
+ self.debug = False
+ self.irc_servers = {} # returned by interface (list from irc)
+ self.recent_servers = self.read_recent_servers() # note: needs self.recent_servers_lock
+
+ self.banner = ''
+ self.donation_address = ''
+ self.relay_fee = None
+ # callbacks passed with subscriptions
+ self.subscriptions = defaultdict(list) # note: needs self.callback_lock
+ self.sub_cache = {} # note: needs self.interface_lock
+ # callbacks set by the GUI
+ self.callbacks = defaultdict(list) # note: needs self.callback_lock
+
+ dir_path = os.path.join(self.config.path, 'certs')
+ util.make_dir(dir_path)
+
+ # subscriptions and requests
+ self.subscribed_addresses = set() # note: needs self.subscribed_addresses_lock
+ self.h2addr = {}
+ # Requests from client we've not seen a response to
+ self.unanswered_requests = {}
+ # retry times
+ self.server_retry_time = time.time()
+ self.nodes_retry_time = time.time()
+ # kick off the network. interface is the main server we are currently
+ # communicating with. interfaces is the set of servers we are connecting
+ # to or have an ongoing connection with
+ self.interface = None # note: needs self.interface_lock
+ self.interfaces = {} # note: needs self.interface_lock
+ self.auto_connect = self.config.get('auto_connect', True)
+ self.connecting = set()
+ self.requested_chunks = set()
+ self.socket_queue = queue.Queue()
+ self.start_network(deserialize_server(self.default_server)[2],
+ deserialize_proxy(self.config.get('proxy')))
+
+ def with_interface_lock(func):
+ def func_wrapper(self, *args, **kwargs):
+ with self.interface_lock:
+ return func(self, *args, **kwargs)
+ return func_wrapper
+
+ def with_recent_servers_lock(func):
+ def func_wrapper(self, *args, **kwargs):
+ with self.recent_servers_lock:
+ return func(self, *args, **kwargs)
+ return func_wrapper
+
+ def register_callback(self, callback, events):
+ with self.callback_lock:
+ for event in events:
+ self.callbacks[event].append(callback)
+
+ def unregister_callback(self, callback):
+ with self.callback_lock:
+ for callbacks in self.callbacks.values():
+ if callback in callbacks:
+ callbacks.remove(callback)
+
+ def trigger_callback(self, event, *args):
+ with self.callback_lock:
+ callbacks = self.callbacks[event][:]
+ [callback(event, *args) for callback in callbacks]
+
+ def read_recent_servers(self):
+ if not self.config.path:
+ return []
+ path = os.path.join(self.config.path, "recent_servers")
+ try:
+ with open(path, "r", encoding='utf-8') as f:
+ data = f.read()
+ return json.loads(data)
+ except:
+ return []
+
+ @with_recent_servers_lock
+ def save_recent_servers(self):
+ if not self.config.path:
+ return
+ path = os.path.join(self.config.path, "recent_servers")
+ s = json.dumps(self.recent_servers, indent=4, sort_keys=True)
+ try:
+ with open(path, "w", encoding='utf-8') as f:
+ f.write(s)
+ except:
+ pass
+
+ @with_interface_lock
+ def get_server_height(self):
+ return self.interface.tip if self.interface else 0
+
+ def server_is_lagging(self):
+ sh = self.get_server_height()
+ if not sh:
+ self.print_error('no height for main interface')
+ return True
+ lh = self.get_local_height()
+ result = (lh - sh) > 1
+ if result:
+ self.print_error('%s is lagging (%d vs %d)' % (self.default_server, sh, lh))
+ return result
+
+ def set_status(self, status):
+ self.connection_status = status
+ self.notify('status')
+
+ def is_connected(self):
+ return self.interface is not None
+
+ def is_connecting(self):
+ return self.connection_status == 'connecting'
+
+ @with_interface_lock
+ def queue_request(self, method, params, interface=None):
+ # If you want to queue a request on any interface it must go
+ # through this function so message ids are properly tracked
+ if interface is None:
+ interface = self.interface
+ if interface is None:
+ self.print_error('warning: dropping request', method, params)
+ return
+ message_id = self.message_id
+ self.message_id += 1
+ if self.debug:
+ self.print_error(interface.host, "-->", method, params, message_id)
+ interface.queue_request(method, params, message_id)
+ return message_id
+
+ @with_interface_lock
+ def send_subscriptions(self):
+ assert self.interface
+ self.print_error('sending subscriptions to', self.interface.server, len(self.unanswered_requests), len(self.subscribed_addresses))
+ self.sub_cache.clear()
+ # Resend unanswered requests
+ requests = self.unanswered_requests.values()
+ self.unanswered_requests = {}
+ for request in requests:
+ message_id = self.queue_request(request[0], request[1])
+ self.unanswered_requests[message_id] = request
+ self.queue_request('server.banner', [])
+ self.queue_request('server.donation_address', [])
+ self.queue_request('server.peers.subscribe', [])
+ self.request_fee_estimates()
+ self.queue_request('blockchain.relayfee', [])
+ with self.subscribed_addresses_lock:
+ for h in self.subscribed_addresses:
+ self.queue_request('blockchain.scripthash.subscribe', [h])
+
+ def request_fee_estimates(self):
+ from .simple_config import FEE_ETA_TARGETS
+ self.config.requested_fee_estimates()
+ self.queue_request('mempool.get_fee_histogram', [])
+ for i in FEE_ETA_TARGETS:
+ self.queue_request('blockchain.estimatefee', [i])
+
+ def get_status_value(self, key):
+ if key == 'status':
+ value = self.connection_status
+ elif key == 'banner':
+ value = self.banner
+ elif key == 'fee':
+ value = self.config.fee_estimates
+ elif key == 'fee_histogram':
+ value = self.config.mempool_fees
+ elif key == 'updated':
+ value = (self.get_local_height(), self.get_server_height())
+ elif key == 'servers':
+ value = self.get_servers()
+ elif key == 'interfaces':
+ value = self.get_interfaces()
+ return value
+
+ def notify(self, key):
+ if key in ['status', 'updated']:
+ self.trigger_callback(key)
+ else:
+ self.trigger_callback(key, self.get_status_value(key))
+
+ def get_parameters(self):
+ host, port, protocol = deserialize_server(self.default_server)
+ return host, port, protocol, self.proxy, self.auto_connect
+
+ def get_donation_address(self):
+ if self.is_connected():
+ return self.donation_address
+
+ @with_interface_lock
+ def get_interfaces(self):
+ '''The interfaces that are in connected state'''
+ return list(self.interfaces.keys())
+
+ @with_recent_servers_lock
+ def get_servers(self):
+ out = constants.net.DEFAULT_SERVERS
+ if self.irc_servers:
+ out.update(filter_version(self.irc_servers.copy()))
+ else:
+ for s in self.recent_servers:
+ try:
+ host, port, protocol = deserialize_server(s)
+ except:
+ continue
+ if host not in out:
+ out[host] = {protocol: port}
+ return out
+
+ @with_interface_lock
+ def start_interface(self, server):
+ if (not server in self.interfaces and not server in self.connecting):
+ if server == self.default_server:
+ self.print_error("connecting to %s as new interface" % server)
+ self.set_status('connecting')
+ self.connecting.add(server)
+ Connection(server, self.socket_queue, self.config.path)
+
+ def start_random_interface(self):
+ with self.interface_lock:
+ exclude_set = self.disconnected_servers.union(set(self.interfaces))
+ server = pick_random_server(self.get_servers(), self.protocol, exclude_set)
+ if server:
+ self.start_interface(server)
+
+ def start_interfaces(self):
+ self.start_interface(self.default_server)
+ for i in range(self.num_server - 1):
+ self.start_random_interface()
+
+ def set_proxy(self, proxy):
+ self.proxy = proxy
+ # Store these somewhere so we can un-monkey-patch
+ if not hasattr(socket, "_socketobject"):
+ socket._socketobject = socket.socket
+ socket._getaddrinfo = socket.getaddrinfo
+ if proxy:
+ self.print_error('setting proxy', proxy)
+ proxy_mode = proxy_modes.index(proxy["mode"]) + 1
+ socks.setdefaultproxy(proxy_mode,
+ proxy["host"],
+ int(proxy["port"]),
+ # socks.py seems to want either None or a non-empty string
+ username=(proxy.get("user", "") or None),
+ password=(proxy.get("password", "") or None))
+ socket.socket = socks.socksocket
+ # prevent dns leaks, see http://stackoverflow.com/questions/13184205/dns-over-proxy
+ socket.getaddrinfo = lambda *args: [(socket.AF_INET, socket.SOCK_STREAM, 6, '', (args[0], args[1]))]
+ else:
+ socket.socket = socket._socketobject
+ if sys.platform == 'win32':
+ # On Windows, socket.getaddrinfo takes a mutex, and might hold it for up to 10 seconds
+ # when dns-resolving. To speed it up drastically, we resolve dns ourselves, outside that lock.
+ # see #4421
+ socket.getaddrinfo = self._fast_getaddrinfo
+ else:
+ socket.getaddrinfo = socket._getaddrinfo
+
+ @staticmethod
+ def _fast_getaddrinfo(host, *args, **kwargs):
+ def needs_dns_resolving(host2):
+ try:
+ ipaddress.ip_address(host2)
+ return False # already valid IP
+ except ValueError:
+ pass # not an IP
+ if str(host) in ('localhost', 'localhost.',):
+ return False
+ return True
+ try:
+ if needs_dns_resolving(host):
+ answers = dns.resolver.query(host)
+ addr = str(answers[0])
+ else:
+ addr = host
+ except dns.exception.DNSException:
+ # dns failed for some reason, e.g. dns.resolver.NXDOMAIN
+ # this is normal. Simply report back failure:
+ raise socket.gaierror(11001, 'getaddrinfo failed')
+ except BaseException as e:
+ # Possibly internal error in dnspython :( see #4483
+ # Fall back to original socket.getaddrinfo to resolve dns.
+ print_error('dnspython failed to resolve dns with error:', e)
+ addr = host
+ return socket._getaddrinfo(addr, *args, **kwargs)
+
+ @with_interface_lock
+ def start_network(self, protocol, proxy):
+ assert not self.interface and not self.interfaces
+ assert not self.connecting and self.socket_queue.empty()
+ self.print_error('starting network')
+ self.disconnected_servers = set([]) # note: needs self.interface_lock
+ self.protocol = protocol
+ self.set_proxy(proxy)
+ self.start_interfaces()
+
+ @with_interface_lock
+ def stop_network(self):
+ self.print_error("stopping network")
+ for interface in list(self.interfaces.values()):
+ self.close_interface(interface)
+ if self.interface:
+ self.close_interface(self.interface)
+ assert self.interface is None
+ assert not self.interfaces
+ self.connecting = set()
+ # Get a new queue - no old pending connections thanks!
+ self.socket_queue = queue.Queue()
+
+ def set_parameters(self, host, port, protocol, proxy, auto_connect):
+ proxy_str = serialize_proxy(proxy)
+ server = serialize_server(host, port, protocol)
+ # sanitize parameters
+ try:
+ deserialize_server(serialize_server(host, port, protocol))
+ if proxy:
+ proxy_modes.index(proxy["mode"]) + 1
+ int(proxy['port'])
+ except:
+ return
+ self.config.set_key('auto_connect', auto_connect, False)
+ self.config.set_key("proxy", proxy_str, False)
+ self.config.set_key("server", server, True)
+ # abort if changes were not allowed by config
+ if self.config.get('server') != server or self.config.get('proxy') != proxy_str:
+ return
+ self.auto_connect = auto_connect
+ if self.proxy != proxy or self.protocol != protocol:
+ # Restart the network defaulting to the given server
+ with self.interface_lock:
+ self.stop_network()
+ self.default_server = server
+ self.start_network(protocol, proxy)
+ elif self.default_server != server:
+ self.switch_to_interface(server)
+ else:
+ self.switch_lagging_interface()
+ self.notify('updated')
+
+ def switch_to_random_interface(self):
+ '''Switch to a random connected server other than the current one'''
+ servers = self.get_interfaces() # Those in connected state
+ if self.default_server in servers:
+ servers.remove(self.default_server)
+ if servers:
+ self.switch_to_interface(random.choice(servers))
+
+ @with_interface_lock
+ def switch_lagging_interface(self):
+ '''If auto_connect and lagging, switch interface'''
+ if self.server_is_lagging() and self.auto_connect:
+ # switch to one that has the correct header (not height)
+ header = self.blockchain().read_header(self.get_local_height())
+ filtered = list(map(lambda x: x[0], filter(lambda x: x[1].tip_header == header, self.interfaces.items())))
+ if filtered:
+ choice = random.choice(filtered)
+ self.switch_to_interface(choice)
+
+ @with_interface_lock
+ def switch_to_interface(self, server):
+ '''Switch to server as our interface. If no connection exists nor
+ being opened, start a thread to connect. The actual switch will
+ happen on receipt of the connection notification. Do nothing
+ if server already is our interface.'''
+ self.default_server = server
+ if server not in self.interfaces:
+ self.interface = None
+ self.start_interface(server)
+ return
+
+ i = self.interfaces[server]
+ if self.interface != i:
+ self.print_error("switching to", server)
+ # stop any current interface in order to terminate subscriptions
+ # fixme: we don't want to close headers sub
+ #self.close_interface(self.interface)
+ self.interface = i
+ self.send_subscriptions()
+ self.set_status('connected')
+ self.notify('updated')
+ self.notify('interfaces')
+
+ @with_interface_lock
+ def close_interface(self, interface):
+ if interface:
+ if interface.server in self.interfaces:
+ self.interfaces.pop(interface.server)
+ if interface.server == self.default_server:
+ self.interface = None
+ interface.close()
+
+ @with_recent_servers_lock
+ def add_recent_server(self, server):
+ # list is ordered
+ if server in self.recent_servers:
+ self.recent_servers.remove(server)
+ self.recent_servers.insert(0, server)
+ self.recent_servers = self.recent_servers[0:20]
+ self.save_recent_servers()
+
+ def process_response(self, interface, response, callbacks):
+ if self.debug:
+ self.print_error(interface.host, "<--", response)
+ error = response.get('error')
+ result = response.get('result')
+ method = response.get('method')
+ params = response.get('params')
+
+ # We handle some responses; return the rest to the client.
+ if method == 'server.version':
+ interface.server_version = result
+ elif method == 'blockchain.headers.subscribe':
+ if error is None:
+ self.on_notify_header(interface, result)
+ else:
+ # no point in keeping this connection without headers sub
+ self.connection_down(interface.server)
+ return
+ elif method == 'server.peers.subscribe':
+ if error is None:
+ self.irc_servers = parse_servers(result)
+ self.notify('servers')
+ elif method == 'server.banner':
+ if error is None:
+ self.banner = result
+ self.notify('banner')
+ elif method == 'server.donation_address':
+ if error is None:
+ self.donation_address = result
+ elif method == 'mempool.get_fee_histogram':
+ if error is None:
+ self.print_error('fee_histogram', result)
+ self.config.mempool_fees = result
+ self.notify('fee_histogram')
+ elif method == 'blockchain.estimatefee':
+ if error is None and result > 0:
+ i = params[0]
+ fee = int(result*COIN)
+ self.config.update_fee_estimates(i, fee)
+ self.print_error("fee_estimates[%d]" % i, fee)
+ self.notify('fee')
+ elif method == 'blockchain.relayfee':
+ if error is None:
+ self.relay_fee = int(result * COIN) if result is not None else None
+ self.print_error("relayfee", self.relay_fee)
+ elif method == 'blockchain.block.headers':
+ self.on_block_headers(interface, response)
+ elif method == 'blockchain.block.get_header':
+ self.on_get_header(interface, response)
+
+ for callback in callbacks:
+ callback(response)
+
+ @classmethod
+ def get_index(cls, method, params):
+ """ hashable index for subscriptions and cache"""
+ return str(method) + (':' + str(params[0]) if params else '')
+
+ def process_responses(self, interface):
+ responses = interface.get_responses()
+ for request, response in responses:
+ if request:
+ method, params, message_id = request
+ k = self.get_index(method, params)
+ # client requests go through self.send() with a
+ # callback, are only sent to the current interface,
+ # and are placed in the unanswered_requests dictionary
+ client_req = self.unanswered_requests.pop(message_id, None)
+ if client_req:
+ if interface != self.interface:
+ # we probably changed the current interface
+ # in the meantime; drop this.
+ return
+ callbacks = [client_req[2]]
+ else:
+ # fixme: will only work for subscriptions
+ k = self.get_index(method, params)
+ callbacks = list(self.subscriptions.get(k, []))
+
+ # Copy the request method and params to the response
+ response['method'] = method
+ response['params'] = params
+ # Only once we've received a response to an addr subscription
+ # add it to the list; avoids double-sends on reconnection
+ if method == 'blockchain.scripthash.subscribe':
+ with self.subscribed_addresses_lock:
+ self.subscribed_addresses.add(params[0])
+ else:
+ if not response: # Closed remotely / misbehaving
+ self.connection_down(interface.server)
+ break
+ # Rewrite response shape to match subscription request response
+ method = response.get('method')
+ params = response.get('params')
+ k = self.get_index(method, params)
+ if method == 'blockchain.headers.subscribe':
+ response['result'] = params[0]
+ response['params'] = []
+ elif method == 'blockchain.scripthash.subscribe':
+ response['params'] = [params[0]] # addr
+ response['result'] = params[1]
+ callbacks = list(self.subscriptions.get(k, []))
+
+ # update cache if it's a subscription
+ if method.endswith('.subscribe'):
+ with self.interface_lock:
+ self.sub_cache[k] = response
+ # Response is now in canonical form
+ self.process_response(interface, response, callbacks)
+
+ def send(self, messages, callback):
+ '''Messages is a list of (method, params) tuples'''
+ messages = list(messages)
+ with self.pending_sends_lock:
+ self.pending_sends.append((messages, callback))
+
+ @with_interface_lock
+ def process_pending_sends(self):
+ # Requests needs connectivity. If we don't have an interface,
+ # we cannot process them.
+ if not self.interface:
+ return
+
+ with self.pending_sends_lock:
+ sends = self.pending_sends
+ self.pending_sends = []
+
+ for messages, callback in sends:
+ for method, params in messages:
+ r = None
+ if method.endswith('.subscribe'):
+ k = self.get_index(method, params)
+ # add callback to list
+ l = list(self.subscriptions.get(k, []))
+ if callback not in l:
+ l.append(callback)
+ with self.callback_lock:
+ self.subscriptions[k] = l
+ # check cached response for subscriptions
+ r = self.sub_cache.get(k)
+
+ if r is not None:
+ self.print_error("cache hit", k)
+ callback(r)
+ else:
+ message_id = self.queue_request(method, params)
+ self.unanswered_requests[message_id] = method, params, callback
+
+ def unsubscribe(self, callback):
+ '''Unsubscribe a callback to free object references to enable GC.'''
+ # Note: we can't unsubscribe from the server, so if we receive
+ # subsequent notifications process_response() will emit a harmless
+ # "received unexpected notification" warning
+ with self.callback_lock:
+ for v in self.subscriptions.values():
+ if callback in v:
+ v.remove(callback)
+
+ @with_interface_lock
+ def connection_down(self, server):
+ '''A connection to server either went down, or was never made.
+ We distinguish by whether it is in self.interfaces.'''
+ self.disconnected_servers.add(server)
+ if server == self.default_server:
+ self.set_status('disconnected')
+ if server in self.interfaces:
+ self.close_interface(self.interfaces[server])
+ self.notify('interfaces')
+ with self.blockchains_lock:
+ for b in self.blockchains.values():
+ if b.catch_up == server:
+ b.catch_up = None
+
+ def new_interface(self, server, socket):
+ # todo: get tip first, then decide which checkpoint to use.
+ self.add_recent_server(server)
+ interface = Interface(server, socket)
+ interface.blockchain = None
+ interface.tip_header = None
+ interface.tip = 0
+ interface.mode = 'default'
+ interface.request = None
+ with self.interface_lock:
+ self.interfaces[server] = interface
+ # server.version should be the first message
+ params = [ELECTRUM_VERSION, PROTOCOL_VERSION]
+ self.queue_request('server.version', params, interface)
+ self.queue_request('blockchain.headers.subscribe', [True], interface)
+ if server == self.default_server:
+ self.switch_to_interface(server)
+ #self.notify('interfaces')
+
+ def maintain_sockets(self):
+ '''Socket maintenance.'''
+ # Responses to connection attempts?
+ while not self.socket_queue.empty():
+ server, socket = self.socket_queue.get()
+ if server in self.connecting:
+ self.connecting.remove(server)
+
+ if socket:
+ self.new_interface(server, socket)
+ else:
+ self.connection_down(server)
+
+ # Send pings and shut down stale interfaces
+ # must use copy of values
+ with self.interface_lock:
+ interfaces = list(self.interfaces.values())
+ for interface in interfaces:
+ if interface.has_timed_out():
+ self.connection_down(interface.server)
+ elif interface.ping_required():
+ self.queue_request('server.ping', [], interface)
+
+ now = time.time()
+ # nodes
+ with self.interface_lock:
+ if len(self.interfaces) + len(self.connecting) < self.num_server:
+ self.start_random_interface()
+ if now - self.nodes_retry_time > NODES_RETRY_INTERVAL:
+ self.print_error('network: retrying connections')
+ self.disconnected_servers = set([])
+ self.nodes_retry_time = now
+
+ # main interface
+ with self.interface_lock:
+ if not self.is_connected():
+ if self.auto_connect:
+ if not self.is_connecting():
+ self.switch_to_random_interface()
+ else:
+ if self.default_server in self.disconnected_servers:
+ if now - self.server_retry_time > SERVER_RETRY_INTERVAL:
+ self.disconnected_servers.remove(self.default_server)
+ self.server_retry_time = now
+ else:
+ self.switch_to_interface(self.default_server)
+ else:
+ if self.config.is_fee_estimates_update_required():
+ self.request_fee_estimates()
+
+ def request_chunk(self, interface, index):
+ if index in self.requested_chunks:
+ return
+ interface.print_error("requesting chunk %d" % index)
+ self.requested_chunks.add(index)
+ height = index * 2016
+ self.queue_request('blockchain.block.headers', [height, 2016],
+ interface)
+
+ def on_block_headers(self, interface, response):
+ '''Handle receiving a chunk of block headers'''
+ error = response.get('error')
+ result = response.get('result')
+ params = response.get('params')
+ blockchain = interface.blockchain
+ if result is None or params is None or error is not None:
+ interface.print_error(error or 'bad response')
+ return
+ # Ignore unsolicited chunks
+ height = params[0]
+ index = height // 2016
+ if index * 2016 != height or index not in self.requested_chunks:
+ interface.print_error("received chunk %d (unsolicited)" % index)
+ return
+ else:
+ interface.print_error("received chunk %d" % index)
+ self.requested_chunks.remove(index)
+ hexdata = result['hex']
+ connect = blockchain.connect_chunk(index, hexdata)
+ if not connect:
+ self.connection_down(interface.server)
+ return
+ # If not finished, get the next chunk
+ if index >= len(blockchain.checkpoints) and blockchain.height() < interface.tip:
+ self.request_chunk(interface, index+1)
+ else:
+ interface.mode = 'default'
+ interface.print_error('catch up done', blockchain.height())
+ blockchain.catch_up = None
+ self.notify('updated')
+
+ def on_get_header(self, interface, response):
+ '''Handle receiving a single block header'''
+ header = response.get('result')
+ if not header:
+ interface.print_error(response)
+ self.connection_down(interface.server)
+ return
+ height = header.get('block_height')
+ if interface.request != height:
+ interface.print_error("unsolicited header",interface.request, height)
+ self.connection_down(interface.server)
+ return
+ chain = blockchain.check_header(header)
+ if interface.mode == 'backward':
+ can_connect = blockchain.can_connect(header)
+ if can_connect and can_connect.catch_up is None:
+ interface.mode = 'catch_up'
+ interface.blockchain = can_connect
+ interface.blockchain.save_header(header)
+ next_height = height + 1
+ interface.blockchain.catch_up = interface.server
+ elif chain:
+ interface.print_error("binary search")
+ interface.mode = 'binary'
+ interface.blockchain = chain
+ interface.good = height
+ next_height = (interface.bad + interface.good) // 2
+ assert next_height >= self.max_checkpoint(), (interface.bad, interface.good)
+ else:
+ if height == 0:
+ self.connection_down(interface.server)
+ next_height = None
+ else:
+ interface.bad = height
+ interface.bad_header = header
+ delta = interface.tip - height
+ next_height = max(self.max_checkpoint(), interface.tip - 2 * delta)
+ if height == next_height:
+ self.connection_down(interface.server)
+ next_height = None
+
+ elif interface.mode == 'binary':
+ if chain:
+ interface.good = height
+ interface.blockchain = chain
+ else:
+ interface.bad = height
+ interface.bad_header = header
+ if interface.bad != interface.good + 1:
+ next_height = (interface.bad + interface.good) // 2
+ assert next_height >= self.max_checkpoint()
+ elif not interface.blockchain.can_connect(interface.bad_header, check_height=False):
+ self.connection_down(interface.server)
+ next_height = None
+ else:
+ branch = self.blockchains.get(interface.bad)
+ if branch is not None:
+ if branch.check_header(interface.bad_header):
+ interface.print_error('joining chain', interface.bad)
+ next_height = None
+ elif branch.parent().check_header(header):
+ interface.print_error('reorg', interface.bad, interface.tip)
+ interface.blockchain = branch.parent()
+ next_height = None
+ else:
+ interface.print_error('checkpoint conflicts with existing fork', branch.path())
+ branch.write(b'', 0)
+ branch.save_header(interface.bad_header)
+ interface.mode = 'catch_up'
+ interface.blockchain = branch
+ next_height = interface.bad + 1
+ interface.blockchain.catch_up = interface.server
+ else:
+ bh = interface.blockchain.height()
+ next_height = None
+ if bh > interface.good:
+ if not interface.blockchain.check_header(interface.bad_header):
+ b = interface.blockchain.fork(interface.bad_header)
+ with self.blockchains_lock:
+ self.blockchains[interface.bad] = b
+ interface.blockchain = b
+ interface.print_error("new chain", b.checkpoint)
+ interface.mode = 'catch_up'
+ next_height = interface.bad + 1
+ interface.blockchain.catch_up = interface.server
+ else:
+ assert bh == interface.good
+ if interface.blockchain.catch_up is None and bh < interface.tip:
+ interface.print_error("catching up from %d"% (bh + 1))
+ interface.mode = 'catch_up'
+ next_height = bh + 1
+ interface.blockchain.catch_up = interface.server
+
+ self.notify('updated')
+
+ elif interface.mode == 'catch_up':
+ can_connect = interface.blockchain.can_connect(header)
+ if can_connect:
+ interface.blockchain.save_header(header)
+ next_height = height + 1 if height < interface.tip else None
+ else:
+ # go back
+ interface.print_error("cannot connect", height)
+ interface.mode = 'backward'
+ interface.bad = height
+ interface.bad_header = header
+ next_height = height - 1
+
+ if next_height is None:
+ # exit catch_up state
+ interface.print_error('catch up done', interface.blockchain.height())
+ interface.blockchain.catch_up = None
+ self.switch_lagging_interface()
+ self.notify('updated')
+
+ else:
+ raise Exception(interface.mode)
+ # If not finished, get the next header
+ if next_height is not None:
+ if interface.mode == 'catch_up' and interface.tip > next_height + 50:
+ self.request_chunk(interface, next_height // 2016)
+ else:
+ self.request_header(interface, next_height)
+ else:
+ interface.mode = 'default'
+ interface.request = None
+ self.notify('updated')
+
+ # refresh network dialog
+ self.notify('interfaces')
+
+ def maintain_requests(self):
+ with self.interface_lock:
+ interfaces = list(self.interfaces.values())
+ for interface in interfaces:
+ if interface.request and time.time() - interface.request_time > 20:
+ interface.print_error("blockchain request timed out")
+ self.connection_down(interface.server)
+ continue
+
+ def wait_on_sockets(self):
+ # Python docs say Windows doesn't like empty selects.
+ # Sleep to prevent busy looping
+ if not self.interfaces:
+ time.sleep(0.1)
+ return
+ with self.interface_lock:
+ interfaces = list(self.interfaces.values())
+ rin = [i for i in interfaces]
+ win = [i for i in interfaces if i.num_requests()]
+ try:
+ rout, wout, xout = select.select(rin, win, [], 0.1)
+ except socket.error as e:
+ if e.errno == errno.EINTR:
+ return
+ raise
+ assert not xout
+ for interface in wout:
+ interface.send_requests()
+ for interface in rout:
+ self.process_responses(interface)
+
+ def init_headers_file(self):
+ b = self.blockchains[0]
+ filename = b.path()
+ length = 80 * len(constants.net.CHECKPOINTS) * 2016
+ if not os.path.exists(filename) or os.path.getsize(filename) < length:
+ with open(filename, 'wb') as f:
+ if length>0:
+ f.seek(length-1)
+ f.write(b'\x00')
+ with b.lock:
+ b.update_size()
+
+ def run(self):
+ self.init_headers_file()
+ while self.is_running():
+ self.maintain_sockets()
+ self.wait_on_sockets()
+ self.maintain_requests()
+ self.run_jobs() # Synchronizer and Verifier
+ self.process_pending_sends()
+ self.stop_network()
+ self.on_stop()
+
+ def on_notify_header(self, interface, header_dict):
+ try:
+ header_hex, height = header_dict['hex'], header_dict['height']
+ except KeyError:
+ # no point in keeping this connection without headers sub
+ self.connection_down(interface.server)
+ return
+ header = blockchain.deserialize_header(util.bfh(header_hex), height)
+ if height < self.max_checkpoint():
+ self.connection_down(interface.server)
+ return
+ interface.tip_header = header
+ interface.tip = height
+ if interface.mode != 'default':
+ return
+ b = blockchain.check_header(header)
+ if b:
+ interface.blockchain = b
+ self.switch_lagging_interface()
+ self.notify('updated')
+ self.notify('interfaces')
+ return
+ b = blockchain.can_connect(header)
+ if b:
+ interface.blockchain = b
+ b.save_header(header)
+ self.switch_lagging_interface()
+ self.notify('updated')
+ self.notify('interfaces')
+ return
+ with self.blockchains_lock:
+ tip = max([x.height() for x in self.blockchains.values()])
+ if tip >=0:
+ interface.mode = 'backward'
+ interface.bad = height
+ interface.bad_header = header
+ self.request_header(interface, min(tip +1, height - 1))
+ else:
+ chain = self.blockchains[0]
+ if chain.catch_up is None:
+ chain.catch_up = interface
+ interface.mode = 'catch_up'
+ interface.blockchain = chain
+ with self.blockchains_lock:
+ self.print_error("switching to catchup mode", tip, self.blockchains)
+ self.request_header(interface, 0)
+ else:
+ self.print_error("chain already catching up with", chain.catch_up.server)
+
+ @with_interface_lock
+ def blockchain(self):
+ if self.interface and self.interface.blockchain is not None:
+ self.blockchain_index = self.interface.blockchain.checkpoint
+ return self.blockchains[self.blockchain_index]
+
+ @with_interface_lock
+ def get_blockchains(self):
+ out = {}
+ with self.blockchains_lock:
+ blockchain_items = list(self.blockchains.items())
+ for k, b in blockchain_items:
+ r = list(filter(lambda i: i.blockchain==b, list(self.interfaces.values())))
+ if r:
+ out[k] = r
+ return out
+
+ def follow_chain(self, index):
+ blockchain = self.blockchains.get(index)
+ if blockchain:
+ self.blockchain_index = index
+ self.config.set_key('blockchain_index', index)
+ with self.interface_lock:
+ interfaces = list(self.interfaces.values())
+ for i in interfaces:
+ if i.blockchain == blockchain:
+ self.switch_to_interface(i.server)
+ break
+ else:
+ raise Exception('blockchain not found', index)
+
+ with self.interface_lock:
+ if self.interface:
+ server = self.interface.server
+ host, port, protocol, proxy, auto_connect = self.get_parameters()
+ host, port, protocol = server.split(':')
+ self.set_parameters(host, port, protocol, proxy, auto_connect)
+
+ def get_local_height(self):
+ return self.blockchain().height()
+
+ @staticmethod
+ def __wait_for(it):
+ """Wait for the result of calling lambda `it`."""
+ q = queue.Queue()
+ it(q.put)
+ try:
+ result = q.get(block=True, timeout=30)
+ except queue.Empty:
+ raise util.TimeoutException(_('Server did not answer'))
+
+ if result.get('error'):
+ raise Exception(result.get('error'))
+
+ return result.get('result')
+
+ @staticmethod
+ def __with_default_synchronous_callback(invocation, callback):
+ """ Use this method if you want to make the network request
+ synchronous. """
+ if not callback:
+ return Network.__wait_for(invocation)
+
+ invocation(callback)
+
+ def request_header(self, interface, height):
+ self.queue_request('blockchain.block.get_header', [height], interface)
+ interface.request = height
+ interface.req_time = time.time()
+
+ def map_scripthash_to_address(self, callback):
+ def cb2(x):
+ x2 = x.copy()
+ p = x2.pop('params')
+ addr = self.h2addr[p[0]]
+ x2['params'] = [addr]
+ callback(x2)
+ return cb2
+
+ def subscribe_to_addresses(self, addresses, callback):
+ hash2address = {
+ bitcoin.address_to_scripthash(address): address
+ for address in addresses}
+ self.h2addr.update(hash2address)
+ msgs = [
+ ('blockchain.scripthash.subscribe', [x])
+ for x in hash2address.keys()]
+ self.send(msgs, self.map_scripthash_to_address(callback))
+
+ def request_address_history(self, address, callback):
+ h = bitcoin.address_to_scripthash(address)
+ self.h2addr.update({h: address})
+ self.send([('blockchain.scripthash.get_history', [h])], self.map_scripthash_to_address(callback))
+
+ # NOTE this method handles exceptions and a special edge case, counter to
+ # what the other ElectrumX methods do. This is unexpected.
+ def broadcast_transaction(self, transaction, callback=None):
+ command = 'blockchain.transaction.broadcast'
+ invocation = lambda c: self.send([(command, [str(transaction)])], c)
+
+ if callback:
+ invocation(callback)
+ return
+
+ try:
+ out = Network.__wait_for(invocation)
+ except BaseException as e:
+ return False, "error: " + str(e)
+
+ if out != transaction.txid():
+ return False, "error: " + out
+
+ return True, out
+
+ def get_history_for_scripthash(self, hash, callback=None):
+ command = 'blockchain.scripthash.get_history'
+ invocation = lambda c: self.send([(command, [hash])], c)
+
+ return Network.__with_default_synchronous_callback(invocation, callback)
+
+ def subscribe_to_headers(self, callback=None):
+ command = 'blockchain.headers.subscribe'
+ invocation = lambda c: self.send([(command, [True])], c)
+
+ return Network.__with_default_synchronous_callback(invocation, callback)
+
+ def subscribe_to_address(self, address, callback=None):
+ command = 'blockchain.address.subscribe'
+ invocation = lambda c: self.send([(command, [address])], c)
+
+ return Network.__with_default_synchronous_callback(invocation, callback)
+
+ def get_merkle_for_transaction(self, tx_hash, tx_height, callback=None):
+ command = 'blockchain.transaction.get_merkle'
+ invocation = lambda c: self.send([(command, [tx_hash, tx_height])], c)
+
+ return Network.__with_default_synchronous_callback(invocation, callback)
+
+ def subscribe_to_scripthash(self, scripthash, callback=None):
+ command = 'blockchain.scripthash.subscribe'
+ invocation = lambda c: self.send([(command, [scripthash])], c)
+
+ return Network.__with_default_synchronous_callback(invocation, callback)
+
+ def get_transaction(self, transaction_hash, callback=None):
+ command = 'blockchain.transaction.get'
+ invocation = lambda c: self.send([(command, [transaction_hash])], c)
+
+ return Network.__with_default_synchronous_callback(invocation, callback)
+
+ def get_transactions(self, transaction_hashes, callback=None):
+ command = 'blockchain.transaction.get'
+ messages = [(command, [tx_hash]) for tx_hash in transaction_hashes]
+ invocation = lambda c: self.send(messages, c)
+
+ return Network.__with_default_synchronous_callback(invocation, callback)
+
+ def listunspent_for_scripthash(self, scripthash, callback=None):
+ command = 'blockchain.scripthash.listunspent'
+ invocation = lambda c: self.send([(command, [scripthash])], c)
+
+ return Network.__with_default_synchronous_callback(invocation, callback)
+
+ def get_balance_for_scripthash(self, scripthash, callback=None):
+ command = 'blockchain.scripthash.get_balance'
+ invocation = lambda c: self.send([(command, [scripthash])], c)
+
+ return Network.__with_default_synchronous_callback(invocation, callback)
+
+ def export_checkpoints(self, path):
+ # run manually from the console to generate checkpoints
+ cp = self.blockchain().get_checkpoints()
+ with open(path, 'w', encoding='utf-8') as f:
+ f.write(json.dumps(cp, indent=4))
+
+ @classmethod
+ def max_checkpoint(cls):
+ return max(0, len(constants.net.CHECKPOINTS) * 2016 - 1)
DIR diff --git a/electrum/old_mnemonic.py b/electrum/old_mnemonic.py
t@@ -0,0 +1,1697 @@
+#!/usr/bin/env python
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2011 thomasv@gitorious
+#
+# 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.
+
+
+# list of words from http://en.wiktionary.org/wiki/Wiktionary:Frequency_lists/Contemporary_poetry
+
+words = [
+"like",
+"just",
+"love",
+"know",
+"never",
+"want",
+"time",
+"out",
+"there",
+"make",
+"look",
+"eye",
+"down",
+"only",
+"think",
+"heart",
+"back",
+"then",
+"into",
+"about",
+"more",
+"away",
+"still",
+"them",
+"take",
+"thing",
+"even",
+"through",
+"long",
+"always",
+"world",
+"too",
+"friend",
+"tell",
+"try",
+"hand",
+"thought",
+"over",
+"here",
+"other",
+"need",
+"smile",
+"again",
+"much",
+"cry",
+"been",
+"night",
+"ever",
+"little",
+"said",
+"end",
+"some",
+"those",
+"around",
+"mind",
+"people",
+"girl",
+"leave",
+"dream",
+"left",
+"turn",
+"myself",
+"give",
+"nothing",
+"really",
+"off",
+"before",
+"something",
+"find",
+"walk",
+"wish",
+"good",
+"once",
+"place",
+"ask",
+"stop",
+"keep",
+"watch",
+"seem",
+"everything",
+"wait",
+"got",
+"yet",
+"made",
+"remember",
+"start",
+"alone",
+"run",
+"hope",
+"maybe",
+"believe",
+"body",
+"hate",
+"after",
+"close",
+"talk",
+"stand",
+"own",
+"each",
+"hurt",
+"help",
+"home",
+"god",
+"soul",
+"new",
+"many",
+"two",
+"inside",
+"should",
+"true",
+"first",
+"fear",
+"mean",
+"better",
+"play",
+"another",
+"gone",
+"change",
+"use",
+"wonder",
+"someone",
+"hair",
+"cold",
+"open",
+"best",
+"any",
+"behind",
+"happen",
+"water",
+"dark",
+"laugh",
+"stay",
+"forever",
+"name",
+"work",
+"show",
+"sky",
+"break",
+"came",
+"deep",
+"door",
+"put",
+"black",
+"together",
+"upon",
+"happy",
+"such",
+"great",
+"white",
+"matter",
+"fill",
+"past",
+"please",
+"burn",
+"cause",
+"enough",
+"touch",
+"moment",
+"soon",
+"voice",
+"scream",
+"anything",
+"stare",
+"sound",
+"red",
+"everyone",
+"hide",
+"kiss",
+"truth",
+"death",
+"beautiful",
+"mine",
+"blood",
+"broken",
+"very",
+"pass",
+"next",
+"forget",
+"tree",
+"wrong",
+"air",
+"mother",
+"understand",
+"lip",
+"hit",
+"wall",
+"memory",
+"sleep",
+"free",
+"high",
+"realize",
+"school",
+"might",
+"skin",
+"sweet",
+"perfect",
+"blue",
+"kill",
+"breath",
+"dance",
+"against",
+"fly",
+"between",
+"grow",
+"strong",
+"under",
+"listen",
+"bring",
+"sometimes",
+"speak",
+"pull",
+"person",
+"become",
+"family",
+"begin",
+"ground",
+"real",
+"small",
+"father",
+"sure",
+"feet",
+"rest",
+"young",
+"finally",
+"land",
+"across",
+"today",
+"different",
+"guy",
+"line",
+"fire",
+"reason",
+"reach",
+"second",
+"slowly",
+"write",
+"eat",
+"smell",
+"mouth",
+"step",
+"learn",
+"three",
+"floor",
+"promise",
+"breathe",
+"darkness",
+"push",
+"earth",
+"guess",
+"save",
+"song",
+"above",
+"along",
+"both",
+"color",
+"house",
+"almost",
+"sorry",
+"anymore",
+"brother",
+"okay",
+"dear",
+"game",
+"fade",
+"already",
+"apart",
+"warm",
+"beauty",
+"heard",
+"notice",
+"question",
+"shine",
+"began",
+"piece",
+"whole",
+"shadow",
+"secret",
+"street",
+"within",
+"finger",
+"point",
+"morning",
+"whisper",
+"child",
+"moon",
+"green",
+"story",
+"glass",
+"kid",
+"silence",
+"since",
+"soft",
+"yourself",
+"empty",
+"shall",
+"angel",
+"answer",
+"baby",
+"bright",
+"dad",
+"path",
+"worry",
+"hour",
+"drop",
+"follow",
+"power",
+"war",
+"half",
+"flow",
+"heaven",
+"act",
+"chance",
+"fact",
+"least",
+"tired",
+"children",
+"near",
+"quite",
+"afraid",
+"rise",
+"sea",
+"taste",
+"window",
+"cover",
+"nice",
+"trust",
+"lot",
+"sad",
+"cool",
+"force",
+"peace",
+"return",
+"blind",
+"easy",
+"ready",
+"roll",
+"rose",
+"drive",
+"held",
+"music",
+"beneath",
+"hang",
+"mom",
+"paint",
+"emotion",
+"quiet",
+"clear",
+"cloud",
+"few",
+"pretty",
+"bird",
+"outside",
+"paper",
+"picture",
+"front",
+"rock",
+"simple",
+"anyone",
+"meant",
+"reality",
+"road",
+"sense",
+"waste",
+"bit",
+"leaf",
+"thank",
+"happiness",
+"meet",
+"men",
+"smoke",
+"truly",
+"decide",
+"self",
+"age",
+"book",
+"form",
+"alive",
+"carry",
+"escape",
+"damn",
+"instead",
+"able",
+"ice",
+"minute",
+"throw",
+"catch",
+"leg",
+"ring",
+"course",
+"goodbye",
+"lead",
+"poem",
+"sick",
+"corner",
+"desire",
+"known",
+"problem",
+"remind",
+"shoulder",
+"suppose",
+"toward",
+"wave",
+"drink",
+"jump",
+"woman",
+"pretend",
+"sister",
+"week",
+"human",
+"joy",
+"crack",
+"grey",
+"pray",
+"surprise",
+"dry",
+"knee",
+"less",
+"search",
+"bleed",
+"caught",
+"clean",
+"embrace",
+"future",
+"king",
+"son",
+"sorrow",
+"chest",
+"hug",
+"remain",
+"sat",
+"worth",
+"blow",
+"daddy",
+"final",
+"parent",
+"tight",
+"also",
+"create",
+"lonely",
+"safe",
+"cross",
+"dress",
+"evil",
+"silent",
+"bone",
+"fate",
+"perhaps",
+"anger",
+"class",
+"scar",
+"snow",
+"tiny",
+"tonight",
+"continue",
+"control",
+"dog",
+"edge",
+"mirror",
+"month",
+"suddenly",
+"comfort",
+"given",
+"loud",
+"quickly",
+"gaze",
+"plan",
+"rush",
+"stone",
+"town",
+"battle",
+"ignore",
+"spirit",
+"stood",
+"stupid",
+"yours",
+"brown",
+"build",
+"dust",
+"hey",
+"kept",
+"pay",
+"phone",
+"twist",
+"although",
+"ball",
+"beyond",
+"hidden",
+"nose",
+"taken",
+"fail",
+"float",
+"pure",
+"somehow",
+"wash",
+"wrap",
+"angry",
+"cheek",
+"creature",
+"forgotten",
+"heat",
+"rip",
+"single",
+"space",
+"special",
+"weak",
+"whatever",
+"yell",
+"anyway",
+"blame",
+"job",
+"choose",
+"country",
+"curse",
+"drift",
+"echo",
+"figure",
+"grew",
+"laughter",
+"neck",
+"suffer",
+"worse",
+"yeah",
+"disappear",
+"foot",
+"forward",
+"knife",
+"mess",
+"somewhere",
+"stomach",
+"storm",
+"beg",
+"idea",
+"lift",
+"offer",
+"breeze",
+"field",
+"five",
+"often",
+"simply",
+"stuck",
+"win",
+"allow",
+"confuse",
+"enjoy",
+"except",
+"flower",
+"seek",
+"strength",
+"calm",
+"grin",
+"gun",
+"heavy",
+"hill",
+"large",
+"ocean",
+"shoe",
+"sigh",
+"straight",
+"summer",
+"tongue",
+"accept",
+"crazy",
+"everyday",
+"exist",
+"grass",
+"mistake",
+"sent",
+"shut",
+"surround",
+"table",
+"ache",
+"brain",
+"destroy",
+"heal",
+"nature",
+"shout",
+"sign",
+"stain",
+"choice",
+"doubt",
+"glance",
+"glow",
+"mountain",
+"queen",
+"stranger",
+"throat",
+"tomorrow",
+"city",
+"either",
+"fish",
+"flame",
+"rather",
+"shape",
+"spin",
+"spread",
+"ash",
+"distance",
+"finish",
+"image",
+"imagine",
+"important",
+"nobody",
+"shatter",
+"warmth",
+"became",
+"feed",
+"flesh",
+"funny",
+"lust",
+"shirt",
+"trouble",
+"yellow",
+"attention",
+"bare",
+"bite",
+"money",
+"protect",
+"amaze",
+"appear",
+"born",
+"choke",
+"completely",
+"daughter",
+"fresh",
+"friendship",
+"gentle",
+"probably",
+"six",
+"deserve",
+"expect",
+"grab",
+"middle",
+"nightmare",
+"river",
+"thousand",
+"weight",
+"worst",
+"wound",
+"barely",
+"bottle",
+"cream",
+"regret",
+"relationship",
+"stick",
+"test",
+"crush",
+"endless",
+"fault",
+"itself",
+"rule",
+"spill",
+"art",
+"circle",
+"join",
+"kick",
+"mask",
+"master",
+"passion",
+"quick",
+"raise",
+"smooth",
+"unless",
+"wander",
+"actually",
+"broke",
+"chair",
+"deal",
+"favorite",
+"gift",
+"note",
+"number",
+"sweat",
+"box",
+"chill",
+"clothes",
+"lady",
+"mark",
+"park",
+"poor",
+"sadness",
+"tie",
+"animal",
+"belong",
+"brush",
+"consume",
+"dawn",
+"forest",
+"innocent",
+"pen",
+"pride",
+"stream",
+"thick",
+"clay",
+"complete",
+"count",
+"draw",
+"faith",
+"press",
+"silver",
+"struggle",
+"surface",
+"taught",
+"teach",
+"wet",
+"bless",
+"chase",
+"climb",
+"enter",
+"letter",
+"melt",
+"metal",
+"movie",
+"stretch",
+"swing",
+"vision",
+"wife",
+"beside",
+"crash",
+"forgot",
+"guide",
+"haunt",
+"joke",
+"knock",
+"plant",
+"pour",
+"prove",
+"reveal",
+"steal",
+"stuff",
+"trip",
+"wood",
+"wrist",
+"bother",
+"bottom",
+"crawl",
+"crowd",
+"fix",
+"forgive",
+"frown",
+"grace",
+"loose",
+"lucky",
+"party",
+"release",
+"surely",
+"survive",
+"teacher",
+"gently",
+"grip",
+"speed",
+"suicide",
+"travel",
+"treat",
+"vein",
+"written",
+"cage",
+"chain",
+"conversation",
+"date",
+"enemy",
+"however",
+"interest",
+"million",
+"page",
+"pink",
+"proud",
+"sway",
+"themselves",
+"winter",
+"church",
+"cruel",
+"cup",
+"demon",
+"experience",
+"freedom",
+"pair",
+"pop",
+"purpose",
+"respect",
+"shoot",
+"softly",
+"state",
+"strange",
+"bar",
+"birth",
+"curl",
+"dirt",
+"excuse",
+"lord",
+"lovely",
+"monster",
+"order",
+"pack",
+"pants",
+"pool",
+"scene",
+"seven",
+"shame",
+"slide",
+"ugly",
+"among",
+"blade",
+"blonde",
+"closet",
+"creek",
+"deny",
+"drug",
+"eternity",
+"gain",
+"grade",
+"handle",
+"key",
+"linger",
+"pale",
+"prepare",
+"swallow",
+"swim",
+"tremble",
+"wheel",
+"won",
+"cast",
+"cigarette",
+"claim",
+"college",
+"direction",
+"dirty",
+"gather",
+"ghost",
+"hundred",
+"loss",
+"lung",
+"orange",
+"present",
+"swear",
+"swirl",
+"twice",
+"wild",
+"bitter",
+"blanket",
+"doctor",
+"everywhere",
+"flash",
+"grown",
+"knowledge",
+"numb",
+"pressure",
+"radio",
+"repeat",
+"ruin",
+"spend",
+"unknown",
+"buy",
+"clock",
+"devil",
+"early",
+"false",
+"fantasy",
+"pound",
+"precious",
+"refuse",
+"sheet",
+"teeth",
+"welcome",
+"add",
+"ahead",
+"block",
+"bury",
+"caress",
+"content",
+"depth",
+"despite",
+"distant",
+"marry",
+"purple",
+"threw",
+"whenever",
+"bomb",
+"dull",
+"easily",
+"grasp",
+"hospital",
+"innocence",
+"normal",
+"receive",
+"reply",
+"rhyme",
+"shade",
+"someday",
+"sword",
+"toe",
+"visit",
+"asleep",
+"bought",
+"center",
+"consider",
+"flat",
+"hero",
+"history",
+"ink",
+"insane",
+"muscle",
+"mystery",
+"pocket",
+"reflection",
+"shove",
+"silently",
+"smart",
+"soldier",
+"spot",
+"stress",
+"train",
+"type",
+"view",
+"whether",
+"bus",
+"energy",
+"explain",
+"holy",
+"hunger",
+"inch",
+"magic",
+"mix",
+"noise",
+"nowhere",
+"prayer",
+"presence",
+"shock",
+"snap",
+"spider",
+"study",
+"thunder",
+"trail",
+"admit",
+"agree",
+"bag",
+"bang",
+"bound",
+"butterfly",
+"cute",
+"exactly",
+"explode",
+"familiar",
+"fold",
+"further",
+"pierce",
+"reflect",
+"scent",
+"selfish",
+"sharp",
+"sink",
+"spring",
+"stumble",
+"universe",
+"weep",
+"women",
+"wonderful",
+"action",
+"ancient",
+"attempt",
+"avoid",
+"birthday",
+"branch",
+"chocolate",
+"core",
+"depress",
+"drunk",
+"especially",
+"focus",
+"fruit",
+"honest",
+"match",
+"palm",
+"perfectly",
+"pillow",
+"pity",
+"poison",
+"roar",
+"shift",
+"slightly",
+"thump",
+"truck",
+"tune",
+"twenty",
+"unable",
+"wipe",
+"wrote",
+"coat",
+"constant",
+"dinner",
+"drove",
+"egg",
+"eternal",
+"flight",
+"flood",
+"frame",
+"freak",
+"gasp",
+"glad",
+"hollow",
+"motion",
+"peer",
+"plastic",
+"root",
+"screen",
+"season",
+"sting",
+"strike",
+"team",
+"unlike",
+"victim",
+"volume",
+"warn",
+"weird",
+"attack",
+"await",
+"awake",
+"built",
+"charm",
+"crave",
+"despair",
+"fought",
+"grant",
+"grief",
+"horse",
+"limit",
+"message",
+"ripple",
+"sanity",
+"scatter",
+"serve",
+"split",
+"string",
+"trick",
+"annoy",
+"blur",
+"boat",
+"brave",
+"clearly",
+"cling",
+"connect",
+"fist",
+"forth",
+"imagination",
+"iron",
+"jock",
+"judge",
+"lesson",
+"milk",
+"misery",
+"nail",
+"naked",
+"ourselves",
+"poet",
+"possible",
+"princess",
+"sail",
+"size",
+"snake",
+"society",
+"stroke",
+"torture",
+"toss",
+"trace",
+"wise",
+"bloom",
+"bullet",
+"cell",
+"check",
+"cost",
+"darling",
+"during",
+"footstep",
+"fragile",
+"hallway",
+"hardly",
+"horizon",
+"invisible",
+"journey",
+"midnight",
+"mud",
+"nod",
+"pause",
+"relax",
+"shiver",
+"sudden",
+"value",
+"youth",
+"abuse",
+"admire",
+"blink",
+"breast",
+"bruise",
+"constantly",
+"couple",
+"creep",
+"curve",
+"difference",
+"dumb",
+"emptiness",
+"gotta",
+"honor",
+"plain",
+"planet",
+"recall",
+"rub",
+"ship",
+"slam",
+"soar",
+"somebody",
+"tightly",
+"weather",
+"adore",
+"approach",
+"bond",
+"bread",
+"burst",
+"candle",
+"coffee",
+"cousin",
+"crime",
+"desert",
+"flutter",
+"frozen",
+"grand",
+"heel",
+"hello",
+"language",
+"level",
+"movement",
+"pleasure",
+"powerful",
+"random",
+"rhythm",
+"settle",
+"silly",
+"slap",
+"sort",
+"spoken",
+"steel",
+"threaten",
+"tumble",
+"upset",
+"aside",
+"awkward",
+"bee",
+"blank",
+"board",
+"button",
+"card",
+"carefully",
+"complain",
+"crap",
+"deeply",
+"discover",
+"drag",
+"dread",
+"effort",
+"entire",
+"fairy",
+"giant",
+"gotten",
+"greet",
+"illusion",
+"jeans",
+"leap",
+"liquid",
+"march",
+"mend",
+"nervous",
+"nine",
+"replace",
+"rope",
+"spine",
+"stole",
+"terror",
+"accident",
+"apple",
+"balance",
+"boom",
+"childhood",
+"collect",
+"demand",
+"depression",
+"eventually",
+"faint",
+"glare",
+"goal",
+"group",
+"honey",
+"kitchen",
+"laid",
+"limb",
+"machine",
+"mere",
+"mold",
+"murder",
+"nerve",
+"painful",
+"poetry",
+"prince",
+"rabbit",
+"shelter",
+"shore",
+"shower",
+"soothe",
+"stair",
+"steady",
+"sunlight",
+"tangle",
+"tease",
+"treasure",
+"uncle",
+"begun",
+"bliss",
+"canvas",
+"cheer",
+"claw",
+"clutch",
+"commit",
+"crimson",
+"crystal",
+"delight",
+"doll",
+"existence",
+"express",
+"fog",
+"football",
+"gay",
+"goose",
+"guard",
+"hatred",
+"illuminate",
+"mass",
+"math",
+"mourn",
+"rich",
+"rough",
+"skip",
+"stir",
+"student",
+"style",
+"support",
+"thorn",
+"tough",
+"yard",
+"yearn",
+"yesterday",
+"advice",
+"appreciate",
+"autumn",
+"bank",
+"beam",
+"bowl",
+"capture",
+"carve",
+"collapse",
+"confusion",
+"creation",
+"dove",
+"feather",
+"girlfriend",
+"glory",
+"government",
+"harsh",
+"hop",
+"inner",
+"loser",
+"moonlight",
+"neighbor",
+"neither",
+"peach",
+"pig",
+"praise",
+"screw",
+"shield",
+"shimmer",
+"sneak",
+"stab",
+"subject",
+"throughout",
+"thrown",
+"tower",
+"twirl",
+"wow",
+"army",
+"arrive",
+"bathroom",
+"bump",
+"cease",
+"cookie",
+"couch",
+"courage",
+"dim",
+"guilt",
+"howl",
+"hum",
+"husband",
+"insult",
+"led",
+"lunch",
+"mock",
+"mostly",
+"natural",
+"nearly",
+"needle",
+"nerd",
+"peaceful",
+"perfection",
+"pile",
+"price",
+"remove",
+"roam",
+"sanctuary",
+"serious",
+"shiny",
+"shook",
+"sob",
+"stolen",
+"tap",
+"vain",
+"void",
+"warrior",
+"wrinkle",
+"affection",
+"apologize",
+"blossom",
+"bounce",
+"bridge",
+"cheap",
+"crumble",
+"decision",
+"descend",
+"desperately",
+"dig",
+"dot",
+"flip",
+"frighten",
+"heartbeat",
+"huge",
+"lazy",
+"lick",
+"odd",
+"opinion",
+"process",
+"puzzle",
+"quietly",
+"retreat",
+"score",
+"sentence",
+"separate",
+"situation",
+"skill",
+"soak",
+"square",
+"stray",
+"taint",
+"task",
+"tide",
+"underneath",
+"veil",
+"whistle",
+"anywhere",
+"bedroom",
+"bid",
+"bloody",
+"burden",
+"careful",
+"compare",
+"concern",
+"curtain",
+"decay",
+"defeat",
+"describe",
+"double",
+"dreamer",
+"driver",
+"dwell",
+"evening",
+"flare",
+"flicker",
+"grandma",
+"guitar",
+"harm",
+"horrible",
+"hungry",
+"indeed",
+"lace",
+"melody",
+"monkey",
+"nation",
+"object",
+"obviously",
+"rainbow",
+"salt",
+"scratch",
+"shown",
+"shy",
+"stage",
+"stun",
+"third",
+"tickle",
+"useless",
+"weakness",
+"worship",
+"worthless",
+"afternoon",
+"beard",
+"boyfriend",
+"bubble",
+"busy",
+"certain",
+"chin",
+"concrete",
+"desk",
+"diamond",
+"doom",
+"drawn",
+"due",
+"felicity",
+"freeze",
+"frost",
+"garden",
+"glide",
+"harmony",
+"hopefully",
+"hunt",
+"jealous",
+"lightning",
+"mama",
+"mercy",
+"peel",
+"physical",
+"position",
+"pulse",
+"punch",
+"quit",
+"rant",
+"respond",
+"salty",
+"sane",
+"satisfy",
+"savior",
+"sheep",
+"slept",
+"social",
+"sport",
+"tuck",
+"utter",
+"valley",
+"wolf",
+"aim",
+"alas",
+"alter",
+"arrow",
+"awaken",
+"beaten",
+"belief",
+"brand",
+"ceiling",
+"cheese",
+"clue",
+"confidence",
+"connection",
+"daily",
+"disguise",
+"eager",
+"erase",
+"essence",
+"everytime",
+"expression",
+"fan",
+"flag",
+"flirt",
+"foul",
+"fur",
+"giggle",
+"glorious",
+"ignorance",
+"law",
+"lifeless",
+"measure",
+"mighty",
+"muse",
+"north",
+"opposite",
+"paradise",
+"patience",
+"patient",
+"pencil",
+"petal",
+"plate",
+"ponder",
+"possibly",
+"practice",
+"slice",
+"spell",
+"stock",
+"strife",
+"strip",
+"suffocate",
+"suit",
+"tender",
+"tool",
+"trade",
+"velvet",
+"verse",
+"waist",
+"witch",
+"aunt",
+"bench",
+"bold",
+"cap",
+"certainly",
+"click",
+"companion",
+"creator",
+"dart",
+"delicate",
+"determine",
+"dish",
+"dragon",
+"drama",
+"drum",
+"dude",
+"everybody",
+"feast",
+"forehead",
+"former",
+"fright",
+"fully",
+"gas",
+"hook",
+"hurl",
+"invite",
+"juice",
+"manage",
+"moral",
+"possess",
+"raw",
+"rebel",
+"royal",
+"scale",
+"scary",
+"several",
+"slight",
+"stubborn",
+"swell",
+"talent",
+"tea",
+"terrible",
+"thread",
+"torment",
+"trickle",
+"usually",
+"vast",
+"violence",
+"weave",
+"acid",
+"agony",
+"ashamed",
+"awe",
+"belly",
+"blend",
+"blush",
+"character",
+"cheat",
+"common",
+"company",
+"coward",
+"creak",
+"danger",
+"deadly",
+"defense",
+"define",
+"depend",
+"desperate",
+"destination",
+"dew",
+"duck",
+"dusty",
+"embarrass",
+"engine",
+"example",
+"explore",
+"foe",
+"freely",
+"frustrate",
+"generation",
+"glove",
+"guilty",
+"health",
+"hurry",
+"idiot",
+"impossible",
+"inhale",
+"jaw",
+"kingdom",
+"mention",
+"mist",
+"moan",
+"mumble",
+"mutter",
+"observe",
+"ode",
+"pathetic",
+"pattern",
+"pie",
+"prefer",
+"puff",
+"rape",
+"rare",
+"revenge",
+"rude",
+"scrape",
+"spiral",
+"squeeze",
+"strain",
+"sunset",
+"suspend",
+"sympathy",
+"thigh",
+"throne",
+"total",
+"unseen",
+"weapon",
+"weary"
+]
+
+
+
+n = 1626
+
+# Note about US patent no 5892470: Here each word does not represent a given digit.
+# Instead, the digit represented by a word is variable, it depends on the previous word.
+
+def mn_encode( message ):
+ assert len(message) % 8 == 0
+ out = []
+ for i in range(len(message)//8):
+ word = message[8*i:8*i+8]
+ x = int(word, 16)
+ w1 = (x%n)
+ w2 = ((x//n) + w1)%n
+ w3 = ((x//n//n) + w2)%n
+ out += [ words[w1], words[w2], words[w3] ]
+ return out
+
+
+def mn_decode( wlist ):
+ out = ''
+ for i in range(len(wlist)//3):
+ word1, word2, word3 = wlist[3*i:3*i+3]
+ w1 = words.index(word1)
+ w2 = (words.index(word2))%n
+ w3 = (words.index(word3))%n
+ x = w1 +n*((w2-w1)%n) +n*n*((w3-w2)%n)
+ out += '%08x'%x
+ return out
+
+
+if __name__ == '__main__':
+ import sys
+ if len(sys.argv) == 1:
+ print('I need arguments: a hex string to encode, or a list of words to decode')
+ elif len(sys.argv) == 2:
+ print(' '.join(mn_encode(sys.argv[1])))
+ else:
+ print(mn_decode(sys.argv[1:]))
DIR diff --git a/electrum/paymentrequest.proto b/electrum/paymentrequest.proto
t@@ -0,0 +1,47 @@
+//
+// Simple Bitcoin Payment Protocol messages
+//
+// Use fields 1000+ for extensions;
+// to avoid conflicts, register extensions via pull-req at
+// https://github.com/bitcoin/bips/bip-0070/extensions.mediawiki
+//
+
+syntax = "proto2";
+package payments;
+option java_package = "org.bitcoin.protocols.payments";
+option java_outer_classname = "Protos";
+
+// Generalized form of "send payment to this/these bitcoin addresses"
+message Output {
+ optional uint64 amount = 1 [default = 0]; // amount is integer-number-of-satoshis
+ required bytes script = 2; // usually one of the standard Script forms
+}
+message PaymentDetails {
+ optional string network = 1 [default = "main"]; // "main" or "test"
+ repeated Output outputs = 2; // Where payment should be sent
+ required uint64 time = 3; // Timestamp; when payment request created
+ optional uint64 expires = 4; // Timestamp; when this request should be considered invalid
+ optional string memo = 5; // Human-readable description of request for the customer
+ optional string payment_url = 6; // URL to send Payment and get PaymentACK
+ optional bytes merchant_data = 7; // Arbitrary data to include in the Payment message
+}
+message PaymentRequest {
+ optional uint32 payment_details_version = 1 [default = 1];
+ optional string pki_type = 2 [default = "none"]; // none / x509+sha256 / x509+sha1
+ optional bytes pki_data = 3; // depends on pki_type
+ required bytes serialized_payment_details = 4; // PaymentDetails
+ optional bytes signature = 5; // pki-dependent signature
+}
+message X509Certificates {
+ repeated bytes certificate = 1; // DER-encoded X.509 certificate chain
+}
+message Payment {
+ optional bytes merchant_data = 1; // From PaymentDetails.merchant_data
+ repeated bytes transactions = 2; // Signed transactions that satisfy PaymentDetails.outputs
+ repeated Output refund_to = 3; // Where to send refunds, if a refund is necessary
+ optional string memo = 4; // Human-readable message for the merchant
+}
+message PaymentACK {
+ required Payment payment = 1; // Payment message that triggered this ACK
+ optional string memo = 2; // human-readable message for customer
+}
DIR diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py
t@@ -0,0 +1,523 @@
+#!/usr/bin/env python
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2014 Thomas Voegtlin
+#
+# 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 hashlib
+import sys
+import time
+import traceback
+import json
+import requests
+
+import urllib.parse
+
+
+try:
+ from . import paymentrequest_pb2 as pb2
+except ImportError:
+ sys.exit("Error: could not find paymentrequest_pb2.py. Create it with 'protoc --proto_path=electrum/ --python_out=electrum/ electrum/paymentrequest.proto'")
+
+from . import bitcoin, ecc, util, transaction, x509, rsakey
+from .util import print_error, bh2u, bfh
+from .util import export_meta, import_meta
+
+from .bitcoin import TYPE_ADDRESS
+
+REQUEST_HEADERS = {'Accept': 'application/bitcoin-paymentrequest', 'User-Agent': 'Electrum'}
+ACK_HEADERS = {'Content-Type':'application/bitcoin-payment','Accept':'application/bitcoin-paymentack','User-Agent':'Electrum'}
+
+ca_path = requests.certs.where()
+ca_list = None
+ca_keyID = None
+
+def load_ca_list():
+ global ca_list, ca_keyID
+ if ca_list is None:
+ ca_list, ca_keyID = x509.load_certificates(ca_path)
+
+
+
+# status of payment requests
+PR_UNPAID = 0
+PR_EXPIRED = 1
+PR_UNKNOWN = 2 # sent but not propagated
+PR_PAID = 3 # send and propagated
+
+
+
+def get_payment_request(url):
+ u = urllib.parse.urlparse(url)
+ error = None
+ if u.scheme in ['http', 'https']:
+ try:
+ response = requests.request('GET', url, headers=REQUEST_HEADERS)
+ response.raise_for_status()
+ # Guard against `bitcoin:`-URIs with invalid payment request URLs
+ if "Content-Type" not in response.headers \
+ or response.headers["Content-Type"] != "application/bitcoin-paymentrequest":
+ data = None
+ error = "payment URL not pointing to a payment request handling server"
+ else:
+ data = response.content
+ print_error('fetched payment request', url, len(response.content))
+ except requests.exceptions.RequestException:
+ data = None
+ error = "payment URL not pointing to a valid server"
+ elif u.scheme == 'file':
+ try:
+ with open(u.path, 'r', encoding='utf-8') as f:
+ data = f.read()
+ except IOError:
+ data = None
+ error = "payment URL not pointing to a valid file"
+ else:
+ raise Exception("unknown scheme", url)
+ pr = PaymentRequest(data, error)
+ return pr
+
+
+class PaymentRequest:
+
+ def __init__(self, data, error=None):
+ self.raw = data
+ self.error = error
+ self.parse(data)
+ self.requestor = None # known after verify
+ self.tx = None
+
+ def __str__(self):
+ return str(self.raw)
+
+ def parse(self, r):
+ if self.error:
+ return
+ self.id = bh2u(bitcoin.sha256(r)[0:16])
+ try:
+ self.data = pb2.PaymentRequest()
+ self.data.ParseFromString(r)
+ except:
+ self.error = "cannot parse payment request"
+ return
+ self.details = pb2.PaymentDetails()
+ self.details.ParseFromString(self.data.serialized_payment_details)
+ self.outputs = []
+ for o in self.details.outputs:
+ addr = transaction.get_address_from_output_script(o.script)[1]
+ self.outputs.append((TYPE_ADDRESS, addr, o.amount))
+ self.memo = self.details.memo
+ self.payment_url = self.details.payment_url
+
+ def is_pr(self):
+ return self.get_amount() != 0
+ #return self.get_outputs() != [(TYPE_ADDRESS, self.get_requestor(), self.get_amount())]
+
+ def verify(self, contacts):
+ if self.error:
+ return False
+ if not self.raw:
+ self.error = "Empty request"
+ return False
+ pr = pb2.PaymentRequest()
+ try:
+ pr.ParseFromString(self.raw)
+ except:
+ self.error = "Error: Cannot parse payment request"
+ return False
+ if not pr.signature:
+ # the address will be displayed as requestor
+ self.requestor = None
+ return True
+ if pr.pki_type in ["x509+sha256", "x509+sha1"]:
+ return self.verify_x509(pr)
+ elif pr.pki_type in ["dnssec+btc", "dnssec+ecdsa"]:
+ return self.verify_dnssec(pr, contacts)
+ else:
+ self.error = "ERROR: Unsupported PKI Type for Message Signature"
+ return False
+
+ def verify_x509(self, paymntreq):
+ load_ca_list()
+ if not ca_list:
+ self.error = "Trusted certificate authorities list not found"
+ return False
+ cert = pb2.X509Certificates()
+ cert.ParseFromString(paymntreq.pki_data)
+ # verify the chain of certificates
+ try:
+ x, ca = verify_cert_chain(cert.certificate)
+ except BaseException as e:
+ traceback.print_exc(file=sys.stderr)
+ self.error = str(e)
+ return False
+ # get requestor name
+ self.requestor = x.get_common_name()
+ if self.requestor.startswith('*.'):
+ self.requestor = self.requestor[2:]
+ # verify the BIP70 signature
+ pubkey0 = rsakey.RSAKey(x.modulus, x.exponent)
+ sig = paymntreq.signature
+ paymntreq.signature = b''
+ s = paymntreq.SerializeToString()
+ sigBytes = bytearray(sig)
+ msgBytes = bytearray(s)
+ if paymntreq.pki_type == "x509+sha256":
+ hashBytes = bytearray(hashlib.sha256(msgBytes).digest())
+ verify = pubkey0.verify(sigBytes, x509.PREFIX_RSA_SHA256 + hashBytes)
+ elif paymntreq.pki_type == "x509+sha1":
+ verify = pubkey0.hashAndVerify(sigBytes, msgBytes)
+ if not verify:
+ self.error = "ERROR: Invalid Signature for Payment Request Data"
+ return False
+ ### SIG Verified
+ self.error = 'Signed by Trusted CA: ' + ca.get_common_name()
+ return True
+
+ def verify_dnssec(self, pr, contacts):
+ sig = pr.signature
+ alias = pr.pki_data
+ info = contacts.resolve(alias)
+ if info.get('validated') is not True:
+ self.error = "Alias verification failed (DNSSEC)"
+ return False
+ if pr.pki_type == "dnssec+btc":
+ self.requestor = alias
+ address = info.get('address')
+ pr.signature = b''
+ message = pr.SerializeToString()
+ if ecc.verify_message_with_address(address, sig, message):
+ self.error = 'Verified with DNSSEC'
+ return True
+ else:
+ self.error = "verify failed"
+ return False
+ else:
+ self.error = "unknown algo"
+ return False
+
+ def has_expired(self):
+ return self.details.expires and self.details.expires < int(time.time())
+
+ def get_expiration_date(self):
+ return self.details.expires
+
+ def get_amount(self):
+ return sum(map(lambda x:x[2], self.outputs))
+
+ def get_address(self):
+ o = self.outputs[0]
+ assert o[0] == TYPE_ADDRESS
+ return o[1]
+
+ def get_requestor(self):
+ return self.requestor if self.requestor else self.get_address()
+
+ def get_verify_status(self):
+ return self.error if self.requestor else "No Signature"
+
+ def get_memo(self):
+ return self.memo
+
+ def get_dict(self):
+ return {
+ 'requestor': self.get_requestor(),
+ 'memo':self.get_memo(),
+ 'exp': self.get_expiration_date(),
+ 'amount': self.get_amount(),
+ 'signature': self.get_verify_status(),
+ 'txid': self.tx,
+ 'outputs': self.get_outputs()
+ }
+
+ def get_id(self):
+ return self.id if self.requestor else self.get_address()
+
+ def get_outputs(self):
+ return self.outputs[:]
+
+ def send_ack(self, raw_tx, refund_addr):
+ pay_det = self.details
+ if not self.details.payment_url:
+ return False, "no url"
+ paymnt = pb2.Payment()
+ paymnt.merchant_data = pay_det.merchant_data
+ paymnt.transactions.append(bfh(raw_tx))
+ ref_out = paymnt.refund_to.add()
+ ref_out.script = util.bfh(transaction.Transaction.pay_script(TYPE_ADDRESS, refund_addr))
+ paymnt.memo = "Paid using Electrum"
+ pm = paymnt.SerializeToString()
+ payurl = urllib.parse.urlparse(pay_det.payment_url)
+ try:
+ r = requests.post(payurl.geturl(), data=pm, headers=ACK_HEADERS, verify=ca_path)
+ except requests.exceptions.SSLError:
+ print("Payment Message/PaymentACK verify Failed")
+ try:
+ r = requests.post(payurl.geturl(), data=pm, headers=ACK_HEADERS, verify=False)
+ except Exception as e:
+ print(e)
+ return False, "Payment Message/PaymentACK Failed"
+ if r.status_code >= 500:
+ return False, r.reason
+ try:
+ paymntack = pb2.PaymentACK()
+ paymntack.ParseFromString(r.content)
+ except Exception:
+ return False, "PaymentACK could not be processed. Payment was sent; please manually verify that payment was received."
+ print("PaymentACK message received: %s" % paymntack.memo)
+ return True, paymntack.memo
+
+
+def make_unsigned_request(req):
+ from .transaction import Transaction
+ addr = req['address']
+ time = req.get('time', 0)
+ exp = req.get('exp', 0)
+ if time and type(time) != int:
+ time = 0
+ if exp and type(exp) != int:
+ exp = 0
+ amount = req['amount']
+ if amount is None:
+ amount = 0
+ memo = req['memo']
+ script = bfh(Transaction.pay_script(TYPE_ADDRESS, addr))
+ outputs = [(script, amount)]
+ pd = pb2.PaymentDetails()
+ for script, amount in outputs:
+ pd.outputs.add(amount=amount, script=script)
+ pd.time = time
+ pd.expires = time + exp if exp else 0
+ pd.memo = memo
+ pr = pb2.PaymentRequest()
+ pr.serialized_payment_details = pd.SerializeToString()
+ pr.signature = util.to_bytes('')
+ return pr
+
+
+def sign_request_with_alias(pr, alias, alias_privkey):
+ pr.pki_type = 'dnssec+btc'
+ pr.pki_data = str(alias)
+ message = pr.SerializeToString()
+ ec_key = ecc.ECPrivkey(alias_privkey)
+ compressed = bitcoin.is_compressed(alias_privkey)
+ pr.signature = ec_key.sign_message(message, compressed)
+
+
+def verify_cert_chain(chain):
+ """ Verify a chain of certificates. The last certificate is the CA"""
+ load_ca_list()
+ # parse the chain
+ cert_num = len(chain)
+ x509_chain = []
+ for i in range(cert_num):
+ x = x509.X509(bytearray(chain[i]))
+ x509_chain.append(x)
+ if i == 0:
+ x.check_date()
+ else:
+ if not x.check_ca():
+ raise Exception("ERROR: Supplied CA Certificate Error")
+ if not cert_num > 1:
+ raise Exception("ERROR: CA Certificate Chain Not Provided by Payment Processor")
+ # if the root CA is not supplied, add it to the chain
+ ca = x509_chain[cert_num-1]
+ if ca.getFingerprint() not in ca_list:
+ keyID = ca.get_issuer_keyID()
+ f = ca_keyID.get(keyID)
+ if f:
+ root = ca_list[f]
+ x509_chain.append(root)
+ else:
+ raise Exception("Supplied CA Not Found in Trusted CA Store.")
+ # verify the chain of signatures
+ cert_num = len(x509_chain)
+ for i in range(1, cert_num):
+ x = x509_chain[i]
+ prev_x = x509_chain[i-1]
+ algo, sig, data = prev_x.get_signature()
+ sig = bytearray(sig)
+ pubkey = rsakey.RSAKey(x.modulus, x.exponent)
+ if algo == x509.ALGO_RSA_SHA1:
+ verify = pubkey.hashAndVerify(sig, data)
+ elif algo == x509.ALGO_RSA_SHA256:
+ hashBytes = bytearray(hashlib.sha256(data).digest())
+ verify = pubkey.verify(sig, x509.PREFIX_RSA_SHA256 + hashBytes)
+ elif algo == x509.ALGO_RSA_SHA384:
+ hashBytes = bytearray(hashlib.sha384(data).digest())
+ verify = pubkey.verify(sig, x509.PREFIX_RSA_SHA384 + hashBytes)
+ elif algo == x509.ALGO_RSA_SHA512:
+ hashBytes = bytearray(hashlib.sha512(data).digest())
+ verify = pubkey.verify(sig, x509.PREFIX_RSA_SHA512 + hashBytes)
+ else:
+ raise Exception("Algorithm not supported")
+ util.print_error(self.error, algo.getComponentByName('algorithm'))
+ if not verify:
+ raise Exception("Certificate not Signed by Provided CA Certificate Chain")
+
+ return x509_chain[0], ca
+
+
+def check_ssl_config(config):
+ from . import pem
+ key_path = config.get('ssl_privkey')
+ cert_path = config.get('ssl_chain')
+ with open(key_path, 'r', encoding='utf-8') as f:
+ params = pem.parse_private_key(f.read())
+ with open(cert_path, 'r', encoding='utf-8') as f:
+ s = f.read()
+ bList = pem.dePemList(s, "CERTIFICATE")
+ # verify chain
+ x, ca = verify_cert_chain(bList)
+ # verify that privkey and pubkey match
+ privkey = rsakey.RSAKey(*params)
+ pubkey = rsakey.RSAKey(x.modulus, x.exponent)
+ assert x.modulus == params[0]
+ assert x.exponent == params[1]
+ # return requestor
+ requestor = x.get_common_name()
+ if requestor.startswith('*.'):
+ requestor = requestor[2:]
+ return requestor
+
+def sign_request_with_x509(pr, key_path, cert_path):
+ from . import pem
+ with open(key_path, 'r', encoding='utf-8') as f:
+ params = pem.parse_private_key(f.read())
+ privkey = rsakey.RSAKey(*params)
+ with open(cert_path, 'r', encoding='utf-8') as f:
+ s = f.read()
+ bList = pem.dePemList(s, "CERTIFICATE")
+ certificates = pb2.X509Certificates()
+ certificates.certificate.extend(map(bytes, bList))
+ pr.pki_type = 'x509+sha256'
+ pr.pki_data = certificates.SerializeToString()
+ msgBytes = bytearray(pr.SerializeToString())
+ hashBytes = bytearray(hashlib.sha256(msgBytes).digest())
+ sig = privkey.sign(x509.PREFIX_RSA_SHA256 + hashBytes)
+ pr.signature = bytes(sig)
+
+
+def serialize_request(req):
+ pr = make_unsigned_request(req)
+ signature = req.get('sig')
+ requestor = req.get('name')
+ if requestor and signature:
+ pr.signature = bfh(signature)
+ pr.pki_type = 'dnssec+btc'
+ pr.pki_data = str(requestor)
+ return pr
+
+
+def make_request(config, req):
+ pr = make_unsigned_request(req)
+ key_path = config.get('ssl_privkey')
+ cert_path = config.get('ssl_chain')
+ if key_path and cert_path:
+ sign_request_with_x509(pr, key_path, cert_path)
+ return pr
+
+
+
+class InvoiceStore(object):
+
+ def __init__(self, storage):
+ self.storage = storage
+ self.invoices = {}
+ self.paid = {}
+ d = self.storage.get('invoices', {})
+ self.load(d)
+
+ def set_paid(self, pr, txid):
+ pr.tx = txid
+ pr_id = pr.get_id()
+ self.paid[txid] = pr_id
+ if pr_id not in self.invoices:
+ # in case the user had deleted it previously
+ self.add(pr)
+
+ def load(self, d):
+ for k, v in d.items():
+ try:
+ pr = PaymentRequest(bfh(v.get('hex')))
+ pr.tx = v.get('txid')
+ pr.requestor = v.get('requestor')
+ self.invoices[k] = pr
+ if pr.tx:
+ self.paid[pr.tx] = k
+ except:
+ continue
+
+ def import_file(self, path):
+ def validate(data):
+ return data # TODO
+ import_meta(path, validate, self.on_import)
+
+ def on_import(self, data):
+ self.load(data)
+ self.save()
+
+ def export_file(self, filename):
+ export_meta(self.dump(), filename)
+
+ def dump(self):
+ d = {}
+ for k, pr in self.invoices.items():
+ d[k] = {
+ 'hex': bh2u(pr.raw),
+ 'requestor': pr.requestor,
+ 'txid': pr.tx
+ }
+ return d
+
+ def save(self):
+ self.storage.put('invoices', self.dump())
+
+ def get_status(self, key):
+ pr = self.get(key)
+ if pr is None:
+ print_error("[InvoiceStore] get_status() can't find pr for", key)
+ return
+ if pr.tx is not None:
+ return PR_PAID
+ if pr.has_expired():
+ return PR_EXPIRED
+ return PR_UNPAID
+
+ def add(self, pr):
+ key = pr.get_id()
+ self.invoices[key] = pr
+ self.save()
+ return key
+
+ def remove(self, key):
+ self.invoices.pop(key)
+ self.save()
+
+ def get(self, k):
+ return self.invoices.get(k)
+
+ def sorted_list(self):
+ # sort
+ return self.invoices.values()
+
+ def unpaid_invoices(self):
+ return [ self.invoices[k] for k in filter(lambda x: self.get_status(x)!=PR_PAID, self.invoices.keys())]
DIR diff --git a/electrum/paymentrequest_pb2.py b/electrum/paymentrequest_pb2.py
t@@ -0,0 +1,367 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: paymentrequest.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf import descriptor_pb2
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='paymentrequest.proto',
+ package='payments',
parazyd.org:70 /git/electrum/commit/097ac144d976eb46dff809e1809783dc78ab6d8b.gph:20797: line too long